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 */ 021package com.unboundid.ldap.sdk.examples; 022 023 024 025import java.io.OutputStream; 026import java.util.Collections; 027import java.util.LinkedHashMap; 028import java.util.LinkedHashSet; 029import java.util.List; 030import java.util.Map; 031import java.util.TreeMap; 032import java.util.concurrent.atomic.AtomicLong; 033 034import com.unboundid.asn1.ASN1OctetString; 035import com.unboundid.ldap.sdk.Attribute; 036import com.unboundid.ldap.sdk.DereferencePolicy; 037import com.unboundid.ldap.sdk.DN; 038import com.unboundid.ldap.sdk.Filter; 039import com.unboundid.ldap.sdk.LDAPConnection; 040import com.unboundid.ldap.sdk.LDAPException; 041import com.unboundid.ldap.sdk.LDAPSearchException; 042import com.unboundid.ldap.sdk.ResultCode; 043import com.unboundid.ldap.sdk.SearchRequest; 044import com.unboundid.ldap.sdk.SearchResult; 045import com.unboundid.ldap.sdk.SearchResultEntry; 046import com.unboundid.ldap.sdk.SearchResultReference; 047import com.unboundid.ldap.sdk.SearchResultListener; 048import com.unboundid.ldap.sdk.SearchScope; 049import com.unboundid.ldap.sdk.Version; 050import com.unboundid.ldap.sdk.controls.SimplePagedResultsControl; 051import com.unboundid.util.Debug; 052import com.unboundid.util.LDAPCommandLineTool; 053import com.unboundid.util.StaticUtils; 054import com.unboundid.util.ThreadSafety; 055import com.unboundid.util.ThreadSafetyLevel; 056import com.unboundid.util.args.ArgumentException; 057import com.unboundid.util.args.ArgumentParser; 058import com.unboundid.util.args.DNArgument; 059import com.unboundid.util.args.FilterArgument; 060import com.unboundid.util.args.IntegerArgument; 061import 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) 097public 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 796baseDNLoop: 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}