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