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