001/* 002 * Copyright 2020-2025 Ping Identity Corporation 003 * All Rights Reserved. 004 */ 005/* 006 * Copyright 2020-2025 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-2025 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 public void doExtendedArgumentValidation() 816 throws ArgumentException 817 { 818 // Make sure that the source LDIF, changesLDIF, and targetLDIF files are 819 // all different. 820 final File sourceLDIFFile = sourceLDIF.getValue(); 821 final File changesLDIFFile = changesLDIF.getValue(); 822 final File targetLDIFFile = targetLDIF.getValue(); 823 824 if (sourceLDIFFile != null) 825 { 826 if (sourceLDIFFile.equals(changesLDIFFile)) 827 { 828 throw new ArgumentException( 829 ERR_LDIFMODIFY_ARGS_CANNOT_REFER_TO_SAME_FILE.get( 830 sourceLDIF.getIdentifierString(), 831 changesLDIF.getIdentifierString())); 832 833 } 834 835 if (sourceLDIFFile.equals(targetLDIFFile)) 836 { 837 throw new ArgumentException( 838 ERR_LDIFMODIFY_ARGS_CANNOT_REFER_TO_SAME_FILE.get( 839 sourceLDIF.getIdentifierString(), 840 targetLDIF.getIdentifierString())); 841 842 } 843 } 844 845 if (changesLDIFFile != null) 846 { 847 if (changesLDIFFile.equals(targetLDIFFile)) 848 { 849 throw new ArgumentException( 850 ERR_LDIFMODIFY_ARGS_CANNOT_REFER_TO_SAME_FILE.get( 851 changesLDIF.getIdentifierString(), 852 targetLDIF.getIdentifierString())); 853 854 } 855 } 856 } 857 858 859 860 /** 861 * {@inheritDoc} 862 */ 863 @Override() 864 @NotNull() 865 public ResultCode doToolProcessing() 866 { 867 // Read all of the changes into memory. 868 final Map<DN,List<LDIFChangeRecord>> addAndSubsequentChangeRecords = 869 new TreeMap<>(); 870 final Map<DN,Boolean> deletedEntryDNs = new TreeMap<>(); 871 final Map<DN,List<LDIFModifyChangeRecord>> modifyChangeRecords = 872 new HashMap<>(); 873 final Map<DN,ObjectPair<DN,List<LDIFChangeRecord>>> 874 modifyDNAndSubsequentChangeRecords = new TreeMap<>(); 875 final AtomicReference<ResultCode> resultCode = new AtomicReference<>(); 876 try 877 { 878 readChangeRecords(addAndSubsequentChangeRecords, deletedEntryDNs, 879 modifyChangeRecords, modifyDNAndSubsequentChangeRecords, resultCode); 880 } 881 catch (final LDAPException e) 882 { 883 Debug.debugException(e); 884 logCompletionMessage(true, e.getMessage()); 885 resultCode.compareAndSet(null, e.getResultCode()); 886 return resultCode.get(); 887 } 888 889 890 boolean changesIgnored = false; 891 LDIFReader ldifReader = null; 892 LDIFWriter ldifWriter = null; 893 final AtomicLong entriesRead = new AtomicLong(0L); 894 final AtomicLong entriesUpdated = new AtomicLong(0L); 895 try 896 { 897 // Open the source LDIF file for reading. 898 try 899 { 900 ldifReader = getLDIFReader(sourceReader, sourceLDIF.getValue(), 901 sourceEncryptionPassphraseFile.getValue()); 902 } 903 catch (final LDAPException e) 904 { 905 Debug.debugException(e); 906 logCompletionMessage(true, e.getMessage()); 907 return e.getResultCode(); 908 } 909 910 911 // Open the target LDIF file for writing. 912 try 913 { 914 ldifWriter = getLDIFWriter(targetWriter); 915 } 916 catch (final LDAPException e) 917 { 918 Debug.debugException(e); 919 logCompletionMessage(true, e.getMessage()); 920 return e.getResultCode(); 921 } 922 923 924 // Iterate through the source LDIF file and apply changes as appropriate. 925 final StringBuilder comment = new StringBuilder(); 926 while (true) 927 { 928 final LDIFRecord sourceRecord; 929 try 930 { 931 sourceRecord = ldifReader.readLDIFRecord(); 932 } 933 catch (final LDIFException e) 934 { 935 Debug.debugException(e); 936 937 if (e.mayContinueReading()) 938 { 939 resultCode.compareAndSet(null, ResultCode.DECODING_ERROR); 940 wrapErr(ERR_LDIFMODIFY_RECOVERABLE_DECODE_ERROR.get( 941 sourceLDIF.getValue(), StaticUtils.getExceptionMessage(e))); 942 continue; 943 } 944 else 945 { 946 logCompletionMessage(true, 947 ERR_LDIFMODIFY_UNRECOVERABLE_DECODE_ERROR.get( 948 sourceLDIF.getValue(), 949 StaticUtils.getExceptionMessage(e))); 950 return ResultCode.DECODING_ERROR; 951 } 952 } 953 catch (final IOException e) 954 { 955 Debug.debugException(e); 956 logCompletionMessage(true, 957 ERR_LDIFMODIFY_READ_ERROR.get(sourceLDIF.getValue(), 958 StaticUtils.getExceptionMessage(e))); 959 return ResultCode.LOCAL_ERROR; 960 } 961 962 963 // If the record we read was null, then we've hit the end of the source 964 // content. 965 if (sourceRecord == null) 966 { 967 break; 968 } 969 970 971 // If the record we read was an entry, then apply changes to it. If it 972 // was not, then that's an error. 973 comment.setLength(0); 974 975 final LDIFRecord targetRecord; 976 if (sourceRecord instanceof Entry) 977 { 978 entriesRead.incrementAndGet(); 979 targetRecord = updateEntry((Entry) sourceRecord, 980 addAndSubsequentChangeRecords, deletedEntryDNs, 981 modifyChangeRecords, modifyDNAndSubsequentChangeRecords, comment, 982 resultCode, entriesUpdated); 983 } 984 else 985 { 986 targetRecord = sourceRecord; 987 // NOTE: We're using false for the isError flag in this case because 988 // a better error will be recorded by the createChangeRecordComment 989 // call below. 990 appendComment(comment, 991 ERR_LDIFMODIFY_COMMENT_SOURCE_RECORD_NOT_ENTRY.get(), false); 992 993 final StringBuilder msgBuffer = new StringBuilder(); 994 createChangeRecordComment(msgBuffer, 995 ERR_LDIFMODIFY_OUTPUT_SOURCE_RECORD_NOT_ENTRY.get( 996 sourceLDIF.getValue().getAbsolutePath()), 997 sourceRecord, true); 998 wrapErr(msgBuffer.toString()); 999 resultCode.compareAndSet(null, ResultCode.DECODING_ERROR); 1000 } 1001 1002 1003 // Write the potentially updated entry to the target LDIF file. If the 1004 // target record is null, then that means the entry has been deleted, 1005 // but we still may want to write a comment about the deleted entry to 1006 // the target file. 1007 try 1008 { 1009 if (targetRecord == null) 1010 { 1011 if ((comment.length() > 0) && (! suppressComments.isPresent())) 1012 { 1013 writeLDIFComment(ldifWriter, comment, false); 1014 } 1015 } 1016 else 1017 { 1018 writeLDIFRecord(ldifWriter, targetRecord, comment); 1019 } 1020 } 1021 catch (final IOException e) 1022 { 1023 Debug.debugException(e); 1024 logCompletionMessage(true, 1025 ERR_LDIFMODIFY_WRITE_ERROR.get(targetLDIF.getValue(), 1026 StaticUtils.getExceptionMessage(e))); 1027 return ResultCode.LOCAL_ERROR; 1028 } 1029 } 1030 1031 1032 try 1033 { 1034 // If there are any remaining add records, then process them. 1035 final AtomicBoolean isUpdated = new AtomicBoolean(); 1036 for (final List<LDIFChangeRecord> records : 1037 addAndSubsequentChangeRecords.values()) 1038 { 1039 final Iterator<LDIFChangeRecord> iterator = records.iterator(); 1040 final LDIFAddChangeRecord addChangeRecord = 1041 (LDIFAddChangeRecord) iterator.next(); 1042 Entry entry = addChangeRecord.getEntryToAdd(); 1043 comment.setLength(0); 1044 if (iterator.hasNext()) 1045 { 1046 createChangeRecordComment(comment, 1047 INFO_LDIFMODIFY_ADDING_ENTRY_WITH_MODS.get(), addChangeRecord, 1048 false); 1049 while (iterator.hasNext()) 1050 { 1051 entry = applyModification(entry, 1052 (LDIFModifyChangeRecord) iterator.next(), isUpdated, 1053 resultCode, comment); 1054 } 1055 } 1056 else 1057 { 1058 appendComment(comment, 1059 INFO_LDIFMODIFY_ADDING_ENTRY_NO_MODS.get(), false); 1060 } 1061 1062 writeLDIFRecord(ldifWriter, entry, comment); 1063 entriesUpdated.incrementAndGet(); 1064 } 1065 1066 1067 // If there are any remaining DNs to delete, then those entries must not 1068 // have been in the source LDIF. 1069 for (final Map.Entry<DN,Boolean> e : deletedEntryDNs.entrySet()) 1070 { 1071 if (e.getValue() == Boolean.FALSE) 1072 { 1073 if (ignoreDeletesOfNonexistentEntries.isPresent()) 1074 { 1075 changesIgnored = true; 1076 } 1077 else 1078 { 1079 resultCode.compareAndSet(null, ResultCode.NO_SUCH_OBJECT); 1080 writeLDIFComment(ldifWriter, 1081 ERR_LDIFMODIFY_NO_SUCH_ENTRY_TO_DELETE.get( 1082 e.getKey().toString()), 1083 true); 1084 } 1085 } 1086 } 1087 1088 1089 // If there are any remaining modify change records, then those entries 1090 // must not have been in the source LDIF. 1091 for (final List<LDIFModifyChangeRecord> l : 1092 modifyChangeRecords.values()) 1093 { 1094 for (final LDIFChangeRecord r : l) 1095 { 1096 if (ignoreModifiesOfNonexistentEntries.isPresent()) 1097 { 1098 changesIgnored = true; 1099 } 1100 else 1101 { 1102 resultCode.compareAndSet(null, ResultCode.NO_SUCH_OBJECT); 1103 comment.setLength(0); 1104 createChangeRecordComment(comment, 1105 ERR_LDIFMODIFY_NO_SUCH_ENTRY_TO_MODIFY.get(), r, true); 1106 writeLDIFComment(ldifWriter, comment, false); 1107 } 1108 } 1109 } 1110 1111 1112 // If there are any remaining modify DN change records, then those 1113 // entries must not have been in the source LDIF. 1114 for (final ObjectPair<DN,List<LDIFChangeRecord>> l : 1115 modifyDNAndSubsequentChangeRecords.values()) 1116 { 1117 for (final LDIFChangeRecord r : l.getSecond()) 1118 { 1119 resultCode.compareAndSet(null, ResultCode.NO_SUCH_OBJECT); 1120 comment.setLength(0); 1121 if (r instanceof LDIFModifyDNChangeRecord) 1122 { 1123 createChangeRecordComment(comment, 1124 ERR_LDIFMODIFY_NO_SUCH_ENTRY_TO_RENAME.get(), r, true); 1125 } 1126 else 1127 { 1128 createChangeRecordComment(comment, 1129 ERR_LDIFMODIFY_NO_SUCH_ENTRY_TO_MODIFY.get(), r, true); 1130 } 1131 writeLDIFComment(ldifWriter, comment, false); 1132 } 1133 } 1134 } 1135 catch (final IOException e) 1136 { 1137 Debug.debugException(e); 1138 logCompletionMessage(true, 1139 ERR_LDIFMODIFY_WRITE_ERROR.get( 1140 targetLDIF.getValue().getAbsolutePath(), 1141 StaticUtils.getExceptionMessage(e))); 1142 return ResultCode.LOCAL_ERROR; 1143 } 1144 } 1145 finally 1146 { 1147 if (ldifReader != null) 1148 { 1149 try 1150 { 1151 ldifReader.close(); 1152 } 1153 catch (final Exception e) 1154 { 1155 Debug.debugException(e); 1156 resultCode.compareAndSet(null, ResultCode.LOCAL_ERROR); 1157 logCompletionMessage(true, 1158 ERR_LDIFMODIFY_ERROR_CLOSING_READER.get( 1159 sourceLDIF.getValue().getAbsolutePath(), 1160 StaticUtils.getExceptionMessage(e))); 1161 } 1162 } 1163 1164 if (ldifWriter != null) 1165 { 1166 try 1167 { 1168 ldifWriter.close(); 1169 } 1170 catch (final Exception e) 1171 { 1172 Debug.debugException(e); 1173 resultCode.compareAndSet(null, ResultCode.LOCAL_ERROR); 1174 logCompletionMessage(true, 1175 ERR_LDIFMODIFY_ERROR_CLOSING_WRITER.get( 1176 sourceLDIF.getValue().getAbsolutePath(), 1177 StaticUtils.getExceptionMessage(e))); 1178 } 1179 } 1180 } 1181 1182 1183 // If no entries were read and no updates were applied, then we'll consider 1184 // that an error, regardless of whether a read error was encountered. 1185 if ((entriesRead.get() == 0L) && (entriesUpdated.get() == 0L)) 1186 { 1187 if (resultCode.get() == null) 1188 { 1189 logCompletionMessage(true, 1190 ERR_LDIFMODIFY_NO_SOURCE_ENTRIES.get( 1191 sourceLDIF.getValue().getAbsolutePath())); 1192 return ResultCode.PARAM_ERROR; 1193 } 1194 else 1195 { 1196 logCompletionMessage(true, 1197 ERR_LDIFMODIFY_COULD_NOT_READ_SOURCE_ENTRIES.get( 1198 sourceLDIF.getValue().getAbsolutePath())); 1199 return resultCode.get(); 1200 } 1201 } 1202 1203 1204 // If no entries were updated, then we'll also consider that an error. 1205 if ((entriesUpdated.get() == 0L) && (! changesIgnored)) 1206 { 1207 logCompletionMessage(true, 1208 ERR_LDIFMODIFY_NO_CHANGES_APPLIED_WITH_ERRORS.get( 1209 changesLDIF.getValue().getAbsolutePath(), 1210 sourceLDIF.getValue().getAbsolutePath())); 1211 resultCode.compareAndSet(null, ResultCode.PARAM_ERROR); 1212 return resultCode.get(); 1213 } 1214 1215 1216 // Create the final completion message that will be used. 1217 final long entriesNotUpdated = 1218 Math.max((entriesRead.get() - entriesUpdated.get()), 0); 1219 if (resultCode.get() == null) 1220 { 1221 logCompletionMessage(false, 1222 INFO_LDIFMODIFY_COMPLETED_SUCCESSFULLY.get(entriesRead.get(), 1223 entriesUpdated.get(), entriesNotUpdated)); 1224 return ResultCode.SUCCESS; 1225 } 1226 else 1227 { 1228 logCompletionMessage(true, 1229 ERR_LDIFMODIFY_COMPLETED_WITH_ERRORS.get(entriesRead.get(), 1230 entriesUpdated.get(), entriesNotUpdated)); 1231 return resultCode.get(); 1232 } 1233 } 1234 1235 1236 1237 /** 1238 * Reads all of the LDIF change records from the changes file into a list. 1239 * 1240 * @param addAndSubsequentChangeRecords 1241 * A map that will be updated with add change records for a given 1242 * entry, along with any subsequent change records that apply to 1243 * the entry after it has been added. It must not be 1244 * {@code null}, must be empty, and must be updatable. 1245 * @param deletedEntryDNs 1246 * A map that will be updated with the DNs of any entries that 1247 * are targeted by delete modifications and that have not been 1248 * previously added or renamed. It must not be {@code null}, 1249 * must be empty, and must be updatable. 1250 * @param modifyChangeRecords 1251 * A map that will be updated with any modify change records 1252 * that target an entry that has not been targeted by any other 1253 * type of change. It must not be {@code null}, must be empty, 1254 * and must be updatable. 1255 * @param modifyDNAndSubsequentChangeRecords 1256 * A map that will be updated with any change records for modify 1257 * DN operations that target a given entry, and any subsequent 1258 * operations that target the entry with its new DN. It must not 1259 * be {@code null}, must be empty, and must be updatable. 1260 * @param resultCode 1261 * A reference to the final result code that should be used for 1262 * the tool. This may be updated if an error occurred during 1263 * processing and no value is already set. It must not be 1264 * {@code null}, but is allowed to have no value assigned. 1265 * 1266 * @throws LDAPException If an unrecoverable error occurs during processing. 1267 */ 1268 private void readChangeRecords( 1269 @NotNull final Map<DN,List<LDIFChangeRecord>> 1270 addAndSubsequentChangeRecords, 1271 @NotNull final Map<DN,Boolean> deletedEntryDNs, 1272 @NotNull final Map<DN,List<LDIFModifyChangeRecord>> modifyChangeRecords, 1273 @NotNull final Map<DN,ObjectPair<DN,List<LDIFChangeRecord>>> 1274 modifyDNAndSubsequentChangeRecords, 1275 @NotNull final AtomicReference<ResultCode> resultCode) 1276 throws LDAPException 1277 { 1278 LDIFException firstRecoverableException = null; 1279 try (LDIFReader ldifReader = getLDIFReader(changesReader, 1280 changesLDIF.getValue(), changesEncryptionPassphraseFile.getValue())) 1281 { 1282changeRecordLoop: 1283 while (true) 1284 { 1285 // Read the next record from the changes file. 1286 final LDIFRecord ldifRecord; 1287 try 1288 { 1289 ldifRecord = ldifReader.readLDIFRecord(); 1290 } 1291 catch (final LDIFException e) 1292 { 1293 Debug.debugException(e); 1294 1295 if (e.mayContinueReading()) 1296 { 1297 if (firstRecoverableException == null) 1298 { 1299 firstRecoverableException = e; 1300 } 1301 1302 err(); 1303 wrapErr(ERR_LDIFMODIFY_CANNOT_READ_RECORD_CAN_CONTINUE.get( 1304 changesLDIF.getValue().getAbsolutePath(), 1305 StaticUtils.getExceptionMessage(e))); 1306 resultCode.compareAndSet(null, ResultCode.DECODING_ERROR); 1307 continue changeRecordLoop; 1308 } 1309 else 1310 { 1311 throw new LDAPException(ResultCode.DECODING_ERROR, 1312 ERR_LDIFMODIFY_CANNOT_READ_RECORD_CANNOT_CONTINUE.get( 1313 changesLDIF.getValue().getAbsolutePath(), 1314 StaticUtils.getExceptionMessage(e)), 1315 e); 1316 } 1317 } 1318 1319 if (ldifRecord == null) 1320 { 1321 break; 1322 } 1323 1324 1325 // Make sure that we can parse the DN for the change record. If not, 1326 // then that's an error. 1327 final DN parsedDN; 1328 try 1329 { 1330 parsedDN = ldifRecord.getParsedDN(); 1331 } 1332 catch (final LDAPException e) 1333 { 1334 Debug.debugException(e); 1335 1336 err(); 1337 wrapErr(ERR_LDIFMODIFY_CANNOT_PARSE_CHANGE_RECORD_DN.get( 1338 String.valueOf(ldifRecord), 1339 changesLDIF.getValue().getAbsolutePath(), e.getMessage())); 1340 resultCode.compareAndSet(null, e.getResultCode()); 1341 continue changeRecordLoop; 1342 } 1343 1344 1345 // Get the LDIF record as a change record. If the record is an entry 1346 // rather than a change record, then we'll treat it as an add change 1347 // record. 1348 final LDIFChangeRecord changeRecord; 1349 if (ldifRecord instanceof Entry) 1350 { 1351 changeRecord = new LDIFAddChangeRecord((Entry) ldifRecord); 1352 } 1353 else 1354 { 1355 changeRecord = (LDIFChangeRecord) ldifRecord; 1356 } 1357 1358 1359 // If the change record is for a modify DN, then make sure that we can 1360 // parse the new DN. 1361 final DN parsedNewDN; 1362 if (changeRecord.getChangeType() == ChangeType.MODIFY_DN) 1363 { 1364 try 1365 { 1366 parsedNewDN = ((LDIFModifyDNChangeRecord) changeRecord).getNewDN(); 1367 } 1368 catch (final LDAPException e) 1369 { 1370 Debug.debugException(e); 1371 1372 err(); 1373 wrapErr(ERR_LDIFMODIFY_CANNOT_PARSE_NEW_DN.get( 1374 String.valueOf(changeRecord), 1375 changesLDIF.getValue().getAbsolutePath(), e.getMessage())); 1376 resultCode.compareAndSet(null, e.getResultCode()); 1377 continue changeRecordLoop; 1378 } 1379 } 1380 else 1381 { 1382 parsedNewDN = parsedDN; 1383 } 1384 1385 1386 // Look at the change type and determine how to handle the operation. 1387 switch (changeRecord.getChangeType()) 1388 { 1389 case ADD: 1390 // Make sure that we haven't already seen an add for an entry with 1391 // the same DN (unless that add was subsequently deleted). 1392 if (addAndSubsequentChangeRecords.containsKey(parsedDN)) 1393 { 1394 err(); 1395 wrapErr(ERR_LDIFMODIFY_MULTIPLE_ADDS_FOR_DN.get( 1396 changesLDIF.getValue().getAbsolutePath(), 1397 parsedDN.toString())); 1398 resultCode.compareAndSet(null, ResultCode.ENTRY_ALREADY_EXISTS); 1399 continue changeRecordLoop; 1400 } 1401 1402 // Make sure that there are no modifies targeting an entry with the 1403 // same DN. 1404 if (modifyChangeRecords.containsKey(parsedDN)) 1405 { 1406 err(); 1407 wrapErr(ERR_LDIFMODIFY_ADD_TARGETS_MODIFIED_ENTRY.get( 1408 changesLDIF.getValue().getAbsolutePath(), 1409 parsedDN.toString())); 1410 resultCode.compareAndSet(null, ResultCode.ENTRY_ALREADY_EXISTS); 1411 continue changeRecordLoop; 1412 } 1413 1414 // Make sure that there aren't any modify DN operations that will 1415 // create an entry with the same or a subordinate DN. 1416 for (final Map.Entry<DN,ObjectPair<DN,List<LDIFChangeRecord>>> e : 1417 modifyDNAndSubsequentChangeRecords.entrySet()) 1418 { 1419 final DN newDN = e.getValue().getFirst(); 1420 if (parsedDN.isAncestorOf(newDN, true)) 1421 { 1422 err(); 1423 wrapErr(ERR_LDIFMODIFY_ADD_CONFLICTS_WITH_MOD_DN.get( 1424 changesLDIF.getValue().getAbsolutePath(), 1425 parsedDN.toString(), e.getKey().toString(), 1426 newDN.toString())); 1427 resultCode.compareAndSet(null, ResultCode.ENTRY_ALREADY_EXISTS); 1428 continue changeRecordLoop; 1429 } 1430 } 1431 1432 final List<LDIFChangeRecord> addList = new ArrayList<>(); 1433 addList.add(changeRecord); 1434 addAndSubsequentChangeRecords.put(parsedDN, addList); 1435 break; 1436 1437 1438 case DELETE: 1439 // If the set of changes already included an add for this entry, 1440 // then remove that add and any subsequent changes for it. This 1441 // isn't an error, so we don't need to set a result code. 1442 if (addAndSubsequentChangeRecords.containsKey(parsedDN)) 1443 { 1444 addAndSubsequentChangeRecords.remove(parsedDN); 1445 err(); 1446 wrapErr(WARN_LDIFMODIFY_DELETE_OF_PREVIOUS_ADD.get( 1447 changesLDIF.getValue().getAbsolutePath(), 1448 parsedDN.toString())); 1449 continue changeRecordLoop; 1450 } 1451 1452 // If the set of changes already included a modify DN that targeted 1453 // the entry, then reject the change. 1454 if (modifyDNAndSubsequentChangeRecords.containsKey(parsedDN)) 1455 { 1456 final DN newDN = 1457 modifyDNAndSubsequentChangeRecords.get(parsedDN).getFirst(); 1458 1459 err(); 1460 wrapErr(ERR_LDIFMODIFY_DELETE_OF_PREVIOUS_RENAME.get( 1461 changesLDIF.getValue().getAbsolutePath(), 1462 parsedDN.toString(), newDN.toString())); 1463 resultCode.compareAndSet(null, ResultCode.NO_SUCH_OBJECT); 1464 continue changeRecordLoop; 1465 } 1466 1467 // If the set of changes already included a modify DN whose new DN 1468 // equals or is subordinate to the DN for the delete change 1469 // record, then remove that modify DN operation and any subsequent 1470 // changes for it, and instead add a delete for the original DN. 1471 // This isn't an error, so we don't need to set a result code. 1472 final Iterator<Map.Entry<DN,ObjectPair<DN,List<LDIFChangeRecord>>>> 1473 deleteModDNIterator = 1474 modifyDNAndSubsequentChangeRecords.entrySet().iterator(); 1475 while (deleteModDNIterator.hasNext()) 1476 { 1477 final Map.Entry<DN,ObjectPair<DN,List<LDIFChangeRecord>>> e = 1478 deleteModDNIterator.next(); 1479 final DN newDN = e.getValue().getFirst(); 1480 if (parsedDN.isAncestorOf(newDN, true)) 1481 { 1482 final DN originalDN = e.getKey(); 1483 deleteModDNIterator.remove(); 1484 deletedEntryDNs.put(originalDN, Boolean.FALSE); 1485 1486 err(); 1487 wrapErr(WARN_LDIFMODIFY_DELETE_OF_PREVIOUSLY_RENAMED.get( 1488 changesLDIF.getValue().getAbsolutePath(), 1489 parsedDN.toString(), originalDN.toString(), 1490 newDN.toString())); 1491 continue changeRecordLoop; 1492 } 1493 } 1494 1495 // If the set of changes already included a delete for the same 1496 // DN, then reject the new change. 1497 if (deletedEntryDNs.containsKey(parsedDN)) 1498 { 1499 if (! ignoreDuplicateDeletes.isPresent()) 1500 { 1501 err(); 1502 wrapErr(ERR_LDIFMODIFY_MULTIPLE_DELETES_FOR_DN.get( 1503 changesLDIF.getValue().getAbsolutePath(), 1504 parsedDN.toString())); 1505 resultCode.compareAndSet(null, ResultCode.NO_SUCH_OBJECT); 1506 } 1507 continue changeRecordLoop; 1508 } 1509 1510 // If the set of changes included any modifications for the same DN, 1511 // then remove those modifications. This isn't an error, so we 1512 // don't need to set a result code. 1513 if (modifyChangeRecords.containsKey(parsedDN)) 1514 { 1515 modifyChangeRecords.remove(parsedDN); 1516 err(); 1517 wrapErr(WARN_LDIFMODIFY_DELETE_OF_PREVIOUSLY_MODIFIED.get( 1518 changesLDIF.getValue().getAbsolutePath(), 1519 parsedDN.toString())); 1520 } 1521 1522 deletedEntryDNs.put(parsedDN, Boolean.FALSE); 1523 break; 1524 1525 1526 case MODIFY: 1527 // If the set of changes already included an add for an entry with 1528 // the same DN, then add the modify change record to the set of 1529 // changes following that add. 1530 if (addAndSubsequentChangeRecords.containsKey(parsedDN)) 1531 { 1532 addAndSubsequentChangeRecords.get(parsedDN).add(changeRecord); 1533 continue changeRecordLoop; 1534 } 1535 1536 // If the set of changes already included a modify DN for an entry 1537 // with the same DN, then reject the new change. 1538 if (modifyDNAndSubsequentChangeRecords.containsKey(parsedDN)) 1539 { 1540 final DN newDN = 1541 modifyDNAndSubsequentChangeRecords.get(parsedDN).getFirst(); 1542 1543 err(); 1544 wrapErr(ERR_LDIFMODIFY_MODIFY_OF_RENAMED_ENTRY.get( 1545 changesLDIF.getValue().getAbsolutePath(), 1546 parsedDN.toString(), newDN.toString())); 1547 resultCode.compareAndSet(null, ResultCode.NO_SUCH_OBJECT); 1548 continue changeRecordLoop; 1549 } 1550 1551 // If the set of changes already included a modify DN that would 1552 // result in an entry with the same DN as the modify, then add 1553 // the modify change record to the modify DN record's change list. 1554 for (final Map.Entry<DN,ObjectPair<DN,List<LDIFChangeRecord>>> e : 1555 modifyDNAndSubsequentChangeRecords.entrySet()) 1556 { 1557 if (parsedDN.equals(e.getValue().getFirst())) 1558 { 1559 e.getValue().getSecond().add(changeRecord); 1560 continue changeRecordLoop; 1561 } 1562 } 1563 1564 // If the set of changes already included a delete for an entry with 1565 // the same DN, then reject the new change. 1566 if (deletedEntryDNs.containsKey(parsedDN)) 1567 { 1568 err(); 1569 wrapErr(ERR_LDIFMODIFY_MODIFY_OF_DELETED_ENTRY.get( 1570 changesLDIF.getValue().getAbsolutePath(), 1571 parsedDN.toString())); 1572 resultCode.compareAndSet(null, ResultCode.NO_SUCH_OBJECT); 1573 continue changeRecordLoop; 1574 } 1575 1576 // If the set of changes already included a modify for an entry with 1577 // the same DN, then add the new change to that list. 1578 if (modifyChangeRecords.containsKey(parsedDN)) 1579 { 1580 modifyChangeRecords.get(parsedDN).add( 1581 (LDIFModifyChangeRecord) changeRecord); 1582 continue changeRecordLoop; 1583 } 1584 1585 // Start a new change record list for the modify operation. 1586 final List<LDIFModifyChangeRecord> modList = new ArrayList<>(); 1587 modList.add((LDIFModifyChangeRecord) changeRecord); 1588 modifyChangeRecords.put(parsedDN, modList); 1589 break; 1590 1591 1592 case MODIFY_DN: 1593 // If the set of changes already included an add for an entry with 1594 // the same DN, then reject the modify DN. 1595 if (addAndSubsequentChangeRecords.containsKey(parsedDN)) 1596 { 1597 err(); 1598 wrapErr(ERR_LDIFMODIFY_MOD_DN_OF_ADDED_ENTRY.get( 1599 changesLDIF.getValue().getAbsolutePath(), 1600 parsedDN.toString())); 1601 resultCode.compareAndSet(null, ResultCode.UNWILLING_TO_PERFORM); 1602 continue changeRecordLoop; 1603 } 1604 1605 // If the set of changes already included an add for an entry with 1606 // an entry at or below the new DN, then reject the modify DN. 1607 for (final DN addedDN : addAndSubsequentChangeRecords.keySet()) 1608 { 1609 if (addedDN.isDescendantOf(parsedNewDN, true)) 1610 { 1611 err(); 1612 wrapErr(ERR_LDIFMODIFY_MOD_DN_NEW_DN_CONFLICTS_WITH_ADD.get( 1613 changesLDIF.getValue().getAbsolutePath(), 1614 parsedDN.toString(), parsedNewDN.toString(), 1615 addedDN.toString())); 1616 resultCode.compareAndSet(null, ResultCode.ENTRY_ALREADY_EXISTS); 1617 continue changeRecordLoop; 1618 } 1619 } 1620 1621 // If the set of changes already included a modify DN for an entry 1622 // with the same DN, then reject the modify DN. 1623 if (modifyDNAndSubsequentChangeRecords.containsKey(parsedDN)) 1624 { 1625 err(); 1626 wrapErr(ERR_LDIFMODIFY_MULTIPLE_MOD_DN_WITH_DN.get( 1627 changesLDIF.getValue().getAbsolutePath(), 1628 parsedDN.toString())); 1629 resultCode.compareAndSet(null, ResultCode.NO_SUCH_OBJECT); 1630 continue changeRecordLoop; 1631 } 1632 1633 // If the set of changes already included a modify DN for an entry 1634 // that set a new DN that matches the DN of the new record, then 1635 // reject the modify DN. 1636 for (final Map.Entry<DN,ObjectPair<DN,List<LDIFChangeRecord>>> e : 1637 modifyDNAndSubsequentChangeRecords.entrySet()) 1638 { 1639 final DN newDN = e.getValue().getFirst(); 1640 if (newDN.isDescendantOf(parsedDN, true)) 1641 { 1642 err(); 1643 wrapErr( 1644 ERR_LDIFMODIFY_UNWILLING_TO_MODIFY_DN_MULTIPLE_TIMES.get( 1645 changesLDIF.getValue().getAbsolutePath(), 1646 parsedDN.toString(), parsedNewDN.toString(), 1647 e.getKey().toString())); 1648 resultCode.compareAndSet(null, ResultCode.UNWILLING_TO_PERFORM); 1649 continue changeRecordLoop; 1650 } 1651 } 1652 1653 // If the set of changes already included a modify DN that set a 1654 // new DN that is at or below the new DN, then reject the modify DN. 1655 for (final Map.Entry<DN,ObjectPair<DN,List<LDIFChangeRecord>>> e : 1656 modifyDNAndSubsequentChangeRecords.entrySet()) 1657 { 1658 final DN newDN = e.getValue().getFirst(); 1659 if (newDN.isDescendantOf(parsedNewDN, true)) 1660 { 1661 err(); 1662 wrapErr(ERR_LDIFMODIFY_MOD_DN_CONFLICTS_WITH_MOD_DN.get( 1663 changesLDIF.getValue().getAbsolutePath(), 1664 parsedDN.toString(), parsedNewDN.toString(), 1665 e.getKey().toString(), newDN.toString())); 1666 resultCode.compareAndSet(null, ResultCode.ENTRY_ALREADY_EXISTS); 1667 continue changeRecordLoop; 1668 } 1669 } 1670 1671 // If the set of changes already included a delete for an entry with 1672 //t he same DN, then reject the modify DN. 1673 if (deletedEntryDNs.containsKey(parsedDN)) 1674 { 1675 err(); 1676 wrapErr(ERR_LDIFMODIFY_MOD_DN_OF_DELETED_ENTRY.get( 1677 changesLDIF.getValue().getAbsolutePath(), 1678 parsedDN.toString())); 1679 resultCode.compareAndSet(null, ResultCode.NO_SUCH_OBJECT); 1680 continue changeRecordLoop; 1681 } 1682 1683 // If the set of changes already included a modify for an entry that 1684 // is at or below the new DN, then reject the modify DN. 1685 for (final DN dn : modifyChangeRecords.keySet()) 1686 { 1687 if (dn.isDescendantOf(parsedNewDN, true)) 1688 { 1689 err(); 1690 wrapErr(ERR_LDIFMODIFY_MOD_DN_NEW_DN_CONFLICTS_WITH_MOD.get( 1691 changesLDIF.getValue().getAbsolutePath(), 1692 parsedDN.toString(), parsedNewDN.toString(), 1693 dn.toString())); 1694 resultCode.compareAndSet(null, ResultCode.ENTRY_ALREADY_EXISTS); 1695 continue changeRecordLoop; 1696 } 1697 } 1698 1699 final List<LDIFChangeRecord> modDNList = new ArrayList<>(); 1700 modDNList.add(changeRecord); 1701 modifyDNAndSubsequentChangeRecords.put(parsedDN, 1702 new ObjectPair<DN,List<LDIFChangeRecord>>(parsedNewDN, 1703 modDNList)); 1704 break; 1705 } 1706 } 1707 } 1708 catch (final LDAPException e) 1709 { 1710 Debug.debugException(e); 1711 throw new LDAPException(e.getResultCode(), 1712 ERR_LDIFMODIFY_ERROR_OPENING_CHANGES_FILE.get( 1713 changesLDIF.getValue().getAbsolutePath(), e.getMessage()), 1714 e); 1715 } 1716 catch (final IOException e) 1717 { 1718 Debug.debugException(e); 1719 throw new LDAPException(ResultCode.LOCAL_ERROR, 1720 ERR_LDIFMODIFY_ERROR_READING_CHANGES_FILE.get( 1721 changesLDIF.getValue().getAbsolutePath(), 1722 StaticUtils.getExceptionMessage(e)), 1723 e); 1724 } 1725 1726 if (addAndSubsequentChangeRecords.isEmpty() && deletedEntryDNs.isEmpty() && 1727 modifyChangeRecords.isEmpty() && 1728 modifyDNAndSubsequentChangeRecords.isEmpty()) 1729 { 1730 if (firstRecoverableException == null) 1731 { 1732 throw new LDAPException(ResultCode.PARAM_ERROR, 1733 ERR_LDIFMODIFY_NO_CHANGES.get( 1734 changesLDIF.getValue().getAbsolutePath())); 1735 } 1736 else 1737 { 1738 throw new LDAPException(ResultCode.PARAM_ERROR, 1739 ERR_LDIFMODIFY_NO_CHANGES_WITH_ERROR.get( 1740 changesLDIF.getValue().getAbsolutePath()), 1741 firstRecoverableException); 1742 } 1743 } 1744 } 1745 1746 1747 1748 /** 1749 * Retrieves an LDIF reader that may be used to read LDIF records (either 1750 * entries or change records) from the specified LDIF file. 1751 * 1752 * @param existingReader An LDIF reader that was already provided to the 1753 * tool for this purpose. It may be {@code null} if 1754 * the LDIF reader should be created with the given 1755 * LDIF file and passphrase file. 1756 * @param ldifFile The LDIF file for which to create the reader. It 1757 * may be {@code null} only if {@code existingReader} 1758 * is non-{@code null}. 1759 * @param passphraseFile The file containing the encryption passphrase 1760 * needed to decrypt the contents of the provided LDIF 1761 * file. It may be {@code null} if the LDIF file is 1762 * not encrypted or if the user should be 1763 * interactively prompted for the passphrase. 1764 * 1765 * @return The LDIF reader that was created. 1766 * 1767 * @throws LDAPException If a problem occurs while creating the LDIF reader. 1768 */ 1769 @NotNull() 1770 private LDIFReader getLDIFReader(@Nullable final LDIFReader existingReader, 1771 @Nullable final File ldifFile, 1772 @Nullable final File passphraseFile) 1773 throws LDAPException 1774 { 1775 if (existingReader != null) 1776 { 1777 return existingReader; 1778 } 1779 1780 if (passphraseFile != null) 1781 { 1782 readPassphraseFile(passphraseFile); 1783 } 1784 1785 1786 boolean closeStream = true; 1787 InputStream inputStream = null; 1788 try 1789 { 1790 inputStream = new FileInputStream(ldifFile); 1791 1792 final ObjectPair<InputStream,char[]> p = 1793 ToolUtils.getPossiblyPassphraseEncryptedInputStream( 1794 inputStream, inputEncryptionPassphrases, 1795 (passphraseFile != null), 1796 INFO_LDIFMODIFY_ENTER_INPUT_ENCRYPTION_PW.get( 1797 ldifFile.getName()), 1798 ERR_LDIFMODIFY_WRONG_ENCRYPTION_PW.get(), getOut(), getErr()); 1799 inputStream = p.getFirst(); 1800 addPassphrase(p.getSecond()); 1801 1802 inputStream = ToolUtils.getPossiblyGZIPCompressedInputStream(inputStream); 1803 1804 final LDIFReader ldifReader = new LDIFReader(inputStream); 1805 if (stripTrailingSpaces.isPresent()) 1806 { 1807 ldifReader.setTrailingSpaceBehavior(TrailingSpaceBehavior.STRIP); 1808 } 1809 else 1810 { 1811 ldifReader.setTrailingSpaceBehavior(TrailingSpaceBehavior.REJECT); 1812 } 1813 1814 ldifReader.setSchema(Schema.getDefaultStandardSchema()); 1815 1816 closeStream = false; 1817 return ldifReader; 1818 } 1819 catch (final Exception e) 1820 { 1821 Debug.debugException(e); 1822 throw new LDAPException(ResultCode.LOCAL_ERROR, 1823 ERR_LDIFMODIFY_ERROR_OPENING_INPUT_FILE.get( 1824 ldifFile.getAbsolutePath(), 1825 StaticUtils.getExceptionMessage(e)), 1826 e); 1827 } 1828 finally 1829 { 1830 if ((inputStream != null) && closeStream) 1831 { 1832 try 1833 { 1834 inputStream.close(); 1835 } 1836 catch (final Exception e) 1837 { 1838 Debug.debugException(e); 1839 } 1840 } 1841 } 1842 } 1843 1844 1845 1846 /** 1847 * Reads the contents of the specified passphrase file and adds it to the list 1848 * of passphrases. 1849 * 1850 * @param f The passphrase file to read. 1851 * 1852 * @throws LDAPException If a problem is encountered while trying to read 1853 * the passphrase from the provided file. 1854 */ 1855 private void readPassphraseFile(@NotNull final File f) 1856 throws LDAPException 1857 { 1858 try 1859 { 1860 addPassphrase(getPasswordFileReader().readPassword(f)); 1861 } 1862 catch (final Exception e) 1863 { 1864 Debug.debugException(e); 1865 throw new LDAPException(ResultCode.LOCAL_ERROR, 1866 ERR_LDIFMODIFY_CANNOT_READ_PW_FILE.get(f.getAbsolutePath(), 1867 StaticUtils.getExceptionMessage(e)), 1868 e); 1869 } 1870 } 1871 1872 1873 1874 /** 1875 * Updates the list of encryption passphrases with the provided passphrase, if 1876 * it is not already present. 1877 * 1878 * @param passphrase The passphrase to be added. It may optionally be 1879 * {@code null} (in which case no action will be taken). 1880 */ 1881 private void addPassphrase(@Nullable final char[] passphrase) 1882 { 1883 if (passphrase == null) 1884 { 1885 return; 1886 } 1887 1888 for (final char[] existingPassphrase : inputEncryptionPassphrases) 1889 { 1890 if (Arrays.equals(existingPassphrase, passphrase)) 1891 { 1892 return; 1893 } 1894 } 1895 1896 inputEncryptionPassphrases.add(passphrase); 1897 } 1898 1899 1900 1901 /** 1902 * Creates the LDIF writer to use to write the output. 1903 * 1904 * @param existingWriter An LDIF writer that was already provided to the 1905 * tool for this purpose. It may be {@code null} if 1906 * the LDIF writer should be created using the 1907 * provided arguments. 1908 * 1909 * @return The LDIF writer that was created. 1910 * 1911 * @throws LDAPException If a problem occurs while creating the LDIF writer. 1912 */ 1913 @NotNull() 1914 private LDIFWriter getLDIFWriter(@Nullable final LDIFWriter existingWriter) 1915 throws LDAPException 1916 { 1917 if (existingWriter != null) 1918 { 1919 return existingWriter; 1920 } 1921 1922 final File outputFile = targetLDIF.getValue(); 1923 final File passphraseFile = targetEncryptionPassphraseFile.getValue(); 1924 1925 1926 OutputStream outputStream = null; 1927 boolean closeOutputStream = true; 1928 try 1929 { 1930 try 1931 { 1932 1933 outputStream = new FileOutputStream(targetLDIF.getValue()); 1934 } 1935 catch (final Exception e) 1936 { 1937 Debug.debugException(e); 1938 throw new LDAPException(ResultCode.LOCAL_ERROR, 1939 ERR_LDIFMODIFY_CANNOT_OPEN_OUTPUT_FILE.get( 1940 outputFile.getAbsolutePath(), 1941 StaticUtils.getExceptionMessage(e)), 1942 e); 1943 } 1944 1945 if (encryptTarget.isPresent()) 1946 { 1947 try 1948 { 1949 final char[] passphrase; 1950 if (passphraseFile != null) 1951 { 1952 passphrase = getPasswordFileReader().readPassword(passphraseFile); 1953 } 1954 else 1955 { 1956 passphrase = ToolUtils.promptForEncryptionPassphrase(false, true, 1957 INFO_LDIFMODIFY_ENTER_OUTPUT_ENCRYPTION_PW.get(), 1958 INFO_LDIFMODIFY_CONFIRM_OUTPUT_ENCRYPTION_PW.get(), getOut(), 1959 getErr()).toCharArray(); 1960 } 1961 1962 outputStream = new PassphraseEncryptedOutputStream(passphrase, 1963 outputStream, null, true, true); 1964 } 1965 catch (final Exception e) 1966 { 1967 Debug.debugException(e); 1968 throw new LDAPException(ResultCode.LOCAL_ERROR, 1969 ERR_LDIFMODIFY_CANNOT_ENCRYPT_OUTPUT_FILE.get( 1970 outputFile.getAbsolutePath(), 1971 StaticUtils.getExceptionMessage(e)), 1972 e); 1973 } 1974 } 1975 1976 if (compressTarget.isPresent()) 1977 { 1978 try 1979 { 1980 outputStream = new GZIPOutputStream(outputStream); 1981 } 1982 catch (final Exception e) 1983 { 1984 Debug.debugException(e); 1985 throw new LDAPException(ResultCode.LOCAL_ERROR, 1986 ERR_LDIFMODIFY_CANNOT_COMPRESS_OUTPUT_FILE.get( 1987 outputFile.getAbsolutePath(), 1988 StaticUtils.getExceptionMessage(e)), 1989 e); 1990 } 1991 } 1992 1993 final LDIFWriter ldifWriter = new LDIFWriter(outputStream); 1994 if (doNotWrap.isPresent()) 1995 { 1996 ldifWriter.setWrapColumn(0); 1997 } 1998 else if (wrapColumn.isPresent()) 1999 { 2000 ldifWriter.setWrapColumn(wrapColumn.getValue()); 2001 } 2002 else 2003 { 2004 ldifWriter.setWrapColumn(WRAP_COLUMN); 2005 } 2006 2007 closeOutputStream = false; 2008 return ldifWriter; 2009 } 2010 finally 2011 { 2012 if (closeOutputStream && (outputStream != null)) 2013 { 2014 try 2015 { 2016 outputStream.close(); 2017 } 2018 catch (final Exception e) 2019 { 2020 Debug.debugException(e); 2021 } 2022 } 2023 } 2024 } 2025 2026 2027 2028 /** 2029 * Updates the provided entry with any appropriate changes. 2030 * 2031 * @param entry 2032 * The entry to be processed. It must not be {@code null}. 2033 * @param addAndSubsequentChangeRecords 2034 * A map that will be updated with add change records for a given 2035 * entry, along with any subsequent change records that apply to 2036 * the entry after it has been added. It must not be 2037 * {@code null}, must be empty, and must be updatable. 2038 * @param deletedEntryDNs 2039 * A map that will be updated with the DNs of any entries that 2040 * are targeted by delete modifications and that have not been 2041 * previously added or renamed. It must not be {@code null}, 2042 * must be empty, and must be updatable. 2043 * @param modifyChangeRecords 2044 * A map that will be updated with any modify change records 2045 * that target an entry that has not been targeted by any other 2046 * type of change. It must not be {@code null}, must be empty, 2047 * and must be updatable. 2048 * @param modifyDNAndSubsequentChangeRecords 2049 * A map that will be updated with any change records for modify 2050 * DN operations that target a given entry, and any subsequent 2051 * operations that target the entry with its new DN. It must not 2052 * be {@code null}, must be empty, and must be updatable. 2053 * @param comment 2054 * A buffer that should be updated with any comment to be 2055 * included in the output, even if the entry is not altered. It 2056 * must not be {@code null}, but it should be empty. 2057 * @param resultCode 2058 * A reference to the final result code that should be used for 2059 * the tool. This may be updated if an error occurred during 2060 * processing and no value is already set. It must not be 2061 * {@code null}, but is allowed to have no value assigned. 2062 * @param entriesUpdated 2063 * A counter that should be incremented if any changes are 2064 * applied (including deleting the entry). It should not be 2065 * updated if none of the changes are applicable to the provided 2066 * entry. It must not be {@code null}. 2067 * 2068 * @return The provided entry if none of the changes are applicable, an 2069 * updated entry if changes are applied, or {@code null} if the entry 2070 * should be deleted and therefore omitted from the target LDIF file. 2071 */ 2072 @Nullable() 2073 private Entry updateEntry(@NotNull final Entry entry, 2074 @NotNull final Map<DN,List<LDIFChangeRecord>> 2075 addAndSubsequentChangeRecords, 2076 @NotNull final Map<DN,Boolean> deletedEntryDNs, 2077 @NotNull final Map<DN,List<LDIFModifyChangeRecord>> modifyChangeRecords, 2078 @NotNull final Map<DN,ObjectPair<DN,List<LDIFChangeRecord>>> 2079 modifyDNAndSubsequentChangeRecords, 2080 @NotNull final StringBuilder comment, 2081 @NotNull final AtomicReference<ResultCode> resultCode, 2082 @NotNull final AtomicLong entriesUpdated) 2083 { 2084 // Get the parsed DN for the entry. If that fails, then we'll just return 2085 // the provided entry along with a comment explaining that its DN could not 2086 // be parsed. 2087 final DN entryDN; 2088 try 2089 { 2090 entryDN = entry.getParsedDN(); 2091 2092 } 2093 catch (final LDAPException e) 2094 { 2095 Debug.debugException(e); 2096 resultCode.compareAndSet(null, e.getResultCode()); 2097 appendComment(comment, 2098 ERR_LDIFMODIFY_CANNOT_PARSE_ENTRY_DN.get(e.getMessage()), true); 2099 return entry; 2100 } 2101 2102 2103 // See if there is a delete change record for the entry. If so, then mark 2104 // the entry as deleted and return null. 2105 if (deletedEntryDNs.containsKey(entryDN)) 2106 { 2107 deletedEntryDNs.put(entryDN, Boolean.TRUE); 2108 createChangeRecordComment(comment, INFO_LDIFMODIFY_APPLIED_DELETE.get(), 2109 entry, false); 2110 entriesUpdated.incrementAndGet(); 2111 return null; 2112 } 2113 2114 2115 // See if there is a delete change record for one of the entry's superiors. 2116 // If so, then mark the entry as deleted and return null. 2117 DN parentDN = entryDN.getParent(); 2118 while (parentDN != null) 2119 { 2120 if (deletedEntryDNs.containsKey(parentDN)) 2121 { 2122 createChangeRecordComment(comment, 2123 INFO_LDIFMODIFY_APPLIED_DELETE_OF_ANCESTOR.get( 2124 parentDN.toString()), 2125 entry, false); 2126 entriesUpdated.incrementAndGet(); 2127 return null; 2128 } 2129 2130 parentDN = parentDN.getParent(); 2131 } 2132 2133 2134 // See if there are any modify change records that target the entry. If so, 2135 // then apply those modifications. 2136 Entry updatedEntry = entry; 2137 final AtomicBoolean isUpdated = new AtomicBoolean(false); 2138 final List<String> errors = new ArrayList<>(); 2139 final List<LDIFModifyChangeRecord> modRecords = 2140 modifyChangeRecords.remove(entryDN); 2141 if (modRecords != null) 2142 { 2143 for (final LDIFModifyChangeRecord r : modRecords) 2144 { 2145 updatedEntry = applyModification(updatedEntry, r, isUpdated, resultCode, 2146 comment); 2147 } 2148 } 2149 2150 2151 // See if the entry was targeted by a modify DN operation. If so, then 2152 // rename the entry and see if there are any follow-on modifications. 2153 final ObjectPair<DN,List<LDIFChangeRecord>> modDNRecords = 2154 modifyDNAndSubsequentChangeRecords.remove(entryDN); 2155 if (modDNRecords != null) 2156 { 2157 for (final LDIFChangeRecord r : modDNRecords.getSecond()) 2158 { 2159 if (r instanceof LDIFModifyDNChangeRecord) 2160 { 2161 final LDIFModifyDNChangeRecord modDNChangeRecord = 2162 (LDIFModifyDNChangeRecord) r; 2163 updatedEntry = applyModifyDN(updatedEntry, entryDN, 2164 modDNRecords.getFirst(), modDNChangeRecord.deleteOldRDN()); 2165 createChangeRecordComment(comment, 2166 INFO_LDIFMODIFY_APPLIED_MODIFY_DN.get(), r, false); 2167 isUpdated.set(true); 2168 } 2169 else 2170 { 2171 updatedEntry = applyModification(updatedEntry, 2172 (LDIFModifyChangeRecord) r, isUpdated, resultCode, comment); 2173 } 2174 } 2175 } 2176 2177 2178 // See if there is an add change record that targets the same entry. If so, 2179 // then the add won't be processed but maybe subsequent changes will be. 2180 final List<LDIFChangeRecord> addAndMods = 2181 addAndSubsequentChangeRecords.remove(entryDN); 2182 if (addAndMods != null) 2183 { 2184 for (final LDIFChangeRecord r : addAndMods) 2185 { 2186 if (r instanceof LDIFAddChangeRecord) 2187 { 2188 resultCode.compareAndSet(null, ResultCode.ENTRY_ALREADY_EXISTS); 2189 createChangeRecordComment(comment, 2190 ERR_LDIFMODIFY_NOT_ADDING_EXISTING_ENTRY.get(), r, true); 2191 } 2192 else 2193 { 2194 updatedEntry = applyModification(updatedEntry, 2195 (LDIFModifyChangeRecord) r, isUpdated, resultCode, comment); 2196 } 2197 } 2198 } 2199 2200 2201 if (isUpdated.get()) 2202 { 2203 entriesUpdated.incrementAndGet(); 2204 } 2205 else 2206 { 2207 if (comment.length() > 0) 2208 { 2209 appendComment(comment, StaticUtils.EOL, false); 2210 appendComment(comment, StaticUtils.EOL, false); 2211 } 2212 appendComment(comment, INFO_LDIFMODIFY_ENTRY_NOT_UPDATED.get(), false); 2213 } 2214 2215 return updatedEntry; 2216 } 2217 2218 2219 2220 /** 2221 * Creates a copy of the provided entry with the given modification applied. 2222 * 2223 * @param entry The entry to be updated. It must not be 2224 * {@code null}. 2225 * @param modifyChangeRecord The modify change record to apply. It must not 2226 * be {@code null}. 2227 * @param isUpdated A value that should be updated if the entry is 2228 * successfully modified. It must not be 2229 * {@code null}. 2230 * @param resultCode A reference to the final result code that 2231 * should be used for the tool. This may be 2232 * updated if an error occurred during processing 2233 * and no value is already set. It must not be 2234 * {@code null}, but is allowed to have no value 2235 * assigned. 2236 * @param comment A buffer that should be updated with any 2237 * comment to be included in the output, even if 2238 * the entry is not altered. It must not be 2239 * {@code null}, but it may be empty. 2240 * 2241 * @return The entry with the modifications applied, or the original entry if 2242 * an error occurred while applying the change. 2243 */ 2244 @NotNull() 2245 private Entry applyModification(@NotNull final Entry entry, 2246 @NotNull final LDIFModifyChangeRecord modifyChangeRecord, 2247 @NotNull final AtomicBoolean isUpdated, 2248 @NotNull final AtomicReference<ResultCode> resultCode, 2249 @NotNull final StringBuilder comment) 2250 { 2251 try 2252 { 2253 final Entry updatedEntry = Entry.applyModifications(entry, 2254 (! strictModifications.isPresent()), 2255 modifyChangeRecord.getModifications()); 2256 createChangeRecordComment(comment, INFO_LDIFMODIFY_APPLIED_MODIFY.get(), 2257 modifyChangeRecord, false); 2258 isUpdated.set(true); 2259 return updatedEntry; 2260 } 2261 catch (final LDAPException e) 2262 { 2263 Debug.debugException(e); 2264 resultCode.compareAndSet(null, e.getResultCode()); 2265 createChangeRecordComment(comment, 2266 ERR_LDIFMODIFY_ERROR_APPLYING_MODIFY.get( 2267 String.valueOf(e.getResultCode()), e.getMessage()), 2268 modifyChangeRecord, true); 2269 return entry; 2270 } 2271 } 2272 2273 2274 2275 /** 2276 * Creates a copy of the provided entry with the given new DN. 2277 * 2278 * @param entry The entry to be renamed. It must not be 2279 * {@code null}. 2280 * @param originalDN A parsed representation of the original DN for the 2281 * entry. It must not be {@code null}. 2282 * @param newDN A parsed representation of the new DN for the entry. 2283 * It must not be {@code null}. 2284 * @param deleteOldRDN Indicates whether the old RDN values should be 2285 * removed from the entry. 2286 * 2287 * @return The updated entry with the new DN and any other associated 2288 * changes. 2289 */ 2290 @NotNull() 2291 private Entry applyModifyDN(@NotNull final Entry entry, 2292 @NotNull final DN originalDN, 2293 @NotNull final DN newDN, 2294 final boolean deleteOldRDN) 2295 { 2296 final Entry copy = entry.duplicate(); 2297 copy.setDN(newDN); 2298 2299 final RDN oldRDN = originalDN.getRDN(); 2300 if (deleteOldRDN && (oldRDN != null)) 2301 { 2302 for (final Attribute a : oldRDN.getAttributes()) 2303 { 2304 for (final byte[] value : a.getValueByteArrays()) 2305 { 2306 copy.removeAttributeValue(a.getName(), value); 2307 } 2308 } 2309 } 2310 2311 final RDN newRDN = newDN.getRDN(); 2312 if (newRDN != null) 2313 { 2314 for (final Attribute a : newRDN.getAttributes()) 2315 { 2316 for (final byte[] value : a.getValueByteArrays()) 2317 { 2318 copy.addAttribute(a); 2319 } 2320 } 2321 } 2322 2323 return copy; 2324 } 2325 2326 2327 2328 /** 2329 * Writes the provided LDIF record to the LDIF writer. 2330 * 2331 * @param ldifWriter The writer to which the LDIF record should be written. 2332 * It must not be {@code null}. 2333 * @param ldifRecord The LDIF record to be written. It must not be 2334 * {@code null}. 2335 * @param comment The comment to include as part of the LDIF record. It 2336 * may be {@code null} or empty if no comment should be 2337 * included. 2338 * 2339 * @throws IOException If an error occurs while attempting to write to the 2340 * LDIF writer. 2341 */ 2342 private void writeLDIFRecord(@NotNull final LDIFWriter ldifWriter, 2343 @NotNull final LDIFRecord ldifRecord, 2344 @Nullable final CharSequence comment) 2345 throws IOException 2346 { 2347 if (suppressComments.isPresent() || (comment == null) || 2348 (comment.length() == 0)) 2349 { 2350 ldifWriter.writeLDIFRecord(ldifRecord); 2351 } 2352 else 2353 { 2354 ldifWriter.writeLDIFRecord(ldifRecord, comment.toString()); 2355 } 2356 } 2357 2358 2359 2360 /** 2361 * Appends the provided comment to the given buffer. 2362 * 2363 * @param buffer The buffer to which the comment should be appended. 2364 * @param comment The comment to be appended. 2365 * @param isError Indicates whether the comment represents an error that 2366 * should be added to the error list if it exists. It should 2367 * be {@code false} if the comment is not an error, or if it 2368 * is an error but should not be added to the list of error 2369 * messages (e.g., because a message will be added through 2370 * some other means). 2371 */ 2372 private void appendComment(@NotNull final StringBuilder buffer, 2373 @NotNull final String comment, 2374 final boolean isError) 2375 { 2376 buffer.append(comment); 2377 if (isError && (errorMessages != null)) 2378 { 2379 errorMessages.add(comment); 2380 } 2381 } 2382 2383 2384 2385 /** 2386 * Writes the provided comment to the LDIF writer. 2387 * 2388 * @param ldifWriter The writer to which the comment should be written. It 2389 * must not be {@code null}. 2390 * @param comment The comment to be written. It may be {@code null} or 2391 * empty if no comment should actually be written. 2392 * @param isError Indicates whether the comment represents an error that 2393 * should be added to the error list if it exists. It 2394 * should be {@code false} if the comment is not an error, 2395 * or if it is an error but should not be added to the 2396 * list of error messages (e.g., because a message will be 2397 * added through some other means). 2398 * 2399 * @throws IOException If an error occurs while attempting to write to the 2400 * LDIF writer. 2401 */ 2402 private void writeLDIFComment(@NotNull final LDIFWriter ldifWriter, 2403 @Nullable final CharSequence comment, 2404 final boolean isError) 2405 throws IOException 2406 { 2407 if (! (suppressComments.isPresent() || (comment == null) || 2408 (comment.length() == 0))) 2409 { 2410 ldifWriter.writeComment(comment.toString(), false, true); 2411 } 2412 2413 if (isError && (errorMessages != null) && (comment != null)) 2414 { 2415 errorMessages.add(comment.toString()); 2416 } 2417 } 2418 2419 2420 2421 /** 2422 * Appends a comment to the provided buffer for the given LDIF record. 2423 * 2424 * @param buffer The buffer to which the comment should be appended. It 2425 * must not be {@code null}. 2426 * @param message The message to include before the LDIF record. It must 2427 * not be {@code null}. 2428 * @param record The LDIF record to include in the comment. 2429 * @param isError Indicates whether the comment represents an error that 2430 * should be added to the error list if it exists. It should 2431 * be {@code false} if the comment is not an error, or if it 2432 * is an error but should not be added to the list of error 2433 * messages (e.g., because a message will be added through 2434 * some other means). 2435 */ 2436 private void createChangeRecordComment(@NotNull final StringBuilder buffer, 2437 @NotNull final String message, 2438 @NotNull final LDIFRecord record, 2439 final boolean isError) 2440 { 2441 final int initialLength = buffer.length(); 2442 if (initialLength > 0) 2443 { 2444 buffer.append(StaticUtils.EOL); 2445 buffer.append(StaticUtils.EOL); 2446 } 2447 2448 buffer.append(message); 2449 buffer.append(StaticUtils.EOL); 2450 2451 final int wrapCol; 2452 if (wrapColumn.isPresent() && (wrapColumn.getValue() > 20) && 2453 (wrapColumn.getValue() <= 85)) 2454 { 2455 wrapCol = wrapColumn.getValue() - 10; 2456 } 2457 else 2458 { 2459 wrapCol = 75; 2460 } 2461 2462 for (final String line : record.toLDIF(wrapCol)) 2463 { 2464 buffer.append(" "); 2465 buffer.append(line); 2466 buffer.append(StaticUtils.EOL); 2467 } 2468 2469 if (isError && (errorMessages != null)) 2470 { 2471 if (initialLength == 0) 2472 { 2473 errorMessages.add(buffer.toString()); 2474 } 2475 else 2476 { 2477 errorMessages.add(buffer.toString().substring(initialLength)); 2478 } 2479 } 2480 } 2481 2482 2483 2484 /** 2485 * Writes a wrapped version of the provided message to standard error. If an 2486 * {@code errorList} is also available, then the message will also be added to 2487 * that list. 2488 * 2489 * @param message The message to be written. It must not be {@code null]. 2490 */ 2491 private void wrapErr(@NotNull final String message) 2492 { 2493 wrapErr(0, WRAP_COLUMN, message); 2494 if (errorMessages != null) 2495 { 2496 errorMessages.add(message); 2497 } 2498 } 2499 2500 2501 2502 /** 2503 * Writes the provided message and sets it as the completion message. 2504 * 2505 * @param isError Indicates whether the message should be written to 2506 * standard error rather than standard output. 2507 * @param message The message to be written. 2508 */ 2509 private void logCompletionMessage(final boolean isError, 2510 @NotNull final String message) 2511 { 2512 completionMessage.compareAndSet(null, message); 2513 2514 if (isError) 2515 { 2516 wrapErr(message); 2517 } 2518 else 2519 { 2520 wrapOut(0, WRAP_COLUMN, message); 2521 } 2522 } 2523 2524 2525 2526 /** 2527 * {@inheritDoc} 2528 */ 2529 @Override() 2530 @NotNull() 2531 public LinkedHashMap<String[],String> getExampleUsages() 2532 { 2533 final LinkedHashMap<String[],String> examples = new LinkedHashMap<>(); 2534 2535 examples.put( 2536 new String[] 2537 { 2538 "--sourceLDIF", "original.ldif", 2539 "--changesLDIF", "changes.ldif", 2540 "--targetLDIF", "updated.ldif" 2541 }, 2542 INFO_LDIFMODIFY_EXAMPLE.get("changes.ldif", "original.ldif", 2543 "updated.ldif")); 2544 2545 return examples; 2546 } 2547}