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