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