001/*
002 * Copyright 2013-2016 UnboundID Corp.
003 * All Rights Reserved.
004 */
005/*
006 * Copyright (C) 2013-2016 UnboundID Corp.
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.Collections;
027import java.util.LinkedHashMap;
028import java.util.LinkedHashSet;
029import java.util.List;
030import java.util.Map;
031import java.util.TreeMap;
032import java.util.concurrent.atomic.AtomicLong;
033
034import com.unboundid.asn1.ASN1OctetString;
035import com.unboundid.ldap.sdk.Attribute;
036import com.unboundid.ldap.sdk.DereferencePolicy;
037import com.unboundid.ldap.sdk.DN;
038import com.unboundid.ldap.sdk.Filter;
039import com.unboundid.ldap.sdk.LDAPConnectionOptions;
040import com.unboundid.ldap.sdk.LDAPConnectionPool;
041import com.unboundid.ldap.sdk.LDAPException;
042import com.unboundid.ldap.sdk.LDAPSearchException;
043import com.unboundid.ldap.sdk.ResultCode;
044import com.unboundid.ldap.sdk.SearchRequest;
045import com.unboundid.ldap.sdk.SearchResult;
046import com.unboundid.ldap.sdk.SearchResultEntry;
047import com.unboundid.ldap.sdk.SearchResultReference;
048import com.unboundid.ldap.sdk.SearchResultListener;
049import com.unboundid.ldap.sdk.SearchScope;
050import com.unboundid.ldap.sdk.Version;
051import com.unboundid.ldap.sdk.controls.SimplePagedResultsControl;
052import com.unboundid.util.Debug;
053import com.unboundid.util.LDAPCommandLineTool;
054import com.unboundid.util.StaticUtils;
055import com.unboundid.util.ThreadSafety;
056import com.unboundid.util.ThreadSafetyLevel;
057import com.unboundid.util.args.ArgumentException;
058import com.unboundid.util.args.ArgumentParser;
059import com.unboundid.util.args.DNArgument;
060import com.unboundid.util.args.FilterArgument;
061import com.unboundid.util.args.IntegerArgument;
062import com.unboundid.util.args.StringArgument;
063
064
065
066/**
067 * This class provides a tool that may be used to identify unique attribute
068 * conflicts (i.e., attributes which are supposed to be unique but for which
069 * some values exist in multiple entries).
070 * <BR><BR>
071 * All of the necessary information is provided using command line arguments.
072 * Supported arguments include those allowed by the {@link LDAPCommandLineTool}
073 * class, as well as the following additional arguments:
074 * <UL>
075 *   <LI>"-b {baseDN}" or "--baseDN {baseDN}" -- specifies the base DN to use
076 *       for the searches.  At least one base DN must be provided.</LI>
077 *   <LI>"-f" {filter}" or "--filter "{filter}" -- specifies an optional
078 *       filter to use for identifying entries across which uniqueness should be
079 *       enforced.  If this is not provided, then all entries containing the
080 *       target attribute(s) will be examined.</LI>
081 *   <LI>"-A {attribute}" or "--attribute {attribute}" -- specifies an attribute
082 *       for which to enforce uniqueness.  At least one unique attribute must be
083 *       provided.</LI>
084 *   <LI>"-m {behavior}" or "--multipleAttributeBehavior {behavior}" --
085 *       specifies the behavior that the tool should exhibit if multiple
086 *       unique attributes are provided.  Allowed values include
087 *       unique-within-each-attribute,
088 *       unique-across-all-attributes-including-in-same-entry, and
089 *       unique-across-all-attributes-except-in-same-entry.</LI>
090 *   <LI>"-z {size}" or "--simplePageSize {size}" -- indicates that the search
091 *       to find entries with unique attributes should use the simple paged
092 *       results control to iterate across entries in fixed-size pages rather
093 *       than trying to use a single search to identify all entries containing
094 *       unique attributes.</LI>
095 * </UL>
096 */
097@ThreadSafety(level=ThreadSafetyLevel.NOT_THREADSAFE)
098public final class IdentifyUniqueAttributeConflicts
099       extends LDAPCommandLineTool
100       implements SearchResultListener
101{
102  /**
103   * The unique attribute behavior value that indicates uniqueness should only
104   * be ensured within each attribute.
105   */
106  private static final String BEHAVIOR_UNIQUE_WITHIN_ATTR =
107       "unique-within-each-attribute";
108
109
110
111  /**
112   * The unique attribute behavior value that indicates uniqueness should be
113   * ensured across all attributes, and conflicts will not be allowed across
114   * attributes in the same entry.
115   */
116  private static final String BEHAVIOR_UNIQUE_ACROSS_ATTRS_INCLUDING_SAME =
117       "unique-across-all-attributes-including-in-same-entry";
118
119
120
121  /**
122   * The unique attribute behavior value that indicates uniqueness should be
123   * ensured across all attributes, except that conflicts will not be allowed
124   * across attributes in the same entry.
125   */
126  private static final String BEHAVIOR_UNIQUE_ACROSS_ATTRS_EXCEPT_SAME =
127       "unique-across-all-attributes-except-in-same-entry";
128
129
130
131  /**
132   * The serial version UID for this serializable class.
133   */
134  private static final long serialVersionUID = -7506817625818259323L;
135
136
137
138  // The number of entries examined so far.
139  private final AtomicLong entriesExamined;
140
141  // Indicates whether cross-attribute uniqueness conflicts should be allowed
142  // in the same entry.
143  private boolean allowConflictsInSameEntry;
144
145  // Indicates whether uniqueness should be enforced across all attributes
146  // rather than within each attribute.
147  private boolean uniqueAcrossAttributes;
148
149  // The argument used to specify the base DNs to use for searches.
150  private DNArgument baseDNArgument;
151
152  // The argument used to specify a filter indicating which entries to examine.
153  private FilterArgument filterArgument;
154
155  // The argument used to specify the search page size.
156  private IntegerArgument pageSizeArgument;
157
158  // The connection to use for finding unique attribute conflicts.
159  private LDAPConnectionPool findConflictsPool;
160
161  // A map with counts of unique attribute conflicts by attribute type.
162  private final Map<String, AtomicLong> conflictCounts;
163
164  // The names of the attributes for which to find uniqueness conflicts.
165  private String[] attributes;
166
167  // The set of base DNs to use for the searches.
168  private String[] baseDNs;
169
170  // The argument used to specify the attributes for which to find uniqueness
171  // conflicts.
172  private StringArgument attributeArgument;
173
174  // The argument used to specify the behavior that should be exhibited if
175  // multiple attributes are specified.
176  private StringArgument multipleAttributeBehaviorArgument;
177
178
179
180  /**
181   * Parse the provided command line arguments and perform the appropriate
182   * processing.
183   *
184   * @param  args  The command line arguments provided to this program.
185   */
186  public static void main(final String... args)
187  {
188    final ResultCode resultCode = main(args, System.out, System.err);
189    if (resultCode != ResultCode.SUCCESS)
190    {
191      System.exit(resultCode.intValue());
192    }
193  }
194
195
196
197  /**
198   * Parse the provided command line arguments and perform the appropriate
199   * processing.
200   *
201   * @param  args       The command line arguments provided to this program.
202   * @param  outStream  The output stream to which standard out should be
203   *                    written.  It may be {@code null} if output should be
204   *                    suppressed.
205   * @param  errStream  The output stream to which standard error should be
206   *                    written.  It may be {@code null} if error messages
207   *                    should be suppressed.
208   *
209   * @return A result code indicating whether the processing was successful.
210   */
211  public static ResultCode main(final String[] args,
212                                final OutputStream outStream,
213                                final OutputStream errStream)
214  {
215    final IdentifyUniqueAttributeConflicts tool =
216         new IdentifyUniqueAttributeConflicts(outStream, errStream);
217    return tool.runTool(args);
218  }
219
220
221
222  /**
223   * Creates a new instance of this tool.
224   *
225   * @param  outStream  The output stream to which standard out should be
226   *                    written.  It may be {@code null} if output should be
227   *                    suppressed.
228   * @param  errStream  The output stream to which standard error should be
229   *                    written.  It may be {@code null} if error messages
230   *                    should be suppressed.
231   */
232  public IdentifyUniqueAttributeConflicts(final OutputStream outStream,
233                                          final OutputStream errStream)
234  {
235    super(outStream, errStream);
236
237    baseDNArgument = null;
238    filterArgument = null;
239    pageSizeArgument = null;
240    attributeArgument = null;
241    multipleAttributeBehaviorArgument = null;
242    findConflictsPool = null;
243    allowConflictsInSameEntry = false;
244    uniqueAcrossAttributes = false;
245    attributes = null;
246    baseDNs = null;
247
248    entriesExamined = new AtomicLong(0L);
249    conflictCounts = new TreeMap<String, AtomicLong>();
250  }
251
252
253
254  /**
255   * Retrieves the name of this tool.  It should be the name of the command used
256   * to invoke this tool.
257   *
258   * @return The name for this tool.
259   */
260  @Override()
261  public String getToolName()
262  {
263    return "identify-unique-attribute-conflicts";
264  }
265
266
267
268  /**
269   * Retrieves a human-readable description for this tool.
270   *
271   * @return A human-readable description for this tool.
272   */
273  @Override()
274  public String getToolDescription()
275  {
276    return "This tool may be used to identify unique attribute conflicts.  " +
277         "That is, it may identify values of one or more attributes which " +
278         "are supposed to exist only in a single entry but are found in " +
279         "multiple entries.";
280  }
281
282
283
284  /**
285   * Retrieves a version string for this tool, if available.
286   *
287   * @return A version string for this tool, or {@code null} if none is
288   *          available.
289   */
290  @Override()
291  public String getToolVersion()
292  {
293    return Version.NUMERIC_VERSION_STRING;
294  }
295
296
297
298  /**
299   * Indicates whether this tool should provide support for an interactive mode,
300   * in which the tool offers a mode in which the arguments can be provided in
301   * a text-driven menu rather than requiring them to be given on the command
302   * line.  If interactive mode is supported, it may be invoked using the
303   * "--interactive" argument.  Alternately, if interactive mode is supported
304   * and {@link #defaultsToInteractiveMode()} returns {@code true}, then
305   * interactive mode may be invoked by simply launching the tool without any
306   * arguments.
307   *
308   * @return  {@code true} if this tool supports interactive mode, or
309   *          {@code false} if not.
310   */
311  @Override()
312  public boolean supportsInteractiveMode()
313  {
314    return true;
315  }
316
317
318
319  /**
320   * Indicates whether this tool defaults to launching in interactive mode if
321   * the tool is invoked without any command-line arguments.  This will only be
322   * used if {@link #supportsInteractiveMode()} returns {@code true}.
323   *
324   * @return  {@code true} if this tool defaults to using interactive mode if
325   *          launched without any command-line arguments, or {@code false} if
326   *          not.
327   */
328  @Override()
329  public boolean defaultsToInteractiveMode()
330  {
331    return true;
332  }
333
334
335
336  /**
337   * Indicates whether this tool should provide arguments for redirecting output
338   * to a file.  If this method returns {@code true}, then the tool will offer
339   * an "--outputFile" argument that will specify the path to a file to which
340   * all standard output and standard error content will be written, and it will
341   * also offer a "--teeToStandardOut" argument that can only be used if the
342   * "--outputFile" argument is present and will cause all output to be written
343   * to both the specified output file and to standard output.
344   *
345   * @return  {@code true} if this tool should provide arguments for redirecting
346   *          output to a file, or {@code false} if not.
347   */
348  @Override()
349  protected boolean supportsOutputFile()
350  {
351    return true;
352  }
353
354
355
356  /**
357   * Indicates whether this tool should default to interactively prompting for
358   * the bind password if a password is required but no argument was provided
359   * to indicate how to get the password.
360   *
361   * @return  {@code true} if this tool should default to interactively
362   *          prompting for the bind password, or {@code false} if not.
363   */
364  @Override()
365  protected boolean defaultToPromptForBindPassword()
366  {
367    return true;
368  }
369
370
371
372  /**
373   * Indicates whether this tool supports the use of a properties file for
374   * specifying default values for arguments that aren't specified on the
375   * command line.
376   *
377   * @return  {@code true} if this tool supports the use of a properties file
378   *          for specifying default values for arguments that aren't specified
379   *          on the command line, or {@code false} if not.
380   */
381  @Override()
382  public boolean supportsPropertiesFile()
383  {
384    return true;
385  }
386
387
388
389  /**
390   * Indicates whether the LDAP-specific arguments should include alternate
391   * versions of all long identifiers that consist of multiple words so that
392   * they are available in both camelCase and dash-separated versions.
393   *
394   * @return  {@code true} if this tool should provide multiple versions of
395   *          long identifiers for LDAP-specific arguments, or {@code false} if
396   *          not.
397   */
398  @Override()
399  protected boolean includeAlternateLongIdentifiers()
400  {
401    return true;
402  }
403
404
405
406  /**
407   * Adds the arguments needed by this command-line tool to the provided
408   * argument parser which are not related to connecting or authenticating to
409   * the directory server.
410   *
411   * @param  parser  The argument parser to which the arguments should be added.
412   *
413   * @throws ArgumentException  If a problem occurs while adding the arguments.
414   */
415  @Override()
416  public void addNonLDAPArguments(final ArgumentParser parser)
417       throws ArgumentException
418  {
419    String description = "The search base DN(s) to use to find entries with " +
420         "attributes for which to find uniqueness conflicts.  At least one " +
421         "base DN must be specified.";
422    baseDNArgument = new DNArgument('b', "baseDN", true, 0, "{dn}",
423         description);
424    baseDNArgument.addLongIdentifier("base-dn");
425    parser.addArgument(baseDNArgument);
426
427    description = "A filter that will be used to identify the set of " +
428         "entries in which to identify uniqueness conflicts.  If this is not " +
429         "specified, then all entries containing the target attribute(s) " +
430         "will be examined.";
431    filterArgument = new FilterArgument('f', "filter", false, 1, "{filter}",
432         description);
433    parser.addArgument(filterArgument);
434
435    description = "The attributes for which to find uniqueness conflicts.  " +
436         "At least one attribute must be specified, and each attribute " +
437         "must be indexed for equality searches.";
438    attributeArgument = new StringArgument('A', "attribute", true, 0, "{attr}",
439         description);
440    parser.addArgument(attributeArgument);
441
442    description = "Indicates the behavior to exhibit if multiple unique " +
443         "attributes are provided.  Allowed values are '" +
444         BEHAVIOR_UNIQUE_WITHIN_ATTR + "' (indicates that each value only " +
445         "needs to be unique within its own attribute type), '" +
446         BEHAVIOR_UNIQUE_ACROSS_ATTRS_INCLUDING_SAME + "' (indicates that " +
447         "each value needs to be unique across all of the specified " +
448         "attributes), and '" + BEHAVIOR_UNIQUE_ACROSS_ATTRS_EXCEPT_SAME +
449         "' (indicates each value needs to be unique across all of the " +
450         "specified attributes, except that multiple attributes in the same " +
451         "entry are allowed to share the same value).";
452    final LinkedHashSet<String> allowedValues = new LinkedHashSet<String>(3);
453    allowedValues.add(BEHAVIOR_UNIQUE_WITHIN_ATTR);
454    allowedValues.add(BEHAVIOR_UNIQUE_ACROSS_ATTRS_INCLUDING_SAME);
455    allowedValues.add(BEHAVIOR_UNIQUE_ACROSS_ATTRS_EXCEPT_SAME);
456    multipleAttributeBehaviorArgument = new StringArgument('m',
457         "multipleAttributeBehavior", false, 1, "{behavior}", description,
458         allowedValues, BEHAVIOR_UNIQUE_WITHIN_ATTR);
459    multipleAttributeBehaviorArgument.addLongIdentifier(
460         "multiple-attribute-behavior");
461    parser.addArgument(multipleAttributeBehaviorArgument);
462
463    description = "The maximum number of entries to retrieve at a time when " +
464         "attempting to find uniqueness conflicts.  This requires that the " +
465         "authenticated user have permission to use the simple paged results " +
466         "control, but it can avoid problems with the server sending entries " +
467         "too quickly for the client to handle.  By default, the simple " +
468         "paged results control will not be used.";
469    pageSizeArgument =
470         new IntegerArgument('z', "simplePageSize", false, 1, "{num}",
471              description, 1, Integer.MAX_VALUE);
472    pageSizeArgument.addLongIdentifier("simple-page-size");
473    parser.addArgument(pageSizeArgument);
474  }
475
476
477
478  /**
479   * Retrieves the connection options that should be used for connections that
480   * are created with this command line tool.  Subclasses may override this
481   * method to use a custom set of connection options.
482   *
483   * @return  The connection options that should be used for connections that
484   *          are created with this command line tool.
485   */
486  @Override()
487  public LDAPConnectionOptions getConnectionOptions()
488  {
489    final LDAPConnectionOptions options = new LDAPConnectionOptions();
490
491    options.setUseSynchronousMode(true);
492    options.setResponseTimeoutMillis(0L);
493
494    return options;
495  }
496
497
498
499  /**
500   * Performs the core set of processing for this tool.
501   *
502   * @return  A result code that indicates whether the processing completed
503   *          successfully.
504   */
505  @Override()
506  public ResultCode doToolProcessing()
507  {
508    // Determine the multi-attribute behavior that we should exhibit.
509    final List<String> attrList = attributeArgument.getValues();
510    final String multiAttrBehavior =
511         multipleAttributeBehaviorArgument.getValue();
512    if (attrList.size() > 1)
513    {
514      if (multiAttrBehavior.equalsIgnoreCase(
515           BEHAVIOR_UNIQUE_ACROSS_ATTRS_INCLUDING_SAME))
516      {
517        uniqueAcrossAttributes = true;
518        allowConflictsInSameEntry = false;
519      }
520      else if (multiAttrBehavior.equalsIgnoreCase(
521           BEHAVIOR_UNIQUE_ACROSS_ATTRS_EXCEPT_SAME))
522      {
523        uniqueAcrossAttributes = true;
524        allowConflictsInSameEntry = true;
525      }
526      else
527      {
528        uniqueAcrossAttributes = false;
529        allowConflictsInSameEntry = true;
530      }
531    }
532    else
533    {
534      uniqueAcrossAttributes = false;
535      allowConflictsInSameEntry = true;
536    }
537
538
539    // Get the string representations of the base DNs.
540    final List<DN> dnList = baseDNArgument.getValues();
541    baseDNs = new String[dnList.size()];
542    for (int i=0; i < baseDNs.length; i++)
543    {
544      baseDNs[i] = dnList.get(i).toString();
545    }
546
547    // Establish a connection to the target directory server to use for finding
548    // entries with unique attributes.
549    final LDAPConnectionPool findUniqueAttributesPool;
550    try
551    {
552      findUniqueAttributesPool = getConnectionPool(1, 1);
553      findUniqueAttributesPool.
554           setRetryFailedOperationsDueToInvalidConnections(true);
555    }
556    catch (final LDAPException le)
557    {
558      Debug.debugException(le);
559      err("Unable to establish a connection to the directory server:  ",
560           StaticUtils.getExceptionMessage(le));
561      return le.getResultCode();
562    }
563
564    try
565    {
566      // Establish a connection to use for finding unique attribute conflicts.
567      try
568      {
569        findConflictsPool= getConnectionPool(1, 1);
570        findConflictsPool.setRetryFailedOperationsDueToInvalidConnections(true);
571      }
572      catch (final LDAPException le)
573      {
574        Debug.debugException(le);
575        err("Unable to establish a connection to the directory server:  ",
576             StaticUtils.getExceptionMessage(le));
577        return le.getResultCode();
578      }
579
580      // Get the set of attributes for which to ensure uniqueness.
581      attributes = new String[attrList.size()];
582      attrList.toArray(attributes);
583
584
585      // Construct a search filter that will be used to find all entries with
586      // unique attributes.
587      Filter filter;
588      if (attributes.length == 1)
589      {
590        filter = Filter.createPresenceFilter(attributes[0]);
591        conflictCounts.put(attributes[0], new AtomicLong(0L));
592      }
593      else
594      {
595        final Filter[] orComps = new Filter[attributes.length];
596        for (int i=0; i < attributes.length; i++)
597        {
598          orComps[i] = Filter.createPresenceFilter(attributes[i]);
599          conflictCounts.put(attributes[i], new AtomicLong(0L));
600        }
601        filter = Filter.createORFilter(orComps);
602      }
603
604      if (filterArgument.isPresent())
605      {
606        filter = Filter.createANDFilter(filterArgument.getValue(), filter);
607      }
608
609
610      // Iterate across all of the search base DNs and perform searches to find
611      // unique attributes.
612      for (final String baseDN : baseDNs)
613      {
614        ASN1OctetString cookie = null;
615        do
616        {
617          final SearchRequest searchRequest = new SearchRequest(this, baseDN,
618               SearchScope.SUB, filter, attributes);
619          if (pageSizeArgument.isPresent())
620          {
621            searchRequest.addControl(new SimplePagedResultsControl(
622                 pageSizeArgument.getValue(), cookie, false));
623          }
624
625          SearchResult searchResult;
626          try
627          {
628            searchResult = findUniqueAttributesPool.search(searchRequest);
629          }
630          catch (final LDAPSearchException lse)
631          {
632            Debug.debugException(lse);
633            try
634            {
635              searchResult = findConflictsPool.search(searchRequest);
636            }
637            catch (final LDAPSearchException lse2)
638            {
639              Debug.debugException(lse2);
640              searchResult = lse2.getSearchResult();
641            }
642          }
643
644          if (searchResult.getResultCode() != ResultCode.SUCCESS)
645          {
646            err("An error occurred while attempting to search for unique " +
647                 "attributes in entries below " + baseDN + ":  " +
648                 searchResult.getDiagnosticMessage());
649            return searchResult.getResultCode();
650          }
651
652          final SimplePagedResultsControl pagedResultsResponse;
653          try
654          {
655            pagedResultsResponse = SimplePagedResultsControl.get(searchResult);
656          }
657          catch (final LDAPException le)
658          {
659            Debug.debugException(le);
660            err("An error occurred while attempting to decode a simple " +
661                 "paged results response control in the response to a " +
662                 "search for entries below " + baseDN + ":  " +
663                 StaticUtils.getExceptionMessage(le));
664            return le.getResultCode();
665          }
666
667          if (pagedResultsResponse != null)
668          {
669            if (pagedResultsResponse.moreResultsToReturn())
670            {
671              cookie = pagedResultsResponse.getCookie();
672            }
673            else
674            {
675              cookie = null;
676            }
677          }
678        }
679        while (cookie != null);
680      }
681
682
683      // See if there were any uniqueness conflicts found.
684      boolean conflictFound = false;
685      for (final Map.Entry<String,AtomicLong> e : conflictCounts.entrySet())
686      {
687        final long numConflicts = e.getValue().get();
688        if (numConflicts > 0L)
689        {
690          if (! conflictFound)
691          {
692            err();
693            conflictFound = true;
694          }
695
696          err("Found " + numConflicts +
697               " unique value conflicts in attribute " + e.getKey());
698        }
699      }
700
701      if (conflictFound)
702      {
703        return ResultCode.CONSTRAINT_VIOLATION;
704      }
705      else
706      {
707        out("No unique attribute conflicts were found.");
708        return ResultCode.SUCCESS;
709      }
710    }
711    finally
712    {
713      findUniqueAttributesPool.close();
714
715      if (findConflictsPool != null)
716      {
717        findConflictsPool.close();
718      }
719    }
720  }
721
722
723
724  /**
725   * Retrieves a map that correlates the number of uniqueness conflicts found by
726   * attribute type.
727   *
728   * @return  A map that correlates the number of uniqueness conflicts found by
729   *          attribute type.
730   */
731  public Map<String,AtomicLong> getConflictCounts()
732  {
733    return Collections.unmodifiableMap(conflictCounts);
734  }
735
736
737
738  /**
739   * Retrieves a set of information that may be used to generate example usage
740   * information.  Each element in the returned map should consist of a map
741   * between an example set of arguments and a string that describes the
742   * behavior of the tool when invoked with that set of arguments.
743   *
744   * @return  A set of information that may be used to generate example usage
745   *          information.  It may be {@code null} or empty if no example usage
746   *          information is available.
747   */
748  @Override()
749  public LinkedHashMap<String[],String> getExampleUsages()
750  {
751    final LinkedHashMap<String[],String> exampleMap =
752         new LinkedHashMap<String[],String>(1);
753
754    final String[] args =
755    {
756      "--hostname", "server.example.com",
757      "--port", "389",
758      "--bindDN", "uid=john.doe,ou=People,dc=example,dc=com",
759      "--bindPassword", "password",
760      "--baseDN", "dc=example,dc=com",
761      "--attribute", "uid",
762      "--simplePageSize", "100"
763    };
764    exampleMap.put(args,
765         "Identify any values of the uid attribute that are not unique " +
766              "across all entries below dc=example,dc=com.");
767
768    return exampleMap;
769  }
770
771
772
773  /**
774   * Indicates that the provided search result entry has been returned by the
775   * server and may be processed by this search result listener.
776   *
777   * @param  searchEntry  The search result entry that has been returned by the
778   *                      server.
779   */
780  public void searchEntryReturned(final SearchResultEntry searchEntry)
781  {
782    try
783    {
784      // If we need to check for conflicts in the same entry, then do that
785      // first.
786      if (! allowConflictsInSameEntry)
787      {
788        boolean conflictFound = false;
789        for (int i=0; i < attributes.length; i++)
790        {
791          final List<Attribute> l1 =
792               searchEntry.getAttributesWithOptions(attributes[i], null);
793          if (l1 != null)
794          {
795            for (int j=i+1; j < attributes.length; j++)
796            {
797              final List<Attribute> l2 =
798                   searchEntry.getAttributesWithOptions(attributes[j], null);
799              if (l2 != null)
800              {
801                for (final Attribute a1 : l1)
802                {
803                  for (final String value : a1.getValues())
804                  {
805                    for (final Attribute a2 : l2)
806                    {
807                      if (a2.hasValue(value))
808                      {
809                        err("Value '", value, "' in attribute ", a1.getName(),
810                             " of entry '", searchEntry.getDN(),
811                             " is also present in attribute ", a2.getName(),
812                             " of the same entry.");
813                        conflictFound = true;
814                        conflictCounts.get(attributes[i]).incrementAndGet();
815                      }
816                    }
817                  }
818                }
819              }
820            }
821          }
822        }
823
824        if (conflictFound)
825        {
826          return;
827        }
828      }
829
830
831      // Get the unique attributes from the entry and search for conflicts with
832      // each value in other entries.  Although we could theoretically do this
833      // with fewer searches, most uses of unique attributes don't have multiple
834      // values, so the following code (which is much simpler) is just as
835      // efficient in the common case.
836      for (final String attrName : attributes)
837      {
838        final List<Attribute> attrList =
839             searchEntry.getAttributesWithOptions(attrName, null);
840        for (final Attribute a : attrList)
841        {
842          for (final String value : a.getValues())
843          {
844            Filter filter;
845            if (uniqueAcrossAttributes)
846            {
847              final Filter[] orComps = new Filter[attributes.length];
848              for (int i=0; i < attributes.length; i++)
849              {
850                orComps[i] = Filter.createEqualityFilter(attributes[i], value);
851              }
852              filter = Filter.createORFilter(orComps);
853            }
854            else
855            {
856              filter = Filter.createEqualityFilter(attrName, value);
857            }
858
859            if (filterArgument.isPresent())
860            {
861              filter = Filter.createANDFilter(filterArgument.getValue(),
862                   filter);
863            }
864
865baseDNLoop:
866            for (final String baseDN : baseDNs)
867            {
868              SearchResult searchResult;
869              final SearchRequest searchRequest = new SearchRequest(baseDN,
870                   SearchScope.SUB, DereferencePolicy.NEVER, 2, 0, false,
871                   filter, "1.1");
872              try
873              {
874                searchResult = findConflictsPool.search(searchRequest);
875              }
876              catch (final LDAPSearchException lse)
877              {
878                Debug.debugException(lse);
879                if (lse.getResultCode().isConnectionUsable())
880                {
881                  searchResult = lse.getSearchResult();
882                }
883                else
884                {
885                  try
886                  {
887                    searchResult = findConflictsPool.search(searchRequest);
888                  }
889                  catch (final LDAPSearchException lse2)
890                  {
891                    Debug.debugException(lse2);
892                    searchResult = lse2.getSearchResult();
893                  }
894                }
895              }
896
897              for (final SearchResultEntry e : searchResult.getSearchEntries())
898              {
899                try
900                {
901                  if (DN.equals(searchEntry.getDN(), e.getDN()))
902                  {
903                    continue;
904                  }
905                }
906                catch (final Exception ex)
907                {
908                  Debug.debugException(ex);
909                }
910
911                err("Value '", value, "' in attribute ", a.getName(),
912                     " of entry '" + searchEntry.getDN(),
913                     "' is also present in entry '", e.getDN(), "'.");
914                conflictCounts.get(attrName).incrementAndGet();
915                break baseDNLoop;
916              }
917
918              if (searchResult.getResultCode() != ResultCode.SUCCESS)
919              {
920                err("An error occurred while attempting to search for " +
921                     "conflicts with " + a.getName() + " value '" + value +
922                     "' (as found in entry '" + searchEntry.getDN() +
923                     "') below '" + baseDN + "':  " +
924                     searchResult.getDiagnosticMessage());
925                conflictCounts.get(attrName).incrementAndGet();
926                break baseDNLoop;
927              }
928            }
929          }
930        }
931      }
932    }
933    finally
934    {
935      final long count = entriesExamined.incrementAndGet();
936      if ((count % 1000L) == 0L)
937      {
938        out(count, " entries examined");
939      }
940    }
941  }
942
943
944
945  /**
946   * Indicates that the provided search result reference has been returned by
947   * the server and may be processed by this search result listener.
948   *
949   * @param  searchReference  The search result reference that has been returned
950   *                          by the server.
951   */
952  public void searchReferenceReturned(
953                   final SearchResultReference searchReference)
954  {
955    // No implementation is required.  This tool will not follow referrals.
956  }
957}