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