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