001/*
002 * Copyright 2020-2024 Ping Identity Corporation
003 * All Rights Reserved.
004 */
005/*
006 * Copyright 2020-2024 Ping Identity Corporation
007 *
008 * Licensed under the Apache License, Version 2.0 (the "License");
009 * you may not use this file except in compliance with the License.
010 * You may obtain a copy of the License at
011 *
012 *    http://www.apache.org/licenses/LICENSE-2.0
013 *
014 * Unless required by applicable law or agreed to in writing, software
015 * distributed under the License is distributed on an "AS IS" BASIS,
016 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
017 * See the License for the specific language governing permissions and
018 * limitations under the License.
019 */
020/*
021 * Copyright (C) 2020-2024 Ping Identity Corporation
022 *
023 * This program is free software; you can redistribute it and/or modify
024 * it under the terms of the GNU General Public License (GPLv2 only)
025 * or the terms of the GNU Lesser General Public License (LGPLv2.1 only)
026 * as published by the Free Software Foundation.
027 *
028 * This program is distributed in the hope that it will be useful,
029 * but WITHOUT ANY WARRANTY; without even the implied warranty of
030 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
031 * GNU General Public License for more details.
032 *
033 * You should have received a copy of the GNU General Public License
034 * along with this program; if not, see <http://www.gnu.org/licenses>.
035 */
036package com.unboundid.ldap.sdk.unboundidds.tools;
037
038
039
040import java.io.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 supportsDebugLogging()
399  {
400    return true;
401  }
402
403
404
405  /**
406   * {@inheritDoc}
407   */
408  @Override()
409  protected boolean supportsAuthentication()
410  {
411    return true;
412  }
413
414
415
416  /**
417   * {@inheritDoc}
418   */
419  @Override()
420  protected boolean defaultToPromptForBindPassword()
421  {
422    return true;
423  }
424
425
426
427  /**
428   * {@inheritDoc}
429   */
430  @Override()
431  protected boolean supportsSASLHelp()
432  {
433    return true;
434  }
435
436
437
438  /**
439   * {@inheritDoc}
440   */
441  @Override()
442  protected boolean includeAlternateLongIdentifiers()
443  {
444    return true;
445  }
446
447
448
449  /**
450   * {@inheritDoc}
451   */
452  @Override()
453  @NotNull()
454  protected List<Control> getBindControls()
455  {
456    final List<Control> bindControls = new ArrayList<>(10);
457
458    if (bindControl.isPresent())
459    {
460      bindControls.addAll(bindControl.getValues());
461    }
462
463    if (authorizationIdentity.isPresent())
464    {
465      bindControls.add(new AuthorizationIdentityRequestControl(false));
466    }
467
468    if (getAuthorizationEntryAttribute.isPresent())
469    {
470      bindControls.add(new GetAuthorizationEntryRequestControl(true, true,
471           getAuthorizationEntryAttribute.getValues()));
472    }
473
474    if (getUserResourceLimits.isPresent())
475    {
476      bindControls.add(new GetUserResourceLimitsRequestControl());
477    }
478
479    if (usePasswordPolicyControl.isPresent())
480    {
481      bindControls.add(new PasswordPolicyRequestControl());
482    }
483
484    return bindControls;
485  }
486
487
488
489  /**
490   * {@inheritDoc}
491   */
492  @Override()
493  protected boolean supportsMultipleServers()
494  {
495    return true;
496  }
497
498
499
500  /**
501   * {@inheritDoc}
502   */
503  @Override()
504  protected boolean supportsSSLDebugging()
505  {
506    return true;
507  }
508
509
510
511  /**
512   * {@inheritDoc}
513   */
514  @Override()
515  @NotNull()
516  public LDAPConnectionOptions getConnectionOptions()
517  {
518    final LDAPConnectionOptions options = new LDAPConnectionOptions();
519
520    options.setUseSynchronousMode(true);
521    options.setFollowReferrals(followReferrals.isPresent());
522    options.setUnsolicitedNotificationHandler(this);
523    options.setResponseTimeoutMillis(0L);
524
525    return options;
526  }
527
528
529
530  /**
531   * {@inheritDoc}
532   */
533  @Override()
534  protected boolean logToolInvocationByDefault()
535  {
536    return false;
537  }
538
539
540
541  /**
542   * {@inheritDoc}
543   */
544  @Override()
545  @Nullable()
546  protected String getToolCompletionMessage()
547  {
548    return completionMessage.get();
549  }
550
551
552
553  /**
554   * {@inheritDoc}
555   */
556  @Override()
557  public void addNonLDAPArguments(@NotNull final ArgumentParser parser)
558         throws ArgumentException
559  {
560    argumentParser = parser;
561
562    // Compare operation processing arguments.
563    dnFile = new FileArgument('f', "dnFile", false, 1, null,
564         INFO_LDAPCOMPARE_ARG_DESCRIPTION_DN_FILE.get(), true, true, true,
565         false);
566    dnFile.addLongIdentifier("dn-file", true);
567    dnFile.addLongIdentifier("filename", true);
568    dnFile.setArgumentGroupName(INFO_LDAPCOMPARE_ARG_GROUP_PROCESSING.get());
569    parser.addArgument(dnFile);
570
571    assertionFile = new FileArgument(null, "assertionFile", false, 1, null,
572         INFO_LDAPCOMPARE_ARG_DESCRIPTION_ASSERTION_FILE.get(), true, true,
573         true, false);
574    assertionFile.addLongIdentifier("assertion-file", true);
575    assertionFile.setArgumentGroupName(
576         INFO_LDAPCOMPARE_ARG_GROUP_PROCESSING.get());
577    parser.addArgument(assertionFile);
578
579    followReferrals = new BooleanArgument(null, "followReferrals", 1,
580         INFO_LDAPCOMPARE_ARG_DESCRIPTION_FOLLOW_REFERRALS.get());
581    followReferrals.addLongIdentifier("follow-referrals", true);
582    followReferrals.setArgumentGroupName(
583         INFO_LDAPCOMPARE_ARG_GROUP_PROCESSING.get());
584    parser.addArgument(followReferrals);
585
586    useAdministrativeSession = new BooleanArgument(null,
587         "useAdministrativeSession", 1,
588         INFO_LDAPCOMPARE_ARG_DESCRIPTION_USE_ADMIN_SESSION.get());
589    useAdministrativeSession.addLongIdentifier("use-administrative-session",
590         true);
591    useAdministrativeSession.setArgumentGroupName(
592         INFO_LDAPCOMPARE_ARG_GROUP_PROCESSING.get());
593    parser.addArgument(useAdministrativeSession);
594
595    continueOnError = new BooleanArgument('c', "continueOnError", 1,
596         INFO_LDAPCOMPARE_ARG_DESCRIPTION_CONTINUE_ON_ERROR.get());
597    continueOnError.addLongIdentifier("continue-on-error", true);
598    continueOnError.setArgumentGroupName(
599         INFO_LDAPCOMPARE_ARG_GROUP_PROCESSING.get());
600    parser.addArgument(continueOnError);
601
602    dryRun = new BooleanArgument('n', "dryRun", 1,
603         INFO_LDAPCOMPARE_ARG_DESCRIPTION_DRY_RUN.get());
604    dryRun.addLongIdentifier("dry-run", true);
605    dryRun.setArgumentGroupName(INFO_LDAPCOMPARE_ARG_GROUP_PROCESSING.get());
606    parser.addArgument(dryRun);
607
608
609    // Bind control arguments.
610    bindControl = new ControlArgument(null, "bindControl", false, 0, null,
611         INFO_LDAPCOMPARE_ARG_DESCRIPTION_BIND_CONTROL.get());
612    bindControl.addLongIdentifier("bind-control", true);
613    bindControl.setArgumentGroupName(
614         INFO_LDAPCOMPARE_ARG_GROUP_BIND_CONTROLS.get());
615    parser.addArgument(bindControl);
616
617    authorizationIdentity = new BooleanArgument('E', "authorizationIdentity", 1,
618         INFO_LDAPCOMPARE_ARG_DESCRIPTION_AUTHZ_IDENTITY.get());
619    authorizationIdentity.addLongIdentifier("authorization-identity", true);
620    authorizationIdentity.addLongIdentifier("useAuthorizationIdentity", true);
621    authorizationIdentity.addLongIdentifier("use-authorization-identity", true);
622    authorizationIdentity.addLongIdentifier("useAuthorizationIdentityControl",
623         true);
624    authorizationIdentity.addLongIdentifier(
625         "use-authorization-identity-control", true);
626    authorizationIdentity.setArgumentGroupName(
627         INFO_LDAPCOMPARE_ARG_GROUP_BIND_CONTROLS.get());
628    parser.addArgument(authorizationIdentity);
629
630    usePasswordPolicyControl = new BooleanArgument(null,
631         "usePasswordPolicyControl", 1,
632         INFO_LDAPCOMPARE_ARG_DESCRIPTION_USE_PW_POLICY_CONTROL.get());
633    usePasswordPolicyControl.addLongIdentifier("use-password-policy-control",
634         true);
635    usePasswordPolicyControl.addLongIdentifier("passwordPolicyControl", true);
636    usePasswordPolicyControl.addLongIdentifier("password-policy-control", true);
637    usePasswordPolicyControl.addLongIdentifier("passwordPolicy", true);
638    usePasswordPolicyControl.addLongIdentifier("password-policy", true);
639    usePasswordPolicyControl.setArgumentGroupName(
640         INFO_LDAPCOMPARE_ARG_GROUP_BIND_CONTROLS.get());
641    parser.addArgument(usePasswordPolicyControl);
642
643    getAuthorizationEntryAttribute = new StringArgument(null,
644         "getAuthorizationEntryAttribute", false, 0,
645         INFO_LDAPCOMPARE_ARG_PLACEHOLDER_ATTRIBUTE.get(),
646         INFO_LDAPCOMPARE_ARG_DESCRIPTION_GET_AUTHZ_ENTRY_ATTR.get());
647    getAuthorizationEntryAttribute.addLongIdentifier(
648         "get-authorization-entry-attribute", true);
649    getAuthorizationEntryAttribute.addLongIdentifier("getAuthzEntryAttribute",
650         true);
651    getAuthorizationEntryAttribute.addLongIdentifier(
652         "get-authz-entry-attribute", true);
653    getAuthorizationEntryAttribute.setArgumentGroupName(
654         INFO_LDAPCOMPARE_ARG_GROUP_BIND_CONTROLS.get());
655    parser.addArgument(getAuthorizationEntryAttribute);
656
657    getUserResourceLimits = new BooleanArgument(null, "getUserResourceLimits",
658         1, INFO_LDAPCOMPARE_ARG_PLACEHOLDER_GET_USER_RESOURCE_LIMITS.get());
659    getUserResourceLimits.addLongIdentifier("get-user-resource-limits", true);
660    getUserResourceLimits.setArgumentGroupName(
661         INFO_LDAPCOMPARE_ARG_GROUP_BIND_CONTROLS.get());
662    parser.addArgument(getUserResourceLimits);
663
664
665    // Compare control arguments.
666    compareControl = new ControlArgument('J', "compareControl", false, 0, null,
667         INFO_LDAPCOMPARE_ARG_DESCRIPTION_COMPARE_CONTROL.get());
668    compareControl.addLongIdentifier("compare-control", true);
669    compareControl.addLongIdentifier("control", true);
670    compareControl.setArgumentGroupName(
671         INFO_LDAPCOMPARE_ARG_GROUP_COMPARE_CONTROLS.get());
672    parser.addArgument(compareControl);
673
674    proxyAs = new StringArgument('Y', "proxyAs", false, 1,
675         INFO_LDAPCOMPARE_ARG_PLACEHOLDER_AUTHZ_ID.get(),
676         INFO_LDAPCOMPARE_ARG_DESCRIPTION_PROXY_AS.get());
677    proxyAs.addLongIdentifier("proxy-as", true);
678    proxyAs.addLongIdentifier("proxyV2As", true);
679    proxyAs.addLongIdentifier("proxy-v2-as", true);
680    proxyAs.addLongIdentifier("proxyV2", true);
681    proxyAs.addLongIdentifier("proxy-v2", true);
682    proxyAs.setArgumentGroupName(
683         INFO_LDAPCOMPARE_ARG_GROUP_COMPARE_CONTROLS.get());
684    parser.addArgument(proxyAs);
685
686    proxyV1As = new DNArgument(null, "proxyV1As", false, 1, null,
687         INFO_LDAPCOMPARE_ARG_DESCRIPTION_PROXY_V1_AS.get());
688    proxyV1As.addLongIdentifier("proxy-v1-as", true);
689    proxyV1As.addLongIdentifier("proxyV1", true);
690    proxyV1As.addLongIdentifier("proxy-v1", true);
691    proxyV1As.setArgumentGroupName(
692         INFO_LDAPCOMPARE_ARG_GROUP_COMPARE_CONTROLS.get());
693    parser.addArgument(proxyV1As);
694
695    manageDsaIT = new BooleanArgument(null, "manageDsaIT", 1,
696         INFO_LDAPCOMPARE_ARG_DESCRIPTION_MANAGE_DSA_IT.get());
697    manageDsaIT.addLongIdentifier("manage-dsa-it", true);
698    manageDsaIT.setArgumentGroupName(
699         INFO_LDAPCOMPARE_ARG_GROUP_COMPARE_CONTROLS.get());
700    parser.addArgument(manageDsaIT);
701
702    assertionFilter = new FilterArgument(null, "assertionFilter", false, 1,
703         null, INFO_LDAPCOMPARE_ARG_DESCRIPTION_ASSERTION_FILTER.get());
704    assertionFilter.addLongIdentifier("assertion-filter", true);
705    assertionFilter.addLongIdentifier("assertionControlFilter", true);
706    assertionFilter.addLongIdentifier("assertion-control-filter", true);
707    assertionFilter.addLongIdentifier("useAssertionControl", true);
708    assertionFilter.addLongIdentifier("use-assertion-control", true);
709    assertionFilter.setArgumentGroupName(
710         INFO_LDAPCOMPARE_ARG_GROUP_COMPARE_CONTROLS.get());
711    parser.addArgument(assertionFilter);
712
713    operationPurpose = new StringArgument(null, "operationPurpose", false, 1,
714         INFO_LDAPCOMPARE_ARG_PLACEHOLDER_PURPOSE.get(),
715         INFO_LDAPCOMPARE_ARG_DESCRIPTION_OPERATION_PURPOSE.get());
716    operationPurpose.addLongIdentifier("operation-purpose", true);
717    operationPurpose.addLongIdentifier("purpose", true);
718    operationPurpose.setArgumentGroupName(
719         INFO_LDAPCOMPARE_ARG_GROUP_COMPARE_CONTROLS.get());
720    parser.addArgument(operationPurpose);
721
722
723    // Output Arguments.
724    outputFile = new FileArgument(null, "outputFile", false, 1, null,
725         INFO_LDAPCOMPARE_ARG_DESCRIPTION_OUTPUT_FILE.get(), false, true, true,
726         false);
727    outputFile.addLongIdentifier("output-file", true);
728    outputFile.setArgumentGroupName(INFO_LDAPCOMPARE_ARG_GROUP_OUTPUT.get());
729    parser.addArgument(outputFile);
730
731    teeOutput = new BooleanArgument(null, "teeOutput", 1,
732         INFO_LDAPCOMPARE_ARG_DESCRIPTION_TEE_OUTPUT.get());
733    teeOutput.addLongIdentifier("tee-output", true);
734    teeOutput.addLongIdentifier("tee", true);
735    teeOutput.setArgumentGroupName(INFO_LDAPCOMPARE_ARG_GROUP_OUTPUT.get());
736    parser.addArgument(teeOutput);
737
738    outputFormat = new StringArgument(null, "outputFormat", false, 1,
739         INFO_LDAPCOMPARE_ARG_PLACEHOLDER_FORMAT.get(),
740         INFO_LDAPCOMPARE_ARG_DESCRIPTION_OUTPUT_FORMAT.get(),
741         StaticUtils.setOf(
742              OUTPUT_FORMAT_TAB_DELIMITED,
743              OUTPUT_FORMAT_CSV,
744              OUTPUT_FORMAT_JSON),
745         OUTPUT_FORMAT_TAB_DELIMITED);
746    outputFormat.addLongIdentifier("output-format", true);
747    outputFormat.setArgumentGroupName(INFO_LDAPCOMPARE_ARG_GROUP_OUTPUT.get());
748    parser.addArgument(outputFormat);
749
750    scriptFriendly = new BooleanArgument(null, "scriptFriendly", 1,
751         INFO_LDAPCOMPARE_ARG_DESCRIPTION_SCRIPT_FRIENDLY.get());
752    scriptFriendly.addLongIdentifier("script-friendly", true);
753    scriptFriendly.setArgumentGroupName(
754         INFO_LDAPCOMPARE_ARG_GROUP_OUTPUT.get());
755    scriptFriendly.setHidden(true);
756    parser.addArgument(scriptFriendly);
757
758    verbose = new BooleanArgument('v', "verbose", 1,
759         INFO_LDAPCOMPARE_ARG_DESCRIPTION_VERBOSE.get());
760    verbose.setArgumentGroupName(INFO_LDAPCOMPARE_ARG_GROUP_OUTPUT.get());
761    parser.addArgument(verbose);
762
763    terse = new BooleanArgument(null, "terse", 1,
764         INFO_LDAPCOMPARE_ARG_DESCRIPTION_TERSE.get());
765    terse.setArgumentGroupName(INFO_LDAPCOMPARE_ARG_GROUP_OUTPUT.get());
766    parser.addArgument(terse);
767
768    useCompareResultCodeAsExitCode = new BooleanArgument(null,
769         "useCompareResultCodeAsExitCode", 1,
770         INFO_LDAPCOMPARE_ARG_DESC_USE_COMPARE_RESULT_CODE_AS_EXIT_CODE.get());
771    useCompareResultCodeAsExitCode.addLongIdentifier(
772         "use-compare-result-code-as-exit-code", true);
773    useCompareResultCodeAsExitCode.addLongIdentifier(
774         "useCompareResultCode", true);
775    useCompareResultCodeAsExitCode.addLongIdentifier(
776         "use-compare-result-code", true);
777    useCompareResultCodeAsExitCode.setArgumentGroupName(
778         INFO_LDAPCOMPARE_ARG_GROUP_OUTPUT.get());
779    parser.addArgument(useCompareResultCodeAsExitCode);
780
781    parser.addExclusiveArgumentSet(dnFile, assertionFile);
782
783    parser.addExclusiveArgumentSet(proxyAs, proxyV1As);
784
785    parser.addDependentArgumentSet(teeOutput, outputFile);
786
787    parser.addExclusiveArgumentSet(verbose, terse);
788  }
789
790
791
792  /**
793   * {@inheritDoc}
794   */
795  @Override()
796  @NotNull()
797  public ResultCode doToolProcessing()
798  {
799    final List<CompareRequest> compareRequests;
800    try
801    {
802      compareRequests = getCompareRequests();
803    }
804    catch (final LDAPException e)
805    {
806      Debug.debugException(e);
807      logCompletionMessage(true, e.getMessage());
808      return e.getResultCode();
809    }
810
811
812    LDAPConnectionPool pool = null;
813    PrintStream writer = null;
814    try
815    {
816      // Create a connection pool that will be used to communicate with the
817      // directory server.  If we should use an administrative session, then
818      // create a connect processor that will be used to start the session
819      // before performing the bind.
820      try
821      {
822        final StartAdministrativeSessionPostConnectProcessor p;
823        if (useAdministrativeSession.isPresent())
824        {
825          p = new StartAdministrativeSessionPostConnectProcessor(
826               new StartAdministrativeSessionExtendedRequest(getToolName(),
827                    true));
828        }
829        else
830        {
831          p = null;
832        }
833
834        pool = getConnectionPool(1, 2, 0, p, null, true,
835             new ReportBindResultLDAPConnectionPoolHealthCheck(this, true,
836                  verbose.isPresent()));
837        pool.setRetryFailedOperationsDueToInvalidConnections(true);
838
839
840        if (outputFile.isPresent())
841        {
842          try
843          {
844            writer = new PrintStream(outputFile.getValue());
845          }
846          catch (final Exception e)
847          {
848            Debug.debugException(e);
849            logCompletionMessage(true,
850                 ERR_LDAPCOMPARE_CANNOT_OPEN_OUTPUT_FILE.get(
851                      outputFile.getValue().getAbsolutePath(),
852                      StaticUtils.getExceptionMessage(e)));
853            return ResultCode.LOCAL_ERROR;
854          }
855        }
856        else
857        {
858          writer = null;
859        }
860
861
862        final LDAPCompareOutputHandler outputHandler;
863        switch (StaticUtils.toLowerCase(outputFormat.getValue()))
864        {
865          case OUTPUT_FORMAT_CSV:
866            outputHandler = new LDAPCompareCSVOutputHandler();
867            break;
868          case OUTPUT_FORMAT_JSON:
869            outputHandler = new LDAPCompareJSONOutputHandler();
870            break;
871          case OUTPUT_FORMAT_TAB_DELIMITED:
872          default:
873            outputHandler = new LDAPCompareTabDelimitedOutputHandler();
874            break;
875        }
876
877        if (! terse.isPresent())
878        {
879          for (final String line : outputHandler.getHeaderLines())
880          {
881            if (writer != null)
882            {
883              writer.println(line);
884            }
885
886            if ((writer == null) || teeOutput.isPresent())
887            {
888              out(line);
889            }
890          }
891        }
892
893
894        ResultCode resultCode = null;
895        int numTrue = 0;
896        int numFalse = 0;
897        int numErrors = 0;
898        for (final CompareRequest compareRequest : compareRequests)
899        {
900          LDAPResult compareResult;
901          try
902          {
903            compareResult = pool.compare(compareRequest);
904          }
905          catch (final LDAPException e)
906          {
907            Debug.debugException(e);
908            compareResult = e.toLDAPResult();
909          }
910
911          try
912          {
913            writeResult(writer, outputHandler, compareRequest, compareResult);
914          }
915          catch (final LDAPException e)
916          {
917            Debug.debugException(e);
918            logCompletionMessage(true, e.getMessage());
919            return e.getResultCode();
920          }
921
922          final ResultCode compareResultCode = compareResult.getResultCode();
923          if (compareResultCode == ResultCode.COMPARE_TRUE)
924          {
925            numTrue++;
926            if (resultCode == null)
927            {
928              resultCode = ResultCode.COMPARE_TRUE;
929            }
930          }
931          else if (compareResultCode == ResultCode.COMPARE_FALSE)
932          {
933            numFalse++;
934            if (resultCode == null)
935            {
936              resultCode = ResultCode.COMPARE_FALSE;
937            }
938          }
939          else
940          {
941            numErrors++;
942            if ((resultCode == null) ||
943                 (resultCode == ResultCode.COMPARE_TRUE) ||
944                 (resultCode == ResultCode.COMPARE_FALSE))
945            {
946              resultCode = compareResultCode;
947            }
948
949            if (! continueOnError.isPresent())
950            {
951              return resultCode;
952            }
953          }
954        }
955
956        if (resultCode == ResultCode.COMPARE_TRUE)
957        {
958          if (compareRequests.size() > 1)
959          {
960            resultCode = ResultCode.SUCCESS;
961            logCompletionMessage(false,
962                 INFO_LDAPCOMPARE_RESULT_ALL_SUCCEEDED.get(numTrue, numFalse));
963          }
964          else
965          {
966            if (! useCompareResultCodeAsExitCode.isPresent())
967            {
968              resultCode = ResultCode.SUCCESS;
969            }
970
971            logCompletionMessage(false,
972                 INFO_LDAPCOMPARE_RESULT_COMPARE_MATCHED.get());
973          }
974        }
975        else if (resultCode == ResultCode.COMPARE_FALSE)
976        {
977          if (compareRequests.size() > 1)
978          {
979            resultCode = ResultCode.SUCCESS;
980            logCompletionMessage(false,
981                 INFO_LDAPCOMPARE_RESULT_ALL_SUCCEEDED.get(numTrue, numFalse));
982          }
983          else
984          {
985            if (! useCompareResultCodeAsExitCode.isPresent())
986            {
987              resultCode = ResultCode.SUCCESS;
988            }
989
990            logCompletionMessage(false,
991                 INFO_LDAPCOMPARE_RESULT_COMPARE_DID_NOT_MATCH.get());
992          }
993        }
994        else
995        {
996          if (compareRequests.size() > 1)
997          {
998            logCompletionMessage(true,
999                 ERR_LDAPCOMPARE_RESULT_WITH_ERRORS.get(numErrors, numTrue,
1000                      numFalse));
1001          }
1002          else
1003          {
1004            logCompletionMessage(true,
1005                 ERR_LDAPCOMPARE_RESULT_FAILED.get());
1006          }
1007        }
1008
1009        return resultCode;
1010      }
1011      catch (final LDAPException le)
1012      {
1013        Debug.debugException(le);
1014
1015        // Unable to create the connection pool, which means that either the
1016        // connection could not be established or the attempt to authenticate
1017        // the connection failed.  If the bind failed, then the report bind
1018        // result health check should have already reported the bind failure.
1019        // If the failure was something else, then display that failure result.
1020        if (le.getResultCode() != ResultCode.INVALID_CREDENTIALS)
1021        {
1022          for (final String line :
1023               ResultUtils.formatResult(le, true, 0, WRAP_COLUMN))
1024          {
1025            err(line);
1026          }
1027        }
1028        return le.getResultCode();
1029      }
1030    }
1031    finally
1032    {
1033      if (pool != null)
1034      {
1035        pool.close();
1036      }
1037
1038      if (writer != null)
1039      {
1040        writer.close();
1041      }
1042    }
1043  }
1044
1045
1046
1047  /**
1048   * Retrieves a list of the compare requests that should be issued.
1049   *
1050   * @return  A list of the compare requests that should be issued.
1051   *
1052   * @throws  LDAPException  If a problem occurs while obtaining the compare
1053   *                         requests to process.
1054   */
1055  @NotNull()
1056  private List<CompareRequest> getCompareRequests()
1057          throws LDAPException
1058  {
1059    final List<String> trailingArgs = argumentParser.getTrailingArguments();
1060    final int numTrailingArgs = trailingArgs.size();
1061
1062    if (assertionFile.isPresent())
1063    {
1064      if (numTrailingArgs != 0)
1065      {
1066        throw new LDAPException(ResultCode.PARAM_ERROR,
1067             ERR_LDAPCOMPARE_TRAILING_ARGS_WITH_ASSERTION_FILE.get(
1068                  assertionFile.getIdentifierString()));
1069      }
1070
1071      return readAssertionFile(getCompareControls());
1072    }
1073    else if (dnFile.isPresent())
1074    {
1075      if (numTrailingArgs != 1)
1076      {
1077        throw new LDAPException(ResultCode.PARAM_ERROR,
1078             ERR_LDAPCOMPARE_INVALID_TRAILING_ARG_COUNT_WITH_DN_FILE.get(
1079                  dnFile.getIdentifierString()));
1080      }
1081
1082      final ObjectPair<String,byte[]> ava =
1083           parseAttributeValueAssertion(trailingArgs.get(0));
1084      return readDNFile(ava.getFirst(), ava.getSecond(), getCompareControls());
1085    }
1086    else
1087    {
1088      if (numTrailingArgs < 2)
1089      {
1090        throw new LDAPException(ResultCode.PARAM_ERROR,
1091             ERR_LDAPCOMPARE_INVALID_TRAILING_ARG_COUNT_WITHOUT_FILE.get(
1092                  dnFile.getIdentifierString(),
1093                  assertionFile.getIdentifierString()));
1094      }
1095
1096      final Iterator<String> trailingArgsIterator = trailingArgs.iterator();
1097      final ObjectPair<String,byte[]> ava =
1098           parseAttributeValueAssertion(trailingArgsIterator.next());
1099      final String attributeName = ava.getFirst();
1100      final byte[] assertionValue = ava.getSecond();
1101
1102      final Control[] controls = getCompareControls();
1103
1104      final List<CompareRequest> requests = new ArrayList<>(numTrailingArgs-1);
1105      while (trailingArgsIterator.hasNext())
1106      {
1107        final String dnString = trailingArgsIterator.next();
1108        try
1109        {
1110          new DN(dnString);
1111        }
1112        catch (final LDAPException e)
1113        {
1114          Debug.debugException(e);
1115          throw new LDAPException(ResultCode.PARAM_ERROR,
1116               ERR_LDAPCOMPARE_MALFORMED_TRAILING_ARG_DN.get(dnString,
1117                    e.getMessage()),
1118               e);
1119        }
1120
1121        requests.add(new CompareRequest(dnString, attributeName,
1122             assertionValue, controls));
1123      }
1124
1125      return requests;
1126    }
1127  }
1128
1129
1130
1131  /**
1132   * Parses the provided string as an attribute value assertion.  It must
1133   * start with an attribute name or OID, and that must be followed by either a
1134   * single colon and the string representation of the assertion value, or
1135   * two colons and the base64-encoded representation of the assertion value.
1136   *
1137   * @param  avaString  The string to parse as an attribute value assertion.  It
1138   *                    must not be {@code null}.
1139   *
1140   * @return  An object pair in which the first element is the parsed attribute
1141   *          name or OID, and the second element is the parsed assertion value.
1142   *
1143   * @throws  LDAPException  If the provided string cannot be parsed as a valid
1144   *                         attribute value assertion.
1145   */
1146  @NotNull()
1147  private static ObjectPair<String,byte[]> parseAttributeValueAssertion(
1148                                                @NotNull final String avaString)
1149          throws LDAPException
1150  {
1151    final int colonPos = avaString.indexOf(':');
1152    if (colonPos < 0)
1153    {
1154      throw new LDAPException(ResultCode.PARAM_ERROR,
1155           ERR_LDAPCOMPARE_AVA_NO_COLON.get(avaString));
1156    }
1157    else if (colonPos == 0)
1158    {
1159      throw new LDAPException(ResultCode.PARAM_ERROR,
1160           ERR_LDAPCOMPARE_AVA_NO_ATTR.get(avaString));
1161    }
1162
1163    final String attributeName = avaString.substring(0, colonPos);
1164    if (colonPos == (avaString.length() - 1))
1165    {
1166      // This means that the assertion value is empty.
1167      return new ObjectPair<>(attributeName, StaticUtils.NO_BYTES);
1168    }
1169
1170    if (avaString.charAt(colonPos+1) == ':')
1171    {
1172      // This means that the assertion value is base64-encoded.
1173      try
1174      {
1175        final byte[] avaBytes = Base64.decode(avaString.substring(colonPos+2));
1176        return new ObjectPair<>(attributeName, avaBytes);
1177      }
1178      catch (final Exception e)
1179      {
1180        Debug.debugException(e);
1181        throw new LDAPException(ResultCode.PARAM_ERROR,
1182             ERR_LDAPCOMPARE_AVA_CANNOT_BASE64_DECODE_VALUE.get(avaString,
1183                  e.getMessage()),
1184             e);
1185      }
1186    }
1187    else if (avaString.charAt(colonPos+1) == '<')
1188    {
1189      // This means that the assertion value should be read from a file.  The
1190      // path to that file should immediately follow the less-than symbol, and
1191      // the exact bytes contained in that file (including line breaks) will be
1192      // used as the assertion value.
1193      final String path = avaString.substring(colonPos+2);
1194      final File file = new File(path);
1195      if (file.exists())
1196      {
1197        try
1198        {
1199          final byte[] fileBytes = StaticUtils.readFileBytes(file);
1200          return new ObjectPair<>(attributeName, fileBytes);
1201        }
1202        catch (final Exception e)
1203        {
1204          Debug.debugException(e);
1205          throw new LDAPException(ResultCode.LOCAL_ERROR,
1206               ERR_LDAPCOMPARE_AVA_CANNOT_READ_FILE.get(avaString,
1207                    file.getAbsolutePath(),
1208                    StaticUtils.getExceptionMessage(e)),
1209               e);
1210        }
1211      }
1212      else
1213      {
1214        throw new LDAPException(ResultCode.PARAM_ERROR,
1215             ERR_LDAPCOMPARE_AVA_NO_SUCH_FILE.get(avaString,
1216                  file.getAbsolutePath()));
1217      }
1218    }
1219
1220    return new ObjectPair<>(attributeName,
1221         StaticUtils.getBytes(avaString.substring(colonPos+1)));
1222  }
1223
1224
1225
1226  /**
1227   * Reads the compare requests to process from the information in the
1228   * specified assertion file.  Each line of the file must contain the DN of
1229   * the target entry followed by one or more tab characters and the
1230   * attribute-value assertion in the form expected by the
1231   * {@link #parseAttributeValueAssertion} method.  Empty lines and lines that
1232   * start with the octothorpe (#) character will be ignored.
1233   *
1234   * @param  controls  The controls to include in each of the compare requests.
1235   *                   It must not be {@code null} but may be empty.
1236   *
1237   * @return  A list of the compare requests that should be issued.
1238   *
1239   * @throws  LDAPException  If a problem is encountered while parsing the
1240   *                         contents of the assertion file.
1241   */
1242  @NotNull()
1243  private List<CompareRequest> readAssertionFile(
1244               @NotNull final Control[] controls)
1245          throws LDAPException
1246  {
1247    final File f = assertionFile.getValue();
1248    try (FileReader fileReader = new FileReader(f);
1249         BufferedReader bufferedReader = new BufferedReader(fileReader))
1250    {
1251      int lineNumber = 0;
1252      final List<CompareRequest> compareRequests = new ArrayList<>();
1253      while (true)
1254      {
1255        // Read the next line from the file.  If it is null, then we've hit the
1256        // end fo the file.  If the line is empty or starts with an octothorpe,
1257        // then skip it and read the next line.
1258        final String line = bufferedReader.readLine();
1259        if (line == null)
1260        {
1261          if (compareRequests.isEmpty())
1262          {
1263            throw new LDAPException(ResultCode.PARAM_ERROR,
1264                 ERR_LDAPCOMPARE_ASSERTION_FILE_EMPTY.get(f.getAbsolutePath()));
1265          }
1266
1267          return compareRequests;
1268        }
1269
1270        lineNumber++;
1271        if (line.isEmpty() || line.startsWith("#"))
1272        {
1273          continue;
1274        }
1275
1276
1277        // Find the first tab on the line.  Then, skip over any subsequent
1278        // tabs to find the assertion value.
1279        int tabPos = line.indexOf('\t');
1280        if (tabPos < 0)
1281        {
1282          throw new LDAPException(ResultCode.DECODING_ERROR,
1283               ERR_LDAPCOMPARE_ASSERTION_FILE_LINE_MISSING_TAB.get(
1284                    line, lineNumber, f.getAbsolutePath()));
1285        }
1286
1287
1288        final String dn = line.substring(0, tabPos);
1289        try
1290        {
1291          new DN(dn);
1292        }
1293        catch (final LDAPException e)
1294        {
1295          Debug.debugException(e);
1296          throw new LDAPException(ResultCode.DECODING_ERROR,
1297               ERR_LDAPCOMPARE_ASSERTION_FILE_LINE_INVALID_DN.get(
1298                    line, lineNumber, f.getAbsolutePath(), dn, e.getMessage()),
1299               e);
1300        }
1301
1302        for (int i=(tabPos+1); i < line.length(); i++)
1303        {
1304          if (line.charAt(i) == '\t')
1305          {
1306            tabPos = i;
1307          }
1308        }
1309
1310        final String avaString = line.substring(tabPos+1);
1311        if (avaString.isEmpty())
1312        {
1313          throw new LDAPException(ResultCode.DECODING_ERROR,
1314               ERR_LDAPCOMPARE_ASSERTION_FILE_LINE_MISSING_AVA.get(
1315                    line, lineNumber, f.getAbsolutePath()));
1316        }
1317
1318
1319        final ObjectPair<String,byte[]> ava;
1320        try
1321        {
1322          ava = parseAttributeValueAssertion(avaString);
1323        }
1324        catch (final LDAPException e)
1325        {
1326          Debug.debugException(e);
1327          throw new LDAPException(ResultCode.DECODING_ERROR,
1328               ERR_LDAPCOMPARE_ASSERTION_FILE_CANNOT_PARSE_AVA.get(
1329                    line, lineNumber, f.getAbsolutePath(), e.getMessage()),
1330               e);
1331        }
1332
1333        compareRequests.add(new CompareRequest(dn, ava.getFirst(),
1334             ava.getSecond(), controls));
1335      }
1336    }
1337    catch (final IOException e)
1338    {
1339      Debug.debugException(e);
1340      throw new LDAPException(ResultCode.LOCAL_ERROR,
1341           ERR_LDAPCOMPARE_CANNOT_READ_ASSERTION_FILE.get(
1342                f.getAbsolutePath(), StaticUtils.getExceptionMessage(e)),
1343           e);
1344    }
1345  }
1346
1347
1348
1349  /**
1350   * Reads the DN file to obtain the DNs of the entries to target and creates
1351   * the list of compare requests to process.  Each line of the file should
1352   * contain the DN of an entry to process.  Empty lines and lines that start
1353   * with the octothorpe (#) character will be ignored.
1354   *
1355   * @param  attributeName   The name or OID of the attribute to target with
1356   *                         each of the compare requests.  It must not be
1357   *                         {@code null} or empty.
1358   * @param  assertionValue  The assertion value to use for each of the
1359   *                         compare requests.  It must not be {@code null}.
1360   * @param  controls        The controls to include in each of the compare
1361   *                         requests.  It must not be {@code null} but may be
1362   *                         empty.
1363   *
1364   * @return  A list of the compare requests that should be issued.
1365   *
1366   * @throws  LDAPException  If a problem is encountered while parsing the
1367   *                         contents of the assertion file.
1368   */
1369  @NotNull()
1370  private List<CompareRequest> readDNFile(@NotNull final String attributeName,
1371                                          @NotNull final byte[] assertionValue,
1372                                          @NotNull final Control[] controls)
1373          throws LDAPException
1374  {
1375    try (DNFileReader dnFileReader = new DNFileReader(dnFile.getValue()))
1376    {
1377      final List<CompareRequest> compareRequests = new ArrayList<>();
1378      while (true)
1379      {
1380        final DN dn;
1381        try
1382        {
1383          dn = dnFileReader.readDN();
1384        }
1385        catch (final LDAPException e)
1386        {
1387          Debug.debugException(e);
1388          throw new LDAPException(ResultCode.DECODING_ERROR, e.getMessage(), e);
1389        }
1390
1391        if (dn == null)
1392        {
1393          if (compareRequests.isEmpty())
1394          {
1395            throw new LDAPException(ResultCode.PARAM_ERROR,
1396                 ERR_LDAPCOMPARE_DN_FILE_EMPTY.get(
1397                      dnFile.getValue().getAbsolutePath()));
1398          }
1399
1400          return compareRequests;
1401        }
1402
1403        compareRequests.add(new CompareRequest(dn, attributeName,
1404             assertionValue, controls));
1405      }
1406    }
1407    catch (final IOException e)
1408    {
1409      Debug.debugException(e);
1410      throw new LDAPException(ResultCode.LOCAL_ERROR,
1411           ERR_LDAPCOMPARE_CANNOT_READ_DN_FILE.get(
1412                dnFile.getValue().getAbsolutePath(),
1413                StaticUtils.getExceptionMessage(e)),
1414           e);
1415    }
1416  }
1417
1418
1419
1420  /**
1421   * Retrieves the controls that should be included in compare requests.
1422   *
1423   * @return  The controls that should be included in compare requests, or an
1424   *          empty array if no controls should be included.
1425   *
1426   * @throws  LDAPException  If a problem occurs while trying to create any of
1427   *                         the controls.
1428   */
1429  @NotNull()
1430  private Control[] getCompareControls()
1431          throws LDAPException
1432  {
1433    final List<Control> controls = new ArrayList<>();
1434
1435    if (compareControl.isPresent())
1436    {
1437      controls.addAll(compareControl.getValues());
1438    }
1439
1440    if (proxyAs.isPresent())
1441    {
1442      controls.add(new ProxiedAuthorizationV2RequestControl(
1443           proxyAs.getValue()));
1444    }
1445
1446    if (proxyV1As.isPresent())
1447    {
1448      controls.add(new ProxiedAuthorizationV1RequestControl(
1449           proxyV1As.getValue()));
1450    }
1451
1452    if (manageDsaIT.isPresent())
1453    {
1454      controls.add(new ManageDsaITRequestControl(false));
1455    }
1456
1457    if (assertionFilter.isPresent())
1458    {
1459      controls.add(new AssertionRequestControl(assertionFilter.getValue()));
1460    }
1461
1462    if (operationPurpose.isPresent())
1463    {
1464      controls.add(new OperationPurposeRequestControl(false, getToolName(),
1465           getToolVersion(),
1466           LDAPPasswordModify.class.getName() + ".getUpdateControls",
1467           operationPurpose.getValue()));
1468    }
1469
1470    return controls.toArray(StaticUtils.NO_CONTROLS);
1471  }
1472
1473
1474
1475  /**
1476   * Writes information about the compare result.
1477   *
1478   * @param  writer         The writer to use to write to the output file.  It
1479   *                        may be {@code null} if no output file should be
1480   *                        used.
1481   * @param  outputHandler  The output handler that should be used to format the
1482   *                        result information.  It must not be {@code null}.
1483   * @param  request        The compare request that was processed.  It must not
1484   *                        be {@code null}.
1485   * @param  result         The result for the compare operation.  It must not
1486   *                        be {@code null}.
1487   *
1488   * @throws  LDAPException  If a problem occurred while trying to write the
1489   *                         result.
1490   */
1491  private void writeResult(@Nullable final PrintStream writer,
1492                    @NotNull final LDAPCompareOutputHandler outputHandler,
1493                    @NotNull final CompareRequest request,
1494                    @NotNull final LDAPResult result)
1495          throws LDAPException
1496  {
1497    if (shouldWriteResultToStdErr(result))
1498    {
1499      err();
1500      err(INFO_LDAPCOMPARE_RESULT_HEADER.get());
1501      err(INFO_LDAPCOMPARE_RESULT_HEADER_DN.get(request.getDN()));
1502      err(INFO_LDAPCOMPARE_RESULT_HEADER_ATTR.get(request.getAttributeName()));
1503      err(INFO_LDAPCOMPARE_RESULT_HEADER_VALUE.get(
1504           request.getAssertionValue()));
1505      for (final String line : ResultUtils.formatResult(result, true, 0,
1506           WRAP_COLUMN))
1507      {
1508        err(line);
1509      }
1510    }
1511
1512    final String message = outputHandler.formatResult(request, result);
1513    if (writer != null)
1514    {
1515      writer.println(message);
1516    }
1517
1518    if ((writer == null) || teeOutput.isPresent())
1519    {
1520      out(message);
1521    }
1522  }
1523
1524
1525
1526  /**
1527   * Indicates whether to write information about the provided result to
1528   * standard error.
1529   *
1530   * @param  result  The result for which to make the determination.  It must
1531   *                 not be {@code mull}.
1532   *
1533   * @return  {@code true} if information about the result should be written to
1534   *          standard error, or {@code false} if not.
1535   */
1536  private boolean shouldWriteResultToStdErr(@NotNull final LDAPResult result)
1537  {
1538    if (verbose.isPresent())
1539    {
1540      return true;
1541    }
1542
1543    if (terse.isPresent())
1544    {
1545      return false;
1546    }
1547
1548    if (result.hasResponseControl())
1549    {
1550      return true;
1551    }
1552
1553    return ((result.getResultCode() != ResultCode.COMPARE_TRUE) &&
1554         (result.getResultCode() != ResultCode.COMPARE_FALSE));
1555  }
1556
1557
1558
1559  /**
1560   * Writes the provided message and sets it as the completion message.
1561   *
1562   * @param  isError  Indicates whether the message should be written to
1563   *                  standard error rather than standard output.
1564   * @param  message  The message to be written.
1565   */
1566  private void logCompletionMessage(final boolean isError,
1567                                    @NotNull final String message)
1568  {
1569    completionMessage.compareAndSet(null, message);
1570
1571    if (! terse.isPresent())
1572    {
1573      if (isError)
1574      {
1575        wrapErr(0, WRAP_COLUMN, message);
1576      }
1577      else
1578      {
1579        wrapOut(0, WRAP_COLUMN, message);
1580      }
1581    }
1582  }
1583
1584
1585
1586  /**
1587   * {@inheritDoc}
1588   */
1589  @Override()
1590  public void handleUnsolicitedNotification(
1591                   @NotNull final LDAPConnection connection,
1592                   @NotNull final ExtendedResult notification)
1593  {
1594    if (! terse.isPresent())
1595    {
1596      final ArrayList<String> lines = new ArrayList<>(10);
1597      ResultUtils.formatUnsolicitedNotification(lines, notification, true, 0,
1598           WRAP_COLUMN);
1599      for (final String line : lines)
1600      {
1601        err(line);
1602      }
1603      err();
1604    }
1605  }
1606
1607
1608
1609  /**
1610   * {@inheritDoc}
1611   */
1612  @Override()
1613  @NotNull()
1614  public LinkedHashMap<String[],String> getExampleUsages()
1615  {
1616    final LinkedHashMap<String[],String> examples = new LinkedHashMap<>();
1617
1618    examples.put(
1619         new String[]
1620         {
1621           "--hostname", "ds.example.com",
1622           "--port", "636",
1623           "--useSSL",
1624           "--bindDN", "uid=admin,dc=example,dc=com",
1625           "l:Austin",
1626           "uid=jdoe,ou=People,dc=example,dc=com"
1627         },
1628         INFO_LDAPCOMPARE_EXAMPLE_1.get());
1629
1630    examples.put(
1631         new String[]
1632         {
1633           "--hostname", "ds.example.com",
1634           "--port", "636",
1635           "--useSSL",
1636           "--bindDN", "uid=admin,dc=example,dc=com",
1637           "--dnFile", "entry-dns.txt",
1638           "--outputFormat", "csv",
1639           "--terse",
1640           "title:manager"
1641         },
1642         INFO_LDAPCOMPARE_EXAMPLE_2.get());
1643
1644    examples.put(
1645         new String[]
1646         {
1647           "--hostname", "ds.example.com",
1648           "--port", "636",
1649           "--useSSL",
1650           "--bindDN", "uid=admin,dc=example,dc=com",
1651           "--assertionFile", "compare-assertions.txt",
1652           "--outputFormat", "json",
1653           "--outputFile", "compare-assertion-results.json",
1654           "--verbose"
1655         },
1656         INFO_LDAPCOMPARE_EXAMPLE_3.get());
1657
1658    return examples;
1659  }
1660}