001/* 002 * Copyright 2013-2014 UnboundID Corp. 003 * All Rights Reserved. 004 */ 005/* 006 * Copyright (C) 2013-2014 UnboundID Corp. 007 * 008 * This program is free software; you can redistribute it and/or modify 009 * it under the terms of the GNU General Public License (GPLv2 only) 010 * or the terms of the GNU Lesser General Public License (LGPLv2.1 only) 011 * as published by the Free Software Foundation. 012 * 013 * This program is distributed in the hope that it will be useful, 014 * but WITHOUT ANY WARRANTY; without even the implied warranty of 015 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 016 * GNU General Public License for more details. 017 * 018 * You should have received a copy of the GNU General Public License 019 * along with this program; if not, see <http://www.gnu.org/licenses>. 020 */ 021package com.unboundid.ldap.sdk.examples; 022 023 024 025import java.io.OutputStream; 026import java.util.Collections; 027import java.util.LinkedHashMap; 028import java.util.LinkedHashSet; 029import java.util.List; 030import java.util.Map; 031import java.util.TreeMap; 032import java.util.concurrent.atomic.AtomicLong; 033 034import com.unboundid.asn1.ASN1OctetString; 035import com.unboundid.ldap.sdk.Attribute; 036import com.unboundid.ldap.sdk.DereferencePolicy; 037import com.unboundid.ldap.sdk.DN; 038import com.unboundid.ldap.sdk.Filter; 039import com.unboundid.ldap.sdk.LDAPConnection; 040import com.unboundid.ldap.sdk.LDAPException; 041import com.unboundid.ldap.sdk.LDAPSearchException; 042import com.unboundid.ldap.sdk.ResultCode; 043import com.unboundid.ldap.sdk.SearchRequest; 044import com.unboundid.ldap.sdk.SearchResult; 045import com.unboundid.ldap.sdk.SearchResultEntry; 046import com.unboundid.ldap.sdk.SearchResultReference; 047import com.unboundid.ldap.sdk.SearchResultListener; 048import com.unboundid.ldap.sdk.SearchScope; 049import com.unboundid.ldap.sdk.Version; 050import com.unboundid.ldap.sdk.controls.SimplePagedResultsControl; 051import com.unboundid.util.Debug; 052import com.unboundid.util.LDAPCommandLineTool; 053import com.unboundid.util.StaticUtils; 054import com.unboundid.util.ThreadSafety; 055import com.unboundid.util.ThreadSafetyLevel; 056import com.unboundid.util.args.ArgumentException; 057import com.unboundid.util.args.ArgumentParser; 058import com.unboundid.util.args.DNArgument; 059import com.unboundid.util.args.IntegerArgument; 060import com.unboundid.util.args.StringArgument; 061 062 063 064/** 065 * This class provides a tool that may be used to identify unique attribute 066 * conflicts (i.e., attributes which are supposed to be unique but for which 067 * some values exist in multiple entries). 068 * <BR><BR> 069 * All of the necessary information is provided using command line arguments. 070 * Supported arguments include those allowed by the {@link LDAPCommandLineTool} 071 * class, as well as the following additional arguments: 072 * <UL> 073 * <LI>"-b {baseDN}" or "--baseDN {baseDN}" -- specifies the base DN to use 074 * for the searches. At least one base DN must be provided.</LI> 075 * <LI>"-A {attribute}" or "--attribute {attribute}" -- specifies an attribute 076 * for which to enforce uniqueness. At least one unique attribute must be 077 * provided.</LI> 078 * <LI>"-m {behavior}" or "--multipleAttributeBehavior {behavior}" -- 079 * specifies the behavior that the tool should exhibit if multiple 080 * unique attributes are provided. Allowed values include 081 * unique-within-each-attribute, 082 * unique-across-all-attributes-including-in-same-entry, and 083 * unique-across-all-attributes-except-in-same-entry.</LI> 084 * <LI>"-z {size}" or "--simplePageSize {size}" -- indicates that the search 085 * to find entries with unique attributes should use the simple paged 086 * results control to iterate across entries in fixed-size pages rather 087 * than trying to use a single search to identify all entries containing 088 * unique attributes.</LI> 089 * </UL> 090 */ 091@ThreadSafety(level=ThreadSafetyLevel.NOT_THREADSAFE) 092public final class IdentifyUniqueAttributeConflicts 093 extends LDAPCommandLineTool 094 implements SearchResultListener 095{ 096 /** 097 * The unique attribute behavior value that indicates uniqueness should only 098 * be ensured within each attribute. 099 */ 100 private static final String BEHAVIOR_UNIQUE_WITHIN_ATTR = 101 "unique-within-each-attribute"; 102 103 104 105 /** 106 * The unique attribute behavior value that indicates uniqueness should be 107 * ensured across all attributes, and conflicts will not be allowed across 108 * attributes in the same entry. 109 */ 110 private static final String BEHAVIOR_UNIQUE_ACROSS_ATTRS_INCLUDING_SAME = 111 "unique-across-all-attributes-including-in-same-entry"; 112 113 114 115 /** 116 * The unique attribute behavior value that indicates uniqueness should be 117 * ensured across all attributes, except that conflicts will not be allowed 118 * across attributes in the same entry. 119 */ 120 private static final String BEHAVIOR_UNIQUE_ACROSS_ATTRS_EXCEPT_SAME = 121 "unique-across-all-attributes-except-in-same-entry"; 122 123 124 125 /** 126 * The serial version UID for this serializable class. 127 */ 128 private static final long serialVersionUID = -7904414224384249176L; 129 130 131 132 // The number of entries examined so far. 133 private final AtomicLong entriesExamined; 134 135 // Indicates whether cross-attribute uniqueness conflicts should be allowed 136 // in the same entry. 137 private boolean allowConflictsInSameEntry; 138 139 // Indicates whether uniqueness should be enforced across all attributes 140 // rather than within each attribute. 141 private boolean uniqueAcrossAttributes; 142 143 // The argument used to specify the base DNs to use for searches. 144 private DNArgument baseDNArgument; 145 146 // The argument used to specify the search page size. 147 private IntegerArgument pageSizeArgument; 148 149 // The connection to use for finding unique attribute conflicts. 150 private LDAPConnection findConflictsConnection; 151 152 // A map with counts of unique attribute conflicts by attribute type. 153 private final Map<String, AtomicLong> conflictCounts; 154 155 // The names of the attributes for which to find uniqueness conflicts. 156 private String[] attributes; 157 158 // The set of base DNs to use for the searches. 159 private String[] baseDNs; 160 161 // The argument used to specify the attributes for which to find uniqueness 162 // conflicts. 163 private StringArgument attributeArgument; 164 165 // The argument used to specify the behavior that should be exhibited if 166 // multiple attributes are specified. 167 private StringArgument multipleAttributeBehaviorArgument; 168 169 170 171 /** 172 * Parse the provided command line arguments and perform the appropriate 173 * processing. 174 * 175 * @param args The command line arguments provided to this program. 176 */ 177 public static void main(final String... args) 178 { 179 final ResultCode resultCode = main(args, System.out, System.err); 180 if (resultCode != ResultCode.SUCCESS) 181 { 182 System.exit(resultCode.intValue()); 183 } 184 } 185 186 187 188 /** 189 * Parse the provided command line arguments and perform the appropriate 190 * processing. 191 * 192 * @param args The command line arguments provided to this program. 193 * @param outStream The output stream to which standard out should be 194 * written. It may be {@code null} if output should be 195 * suppressed. 196 * @param errStream The output stream to which standard error should be 197 * written. It may be {@code null} if error messages 198 * should be suppressed. 199 * 200 * @return A result code indicating whether the processing was successful. 201 */ 202 public static ResultCode main(final String[] args, 203 final OutputStream outStream, 204 final OutputStream errStream) 205 { 206 final IdentifyUniqueAttributeConflicts tool = 207 new IdentifyUniqueAttributeConflicts(outStream, errStream); 208 return tool.runTool(args); 209 } 210 211 212 213 /** 214 * Creates a new instance of this tool. 215 * 216 * @param outStream The output stream to which standard out should be 217 * written. It may be {@code null} if output should be 218 * suppressed. 219 * @param errStream The output stream to which standard error should be 220 * written. It may be {@code null} if error messages 221 * should be suppressed. 222 */ 223 public IdentifyUniqueAttributeConflicts(final OutputStream outStream, 224 final OutputStream errStream) 225 { 226 super(outStream, errStream); 227 228 baseDNArgument = null; 229 pageSizeArgument = null; 230 attributeArgument = null; 231 multipleAttributeBehaviorArgument = null; 232 findConflictsConnection = null; 233 allowConflictsInSameEntry = false; 234 uniqueAcrossAttributes = false; 235 attributes = null; 236 baseDNs = null; 237 238 entriesExamined = new AtomicLong(0L); 239 conflictCounts = new TreeMap<String, AtomicLong>(); 240 } 241 242 243 244 /** 245 * Retrieves the name of this tool. It should be the name of the command used 246 * to invoke this tool. 247 * 248 * @return The name for this tool. 249 */ 250 @Override() 251 public String getToolName() 252 { 253 return "identify-unique-attribute-conflicts"; 254 } 255 256 257 258 /** 259 * Retrieves a human-readable description for this tool. 260 * 261 * @return A human-readable description for this tool. 262 */ 263 @Override() 264 public String getToolDescription() 265 { 266 return "This tool may be used to identify unique attribute conflicts. " + 267 "That is, it may identify values of one or more attributes which " + 268 "are supposed to exist only in a single entry but are found in " + 269 "multiple entries."; 270 } 271 272 273 274 /** 275 * Retrieves a version string for this tool, if available. 276 * 277 * @return A version string for this tool, or {@code null} if none is 278 * available. 279 */ 280 @Override() 281 public String getToolVersion() 282 { 283 return Version.NUMERIC_VERSION_STRING; 284 } 285 286 287 288 /** 289 * Adds the arguments needed by this command-line tool to the provided 290 * argument parser which are not related to connecting or authenticating to 291 * the directory server. 292 * 293 * @param parser The argument parser to which the arguments should be added. 294 * 295 * @throws ArgumentException If a problem occurs while adding the arguments. 296 */ 297 @Override() 298 public void addNonLDAPArguments(final ArgumentParser parser) 299 throws ArgumentException 300 { 301 String description = "The search base DN(s) to use to find entries with " + 302 "attributes for which to find uniqueness conflicts. At least one " + 303 "base DN must be specified."; 304 baseDNArgument = new DNArgument('b', "baseDN", true, 0, "{dn}", 305 description); 306 parser.addArgument(baseDNArgument); 307 308 description = "The attribute(s) for which to find missing references. " + 309 "At least one attribute must be specified, and each attribute " + 310 "must be indexed for equality searches and have values which are DNs."; 311 attributeArgument = new StringArgument('A', "attribute", true, 0, "{attr}", 312 description); 313 parser.addArgument(attributeArgument); 314 315 description = "Indicates the behavior to exhibit if multiple unique " + 316 "attributes are provided. Allowed values are '" + 317 BEHAVIOR_UNIQUE_WITHIN_ATTR + "' (indicates that each value only " + 318 "needs to be unique within its own attribute type), '" + 319 BEHAVIOR_UNIQUE_ACROSS_ATTRS_INCLUDING_SAME + "' (indicates that " + 320 "each value needs to be unique across all of the specified " + 321 "attributes), and '" + BEHAVIOR_UNIQUE_ACROSS_ATTRS_EXCEPT_SAME + 322 "' (indicates each value needs to be unique across all of the " + 323 "specified attributes, except that multiple attributes in the same " + 324 "entry are allowed to share the same value)."; 325 final LinkedHashSet<String> allowedValues = new LinkedHashSet<String>(3); 326 allowedValues.add(BEHAVIOR_UNIQUE_WITHIN_ATTR); 327 allowedValues.add(BEHAVIOR_UNIQUE_ACROSS_ATTRS_INCLUDING_SAME); 328 allowedValues.add(BEHAVIOR_UNIQUE_ACROSS_ATTRS_EXCEPT_SAME); 329 multipleAttributeBehaviorArgument = new StringArgument('m', 330 "multipleAttributeBehavior", false, 1, "{behavior}", description, 331 allowedValues, BEHAVIOR_UNIQUE_WITHIN_ATTR); 332 parser.addArgument(multipleAttributeBehaviorArgument); 333 334 description = "The maximum number of entries to retrieve at a time when " + 335 "attempting to find entries with references to other entries. This " + 336 "requires that the authenticated user have permission to use the " + 337 "simple paged results control, but it can avoid problems with the " + 338 "server sending entries too quickly for the client to handle. By " + 339 "default, the simple paged results control will not be used."; 340 pageSizeArgument = 341 new IntegerArgument('z', "simplePageSize", false, 1, "{num}", 342 description, 1, Integer.MAX_VALUE); 343 parser.addArgument(pageSizeArgument); 344 } 345 346 347 348 /** 349 * Performs the core set of processing for this tool. 350 * 351 * @return A result code that indicates whether the processing completed 352 * successfully. 353 */ 354 @Override() 355 public ResultCode doToolProcessing() 356 { 357 // Determine the multi-attribute behavior that we should exhibit. 358 final List<String> attrList = attributeArgument.getValues(); 359 final String multiAttrBehavior = 360 multipleAttributeBehaviorArgument.getValue(); 361 if (attrList.size() > 1) 362 { 363 if (multiAttrBehavior.equalsIgnoreCase( 364 BEHAVIOR_UNIQUE_ACROSS_ATTRS_INCLUDING_SAME)) 365 { 366 uniqueAcrossAttributes = true; 367 allowConflictsInSameEntry = false; 368 } 369 else if (multiAttrBehavior.equalsIgnoreCase( 370 BEHAVIOR_UNIQUE_ACROSS_ATTRS_EXCEPT_SAME)) 371 { 372 uniqueAcrossAttributes = true; 373 allowConflictsInSameEntry = true; 374 } 375 else 376 { 377 uniqueAcrossAttributes = false; 378 allowConflictsInSameEntry = true; 379 } 380 } 381 else 382 { 383 uniqueAcrossAttributes = false; 384 allowConflictsInSameEntry = true; 385 } 386 387 388 // Get the string representations of the base DNs. 389 final List<DN> dnList = baseDNArgument.getValues(); 390 baseDNs = new String[dnList.size()]; 391 for (int i=0; i < baseDNs.length; i++) 392 { 393 baseDNs[i] = dnList.get(i).toString(); 394 } 395 396 // Establish a connection to the target directory server to use for finding 397 // entries with unique attributes. 398 final LDAPConnection findUniqueAttributesConnection; 399 try 400 { 401 findUniqueAttributesConnection = getConnection(); 402 } 403 catch (final LDAPException le) 404 { 405 Debug.debugException(le); 406 err("Unable to establish a connection to the directory server: ", 407 StaticUtils.getExceptionMessage(le)); 408 return le.getResultCode(); 409 } 410 411 try 412 { 413 // Establish a connection to use for finding unique attribute conflicts. 414 try 415 { 416 findConflictsConnection = getConnection(); 417 } 418 catch (final LDAPException le) 419 { 420 Debug.debugException(le); 421 err("Unable to establish a connection to the directory server: ", 422 StaticUtils.getExceptionMessage(le)); 423 return le.getResultCode(); 424 } 425 426 // Get the set of attributes for which to ensure uniqueness. 427 attributes = new String[attrList.size()]; 428 attrList.toArray(attributes); 429 430 431 // Construct a search filter that will be used to find all entries with 432 // unique attributes. 433 final Filter filter; 434 if (attributes.length == 1) 435 { 436 filter = Filter.createPresenceFilter(attributes[0]); 437 conflictCounts.put(attributes[0], new AtomicLong(0L)); 438 } 439 else 440 { 441 final Filter[] orComps = new Filter[attributes.length]; 442 for (int i=0; i < attributes.length; i++) 443 { 444 orComps[i] = Filter.createPresenceFilter(attributes[i]); 445 conflictCounts.put(attributes[i], new AtomicLong(0L)); 446 } 447 filter = Filter.createORFilter(orComps); 448 } 449 450 451 // Iterate across all of the search base DNs and perform searches to find 452 // unique attributes. 453 for (final String baseDN : baseDNs) 454 { 455 ASN1OctetString cookie = null; 456 do 457 { 458 final SearchRequest searchRequest = new SearchRequest(this, baseDN, 459 SearchScope.SUB, filter, attributes); 460 if (pageSizeArgument.isPresent()) 461 { 462 searchRequest.addControl(new SimplePagedResultsControl( 463 pageSizeArgument.getValue(), cookie, false)); 464 } 465 466 SearchResult searchResult; 467 try 468 { 469 searchResult = findUniqueAttributesConnection.search(searchRequest); 470 } 471 catch (final LDAPSearchException lse) 472 { 473 Debug.debugException(lse); 474 searchResult = lse.getSearchResult(); 475 } 476 477 if (searchResult.getResultCode() != ResultCode.SUCCESS) 478 { 479 err("An error occurred while attempting to search for unique " + 480 "attributes in entries below " + baseDN + ": " + 481 searchResult.getDiagnosticMessage()); 482 return searchResult.getResultCode(); 483 } 484 485 final SimplePagedResultsControl pagedResultsResponse; 486 try 487 { 488 pagedResultsResponse = SimplePagedResultsControl.get(searchResult); 489 } 490 catch (final LDAPException le) 491 { 492 Debug.debugException(le); 493 err("An error occurred while attempting to decode a simple " + 494 "paged results response control in the response to a " + 495 "search for entries below " + baseDN + ": " + 496 StaticUtils.getExceptionMessage(le)); 497 return le.getResultCode(); 498 } 499 500 if (pagedResultsResponse != null) 501 { 502 if (pagedResultsResponse.moreResultsToReturn()) 503 { 504 cookie = pagedResultsResponse.getCookie(); 505 } 506 else 507 { 508 cookie = null; 509 } 510 } 511 } 512 while (cookie != null); 513 } 514 515 516 // See if there were any missing references found. 517 boolean conflictFound = false; 518 for (final Map.Entry<String,AtomicLong> e : conflictCounts.entrySet()) 519 { 520 final long numConflicts = e.getValue().get(); 521 if (numConflicts > 0L) 522 { 523 if (! conflictFound) 524 { 525 err(); 526 conflictFound = true; 527 } 528 529 err("Found " + numConflicts + 530 " unique value conflicts in attribute " + e.getKey()); 531 } 532 } 533 534 if (conflictFound) 535 { 536 return ResultCode.CONSTRAINT_VIOLATION; 537 } 538 else 539 { 540 out("No unique attribute conflicts were found."); 541 return ResultCode.SUCCESS; 542 } 543 } 544 finally 545 { 546 findUniqueAttributesConnection.close(); 547 548 if (findConflictsConnection != null) 549 { 550 findConflictsConnection.close(); 551 } 552 } 553 } 554 555 556 557 /** 558 * Retrieves a map that correlates the number of missing references found by 559 * attribute type. 560 * 561 * @return A map that correlates the number of missing references found by 562 * attribute type. 563 */ 564 public Map<String,AtomicLong> getConflictCounts() 565 { 566 return Collections.unmodifiableMap(conflictCounts); 567 } 568 569 570 571 /** 572 * Retrieves a set of information that may be used to generate example usage 573 * information. Each element in the returned map should consist of a map 574 * between an example set of arguments and a string that describes the 575 * behavior of the tool when invoked with that set of arguments. 576 * 577 * @return A set of information that may be used to generate example usage 578 * information. It may be {@code null} or empty if no example usage 579 * information is available. 580 */ 581 @Override() 582 public LinkedHashMap<String[],String> getExampleUsages() 583 { 584 final LinkedHashMap<String[],String> exampleMap = 585 new LinkedHashMap<String[],String>(1); 586 587 final String[] args = 588 { 589 "--hostname", "server.example.com", 590 "--port", "389", 591 "--bindDN", "uid=john.doe,ou=People,dc=example,dc=com", 592 "--bindPassword", "password", 593 "--baseDN", "dc=example,dc=com", 594 "--attribute", "uid", 595 "--simplePageSize", "100" 596 }; 597 exampleMap.put(args, 598 "Identify any values of the uid attribute that are not unique " + 599 "across all entries below dc=example,dc=com."); 600 601 return exampleMap; 602 } 603 604 605 606 /** 607 * Indicates that the provided search result entry has been returned by the 608 * server and may be processed by this search result listener. 609 * 610 * @param searchEntry The search result entry that has been returned by the 611 * server. 612 */ 613 public void searchEntryReturned(final SearchResultEntry searchEntry) 614 { 615 try 616 { 617 // If we need to check for conflicts in the same entry, then do that 618 // first. 619 if (! allowConflictsInSameEntry) 620 { 621 boolean conflictFound = false; 622 for (int i=0; i < attributes.length; i++) 623 { 624 final List<Attribute> l1 = 625 searchEntry.getAttributesWithOptions(attributes[i], null); 626 if (l1 != null) 627 { 628 for (int j=i+1; j < attributes.length; j++) 629 { 630 final List<Attribute> l2 = 631 searchEntry.getAttributesWithOptions(attributes[j], null); 632 if (l2 != null) 633 { 634 for (final Attribute a1 : l1) 635 { 636 for (final String value : a1.getValues()) 637 { 638 for (final Attribute a2 : l2) 639 { 640 if (a2.hasValue(value)) 641 { 642 err("Value '", value, "' in attribute ", a1.getName(), 643 " of entry '", searchEntry.getDN(), 644 " is also present in attribute ", a2.getName(), 645 " of the same entry."); 646 conflictFound = true; 647 conflictCounts.get(attributes[i]).incrementAndGet(); 648 } 649 } 650 } 651 } 652 } 653 } 654 } 655 } 656 657 if (conflictFound) 658 { 659 return; 660 } 661 } 662 663 664 // Get the unique attributes from the entry and search for conflicts with 665 // each value in other entries. Although we could theoretically do this 666 // with fewer searches, most uses of unique attributes don't have multiple 667 // values, so the following code (which is much simpler) is just as 668 // efficient in the common case. 669 for (final String attrName : attributes) 670 { 671 final List<Attribute> attrList = 672 searchEntry.getAttributesWithOptions(attrName, null); 673 for (final Attribute a : attrList) 674 { 675 for (final String value : a.getValues()) 676 { 677 final Filter filter; 678 if (uniqueAcrossAttributes) 679 { 680 final Filter[] orComps = new Filter[attributes.length]; 681 for (int i=0; i < attributes.length; i++) 682 { 683 orComps[i] = Filter.createEqualityFilter(attributes[i], value); 684 } 685 filter = Filter.createORFilter(orComps); 686 } 687 else 688 { 689 filter = Filter.createEqualityFilter(attrName, value); 690 } 691 692baseDNLoop: 693 for (final String baseDN : baseDNs) 694 { 695 SearchResult searchResult; 696 try 697 { 698 searchResult = findConflictsConnection.search(baseDN, 699 SearchScope.SUB, DereferencePolicy.NEVER, 2, 0, false, 700 filter, "1.1"); 701 } 702 catch (final LDAPSearchException lse) 703 { 704 Debug.debugException(lse); 705 searchResult = lse.getSearchResult(); 706 } 707 708 for (final SearchResultEntry e : searchResult.getSearchEntries()) 709 { 710 try 711 { 712 if (DN.equals(searchEntry.getDN(), e.getDN())) 713 { 714 continue; 715 } 716 } 717 catch (final Exception ex) 718 { 719 Debug.debugException(ex); 720 } 721 722 err("Value '", value, "' in attribute ", a.getName(), 723 " of entry '" + searchEntry.getDN(), 724 "' is also present in entry '", e.getDN(), "'."); 725 conflictCounts.get(attrName).incrementAndGet(); 726 break baseDNLoop; 727 } 728 729 if (searchResult.getResultCode() != ResultCode.SUCCESS) 730 { 731 err("An error occurred while attempting to search for " + 732 "conflicts with " + a.getName() + " value '" + value + 733 "' (as found in entry '" + searchEntry.getDN() + 734 "') below '" + baseDN + "': " + 735 searchResult.getDiagnosticMessage()); 736 conflictCounts.get(attrName).incrementAndGet(); 737 break baseDNLoop; 738 } 739 } 740 } 741 } 742 } 743 } 744 finally 745 { 746 final long count = entriesExamined.incrementAndGet(); 747 if ((count % 1000L) == 0L) 748 { 749 out(count, " entries examined"); 750 } 751 } 752 } 753 754 755 756 /** 757 * Indicates that the provided search result reference has been returned by 758 * the server and may be processed by this search result listener. 759 * 760 * @param searchReference The search result reference that has been returned 761 * by the server. 762 */ 763 public void searchReferenceReturned( 764 final SearchResultReference searchReference) 765 { 766 // No implementation is required. This tool will not follow referrals. 767 } 768}