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