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