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}