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