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 the LDAP-specific arguments should include alternate
361   * versions of all long identifiers that consist of multiple words so that
362   * they are available in both camelCase and dash-separated versions.
363   *
364   * @return  {@code true} if this tool should provide multiple versions of
365   *          long identifiers for LDAP-specific arguments, or {@code false} if
366   *          not.
367   */
368  @Override()
369  protected boolean includeAlternateLongIdentifiers()
370  {
371    return true;
372  }
373
374
375
376  /**
377   * Indicates whether this tool should provide a command-line argument that
378   * allows for low-level SSL debugging.  If this returns {@code true}, then an
379   * "--enableSSLDebugging}" argument will be added that sets the
380   * "javax.net.debug" system property to "all" before attempting any
381   * communication.
382   *
383   * @return  {@code true} if this tool should offer an "--enableSSLDebugging"
384   *          argument, or {@code false} if not.
385   */
386  @Override()
387  protected boolean supportsSSLDebugging()
388  {
389    return true;
390  }
391
392
393
394  /**
395   * Adds the arguments needed by this command-line tool to the provided
396   * argument parser which are not related to connecting or authenticating to
397   * the directory server.
398   *
399   * @param  parser  The argument parser to which the arguments should be added.
400   *
401   * @throws  ArgumentException  If a problem occurs while adding the arguments.
402   */
403  @Override()
404  public void addNonLDAPArguments(@NotNull final ArgumentParser parser)
405         throws ArgumentException
406  {
407    String description = "The search base DN(s) to use to find entries with " +
408         "references to other entries.  At least one base DN must be " +
409         "specified.";
410    baseDNArgument = new DNArgument('b', "baseDN", true, 0, "{dn}",
411         description);
412    baseDNArgument.addLongIdentifier("base-dn", true);
413    parser.addArgument(baseDNArgument);
414
415    description = "The attribute(s) for which to find missing references.  " +
416         "At least one attribute must be specified, and each attribute " +
417         "must be indexed for equality searches and have values which are DNs.";
418    attributeArgument = new StringArgument('A', "attribute", true, 0, "{attr}",
419         description);
420    parser.addArgument(attributeArgument);
421
422    description = "The maximum number of entries to retrieve at a time when " +
423         "attempting to find entries with references to other entries.  This " +
424         "requires that the authenticated user have permission to use the " +
425         "simple paged results control, but it can avoid problems with the " +
426         "server sending entries too quickly for the client to handle.  By " +
427         "default, the simple paged results control will not be used.";
428    pageSizeArgument =
429         new IntegerArgument('z', "simplePageSize", false, 1, "{num}",
430              description, 1, Integer.MAX_VALUE);
431    pageSizeArgument.addLongIdentifier("simple-page-size", true);
432    parser.addArgument(pageSizeArgument);
433
434    description = "The path to a file that should be written with the LDIF " +
435         "representation of any changes that may be needed to remove " +
436         "references to missing entries.  If this is omitted, then " +
437         "information about the missing entries will only be written to " +
438         "standard output in a human-readable form.";
439    outputLDIFArgument = new FileArgument('l', "outputLDIF", false, 1,
440         "{path}", description, false, true, true, false);
441    outputLDIFArgument.addLongIdentifier("output-ldif", true);
442    parser.addArgument(outputLDIFArgument);
443  }
444
445
446
447  /**
448   * Retrieves the connection options that should be used for connections that
449   * are created with this command line tool.  Subclasses may override this
450   * method to use a custom set of connection options.
451   *
452   * @return  The connection options that should be used for connections that
453   *          are created with this command line tool.
454   */
455  @Override()
456  @NotNull()
457  public LDAPConnectionOptions getConnectionOptions()
458  {
459    final LDAPConnectionOptions options = new LDAPConnectionOptions();
460
461    options.setUseSynchronousMode(true);
462    options.setResponseTimeoutMillis(0L);
463
464    return options;
465  }
466
467
468
469  /**
470   * Performs the core set of processing for this tool.
471   *
472   * @return  A result code that indicates whether the processing completed
473   *          successfully.
474   */
475  @Override()
476  @NotNull()
477  public ResultCode doToolProcessing()
478  {
479    // Establish a connection to the target directory server to use for
480    // finding references to entries.
481    final LDAPConnectionPool findReferencesPool;
482    try
483    {
484      findReferencesPool = getConnectionPool(1, 1);
485      findReferencesPool.setRetryFailedOperationsDueToInvalidConnections(true);
486    }
487    catch (final LDAPException le)
488    {
489      Debug.debugException(le);
490      err("Unable to establish a connection to the directory server:  ",
491           StaticUtils.getExceptionMessage(le));
492      return le.getResultCode();
493    }
494
495
496    try
497    {
498      // Establish a second connection to use for retrieving referenced entries.
499      try
500      {
501        getReferencedEntriesPool = getConnectionPool(1,1);
502        getReferencedEntriesPool.
503             setRetryFailedOperationsDueToInvalidConnections(true);
504      }
505      catch (final LDAPException le)
506      {
507        Debug.debugException(le);
508        err("Unable to establish a connection to the directory server:  ",
509             StaticUtils.getExceptionMessage(le));
510        return le.getResultCode();
511      }
512
513
514      // If we should write an LDIF file with the identified missing entries,
515      // then create it now.
516      if (outputLDIFArgument.isPresent())
517      {
518        try
519        {
520          outputLDIFWriter = new LDIFWriter(outputLDIFArgument.getValue());
521        }
522        catch (final Exception e)
523        {
524          Debug.debugException(e);
525          err("Unale to open LDIF file '" +
526               outputLDIFArgument.getValue().getAbsolutePath() +
527               " for writing:  " + StaticUtils.getExceptionMessage(e));
528          return ResultCode.LOCAL_ERROR;
529        }
530      }
531
532
533      // Get the set of attributes for which to find missing references.
534      final List<String> attrList = attributeArgument.getValues();
535      attributes = new String[attrList.size()];
536      attrList.toArray(attributes);
537
538
539      // Construct a search filter that will be used to find all entries with
540      // references to other entries.
541      final Filter filter;
542      if (attributes.length == 1)
543      {
544        filter = Filter.createPresenceFilter(attributes[0]);
545        missingReferenceCounts.put(attributes[0], new AtomicLong(0L));
546      }
547      else
548      {
549        final Filter[] orComps = new Filter[attributes.length];
550        for (int i=0; i < attributes.length; i++)
551        {
552          orComps[i] = Filter.createPresenceFilter(attributes[i]);
553          missingReferenceCounts.put(attributes[i], new AtomicLong(0L));
554        }
555        filter = Filter.createORFilter(orComps);
556      }
557
558
559      // Iterate across all of the search base DNs and perform searches to find
560      // missing references.
561      for (final DN baseDN : baseDNArgument.getValues())
562      {
563        ASN1OctetString cookie = null;
564        do
565        {
566          final SearchRequest searchRequest = new SearchRequest(this,
567               baseDN.toString(), SearchScope.SUB, filter, attributes);
568          if (pageSizeArgument.isPresent())
569          {
570            searchRequest.addControl(new SimplePagedResultsControl(
571                 pageSizeArgument.getValue(), cookie, false));
572          }
573
574          SearchResult searchResult;
575          try
576          {
577            searchResult = findReferencesPool.search(searchRequest);
578          }
579          catch (final LDAPSearchException lse)
580          {
581            Debug.debugException(lse);
582            try
583            {
584              searchResult = findReferencesPool.search(searchRequest);
585            }
586            catch (final LDAPSearchException lse2)
587            {
588              Debug.debugException(lse2);
589              searchResult = lse2.getSearchResult();
590            }
591          }
592
593          if (searchResult.getResultCode() != ResultCode.SUCCESS)
594          {
595            err("An error occurred while attempting to search for missing " +
596                 "references to entries below " + baseDN + ":  " +
597                 searchResult.getDiagnosticMessage());
598            return searchResult.getResultCode();
599          }
600
601          final SimplePagedResultsControl pagedResultsResponse;
602          try
603          {
604            pagedResultsResponse = SimplePagedResultsControl.get(searchResult);
605          }
606          catch (final LDAPException le)
607          {
608            Debug.debugException(le);
609            err("An error occurred while attempting to decode a simple " +
610                 "paged results response control in the response to a " +
611                 "search for entries below " + baseDN + ":  " +
612                 StaticUtils.getExceptionMessage(le));
613            return le.getResultCode();
614          }
615
616          if (pagedResultsResponse != null)
617          {
618            if (pagedResultsResponse.moreResultsToReturn())
619            {
620              cookie = pagedResultsResponse.getCookie();
621            }
622            else
623            {
624              cookie = null;
625            }
626          }
627        }
628        while (cookie != null);
629      }
630
631
632      // See if there were any missing references found.
633      boolean missingReferenceFound = false;
634      for (final Map.Entry<String,AtomicLong> e :
635           missingReferenceCounts.entrySet())
636      {
637        final long numMissing = e.getValue().get();
638        if (numMissing > 0L)
639        {
640          if (! missingReferenceFound)
641          {
642            err();
643            missingReferenceFound = true;
644          }
645
646          err("Found " + numMissing + ' ' + e.getKey() +
647               " references to entries that do not exist.");
648        }
649      }
650
651      if (missingReferenceFound)
652      {
653        return ResultCode.CONSTRAINT_VIOLATION;
654      }
655      else
656      {
657        out("No references were found to entries that do not exist.");
658        return ResultCode.SUCCESS;
659      }
660    }
661    finally
662    {
663      findReferencesPool.close();
664
665      if (getReferencedEntriesPool != null)
666      {
667        getReferencedEntriesPool.close();
668      }
669
670      if (outputLDIFWriter != null)
671      {
672        try
673        {
674          outputLDIFWriter.close();
675        }
676        catch (final Exception e)
677        {
678          err();
679          err("An error occurred while closing the output LDIF file:"  +
680               StaticUtils.getExceptionMessage(e));
681        }
682      }
683    }
684  }
685
686
687
688  /**
689   * Retrieves a map that correlates the number of missing references found by
690   * attribute type.
691   *
692   * @return  A map that correlates the number of missing references found by
693   *          attribute type.
694   */
695  @NotNull()
696  public Map<String,AtomicLong> getMissingReferenceCounts()
697  {
698    return Collections.unmodifiableMap(missingReferenceCounts);
699  }
700
701
702
703  /**
704   * Retrieves a set of information that may be used to generate example usage
705   * information.  Each element in the returned map should consist of a map
706   * between an example set of arguments and a string that describes the
707   * behavior of the tool when invoked with that set of arguments.
708   *
709   * @return  A set of information that may be used to generate example usage
710   *          information.  It may be {@code null} or empty if no example usage
711   *          information is available.
712   */
713  @Override()
714  @NotNull()
715  public LinkedHashMap<String[],String> getExampleUsages()
716  {
717    final LinkedHashMap<String[],String> exampleMap =
718         new LinkedHashMap<>(StaticUtils.computeMapCapacity(1));
719
720    final String[] args =
721    {
722      "--hostname", "server.example.com",
723      "--port", "389",
724      "--bindDN", "uid=john.doe,ou=People,dc=example,dc=com",
725      "--bindPassword", "password",
726      "--baseDN", "dc=example,dc=com",
727      "--attribute", "member",
728      "--attribute", "uniqueMember",
729      "--simplePageSize", "100"
730    };
731    exampleMap.put(args,
732         "Identify all entries below dc=example,dc=com in which either the " +
733              "member or uniqueMember attribute references an entry that " +
734              "does not exist.");
735
736    return exampleMap;
737  }
738
739
740
741  /**
742   * Indicates that the provided search result entry has been returned by the
743   * server and may be processed by this search result listener.
744   *
745   * @param  searchEntry  The search result entry that has been returned by the
746   *                      server.
747   */
748  @Override()
749  public void searchEntryReturned(@NotNull final SearchResultEntry searchEntry)
750  {
751    try
752    {
753      // Find attributes which references to entries that do not exist.
754      for (final String attr : attributes)
755      {
756        final List<Attribute> attrList =
757             searchEntry.getAttributesWithOptions(attr, null);
758        for (final Attribute a : attrList)
759        {
760          for (final String value : a.getValues())
761          {
762            try
763            {
764              final SearchResultEntry e =
765                   getReferencedEntriesPool.getEntry(value, "1.1");
766              if (e == null)
767              {
768                err("Entry '", searchEntry.getDN(), "' includes attribute ",
769                     a.getName(), " that references entry '", value,
770                     "' which does not exist.");
771                missingReferenceCounts.get(attr).incrementAndGet();
772
773                if (outputLDIFWriter != null)
774                {
775                  final LDIFModifyChangeRecord changeRecord =
776                       new LDIFModifyChangeRecord(searchEntry.getDN(),
777                            new Modification(ModificationType.DELETE,
778                                 a.getName(), value));
779                  try
780                  {
781                    outputLDIFWriter.writeChangeRecord(changeRecord);
782                  }
783                  catch (final Exception ex)
784                  {
785                    Debug.debugException(ex);
786                    err("An error occurred while attempting to write an LDIF " +
787                         "change record to address the above issue:  " +
788                         StaticUtils.getExceptionMessage(ex));
789                  }
790                }
791              }
792            }
793            catch (final LDAPException le)
794            {
795              Debug.debugException(le);
796              err("An error occurred while attempting to determine whether " +
797                   "entry '" + value + "' referenced in attribute " +
798                   a.getName() + " of entry '" + searchEntry.getDN() +
799                   "' exists:  " + StaticUtils.getExceptionMessage(le));
800              missingReferenceCounts.get(attr).incrementAndGet();
801            }
802          }
803        }
804      }
805    }
806    finally
807    {
808      final long count = entriesExamined.incrementAndGet();
809      if ((count % 1000L) == 0L)
810      {
811        out(count, " entries examined");
812      }
813    }
814  }
815
816
817
818  /**
819   * Indicates that the provided search result reference has been returned by
820   * the server and may be processed by this search result listener.
821   *
822   * @param  searchReference  The search result reference that has been returned
823   *                          by the server.
824   */
825  @Override()
826  public void searchReferenceReturned(
827                   @NotNull final SearchResultReference searchReference)
828  {
829    // No implementation is required.  This tool will not follow referrals.
830  }
831}