001/*
002 * Copyright 2008-2017 UnboundID Corp.
003 * All Rights Reserved.
004 */
005/*
006 * Copyright (C) 2008-2017 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.text.SimpleDateFormat;
027import java.util.Date;
028import java.util.LinkedHashMap;
029import java.util.List;
030
031import com.unboundid.ldap.sdk.Control;
032import com.unboundid.ldap.sdk.DereferencePolicy;
033import com.unboundid.ldap.sdk.Filter;
034import com.unboundid.ldap.sdk.LDAPConnection;
035import com.unboundid.ldap.sdk.LDAPException;
036import com.unboundid.ldap.sdk.ResultCode;
037import com.unboundid.ldap.sdk.SearchRequest;
038import com.unboundid.ldap.sdk.SearchResult;
039import com.unboundid.ldap.sdk.SearchResultEntry;
040import com.unboundid.ldap.sdk.SearchResultListener;
041import com.unboundid.ldap.sdk.SearchResultReference;
042import com.unboundid.ldap.sdk.SearchScope;
043import com.unboundid.ldap.sdk.Version;
044import com.unboundid.util.Debug;
045import com.unboundid.util.LDAPCommandLineTool;
046import com.unboundid.util.StaticUtils;
047import com.unboundid.util.ThreadSafety;
048import com.unboundid.util.ThreadSafetyLevel;
049import com.unboundid.util.WakeableSleeper;
050import com.unboundid.util.args.ArgumentException;
051import com.unboundid.util.args.ArgumentParser;
052import com.unboundid.util.args.BooleanArgument;
053import com.unboundid.util.args.ControlArgument;
054import com.unboundid.util.args.DNArgument;
055import com.unboundid.util.args.IntegerArgument;
056import 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)
115public 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}