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    PrintStream writer = null;
803    try
804    {
805      // Create a connection pool that will be used to communicate with the
806      // directory server.  If we should use an administrative session, then
807      // create a connect processor that will be used to start the session
808      // before performing the bind.
809      try
810      {
811        final StartAdministrativeSessionPostConnectProcessor p;
812        if (useAdministrativeSession.isPresent())
813        {
814          p = new StartAdministrativeSessionPostConnectProcessor(
815               new StartAdministrativeSessionExtendedRequest(getToolName(),
816                    true));
817        }
818        else
819        {
820          p = null;
821        }
822
823        pool = getConnectionPool(1, 2, 0, p, null, true,
824             new ReportBindResultLDAPConnectionPoolHealthCheck(this, true,
825                  verbose.isPresent()));
826        pool.setRetryFailedOperationsDueToInvalidConnections(true);
827
828
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      if (writer != null)
1028      {
1029        writer.close();
1030      }
1031    }
1032  }
1033
1034
1035
1036  /**
1037   * Retrieves a list of the compare requests that should be issued.
1038   *
1039   * @return  A list of the compare requests that should be issued.
1040   *
1041   * @throws  LDAPException  If a problem occurs while obtaining the compare
1042   *                         requests to process.
1043   */
1044  @NotNull()
1045  private List<CompareRequest> getCompareRequests()
1046          throws LDAPException
1047  {
1048    final List<String> trailingArgs = argumentParser.getTrailingArguments();
1049    final int numTrailingArgs = trailingArgs.size();
1050
1051    if (assertionFile.isPresent())
1052    {
1053      if (numTrailingArgs != 0)
1054      {
1055        throw new LDAPException(ResultCode.PARAM_ERROR,
1056             ERR_LDAPCOMPARE_TRAILING_ARGS_WITH_ASSERTION_FILE.get(
1057                  assertionFile.getIdentifierString()));
1058      }
1059
1060      return readAssertionFile(getCompareControls());
1061    }
1062    else if (dnFile.isPresent())
1063    {
1064      if (numTrailingArgs != 1)
1065      {
1066        throw new LDAPException(ResultCode.PARAM_ERROR,
1067             ERR_LDAPCOMPARE_INVALID_TRAILING_ARG_COUNT_WITH_DN_FILE.get(
1068                  dnFile.getIdentifierString()));
1069      }
1070
1071      final ObjectPair<String,byte[]> ava =
1072           parseAttributeValueAssertion(trailingArgs.get(0));
1073      return readDNFile(ava.getFirst(), ava.getSecond(), getCompareControls());
1074    }
1075    else
1076    {
1077      if (numTrailingArgs < 2)
1078      {
1079        throw new LDAPException(ResultCode.PARAM_ERROR,
1080             ERR_LDAPCOMPARE_INVALID_TRAILING_ARG_COUNT_WITHOUT_FILE.get(
1081                  dnFile.getIdentifierString(),
1082                  assertionFile.getIdentifierString()));
1083      }
1084
1085      final Iterator<String> trailingArgsIterator = trailingArgs.iterator();
1086      final ObjectPair<String,byte[]> ava =
1087           parseAttributeValueAssertion(trailingArgsIterator.next());
1088      final String attributeName = ava.getFirst();
1089      final byte[] assertionValue = ava.getSecond();
1090
1091      final Control[] controls = getCompareControls();
1092
1093      final List<CompareRequest> requests = new ArrayList<>(numTrailingArgs-1);
1094      while (trailingArgsIterator.hasNext())
1095      {
1096        final String dnString = trailingArgsIterator.next();
1097        try
1098        {
1099          new DN(dnString);
1100        }
1101        catch (final LDAPException e)
1102        {
1103          Debug.debugException(e);
1104          throw new LDAPException(ResultCode.PARAM_ERROR,
1105               ERR_LDAPCOMPARE_MALFORMED_TRAILING_ARG_DN.get(dnString,
1106                    e.getMessage()),
1107               e);
1108        }
1109
1110        requests.add(new CompareRequest(dnString, attributeName,
1111             assertionValue, controls));
1112      }
1113
1114      return requests;
1115    }
1116  }
1117
1118
1119
1120  /**
1121   * Parses the provided string as an attribute value assertion.  It must
1122   * start with an attribute name or OID, and that must be followed by either a
1123   * single colon and the string representation of the assertion value, or
1124   * two colons and the base64-encoded representation of the assertion value.
1125   *
1126   * @param  avaString  The string to parse as an attribute value assertion.  It
1127   *                    must not be {@code null}.
1128   *
1129   * @return  An object pair in which the first element is the parsed attribute
1130   *          name or OID, and the second element is the parsed assertion value.
1131   *
1132   * @throws  LDAPException  If the provided string cannot be parsed as a valid
1133   *                         attribute value assertion.
1134   */
1135  @NotNull()
1136  private static ObjectPair<String,byte[]> parseAttributeValueAssertion(
1137                                                @NotNull final String avaString)
1138          throws LDAPException
1139  {
1140    final int colonPos = avaString.indexOf(':');
1141    if (colonPos < 0)
1142    {
1143      throw new LDAPException(ResultCode.PARAM_ERROR,
1144           ERR_LDAPCOMPARE_AVA_NO_COLON.get(avaString));
1145    }
1146    else if (colonPos == 0)
1147    {
1148      throw new LDAPException(ResultCode.PARAM_ERROR,
1149           ERR_LDAPCOMPARE_AVA_NO_ATTR.get(avaString));
1150    }
1151
1152    final String attributeName = avaString.substring(0, colonPos);
1153    if (colonPos == (avaString.length() - 1))
1154    {
1155      // This means that the assertion value is empty.
1156      return new ObjectPair<>(attributeName, StaticUtils.NO_BYTES);
1157    }
1158
1159    if (avaString.charAt(colonPos+1) == ':')
1160    {
1161      // This means that the assertion value is base64-encoded.
1162      try
1163      {
1164        final byte[] avaBytes = Base64.decode(avaString.substring(colonPos+2));
1165        return new ObjectPair<>(attributeName, avaBytes);
1166      }
1167      catch (final Exception e)
1168      {
1169        Debug.debugException(e);
1170        throw new LDAPException(ResultCode.PARAM_ERROR,
1171             ERR_LDAPCOMPARE_AVA_CANNOT_BASE64_DECODE_VALUE.get(avaString,
1172                  e.getMessage()),
1173             e);
1174      }
1175    }
1176    else if (avaString.charAt(colonPos+1) == '<')
1177    {
1178      // This means that the assertion value should be read from a file.  The
1179      // path to that file should immediately follow the less-than symbol, and
1180      // the exact bytes contained in that file (including line breaks) will be
1181      // used as the assertion value.
1182      final String path = avaString.substring(colonPos+2);
1183      final File file = new File(path);
1184      if (file.exists())
1185      {
1186        try
1187        {
1188          final byte[] fileBytes = StaticUtils.readFileBytes(file);
1189          return new ObjectPair<>(attributeName, fileBytes);
1190        }
1191        catch (final Exception e)
1192        {
1193          Debug.debugException(e);
1194          throw new LDAPException(ResultCode.LOCAL_ERROR,
1195               ERR_LDAPCOMPARE_AVA_CANNOT_READ_FILE.get(avaString,
1196                    file.getAbsolutePath(),
1197                    StaticUtils.getExceptionMessage(e)),
1198               e);
1199        }
1200      }
1201      else
1202      {
1203        throw new LDAPException(ResultCode.PARAM_ERROR,
1204             ERR_LDAPCOMPARE_AVA_NO_SUCH_FILE.get(avaString,
1205                  file.getAbsolutePath()));
1206      }
1207    }
1208
1209    return new ObjectPair<>(attributeName,
1210         StaticUtils.getBytes(avaString.substring(colonPos+1)));
1211  }
1212
1213
1214
1215  /**
1216   * Reads the compare requests to process from the information in the
1217   * specified assertion file.  Each line of the file must contain the DN of
1218   * the target entry followed by one or more tab characters and the
1219   * attribute-value assertion in the form expected by the
1220   * {@link #parseAttributeValueAssertion} method.  Empty lines and lines that
1221   * start with the octothorpe (#) character will be ignored.
1222   *
1223   * @param  controls  The controls to include in each of the compare requests.
1224   *                   It must not be {@code null} but may be empty.
1225   *
1226   * @return  A list of the compare requests that should be issued.
1227   *
1228   * @throws  LDAPException  If a problem is encountered while parsing the
1229   *                         contents of the assertion file.
1230   */
1231  @NotNull()
1232  private List<CompareRequest> readAssertionFile(
1233               @NotNull final Control[] controls)
1234          throws LDAPException
1235  {
1236    final File f = assertionFile.getValue();
1237    try (FileReader fileReader = new FileReader(f);
1238         BufferedReader bufferedReader = new BufferedReader(fileReader))
1239    {
1240      int lineNumber = 0;
1241      final List<CompareRequest> compareRequests = new ArrayList<>();
1242      while (true)
1243      {
1244        // Read the next line from the file.  If it is null, then we've hit the
1245        // end fo the file.  If the line is empty or starts with an octothorpe,
1246        // then skip it and read the next line.
1247        final String line = bufferedReader.readLine();
1248        if (line == null)
1249        {
1250          if (compareRequests.isEmpty())
1251          {
1252            throw new LDAPException(ResultCode.PARAM_ERROR,
1253                 ERR_LDAPCOMPARE_ASSERTION_FILE_EMPTY.get(f.getAbsolutePath()));
1254          }
1255
1256          return compareRequests;
1257        }
1258
1259        lineNumber++;
1260        if (line.isEmpty() || line.startsWith("#"))
1261        {
1262          continue;
1263        }
1264
1265
1266        // Find the first tab on the line.  Then, skip over any subsequent
1267        // tabs to find the assertion value.
1268        int tabPos = line.indexOf('\t');
1269        if (tabPos < 0)
1270        {
1271          throw new LDAPException(ResultCode.DECODING_ERROR,
1272               ERR_LDAPCOMPARE_ASSERTION_FILE_LINE_MISSING_TAB.get(
1273                    line, lineNumber, f.getAbsolutePath()));
1274        }
1275
1276
1277        final String dn = line.substring(0, tabPos);
1278        try
1279        {
1280          new DN(dn);
1281        }
1282        catch (final LDAPException e)
1283        {
1284          Debug.debugException(e);
1285          throw new LDAPException(ResultCode.DECODING_ERROR,
1286               ERR_LDAPCOMPARE_ASSERTION_FILE_LINE_INVALID_DN.get(
1287                    line, lineNumber, f.getAbsolutePath(), dn, e.getMessage()),
1288               e);
1289        }
1290
1291        for (int i=(tabPos+1); i < line.length(); i++)
1292        {
1293          if (line.charAt(i) == '\t')
1294          {
1295            tabPos = i;
1296          }
1297        }
1298
1299        final String avaString = line.substring(tabPos+1);
1300        if (avaString.isEmpty())
1301        {
1302          throw new LDAPException(ResultCode.DECODING_ERROR,
1303               ERR_LDAPCOMPARE_ASSERTION_FILE_LINE_MISSING_AVA.get(
1304                    line, lineNumber, f.getAbsolutePath()));
1305        }
1306
1307
1308        final ObjectPair<String,byte[]> ava;
1309        try
1310        {
1311          ava = parseAttributeValueAssertion(avaString);
1312        }
1313        catch (final LDAPException e)
1314        {
1315          Debug.debugException(e);
1316          throw new LDAPException(ResultCode.DECODING_ERROR,
1317               ERR_LDAPCOMPARE_ASSERTION_FILE_CANNOT_PARSE_AVA.get(
1318                    line, lineNumber, f.getAbsolutePath(), e.getMessage()),
1319               e);
1320        }
1321
1322        compareRequests.add(new CompareRequest(dn, ava.getFirst(),
1323             ava.getSecond(), controls));
1324      }
1325    }
1326    catch (final IOException e)
1327    {
1328      Debug.debugException(e);
1329      throw new LDAPException(ResultCode.LOCAL_ERROR,
1330           ERR_LDAPCOMPARE_CANNOT_READ_ASSERTION_FILE.get(
1331                f.getAbsolutePath(), StaticUtils.getExceptionMessage(e)),
1332           e);
1333    }
1334  }
1335
1336
1337
1338  /**
1339   * Reads the DN file to obtain the DNs of the entries to target and creates
1340   * the list of compare requests to process.  Each line of the file should
1341   * contain the DN of an entry to process.  Empty lines and lines that start
1342   * with the octothorpe (#) character will be ignored.
1343   *
1344   * @param  attributeName   The name or OID of the attribute to target with
1345   *                         each of the compare requests.  It must not be
1346   *                         {@code null} or empty.
1347   * @param  assertionValue  The assertion value to use for each of the
1348   *                         compare requests.  It must not be {@code null}.
1349   * @param  controls        The controls to include in each of the compare
1350   *                         requests.  It must not be {@code null} but may be
1351   *                         empty.
1352   *
1353   * @return  A list of the compare requests that should be issued.
1354   *
1355   * @throws  LDAPException  If a problem is encountered while parsing the
1356   *                         contents of the assertion file.
1357   */
1358  @NotNull()
1359  private List<CompareRequest> readDNFile(@NotNull final String attributeName,
1360                                          @NotNull final byte[] assertionValue,
1361                                          @NotNull final Control[] controls)
1362          throws LDAPException
1363  {
1364    try (DNFileReader dnFileReader = new DNFileReader(dnFile.getValue()))
1365    {
1366      final List<CompareRequest> compareRequests = new ArrayList<>();
1367      while (true)
1368      {
1369        final DN dn;
1370        try
1371        {
1372          dn = dnFileReader.readDN();
1373        }
1374        catch (final LDAPException e)
1375        {
1376          Debug.debugException(e);
1377          throw new LDAPException(ResultCode.DECODING_ERROR, e.getMessage(), e);
1378        }
1379
1380        if (dn == null)
1381        {
1382          if (compareRequests.isEmpty())
1383          {
1384            throw new LDAPException(ResultCode.PARAM_ERROR,
1385                 ERR_LDAPCOMPARE_DN_FILE_EMPTY.get(
1386                      dnFile.getValue().getAbsolutePath()));
1387          }
1388
1389          return compareRequests;
1390        }
1391
1392        compareRequests.add(new CompareRequest(dn, attributeName,
1393             assertionValue, controls));
1394      }
1395    }
1396    catch (final IOException e)
1397    {
1398      Debug.debugException(e);
1399      throw new LDAPException(ResultCode.LOCAL_ERROR,
1400           ERR_LDAPCOMPARE_CANNOT_READ_DN_FILE.get(
1401                dnFile.getValue().getAbsolutePath(),
1402                StaticUtils.getExceptionMessage(e)),
1403           e);
1404    }
1405  }
1406
1407
1408
1409  /**
1410   * Retrieves the controls that should be included in compare requests.
1411   *
1412   * @return  The controls that should be included in compare requests, or an
1413   *          empty array if no controls should be included.
1414   *
1415   * @throws  LDAPException  If a problem occurs while trying to create any of
1416   *                         the controls.
1417   */
1418  @NotNull()
1419  private Control[] getCompareControls()
1420          throws LDAPException
1421  {
1422    final List<Control> controls = new ArrayList<>();
1423
1424    if (compareControl.isPresent())
1425    {
1426      controls.addAll(compareControl.getValues());
1427    }
1428
1429    if (proxyAs.isPresent())
1430    {
1431      controls.add(new ProxiedAuthorizationV2RequestControl(
1432           proxyAs.getValue()));
1433    }
1434
1435    if (proxyV1As.isPresent())
1436    {
1437      controls.add(new ProxiedAuthorizationV1RequestControl(
1438           proxyV1As.getValue()));
1439    }
1440
1441    if (manageDsaIT.isPresent())
1442    {
1443      controls.add(new ManageDsaITRequestControl(false));
1444    }
1445
1446    if (assertionFilter.isPresent())
1447    {
1448      controls.add(new AssertionRequestControl(assertionFilter.getValue()));
1449    }
1450
1451    if (operationPurpose.isPresent())
1452    {
1453      controls.add(new OperationPurposeRequestControl(false, getToolName(),
1454           getToolVersion(),
1455           LDAPPasswordModify.class.getName() + ".getUpdateControls",
1456           operationPurpose.getValue()));
1457    }
1458
1459    return controls.toArray(StaticUtils.NO_CONTROLS);
1460  }
1461
1462
1463
1464  /**
1465   * Writes information about the compare result.
1466   *
1467   * @param  writer         The writer to use to write to the output file.  It
1468   *                        may be {@code null} if no output file should be
1469   *                        used.
1470   * @param  outputHandler  The output handler that should be used to format the
1471   *                        result information.  It must not be {@code null}.
1472   * @param  request        The compare request that was processed.  It must not
1473   *                        be {@code null}.
1474   * @param  result         The result for the compare operation.  It must not
1475   *                        be {@code null}.
1476   *
1477   * @throws  LDAPException  If a problem occurred while trying to write the
1478   *                         result.
1479   */
1480  private void writeResult(@Nullable final PrintStream writer,
1481                    @NotNull final LDAPCompareOutputHandler outputHandler,
1482                    @NotNull final CompareRequest request,
1483                    @NotNull final LDAPResult result)
1484          throws LDAPException
1485  {
1486    if (shouldWriteResultToStdErr(result))
1487    {
1488      err();
1489      err(INFO_LDAPCOMPARE_RESULT_HEADER.get());
1490      err(INFO_LDAPCOMPARE_RESULT_HEADER_DN.get(request.getDN()));
1491      err(INFO_LDAPCOMPARE_RESULT_HEADER_ATTR.get(request.getAttributeName()));
1492      err(INFO_LDAPCOMPARE_RESULT_HEADER_VALUE.get(
1493           request.getAssertionValue()));
1494      for (final String line : ResultUtils.formatResult(result, true, 0,
1495           WRAP_COLUMN))
1496      {
1497        err(line);
1498      }
1499    }
1500
1501    final String message = outputHandler.formatResult(request, result);
1502    if (writer != null)
1503    {
1504      writer.println(message);
1505    }
1506
1507    if ((writer == null) || teeOutput.isPresent())
1508    {
1509      out(message);
1510    }
1511  }
1512
1513
1514
1515  /**
1516   * Indicates whether to write information about the provided result to
1517   * standard error.
1518   *
1519   * @param  result  The result for which to make the determination.  It must
1520   *                 not be {@code mull}.
1521   *
1522   * @return  {@code true} if information about the result should be written to
1523   *          standard error, or {@code false} if not.
1524   */
1525  private boolean shouldWriteResultToStdErr(@NotNull final LDAPResult result)
1526  {
1527    if (verbose.isPresent())
1528    {
1529      return true;
1530    }
1531
1532    if (terse.isPresent())
1533    {
1534      return false;
1535    }
1536
1537    if (result.hasResponseControl())
1538    {
1539      return true;
1540    }
1541
1542    return ((result.getResultCode() != ResultCode.COMPARE_TRUE) &&
1543         (result.getResultCode() != ResultCode.COMPARE_FALSE));
1544  }
1545
1546
1547
1548  /**
1549   * Writes the provided message and sets it as the completion message.
1550   *
1551   * @param  isError  Indicates whether the message should be written to
1552   *                  standard error rather than standard output.
1553   * @param  message  The message to be written.
1554   */
1555  private void logCompletionMessage(final boolean isError,
1556                                    @NotNull final String message)
1557  {
1558    completionMessage.compareAndSet(null, message);
1559
1560    if (! terse.isPresent())
1561    {
1562      if (isError)
1563      {
1564        wrapErr(0, WRAP_COLUMN, message);
1565      }
1566      else
1567      {
1568        wrapOut(0, WRAP_COLUMN, message);
1569      }
1570    }
1571  }
1572
1573
1574
1575  /**
1576   * {@inheritDoc}
1577   */
1578  @Override()
1579  public void handleUnsolicitedNotification(
1580                   @NotNull final LDAPConnection connection,
1581                   @NotNull final ExtendedResult notification)
1582  {
1583    if (! terse.isPresent())
1584    {
1585      final ArrayList<String> lines = new ArrayList<>(10);
1586      ResultUtils.formatUnsolicitedNotification(lines, notification, true, 0,
1587           WRAP_COLUMN);
1588      for (final String line : lines)
1589      {
1590        err(line);
1591      }
1592      err();
1593    }
1594  }
1595
1596
1597
1598  /**
1599   * {@inheritDoc}
1600   */
1601  @Override()
1602  @NotNull()
1603  public LinkedHashMap<String[],String> getExampleUsages()
1604  {
1605    final LinkedHashMap<String[],String> examples = new LinkedHashMap<>();
1606
1607    examples.put(
1608         new String[]
1609         {
1610           "--hostname", "ds.example.com",
1611           "--port", "636",
1612           "--useSSL",
1613           "--bindDN", "uid=admin,dc=example,dc=com",
1614           "l:Austin",
1615           "uid=jdoe,ou=People,dc=example,dc=com"
1616         },
1617         INFO_LDAPCOMPARE_EXAMPLE_1.get());
1618
1619    examples.put(
1620         new String[]
1621         {
1622           "--hostname", "ds.example.com",
1623           "--port", "636",
1624           "--useSSL",
1625           "--bindDN", "uid=admin,dc=example,dc=com",
1626           "--dnFile", "entry-dns.txt",
1627           "--outputFormat", "csv",
1628           "--terse",
1629           "title:manager"
1630         },
1631         INFO_LDAPCOMPARE_EXAMPLE_2.get());
1632
1633    examples.put(
1634         new String[]
1635         {
1636           "--hostname", "ds.example.com",
1637           "--port", "636",
1638           "--useSSL",
1639           "--bindDN", "uid=admin,dc=example,dc=com",
1640           "--assertionFile", "compare-assertions.txt",
1641           "--outputFormat", "json",
1642           "--outputFile", "compare-assertion-results.json",
1643           "--verbose"
1644         },
1645         INFO_LDAPCOMPARE_EXAMPLE_3.get());
1646
1647    return examples;
1648  }
1649}