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