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