001/*
002 * Copyright 2013-2016 UnboundID Corp.
003 * All Rights Reserved.
004 */
005/*
006 * Copyright (C) 2013-2016 UnboundID Corp.
007 *
008 * This program is free software; you can redistribute it and/or modify
009 * it under the terms of the GNU General Public License (GPLv2 only)
010 * or the terms of the GNU Lesser General Public License (LGPLv2.1 only)
011 * as published by the Free Software Foundation.
012 *
013 * This program is distributed in the hope that it will be useful,
014 * but WITHOUT ANY WARRANTY; without even the implied warranty of
015 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
016 * GNU General Public License for more details.
017 *
018 * You should have received a copy of the GNU General Public License
019 * along with this program; if not, see <http://www.gnu.org/licenses>.
020 */
021package com.unboundid.ldap.sdk.examples;
022
023
024
025import java.io.OutputStream;
026import java.util.Collections;
027import java.util.LinkedHashMap;
028import java.util.LinkedHashSet;
029import java.util.List;
030import java.util.Map;
031import java.util.TreeMap;
032import java.util.concurrent.atomic.AtomicLong;
033
034import com.unboundid.asn1.ASN1OctetString;
035import com.unboundid.ldap.sdk.Attribute;
036import com.unboundid.ldap.sdk.DereferencePolicy;
037import com.unboundid.ldap.sdk.DN;
038import com.unboundid.ldap.sdk.Filter;
039import com.unboundid.ldap.sdk.LDAPConnection;
040import com.unboundid.ldap.sdk.LDAPException;
041import com.unboundid.ldap.sdk.LDAPSearchException;
042import com.unboundid.ldap.sdk.ResultCode;
043import com.unboundid.ldap.sdk.SearchRequest;
044import com.unboundid.ldap.sdk.SearchResult;
045import com.unboundid.ldap.sdk.SearchResultEntry;
046import com.unboundid.ldap.sdk.SearchResultReference;
047import com.unboundid.ldap.sdk.SearchResultListener;
048import com.unboundid.ldap.sdk.SearchScope;
049import com.unboundid.ldap.sdk.Version;
050import com.unboundid.ldap.sdk.controls.SimplePagedResultsControl;
051import com.unboundid.util.Debug;
052import com.unboundid.util.LDAPCommandLineTool;
053import com.unboundid.util.StaticUtils;
054import com.unboundid.util.ThreadSafety;
055import com.unboundid.util.ThreadSafetyLevel;
056import com.unboundid.util.args.ArgumentException;
057import com.unboundid.util.args.ArgumentParser;
058import com.unboundid.util.args.DNArgument;
059import com.unboundid.util.args.FilterArgument;
060import com.unboundid.util.args.IntegerArgument;
061import 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)
097public 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
796baseDNLoop:
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}