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     */
021    package com.unboundid.ldap.sdk.examples;
022    
023    
024    
025    import java.io.OutputStream;
026    import java.util.Collections;
027    import java.util.LinkedHashMap;
028    import java.util.LinkedHashSet;
029    import java.util.List;
030    import java.util.Map;
031    import java.util.TreeMap;
032    import java.util.concurrent.atomic.AtomicLong;
033    
034    import com.unboundid.asn1.ASN1OctetString;
035    import com.unboundid.ldap.sdk.Attribute;
036    import com.unboundid.ldap.sdk.DereferencePolicy;
037    import com.unboundid.ldap.sdk.DN;
038    import com.unboundid.ldap.sdk.Filter;
039    import com.unboundid.ldap.sdk.LDAPConnection;
040    import com.unboundid.ldap.sdk.LDAPException;
041    import com.unboundid.ldap.sdk.LDAPSearchException;
042    import com.unboundid.ldap.sdk.ResultCode;
043    import com.unboundid.ldap.sdk.SearchRequest;
044    import com.unboundid.ldap.sdk.SearchResult;
045    import com.unboundid.ldap.sdk.SearchResultEntry;
046    import com.unboundid.ldap.sdk.SearchResultReference;
047    import com.unboundid.ldap.sdk.SearchResultListener;
048    import com.unboundid.ldap.sdk.SearchScope;
049    import com.unboundid.ldap.sdk.Version;
050    import com.unboundid.ldap.sdk.controls.SimplePagedResultsControl;
051    import com.unboundid.util.Debug;
052    import com.unboundid.util.LDAPCommandLineTool;
053    import com.unboundid.util.StaticUtils;
054    import com.unboundid.util.ThreadSafety;
055    import com.unboundid.util.ThreadSafetyLevel;
056    import com.unboundid.util.args.ArgumentException;
057    import com.unboundid.util.args.ArgumentParser;
058    import com.unboundid.util.args.DNArgument;
059    import com.unboundid.util.args.IntegerArgument;
060    import 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)
092    public 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    
692    baseDNLoop:
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    }