001/* 002 * Copyright 2021-2024 Ping Identity Corporation 003 * All Rights Reserved. 004 */ 005/* 006 * Copyright 2021-2024 Ping Identity Corporation 007 * 008 * Licensed under the Apache License, Version 2.0 (the "License"); 009 * you may not use this file except in compliance with the License. 010 * You may obtain a copy of the License at 011 * 012 * http://www.apache.org/licenses/LICENSE-2.0 013 * 014 * Unless required by applicable law or agreed to in writing, software 015 * distributed under the License is distributed on an "AS IS" BASIS, 016 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 017 * See the License for the specific language governing permissions and 018 * limitations under the License. 019 */ 020/* 021 * Copyright (C) 2021-2024 Ping Identity Corporation 022 * 023 * This program is free software; you can redistribute it and/or modify 024 * it under the terms of the GNU General Public License (GPLv2 only) 025 * or the terms of the GNU Lesser General Public License (LGPLv2.1 only) 026 * as published by the Free Software Foundation. 027 * 028 * This program is distributed in the hope that it will be useful, 029 * but WITHOUT ANY WARRANTY; without even the implied warranty of 030 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 031 * GNU General Public License for more details. 032 * 033 * You should have received a copy of the GNU General Public License 034 * along with this program; if not, see <http://www.gnu.org/licenses>. 035 */ 036package com.unboundid.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 supportsDebugLogging() 1079 { 1080 return true; 1081 } 1082 1083 1084 1085 /** 1086 * {@inheritDoc} 1087 */ 1088 @Override() 1089 protected boolean logToolInvocationByDefault() 1090 { 1091 return false; 1092 } 1093 1094 1095 1096 /** 1097 * {@inheritDoc} 1098 */ 1099 @Override() 1100 @Nullable() 1101 protected String getToolCompletionMessage() 1102 { 1103 return toolCompletionMessageRef.get(); 1104 } 1105 1106 1107 1108 /** 1109 * {@inheritDoc} 1110 */ 1111 @Override() 1112 @NotNull() 1113 public ResultCode doToolProcessing() 1114 { 1115 // Establish connection pools to the source and target servers. 1116 LDAPConnectionPool sourcePool = null; 1117 LDAPConnectionPool targetPool = null; 1118 try 1119 { 1120 try 1121 { 1122 sourcePool = createConnectionPool(0, "SourceServer"); 1123 } 1124 catch (final LDAPException e) 1125 { 1126 Debug.debugException(e); 1127 writeCompletionMessage(true, 1128 ERR_LDAP_DIFF_CANNOT_CONNECT_TO_SOURCE.get( 1129 StaticUtils.getExceptionMessage(e))); 1130 return e.getResultCode(); 1131 } 1132 1133 try 1134 { 1135 targetPool = createConnectionPool(1, "TargetServer"); 1136 } 1137 catch (final LDAPException e) 1138 { 1139 Debug.debugException(e); 1140 writeCompletionMessage(true, 1141 ERR_LDAP_DIFF_CANNOT_CONNECT_TO_TARGET.get( 1142 StaticUtils.getExceptionMessage(e))); 1143 return e.getResultCode(); 1144 } 1145 1146 1147 // Get the schema that we'll use for matching operations. Retrieve it 1148 // from the target server. 1149 Schema schema = null; 1150 try 1151 { 1152 schema = targetPool.getSchema(); 1153 } 1154 catch (final Exception e) 1155 { 1156 Debug.debugException(e); 1157 } 1158 1159 1160 // Get the base DN to use when identifying entries to compare. Use the 1161 // schema if possible. 1162 DN baseDN; 1163 try 1164 { 1165 baseDN = new DN(baseDNArg.getStringValue(), schema); 1166 } 1167 catch (final Exception e) 1168 { 1169 Debug.debugException(e); 1170 baseDN = baseDNArg.getValue(); 1171 } 1172 1173 1174 // Get a set containing the DNs of the entries to examine from each of the 1175 // servers. 1176 final TreeSet<LDAPDiffCompactDN> dnsToExamine; 1177 try 1178 { 1179 dnsToExamine = getDNsToExamine(sourcePool, targetPool, baseDN, schema); 1180 } 1181 catch (final LDAPException e) 1182 { 1183 Debug.debugException(e); 1184 writeCompletionMessage(true, e.getMessage()); 1185 return e.getResultCode(); 1186 } 1187 1188 1189 // Compare the entries in each server and write the results. 1190 try 1191 { 1192 final AtomicReference<ResultCode> resultCodeRef = 1193 new AtomicReference<>(); 1194 final long[] entryCounts = identifyDifferences(sourcePool, targetPool, 1195 baseDN, schema, resultCodeRef, dnsToExamine); 1196 final long inSyncCount = entryCounts[0]; 1197 final long addCount = entryCounts[1]; 1198 final long delCount = entryCounts[2]; 1199 final long modCount = entryCounts[3]; 1200 final long missingCount = entryCounts[4]; 1201 final long errorCount = entryCounts[5]; 1202 final long totalDifferenceCount = addCount + delCount + modCount; 1203 final long totalExaminedCount = inSyncCount + totalDifferenceCount; 1204 1205 if (! quietArg.isPresent()) 1206 { 1207 out(); 1208 } 1209 1210 wrapOut(0, WRAP_COLUMN, 1211 INFO_LDAP_DIFF_SUMMARY_PROCESSING_COMPLETE.get(getToolName())); 1212 out(); 1213 1214 wrapOut(0, WRAP_COLUMN, 1215 INFO_LDAP_DIFF_SUMMARY_TOTAL_EXAMINED.get(totalExaminedCount)); 1216 wrapOut(0, WRAP_COLUMN, 1217 INFO_LDAP_DIFF_SUMMARY_ADD_COUNT.get(addCount)); 1218 wrapOut(0, WRAP_COLUMN, 1219 INFO_LDAP_DIFF_SUMMARY_DEL_COUNT.get(delCount)); 1220 wrapOut(0, WRAP_COLUMN, 1221 INFO_LDAP_DIFF_SUMMARY_MOD_COUNT.get(modCount)); 1222 wrapOut(0, WRAP_COLUMN, 1223 INFO_LDAP_DIFF_SUMMARY_IN_SYNC_COUNT.get(inSyncCount)); 1224 1225 if (missingCount > 0) 1226 { 1227 wrapOut(0, WRAP_COLUMN, 1228 INFO_LDAP_DIFF_SUMMARY_MISSING_COUNT.get(missingCount)); 1229 } 1230 1231 if (errorCount > 0) 1232 { 1233 wrapOut(0, WRAP_COLUMN, 1234 INFO_LDAP_DIFF_SUMMARY_ERROR_COUNT.get(errorCount)); 1235 } 1236 1237 out(); 1238 1239 if (errorCount > 0) 1240 { 1241 writeCompletionMessage(false, 1242 INFO_LDAP_DIFF_ERRORS_IDENTIFYING_ENTRIES.get()); 1243 resultCodeRef.compareAndSet(null, ResultCode.LOCAL_ERROR); 1244 return resultCodeRef.get(); 1245 } 1246 else if (totalDifferenceCount == 0) 1247 { 1248 writeCompletionMessage(false, 1249 INFO_LDAP_DIFF_SERVERS_IN_SYNC.get()); 1250 resultCodeRef.compareAndSet(null, ResultCode.SUCCESS); 1251 return resultCodeRef.get(); 1252 } 1253 else 1254 { 1255 if (totalDifferenceCount == 1) 1256 { 1257 writeCompletionMessage(true, 1258 WARN_LDAP_DIFF_DIFFERENCE_FOUND.get()); 1259 } 1260 else 1261 { 1262 writeCompletionMessage(true, 1263 WARN_LDAP_DIFF_DIFFERENCES_FOUND.get(totalDifferenceCount)); 1264 } 1265 1266 resultCodeRef.compareAndSet(null, ResultCode.COMPARE_FALSE); 1267 return resultCodeRef.get(); 1268 } 1269 } 1270 catch (final LDAPException e) 1271 { 1272 Debug.debugException(e); 1273 writeCompletionMessage(true, 1274 ERR_LDAP_DIFF_ERROR_IDENTIFYING_DIFFERENCES.get( 1275 StaticUtils.getExceptionMessage(e))); 1276 return e.getResultCode(); 1277 } 1278 } 1279 finally 1280 { 1281 if (sourcePool != null) 1282 { 1283 sourcePool.close(); 1284 } 1285 1286 if (targetPool != null) 1287 { 1288 targetPool.close(); 1289 } 1290 } 1291 } 1292 1293 1294 1295 /** 1296 * Creates a connection pool that is established to the sever with the 1297 * indicated index. 1298 * 1299 * @param serverIndex The index of the server to which the pool should be 1300 * established. 1301 * @param name The name to use for the connection pool. It must not 1302 * be {@code null}. 1303 * 1304 * @return The connection pool that was created. 1305 * 1306 * @throws LDAPException If a problem occurs while creating the connection 1307 * pool. 1308 */ 1309 @NotNull() 1310 private LDAPConnectionPool createConnectionPool(final int serverIndex, 1311 @NotNull final String name) 1312 throws LDAPException 1313 { 1314 final LDAPConnectionPool pool = getConnectionPool(serverIndex, 1, 1315 numThreadsArg.getValue()); 1316 pool.setRetryFailedOperationsDueToInvalidConnections(true); 1317 pool.setConnectionPoolName(name); 1318 return pool; 1319 } 1320 1321 1322 1323 /** 1324 * Writes the provided message to standard output or standard error and sets 1325 * it as the tool completion message. 1326 * 1327 * @param isError Indicates whether the message represents an error 1328 * condition. 1329 * @param message The message to be written and set as the tool completion 1330 * message. It must not be {@code null}. 1331 */ 1332 private void writeCompletionMessage(final boolean isError, 1333 @NotNull final String message) 1334 { 1335 if (isError) 1336 { 1337 wrapErr(0, WRAP_COLUMN, message); 1338 } 1339 else 1340 { 1341 wrapOut(0, WRAP_COLUMN, message); 1342 } 1343 1344 toolCompletionMessageRef.compareAndSet(null, message); 1345 } 1346 1347 1348 1349 /** 1350 * Retrieves an ordered set of the DNs of the entries to examine from each of 1351 * the servers. This will be done in parallel. 1352 * 1353 * @param sourcePool A connection pool that may be used to communicate with 1354 * the source server. It must not be {@code null}. 1355 * @param targetPool A connection pool that may be used to communicate with 1356 * the target server. It must not be {@code null}. 1357 * @param baseDN The base DN for entries to examine. It must not be 1358 * {@code null}. 1359 * @param schema The schema to use during processing. It may optionally 1360 * be {@code null} if no schema is available. 1361 * 1362 * @return An ordered set of the DNs of the entries to exazmine from each of 1363 * the servers. 1364 * 1365 * @throws LDAPException If a problem is encountered while obtaining the 1366 * set of DNs from the source or target server. 1367 */ 1368 @NotNull() 1369 private TreeSet<LDAPDiffCompactDN> getDNsToExamine( 1370 @NotNull final LDAPConnectionPool sourcePool, 1371 @NotNull final LDAPConnectionPool targetPool, 1372 @NotNull final DN baseDN, 1373 @Nullable final Schema schema) 1374 throws LDAPException 1375 { 1376 if (! quietArg.isPresent()) 1377 { 1378 wrapOut(0, WRAP_COLUMN, 1379 INFO_LDAP_DIFF_IDENTIFYING_ENTRIES.get()); 1380 } 1381 1382 final TreeSet<LDAPDiffCompactDN> dnSet = new TreeSet<>(); 1383 final LDAPDiffDNDumper sourceDNDumper = new LDAPDiffDNDumper(this, 1384 "LDAPDiff Source Server DN Dumper", sourceDNsFileArg.getValue(), 1385 sourcePool, baseDN, searchScopeArg.getValue(), 1386 excludeBranchArg.getValues(), searchFilterArg.getValue(), schema, 1387 missingOnlyArg.isPresent(), quietArg.isPresent(), dnSet); 1388 sourceDNDumper.start(); 1389 1390 final LDAPDiffDNDumper targetDNDumper = new LDAPDiffDNDumper(this, 1391 "LDAPDiff Target Server DN Dumper", targetDNsFileArg.getValue(), 1392 targetPool, baseDN, searchScopeArg.getValue(), 1393 excludeBranchArg.getValues(), searchFilterArg.getValue(), schema, 1394 missingOnlyArg.isPresent(), quietArg.isPresent(), dnSet); 1395 targetDNDumper.start(); 1396 1397 try 1398 { 1399 sourceDNDumper.join(); 1400 } 1401 catch (final Exception e) 1402 { 1403 Debug.debugException(e); 1404 throw new LDAPException(ResultCode.LOCAL_ERROR, 1405 ERR_LDAP_DIFF_ERROR_GETTING_SOURCE_DNS.get( 1406 StaticUtils.getExceptionMessage(e))); 1407 } 1408 1409 final LDAPException sourceException = 1410 sourceDNDumper.getProcessingException(); 1411 if (sourceException != null) 1412 { 1413 throw new LDAPException(sourceException.getResultCode(), 1414 ERR_LDAP_DIFF_ERROR_GETTING_SOURCE_DNS.get( 1415 sourceException.getMessage()), 1416 sourceException); 1417 } 1418 1419 try 1420 { 1421 targetDNDumper.join(); 1422 } 1423 catch (final Exception e) 1424 { 1425 Debug.debugException(e); 1426 throw new LDAPException(ResultCode.LOCAL_ERROR, 1427 ERR_LDAP_DIFF_ERROR_GETTING_TARGET_DNS.get( 1428 StaticUtils.getExceptionMessage(e))); 1429 } 1430 1431 final LDAPException targetException = 1432 targetDNDumper.getProcessingException(); 1433 if (targetException != null) 1434 { 1435 throw new LDAPException(targetException.getResultCode(), 1436 ERR_LDAP_DIFF_ERROR_GETTING_TARGET_DNS.get( 1437 targetException.getMessage()), 1438 targetException); 1439 } 1440 1441 if (! quietArg.isPresent()) 1442 { 1443 wrapOut(0, WRAP_COLUMN, 1444 INFO_LDAP_DIFF_IDENTIFIED_ENTRIES.get(dnSet.size())); 1445 } 1446 1447 return dnSet; 1448 } 1449 1450 1451 1452 /** 1453 * Examines all of the entries in the provided set and identifies differences 1454 * between the source and target servers. The differences will be written to 1455 * output files, and the return value will provide information about the 1456 * number of entries in each result category. 1457 * 1458 * @param sourcePool A connection pool that may be used to communicate 1459 * with the source server. It must not be 1460 * {@code null}. 1461 * @param targetPool A connection pool that may be used to communicate 1462 * with the target server. It must not be 1463 * {@code null}. 1464 * @param baseDN The base DN for entries to examine. It must not be 1465 * {@code null}. 1466 * @param schema The schema to use in processing. It may optionally 1467 * be {@code null} if no schema is available. 1468 * @param resultCodeRef A reference that may be updated to set the result 1469 * code that should be returned. It must not be 1470 * {@code null} but may be unset. 1471 * @param dnsToExamine The set of DNs to examine. It must not be 1472 * {@code null}. 1473 * 1474 * @return An array of {@code long} values that provide the number of entries 1475 * in each result category. The array that is returned will contain 1476 * six elements. The first will be the number of entries that were 1477 * found to be in sync between the source and target servers. The 1478 * second will be the number of entries that were present only in the 1479 * target server and need to be added to the source server. The 1480 * third will be the number of entries that were present only in the 1481 * source server and need to be removed. The fourth will be the 1482 * number of entries that were present in both servers but were not 1483 * equivalent and therefore need to be modified in the source server. 1484 * The fifth will be the number of entries that were initially 1485 * identified but were subsequently not found in either server. The 1486 * sixth element will be the number of errors encountered while 1487 * attempting to examine entries. 1488 * 1489 * @throws LDAPException If an unrecoverable error occurs during processing. 1490 */ 1491 @NotNull() 1492 private long[] identifyDifferences( 1493 @NotNull final LDAPConnectionPool sourcePool, 1494 @NotNull final LDAPConnectionPool targetPool, 1495 @NotNull final DN baseDN, 1496 @Nullable final Schema schema, 1497 @NotNull final AtomicReference<ResultCode> resultCodeRef, 1498 @NotNull final TreeSet<LDAPDiffCompactDN> dnsToExamine) 1499 throws LDAPException 1500 { 1501 // Create LDIF writers that will be used to write the output files. We want 1502 // to create the main output file even if we don't end up identifying any 1503 // changes, and it's also convenient to just go ahead and create the 1504 // temporary add and modify files now, too, even if we don't end up using 1505 // them. 1506 final File mergedOutputFile = outputLDIFArg.getValue(); 1507 1508 final File addFile = new File(mergedOutputFile.getAbsolutePath() + ".add"); 1509 addFile.deleteOnExit(); 1510 1511 final File modFile = new File(mergedOutputFile.getAbsolutePath() + ".mod"); 1512 modFile.deleteOnExit(); 1513 1514 long inSyncCount = 0L; 1515 long addCount = 0L; 1516 long deleteCount = 0L; 1517 long modifyCount = 0L; 1518 long missingCount = 0L; 1519 long errorCount = 0L; 1520 ParallelProcessor<LDAPDiffCompactDN,LDAPDiffProcessorResult> 1521 parallelProcessor = null; 1522 final String sourceHostPort = 1523 getServerHostPort("sourceHostname", "sourcePort"); 1524 final String targetHostPort = 1525 getServerHostPort("targetHostname", "targetPort"); 1526 final TreeSet<LDAPDiffCompactDN> missingEntryDNs = new TreeSet<>(); 1527 try (LDIFWriter mergedWriter = createLDIFWriter(mergedOutputFile, 1528 INFO_LDAP_DIFF_MERGED_FILE_COMMENT.get(sourceHostPort, 1529 targetHostPort)); 1530 LDIFWriter addWriter = createLDIFWriter(addFile); 1531 LDIFWriter modWriter = createLDIFWriter(modFile)) 1532 { 1533 // Create a parallel processor that will be used to retrieve and compare 1534 // entries from the source and target servers. 1535 final String[] attributes = 1536 parser.getTrailingArguments().toArray(StaticUtils.NO_STRINGS); 1537 1538 final LDAPDiffProcessor processor = new LDAPDiffProcessor(sourcePool, 1539 targetPool, baseDN, schema, byteForByteArg.isPresent(), attributes, 1540 missingOnlyArg.isPresent()); 1541 1542 parallelProcessor = new ParallelProcessor<>(processor, 1543 new LDAPSDKThreadFactory("LDAPDiff Compare Processor", true), 1544 numThreadsArg.getValue(), 5); 1545 1546 1547 // Define variables that will be used to monitor progress and keep track 1548 // of information between passes. 1549 TreeSet<LDAPDiffCompactDN> currentPassDNs = dnsToExamine; 1550 TreeSet<LDAPDiffCompactDN> nextPassDNs = new TreeSet<>(); 1551 final TreeSet<LDAPDiffCompactDN> deletedEntryDNs = new TreeSet<>(); 1552 final List<LDAPDiffCompactDN> currentBatchOfDNs = 1553 new ArrayList<>(MAX_ENTRIES_PER_BATCH); 1554 1555 1556 // Process each pass, or until we confirm that there aren't any changes 1557 // between the source and target servers. 1558 for (int i=1; i <= numPassesArg.getValue(); i++) 1559 { 1560 final boolean isLastPass = (i == numPassesArg.getValue()); 1561 1562 if (! quietArg.isPresent()) 1563 { 1564 out(); 1565 wrapOut(0, WRAP_COLUMN, 1566 INFO_LDAP_DIFF_STARTING_COMPARE_PASS.get(i, 1567 numPassesArg.getValue(), currentPassDNs.size())); 1568 } 1569 1570 1571 // Process the changes in batches until we have gone through all of the 1572 // entries. 1573 nextPassDNs.clear(); 1574 int differencesIdentifiedCount = 0; 1575 int processedCurrentPassCount = 0; 1576 final int totalCurrentPassCount = currentPassDNs.size(); 1577 final Iterator<LDAPDiffCompactDN> dnIterator = 1578 currentPassDNs.iterator(); 1579 while (dnIterator.hasNext()) 1580 { 1581 // Build a batch of DNs. 1582 currentBatchOfDNs.clear(); 1583 while (dnIterator.hasNext()) 1584 { 1585 currentBatchOfDNs.add(dnIterator.next()); 1586 dnIterator.remove(); 1587 1588 if (currentBatchOfDNs.size() >= MAX_ENTRIES_PER_BATCH) 1589 { 1590 break; 1591 } 1592 } 1593 1594 // Process the batch of entries. 1595 final List<Result<LDAPDiffCompactDN,LDAPDiffProcessorResult>> results; 1596 try 1597 { 1598 results = parallelProcessor.processAll(currentBatchOfDNs); 1599 } 1600 catch (final Exception e) 1601 { 1602 Debug.debugException(e); 1603 throw new LDAPException(ResultCode.LOCAL_ERROR, 1604 ERR_LDAP_DIFF_ERROR_PROCESSING_BATCH.get( 1605 StaticUtils.getExceptionMessage(e)), 1606 e); 1607 } 1608 1609 // Iterate through and handle the results. 1610 for (final Result<LDAPDiffCompactDN,LDAPDiffProcessorResult> result : 1611 results) 1612 { 1613 processedCurrentPassCount++; 1614 1615 final Throwable exception = result.getFailureCause(); 1616 if (exception != null) 1617 { 1618 final LDAPDiffCompactDN compactDN = result.getInput(); 1619 if (! isLastPass) 1620 { 1621 nextPassDNs.add(compactDN); 1622 differencesIdentifiedCount++; 1623 } 1624 else 1625 { 1626 final LDAPException reportException; 1627 if (exception instanceof LDAPException) 1628 { 1629 final LDAPException caughtException = 1630 (LDAPException) exception; 1631 reportException = new LDAPException( 1632 caughtException.getResultCode(), 1633 ERR_LDAP_DIFF_ERROR_COMPARING_ENTRY.get( 1634 compactDN.toDN(baseDN, schema).toString(), 1635 caughtException.getMessage()), 1636 caughtException.getMatchedDN(), 1637 caughtException.getReferralURLs(), 1638 caughtException.getResponseControls(), 1639 caughtException.getCause()); 1640 } 1641 else 1642 { 1643 reportException = new LDAPException(ResultCode.LOCAL_ERROR, 1644 ERR_LDAP_DIFF_ERROR_COMPARING_ENTRY.get( 1645 compactDN.toDN(baseDN, schema).toString(), 1646 StaticUtils.getExceptionMessage(exception)), 1647 exception); 1648 } 1649 1650 errorCount++; 1651 resultCodeRef.compareAndSet(null, 1652 reportException.getResultCode()); 1653 1654 final List<String> formattedResultLines = 1655 ResultUtils.formatResult(reportException, false, 0, 1656 (WRAP_COLUMN - 2)); 1657 final Iterator<String> resultLineIterator = 1658 formattedResultLines.iterator(); 1659 while (resultLineIterator.hasNext()) 1660 { 1661 mergedWriter.writeComment(resultLineIterator.next(), false, 1662 (! resultLineIterator.hasNext())); 1663 } 1664 } 1665 1666 continue; 1667 } 1668 1669 final LDAPDiffProcessorResult resultOutput = result.getOutput(); 1670 final ChangeType changeType = resultOutput.getChangeType(); 1671 if (changeType == null) 1672 { 1673 // This indicates that either the entry is in sync between the 1674 // source and target servers or that it was missing from both 1675 // servers. If it's the former, then we just need to increment 1676 // a counter. If it's the latter, then we also need to hold onto 1677 // the DN for including in a comment at the end of the LDIF file. 1678 if (resultOutput.isEntryMissing()) 1679 { 1680 missingCount++; 1681 missingEntryDNs.add(result.getInput()); 1682 } 1683 else 1684 { 1685 inSyncCount++; 1686 } 1687 1688 // This indicates that the entry is in sync between the source 1689 // and target servers. We don't need to do anything in this case. 1690 inSyncCount++; 1691 } 1692 else if (! isLastPass) 1693 { 1694 // This entry is out of sync, but this isn't the last pass, so 1695 // just hold on to the DN so that we'll re-examine the entry on 1696 // the next pass. 1697 nextPassDNs.add(result.getInput()); 1698 differencesIdentifiedCount++; 1699 } 1700 else 1701 { 1702 // The entry is out of sync, and this is the last pass. If the 1703 // entry should be deleted, then capture the DN in a sorted list. 1704 // If it's an add or modify, then write it to an appropriate 1705 // temporary file. In each case, update the appropriate counter. 1706 differencesIdentifiedCount++; 1707 switch (changeType) 1708 { 1709 case DELETE: 1710 deletedEntryDNs.add(result.getInput()); 1711 deleteCount++; 1712 break; 1713 1714 case ADD: 1715 addWriter.writeChangeRecord( 1716 new LDIFAddChangeRecord(resultOutput.getEntry()), 1717 WARN_LDAP_DIFF_COMMENT_ADDED_ENTRY.get(targetHostPort, 1718 sourceHostPort)); 1719 addCount++; 1720 break; 1721 1722 case MODIFY: 1723 default: 1724 modWriter.writeChangeRecord( 1725 new LDIFModifyChangeRecord(resultOutput.getDN(), 1726 resultOutput.getModifications()), 1727 WARN_LDAP_DIFF_COMMENT_MODIFIED_ENTRY.get(sourceHostPort, 1728 targetHostPort)); 1729 modifyCount++; 1730 break; 1731 } 1732 } 1733 } 1734 1735 // Write a progress message. 1736 if (! quietArg.isPresent()) 1737 { 1738 final int percentComplete = Math.round(100.0f * 1739 processedCurrentPassCount / totalCurrentPassCount); 1740 wrapOut(0, WRAP_COLUMN, 1741 INFO_LDAP_DIFF_COMPARE_PROGRESS.get(processedCurrentPassCount, 1742 totalCurrentPassCount, percentComplete, 1743 differencesIdentifiedCount)); 1744 } 1745 } 1746 1747 1748 // If this isn't the last pass, and if there are still outstanding 1749 // differences, then sleep before the next iteration. 1750 if (isLastPass) 1751 { 1752 break; 1753 } 1754 else if (nextPassDNs.isEmpty()) 1755 { 1756 if (! quietArg.isPresent()) 1757 { 1758 wrapOut(0, WRAP_COLUMN, 1759 INFO_LDAP_DIFF_NO_NEED_FOR_ADDITIONAL_PASS.get()); 1760 } 1761 break; 1762 } 1763 else 1764 { 1765 try 1766 { 1767 final int sleepTimeSeconds = secondsBetweenPassesArg.getValue(); 1768 if (! quietArg.isPresent()) 1769 { 1770 wrapOut(0, WRAP_COLUMN, 1771 INFO_LDAP_DIFF_WAITING_BEFORE_NEXT_PASS.get( 1772 sleepTimeSeconds)); 1773 } 1774 1775 Thread.sleep(TimeUnit.SECONDS.toMillis(sleepTimeSeconds)); 1776 } 1777 catch (final Exception e) 1778 { 1779 Debug.debugException(e); 1780 } 1781 } 1782 1783 1784 // Swap currentPassDNs (which will now be empty) and nextPassDN (which 1785 // contains the DNs of entries that were found out of sync in the 1786 // current pass) sets so that they will be correct for the next pass. 1787 final TreeSet<LDAPDiffCompactDN> emptyDNSet = currentPassDNs; 1788 currentPassDNs = nextPassDNs; 1789 nextPassDNs = emptyDNSet; 1790 } 1791 1792 1793 // If we've gotten here, then we've completed all of the passes. If no 1794 // differences were identified, then write a comment indicating that to 1795 // the end of the LDIF file. 1796 if ((addCount == 0) && (deleteCount == 0) && (modifyCount == 0)) 1797 { 1798 mergedWriter.writeComment(INFO_LDAP_DIFF_SERVERS_IN_SYNC.get(), true, 1799 false); 1800 } 1801 1802 1803 // If we've gotten here, then we've completed all of the passes. If we've 1804 // identified any deleted entries, then add them to the output first (in 1805 // descending order so that children are deleted before parents). The 1806 // modify and add records will be added later, after we've closed all of 1807 // the writers. 1808 if (! deletedEntryDNs.isEmpty()) 1809 { 1810 mergedWriter.writeComment(INFO_LDAP_DIFF_COMMENT_DELETED_ENTRIES.get(), 1811 true, true); 1812 1813 if (! quietArg.isPresent()) 1814 { 1815 out(); 1816 wrapOut(0, WRAP_COLUMN, 1817 INFO_LDAP_DIFF_STARTING_DELETE_PASS.get(deleteCount)); 1818 } 1819 1820 int entryCount = 0; 1821 for (final LDAPDiffCompactDN compactDN : 1822 deletedEntryDNs.descendingSet()) 1823 { 1824 SearchResultEntry entry = null; 1825 LDAPException ldapException = null; 1826 final String dnString = compactDN.toDN(baseDN, schema).toString(); 1827 try 1828 { 1829 entry = sourcePool.getEntry(dnString, attributes); 1830 } 1831 catch (final LDAPException e) 1832 { 1833 Debug.debugException(e); 1834 ldapException = new LDAPException(e.getResultCode(), 1835 ERR_LDAP_DIFF_CANNOT_GET_ENTRY_TO_DELETE.get(dnString, 1836 StaticUtils.getExceptionMessage(e)), 1837 e); 1838 } 1839 1840 if (entry != null) 1841 { 1842 mergedWriter.writeComment( 1843 INFO_LDAP_DIFF_COMMENT_DELETED_ENTRY.get(sourceHostPort, 1844 targetHostPort), 1845 false, false); 1846 mergedWriter.writeComment("", false, false); 1847 for (final String line : entry.toLDIF(75)) 1848 { 1849 mergedWriter.writeComment(line, false, false); 1850 } 1851 1852 mergedWriter.writeChangeRecord( 1853 new LDIFDeleteChangeRecord(entry.getDN())); 1854 } 1855 else if (ldapException != null) 1856 { 1857 mergedWriter.writeComment(ldapException.getExceptionMessage(), 1858 false, false); 1859 mergedWriter.writeChangeRecord( 1860 new LDIFDeleteChangeRecord(entry.getDN())); 1861 } 1862 1863 entryCount++; 1864 if ((! quietArg.isPresent()) && 1865 ((entryCount % MAX_ENTRIES_PER_BATCH) == 0)) 1866 { 1867 final int percentComplete = 1868 Math.round(100.0f * entryCount / deleteCount); 1869 wrapOut(0, WRAP_COLUMN, 1870 INFO_LDAP_DIFF_DELETE_PROGRESS.get(entryCount, 1871 deleteCount, percentComplete)); 1872 } 1873 } 1874 1875 if (! quietArg.isPresent()) 1876 { 1877 final int percentComplete = 1878 Math.round(100.0f * entryCount / deleteCount); 1879 wrapOut(0, WRAP_COLUMN, 1880 INFO_LDAP_DIFF_DELETE_PROGRESS.get(entryCount, deleteCount, 1881 percentComplete)); 1882 } 1883 } 1884 } 1885 catch (final IOException e) 1886 { 1887 Debug.debugException(e); 1888 throw new LDAPException(ResultCode.LOCAL_ERROR, 1889 ERR_LDAP_DIFF_ERROR_WRITING_OUTPUT.get(getToolName(), 1890 StaticUtils.getExceptionMessage(e)), 1891 e); 1892 } 1893 finally 1894 { 1895 if (parallelProcessor != null) 1896 { 1897 try 1898 { 1899 parallelProcessor.shutdown(); 1900 } 1901 catch (final Exception e) 1902 { 1903 Debug.debugException(e); 1904 } 1905 } 1906 } 1907 1908 1909 // If any modified entries were identified, then append the modify LDIF 1910 // file to the merged change file. 1911 if (modifyCount > 0L) 1912 { 1913 appendFileToFile(modFile, mergedOutputFile, 1914 INFO_LDAP_DIFF_COMMENT_ADDED_ENTRIES.get()); 1915 modFile.delete(); 1916 } 1917 1918 1919 // If any added entries were identified, then append the add LDIF file to 1920 // the merged change file. 1921 if (addCount > 0L) 1922 { 1923 appendFileToFile(addFile, mergedOutputFile, 1924 INFO_LDAP_DIFF_COMMENT_MODIFIED_ENTRIES.get()); 1925 addFile.delete(); 1926 } 1927 1928 1929 // If there are any missing entries, then update the merged LDIF file to 1930 // list them. 1931 if (! missingEntryDNs.isEmpty()) 1932 { 1933 try (FileOutputStream outputStream = 1934 new FileOutputStream(mergedOutputFile, true); 1935 LDIFWriter ldifWriter = new LDIFWriter(outputStream)) 1936 { 1937 ldifWriter.writeComment(INFO_LDAP_DIFF_COMMENT_MISSING_ENTRIES.get(), 1938 true, true); 1939 for (final LDAPDiffCompactDN missingEntryDN : missingEntryDNs) 1940 { 1941 ldifWriter.writeComment( 1942 INFO_LDAP_DIFF_COMMENT_MISSING_ENTRY.get(missingEntryDN.toDN( 1943 baseDN, schema).toString()), 1944 false, true); 1945 } 1946 } 1947 catch (final Exception e) 1948 { 1949 Debug.debugException(e); 1950 throw new LDAPException(ResultCode.LOCAL_ERROR, 1951 ERR_LDAP_DIFF_ERROR_WRITING_OUTPUT.get(getToolName(), 1952 StaticUtils.getExceptionMessage(e)), 1953 e); 1954 } 1955 } 1956 1957 return new long[] 1958 { 1959 inSyncCount, 1960 addCount, 1961 deleteCount, 1962 modifyCount, 1963 missingCount, 1964 errorCount 1965 }; 1966 } 1967 1968 1969 1970 /** 1971 * Retrieves a string representation of the address and port for the server 1972 * identified by the specified arguments. 1973 * 1974 * @param hostnameArgName The name of the argument used to specify the 1975 * hostname for the target server. It must not be 1976 * {@code null}. 1977 * @param portArgName The name of the argument used to specify the port 1978 * of the target server. It must not be 1979 * {@code null}. 1980 * 1981 * @return A string representation of the address and port for the server 1982 * identified by the specified arguments. 1983 */ 1984 @NotNull() 1985 private String getServerHostPort(@NotNull final String hostnameArgName, 1986 @NotNull final String portArgName) 1987 { 1988 final StringArgument hostnameArg = 1989 parser.getStringArgument(hostnameArgName); 1990 final IntegerArgument portArg = parser.getIntegerArgument(portArgName); 1991 return hostnameArg.getValue() + ':' + portArg.getValue(); 1992 } 1993 1994 1995 1996 /** 1997 * Creates the LDIF writer that will be used when writing identified 1998 * differences. 1999 * 2000 * @param ldifFile The LDIF file to be written. It must not be 2001 * {@code null}. 2002 * @param comments The set of comments to be included at the top of the 2003 * file. It must not be {@code null} but may be empty. 2004 * 2005 * @return The LDIF writer that was created. 2006 * 2007 * @throws LDAPException If a problem occurs while creating the LDIF writer. 2008 */ 2009 @NotNull() 2010 private LDIFWriter createLDIFWriter(@NotNull final File ldifFile, 2011 @NotNull final String... comments) 2012 throws LDAPException 2013 { 2014 try 2015 { 2016 final LDIFWriter writer = new LDIFWriter(ldifFile); 2017 writer.setWrapColumn(wrapColumnArg.getValue()); 2018 2019 for (final String comment : comments) 2020 { 2021 writer.writeComment(comment, false, true); 2022 } 2023 2024 return writer; 2025 } 2026 catch (final Exception e) 2027 { 2028 Debug.debugException(e); 2029 throw new LDAPException(ResultCode.LOCAL_ERROR, 2030 ERR_LDAP_DIFF_CANNOT_CREATE_LDIF_WRITER.get( 2031 ldifFile.getAbsolutePath(), 2032 StaticUtils.getExceptionMessage(e)), 2033 e); 2034 } 2035 } 2036 2037 2038 2039 /** 2040 * Appends the contents of the specified file to the end of the indicated 2041 * file. 2042 * 2043 * @param fileToAppend The file whose contents should be appended to 2044 * the end of the indicated file. It must not be 2045 * {@code null}, and the file must exist. 2046 * @param fileToBeAppendedTo The file to which the source file should be 2047 * appended. It must not be {@code null}, and the 2048 * file must exist. 2049 * @param comment A comment that should be placed before the 2050 * content of the file to append. It must not be 2051 * {@code null} or empty. 2052 * 2053 * @throws LDAPException If a problem occurs while reading from the file to 2054 * append or writing to the file to be appended to. 2055 */ 2056 private void appendFileToFile(@NotNull final File fileToAppend, 2057 @NotNull final File fileToBeAppendedTo, 2058 @NotNull final String comment) 2059 throws LDAPException 2060 { 2061 try (FileInputStream inputStream = new FileInputStream(fileToAppend); 2062 FileOutputStream outputStream = 2063 new FileOutputStream(fileToBeAppendedTo, true)) 2064 { 2065 outputStream.write(StaticUtils.getBytes(StaticUtils.EOL)); 2066 for (final String line : StaticUtils.wrapLine(comment, (WRAP_COLUMN - 2))) 2067 { 2068 outputStream.write(StaticUtils.getBytes("# " + line + StaticUtils.EOL)); 2069 } 2070 outputStream.write(StaticUtils.getBytes(StaticUtils.EOL)); 2071 2072 final byte[] buffer = new byte[1024 * 1024]; 2073 while (true) 2074 { 2075 final int bytesRead = inputStream.read(buffer); 2076 if (bytesRead < 0) 2077 { 2078 return; 2079 } 2080 2081 outputStream.write(buffer, 0, bytesRead); 2082 } 2083 } 2084 catch (final Exception e) 2085 { 2086 Debug.debugException(e); 2087 throw new LDAPException(ResultCode.LOCAL_ERROR, 2088 ERR_LDAP_DIFF_ERROR_WRITING_OUTPUT.get(getToolName(), 2089 StaticUtils.getExceptionMessage(e)), 2090 e); 2091 } 2092 } 2093 2094 2095 2096 /** 2097 * {@inheritDoc} 2098 */ 2099 @Override() 2100 @NotNull() 2101 public LinkedHashMap<String[],String> getExampleUsages() 2102 { 2103 final LinkedHashMap<String[],String> examples = new LinkedHashMap<>(); 2104 2105 examples.put( 2106 new String[] 2107 { 2108 "--sourceHostname", "source.example.com", 2109 "--sourcePort", "636", 2110 "--sourceUseSSL", 2111 "--sourceBindDN", "cn=Directory Manager", 2112 "--sourceBindPasswordFile", "/path/to/password.txt", 2113 "--targetHostname", "target.example.com", 2114 "--targetPort", "636", 2115 "--targetUseSSL", 2116 "--targetBindDN", "cn=Directory Manager", 2117 "--targetBindPasswordFile", "/path/to/password.txt", 2118 "--baseDN", "dc=example,dc=com", 2119 "--outputLDIF", "diff.ldif" 2120 }, 2121 INFO_LDAP_DIFF_EXAMPLE.get()); 2122 2123 return examples; 2124 } 2125}