001    /*
002     * Copyright 2013-2016 UnboundID Corp.
003     * All Rights Reserved.
004     */
005    /*
006     * Copyright (C) 2013-2016 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.List;
029    import java.util.Map;
030    import java.util.TreeMap;
031    import java.util.concurrent.atomic.AtomicLong;
032    
033    import com.unboundid.asn1.ASN1OctetString;
034    import com.unboundid.ldap.sdk.Attribute;
035    import com.unboundid.ldap.sdk.DN;
036    import com.unboundid.ldap.sdk.Filter;
037    import com.unboundid.ldap.sdk.LDAPConnection;
038    import com.unboundid.ldap.sdk.LDAPException;
039    import com.unboundid.ldap.sdk.LDAPSearchException;
040    import com.unboundid.ldap.sdk.ResultCode;
041    import com.unboundid.ldap.sdk.SearchRequest;
042    import com.unboundid.ldap.sdk.SearchResult;
043    import com.unboundid.ldap.sdk.SearchResultEntry;
044    import com.unboundid.ldap.sdk.SearchResultReference;
045    import com.unboundid.ldap.sdk.SearchResultListener;
046    import com.unboundid.ldap.sdk.SearchScope;
047    import com.unboundid.ldap.sdk.Version;
048    import com.unboundid.ldap.sdk.controls.SimplePagedResultsControl;
049    import com.unboundid.util.Debug;
050    import com.unboundid.util.LDAPCommandLineTool;
051    import com.unboundid.util.StaticUtils;
052    import com.unboundid.util.ThreadSafety;
053    import com.unboundid.util.ThreadSafetyLevel;
054    import com.unboundid.util.args.ArgumentException;
055    import com.unboundid.util.args.ArgumentParser;
056    import com.unboundid.util.args.DNArgument;
057    import com.unboundid.util.args.IntegerArgument;
058    import com.unboundid.util.args.StringArgument;
059    
060    
061    
062    /**
063     * This class provides a tool that may be used to identify references to entries
064     * that do not exist.  This tool can be useful for verifying existing data in
065     * directory servers that provide support for referential integrity.
066     * <BR><BR>
067     * All of the necessary information is provided using command line arguments.
068     * Supported arguments include those allowed by the {@link LDAPCommandLineTool}
069     * class, as well as the following additional arguments:
070     * <UL>
071     *   <LI>"-b {baseDN}" or "--baseDN {baseDN}" -- specifies the base DN to use
072     *       for the searches.  At least one base DN must be provided.</LI>
073     *   <LI>"-A {attribute}" or "--attribute {attribute}" -- specifies an attribute
074     *       that is expected to contain references to other entries.  This
075     *       attribute should be indexed for equality searches, and its values
076     *       should be DNs.  At least one attribute must be provided.</LI>
077     *   <LI>"-z {size}" or "--simplePageSize {size}" -- indicates that the search
078     *       to find entries with references to other entries should use the simple
079     *       paged results control to iterate across entries in fixed-size pages
080     *       rather than trying to use a single search to identify all entries that
081     *       reference other entries.</LI>
082     * </UL>
083     */
084    @ThreadSafety(level=ThreadSafetyLevel.NOT_THREADSAFE)
085    public final class IdentifyReferencesToMissingEntries
086           extends LDAPCommandLineTool
087           implements SearchResultListener
088    {
089      /**
090       * The serial version UID for this serializable class.
091       */
092      private static final long serialVersionUID = 1981894839719501258L;
093    
094    
095    
096      // The number of entries examined so far.
097      private final AtomicLong entriesExamined;
098    
099      // The argument used to specify the base DNs to use for searches.
100      private DNArgument baseDNArgument;
101    
102      // The argument used to specify the search page size.
103      private IntegerArgument pageSizeArgument;
104    
105      // The connection to use for retrieving referenced entries.
106      private LDAPConnection getReferencedEntriesConnection;
107    
108      // A map with counts of missing references by attribute type.
109      private final Map<String,AtomicLong> missingReferenceCounts;
110    
111      // The names of the attributes for which to find missing references.
112      private String[] attributes;
113    
114      // The argument used to specify the attributes for which to find missing
115      // references.
116      private StringArgument attributeArgument;
117    
118    
119    
120      /**
121       * Parse the provided command line arguments and perform the appropriate
122       * processing.
123       *
124       * @param  args  The command line arguments provided to this program.
125       */
126      public static void main(final String... args)
127      {
128        final ResultCode resultCode = main(args, System.out, System.err);
129        if (resultCode != ResultCode.SUCCESS)
130        {
131          System.exit(resultCode.intValue());
132        }
133      }
134    
135    
136    
137      /**
138       * Parse the provided command line arguments and perform the appropriate
139       * processing.
140       *
141       * @param  args       The command line arguments provided to this program.
142       * @param  outStream  The output stream to which standard out should be
143       *                    written.  It may be {@code null} if output should be
144       *                    suppressed.
145       * @param  errStream  The output stream to which standard error should be
146       *                    written.  It may be {@code null} if error messages
147       *                    should be suppressed.
148       *
149       * @return A result code indicating whether the processing was successful.
150       */
151      public static ResultCode main(final String[] args,
152                                    final OutputStream outStream,
153                                    final OutputStream errStream)
154      {
155        final IdentifyReferencesToMissingEntries tool =
156             new IdentifyReferencesToMissingEntries(outStream, errStream);
157        return tool.runTool(args);
158      }
159    
160    
161    
162      /**
163       * Creates a new instance of this tool.
164       *
165       * @param  outStream  The output stream to which standard out should be
166       *                    written.  It may be {@code null} if output should be
167       *                    suppressed.
168       * @param  errStream  The output stream to which standard error should be
169       *                    written.  It may be {@code null} if error messages
170       *                    should be suppressed.
171       */
172      public IdentifyReferencesToMissingEntries(final OutputStream outStream,
173                                                final OutputStream errStream)
174      {
175        super(outStream, errStream);
176    
177        baseDNArgument = null;
178        pageSizeArgument = null;
179        attributeArgument = null;
180        getReferencedEntriesConnection = null;
181    
182        entriesExamined = new AtomicLong(0L);
183        missingReferenceCounts = new TreeMap<String, AtomicLong>();
184      }
185    
186    
187    
188      /**
189       * Retrieves the name of this tool.  It should be the name of the command used
190       * to invoke this tool.
191       *
192       * @return  The name for this tool.
193       */
194      @Override()
195      public String getToolName()
196      {
197        return "identify-references-to-missing-entries";
198      }
199    
200    
201    
202      /**
203       * Retrieves a human-readable description for this tool.
204       *
205       * @return  A human-readable description for this tool.
206       */
207      @Override()
208      public String getToolDescription()
209      {
210        return "This tool may be used to identify entries containing one or more " +
211             "attributes which reference entries that do not exist.  This may " +
212             "require the ability to perform unindexed searches and/or the " +
213             "ability to use the simple paged results control.";
214      }
215    
216    
217    
218      /**
219       * Retrieves a version string for this tool, if available.
220       *
221       * @return  A version string for this tool, or {@code null} if none is
222       *          available.
223       */
224      @Override()
225      public String getToolVersion()
226      {
227        return Version.NUMERIC_VERSION_STRING;
228      }
229    
230    
231    
232      /**
233       * Indicates whether this tool should provide support for an interactive mode,
234       * in which the tool offers a mode in which the arguments can be provided in
235       * a text-driven menu rather than requiring them to be given on the command
236       * line.  If interactive mode is supported, it may be invoked using the
237       * "--interactive" argument.  Alternately, if interactive mode is supported
238       * and {@link #defaultsToInteractiveMode()} returns {@code true}, then
239       * interactive mode may be invoked by simply launching the tool without any
240       * arguments.
241       *
242       * @return  {@code true} if this tool supports interactive mode, or
243       *          {@code false} if not.
244       */
245      @Override()
246      public boolean supportsInteractiveMode()
247      {
248        return true;
249      }
250    
251    
252    
253      /**
254       * Indicates whether this tool defaults to launching in interactive mode if
255       * the tool is invoked without any command-line arguments.  This will only be
256       * used if {@link #supportsInteractiveMode()} returns {@code true}.
257       *
258       * @return  {@code true} if this tool defaults to using interactive mode if
259       *          launched without any command-line arguments, or {@code false} if
260       *          not.
261       */
262      @Override()
263      public boolean defaultsToInteractiveMode()
264      {
265        return true;
266      }
267    
268    
269    
270      /**
271       * Indicates whether this tool supports the use of a properties file for
272       * specifying default values for arguments that aren't specified on the
273       * command line.
274       *
275       * @return  {@code true} if this tool supports the use of a properties file
276       *          for specifying default values for arguments that aren't specified
277       *          on the command line, or {@code false} if not.
278       */
279      @Override()
280      public boolean supportsPropertiesFile()
281      {
282        return true;
283      }
284    
285    
286    
287      /**
288       * Indicates whether the LDAP-specific arguments should include alternate
289       * versions of all long identifiers that consist of multiple words so that
290       * they are available in both camelCase and dash-separated versions.
291       *
292       * @return  {@code true} if this tool should provide multiple versions of
293       *          long identifiers for LDAP-specific arguments, or {@code false} if
294       *          not.
295       */
296      @Override()
297      protected boolean includeAlternateLongIdentifiers()
298      {
299        return true;
300      }
301    
302    
303    
304      /**
305       * Adds the arguments needed by this command-line tool to the provided
306       * argument parser which are not related to connecting or authenticating to
307       * the directory server.
308       *
309       * @param  parser  The argument parser to which the arguments should be added.
310       *
311       * @throws  ArgumentException  If a problem occurs while adding the arguments.
312       */
313      @Override()
314      public void addNonLDAPArguments(final ArgumentParser parser)
315             throws ArgumentException
316      {
317        String description = "The search base DN(s) to use to find entries with " +
318             "references to other entries.  At least one base DN must be " +
319             "specified.";
320        baseDNArgument = new DNArgument('b', "baseDN", true, 0, "{dn}",
321             description);
322        baseDNArgument.addLongIdentifier("base-dn");
323        parser.addArgument(baseDNArgument);
324    
325        description = "The attribute(s) for which to find missing references.  " +
326             "At least one attribute must be specified, and each attribute " +
327             "must be indexed for equality searches and have values which are DNs.";
328        attributeArgument = new StringArgument('A', "attribute", true, 0, "{attr}",
329             description);
330        parser.addArgument(attributeArgument);
331    
332        description = "The maximum number of entries to retrieve at a time when " +
333             "attempting to find entries with references to other entries.  This " +
334             "requires that the authenticated user have permission to use the " +
335             "simple paged results control, but it can avoid problems with the " +
336             "server sending entries too quickly for the client to handle.  By " +
337             "default, the simple paged results control will not be used.";
338        pageSizeArgument =
339             new IntegerArgument('z', "simplePageSize", false, 1, "{num}",
340                  description, 1, Integer.MAX_VALUE);
341        pageSizeArgument.addLongIdentifier("simple-page-size");
342        parser.addArgument(pageSizeArgument);
343      }
344    
345    
346    
347      /**
348       * Performs the core set of processing for this tool.
349       *
350       * @return  A result code that indicates whether the processing completed
351       *          successfully.
352       */
353      @Override()
354      public ResultCode doToolProcessing()
355      {
356        // Establish a connection to the target directory server to use for
357        // finding references to entries.
358        final LDAPConnection findReferencesConnection;
359        try
360        {
361          findReferencesConnection = getConnection();
362        }
363        catch (final LDAPException le)
364        {
365          Debug.debugException(le);
366          err("Unable to establish a connection to the directory server:  ",
367               StaticUtils.getExceptionMessage(le));
368          return le.getResultCode();
369        }
370    
371        try
372        {
373          // Establish a second connection to use for retrieving referenced entries.
374          try
375          {
376            getReferencedEntriesConnection = getConnection();
377          }
378          catch (final LDAPException le)
379          {
380            Debug.debugException(le);
381            err("Unable to establish a connection to the directory server:  ",
382                 StaticUtils.getExceptionMessage(le));
383            return le.getResultCode();
384          }
385    
386    
387          // Get the set of attributes for which to find missing references.
388          final List<String> attrList = attributeArgument.getValues();
389          attributes = new String[attrList.size()];
390          attrList.toArray(attributes);
391    
392    
393          // Construct a search filter that will be used to find all entries with
394          // references to other entries.
395          final Filter filter;
396          if (attributes.length == 1)
397          {
398            filter = Filter.createPresenceFilter(attributes[0]);
399            missingReferenceCounts.put(attributes[0], new AtomicLong(0L));
400          }
401          else
402          {
403            final Filter[] orComps = new Filter[attributes.length];
404            for (int i=0; i < attributes.length; i++)
405            {
406              orComps[i] = Filter.createPresenceFilter(attributes[i]);
407              missingReferenceCounts.put(attributes[i], new AtomicLong(0L));
408            }
409            filter = Filter.createORFilter(orComps);
410          }
411    
412    
413          // Iterate across all of the search base DNs and perform searches to find
414          // missing references.
415          for (final DN baseDN : baseDNArgument.getValues())
416          {
417            ASN1OctetString cookie = null;
418            do
419            {
420              final SearchRequest searchRequest = new SearchRequest(this,
421                   baseDN.toString(), SearchScope.SUB, filter, attributes);
422              if (pageSizeArgument.isPresent())
423              {
424                searchRequest.addControl(new SimplePagedResultsControl(
425                     pageSizeArgument.getValue(), cookie, false));
426              }
427    
428              SearchResult searchResult;
429              try
430              {
431                searchResult = findReferencesConnection.search(searchRequest);
432              }
433              catch (final LDAPSearchException lse)
434              {
435                Debug.debugException(lse);
436                searchResult = lse.getSearchResult();
437              }
438    
439              if (searchResult.getResultCode() != ResultCode.SUCCESS)
440              {
441                err("An error occurred while attempting to search for missing " +
442                     "references to entries below " + baseDN + ":  " +
443                     searchResult.getDiagnosticMessage());
444                return searchResult.getResultCode();
445              }
446    
447              final SimplePagedResultsControl pagedResultsResponse;
448              try
449              {
450                pagedResultsResponse = SimplePagedResultsControl.get(searchResult);
451              }
452              catch (final LDAPException le)
453              {
454                Debug.debugException(le);
455                err("An error occurred while attempting to decode a simple " +
456                     "paged results response control in the response to a " +
457                     "search for entries below " + baseDN + ":  " +
458                     StaticUtils.getExceptionMessage(le));
459                return le.getResultCode();
460              }
461    
462              if (pagedResultsResponse != null)
463              {
464                if (pagedResultsResponse.moreResultsToReturn())
465                {
466                  cookie = pagedResultsResponse.getCookie();
467                }
468                else
469                {
470                  cookie = null;
471                }
472              }
473            }
474            while (cookie != null);
475          }
476    
477    
478          // See if there were any missing references found.
479          boolean missingReferenceFound = false;
480          for (final Map.Entry<String,AtomicLong> e :
481               missingReferenceCounts.entrySet())
482          {
483            final long numMissing = e.getValue().get();
484            if (numMissing > 0L)
485            {
486              if (! missingReferenceFound)
487              {
488                err();
489                missingReferenceFound = true;
490              }
491    
492              err("Found " + numMissing + ' ' + e.getKey() +
493                   " references to entries that do not exist.");
494            }
495          }
496    
497          if (missingReferenceFound)
498          {
499            return ResultCode.CONSTRAINT_VIOLATION;
500          }
501          else
502          {
503            out("No references were found to entries that do not exist.");
504            return ResultCode.SUCCESS;
505          }
506        }
507        finally
508        {
509          findReferencesConnection.close();
510    
511          if (getReferencedEntriesConnection != null)
512          {
513            getReferencedEntriesConnection.close();
514          }
515        }
516      }
517    
518    
519    
520      /**
521       * Retrieves a map that correlates the number of missing references found by
522       * attribute type.
523       *
524       * @return  A map that correlates the number of missing references found by
525       *          attribute type.
526       */
527      public Map<String,AtomicLong> getMissingReferenceCounts()
528      {
529        return Collections.unmodifiableMap(missingReferenceCounts);
530      }
531    
532    
533    
534      /**
535       * Retrieves a set of information that may be used to generate example usage
536       * information.  Each element in the returned map should consist of a map
537       * between an example set of arguments and a string that describes the
538       * behavior of the tool when invoked with that set of arguments.
539       *
540       * @return  A set of information that may be used to generate example usage
541       *          information.  It may be {@code null} or empty if no example usage
542       *          information is available.
543       */
544      @Override()
545      public LinkedHashMap<String[],String> getExampleUsages()
546      {
547        final LinkedHashMap<String[],String> exampleMap =
548             new LinkedHashMap<String[],String>(1);
549    
550        final String[] args =
551        {
552          "--hostname", "server.example.com",
553          "--port", "389",
554          "--bindDN", "uid=john.doe,ou=People,dc=example,dc=com",
555          "--bindPassword", "password",
556          "--baseDN", "dc=example,dc=com",
557          "--attribute", "member",
558          "--attribute", "uniqueMember",
559          "--simplePageSize", "100"
560        };
561        exampleMap.put(args,
562             "Identify all entries below dc=example,dc=com in which either the " +
563                  "member or uniqueMember attribute references an entry that " +
564                  "does not exist.");
565    
566        return exampleMap;
567      }
568    
569    
570    
571      /**
572       * Indicates that the provided search result entry has been returned by the
573       * server and may be processed by this search result listener.
574       *
575       * @param  searchEntry  The search result entry that has been returned by the
576       *                      server.
577       */
578      public void searchEntryReturned(final SearchResultEntry searchEntry)
579      {
580        try
581        {
582          // Find attributes which references to entries that do not exist.
583          for (final String attr : attributes)
584          {
585            final List<Attribute> attrList =
586                 searchEntry.getAttributesWithOptions(attr, null);
587            for (final Attribute a : attrList)
588            {
589              for (final String value : a.getValues())
590              {
591                try
592                {
593                  final SearchResultEntry e =
594                       getReferencedEntriesConnection.getEntry(value, "1.1");
595                  if (e == null)
596                  {
597                    err("Entry '", searchEntry.getDN(), "' includes attribute ",
598                         a.getName(), " that references entry '", value,
599                         "' which does not exist.");
600                    missingReferenceCounts.get(attr).incrementAndGet();
601                  }
602                }
603                catch (final LDAPException le)
604                {
605                  Debug.debugException(le);
606                  err("An error occurred while attempting to determine whether " +
607                       "entry '" + value + "' referenced in attribute " +
608                       a.getName() + " of entry '" + searchEntry.getDN() +
609                       "' exists:  " + StaticUtils.getExceptionMessage(le));
610                  missingReferenceCounts.get(attr).incrementAndGet();
611                }
612              }
613            }
614          }
615        }
616        finally
617        {
618          final long count = entriesExamined.incrementAndGet();
619          if ((count % 1000L) == 0L)
620          {
621            out(count, " entries examined");
622          }
623        }
624      }
625    
626    
627    
628      /**
629       * Indicates that the provided search result reference has been returned by
630       * the server and may be processed by this search result listener.
631       *
632       * @param  searchReference  The search result reference that has been returned
633       *                          by the server.
634       */
635      public void searchReferenceReturned(
636                       final SearchResultReference searchReference)
637      {
638        // No implementation is required.  This tool will not follow referrals.
639      }
640    }