001/* 002 * Copyright 2020-2024 Ping Identity Corporation 003 * All Rights Reserved. 004 */ 005/* 006 * Copyright 2020-2024 Ping Identity Corporation 007 * 008 * Licensed under the Apache License, Version 2.0 (the "License"); 009 * you may not use this file except in compliance with the License. 010 * You may obtain a copy of the License at 011 * 012 * http://www.apache.org/licenses/LICENSE-2.0 013 * 014 * Unless required by applicable law or agreed to in writing, software 015 * distributed under the License is distributed on an "AS IS" BASIS, 016 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 017 * See the License for the specific language governing permissions and 018 * limitations under the License. 019 */ 020/* 021 * Copyright (C) 2020-2024 Ping Identity Corporation 022 * 023 * This program is free software; you can redistribute it and/or modify 024 * it under the terms of the GNU General Public License (GPLv2 only) 025 * or the terms of the GNU Lesser General Public License (LGPLv2.1 only) 026 * as published by the Free Software Foundation. 027 * 028 * This program is distributed in the hope that it will be useful, 029 * but WITHOUT ANY WARRANTY; without even the implied warranty of 030 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 031 * GNU General Public License for more details. 032 * 033 * You should have received a copy of the GNU General Public License 034 * along with this program; if not, see <http://www.gnu.org/licenses>. 035 */ 036package com.unboundid.ldif; 037 038 039 040import java.io.File; 041import java.io.FileInputStream; 042import java.io.FileOutputStream; 043import java.io.InputStream; 044import java.io.OutputStream; 045import java.util.ArrayList; 046import java.util.Arrays; 047import java.util.Collections; 048import java.util.EnumSet; 049import java.util.HashSet; 050import java.util.LinkedHashMap; 051import java.util.LinkedHashSet; 052import java.util.List; 053import java.util.Map; 054import java.util.Set; 055import java.util.TreeMap; 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.Filter; 064import com.unboundid.ldap.sdk.InternalSDKHelper; 065import com.unboundid.ldap.sdk.LDAPException; 066import com.unboundid.ldap.sdk.Modification; 067import com.unboundid.ldap.sdk.ResultCode; 068import com.unboundid.ldap.sdk.Version; 069import com.unboundid.ldap.sdk.schema.AttributeTypeDefinition; 070import com.unboundid.ldap.sdk.schema.Schema; 071import com.unboundid.ldap.sdk.unboundidds.tools.ToolUtils; 072import com.unboundid.util.CommandLineTool; 073import com.unboundid.util.Debug; 074import com.unboundid.util.NotNull; 075import com.unboundid.util.Nullable; 076import com.unboundid.util.ObjectPair; 077import com.unboundid.util.PassphraseEncryptedOutputStream; 078import com.unboundid.util.StaticUtils; 079import com.unboundid.util.ThreadSafety; 080import com.unboundid.util.ThreadSafetyLevel; 081import com.unboundid.util.args.ArgumentException; 082import com.unboundid.util.args.ArgumentParser; 083import com.unboundid.util.args.BooleanArgument; 084import com.unboundid.util.args.FileArgument; 085import com.unboundid.util.args.FilterArgument; 086import com.unboundid.util.args.StringArgument; 087 088import static com.unboundid.ldif.LDIFMessages.*; 089 090 091 092/** 093 * This class provides a command-line tool that can be used to identify the 094 * differences between two LDIF files. The output will itself be an LDIF file 095 * that contains the add, delete, and modify operations that can be processed 096 * against the source LDIF file to result in the target LDIF file. 097 */ 098@ThreadSafety(level=ThreadSafetyLevel.NOT_THREADSAFE) 099public final class LDIFDiff 100 extends CommandLineTool 101{ 102 /** 103 * The server root directory for the Ping Identity Directory Server (or 104 * related Ping Identity server product) that contains this tool, if 105 * applicable. 106 */ 107 @Nullable private static final File PING_SERVER_ROOT = 108 InternalSDKHelper.getPingIdentityServerRoot(); 109 110 111 112 /** 113 * Indicates whether the tool is running as part of a Ping Identity Directory 114 * Server (or related Ping Identity Server Product) installation. 115 */ 116 private static final boolean PING_SERVER_AVAILABLE = 117 (PING_SERVER_ROOT != null); 118 119 120 121 /** 122 * The column at which to wrap long lines. 123 */ 124 private static final int WRAP_COLUMN = StaticUtils.TERMINAL_WIDTH_COLUMNS - 1; 125 126 127 128 /** 129 * The change type name used to indicate that add operations should be 130 * included in the output. 131 */ 132 @NotNull 133 private static final String CHANGE_TYPE_ADD = "add"; 134 135 136 137 /** 138 * The change type name used to indicate that delete operations should be 139 * included in the output. 140 */ 141 @NotNull private static final String CHANGE_TYPE_DELETE = "delete"; 142 143 144 145 /** 146 * The change type name used to indicate that modify operations should be 147 * included in the output. 148 */ 149 @NotNull private static final String CHANGE_TYPE_MODIFY = "modify"; 150 151 152 153 // The completion message for this tool. 154 @NotNull private final AtomicReference<String> completionMessage; 155 156 // Encryption passphrases used thus far. 157 @NotNull private final List<char[]> encryptionPassphrases; 158 159 // The command-line arguments supported by this tool. 160 @Nullable private BooleanArgument byteForByte; 161 @Nullable private BooleanArgument compressOutput; 162 @Nullable private BooleanArgument encryptOutput; 163 @Nullable private BooleanArgument excludeNoUserModificationAttributes; 164 @Nullable private BooleanArgument includeOperationalAttributes; 165 @Nullable private BooleanArgument nonReversibleModifications; 166 @Nullable private BooleanArgument overwriteExistingOutputLDIF; 167 @Nullable private BooleanArgument singleValueChanges; 168 @Nullable private BooleanArgument stripTrailingSpaces; 169 @Nullable private FileArgument outputEncryptionPassphraseFile; 170 @Nullable private FileArgument outputLDIF; 171 @Nullable private FileArgument schemaPath; 172 @Nullable private FileArgument sourceEncryptionPassphraseFile; 173 @Nullable private FileArgument sourceLDIF; 174 @Nullable private FileArgument targetEncryptionPassphraseFile; 175 @Nullable private FileArgument targetLDIF; 176 @Nullable private FilterArgument excludeFilter; 177 @Nullable private FilterArgument includeFilter; 178 @Nullable private StringArgument changeType; 179 @Nullable private StringArgument excludeAttribute; 180 @Nullable private StringArgument includeAttribute; 181 182 183 184 /** 185 * Invokes this tool with the provided set of command-line arguments. 186 * 187 * @param args The set of arguments provided to this tool. It may be 188 * empty but must not be {@code null}. 189 */ 190 public static void main(@NotNull final String... args) 191 { 192 final ResultCode resultCode = main(System.out, System.err, args); 193 if (resultCode != ResultCode.SUCCESS) 194 { 195 System.exit(resultCode.intValue()); 196 } 197 } 198 199 200 201 /** 202 * Invokes this tool with the provided set of command-line arguments, using 203 * the given output and error streams. 204 * 205 * @param out The output stream to use for standard output. It may be 206 * {@code null} if standard output should be suppressed. 207 * @param err The output stream to use for standard error. It may be 208 * {@code null} if standard error should be suppressed. 209 * @param args The set of arguments provided to this tool. It may be 210 * empty but must not be {@code null}. 211 * 212 * @return A result code indicating the status of processing. Any result 213 * code other than {@link ResultCode#SUCCESS} should be considered 214 * an error. 215 */ 216 @NotNull() 217 public static ResultCode main(@Nullable final OutputStream out, 218 @Nullable final OutputStream err, 219 @NotNull final String... args) 220 { 221 final LDIFDiff tool = new LDIFDiff(out, err); 222 return tool.runTool(args); 223 } 224 225 226 227 /** 228 * Creates a new instance of this tool with the provided output and error 229 * streams. 230 * 231 * @param out The output stream to use for standard output. It may be 232 * {@code null} if standard output should be suppressed. 233 * @param err The output stream to use for standard error. It may be 234 * {@code null} if standard error should be suppressed. 235 */ 236 public LDIFDiff(@Nullable final OutputStream out, 237 @Nullable final OutputStream err) 238 { 239 super(out, err); 240 241 encryptionPassphrases = new ArrayList<>(5); 242 completionMessage = new AtomicReference<>(); 243 244 compressOutput = null; 245 encryptOutput = null; 246 excludeNoUserModificationAttributes = null; 247 includeOperationalAttributes = null; 248 nonReversibleModifications = null; 249 overwriteExistingOutputLDIF = null; 250 singleValueChanges = null; 251 stripTrailingSpaces = null; 252 outputEncryptionPassphraseFile = null; 253 outputLDIF = null; 254 schemaPath = null; 255 sourceEncryptionPassphraseFile = null; 256 sourceLDIF = null; 257 targetEncryptionPassphraseFile = null; 258 targetLDIF = null; 259 changeType = null; 260 excludeFilter = null; 261 includeFilter = null; 262 excludeAttribute = null; 263 includeAttribute = null; 264 } 265 266 267 268 /** 269 * {@inheritDoc} 270 */ 271 @Override() 272 @NotNull() 273 public String getToolName() 274 { 275 return "ldif-diff"; 276 } 277 278 279 280 /** 281 * {@inheritDoc} 282 */ 283 @Override() 284 @NotNull() 285 public String getToolDescription() 286 { 287 return INFO_LDIF_DIFF_TOOL_DESCRIPTION_1.get(); 288 } 289 290 291 292 /** 293 * {@inheritDoc} 294 */ 295 @Override() 296 @NotNull() 297 public List<String> getAdditionalDescriptionParagraphs() 298 { 299 final List<String> messages = new ArrayList<>(3); 300 messages.add(INFO_LDIF_DIFF_TOOL_DESCRIPTION_2.get()); 301 messages.add(INFO_LDIF_DIFF_TOOL_DESCRIPTION_3.get()); 302 303 if (PING_SERVER_AVAILABLE) 304 { 305 messages.add(INFO_LDIF_DIFF_TOOL_DESCRIPTION_4_PING_SERVER.get( 306 getToolName())); 307 } 308 else 309 { 310 messages.add(INFO_LDIF_DIFF_TOOL_DESCRIPTION_4_STANDALONE.get( 311 getToolName())); 312 } 313 314 return messages; 315 } 316 317 318 319 /** 320 * {@inheritDoc} 321 */ 322 @Override() 323 @NotNull() 324 public String getToolVersion() 325 { 326 return Version.NUMERIC_VERSION_STRING; 327 } 328 329 330 331 /** 332 * {@inheritDoc} 333 */ 334 @Override() 335 public boolean supportsInteractiveMode() 336 { 337 return true; 338 } 339 340 341 342 /** 343 * {@inheritDoc} 344 */ 345 @Override() 346 public boolean defaultsToInteractiveMode() 347 { 348 return true; 349 } 350 351 352 353 /** 354 * {@inheritDoc} 355 */ 356 @Override() 357 public boolean supportsPropertiesFile() 358 { 359 return true; 360 } 361 362 363 364 /** 365 * {@inheritDoc} 366 */ 367 @Override() 368 protected boolean supportsDebugLogging() 369 { 370 return true; 371 } 372 373 374 375 /** 376 * {@inheritDoc} 377 */ 378 @Override() 379 @Nullable() 380 protected String getToolCompletionMessage() 381 { 382 return completionMessage.get(); 383 } 384 385 386 387 /** 388 * {@inheritDoc} 389 */ 390 @Override() 391 public void addToolArguments(@NotNull final ArgumentParser parser) 392 throws ArgumentException 393 { 394 sourceLDIF = new FileArgument('s', "sourceLDIF", true, 1, null, 395 INFO_LDIF_DIFF_ARG_DESC_SOURCE_LDIF.get(), true, true, true, false); 396 sourceLDIF.addLongIdentifier("source-ldif", true); 397 sourceLDIF.addLongIdentifier("source", true); 398 sourceLDIF.addLongIdentifier("sourceFile", true); 399 sourceLDIF.addLongIdentifier("source-file", true); 400 sourceLDIF.addLongIdentifier("sourceLDIFFile", true); 401 sourceLDIF.addLongIdentifier("source-ldif-file", true); 402 sourceLDIF.setArgumentGroupName(INFO_LDIF_DIFF_ARG_GROUP_SOURCE.get()); 403 parser.addArgument(sourceLDIF); 404 405 406 final String sourcePWDesc; 407 if (PING_SERVER_AVAILABLE) 408 { 409 sourcePWDesc = INFO_LDIF_DIFF_ARG_DESC_SOURCE_PW_FILE_PING_SERVER.get(); 410 } 411 else 412 { 413 sourcePWDesc = INFO_LDIF_DIFF_ARG_DESC_SOURCE_PW_FILE_STANDALONE.get(); 414 } 415 sourceEncryptionPassphraseFile = new FileArgument(null, 416 "sourceEncryptionPassphraseFile", false, 1, null, sourcePWDesc, true, 417 true, true, false); 418 sourceEncryptionPassphraseFile.addLongIdentifier( 419 "source-encryption-passphrase-file", true); 420 sourceEncryptionPassphraseFile.addLongIdentifier("sourcePassphraseFile", 421 true); 422 sourceEncryptionPassphraseFile.addLongIdentifier("source-passphrase-file", 423 true); 424 sourceEncryptionPassphraseFile.addLongIdentifier( 425 "sourceEncryptionPasswordFile", true); 426 sourceEncryptionPassphraseFile.addLongIdentifier( 427 "source-encryption-password-file", true); 428 sourceEncryptionPassphraseFile.addLongIdentifier("sourcePasswordFile", 429 true); 430 sourceEncryptionPassphraseFile.addLongIdentifier("source-password-file", 431 true); 432 sourceEncryptionPassphraseFile.setArgumentGroupName( 433 INFO_LDIF_DIFF_ARG_GROUP_SOURCE.get()); 434 parser.addArgument(sourceEncryptionPassphraseFile); 435 436 437 targetLDIF = new FileArgument('t', "targetLDIF", true, 1, null, 438 INFO_LDIF_DIFF_ARG_DESC_TARGET_LDIF.get(), true, true, true, false); 439 targetLDIF.addLongIdentifier("target-ldif", true); 440 targetLDIF.addLongIdentifier("target", true); 441 targetLDIF.addLongIdentifier("targetFile", true); 442 targetLDIF.addLongIdentifier("target-file", true); 443 targetLDIF.addLongIdentifier("targetLDIFFile", true); 444 targetLDIF.addLongIdentifier("target-ldif-file", true); 445 targetLDIF.setArgumentGroupName(INFO_LDIF_DIFF_ARG_GROUP_TARGET.get()); 446 parser.addArgument(targetLDIF); 447 448 449 final String targetPWDesc; 450 if (PING_SERVER_AVAILABLE) 451 { 452 targetPWDesc = INFO_LDIF_DIFF_ARG_DESC_TARGET_PW_FILE_PING_SERVER.get(); 453 } 454 else 455 { 456 targetPWDesc = INFO_LDIF_DIFF_ARG_DESC_TARGET_PW_FILE_STANDALONE.get(); 457 } 458 targetEncryptionPassphraseFile = new FileArgument(null, 459 "targetEncryptionPassphraseFile", false, 1, null, targetPWDesc, true, 460 true, true, false); 461 targetEncryptionPassphraseFile.addLongIdentifier( 462 "target-encryption-passphrase-file", true); 463 targetEncryptionPassphraseFile.addLongIdentifier("targetPassphraseFile", 464 true); 465 targetEncryptionPassphraseFile.addLongIdentifier("target-passphrase-file", 466 true); 467 targetEncryptionPassphraseFile.addLongIdentifier( 468 "targetEncryptionPasswordFile", true); 469 targetEncryptionPassphraseFile.addLongIdentifier( 470 "target-encryption-password-file", true); 471 targetEncryptionPassphraseFile.addLongIdentifier("targetPasswordFile", 472 true); 473 targetEncryptionPassphraseFile.addLongIdentifier("target-password-file", 474 true); 475 targetEncryptionPassphraseFile.setArgumentGroupName( 476 INFO_LDIF_DIFF_ARG_GROUP_TARGET.get()); 477 parser.addArgument(targetEncryptionPassphraseFile); 478 479 480 outputLDIF = new FileArgument('o', "outputLDIF", false, 1, null, 481 INFO_LDIF_DIFF_ARG_DESC_OUTPUT_LDIF.get(), false, true, true, false); 482 outputLDIF.addLongIdentifier("output-ldif", true); 483 outputLDIF.addLongIdentifier("output", true); 484 outputLDIF.addLongIdentifier("outputFile", true); 485 outputLDIF.addLongIdentifier("output-file", true); 486 outputLDIF.addLongIdentifier("outputLDIFFile", true); 487 outputLDIF.addLongIdentifier("output-ldif-file", true); 488 outputLDIF.setArgumentGroupName(INFO_LDIF_DIFF_ARG_GROUP_OUTPUT.get()); 489 parser.addArgument(outputLDIF); 490 491 492 compressOutput = new BooleanArgument(null, "compressOutput", 1, 493 INFO_LDIF_DIFF_ARG_DESC_COMPRESS_OUTPUT.get()); 494 compressOutput.addLongIdentifier("compress-output", true); 495 compressOutput.addLongIdentifier("compress", true); 496 compressOutput.setArgumentGroupName(INFO_LDIF_DIFF_ARG_GROUP_OUTPUT.get()); 497 parser.addArgument(compressOutput); 498 499 500 encryptOutput = new BooleanArgument(null, "encryptOutput", 1, 501 INFO_LDIF_DIFF_ARG_DESC_ENCRYPT_OUTPUT.get()); 502 encryptOutput.addLongIdentifier("encrypt-output", true); 503 encryptOutput.addLongIdentifier("encrypt", true); 504 encryptOutput.setArgumentGroupName(INFO_LDIF_DIFF_ARG_GROUP_OUTPUT.get()); 505 parser.addArgument(encryptOutput); 506 507 508 outputEncryptionPassphraseFile = new FileArgument(null, 509 "outputEncryptionPassphraseFile", false, 1, null, 510 INFO_LDIF_DIFF_ARG_DESC_OUTPUT_PW_FILE.get(), true, true, true, false); 511 outputEncryptionPassphraseFile.addLongIdentifier( 512 "output-encryption-passphrase-file", true); 513 outputEncryptionPassphraseFile.addLongIdentifier("outputPassphraseFile", 514 true); 515 outputEncryptionPassphraseFile.addLongIdentifier("output-passphrase-file", 516 true); 517 outputEncryptionPassphraseFile.addLongIdentifier( 518 "outputEncryptionPasswordFile", true); 519 outputEncryptionPassphraseFile.addLongIdentifier( 520 "output-encryption-password-file", true); 521 outputEncryptionPassphraseFile.addLongIdentifier("outputPasswordFile", 522 true); 523 outputEncryptionPassphraseFile.addLongIdentifier("output-password-file", 524 true); 525 outputEncryptionPassphraseFile.setArgumentGroupName( 526 INFO_LDIF_DIFF_ARG_GROUP_OUTPUT.get()); 527 parser.addArgument(outputEncryptionPassphraseFile); 528 529 530 overwriteExistingOutputLDIF = new BooleanArgument('O', 531 "overwriteExistingOutputLDIF", 1, 532 INFO_LDIF_DIFF_ARG_DESC_OVERWRITE_EXISTING.get()); 533 overwriteExistingOutputLDIF.addLongIdentifier( 534 "overwrite-existing-output-ldif", true); 535 overwriteExistingOutputLDIF.addLongIdentifier( 536 "overwriteExistingOutputFile", true); 537 overwriteExistingOutputLDIF.addLongIdentifier( 538 "overwrite-existing-output-file", true); 539 overwriteExistingOutputLDIF.addLongIdentifier("overwriteExistingOutput", 540 true); 541 overwriteExistingOutputLDIF.addLongIdentifier("overwrite-existing-output", 542 true); 543 overwriteExistingOutputLDIF.addLongIdentifier("overwriteExisting", true); 544 overwriteExistingOutputLDIF.addLongIdentifier("overwrite-existing", true); 545 overwriteExistingOutputLDIF.addLongIdentifier("overwriteOutputLDIF", true); 546 overwriteExistingOutputLDIF.addLongIdentifier("overwrite-output-ldif", 547 true); 548 overwriteExistingOutputLDIF.addLongIdentifier("overwriteOutputFile", true); 549 overwriteExistingOutputLDIF.addLongIdentifier("overwrite-output-file", 550 true); 551 overwriteExistingOutputLDIF.addLongIdentifier("overwriteOutput", true); 552 overwriteExistingOutputLDIF.addLongIdentifier("overwrite-output", true); 553 overwriteExistingOutputLDIF.addLongIdentifier("overwrite", true); 554 overwriteExistingOutputLDIF.setArgumentGroupName( 555 INFO_LDIF_DIFF_ARG_GROUP_OUTPUT.get()); 556 parser.addArgument(overwriteExistingOutputLDIF); 557 558 559 changeType = new StringArgument(null, "changeType", false, 0, 560 INFO_LDIF_DIFF_ARG_PLACEHOLDER_CHANGE_TYPE.get(), 561 INFO_LDIF_DIFF_ARG_DESC_CHANGE_TYPE.get(), 562 StaticUtils.setOf( 563 CHANGE_TYPE_ADD, 564 CHANGE_TYPE_DELETE, 565 CHANGE_TYPE_MODIFY), 566 Collections.unmodifiableList(Arrays.asList( 567 CHANGE_TYPE_ADD, 568 CHANGE_TYPE_DELETE, 569 CHANGE_TYPE_MODIFY))); 570 changeType.addLongIdentifier("change-type", true); 571 changeType.addLongIdentifier("operationType", true); 572 changeType.addLongIdentifier("operation-type", true); 573 changeType.setArgumentGroupName(INFO_LDIF_DIFF_ARG_GROUP_CONTENT.get()); 574 parser.addArgument(changeType); 575 576 577 includeFilter = new FilterArgument(null, "includeFilter", false, 0, null, 578 INFO_LDIF_DIFF_ARG_DESC_INCLUDE_FILTER.get()); 579 includeFilter.addLongIdentifier("include-filter", true); 580 includeFilter.setArgumentGroupName(INFO_LDIF_DIFF_ARG_GROUP_CONTENT.get()); 581 parser.addArgument(includeFilter); 582 583 584 excludeFilter = new FilterArgument(null, "excludeFilter", false, 0, null, 585 INFO_LDIF_DIFF_ARG_DESC_EXCLUDE_FILTER.get()); 586 excludeFilter.addLongIdentifier("exclude-filter", true); 587 excludeFilter.setArgumentGroupName(INFO_LDIF_DIFF_ARG_GROUP_CONTENT.get()); 588 parser.addArgument(excludeFilter); 589 590 591 includeAttribute = new StringArgument(null, "includeAttribute", false, 0, 592 INFO_LDIF_DIFF_ARG_PLACEHOLDER_ATTRIBUTE.get(), 593 INFO_LDIF_DIFF_ARG_DESC_INCLUDE_ATTRIBUTE.get()); 594 includeAttribute.addLongIdentifier("include-attribute", true); 595 includeAttribute.addLongIdentifier("includeAttr", true); 596 includeAttribute.addLongIdentifier("include-attr", true); 597 includeAttribute.setArgumentGroupName( 598 INFO_LDIF_DIFF_ARG_GROUP_CONTENT.get()); 599 parser.addArgument(includeAttribute); 600 601 602 excludeAttribute = new StringArgument(null, "excludeAttribute", false, 0, 603 INFO_LDIF_DIFF_ARG_PLACEHOLDER_ATTRIBUTE.get(), 604 INFO_LDIF_DIFF_ARG_DESC_EXCLUDE_ATTRIBUTE.get()); 605 excludeAttribute.addLongIdentifier("exclude-attribute", true); 606 excludeAttribute.addLongIdentifier("excludeAttr", true); 607 excludeAttribute.addLongIdentifier("exclude-attr", true); 608 excludeAttribute.setArgumentGroupName( 609 INFO_LDIF_DIFF_ARG_GROUP_CONTENT.get()); 610 parser.addArgument(excludeAttribute); 611 612 613 includeOperationalAttributes = new BooleanArgument('i', 614 "includeOperationalAttributes", 1, 615 INFO_LDIF_DIFF_ARG_DESC_INCLUDE_OPERATIONAL.get()); 616 includeOperationalAttributes.addLongIdentifier( 617 "include-operational-attributes", true); 618 includeOperationalAttributes.addLongIdentifier("includeOperational", true); 619 includeOperationalAttributes.addLongIdentifier("include-operational", true); 620 includeOperationalAttributes.setArgumentGroupName( 621 INFO_LDIF_DIFF_ARG_GROUP_CONTENT.get()); 622 parser.addArgument(includeOperationalAttributes); 623 624 625 excludeNoUserModificationAttributes = new BooleanArgument('e', 626 "excludeNoUserModificationAttributes", 1, 627 INFO_LDIF_DIFF_ARG_DESC_EXCLUDE_NO_USER_MOD.get()); 628 excludeNoUserModificationAttributes.addLongIdentifier( 629 "exclude-no-user-modification-attributes", true); 630 excludeNoUserModificationAttributes.addLongIdentifier( 631 "excludeNoUserModAttributes", true); 632 excludeNoUserModificationAttributes.addLongIdentifier( 633 "exclude-no-user-mod-attributes", true); 634 excludeNoUserModificationAttributes.addLongIdentifier( 635 "excludeNoUserModification", true); 636 excludeNoUserModificationAttributes.addLongIdentifier( 637 "exclude-no-user-modification", true); 638 excludeNoUserModificationAttributes.addLongIdentifier("excludeNoUserMod", 639 true); 640 excludeNoUserModificationAttributes.addLongIdentifier("exclude-no-user-mod", 641 true); 642 excludeNoUserModificationAttributes.setArgumentGroupName( 643 INFO_LDIF_DIFF_ARG_GROUP_CONTENT.get()); 644 parser.addArgument(excludeNoUserModificationAttributes); 645 646 647 nonReversibleModifications = new BooleanArgument(null, 648 "nonReversibleModifications", 1, 649 INFO_LDIF_DIFF_ARG_DESC_NON_REVERSIBLE_MODS.get()); 650 nonReversibleModifications.addLongIdentifier("non-reversible-modifications", 651 true); 652 nonReversibleModifications.addLongIdentifier("nonReversibleMods", true); 653 nonReversibleModifications.addLongIdentifier("non-reversible-mods", true); 654 nonReversibleModifications.addLongIdentifier("nonReversible", true); 655 nonReversibleModifications.addLongIdentifier("non-reversible", true); 656 nonReversibleModifications.setArgumentGroupName( 657 INFO_LDIF_DIFF_ARG_GROUP_CONTENT.get()); 658 parser.addArgument(nonReversibleModifications); 659 660 661 singleValueChanges = new BooleanArgument('S', "singleValueChanges", 1, 662 INFO_LDIF_DIFF_ARG_DESC_SINGLE_VALUE_CHANGES.get()); 663 singleValueChanges.addLongIdentifier("single-value-changes", true); 664 parser.addArgument(singleValueChanges); 665 666 667 stripTrailingSpaces = new BooleanArgument(null, "stripTrailingSpaces", 1, 668 INFO_LDIF_DIFF_ARG_DESC_STRIP_TRAILING_SPACES.get()); 669 stripTrailingSpaces.addLongIdentifier("strip-trailing-spaces", true); 670 stripTrailingSpaces.addLongIdentifier("ignoreTrailingSpaces", true); 671 stripTrailingSpaces.addLongIdentifier("ignore-trailing-spaces", true); 672 stripTrailingSpaces.setArgumentGroupName( 673 INFO_LDIF_DIFF_ARG_GROUP_CONTENT.get()); 674 parser.addArgument(stripTrailingSpaces); 675 676 677 final String schemaPathDesc; 678 if (PING_SERVER_AVAILABLE) 679 { 680 schemaPathDesc = INFO_LDIF_DIFF_ARG_DESC_SCHEMA_PATH_PING_SERVER.get(); 681 } 682 else 683 { 684 schemaPathDesc = INFO_LDIF_DIFF_ARG_DESC_SCHEMA_PATH_STANDALONE.get(); 685 } 686 schemaPath = new FileArgument(null, "schemaPath", false, 0, null, 687 schemaPathDesc, true, true, false, false); 688 schemaPath.addLongIdentifier("schema-path", true); 689 schemaPath.addLongIdentifier("schemaFile", true); 690 schemaPath.addLongIdentifier("schema-file", true); 691 schemaPath.addLongIdentifier("schemaDirectory", true); 692 schemaPath.addLongIdentifier("schema-directory", true); 693 schemaPath.addLongIdentifier("schema", true); 694 parser.addArgument(schemaPath); 695 696 697 byteForByte = new BooleanArgument(null, "byteForByte", 1, 698 INFO_LDIF_DIFF_ARG_DESC_BYTE_FOR_BYTE.get()); 699 byteForByte.addLongIdentifier("byte-for-byte", true); 700 parser.addArgument(byteForByte); 701 702 703 parser.addDependentArgumentSet(compressOutput, outputLDIF); 704 parser.addDependentArgumentSet(encryptOutput, outputLDIF); 705 parser.addDependentArgumentSet(outputEncryptionPassphraseFile, outputLDIF); 706 parser.addDependentArgumentSet(overwriteExistingOutputLDIF, outputLDIF); 707 708 parser.addDependentArgumentSet(outputEncryptionPassphraseFile, 709 encryptOutput); 710 711 parser.addExclusiveArgumentSet(includeAttribute, excludeAttribute); 712 parser.addExclusiveArgumentSet(includeAttribute, 713 includeOperationalAttributes); 714 715 parser.addExclusiveArgumentSet(includeFilter, excludeFilter); 716 717 parser.addDependentArgumentSet(excludeNoUserModificationAttributes, 718 includeOperationalAttributes); 719 720 parser.addExclusiveArgumentSet(nonReversibleModifications, 721 singleValueChanges); 722 723 parser.addExclusiveArgumentSet(schemaPath, byteForByte); 724 } 725 726 727 728 /** 729 * {@inheritDoc} 730 */ 731 @Override() 732 public void doExtendedArgumentValidation() 733 throws ArgumentException 734 { 735 // If the LDIF file exists and either compressOutput or encryptOutput is 736 // present, then the overwrite argument must also be present. 737 final File outputFile = outputLDIF.getValue(); 738 if ((outputFile != null) && outputFile.exists() && 739 (compressOutput.isPresent() || encryptOutput.isPresent()) && 740 (! overwriteExistingOutputLDIF.isPresent())) 741 { 742 throw new ArgumentException( 743 ERR_LDIF_DIFF_APPEND_WITH_COMPRESSION_OR_ENCRYPTION.get( 744 compressOutput.getIdentifierString(), 745 encryptOutput.getIdentifierString(), 746 overwriteExistingOutputLDIF.getIdentifierString())); 747 } 748 } 749 750 751 752 /** 753 * {@inheritDoc} 754 */ 755 @Override() 756 @NotNull() 757 public ResultCode doToolProcessing() 758 { 759 // Get the change types to use for processing. 760 final Set<ChangeType> changeTypes = EnumSet.noneOf(ChangeType.class); 761 for (final String value : changeType.getValues()) 762 { 763 switch (StaticUtils.toLowerCase(value)) 764 { 765 case CHANGE_TYPE_ADD: 766 changeTypes.add(ChangeType.ADD); 767 break; 768 case CHANGE_TYPE_DELETE: 769 changeTypes.add(ChangeType.DELETE); 770 break; 771 case CHANGE_TYPE_MODIFY: 772 changeTypes.add(ChangeType.MODIFY); 773 break; 774 } 775 } 776 777 778 // Get the schema to use when performing LDIF processing. 779 final Schema schema; 780 try 781 { 782 if (schemaPath.isPresent()) 783 { 784 schema = getSchema(schemaPath.getValues()); 785 } 786 else if (PING_SERVER_AVAILABLE) 787 { 788 schema = getSchema(Collections.singletonList(StaticUtils.constructPath( 789 PING_SERVER_ROOT, "config", "schema"))); 790 } 791 else 792 { 793 schema = Schema.getDefaultStandardSchema(); 794 } 795 } 796 catch (final Exception e) 797 { 798 Debug.debugException(e); 799 logCompletionMessage(true, 800 ERR_LDIF_DIFF_CANNOT_GET_SCHEMA.get( 801 StaticUtils.getExceptionMessage(e))); 802 return ResultCode.LOCAL_ERROR; 803 } 804 805 806 // Identify the sets of include and exclude attributes. 807 final Set<String> includeAttrs; 808 if (includeAttribute.isPresent()) 809 { 810 final Set<String> s = new HashSet<>(); 811 for (final String includeAttr : includeAttribute.getValues()) 812 { 813 final String lowerName = StaticUtils.toLowerCase(includeAttr); 814 s.add(lowerName); 815 816 final AttributeTypeDefinition at = schema.getAttributeType(lowerName); 817 if (at != null) 818 { 819 s.add(StaticUtils.toLowerCase(at.getOID())); 820 for (final String name : at.getNames()) 821 { 822 s.add(StaticUtils.toLowerCase(name)); 823 } 824 } 825 } 826 includeAttrs = Collections.unmodifiableSet(s); 827 } 828 else 829 { 830 includeAttrs = Collections.emptySet(); 831 } 832 833 final Set<String> excludeAttrs; 834 if (excludeAttribute.isPresent()) 835 { 836 final Set<String> s = new HashSet<>(); 837 for (final String excludeAttr : excludeAttribute.getValues()) 838 { 839 final String lowerName = StaticUtils.toLowerCase(excludeAttr); 840 s.add(lowerName); 841 842 final AttributeTypeDefinition at = schema.getAttributeType(lowerName); 843 if (at != null) 844 { 845 s.add(StaticUtils.toLowerCase(at.getOID())); 846 for (final String name : at.getNames()) 847 { 848 s.add(StaticUtils.toLowerCase(name)); 849 } 850 } 851 } 852 excludeAttrs = Collections.unmodifiableSet(s); 853 } 854 else 855 { 856 excludeAttrs = Collections.emptySet(); 857 } 858 859 860 // Read the source and target LDIF files into memory. 861 final TreeMap<DN,Entry> sourceEntries; 862 try 863 { 864 sourceEntries = readEntries(sourceLDIF.getValue(), 865 sourceEncryptionPassphraseFile.getValue(), schema); 866 out(INFO_LDIF_DIFF_READ_FROM_SOURCE_LDIF.get( 867 sourceLDIF.getValue().getName(), sourceEntries.size())); 868 } 869 catch (final LDAPException e) 870 { 871 Debug.debugException(e); 872 logCompletionMessage(true, 873 ERR_LDIF_DIFF_CANNOT_READ_SOURCE_LDIF.get( 874 sourceLDIF.getValue().getAbsolutePath(), e.getMessage())); 875 return e.getResultCode(); 876 } 877 878 final TreeMap<DN,Entry> targetEntries; 879 try 880 { 881 targetEntries = readEntries(targetLDIF.getValue(), 882 targetEncryptionPassphraseFile.getValue(), schema); 883 out(INFO_LDIF_DIFF_READ_FROM_TARGET_LDIF.get( 884 targetLDIF.getValue().getName(), targetEntries.size())); 885 out(); 886 } 887 catch (final LDAPException e) 888 { 889 Debug.debugException(e); 890 logCompletionMessage(true, 891 ERR_LDIF_DIFF_CANNOT_READ_TARGET_LDIF.get( 892 targetLDIF.getValue().getAbsolutePath(), e.getMessage())); 893 return e.getResultCode(); 894 } 895 896 897 final String outputFilePath; 898 if (outputLDIF.isPresent()) 899 { 900 outputFilePath = outputLDIF.getValue().getAbsolutePath(); 901 } 902 else 903 { 904 outputFilePath = "{STDOUT}"; 905 } 906 907 908 // Open the output file for writing. 909 long addCount = 0L; 910 long deleteCount = 0L; 911 long modifyCount = 0L; 912 try (OutputStream outputStream = openOutputStream(); 913 LDIFWriter ldifWriter = new LDIFWriter(outputStream)) 914 { 915 // First, identify any entries that have been added (that is, entries in 916 // the target set that are not in the source set), and write them to the 917 // output file. 918 if (changeTypes.contains(ChangeType.ADD)) 919 { 920 try 921 { 922 addCount = writeAdds(sourceEntries, targetEntries, ldifWriter, 923 schema, includeAttrs, excludeAttrs); 924 } 925 catch (final LDAPException e) 926 { 927 Debug.debugException(e); 928 logCompletionMessage(true, 929 ERR_LDIF_DIFF_ERROR_WRITING_OUTPUT.get(outputFilePath, 930 e.getMessage())); 931 return e.getResultCode(); 932 } 933 } 934 935 936 // Next, identify any entries that have been modified (that is, entries 937 // that exist in both sets, but are different between those sets), and 938 // write them to the output file. We'll write modifies after adds because 939 // that allows modifications to reference newly created entries, and we'll 940 // write modifies before deletes because that allows modifications to 941 // remove references to entries that will be removed. 942 if (changeTypes.contains(ChangeType.MODIFY)) 943 { 944 try 945 { 946 modifyCount = writeModifications(sourceEntries, targetEntries, 947 ldifWriter, schema, includeAttrs, excludeAttrs); 948 } 949 catch (final LDAPException e) 950 { 951 Debug.debugException(e); 952 logCompletionMessage(true, 953 ERR_LDIF_DIFF_ERROR_WRITING_OUTPUT.get(outputFilePath, 954 e.getMessage())); 955 return e.getResultCode(); 956 } 957 } 958 959 960 // Finally, identify any deletes (entries that were only in the set of 961 // source entries) and write them to the output file. 962 if (changeTypes.contains(ChangeType.DELETE)) 963 { 964 try 965 { 966 deleteCount = writeDeletes(sourceEntries, targetEntries, ldifWriter, 967 schema, includeAttrs, excludeAttrs); 968 } 969 catch (final LDAPException e) 970 { 971 Debug.debugException(e); 972 logCompletionMessage(true, 973 ERR_LDIF_DIFF_ERROR_WRITING_OUTPUT.get(outputFilePath, 974 e.getMessage())); 975 return e.getResultCode(); 976 } 977 } 978 979 980 // If we've gotten here, then everything was successful. 981 ldifWriter.flush(); 982 logCompletionMessage(false, INFO_LDIF_DIFF_COMPLETED.get()); 983 if (changeTypes.contains(ChangeType.ADD)) 984 { 985 out(INFO_LDIF_DIFF_COMPLETED_ADD_COUNT.get(addCount)); 986 } 987 988 if (changeTypes.contains(ChangeType.MODIFY)) 989 { 990 out(INFO_LDIF_DIFF_COMPLETED_MODIFY_COUNT.get(modifyCount)); 991 } 992 993 if (changeTypes.contains(ChangeType.DELETE)) 994 { 995 out(INFO_LDIF_DIFF_COMPLETED_DELETE_COUNT.get(deleteCount)); 996 } 997 998 return ResultCode.SUCCESS; 999 } 1000 catch (final LDAPException e) 1001 { 1002 Debug.debugException(e); 1003 logCompletionMessage(true, 1004 ERR_LDIF_DIFF_CANNOT_OPEN_OUTPUT.get(outputFilePath, 1005 e.getMessage())); 1006 return e.getResultCode(); 1007 } 1008 catch (final Exception e) 1009 { 1010 Debug.debugException(e); 1011 logCompletionMessage(true, 1012 ERR_LDIF_DIFF_ERROR_WRITING_OUTPUT.get(outputFilePath, 1013 StaticUtils.getExceptionMessage(e))); 1014 return ResultCode.LOCAL_ERROR; 1015 } 1016 } 1017 1018 1019 1020 /** 1021 * Retrieves the schema contained in the specified paths. 1022 * 1023 * @param paths The paths to use to access the schema. 1024 * 1025 * @return The schema read from the specified files. 1026 * 1027 * @throws Exception If a problem is encountered while loading the schema. 1028 */ 1029 @NotNull() 1030 private static Schema getSchema(@NotNull final List<File> paths) 1031 throws Exception 1032 { 1033 final Set<File> schemaFiles = new LinkedHashSet<>(); 1034 for (final File f : paths) 1035 { 1036 if (f.exists()) 1037 { 1038 if (f.isFile()) 1039 { 1040 schemaFiles.add(f); 1041 } 1042 else if (f.isDirectory()) 1043 { 1044 final TreeMap<String,File> sortedFiles = new TreeMap<>(); 1045 for (final File fileInDir : f.listFiles()) 1046 { 1047 if (fileInDir.isFile()) 1048 { 1049 sortedFiles.put(fileInDir.getName(), fileInDir); 1050 } 1051 } 1052 1053 schemaFiles.addAll(sortedFiles.values()); 1054 } 1055 } 1056 } 1057 1058 return Schema.getSchema(new ArrayList<>(schemaFiles)); 1059 } 1060 1061 1062 1063 /** 1064 * Reads all of the entries in the specified LDIF file into a map. 1065 * 1066 * @param ldifFile The path to the LDIF file to read. It must not be 1067 * {@code null}. 1068 * @param encPWFile The path to the file containing the passphrase used to 1069 * encrypt the LDIF file. It may be {@code null} if the 1070 * LDIF file is not encrypted, or if the encryption key is 1071 * to be obtained through an alternate means. 1072 * @param schema The schema to use when reading the LDIF file. It must 1073 * not be {@code null}. 1074 * 1075 * @return The map of entries read from the file. 1076 * 1077 * @throws LDAPException If a problem occurs while attempting to read the 1078 * entries. 1079 */ 1080 @NotNull() 1081 private TreeMap<DN,Entry> readEntries(@NotNull final File ldifFile, 1082 @Nullable final File encPWFile, 1083 @NotNull final Schema schema) 1084 throws LDAPException 1085 { 1086 if (encPWFile != null) 1087 { 1088 try 1089 { 1090 addPassphrase(getPasswordFileReader().readPassword(encPWFile)); 1091 } 1092 catch (final Exception e) 1093 { 1094 Debug.debugException(e); 1095 throw new LDAPException(ResultCode.LOCAL_ERROR, 1096 ERR_LDIF_DIFF_CANNOT_OPEN_PW_FILE.get(encPWFile.getAbsolutePath(), 1097 StaticUtils.getExceptionMessage(e)), 1098 e); 1099 } 1100 } 1101 1102 1103 InputStream inputStream = null; 1104 try 1105 { 1106 try 1107 { 1108 inputStream = new FileInputStream(ldifFile); 1109 } 1110 catch (final Exception e) 1111 { 1112 Debug.debugException(e); 1113 throw new LDAPException(ResultCode.LOCAL_ERROR, 1114 ERR_LDIF_DIFF_CANNOT_OPEN_LDIF_FILE.get( 1115 StaticUtils.getExceptionMessage(e)), 1116 e); 1117 } 1118 1119 try 1120 { 1121 final ObjectPair<InputStream,char[]> p = 1122 ToolUtils.getPossiblyPassphraseEncryptedInputStream(inputStream, 1123 encryptionPassphrases, (encPWFile == null), 1124 INFO_LDIF_DIFF_PROMPT_FOR_ENC_PW.get(ldifFile.getName()), 1125 ERR_LDIF_DIFF_PROMPT_WRONG_ENC_PW.get(), getOut(), getErr()); 1126 inputStream = p.getFirst(); 1127 addPassphrase(p.getSecond()); 1128 } 1129 catch (final Exception e) 1130 { 1131 Debug.debugException(e); 1132 throw new LDAPException(ResultCode.LOCAL_ERROR, 1133 ERR_LDIF_DIFF_CANNOT_DECRYPT_LDIF_FILE.get( 1134 StaticUtils.getExceptionMessage(e)), 1135 e); 1136 } 1137 1138 try 1139 { 1140 inputStream = 1141 ToolUtils.getPossiblyGZIPCompressedInputStream(inputStream); 1142 } 1143 catch (final Exception e) 1144 { 1145 Debug.debugException(e); 1146 throw new LDAPException(ResultCode.LOCAL_ERROR, 1147 ERR_LDIF_DIFF_CANNOT_DECOMPRESS_LDIF_FILE.get( 1148 StaticUtils.getExceptionMessage(e)), 1149 e); 1150 } 1151 1152 try (LDIFReader reader = new LDIFReader(inputStream)) 1153 { 1154 reader.setSchema(schema); 1155 if (stripTrailingSpaces.isPresent()) 1156 { 1157 reader.setTrailingSpaceBehavior(TrailingSpaceBehavior.STRIP); 1158 } 1159 else 1160 { 1161 reader.setTrailingSpaceBehavior(TrailingSpaceBehavior.REJECT); 1162 } 1163 1164 final TreeMap<DN,Entry> entryMap = new TreeMap<>(); 1165 while (true) 1166 { 1167 final Entry entry = reader.readEntry(); 1168 if (entry == null) 1169 { 1170 break; 1171 } 1172 1173 entryMap.put(entry.getParsedDN(), entry); 1174 } 1175 1176 return entryMap; 1177 } 1178 catch (final Exception e) 1179 { 1180 Debug.debugException(e); 1181 throw new LDAPException(ResultCode.LOCAL_ERROR, 1182 ERR_LDIF_DIFF_ERROR_READING_OR_DECODING.get( 1183 StaticUtils.getExceptionMessage(e)), 1184 e); 1185 } 1186 } 1187 finally 1188 { 1189 if (inputStream != null) 1190 { 1191 try 1192 { 1193 inputStream.close(); 1194 } 1195 catch (final Exception e) 1196 { 1197 Debug.debugException(e); 1198 } 1199 } 1200 } 1201 } 1202 1203 1204 1205 /** 1206 * Updates the list of encryption passphrases with the provided passphrase, if 1207 * it is not already present. 1208 * 1209 * @param passphrase The passphrase to be added. It may optionally be 1210 * {@code null} (in which case no action will be taken). 1211 */ 1212 private void addPassphrase(@Nullable final char[] passphrase) 1213 { 1214 if (passphrase == null) 1215 { 1216 return; 1217 } 1218 1219 for (final char[] existingPassphrase : encryptionPassphrases) 1220 { 1221 if (Arrays.equals(existingPassphrase, passphrase)) 1222 { 1223 return; 1224 } 1225 } 1226 1227 encryptionPassphrases.add(passphrase); 1228 } 1229 1230 1231 1232 /** 1233 * Opens the output stream to use to write the identified differences. 1234 * 1235 * @return The output stream that was opened. 1236 * 1237 * @throws LDAPException If a problem is encountered while opening the 1238 * output stream. 1239 */ 1240 @NotNull() 1241 private OutputStream openOutputStream() 1242 throws LDAPException 1243 { 1244 if (! outputLDIF.isPresent()) 1245 { 1246 return getOut(); 1247 } 1248 1249 OutputStream outputStream = null; 1250 boolean closeOutputStream = true; 1251 try 1252 { 1253 try 1254 { 1255 1256 outputStream = new FileOutputStream(outputLDIF.getValue(), 1257 (! overwriteExistingOutputLDIF.isPresent())); 1258 } 1259 catch (final Exception e) 1260 { 1261 Debug.debugException(e); 1262 throw new LDAPException(ResultCode.LOCAL_ERROR, 1263 ERR_LDIF_DIFF_CANNOT_OPEN_OUTPUT_FILE.get( 1264 StaticUtils.getExceptionMessage(e)), 1265 e); 1266 } 1267 1268 if (encryptOutput.isPresent()) 1269 { 1270 try 1271 { 1272 final char[] passphrase; 1273 if (outputEncryptionPassphraseFile.isPresent()) 1274 { 1275 passphrase = getPasswordFileReader().readPassword( 1276 outputEncryptionPassphraseFile.getValue()); 1277 } 1278 else 1279 { 1280 passphrase = ToolUtils.promptForEncryptionPassphrase(false, true, 1281 INFO_LDIF_DIFF_PROMPT_OUTPUT_FILE_ENC_PW.get(), 1282 INFO_LDIF_DIFF_CONFIRM_OUTPUT_FILE_ENC_PW.get(), getOut(), 1283 getErr()).toCharArray(); 1284 } 1285 1286 outputStream = new PassphraseEncryptedOutputStream(passphrase, 1287 outputStream, null, true, true); 1288 } 1289 catch (final Exception e) 1290 { 1291 Debug.debugException(e); 1292 throw new LDAPException(ResultCode.LOCAL_ERROR, 1293 ERR_LDIF_DIFF_CANNOT_ENCRYPT_OUTPUT_FILE.get( 1294 StaticUtils.getExceptionMessage(e)), 1295 e); 1296 } 1297 } 1298 1299 if (compressOutput.isPresent()) 1300 { 1301 try 1302 { 1303 outputStream = new GZIPOutputStream(outputStream); 1304 } 1305 catch (final Exception e) 1306 { 1307 Debug.debugException(e); 1308 throw new LDAPException(ResultCode.LOCAL_ERROR, 1309 ERR_LDIF_DIFF_CANNOT_COMPRESS_OUTPUT_FILE.get( 1310 StaticUtils.getExceptionMessage(e)), 1311 e); 1312 } 1313 } 1314 1315 closeOutputStream = false; 1316 return outputStream; 1317 } 1318 finally 1319 { 1320 if (closeOutputStream && (outputStream != null)) 1321 { 1322 try 1323 { 1324 outputStream.close(); 1325 } 1326 catch (final Exception e) 1327 { 1328 Debug.debugException(e); 1329 } 1330 } 1331 } 1332 } 1333 1334 1335 1336 /** 1337 * Writes add change records for all entries contained in the given target 1338 * entry map that are not in the source entry map. 1339 * 1340 * @param sourceEntries The map of entries read from the source LDIF file. 1341 * It must not be {@code null}. 1342 * @param targetEntries The map of entries read from the target LDIF file. 1343 * It must not be {@code null}. 1344 * @param writer The LDIF writer to use to write any changes. It 1345 * must not be {@code null} and it must be open. 1346 * @param schema The schema to use to identify operational 1347 * attributes. It must not be {@code null}. 1348 * @param includeAttrs A set containing all names and OIDs for all 1349 * attribute types that should be included in the 1350 * entry. It must not be {@code null} but may be 1351 * empty. All values must be formatted entirely in 1352 * lowercase. 1353 * @param excludeAttrs A set containing all names and OIDs for all 1354 * attribute types that should be excluded from the 1355 * entry. It must not be {@code null} but may be 1356 * empty. All values must be formatted entirely in 1357 * lowercase. 1358 * 1359 * @return The number of added entries that were identified during 1360 * processing. 1361 * 1362 * @throws LDAPException If a problem is encountered while writing the ad 1363 * change records. 1364 */ 1365 private long writeAdds(@NotNull final TreeMap<DN,Entry> sourceEntries, 1366 @NotNull final TreeMap<DN,Entry> targetEntries, 1367 @NotNull final LDIFWriter writer, 1368 @NotNull final Schema schema, 1369 @NotNull final Set<String> includeAttrs, 1370 @NotNull final Set<String> excludeAttrs) 1371 throws LDAPException 1372 { 1373 long addCount = 0L; 1374 1375 for (final Map.Entry<DN,Entry> e : targetEntries.entrySet()) 1376 { 1377 final DN entryDN = e.getKey(); 1378 final Entry entry = e.getValue(); 1379 if (! sourceEntries.containsKey(entryDN)) 1380 { 1381 if (! includeEntryByFilter(schema, entry)) 1382 { 1383 continue; 1384 } 1385 1386 final Entry paredEntry = pareEntry(entry, schema, includeAttrs, 1387 excludeAttrs); 1388 if (paredEntry == null) 1389 { 1390 continue; 1391 } 1392 1393 try 1394 { 1395 writer.writeChangeRecord(new LDIFAddChangeRecord(paredEntry), 1396 INFO_LDIF_DIFF_ADD_COMMENT.get()); 1397 addCount++; 1398 } 1399 catch (final Exception ex) 1400 { 1401 Debug.debugException(ex); 1402 throw new LDAPException(ResultCode.LOCAL_ERROR, 1403 ERR_LDIF_DIFF_CANNOT_WRITE_ADD_FOR_ENTRY.get(entry.getDN(), 1404 StaticUtils.getExceptionMessage(ex)), 1405 ex); 1406 } 1407 } 1408 } 1409 1410 return addCount; 1411 } 1412 1413 1414 1415 /** 1416 * Indicates whether the specified entry may be included in the output based 1417 * on the include filter and exclude filter configuration. 1418 * 1419 * @param schema The schema to use when making the determination. It must 1420 * not be {@code null}. 1421 * @param entries The entries for which to make the determination. It must 1422 * not be {@code null} or empty. 1423 * 1424 * @return {@code true} if the entry should be included, or {@code false} if] 1425 * not. 1426 */ 1427 private boolean includeEntryByFilter(@NotNull final Schema schema, 1428 @NotNull final Entry... entries) 1429 { 1430 for (final Entry entry : entries) 1431 { 1432 for (final Filter f : excludeFilter.getValues()) 1433 { 1434 try 1435 { 1436 if (f.matchesEntry(entry, schema)) 1437 { 1438 return false; 1439 } 1440 } 1441 catch (final Exception ex) 1442 { 1443 Debug.debugException(ex); 1444 } 1445 } 1446 } 1447 1448 if (includeFilter.isPresent()) 1449 { 1450 for (final Entry entry : entries) 1451 { 1452 for (final Filter f : includeFilter.getValues()) 1453 { 1454 try 1455 { 1456 if (f.matchesEntry(entry, schema)) 1457 { 1458 return true; 1459 } 1460 } 1461 catch (final Exception e) 1462 { 1463 Debug.debugException(e); 1464 } 1465 } 1466 } 1467 1468 return false; 1469 } 1470 1471 return true; 1472 } 1473 1474 1475 1476 /** 1477 * Creates a pared-down copy of the provided entry based on the requested 1478 * set of options. 1479 * 1480 * @param entry The entry to be pared down. It must not be 1481 * {@code null}. 1482 * @param schema The schema to use during processing. It must not be 1483 * {@code null}. 1484 * @param includeAttrs A set containing all names and OIDs for all attribute 1485 * types that should be included in the entry. It must 1486 * not be {@code null} but may be empty. All values 1487 * must be formatted entirely in lowercase. 1488 * @param excludeAttrs A set containing all names and OIDs for all attribute 1489 * types that should be excluded from the entry. It 1490 * must not be {@code null} but may be empty. All 1491 * values must be formatted entirely in lowercase. 1492 * 1493 * @return A pared-down copy of the provided entry, or {@code null} if the 1494 * pared-down entry would not include any attributes. 1495 */ 1496 @Nullable() 1497 private Entry pareEntry(@NotNull final Entry entry, 1498 @NotNull final Schema schema, 1499 @NotNull final Set<String> includeAttrs, 1500 @NotNull final Set<String> excludeAttrs) 1501 { 1502 final List<Attribute> paredAttributeList = new ArrayList<>(); 1503 for (final Attribute a : entry.getAttributes()) 1504 { 1505 final String baseName = StaticUtils.toLowerCase(a.getBaseName()); 1506 if (excludeAttrs.contains(baseName)) 1507 { 1508 continue; 1509 } 1510 1511 if ((! includeAttrs.isEmpty()) && (! includeAttrs.contains(baseName))) 1512 { 1513 continue; 1514 } 1515 1516 final AttributeTypeDefinition at = schema.getAttributeType(baseName); 1517 if ((at != null) && at.isOperational()) 1518 { 1519 if (! includeOperationalAttributes.isPresent()) 1520 { 1521 continue; 1522 } 1523 1524 if (at.isNoUserModification() && 1525 excludeNoUserModificationAttributes.isPresent()) 1526 { 1527 continue; 1528 } 1529 } 1530 1531 paredAttributeList.add(a); 1532 } 1533 1534 if (paredAttributeList.isEmpty()) 1535 { 1536 return null; 1537 } 1538 1539 return new Entry(entry.getDN(), paredAttributeList); 1540 } 1541 1542 1543 1544 /** 1545 * Identifies entries that exist in both the source and target maps and 1546 * determines whether there are any changes between them. 1547 * 1548 * @param sourceEntries The map of entries read from the source LDIF file. 1549 * It must not be {@code null}. 1550 * @param targetEntries The map of entries read from the target LDIF file. 1551 * It must not be {@code null}. 1552 * @param writer The LDIF writer to use to write any changes. It 1553 * must not be {@code null} and it must be open. 1554 * @param schema The schema to use to identify operational 1555 * attributes. It must not be {@code null}. 1556 * @param includeAttrs A set containing all names and OIDs for all 1557 * attribute types that should be included in the 1558 * set of modifications. It must not be {@code null} 1559 * but may be empty. All values must be formatted 1560 * entirely in lowercase. 1561 * @param excludeAttrs A set containing all names and OIDs for all 1562 * attribute types that should be excluded from the 1563 * set of modifications. It must not be {@code null} 1564 * but may be empty. All values must be formatted 1565 * entirely in lowercase. 1566 * 1567 * @return The number of modified entries that were identified during 1568 * processing. 1569 * 1570 * @throws LDAPException If a problem is encountered while writing 1571 * modified entries. 1572 */ 1573 private long writeModifications( 1574 @NotNull final TreeMap<DN,Entry> sourceEntries, 1575 @NotNull final TreeMap<DN,Entry> targetEntries, 1576 @NotNull final LDIFWriter writer, 1577 @NotNull final Schema schema, 1578 @NotNull final Set<String> includeAttrs, 1579 @NotNull final Set<String> excludeAttrs) 1580 throws LDAPException 1581 { 1582 long modCount = 0L; 1583 1584 for (final Map.Entry<DN,Entry> sourceMapEntry : sourceEntries.entrySet()) 1585 { 1586 final DN sourceDN = sourceMapEntry.getKey(); 1587 1588 final Entry targetEntry = targetEntries.get(sourceDN); 1589 if (targetEntry == null) 1590 { 1591 continue; 1592 } 1593 1594 final Entry sourceEntry = sourceMapEntry.getValue(); 1595 1596 if (! includeEntryByFilter(schema, sourceEntry, targetEntry)) 1597 { 1598 continue; 1599 } 1600 1601 final List<Modification> mods = Entry.diff(sourceEntry, targetEntry, 1602 false, (! nonReversibleModifications.isPresent()), 1603 byteForByte.isPresent()); 1604 if (writeModifiedEntry(sourceDN, mods, writer, schema, includeAttrs, 1605 excludeAttrs)) 1606 { 1607 modCount++; 1608 } 1609 } 1610 1611 return modCount; 1612 } 1613 1614 1615 1616 /** 1617 * Writes a modified entry to the LDIF writer. 1618 * 1619 * @param dn The DN of the entry to write. It must not be 1620 * {@code null}. 1621 * @param mods The modifications to be written. 1622 * @param writer The LDIF writer to use to write the modify change 1623 * record(s). 1624 * @param schema The schema to use to identify operational attributes. 1625 * It must not be {@code null}. 1626 * @param includeAttrs A set containing all names and OIDs for all attribute 1627 * types that should be included in the set of 1628 * modifications. It must not be {@code null} but may 1629 * be empty. All values must be formatted entirely in 1630 * lowercase. 1631 * @param excludeAttrs A set containing all names and OIDs for all attribute 1632 * types that should be excluded from the set of 1633 * modifications. It must not be {@code null} but may 1634 * be empty. All values must be formatted entirely in 1635 * lowercase. 1636 * 1637 * @return {@code true} if one or more modify change records were written, or 1638 * {@code false} if not. 1639 * 1640 * @throws LDAPException If a problem occurs while trying to write the 1641 * modifications. 1642 */ 1643 private boolean writeModifiedEntry(@NotNull final DN dn, 1644 @NotNull final List<Modification> mods, 1645 @NotNull final LDIFWriter writer, 1646 @NotNull final Schema schema, 1647 @NotNull final Set<String> includeAttrs, 1648 @NotNull final Set<String> excludeAttrs) 1649 throws LDAPException 1650 { 1651 if (mods.isEmpty()) 1652 { 1653 return false; 1654 } 1655 1656 final List<Modification> paredMods = new ArrayList<>(mods.size()); 1657 for (final Modification m : mods) 1658 { 1659 final Attribute a = m.getAttribute(); 1660 final String baseName = StaticUtils.toLowerCase(a.getBaseName()); 1661 if (excludeAttrs.contains(baseName)) 1662 { 1663 continue; 1664 } 1665 1666 if ((! includeAttrs.isEmpty()) && ! includeAttrs.contains(baseName)) 1667 { 1668 continue; 1669 } 1670 1671 final AttributeTypeDefinition at = 1672 schema.getAttributeType(a.getBaseName()); 1673 if ((at != null) && at.isOperational()) 1674 { 1675 if (includeOperationalAttributes.isPresent()) 1676 { 1677 if (at.isNoUserModification()) 1678 { 1679 if (! excludeNoUserModificationAttributes.isPresent()) 1680 { 1681 paredMods.add(m); 1682 } 1683 } 1684 else 1685 { 1686 paredMods.add(m); 1687 } 1688 } 1689 } 1690 else 1691 { 1692 paredMods.add(m); 1693 } 1694 } 1695 1696 if (paredMods.isEmpty()) 1697 { 1698 return false; 1699 } 1700 1701 try 1702 { 1703 if (singleValueChanges.isPresent()) 1704 { 1705 for (final Modification m : paredMods) 1706 { 1707 final Attribute a = m.getAttribute(); 1708 if (a.size() > 1) 1709 { 1710 for (final byte[] value : a.getValueByteArrays()) 1711 { 1712 writer.writeChangeRecord(new LDIFModifyChangeRecord(dn.toString(), 1713 new Modification(m.getModificationType(), 1714 m.getAttributeName(), value))); 1715 } 1716 } 1717 else 1718 { 1719 writer.writeChangeRecord(new LDIFModifyChangeRecord(dn.toString(), 1720 m)); 1721 } 1722 } 1723 } 1724 else 1725 { 1726 writer.writeChangeRecord( 1727 new LDIFModifyChangeRecord(dn.toString(), paredMods), 1728 INFO_LDIF_DIFF_MODIFY_COMMENT.get()); 1729 } 1730 } 1731 catch (final Exception e) 1732 { 1733 Debug.debugException(e); 1734 throw new LDAPException(ResultCode.LOCAL_ERROR, 1735 ERR_LDIF_DIFF_CANNOT_WRITE_MODS_TO_ENTRY.get(dn.toString(), 1736 StaticUtils.getExceptionMessage(e)), 1737 e); 1738 } 1739 1740 return true; 1741 } 1742 1743 1744 1745 /** 1746 * Writes delete change records for all entries contained in the given source 1747 * entry map that are not in the target entry map. 1748 * 1749 * @param sourceEntries The map of entries read from the source LDIF file. 1750 * It must not be {@code null}. 1751 * @param targetEntries The map of entries read from the target LDIF file. 1752 * It must not be {@code null}. 1753 * @param writer The LDIF writer to use to write any changes. It 1754 * must not be {@code null} and it must be open. 1755 * @param schema The schema to use to identify operational 1756 * attributes. It must not be {@code null}. 1757 * @param includeAttrs A set containing all names and OIDs for all 1758 * attribute types that should be included in the 1759 * entry. It must not be {@code null} but may be 1760 * empty. All values must be formatted entirely in 1761 * lowercase. 1762 * @param excludeAttrs A set containing all names and OIDs for all 1763 * attribute types that should be excluded from the 1764 * entry. It must not be {@code null} but may be 1765 * empty. All values must be formatted entirely in 1766 * lowercase. 1767 * 1768 * @return The number of deleted entries that were identified during 1769 * processing. 1770 * 1771 * @throws LDAPException If a problem is encountered while writing the 1772 * delete change records. 1773 */ 1774 private long writeDeletes(@NotNull final TreeMap<DN,Entry> sourceEntries, 1775 @NotNull final TreeMap<DN,Entry> targetEntries, 1776 @NotNull final LDIFWriter writer, 1777 @NotNull final Schema schema, 1778 @NotNull final Set<String> includeAttrs, 1779 @NotNull final Set<String> excludeAttrs) 1780 throws LDAPException 1781 { 1782 long deleteCount = 0L; 1783 1784 for (final Map.Entry<DN,Entry> e : sourceEntries.descendingMap().entrySet()) 1785 { 1786 final DN entryDN = e.getKey(); 1787 final Entry entry = e.getValue(); 1788 if (! targetEntries.containsKey(entryDN)) 1789 { 1790 if (! includeEntryByFilter(schema, entry)) 1791 { 1792 continue; 1793 } 1794 1795 final Entry paredEntry = pareEntry(entry, schema, includeAttrs, 1796 excludeAttrs); 1797 if (paredEntry == null) 1798 { 1799 continue; 1800 } 1801 1802 try 1803 { 1804 final String comment = INFO_LDIF_DIFF_DELETE_COMMENT.get() + 1805 StaticUtils.EOL + paredEntry.toLDIFString(75); 1806 writer.writeChangeRecord( 1807 new LDIFDeleteChangeRecord(paredEntry.getDN()), comment); 1808 deleteCount++; 1809 } 1810 catch (final Exception ex) 1811 { 1812 Debug.debugException(ex); 1813 throw new LDAPException(ResultCode.LOCAL_ERROR, 1814 ERR_LDIF_DIFF_CANNOT_WRITE_DELETE_FOR_ENTRY.get(entry.getDN(), 1815 StaticUtils.getExceptionMessage(ex)), 1816 ex); 1817 } 1818 } 1819 } 1820 1821 return deleteCount; 1822 } 1823 1824 1825 1826 /** 1827 * Writes the provided message and sets it as the completion message. 1828 * 1829 * @param isError Indicates whether the message should be written to 1830 * standard error rather than standard output. 1831 * @param message The message to be written. 1832 */ 1833 private void logCompletionMessage(final boolean isError, 1834 @NotNull final String message) 1835 { 1836 completionMessage.compareAndSet(null, message); 1837 1838 if (isError) 1839 { 1840 wrapErr(0, WRAP_COLUMN, message); 1841 } 1842 else 1843 { 1844 wrapOut(0, WRAP_COLUMN, message); 1845 } 1846 } 1847 1848 1849 1850 /** 1851 * {@inheritDoc} 1852 */ 1853 @Override() 1854 @NotNull() 1855 public LinkedHashMap<String[],String> getExampleUsages() 1856 { 1857 final LinkedHashMap<String[],String> examples = new LinkedHashMap<>(); 1858 1859 examples.put( 1860 new String[] 1861 { 1862 "--sourceLDIF", "actual.ldif", 1863 "--targetLDIF", "desired.ldif", 1864 "--outputLDIF", "diff.ldif" 1865 }, 1866 INFO_LDIF_DIFF_EXAMPLE_1.get()); 1867 1868 examples.put( 1869 new String[] 1870 { 1871 "--sourceLDIF", "actual.ldif", 1872 "--targetLDIF", "desired.ldif", 1873 "--outputLDIF", "diff.ldif", 1874 "--includeOperationalAttributes", 1875 "--excludeNoUserModificationAttributes", 1876 "--nonReversibleModifications" 1877 }, 1878 INFO_LDIF_DIFF_EXAMPLE_2.get()); 1879 1880 return examples; 1881 } 1882}