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 }