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