001/* 002 * Copyright 2008-2017 UnboundID Corp. 003 * All Rights Reserved. 004 */ 005/* 006 * Copyright (C) 2008-2017 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.Debug; 045import com.unboundid.util.LDAPCommandLineTool; 046import com.unboundid.util.StaticUtils; 047import com.unboundid.util.ThreadSafety; 048import com.unboundid.util.ThreadSafetyLevel; 049import com.unboundid.util.WakeableSleeper; 050import com.unboundid.util.args.ArgumentException; 051import com.unboundid.util.args.ArgumentParser; 052import com.unboundid.util.args.BooleanArgument; 053import com.unboundid.util.args.ControlArgument; 054import com.unboundid.util.args.DNArgument; 055import com.unboundid.util.args.IntegerArgument; 056import 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) 115public 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}