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