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