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}