001/* 002 * Copyright 2020-2023 Ping Identity Corporation 003 * All Rights Reserved. 004 */ 005/* 006 * Copyright 2020-2023 Ping Identity Corporation 007 * 008 * Licensed under the Apache License, Version 2.0 (the "License"); 009 * you may not use this file except in compliance with the License. 010 * You may obtain a copy of the License at 011 * 012 * http://www.apache.org/licenses/LICENSE-2.0 013 * 014 * Unless required by applicable law or agreed to in writing, software 015 * distributed under the License is distributed on an "AS IS" BASIS, 016 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 017 * See the License for the specific language governing permissions and 018 * limitations under the License. 019 */ 020/* 021 * Copyright (C) 2020-2023 Ping Identity Corporation 022 * 023 * This program is free software; you can redistribute it and/or modify 024 * it under the terms of the GNU General Public License (GPLv2 only) 025 * or the terms of the GNU Lesser General Public License (LGPLv2.1 only) 026 * as published by the Free Software Foundation. 027 * 028 * This program is distributed in the hope that it will be useful, 029 * but WITHOUT ANY WARRANTY; without even the implied warranty of 030 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 031 * GNU General Public License for more details. 032 * 033 * You should have received a copy of the GNU General Public License 034 * along with this program; if not, see <http://www.gnu.org/licenses>. 035 */ 036package com.unboundid.ldif; 037 038 039 040import java.io.File; 041import java.io.FileInputStream; 042import java.io.FileOutputStream; 043import java.io.IOException; 044import java.io.InputStream; 045import java.io.OutputStream; 046import java.util.ArrayList; 047import java.util.Arrays; 048import java.util.HashMap; 049import java.util.Iterator; 050import java.util.LinkedHashMap; 051import java.util.List; 052import java.util.Map; 053import java.util.TreeMap; 054import java.util.concurrent.atomic.AtomicBoolean; 055import java.util.concurrent.atomic.AtomicLong; 056import java.util.concurrent.atomic.AtomicReference; 057import java.util.zip.GZIPOutputStream; 058 059import com.unboundid.ldap.sdk.Attribute; 060import com.unboundid.ldap.sdk.ChangeType; 061import com.unboundid.ldap.sdk.DN; 062import com.unboundid.ldap.sdk.Entry; 063import com.unboundid.ldap.sdk.InternalSDKHelper; 064import com.unboundid.ldap.sdk.LDAPException; 065import com.unboundid.ldap.sdk.RDN; 066import com.unboundid.ldap.sdk.ResultCode; 067import com.unboundid.ldap.sdk.Version; 068import com.unboundid.ldap.sdk.schema.Schema; 069import com.unboundid.ldap.sdk.unboundidds.tools.ToolUtils; 070import com.unboundid.util.CommandLineTool; 071import com.unboundid.util.Debug; 072import com.unboundid.util.NotNull; 073import com.unboundid.util.Nullable; 074import com.unboundid.util.ObjectPair; 075import com.unboundid.util.PassphraseEncryptedOutputStream; 076import com.unboundid.util.StaticUtils; 077import com.unboundid.util.ThreadSafety; 078import com.unboundid.util.ThreadSafetyLevel; 079import com.unboundid.util.Validator; 080import com.unboundid.util.args.ArgumentException; 081import com.unboundid.util.args.ArgumentParser; 082import com.unboundid.util.args.BooleanArgument; 083import com.unboundid.util.args.FileArgument; 084import com.unboundid.util.args.IntegerArgument; 085 086import static com.unboundid.ldif.LDIFMessages.*; 087 088 089 090/** 091 * This class provides a command-line tool that can be used to apply a set of 092 * changes to data in an LDIF file. 093 */ 094@ThreadSafety(level=ThreadSafetyLevel.NOT_THREADSAFE) 095public final class LDIFModify 096 extends CommandLineTool 097{ 098 /** 099 * The server root directory for the Ping Identity Directory Server (or 100 * related Ping Identity server product) that contains this tool, if 101 * applicable. 102 */ 103 @NotNull private static final File PING_SERVER_ROOT = 104 InternalSDKHelper.getPingIdentityServerRoot(); 105 106 107 108 /** 109 * Indicates whether the tool is running as part of a Ping Identity Directory 110 * Server (or related Ping Identity Server Product) installation. 111 */ 112 private static final boolean PING_SERVER_AVAILABLE = 113 (PING_SERVER_ROOT != null); 114 115 116 117 /** 118 * The column at which to wrap long lines. 119 */ 120 private static final int WRAP_COLUMN = StaticUtils.TERMINAL_WIDTH_COLUMNS - 1; 121 122 123 124 // The completion message for this tool. 125 @NotNull private final AtomicReference<String> completionMessage; 126 127 // Encryption passphrases used thus far. 128 @NotNull private final List<char[]> inputEncryptionPassphrases; 129 130 // The command-line arguments supported by this tool. 131 @Nullable private BooleanArgument compressTarget; 132 @Nullable private BooleanArgument doNotWrap; 133 @Nullable private BooleanArgument encryptTarget; 134 @Nullable private BooleanArgument ignoreDeletesOfNonexistentEntries; 135 @Nullable private BooleanArgument ignoreDuplicateDeletes; 136 @Nullable private BooleanArgument ignoreModifiesOfNonexistentEntries; 137 @Nullable private BooleanArgument lenientModifications; 138 @Nullable private BooleanArgument strictModifications; 139 @Nullable private BooleanArgument noSchemaCheck; 140 @Nullable private BooleanArgument stripTrailingSpaces; 141 @Nullable private BooleanArgument suppressComments; 142 @Nullable private FileArgument changesEncryptionPassphraseFile; 143 @Nullable private FileArgument changesLDIF; 144 @Nullable private FileArgument sourceEncryptionPassphraseFile; 145 @Nullable private FileArgument sourceLDIF; 146 @Nullable private FileArgument targetEncryptionPassphraseFile; 147 @Nullable private FileArgument targetLDIF; 148 @Nullable private IntegerArgument wrapColumn; 149 150 // Variables that may be used by support for a legacy implementation. 151 @Nullable private LDIFReader changesReader; 152 @Nullable private LDIFReader sourceReader; 153 @Nullable private LDIFWriter targetWriter; 154 @Nullable private List<String> errorMessages; 155 156 157 158 /** 159 * Invokes this tool with the provided set of command-line arguments. 160 * 161 * @param args The set of arguments provided to this tool. It may be 162 * empty but must not be {@code null}. 163 */ 164 public static void main(@NotNull final String... args) 165 { 166 final ResultCode resultCode = main(System.out, System.err, args); 167 if (resultCode != ResultCode.SUCCESS) 168 { 169 System.exit(resultCode.intValue()); 170 } 171 } 172 173 174 175 /** 176 * Invokes this tool with the provided set of command-line arguments, using 177 * the given output and error streams. 178 * 179 * @param out The output stream to use for standard output. It may be 180 * {@code null} if standard output should be suppressed. 181 * @param err The output stream to use for standard error. It may be 182 * {@code null} if standard error should be suppressed. 183 * @param args The set of arguments provided to this tool. It may be 184 * empty but must not be {@code null}. 185 * 186 * @return A result code indicating the status of processing. Any result 187 * code other than {@link ResultCode#SUCCESS} should be considered 188 * an error. 189 */ 190 @NotNull() 191 public static ResultCode main(@Nullable final OutputStream out, 192 @Nullable final OutputStream err, 193 @NotNull final String... args) 194 { 195 final LDIFModify tool = new LDIFModify(out, err); 196 return tool.runTool(args); 197 } 198 199 200 201 /** 202 * Invokes this tool with the provided readers and writer. This method is 203 * primarily intended for legacy backward compatibility with the Ping Identity 204 * Directory Server and does not provide access to all functionality offered 205 * by this tool. 206 * 207 * @param sourceReader An LDIF reader that may be used to read the entries 208 * to be updated. It must not be {@code null}. Note 209 * this the reader will be closed when the tool 210 * completes. 211 * @param changesReader An LDIF reader that may be used to read the changes 212 * to apply. It must not be {@code null}. Note that 213 * this reader will be closed when the tool completes. 214 * @param targetWriter An LDIF writer that may be used to write the updated 215 * entries. It must not be {@code null}. Note that 216 * this writer will be closed when the tool completes. 217 * @param errorMessages A list that will be updated with any errors 218 * encountered during processing. It must not be 219 * {@code null} and must be updatable. 220 * 221 * @return {@code true} if processing completed successfully, or 222 * {@code false} if one or more errors were encountered. 223 */ 224 public static boolean main(@NotNull final LDIFReader sourceReader, 225 @NotNull final LDIFReader changesReader, 226 @NotNull final LDIFWriter targetWriter, 227 @NotNull final List<String> errorMessages) 228 { 229 Validator.ensureNotNull(sourceReader, changesReader, targetWriter, 230 errorMessages); 231 232 final LDIFModify tool = new LDIFModify(null, null); 233 tool.sourceReader = sourceReader; 234 tool.changesReader = changesReader; 235 tool.targetWriter = targetWriter; 236 tool.errorMessages = errorMessages; 237 238 try 239 { 240 final ResultCode resultCode = 241 tool.runTool("--suppressComments", "--lenientModifications"); 242 return (resultCode == ResultCode.SUCCESS); 243 } 244 finally 245 { 246 try 247 { 248 sourceReader.close(); 249 } 250 catch (final Exception e) 251 { 252 Debug.debugException(e); 253 } 254 255 try 256 { 257 changesReader.close(); 258 } 259 catch (final Exception e) 260 { 261 Debug.debugException(e); 262 } 263 264 try 265 { 266 targetWriter.close(); 267 } 268 catch (final Exception e) 269 { 270 Debug.debugException(e); 271 } 272 } 273 } 274 275 276 277 /** 278 * Creates a new instance of this tool with the provided output and error 279 * streams. 280 * 281 * @param out The output stream to use for standard output. It may be 282 * {@code null} if standard output should be suppressed. 283 * @param err The output stream to use for standard error. It may be 284 * {@code null} if standard error should be suppressed. 285 */ 286 public LDIFModify(@Nullable final OutputStream out, 287 @Nullable final OutputStream err) 288 { 289 super(out, err); 290 291 completionMessage = new AtomicReference<>(); 292 inputEncryptionPassphrases = new ArrayList<>(5); 293 294 compressTarget = null; 295 doNotWrap = null; 296 encryptTarget = null; 297 ignoreDeletesOfNonexistentEntries = null; 298 ignoreDuplicateDeletes = null; 299 ignoreModifiesOfNonexistentEntries = null; 300 lenientModifications = null; 301 noSchemaCheck = null; 302 strictModifications = null; 303 stripTrailingSpaces = null; 304 suppressComments = null; 305 changesEncryptionPassphraseFile = null; 306 changesLDIF = null; 307 sourceEncryptionPassphraseFile = null; 308 sourceLDIF = null; 309 targetEncryptionPassphraseFile = null; 310 targetLDIF = null; 311 wrapColumn = null; 312 313 changesReader = null; 314 sourceReader = null; 315 targetWriter = null; 316 errorMessages = null; 317 } 318 319 320 321 /** 322 * {@inheritDoc} 323 */ 324 @Override() 325 @NotNull() 326 public String getToolName() 327 { 328 return "ldifmodify"; 329 } 330 331 332 333 /** 334 * {@inheritDoc} 335 */ 336 @Override() 337 @NotNull() 338 public String getToolDescription() 339 { 340 return INFO_LDIFMODIFY_TOOL_DESCRIPTION.get(); 341 } 342 343 344 345 /** 346 * {@inheritDoc} 347 */ 348 @Override() 349 @NotNull() 350 public List<String> getAdditionalDescriptionParagraphs() 351 { 352 return Arrays.asList( 353 INFO_LDIFMODIFY_TOOL_DESCRIPTION_2.get(), 354 INFO_LDIFMODIFY_TOOL_DESCRIPTION_3.get(), 355 INFO_LDIFMODIFY_TOOL_DESCRIPTION_4.get(), 356 INFO_LDIFMODIFY_TOOL_DESCRIPTION_5.get()); 357 } 358 359 360 361 /** 362 * {@inheritDoc} 363 */ 364 @Override() 365 @NotNull() 366 public String getToolVersion() 367 { 368 return Version.NUMERIC_VERSION_STRING; 369 } 370 371 372 373 /** 374 * {@inheritDoc} 375 */ 376 @Override() 377 public boolean supportsInteractiveMode() 378 { 379 return true; 380 } 381 382 383 384 /** 385 * {@inheritDoc} 386 */ 387 @Override() 388 public boolean defaultsToInteractiveMode() 389 { 390 return true; 391 } 392 393 394 395 /** 396 * {@inheritDoc} 397 */ 398 @Override() 399 public boolean supportsPropertiesFile() 400 { 401 return true; 402 } 403 404 405 406 /** 407 * {@inheritDoc} 408 */ 409 @Override() 410 @Nullable() 411 protected String getToolCompletionMessage() 412 { 413 return completionMessage.get(); 414 } 415 416 417 418 /** 419 * {@inheritDoc} 420 */ 421 @Override() 422 public void addToolArguments(@NotNull final ArgumentParser parser) 423 throws ArgumentException 424 { 425 sourceLDIF = new FileArgument('s', "sourceLDIF", (sourceReader == null), 1, 426 null, INFO_LDIFMODIFY_ARG_DESC_SOURCE_LDIF.get(), true, true, true, 427 false); 428 sourceLDIF.addLongIdentifier("source-ldif", true); 429 sourceLDIF.addLongIdentifier("sourceFile", true); 430 sourceLDIF.addLongIdentifier("source-file", true); 431 sourceLDIF.addLongIdentifier("source", true); 432 sourceLDIF.addLongIdentifier("inputLDIF", true); 433 sourceLDIF.addLongIdentifier("input-ldif", true); 434 sourceLDIF.addLongIdentifier("inputFile", true); 435 sourceLDIF.addLongIdentifier("input-file", true); 436 sourceLDIF.addLongIdentifier("input", true); 437 sourceLDIF.addLongIdentifier("ldifFile", true); 438 sourceLDIF.addLongIdentifier("ldif-file", true); 439 sourceLDIF.addLongIdentifier("ldif", true); 440 sourceLDIF.setArgumentGroupName(INFO_LDIFMODIFY_ARG_GROUP_INPUT.get()); 441 parser.addArgument(sourceLDIF); 442 443 444 final String sourcePWDesc; 445 if (PING_SERVER_AVAILABLE) 446 { 447 sourcePWDesc = INFO_LDIFMODIFY_ARG_DESC_SOURCE_PW_FILE_PING_SERVER.get(); 448 } 449 else 450 { 451 sourcePWDesc = INFO_LDIFMODIFY_ARG_DESC_SOURCE_PW_FILE_STANDALONE.get(); 452 } 453 sourceEncryptionPassphraseFile = new FileArgument(null, 454 "sourceEncryptionPassphraseFile", false, 1, null, sourcePWDesc, true, 455 true, true, false); 456 sourceEncryptionPassphraseFile.addLongIdentifier( 457 "source-encryption-passphrase-file", true); 458 sourceEncryptionPassphraseFile.addLongIdentifier("sourcePassphraseFile", 459 true); 460 sourceEncryptionPassphraseFile.addLongIdentifier("source-passphrase-file", 461 true); 462 sourceEncryptionPassphraseFile.addLongIdentifier( 463 "sourceEncryptionPasswordFile", true); 464 sourceEncryptionPassphraseFile.addLongIdentifier( 465 "source-encryption-password-file", true); 466 sourceEncryptionPassphraseFile.addLongIdentifier("sourcePasswordFile", 467 true); 468 sourceEncryptionPassphraseFile.addLongIdentifier("source-password-file", 469 true); 470 sourceEncryptionPassphraseFile.addLongIdentifier( 471 "inputEncryptionPassphraseFile", true); 472 sourceEncryptionPassphraseFile.addLongIdentifier( 473 "input-encryption-passphrase-file", true); 474 sourceEncryptionPassphraseFile.addLongIdentifier("inputPassphraseFile", 475 true); 476 sourceEncryptionPassphraseFile.addLongIdentifier("input-passphrase-file", 477 true); 478 sourceEncryptionPassphraseFile.addLongIdentifier( 479 "inputEncryptionPasswordFile", true); 480 sourceEncryptionPassphraseFile.addLongIdentifier( 481 "input-encryption-password-file", true); 482 sourceEncryptionPassphraseFile.addLongIdentifier("inputPasswordFile", true); 483 sourceEncryptionPassphraseFile.addLongIdentifier("input-password-file", 484 true); 485 sourceEncryptionPassphraseFile.setArgumentGroupName( 486 INFO_LDIFMODIFY_ARG_GROUP_INPUT.get()); 487 parser.addArgument(sourceEncryptionPassphraseFile); 488 489 490 changesLDIF = new FileArgument('m', "changesLDIF", (changesReader == null), 491 1, null, INFO_LDIFMODIFY_ARG_DESC_CHANGES_LDIF.get(), true, true, true, 492 false); 493 changesLDIF.addLongIdentifier("changes-ldif", true); 494 changesLDIF.addLongIdentifier("changesFile", true); 495 changesLDIF.addLongIdentifier("changes-file", true); 496 changesLDIF.addLongIdentifier("changes", true); 497 changesLDIF.addLongIdentifier("updatesLDIF", true); 498 changesLDIF.addLongIdentifier("updates-ldif", true); 499 changesLDIF.addLongIdentifier("updatesFile", true); 500 changesLDIF.addLongIdentifier("updates-file", true); 501 changesLDIF.addLongIdentifier("updates", true); 502 changesLDIF.addLongIdentifier("modificationsLDIF", true); 503 changesLDIF.addLongIdentifier("modifications-ldif", true); 504 changesLDIF.addLongIdentifier("modificationsFile", true); 505 changesLDIF.addLongIdentifier("modifications-file", true); 506 changesLDIF.addLongIdentifier("modifications", true); 507 changesLDIF.addLongIdentifier("modsLDIF", true); 508 changesLDIF.addLongIdentifier("mods-ldif", true); 509 changesLDIF.addLongIdentifier("modsFile", true); 510 changesLDIF.addLongIdentifier("mods-file", true); 511 changesLDIF.addLongIdentifier("mods", true); 512 changesLDIF.setArgumentGroupName(INFO_LDIFMODIFY_ARG_GROUP_INPUT.get()); 513 parser.addArgument(changesLDIF); 514 515 516 final String changesPWDesc; 517 if (PING_SERVER_AVAILABLE) 518 { 519 changesPWDesc = 520 INFO_LDIFMODIFY_ARG_DESC_CHANGES_PW_FILE_PING_SERVER.get(); 521 } 522 else 523 { 524 changesPWDesc = INFO_LDIFMODIFY_ARG_DESC_CHANGES_PW_FILE_STANDALONE.get(); 525 } 526 changesEncryptionPassphraseFile = new FileArgument(null, 527 "changesEncryptionPassphraseFile", false, 1, null, changesPWDesc, true, 528 true, true, false); 529 changesEncryptionPassphraseFile.addLongIdentifier( 530 "changes-encryption-passphrase-file", true); 531 changesEncryptionPassphraseFile.addLongIdentifier("changesPassphraseFile", 532 true); 533 changesEncryptionPassphraseFile.addLongIdentifier("changes-passphrase-file", 534 true); 535 changesEncryptionPassphraseFile.addLongIdentifier( 536 "changesEncryptionPasswordFile", true); 537 changesEncryptionPassphraseFile.addLongIdentifier( 538 "changes-encryption-password-file", true); 539 changesEncryptionPassphraseFile.addLongIdentifier("changesPasswordFile", 540 true); 541 changesEncryptionPassphraseFile.addLongIdentifier("changes-password-file", 542 true); 543 changesEncryptionPassphraseFile.addLongIdentifier( 544 "updatesEncryptionPassphraseFile", true); 545 changesEncryptionPassphraseFile.addLongIdentifier( 546 "updates-encryption-passphrase-file", true); 547 changesEncryptionPassphraseFile.addLongIdentifier( 548 "updatesPassphraseFile", true); 549 changesEncryptionPassphraseFile.addLongIdentifier( 550 "updates-passphrase-file", true); 551 changesEncryptionPassphraseFile.addLongIdentifier( 552 "updatesEncryptionPasswordFile", true); 553 changesEncryptionPassphraseFile.addLongIdentifier( 554 "updates-encryption-password-file", true); 555 changesEncryptionPassphraseFile.addLongIdentifier( 556 "updatesPasswordFile", true); 557 changesEncryptionPassphraseFile.addLongIdentifier( 558 "updates-password-file", true); 559 changesEncryptionPassphraseFile.addLongIdentifier( 560 "modificationsEncryptionPassphraseFile", true); 561 changesEncryptionPassphraseFile.addLongIdentifier( 562 "modifications-encryption-passphrase-file", true); 563 changesEncryptionPassphraseFile.addLongIdentifier( 564 "modificationsPassphraseFile", true); 565 changesEncryptionPassphraseFile.addLongIdentifier( 566 "modifications-passphrase-file", true); 567 changesEncryptionPassphraseFile.addLongIdentifier( 568 "modificationsEncryptionPasswordFile", true); 569 changesEncryptionPassphraseFile.addLongIdentifier( 570 "modifications-encryption-password-file", true); 571 changesEncryptionPassphraseFile.addLongIdentifier( 572 "modificationsPasswordFile", true); 573 changesEncryptionPassphraseFile.addLongIdentifier( 574 "modifications-password-file", true); 575 changesEncryptionPassphraseFile.addLongIdentifier( 576 "modsEncryptionPassphraseFile", true); 577 changesEncryptionPassphraseFile.addLongIdentifier( 578 "mods-encryption-passphrase-file", true); 579 changesEncryptionPassphraseFile.addLongIdentifier( 580 "modsPassphraseFile", true); 581 changesEncryptionPassphraseFile.addLongIdentifier( 582 "mods-passphrase-file", true); 583 changesEncryptionPassphraseFile.addLongIdentifier( 584 "modsEncryptionPasswordFile", true); 585 changesEncryptionPassphraseFile.addLongIdentifier( 586 "mods-encryption-password-file", true); 587 changesEncryptionPassphraseFile.addLongIdentifier( 588 "modsPasswordFile", true); 589 changesEncryptionPassphraseFile.addLongIdentifier( 590 "mods-password-file", true); 591 changesEncryptionPassphraseFile.setArgumentGroupName( 592 INFO_LDIFMODIFY_ARG_GROUP_INPUT.get()); 593 parser.addArgument(changesEncryptionPassphraseFile); 594 595 596 stripTrailingSpaces = new BooleanArgument(null, "stripTrailingSpaces", 1, 597 INFO_LDIFMODIFY_ARG_DESC_STRIP_TRAILING_SPACES.get()); 598 stripTrailingSpaces.addLongIdentifier("strip-trailing-spaces", true); 599 stripTrailingSpaces.addLongIdentifier("ignoreTrailingSpaces", true); 600 stripTrailingSpaces.addLongIdentifier("ignore-trailing-spaces", true); 601 stripTrailingSpaces.setArgumentGroupName( 602 INFO_LDIFMODIFY_ARG_GROUP_INPUT.get()); 603 parser.addArgument(stripTrailingSpaces); 604 605 606 lenientModifications = new BooleanArgument(null, "lenientModifications", 1, 607 INFO_LDIFMODIFY_ARG_DESC_LENIENT_MODIFICATIONS.get()); 608 lenientModifications.addLongIdentifier("lenient-modifications", true); 609 lenientModifications.addLongIdentifier("lenientModification", true); 610 lenientModifications.addLongIdentifier("lenient-modification", true); 611 lenientModifications.addLongIdentifier("lenientMods", true); 612 lenientModifications.addLongIdentifier("lenient-mods", true); 613 lenientModifications.addLongIdentifier("lenientMod", true); 614 lenientModifications.addLongIdentifier("lenient-mod", true); 615 lenientModifications.addLongIdentifier("lenient", true); 616 lenientModifications.setArgumentGroupName( 617 INFO_LDIFMODIFY_ARG_GROUP_INPUT.get()); 618 lenientModifications.setHidden(true); 619 parser.addArgument(lenientModifications); 620 621 622 strictModifications = new BooleanArgument(null, "strictModifications", 1, 623 INFO_LDIFMODIFY_ARG_DESC_STRICT_MODIFICATIONS.get()); 624 strictModifications.addLongIdentifier("strict-modifications", true); 625 strictModifications.addLongIdentifier("strictModification", true); 626 strictModifications.addLongIdentifier("strict-modification", true); 627 strictModifications.addLongIdentifier("strictMods", true); 628 strictModifications.addLongIdentifier("strict-mods", true); 629 strictModifications.addLongIdentifier("strictMod", true); 630 strictModifications.addLongIdentifier("strict-mod", true); 631 strictModifications.addLongIdentifier("strict", true); 632 strictModifications.setArgumentGroupName( 633 INFO_LDIFMODIFY_ARG_GROUP_INPUT.get()); 634 parser.addArgument(strictModifications); 635 636 637 ignoreDuplicateDeletes = new BooleanArgument(null, "ignoreDuplicateDeletes", 638 1, INFO_LDIFMODIFY_ARG_DESC_IGNORE_DUPLICATE_DELETES.get()); 639 ignoreDuplicateDeletes.addLongIdentifier("ignore-duplicate-deletes", true); 640 ignoreDuplicateDeletes.addLongIdentifier("ignoreRepeatedDeletes", true); 641 ignoreDuplicateDeletes.addLongIdentifier("ignore-repeated-deletes", true); 642 ignoreDuplicateDeletes.addLongIdentifier("ignoreRepeatDeletes", true); 643 ignoreDuplicateDeletes.addLongIdentifier("ignore-repeat-deletes", true); 644 ignoreDuplicateDeletes.setArgumentGroupName( 645 INFO_LDIFMODIFY_ARG_GROUP_INPUT.get()); 646 parser.addArgument(ignoreDuplicateDeletes); 647 648 649 ignoreDeletesOfNonexistentEntries = new BooleanArgument(null, 650 "ignoreDeletesOfNonexistentEntries", 1, 651 INFO_LDIFMODIFY_ARG_DESC_IGNORE_NONEXISTENT_DELETES.get()); 652 ignoreDeletesOfNonexistentEntries.addLongIdentifier( 653 "ignore-deletes-of-nonexistent-entries", true); 654 ignoreDeletesOfNonexistentEntries.addLongIdentifier( 655 "ignoreNonexistentDeletes", true); 656 ignoreDeletesOfNonexistentEntries.addLongIdentifier( 657 "ignore-nonexistent-deletes", true); 658 ignoreDeletesOfNonexistentEntries.setArgumentGroupName( 659 INFO_LDIFMODIFY_ARG_GROUP_INPUT.get()); 660 parser.addArgument(ignoreDeletesOfNonexistentEntries); 661 662 663 ignoreModifiesOfNonexistentEntries = new BooleanArgument(null, 664 "ignoreModifiesOfNonexistentEntries", 1, 665 INFO_LDIFMODIFY_ARG_DESC_IGNORE_NONEXISTENT_MODIFIES.get()); 666 ignoreModifiesOfNonexistentEntries.addLongIdentifier( 667 "ignore-modifies-of-nonexistent-entries", true); 668 ignoreModifiesOfNonexistentEntries.addLongIdentifier( 669 "ignoreNonexistentModifies", true); 670 ignoreModifiesOfNonexistentEntries.addLongIdentifier( 671 "ignore-nonexistent-modifies", true); 672 ignoreModifiesOfNonexistentEntries.setArgumentGroupName( 673 INFO_LDIFMODIFY_ARG_GROUP_INPUT.get()); 674 parser.addArgument(ignoreModifiesOfNonexistentEntries); 675 676 677 targetLDIF = new FileArgument('t', "targetLDIF", (targetWriter == null), 1, 678 null, INFO_LDIFMODIFY_ARG_DESC_TARGET_LDIF.get(), false, true, true, 679 false); 680 targetLDIF.addLongIdentifier("target-ldif", true); 681 targetLDIF.addLongIdentifier("targetFile", true); 682 targetLDIF.addLongIdentifier("target-file", true); 683 targetLDIF.addLongIdentifier("target", true); 684 targetLDIF.addLongIdentifier("outputLDIF", true); 685 targetLDIF.addLongIdentifier("output-ldif", true); 686 targetLDIF.addLongIdentifier("outputFile", true); 687 targetLDIF.addLongIdentifier("output-file", true); 688 targetLDIF.addLongIdentifier("output", true); 689 targetLDIF.setArgumentGroupName(INFO_LDIFMODIFY_ARG_GROUP_OUTPUT.get()); 690 parser.addArgument(targetLDIF); 691 692 693 compressTarget = new BooleanArgument(null, "compressTarget", 1, 694 INFO_LDIFMODIFY_ARG_DESC_COMPRESS_TARGET.get()); 695 compressTarget.addLongIdentifier("compress-target", true); 696 compressTarget.addLongIdentifier("compressOutput", true); 697 compressTarget.addLongIdentifier("compress-output", true); 698 compressTarget.addLongIdentifier("compress", true); 699 compressTarget.setArgumentGroupName(INFO_LDIFMODIFY_ARG_GROUP_OUTPUT.get()); 700 parser.addArgument(compressTarget); 701 702 703 encryptTarget = new BooleanArgument(null, "encryptTarget", 1, 704 INFO_LDIFMODIFY_ARG_DESC_ENCRYPT_TARGET.get()); 705 encryptTarget.addLongIdentifier("encrypt-target", true); 706 encryptTarget.addLongIdentifier("encryptOutput", true); 707 encryptTarget.addLongIdentifier("encrypt-output", true); 708 encryptTarget.addLongIdentifier("encrypt", true); 709 encryptTarget.setArgumentGroupName(INFO_LDIFMODIFY_ARG_GROUP_OUTPUT.get()); 710 parser.addArgument(encryptTarget); 711 712 713 targetEncryptionPassphraseFile = new FileArgument(null, 714 "targetEncryptionPassphraseFile", false, 1, null, 715 INFO_LDIFMODIFY_ARG_DESC_TARGET_PW_FILE.get(), true, true, true, 716 false); 717 targetEncryptionPassphraseFile.addLongIdentifier( 718 "target-encryption-passphrase-file", true); 719 targetEncryptionPassphraseFile.addLongIdentifier("targetPassphraseFile", 720 true); 721 targetEncryptionPassphraseFile.addLongIdentifier("target-passphrase-file", 722 true); 723 targetEncryptionPassphraseFile.addLongIdentifier( 724 "targetEncryptionPasswordFile", true); 725 targetEncryptionPassphraseFile.addLongIdentifier( 726 "target-encryption-password-file", true); 727 targetEncryptionPassphraseFile.addLongIdentifier("targetPasswordFile", 728 true); 729 targetEncryptionPassphraseFile.addLongIdentifier("target-password-file", 730 true); 731 targetEncryptionPassphraseFile.addLongIdentifier( 732 "outputEncryptionPassphraseFile", true); 733 targetEncryptionPassphraseFile.addLongIdentifier( 734 "output-encryption-passphrase-file", true); 735 targetEncryptionPassphraseFile.addLongIdentifier("outputPassphraseFile", 736 true); 737 targetEncryptionPassphraseFile.addLongIdentifier("output-passphrase-file", 738 true); 739 targetEncryptionPassphraseFile.addLongIdentifier( 740 "outputEncryptionPasswordFile", true); 741 targetEncryptionPassphraseFile.addLongIdentifier( 742 "output-encryption-password-file", true); 743 targetEncryptionPassphraseFile.addLongIdentifier("outputPasswordFile", 744 true); 745 targetEncryptionPassphraseFile.addLongIdentifier("output-password-file", 746 true); 747 targetEncryptionPassphraseFile.setArgumentGroupName( 748 INFO_LDIFMODIFY_ARG_GROUP_OUTPUT.get()); 749 750 parser.addArgument(targetEncryptionPassphraseFile); 751 752 753 wrapColumn = new IntegerArgument(null, "wrapColumn", false, 1, null, 754 INFO_LDIFMODIFY_ARG_DESC_WRAP_COLUMN.get(), 5, Integer.MAX_VALUE); 755 wrapColumn.addLongIdentifier("wrap-column", true); 756 wrapColumn.setArgumentGroupName(INFO_LDIFMODIFY_ARG_GROUP_OUTPUT.get()); 757 parser.addArgument(wrapColumn); 758 759 760 doNotWrap = new BooleanArgument('T', "doNotWrap", 1, 761 INFO_LDIFMODIFY_ARG_DESC_DO_NOT_WRAP.get()); 762 doNotWrap.addLongIdentifier("do-not-wrap", true); 763 doNotWrap.addLongIdentifier("dontWrap", true); 764 doNotWrap.addLongIdentifier("dont-wrap", true); 765 doNotWrap.addLongIdentifier("noWrap", true); 766 doNotWrap.addLongIdentifier("no-wrap", true); 767 doNotWrap.setArgumentGroupName(INFO_LDIFMODIFY_ARG_GROUP_OUTPUT.get()); 768 parser.addArgument(doNotWrap); 769 770 771 suppressComments = new BooleanArgument(null, "suppressComments", 1, 772 INFO_LDIFMODIFY_ARG_DESC_SUPPRESS_COMMENTS.get()); 773 suppressComments.addLongIdentifier("suppress-comments", true); 774 suppressComments.addLongIdentifier("excludeComments", true); 775 suppressComments.addLongIdentifier("exclude-comments", true); 776 suppressComments.addLongIdentifier("noComments", true); 777 suppressComments.addLongIdentifier("no-comments", true); 778 suppressComments.setArgumentGroupName( 779 INFO_LDIFMODIFY_ARG_GROUP_OUTPUT.get()); 780 parser.addArgument(suppressComments); 781 782 783 noSchemaCheck = new BooleanArgument(null, "noSchemaCheck", 1, 784 INFO_LDIFMODIFY_ARG_DESC_NO_SCHEMA_CHECK.get()); 785 noSchemaCheck.addLongIdentifier("no-schema-check", true); 786 noSchemaCheck.setHidden(true); 787 parser.addArgument(noSchemaCheck); 788 789 790 parser.addExclusiveArgumentSet(lenientModifications, strictModifications); 791 792 parser.addExclusiveArgumentSet(wrapColumn, doNotWrap); 793 794 parser.addDependentArgumentSet(targetEncryptionPassphraseFile, 795 encryptTarget); 796 } 797 798 799 800 /** 801 * {@inheritDoc} 802 */ 803 @Override() 804 @NotNull() 805 public ResultCode doToolProcessing() 806 { 807 // Read all of the changes into memory. 808 final Map<DN,List<LDIFChangeRecord>> addAndSubsequentChangeRecords = 809 new TreeMap<>(); 810 final Map<DN,Boolean> deletedEntryDNs = new TreeMap<>(); 811 final Map<DN,List<LDIFModifyChangeRecord>> modifyChangeRecords = 812 new HashMap<>(); 813 final Map<DN,ObjectPair<DN,List<LDIFChangeRecord>>> 814 modifyDNAndSubsequentChangeRecords = new TreeMap<>(); 815 final AtomicReference<ResultCode> resultCode = new AtomicReference<>(); 816 try 817 { 818 readChangeRecords(addAndSubsequentChangeRecords, deletedEntryDNs, 819 modifyChangeRecords, modifyDNAndSubsequentChangeRecords, resultCode); 820 } 821 catch (final LDAPException e) 822 { 823 Debug.debugException(e); 824 logCompletionMessage(true, e.getMessage()); 825 resultCode.compareAndSet(null, e.getResultCode()); 826 return resultCode.get(); 827 } 828 829 830 boolean changesIgnored = false; 831 LDIFReader ldifReader = null; 832 LDIFWriter ldifWriter = null; 833 final AtomicLong entriesRead = new AtomicLong(0L); 834 final AtomicLong entriesUpdated = new AtomicLong(0L); 835 try 836 { 837 // Open the source LDIF file for reading. 838 try 839 { 840 ldifReader = getLDIFReader(sourceReader, sourceLDIF.getValue(), 841 sourceEncryptionPassphraseFile.getValue()); 842 } 843 catch (final LDAPException e) 844 { 845 Debug.debugException(e); 846 logCompletionMessage(true, e.getMessage()); 847 return e.getResultCode(); 848 } 849 850 851 // Open the target LDIF file for writing. 852 try 853 { 854 ldifWriter = getLDIFWriter(targetWriter); 855 } 856 catch (final LDAPException e) 857 { 858 Debug.debugException(e); 859 logCompletionMessage(true, e.getMessage()); 860 return e.getResultCode(); 861 } 862 863 864 // Iterate through the source LDIF file and apply changes as appropriate. 865 final StringBuilder comment = new StringBuilder(); 866 while (true) 867 { 868 final LDIFRecord sourceRecord; 869 try 870 { 871 sourceRecord = ldifReader.readLDIFRecord(); 872 } 873 catch (final LDIFException e) 874 { 875 Debug.debugException(e); 876 877 if (e.mayContinueReading()) 878 { 879 resultCode.compareAndSet(null, ResultCode.DECODING_ERROR); 880 wrapErr(ERR_LDIFMODIFY_RECOVERABLE_DECODE_ERROR.get( 881 sourceLDIF.getValue(), StaticUtils.getExceptionMessage(e))); 882 continue; 883 } 884 else 885 { 886 logCompletionMessage(true, 887 ERR_LDIFMODIFY_UNRECOVERABLE_DECODE_ERROR.get( 888 sourceLDIF.getValue(), 889 StaticUtils.getExceptionMessage(e))); 890 return ResultCode.DECODING_ERROR; 891 } 892 } 893 catch (final IOException e) 894 { 895 Debug.debugException(e); 896 logCompletionMessage(true, 897 ERR_LDIFMODIFY_READ_ERROR.get(sourceLDIF.getValue(), 898 StaticUtils.getExceptionMessage(e))); 899 return ResultCode.LOCAL_ERROR; 900 } 901 902 903 // If the record we read was null, then we've hit the end of the source 904 // content. 905 if (sourceRecord == null) 906 { 907 break; 908 } 909 910 911 // If the record we read was an entry, then apply changes to it. If it 912 // was not, then that's an error. 913 comment.setLength(0); 914 915 final LDIFRecord targetRecord; 916 if (sourceRecord instanceof Entry) 917 { 918 entriesRead.incrementAndGet(); 919 targetRecord = updateEntry((Entry) sourceRecord, 920 addAndSubsequentChangeRecords, deletedEntryDNs, 921 modifyChangeRecords, modifyDNAndSubsequentChangeRecords, comment, 922 resultCode, entriesUpdated); 923 } 924 else 925 { 926 targetRecord = sourceRecord; 927 // NOTE: We're using false for the isError flag in this case because 928 // a better error will be recorded by the createChangeRecordComment 929 // call below. 930 appendComment(comment, 931 ERR_LDIFMODIFY_COMMENT_SOURCE_RECORD_NOT_ENTRY.get(), false); 932 933 final StringBuilder msgBuffer = new StringBuilder(); 934 createChangeRecordComment(msgBuffer, 935 ERR_LDIFMODIFY_OUTPUT_SOURCE_RECORD_NOT_ENTRY.get( 936 sourceLDIF.getValue().getAbsolutePath()), 937 sourceRecord, true); 938 wrapErr(msgBuffer.toString()); 939 resultCode.compareAndSet(null, ResultCode.DECODING_ERROR); 940 } 941 942 943 // Write the potentially updated entry to the target LDIF file. If the 944 // target record is null, then that means the entry has been deleted, 945 // but we still may want to write a comment about the deleted entry to 946 // the target file. 947 try 948 { 949 if (targetRecord == null) 950 { 951 if ((comment.length() > 0) && (! suppressComments.isPresent())) 952 { 953 writeLDIFComment(ldifWriter, comment, false); 954 } 955 } 956 else 957 { 958 writeLDIFRecord(ldifWriter, targetRecord, comment); 959 } 960 } 961 catch (final IOException e) 962 { 963 Debug.debugException(e); 964 logCompletionMessage(true, 965 ERR_LDIFMODIFY_WRITE_ERROR.get(targetLDIF.getValue(), 966 StaticUtils.getExceptionMessage(e))); 967 return ResultCode.LOCAL_ERROR; 968 } 969 } 970 971 972 try 973 { 974 // If there are any remaining add records, then process them. 975 final AtomicBoolean isUpdated = new AtomicBoolean(); 976 for (final List<LDIFChangeRecord> records : 977 addAndSubsequentChangeRecords.values()) 978 { 979 final Iterator<LDIFChangeRecord> iterator = records.iterator(); 980 final LDIFAddChangeRecord addChangeRecord = 981 (LDIFAddChangeRecord) iterator.next(); 982 Entry entry = addChangeRecord.getEntryToAdd(); 983 comment.setLength(0); 984 if (iterator.hasNext()) 985 { 986 createChangeRecordComment(comment, 987 INFO_LDIFMODIFY_ADDING_ENTRY_WITH_MODS.get(), addChangeRecord, 988 false); 989 while (iterator.hasNext()) 990 { 991 entry = applyModification(entry, 992 (LDIFModifyChangeRecord) iterator.next(), isUpdated, 993 resultCode, comment); 994 } 995 } 996 else 997 { 998 appendComment(comment, 999 INFO_LDIFMODIFY_ADDING_ENTRY_NO_MODS.get(), false); 1000 } 1001 1002 writeLDIFRecord(ldifWriter, entry, comment); 1003 entriesUpdated.incrementAndGet(); 1004 } 1005 1006 1007 // If there are any remaining DNs to delete, then those entries must not 1008 // have been in the source LDIF. 1009 for (final Map.Entry<DN,Boolean> e : deletedEntryDNs.entrySet()) 1010 { 1011 if (e.getValue() == Boolean.FALSE) 1012 { 1013 if (ignoreDeletesOfNonexistentEntries.isPresent()) 1014 { 1015 changesIgnored = true; 1016 } 1017 else 1018 { 1019 resultCode.compareAndSet(null, ResultCode.NO_SUCH_OBJECT); 1020 writeLDIFComment(ldifWriter, 1021 ERR_LDIFMODIFY_NO_SUCH_ENTRY_TO_DELETE.get( 1022 e.getKey().toString()), 1023 true); 1024 } 1025 } 1026 } 1027 1028 1029 // If there are any remaining modify change records, then those entries 1030 // must not have been in the source LDIF. 1031 for (final List<LDIFModifyChangeRecord> l : 1032 modifyChangeRecords.values()) 1033 { 1034 for (final LDIFChangeRecord r : l) 1035 { 1036 if (ignoreModifiesOfNonexistentEntries.isPresent()) 1037 { 1038 changesIgnored = true; 1039 } 1040 else 1041 { 1042 resultCode.compareAndSet(null, ResultCode.NO_SUCH_OBJECT); 1043 comment.setLength(0); 1044 createChangeRecordComment(comment, 1045 ERR_LDIFMODIFY_NO_SUCH_ENTRY_TO_MODIFY.get(), r, true); 1046 writeLDIFComment(ldifWriter, comment, false); 1047 } 1048 } 1049 } 1050 1051 1052 // If there are any remaining modify DN change records, then those 1053 // entries must not have been in the source LDIF. 1054 for (final ObjectPair<DN,List<LDIFChangeRecord>> l : 1055 modifyDNAndSubsequentChangeRecords.values()) 1056 { 1057 for (final LDIFChangeRecord r : l.getSecond()) 1058 { 1059 resultCode.compareAndSet(null, ResultCode.NO_SUCH_OBJECT); 1060 comment.setLength(0); 1061 if (r instanceof LDIFModifyDNChangeRecord) 1062 { 1063 createChangeRecordComment(comment, 1064 ERR_LDIFMODIFY_NO_SUCH_ENTRY_TO_RENAME.get(), r, true); 1065 } 1066 else 1067 { 1068 createChangeRecordComment(comment, 1069 ERR_LDIFMODIFY_NO_SUCH_ENTRY_TO_MODIFY.get(), r, true); 1070 } 1071 writeLDIFComment(ldifWriter, comment, false); 1072 } 1073 } 1074 } 1075 catch (final IOException e) 1076 { 1077 Debug.debugException(e); 1078 logCompletionMessage(true, 1079 ERR_LDIFMODIFY_WRITE_ERROR.get( 1080 targetLDIF.getValue().getAbsolutePath(), 1081 StaticUtils.getExceptionMessage(e))); 1082 return ResultCode.LOCAL_ERROR; 1083 } 1084 } 1085 finally 1086 { 1087 if (ldifReader != null) 1088 { 1089 try 1090 { 1091 ldifReader.close(); 1092 } 1093 catch (final Exception e) 1094 { 1095 Debug.debugException(e); 1096 resultCode.compareAndSet(null, ResultCode.LOCAL_ERROR); 1097 logCompletionMessage(true, 1098 ERR_LDIFMODIFY_ERROR_CLOSING_READER.get( 1099 sourceLDIF.getValue().getAbsolutePath(), 1100 StaticUtils.getExceptionMessage(e))); 1101 } 1102 } 1103 1104 if (ldifWriter != null) 1105 { 1106 try 1107 { 1108 ldifWriter.close(); 1109 } 1110 catch (final Exception e) 1111 { 1112 Debug.debugException(e); 1113 resultCode.compareAndSet(null, ResultCode.LOCAL_ERROR); 1114 logCompletionMessage(true, 1115 ERR_LDIFMODIFY_ERROR_CLOSING_WRITER.get( 1116 sourceLDIF.getValue().getAbsolutePath(), 1117 StaticUtils.getExceptionMessage(e))); 1118 } 1119 } 1120 } 1121 1122 1123 // If no entries were read and no updates were applied, then we'll consider 1124 // that an error, regardless of whether a read error was encountered. 1125 if ((entriesRead.get() == 0L) && (entriesUpdated.get() == 0L)) 1126 { 1127 if (resultCode.get() == null) 1128 { 1129 logCompletionMessage(true, 1130 ERR_LDIFMODIFY_NO_SOURCE_ENTRIES.get( 1131 sourceLDIF.getValue().getAbsolutePath())); 1132 return ResultCode.PARAM_ERROR; 1133 } 1134 else 1135 { 1136 logCompletionMessage(true, 1137 ERR_LDIFMODIFY_COULD_NOT_READ_SOURCE_ENTRIES.get( 1138 sourceLDIF.getValue().getAbsolutePath())); 1139 return resultCode.get(); 1140 } 1141 } 1142 1143 1144 // If no entries were updated, then we'll also consider that an error. 1145 if ((entriesUpdated.get() == 0L) && (! changesIgnored)) 1146 { 1147 logCompletionMessage(true, 1148 ERR_LDIFMODIFY_NO_CHANGES_APPLIED_WITH_ERRORS.get( 1149 changesLDIF.getValue().getAbsolutePath(), 1150 sourceLDIF.getValue().getAbsolutePath())); 1151 resultCode.compareAndSet(null, ResultCode.PARAM_ERROR); 1152 return resultCode.get(); 1153 } 1154 1155 1156 // Create the final completion message that will be used. 1157 final long entriesNotUpdated = 1158 Math.max((entriesRead.get() - entriesUpdated.get()), 0); 1159 if (resultCode.get() == null) 1160 { 1161 logCompletionMessage(false, 1162 INFO_LDIFMODIFY_COMPLETED_SUCCESSFULLY.get(entriesRead.get(), 1163 entriesUpdated.get(), entriesNotUpdated)); 1164 return ResultCode.SUCCESS; 1165 } 1166 else 1167 { 1168 logCompletionMessage(true, 1169 ERR_LDIFMODIFY_COMPLETED_WITH_ERRORS.get(entriesRead.get(), 1170 entriesUpdated.get(), entriesNotUpdated)); 1171 return resultCode.get(); 1172 } 1173 } 1174 1175 1176 1177 /** 1178 * Reads all of the LDIF change records from the changes file into a list. 1179 * 1180 * @param addAndSubsequentChangeRecords 1181 * A map that will be updated with add change records for a given 1182 * entry, along with any subsequent change records that apply to 1183 * the entry after it has been added. It must not be 1184 * {@code null}, must be empty, and must be updatable. 1185 * @param deletedEntryDNs 1186 * A map that will be updated with the DNs of any entries that 1187 * are targeted by delete modifications and that have not been 1188 * previously added or renamed. It must not be {@code null}, 1189 * must be empty, and must be updatable. 1190 * @param modifyChangeRecords 1191 * A map that will be updated with any modify change records 1192 * that target an entry that has not been targeted by any other 1193 * type of change. It must not be {@code null}, must be empty, 1194 * and must be updatable. 1195 * @param modifyDNAndSubsequentChangeRecords 1196 * A map that will be updated with any change records for modify 1197 * DN operations that target a given entry, and any subsequent 1198 * operations that target the entry with its new DN. It must not 1199 * be {@code null}, must be empty, and must be updatable. 1200 * @param resultCode 1201 * A reference to the final result code that should be used for 1202 * the tool. This may be updated if an error occurred during 1203 * processing and no value is already set. It must not be 1204 * {@code null}, but is allowed to have no value assigned. 1205 * 1206 * @throws LDAPException If an unrecoverable error occurs during processing. 1207 */ 1208 private void readChangeRecords( 1209 @NotNull final Map<DN,List<LDIFChangeRecord>> 1210 addAndSubsequentChangeRecords, 1211 @NotNull final Map<DN,Boolean> deletedEntryDNs, 1212 @NotNull final Map<DN,List<LDIFModifyChangeRecord>> modifyChangeRecords, 1213 @NotNull final Map<DN,ObjectPair<DN,List<LDIFChangeRecord>>> 1214 modifyDNAndSubsequentChangeRecords, 1215 @NotNull final AtomicReference<ResultCode> resultCode) 1216 throws LDAPException 1217 { 1218 LDIFException firstRecoverableException = null; 1219 try (LDIFReader ldifReader = getLDIFReader(changesReader, 1220 changesLDIF.getValue(), changesEncryptionPassphraseFile.getValue())) 1221 { 1222changeRecordLoop: 1223 while (true) 1224 { 1225 // Read the next record from the changes file. 1226 final LDIFRecord ldifRecord; 1227 try 1228 { 1229 ldifRecord = ldifReader.readLDIFRecord(); 1230 } 1231 catch (final LDIFException e) 1232 { 1233 Debug.debugException(e); 1234 1235 if (e.mayContinueReading()) 1236 { 1237 if (firstRecoverableException == null) 1238 { 1239 firstRecoverableException = e; 1240 } 1241 1242 err(); 1243 wrapErr(ERR_LDIFMODIFY_CANNOT_READ_RECORD_CAN_CONTINUE.get( 1244 changesLDIF.getValue().getAbsolutePath(), 1245 StaticUtils.getExceptionMessage(e))); 1246 resultCode.compareAndSet(null, ResultCode.DECODING_ERROR); 1247 continue changeRecordLoop; 1248 } 1249 else 1250 { 1251 throw new LDAPException(ResultCode.DECODING_ERROR, 1252 ERR_LDIFMODIFY_CANNOT_READ_RECORD_CANNOT_CONTINUE.get( 1253 changesLDIF.getValue().getAbsolutePath(), 1254 StaticUtils.getExceptionMessage(e)), 1255 e); 1256 } 1257 } 1258 1259 if (ldifRecord == null) 1260 { 1261 break; 1262 } 1263 1264 1265 // Make sure that we can parse the DN for the change record. If not, 1266 // then that's an error. 1267 final DN parsedDN; 1268 try 1269 { 1270 parsedDN = ldifRecord.getParsedDN(); 1271 } 1272 catch (final LDAPException e) 1273 { 1274 Debug.debugException(e); 1275 1276 err(); 1277 wrapErr(ERR_LDIFMODIFY_CANNOT_PARSE_CHANGE_RECORD_DN.get( 1278 String.valueOf(ldifRecord), 1279 changesLDIF.getValue().getAbsolutePath(), e.getMessage())); 1280 resultCode.compareAndSet(null, e.getResultCode()); 1281 continue changeRecordLoop; 1282 } 1283 1284 1285 // Get the LDIF record as a change record. If the record is an entry 1286 // rather than a change record, then we'll treat it as an add change 1287 // record. 1288 final LDIFChangeRecord changeRecord; 1289 if (ldifRecord instanceof Entry) 1290 { 1291 changeRecord = new LDIFAddChangeRecord((Entry) ldifRecord); 1292 } 1293 else 1294 { 1295 changeRecord = (LDIFChangeRecord) ldifRecord; 1296 } 1297 1298 1299 // If the change record is for a modify DN, then make sure that we can 1300 // parse the new DN. 1301 final DN parsedNewDN; 1302 if (changeRecord.getChangeType() == ChangeType.MODIFY_DN) 1303 { 1304 try 1305 { 1306 parsedNewDN = ((LDIFModifyDNChangeRecord) changeRecord).getNewDN(); 1307 } 1308 catch (final LDAPException e) 1309 { 1310 Debug.debugException(e); 1311 1312 err(); 1313 wrapErr(ERR_LDIFMODIFY_CANNOT_PARSE_NEW_DN.get( 1314 String.valueOf(changeRecord), 1315 changesLDIF.getValue().getAbsolutePath(), e.getMessage())); 1316 resultCode.compareAndSet(null, e.getResultCode()); 1317 continue changeRecordLoop; 1318 } 1319 } 1320 else 1321 { 1322 parsedNewDN = parsedDN; 1323 } 1324 1325 1326 // Look at the change type and determine how to handle the operation. 1327 switch (changeRecord.getChangeType()) 1328 { 1329 case ADD: 1330 // Make sure that we haven't already seen an add for an entry with 1331 // the same DN (unless that add was subsequently deleted). 1332 if (addAndSubsequentChangeRecords.containsKey(parsedDN)) 1333 { 1334 err(); 1335 wrapErr(ERR_LDIFMODIFY_MULTIPLE_ADDS_FOR_DN.get( 1336 changesLDIF.getValue().getAbsolutePath(), 1337 parsedDN.toString())); 1338 resultCode.compareAndSet(null, ResultCode.ENTRY_ALREADY_EXISTS); 1339 continue changeRecordLoop; 1340 } 1341 1342 // Make sure that there are no modifies targeting an entry with the 1343 // same DN. 1344 if (modifyChangeRecords.containsKey(parsedDN)) 1345 { 1346 err(); 1347 wrapErr(ERR_LDIFMODIFY_ADD_TARGETS_MODIFIED_ENTRY.get( 1348 changesLDIF.getValue().getAbsolutePath(), 1349 parsedDN.toString())); 1350 resultCode.compareAndSet(null, ResultCode.ENTRY_ALREADY_EXISTS); 1351 continue changeRecordLoop; 1352 } 1353 1354 // Make sure that there aren't any modify DN operations that will 1355 // create an entry with the same or a subordinate DN. 1356 for (final Map.Entry<DN,ObjectPair<DN,List<LDIFChangeRecord>>> e : 1357 modifyDNAndSubsequentChangeRecords.entrySet()) 1358 { 1359 final DN newDN = e.getValue().getFirst(); 1360 if (parsedDN.isAncestorOf(newDN, true)) 1361 { 1362 err(); 1363 wrapErr(ERR_LDIFMODIFY_ADD_CONFLICTS_WITH_MOD_DN.get( 1364 changesLDIF.getValue().getAbsolutePath(), 1365 parsedDN.toString(), e.getKey().toString(), 1366 newDN.toString())); 1367 resultCode.compareAndSet(null, ResultCode.ENTRY_ALREADY_EXISTS); 1368 continue changeRecordLoop; 1369 } 1370 } 1371 1372 final List<LDIFChangeRecord> addList = new ArrayList<>(); 1373 addList.add(changeRecord); 1374 addAndSubsequentChangeRecords.put(parsedDN, addList); 1375 break; 1376 1377 1378 case DELETE: 1379 // If the set of changes already included an add for this entry, 1380 // then remove that add and any subsequent changes for it. This 1381 // isn't an error, so we don't need to set a result code. 1382 if (addAndSubsequentChangeRecords.containsKey(parsedDN)) 1383 { 1384 addAndSubsequentChangeRecords.remove(parsedDN); 1385 err(); 1386 wrapErr(WARN_LDIFMODIFY_DELETE_OF_PREVIOUS_ADD.get( 1387 changesLDIF.getValue().getAbsolutePath(), 1388 parsedDN.toString())); 1389 continue changeRecordLoop; 1390 } 1391 1392 // If the set of changes already included a modify DN that targeted 1393 // the entry, then reject the change. 1394 if (modifyDNAndSubsequentChangeRecords.containsKey(parsedDN)) 1395 { 1396 final DN newDN = 1397 modifyDNAndSubsequentChangeRecords.get(parsedDN).getFirst(); 1398 1399 err(); 1400 wrapErr(ERR_LDIFMODIFY_DELETE_OF_PREVIOUS_RENAME.get( 1401 changesLDIF.getValue().getAbsolutePath(), 1402 parsedDN.toString(), newDN.toString())); 1403 resultCode.compareAndSet(null, ResultCode.NO_SUCH_OBJECT); 1404 continue changeRecordLoop; 1405 } 1406 1407 // If the set of changes already included a modify DN whose new DN 1408 // equals or is subordinate to the DN for the delete change 1409 // record, then remove that modify DN operation and any subsequent 1410 // changes for it, and instead add a delete for the original DN. 1411 // This isn't an error, so we don't need to set a result code. 1412 final Iterator<Map.Entry<DN,ObjectPair<DN,List<LDIFChangeRecord>>>> 1413 deleteModDNIterator = 1414 modifyDNAndSubsequentChangeRecords.entrySet().iterator(); 1415 while (deleteModDNIterator.hasNext()) 1416 { 1417 final Map.Entry<DN,ObjectPair<DN,List<LDIFChangeRecord>>> e = 1418 deleteModDNIterator.next(); 1419 final DN newDN = e.getValue().getFirst(); 1420 if (parsedDN.isAncestorOf(newDN, true)) 1421 { 1422 final DN originalDN = e.getKey(); 1423 deleteModDNIterator.remove(); 1424 deletedEntryDNs.put(originalDN, Boolean.FALSE); 1425 1426 err(); 1427 wrapErr(WARN_LDIFMODIFY_DELETE_OF_PREVIOUSLY_RENAMED.get( 1428 changesLDIF.getValue().getAbsolutePath(), 1429 parsedDN.toString(), originalDN.toString(), 1430 newDN.toString())); 1431 continue changeRecordLoop; 1432 } 1433 } 1434 1435 // If the set of changes already included a delete for the same 1436 // DN, then reject the new change. 1437 if (deletedEntryDNs.containsKey(parsedDN)) 1438 { 1439 if (! ignoreDuplicateDeletes.isPresent()) 1440 { 1441 err(); 1442 wrapErr(ERR_LDIFMODIFY_MULTIPLE_DELETES_FOR_DN.get( 1443 changesLDIF.getValue().getAbsolutePath(), 1444 parsedDN.toString())); 1445 resultCode.compareAndSet(null, ResultCode.NO_SUCH_OBJECT); 1446 } 1447 continue changeRecordLoop; 1448 } 1449 1450 // If the set of changes included any modifications for the same DN, 1451 // then remove those modifications. This isn't an error, so we 1452 // don't need to set a result code. 1453 if (modifyChangeRecords.containsKey(parsedDN)) 1454 { 1455 modifyChangeRecords.remove(parsedDN); 1456 err(); 1457 wrapErr(WARN_LDIFMODIFY_DELETE_OF_PREVIOUSLY_MODIFIED.get( 1458 changesLDIF.getValue().getAbsolutePath(), 1459 parsedDN.toString())); 1460 } 1461 1462 deletedEntryDNs.put(parsedDN, Boolean.FALSE); 1463 break; 1464 1465 1466 case MODIFY: 1467 // If the set of changes already included an add for an entry with 1468 // the same DN, then add the modify change record to the set of 1469 // changes following that add. 1470 if (addAndSubsequentChangeRecords.containsKey(parsedDN)) 1471 { 1472 addAndSubsequentChangeRecords.get(parsedDN).add(changeRecord); 1473 continue changeRecordLoop; 1474 } 1475 1476 // If the set of changes already included a modify DN for an entry 1477 // with the same DN, then reject the new change. 1478 if (modifyDNAndSubsequentChangeRecords.containsKey(parsedDN)) 1479 { 1480 final DN newDN = 1481 modifyDNAndSubsequentChangeRecords.get(parsedDN).getFirst(); 1482 1483 err(); 1484 wrapErr(ERR_LDIFMODIFY_MODIFY_OF_RENAMED_ENTRY.get( 1485 changesLDIF.getValue().getAbsolutePath(), 1486 parsedDN.toString(), newDN.toString())); 1487 resultCode.compareAndSet(null, ResultCode.NO_SUCH_OBJECT); 1488 continue changeRecordLoop; 1489 } 1490 1491 // If the set of changes already included a modify DN that would 1492 // result in an entry with the same DN as the modify, then add 1493 // the modify change record to the modify DN record's change list. 1494 for (final Map.Entry<DN,ObjectPair<DN,List<LDIFChangeRecord>>> e : 1495 modifyDNAndSubsequentChangeRecords.entrySet()) 1496 { 1497 if (parsedDN.equals(e.getValue().getFirst())) 1498 { 1499 e.getValue().getSecond().add(changeRecord); 1500 continue changeRecordLoop; 1501 } 1502 } 1503 1504 // If the set of changes already included a delete for an entry with 1505 // the same DN, then reject the new change. 1506 if (deletedEntryDNs.containsKey(parsedDN)) 1507 { 1508 err(); 1509 wrapErr(ERR_LDIFMODIFY_MODIFY_OF_DELETED_ENTRY.get( 1510 changesLDIF.getValue().getAbsolutePath(), 1511 parsedDN.toString())); 1512 resultCode.compareAndSet(null, ResultCode.NO_SUCH_OBJECT); 1513 continue changeRecordLoop; 1514 } 1515 1516 // If the set of changes already included a modify for an entry with 1517 // the same DN, then add the new change to that list. 1518 if (modifyChangeRecords.containsKey(parsedDN)) 1519 { 1520 modifyChangeRecords.get(parsedDN).add( 1521 (LDIFModifyChangeRecord) changeRecord); 1522 continue changeRecordLoop; 1523 } 1524 1525 // Start a new change record list for the modify operation. 1526 final List<LDIFModifyChangeRecord> modList = new ArrayList<>(); 1527 modList.add((LDIFModifyChangeRecord) changeRecord); 1528 modifyChangeRecords.put(parsedDN, modList); 1529 break; 1530 1531 1532 case MODIFY_DN: 1533 // If the set of changes already included an add for an entry with 1534 // the same DN, then reject the modify DN. 1535 if (addAndSubsequentChangeRecords.containsKey(parsedDN)) 1536 { 1537 err(); 1538 wrapErr(ERR_LDIFMODIFY_MOD_DN_OF_ADDED_ENTRY.get( 1539 changesLDIF.getValue().getAbsolutePath(), 1540 parsedDN.toString())); 1541 resultCode.compareAndSet(null, ResultCode.UNWILLING_TO_PERFORM); 1542 continue changeRecordLoop; 1543 } 1544 1545 // If the set of changes already included an add for an entry with 1546 // an entry at or below the new DN, then reject the modify DN. 1547 for (final DN addedDN : addAndSubsequentChangeRecords.keySet()) 1548 { 1549 if (addedDN.isDescendantOf(parsedNewDN, true)) 1550 { 1551 err(); 1552 wrapErr(ERR_LDIFMODIFY_MOD_DN_NEW_DN_CONFLICTS_WITH_ADD.get( 1553 changesLDIF.getValue().getAbsolutePath(), 1554 parsedDN.toString(), parsedNewDN.toString(), 1555 addedDN.toString())); 1556 resultCode.compareAndSet(null, ResultCode.ENTRY_ALREADY_EXISTS); 1557 continue changeRecordLoop; 1558 } 1559 } 1560 1561 // If the set of changes already included a modify DN for an entry 1562 // with the same DN, then reject the modify DN. 1563 if (modifyDNAndSubsequentChangeRecords.containsKey(parsedDN)) 1564 { 1565 err(); 1566 wrapErr(ERR_LDIFMODIFY_MULTIPLE_MOD_DN_WITH_DN.get( 1567 changesLDIF.getValue().getAbsolutePath(), 1568 parsedDN.toString())); 1569 resultCode.compareAndSet(null, ResultCode.NO_SUCH_OBJECT); 1570 continue changeRecordLoop; 1571 } 1572 1573 // If the set of changes already included a modify DN for an entry 1574 // that set a new DN that matches the DN of the new record, then 1575 // reject the modify DN. 1576 for (final Map.Entry<DN,ObjectPair<DN,List<LDIFChangeRecord>>> e : 1577 modifyDNAndSubsequentChangeRecords.entrySet()) 1578 { 1579 final DN newDN = e.getValue().getFirst(); 1580 if (newDN.isDescendantOf(parsedDN, true)) 1581 { 1582 err(); 1583 wrapErr( 1584 ERR_LDIFMODIFY_UNWILLING_TO_MODIFY_DN_MULTIPLE_TIMES.get( 1585 changesLDIF.getValue().getAbsolutePath(), 1586 parsedDN.toString(), parsedNewDN.toString(), 1587 e.getKey().toString())); 1588 resultCode.compareAndSet(null, ResultCode.UNWILLING_TO_PERFORM); 1589 continue changeRecordLoop; 1590 } 1591 } 1592 1593 // If the set of changes already included a modify DN that set a 1594 // new DN that is at or below the new DN, then reject the modify DN. 1595 for (final Map.Entry<DN,ObjectPair<DN,List<LDIFChangeRecord>>> e : 1596 modifyDNAndSubsequentChangeRecords.entrySet()) 1597 { 1598 final DN newDN = e.getValue().getFirst(); 1599 if (newDN.isDescendantOf(parsedNewDN, true)) 1600 { 1601 err(); 1602 wrapErr(ERR_LDIFMODIFY_MOD_DN_CONFLICTS_WITH_MOD_DN.get( 1603 changesLDIF.getValue().getAbsolutePath(), 1604 parsedDN.toString(), parsedNewDN.toString(), 1605 e.getKey().toString(), newDN.toString())); 1606 resultCode.compareAndSet(null, ResultCode.ENTRY_ALREADY_EXISTS); 1607 continue changeRecordLoop; 1608 } 1609 } 1610 1611 // If the set of changes already included a delete for an entry with 1612 //t he same DN, then reject the modify DN. 1613 if (deletedEntryDNs.containsKey(parsedDN)) 1614 { 1615 err(); 1616 wrapErr(ERR_LDIFMODIFY_MOD_DN_OF_DELETED_ENTRY.get( 1617 changesLDIF.getValue().getAbsolutePath(), 1618 parsedDN.toString())); 1619 resultCode.compareAndSet(null, ResultCode.NO_SUCH_OBJECT); 1620 continue changeRecordLoop; 1621 } 1622 1623 // If the set of changes already included a modify for an entry that 1624 // is at or below the new DN, then reject the modify DN. 1625 for (final DN dn : modifyChangeRecords.keySet()) 1626 { 1627 if (dn.isDescendantOf(parsedNewDN, true)) 1628 { 1629 err(); 1630 wrapErr(ERR_LDIFMODIFY_MOD_DN_NEW_DN_CONFLICTS_WITH_MOD.get( 1631 changesLDIF.getValue().getAbsolutePath(), 1632 parsedDN.toString(), parsedNewDN.toString(), 1633 dn.toString())); 1634 resultCode.compareAndSet(null, ResultCode.ENTRY_ALREADY_EXISTS); 1635 continue changeRecordLoop; 1636 } 1637 } 1638 1639 final List<LDIFChangeRecord> modDNList = new ArrayList<>(); 1640 modDNList.add(changeRecord); 1641 modifyDNAndSubsequentChangeRecords.put(parsedDN, 1642 new ObjectPair<DN,List<LDIFChangeRecord>>(parsedNewDN, 1643 modDNList)); 1644 break; 1645 } 1646 } 1647 } 1648 catch (final LDAPException e) 1649 { 1650 Debug.debugException(e); 1651 throw new LDAPException(e.getResultCode(), 1652 ERR_LDIFMODIFY_ERROR_OPENING_CHANGES_FILE.get( 1653 changesLDIF.getValue().getAbsolutePath(), e.getMessage()), 1654 e); 1655 } 1656 catch (final IOException e) 1657 { 1658 Debug.debugException(e); 1659 throw new LDAPException(ResultCode.LOCAL_ERROR, 1660 ERR_LDIFMODIFY_ERROR_READING_CHANGES_FILE.get( 1661 changesLDIF.getValue().getAbsolutePath(), 1662 StaticUtils.getExceptionMessage(e)), 1663 e); 1664 } 1665 1666 if (addAndSubsequentChangeRecords.isEmpty() && deletedEntryDNs.isEmpty() && 1667 modifyChangeRecords.isEmpty() && 1668 modifyDNAndSubsequentChangeRecords.isEmpty()) 1669 { 1670 if (firstRecoverableException == null) 1671 { 1672 throw new LDAPException(ResultCode.PARAM_ERROR, 1673 ERR_LDIFMODIFY_NO_CHANGES.get( 1674 changesLDIF.getValue().getAbsolutePath())); 1675 } 1676 else 1677 { 1678 throw new LDAPException(ResultCode.PARAM_ERROR, 1679 ERR_LDIFMODIFY_NO_CHANGES_WITH_ERROR.get( 1680 changesLDIF.getValue().getAbsolutePath()), 1681 firstRecoverableException); 1682 } 1683 } 1684 } 1685 1686 1687 1688 /** 1689 * Retrieves an LDIF reader that may be used to read LDIF records (either 1690 * entries or change records) from the specified LDIF file. 1691 * 1692 * @param existingReader An LDIF reader that was already provided to the 1693 * tool for this purpose. It may be {@code null} if 1694 * the LDIF reader should be created with the given 1695 * LDIF file and passphrase file. 1696 * @param ldifFile The LDIF file for which to create the reader. It 1697 * may be {@code null} only if {@code existingReader} 1698 * is non-{@code null}. 1699 * @param passphraseFile The file containing the encryption passphrase 1700 * needed to decrypt the contents of the provided LDIF 1701 * file. It may be {@code null} if the LDIF file is 1702 * not encrypted or if the user should be 1703 * interactively prompted for the passphrase. 1704 * 1705 * @return The LDIF reader that was created. 1706 * 1707 * @throws LDAPException If a problem occurs while creating the LDIF reader. 1708 */ 1709 @NotNull() 1710 private LDIFReader getLDIFReader(@Nullable final LDIFReader existingReader, 1711 @Nullable final File ldifFile, 1712 @Nullable final File passphraseFile) 1713 throws LDAPException 1714 { 1715 if (existingReader != null) 1716 { 1717 return existingReader; 1718 } 1719 1720 if (passphraseFile != null) 1721 { 1722 readPassphraseFile(passphraseFile); 1723 } 1724 1725 1726 boolean closeStream = true; 1727 InputStream inputStream = null; 1728 try 1729 { 1730 inputStream = new FileInputStream(ldifFile); 1731 1732 final ObjectPair<InputStream,char[]> p = 1733 ToolUtils.getPossiblyPassphraseEncryptedInputStream( 1734 inputStream, inputEncryptionPassphrases, 1735 (passphraseFile != null), 1736 INFO_LDIFMODIFY_ENTER_INPUT_ENCRYPTION_PW.get( 1737 ldifFile.getName()), 1738 ERR_LDIFMODIFY_WRONG_ENCRYPTION_PW.get(), getOut(), getErr()); 1739 inputStream = p.getFirst(); 1740 addPassphrase(p.getSecond()); 1741 1742 inputStream = ToolUtils.getPossiblyGZIPCompressedInputStream(inputStream); 1743 1744 final LDIFReader ldifReader = new LDIFReader(inputStream); 1745 if (stripTrailingSpaces.isPresent()) 1746 { 1747 ldifReader.setTrailingSpaceBehavior(TrailingSpaceBehavior.STRIP); 1748 } 1749 else 1750 { 1751 ldifReader.setTrailingSpaceBehavior(TrailingSpaceBehavior.REJECT); 1752 } 1753 1754 ldifReader.setSchema(Schema.getDefaultStandardSchema()); 1755 1756 closeStream = false; 1757 return ldifReader; 1758 } 1759 catch (final Exception e) 1760 { 1761 Debug.debugException(e); 1762 throw new LDAPException(ResultCode.LOCAL_ERROR, 1763 ERR_LDIFMODIFY_ERROR_OPENING_INPUT_FILE.get( 1764 ldifFile.getAbsolutePath(), 1765 StaticUtils.getExceptionMessage(e)), 1766 e); 1767 } 1768 finally 1769 { 1770 if ((inputStream != null) && closeStream) 1771 { 1772 try 1773 { 1774 inputStream.close(); 1775 } 1776 catch (final Exception e) 1777 { 1778 Debug.debugException(e); 1779 } 1780 } 1781 } 1782 } 1783 1784 1785 1786 /** 1787 * Reads the contents of the specified passphrase file and adds it to the list 1788 * of passphrases. 1789 * 1790 * @param f The passphrase file to read. 1791 * 1792 * @throws LDAPException If a problem is encountered while trying to read 1793 * the passphrase from the provided file. 1794 */ 1795 private void readPassphraseFile(@NotNull final File f) 1796 throws LDAPException 1797 { 1798 try 1799 { 1800 addPassphrase(getPasswordFileReader().readPassword(f)); 1801 } 1802 catch (final Exception e) 1803 { 1804 Debug.debugException(e); 1805 throw new LDAPException(ResultCode.LOCAL_ERROR, 1806 ERR_LDIFMODIFY_CANNOT_READ_PW_FILE.get(f.getAbsolutePath(), 1807 StaticUtils.getExceptionMessage(e)), 1808 e); 1809 } 1810 } 1811 1812 1813 1814 /** 1815 * Updates the list of encryption passphrases with the provided passphrase, if 1816 * it is not already present. 1817 * 1818 * @param passphrase The passphrase to be added. It may optionally be 1819 * {@code null} (in which case no action will be taken). 1820 */ 1821 private void addPassphrase(@Nullable final char[] passphrase) 1822 { 1823 if (passphrase == null) 1824 { 1825 return; 1826 } 1827 1828 for (final char[] existingPassphrase : inputEncryptionPassphrases) 1829 { 1830 if (Arrays.equals(existingPassphrase, passphrase)) 1831 { 1832 return; 1833 } 1834 } 1835 1836 inputEncryptionPassphrases.add(passphrase); 1837 } 1838 1839 1840 1841 /** 1842 * Creates the LDIF writer to use to write the output. 1843 * 1844 * @param existingWriter An LDIF writer that was already provided to the 1845 * tool for this purpose. It may be {@code null} if 1846 * the LDIF writer should be created using the 1847 * provided arguments. 1848 * 1849 * @return The LDIF writer that was created. 1850 * 1851 * @throws LDAPException If a problem occurs while creating the LDIF writer. 1852 */ 1853 @NotNull() 1854 private LDIFWriter getLDIFWriter(@Nullable final LDIFWriter existingWriter) 1855 throws LDAPException 1856 { 1857 if (existingWriter != null) 1858 { 1859 return existingWriter; 1860 } 1861 1862 final File outputFile = targetLDIF.getValue(); 1863 final File passphraseFile = targetEncryptionPassphraseFile.getValue(); 1864 1865 1866 OutputStream outputStream = null; 1867 boolean closeOutputStream = true; 1868 try 1869 { 1870 try 1871 { 1872 1873 outputStream = new FileOutputStream(targetLDIF.getValue()); 1874 } 1875 catch (final Exception e) 1876 { 1877 Debug.debugException(e); 1878 throw new LDAPException(ResultCode.LOCAL_ERROR, 1879 ERR_LDIFMODIFY_CANNOT_OPEN_OUTPUT_FILE.get( 1880 outputFile.getAbsolutePath(), 1881 StaticUtils.getExceptionMessage(e)), 1882 e); 1883 } 1884 1885 if (encryptTarget.isPresent()) 1886 { 1887 try 1888 { 1889 final char[] passphrase; 1890 if (passphraseFile != null) 1891 { 1892 passphrase = getPasswordFileReader().readPassword(passphraseFile); 1893 } 1894 else 1895 { 1896 passphrase = ToolUtils.promptForEncryptionPassphrase(false, true, 1897 INFO_LDIFMODIFY_ENTER_OUTPUT_ENCRYPTION_PW.get(), 1898 INFO_LDIFMODIFY_CONFIRM_OUTPUT_ENCRYPTION_PW.get(), getOut(), 1899 getErr()).toCharArray(); 1900 } 1901 1902 outputStream = new PassphraseEncryptedOutputStream(passphrase, 1903 outputStream, null, true, true); 1904 } 1905 catch (final Exception e) 1906 { 1907 Debug.debugException(e); 1908 throw new LDAPException(ResultCode.LOCAL_ERROR, 1909 ERR_LDIFMODIFY_CANNOT_ENCRYPT_OUTPUT_FILE.get( 1910 outputFile.getAbsolutePath(), 1911 StaticUtils.getExceptionMessage(e)), 1912 e); 1913 } 1914 } 1915 1916 if (compressTarget.isPresent()) 1917 { 1918 try 1919 { 1920 outputStream = new GZIPOutputStream(outputStream); 1921 } 1922 catch (final Exception e) 1923 { 1924 Debug.debugException(e); 1925 throw new LDAPException(ResultCode.LOCAL_ERROR, 1926 ERR_LDIFMODIFY_CANNOT_COMPRESS_OUTPUT_FILE.get( 1927 outputFile.getAbsolutePath(), 1928 StaticUtils.getExceptionMessage(e)), 1929 e); 1930 } 1931 } 1932 1933 final LDIFWriter ldifWriter = new LDIFWriter(outputStream); 1934 if (doNotWrap.isPresent()) 1935 { 1936 ldifWriter.setWrapColumn(0); 1937 } 1938 else if (wrapColumn.isPresent()) 1939 { 1940 ldifWriter.setWrapColumn(wrapColumn.getValue()); 1941 } 1942 else 1943 { 1944 ldifWriter.setWrapColumn(WRAP_COLUMN); 1945 } 1946 1947 closeOutputStream = false; 1948 return ldifWriter; 1949 } 1950 finally 1951 { 1952 if (closeOutputStream && (outputStream != null)) 1953 { 1954 try 1955 { 1956 outputStream.close(); 1957 } 1958 catch (final Exception e) 1959 { 1960 Debug.debugException(e); 1961 } 1962 } 1963 } 1964 } 1965 1966 1967 1968 /** 1969 * Updates the provided entry with any appropriate changes. 1970 * 1971 * @param entry 1972 * The entry to be processed. It must not be {@code null}. 1973 * @param addAndSubsequentChangeRecords 1974 * A map that will be updated with add change records for a given 1975 * entry, along with any subsequent change records that apply to 1976 * the entry after it has been added. It must not be 1977 * {@code null}, must be empty, and must be updatable. 1978 * @param deletedEntryDNs 1979 * A map that will be updated with the DNs of any entries that 1980 * are targeted by delete modifications and that have not been 1981 * previously added or renamed. It must not be {@code null}, 1982 * must be empty, and must be updatable. 1983 * @param modifyChangeRecords 1984 * A map that will be updated with any modify change records 1985 * that target an entry that has not been targeted by any other 1986 * type of change. It must not be {@code null}, must be empty, 1987 * and must be updatable. 1988 * @param modifyDNAndSubsequentChangeRecords 1989 * A map that will be updated with any change records for modify 1990 * DN operations that target a given entry, and any subsequent 1991 * operations that target the entry with its new DN. It must not 1992 * be {@code null}, must be empty, and must be updatable. 1993 * @param comment 1994 * A buffer that should be updated with any comment to be 1995 * included in the output, even if the entry is not altered. It 1996 * must not be {@code null}, but it should be empty. 1997 * @param resultCode 1998 * A reference to the final result code that should be used for 1999 * the tool. This may be updated if an error occurred during 2000 * processing and no value is already set. It must not be 2001 * {@code null}, but is allowed to have no value assigned. 2002 * @param entriesUpdated 2003 * A counter that should be incremented if any changes are 2004 * applied (including deleting the entry). It should not be 2005 * updated if none of the changes are applicable to the provided 2006 * entry. It must not be {@code null}. 2007 * 2008 * @return The provided entry if none of the changes are applicable, an 2009 * updated entry if changes are applied, or {@code null} if the entry 2010 * should be deleted and therefore omitted from the target LDIF file. 2011 */ 2012 @Nullable() 2013 private Entry updateEntry(@NotNull final Entry entry, 2014 @NotNull final Map<DN,List<LDIFChangeRecord>> 2015 addAndSubsequentChangeRecords, 2016 @NotNull final Map<DN,Boolean> deletedEntryDNs, 2017 @NotNull final Map<DN,List<LDIFModifyChangeRecord>> modifyChangeRecords, 2018 @NotNull final Map<DN,ObjectPair<DN,List<LDIFChangeRecord>>> 2019 modifyDNAndSubsequentChangeRecords, 2020 @NotNull final StringBuilder comment, 2021 @NotNull final AtomicReference<ResultCode> resultCode, 2022 @NotNull final AtomicLong entriesUpdated) 2023 { 2024 // Get the parsed DN for the entry. If that fails, then we'll just return 2025 // the provided entry along with a comment explaining that its DN could not 2026 // be parsed. 2027 final DN entryDN; 2028 try 2029 { 2030 entryDN = entry.getParsedDN(); 2031 2032 } 2033 catch (final LDAPException e) 2034 { 2035 Debug.debugException(e); 2036 resultCode.compareAndSet(null, e.getResultCode()); 2037 appendComment(comment, 2038 ERR_LDIFMODIFY_CANNOT_PARSE_ENTRY_DN.get(e.getMessage()), true); 2039 return entry; 2040 } 2041 2042 2043 // See if there is a delete change record for the entry. If so, then mark 2044 // the entry as deleted and return null. 2045 if (deletedEntryDNs.containsKey(entryDN)) 2046 { 2047 deletedEntryDNs.put(entryDN, Boolean.TRUE); 2048 createChangeRecordComment(comment, INFO_LDIFMODIFY_APPLIED_DELETE.get(), 2049 entry, false); 2050 entriesUpdated.incrementAndGet(); 2051 return null; 2052 } 2053 2054 2055 // See if there is a delete change record for one of the entry's superiors. 2056 // If so, then mark the entry as deleted and return null. 2057 DN parentDN = entryDN.getParent(); 2058 while (parentDN != null) 2059 { 2060 if (deletedEntryDNs.containsKey(parentDN)) 2061 { 2062 createChangeRecordComment(comment, 2063 INFO_LDIFMODIFY_APPLIED_DELETE_OF_ANCESTOR.get( 2064 parentDN.toString()), 2065 entry, false); 2066 entriesUpdated.incrementAndGet(); 2067 return null; 2068 } 2069 2070 parentDN = parentDN.getParent(); 2071 } 2072 2073 2074 // See if there are any modify change records that target the entry. If so, 2075 // then apply those modifications. 2076 Entry updatedEntry = entry; 2077 final AtomicBoolean isUpdated = new AtomicBoolean(false); 2078 final List<String> errors = new ArrayList<>(); 2079 final List<LDIFModifyChangeRecord> modRecords = 2080 modifyChangeRecords.remove(entryDN); 2081 if (modRecords != null) 2082 { 2083 for (final LDIFModifyChangeRecord r : modRecords) 2084 { 2085 updatedEntry = applyModification(updatedEntry, r, isUpdated, resultCode, 2086 comment); 2087 } 2088 } 2089 2090 2091 // See if the entry was targeted by a modify DN operation. If so, then 2092 // rename the entry and see if there are any follow-on modifications. 2093 final ObjectPair<DN,List<LDIFChangeRecord>> modDNRecords = 2094 modifyDNAndSubsequentChangeRecords.remove(entryDN); 2095 if (modDNRecords != null) 2096 { 2097 for (final LDIFChangeRecord r : modDNRecords.getSecond()) 2098 { 2099 if (r instanceof LDIFModifyDNChangeRecord) 2100 { 2101 final LDIFModifyDNChangeRecord modDNChangeRecord = 2102 (LDIFModifyDNChangeRecord) r; 2103 updatedEntry = applyModifyDN(updatedEntry, entryDN, 2104 modDNRecords.getFirst(), modDNChangeRecord.deleteOldRDN()); 2105 createChangeRecordComment(comment, 2106 INFO_LDIFMODIFY_APPLIED_MODIFY_DN.get(), r, false); 2107 isUpdated.set(true); 2108 } 2109 else 2110 { 2111 updatedEntry = applyModification(updatedEntry, 2112 (LDIFModifyChangeRecord) r, isUpdated, resultCode, comment); 2113 } 2114 } 2115 } 2116 2117 2118 // See if there is an add change record that targets the same entry. If so, 2119 // then the add won't be processed but maybe subsequent changes will be. 2120 final List<LDIFChangeRecord> addAndMods = 2121 addAndSubsequentChangeRecords.remove(entryDN); 2122 if (addAndMods != null) 2123 { 2124 for (final LDIFChangeRecord r : addAndMods) 2125 { 2126 if (r instanceof LDIFAddChangeRecord) 2127 { 2128 resultCode.compareAndSet(null, ResultCode.ENTRY_ALREADY_EXISTS); 2129 createChangeRecordComment(comment, 2130 ERR_LDIFMODIFY_NOT_ADDING_EXISTING_ENTRY.get(), r, true); 2131 } 2132 else 2133 { 2134 updatedEntry = applyModification(updatedEntry, 2135 (LDIFModifyChangeRecord) r, isUpdated, resultCode, comment); 2136 } 2137 } 2138 } 2139 2140 2141 if (isUpdated.get()) 2142 { 2143 entriesUpdated.incrementAndGet(); 2144 } 2145 else 2146 { 2147 if (comment.length() > 0) 2148 { 2149 appendComment(comment, StaticUtils.EOL, false); 2150 appendComment(comment, StaticUtils.EOL, false); 2151 } 2152 appendComment(comment, INFO_LDIFMODIFY_ENTRY_NOT_UPDATED.get(), false); 2153 } 2154 2155 return updatedEntry; 2156 } 2157 2158 2159 2160 /** 2161 * Creates a copy of the provided entry with the given modification applied. 2162 * 2163 * @param entry The entry to be updated. It must not be 2164 * {@code null}. 2165 * @param modifyChangeRecord The modify change record to apply. It must not 2166 * be {@code null}. 2167 * @param isUpdated A value that should be updated if the entry is 2168 * successfully modified. It must not be 2169 * {@code null}. 2170 * @param resultCode A reference to the final result code that 2171 * should be used for the tool. This may be 2172 * updated if an error occurred during processing 2173 * and no value is already set. It must not be 2174 * {@code null}, but is allowed to have no value 2175 * assigned. 2176 * @param comment A buffer that should be updated with any 2177 * comment to be included in the output, even if 2178 * the entry is not altered. It must not be 2179 * {@code null}, but it may be empty. 2180 * 2181 * @return The entry with the modifications applied, or the original entry if 2182 * an error occurred while applying the change. 2183 */ 2184 @NotNull() 2185 private Entry applyModification(@NotNull final Entry entry, 2186 @NotNull final LDIFModifyChangeRecord modifyChangeRecord, 2187 @NotNull final AtomicBoolean isUpdated, 2188 @NotNull final AtomicReference<ResultCode> resultCode, 2189 @NotNull final StringBuilder comment) 2190 { 2191 try 2192 { 2193 final Entry updatedEntry = Entry.applyModifications(entry, 2194 (! strictModifications.isPresent()), 2195 modifyChangeRecord.getModifications()); 2196 createChangeRecordComment(comment, INFO_LDIFMODIFY_APPLIED_MODIFY.get(), 2197 modifyChangeRecord, false); 2198 isUpdated.set(true); 2199 return updatedEntry; 2200 } 2201 catch (final LDAPException e) 2202 { 2203 Debug.debugException(e); 2204 resultCode.compareAndSet(null, e.getResultCode()); 2205 createChangeRecordComment(comment, 2206 ERR_LDIFMODIFY_ERROR_APPLYING_MODIFY.get( 2207 String.valueOf(e.getResultCode()), e.getMessage()), 2208 modifyChangeRecord, true); 2209 return entry; 2210 } 2211 } 2212 2213 2214 2215 /** 2216 * Creates a copy of the provided entry with the given new DN. 2217 * 2218 * @param entry The entry to be renamed. It must not be 2219 * {@code null}. 2220 * @param originalDN A parsed representation of the original DN for the 2221 * entry. It must not be {@code null}. 2222 * @param newDN A parsed representation of the new DN for the entry. 2223 * It must not be {@code null}. 2224 * @param deleteOldRDN Indicates whether the old RDN values should be 2225 * removed from the entry. 2226 * 2227 * @return The updated entry with the new DN and any other associated 2228 * changes. 2229 */ 2230 @NotNull() 2231 private Entry applyModifyDN(@NotNull final Entry entry, 2232 @NotNull final DN originalDN, 2233 @NotNull final DN newDN, 2234 final boolean deleteOldRDN) 2235 { 2236 final Entry copy = entry.duplicate(); 2237 copy.setDN(newDN); 2238 2239 final RDN oldRDN = originalDN.getRDN(); 2240 if (deleteOldRDN && (oldRDN != null)) 2241 { 2242 for (final Attribute a : oldRDN.getAttributes()) 2243 { 2244 for (final byte[] value : a.getValueByteArrays()) 2245 { 2246 copy.removeAttributeValue(a.getName(), value); 2247 } 2248 } 2249 } 2250 2251 final RDN newRDN = newDN.getRDN(); 2252 if (newRDN != null) 2253 { 2254 for (final Attribute a : newRDN.getAttributes()) 2255 { 2256 for (final byte[] value : a.getValueByteArrays()) 2257 { 2258 copy.addAttribute(a); 2259 } 2260 } 2261 } 2262 2263 return copy; 2264 } 2265 2266 2267 2268 /** 2269 * Writes the provided LDIF record to the LDIF writer. 2270 * 2271 * @param ldifWriter The writer to which the LDIF record should be written. 2272 * It must not be {@code null}. 2273 * @param ldifRecord The LDIF record to be written. It must not be 2274 * {@code null}. 2275 * @param comment The comment to include as part of the LDIF record. It 2276 * may be {@code null} or empty if no comment should be 2277 * included. 2278 * 2279 * @throws IOException If an error occurs while attempting to write to the 2280 * LDIF writer. 2281 */ 2282 private void writeLDIFRecord(@NotNull final LDIFWriter ldifWriter, 2283 @NotNull final LDIFRecord ldifRecord, 2284 @Nullable final CharSequence comment) 2285 throws IOException 2286 { 2287 if (suppressComments.isPresent() || (comment == null) || 2288 (comment.length() == 0)) 2289 { 2290 ldifWriter.writeLDIFRecord(ldifRecord); 2291 } 2292 else 2293 { 2294 ldifWriter.writeLDIFRecord(ldifRecord, comment.toString()); 2295 } 2296 } 2297 2298 2299 2300 /** 2301 * Appends the provided comment to the given buffer. 2302 * 2303 * @param buffer The buffer to which the comment should be appended. 2304 * @param comment The comment to be appended. 2305 * @param isError Indicates whether the comment represents an error that 2306 * should be added to the error list if it exists. It should 2307 * be {@code false} if the comment is not an error, or if it 2308 * is an error but should not be added to the list of error 2309 * messages (e.g., because a message will be added through 2310 * some other means). 2311 */ 2312 private void appendComment(@NotNull final StringBuilder buffer, 2313 @NotNull final String comment, 2314 final boolean isError) 2315 { 2316 buffer.append(comment); 2317 if (isError && (errorMessages != null)) 2318 { 2319 errorMessages.add(comment); 2320 } 2321 } 2322 2323 2324 2325 /** 2326 * Writes the provided comment to the LDIF writer. 2327 * 2328 * @param ldifWriter The writer to which the comment should be written. It 2329 * must not be {@code null}. 2330 * @param comment The comment to be written. It may be {@code null} or 2331 * empty if no comment should actually be written. 2332 * @param isError Indicates whether the comment represents an error that 2333 * should be added to the error list if it exists. It 2334 * should be {@code false} if the comment is not an error, 2335 * or if it is an error but should not be added to the 2336 * list of error messages (e.g., because a message will be 2337 * added through some other means). 2338 * 2339 * @throws IOException If an error occurs while attempting to write to the 2340 * LDIF writer. 2341 */ 2342 private void writeLDIFComment(@NotNull final LDIFWriter ldifWriter, 2343 @Nullable final CharSequence comment, 2344 final boolean isError) 2345 throws IOException 2346 { 2347 if (! (suppressComments.isPresent() || (comment == null) || 2348 (comment.length() == 0))) 2349 { 2350 ldifWriter.writeComment(comment.toString(), false, true); 2351 } 2352 2353 if (isError && (errorMessages != null) && (comment != null)) 2354 { 2355 errorMessages.add(comment.toString()); 2356 } 2357 } 2358 2359 2360 2361 /** 2362 * Appends a comment to the provided buffer for the given LDIF record. 2363 * 2364 * @param buffer The buffer to which the comment should be appended. It 2365 * must not be {@code null}. 2366 * @param message The message to include before the LDIF record. It must 2367 * not be {@code null}. 2368 * @param record The LDIF record to include in the comment. 2369 * @param isError Indicates whether the comment represents an error that 2370 * should be added to the error list if it exists. It should 2371 * be {@code false} if the comment is not an error, or if it 2372 * is an error but should not be added to the list of error 2373 * messages (e.g., because a message will be added through 2374 * some other means). 2375 */ 2376 private void createChangeRecordComment(@NotNull final StringBuilder buffer, 2377 @NotNull final String message, 2378 @NotNull final LDIFRecord record, 2379 final boolean isError) 2380 { 2381 final int initialLength = buffer.length(); 2382 if (initialLength > 0) 2383 { 2384 buffer.append(StaticUtils.EOL); 2385 buffer.append(StaticUtils.EOL); 2386 } 2387 2388 buffer.append(message); 2389 buffer.append(StaticUtils.EOL); 2390 2391 final int wrapCol; 2392 if (wrapColumn.isPresent() && (wrapColumn.getValue() > 20) && 2393 (wrapColumn.getValue() <= 85)) 2394 { 2395 wrapCol = wrapColumn.getValue() - 10; 2396 } 2397 else 2398 { 2399 wrapCol = 75; 2400 } 2401 2402 for (final String line : record.toLDIF(wrapCol)) 2403 { 2404 buffer.append(" "); 2405 buffer.append(line); 2406 buffer.append(StaticUtils.EOL); 2407 } 2408 2409 if (isError && (errorMessages != null)) 2410 { 2411 if (initialLength == 0) 2412 { 2413 errorMessages.add(buffer.toString()); 2414 } 2415 else 2416 { 2417 errorMessages.add(buffer.toString().substring(initialLength)); 2418 } 2419 } 2420 } 2421 2422 2423 2424 /** 2425 * Writes a wrapped version of the provided message to standard error. If an 2426 * {@code errorList} is also available, then the message will also be added to 2427 * that list. 2428 * 2429 * @param message The message to be written. It must not be {@code null]. 2430 */ 2431 private void wrapErr(@NotNull final String message) 2432 { 2433 wrapErr(0, WRAP_COLUMN, message); 2434 if (errorMessages != null) 2435 { 2436 errorMessages.add(message); 2437 } 2438 } 2439 2440 2441 2442 /** 2443 * Writes the provided message and sets it as the completion message. 2444 * 2445 * @param isError Indicates whether the message should be written to 2446 * standard error rather than standard output. 2447 * @param message The message to be written. 2448 */ 2449 private void logCompletionMessage(final boolean isError, 2450 @NotNull final String message) 2451 { 2452 completionMessage.compareAndSet(null, message); 2453 2454 if (isError) 2455 { 2456 wrapErr(message); 2457 } 2458 else 2459 { 2460 wrapOut(0, WRAP_COLUMN, message); 2461 } 2462 } 2463 2464 2465 2466 /** 2467 * {@inheritDoc} 2468 */ 2469 @Override() 2470 @NotNull() 2471 public LinkedHashMap<String[],String> getExampleUsages() 2472 { 2473 final LinkedHashMap<String[],String> examples = new LinkedHashMap<>(); 2474 2475 examples.put( 2476 new String[] 2477 { 2478 "--sourceLDIF", "original.ldif", 2479 "--changesLDIF", "changes.ldif", 2480 "--targetLDIF", "updated.ldif" 2481 }, 2482 INFO_LDIFMODIFY_EXAMPLE.get("changes.ldif", "original.ldif", 2483 "updated.ldif")); 2484 2485 return examples; 2486 } 2487}