001/*
002 * Copyright 2013-2024 Ping Identity Corporation
003 * All Rights Reserved.
004 */
005/*
006 * Copyright 2013-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) 2013-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.examples;
037
038
039
040import java.io.OutputStream;
041import java.util.ArrayList;
042import java.util.Collections;
043import java.util.LinkedHashMap;
044import java.util.LinkedHashSet;
045import java.util.List;
046import java.util.Map;
047import java.util.Set;
048import java.util.TreeMap;
049import java.util.concurrent.atomic.AtomicBoolean;
050import java.util.concurrent.atomic.AtomicLong;
051
052import com.unboundid.asn1.ASN1OctetString;
053import com.unboundid.ldap.sdk.Attribute;
054import com.unboundid.ldap.sdk.DereferencePolicy;
055import com.unboundid.ldap.sdk.DN;
056import com.unboundid.ldap.sdk.Filter;
057import com.unboundid.ldap.sdk.LDAPConnectionOptions;
058import com.unboundid.ldap.sdk.LDAPConnectionPool;
059import com.unboundid.ldap.sdk.LDAPException;
060import com.unboundid.ldap.sdk.LDAPSearchException;
061import com.unboundid.ldap.sdk.ResultCode;
062import com.unboundid.ldap.sdk.SearchRequest;
063import com.unboundid.ldap.sdk.SearchResult;
064import com.unboundid.ldap.sdk.SearchResultEntry;
065import com.unboundid.ldap.sdk.SearchResultReference;
066import com.unboundid.ldap.sdk.SearchResultListener;
067import com.unboundid.ldap.sdk.SearchScope;
068import com.unboundid.ldap.sdk.Version;
069import com.unboundid.ldap.sdk.controls.SimplePagedResultsControl;
070import com.unboundid.ldap.sdk.extensions.CancelExtendedRequest;
071import com.unboundid.util.Debug;
072import com.unboundid.util.LDAPCommandLineTool;
073import com.unboundid.util.NotNull;
074import com.unboundid.util.Nullable;
075import com.unboundid.util.StaticUtils;
076import com.unboundid.util.ThreadSafety;
077import com.unboundid.util.ThreadSafetyLevel;
078import com.unboundid.util.args.ArgumentException;
079import com.unboundid.util.args.ArgumentParser;
080import com.unboundid.util.args.DNArgument;
081import com.unboundid.util.args.FilterArgument;
082import com.unboundid.util.args.IntegerArgument;
083import com.unboundid.util.args.StringArgument;
084
085
086
087/**
088 * This class provides a tool that may be used to identify unique attribute
089 * conflicts (i.e., attributes which are supposed to be unique but for which
090 * some values exist in multiple entries).
091 * <BR><BR>
092 * All of the necessary information is provided using command line arguments.
093 * Supported arguments include those allowed by the {@link LDAPCommandLineTool}
094 * class, as well as the following additional arguments:
095 * <UL>
096 *   <LI>"-b {baseDN}" or "--baseDN {baseDN}" -- specifies the base DN to use
097 *       for the searches.  At least one base DN must be provided.</LI>
098 *   <LI>"-f" {filter}" or "--filter "{filter}" -- specifies an optional
099 *       filter to use for identifying entries across which uniqueness should be
100 *       enforced.  If this is not provided, then all entries containing the
101 *       target attribute(s) will be examined.</LI>
102 *   <LI>"-A {attribute}" or "--attribute {attribute}" -- specifies an attribute
103 *       for which to enforce uniqueness.  At least one unique attribute must be
104 *       provided.</LI>
105 *   <LI>"-m {behavior}" or "--multipleAttributeBehavior {behavior}" --
106 *       specifies the behavior that the tool should exhibit if multiple
107 *       unique attributes are provided.  Allowed values include
108 *       unique-within-each-attribute,
109 *       unique-across-all-attributes-including-in-same-entry,
110 *       unique-across-all-attributes-except-in-same-entry, and
111 *       unique-in-combination.</LI>
112 *   <LI>"-z {size}" or "--simplePageSize {size}" -- indicates that the search
113 *       to find entries with unique attributes should use the simple paged
114 *       results control to iterate across entries in fixed-size pages rather
115 *       than trying to use a single search to identify all entries containing
116 *       unique attributes.</LI>
117 * </UL>
118 */
119@ThreadSafety(level=ThreadSafetyLevel.NOT_THREADSAFE)
120public final class IdentifyUniqueAttributeConflicts
121       extends LDAPCommandLineTool
122       implements SearchResultListener
123{
124  /**
125   * The unique attribute behavior value that indicates uniqueness should only
126   * be ensured within each attribute.
127   */
128  @NotNull private static final String BEHAVIOR_UNIQUE_WITHIN_ATTR =
129       "unique-within-each-attribute";
130
131
132
133  /**
134   * The unique attribute behavior value that indicates uniqueness should be
135   * ensured across all attributes, and conflicts will not be allowed across
136   * attributes in the same entry.
137   */
138  @NotNull private static final String
139       BEHAVIOR_UNIQUE_ACROSS_ATTRS_INCLUDING_SAME =
140            "unique-across-all-attributes-including-in-same-entry";
141
142
143
144  /**
145   * The unique attribute behavior value that indicates uniqueness should be
146   * ensured across all attributes, except that conflicts will not be allowed
147   * across attributes in the same entry.
148   */
149  @NotNull private static final String
150       BEHAVIOR_UNIQUE_ACROSS_ATTRS_EXCEPT_SAME =
151            "unique-across-all-attributes-except-in-same-entry";
152
153
154
155  /**
156   * The unique attribute behavior value that indicates uniqueness should be
157   * ensured for the combination of attribute values.
158   */
159  @NotNull private static final String BEHAVIOR_UNIQUE_IN_COMBINATION =
160       "unique-in-combination";
161
162
163
164  /**
165   * The default value for the timeLimit argument.
166   */
167  private static final int DEFAULT_TIME_LIMIT_SECONDS = 10;
168
169
170
171  /**
172   * The serial version UID for this serializable class.
173   */
174  private static final long serialVersionUID = 4216291898088659008L;
175
176
177
178  // Indicates whether a TIME_LIMIT_EXCEEDED result has been encountered during
179  // processing.
180  @NotNull private final AtomicBoolean timeLimitExceeded;
181
182  // The number of entries examined so far.
183  @NotNull private final AtomicLong entriesExamined;
184
185  // The number of conflicts found from a combination of attributes.
186  @NotNull private final AtomicLong combinationConflictCounts;
187
188  // Indicates whether cross-attribute uniqueness conflicts should be allowed
189  // in the same entry.
190  private boolean allowConflictsInSameEntry;
191
192  // Indicates whether uniqueness should be enforced across all attributes
193  // rather than within each attribute.
194  private boolean uniqueAcrossAttributes;
195
196  // Indicates whether uniqueness should be enforced for the combination
197  // of attribute values.
198  private boolean uniqueInCombination;
199
200  // The argument used to specify the base DNs to use for searches.
201  @Nullable private DNArgument baseDNArgument;
202
203  // The argument used to specify a filter indicating which entries to examine.
204  @Nullable private FilterArgument filterArgument;
205
206  // The argument used to specify the search page size.
207  @Nullable private IntegerArgument pageSizeArgument;
208
209  // The argument used to specify the time limit for the searches used to find
210  // conflicting entries.
211  @Nullable private IntegerArgument timeLimitArgument;
212
213  // The connection to use for finding unique attribute conflicts.
214  @Nullable private LDAPConnectionPool findConflictsPool;
215
216  // A map with counts of unique attribute conflicts by attribute type.
217  @NotNull private final Map<String, AtomicLong> conflictCounts;
218
219  // The names of the attributes for which to find uniqueness conflicts.
220  @Nullable private String[] attributes;
221
222  // The set of base DNs to use for the searches.
223  @Nullable private String[] baseDNs;
224
225  // The argument used to specify the attributes for which to find uniqueness
226  // conflicts.
227  @Nullable private StringArgument attributeArgument;
228
229  // The argument used to specify the behavior that should be exhibited if
230  // multiple attributes are specified.
231  @Nullable private StringArgument multipleAttributeBehaviorArgument;
232
233
234  /**
235   * Parse the provided command line arguments and perform the appropriate
236   * processing.
237   *
238   * @param  args  The command line arguments provided to this program.
239   */
240  public static void main(@NotNull final String... args)
241  {
242    final ResultCode resultCode = main(args, System.out, System.err);
243    if (resultCode != ResultCode.SUCCESS)
244    {
245      System.exit(resultCode.intValue());
246    }
247  }
248
249
250
251  /**
252   * Parse the provided command line arguments and perform the appropriate
253   * processing.
254   *
255   * @param  args       The command line arguments provided to this program.
256   * @param  outStream  The output stream to which standard out should be
257   *                    written.  It may be {@code null} if output should be
258   *                    suppressed.
259   * @param  errStream  The output stream to which standard error should be
260   *                    written.  It may be {@code null} if error messages
261   *                    should be suppressed.
262   *
263   * @return A result code indicating whether the processing was successful.
264   */
265  @NotNull()
266  public static ResultCode main(@NotNull final String[] args,
267                                @Nullable final OutputStream outStream,
268                                @Nullable final OutputStream errStream)
269  {
270    final IdentifyUniqueAttributeConflicts tool =
271         new IdentifyUniqueAttributeConflicts(outStream, errStream);
272    return tool.runTool(args);
273  }
274
275
276
277  /**
278   * Creates a new instance of this tool.
279   *
280   * @param  outStream  The output stream to which standard out should be
281   *                    written.  It may be {@code null} if output should be
282   *                    suppressed.
283   * @param  errStream  The output stream to which standard error should be
284   *                    written.  It may be {@code null} if error messages
285   *                    should be suppressed.
286   */
287  public IdentifyUniqueAttributeConflicts(
288              @Nullable final OutputStream outStream,
289              @Nullable final OutputStream errStream)
290  {
291    super(outStream, errStream);
292
293    baseDNArgument = null;
294    filterArgument = null;
295    pageSizeArgument = null;
296    attributeArgument = null;
297    multipleAttributeBehaviorArgument = null;
298    findConflictsPool = null;
299    allowConflictsInSameEntry = false;
300    uniqueAcrossAttributes = false;
301    uniqueInCombination = false;
302    attributes = null;
303    baseDNs = null;
304    timeLimitArgument = null;
305
306    timeLimitExceeded = new AtomicBoolean(false);
307    entriesExamined = new AtomicLong(0L);
308    combinationConflictCounts = new AtomicLong(0L);
309    conflictCounts = new TreeMap<>();
310  }
311
312
313
314  /**
315   * Retrieves the name of this tool.  It should be the name of the command used
316   * to invoke this tool.
317   *
318   * @return The name for this tool.
319   */
320  @Override()
321  @NotNull()
322  public String getToolName()
323  {
324    return "identify-unique-attribute-conflicts";
325  }
326
327
328
329  /**
330   * Retrieves a human-readable description for this tool.
331   *
332   * @return A human-readable description for this tool.
333   */
334  @Override()
335  @NotNull()
336  public String getToolDescription()
337  {
338    return "This tool may be used to identify unique attribute conflicts.  " +
339         "That is, it may identify values of one or more attributes which " +
340         "are supposed to exist only in a single entry but are found in " +
341         "multiple entries.";
342  }
343
344
345
346  /**
347   * Retrieves a version string for this tool, if available.
348   *
349   * @return A version string for this tool, or {@code null} if none is
350   *          available.
351   */
352  @Override()
353  @NotNull()
354  public String getToolVersion()
355  {
356    return Version.NUMERIC_VERSION_STRING;
357  }
358
359
360
361  /**
362   * Indicates whether this tool should provide support for an interactive mode,
363   * in which the tool offers a mode in which the arguments can be provided in
364   * a text-driven menu rather than requiring them to be given on the command
365   * line.  If interactive mode is supported, it may be invoked using the
366   * "--interactive" argument.  Alternately, if interactive mode is supported
367   * and {@link #defaultsToInteractiveMode()} returns {@code true}, then
368   * interactive mode may be invoked by simply launching the tool without any
369   * arguments.
370   *
371   * @return  {@code true} if this tool supports interactive mode, or
372   *          {@code false} if not.
373   */
374  @Override()
375  public boolean supportsInteractiveMode()
376  {
377    return true;
378  }
379
380
381
382  /**
383   * Indicates whether this tool defaults to launching in interactive mode if
384   * the tool is invoked without any command-line arguments.  This will only be
385   * used if {@link #supportsInteractiveMode()} returns {@code true}.
386   *
387   * @return  {@code true} if this tool defaults to using interactive mode if
388   *          launched without any command-line arguments, or {@code false} if
389   *          not.
390   */
391  @Override()
392  public boolean defaultsToInteractiveMode()
393  {
394    return true;
395  }
396
397
398
399  /**
400   * Indicates whether this tool should provide arguments for redirecting output
401   * to a file.  If this method returns {@code true}, then the tool will offer
402   * an "--outputFile" argument that will specify the path to a file to which
403   * all standard output and standard error content will be written, and it will
404   * also offer a "--teeToStandardOut" argument that can only be used if the
405   * "--outputFile" argument is present and will cause all output to be written
406   * to both the specified output file and to standard output.
407   *
408   * @return  {@code true} if this tool should provide arguments for redirecting
409   *          output to a file, or {@code false} if not.
410   */
411  @Override()
412  protected boolean supportsOutputFile()
413  {
414    return true;
415  }
416
417
418
419  /**
420   * Indicates whether this tool should default to interactively prompting for
421   * the bind password if a password is required but no argument was provided
422   * to indicate how to get the password.
423   *
424   * @return  {@code true} if this tool should default to interactively
425   *          prompting for the bind password, or {@code false} if not.
426   */
427  @Override()
428  protected boolean defaultToPromptForBindPassword()
429  {
430    return true;
431  }
432
433
434
435  /**
436   * Indicates whether this tool supports the use of a properties file for
437   * specifying default values for arguments that aren't specified on the
438   * command line.
439   *
440   * @return  {@code true} if this tool supports the use of a properties file
441   *          for specifying default values for arguments that aren't specified
442   *          on the command line, or {@code false} if not.
443   */
444  @Override()
445  public boolean supportsPropertiesFile()
446  {
447    return true;
448  }
449
450
451
452  /**
453   * Indicates whether this tool supports the ability to generate a debug log
454   * file.  If this method returns {@code true}, then the tool will expose
455   * additional arguments that can control debug logging.
456   *
457   * @return  {@code true} if this tool supports the ability to generate a debug
458   *          log file, or {@code false} if not.
459   */
460  @Override()
461  protected boolean supportsDebugLogging()
462  {
463    return true;
464  }
465
466
467
468  /**
469   * Indicates whether the LDAP-specific arguments should include alternate
470   * versions of all long identifiers that consist of multiple words so that
471   * they are available in both camelCase and dash-separated versions.
472   *
473   * @return  {@code true} if this tool should provide multiple versions of
474   *          long identifiers for LDAP-specific arguments, or {@code false} if
475   *          not.
476   */
477  @Override()
478  protected boolean includeAlternateLongIdentifiers()
479  {
480    return true;
481  }
482
483
484
485  /**
486   * Indicates whether this tool should provide a command-line argument that
487   * allows for low-level SSL debugging.  If this returns {@code true}, then an
488   * "--enableSSLDebugging}" argument will be added that sets the
489   * "javax.net.debug" system property to "all" before attempting any
490   * communication.
491   *
492   * @return  {@code true} if this tool should offer an "--enableSSLDebugging"
493   *          argument, or {@code false} if not.
494   */
495  @Override()
496  protected boolean supportsSSLDebugging()
497  {
498    return true;
499  }
500
501
502
503  /**
504   * Adds the arguments needed by this command-line tool to the provided
505   * argument parser which are not related to connecting or authenticating to
506   * the directory server.
507   *
508   * @param  parser  The argument parser to which the arguments should be added.
509   *
510   * @throws ArgumentException  If a problem occurs while adding the arguments.
511   */
512  @Override()
513  public void addNonLDAPArguments(@NotNull final ArgumentParser parser)
514       throws ArgumentException
515  {
516    String description = "The search base DN(s) to use to find entries with " +
517         "attributes for which to find uniqueness conflicts.  At least one " +
518         "base DN must be specified.";
519    baseDNArgument = new DNArgument('b', "baseDN", true, 0, "{dn}",
520         description);
521    baseDNArgument.addLongIdentifier("base-dn", true);
522    parser.addArgument(baseDNArgument);
523
524    description = "A filter that will be used to identify the set of " +
525         "entries in which to identify uniqueness conflicts.  If this is not " +
526         "specified, then all entries containing the target attribute(s) " +
527         "will be examined.";
528    filterArgument = new FilterArgument('f', "filter", false, 1, "{filter}",
529         description);
530    parser.addArgument(filterArgument);
531
532    description = "The attributes for which to find uniqueness conflicts.  " +
533         "At least one attribute must be specified, and each attribute " +
534         "must be indexed for equality searches.";
535    attributeArgument = new StringArgument('A', "attribute", true, 0, "{attr}",
536         description);
537    parser.addArgument(attributeArgument);
538
539    description = "Indicates the behavior to exhibit if multiple unique " +
540         "attributes are provided.  Allowed values are '" +
541         BEHAVIOR_UNIQUE_WITHIN_ATTR + "' (indicates that each value only " +
542         "needs to be unique within its own attribute type), '" +
543         BEHAVIOR_UNIQUE_ACROSS_ATTRS_INCLUDING_SAME + "' (indicates that " +
544         "each value needs to be unique across all of the specified " +
545         "attributes), '" + BEHAVIOR_UNIQUE_ACROSS_ATTRS_EXCEPT_SAME +
546         "' (indicates each value needs to be unique across all of the " +
547         "specified attributes, except that multiple attributes in the same " +
548         "entry are allowed to share the same value), and '" +
549         BEHAVIOR_UNIQUE_IN_COMBINATION + "' (indicates that every " +
550         "combination of the values of the specified attributes must be " +
551         "unique across each entry).";
552    final Set<String> allowedValues = StaticUtils.setOf(
553         BEHAVIOR_UNIQUE_WITHIN_ATTR,
554         BEHAVIOR_UNIQUE_ACROSS_ATTRS_INCLUDING_SAME,
555         BEHAVIOR_UNIQUE_ACROSS_ATTRS_EXCEPT_SAME,
556         BEHAVIOR_UNIQUE_IN_COMBINATION);
557    multipleAttributeBehaviorArgument = new StringArgument('m',
558         "multipleAttributeBehavior", false, 1, "{behavior}", description,
559         allowedValues, BEHAVIOR_UNIQUE_WITHIN_ATTR);
560    multipleAttributeBehaviorArgument.addLongIdentifier(
561         "multiple-attribute-behavior", true);
562    parser.addArgument(multipleAttributeBehaviorArgument);
563
564    description = "The maximum number of entries to retrieve at a time when " +
565         "attempting to find uniqueness conflicts.  This requires that the " +
566         "authenticated user have permission to use the simple paged results " +
567         "control, but it can avoid problems with the server sending entries " +
568         "too quickly for the client to handle.  By default, the simple " +
569         "paged results control will not be used.";
570    pageSizeArgument =
571         new IntegerArgument('z', "simplePageSize", false, 1, "{num}",
572              description, 1, Integer.MAX_VALUE);
573    pageSizeArgument.addLongIdentifier("simple-page-size", true);
574    parser.addArgument(pageSizeArgument);
575
576    description = "The time limit in seconds that will be used for search " +
577         "requests attempting to identify conflicts for each value of any of " +
578         "the unique attributes.  This time limit is used to avoid sending " +
579         "expensive unindexed search requests that can consume significant " +
580         "server resources.  If any of these search operations fails in a " +
581         "way that indicates the requested time limit was exceeded, the " +
582         "tool will abort its processing.  A value of zero indicates that no " +
583         "time limit will be enforced.  If this argument is not provided, a " +
584         "default time limit of " + DEFAULT_TIME_LIMIT_SECONDS +
585         " will be used.";
586    timeLimitArgument = new IntegerArgument('l', "timeLimitSeconds", false, 1,
587         "{num}", description, 0, Integer.MAX_VALUE,
588         DEFAULT_TIME_LIMIT_SECONDS);
589    timeLimitArgument.addLongIdentifier("timeLimit", true);
590    timeLimitArgument.addLongIdentifier("time-limit-seconds", true);
591    timeLimitArgument.addLongIdentifier("time-limit", true);
592
593    parser.addArgument(timeLimitArgument);
594  }
595
596
597
598  /**
599   * Retrieves the connection options that should be used for connections that
600   * are created with this command line tool.  Subclasses may override this
601   * method to use a custom set of connection options.
602   *
603   * @return  The connection options that should be used for connections that
604   *          are created with this command line tool.
605   */
606  @Override()
607  @NotNull()
608  public LDAPConnectionOptions getConnectionOptions()
609  {
610    final LDAPConnectionOptions options = new LDAPConnectionOptions();
611
612    options.setUseSynchronousMode(true);
613    options.setResponseTimeoutMillis(0L);
614
615    return options;
616  }
617
618
619
620  /**
621   * Performs the core set of processing for this tool.
622   *
623   * @return  A result code that indicates whether the processing completed
624   *          successfully.
625   */
626  @Override()
627  @NotNull()
628  public ResultCode doToolProcessing()
629  {
630    // Determine the multi-attribute behavior that we should exhibit.
631    final List<String> attrList = attributeArgument.getValues();
632    final String multiAttrBehavior =
633         multipleAttributeBehaviorArgument.getValue();
634    if (attrList.size() > 1)
635    {
636      if (multiAttrBehavior.equalsIgnoreCase(
637           BEHAVIOR_UNIQUE_ACROSS_ATTRS_INCLUDING_SAME))
638      {
639        uniqueAcrossAttributes = true;
640        uniqueInCombination = false;
641        allowConflictsInSameEntry = false;
642      }
643      else if (multiAttrBehavior.equalsIgnoreCase(
644           BEHAVIOR_UNIQUE_ACROSS_ATTRS_EXCEPT_SAME))
645      {
646        uniqueAcrossAttributes = true;
647        uniqueInCombination = false;
648        allowConflictsInSameEntry = true;
649      }
650      else if (multiAttrBehavior.equalsIgnoreCase(
651           BEHAVIOR_UNIQUE_IN_COMBINATION))
652      {
653        uniqueAcrossAttributes = false;
654        uniqueInCombination = true;
655        allowConflictsInSameEntry = true;
656      }
657      else
658      {
659        uniqueAcrossAttributes = false;
660        uniqueInCombination = false;
661        allowConflictsInSameEntry = true;
662      }
663    }
664    else
665    {
666      uniqueAcrossAttributes = false;
667      uniqueInCombination = false;
668      allowConflictsInSameEntry = true;
669    }
670
671
672    // Get the string representations of the base DNs.
673    final List<DN> dnList = baseDNArgument.getValues();
674    baseDNs = new String[dnList.size()];
675    for (int i=0; i < baseDNs.length; i++)
676    {
677      baseDNs[i] = dnList.get(i).toString();
678    }
679
680    // Establish a connection to the target directory server to use for finding
681    // entries with unique attributes.
682    final LDAPConnectionPool findUniqueAttributesPool;
683    try
684    {
685      findUniqueAttributesPool = getConnectionPool(1, 1);
686      findUniqueAttributesPool.
687           setRetryFailedOperationsDueToInvalidConnections(true);
688    }
689    catch (final LDAPException le)
690    {
691      Debug.debugException(le);
692      err("Unable to establish a connection to the directory server:  ",
693           StaticUtils.getExceptionMessage(le));
694      return le.getResultCode();
695    }
696
697    try
698    {
699      // Establish a connection to use for finding unique attribute conflicts.
700      try
701      {
702        findConflictsPool= getConnectionPool(1, 1);
703        findConflictsPool.setRetryFailedOperationsDueToInvalidConnections(true);
704      }
705      catch (final LDAPException le)
706      {
707        Debug.debugException(le);
708        err("Unable to establish a connection to the directory server:  ",
709             StaticUtils.getExceptionMessage(le));
710        return le.getResultCode();
711      }
712
713      // Get the set of attributes for which to ensure uniqueness.
714      attributes = new String[attrList.size()];
715      attrList.toArray(attributes);
716
717
718      // Construct a search filter that will be used to find all entries with
719      // unique attributes.
720      Filter filter;
721      if (attributes.length == 1)
722      {
723        filter = Filter.createPresenceFilter(attributes[0]);
724        conflictCounts.put(attributes[0], new AtomicLong(0L));
725      }
726      else if (uniqueInCombination)
727      {
728        final Filter[] andComps = new Filter[attributes.length];
729        for (int i=0; i < attributes.length; i++)
730        {
731          andComps[i] = Filter.createPresenceFilter(attributes[i]);
732          conflictCounts.put(attributes[i], new AtomicLong(0L));
733        }
734        filter = Filter.createANDFilter(andComps);
735      }
736      else
737      {
738        final Filter[] orComps = new Filter[attributes.length];
739        for (int i=0; i < attributes.length; i++)
740        {
741          orComps[i] = Filter.createPresenceFilter(attributes[i]);
742          conflictCounts.put(attributes[i], new AtomicLong(0L));
743        }
744        filter = Filter.createORFilter(orComps);
745      }
746
747      if (filterArgument.isPresent())
748      {
749        filter = Filter.createANDFilter(filterArgument.getValue(), filter);
750      }
751
752      // Iterate across all of the search base DNs and perform searches to find
753      // unique attributes.
754      for (final String baseDN : baseDNs)
755      {
756        ASN1OctetString cookie = null;
757        do
758        {
759          if (timeLimitExceeded.get())
760          {
761            break;
762          }
763
764          final SearchRequest searchRequest = new SearchRequest(this, baseDN,
765               SearchScope.SUB, filter, attributes);
766          if (pageSizeArgument.isPresent())
767          {
768            searchRequest.addControl(new SimplePagedResultsControl(
769                 pageSizeArgument.getValue(), cookie, false));
770          }
771
772          SearchResult searchResult;
773          try
774          {
775            searchResult = findUniqueAttributesPool.search(searchRequest);
776          }
777          catch (final LDAPSearchException lse)
778          {
779            Debug.debugException(lse);
780            try
781            {
782              searchResult = findConflictsPool.search(searchRequest);
783            }
784            catch (final LDAPSearchException lse2)
785            {
786              Debug.debugException(lse2);
787              searchResult = lse2.getSearchResult();
788            }
789          }
790
791          if (searchResult.getResultCode() != ResultCode.SUCCESS)
792          {
793            err("An error occurred while attempting to search for unique " +
794                 "attributes in entries below " + baseDN + ":  " +
795                 searchResult.getDiagnosticMessage());
796            return searchResult.getResultCode();
797          }
798
799          final SimplePagedResultsControl pagedResultsResponse;
800          try
801          {
802            pagedResultsResponse = SimplePagedResultsControl.get(searchResult);
803          }
804          catch (final LDAPException le)
805          {
806            Debug.debugException(le);
807            err("An error occurred while attempting to decode a simple " +
808                 "paged results response control in the response to a " +
809                 "search for entries below " + baseDN + ":  " +
810                 StaticUtils.getExceptionMessage(le));
811            return le.getResultCode();
812          }
813
814          if (pagedResultsResponse != null)
815          {
816            if (pagedResultsResponse.moreResultsToReturn())
817            {
818              cookie = pagedResultsResponse.getCookie();
819            }
820            else
821            {
822              cookie = null;
823            }
824          }
825        }
826        while (cookie != null);
827      }
828
829
830      // See if there were any uniqueness conflicts found.
831      boolean conflictFound = false;
832      if (uniqueInCombination)
833      {
834        final long count = combinationConflictCounts.get();
835        if (count > 0L)
836        {
837          conflictFound = true;
838          err("Found " + count + " total conflicts.");
839        }
840      }
841      else
842      {
843        for (final Map.Entry<String,AtomicLong> e : conflictCounts.entrySet())
844        {
845          final long numConflicts = e.getValue().get();
846          if (numConflicts > 0L)
847          {
848            if (! conflictFound)
849            {
850              err();
851              conflictFound = true;
852            }
853
854            err("Found " + numConflicts +
855                 " unique value conflicts in attribute " + e.getKey());
856          }
857        }
858      }
859
860      if (conflictFound)
861      {
862        return ResultCode.CONSTRAINT_VIOLATION;
863      }
864      else if (timeLimitExceeded.get())
865      {
866        return ResultCode.TIME_LIMIT_EXCEEDED;
867      }
868      else
869      {
870        out("No unique attribute conflicts were found.");
871        return ResultCode.SUCCESS;
872      }
873    }
874    finally
875    {
876      findUniqueAttributesPool.close();
877
878      if (findConflictsPool != null)
879      {
880        findConflictsPool.close();
881      }
882    }
883  }
884
885
886
887  /**
888   * Retrieves the number of conflicts identified across multiple attributes in
889   * combination.
890   *
891   * @return  The number of conflicts identified across multiple attributes in
892   *          combination.
893   */
894  public long getCombinationConflictCounts()
895  {
896    return combinationConflictCounts.get();
897  }
898
899
900
901  /**
902   * Retrieves a map that correlates the number of uniqueness conflicts found by
903   * attribute type.
904   *
905   * @return  A map that correlates the number of uniqueness conflicts found by
906   *          attribute type.
907   */
908  @NotNull()
909  public Map<String,AtomicLong> getConflictCounts()
910  {
911    return Collections.unmodifiableMap(conflictCounts);
912  }
913
914
915
916  /**
917   * Retrieves a set of information that may be used to generate example usage
918   * information.  Each element in the returned map should consist of a map
919   * between an example set of arguments and a string that describes the
920   * behavior of the tool when invoked with that set of arguments.
921   *
922   * @return  A set of information that may be used to generate example usage
923   *          information.  It may be {@code null} or empty if no example usage
924   *          information is available.
925   */
926  @Override()
927  @NotNull()
928  public LinkedHashMap<String[],String> getExampleUsages()
929  {
930    final LinkedHashMap<String[],String> exampleMap =
931         new LinkedHashMap<>(StaticUtils.computeMapCapacity(1));
932
933    final String[] args =
934    {
935      "--hostname", "server.example.com",
936      "--port", "389",
937      "--bindDN", "uid=john.doe,ou=People,dc=example,dc=com",
938      "--bindPassword", "password",
939      "--baseDN", "dc=example,dc=com",
940      "--attribute", "uid",
941      "--simplePageSize", "100"
942    };
943    exampleMap.put(args,
944         "Identify any values of the uid attribute that are not unique " +
945              "across all entries below dc=example,dc=com.");
946
947    return exampleMap;
948  }
949
950
951
952  /**
953   * Indicates that the provided search result entry has been returned by the
954   * server and may be processed by this search result listener.
955   *
956   * @param  searchEntry  The search result entry that has been returned by the
957   *                      server.
958   */
959  @Override()
960  public void searchEntryReturned(
961                   @NotNull final SearchResultEntry searchEntry)
962  {
963    // If we have encountered a "time limit exceeded" error, then don't even
964    // bother processing any more entries.
965    if (timeLimitExceeded.get())
966    {
967      return;
968    }
969
970    if (uniqueInCombination)
971    {
972      checkForConflictsInCombination(searchEntry);
973      return;
974    }
975
976    try
977    {
978      // If we need to check for conflicts in the same entry, then do that
979      // first.
980      if (! allowConflictsInSameEntry)
981      {
982        boolean conflictFound = false;
983        for (int i=0; i < attributes.length; i++)
984        {
985          final List<Attribute> l1 =
986               searchEntry.getAttributesWithOptions(attributes[i], null);
987          if (l1 != null)
988          {
989            for (int j=i+1; j < attributes.length; j++)
990            {
991              final List<Attribute> l2 =
992                   searchEntry.getAttributesWithOptions(attributes[j], null);
993              if (l2 != null)
994              {
995                for (final Attribute a1 : l1)
996                {
997                  for (final String value : a1.getValues())
998                  {
999                    for (final Attribute a2 : l2)
1000                    {
1001                      if (a2.hasValue(value))
1002                      {
1003                        err("Value '", value, "' in attribute ", a1.getName(),
1004                             " of entry '", searchEntry.getDN(),
1005                             " is also present in attribute ", a2.getName(),
1006                             " of the same entry.");
1007                        conflictFound = true;
1008                        conflictCounts.get(attributes[i]).incrementAndGet();
1009                      }
1010                    }
1011                  }
1012                }
1013              }
1014            }
1015          }
1016        }
1017
1018        if (conflictFound)
1019        {
1020          return;
1021        }
1022      }
1023
1024
1025      // Get the unique attributes from the entry and search for conflicts with
1026      // each value in other entries.  Although we could theoretically do this
1027      // with fewer searches, most uses of unique attributes don't have multiple
1028      // values, so the following code (which is much simpler) is just as
1029      // efficient in the common case.
1030      for (final String attrName : attributes)
1031      {
1032        final List<Attribute> attrList =
1033             searchEntry.getAttributesWithOptions(attrName, null);
1034        for (final Attribute a : attrList)
1035        {
1036          for (final String value : a.getValues())
1037          {
1038            Filter filter;
1039            if (uniqueAcrossAttributes)
1040            {
1041              final Filter[] orComps = new Filter[attributes.length];
1042              for (int i=0; i < attributes.length; i++)
1043              {
1044                orComps[i] = Filter.createEqualityFilter(attributes[i], value);
1045              }
1046              filter = Filter.createORFilter(orComps);
1047            }
1048            else
1049            {
1050              filter = Filter.createEqualityFilter(attrName, value);
1051            }
1052
1053            if (filterArgument.isPresent())
1054            {
1055              filter = Filter.createANDFilter(filterArgument.getValue(),
1056                   filter);
1057            }
1058
1059baseDNLoop:
1060            for (final String baseDN : baseDNs)
1061            {
1062              SearchResult searchResult;
1063              final SearchRequest searchRequest = new SearchRequest(baseDN,
1064                   SearchScope.SUB, DereferencePolicy.NEVER, 2,
1065                   timeLimitArgument.getValue(), false, filter, "1.1");
1066              try
1067              {
1068                searchResult = findConflictsPool.search(searchRequest);
1069              }
1070              catch (final LDAPSearchException lse)
1071              {
1072                Debug.debugException(lse);
1073                if (lse.getResultCode() == ResultCode.TIME_LIMIT_EXCEEDED)
1074                {
1075                  // The server spent more time than the configured time limit
1076                  // to process the search.  This almost certainly means that
1077                  // the search is unindexed, and we don't want to continue.
1078                  // Indicate that the time limit has been exceeded, cancel the
1079                  // outer search, and display an error message to the user.
1080                  timeLimitExceeded.set(true);
1081                  try
1082                  {
1083                    findConflictsPool.processExtendedOperation(
1084                         new CancelExtendedRequest(searchEntry.getMessageID()));
1085                  }
1086                  catch (final Exception e)
1087                  {
1088                    Debug.debugException(e);
1089                  }
1090
1091                  err("A server-side time limit was exceeded when searching " +
1092                       "below base DN '" + baseDN + "' with filter '" +
1093                       filter + "', which likely means that the search " +
1094                       "request is not indexed in the server.  Check the " +
1095                       "server configuration to ensure that any appropriate " +
1096                       "indexes are in place.  To indicate that searches " +
1097                       "should not request any time limit, use the " +
1098                       timeLimitArgument.getIdentifierString() +
1099                       " to indicate a time limit of zero seconds.");
1100                  return;
1101                }
1102                else if (lse.getResultCode().isConnectionUsable())
1103                {
1104                  searchResult = lse.getSearchResult();
1105                }
1106                else
1107                {
1108                  try
1109                  {
1110                    searchResult = findConflictsPool.search(searchRequest);
1111                  }
1112                  catch (final LDAPSearchException lse2)
1113                  {
1114                    Debug.debugException(lse2);
1115                    searchResult = lse2.getSearchResult();
1116                  }
1117                }
1118              }
1119
1120              for (final SearchResultEntry e : searchResult.getSearchEntries())
1121              {
1122                try
1123                {
1124                  if (DN.equals(searchEntry.getDN(), e.getDN()))
1125                  {
1126                    continue;
1127                  }
1128                }
1129                catch (final Exception ex)
1130                {
1131                  Debug.debugException(ex);
1132                }
1133
1134                err("Value '", value, "' in attribute ", a.getName(),
1135                     " of entry '" + searchEntry.getDN(),
1136                     "' is also present in entry '", e.getDN(), "'.");
1137                conflictCounts.get(attrName).incrementAndGet();
1138                break baseDNLoop;
1139              }
1140
1141              if (searchResult.getResultCode() != ResultCode.SUCCESS)
1142              {
1143                err("An error occurred while attempting to search for " +
1144                     "conflicts with " + a.getName() + " value '" + value +
1145                     "' (as found in entry '" + searchEntry.getDN() +
1146                     "') below '" + baseDN + "':  " +
1147                     searchResult.getDiagnosticMessage());
1148                conflictCounts.get(attrName).incrementAndGet();
1149                break baseDNLoop;
1150              }
1151            }
1152          }
1153        }
1154      }
1155    }
1156    finally
1157    {
1158      final long count = entriesExamined.incrementAndGet();
1159      if ((count % 1000L) == 0L)
1160      {
1161        out(count, " entries examined");
1162      }
1163    }
1164  }
1165
1166
1167
1168  /**
1169   * Performs the processing necessary to check for conflicts between a
1170   * combination of attribute values obtained from the provided entry.
1171   *
1172   * @param  entry  The entry to examine.
1173   */
1174  private void checkForConflictsInCombination(
1175                    @NotNull final SearchResultEntry entry)
1176  {
1177    // Construct a filter used to identify conflicting entries as an AND for
1178    // each attribute.  Handle the possibility of multivalued attributes by
1179    // creating an OR of all values for each attribute.  And if an additional
1180    // filter was also specified, include it in the AND as well.
1181    final ArrayList<Filter> andComponents =
1182         new ArrayList<>(attributes.length + 1);
1183    for (final String attrName : attributes)
1184    {
1185      final LinkedHashSet<Filter> values =
1186           new LinkedHashSet<>(StaticUtils.computeMapCapacity(5));
1187      for (final Attribute a : entry.getAttributesWithOptions(attrName, null))
1188      {
1189        for (final byte[] value : a.getValueByteArrays())
1190        {
1191          final Filter equalityFilter =
1192               Filter.createEqualityFilter(attrName, value);
1193          values.add(Filter.createEqualityFilter(attrName, value));
1194        }
1195      }
1196
1197      switch (values.size())
1198      {
1199        case 0:
1200          // This means that the returned entry didn't include any values for
1201          // the target attribute.  This should only happen if the user doesn't
1202          // have permission to see those values.  At any rate, we can't check
1203          // this entry for conflicts, so just assume there aren't any.
1204          return;
1205
1206        case 1:
1207          andComponents.add(values.iterator().next());
1208          break;
1209
1210        default:
1211          andComponents.add(Filter.createORFilter(values));
1212          break;
1213      }
1214    }
1215
1216    if (filterArgument.isPresent())
1217    {
1218      andComponents.add(filterArgument.getValue());
1219    }
1220
1221    final Filter filter = Filter.createANDFilter(andComponents);
1222
1223
1224    // Search below each of the configured base DNs.
1225baseDNLoop:
1226    for (final DN baseDN : baseDNArgument.getValues())
1227    {
1228      SearchResult searchResult;
1229      final SearchRequest searchRequest = new SearchRequest(baseDN.toString(),
1230           SearchScope.SUB, DereferencePolicy.NEVER, 2,
1231           timeLimitArgument.getValue(), false, filter, "1.1");
1232
1233      try
1234      {
1235        searchResult = findConflictsPool.search(searchRequest);
1236      }
1237      catch (final LDAPSearchException lse)
1238      {
1239        Debug.debugException(lse);
1240        if (lse.getResultCode() == ResultCode.TIME_LIMIT_EXCEEDED)
1241        {
1242          // The server spent more time than the configured time limit to
1243          // process the search.  This almost certainly means that the search is
1244          // unindexed, and we don't want to continue. Indicate that the time
1245          // limit has been exceeded, cancel the outer search, and display an
1246          // error message to the user.
1247          timeLimitExceeded.set(true);
1248          try
1249          {
1250            findConflictsPool.processExtendedOperation(
1251                 new CancelExtendedRequest(entry.getMessageID()));
1252          }
1253          catch (final Exception e)
1254          {
1255            Debug.debugException(e);
1256          }
1257
1258          err("A server-side time limit was exceeded when searching below " +
1259               "base DN '" + baseDN + "' with filter '" + filter +
1260               "', which likely means that the search request is not indexed " +
1261               "in the server.  Check the server configuration to ensure " +
1262               "that any appropriate indexes are in place.  To indicate that " +
1263               "searches should not request any time limit, use the " +
1264               timeLimitArgument.getIdentifierString() +
1265               " to indicate a time limit of zero seconds.");
1266          return;
1267        }
1268        else if (lse.getResultCode().isConnectionUsable())
1269        {
1270          searchResult = lse.getSearchResult();
1271        }
1272        else
1273        {
1274          try
1275          {
1276            searchResult = findConflictsPool.search(searchRequest);
1277          }
1278          catch (final LDAPSearchException lse2)
1279          {
1280            Debug.debugException(lse2);
1281            searchResult = lse2.getSearchResult();
1282          }
1283        }
1284      }
1285
1286      for (final SearchResultEntry e : searchResult.getSearchEntries())
1287      {
1288        try
1289        {
1290          if (DN.equals(entry.getDN(), e.getDN()))
1291          {
1292            continue;
1293          }
1294        }
1295        catch (final Exception ex)
1296        {
1297          Debug.debugException(ex);
1298        }
1299
1300        err("Entry '" + entry.getDN() + " has a combination of values that " +
1301             "are also present in entry '" + e.getDN() + "'.");
1302        combinationConflictCounts.incrementAndGet();
1303        break baseDNLoop;
1304      }
1305
1306      if (searchResult.getResultCode() != ResultCode.SUCCESS)
1307      {
1308        err("An error occurred while attempting to search for conflicts " +
1309             " with entry '" + entry.getDN() + "' below '" + baseDN + "':  " +
1310             searchResult.getDiagnosticMessage());
1311        combinationConflictCounts.incrementAndGet();
1312        break baseDNLoop;
1313      }
1314    }
1315  }
1316
1317
1318
1319  /**
1320   * Indicates that the provided search result reference has been returned by
1321   * the server and may be processed by this search result listener.
1322   *
1323   * @param  searchReference  The search result reference that has been returned
1324   *                          by the server.
1325   */
1326  @Override()
1327  public void searchReferenceReturned(
1328                   @NotNull final SearchResultReference searchReference)
1329  {
1330    // No implementation is required.  This tool will not follow referrals.
1331  }
1332}