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