001/*
002 * Copyright 2020-2022 Ping Identity Corporation
003 * All Rights Reserved.
004 */
005/*
006 * Copyright 2020-2022 Ping Identity Corporation
007 *
008 * Licensed under the Apache License, Version 2.0 (the "License");
009 * you may not use this file except in compliance with the License.
010 * You may obtain a copy of the License at
011 *
012 *    http://www.apache.org/licenses/LICENSE-2.0
013 *
014 * Unless required by applicable law or agreed to in writing, software
015 * distributed under the License is distributed on an "AS IS" BASIS,
016 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
017 * See the License for the specific language governing permissions and
018 * limitations under the License.
019 */
020/*
021 * Copyright (C) 2020-2022 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.BufferedReader;
041import java.io.File;
042import java.io.FileReader;
043import java.io.IOException;
044import java.io.OutputStream;
045import java.io.PrintStream;
046import java.util.ArrayList;
047import java.util.Arrays;
048import java.util.Collections;
049import java.util.Iterator;
050import java.util.LinkedHashMap;
051import java.util.List;
052import java.util.concurrent.atomic.AtomicReference;
053
054import com.unboundid.ldap.sdk.CompareRequest;
055import com.unboundid.ldap.sdk.Control;
056import com.unboundid.ldap.sdk.DN;
057import com.unboundid.ldap.sdk.ExtendedResult;
058import com.unboundid.ldap.sdk.LDAPConnection;
059import com.unboundid.ldap.sdk.LDAPConnectionOptions;
060import com.unboundid.ldap.sdk.LDAPConnectionPool;
061import com.unboundid.ldap.sdk.LDAPException;
062import com.unboundid.ldap.sdk.LDAPResult;
063import com.unboundid.ldap.sdk.ResultCode;
064import com.unboundid.ldap.sdk.UnsolicitedNotificationHandler;
065import com.unboundid.ldap.sdk.Version;
066import com.unboundid.ldap.sdk.controls.AssertionRequestControl;
067import com.unboundid.ldap.sdk.controls.AuthorizationIdentityRequestControl;
068import com.unboundid.ldap.sdk.controls.ManageDsaITRequestControl;
069import com.unboundid.ldap.sdk.controls.ProxiedAuthorizationV1RequestControl;
070import com.unboundid.ldap.sdk.controls.ProxiedAuthorizationV2RequestControl;
071import com.unboundid.ldap.sdk.unboundidds.controls.
072            GetAuthorizationEntryRequestControl;
073import com.unboundid.ldap.sdk.unboundidds.controls.
074            GetUserResourceLimitsRequestControl;
075import com.unboundid.ldap.sdk.unboundidds.controls.
076            OperationPurposeRequestControl;
077import com.unboundid.ldap.sdk.unboundidds.controls.
078            PasswordPolicyRequestControl;
079import com.unboundid.ldap.sdk.unboundidds.extensions.
080            StartAdministrativeSessionExtendedRequest;
081import com.unboundid.ldap.sdk.unboundidds.extensions.
082            StartAdministrativeSessionPostConnectProcessor;
083import com.unboundid.util.Base64;
084import com.unboundid.util.DNFileReader;
085import com.unboundid.util.Debug;
086import com.unboundid.util.LDAPCommandLineTool;
087import com.unboundid.util.NotNull;
088import com.unboundid.util.Nullable;
089import com.unboundid.util.ObjectPair;
090import com.unboundid.util.StaticUtils;
091import com.unboundid.util.ThreadSafety;
092import com.unboundid.util.ThreadSafetyLevel;
093import com.unboundid.util.args.ArgumentException;
094import com.unboundid.util.args.ArgumentParser;
095import com.unboundid.util.args.BooleanArgument;
096import com.unboundid.util.args.ControlArgument;
097import com.unboundid.util.args.DNArgument;
098import com.unboundid.util.args.FileArgument;
099import com.unboundid.util.args.FilterArgument;
100import com.unboundid.util.args.StringArgument;
101
102import static com.unboundid.ldap.sdk.unboundidds.tools.ToolMessages.*;
103
104
105
106/**
107 * This class provide an LDAP command-line tool that may be used to perform
108 * compare operations in an LDAP directory server.
109 * <BR>
110 * <BLOCKQUOTE>
111 *   <B>NOTE:</B>  This class, and other classes within the
112 *   {@code com.unboundid.ldap.sdk.unboundidds} package structure, are only
113 *   supported for use against Ping Identity, UnboundID, and
114 *   Nokia/Alcatel-Lucent 8661 server products.  These classes provide support
115 *   for proprietary functionality or for external specifications that are not
116 *   considered stable or mature enough to be guaranteed to work in an
117 *   interoperable way with other types of LDAP servers.
118 * </BLOCKQUOTE>
119 */
120@ThreadSafety(level=ThreadSafetyLevel.NOT_THREADSAFE)
121public final class LDAPCompare
122       extends LDAPCommandLineTool
123       implements UnsolicitedNotificationHandler
124{
125  /**
126   * The column at which output should be wrapped.
127   */
128  private static final int WRAP_COLUMN = StaticUtils.TERMINAL_WIDTH_COLUMNS - 1;
129
130
131
132  /**
133   * The value used to select the CSV output format.
134   */
135  @NotNull private static final String OUTPUT_FORMAT_CSV = "csv";
136
137
138
139  /**
140   * The value used to select the JSON output format.
141   */
142  @NotNull private static final String OUTPUT_FORMAT_JSON = "json";
143
144
145
146  /**
147   * The value used to select the tab-delimited output format.
148   */
149  @NotNull private static final String OUTPUT_FORMAT_TAB_DELIMITED =
150       "tab-delimited";
151
152
153
154  // A reference to the completion message to return for this tool.
155  @NotNull private final AtomicReference<String> completionMessage;
156
157  // A reference to the argument parser for this tool.
158  @Nullable private ArgumentParser argumentParser;
159
160  // The supported command-line arguments.
161  @Nullable private BooleanArgument authorizationIdentity;
162  @Nullable private BooleanArgument continueOnError;
163  @Nullable private BooleanArgument dryRun;
164  @Nullable private BooleanArgument followReferrals;
165  @Nullable private BooleanArgument getUserResourceLimits;
166  @Nullable private BooleanArgument manageDsaIT;
167  @Nullable private BooleanArgument scriptFriendly;
168  @Nullable private BooleanArgument teeOutput;
169  @Nullable private BooleanArgument terse;
170  @Nullable private BooleanArgument useAdministrativeSession;
171  @Nullable private BooleanArgument useCompareResultCodeAsExitCode;
172  @Nullable private BooleanArgument usePasswordPolicyControl;
173  @Nullable private BooleanArgument verbose;
174  @Nullable private ControlArgument bindControl;
175  @Nullable private ControlArgument compareControl;
176  @Nullable private DNArgument proxyV1As;
177  @Nullable private FileArgument assertionFile;
178  @Nullable private FileArgument dnFile;
179  @Nullable private FileArgument outputFile;
180  @Nullable private FilterArgument assertionFilter;
181  @Nullable private StringArgument getAuthorizationEntryAttribute;
182  @Nullable private StringArgument operationPurpose;
183  @Nullable private StringArgument outputFormat;
184  @Nullable private StringArgument proxyAs;
185
186
187
188  /**
189   * Invokes this tool with the provided set of arguments.  The default standard
190   * output and error streams will be used.
191   *
192   * @param  args  The command-line arguments provided to this program.
193   */
194  public static void main(@NotNull final String... args)
195  {
196    final ResultCode resultCode = main(System.out, System.err, args);
197    if (resultCode != ResultCode.SUCCESS)
198    {
199      System.exit(resultCode.intValue());
200    }
201  }
202
203
204
205  /**
206   * Invokes this tool with the provided set of arguments, and using the
207   * provided streams for standard output and error.
208   *
209   * @param  out   The output stream to use for standard output.  It may be
210   *               {@code null} if standard output should be suppressed.
211   * @param  err   The output stream to use for standard error.  It may be
212   *               {@code null} if standard error should be suppressed.
213   * @param  args  The command-line arguments provided to this program.
214   *
215   * @return  The result code obtained when running the tool.  Any result code
216   *          other than {@link ResultCode#SUCCESS} indicates an error.
217   */
218  @NotNull()
219  public static ResultCode main(@Nullable final OutputStream out,
220                                @Nullable final OutputStream err,
221                                @NotNull final String... args)
222  {
223    final LDAPCompare tool = new LDAPCompare(out, err);
224    return tool.runTool(args);
225  }
226
227
228
229  /**
230   * Creates a new instance of this tool with the provided output and error
231   * streams.
232   *
233   * @param  out  The output stream to use for standard output.  It may be
234   *              {@code null} if standard output should be suppressed.
235   * @param  err  The output stream to use for standard error.  It may be
236   *              {@code null} if standard error should be suppressed.
237   */
238  public LDAPCompare(@Nullable final OutputStream out,
239                     @Nullable final OutputStream err)
240  {
241    super(out, err);
242
243    completionMessage = new AtomicReference<>();
244
245    argumentParser = null;
246
247    authorizationIdentity = null;
248    continueOnError = null;
249    dryRun = null;
250    followReferrals = null;
251    getUserResourceLimits = null;
252    manageDsaIT = null;
253    scriptFriendly = null;
254    teeOutput = null;
255    terse = null;
256    useAdministrativeSession = null;
257    useCompareResultCodeAsExitCode = null;
258    usePasswordPolicyControl = null;
259    verbose = null;
260    bindControl = null;
261    compareControl = null;
262    proxyV1As = null;
263    assertionFile = null;
264    dnFile = null;
265    outputFile  = null;
266    assertionFilter = null;
267    getAuthorizationEntryAttribute = null;
268    operationPurpose = null;
269    outputFormat = null;
270    proxyAs = null;
271  }
272
273
274
275  /**
276   * {@inheritDoc}
277   */
278  @Override()
279  @NotNull()
280  public String getToolName()
281  {
282    return "ldapcompare";
283  }
284
285
286
287  /**
288   * {@inheritDoc}
289   */
290  @Override()
291  @NotNull()
292  public String getToolDescription()
293  {
294    return INFO_LDAPCOMPARE_TOOL_DESCRIPTION_1.get();
295  }
296
297
298
299  /**
300   * {@inheritDoc}
301   */
302  @Override()
303  @NotNull()
304  public List<String> getAdditionalDescriptionParagraphs()
305  {
306    return Collections.unmodifiableList(Arrays.asList(
307         INFO_LDAPCOMPARE_TOOL_DESCRIPTION_2.get(),
308         INFO_LDAPCOMPARE_TOOL_DESCRIPTION_3.get(),
309         INFO_LDAPCOMPARE_TOOL_DESCRIPTION_4.get(),
310         INFO_LDAPCOMPARE_TOOL_DESCRIPTION_5.get()));
311  }
312
313
314
315  /**
316   * {@inheritDoc}
317   */
318  @Override()
319  @NotNull()
320  public String getToolVersion()
321  {
322    return Version.NUMERIC_VERSION_STRING;
323  }
324
325
326
327  /**
328   * {@inheritDoc}
329   */
330  @Override()
331  public int getMinTrailingArguments()
332  {
333    return 0;
334  }
335
336
337
338  /**
339   * {@inheritDoc}
340   */
341  @Override()
342  public int getMaxTrailingArguments()
343  {
344    return -1;
345  }
346
347
348
349  /**
350   * {@inheritDoc}
351   */
352  @Override()
353  @NotNull()
354  public String getTrailingArgumentsPlaceholder()
355  {
356    return INFO_LDAPCOMPARE_TRAILING_ARGS_PLACEHOLDER.get();
357  }
358
359
360
361  /**
362   * {@inheritDoc}
363   */
364  @Override()
365  public boolean supportsInteractiveMode()
366  {
367    return true;
368  }
369
370
371
372  /**
373   * {@inheritDoc}
374   */
375  @Override()
376  public boolean defaultsToInteractiveMode()
377  {
378    return true;
379  }
380
381
382
383  /**
384   * {@inheritDoc}
385   */
386  @Override()
387  public boolean supportsPropertiesFile()
388  {
389    return true;
390  }
391
392
393
394  /**
395   * {@inheritDoc}
396   */
397  @Override()
398  protected boolean supportsAuthentication()
399  {
400    return true;
401  }
402
403
404
405  /**
406   * {@inheritDoc}
407   */
408  @Override()
409  protected boolean defaultToPromptForBindPassword()
410  {
411    return true;
412  }
413
414
415
416  /**
417   * {@inheritDoc}
418   */
419  @Override()
420  protected boolean supportsSASLHelp()
421  {
422    return true;
423  }
424
425
426
427  /**
428   * {@inheritDoc}
429   */
430  @Override()
431  protected boolean includeAlternateLongIdentifiers()
432  {
433    return true;
434  }
435
436
437
438  /**
439   * {@inheritDoc}
440   */
441  @Override()
442  @NotNull()
443  protected List<Control> getBindControls()
444  {
445    final List<Control> bindControls = new ArrayList<>(10);
446
447    if (bindControl.isPresent())
448    {
449      bindControls.addAll(bindControl.getValues());
450    }
451
452    if (authorizationIdentity.isPresent())
453    {
454      bindControls.add(new AuthorizationIdentityRequestControl(false));
455    }
456
457    if (getAuthorizationEntryAttribute.isPresent())
458    {
459      bindControls.add(new GetAuthorizationEntryRequestControl(true, true,
460           getAuthorizationEntryAttribute.getValues()));
461    }
462
463    if (getUserResourceLimits.isPresent())
464    {
465      bindControls.add(new GetUserResourceLimitsRequestControl());
466    }
467
468    if (usePasswordPolicyControl.isPresent())
469    {
470      bindControls.add(new PasswordPolicyRequestControl());
471    }
472
473    return bindControls;
474  }
475
476
477
478  /**
479   * {@inheritDoc}
480   */
481  @Override()
482  protected boolean supportsMultipleServers()
483  {
484    return true;
485  }
486
487
488
489  /**
490   * {@inheritDoc}
491   */
492  @Override()
493  protected boolean supportsSSLDebugging()
494  {
495    return true;
496  }
497
498
499
500  /**
501   * {@inheritDoc}
502   */
503  @Override()
504  @NotNull()
505  public LDAPConnectionOptions getConnectionOptions()
506  {
507    final LDAPConnectionOptions options = new LDAPConnectionOptions();
508
509    options.setUseSynchronousMode(true);
510    options.setFollowReferrals(followReferrals.isPresent());
511    options.setUnsolicitedNotificationHandler(this);
512    options.setResponseTimeoutMillis(0L);
513
514    return options;
515  }
516
517
518
519  /**
520   * {@inheritDoc}
521   */
522  @Override()
523  protected boolean logToolInvocationByDefault()
524  {
525    return false;
526  }
527
528
529
530  /**
531   * {@inheritDoc}
532   */
533  @Override()
534  @Nullable()
535  protected String getToolCompletionMessage()
536  {
537    return completionMessage.get();
538  }
539
540
541
542  /**
543   * {@inheritDoc}
544   */
545  @Override()
546  public void addNonLDAPArguments(@NotNull final ArgumentParser parser)
547         throws ArgumentException
548  {
549    argumentParser = parser;
550
551    // Compare operation processing arguments.
552    dnFile = new FileArgument('f', "dnFile", false, 1, null,
553         INFO_LDAPCOMPARE_ARG_DESCRIPTION_DN_FILE.get(), true, true, true,
554         false);
555    dnFile.addLongIdentifier("dn-file", true);
556    dnFile.addLongIdentifier("filename", true);
557    dnFile.setArgumentGroupName(INFO_LDAPCOMPARE_ARG_GROUP_PROCESSING.get());
558    parser.addArgument(dnFile);
559
560    assertionFile = new FileArgument(null, "assertionFile", false, 1, null,
561         INFO_LDAPCOMPARE_ARG_DESCRIPTION_ASSERTION_FILE.get(), true, true,
562         true, false);
563    assertionFile.addLongIdentifier("assertion-file", true);
564    assertionFile.setArgumentGroupName(
565         INFO_LDAPCOMPARE_ARG_GROUP_PROCESSING.get());
566    parser.addArgument(assertionFile);
567
568    followReferrals = new BooleanArgument(null, "followReferrals", 1,
569         INFO_LDAPCOMPARE_ARG_DESCRIPTION_FOLLOW_REFERRALS.get());
570    followReferrals.addLongIdentifier("follow-referrals", true);
571    followReferrals.setArgumentGroupName(
572         INFO_LDAPCOMPARE_ARG_GROUP_PROCESSING.get());
573    parser.addArgument(followReferrals);
574
575    useAdministrativeSession = new BooleanArgument(null,
576         "useAdministrativeSession", 1,
577         INFO_LDAPCOMPARE_ARG_DESCRIPTION_USE_ADMIN_SESSION.get());
578    useAdministrativeSession.addLongIdentifier("use-administrative-session",
579         true);
580    useAdministrativeSession.setArgumentGroupName(
581         INFO_LDAPCOMPARE_ARG_GROUP_PROCESSING.get());
582    parser.addArgument(useAdministrativeSession);
583
584    continueOnError = new BooleanArgument('c', "continueOnError", 1,
585         INFO_LDAPCOMPARE_ARG_DESCRIPTION_CONTINUE_ON_ERROR.get());
586    continueOnError.addLongIdentifier("continue-on-error", true);
587    continueOnError.setArgumentGroupName(
588         INFO_LDAPCOMPARE_ARG_GROUP_PROCESSING.get());
589    parser.addArgument(continueOnError);
590
591    dryRun = new BooleanArgument('n', "dryRun", 1,
592         INFO_LDAPCOMPARE_ARG_DESCRIPTION_DRY_RUN.get());
593    dryRun.addLongIdentifier("dry-run", true);
594    dryRun.setArgumentGroupName(INFO_LDAPCOMPARE_ARG_GROUP_PROCESSING.get());
595    parser.addArgument(dryRun);
596
597
598    // Bind control arguments.
599    bindControl = new ControlArgument(null, "bindControl", false, 0, null,
600         INFO_LDAPCOMPARE_ARG_DESCRIPTION_BIND_CONTROL.get());
601    bindControl.addLongIdentifier("bind-control", true);
602    bindControl.setArgumentGroupName(
603         INFO_LDAPCOMPARE_ARG_GROUP_BIND_CONTROLS.get());
604    parser.addArgument(bindControl);
605
606    authorizationIdentity = new BooleanArgument('E', "authorizationIdentity", 1,
607         INFO_LDAPCOMPARE_ARG_DESCRIPTION_AUTHZ_IDENTITY.get());
608    authorizationIdentity.addLongIdentifier("authorization-identity", true);
609    authorizationIdentity.addLongIdentifier("useAuthorizationIdentity", true);
610    authorizationIdentity.addLongIdentifier("use-authorization-identity", true);
611    authorizationIdentity.addLongIdentifier("useAuthorizationIdentityControl",
612         true);
613    authorizationIdentity.addLongIdentifier(
614         "use-authorization-identity-control", true);
615    authorizationIdentity.setArgumentGroupName(
616         INFO_LDAPCOMPARE_ARG_GROUP_BIND_CONTROLS.get());
617    parser.addArgument(authorizationIdentity);
618
619    usePasswordPolicyControl = new BooleanArgument(null,
620         "usePasswordPolicyControl", 1,
621         INFO_LDAPCOMPARE_ARG_DESCRIPTION_USE_PW_POLICY_CONTROL.get());
622    usePasswordPolicyControl.addLongIdentifier("use-password-policy-control",
623         true);
624    usePasswordPolicyControl.addLongIdentifier("passwordPolicyControl", true);
625    usePasswordPolicyControl.addLongIdentifier("password-policy-control", true);
626    usePasswordPolicyControl.addLongIdentifier("passwordPolicy", true);
627    usePasswordPolicyControl.addLongIdentifier("password-policy", true);
628    usePasswordPolicyControl.setArgumentGroupName(
629         INFO_LDAPCOMPARE_ARG_GROUP_BIND_CONTROLS.get());
630    parser.addArgument(usePasswordPolicyControl);
631
632    getAuthorizationEntryAttribute = new StringArgument(null,
633         "getAuthorizationEntryAttribute", false, 0,
634         INFO_LDAPCOMPARE_ARG_PLACEHOLDER_ATTRIBUTE.get(),
635         INFO_LDAPCOMPARE_ARG_DESCRIPTION_GET_AUTHZ_ENTRY_ATTR.get());
636    getAuthorizationEntryAttribute.addLongIdentifier(
637         "get-authorization-entry-attribute", true);
638    getAuthorizationEntryAttribute.addLongIdentifier("getAuthzEntryAttribute",
639         true);
640    getAuthorizationEntryAttribute.addLongIdentifier(
641         "get-authz-entry-attribute", true);
642    getAuthorizationEntryAttribute.setArgumentGroupName(
643         INFO_LDAPCOMPARE_ARG_GROUP_BIND_CONTROLS.get());
644    parser.addArgument(getAuthorizationEntryAttribute);
645
646    getUserResourceLimits = new BooleanArgument(null, "getUserResourceLimits",
647         1, INFO_LDAPCOMPARE_ARG_PLACEHOLDER_GET_USER_RESOURCE_LIMITS.get());
648    getUserResourceLimits.addLongIdentifier("get-user-resource-limits", true);
649    getUserResourceLimits.setArgumentGroupName(
650         INFO_LDAPCOMPARE_ARG_GROUP_BIND_CONTROLS.get());
651    parser.addArgument(getUserResourceLimits);
652
653
654    // Compare control arguments.
655    compareControl = new ControlArgument('J', "compareControl", false, 0, null,
656         INFO_LDAPCOMPARE_ARG_DESCRIPTION_COMPARE_CONTROL.get());
657    compareControl.addLongIdentifier("compare-control", true);
658    compareControl.addLongIdentifier("control", true);
659    compareControl.setArgumentGroupName(
660         INFO_LDAPCOMPARE_ARG_GROUP_COMPARE_CONTROLS.get());
661    parser.addArgument(compareControl);
662
663    proxyAs = new StringArgument('Y', "proxyAs", false, 1,
664         INFO_LDAPCOMPARE_ARG_PLACEHOLDER_AUTHZ_ID.get(),
665         INFO_LDAPCOMPARE_ARG_DESCRIPTION_PROXY_AS.get());
666    proxyAs.addLongIdentifier("proxy-as", true);
667    proxyAs.addLongIdentifier("proxyV2As", true);
668    proxyAs.addLongIdentifier("proxy-v2-as", true);
669    proxyAs.addLongIdentifier("proxyV2", true);
670    proxyAs.addLongIdentifier("proxy-v2", true);
671    proxyAs.setArgumentGroupName(
672         INFO_LDAPCOMPARE_ARG_GROUP_COMPARE_CONTROLS.get());
673    parser.addArgument(proxyAs);
674
675    proxyV1As = new DNArgument(null, "proxyV1As", false, 1, null,
676         INFO_LDAPCOMPARE_ARG_DESCRIPTION_PROXY_V1_AS.get());
677    proxyV1As.addLongIdentifier("proxy-v1-as", true);
678    proxyV1As.addLongIdentifier("proxyV1", true);
679    proxyV1As.addLongIdentifier("proxy-v1", true);
680    proxyV1As.setArgumentGroupName(
681         INFO_LDAPCOMPARE_ARG_GROUP_COMPARE_CONTROLS.get());
682    parser.addArgument(proxyV1As);
683
684    manageDsaIT = new BooleanArgument(null, "manageDsaIT", 1,
685         INFO_LDAPCOMPARE_ARG_DESCRIPTION_MANAGE_DSA_IT.get());
686    manageDsaIT.addLongIdentifier("manage-dsa-it", true);
687    manageDsaIT.setArgumentGroupName(
688         INFO_LDAPCOMPARE_ARG_GROUP_COMPARE_CONTROLS.get());
689    parser.addArgument(manageDsaIT);
690
691    assertionFilter = new FilterArgument(null, "assertionFilter", false, 1,
692         null, INFO_LDAPCOMPARE_ARG_DESCRIPTION_ASSERTION_FILTER.get());
693    assertionFilter.addLongIdentifier("assertion-filter", true);
694    assertionFilter.addLongIdentifier("assertionControlFilter", true);
695    assertionFilter.addLongIdentifier("assertion-control-filter", true);
696    assertionFilter.addLongIdentifier("useAssertionControl", true);
697    assertionFilter.addLongIdentifier("use-assertion-control", true);
698    assertionFilter.setArgumentGroupName(
699         INFO_LDAPCOMPARE_ARG_GROUP_COMPARE_CONTROLS.get());
700    parser.addArgument(assertionFilter);
701
702    operationPurpose = new StringArgument(null, "operationPurpose", false, 1,
703         INFO_LDAPCOMPARE_ARG_PLACEHOLDER_PURPOSE.get(),
704         INFO_LDAPCOMPARE_ARG_DESCRIPTION_OPERATION_PURPOSE.get());
705    operationPurpose.addLongIdentifier("operation-purpose", true);
706    operationPurpose.addLongIdentifier("purpose", true);
707    operationPurpose.setArgumentGroupName(
708         INFO_LDAPCOMPARE_ARG_GROUP_COMPARE_CONTROLS.get());
709    parser.addArgument(operationPurpose);
710
711
712    // Output Arguments.
713    outputFile = new FileArgument(null, "outputFile", false, 1, null,
714         INFO_LDAPCOMPARE_ARG_DESCRIPTION_OUTPUT_FILE.get(), false, true, true,
715         false);
716    outputFile.addLongIdentifier("output-file", true);
717    outputFile.setArgumentGroupName(INFO_LDAPCOMPARE_ARG_GROUP_OUTPUT.get());
718    parser.addArgument(outputFile);
719
720    teeOutput = new BooleanArgument(null, "teeOutput", 1,
721         INFO_LDAPCOMPARE_ARG_DESCRIPTION_TEE_OUTPUT.get());
722    teeOutput.addLongIdentifier("tee-output", true);
723    teeOutput.addLongIdentifier("tee", true);
724    teeOutput.setArgumentGroupName(INFO_LDAPCOMPARE_ARG_GROUP_OUTPUT.get());
725    parser.addArgument(teeOutput);
726
727    outputFormat = new StringArgument(null, "outputFormat", false, 1,
728         INFO_LDAPCOMPARE_ARG_PLACEHOLDER_FORMAT.get(),
729         INFO_LDAPCOMPARE_ARG_DESCRIPTION_OUTPUT_FORMAT.get(),
730         StaticUtils.setOf(
731              OUTPUT_FORMAT_TAB_DELIMITED,
732              OUTPUT_FORMAT_CSV,
733              OUTPUT_FORMAT_JSON),
734         OUTPUT_FORMAT_TAB_DELIMITED);
735    outputFormat.addLongIdentifier("output-format", true);
736    outputFormat.setArgumentGroupName(INFO_LDAPCOMPARE_ARG_GROUP_OUTPUT.get());
737    parser.addArgument(outputFormat);
738
739    scriptFriendly = new BooleanArgument(null, "scriptFriendly", 1,
740         INFO_LDAPCOMPARE_ARG_DESCRIPTION_SCRIPT_FRIENDLY.get());
741    scriptFriendly.addLongIdentifier("script-friendly", true);
742    scriptFriendly.setArgumentGroupName(
743         INFO_LDAPCOMPARE_ARG_GROUP_OUTPUT.get());
744    scriptFriendly.setHidden(true);
745    parser.addArgument(scriptFriendly);
746
747    verbose = new BooleanArgument('v', "verbose", 1,
748         INFO_LDAPCOMPARE_ARG_DESCRIPTION_VERBOSE.get());
749    verbose.setArgumentGroupName(INFO_LDAPCOMPARE_ARG_GROUP_OUTPUT.get());
750    parser.addArgument(verbose);
751
752    terse = new BooleanArgument(null, "terse", 1,
753         INFO_LDAPCOMPARE_ARG_DESCRIPTION_TERSE.get());
754    terse.setArgumentGroupName(INFO_LDAPCOMPARE_ARG_GROUP_OUTPUT.get());
755    parser.addArgument(terse);
756
757    useCompareResultCodeAsExitCode = new BooleanArgument(null,
758         "useCompareResultCodeAsExitCode", 1,
759         INFO_LDAPCOMPARE_ARG_DESC_USE_COMPARE_RESULT_CODE_AS_EXIT_CODE.get());
760    useCompareResultCodeAsExitCode.addLongIdentifier(
761         "use-compare-result-code-as-exit-code", true);
762    useCompareResultCodeAsExitCode.addLongIdentifier(
763         "useCompareResultCode", true);
764    useCompareResultCodeAsExitCode.addLongIdentifier(
765         "use-compare-result-code", true);
766    useCompareResultCodeAsExitCode.setArgumentGroupName(
767         INFO_LDAPCOMPARE_ARG_GROUP_OUTPUT.get());
768    parser.addArgument(useCompareResultCodeAsExitCode);
769
770    parser.addExclusiveArgumentSet(dnFile, assertionFile);
771
772    parser.addExclusiveArgumentSet(proxyAs, proxyV1As);
773
774    parser.addDependentArgumentSet(teeOutput, outputFile);
775
776    parser.addExclusiveArgumentSet(verbose, terse);
777  }
778
779
780
781  /**
782   * {@inheritDoc}
783   */
784  @Override()
785  @NotNull()
786  public ResultCode doToolProcessing()
787  {
788    final List<CompareRequest> compareRequests;
789    try
790    {
791      compareRequests = getCompareRequests();
792    }
793    catch (final LDAPException e)
794    {
795      Debug.debugException(e);
796      logCompletionMessage(true, e.getMessage());
797      return e.getResultCode();
798    }
799
800
801    LDAPConnectionPool pool = null;
802    try
803    {
804      // Create a connection pool that will be used to communicate with the
805      // directory server.  If we should use an administrative session, then
806      // create a connect processor that will be used to start the session
807      // before performing the bind.
808      try
809      {
810        final StartAdministrativeSessionPostConnectProcessor p;
811        if (useAdministrativeSession.isPresent())
812        {
813          p = new StartAdministrativeSessionPostConnectProcessor(
814               new StartAdministrativeSessionExtendedRequest(getToolName(),
815                    true));
816        }
817        else
818        {
819          p = null;
820        }
821
822        pool = getConnectionPool(1, 2, 0, p, null, true,
823             new ReportBindResultLDAPConnectionPoolHealthCheck(this, true,
824                  verbose.isPresent()));
825        pool.setRetryFailedOperationsDueToInvalidConnections(true);
826
827
828        final PrintStream writer;
829        if (outputFile.isPresent())
830        {
831          try
832          {
833            writer = new PrintStream(outputFile.getValue());
834          }
835          catch (final Exception e)
836          {
837            Debug.debugException(e);
838            logCompletionMessage(true,
839                 ERR_LDAPCOMPARE_CANNOT_OPEN_OUTPUT_FILE.get(
840                      outputFile.getValue().getAbsolutePath(),
841                      StaticUtils.getExceptionMessage(e)));
842            return ResultCode.LOCAL_ERROR;
843          }
844        }
845        else
846        {
847          writer = null;
848        }
849
850
851        final LDAPCompareOutputHandler outputHandler;
852        switch (StaticUtils.toLowerCase(outputFormat.getValue()))
853        {
854          case OUTPUT_FORMAT_CSV:
855            outputHandler = new LDAPCompareCSVOutputHandler();
856            break;
857          case OUTPUT_FORMAT_JSON:
858            outputHandler = new LDAPCompareJSONOutputHandler();
859            break;
860          case OUTPUT_FORMAT_TAB_DELIMITED:
861          default:
862            outputHandler = new LDAPCompareTabDelimitedOutputHandler();
863            break;
864        }
865
866        if (! terse.isPresent())
867        {
868          for (final String line : outputHandler.getHeaderLines())
869          {
870            if (writer != null)
871            {
872              writer.println(line);
873            }
874
875            if ((writer == null) || teeOutput.isPresent())
876            {
877              out(line);
878            }
879          }
880        }
881
882
883        ResultCode resultCode = null;
884        int numTrue = 0;
885        int numFalse = 0;
886        int numErrors = 0;
887        for (final CompareRequest compareRequest : compareRequests)
888        {
889          LDAPResult compareResult;
890          try
891          {
892            compareResult = pool.compare(compareRequest);
893          }
894          catch (final LDAPException e)
895          {
896            Debug.debugException(e);
897            compareResult = e.toLDAPResult();
898          }
899
900          try
901          {
902            writeResult(writer, outputHandler, compareRequest, compareResult);
903          }
904          catch (final LDAPException e)
905          {
906            Debug.debugException(e);
907            logCompletionMessage(true, e.getMessage());
908            return e.getResultCode();
909          }
910
911          final ResultCode compareResultCode = compareResult.getResultCode();
912          if (compareResultCode == ResultCode.COMPARE_TRUE)
913          {
914            numTrue++;
915            if (resultCode == null)
916            {
917              resultCode = ResultCode.COMPARE_TRUE;
918            }
919          }
920          else if (compareResultCode == ResultCode.COMPARE_FALSE)
921          {
922            numFalse++;
923            if (resultCode == null)
924            {
925              resultCode = ResultCode.COMPARE_FALSE;
926            }
927          }
928          else
929          {
930            numErrors++;
931            if ((resultCode == null) ||
932                 (resultCode == ResultCode.COMPARE_TRUE) ||
933                 (resultCode == ResultCode.COMPARE_FALSE))
934            {
935              resultCode = compareResultCode;
936            }
937
938            if (! continueOnError.isPresent())
939            {
940              return resultCode;
941            }
942          }
943        }
944
945        if (resultCode == ResultCode.COMPARE_TRUE)
946        {
947          if (compareRequests.size() > 1)
948          {
949            resultCode = ResultCode.SUCCESS;
950            logCompletionMessage(false,
951                 INFO_LDAPCOMPARE_RESULT_ALL_SUCCEEDED.get(numTrue, numFalse));
952          }
953          else
954          {
955            if (! useCompareResultCodeAsExitCode.isPresent())
956            {
957              resultCode = ResultCode.SUCCESS;
958            }
959
960            logCompletionMessage(false,
961                 INFO_LDAPCOMPARE_RESULT_COMPARE_MATCHED.get());
962          }
963        }
964        else if (resultCode == ResultCode.COMPARE_FALSE)
965        {
966          if (compareRequests.size() > 1)
967          {
968            resultCode = ResultCode.SUCCESS;
969            logCompletionMessage(false,
970                 INFO_LDAPCOMPARE_RESULT_ALL_SUCCEEDED.get(numTrue, numFalse));
971          }
972          else
973          {
974            if (! useCompareResultCodeAsExitCode.isPresent())
975            {
976              resultCode = ResultCode.SUCCESS;
977            }
978
979            logCompletionMessage(false,
980                 INFO_LDAPCOMPARE_RESULT_COMPARE_DID_NOT_MATCH.get());
981          }
982        }
983        else
984        {
985          if (compareRequests.size() > 1)
986          {
987            logCompletionMessage(true,
988                 ERR_LDAPCOMPARE_RESULT_WITH_ERRORS.get(numErrors, numTrue,
989                      numFalse));
990          }
991          else
992          {
993            logCompletionMessage(true,
994                 ERR_LDAPCOMPARE_RESULT_FAILED.get());
995          }
996        }
997
998        return resultCode;
999      }
1000      catch (final LDAPException le)
1001      {
1002        Debug.debugException(le);
1003
1004        // Unable to create the connection pool, which means that either the
1005        // connection could not be established or the attempt to authenticate
1006        // the connection failed.  If the bind failed, then the report bind
1007        // result health check should have already reported the bind failure.
1008        // If the failure was something else, then display that failure result.
1009        if (le.getResultCode() != ResultCode.INVALID_CREDENTIALS)
1010        {
1011          for (final String line :
1012               ResultUtils.formatResult(le, true, 0, WRAP_COLUMN))
1013          {
1014            err(line);
1015          }
1016        }
1017        return le.getResultCode();
1018      }
1019    }
1020    finally
1021    {
1022      if (pool != null)
1023      {
1024        pool.close();
1025      }
1026    }
1027  }
1028
1029
1030
1031  /**
1032   * Retrieves a list of the compare requests that should be issued.
1033   *
1034   * @return  A list of the compare requests that should be issued.
1035   *
1036   * @throws  LDAPException  If a problem occurs while obtaining the compare
1037   *                         requests to process.
1038   */
1039  @NotNull()
1040  private List<CompareRequest> getCompareRequests()
1041          throws LDAPException
1042  {
1043    final List<String> trailingArgs = argumentParser.getTrailingArguments();
1044    final int numTrailingArgs = trailingArgs.size();
1045
1046    if (assertionFile.isPresent())
1047    {
1048      if (numTrailingArgs != 0)
1049      {
1050        throw new LDAPException(ResultCode.PARAM_ERROR,
1051             ERR_LDAPCOMPARE_TRAILING_ARGS_WITH_ASSERTION_FILE.get(
1052                  assertionFile.getIdentifierString()));
1053      }
1054
1055      return readAssertionFile(getCompareControls());
1056    }
1057    else if (dnFile.isPresent())
1058    {
1059      if (numTrailingArgs != 1)
1060      {
1061        throw new LDAPException(ResultCode.PARAM_ERROR,
1062             ERR_LDAPCOMPARE_INVALID_TRAILING_ARG_COUNT_WITH_DN_FILE.get(
1063                  dnFile.getIdentifierString()));
1064      }
1065
1066      final ObjectPair<String,byte[]> ava =
1067           parseAttributeValueAssertion(trailingArgs.get(0));
1068      return readDNFile(ava.getFirst(), ava.getSecond(), getCompareControls());
1069    }
1070    else
1071    {
1072      if (numTrailingArgs < 2)
1073      {
1074        throw new LDAPException(ResultCode.PARAM_ERROR,
1075             ERR_LDAPCOMPARE_INVALID_TRAILING_ARG_COUNT_WITHOUT_FILE.get(
1076                  dnFile.getIdentifierString(),
1077                  assertionFile.getIdentifierString()));
1078      }
1079
1080      final Iterator<String> trailingArgsIterator = trailingArgs.iterator();
1081      final ObjectPair<String,byte[]> ava =
1082           parseAttributeValueAssertion(trailingArgsIterator.next());
1083      final String attributeName = ava.getFirst();
1084      final byte[] assertionValue = ava.getSecond();
1085
1086      final Control[] controls = getCompareControls();
1087
1088      final List<CompareRequest> requests = new ArrayList<>(numTrailingArgs-1);
1089      while (trailingArgsIterator.hasNext())
1090      {
1091        final String dnString = trailingArgsIterator.next();
1092        try
1093        {
1094          new DN(dnString);
1095        }
1096        catch (final LDAPException e)
1097        {
1098          Debug.debugException(e);
1099          throw new LDAPException(ResultCode.PARAM_ERROR,
1100               ERR_LDAPCOMPARE_MALFORMED_TRAILING_ARG_DN.get(dnString,
1101                    e.getMessage()),
1102               e);
1103        }
1104
1105        requests.add(new CompareRequest(dnString, attributeName,
1106             assertionValue, controls));
1107      }
1108
1109      return requests;
1110    }
1111  }
1112
1113
1114
1115  /**
1116   * Parses the provided string as an attribute value assertion.  It must
1117   * start with an attribute name or OID, and that must be followed by either a
1118   * single colon and the string representation of the assertion value, or
1119   * two colons and the base64-encoded representation of the assertion value.
1120   *
1121   * @param  avaString  The string to parse as an attribute value assertion.  It
1122   *                    must not be {@code null}.
1123   *
1124   * @return  An object pair in which the first element is the parsed attribute
1125   *          name or OID, and the second element is the parsed assertion value.
1126   *
1127   * @throws  LDAPException  If the provided string cannot be parsed as a valid
1128   *                         attribute value assertion.
1129   */
1130  @NotNull()
1131  private static ObjectPair<String,byte[]> parseAttributeValueAssertion(
1132                                                @NotNull final String avaString)
1133          throws LDAPException
1134  {
1135    final int colonPos = avaString.indexOf(':');
1136    if (colonPos < 0)
1137    {
1138      throw new LDAPException(ResultCode.PARAM_ERROR,
1139           ERR_LDAPCOMPARE_AVA_NO_COLON.get(avaString));
1140    }
1141    else if (colonPos == 0)
1142    {
1143      throw new LDAPException(ResultCode.PARAM_ERROR,
1144           ERR_LDAPCOMPARE_AVA_NO_ATTR.get(avaString));
1145    }
1146
1147    final String attributeName = avaString.substring(0, colonPos);
1148    if (colonPos == (avaString.length() - 1))
1149    {
1150      // This means that the assertion value is empty.
1151      return new ObjectPair<>(attributeName, StaticUtils.NO_BYTES);
1152    }
1153
1154    if (avaString.charAt(colonPos+1) == ':')
1155    {
1156      // This means that the assertion value is base64-encoded.
1157      try
1158      {
1159        final byte[] avaBytes = Base64.decode(avaString.substring(colonPos+2));
1160        return new ObjectPair<>(attributeName, avaBytes);
1161      }
1162      catch (final Exception e)
1163      {
1164        Debug.debugException(e);
1165        throw new LDAPException(ResultCode.PARAM_ERROR,
1166             ERR_LDAPCOMPARE_AVA_CANNOT_BASE64_DECODE_VALUE.get(avaString,
1167                  e.getMessage()),
1168             e);
1169      }
1170    }
1171    else if (avaString.charAt(colonPos+1) == '<')
1172    {
1173      // This means that the assertion value should be read from a file.  The
1174      // path to that file should immediately follow the less-than symbol, and
1175      // the exact bytes contained in that file (including line breaks) will be
1176      // used as the assertion value.
1177      final String path = avaString.substring(colonPos+2);
1178      final File file = new File(path);
1179      if (file.exists())
1180      {
1181        try
1182        {
1183          final byte[] fileBytes = StaticUtils.readFileBytes(file);
1184          return new ObjectPair<>(attributeName, fileBytes);
1185        }
1186        catch (final Exception e)
1187        {
1188          Debug.debugException(e);
1189          throw new LDAPException(ResultCode.LOCAL_ERROR,
1190               ERR_LDAPCOMPARE_AVA_CANNOT_READ_FILE.get(avaString,
1191                    file.getAbsolutePath(),
1192                    StaticUtils.getExceptionMessage(e)),
1193               e);
1194        }
1195      }
1196      else
1197      {
1198        throw new LDAPException(ResultCode.PARAM_ERROR,
1199             ERR_LDAPCOMPARE_AVA_NO_SUCH_FILE.get(avaString,
1200                  file.getAbsolutePath()));
1201      }
1202    }
1203
1204    return new ObjectPair<>(attributeName,
1205         StaticUtils.getBytes(avaString.substring(colonPos+1)));
1206  }
1207
1208
1209
1210  /**
1211   * Reads the compare requests to process from the information in the
1212   * specified assertion file.  Each line of the file must contain the DN of
1213   * the target entry followed by one or more tab characters and the
1214   * attribute-value assertion in the form expected by the
1215   * {@link #parseAttributeValueAssertion} method.  Empty lines and lines that
1216   * start with the octothorpe (#) character will be ignored.
1217   *
1218   * @param  controls  The controls to include in each of the compare requests.
1219   *                   It must not be {@code null} but may be empty.
1220   *
1221   * @return  A list of the compare requests that should be issued.
1222   *
1223   * @throws  LDAPException  If a problem is encountered while parsing the
1224   *                         contents of the assertion file.
1225   */
1226  @NotNull()
1227  private List<CompareRequest> readAssertionFile(
1228               @NotNull final Control[] controls)
1229          throws LDAPException
1230  {
1231    final File f = assertionFile.getValue();
1232    try (FileReader fileReader = new FileReader(f);
1233         BufferedReader bufferedReader = new BufferedReader(fileReader))
1234    {
1235      int lineNumber = 0;
1236      final List<CompareRequest> compareRequests = new ArrayList<>();
1237      while (true)
1238      {
1239        // Read the next line from the file.  If it is null, then we've hit the
1240        // end fo the file.  If the line is empty or starts with an octothorpe,
1241        // then skip it and read the next line.
1242        final String line = bufferedReader.readLine();
1243        if (line == null)
1244        {
1245          if (compareRequests.isEmpty())
1246          {
1247            throw new LDAPException(ResultCode.PARAM_ERROR,
1248                 ERR_LDAPCOMPARE_ASSERTION_FILE_EMPTY.get(f.getAbsolutePath()));
1249          }
1250
1251          return compareRequests;
1252        }
1253
1254        lineNumber++;
1255        if (line.isEmpty() || line.startsWith("#"))
1256        {
1257          continue;
1258        }
1259
1260
1261        // Find the first tab on the line.  Then, skip over any subsequent
1262        // tabs to find the assertion value.
1263        int tabPos = line.indexOf('\t');
1264        if (tabPos < 0)
1265        {
1266          throw new LDAPException(ResultCode.DECODING_ERROR,
1267               ERR_LDAPCOMPARE_ASSERTION_FILE_LINE_MISSING_TAB.get(
1268                    line, lineNumber, f.getAbsolutePath()));
1269        }
1270
1271
1272        final String dn = line.substring(0, tabPos);
1273        try
1274        {
1275          new DN(dn);
1276        }
1277        catch (final LDAPException e)
1278        {
1279          Debug.debugException(e);
1280          throw new LDAPException(ResultCode.DECODING_ERROR,
1281               ERR_LDAPCOMPARE_ASSERTION_FILE_LINE_INVALID_DN.get(
1282                    line, lineNumber, f.getAbsolutePath(), dn, e.getMessage()),
1283               e);
1284        }
1285
1286        for (int i=(tabPos+1); i < line.length(); i++)
1287        {
1288          if (line.charAt(i) == '\t')
1289          {
1290            tabPos = i;
1291          }
1292        }
1293
1294        final String avaString = line.substring(tabPos+1);
1295        if (avaString.isEmpty())
1296        {
1297          throw new LDAPException(ResultCode.DECODING_ERROR,
1298               ERR_LDAPCOMPARE_ASSERTION_FILE_LINE_MISSING_AVA.get(
1299                    line, lineNumber, f.getAbsolutePath()));
1300        }
1301
1302
1303        final ObjectPair<String,byte[]> ava;
1304        try
1305        {
1306          ava = parseAttributeValueAssertion(avaString);
1307        }
1308        catch (final LDAPException e)
1309        {
1310          Debug.debugException(e);
1311          throw new LDAPException(ResultCode.DECODING_ERROR,
1312               ERR_LDAPCOMPARE_ASSERTION_FILE_CANNOT_PARSE_AVA.get(
1313                    line, lineNumber, f.getAbsolutePath(), e.getMessage()),
1314               e);
1315        }
1316
1317        compareRequests.add(new CompareRequest(dn, ava.getFirst(),
1318             ava.getSecond(), controls));
1319      }
1320    }
1321    catch (final IOException e)
1322    {
1323      Debug.debugException(e);
1324      throw new LDAPException(ResultCode.LOCAL_ERROR,
1325           ERR_LDAPCOMPARE_CANNOT_READ_ASSERTION_FILE.get(
1326                f.getAbsolutePath(), StaticUtils.getExceptionMessage(e)),
1327           e);
1328    }
1329  }
1330
1331
1332
1333  /**
1334   * Reads the DN file to obtain the DNs of the entries to target and creates
1335   * the list of compare requests to process.  Each line of the file should
1336   * contain the DN of an entry to process.  Empty lines and lines that start
1337   * with the octothorpe (#) character will be ignored.
1338   *
1339   * @param  attributeName   The name or OID of the attribute to target with
1340   *                         each of the compare requests.  It must not be
1341   *                         {@code null} or empty.
1342   * @param  assertionValue  The assertion value to use for each of the
1343   *                         compare requests.  It must not be {@code null}.
1344   * @param  controls        The controls to include in each of the compare
1345   *                         requests.  It must not be {@code null} but may be
1346   *                         empty.
1347   *
1348   * @return  A list of the compare requests that should be issued.
1349   *
1350   * @throws  LDAPException  If a problem is encountered while parsing the
1351   *                         contents of the assertion file.
1352   */
1353  @NotNull()
1354  private List<CompareRequest> readDNFile(@NotNull final String attributeName,
1355                                          @NotNull final byte[] assertionValue,
1356                                          @NotNull final Control[] controls)
1357          throws LDAPException
1358  {
1359    try (DNFileReader dnFileReader = new DNFileReader(dnFile.getValue()))
1360    {
1361      final List<CompareRequest> compareRequests = new ArrayList<>();
1362      while (true)
1363      {
1364        final DN dn;
1365        try
1366        {
1367          dn = dnFileReader.readDN();
1368        }
1369        catch (final LDAPException e)
1370        {
1371          Debug.debugException(e);
1372          throw new LDAPException(ResultCode.DECODING_ERROR, e.getMessage(), e);
1373        }
1374
1375        if (dn == null)
1376        {
1377          if (compareRequests.isEmpty())
1378          {
1379            throw new LDAPException(ResultCode.PARAM_ERROR,
1380                 ERR_LDAPCOMPARE_DN_FILE_EMPTY.get(
1381                      dnFile.getValue().getAbsolutePath()));
1382          }
1383
1384          return compareRequests;
1385        }
1386
1387        compareRequests.add(new CompareRequest(dn, attributeName,
1388             assertionValue, controls));
1389      }
1390    }
1391    catch (final IOException e)
1392    {
1393      Debug.debugException(e);
1394      throw new LDAPException(ResultCode.LOCAL_ERROR,
1395           ERR_LDAPCOMPARE_CANNOT_READ_DN_FILE.get(
1396                dnFile.getValue().getAbsolutePath(),
1397                StaticUtils.getExceptionMessage(e)),
1398           e);
1399    }
1400  }
1401
1402
1403
1404  /**
1405   * Retrieves the controls that should be included in compare requests.
1406   *
1407   * @return  The controls that should be included in compare requests, or an
1408   *          empty array if no controls should be included.
1409   *
1410   * @throws  LDAPException  If a problem occurs while trying to create any of
1411   *                         the controls.
1412   */
1413  @NotNull()
1414  private Control[] getCompareControls()
1415          throws LDAPException
1416  {
1417    final List<Control> controls = new ArrayList<>();
1418
1419    if (compareControl.isPresent())
1420    {
1421      controls.addAll(compareControl.getValues());
1422    }
1423
1424    if (proxyAs.isPresent())
1425    {
1426      controls.add(new ProxiedAuthorizationV2RequestControl(
1427           proxyAs.getValue()));
1428    }
1429
1430    if (proxyV1As.isPresent())
1431    {
1432      controls.add(new ProxiedAuthorizationV1RequestControl(
1433           proxyV1As.getValue()));
1434    }
1435
1436    if (manageDsaIT.isPresent())
1437    {
1438      controls.add(new ManageDsaITRequestControl(false));
1439    }
1440
1441    if (assertionFilter.isPresent())
1442    {
1443      controls.add(new AssertionRequestControl(assertionFilter.getValue()));
1444    }
1445
1446    if (operationPurpose.isPresent())
1447    {
1448      controls.add(new OperationPurposeRequestControl(false, getToolName(),
1449           getToolVersion(),
1450           LDAPPasswordModify.class.getName() + ".getUpdateControls",
1451           operationPurpose.getValue()));
1452    }
1453
1454    return controls.toArray(StaticUtils.NO_CONTROLS);
1455  }
1456
1457
1458
1459  /**
1460   * Writes information about the compare result.
1461   *
1462   * @param  writer         The writer to use to write to the output file.  It
1463   *                        may be {@code null} if no output file should be
1464   *                        used.
1465   * @param  outputHandler  The output handler that should be used to format the
1466   *                        result information.  It must not be {@code null}.
1467   * @param  request        The compare request that was processed.  It must not
1468   *                        be {@code null}.
1469   * @param  result         The result for the compare operation.  It must not
1470   *                        be {@code null}.
1471   *
1472   * @throws  LDAPException  If a problem occurred while trying to write the
1473   *                         result.
1474   */
1475  private void writeResult(@Nullable final PrintStream writer,
1476                    @NotNull final LDAPCompareOutputHandler outputHandler,
1477                    @NotNull final CompareRequest request,
1478                    @NotNull final LDAPResult result)
1479          throws LDAPException
1480  {
1481    if (shouldWriteResultToStdErr(result))
1482    {
1483      err();
1484      err(INFO_LDAPCOMPARE_RESULT_HEADER.get());
1485      err(INFO_LDAPCOMPARE_RESULT_HEADER_DN.get(request.getDN()));
1486      err(INFO_LDAPCOMPARE_RESULT_HEADER_ATTR.get(request.getAttributeName()));
1487      err(INFO_LDAPCOMPARE_RESULT_HEADER_VALUE.get(
1488           request.getAssertionValue()));
1489      for (final String line : ResultUtils.formatResult(result, true, 0,
1490           WRAP_COLUMN))
1491      {
1492        err(line);
1493      }
1494    }
1495
1496    final String message = outputHandler.formatResult(request, result);
1497    if (writer != null)
1498    {
1499      writer.println(message);
1500    }
1501
1502    if ((writer == null) || teeOutput.isPresent())
1503    {
1504      out(message);
1505    }
1506  }
1507
1508
1509
1510  /**
1511   * Indicates whether to write information about the provided result to
1512   * standard error.
1513   *
1514   * @param  result  The result for which to make the determination.  It must
1515   *                 not be {@code mull}.
1516   *
1517   * @return  {@code true} if information about the result should be written to
1518   *          standard error, or {@code false} if not.
1519   */
1520  private boolean shouldWriteResultToStdErr(@NotNull final LDAPResult result)
1521  {
1522    if (verbose.isPresent())
1523    {
1524      return true;
1525    }
1526
1527    if (terse.isPresent())
1528    {
1529      return false;
1530    }
1531
1532    if (result.hasResponseControl())
1533    {
1534      return true;
1535    }
1536
1537    return ((result.getResultCode() != ResultCode.COMPARE_TRUE) &&
1538         (result.getResultCode() != ResultCode.COMPARE_FALSE));
1539  }
1540
1541
1542
1543  /**
1544   * Writes the provided message and sets it as the completion message.
1545   *
1546   * @param  isError  Indicates whether the message should be written to
1547   *                  standard error rather than standard output.
1548   * @param  message  The message to be written.
1549   */
1550  private void logCompletionMessage(final boolean isError,
1551                                    @NotNull final String message)
1552  {
1553    completionMessage.compareAndSet(null, message);
1554
1555    if (! terse.isPresent())
1556    {
1557      if (isError)
1558      {
1559        wrapErr(0, WRAP_COLUMN, message);
1560      }
1561      else
1562      {
1563        wrapOut(0, WRAP_COLUMN, message);
1564      }
1565    }
1566  }
1567
1568
1569
1570  /**
1571   * {@inheritDoc}
1572   */
1573  @Override()
1574  public void handleUnsolicitedNotification(
1575                   @NotNull final LDAPConnection connection,
1576                   @NotNull final ExtendedResult notification)
1577  {
1578    if (! terse.isPresent())
1579    {
1580      final ArrayList<String> lines = new ArrayList<>(10);
1581      ResultUtils.formatUnsolicitedNotification(lines, notification, true, 0,
1582           WRAP_COLUMN);
1583      for (final String line : lines)
1584      {
1585        err(line);
1586      }
1587      err();
1588    }
1589  }
1590
1591
1592
1593  /**
1594   * {@inheritDoc}
1595   */
1596  @Override()
1597  @NotNull()
1598  public LinkedHashMap<String[],String> getExampleUsages()
1599  {
1600    final LinkedHashMap<String[],String> examples = new LinkedHashMap<>();
1601
1602    examples.put(
1603         new String[]
1604         {
1605           "--hostname", "ds.example.com",
1606           "--port", "636",
1607           "--useSSL",
1608           "--bindDN", "uid=admin,dc=example,dc=com",
1609           "l:Austin",
1610           "uid=jdoe,ou=People,dc=example,dc=com"
1611         },
1612         INFO_LDAPCOMPARE_EXAMPLE_1.get());
1613
1614    examples.put(
1615         new String[]
1616         {
1617           "--hostname", "ds.example.com",
1618           "--port", "636",
1619           "--useSSL",
1620           "--bindDN", "uid=admin,dc=example,dc=com",
1621           "--dnFile", "entry-dns.txt",
1622           "--outputFormat", "csv",
1623           "--terse",
1624           "title:manager"
1625         },
1626         INFO_LDAPCOMPARE_EXAMPLE_2.get());
1627
1628    examples.put(
1629         new String[]
1630         {
1631           "--hostname", "ds.example.com",
1632           "--port", "636",
1633           "--useSSL",
1634           "--bindDN", "uid=admin,dc=example,dc=com",
1635           "--assertionFile", "compare-assertions.txt",
1636           "--outputFormat", "json",
1637           "--outputFile", "compare-assertion-results.json",
1638           "--verbose"
1639         },
1640         INFO_LDAPCOMPARE_EXAMPLE_3.get());
1641
1642    return examples;
1643  }
1644}