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