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