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