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