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.io.Serializable; 027import java.text.ParseException; 028import java.util.Iterator; 029import java.util.LinkedHashMap; 030import java.util.List; 031 032import com.unboundid.ldap.sdk.CompareRequest; 033import com.unboundid.ldap.sdk.CompareResult; 034import com.unboundid.ldap.sdk.Control; 035import com.unboundid.ldap.sdk.DN; 036import com.unboundid.ldap.sdk.LDAPConnection; 037import com.unboundid.ldap.sdk.LDAPException; 038import com.unboundid.ldap.sdk.ResultCode; 039import com.unboundid.ldap.sdk.Version; 040import com.unboundid.util.Base64; 041import com.unboundid.util.Debug; 042import com.unboundid.util.LDAPCommandLineTool; 043import com.unboundid.util.StaticUtils; 044import com.unboundid.util.ThreadSafety; 045import com.unboundid.util.ThreadSafetyLevel; 046import com.unboundid.util.args.ArgumentException; 047import com.unboundid.util.args.ArgumentParser; 048import com.unboundid.util.args.ControlArgument; 049 050 051 052/** 053 * This class provides a simple tool that can be used to perform compare 054 * operations in an LDAP directory server. All of the necessary information is 055 * provided using command line arguments. Supported arguments include those 056 * allowed by the {@link LDAPCommandLineTool} class. In addition, a set of at 057 * least two unnamed trailing arguments must be given. The first argument 058 * should be a string containing the name of the target attribute followed by a 059 * colon and the assertion value to use for that attribute (e.g., 060 * "cn:john doe"). Alternately, the attribute name may be followed by two 061 * colons and the base64-encoded representation of the assertion value 062 * (e.g., "cn:: am9obiBkb2U="). Any subsequent trailing arguments will be the 063 * DN(s) of entries in which to perform the compare operation(s). 064 * <BR><BR> 065 * Some of the APIs demonstrated by this example include: 066 * <UL> 067 * <LI>Argument Parsing (from the {@code com.unboundid.util.args} 068 * package)</LI> 069 * <LI>LDAP Command-Line Tool (from the {@code com.unboundid.util} 070 * package)</LI> 071 * <LI>LDAP Communication (from the {@code com.unboundid.ldap.sdk} 072 * package)</LI> 073 * </UL> 074 */ 075@ThreadSafety(level=ThreadSafetyLevel.NOT_THREADSAFE) 076public final class LDAPCompare 077 extends LDAPCommandLineTool 078 implements Serializable 079{ 080 /** 081 * The serial version UID for this serializable class. 082 */ 083 private static final long serialVersionUID = 719069383330181184L; 084 085 086 087 // The argument parser for this tool. 088 private ArgumentParser parser; 089 090 // The argument used to specify any bind controls that should be used. 091 private ControlArgument bindControls; 092 093 // The argument used to specify any compare controls that should be used. 094 private ControlArgument compareControls; 095 096 097 098 /** 099 * Parse the provided command line arguments and make the appropriate set of 100 * changes. 101 * 102 * @param args The command line arguments provided to this program. 103 */ 104 public static void main(final String[] args) 105 { 106 final ResultCode resultCode = main(args, System.out, System.err); 107 if (resultCode != ResultCode.SUCCESS) 108 { 109 System.exit(resultCode.intValue()); 110 } 111 } 112 113 114 115 /** 116 * Parse the provided command line arguments and make the appropriate set of 117 * changes. 118 * 119 * @param args The command line arguments provided to this program. 120 * @param outStream The output stream to which standard out should be 121 * written. It may be {@code null} if output should be 122 * suppressed. 123 * @param errStream The output stream to which standard error should be 124 * written. It may be {@code null} if error messages 125 * should be suppressed. 126 * 127 * @return A result code indicating whether the processing was successful. 128 */ 129 public static ResultCode main(final String[] args, 130 final OutputStream outStream, 131 final OutputStream errStream) 132 { 133 final LDAPCompare ldapCompare = new LDAPCompare(outStream, errStream); 134 return ldapCompare.runTool(args); 135 } 136 137 138 139 /** 140 * Creates a new instance of this tool. 141 * 142 * @param outStream The output stream to which standard out should be 143 * written. It may be {@code null} if output should be 144 * suppressed. 145 * @param errStream The output stream to which standard error should be 146 * written. It may be {@code null} if error messages 147 * should be suppressed. 148 */ 149 public LDAPCompare(final OutputStream outStream, final OutputStream errStream) 150 { 151 super(outStream, errStream); 152 } 153 154 155 156 /** 157 * Retrieves the name for this tool. 158 * 159 * @return The name for this tool. 160 */ 161 @Override() 162 public String getToolName() 163 { 164 return "ldapcompare"; 165 } 166 167 168 169 /** 170 * Retrieves the description for this tool. 171 * 172 * @return The description for this tool. 173 */ 174 @Override() 175 public String getToolDescription() 176 { 177 return "Process compare operations in LDAP directory server."; 178 } 179 180 181 182 /** 183 * Retrieves the version string for this tool. 184 * 185 * @return The version string for this tool. 186 */ 187 @Override() 188 public String getToolVersion() 189 { 190 return Version.NUMERIC_VERSION_STRING; 191 } 192 193 194 195 /** 196 * Retrieves the minimum number of unnamed trailing arguments that are 197 * required. 198 * 199 * @return Two, to indicate that at least two trailing arguments 200 * (representing the attribute value assertion and at least one entry 201 * DN) must be provided. 202 */ 203 @Override() 204 public int getMinTrailingArguments() 205 { 206 return 2; 207 } 208 209 210 211 /** 212 * Retrieves the maximum number of unnamed trailing arguments that are 213 * allowed. 214 * 215 * @return A negative value to indicate that any number of trailing arguments 216 * may be provided. 217 */ 218 @Override() 219 public int getMaxTrailingArguments() 220 { 221 return -1; 222 } 223 224 225 226 /** 227 * Retrieves a placeholder string that may be used to indicate what kinds of 228 * trailing arguments are allowed. 229 * 230 * @return A placeholder string that may be used to indicate what kinds of 231 * trailing arguments are allowed. 232 */ 233 @Override() 234 public String getTrailingArgumentsPlaceholder() 235 { 236 return "attr:value dn1 [dn2 [dn3 [...]]]"; 237 } 238 239 240 241 /** 242 * Indicates whether this tool should provide support for an interactive mode, 243 * in which the tool offers a mode in which the arguments can be provided in 244 * a text-driven menu rather than requiring them to be given on the command 245 * line. If interactive mode is supported, it may be invoked using the 246 * "--interactive" argument. Alternately, if interactive mode is supported 247 * and {@link #defaultsToInteractiveMode()} returns {@code true}, then 248 * interactive mode may be invoked by simply launching the tool without any 249 * arguments. 250 * 251 * @return {@code true} if this tool supports interactive mode, or 252 * {@code false} if not. 253 */ 254 @Override() 255 public boolean supportsInteractiveMode() 256 { 257 return true; 258 } 259 260 261 262 /** 263 * Indicates whether this tool defaults to launching in interactive mode if 264 * the tool is invoked without any command-line arguments. This will only be 265 * used if {@link #supportsInteractiveMode()} returns {@code true}. 266 * 267 * @return {@code true} if this tool defaults to using interactive mode if 268 * launched without any command-line arguments, or {@code false} if 269 * not. 270 */ 271 @Override() 272 public boolean defaultsToInteractiveMode() 273 { 274 return true; 275 } 276 277 278 279 /** 280 * Indicates whether this tool should provide arguments for redirecting output 281 * to a file. If this method returns {@code true}, then the tool will offer 282 * an "--outputFile" argument that will specify the path to a file to which 283 * all standard output and standard error content will be written, and it will 284 * also offer a "--teeToStandardOut" argument that can only be used if the 285 * "--outputFile" argument is present and will cause all output to be written 286 * to both the specified output file and to standard output. 287 * 288 * @return {@code true} if this tool should provide arguments for redirecting 289 * output to a file, or {@code false} if not. 290 */ 291 @Override() 292 protected boolean supportsOutputFile() 293 { 294 return true; 295 } 296 297 298 299 /** 300 * Indicates whether this tool should default to interactively prompting for 301 * the bind password if a password is required but no argument was provided 302 * to indicate how to get the password. 303 * 304 * @return {@code true} if this tool should default to interactively 305 * prompting for the bind password, or {@code false} if not. 306 */ 307 @Override() 308 protected boolean defaultToPromptForBindPassword() 309 { 310 return true; 311 } 312 313 314 315 /** 316 * Indicates whether this tool supports the use of a properties file for 317 * specifying default values for arguments that aren't specified on the 318 * command line. 319 * 320 * @return {@code true} if this tool supports the use of a properties file 321 * for specifying default values for arguments that aren't specified 322 * on the command line, or {@code false} if not. 323 */ 324 @Override() 325 public boolean supportsPropertiesFile() 326 { 327 return true; 328 } 329 330 331 332 /** 333 * Indicates whether the LDAP-specific arguments should include alternate 334 * versions of all long identifiers that consist of multiple words so that 335 * they are available in both camelCase and dash-separated versions. 336 * 337 * @return {@code true} if this tool should provide multiple versions of 338 * long identifiers for LDAP-specific arguments, or {@code false} if 339 * not. 340 */ 341 @Override() 342 protected boolean includeAlternateLongIdentifiers() 343 { 344 return true; 345 } 346 347 348 349 /** 350 * Indicates whether this tool should provide a command-line argument that 351 * allows for low-level SSL debugging. If this returns {@code true}, then an 352 * "--enableSSLDebugging}" argument will be added that sets the 353 * "javax.net.debug" system property to "all" before attempting any 354 * communication. 355 * 356 * @return {@code true} if this tool should offer an "--enableSSLDebugging" 357 * argument, or {@code false} if not. 358 */ 359 @Override() 360 protected boolean supportsSSLDebugging() 361 { 362 return true; 363 } 364 365 366 367 /** 368 * Adds the arguments used by this program that aren't already provided by the 369 * generic {@code LDAPCommandLineTool} framework. 370 * 371 * @param parser The argument parser to which the arguments should be added. 372 * 373 * @throws ArgumentException If a problem occurs while adding the arguments. 374 */ 375 @Override() 376 public void addNonLDAPArguments(final ArgumentParser parser) 377 throws ArgumentException 378 { 379 // Save a reference to the argument parser. 380 this.parser = parser; 381 382 String description = 383 "Information about a control to include in the bind request."; 384 bindControls = new ControlArgument(null, "bindControl", false, 0, null, 385 description); 386 bindControls.addLongIdentifier("bind-control", true); 387 parser.addArgument(bindControls); 388 389 390 description = "Information about a control to include in compare requests."; 391 compareControls = new ControlArgument('J', "control", false, 0, null, 392 description); 393 parser.addArgument(compareControls); 394 } 395 396 397 398 /** 399 * {@inheritDoc} 400 */ 401 @Override() 402 public void doExtendedNonLDAPArgumentValidation() 403 throws ArgumentException 404 { 405 // There must have been at least two trailing arguments provided. The first 406 // must be in the form "attr:value". All subsequent trailing arguments 407 // must be parsable as valid DNs. 408 final List<String> trailingArgs = parser.getTrailingArguments(); 409 if (trailingArgs.size() < 2) 410 { 411 throw new ArgumentException("At least two trailing argument must be " + 412 "provided to specify the assertion criteria in the form " + 413 "'attr:value'. All additional trailing arguments must be the " + 414 "DNs of the entries against which to perform the compare."); 415 } 416 417 final Iterator<String> argIterator = trailingArgs.iterator(); 418 final String ava = argIterator.next(); 419 if (ava.indexOf(':') < 1) 420 { 421 throw new ArgumentException("The first trailing argument value must " + 422 "specify the assertion criteria in the form 'attr:value'."); 423 } 424 425 while (argIterator.hasNext()) 426 { 427 final String arg = argIterator.next(); 428 try 429 { 430 new DN(arg); 431 } 432 catch (final Exception e) 433 { 434 Debug.debugException(e); 435 throw new ArgumentException( 436 "Unable to parse trailing argument '" + arg + "' as a valid DN.", 437 e); 438 } 439 } 440 } 441 442 443 444 /** 445 * {@inheritDoc} 446 */ 447 @Override() 448 protected List<Control> getBindControls() 449 { 450 return bindControls.getValues(); 451 } 452 453 454 455 /** 456 * Performs the actual processing for this tool. In this case, it gets a 457 * connection to the directory server and uses it to perform the requested 458 * comparisons. 459 * 460 * @return The result code for the processing that was performed. 461 */ 462 @Override() 463 public ResultCode doToolProcessing() 464 { 465 // Make sure that at least two trailing arguments were provided, which will 466 // be the attribute value assertion and at least one entry DN. 467 final List<String> trailingArguments = parser.getTrailingArguments(); 468 if (trailingArguments.isEmpty()) 469 { 470 err("No attribute value assertion was provided."); 471 err(); 472 err(parser.getUsageString(StaticUtils.TERMINAL_WIDTH_COLUMNS - 1)); 473 return ResultCode.PARAM_ERROR; 474 } 475 else if (trailingArguments.size() == 1) 476 { 477 err("No target entry DNs were provided."); 478 err(); 479 err(parser.getUsageString(StaticUtils.TERMINAL_WIDTH_COLUMNS - 1)); 480 return ResultCode.PARAM_ERROR; 481 } 482 483 484 // Parse the attribute value assertion. 485 final String avaString = trailingArguments.get(0); 486 final int colonPos = avaString.indexOf(':'); 487 if (colonPos <= 0) 488 { 489 err("Malformed attribute value assertion."); 490 err(); 491 err(parser.getUsageString(StaticUtils.TERMINAL_WIDTH_COLUMNS - 1)); 492 return ResultCode.PARAM_ERROR; 493 } 494 495 final String attributeName = avaString.substring(0, colonPos); 496 final byte[] assertionValueBytes; 497 final int doubleColonPos = avaString.indexOf("::"); 498 if (doubleColonPos == colonPos) 499 { 500 // There are two colons, so it's a base64-encoded assertion value. 501 try 502 { 503 assertionValueBytes = Base64.decode(avaString.substring(colonPos+2)); 504 } 505 catch (final ParseException pe) 506 { 507 err("Unable to base64-decode the assertion value: ", 508 pe.getMessage()); 509 err(); 510 err(parser.getUsageString(StaticUtils.TERMINAL_WIDTH_COLUMNS - 1)); 511 return ResultCode.PARAM_ERROR; 512 } 513 } 514 else 515 { 516 // There is only a single colon, so it's a simple UTF-8 string. 517 assertionValueBytes = 518 StaticUtils.getBytes(avaString.substring(colonPos+1)); 519 } 520 521 522 // Get the connection to the directory server. 523 final LDAPConnection connection; 524 try 525 { 526 connection = getConnection(); 527 out("Connected to ", connection.getConnectedAddress(), ':', 528 connection.getConnectedPort()); 529 } 530 catch (final LDAPException le) 531 { 532 err("Error connecting to the directory server: ", le.getMessage()); 533 return le.getResultCode(); 534 } 535 536 537 // For each of the target entry DNs, process the compare. 538 ResultCode resultCode = ResultCode.SUCCESS; 539 CompareRequest compareRequest = null; 540 for (int i=1; i < trailingArguments.size(); i++) 541 { 542 final String targetDN = trailingArguments.get(i); 543 if (compareRequest == null) 544 { 545 compareRequest = new CompareRequest(targetDN, attributeName, 546 assertionValueBytes); 547 compareRequest.setControls(compareControls.getValues()); 548 } 549 else 550 { 551 compareRequest.setDN(targetDN); 552 } 553 554 try 555 { 556 out("Processing compare request for entry ", targetDN); 557 final CompareResult result = connection.compare(compareRequest); 558 if (result.compareMatched()) 559 { 560 out("The compare operation matched."); 561 } 562 else 563 { 564 out("The compare operation did not match."); 565 } 566 } 567 catch (final LDAPException le) 568 { 569 resultCode = le.getResultCode(); 570 err("An error occurred while processing the request: ", 571 le.getMessage()); 572 err("Result Code: ", le.getResultCode().intValue(), " (", 573 le.getResultCode().getName(), ')'); 574 if (le.getMatchedDN() != null) 575 { 576 err("Matched DN: ", le.getMatchedDN()); 577 } 578 if (le.getReferralURLs() != null) 579 { 580 for (final String url : le.getReferralURLs()) 581 { 582 err("Referral URL: ", url); 583 } 584 } 585 } 586 out(); 587 } 588 589 590 // Close the connection to the directory server and exit. 591 connection.close(); 592 out(); 593 out("Disconnected from the server"); 594 return resultCode; 595 } 596 597 598 599 /** 600 * {@inheritDoc} 601 */ 602 @Override() 603 public LinkedHashMap<String[],String> getExampleUsages() 604 { 605 final LinkedHashMap<String[],String> examples = 606 new LinkedHashMap<>(StaticUtils.computeMapCapacity(1)); 607 608 final String[] args = 609 { 610 "--hostname", "server.example.com", 611 "--port", "389", 612 "--bindDN", "uid=admin,dc=example,dc=com", 613 "--bindPassword", "password", 614 "givenName:John", 615 "uid=jdoe,ou=People,dc=example,dc=com" 616 }; 617 final String description = 618 "Attempt to determine whether the entry for user " + 619 "'uid=jdoe,ou=People,dc=example,dc=com' has a value of 'John' for " + 620 "the givenName attribute."; 621 examples.put(args, description); 622 623 return examples; 624 } 625}