001/* 002 * Copyright 2021-2023 Ping Identity Corporation 003 * All Rights Reserved. 004 */ 005/* 006 * Copyright 2021-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) 2021-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.ldap.sdk.unboundidds.tools; 037 038 039 040import java.io.File; 041import java.io.FileInputStream; 042import java.io.FileOutputStream; 043import java.io.IOException; 044import java.io.OutputStream; 045import java.lang.reflect.Method; 046import java.util.ArrayList; 047import java.util.Arrays; 048import java.util.Iterator; 049import java.util.LinkedHashMap; 050import java.util.List; 051import java.util.TreeSet; 052import java.util.concurrent.TimeUnit; 053import java.util.concurrent.atomic.AtomicReference; 054 055import com.unboundid.ldap.sdk.ChangeType; 056import com.unboundid.ldap.sdk.DN; 057import com.unboundid.ldap.sdk.Filter; 058import com.unboundid.ldap.sdk.InternalSDKHelper; 059import com.unboundid.ldap.sdk.LDAPConnectionOptions; 060import com.unboundid.ldap.sdk.LDAPConnectionPool; 061import com.unboundid.ldap.sdk.LDAPException; 062import com.unboundid.ldap.sdk.ResultCode; 063import com.unboundid.ldap.sdk.SearchResultEntry; 064import com.unboundid.ldap.sdk.SearchScope; 065import com.unboundid.ldap.sdk.Version; 066import com.unboundid.ldap.sdk.schema.Schema; 067import com.unboundid.ldap.sdk.unboundidds.extensions. 068 StreamDirectoryValuesExtendedRequest; 069import com.unboundid.ldif.LDIFAddChangeRecord; 070import com.unboundid.ldif.LDIFDeleteChangeRecord; 071import com.unboundid.ldif.LDIFModifyChangeRecord; 072import com.unboundid.ldif.LDIFWriter; 073import com.unboundid.util.Debug; 074import com.unboundid.util.LDAPSDKThreadFactory; 075import com.unboundid.util.MultiServerLDAPCommandLineTool; 076import com.unboundid.util.NotNull; 077import com.unboundid.util.Nullable; 078import com.unboundid.util.StaticUtils; 079import com.unboundid.util.ThreadSafety; 080import com.unboundid.util.ThreadSafetyLevel; 081import com.unboundid.util.args.Argument; 082import com.unboundid.util.args.ArgumentException; 083import com.unboundid.util.args.ArgumentParser; 084import com.unboundid.util.args.BooleanArgument; 085import com.unboundid.util.args.DNArgument; 086import com.unboundid.util.args.FileArgument; 087import com.unboundid.util.args.FilterArgument; 088import com.unboundid.util.args.IntegerArgument; 089import com.unboundid.util.args.ScopeArgument; 090import com.unboundid.util.args.StringArgument; 091import com.unboundid.util.parallel.ParallelProcessor; 092import com.unboundid.util.parallel.Result; 093 094import static com.unboundid.ldap.sdk.unboundidds.tools.ToolMessages.*; 095 096 097 098/** 099 * This class provides a tool that can be used to compare the contents of two 100 * LDAPv3 servers and report the differences in an LDIF file that can be used to 101 * update the source server to match the target. It should work with any pair 102 * of LDAPv3 servers, including servers of different types. 103 * <BR> 104 * <BLOCKQUOTE> 105 * <B>NOTE:</B> This class, and other classes within the 106 * {@code com.unboundid.ldap.sdk.unboundidds} package structure, are only 107 * supported for use against Ping Identity, UnboundID, and 108 * Nokia/Alcatel-Lucent 8661 server products. These classes provide support 109 * for proprietary functionality or for external specifications that are not 110 * considered stable or mature enough to be guaranteed to work in an 111 * interoperable way with other types of LDAP servers. 112 * </BLOCKQUOTE> 113 * <BR> 114 * This tool can be used to determine whether two LDAP replicas are in sync. It 115 * can also account for replication delay by checking differing entries multiple 116 * times. 117 * <BR><BR> 118 * At a minimum, the user must provide information needed to connect and 119 * authenticate to the two servers to compare, as well as the base DN below 120 * authenticate to the two servers to compare, as well as the base DN below 121 * which to search (note that the empty base DN is not supported). The user can 122 * optionally also specify a filter used to identify which entries should be 123 * compared. 124 * <BR><BR> 125 * This tool tries to compare the contents of both servers as quickly as 126 * possible while also maintaining a low memory overhead and eliminating false 127 * positives that result from entries that are temporarily out of sync as a 128 * result of replication latencies. It does this using the following approach: 129 * <UL> 130 * <LI> 131 * Retrieve the DNs from each server in parallel. For servers that 132 * advertise support for the {@link StreamDirectoryValuesExtendedRequest}, 133 * then that operation will be used to retrieve the DNs. Otherwise, a 134 * search will be used with the configured base DN, scope, and filter to 135 * retrieve all matching entries (without any attributes). 136 * </LI> 137 * <LI> 138 * For up to a configurable number of passes: 139 * <OL> 140 * <LI> 141 * Use a thread pool to iterate through all of the identified entry DNs, 142 * fetching and comparing each entry from both servers. By default, 143 * multiple threads will be used to perform the comparison as fast as 144 * possible, but this can be configured as needed to adjust the 145 * performance impact on the directory servers. 146 * </LI> 147 * <LI> 148 * If the version of the entry retrieved from each server is the same, 149 * then it is considered in sync and will not be compared again. If the 150 * entry differs between the source and target servers, and if there are 151 * no more passes to complete, then the differences will be computed and 152 * written in LDIF form to an output file. 153 * </LI> 154 * <LI> 155 * If any differing entries were identified, and if there are more 156 * passes remaining, then the tool will wait for a specified length of 157 * time before re-retrieving and re-comparing each of the entries that 158 * differed in the last pass. 159 * </LI> 160 * </OL> 161 * </LI> 162 * </UL> 163 * Note that even though the tool operates in parallel, it ensures that the 164 * differences are written to the output file in an appropriate order to ensure 165 * that they can be replayed. The tool keeps the adds, modifies, and deletes 166 * separate during processing and then joins them at the end in an appropriate 167 * order (with deletes in reverse order to ensure that children are removed 168 * before parents, followed by modifies, and finally adds). Intermediate files 169 * are used during processing to hold the add and modify records to minimize 170 * memory consumption. 171 * <BR><BR> 172 * Note that the accounts used to run this tool must be sufficiently privileged 173 * to perform the necessary processing, including being able to access all of 174 * the appropriate entries (and all relevant attributes in those entries) in 175 * each server. 176 */ 177@ThreadSafety(level=ThreadSafetyLevel.NOT_THREADSAFE) 178public final class LDAPDiff 179 extends MultiServerLDAPCommandLineTool 180{ 181 /** 182 * The column at which to wrap long lines. 183 */ 184 static final int WRAP_COLUMN = StaticUtils.TERMINAL_WIDTH_COLUMNS - 1; 185 186 187 188 /** 189 * The maximum number of entries to process in parallel in a batch. 190 */ 191 private static final int MAX_ENTRIES_PER_BATCH = 1_000; 192 193 194 195 /** 196 * The default value that will be used for the default bind DN if none is 197 * specified. 198 */ 199 @NotNull private static final String DEFAULT_BIND_DN = "cn=Directory Manager"; 200 201 202 203 /** 204 * The legacy version of the result code that will be used to indicate that 205 * an error occurred while processing command-line arguments for the tool. 206 */ 207 @NotNull private static final ResultCode LEGACY_EXIT_CODE_ARG_PARSING_ERROR = 208 ResultCode.PROTOCOL_ERROR; 209 210 211 212 /** 213 * The legacy version of the result code that will be used to indicate that 214 * all processing completed successfully, but that one or more differences 215 * were identified between the source and target servers. 216 */ 217 @NotNull private static final ResultCode LEGACY_EXIT_CODE_OUT_OF_SYNC = 218 ResultCode.TIME_LIMIT_EXCEEDED; 219 220 221 222 /** 223 * The legacy version of the result code that will be used to indicate that 224 * all processing completed successfully and no differences were identified 225 * between the source and target servers. 226 */ 227 @NotNull private static final ResultCode LEGACY_EXIT_CODE_SUCCESS = 228 ResultCode.SUCCESS; 229 230 231 232 /** 233 * The legacy version of the result code that will be used to indicate that 234 * an unexpected error occurred during processing. 235 */ 236 @NotNull private static final ResultCode LEGACY_EXIT_CODE_UNEXPECTED_ERROR = 237 ResultCode.OPERATIONS_ERROR; 238 239 240 241 // A reference to the tool completion message for this tool. 242 @NotNull private final AtomicReference<String> toolCompletionMessageRef; 243 244 // The argument parser used by this program. 245 @Nullable private ArgumentParser parser; 246 247 // Arguments to use when processing. 248 @Nullable private BooleanArgument byteForByteArg; 249 @Nullable private BooleanArgument missingOnlyArg; 250 @Nullable private BooleanArgument quietArg; 251 @Nullable private DNArgument baseDNArg; 252 @Nullable private DNArgument excludeBranchArg; 253 @Nullable private FileArgument outputLDIFArg; 254 @Nullable private FileArgument sourceDNsFileArg; 255 @Nullable private FileArgument targetDNsFileArg; 256 @Nullable private FilterArgument searchFilterArg; 257 @Nullable private IntegerArgument numPassesArg; 258 @Nullable private IntegerArgument numThreadsArg; 259 @Nullable private IntegerArgument secondsBetweenPassesArg; 260 @Nullable private IntegerArgument wrapColumnArg; 261 @Nullable private ScopeArgument searchScopeArg; 262 263 // Legacy arguments used only to provide compatibility with an older version 264 // of this tool. 265 @Nullable private BooleanArgument legacyTrustAllArg; 266 @Nullable private BooleanArgument useLegacyExitCodeArg; 267 @Nullable private DNArgument legacySourceBindDNArg; 268 @Nullable private FileArgument legacyKeyStorePathArg; 269 @Nullable private FileArgument legacyKeyStorePasswordFileArg; 270 @Nullable private FileArgument legacyTargetBindPasswordFileArg; 271 @Nullable private FileArgument legacyTrustStorePathArg; 272 @Nullable private FileArgument legacyTrustStorePasswordFileArg; 273 @Nullable private IntegerArgument legacySourcePortArg; 274 @Nullable private StringArgument legacyCertNicknameArg; 275 @Nullable private StringArgument legacyKeyStoreFormatArg; 276 @Nullable private StringArgument legacyKeyStorePasswordArg; 277 @Nullable private StringArgument legacySourceBindPasswordArg; 278 @Nullable private StringArgument legacySourceHostArg; 279 @Nullable private StringArgument legacyTargetHostArg; 280 @Nullable private StringArgument legacyTrustStoreFormatArg; 281 @Nullable private StringArgument legacyTrustStorePasswordArg; 282 283 284 285 /** 286 * Invokes this tool using the provided set of command-line arguments. 287 * 288 * @param args The command-line arguments provided to this program. It must 289 * not be {@code null} or empty. 290 */ 291 public static void main(@NotNull final String... args) 292 { 293 final ResultCode resultCode = main(System.out, System.err, args); 294 if (resultCode != ResultCode.SUCCESS) 295 { 296 System.exit(Math.min(resultCode.intValue(), 255)); 297 } 298 } 299 300 301 302 /** 303 * Invokes this tool using the provided set of command-line arguments. 304 * 305 * @param out The output stream to use for standard output. It may be 306 * {@code null} if standard output should be suppressed. 307 * @param err The output stream to use for standard error. It may be 308 * {@code null} if standard error should be suppressed. 309 * @param args The command-line arguments provided to this program. It must 310 * not be {@code null} or empty. 311 * 312 * @return A result code that indicates the result of tool processing. A 313 * result code of {@link ResultCode#SUCCESS} indicates that all 314 * processing completed successfully and no differences were 315 * identified. A result code of {@link ResultCode#COMPARE_FALSE} 316 * indicates that all processing completed successfully but that one 317 * or more differences were identified between the source and target 318 * servers. Any other result code indicates that an error occurred 319 * during processing. 320 */ 321 @NotNull() 322 public static ResultCode main(@Nullable final OutputStream out, 323 @Nullable final OutputStream err, 324 @NotNull final String... args) 325 { 326 final LDAPDiff ldapDiff = new LDAPDiff(out, err); 327 328 ResultCode resultCode = ldapDiff.runTool(args); 329 if ((ldapDiff.useLegacyExitCodeArg != null) && 330 (ldapDiff.useLegacyExitCodeArg.isPresent())) 331 { 332 switch (resultCode.intValue()) 333 { 334 case ResultCode.SUCCESS_INT_VALUE: 335 resultCode = LEGACY_EXIT_CODE_SUCCESS; 336 break; 337 case ResultCode.COMPARE_FALSE_INT_VALUE: 338 resultCode = LEGACY_EXIT_CODE_OUT_OF_SYNC; 339 break; 340 case ResultCode.PARAM_ERROR_INT_VALUE: 341 resultCode = LEGACY_EXIT_CODE_ARG_PARSING_ERROR; 342 break; 343 default: 344 resultCode = LEGACY_EXIT_CODE_UNEXPECTED_ERROR; 345 break; 346 } 347 } 348 349 return resultCode; 350 } 351 352 353 354 /** 355 * Creates a new instance of this tool with the provided information. 356 * 357 * @param out The output stream to use for standard output. It may be 358 * {@code null} if standard output should be suppressed. 359 * @param err The output stream to use for standard error. It may be 360 * {@code null} if standard error should be suppressed. 361 */ 362 public LDAPDiff(@Nullable final OutputStream out, 363 @Nullable final OutputStream err) 364 { 365 super(out, err, new String[] { "source", "target" }, null); 366 367 toolCompletionMessageRef = new AtomicReference<>(); 368 369 parser = null; 370 371 missingOnlyArg = null; 372 quietArg = null; 373 baseDNArg = null; 374 excludeBranchArg = null; 375 outputLDIFArg = null; 376 sourceDNsFileArg = null; 377 targetDNsFileArg = null; 378 searchFilterArg = null; 379 numPassesArg = null; 380 numThreadsArg = null; 381 secondsBetweenPassesArg = null; 382 wrapColumnArg = null; 383 searchScopeArg = null; 384 385 legacyTrustAllArg = null; 386 useLegacyExitCodeArg = null; 387 legacySourceBindDNArg = null; 388 legacyKeyStorePathArg = null; 389 legacyKeyStorePasswordFileArg = null; 390 legacyTargetBindPasswordFileArg = null; 391 legacyTrustStorePathArg = null; 392 legacyTrustStorePasswordFileArg = null; 393 legacySourcePortArg = null; 394 legacyCertNicknameArg = null; 395 legacyKeyStoreFormatArg = null; 396 legacyKeyStorePasswordArg = null; 397 legacySourceBindPasswordArg = null; 398 legacySourceHostArg = null; 399 legacyTargetHostArg = null; 400 legacyTrustStoreFormatArg = null; 401 legacyTrustStorePasswordArg = null; 402 } 403 404 405 406 /** 407 * {@inheritDoc} 408 */ 409 @Override() 410 @NotNull() 411 public String getToolName() 412 { 413 return "ldap-diff"; 414 } 415 416 417 418 /** 419 * {@inheritDoc} 420 */ 421 @Override() 422 @NotNull() 423 public String getToolDescription() 424 { 425 return INFO_LDAP_DIFF_TOOL_DESCRIPTION_1.get(); 426 } 427 428 429 430 /** 431 * {@inheritDoc} 432 */ 433 @Override() 434 @NotNull() 435 public List<String> getAdditionalDescriptionParagraphs() 436 { 437 final File pingIdentityServerRoot = 438 InternalSDKHelper.getPingIdentityServerRoot(); 439 if (pingIdentityServerRoot == null) 440 { 441 return Arrays.asList( 442 INFO_LDAP_DIFF_TOOL_DESCRIPTION_2.get(), 443 INFO_LDAP_DIFF_TOOL_DESCRIPTION_3.get(), 444 INFO_LDAP_DIFF_TOOL_DESCRIPTION_4_NON_PING_DS.get(), 445 INFO_LDAP_DIFF_TOOL_DESCRIPTION_5_NON_PING_DS.get()); 446 } 447 else 448 { 449 return Arrays.asList( 450 INFO_LDAP_DIFF_TOOL_DESCRIPTION_2.get(), 451 INFO_LDAP_DIFF_TOOL_DESCRIPTION_3.get(), 452 INFO_LDAP_DIFF_TOOL_DESCRIPTION_4_PING_DS.get(), 453 INFO_LDAP_DIFF_TOOL_DESCRIPTION_5_PING_DS.get()); 454 } 455 } 456 457 458 459 /** 460 * {@inheritDoc} 461 */ 462 @Override() 463 @NotNull() 464 public String getToolVersion() 465 { 466 return Version.NUMERIC_VERSION_STRING; 467 } 468 469 470 471 /** 472 * {@inheritDoc} 473 */ 474 @Override() 475 @NotNull() 476 public LDAPConnectionOptions getConnectionOptions() 477 { 478 final LDAPConnectionOptions options = new LDAPConnectionOptions(); 479 options.setUseSynchronousMode(true); 480 options.setUsePooledSchema(true); 481 return options; 482 } 483 484 485 486 /** 487 * {@inheritDoc} 488 */ 489 @Override() 490 public int getMinTrailingArguments() 491 { 492 return 0; 493 } 494 495 496 497 /** 498 * {@inheritDoc} 499 */ 500 @Override() 501 public int getMaxTrailingArguments() 502 { 503 return Integer.MAX_VALUE; 504 } 505 506 507 508 /** 509 * {@inheritDoc} 510 */ 511 @Override() 512 @NotNull() 513 public String getTrailingArgumentsPlaceholder() 514 { 515 return INFO_LDAP_DIFF_TRAILING_ARGS_PLACEHOLDER.get(); 516 } 517 518 519 520 /** 521 * {@inheritDoc} 522 */ 523 @Override() 524 protected boolean includeAlternateLongIdentifiers() 525 { 526 return true; 527 } 528 529 530 531 /** 532 * {@inheritDoc} 533 */ 534 @Override() 535 public void addNonLDAPArguments(@NotNull final ArgumentParser parser) 536 throws ArgumentException 537 { 538 this.parser = parser; 539 540 // Add the general arguments. 541 baseDNArg = new DNArgument('b', "baseDN", true, 1, 542 INFO_LDAP_DIFF_ARG_PLACEHOLDER_BASE_DN.get(), 543 INFO_LDAP_DIFF_ARG_DESC_BASE_DN.get()); 544 baseDNArg.addLongIdentifier("base-dn", true); 545 baseDNArg.setArgumentGroupName( 546 INFO_LDAP_DIFF_ARG_GROUP_PROCESSING_ARGS.get()); 547 parser.addArgument(baseDNArg); 548 549 sourceDNsFileArg = new FileArgument(null, "sourceDNsFile", false, 1, null, 550 INFO_LDAP_DIFF_ARG_DESC_SOURCE_DNS_FILE.get(), true, true, true, 551 false); 552 sourceDNsFileArg.addLongIdentifier("source-dns-file", true); 553 sourceDNsFileArg.addLongIdentifier("sourceDNFile", true); 554 sourceDNsFileArg.addLongIdentifier("source-dn-file", true); 555 sourceDNsFileArg.setArgumentGroupName( 556 INFO_LDAP_DIFF_ARG_GROUP_PROCESSING_ARGS.get()); 557 parser.addArgument(sourceDNsFileArg); 558 559 targetDNsFileArg = new FileArgument(null, "targetDNsFile", false, 1, null, 560 INFO_LDAP_DIFF_ARG_DESC_TARGET_DNS_FILE.get(), true, true, true, 561 false); 562 targetDNsFileArg.addLongIdentifier("target-dns-file", true); 563 targetDNsFileArg.addLongIdentifier("targetDNFile", true); 564 targetDNsFileArg.addLongIdentifier("target-dn-file", true); 565 targetDNsFileArg.setArgumentGroupName( 566 INFO_LDAP_DIFF_ARG_GROUP_PROCESSING_ARGS.get()); 567 parser.addArgument(targetDNsFileArg); 568 569 excludeBranchArg = new DNArgument('B', "excludeBranch", false, 0, null, 570 INFO_LDAP_DIFF_ARG_DESC_EXCLUDE_BRANCH.get()); 571 excludeBranchArg.addLongIdentifier("exclude-branch", true); 572 excludeBranchArg.setArgumentGroupName( 573 INFO_LDAP_DIFF_ARG_GROUP_PROCESSING_ARGS.get()); 574 parser.addArgument(excludeBranchArg); 575 576 searchFilterArg = new FilterArgument('f', "searchFilter", false, 1, null, 577 INFO_LDAP_DIFF_ARG_DESC_FILTER.get(), 578 Filter.createPresenceFilter("objectClass")); 579 searchFilterArg.addLongIdentifier("search-filter", true); 580 searchFilterArg.addLongIdentifier("filter", true); 581 searchFilterArg.setArgumentGroupName( 582 INFO_LDAP_DIFF_ARG_GROUP_PROCESSING_ARGS.get()); 583 parser.addArgument(searchFilterArg); 584 585 searchScopeArg = new ScopeArgument('s', "searchScope", false, null, 586 INFO_LDAP_DIFF_ARG_DESC_SCOPE.get(), SearchScope.SUB); 587 searchScopeArg.addLongIdentifier("search-scope", true); 588 searchScopeArg.addLongIdentifier("scope", true); 589 searchScopeArg.setArgumentGroupName( 590 INFO_LDAP_DIFF_ARG_GROUP_PROCESSING_ARGS.get()); 591 parser.addArgument(searchScopeArg); 592 593 outputLDIFArg = new FileArgument('o', "outputLDIF", true, 1, null, 594 INFO_LDAP_DIFF_ARG_DESC_OUTPUT_LDIF.get(), false, true, true, false); 595 outputLDIFArg.addLongIdentifier("output-ldif", true); 596 outputLDIFArg.addLongIdentifier("outputFile", true); 597 outputLDIFArg.addLongIdentifier("output-file", true); 598 outputLDIFArg.setArgumentGroupName( 599 INFO_LDAP_DIFF_ARG_GROUP_PROCESSING_ARGS.get()); 600 parser.addArgument(outputLDIFArg); 601 602 wrapColumnArg = new IntegerArgument(null, "wrapColumn", false, 1, null, 603 INFO_LDAP_DIFF_ARG_DESC_WRAP_COLUMN.get(), 0, Integer.MAX_VALUE, 0); 604 wrapColumnArg.addLongIdentifier("wrap-column", true); 605 wrapColumnArg.setArgumentGroupName( 606 INFO_LDAP_DIFF_ARG_GROUP_PROCESSING_ARGS.get()); 607 parser.addArgument(wrapColumnArg); 608 609 quietArg = new BooleanArgument('Q', "quiet", 1, 610 INFO_LDAP_DIFF_ARG_DESC_QUIET.get()); 611 quietArg.setArgumentGroupName( 612 INFO_LDAP_DIFF_ARG_GROUP_PROCESSING_ARGS.get()); 613 parser.addArgument(quietArg); 614 615 numThreadsArg = new IntegerArgument(null, "numThreads", false, 1, 616 null, INFO_LDAP_DIFF_ARG_DESC_NUM_THREADS.get(), 1, 617 Integer.MAX_VALUE, 20); 618 numThreadsArg.addLongIdentifier("num-threads", true); 619 numThreadsArg.addLongIdentifier("numConnections", true); 620 numThreadsArg.addLongIdentifier("num-connections", true); 621 numThreadsArg.setArgumentGroupName( 622 INFO_LDAP_DIFF_ARG_GROUP_PROCESSING_ARGS.get()); 623 parser.addArgument(numThreadsArg); 624 625 numPassesArg = new IntegerArgument(null, "numPasses", false, 1, null, 626 INFO_LDAP_DIFF_ARG_DESC_NUM_PASSES.get(), 1, Integer.MAX_VALUE, 3); 627 numPassesArg.addLongIdentifier("num-passes", true); 628 numPassesArg.addLongIdentifier("maxPasses", true); 629 numPassesArg.addLongIdentifier("max-passes", true); 630 numPassesArg.addLongIdentifier("maximum-Passes", true); 631 numPassesArg.addLongIdentifier("maximum-passes", true); 632 numPassesArg.addLongIdentifier("passes", true); 633 numPassesArg.setArgumentGroupName( 634 INFO_LDAP_DIFF_ARG_GROUP_PROCESSING_ARGS.get()); 635 parser.addArgument(numPassesArg); 636 637 secondsBetweenPassesArg = new IntegerArgument(null, "secondsBetweenPasses", 638 false, 1, null, INFO_LDAP_DIFF_ARG_DESC_SECONDS_BETWEEN_PASSES.get(), 639 0, Integer.MAX_VALUE, 2); 640 secondsBetweenPassesArg.addLongIdentifier("seconds-between-passes", true); 641 secondsBetweenPassesArg.addLongIdentifier("secondsBetweenPass", true); 642 secondsBetweenPassesArg.addLongIdentifier("seconds-between-pass", true); 643 secondsBetweenPassesArg.setArgumentGroupName( 644 INFO_LDAP_DIFF_ARG_GROUP_PROCESSING_ARGS.get()); 645 parser.addArgument(secondsBetweenPassesArg); 646 647 byteForByteArg = new BooleanArgument(null, "byteForByte", 1, 648 INFO_LDAP_DIFF_ARG_DESC_BYTE_FOR_BYTE.get()); 649 byteForByteArg.addLongIdentifier("byte-for-byte", true); 650 byteForByteArg.setArgumentGroupName( 651 INFO_LDAP_DIFF_ARG_GROUP_PROCESSING_ARGS.get()); 652 parser.addArgument(byteForByteArg); 653 654 missingOnlyArg = new BooleanArgument(null, "missingOnly", 1, 655 INFO_LDAP_DIFF_ARG_DESC_MISSING_ONLY.get()); 656 missingOnlyArg.addLongIdentifier("missing-only", true); 657 missingOnlyArg.addLongIdentifier("onlyMissing", true); 658 missingOnlyArg.addLongIdentifier("only-missing", true); 659 missingOnlyArg.setArgumentGroupName( 660 INFO_LDAP_DIFF_ARG_GROUP_PROCESSING_ARGS.get()); 661 parser.addArgument(missingOnlyArg); 662 663 664 // Add legacy arguments that will be used to help provide compatibility with 665 // an older version of this tool. 666 useLegacyExitCodeArg = new BooleanArgument(null, "useLegacyExitCode", 1, 667 INFO_LDAP_DIFF_ARG_DESC_USE_LEGACY_EXIT_CODE.get()); 668 useLegacyExitCodeArg.addLongIdentifier("use-legacy-exit-code", true); 669 useLegacyExitCodeArg.addLongIdentifier("useLegacyResultCode", true); 670 useLegacyExitCodeArg.addLongIdentifier("use-legacy-result-code", true); 671 useLegacyExitCodeArg.addLongIdentifier("legacyExitCode", true); 672 useLegacyExitCodeArg.addLongIdentifier("legacy-exit-code", true); 673 useLegacyExitCodeArg.addLongIdentifier("legacyResultCode", true); 674 useLegacyExitCodeArg.addLongIdentifier("legacy-result-code", true); 675 useLegacyExitCodeArg.setArgumentGroupName( 676 INFO_LDAP_DIFF_ARG_GROUP_PROCESSING_ARGS.get()); 677 parser.addArgument(useLegacyExitCodeArg); 678 679 680 legacySourceHostArg = new StringArgument('h', null, false, 1, null, ""); 681 legacySourceHostArg.setHidden(true); 682 parser.addArgument(legacySourceHostArg); 683 parser.addExclusiveArgumentSet(parser.getNamedArgument("sourceHostname"), 684 legacySourceHostArg); 685 686 legacySourcePortArg = new IntegerArgument('p', null, false, 1, null, "", 687 1, 65535); 688 legacySourcePortArg.setHidden(true); 689 parser.addArgument(legacySourcePortArg); 690 parser.addExclusiveArgumentSet(parser.getNamedArgument("sourcePort"), 691 legacySourcePortArg); 692 693 legacySourceBindDNArg = new DNArgument('D', null, false, 1, null, ""); 694 legacySourceBindDNArg.setHidden(true); 695 parser.addArgument(legacySourceBindDNArg); 696 parser.addExclusiveArgumentSet(parser.getNamedArgument("sourceBindDN"), 697 legacySourceBindDNArg); 698 699 legacySourceBindPasswordArg = 700 new StringArgument('w', null, false, 1, null, ""); 701 legacySourceBindPasswordArg.setHidden(true); 702 parser.addArgument(legacySourceBindPasswordArg); 703 parser.addExclusiveArgumentSet( 704 parser.getNamedArgument("sourceBindPassword"), 705 legacySourceBindPasswordArg); 706 707 legacyTargetHostArg = new StringArgument('O', null, false, 1, null, ""); 708 legacyTargetHostArg.setHidden(true); 709 parser.addArgument(legacyTargetHostArg); 710 parser.addExclusiveArgumentSet(parser.getNamedArgument("targetHostname"), 711 legacyTargetHostArg); 712 713 legacyTargetBindPasswordFileArg = new FileArgument('F', null, false, 1, 714 null, "", true, true, true, false); 715 legacyTargetBindPasswordFileArg.setHidden(true); 716 parser.addArgument(legacyTargetBindPasswordFileArg); 717 parser.addExclusiveArgumentSet( 718 parser.getNamedArgument("targetBindPasswordFile"), 719 legacyTargetBindPasswordFileArg); 720 721 legacyTrustAllArg = new BooleanArgument('X', "trustAll", 1, ""); 722 legacyTrustAllArg.setHidden(true); 723 parser.addArgument(legacyTrustAllArg); 724 parser.addExclusiveArgumentSet(parser.getNamedArgument("sourceTrustAll"), 725 legacyTrustAllArg); 726 parser.addExclusiveArgumentSet(parser.getNamedArgument("targetTrustAll"), 727 legacyTrustAllArg); 728 729 legacyKeyStorePathArg = new FileArgument('K', "keyStorePath", false, 1, 730 null, "", true, true, true, false); 731 legacyKeyStorePathArg.setHidden(true); 732 parser.addArgument(legacyKeyStorePathArg); 733 parser.addExclusiveArgumentSet( 734 parser.getNamedArgument("sourceKeyStorePath"), 735 legacyKeyStorePathArg); 736 parser.addExclusiveArgumentSet( 737 parser.getNamedArgument("targetKeyStorePath"), 738 legacyKeyStorePathArg); 739 740 legacyKeyStorePasswordArg = new StringArgument('W', "keyStorePassword", 741 false, 1, null, ""); 742 legacyKeyStorePasswordArg.setSensitive(true); 743 legacyKeyStorePasswordArg.setHidden(true); 744 parser.addArgument(legacyKeyStorePasswordArg); 745 parser.addExclusiveArgumentSet( 746 parser.getNamedArgument("sourceKeyStorePassword"), 747 legacyKeyStorePasswordArg); 748 parser.addExclusiveArgumentSet( 749 parser.getNamedArgument("targetKeyStorePassword"), 750 legacyKeyStorePasswordArg); 751 752 legacyKeyStorePasswordFileArg = new FileArgument('u', 753 "keyStorePasswordFile", false, 1, null, "", true, true, true, false); 754 legacyKeyStorePasswordFileArg.setHidden(true); 755 parser.addArgument(legacyKeyStorePasswordFileArg); 756 parser.addExclusiveArgumentSet( 757 parser.getNamedArgument("sourceKeyStorePasswordFile"), 758 legacyKeyStorePasswordFileArg); 759 parser.addExclusiveArgumentSet( 760 parser.getNamedArgument("targetKeyStorePasswordFile"), 761 legacyKeyStorePasswordFileArg); 762 763 legacyKeyStoreFormatArg = new StringArgument(null, "keyStoreFormat", false, 764 1, null, ""); 765 legacyKeyStoreFormatArg.setHidden(true); 766 parser.addArgument(legacyKeyStoreFormatArg); 767 parser.addExclusiveArgumentSet( 768 parser.getNamedArgument("sourceKeyStoreFormat"), 769 legacyKeyStoreFormatArg); 770 parser.addExclusiveArgumentSet( 771 parser.getNamedArgument("targetKeyStoreFormat"), 772 legacyKeyStoreFormatArg); 773 774 legacyCertNicknameArg = new StringArgument('N', "certNickname", false, 1, 775 null, ""); 776 legacyCertNicknameArg.setHidden(true); 777 parser.addArgument(legacyCertNicknameArg); 778 parser.addExclusiveArgumentSet( 779 parser.getNamedArgument("sourceCertNickname"), 780 legacyCertNicknameArg); 781 parser.addExclusiveArgumentSet( 782 parser.getNamedArgument("targetCertNickname"), 783 legacyCertNicknameArg); 784 785 legacyTrustStorePathArg = new FileArgument('P', "trustStorePath", false, 1, 786 null, "", true, true, true, false); 787 legacyTrustStorePathArg.setHidden(true); 788 parser.addArgument(legacyTrustStorePathArg); 789 parser.addExclusiveArgumentSet( 790 parser.getNamedArgument("sourceTrustStorePath"), 791 legacyTrustStorePathArg); 792 parser.addExclusiveArgumentSet( 793 parser.getNamedArgument("targetTrustStorePath"), 794 legacyTrustStorePathArg); 795 796 legacyTrustStorePasswordArg = new StringArgument(null, "trustStorePassword", 797 false, 1, null, ""); 798 legacyTrustStorePasswordArg.setSensitive(true); 799 legacyTrustStorePasswordArg.setHidden(true); 800 parser.addArgument(legacyTrustStorePasswordArg); 801 parser.addExclusiveArgumentSet( 802 parser.getNamedArgument("sourceTrustStorePassword"), 803 legacyTrustStorePasswordArg); 804 parser.addExclusiveArgumentSet( 805 parser.getNamedArgument("targetTrustStorePassword"), 806 legacyTrustStorePasswordArg); 807 808 legacyTrustStorePasswordFileArg = new FileArgument('U', 809 "trustStorePasswordFile", false, 1, null, "", true, true, true, false); 810 legacyTrustStorePasswordFileArg.setHidden(true); 811 parser.addArgument(legacyTrustStorePasswordFileArg); 812 parser.addExclusiveArgumentSet( 813 parser.getNamedArgument("sourceTrustStorePasswordFile"), 814 legacyTrustStorePasswordFileArg); 815 parser.addExclusiveArgumentSet( 816 parser.getNamedArgument("targetTrustStorePasswordFile"), 817 legacyTrustStorePasswordFileArg); 818 819 legacyTrustStoreFormatArg = new StringArgument(null, "trustStoreFormat", 820 false, 1, null, ""); 821 legacyTrustStoreFormatArg.setHidden(true); 822 parser.addArgument(legacyTrustStoreFormatArg); 823 parser.addExclusiveArgumentSet( 824 parser.getNamedArgument("sourceTrustStoreFormat"), 825 legacyTrustStoreFormatArg); 826 parser.addExclusiveArgumentSet( 827 parser.getNamedArgument("targetTrustStoreFormat"), 828 legacyTrustStoreFormatArg); 829 } 830 831 832 833 /** 834 * {@inheritDoc} 835 */ 836 @Override() 837 public void doExtendedNonLDAPArgumentValidation() 838 throws ArgumentException 839 { 840 // Make sure that the provided base DN was not empty. 841 final DN baseDN = baseDNArg.getValue(); 842 if ((baseDN == null) || baseDN.isNullDN()) 843 { 844 final String message = ERR_LDAP_DIFF_EMPTY_BASE_DN.get(); 845 toolCompletionMessageRef.compareAndSet(null, message); 846 throw new ArgumentException(message); 847 } 848 849 850 // If any of the legacy arguments were provided, then use that argument to 851 // set the value for the corresponding non-legacy argument(s). 852 setArgumentValueFromArgument(legacySourceHostArg, 853 "sourceHostname"); 854 setArgumentValueFromArgument(legacySourcePortArg, 855 "sourcePort"); 856 setArgumentValueFromArgument(legacySourceBindDNArg, 857 "sourceBindDN"); 858 setArgumentValueFromArgument(legacySourceBindPasswordArg, 859 "sourceBindPassword"); 860 setArgumentValueFromArgument(legacyTargetHostArg, 861 "targetHostname"); 862 setArgumentValueFromArgument(legacyTargetBindPasswordFileArg, 863 "targetBindPasswordFile"); 864 setArgumentValueFromArgument(legacyKeyStorePathArg, 865 "sourceKeyStorePath"); 866 setArgumentValueFromArgument(legacyKeyStorePathArg, 867 "targetKeyStorePath"); 868 setArgumentValueFromArgument(legacyKeyStorePasswordArg, 869 "sourceKeyStorePassword"); 870 setArgumentValueFromArgument(legacyKeyStorePasswordArg, 871 "targetKeyStorePassword"); 872 setArgumentValueFromArgument(legacyKeyStorePasswordFileArg, 873 "sourceKeyStorePasswordFile"); 874 setArgumentValueFromArgument(legacyKeyStorePasswordFileArg, 875 "targetKeyStorePasswordFile"); 876 setArgumentValueFromArgument(legacyKeyStoreFormatArg, 877 "sourceKeyStoreFormat"); 878 setArgumentValueFromArgument(legacyKeyStoreFormatArg, 879 "targetKeyStoreFormat"); 880 setArgumentValueFromArgument(legacyCertNicknameArg, 881 "sourceCertNickname"); 882 setArgumentValueFromArgument(legacyCertNicknameArg, 883 "targetCertNickname"); 884 setArgumentValueFromArgument(legacyTrustStorePathArg, 885 "sourceTrustStorePath"); 886 setArgumentValueFromArgument(legacyTrustStorePathArg, 887 "targetTrustStorePath"); 888 setArgumentValueFromArgument(legacyTrustStorePasswordArg, 889 "sourceTrustStorePassword"); 890 setArgumentValueFromArgument(legacyTrustStorePasswordArg, 891 "targetTrustStorePassword"); 892 setArgumentValueFromArgument(legacyTrustStorePasswordFileArg, 893 "sourceTrustStorePasswordFile"); 894 setArgumentValueFromArgument(legacyTrustStorePasswordFileArg, 895 "targetTrustStorePasswordFile"); 896 setArgumentValueFromArgument(legacyTrustStoreFormatArg, 897 "sourceTrustStoreFormat"); 898 setArgumentValueFromArgument(legacyTrustStoreFormatArg, 899 "targetTrustStoreFormat"); 900 901 if (legacyTrustAllArg.isPresent()) 902 { 903 setArgumentPresent("sourceTrustAll"); 904 setArgumentPresent("targetTrustAll"); 905 } 906 907 908 // If no source bind DN was specified, then use a default of 909 // "cn=Directory Manager". 910 final DNArgument sourceBindDNArg = parser.getDNArgument("sourceBindDN"); 911 if (! sourceBindDNArg.isPresent()) 912 { 913 try 914 { 915 final Method addValueMethod = 916 Argument.class.getDeclaredMethod("addValue", String.class); 917 addValueMethod.setAccessible(true); 918 addValueMethod.invoke(sourceBindDNArg, DEFAULT_BIND_DN); 919 920 final Method incrementOccurrencesMethod = 921 Argument.class.getDeclaredMethod("incrementOccurrences"); 922 incrementOccurrencesMethod.setAccessible(true); 923 incrementOccurrencesMethod.invoke(sourceBindDNArg); 924 } 925 catch (final Exception e) 926 { 927 Debug.debugException(e); 928 throw new ArgumentException( 929 ERR_LDAP_DIFF_CANNOT_SET_DEFAULT_BIND_DN.get( 930 DEFAULT_BIND_DN, sourceBindDNArg.getIdentifierString(), 931 StaticUtils.getExceptionMessage(e)), 932 e); 933 } 934 } 935 936 937 // If a source bind DN and password were provided but a target bind DN and 938 // password were not, then use the source values for the target server. 939 final DNArgument targetBindDNArg = parser.getDNArgument("targetBindDN"); 940 if (! targetBindDNArg.isPresent()) 941 { 942 setArgumentValueFromArgument(sourceBindDNArg, "targetBindDN"); 943 } 944 945 final StringArgument sourceBindPasswordArg = 946 parser.getStringArgument("sourceBindPassword"); 947 final StringArgument targetBindPasswordArg = 948 parser.getStringArgument("targetBindPassword"); 949 final FileArgument targetBindPasswordFileArg = 950 parser.getFileArgument("targetBindPasswordFile"); 951 if (sourceBindPasswordArg.isPresent() && 952 (! (targetBindPasswordArg.isPresent() || 953 targetBindPasswordFileArg.isPresent()))) 954 { 955 setArgumentValueFromArgument(sourceBindPasswordArg, 956 "targetBindPassword"); 957 } 958 959 final FileArgument sourceBindPasswordFileArg = 960 parser.getFileArgument("sourceBindPasswordFile"); 961 if (sourceBindPasswordFileArg.isPresent() && 962 (! (targetBindPasswordArg.isPresent() || 963 targetBindPasswordFileArg.isPresent()))) 964 { 965 setArgumentValueFromArgument(sourceBindPasswordFileArg, 966 "targetBindPasswordFile"); 967 } 968 } 969 970 971 972 /** 973 * Updates the specified non-legacy argument with the value from the given 974 * legacy argument, if it is present. 975 * 976 * @param legacyArgument The legacy argument to use to set the value 977 * of the specified non-legacy argument. It 978 * must not be {@code null}. 979 * @param nonLegacyArgumentName The name of the non-legacy argument to 980 * update with the value of the legacy 981 * argument. It must not be {@code null} and 982 * must reference a defined argument that takes 983 * a value. 984 * 985 * @throws ArgumentException If a problem occurs while attempting to set the 986 * value of the specified non-legacy argument from 987 * the given legacy argument. 988 */ 989 private void setArgumentValueFromArgument( 990 @NotNull final Argument legacyArgument, 991 @NotNull final String nonLegacyArgumentName) 992 throws ArgumentException 993 { 994 if (legacyArgument.isPresent()) 995 { 996 try 997 { 998 final Argument nonLegacyArgument = 999 parser.getNamedArgument(nonLegacyArgumentName); 1000 final Method addValueMethod = 1001 Argument.class.getDeclaredMethod("addValue", String.class); 1002 addValueMethod.setAccessible(true); 1003 1004 final Method incrementOccurrencesMethod = 1005 Argument.class.getDeclaredMethod("incrementOccurrences"); 1006 incrementOccurrencesMethod.setAccessible(true); 1007 1008 for (final String valueString : 1009 legacyArgument.getValueStringRepresentations(false)) 1010 { 1011 addValueMethod.invoke(nonLegacyArgument, valueString); 1012 incrementOccurrencesMethod.invoke(nonLegacyArgument); 1013 } 1014 } 1015 catch (final Exception e) 1016 { 1017 Debug.debugException(e); 1018 final String message = ERR_LDAP_DIFF_CANNOT_SET_ARG_FROM_LEGACY.get( 1019 legacyArgument.getIdentifierString(), 1020 nonLegacyArgumentName, StaticUtils.getExceptionMessage(e)); 1021 toolCompletionMessageRef.compareAndSet(null, message); 1022 throw new ArgumentException(message, e); 1023 } 1024 } 1025 } 1026 1027 1028 1029 /** 1030 * Updates the specified argument to indicate that it was provided on the 1031 * command line. 1032 * 1033 * @param argumentName The name of the argument to update as present. It 1034 * must not be {@code null} and must reference a defined 1035 * Boolean argument. 1036 * 1037 * @throws ArgumentException If a problem occurs while attempting to mark 1038 * the specified argument as present. 1039 */ 1040 private void setArgumentPresent(@NotNull final String argumentName) 1041 throws ArgumentException 1042 { 1043 try 1044 { 1045 final BooleanArgument argument = parser.getBooleanArgument(argumentName); 1046 final Method incrementOccurrencesMethod = 1047 Argument.class.getDeclaredMethod("incrementOccurrences"); 1048 incrementOccurrencesMethod.setAccessible(true); 1049 incrementOccurrencesMethod.invoke(argument); 1050 } 1051 catch (final Exception e) 1052 { 1053 Debug.debugException(e); 1054 throw new ArgumentException( 1055 ERR_LDAP_DIFF_CANNOT_SET_ARG_PRESENT.get(argumentName, 1056 StaticUtils.getExceptionMessage(e)), 1057 e); 1058 } 1059 } 1060 1061 1062 1063 /** 1064 * {@inheritDoc} 1065 */ 1066 @Override() 1067 public boolean supportsPropertiesFile() 1068 { 1069 return true; 1070 } 1071 1072 1073 1074 /** 1075 * {@inheritDoc} 1076 */ 1077 @Override() 1078 protected boolean logToolInvocationByDefault() 1079 { 1080 return false; 1081 } 1082 1083 1084 1085 /** 1086 * {@inheritDoc} 1087 */ 1088 @Override() 1089 @Nullable() 1090 protected String getToolCompletionMessage() 1091 { 1092 return toolCompletionMessageRef.get(); 1093 } 1094 1095 1096 1097 /** 1098 * {@inheritDoc} 1099 */ 1100 @Override() 1101 @NotNull() 1102 public ResultCode doToolProcessing() 1103 { 1104 // Establish connection pools to the source and target servers. 1105 LDAPConnectionPool sourcePool = null; 1106 LDAPConnectionPool targetPool = null; 1107 try 1108 { 1109 try 1110 { 1111 sourcePool = createConnectionPool(0, "SourceServer"); 1112 } 1113 catch (final LDAPException e) 1114 { 1115 Debug.debugException(e); 1116 writeCompletionMessage(true, 1117 ERR_LDAP_DIFF_CANNOT_CONNECT_TO_SOURCE.get( 1118 StaticUtils.getExceptionMessage(e))); 1119 return e.getResultCode(); 1120 } 1121 1122 try 1123 { 1124 targetPool = createConnectionPool(1, "TargetServer"); 1125 } 1126 catch (final LDAPException e) 1127 { 1128 Debug.debugException(e); 1129 writeCompletionMessage(true, 1130 ERR_LDAP_DIFF_CANNOT_CONNECT_TO_TARGET.get( 1131 StaticUtils.getExceptionMessage(e))); 1132 return e.getResultCode(); 1133 } 1134 1135 1136 // Get the schema that we'll use for matching operations. Retrieve it 1137 // from the target server. 1138 Schema schema = null; 1139 try 1140 { 1141 schema = targetPool.getSchema(); 1142 } 1143 catch (final Exception e) 1144 { 1145 Debug.debugException(e); 1146 } 1147 1148 1149 // Get the base DN to use when identifying entries to compare. Use the 1150 // schema if possible. 1151 DN baseDN; 1152 try 1153 { 1154 baseDN = new DN(baseDNArg.getStringValue(), schema); 1155 } 1156 catch (final Exception e) 1157 { 1158 Debug.debugException(e); 1159 baseDN = baseDNArg.getValue(); 1160 } 1161 1162 1163 // Get a set containing the DNs of the entries to examine from each of the 1164 // servers. 1165 final TreeSet<LDAPDiffCompactDN> dnsToExamine; 1166 try 1167 { 1168 dnsToExamine = getDNsToExamine(sourcePool, targetPool, baseDN, schema); 1169 } 1170 catch (final LDAPException e) 1171 { 1172 Debug.debugException(e); 1173 writeCompletionMessage(true, e.getMessage()); 1174 return e.getResultCode(); 1175 } 1176 1177 1178 // Compare the entries in each server and write the results. 1179 try 1180 { 1181 final AtomicReference<ResultCode> resultCodeRef = 1182 new AtomicReference<>(); 1183 final long[] entryCounts = identifyDifferences(sourcePool, targetPool, 1184 baseDN, schema, resultCodeRef, dnsToExamine); 1185 final long inSyncCount = entryCounts[0]; 1186 final long addCount = entryCounts[1]; 1187 final long delCount = entryCounts[2]; 1188 final long modCount = entryCounts[3]; 1189 final long missingCount = entryCounts[4]; 1190 final long errorCount = entryCounts[5]; 1191 final long totalDifferenceCount = addCount + delCount + modCount; 1192 final long totalExaminedCount = inSyncCount + totalDifferenceCount; 1193 1194 if (! quietArg.isPresent()) 1195 { 1196 out(); 1197 } 1198 1199 wrapOut(0, WRAP_COLUMN, 1200 INFO_LDAP_DIFF_SUMMARY_PROCESSING_COMPLETE.get(getToolName())); 1201 out(); 1202 1203 wrapOut(0, WRAP_COLUMN, 1204 INFO_LDAP_DIFF_SUMMARY_TOTAL_EXAMINED.get(totalExaminedCount)); 1205 wrapOut(0, WRAP_COLUMN, 1206 INFO_LDAP_DIFF_SUMMARY_ADD_COUNT.get(addCount)); 1207 wrapOut(0, WRAP_COLUMN, 1208 INFO_LDAP_DIFF_SUMMARY_DEL_COUNT.get(delCount)); 1209 wrapOut(0, WRAP_COLUMN, 1210 INFO_LDAP_DIFF_SUMMARY_MOD_COUNT.get(modCount)); 1211 wrapOut(0, WRAP_COLUMN, 1212 INFO_LDAP_DIFF_SUMMARY_IN_SYNC_COUNT.get(inSyncCount)); 1213 1214 if (missingCount > 0) 1215 { 1216 wrapOut(0, WRAP_COLUMN, 1217 INFO_LDAP_DIFF_SUMMARY_MISSING_COUNT.get(missingCount)); 1218 } 1219 1220 if (errorCount > 0) 1221 { 1222 wrapOut(0, WRAP_COLUMN, 1223 INFO_LDAP_DIFF_SUMMARY_ERROR_COUNT.get(errorCount)); 1224 } 1225 1226 out(); 1227 1228 if (errorCount > 0) 1229 { 1230 writeCompletionMessage(false, 1231 INFO_LDAP_DIFF_ERRORS_IDENTIFYING_ENTRIES.get()); 1232 resultCodeRef.compareAndSet(null, ResultCode.LOCAL_ERROR); 1233 return resultCodeRef.get(); 1234 } 1235 else if (totalDifferenceCount == 0) 1236 { 1237 writeCompletionMessage(false, 1238 INFO_LDAP_DIFF_SERVERS_IN_SYNC.get()); 1239 resultCodeRef.compareAndSet(null, ResultCode.SUCCESS); 1240 return resultCodeRef.get(); 1241 } 1242 else 1243 { 1244 if (totalDifferenceCount == 1) 1245 { 1246 writeCompletionMessage(true, 1247 WARN_LDAP_DIFF_DIFFERENCE_FOUND.get()); 1248 } 1249 else 1250 { 1251 writeCompletionMessage(true, 1252 WARN_LDAP_DIFF_DIFFERENCES_FOUND.get(totalDifferenceCount)); 1253 } 1254 1255 resultCodeRef.compareAndSet(null, ResultCode.COMPARE_FALSE); 1256 return resultCodeRef.get(); 1257 } 1258 } 1259 catch (final LDAPException e) 1260 { 1261 Debug.debugException(e); 1262 writeCompletionMessage(true, 1263 ERR_LDAP_DIFF_ERROR_IDENTIFYING_DIFFERENCES.get( 1264 StaticUtils.getExceptionMessage(e))); 1265 return e.getResultCode(); 1266 } 1267 } 1268 finally 1269 { 1270 if (sourcePool != null) 1271 { 1272 sourcePool.close(); 1273 } 1274 1275 if (targetPool != null) 1276 { 1277 targetPool.close(); 1278 } 1279 } 1280 } 1281 1282 1283 1284 /** 1285 * Creates a connection pool that is established to the sever with the 1286 * indicated index. 1287 * 1288 * @param serverIndex The index of the server to which the pool should be 1289 * established. 1290 * @param name The name to use for the connection pool. It must not 1291 * be {@code null}. 1292 * 1293 * @return The connection pool that was created. 1294 * 1295 * @throws LDAPException If a problem occurs while creating the connection 1296 * pool. 1297 */ 1298 @NotNull() 1299 private LDAPConnectionPool createConnectionPool(final int serverIndex, 1300 @NotNull final String name) 1301 throws LDAPException 1302 { 1303 final LDAPConnectionPool pool = getConnectionPool(serverIndex, 1, 1304 numThreadsArg.getValue()); 1305 pool.setRetryFailedOperationsDueToInvalidConnections(true); 1306 pool.setConnectionPoolName(name); 1307 return pool; 1308 } 1309 1310 1311 1312 /** 1313 * Writes the provided message to standard output or standard error and sets 1314 * it as the tool completion message. 1315 * 1316 * @param isError Indicates whether the message represents an error 1317 * condition. 1318 * @param message The message to be written and set as the tool completion 1319 * message. It must not be {@code null}. 1320 */ 1321 private void writeCompletionMessage(final boolean isError, 1322 @NotNull final String message) 1323 { 1324 if (isError) 1325 { 1326 wrapErr(0, WRAP_COLUMN, message); 1327 } 1328 else 1329 { 1330 wrapOut(0, WRAP_COLUMN, message); 1331 } 1332 1333 toolCompletionMessageRef.compareAndSet(null, message); 1334 } 1335 1336 1337 1338 /** 1339 * Retrieves an ordered set of the DNs of the entries to examine from each of 1340 * the servers. This will be done in parallel. 1341 * 1342 * @param sourcePool A connection pool that may be used to communicate with 1343 * the source server. It must not be {@code null}. 1344 * @param targetPool A connection pool that may be used to communicate with 1345 * the target server. It must not be {@code null}. 1346 * @param baseDN The base DN for entries to examine. It must not be 1347 * {@code null}. 1348 * @param schema The schema to use during processing. It may optionally 1349 * be {@code null} if no schema is available. 1350 * 1351 * @return An ordered set of the DNs of the entries to exazmine from each of 1352 * the servers. 1353 * 1354 * @throws LDAPException If a problem is encountered while obtaining the 1355 * set of DNs from the source or target server. 1356 */ 1357 @NotNull() 1358 private TreeSet<LDAPDiffCompactDN> getDNsToExamine( 1359 @NotNull final LDAPConnectionPool sourcePool, 1360 @NotNull final LDAPConnectionPool targetPool, 1361 @NotNull final DN baseDN, 1362 @Nullable final Schema schema) 1363 throws LDAPException 1364 { 1365 if (! quietArg.isPresent()) 1366 { 1367 wrapOut(0, WRAP_COLUMN, 1368 INFO_LDAP_DIFF_IDENTIFYING_ENTRIES.get()); 1369 } 1370 1371 final TreeSet<LDAPDiffCompactDN> dnSet = new TreeSet<>(); 1372 final LDAPDiffDNDumper sourceDNDumper = new LDAPDiffDNDumper(this, 1373 "LDAPDiff Source Server DN Dumper", sourceDNsFileArg.getValue(), 1374 sourcePool, baseDN, searchScopeArg.getValue(), 1375 excludeBranchArg.getValues(), searchFilterArg.getValue(), schema, 1376 missingOnlyArg.isPresent(), quietArg.isPresent(), dnSet); 1377 sourceDNDumper.start(); 1378 1379 final LDAPDiffDNDumper targetDNDumper = new LDAPDiffDNDumper(this, 1380 "LDAPDiff Target Server DN Dumper", targetDNsFileArg.getValue(), 1381 targetPool, baseDN, searchScopeArg.getValue(), 1382 excludeBranchArg.getValues(), searchFilterArg.getValue(), schema, 1383 missingOnlyArg.isPresent(), quietArg.isPresent(), dnSet); 1384 targetDNDumper.start(); 1385 1386 try 1387 { 1388 sourceDNDumper.join(); 1389 } 1390 catch (final Exception e) 1391 { 1392 Debug.debugException(e); 1393 throw new LDAPException(ResultCode.LOCAL_ERROR, 1394 ERR_LDAP_DIFF_ERROR_GETTING_SOURCE_DNS.get( 1395 StaticUtils.getExceptionMessage(e))); 1396 } 1397 1398 final LDAPException sourceException = 1399 sourceDNDumper.getProcessingException(); 1400 if (sourceException != null) 1401 { 1402 throw new LDAPException(sourceException.getResultCode(), 1403 ERR_LDAP_DIFF_ERROR_GETTING_SOURCE_DNS.get( 1404 sourceException.getMessage()), 1405 sourceException); 1406 } 1407 1408 try 1409 { 1410 targetDNDumper.join(); 1411 } 1412 catch (final Exception e) 1413 { 1414 Debug.debugException(e); 1415 throw new LDAPException(ResultCode.LOCAL_ERROR, 1416 ERR_LDAP_DIFF_ERROR_GETTING_TARGET_DNS.get( 1417 StaticUtils.getExceptionMessage(e))); 1418 } 1419 1420 final LDAPException targetException = 1421 targetDNDumper.getProcessingException(); 1422 if (targetException != null) 1423 { 1424 throw new LDAPException(targetException.getResultCode(), 1425 ERR_LDAP_DIFF_ERROR_GETTING_TARGET_DNS.get( 1426 targetException.getMessage()), 1427 targetException); 1428 } 1429 1430 if (! quietArg.isPresent()) 1431 { 1432 wrapOut(0, WRAP_COLUMN, 1433 INFO_LDAP_DIFF_IDENTIFIED_ENTRIES.get(dnSet.size())); 1434 } 1435 1436 return dnSet; 1437 } 1438 1439 1440 1441 /** 1442 * Examines all of the entries in the provided set and identifies differences 1443 * between the source and target servers. The differences will be written to 1444 * output files, and the return value will provide information about the 1445 * number of entries in each result category. 1446 * 1447 * @param sourcePool A connection pool that may be used to communicate 1448 * with the source server. It must not be 1449 * {@code null}. 1450 * @param targetPool A connection pool that may be used to communicate 1451 * with the target server. It must not be 1452 * {@code null}. 1453 * @param baseDN The base DN for entries to examine. It must not be 1454 * {@code null}. 1455 * @param schema The schema to use in processing. It may optionally 1456 * be {@code null} if no schema is available. 1457 * @param resultCodeRef A reference that may be updated to set the result 1458 * code that should be returned. It must not be 1459 * {@code null} but may be unset. 1460 * @param dnsToExamine The set of DNs to examine. It must not be 1461 * {@code null}. 1462 * 1463 * @return An array of {@code long} values that provide the number of entries 1464 * in each result category. The array that is returned will contain 1465 * six elements. The first will be the number of entries that were 1466 * found to be in sync between the source and target servers. The 1467 * second will be the number of entries that were present only in the 1468 * target server and need to be added to the source server. The 1469 * third will be the number of entries that were present only in the 1470 * source server and need to be removed. The fourth will be the 1471 * number of entries that were present in both servers but were not 1472 * equivalent and therefore need to be modified in the source server. 1473 * The fifth will be the number of entries that were initially 1474 * identified but were subsequently not found in either server. The 1475 * sixth element will be the number of errors encountered while 1476 * attempting to examine entries. 1477 * 1478 * @throws LDAPException If an unrecoverable error occurs during processing. 1479 */ 1480 @NotNull() 1481 private long[] identifyDifferences( 1482 @NotNull final LDAPConnectionPool sourcePool, 1483 @NotNull final LDAPConnectionPool targetPool, 1484 @NotNull final DN baseDN, 1485 @Nullable final Schema schema, 1486 @NotNull final AtomicReference<ResultCode> resultCodeRef, 1487 @NotNull final TreeSet<LDAPDiffCompactDN> dnsToExamine) 1488 throws LDAPException 1489 { 1490 // Create LDIF writers that will be used to write the output files. We want 1491 // to create the main output file even if we don't end up identifying any 1492 // changes, and it's also convenient to just go ahead and create the 1493 // temporary add and modify files now, too, even if we don't end up using 1494 // them. 1495 final File mergedOutputFile = outputLDIFArg.getValue(); 1496 1497 final File addFile = new File(mergedOutputFile.getAbsolutePath() + ".add"); 1498 addFile.deleteOnExit(); 1499 1500 final File modFile = new File(mergedOutputFile.getAbsolutePath() + ".mod"); 1501 modFile.deleteOnExit(); 1502 1503 long inSyncCount = 0L; 1504 long addCount = 0L; 1505 long deleteCount = 0L; 1506 long modifyCount = 0L; 1507 long missingCount = 0L; 1508 long errorCount = 0L; 1509 ParallelProcessor<LDAPDiffCompactDN,LDAPDiffProcessorResult> 1510 parallelProcessor = null; 1511 final String sourceHostPort = 1512 getServerHostPort("sourceHostname", "sourcePort"); 1513 final String targetHostPort = 1514 getServerHostPort("targetHostname", "targetPort"); 1515 final TreeSet<LDAPDiffCompactDN> missingEntryDNs = new TreeSet<>(); 1516 try (LDIFWriter mergedWriter = createLDIFWriter(mergedOutputFile, 1517 INFO_LDAP_DIFF_MERGED_FILE_COMMENT.get(sourceHostPort, 1518 targetHostPort)); 1519 LDIFWriter addWriter = createLDIFWriter(addFile); 1520 LDIFWriter modWriter = createLDIFWriter(modFile)) 1521 { 1522 // Create a parallel processor that will be used to retrieve and compare 1523 // entries from the source and target servers. 1524 final String[] attributes = 1525 parser.getTrailingArguments().toArray(StaticUtils.NO_STRINGS); 1526 1527 final LDAPDiffProcessor processor = new LDAPDiffProcessor(sourcePool, 1528 targetPool, baseDN, schema, byteForByteArg.isPresent(), attributes, 1529 missingOnlyArg.isPresent()); 1530 1531 parallelProcessor = new ParallelProcessor<>(processor, 1532 new LDAPSDKThreadFactory("LDAPDiff Compare Processor", true), 1533 numThreadsArg.getValue(), 5); 1534 1535 1536 // Define variables that will be used to monitor progress and keep track 1537 // of information between passes. 1538 TreeSet<LDAPDiffCompactDN> currentPassDNs = dnsToExamine; 1539 TreeSet<LDAPDiffCompactDN> nextPassDNs = new TreeSet<>(); 1540 final TreeSet<LDAPDiffCompactDN> deletedEntryDNs = new TreeSet<>(); 1541 final List<LDAPDiffCompactDN> currentBatchOfDNs = 1542 new ArrayList<>(MAX_ENTRIES_PER_BATCH); 1543 1544 1545 // Process each pass, or until we confirm that there aren't any changes 1546 // between the source and target servers. 1547 for (int i=1; i <= numPassesArg.getValue(); i++) 1548 { 1549 final boolean isLastPass = (i == numPassesArg.getValue()); 1550 1551 if (! quietArg.isPresent()) 1552 { 1553 out(); 1554 wrapOut(0, WRAP_COLUMN, 1555 INFO_LDAP_DIFF_STARTING_COMPARE_PASS.get(i, 1556 numPassesArg.getValue(), currentPassDNs.size())); 1557 } 1558 1559 1560 // Process the changes in batches until we have gone through all of the 1561 // entries. 1562 nextPassDNs.clear(); 1563 int differencesIdentifiedCount = 0; 1564 int processedCurrentPassCount = 0; 1565 final int totalCurrentPassCount = currentPassDNs.size(); 1566 final Iterator<LDAPDiffCompactDN> dnIterator = 1567 currentPassDNs.iterator(); 1568 while (dnIterator.hasNext()) 1569 { 1570 // Build a batch of DNs. 1571 currentBatchOfDNs.clear(); 1572 while (dnIterator.hasNext()) 1573 { 1574 currentBatchOfDNs.add(dnIterator.next()); 1575 dnIterator.remove(); 1576 1577 if (currentBatchOfDNs.size() >= MAX_ENTRIES_PER_BATCH) 1578 { 1579 break; 1580 } 1581 } 1582 1583 // Process the batch of entries. 1584 final List<Result<LDAPDiffCompactDN,LDAPDiffProcessorResult>> results; 1585 try 1586 { 1587 results = parallelProcessor.processAll(currentBatchOfDNs); 1588 } 1589 catch (final Exception e) 1590 { 1591 Debug.debugException(e); 1592 throw new LDAPException(ResultCode.LOCAL_ERROR, 1593 ERR_LDAP_DIFF_ERROR_PROCESSING_BATCH.get( 1594 StaticUtils.getExceptionMessage(e)), 1595 e); 1596 } 1597 1598 // Iterate through and handle the results. 1599 for (final Result<LDAPDiffCompactDN,LDAPDiffProcessorResult> result : 1600 results) 1601 { 1602 processedCurrentPassCount++; 1603 1604 final Throwable exception = result.getFailureCause(); 1605 if (exception != null) 1606 { 1607 final LDAPDiffCompactDN compactDN = result.getInput(); 1608 if (! isLastPass) 1609 { 1610 nextPassDNs.add(compactDN); 1611 differencesIdentifiedCount++; 1612 } 1613 else 1614 { 1615 final LDAPException reportException; 1616 if (exception instanceof LDAPException) 1617 { 1618 final LDAPException caughtException = 1619 (LDAPException) exception; 1620 reportException = new LDAPException( 1621 caughtException.getResultCode(), 1622 ERR_LDAP_DIFF_ERROR_COMPARING_ENTRY.get( 1623 compactDN.toDN(baseDN, schema).toString(), 1624 caughtException.getMessage()), 1625 caughtException.getMatchedDN(), 1626 caughtException.getReferralURLs(), 1627 caughtException.getResponseControls(), 1628 caughtException.getCause()); 1629 } 1630 else 1631 { 1632 reportException = new LDAPException(ResultCode.LOCAL_ERROR, 1633 ERR_LDAP_DIFF_ERROR_COMPARING_ENTRY.get( 1634 compactDN.toDN(baseDN, schema).toString(), 1635 StaticUtils.getExceptionMessage(exception)), 1636 exception); 1637 } 1638 1639 errorCount++; 1640 resultCodeRef.compareAndSet(null, 1641 reportException.getResultCode()); 1642 1643 final List<String> formattedResultLines = 1644 ResultUtils.formatResult(reportException, false, 0, 1645 (WRAP_COLUMN - 2)); 1646 final Iterator<String> resultLineIterator = 1647 formattedResultLines.iterator(); 1648 while (resultLineIterator.hasNext()) 1649 { 1650 mergedWriter.writeComment(resultLineIterator.next(), false, 1651 (! resultLineIterator.hasNext())); 1652 } 1653 } 1654 1655 continue; 1656 } 1657 1658 final LDAPDiffProcessorResult resultOutput = result.getOutput(); 1659 final ChangeType changeType = resultOutput.getChangeType(); 1660 if (changeType == null) 1661 { 1662 // This indicates that either the entry is in sync between the 1663 // source and target servers or that it was missing from both 1664 // servers. If it's the former, then we just need to increment 1665 // a counter. If it's the latter, then we also need to hold onto 1666 // the DN for including in a comment at the end of the LDIF file. 1667 if (resultOutput.isEntryMissing()) 1668 { 1669 missingCount++; 1670 missingEntryDNs.add(result.getInput()); 1671 } 1672 else 1673 { 1674 inSyncCount++; 1675 } 1676 1677 // This indicates that the entry is in sync between the source 1678 // and target servers. We don't need to do anything in this case. 1679 inSyncCount++; 1680 } 1681 else if (! isLastPass) 1682 { 1683 // This entry is out of sync, but this isn't the last pass, so 1684 // just hold on to the DN so that we'll re-examine the entry on 1685 // the next pass. 1686 nextPassDNs.add(result.getInput()); 1687 differencesIdentifiedCount++; 1688 } 1689 else 1690 { 1691 // The entry is out of sync, and this is the last pass. If the 1692 // entry should be deleted, then capture the DN in a sorted list. 1693 // If it's an add or modify, then write it to an appropriate 1694 // temporary file. In each case, update the appropriate counter. 1695 differencesIdentifiedCount++; 1696 switch (changeType) 1697 { 1698 case DELETE: 1699 deletedEntryDNs.add(result.getInput()); 1700 deleteCount++; 1701 break; 1702 1703 case ADD: 1704 addWriter.writeChangeRecord( 1705 new LDIFAddChangeRecord(resultOutput.getEntry()), 1706 WARN_LDAP_DIFF_COMMENT_ADDED_ENTRY.get(targetHostPort, 1707 sourceHostPort)); 1708 addCount++; 1709 break; 1710 1711 case MODIFY: 1712 default: 1713 modWriter.writeChangeRecord( 1714 new LDIFModifyChangeRecord(resultOutput.getDN(), 1715 resultOutput.getModifications()), 1716 WARN_LDAP_DIFF_COMMENT_MODIFIED_ENTRY.get(sourceHostPort, 1717 targetHostPort)); 1718 modifyCount++; 1719 break; 1720 } 1721 } 1722 } 1723 1724 // Write a progress message. 1725 if (! quietArg.isPresent()) 1726 { 1727 final int percentComplete = Math.round(100.0f * 1728 processedCurrentPassCount / totalCurrentPassCount); 1729 wrapOut(0, WRAP_COLUMN, 1730 INFO_LDAP_DIFF_COMPARE_PROGRESS.get(processedCurrentPassCount, 1731 totalCurrentPassCount, percentComplete, 1732 differencesIdentifiedCount)); 1733 } 1734 } 1735 1736 1737 // If this isn't the last pass, and if there are still outstanding 1738 // differences, then sleep before the next iteration. 1739 if (isLastPass) 1740 { 1741 break; 1742 } 1743 else if (nextPassDNs.isEmpty()) 1744 { 1745 if (! quietArg.isPresent()) 1746 { 1747 wrapOut(0, WRAP_COLUMN, 1748 INFO_LDAP_DIFF_NO_NEED_FOR_ADDITIONAL_PASS.get()); 1749 } 1750 break; 1751 } 1752 else 1753 { 1754 try 1755 { 1756 final int sleepTimeSeconds = secondsBetweenPassesArg.getValue(); 1757 if (! quietArg.isPresent()) 1758 { 1759 wrapOut(0, WRAP_COLUMN, 1760 INFO_LDAP_DIFF_WAITING_BEFORE_NEXT_PASS.get( 1761 sleepTimeSeconds)); 1762 } 1763 1764 Thread.sleep(TimeUnit.SECONDS.toMillis(sleepTimeSeconds)); 1765 } 1766 catch (final Exception e) 1767 { 1768 Debug.debugException(e); 1769 } 1770 } 1771 1772 1773 // Swap currentPassDNs (which will now be empty) and nextPassDN (which 1774 // contains the DNs of entries that were found out of sync in the 1775 // current pass) sets so that they will be correct for the next pass. 1776 final TreeSet<LDAPDiffCompactDN> emptyDNSet = currentPassDNs; 1777 currentPassDNs = nextPassDNs; 1778 nextPassDNs = emptyDNSet; 1779 } 1780 1781 1782 // If we've gotten here, then we've completed all of the passes. If no 1783 // differences were identified, then write a comment indicating that to 1784 // the end of the LDIF file. 1785 if ((addCount == 0) && (deleteCount == 0) && (modifyCount == 0)) 1786 { 1787 mergedWriter.writeComment(INFO_LDAP_DIFF_SERVERS_IN_SYNC.get(), true, 1788 false); 1789 } 1790 1791 1792 // If we've gotten here, then we've completed all of the passes. If we've 1793 // identified any deleted entries, then add them to the output first (in 1794 // descending order so that children are deleted before parents). The 1795 // modify and add records will be added later, after we've closed all of 1796 // the writers. 1797 if (! deletedEntryDNs.isEmpty()) 1798 { 1799 mergedWriter.writeComment(INFO_LDAP_DIFF_COMMENT_DELETED_ENTRIES.get(), 1800 true, true); 1801 1802 if (! quietArg.isPresent()) 1803 { 1804 out(); 1805 wrapOut(0, WRAP_COLUMN, 1806 INFO_LDAP_DIFF_STARTING_DELETE_PASS.get(deleteCount)); 1807 } 1808 1809 int entryCount = 0; 1810 for (final LDAPDiffCompactDN compactDN : 1811 deletedEntryDNs.descendingSet()) 1812 { 1813 SearchResultEntry entry = null; 1814 LDAPException ldapException = null; 1815 final String dnString = compactDN.toDN(baseDN, schema).toString(); 1816 try 1817 { 1818 entry = sourcePool.getEntry(dnString, attributes); 1819 } 1820 catch (final LDAPException e) 1821 { 1822 Debug.debugException(e); 1823 ldapException = new LDAPException(e.getResultCode(), 1824 ERR_LDAP_DIFF_CANNOT_GET_ENTRY_TO_DELETE.get(dnString, 1825 StaticUtils.getExceptionMessage(e)), 1826 e); 1827 } 1828 1829 if (entry != null) 1830 { 1831 mergedWriter.writeComment( 1832 INFO_LDAP_DIFF_COMMENT_DELETED_ENTRY.get(sourceHostPort, 1833 targetHostPort), 1834 false, false); 1835 mergedWriter.writeComment("", false, false); 1836 for (final String line : entry.toLDIF(75)) 1837 { 1838 mergedWriter.writeComment(line, false, false); 1839 } 1840 1841 mergedWriter.writeChangeRecord( 1842 new LDIFDeleteChangeRecord(entry.getDN())); 1843 } 1844 else if (ldapException != null) 1845 { 1846 mergedWriter.writeComment(ldapException.getExceptionMessage(), 1847 false, false); 1848 mergedWriter.writeChangeRecord( 1849 new LDIFDeleteChangeRecord(entry.getDN())); 1850 } 1851 1852 entryCount++; 1853 if ((! quietArg.isPresent()) && 1854 ((entryCount % MAX_ENTRIES_PER_BATCH) == 0)) 1855 { 1856 final int percentComplete = 1857 Math.round(100.0f * entryCount / deleteCount); 1858 wrapOut(0, WRAP_COLUMN, 1859 INFO_LDAP_DIFF_DELETE_PROGRESS.get(entryCount, 1860 deleteCount, percentComplete)); 1861 } 1862 } 1863 1864 if (! quietArg.isPresent()) 1865 { 1866 final int percentComplete = 1867 Math.round(100.0f * entryCount / deleteCount); 1868 wrapOut(0, WRAP_COLUMN, 1869 INFO_LDAP_DIFF_DELETE_PROGRESS.get(entryCount, deleteCount, 1870 percentComplete)); 1871 } 1872 } 1873 } 1874 catch (final IOException e) 1875 { 1876 Debug.debugException(e); 1877 throw new LDAPException(ResultCode.LOCAL_ERROR, 1878 ERR_LDAP_DIFF_ERROR_WRITING_OUTPUT.get(getToolName(), 1879 StaticUtils.getExceptionMessage(e)), 1880 e); 1881 } 1882 finally 1883 { 1884 if (parallelProcessor != null) 1885 { 1886 try 1887 { 1888 parallelProcessor.shutdown(); 1889 } 1890 catch (final Exception e) 1891 { 1892 Debug.debugException(e); 1893 } 1894 } 1895 } 1896 1897 1898 // If any modified entries were identified, then append the modify LDIF 1899 // file to the merged change file. 1900 if (modifyCount > 0L) 1901 { 1902 appendFileToFile(modFile, mergedOutputFile, 1903 INFO_LDAP_DIFF_COMMENT_ADDED_ENTRIES.get()); 1904 modFile.delete(); 1905 } 1906 1907 1908 // If any added entries were identified, then append the add LDIF file to 1909 // the merged change file. 1910 if (addCount > 0L) 1911 { 1912 appendFileToFile(addFile, mergedOutputFile, 1913 INFO_LDAP_DIFF_COMMENT_MODIFIED_ENTRIES.get()); 1914 addFile.delete(); 1915 } 1916 1917 1918 // If there are any missing entries, then update the merged LDIF file to 1919 // list them. 1920 if (! missingEntryDNs.isEmpty()) 1921 { 1922 try (FileOutputStream outputStream = 1923 new FileOutputStream(mergedOutputFile, true); 1924 LDIFWriter ldifWriter = new LDIFWriter(outputStream)) 1925 { 1926 ldifWriter.writeComment(INFO_LDAP_DIFF_COMMENT_MISSING_ENTRIES.get(), 1927 true, true); 1928 for (final LDAPDiffCompactDN missingEntryDN : missingEntryDNs) 1929 { 1930 ldifWriter.writeComment( 1931 INFO_LDAP_DIFF_COMMENT_MISSING_ENTRY.get(missingEntryDN.toDN( 1932 baseDN, schema).toString()), 1933 false, true); 1934 } 1935 } 1936 catch (final Exception e) 1937 { 1938 Debug.debugException(e); 1939 throw new LDAPException(ResultCode.LOCAL_ERROR, 1940 ERR_LDAP_DIFF_ERROR_WRITING_OUTPUT.get(getToolName(), 1941 StaticUtils.getExceptionMessage(e)), 1942 e); 1943 } 1944 } 1945 1946 return new long[] 1947 { 1948 inSyncCount, 1949 addCount, 1950 deleteCount, 1951 modifyCount, 1952 missingCount, 1953 errorCount 1954 }; 1955 } 1956 1957 1958 1959 /** 1960 * Retrieves a string representation of the address and port for the server 1961 * identified by the specified arguments. 1962 * 1963 * @param hostnameArgName The name of the argument used to specify the 1964 * hostname for the target server. It must not be 1965 * {@code null}. 1966 * @param portArgName The name of the argument used to specify the port 1967 * of the target server. It must not be 1968 * {@code null}. 1969 * 1970 * @return A string representation of the address and port for the server 1971 * identified by the specified arguments. 1972 */ 1973 @NotNull() 1974 private String getServerHostPort(@NotNull final String hostnameArgName, 1975 @NotNull final String portArgName) 1976 { 1977 final StringArgument hostnameArg = 1978 parser.getStringArgument(hostnameArgName); 1979 final IntegerArgument portArg = parser.getIntegerArgument(portArgName); 1980 return hostnameArg.getValue() + ':' + portArg.getValue(); 1981 } 1982 1983 1984 1985 /** 1986 * Creates the LDIF writer that will be used when writing identified 1987 * differences. 1988 * 1989 * @param ldifFile The LDIF file to be written. It must not be 1990 * {@code null}. 1991 * @param comments The set of comments to be included at the top of the 1992 * file. It must not be {@code null} but may be empty. 1993 * 1994 * @return The LDIF writer that was created. 1995 * 1996 * @throws LDAPException If a problem occurs while creating the LDIF writer. 1997 */ 1998 @NotNull() 1999 private LDIFWriter createLDIFWriter(@NotNull final File ldifFile, 2000 @NotNull final String... comments) 2001 throws LDAPException 2002 { 2003 try 2004 { 2005 final LDIFWriter writer = new LDIFWriter(ldifFile); 2006 writer.setWrapColumn(wrapColumnArg.getValue()); 2007 2008 for (final String comment : comments) 2009 { 2010 writer.writeComment(comment, false, true); 2011 } 2012 2013 return writer; 2014 } 2015 catch (final Exception e) 2016 { 2017 Debug.debugException(e); 2018 throw new LDAPException(ResultCode.LOCAL_ERROR, 2019 ERR_LDAP_DIFF_CANNOT_CREATE_LDIF_WRITER.get( 2020 ldifFile.getAbsolutePath(), 2021 StaticUtils.getExceptionMessage(e)), 2022 e); 2023 } 2024 } 2025 2026 2027 2028 /** 2029 * Appends the contents of the specified file to the end of the indicated 2030 * file. 2031 * 2032 * @param fileToAppend The file whose contents should be appended to 2033 * the end of the indicated file. It must not be 2034 * {@code null}, and the file must exist. 2035 * @param fileToBeAppendedTo The file to which the source file should be 2036 * appended. It must not be {@code null}, and the 2037 * file must exist. 2038 * @param comment A comment that should be placed before the 2039 * content of the file to append. It must not be 2040 * {@code null} or empty. 2041 * 2042 * @throws LDAPException If a problem occurs while reading from the file to 2043 * append or writing to the file to be appended to. 2044 */ 2045 private void appendFileToFile(@NotNull final File fileToAppend, 2046 @NotNull final File fileToBeAppendedTo, 2047 @NotNull final String comment) 2048 throws LDAPException 2049 { 2050 try (FileInputStream inputStream = new FileInputStream(fileToAppend); 2051 FileOutputStream outputStream = 2052 new FileOutputStream(fileToBeAppendedTo, true)) 2053 { 2054 outputStream.write(StaticUtils.getBytes(StaticUtils.EOL)); 2055 for (final String line : StaticUtils.wrapLine(comment, (WRAP_COLUMN - 2))) 2056 { 2057 outputStream.write(StaticUtils.getBytes("# " + line + StaticUtils.EOL)); 2058 } 2059 outputStream.write(StaticUtils.getBytes(StaticUtils.EOL)); 2060 2061 final byte[] buffer = new byte[1024 * 1024]; 2062 while (true) 2063 { 2064 final int bytesRead = inputStream.read(buffer); 2065 if (bytesRead < 0) 2066 { 2067 return; 2068 } 2069 2070 outputStream.write(buffer, 0, bytesRead); 2071 } 2072 } 2073 catch (final Exception e) 2074 { 2075 Debug.debugException(e); 2076 throw new LDAPException(ResultCode.LOCAL_ERROR, 2077 ERR_LDAP_DIFF_ERROR_WRITING_OUTPUT.get(getToolName(), 2078 StaticUtils.getExceptionMessage(e)), 2079 e); 2080 } 2081 } 2082 2083 2084 2085 /** 2086 * {@inheritDoc} 2087 */ 2088 @Override() 2089 @NotNull() 2090 public LinkedHashMap<String[],String> getExampleUsages() 2091 { 2092 final LinkedHashMap<String[],String> examples = new LinkedHashMap<>(); 2093 2094 examples.put( 2095 new String[] 2096 { 2097 "--sourceHostname", "source.example.com", 2098 "--sourcePort", "636", 2099 "--sourceUseSSL", 2100 "--sourceBindDN", "cn=Directory Manager", 2101 "--sourceBindPasswordFile", "/path/to/password.txt", 2102 "--targetHostname", "target.example.com", 2103 "--targetPort", "636", 2104 "--targetUseSSL", 2105 "--targetBindDN", "cn=Directory Manager", 2106 "--targetBindPasswordFile", "/path/to/password.txt", 2107 "--baseDN", "dc=example,dc=com", 2108 "--outputLDIF", "diff.ldif" 2109 }, 2110 INFO_LDAP_DIFF_EXAMPLE.get()); 2111 2112 return examples; 2113 } 2114}