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