001    /*
002     * Copyright 2008-2016 UnboundID Corp.
003     * All Rights Reserved.
004     */
005    /*
006     * Copyright (C) 2008-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.text.SimpleDateFormat;
027    import java.util.Date;
028    import java.util.LinkedHashMap;
029    import java.util.List;
030    
031    import com.unboundid.ldap.sdk.Control;
032    import com.unboundid.ldap.sdk.DereferencePolicy;
033    import com.unboundid.ldap.sdk.Filter;
034    import com.unboundid.ldap.sdk.LDAPConnection;
035    import com.unboundid.ldap.sdk.LDAPException;
036    import com.unboundid.ldap.sdk.ResultCode;
037    import com.unboundid.ldap.sdk.SearchRequest;
038    import com.unboundid.ldap.sdk.SearchResult;
039    import com.unboundid.ldap.sdk.SearchResultEntry;
040    import com.unboundid.ldap.sdk.SearchResultListener;
041    import com.unboundid.ldap.sdk.SearchResultReference;
042    import com.unboundid.ldap.sdk.SearchScope;
043    import com.unboundid.ldap.sdk.Version;
044    import com.unboundid.util.Debug;
045    import com.unboundid.util.LDAPCommandLineTool;
046    import com.unboundid.util.StaticUtils;
047    import com.unboundid.util.ThreadSafety;
048    import com.unboundid.util.ThreadSafetyLevel;
049    import com.unboundid.util.WakeableSleeper;
050    import com.unboundid.util.args.ArgumentException;
051    import com.unboundid.util.args.ArgumentParser;
052    import com.unboundid.util.args.BooleanArgument;
053    import com.unboundid.util.args.ControlArgument;
054    import com.unboundid.util.args.DNArgument;
055    import com.unboundid.util.args.IntegerArgument;
056    import com.unboundid.util.args.ScopeArgument;
057    
058    
059    
060    /**
061     * This class provides a simple tool that can be used to search an LDAP
062     * directory server.  Some of the APIs demonstrated by this example include:
063     * <UL>
064     *   <LI>Argument Parsing (from the {@code com.unboundid.util.args}
065     *       package)</LI>
066     *   <LI>LDAP Command-Line Tool (from the {@code com.unboundid.util}
067     *       package)</LI>
068     *   <LI>LDAP Communication (from the {@code com.unboundid.ldap.sdk}
069     *       package)</LI>
070     * </UL>
071     * <BR><BR>
072     * All of the necessary information is provided using
073     * command line arguments.  Supported arguments include those allowed by the
074     * {@link LDAPCommandLineTool} class, as well as the following additional
075     * arguments:
076     * <UL>
077     *   <LI>"-b {baseDN}" or "--baseDN {baseDN}" -- specifies the base DN to use
078     *       for the search.  This must be provided.</LI>
079     *   <LI>"-s {scope}" or "--scope {scope}" -- specifies the scope to use for the
080     *       search.  The scope value should be one of "base", "one", "sub", or
081     *       "subord".  If this isn't specified, then a scope of "sub" will be
082     *       used.</LI>
083     *   <LI>"-R" or "--followReferrals" -- indicates that the tool should follow
084     *       any referrals encountered while searching.</LI>
085     *   <LI>"-t" or "--terse" -- indicates that the tool should generate minimal
086     *       output beyond the search results.</LI>
087     *   <LI>"-i {millis}" or "--repeatIntervalMillis {millis}" -- indicates that
088     *       the search should be periodically repeated with the specified delay
089     *       (in milliseconds) between requests.</LI>
090     *   <LI>"-n {count}" or "--numSearches {count}" -- specifies the total number
091     *       of times that the search should be performed.  This may only be used in
092     *       conjunction with the "--repeatIntervalMillis" argument.  If
093     *       "--repeatIntervalMillis" is used without "--numSearches", then the
094     *       searches will continue to be repeated until the tool is
095     *       interrupted.</LI>
096     *   <LI>"--bindControl {control}" -- specifies a control that should be
097     *       included in the bind request sent by this tool before performing any
098     *       search operations.</LI>
099     *   <LI>"-J {control}" or "--control {control}" -- specifies a control that
100     *       should be included in the search request(s) sent by this tool.</LI>
101     * </UL>
102     * In addition, after the above named arguments are provided, a set of one or
103     * more unnamed trailing arguments must be given.  The first argument should be
104     * the string representation of the filter to use for the search.  If there are
105     * any additional trailing arguments, then they will be interpreted as the
106     * attributes to return in matching entries.  If no attribute names are given,
107     * then the server should return all user attributes in matching entries.
108     * <BR><BR>
109     * Note that this class implements the SearchResultListener interface, which
110     * will be notified whenever a search result entry or reference is returned from
111     * the server.  Whenever an entry is received, it will simply be printed
112     * displayed in LDIF.
113     */
114    @ThreadSafety(level=ThreadSafetyLevel.NOT_THREADSAFE)
115    public final class LDAPSearch
116           extends LDAPCommandLineTool
117           implements SearchResultListener
118    {
119      /**
120       * The date formatter that should be used when writing timestamps.
121       */
122      private static final SimpleDateFormat DATE_FORMAT =
123           new SimpleDateFormat("dd/MMM/yyyy:HH:mm:ss.SSS");
124    
125    
126    
127      /**
128       * The serial version UID for this serializable class.
129       */
130      private static final long serialVersionUID = 7465188734621412477L;
131    
132    
133    
134      // The argument parser used by this program.
135      private ArgumentParser parser;
136    
137      // Indicates whether the search should be repeated.
138      private boolean repeat;
139    
140      // The argument used to indicate whether to follow referrals.
141      private BooleanArgument followReferrals;
142    
143      // The argument used to indicate whether to use terse mode.
144      private BooleanArgument terseMode;
145    
146      // The argument used to specify any bind controls that should be used.
147      private ControlArgument bindControls;
148    
149      // The argument used to specify any search controls that should be used.
150      private ControlArgument searchControls;
151    
152      // The number of times to perform the search.
153      private IntegerArgument numSearches;
154    
155      // The interval in milliseconds between repeated searches.
156      private IntegerArgument repeatIntervalMillis;
157    
158      // The argument used to specify the base DN for the search.
159      private DNArgument baseDN;
160    
161      // The argument used to specify the scope for the search.
162      private ScopeArgument scopeArg;
163    
164    
165    
166      /**
167       * Parse the provided command line arguments and make the appropriate set of
168       * changes.
169       *
170       * @param  args  The command line arguments provided to this program.
171       */
172      public static void main(final String[] args)
173      {
174        final ResultCode resultCode = main(args, System.out, System.err);
175        if (resultCode != ResultCode.SUCCESS)
176        {
177          System.exit(resultCode.intValue());
178        }
179      }
180    
181    
182    
183      /**
184       * Parse the provided command line arguments and make the appropriate set of
185       * changes.
186       *
187       * @param  args       The command line arguments provided to this program.
188       * @param  outStream  The output stream to which standard out should be
189       *                    written.  It may be {@code null} if output should be
190       *                    suppressed.
191       * @param  errStream  The output stream to which standard error should be
192       *                    written.  It may be {@code null} if error messages
193       *                    should be suppressed.
194       *
195       * @return  A result code indicating whether the processing was successful.
196       */
197      public static ResultCode main(final String[] args,
198                                    final OutputStream outStream,
199                                    final OutputStream errStream)
200      {
201        final LDAPSearch ldapSearch = new LDAPSearch(outStream, errStream);
202        return ldapSearch.runTool(args);
203      }
204    
205    
206    
207      /**
208       * Creates a new instance of this tool.
209       *
210       * @param  outStream  The output stream to which standard out should be
211       *                    written.  It may be {@code null} if output should be
212       *                    suppressed.
213       * @param  errStream  The output stream to which standard error should be
214       *                    written.  It may be {@code null} if error messages
215       *                    should be suppressed.
216       */
217      public LDAPSearch(final OutputStream outStream, final OutputStream errStream)
218      {
219        super(outStream, errStream);
220      }
221    
222    
223    
224      /**
225       * Retrieves the name for this tool.
226       *
227       * @return  The name for this tool.
228       */
229      @Override()
230      public String getToolName()
231      {
232        return "ldapsearch";
233      }
234    
235    
236    
237      /**
238       * Retrieves the description for this tool.
239       *
240       * @return  The description for this tool.
241       */
242      @Override()
243      public String getToolDescription()
244      {
245        return "Search an LDAP directory server.";
246      }
247    
248    
249    
250      /**
251       * Retrieves the version string for this tool.
252       *
253       * @return  The version string for this tool.
254       */
255      @Override()
256      public String getToolVersion()
257      {
258        return Version.NUMERIC_VERSION_STRING;
259      }
260    
261    
262    
263      /**
264       * Retrieves the minimum number of unnamed trailing arguments that are
265       * required.
266       *
267       * @return  One, to indicate that at least one trailing argument (representing
268       *          the search filter) must be provided.
269       */
270      @Override()
271      public int getMinTrailingArguments()
272      {
273        return 1;
274      }
275    
276    
277    
278      /**
279       * Retrieves the maximum number of unnamed trailing arguments that are
280       * allowed.
281       *
282       * @return  A negative value to indicate that any number of trailing arguments
283       *          may be provided.
284       */
285      @Override()
286      public int getMaxTrailingArguments()
287      {
288        return -1;
289      }
290    
291    
292    
293      /**
294       * Retrieves a placeholder string that may be used to indicate what kinds of
295       * trailing arguments are allowed.
296       *
297       * @return  A placeholder string that may be used to indicate what kinds of
298       *          trailing arguments are allowed.
299       */
300      @Override()
301      public String getTrailingArgumentsPlaceholder()
302      {
303        return "{filter} [attr1 [attr2 [...]]]";
304      }
305    
306    
307    
308      /**
309       * Indicates whether this tool should provide support for an interactive mode,
310       * in which the tool offers a mode in which the arguments can be provided in
311       * a text-driven menu rather than requiring them to be given on the command
312       * line.  If interactive mode is supported, it may be invoked using the
313       * "--interactive" argument.  Alternately, if interactive mode is supported
314       * and {@link #defaultsToInteractiveMode()} returns {@code true}, then
315       * interactive mode may be invoked by simply launching the tool without any
316       * arguments.
317       *
318       * @return  {@code true} if this tool supports interactive mode, or
319       *          {@code false} if not.
320       */
321      @Override()
322      public boolean supportsInteractiveMode()
323      {
324        return true;
325      }
326    
327    
328    
329      /**
330       * Indicates whether this tool defaults to launching in interactive mode if
331       * the tool is invoked without any command-line arguments.  This will only be
332       * used if {@link #supportsInteractiveMode()} returns {@code true}.
333       *
334       * @return  {@code true} if this tool defaults to using interactive mode if
335       *          launched without any command-line arguments, or {@code false} if
336       *          not.
337       */
338      @Override()
339      public boolean defaultsToInteractiveMode()
340      {
341        return true;
342      }
343    
344    
345    
346      /**
347       * Indicates whether this tool supports the use of a properties file for
348       * specifying default values for arguments that aren't specified on the
349       * command line.
350       *
351       * @return  {@code true} if this tool supports the use of a properties file
352       *          for specifying default values for arguments that aren't specified
353       *          on the command line, or {@code false} if not.
354       */
355      @Override()
356      public boolean supportsPropertiesFile()
357      {
358        return true;
359      }
360    
361    
362    
363      /**
364       * Indicates whether the LDAP-specific arguments should include alternate
365       * versions of all long identifiers that consist of multiple words so that
366       * they are available in both camelCase and dash-separated versions.
367       *
368       * @return  {@code true} if this tool should provide multiple versions of
369       *          long identifiers for LDAP-specific arguments, or {@code false} if
370       *          not.
371       */
372      @Override()
373      protected boolean includeAlternateLongIdentifiers()
374      {
375        return true;
376      }
377    
378    
379    
380      /**
381       * Adds the arguments used by this program that aren't already provided by the
382       * generic {@code LDAPCommandLineTool} framework.
383       *
384       * @param  parser  The argument parser to which the arguments should be added.
385       *
386       * @throws  ArgumentException  If a problem occurs while adding the arguments.
387       */
388      @Override()
389      public void addNonLDAPArguments(final ArgumentParser parser)
390             throws ArgumentException
391      {
392        this.parser = parser;
393    
394        String description = "The base DN to use for the search.  This must be " +
395                             "provided.";
396        baseDN = new DNArgument('b', "baseDN", true, 1, "{dn}", description);
397        baseDN.addLongIdentifier("base-dn");
398        parser.addArgument(baseDN);
399    
400    
401        description = "The scope to use for the search.  It should be 'base', " +
402                      "'one', 'sub', or 'subord'.  If this is not provided, then " +
403                      "a default scope of 'sub' will be used.";
404        scopeArg = new ScopeArgument('s', "scope", false, "{scope}", description,
405                                     SearchScope.SUB);
406        parser.addArgument(scopeArg);
407    
408    
409        description = "Follow any referrals encountered during processing.";
410        followReferrals = new BooleanArgument('R', "followReferrals", description);
411        followReferrals.addLongIdentifier("follow-referrals");
412        parser.addArgument(followReferrals);
413    
414    
415        description = "Information about a control to include in the bind request.";
416        bindControls = new ControlArgument(null, "bindControl", false, 0, null,
417             description);
418        bindControls.addLongIdentifier("bind-control");
419        parser.addArgument(bindControls);
420    
421    
422        description = "Information about a control to include in search requests.";
423        searchControls = new ControlArgument('J', "control", false, 0, null,
424             description);
425        parser.addArgument(searchControls);
426    
427    
428        description = "Generate terse output with minimal additional information.";
429        terseMode = new BooleanArgument('t', "terse", description);
430        parser.addArgument(terseMode);
431    
432    
433        description = "Specifies the length of time in milliseconds to sleep " +
434                      "before repeating the same search.  If this is not " +
435                      "provided, then the search will only be performed once.";
436        repeatIntervalMillis = new IntegerArgument('i', "repeatIntervalMillis",
437                                                   false, 1, "{millis}",
438                                                   description, 0,
439                                                   Integer.MAX_VALUE);
440        repeatIntervalMillis.addLongIdentifier("repeat-interval-millis");
441        parser.addArgument(repeatIntervalMillis);
442    
443    
444        description = "Specifies the number of times that the search should be " +
445                      "performed.  If this argument is present, then the " +
446                      "--repeatIntervalMillis argument must also be provided to " +
447                      "specify the length of time between searches.  If " +
448                      "--repeatIntervalMillis is used without --numSearches, " +
449                      "then the search will be repeated until the tool is " +
450                      "interrupted.";
451        numSearches = new IntegerArgument('n', "numSearches", false, 1, "{count}",
452                                          description, 1, Integer.MAX_VALUE);
453        numSearches.addLongIdentifier("num-searches");
454        parser.addArgument(numSearches);
455        parser.addDependentArgumentSet(numSearches, repeatIntervalMillis);
456      }
457    
458    
459    
460      /**
461       * {@inheritDoc}
462       */
463      @Override()
464      public void doExtendedNonLDAPArgumentValidation()
465             throws ArgumentException
466      {
467        // There must have been at least one trailing argument provided, and it must
468        // be parsable as a valid search filter.
469        if (parser.getTrailingArguments().isEmpty())
470        {
471          throw new ArgumentException("At least one trailing argument must be " +
472               "provided to specify the search filter.  Additional trailing " +
473               "arguments are allowed to specify the attributes to return in " +
474               "search result entries.");
475        }
476    
477        try
478        {
479          Filter.create(parser.getTrailingArguments().get(0));
480        }
481        catch (final Exception e)
482        {
483          Debug.debugException(e);
484          throw new ArgumentException(
485               "The first trailing argument value could not be parsed as a valid " +
486                    "LDAP search filter.",
487               e);
488        }
489      }
490    
491    
492    
493      /**
494       * {@inheritDoc}
495       */
496      @Override()
497      protected List<Control> getBindControls()
498      {
499        return bindControls.getValues();
500      }
501    
502    
503    
504      /**
505       * Performs the actual processing for this tool.  In this case, it gets a
506       * connection to the directory server and uses it to perform the requested
507       * search.
508       *
509       * @return  The result code for the processing that was performed.
510       */
511      @Override()
512      public ResultCode doToolProcessing()
513      {
514        // Make sure that at least one trailing argument was provided, which will be
515        // the filter.  If there were any other arguments, then they will be the
516        // attributes to return.
517        final List<String> trailingArguments = parser.getTrailingArguments();
518        if (trailingArguments.isEmpty())
519        {
520          err("No search filter was provided.");
521          err();
522          err(parser.getUsageString(StaticUtils.TERMINAL_WIDTH_COLUMNS - 1));
523          return ResultCode.PARAM_ERROR;
524        }
525    
526        final Filter filter;
527        try
528        {
529          filter = Filter.create(trailingArguments.get(0));
530        }
531        catch (LDAPException le)
532        {
533          err("Invalid search filter:  ", le.getMessage());
534          return le.getResultCode();
535        }
536    
537        final String[] attributesToReturn;
538        if (trailingArguments.size() > 1)
539        {
540          attributesToReturn = new String[trailingArguments.size() - 1];
541          for (int i=1; i < trailingArguments.size(); i++)
542          {
543            attributesToReturn[i-1] = trailingArguments.get(i);
544          }
545        }
546        else
547        {
548          attributesToReturn = StaticUtils.NO_STRINGS;
549        }
550    
551    
552        // Get the connection to the directory server.
553        final LDAPConnection connection;
554        try
555        {
556          connection = getConnection();
557          if (! terseMode.isPresent())
558          {
559            out("# Connected to ", connection.getConnectedAddress(), ':',
560                 connection.getConnectedPort());
561          }
562        }
563        catch (LDAPException le)
564        {
565          err("Error connecting to the directory server:  ", le.getMessage());
566          return le.getResultCode();
567        }
568    
569    
570        // Create a search request with the appropriate information and process it
571        // in the server.  Note that in this case, we're creating a search result
572        // listener to handle the results since there could potentially be a lot of
573        // them.
574        final SearchRequest searchRequest =
575             new SearchRequest(this, baseDN.getStringValue(), scopeArg.getValue(),
576                               DereferencePolicy.NEVER, 0, 0, false, filter,
577                               attributesToReturn);
578        searchRequest.setFollowReferrals(followReferrals.isPresent());
579    
580        final List<Control> controlList = searchControls.getValues();
581        if (controlList != null)
582        {
583          searchRequest.setControls(controlList);
584        }
585    
586    
587        final boolean infinite;
588        final int numIterations;
589        if (repeatIntervalMillis.isPresent())
590        {
591          repeat = true;
592    
593          if (numSearches.isPresent())
594          {
595            infinite      = false;
596            numIterations = numSearches.getValue();
597          }
598          else
599          {
600            infinite      = true;
601            numIterations = Integer.MAX_VALUE;
602          }
603        }
604        else
605        {
606          infinite      = false;
607          repeat        = false;
608          numIterations = 1;
609        }
610    
611        ResultCode resultCode = ResultCode.SUCCESS;
612        long lastSearchTime = System.currentTimeMillis();
613        final WakeableSleeper sleeper = new WakeableSleeper();
614        for (int i=0; (infinite || (i < numIterations)); i++)
615        {
616          if (repeat && (i > 0))
617          {
618            final long sleepTime =
619                 (lastSearchTime + repeatIntervalMillis.getValue()) -
620                 System.currentTimeMillis();
621            if (sleepTime > 0)
622            {
623              sleeper.sleep(sleepTime);
624            }
625            lastSearchTime = System.currentTimeMillis();
626          }
627    
628          try
629          {
630            final SearchResult searchResult = connection.search(searchRequest);
631            if ((! repeat) && (! terseMode.isPresent()))
632            {
633              out("# The search operation was processed successfully.");
634              out("# Entries returned:  ", searchResult.getEntryCount());
635              out("# References returned:  ", searchResult.getReferenceCount());
636            }
637          }
638          catch (LDAPException le)
639          {
640            err("An error occurred while processing the search:  ",
641                 le.getMessage());
642            err("Result Code:  ", le.getResultCode().intValue(), " (",
643                 le.getResultCode().getName(), ')');
644            if (le.getMatchedDN() != null)
645            {
646              err("Matched DN:  ", le.getMatchedDN());
647            }
648    
649            if (le.getReferralURLs() != null)
650            {
651              for (final String url : le.getReferralURLs())
652              {
653                err("Referral URL:  ", url);
654              }
655            }
656    
657            if (resultCode == ResultCode.SUCCESS)
658            {
659              resultCode = le.getResultCode();
660            }
661    
662            if (! le.getResultCode().isConnectionUsable())
663            {
664              break;
665            }
666          }
667        }
668    
669    
670        // Close the connection to the directory server and exit.
671        connection.close();
672        if (! terseMode.isPresent())
673        {
674          out();
675          out("# Disconnected from the server");
676        }
677        return resultCode;
678      }
679    
680    
681    
682      /**
683       * Indicates that the provided search result entry was returned from the
684       * associated search operation.
685       *
686       * @param  entry  The entry that was returned from the search.
687       */
688      public void searchEntryReturned(final SearchResultEntry entry)
689      {
690        if (repeat)
691        {
692          out("# ", DATE_FORMAT.format(new Date()));
693        }
694    
695        out(entry.toLDIFString());
696      }
697    
698    
699    
700      /**
701       * Indicates that the provided search result reference was returned from the
702       * associated search operation.
703       *
704       * @param  reference  The reference that was returned from the search.
705       */
706      public void searchReferenceReturned(final SearchResultReference reference)
707      {
708        if (repeat)
709        {
710          out("# ", DATE_FORMAT.format(new Date()));
711        }
712    
713        out(reference.toString());
714      }
715    
716    
717    
718      /**
719       * {@inheritDoc}
720       */
721      @Override()
722      public LinkedHashMap<String[],String> getExampleUsages()
723      {
724        final LinkedHashMap<String[],String> examples =
725             new LinkedHashMap<String[],String>();
726    
727        final String[] args =
728        {
729          "--hostname", "server.example.com",
730          "--port", "389",
731          "--bindDN", "uid=admin,dc=example,dc=com",
732          "--bindPassword", "password",
733          "--baseDN", "dc=example,dc=com",
734          "--scope", "sub",
735          "(uid=jdoe)",
736          "givenName",
737           "sn",
738           "mail"
739        };
740        final String description =
741             "Perform a search in the directory server to find all entries " +
742             "matching the filter '(uid=jdoe)' anywhere below " +
743             "'dc=example,dc=com'.  Include only the givenName, sn, and mail " +
744             "attributes in the entries that are returned.";
745        examples.put(args, description);
746    
747        return examples;
748      }
749    }