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     */
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.Control;
032    import com.unboundid.ldap.sdk.DereferencePolicy;
033    import com.unboundid.ldap.sdk.Filter;
034    import com.unboundid.ldap.sdk.LDAPConnection;
035    import com.unboundid.ldap.sdk.LDAPException;
036    import com.unboundid.ldap.sdk.ResultCode;
037    import com.unboundid.ldap.sdk.SearchRequest;
038    import com.unboundid.ldap.sdk.SearchResult;
039    import com.unboundid.ldap.sdk.SearchResultEntry;
040    import com.unboundid.ldap.sdk.SearchResultListener;
041    import com.unboundid.ldap.sdk.SearchResultReference;
042    import com.unboundid.ldap.sdk.SearchScope;
043    import com.unboundid.ldap.sdk.Version;
044    import com.unboundid.util.LDAPCommandLineTool;
045    import com.unboundid.util.StaticUtils;
046    import com.unboundid.util.ThreadSafety;
047    import com.unboundid.util.ThreadSafetyLevel;
048    import com.unboundid.util.WakeableSleeper;
049    import com.unboundid.util.args.ArgumentException;
050    import com.unboundid.util.args.ArgumentParser;
051    import com.unboundid.util.args.BooleanArgument;
052    import com.unboundid.util.args.ControlArgument;
053    import com.unboundid.util.args.DNArgument;
054    import com.unboundid.util.args.IntegerArgument;
055    import 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)
114    public 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    }