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     */
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.AtomicLong;
033    
034    import com.unboundid.asn1.ASN1OctetString;
035    import com.unboundid.ldap.sdk.Attribute;
036    import com.unboundid.ldap.sdk.DereferencePolicy;
037    import com.unboundid.ldap.sdk.DN;
038    import com.unboundid.ldap.sdk.Filter;
039    import com.unboundid.ldap.sdk.LDAPConnectionOptions;
040    import com.unboundid.ldap.sdk.LDAPConnectionPool;
041    import com.unboundid.ldap.sdk.LDAPException;
042    import com.unboundid.ldap.sdk.LDAPSearchException;
043    import com.unboundid.ldap.sdk.ResultCode;
044    import com.unboundid.ldap.sdk.SearchRequest;
045    import com.unboundid.ldap.sdk.SearchResult;
046    import com.unboundid.ldap.sdk.SearchResultEntry;
047    import com.unboundid.ldap.sdk.SearchResultReference;
048    import com.unboundid.ldap.sdk.SearchResultListener;
049    import com.unboundid.ldap.sdk.SearchScope;
050    import com.unboundid.ldap.sdk.Version;
051    import com.unboundid.ldap.sdk.controls.SimplePagedResultsControl;
052    import com.unboundid.util.Debug;
053    import com.unboundid.util.LDAPCommandLineTool;
054    import com.unboundid.util.StaticUtils;
055    import com.unboundid.util.ThreadSafety;
056    import com.unboundid.util.ThreadSafetyLevel;
057    import com.unboundid.util.args.ArgumentException;
058    import com.unboundid.util.args.ArgumentParser;
059    import com.unboundid.util.args.DNArgument;
060    import com.unboundid.util.args.FilterArgument;
061    import com.unboundid.util.args.IntegerArgument;
062    import 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)
098    public 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    
865    baseDNLoop:
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    }