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