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 }