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