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