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