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