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