001/* 002 * Copyright 2008-2019 Ping Identity Corporation 003 * All Rights Reserved. 004 */ 005/* 006 * Copyright (C) 2008-2019 Ping Identity Corporation 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 * @see com.unboundid.ldap.sdk.unboundidds.tools.LDAPSearch 115 */ 116@ThreadSafety(level=ThreadSafetyLevel.NOT_THREADSAFE) 117public final class LDAPSearch 118 extends LDAPCommandLineTool 119 implements SearchResultListener 120{ 121 /** 122 * The date formatter that should be used when writing timestamps. 123 */ 124 private static final SimpleDateFormat DATE_FORMAT = 125 new SimpleDateFormat("dd/MMM/yyyy:HH:mm:ss.SSS"); 126 127 128 129 /** 130 * The serial version UID for this serializable class. 131 */ 132 private static final long serialVersionUID = 7465188734621412477L; 133 134 135 136 // The argument parser used by this program. 137 private ArgumentParser parser; 138 139 // Indicates whether the search should be repeated. 140 private boolean repeat; 141 142 // The argument used to indicate whether to follow referrals. 143 private BooleanArgument followReferrals; 144 145 // The argument used to indicate whether to use terse mode. 146 private BooleanArgument terseMode; 147 148 // The argument used to specify any bind controls that should be used. 149 private ControlArgument bindControls; 150 151 // The argument used to specify any search controls that should be used. 152 private ControlArgument searchControls; 153 154 // The number of times to perform the search. 155 private IntegerArgument numSearches; 156 157 // The interval in milliseconds between repeated searches. 158 private IntegerArgument repeatIntervalMillis; 159 160 // The argument used to specify the base DN for the search. 161 private DNArgument baseDN; 162 163 // The argument used to specify the scope for the search. 164 private ScopeArgument scopeArg; 165 166 167 168 /** 169 * Parse the provided command line arguments and make the appropriate set of 170 * changes. 171 * 172 * @param args The command line arguments provided to this program. 173 */ 174 public static void main(final String[] args) 175 { 176 final ResultCode resultCode = main(args, System.out, System.err); 177 if (resultCode != ResultCode.SUCCESS) 178 { 179 System.exit(resultCode.intValue()); 180 } 181 } 182 183 184 185 /** 186 * Parse the provided command line arguments and make the appropriate set of 187 * changes. 188 * 189 * @param args The command line arguments provided to this program. 190 * @param outStream The output stream to which standard out should be 191 * written. It may be {@code null} if output should be 192 * suppressed. 193 * @param errStream The output stream to which standard error should be 194 * written. It may be {@code null} if error messages 195 * should be suppressed. 196 * 197 * @return A result code indicating whether the processing was successful. 198 */ 199 public static ResultCode main(final String[] args, 200 final OutputStream outStream, 201 final OutputStream errStream) 202 { 203 final LDAPSearch ldapSearch = new LDAPSearch(outStream, errStream); 204 return ldapSearch.runTool(args); 205 } 206 207 208 209 /** 210 * Creates a new instance of this tool. 211 * 212 * @param outStream The output stream to which standard out should be 213 * written. It may be {@code null} if output should be 214 * suppressed. 215 * @param errStream The output stream to which standard error should be 216 * written. It may be {@code null} if error messages 217 * should be suppressed. 218 */ 219 public LDAPSearch(final OutputStream outStream, final OutputStream errStream) 220 { 221 super(outStream, errStream); 222 } 223 224 225 226 /** 227 * Retrieves the name for this tool. 228 * 229 * @return The name for this tool. 230 */ 231 @Override() 232 public String getToolName() 233 { 234 return "ldapsearch"; 235 } 236 237 238 239 /** 240 * Retrieves the description for this tool. 241 * 242 * @return The description for this tool. 243 */ 244 @Override() 245 public String getToolDescription() 246 { 247 return "Search an LDAP directory server."; 248 } 249 250 251 252 /** 253 * Retrieves the version string for this tool. 254 * 255 * @return The version string for this tool. 256 */ 257 @Override() 258 public String getToolVersion() 259 { 260 return Version.NUMERIC_VERSION_STRING; 261 } 262 263 264 265 /** 266 * Retrieves the minimum number of unnamed trailing arguments that are 267 * required. 268 * 269 * @return One, to indicate that at least one trailing argument (representing 270 * the search filter) must be provided. 271 */ 272 @Override() 273 public int getMinTrailingArguments() 274 { 275 return 1; 276 } 277 278 279 280 /** 281 * Retrieves the maximum number of unnamed trailing arguments that are 282 * allowed. 283 * 284 * @return A negative value to indicate that any number of trailing arguments 285 * may be provided. 286 */ 287 @Override() 288 public int getMaxTrailingArguments() 289 { 290 return -1; 291 } 292 293 294 295 /** 296 * Retrieves a placeholder string that may be used to indicate what kinds of 297 * trailing arguments are allowed. 298 * 299 * @return A placeholder string that may be used to indicate what kinds of 300 * trailing arguments are allowed. 301 */ 302 @Override() 303 public String getTrailingArgumentsPlaceholder() 304 { 305 return "{filter} [attr1 [attr2 [...]]]"; 306 } 307 308 309 310 /** 311 * Indicates whether this tool should provide support for an interactive mode, 312 * in which the tool offers a mode in which the arguments can be provided in 313 * a text-driven menu rather than requiring them to be given on the command 314 * line. If interactive mode is supported, it may be invoked using the 315 * "--interactive" argument. Alternately, if interactive mode is supported 316 * and {@link #defaultsToInteractiveMode()} returns {@code true}, then 317 * interactive mode may be invoked by simply launching the tool without any 318 * arguments. 319 * 320 * @return {@code true} if this tool supports interactive mode, or 321 * {@code false} if not. 322 */ 323 @Override() 324 public boolean supportsInteractiveMode() 325 { 326 return true; 327 } 328 329 330 331 /** 332 * Indicates whether this tool defaults to launching in interactive mode if 333 * the tool is invoked without any command-line arguments. This will only be 334 * used if {@link #supportsInteractiveMode()} returns {@code true}. 335 * 336 * @return {@code true} if this tool defaults to using interactive mode if 337 * launched without any command-line arguments, or {@code false} if 338 * not. 339 */ 340 @Override() 341 public boolean defaultsToInteractiveMode() 342 { 343 return true; 344 } 345 346 347 348 /** 349 * Indicates whether this tool should provide arguments for redirecting output 350 * to a file. If this method returns {@code true}, then the tool will offer 351 * an "--outputFile" argument that will specify the path to a file to which 352 * all standard output and standard error content will be written, and it will 353 * also offer a "--teeToStandardOut" argument that can only be used if the 354 * "--outputFile" argument is present and will cause all output to be written 355 * to both the specified output file and to standard output. 356 * 357 * @return {@code true} if this tool should provide arguments for redirecting 358 * output to a file, or {@code false} if not. 359 */ 360 @Override() 361 protected boolean supportsOutputFile() 362 { 363 return true; 364 } 365 366 367 368 /** 369 * Indicates whether this tool supports the use of a properties file for 370 * specifying default values for arguments that aren't specified on the 371 * command line. 372 * 373 * @return {@code true} if this tool supports the use of a properties file 374 * for specifying default values for arguments that aren't specified 375 * on the command line, or {@code false} if not. 376 */ 377 @Override() 378 public boolean supportsPropertiesFile() 379 { 380 return true; 381 } 382 383 384 385 /** 386 * Indicates whether this tool should default to interactively prompting for 387 * the bind password if a password is required but no argument was provided 388 * to indicate how to get the password. 389 * 390 * @return {@code true} if this tool should default to interactively 391 * prompting for the bind password, or {@code false} if not. 392 */ 393 @Override() 394 protected boolean defaultToPromptForBindPassword() 395 { 396 return true; 397 } 398 399 400 401 /** 402 * Indicates whether the LDAP-specific arguments should include alternate 403 * versions of all long identifiers that consist of multiple words so that 404 * they are available in both camelCase and dash-separated versions. 405 * 406 * @return {@code true} if this tool should provide multiple versions of 407 * long identifiers for LDAP-specific arguments, or {@code false} if 408 * not. 409 */ 410 @Override() 411 protected boolean includeAlternateLongIdentifiers() 412 { 413 return true; 414 } 415 416 417 418 /** 419 * Indicates whether this tool should provide a command-line argument that 420 * allows for low-level SSL debugging. If this returns {@code true}, then an 421 * "--enableSSLDebugging}" argument will be added that sets the 422 * "javax.net.debug" system property to "all" before attempting any 423 * communication. 424 * 425 * @return {@code true} if this tool should offer an "--enableSSLDebugging" 426 * argument, or {@code false} if not. 427 */ 428 @Override() 429 protected boolean supportsSSLDebugging() 430 { 431 return true; 432 } 433 434 435 436 /** 437 * Adds the arguments used by this program that aren't already provided by the 438 * generic {@code LDAPCommandLineTool} framework. 439 * 440 * @param parser The argument parser to which the arguments should be added. 441 * 442 * @throws ArgumentException If a problem occurs while adding the arguments. 443 */ 444 @Override() 445 public void addNonLDAPArguments(final ArgumentParser parser) 446 throws ArgumentException 447 { 448 this.parser = parser; 449 450 String description = "The base DN to use for the search. This must be " + 451 "provided."; 452 baseDN = new DNArgument('b', "baseDN", true, 1, "{dn}", description); 453 baseDN.addLongIdentifier("base-dn", true); 454 parser.addArgument(baseDN); 455 456 457 description = "The scope to use for the search. It should be 'base', " + 458 "'one', 'sub', or 'subord'. If this is not provided, then " + 459 "a default scope of 'sub' will be used."; 460 scopeArg = new ScopeArgument('s', "scope", false, "{scope}", description, 461 SearchScope.SUB); 462 parser.addArgument(scopeArg); 463 464 465 description = "Follow any referrals encountered during processing."; 466 followReferrals = new BooleanArgument('R', "followReferrals", description); 467 followReferrals.addLongIdentifier("follow-referrals", true); 468 parser.addArgument(followReferrals); 469 470 471 description = "Information about a control to include in the bind request."; 472 bindControls = new ControlArgument(null, "bindControl", false, 0, null, 473 description); 474 bindControls.addLongIdentifier("bind-control", true); 475 parser.addArgument(bindControls); 476 477 478 description = "Information about a control to include in search requests."; 479 searchControls = new ControlArgument('J', "control", false, 0, null, 480 description); 481 parser.addArgument(searchControls); 482 483 484 description = "Generate terse output with minimal additional information."; 485 terseMode = new BooleanArgument('t', "terse", description); 486 parser.addArgument(terseMode); 487 488 489 description = "Specifies the length of time in milliseconds to sleep " + 490 "before repeating the same search. If this is not " + 491 "provided, then the search will only be performed once."; 492 repeatIntervalMillis = new IntegerArgument('i', "repeatIntervalMillis", 493 false, 1, "{millis}", 494 description, 0, 495 Integer.MAX_VALUE); 496 repeatIntervalMillis.addLongIdentifier("repeat-interval-millis", true); 497 parser.addArgument(repeatIntervalMillis); 498 499 500 description = "Specifies the number of times that the search should be " + 501 "performed. If this argument is present, then the " + 502 "--repeatIntervalMillis argument must also be provided to " + 503 "specify the length of time between searches. If " + 504 "--repeatIntervalMillis is used without --numSearches, " + 505 "then the search will be repeated until the tool is " + 506 "interrupted."; 507 numSearches = new IntegerArgument('n', "numSearches", false, 1, "{count}", 508 description, 1, Integer.MAX_VALUE); 509 numSearches.addLongIdentifier("num-searches", true); 510 parser.addArgument(numSearches); 511 parser.addDependentArgumentSet(numSearches, repeatIntervalMillis); 512 } 513 514 515 516 /** 517 * {@inheritDoc} 518 */ 519 @Override() 520 public void doExtendedNonLDAPArgumentValidation() 521 throws ArgumentException 522 { 523 // There must have been at least one trailing argument provided, and it must 524 // be parsable as a valid search filter. 525 if (parser.getTrailingArguments().isEmpty()) 526 { 527 throw new ArgumentException("At least one trailing argument must be " + 528 "provided to specify the search filter. Additional trailing " + 529 "arguments are allowed to specify the attributes to return in " + 530 "search result entries."); 531 } 532 533 try 534 { 535 Filter.create(parser.getTrailingArguments().get(0)); 536 } 537 catch (final Exception e) 538 { 539 Debug.debugException(e); 540 throw new ArgumentException( 541 "The first trailing argument value could not be parsed as a valid " + 542 "LDAP search filter.", 543 e); 544 } 545 } 546 547 548 549 /** 550 * {@inheritDoc} 551 */ 552 @Override() 553 protected List<Control> getBindControls() 554 { 555 return bindControls.getValues(); 556 } 557 558 559 560 /** 561 * Performs the actual processing for this tool. In this case, it gets a 562 * connection to the directory server and uses it to perform the requested 563 * search. 564 * 565 * @return The result code for the processing that was performed. 566 */ 567 @Override() 568 public ResultCode doToolProcessing() 569 { 570 // Make sure that at least one trailing argument was provided, which will be 571 // the filter. If there were any other arguments, then they will be the 572 // attributes to return. 573 final List<String> trailingArguments = parser.getTrailingArguments(); 574 if (trailingArguments.isEmpty()) 575 { 576 err("No search filter was provided."); 577 err(); 578 err(parser.getUsageString(StaticUtils.TERMINAL_WIDTH_COLUMNS - 1)); 579 return ResultCode.PARAM_ERROR; 580 } 581 582 final Filter filter; 583 try 584 { 585 filter = Filter.create(trailingArguments.get(0)); 586 } 587 catch (final LDAPException le) 588 { 589 err("Invalid search filter: ", le.getMessage()); 590 return le.getResultCode(); 591 } 592 593 final String[] attributesToReturn; 594 if (trailingArguments.size() > 1) 595 { 596 attributesToReturn = new String[trailingArguments.size() - 1]; 597 for (int i=1; i < trailingArguments.size(); i++) 598 { 599 attributesToReturn[i-1] = trailingArguments.get(i); 600 } 601 } 602 else 603 { 604 attributesToReturn = StaticUtils.NO_STRINGS; 605 } 606 607 608 // Get the connection to the directory server. 609 final LDAPConnection connection; 610 try 611 { 612 connection = getConnection(); 613 if (! terseMode.isPresent()) 614 { 615 out("# Connected to ", connection.getConnectedAddress(), ':', 616 connection.getConnectedPort()); 617 } 618 } 619 catch (final LDAPException le) 620 { 621 err("Error connecting to the directory server: ", le.getMessage()); 622 return le.getResultCode(); 623 } 624 625 626 // Create a search request with the appropriate information and process it 627 // in the server. Note that in this case, we're creating a search result 628 // listener to handle the results since there could potentially be a lot of 629 // them. 630 final SearchRequest searchRequest = 631 new SearchRequest(this, baseDN.getStringValue(), scopeArg.getValue(), 632 DereferencePolicy.NEVER, 0, 0, false, filter, 633 attributesToReturn); 634 searchRequest.setFollowReferrals(followReferrals.isPresent()); 635 636 final List<Control> controlList = searchControls.getValues(); 637 if (controlList != null) 638 { 639 searchRequest.setControls(controlList); 640 } 641 642 643 final boolean infinite; 644 final int numIterations; 645 if (repeatIntervalMillis.isPresent()) 646 { 647 repeat = true; 648 649 if (numSearches.isPresent()) 650 { 651 infinite = false; 652 numIterations = numSearches.getValue(); 653 } 654 else 655 { 656 infinite = true; 657 numIterations = Integer.MAX_VALUE; 658 } 659 } 660 else 661 { 662 infinite = false; 663 repeat = false; 664 numIterations = 1; 665 } 666 667 ResultCode resultCode = ResultCode.SUCCESS; 668 long lastSearchTime = System.currentTimeMillis(); 669 final WakeableSleeper sleeper = new WakeableSleeper(); 670 for (int i=0; (infinite || (i < numIterations)); i++) 671 { 672 if (repeat && (i > 0)) 673 { 674 final long sleepTime = 675 (lastSearchTime + repeatIntervalMillis.getValue()) - 676 System.currentTimeMillis(); 677 if (sleepTime > 0) 678 { 679 sleeper.sleep(sleepTime); 680 } 681 lastSearchTime = System.currentTimeMillis(); 682 } 683 684 try 685 { 686 final SearchResult searchResult = connection.search(searchRequest); 687 if ((! repeat) && (! terseMode.isPresent())) 688 { 689 out("# The search operation was processed successfully."); 690 out("# Entries returned: ", searchResult.getEntryCount()); 691 out("# References returned: ", searchResult.getReferenceCount()); 692 } 693 } 694 catch (final LDAPException le) 695 { 696 err("An error occurred while processing the search: ", 697 le.getMessage()); 698 err("Result Code: ", le.getResultCode().intValue(), " (", 699 le.getResultCode().getName(), ')'); 700 if (le.getMatchedDN() != null) 701 { 702 err("Matched DN: ", le.getMatchedDN()); 703 } 704 705 if (le.getReferralURLs() != null) 706 { 707 for (final String url : le.getReferralURLs()) 708 { 709 err("Referral URL: ", url); 710 } 711 } 712 713 if (resultCode == ResultCode.SUCCESS) 714 { 715 resultCode = le.getResultCode(); 716 } 717 718 if (! le.getResultCode().isConnectionUsable()) 719 { 720 break; 721 } 722 } 723 } 724 725 726 // Close the connection to the directory server and exit. 727 connection.close(); 728 if (! terseMode.isPresent()) 729 { 730 out(); 731 out("# Disconnected from the server"); 732 } 733 return resultCode; 734 } 735 736 737 738 /** 739 * Indicates that the provided search result entry was returned from the 740 * associated search operation. 741 * 742 * @param entry The entry that was returned from the search. 743 */ 744 @Override() 745 public void searchEntryReturned(final SearchResultEntry entry) 746 { 747 if (repeat) 748 { 749 out("# ", DATE_FORMAT.format(new Date())); 750 } 751 752 out(entry.toLDIFString()); 753 } 754 755 756 757 /** 758 * Indicates that the provided search result reference was returned from the 759 * associated search operation. 760 * 761 * @param reference The reference that was returned from the search. 762 */ 763 @Override() 764 public void searchReferenceReturned(final SearchResultReference reference) 765 { 766 if (repeat) 767 { 768 out("# ", DATE_FORMAT.format(new Date())); 769 } 770 771 out(reference.toString()); 772 } 773 774 775 776 /** 777 * {@inheritDoc} 778 */ 779 @Override() 780 public LinkedHashMap<String[],String> getExampleUsages() 781 { 782 final LinkedHashMap<String[],String> examples = 783 new LinkedHashMap<>(StaticUtils.computeMapCapacity(1)); 784 785 final String[] args = 786 { 787 "--hostname", "server.example.com", 788 "--port", "389", 789 "--bindDN", "uid=admin,dc=example,dc=com", 790 "--bindPassword", "password", 791 "--baseDN", "dc=example,dc=com", 792 "--scope", "sub", 793 "(uid=jdoe)", 794 "givenName", 795 "sn", 796 "mail" 797 }; 798 final String description = 799 "Perform a search in the directory server to find all entries " + 800 "matching the filter '(uid=jdoe)' anywhere below " + 801 "'dc=example,dc=com'. Include only the givenName, sn, and mail " + 802 "attributes in the entries that are returned."; 803 examples.put(args, description); 804 805 return examples; 806 } 807}