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