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.Collections; 042import java.util.LinkedHashMap; 043import java.util.List; 044import java.util.Map; 045import java.util.TreeMap; 046import java.util.concurrent.atomic.AtomicLong; 047 048import com.unboundid.asn1.ASN1OctetString; 049import com.unboundid.ldap.sdk.Attribute; 050import com.unboundid.ldap.sdk.DN; 051import com.unboundid.ldap.sdk.Filter; 052import com.unboundid.ldap.sdk.LDAPConnectionOptions; 053import com.unboundid.ldap.sdk.LDAPConnectionPool; 054import com.unboundid.ldap.sdk.LDAPException; 055import com.unboundid.ldap.sdk.LDAPSearchException; 056import com.unboundid.ldap.sdk.Modification; 057import com.unboundid.ldap.sdk.ModificationType; 058import com.unboundid.ldap.sdk.ResultCode; 059import com.unboundid.ldap.sdk.SearchRequest; 060import com.unboundid.ldap.sdk.SearchResult; 061import com.unboundid.ldap.sdk.SearchResultEntry; 062import com.unboundid.ldap.sdk.SearchResultReference; 063import com.unboundid.ldap.sdk.SearchResultListener; 064import com.unboundid.ldap.sdk.SearchScope; 065import com.unboundid.ldap.sdk.Version; 066import com.unboundid.ldap.sdk.controls.SimplePagedResultsControl; 067import com.unboundid.ldif.LDIFModifyChangeRecord; 068import com.unboundid.ldif.LDIFWriter; 069import com.unboundid.util.Debug; 070import com.unboundid.util.LDAPCommandLineTool; 071import com.unboundid.util.NotNull; 072import com.unboundid.util.Nullable; 073import com.unboundid.util.StaticUtils; 074import com.unboundid.util.ThreadSafety; 075import com.unboundid.util.ThreadSafetyLevel; 076import com.unboundid.util.args.ArgumentException; 077import com.unboundid.util.args.ArgumentParser; 078import com.unboundid.util.args.DNArgument; 079import com.unboundid.util.args.FileArgument; 080import com.unboundid.util.args.IntegerArgument; 081import com.unboundid.util.args.StringArgument; 082 083 084 085/** 086 * This class provides a tool that may be used to identify references to entries 087 * that do not exist. This tool can be useful for verifying existing data in 088 * directory servers that provide support for referential integrity. 089 * <BR><BR> 090 * All of the necessary information is provided using command line arguments. 091 * Supported arguments include those allowed by the {@link LDAPCommandLineTool} 092 * class, as well as the following additional arguments: 093 * <UL> 094 * <LI>"-b {baseDN}" or "--baseDN {baseDN}" -- specifies the base DN to use 095 * for the searches. At least one base DN must be provided.</LI> 096 * <LI>"-A {attribute}" or "--attribute {attribute}" -- specifies an attribute 097 * that is expected to contain references to other entries. This 098 * attribute should be indexed for equality searches, and its values 099 * should be DNs. At least one attribute must be provided.</LI> 100 * <LI>"-z {size}" or "--simplePageSize {size}" -- indicates that the search 101 * to find entries with references to other entries should use the simple 102 * paged results control to iterate across entries in fixed-size pages 103 * rather than trying to use a single search to identify all entries that 104 * reference other entries.</LI> 105 * </UL> 106 */ 107@ThreadSafety(level=ThreadSafetyLevel.NOT_THREADSAFE) 108public final class IdentifyReferencesToMissingEntries 109 extends LDAPCommandLineTool 110 implements SearchResultListener 111{ 112 /** 113 * The serial version UID for this serializable class. 114 */ 115 private static final long serialVersionUID = 1981894839719501258L; 116 117 118 119 // The number of entries examined so far. 120 @NotNull private final AtomicLong entriesExamined; 121 122 // The argument used to specify the base DNs to use for searches. 123 @Nullable private DNArgument baseDNArgument; 124 125 // The argument used to specify the path to an output LDIF file. 126 @Nullable private FileArgument outputLDIFArgument; 127 128 // The argument used to specify the search page size. 129 @Nullable private IntegerArgument pageSizeArgument; 130 131 // The connection to use for retrieving referenced entries. 132 @Nullable private LDAPConnectionPool getReferencedEntriesPool; 133 134 // An LDIF writer that may be used to write LDIF changes to remove references 135 // to missing entries. 136 @Nullable private LDIFWriter outputLDIFWriter; 137 138 // A map with counts of missing references by attribute type. 139 @NotNull private final Map<String,AtomicLong> missingReferenceCounts; 140 141 // The names of the attributes for which to find missing references. 142 @Nullable private String[] attributes; 143 144 // The argument used to specify the attributes for which to find missing 145 // references. 146 @Nullable private StringArgument attributeArgument; 147 148 149 150 /** 151 * Parse the provided command line arguments and perform the appropriate 152 * processing. 153 * 154 * @param args The command line arguments provided to this program. 155 */ 156 public static void main(@NotNull final String... args) 157 { 158 final ResultCode resultCode = main(args, System.out, System.err); 159 if (resultCode != ResultCode.SUCCESS) 160 { 161 System.exit(resultCode.intValue()); 162 } 163 } 164 165 166 167 /** 168 * Parse the provided command line arguments and perform the appropriate 169 * processing. 170 * 171 * @param args The command line arguments provided to this program. 172 * @param outStream The output stream to which standard out should be 173 * written. It may be {@code null} if output should be 174 * suppressed. 175 * @param errStream The output stream to which standard error should be 176 * written. It may be {@code null} if error messages 177 * should be suppressed. 178 * 179 * @return A result code indicating whether the processing was successful. 180 */ 181 @NotNull() 182 public static ResultCode main(@NotNull final String[] args, 183 @Nullable final OutputStream outStream, 184 @Nullable final OutputStream errStream) 185 { 186 final IdentifyReferencesToMissingEntries tool = 187 new IdentifyReferencesToMissingEntries(outStream, errStream); 188 return tool.runTool(args); 189 } 190 191 192 193 /** 194 * Creates a new instance of this tool. 195 * 196 * @param outStream The output stream to which standard out should be 197 * written. It may be {@code null} if output should be 198 * suppressed. 199 * @param errStream The output stream to which standard error should be 200 * written. It may be {@code null} if error messages 201 * should be suppressed. 202 */ 203 public IdentifyReferencesToMissingEntries( 204 @Nullable final OutputStream outStream, 205 @Nullable final OutputStream errStream) 206 { 207 super(outStream, errStream); 208 209 baseDNArgument = null; 210 outputLDIFArgument = null; 211 pageSizeArgument = null; 212 attributeArgument = null; 213 getReferencedEntriesPool = null; 214 215 entriesExamined = new AtomicLong(0L); 216 missingReferenceCounts = new TreeMap<>(); 217 } 218 219 220 221 /** 222 * Retrieves the name of this tool. It should be the name of the command used 223 * to invoke this tool. 224 * 225 * @return The name for this tool. 226 */ 227 @Override() 228 @NotNull() 229 public String getToolName() 230 { 231 return "identify-references-to-missing-entries"; 232 } 233 234 235 236 /** 237 * Retrieves a human-readable description for this tool. 238 * 239 * @return A human-readable description for this tool. 240 */ 241 @Override() 242 @NotNull() 243 public String getToolDescription() 244 { 245 return "This tool may be used to identify entries containing one or more " + 246 "attributes which reference entries that do not exist. This may " + 247 "require the ability to perform unindexed searches and/or the " + 248 "ability to use the simple paged results control."; 249 } 250 251 252 253 /** 254 * Retrieves a version string for this tool, if available. 255 * 256 * @return A version string for this tool, or {@code null} if none is 257 * available. 258 */ 259 @Override() 260 @NotNull() 261 public String getToolVersion() 262 { 263 return Version.NUMERIC_VERSION_STRING; 264 } 265 266 267 268 /** 269 * Indicates whether this tool should provide support for an interactive mode, 270 * in which the tool offers a mode in which the arguments can be provided in 271 * a text-driven menu rather than requiring them to be given on the command 272 * line. If interactive mode is supported, it may be invoked using the 273 * "--interactive" argument. Alternately, if interactive mode is supported 274 * and {@link #defaultsToInteractiveMode()} returns {@code true}, then 275 * interactive mode may be invoked by simply launching the tool without any 276 * arguments. 277 * 278 * @return {@code true} if this tool supports interactive mode, or 279 * {@code false} if not. 280 */ 281 @Override() 282 public boolean supportsInteractiveMode() 283 { 284 return true; 285 } 286 287 288 289 /** 290 * Indicates whether this tool defaults to launching in interactive mode if 291 * the tool is invoked without any command-line arguments. This will only be 292 * used if {@link #supportsInteractiveMode()} returns {@code true}. 293 * 294 * @return {@code true} if this tool defaults to using interactive mode if 295 * launched without any command-line arguments, or {@code false} if 296 * not. 297 */ 298 @Override() 299 public boolean defaultsToInteractiveMode() 300 { 301 return true; 302 } 303 304 305 306 /** 307 * Indicates whether this tool should provide arguments for redirecting output 308 * to a file. If this method returns {@code true}, then the tool will offer 309 * an "--outputFile" argument that will specify the path to a file to which 310 * all standard output and standard error content will be written, and it will 311 * also offer a "--teeToStandardOut" argument that can only be used if the 312 * "--outputFile" argument is present and will cause all output to be written 313 * to both the specified output file and to standard output. 314 * 315 * @return {@code true} if this tool should provide arguments for redirecting 316 * output to a file, or {@code false} if not. 317 */ 318 @Override() 319 protected boolean supportsOutputFile() 320 { 321 return true; 322 } 323 324 325 326 /** 327 * Indicates whether this tool should default to interactively prompting for 328 * the bind password if a password is required but no argument was provided 329 * to indicate how to get the password. 330 * 331 * @return {@code true} if this tool should default to interactively 332 * prompting for the bind password, or {@code false} if not. 333 */ 334 @Override() 335 protected boolean defaultToPromptForBindPassword() 336 { 337 return true; 338 } 339 340 341 342 /** 343 * Indicates whether this tool supports the use of a properties file for 344 * specifying default values for arguments that aren't specified on the 345 * command line. 346 * 347 * @return {@code true} if this tool supports the use of a properties file 348 * for specifying default values for arguments that aren't specified 349 * on the command line, or {@code false} if not. 350 */ 351 @Override() 352 public boolean supportsPropertiesFile() 353 { 354 return true; 355 } 356 357 358 359 /** 360 * Indicates whether this tool supports the ability to generate a debug log 361 * file. If this method returns {@code true}, then the tool will expose 362 * additional arguments that can control debug logging. 363 * 364 * @return {@code true} if this tool supports the ability to generate a debug 365 * log file, or {@code false} if not. 366 */ 367 @Override() 368 protected boolean supportsDebugLogging() 369 { 370 return true; 371 } 372 373 374 375 /** 376 * Indicates whether the LDAP-specific arguments should include alternate 377 * versions of all long identifiers that consist of multiple words so that 378 * they are available in both camelCase and dash-separated versions. 379 * 380 * @return {@code true} if this tool should provide multiple versions of 381 * long identifiers for LDAP-specific arguments, or {@code false} if 382 * not. 383 */ 384 @Override() 385 protected boolean includeAlternateLongIdentifiers() 386 { 387 return true; 388 } 389 390 391 392 /** 393 * Indicates whether this tool should provide a command-line argument that 394 * allows for low-level SSL debugging. If this returns {@code true}, then an 395 * "--enableSSLDebugging}" argument will be added that sets the 396 * "javax.net.debug" system property to "all" before attempting any 397 * communication. 398 * 399 * @return {@code true} if this tool should offer an "--enableSSLDebugging" 400 * argument, or {@code false} if not. 401 */ 402 @Override() 403 protected boolean supportsSSLDebugging() 404 { 405 return true; 406 } 407 408 409 410 /** 411 * Adds the arguments needed by this command-line tool to the provided 412 * argument parser which are not related to connecting or authenticating to 413 * the directory server. 414 * 415 * @param parser The argument parser to which the arguments should be added. 416 * 417 * @throws ArgumentException If a problem occurs while adding the arguments. 418 */ 419 @Override() 420 public void addNonLDAPArguments(@NotNull final ArgumentParser parser) 421 throws ArgumentException 422 { 423 String description = "The search base DN(s) to use to find entries with " + 424 "references to other entries. At least one base DN must be " + 425 "specified."; 426 baseDNArgument = new DNArgument('b', "baseDN", true, 0, "{dn}", 427 description); 428 baseDNArgument.addLongIdentifier("base-dn", true); 429 parser.addArgument(baseDNArgument); 430 431 description = "The attribute(s) for which to find missing references. " + 432 "At least one attribute must be specified, and each attribute " + 433 "must be indexed for equality searches and have values which are DNs."; 434 attributeArgument = new StringArgument('A', "attribute", true, 0, "{attr}", 435 description); 436 parser.addArgument(attributeArgument); 437 438 description = "The maximum number of entries to retrieve at a time when " + 439 "attempting to find entries with references to other entries. This " + 440 "requires that the authenticated user have permission to use the " + 441 "simple paged results control, but it can avoid problems with the " + 442 "server sending entries too quickly for the client to handle. By " + 443 "default, the simple paged results control will not be used."; 444 pageSizeArgument = 445 new IntegerArgument('z', "simplePageSize", false, 1, "{num}", 446 description, 1, Integer.MAX_VALUE); 447 pageSizeArgument.addLongIdentifier("simple-page-size", true); 448 parser.addArgument(pageSizeArgument); 449 450 description = "The path to a file that should be written with the LDIF " + 451 "representation of any changes that may be needed to remove " + 452 "references to missing entries. If this is omitted, then " + 453 "information about the missing entries will only be written to " + 454 "standard output in a human-readable form."; 455 outputLDIFArgument = new FileArgument('l', "outputLDIF", false, 1, 456 "{path}", description, false, true, true, false); 457 outputLDIFArgument.addLongIdentifier("output-ldif", true); 458 parser.addArgument(outputLDIFArgument); 459 } 460 461 462 463 /** 464 * Retrieves the connection options that should be used for connections that 465 * are created with this command line tool. Subclasses may override this 466 * method to use a custom set of connection options. 467 * 468 * @return The connection options that should be used for connections that 469 * are created with this command line tool. 470 */ 471 @Override() 472 @NotNull() 473 public LDAPConnectionOptions getConnectionOptions() 474 { 475 final LDAPConnectionOptions options = new LDAPConnectionOptions(); 476 477 options.setUseSynchronousMode(true); 478 options.setResponseTimeoutMillis(0L); 479 480 return options; 481 } 482 483 484 485 /** 486 * Performs the core set of processing for this tool. 487 * 488 * @return A result code that indicates whether the processing completed 489 * successfully. 490 */ 491 @Override() 492 @NotNull() 493 public ResultCode doToolProcessing() 494 { 495 // Establish a connection to the target directory server to use for 496 // finding references to entries. 497 final LDAPConnectionPool findReferencesPool; 498 try 499 { 500 findReferencesPool = getConnectionPool(1, 1); 501 findReferencesPool.setRetryFailedOperationsDueToInvalidConnections(true); 502 } 503 catch (final LDAPException le) 504 { 505 Debug.debugException(le); 506 err("Unable to establish a connection to the directory server: ", 507 StaticUtils.getExceptionMessage(le)); 508 return le.getResultCode(); 509 } 510 511 512 try 513 { 514 // Establish a second connection to use for retrieving referenced entries. 515 try 516 { 517 getReferencedEntriesPool = getConnectionPool(1,1); 518 getReferencedEntriesPool. 519 setRetryFailedOperationsDueToInvalidConnections(true); 520 } 521 catch (final LDAPException le) 522 { 523 Debug.debugException(le); 524 err("Unable to establish a connection to the directory server: ", 525 StaticUtils.getExceptionMessage(le)); 526 return le.getResultCode(); 527 } 528 529 530 // If we should write an LDIF file with the identified missing entries, 531 // then create it now. 532 if (outputLDIFArgument.isPresent()) 533 { 534 try 535 { 536 outputLDIFWriter = new LDIFWriter(outputLDIFArgument.getValue()); 537 } 538 catch (final Exception e) 539 { 540 Debug.debugException(e); 541 err("Unale to open LDIF file '" + 542 outputLDIFArgument.getValue().getAbsolutePath() + 543 " for writing: " + StaticUtils.getExceptionMessage(e)); 544 return ResultCode.LOCAL_ERROR; 545 } 546 } 547 548 549 // Get the set of attributes for which to find missing references. 550 final List<String> attrList = attributeArgument.getValues(); 551 attributes = new String[attrList.size()]; 552 attrList.toArray(attributes); 553 554 555 // Construct a search filter that will be used to find all entries with 556 // references to other entries. 557 final Filter filter; 558 if (attributes.length == 1) 559 { 560 filter = Filter.createPresenceFilter(attributes[0]); 561 missingReferenceCounts.put(attributes[0], new AtomicLong(0L)); 562 } 563 else 564 { 565 final Filter[] orComps = new Filter[attributes.length]; 566 for (int i=0; i < attributes.length; i++) 567 { 568 orComps[i] = Filter.createPresenceFilter(attributes[i]); 569 missingReferenceCounts.put(attributes[i], new AtomicLong(0L)); 570 } 571 filter = Filter.createORFilter(orComps); 572 } 573 574 575 // Iterate across all of the search base DNs and perform searches to find 576 // missing references. 577 for (final DN baseDN : baseDNArgument.getValues()) 578 { 579 ASN1OctetString cookie = null; 580 do 581 { 582 final SearchRequest searchRequest = new SearchRequest(this, 583 baseDN.toString(), SearchScope.SUB, filter, attributes); 584 if (pageSizeArgument.isPresent()) 585 { 586 searchRequest.addControl(new SimplePagedResultsControl( 587 pageSizeArgument.getValue(), cookie, false)); 588 } 589 590 SearchResult searchResult; 591 try 592 { 593 searchResult = findReferencesPool.search(searchRequest); 594 } 595 catch (final LDAPSearchException lse) 596 { 597 Debug.debugException(lse); 598 try 599 { 600 searchResult = findReferencesPool.search(searchRequest); 601 } 602 catch (final LDAPSearchException lse2) 603 { 604 Debug.debugException(lse2); 605 searchResult = lse2.getSearchResult(); 606 } 607 } 608 609 if (searchResult.getResultCode() != ResultCode.SUCCESS) 610 { 611 err("An error occurred while attempting to search for missing " + 612 "references to entries below " + baseDN + ": " + 613 searchResult.getDiagnosticMessage()); 614 return searchResult.getResultCode(); 615 } 616 617 final SimplePagedResultsControl pagedResultsResponse; 618 try 619 { 620 pagedResultsResponse = SimplePagedResultsControl.get(searchResult); 621 } 622 catch (final LDAPException le) 623 { 624 Debug.debugException(le); 625 err("An error occurred while attempting to decode a simple " + 626 "paged results response control in the response to a " + 627 "search for entries below " + baseDN + ": " + 628 StaticUtils.getExceptionMessage(le)); 629 return le.getResultCode(); 630 } 631 632 if (pagedResultsResponse != null) 633 { 634 if (pagedResultsResponse.moreResultsToReturn()) 635 { 636 cookie = pagedResultsResponse.getCookie(); 637 } 638 else 639 { 640 cookie = null; 641 } 642 } 643 } 644 while (cookie != null); 645 } 646 647 648 // See if there were any missing references found. 649 boolean missingReferenceFound = false; 650 for (final Map.Entry<String,AtomicLong> e : 651 missingReferenceCounts.entrySet()) 652 { 653 final long numMissing = e.getValue().get(); 654 if (numMissing > 0L) 655 { 656 if (! missingReferenceFound) 657 { 658 err(); 659 missingReferenceFound = true; 660 } 661 662 err("Found " + numMissing + ' ' + e.getKey() + 663 " references to entries that do not exist."); 664 } 665 } 666 667 if (missingReferenceFound) 668 { 669 return ResultCode.CONSTRAINT_VIOLATION; 670 } 671 else 672 { 673 out("No references were found to entries that do not exist."); 674 return ResultCode.SUCCESS; 675 } 676 } 677 finally 678 { 679 findReferencesPool.close(); 680 681 if (getReferencedEntriesPool != null) 682 { 683 getReferencedEntriesPool.close(); 684 } 685 686 if (outputLDIFWriter != null) 687 { 688 try 689 { 690 outputLDIFWriter.close(); 691 } 692 catch (final Exception e) 693 { 694 err(); 695 err("An error occurred while closing the output LDIF file:" + 696 StaticUtils.getExceptionMessage(e)); 697 } 698 } 699 } 700 } 701 702 703 704 /** 705 * Retrieves a map that correlates the number of missing references found by 706 * attribute type. 707 * 708 * @return A map that correlates the number of missing references found by 709 * attribute type. 710 */ 711 @NotNull() 712 public Map<String,AtomicLong> getMissingReferenceCounts() 713 { 714 return Collections.unmodifiableMap(missingReferenceCounts); 715 } 716 717 718 719 /** 720 * Retrieves a set of information that may be used to generate example usage 721 * information. Each element in the returned map should consist of a map 722 * between an example set of arguments and a string that describes the 723 * behavior of the tool when invoked with that set of arguments. 724 * 725 * @return A set of information that may be used to generate example usage 726 * information. It may be {@code null} or empty if no example usage 727 * information is available. 728 */ 729 @Override() 730 @NotNull() 731 public LinkedHashMap<String[],String> getExampleUsages() 732 { 733 final LinkedHashMap<String[],String> exampleMap = 734 new LinkedHashMap<>(StaticUtils.computeMapCapacity(1)); 735 736 final String[] args = 737 { 738 "--hostname", "server.example.com", 739 "--port", "389", 740 "--bindDN", "uid=john.doe,ou=People,dc=example,dc=com", 741 "--bindPassword", "password", 742 "--baseDN", "dc=example,dc=com", 743 "--attribute", "member", 744 "--attribute", "uniqueMember", 745 "--simplePageSize", "100" 746 }; 747 exampleMap.put(args, 748 "Identify all entries below dc=example,dc=com in which either the " + 749 "member or uniqueMember attribute references an entry that " + 750 "does not exist."); 751 752 return exampleMap; 753 } 754 755 756 757 /** 758 * Indicates that the provided search result entry has been returned by the 759 * server and may be processed by this search result listener. 760 * 761 * @param searchEntry The search result entry that has been returned by the 762 * server. 763 */ 764 @Override() 765 public void searchEntryReturned(@NotNull final SearchResultEntry searchEntry) 766 { 767 try 768 { 769 // Find attributes which references to entries that do not exist. 770 for (final String attr : attributes) 771 { 772 final List<Attribute> attrList = 773 searchEntry.getAttributesWithOptions(attr, null); 774 for (final Attribute a : attrList) 775 { 776 for (final String value : a.getValues()) 777 { 778 try 779 { 780 final SearchResultEntry e = 781 getReferencedEntriesPool.getEntry(value, "1.1"); 782 if (e == null) 783 { 784 err("Entry '", searchEntry.getDN(), "' includes attribute ", 785 a.getName(), " that references entry '", value, 786 "' which does not exist."); 787 missingReferenceCounts.get(attr).incrementAndGet(); 788 789 if (outputLDIFWriter != null) 790 { 791 final LDIFModifyChangeRecord changeRecord = 792 new LDIFModifyChangeRecord(searchEntry.getDN(), 793 new Modification(ModificationType.DELETE, 794 a.getName(), value)); 795 try 796 { 797 outputLDIFWriter.writeChangeRecord(changeRecord); 798 } 799 catch (final Exception ex) 800 { 801 Debug.debugException(ex); 802 err("An error occurred while attempting to write an LDIF " + 803 "change record to address the above issue: " + 804 StaticUtils.getExceptionMessage(ex)); 805 } 806 } 807 } 808 } 809 catch (final LDAPException le) 810 { 811 Debug.debugException(le); 812 err("An error occurred while attempting to determine whether " + 813 "entry '" + value + "' referenced in attribute " + 814 a.getName() + " of entry '" + searchEntry.getDN() + 815 "' exists: " + StaticUtils.getExceptionMessage(le)); 816 missingReferenceCounts.get(attr).incrementAndGet(); 817 } 818 } 819 } 820 } 821 } 822 finally 823 { 824 final long count = entriesExamined.incrementAndGet(); 825 if ((count % 1000L) == 0L) 826 { 827 out(count, " entries examined"); 828 } 829 } 830 } 831 832 833 834 /** 835 * Indicates that the provided search result reference has been returned by 836 * the server and may be processed by this search result listener. 837 * 838 * @param searchReference The search result reference that has been returned 839 * by the server. 840 */ 841 @Override() 842 public void searchReferenceReturned( 843 @NotNull final SearchResultReference searchReference) 844 { 845 // No implementation is required. This tool will not follow referrals. 846 } 847}