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.LDAPConnectionOptions; 040import com.unboundid.ldap.sdk.LDAPConnectionPool; 041import com.unboundid.ldap.sdk.LDAPException; 042import com.unboundid.ldap.sdk.LDAPSearchException; 043import com.unboundid.ldap.sdk.ResultCode; 044import com.unboundid.ldap.sdk.SearchRequest; 045import com.unboundid.ldap.sdk.SearchResult; 046import com.unboundid.ldap.sdk.SearchResultEntry; 047import com.unboundid.ldap.sdk.SearchResultReference; 048import com.unboundid.ldap.sdk.SearchResultListener; 049import com.unboundid.ldap.sdk.SearchScope; 050import com.unboundid.ldap.sdk.Version; 051import com.unboundid.ldap.sdk.controls.SimplePagedResultsControl; 052import com.unboundid.util.Debug; 053import com.unboundid.util.LDAPCommandLineTool; 054import com.unboundid.util.StaticUtils; 055import com.unboundid.util.ThreadSafety; 056import com.unboundid.util.ThreadSafetyLevel; 057import com.unboundid.util.args.ArgumentException; 058import com.unboundid.util.args.ArgumentParser; 059import com.unboundid.util.args.DNArgument; 060import com.unboundid.util.args.FilterArgument; 061import com.unboundid.util.args.IntegerArgument; 062import 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) 098public 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 865baseDNLoop: 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}