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