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 * Adds the arguments needed by this command-line tool to the provided 447 * argument parser which are not related to connecting or authenticating to 448 * the directory server. 449 * 450 * @param parser The argument parser to which the arguments should be added. 451 * 452 * @throws ArgumentException If a problem occurs while adding the arguments. 453 */ 454 @Override() 455 public void addNonLDAPArguments(final ArgumentParser parser) 456 throws ArgumentException 457 { 458 String description = "The search base DN(s) to use to find entries with " + 459 "attributes for which to find uniqueness conflicts. At least one " + 460 "base DN must be specified."; 461 baseDNArgument = new DNArgument('b', "baseDN", true, 0, "{dn}", 462 description); 463 baseDNArgument.addLongIdentifier("base-dn", true); 464 parser.addArgument(baseDNArgument); 465 466 description = "A filter that will be used to identify the set of " + 467 "entries in which to identify uniqueness conflicts. If this is not " + 468 "specified, then all entries containing the target attribute(s) " + 469 "will be examined."; 470 filterArgument = new FilterArgument('f', "filter", false, 1, "{filter}", 471 description); 472 parser.addArgument(filterArgument); 473 474 description = "The attributes for which to find uniqueness conflicts. " + 475 "At least one attribute must be specified, and each attribute " + 476 "must be indexed for equality searches."; 477 attributeArgument = new StringArgument('A', "attribute", true, 0, "{attr}", 478 description); 479 parser.addArgument(attributeArgument); 480 481 description = "Indicates the behavior to exhibit if multiple unique " + 482 "attributes are provided. Allowed values are '" + 483 BEHAVIOR_UNIQUE_WITHIN_ATTR + "' (indicates that each value only " + 484 "needs to be unique within its own attribute type), '" + 485 BEHAVIOR_UNIQUE_ACROSS_ATTRS_INCLUDING_SAME + "' (indicates that " + 486 "each value needs to be unique across all of the specified " + 487 "attributes), '" + BEHAVIOR_UNIQUE_ACROSS_ATTRS_EXCEPT_SAME + 488 "' (indicates each value needs to be unique across all of the " + 489 "specified attributes, except that multiple attributes in the same " + 490 "entry are allowed to share the same value), and '" + 491 BEHAVIOR_UNIQUE_IN_COMBINATION + "' (indicates that every " + 492 "combination of the values of the specified attributes must be " + 493 "unique across each entry)."; 494 final Set<String> allowedValues = StaticUtils.setOf( 495 BEHAVIOR_UNIQUE_WITHIN_ATTR, 496 BEHAVIOR_UNIQUE_ACROSS_ATTRS_INCLUDING_SAME, 497 BEHAVIOR_UNIQUE_ACROSS_ATTRS_EXCEPT_SAME, 498 BEHAVIOR_UNIQUE_IN_COMBINATION); 499 multipleAttributeBehaviorArgument = new StringArgument('m', 500 "multipleAttributeBehavior", false, 1, "{behavior}", description, 501 allowedValues, BEHAVIOR_UNIQUE_WITHIN_ATTR); 502 multipleAttributeBehaviorArgument.addLongIdentifier( 503 "multiple-attribute-behavior", true); 504 parser.addArgument(multipleAttributeBehaviorArgument); 505 506 description = "The maximum number of entries to retrieve at a time when " + 507 "attempting to find uniqueness conflicts. This requires that the " + 508 "authenticated user have permission to use the simple paged results " + 509 "control, but it can avoid problems with the server sending entries " + 510 "too quickly for the client to handle. By default, the simple " + 511 "paged results control will not be used."; 512 pageSizeArgument = 513 new IntegerArgument('z', "simplePageSize", false, 1, "{num}", 514 description, 1, Integer.MAX_VALUE); 515 pageSizeArgument.addLongIdentifier("simple-page-size", true); 516 parser.addArgument(pageSizeArgument); 517 518 description = "The time limit in seconds that will be used for search " + 519 "requests attempting to identify conflicts for each value of any of " + 520 "the unique attributes. This time limit is used to avoid sending " + 521 "expensive unindexed search requests that can consume significant " + 522 "server resources. If any of these search operations fails in a " + 523 "way that indicates the requested time limit was exceeded, the " + 524 "tool will abort its processing. A value of zero indicates that no " + 525 "time limit will be enforced. If this argument is not provided, a " + 526 "default time limit of " + DEFAULT_TIME_LIMIT_SECONDS + 527 " will be used."; 528 timeLimitArgument = new IntegerArgument('l', "timeLimitSeconds", false, 1, 529 "{num}", description, 0, Integer.MAX_VALUE, 530 DEFAULT_TIME_LIMIT_SECONDS); 531 timeLimitArgument.addLongIdentifier("timeLimit", true); 532 timeLimitArgument.addLongIdentifier("time-limit-seconds", true); 533 timeLimitArgument.addLongIdentifier("time-limit", true); 534 535 parser.addArgument(timeLimitArgument); 536 } 537 538 539 540 /** 541 * Retrieves the connection options that should be used for connections that 542 * are created with this command line tool. Subclasses may override this 543 * method to use a custom set of connection options. 544 * 545 * @return The connection options that should be used for connections that 546 * are created with this command line tool. 547 */ 548 @Override() 549 public LDAPConnectionOptions getConnectionOptions() 550 { 551 final LDAPConnectionOptions options = new LDAPConnectionOptions(); 552 553 options.setUseSynchronousMode(true); 554 options.setResponseTimeoutMillis(0L); 555 556 return options; 557 } 558 559 560 561 /** 562 * Performs the core set of processing for this tool. 563 * 564 * @return A result code that indicates whether the processing completed 565 * successfully. 566 */ 567 @Override() 568 public ResultCode doToolProcessing() 569 { 570 // Determine the multi-attribute behavior that we should exhibit. 571 final List<String> attrList = attributeArgument.getValues(); 572 final String multiAttrBehavior = 573 multipleAttributeBehaviorArgument.getValue(); 574 if (attrList.size() > 1) 575 { 576 if (multiAttrBehavior.equalsIgnoreCase( 577 BEHAVIOR_UNIQUE_ACROSS_ATTRS_INCLUDING_SAME)) 578 { 579 uniqueAcrossAttributes = true; 580 uniqueInCombination = false; 581 allowConflictsInSameEntry = false; 582 } 583 else if (multiAttrBehavior.equalsIgnoreCase( 584 BEHAVIOR_UNIQUE_ACROSS_ATTRS_EXCEPT_SAME)) 585 { 586 uniqueAcrossAttributes = true; 587 uniqueInCombination = false; 588 allowConflictsInSameEntry = true; 589 } 590 else if (multiAttrBehavior.equalsIgnoreCase( 591 BEHAVIOR_UNIQUE_IN_COMBINATION)) 592 { 593 uniqueAcrossAttributes = false; 594 uniqueInCombination = true; 595 allowConflictsInSameEntry = true; 596 } 597 else 598 { 599 uniqueAcrossAttributes = false; 600 uniqueInCombination = false; 601 allowConflictsInSameEntry = true; 602 } 603 } 604 else 605 { 606 uniqueAcrossAttributes = false; 607 uniqueInCombination = false; 608 allowConflictsInSameEntry = true; 609 } 610 611 612 // Get the string representations of the base DNs. 613 final List<DN> dnList = baseDNArgument.getValues(); 614 baseDNs = new String[dnList.size()]; 615 for (int i=0; i < baseDNs.length; i++) 616 { 617 baseDNs[i] = dnList.get(i).toString(); 618 } 619 620 // Establish a connection to the target directory server to use for finding 621 // entries with unique attributes. 622 final LDAPConnectionPool findUniqueAttributesPool; 623 try 624 { 625 findUniqueAttributesPool = getConnectionPool(1, 1); 626 findUniqueAttributesPool. 627 setRetryFailedOperationsDueToInvalidConnections(true); 628 } 629 catch (final LDAPException le) 630 { 631 Debug.debugException(le); 632 err("Unable to establish a connection to the directory server: ", 633 StaticUtils.getExceptionMessage(le)); 634 return le.getResultCode(); 635 } 636 637 try 638 { 639 // Establish a connection to use for finding unique attribute conflicts. 640 try 641 { 642 findConflictsPool= getConnectionPool(1, 1); 643 findConflictsPool.setRetryFailedOperationsDueToInvalidConnections(true); 644 } 645 catch (final LDAPException le) 646 { 647 Debug.debugException(le); 648 err("Unable to establish a connection to the directory server: ", 649 StaticUtils.getExceptionMessage(le)); 650 return le.getResultCode(); 651 } 652 653 // Get the set of attributes for which to ensure uniqueness. 654 attributes = new String[attrList.size()]; 655 attrList.toArray(attributes); 656 657 658 // Construct a search filter that will be used to find all entries with 659 // unique attributes. 660 Filter filter; 661 if (attributes.length == 1) 662 { 663 filter = Filter.createPresenceFilter(attributes[0]); 664 conflictCounts.put(attributes[0], new AtomicLong(0L)); 665 } 666 else if (uniqueInCombination) 667 { 668 final Filter[] andComps = new Filter[attributes.length]; 669 for (int i=0; i < attributes.length; i++) 670 { 671 andComps[i] = Filter.createPresenceFilter(attributes[i]); 672 conflictCounts.put(attributes[i], new AtomicLong(0L)); 673 } 674 filter = Filter.createANDFilter(andComps); 675 } 676 else 677 { 678 final Filter[] orComps = new Filter[attributes.length]; 679 for (int i=0; i < attributes.length; i++) 680 { 681 orComps[i] = Filter.createPresenceFilter(attributes[i]); 682 conflictCounts.put(attributes[i], new AtomicLong(0L)); 683 } 684 filter = Filter.createORFilter(orComps); 685 } 686 687 if (filterArgument.isPresent()) 688 { 689 filter = Filter.createANDFilter(filterArgument.getValue(), filter); 690 } 691 692 // Iterate across all of the search base DNs and perform searches to find 693 // unique attributes. 694 for (final String baseDN : baseDNs) 695 { 696 ASN1OctetString cookie = null; 697 do 698 { 699 if (timeLimitExceeded.get()) 700 { 701 break; 702 } 703 704 final SearchRequest searchRequest = new SearchRequest(this, baseDN, 705 SearchScope.SUB, filter, attributes); 706 if (pageSizeArgument.isPresent()) 707 { 708 searchRequest.addControl(new SimplePagedResultsControl( 709 pageSizeArgument.getValue(), cookie, false)); 710 } 711 712 SearchResult searchResult; 713 try 714 { 715 searchResult = findUniqueAttributesPool.search(searchRequest); 716 } 717 catch (final LDAPSearchException lse) 718 { 719 Debug.debugException(lse); 720 try 721 { 722 searchResult = findConflictsPool.search(searchRequest); 723 } 724 catch (final LDAPSearchException lse2) 725 { 726 Debug.debugException(lse2); 727 searchResult = lse2.getSearchResult(); 728 } 729 } 730 731 if (searchResult.getResultCode() != ResultCode.SUCCESS) 732 { 733 err("An error occurred while attempting to search for unique " + 734 "attributes in entries below " + baseDN + ": " + 735 searchResult.getDiagnosticMessage()); 736 return searchResult.getResultCode(); 737 } 738 739 final SimplePagedResultsControl pagedResultsResponse; 740 try 741 { 742 pagedResultsResponse = SimplePagedResultsControl.get(searchResult); 743 } 744 catch (final LDAPException le) 745 { 746 Debug.debugException(le); 747 err("An error occurred while attempting to decode a simple " + 748 "paged results response control in the response to a " + 749 "search for entries below " + baseDN + ": " + 750 StaticUtils.getExceptionMessage(le)); 751 return le.getResultCode(); 752 } 753 754 if (pagedResultsResponse != null) 755 { 756 if (pagedResultsResponse.moreResultsToReturn()) 757 { 758 cookie = pagedResultsResponse.getCookie(); 759 } 760 else 761 { 762 cookie = null; 763 } 764 } 765 } 766 while (cookie != null); 767 } 768 769 770 // See if there were any uniqueness conflicts found. 771 boolean conflictFound = false; 772 if (uniqueInCombination) 773 { 774 final long count = combinationConflictCounts.get(); 775 if (count > 0L) 776 { 777 conflictFound = true; 778 err("Found " + count + " total conflicts."); 779 } 780 } 781 else 782 { 783 for (final Map.Entry<String,AtomicLong> e : conflictCounts.entrySet()) 784 { 785 final long numConflicts = e.getValue().get(); 786 if (numConflicts > 0L) 787 { 788 if (! conflictFound) 789 { 790 err(); 791 conflictFound = true; 792 } 793 794 err("Found " + numConflicts + 795 " unique value conflicts in attribute " + e.getKey()); 796 } 797 } 798 } 799 800 if (conflictFound) 801 { 802 return ResultCode.CONSTRAINT_VIOLATION; 803 } 804 else if (timeLimitExceeded.get()) 805 { 806 return ResultCode.TIME_LIMIT_EXCEEDED; 807 } 808 else 809 { 810 out("No unique attribute conflicts were found."); 811 return ResultCode.SUCCESS; 812 } 813 } 814 finally 815 { 816 findUniqueAttributesPool.close(); 817 818 if (findConflictsPool != null) 819 { 820 findConflictsPool.close(); 821 } 822 } 823 } 824 825 826 827 /** 828 * Retrieves the number of conflicts identified across multiple attributes in 829 * combination. 830 * 831 * @return The number of conflicts identified across multiple attributes in 832 * combination. 833 */ 834 public long getCombinationConflictCounts() 835 { 836 return combinationConflictCounts.get(); 837 } 838 839 840 841 /** 842 * Retrieves a map that correlates the number of uniqueness conflicts found by 843 * attribute type. 844 * 845 * @return A map that correlates the number of uniqueness conflicts found by 846 * attribute type. 847 */ 848 public Map<String,AtomicLong> getConflictCounts() 849 { 850 return Collections.unmodifiableMap(conflictCounts); 851 } 852 853 854 855 /** 856 * Retrieves a set of information that may be used to generate example usage 857 * information. Each element in the returned map should consist of a map 858 * between an example set of arguments and a string that describes the 859 * behavior of the tool when invoked with that set of arguments. 860 * 861 * @return A set of information that may be used to generate example usage 862 * information. It may be {@code null} or empty if no example usage 863 * information is available. 864 */ 865 @Override() 866 public LinkedHashMap<String[],String> getExampleUsages() 867 { 868 final LinkedHashMap<String[],String> exampleMap = 869 new LinkedHashMap<>(StaticUtils.computeMapCapacity(1)); 870 871 final String[] args = 872 { 873 "--hostname", "server.example.com", 874 "--port", "389", 875 "--bindDN", "uid=john.doe,ou=People,dc=example,dc=com", 876 "--bindPassword", "password", 877 "--baseDN", "dc=example,dc=com", 878 "--attribute", "uid", 879 "--simplePageSize", "100" 880 }; 881 exampleMap.put(args, 882 "Identify any values of the uid attribute that are not unique " + 883 "across all entries below dc=example,dc=com."); 884 885 return exampleMap; 886 } 887 888 889 890 /** 891 * Indicates that the provided search result entry has been returned by the 892 * server and may be processed by this search result listener. 893 * 894 * @param searchEntry The search result entry that has been returned by the 895 * server. 896 */ 897 @Override() 898 public void searchEntryReturned(final SearchResultEntry searchEntry) 899 { 900 // If we have encountered a "time limit exceeded" error, then don't even 901 // bother processing any more entries. 902 if (timeLimitExceeded.get()) 903 { 904 return; 905 } 906 907 if (uniqueInCombination) 908 { 909 checkForConflictsInCombination(searchEntry); 910 return; 911 } 912 913 try 914 { 915 // If we need to check for conflicts in the same entry, then do that 916 // first. 917 if (! allowConflictsInSameEntry) 918 { 919 boolean conflictFound = false; 920 for (int i=0; i < attributes.length; i++) 921 { 922 final List<Attribute> l1 = 923 searchEntry.getAttributesWithOptions(attributes[i], null); 924 if (l1 != null) 925 { 926 for (int j=i+1; j < attributes.length; j++) 927 { 928 final List<Attribute> l2 = 929 searchEntry.getAttributesWithOptions(attributes[j], null); 930 if (l2 != null) 931 { 932 for (final Attribute a1 : l1) 933 { 934 for (final String value : a1.getValues()) 935 { 936 for (final Attribute a2 : l2) 937 { 938 if (a2.hasValue(value)) 939 { 940 err("Value '", value, "' in attribute ", a1.getName(), 941 " of entry '", searchEntry.getDN(), 942 " is also present in attribute ", a2.getName(), 943 " of the same entry."); 944 conflictFound = true; 945 conflictCounts.get(attributes[i]).incrementAndGet(); 946 } 947 } 948 } 949 } 950 } 951 } 952 } 953 } 954 955 if (conflictFound) 956 { 957 return; 958 } 959 } 960 961 962 // Get the unique attributes from the entry and search for conflicts with 963 // each value in other entries. Although we could theoretically do this 964 // with fewer searches, most uses of unique attributes don't have multiple 965 // values, so the following code (which is much simpler) is just as 966 // efficient in the common case. 967 for (final String attrName : attributes) 968 { 969 final List<Attribute> attrList = 970 searchEntry.getAttributesWithOptions(attrName, null); 971 for (final Attribute a : attrList) 972 { 973 for (final String value : a.getValues()) 974 { 975 Filter filter; 976 if (uniqueAcrossAttributes) 977 { 978 final Filter[] orComps = new Filter[attributes.length]; 979 for (int i=0; i < attributes.length; i++) 980 { 981 orComps[i] = Filter.createEqualityFilter(attributes[i], value); 982 } 983 filter = Filter.createORFilter(orComps); 984 } 985 else 986 { 987 filter = Filter.createEqualityFilter(attrName, value); 988 } 989 990 if (filterArgument.isPresent()) 991 { 992 filter = Filter.createANDFilter(filterArgument.getValue(), 993 filter); 994 } 995 996baseDNLoop: 997 for (final String baseDN : baseDNs) 998 { 999 SearchResult searchResult; 1000 final SearchRequest searchRequest = new SearchRequest(baseDN, 1001 SearchScope.SUB, DereferencePolicy.NEVER, 2, 1002 timeLimitArgument.getValue(), false, filter, "1.1"); 1003 try 1004 { 1005 searchResult = findConflictsPool.search(searchRequest); 1006 } 1007 catch (final LDAPSearchException lse) 1008 { 1009 Debug.debugException(lse); 1010 if (lse.getResultCode() == ResultCode.TIME_LIMIT_EXCEEDED) 1011 { 1012 // The server spent more time than the configured time limit 1013 // to process the search. This almost certainly means that 1014 // the search is unindexed, and we don't want to continue. 1015 // Indicate that the time limit has been exceeded, cancel the 1016 // outer search, and display an error message to the user. 1017 timeLimitExceeded.set(true); 1018 try 1019 { 1020 findConflictsPool.processExtendedOperation( 1021 new CancelExtendedRequest(searchEntry.getMessageID())); 1022 } 1023 catch (final Exception e) 1024 { 1025 Debug.debugException(e); 1026 } 1027 1028 err("A server-side time limit was exceeded when searching " + 1029 "below base DN '" + baseDN + "' with filter '" + 1030 filter + "', which likely means that the search " + 1031 "request is not indexed in the server. Check the " + 1032 "server configuration to ensure that any appropriate " + 1033 "indexes are in place. To indicate that searches " + 1034 "should not request any time limit, use the " + 1035 timeLimitArgument.getIdentifierString() + 1036 " to indicate a time limit of zero seconds."); 1037 return; 1038 } 1039 else if (lse.getResultCode().isConnectionUsable()) 1040 { 1041 searchResult = lse.getSearchResult(); 1042 } 1043 else 1044 { 1045 try 1046 { 1047 searchResult = findConflictsPool.search(searchRequest); 1048 } 1049 catch (final LDAPSearchException lse2) 1050 { 1051 Debug.debugException(lse2); 1052 searchResult = lse2.getSearchResult(); 1053 } 1054 } 1055 } 1056 1057 for (final SearchResultEntry e : searchResult.getSearchEntries()) 1058 { 1059 try 1060 { 1061 if (DN.equals(searchEntry.getDN(), e.getDN())) 1062 { 1063 continue; 1064 } 1065 } 1066 catch (final Exception ex) 1067 { 1068 Debug.debugException(ex); 1069 } 1070 1071 err("Value '", value, "' in attribute ", a.getName(), 1072 " of entry '" + searchEntry.getDN(), 1073 "' is also present in entry '", e.getDN(), "'."); 1074 conflictCounts.get(attrName).incrementAndGet(); 1075 break baseDNLoop; 1076 } 1077 1078 if (searchResult.getResultCode() != ResultCode.SUCCESS) 1079 { 1080 err("An error occurred while attempting to search for " + 1081 "conflicts with " + a.getName() + " value '" + value + 1082 "' (as found in entry '" + searchEntry.getDN() + 1083 "') below '" + baseDN + "': " + 1084 searchResult.getDiagnosticMessage()); 1085 conflictCounts.get(attrName).incrementAndGet(); 1086 break baseDNLoop; 1087 } 1088 } 1089 } 1090 } 1091 } 1092 } 1093 finally 1094 { 1095 final long count = entriesExamined.incrementAndGet(); 1096 if ((count % 1000L) == 0L) 1097 { 1098 out(count, " entries examined"); 1099 } 1100 } 1101 } 1102 1103 1104 1105 /** 1106 * Performs the processing necessary to check for conflicts between a 1107 * combination of attribute values obtained from the provided entry. 1108 * 1109 * @param entry The entry to examine. 1110 */ 1111 private void checkForConflictsInCombination(final SearchResultEntry entry) 1112 { 1113 // Construct a filter used to identify conflicting entries as an AND for 1114 // each attribute. Handle the possibility of multivalued attributes by 1115 // creating an OR of all values for each attribute. And if an additional 1116 // filter was also specified, include it in the AND as well. 1117 final ArrayList<Filter> andComponents = 1118 new ArrayList<>(attributes.length + 1); 1119 for (final String attrName : attributes) 1120 { 1121 final LinkedHashSet<Filter> values = 1122 new LinkedHashSet<>(StaticUtils.computeMapCapacity(5)); 1123 for (final Attribute a : entry.getAttributesWithOptions(attrName, null)) 1124 { 1125 for (final byte[] value : a.getValueByteArrays()) 1126 { 1127 final Filter equalityFilter = 1128 Filter.createEqualityFilter(attrName, value); 1129 values.add(Filter.createEqualityFilter(attrName, value)); 1130 } 1131 } 1132 1133 switch (values.size()) 1134 { 1135 case 0: 1136 // This means that the returned entry didn't include any values for 1137 // the target attribute. This should only happen if the user doesn't 1138 // have permission to see those values. At any rate, we can't check 1139 // this entry for conflicts, so just assume there aren't any. 1140 return; 1141 1142 case 1: 1143 andComponents.add(values.iterator().next()); 1144 break; 1145 1146 default: 1147 andComponents.add(Filter.createORFilter(values)); 1148 break; 1149 } 1150 } 1151 1152 if (filterArgument.isPresent()) 1153 { 1154 andComponents.add(filterArgument.getValue()); 1155 } 1156 1157 final Filter filter = Filter.createANDFilter(andComponents); 1158 1159 1160 // Search below each of the configured base DNs. 1161baseDNLoop: 1162 for (final DN baseDN : baseDNArgument.getValues()) 1163 { 1164 SearchResult searchResult; 1165 final SearchRequest searchRequest = new SearchRequest(baseDN.toString(), 1166 SearchScope.SUB, DereferencePolicy.NEVER, 2, 1167 timeLimitArgument.getValue(), false, filter, "1.1"); 1168 1169 try 1170 { 1171 searchResult = findConflictsPool.search(searchRequest); 1172 } 1173 catch (final LDAPSearchException lse) 1174 { 1175 Debug.debugException(lse); 1176 if (lse.getResultCode() == ResultCode.TIME_LIMIT_EXCEEDED) 1177 { 1178 // The server spent more time than the configured time limit to 1179 // process the search. This almost certainly means that the search is 1180 // unindexed, and we don't want to continue. Indicate that the time 1181 // limit has been exceeded, cancel the outer search, and display an 1182 // error message to the user. 1183 timeLimitExceeded.set(true); 1184 try 1185 { 1186 findConflictsPool.processExtendedOperation( 1187 new CancelExtendedRequest(entry.getMessageID())); 1188 } 1189 catch (final Exception e) 1190 { 1191 Debug.debugException(e); 1192 } 1193 1194 err("A server-side time limit was exceeded when searching below " + 1195 "base DN '" + baseDN + "' with filter '" + filter + 1196 "', which likely means that the search request is not indexed " + 1197 "in the server. Check the server configuration to ensure " + 1198 "that any appropriate indexes are in place. To indicate that " + 1199 "searches should not request any time limit, use the " + 1200 timeLimitArgument.getIdentifierString() + 1201 " to indicate a time limit of zero seconds."); 1202 return; 1203 } 1204 else if (lse.getResultCode().isConnectionUsable()) 1205 { 1206 searchResult = lse.getSearchResult(); 1207 } 1208 else 1209 { 1210 try 1211 { 1212 searchResult = findConflictsPool.search(searchRequest); 1213 } 1214 catch (final LDAPSearchException lse2) 1215 { 1216 Debug.debugException(lse2); 1217 searchResult = lse2.getSearchResult(); 1218 } 1219 } 1220 } 1221 1222 for (final SearchResultEntry e : searchResult.getSearchEntries()) 1223 { 1224 try 1225 { 1226 if (DN.equals(entry.getDN(), e.getDN())) 1227 { 1228 continue; 1229 } 1230 } 1231 catch (final Exception ex) 1232 { 1233 Debug.debugException(ex); 1234 } 1235 1236 err("Entry '" + entry.getDN() + " has a combination of values that " + 1237 "are also present in entry '" + e.getDN() + "'."); 1238 combinationConflictCounts.incrementAndGet(); 1239 break baseDNLoop; 1240 } 1241 1242 if (searchResult.getResultCode() != ResultCode.SUCCESS) 1243 { 1244 err("An error occurred while attempting to search for conflicts " + 1245 " with entry '" + entry.getDN() + "' below '" + baseDN + "': " + 1246 searchResult.getDiagnosticMessage()); 1247 combinationConflictCounts.incrementAndGet(); 1248 break baseDNLoop; 1249 } 1250 } 1251 } 1252 1253 1254 1255 /** 1256 * Indicates that the provided search result reference has been returned by 1257 * the server and may be processed by this search result listener. 1258 * 1259 * @param searchReference The search result reference that has been returned 1260 * by the server. 1261 */ 1262 @Override() 1263 public void searchReferenceReturned( 1264 final SearchResultReference searchReference) 1265 { 1266 // No implementation is required. This tool will not follow referrals. 1267 } 1268}