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