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     */
021    package com.unboundid.ldap.sdk.examples;
022    
023    
024    
025    import java.io.OutputStream;
026    import java.text.SimpleDateFormat;
027    import java.util.Date;
028    import java.util.LinkedHashMap;
029    import java.util.List;
030    
031    import com.unboundid.ldap.sdk.DereferencePolicy;
032    import com.unboundid.ldap.sdk.Filter;
033    import com.unboundid.ldap.sdk.LDAPConnection;
034    import com.unboundid.ldap.sdk.LDAPException;
035    import com.unboundid.ldap.sdk.ResultCode;
036    import com.unboundid.ldap.sdk.SearchRequest;
037    import com.unboundid.ldap.sdk.SearchResult;
038    import com.unboundid.ldap.sdk.SearchResultEntry;
039    import com.unboundid.ldap.sdk.SearchResultListener;
040    import com.unboundid.ldap.sdk.SearchResultReference;
041    import com.unboundid.ldap.sdk.SearchScope;
042    import com.unboundid.ldap.sdk.Version;
043    import com.unboundid.util.LDAPCommandLineTool;
044    import com.unboundid.util.StaticUtils;
045    import com.unboundid.util.ThreadSafety;
046    import com.unboundid.util.ThreadSafetyLevel;
047    import com.unboundid.util.WakeableSleeper;
048    import com.unboundid.util.args.ArgumentException;
049    import com.unboundid.util.args.ArgumentParser;
050    import com.unboundid.util.args.BooleanArgument;
051    import com.unboundid.util.args.DNArgument;
052    import com.unboundid.util.args.IntegerArgument;
053    import 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)
107    public 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    }