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