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 should provide arguments for redirecting output
348       * to a file.  If this method returns {@code true}, then the tool will offer
349       * an "--outputFile" argument that will specify the path to a file to which
350       * all standard output and standard error content will be written, and it will
351       * also offer a "--teeToStandardOut" argument that can only be used if the
352       * "--outputFile" argument is present and will cause all output to be written
353       * to both the specified output file and to standard output.
354       *
355       * @return  {@code true} if this tool should provide arguments for redirecting
356       *          output to a file, or {@code false} if not.
357       */
358      @Override()
359      protected boolean supportsOutputFile()
360      {
361        return true;
362      }
363    
364    
365    
366      /**
367       * Indicates whether this tool supports the use of a properties file for
368       * specifying default values for arguments that aren't specified on the
369       * command line.
370       *
371       * @return  {@code true} if this tool supports the use of a properties file
372       *          for specifying default values for arguments that aren't specified
373       *          on the command line, or {@code false} if not.
374       */
375      @Override()
376      public boolean supportsPropertiesFile()
377      {
378        return true;
379      }
380    
381    
382    
383      /**
384       * Indicates whether this tool should default to interactively prompting for
385       * the bind password if a password is required but no argument was provided
386       * to indicate how to get the password.
387       *
388       * @return  {@code true} if this tool should default to interactively
389       *          prompting for the bind password, or {@code false} if not.
390       */
391      @Override()
392      protected boolean defaultToPromptForBindPassword()
393      {
394        return true;
395      }
396    
397    
398    
399      /**
400       * Indicates whether the LDAP-specific arguments should include alternate
401       * versions of all long identifiers that consist of multiple words so that
402       * they are available in both camelCase and dash-separated versions.
403       *
404       * @return  {@code true} if this tool should provide multiple versions of
405       *          long identifiers for LDAP-specific arguments, or {@code false} if
406       *          not.
407       */
408      @Override()
409      protected boolean includeAlternateLongIdentifiers()
410      {
411        return true;
412      }
413    
414    
415    
416      /**
417       * Adds the arguments used by this program that aren't already provided by the
418       * generic {@code LDAPCommandLineTool} framework.
419       *
420       * @param  parser  The argument parser to which the arguments should be added.
421       *
422       * @throws  ArgumentException  If a problem occurs while adding the arguments.
423       */
424      @Override()
425      public void addNonLDAPArguments(final ArgumentParser parser)
426             throws ArgumentException
427      {
428        this.parser = parser;
429    
430        String description = "The base DN to use for the search.  This must be " +
431                             "provided.";
432        baseDN = new DNArgument('b', "baseDN", true, 1, "{dn}", description);
433        baseDN.addLongIdentifier("base-dn");
434        parser.addArgument(baseDN);
435    
436    
437        description = "The scope to use for the search.  It should be 'base', " +
438                      "'one', 'sub', or 'subord'.  If this is not provided, then " +
439                      "a default scope of 'sub' will be used.";
440        scopeArg = new ScopeArgument('s', "scope", false, "{scope}", description,
441                                     SearchScope.SUB);
442        parser.addArgument(scopeArg);
443    
444    
445        description = "Follow any referrals encountered during processing.";
446        followReferrals = new BooleanArgument('R', "followReferrals", description);
447        followReferrals.addLongIdentifier("follow-referrals");
448        parser.addArgument(followReferrals);
449    
450    
451        description = "Information about a control to include in the bind request.";
452        bindControls = new ControlArgument(null, "bindControl", false, 0, null,
453             description);
454        bindControls.addLongIdentifier("bind-control");
455        parser.addArgument(bindControls);
456    
457    
458        description = "Information about a control to include in search requests.";
459        searchControls = new ControlArgument('J', "control", false, 0, null,
460             description);
461        parser.addArgument(searchControls);
462    
463    
464        description = "Generate terse output with minimal additional information.";
465        terseMode = new BooleanArgument('t', "terse", description);
466        parser.addArgument(terseMode);
467    
468    
469        description = "Specifies the length of time in milliseconds to sleep " +
470                      "before repeating the same search.  If this is not " +
471                      "provided, then the search will only be performed once.";
472        repeatIntervalMillis = new IntegerArgument('i', "repeatIntervalMillis",
473                                                   false, 1, "{millis}",
474                                                   description, 0,
475                                                   Integer.MAX_VALUE);
476        repeatIntervalMillis.addLongIdentifier("repeat-interval-millis");
477        parser.addArgument(repeatIntervalMillis);
478    
479    
480        description = "Specifies the number of times that the search should be " +
481                      "performed.  If this argument is present, then the " +
482                      "--repeatIntervalMillis argument must also be provided to " +
483                      "specify the length of time between searches.  If " +
484                      "--repeatIntervalMillis is used without --numSearches, " +
485                      "then the search will be repeated until the tool is " +
486                      "interrupted.";
487        numSearches = new IntegerArgument('n', "numSearches", false, 1, "{count}",
488                                          description, 1, Integer.MAX_VALUE);
489        numSearches.addLongIdentifier("num-searches");
490        parser.addArgument(numSearches);
491        parser.addDependentArgumentSet(numSearches, repeatIntervalMillis);
492      }
493    
494    
495    
496      /**
497       * {@inheritDoc}
498       */
499      @Override()
500      public void doExtendedNonLDAPArgumentValidation()
501             throws ArgumentException
502      {
503        // There must have been at least one trailing argument provided, and it must
504        // be parsable as a valid search filter.
505        if (parser.getTrailingArguments().isEmpty())
506        {
507          throw new ArgumentException("At least one trailing argument must be " +
508               "provided to specify the search filter.  Additional trailing " +
509               "arguments are allowed to specify the attributes to return in " +
510               "search result entries.");
511        }
512    
513        try
514        {
515          Filter.create(parser.getTrailingArguments().get(0));
516        }
517        catch (final Exception e)
518        {
519          Debug.debugException(e);
520          throw new ArgumentException(
521               "The first trailing argument value could not be parsed as a valid " +
522                    "LDAP search filter.",
523               e);
524        }
525      }
526    
527    
528    
529      /**
530       * {@inheritDoc}
531       */
532      @Override()
533      protected List<Control> getBindControls()
534      {
535        return bindControls.getValues();
536      }
537    
538    
539    
540      /**
541       * Performs the actual processing for this tool.  In this case, it gets a
542       * connection to the directory server and uses it to perform the requested
543       * search.
544       *
545       * @return  The result code for the processing that was performed.
546       */
547      @Override()
548      public ResultCode doToolProcessing()
549      {
550        // Make sure that at least one trailing argument was provided, which will be
551        // the filter.  If there were any other arguments, then they will be the
552        // attributes to return.
553        final List<String> trailingArguments = parser.getTrailingArguments();
554        if (trailingArguments.isEmpty())
555        {
556          err("No search filter was provided.");
557          err();
558          err(parser.getUsageString(StaticUtils.TERMINAL_WIDTH_COLUMNS - 1));
559          return ResultCode.PARAM_ERROR;
560        }
561    
562        final Filter filter;
563        try
564        {
565          filter = Filter.create(trailingArguments.get(0));
566        }
567        catch (LDAPException le)
568        {
569          err("Invalid search filter:  ", le.getMessage());
570          return le.getResultCode();
571        }
572    
573        final String[] attributesToReturn;
574        if (trailingArguments.size() > 1)
575        {
576          attributesToReturn = new String[trailingArguments.size() - 1];
577          for (int i=1; i < trailingArguments.size(); i++)
578          {
579            attributesToReturn[i-1] = trailingArguments.get(i);
580          }
581        }
582        else
583        {
584          attributesToReturn = StaticUtils.NO_STRINGS;
585        }
586    
587    
588        // Get the connection to the directory server.
589        final LDAPConnection connection;
590        try
591        {
592          connection = getConnection();
593          if (! terseMode.isPresent())
594          {
595            out("# Connected to ", connection.getConnectedAddress(), ':',
596                 connection.getConnectedPort());
597          }
598        }
599        catch (LDAPException le)
600        {
601          err("Error connecting to the directory server:  ", le.getMessage());
602          return le.getResultCode();
603        }
604    
605    
606        // Create a search request with the appropriate information and process it
607        // in the server.  Note that in this case, we're creating a search result
608        // listener to handle the results since there could potentially be a lot of
609        // them.
610        final SearchRequest searchRequest =
611             new SearchRequest(this, baseDN.getStringValue(), scopeArg.getValue(),
612                               DereferencePolicy.NEVER, 0, 0, false, filter,
613                               attributesToReturn);
614        searchRequest.setFollowReferrals(followReferrals.isPresent());
615    
616        final List<Control> controlList = searchControls.getValues();
617        if (controlList != null)
618        {
619          searchRequest.setControls(controlList);
620        }
621    
622    
623        final boolean infinite;
624        final int numIterations;
625        if (repeatIntervalMillis.isPresent())
626        {
627          repeat = true;
628    
629          if (numSearches.isPresent())
630          {
631            infinite      = false;
632            numIterations = numSearches.getValue();
633          }
634          else
635          {
636            infinite      = true;
637            numIterations = Integer.MAX_VALUE;
638          }
639        }
640        else
641        {
642          infinite      = false;
643          repeat        = false;
644          numIterations = 1;
645        }
646    
647        ResultCode resultCode = ResultCode.SUCCESS;
648        long lastSearchTime = System.currentTimeMillis();
649        final WakeableSleeper sleeper = new WakeableSleeper();
650        for (int i=0; (infinite || (i < numIterations)); i++)
651        {
652          if (repeat && (i > 0))
653          {
654            final long sleepTime =
655                 (lastSearchTime + repeatIntervalMillis.getValue()) -
656                 System.currentTimeMillis();
657            if (sleepTime > 0)
658            {
659              sleeper.sleep(sleepTime);
660            }
661            lastSearchTime = System.currentTimeMillis();
662          }
663    
664          try
665          {
666            final SearchResult searchResult = connection.search(searchRequest);
667            if ((! repeat) && (! terseMode.isPresent()))
668            {
669              out("# The search operation was processed successfully.");
670              out("# Entries returned:  ", searchResult.getEntryCount());
671              out("# References returned:  ", searchResult.getReferenceCount());
672            }
673          }
674          catch (LDAPException le)
675          {
676            err("An error occurred while processing the search:  ",
677                 le.getMessage());
678            err("Result Code:  ", le.getResultCode().intValue(), " (",
679                 le.getResultCode().getName(), ')');
680            if (le.getMatchedDN() != null)
681            {
682              err("Matched DN:  ", le.getMatchedDN());
683            }
684    
685            if (le.getReferralURLs() != null)
686            {
687              for (final String url : le.getReferralURLs())
688              {
689                err("Referral URL:  ", url);
690              }
691            }
692    
693            if (resultCode == ResultCode.SUCCESS)
694            {
695              resultCode = le.getResultCode();
696            }
697    
698            if (! le.getResultCode().isConnectionUsable())
699            {
700              break;
701            }
702          }
703        }
704    
705    
706        // Close the connection to the directory server and exit.
707        connection.close();
708        if (! terseMode.isPresent())
709        {
710          out();
711          out("# Disconnected from the server");
712        }
713        return resultCode;
714      }
715    
716    
717    
718      /**
719       * Indicates that the provided search result entry was returned from the
720       * associated search operation.
721       *
722       * @param  entry  The entry that was returned from the search.
723       */
724      public void searchEntryReturned(final SearchResultEntry entry)
725      {
726        if (repeat)
727        {
728          out("# ", DATE_FORMAT.format(new Date()));
729        }
730    
731        out(entry.toLDIFString());
732      }
733    
734    
735    
736      /**
737       * Indicates that the provided search result reference was returned from the
738       * associated search operation.
739       *
740       * @param  reference  The reference that was returned from the search.
741       */
742      public void searchReferenceReturned(final SearchResultReference reference)
743      {
744        if (repeat)
745        {
746          out("# ", DATE_FORMAT.format(new Date()));
747        }
748    
749        out(reference.toString());
750      }
751    
752    
753    
754      /**
755       * {@inheritDoc}
756       */
757      @Override()
758      public LinkedHashMap<String[],String> getExampleUsages()
759      {
760        final LinkedHashMap<String[],String> examples =
761             new LinkedHashMap<String[],String>();
762    
763        final String[] args =
764        {
765          "--hostname", "server.example.com",
766          "--port", "389",
767          "--bindDN", "uid=admin,dc=example,dc=com",
768          "--bindPassword", "password",
769          "--baseDN", "dc=example,dc=com",
770          "--scope", "sub",
771          "(uid=jdoe)",
772          "givenName",
773           "sn",
774           "mail"
775        };
776        final String description =
777             "Perform a search in the directory server to find all entries " +
778             "matching the filter '(uid=jdoe)' anywhere below " +
779             "'dc=example,dc=com'.  Include only the givenName, sn, and mail " +
780             "attributes in the entries that are returned.";
781        examples.put(args, description);
782    
783        return examples;
784      }
785    }