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