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