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.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 * Adds the arguments used by this program that aren't already provided by the 351 * generic {@code LDAPCommandLineTool} framework. 352 * 353 * @param parser The argument parser to which the arguments should be added. 354 * 355 * @throws ArgumentException If a problem occurs while adding the arguments. 356 */ 357 @Override() 358 public void addNonLDAPArguments(final ArgumentParser parser) 359 throws ArgumentException 360 { 361 // Save a reference to the argument parser. 362 this.parser = parser; 363 364 String description = 365 "Information about a control to include in the bind request."; 366 bindControls = new ControlArgument(null, "bindControl", false, 0, null, 367 description); 368 bindControls.addLongIdentifier("bind-control"); 369 parser.addArgument(bindControls); 370 371 372 description = "Information about a control to include in compare requests."; 373 compareControls = new ControlArgument('J', "control", false, 0, null, 374 description); 375 parser.addArgument(compareControls); 376 } 377 378 379 380 /** 381 * {@inheritDoc} 382 */ 383 @Override() 384 public void doExtendedNonLDAPArgumentValidation() 385 throws ArgumentException 386 { 387 // There must have been at least two trailing arguments provided. The first 388 // must be in the form "attr:value". All subsequent trailing arguments 389 // must be parsable as valid DNs. 390 final List<String> trailingArgs = parser.getTrailingArguments(); 391 if (trailingArgs.size() < 2) 392 { 393 throw new ArgumentException("At least two trailing argument must be " + 394 "provided to specify the assertion criteria in the form " + 395 "'attr:value'. All additional trailing arguments must be the " + 396 "DNs of the entries against which to perform the compare."); 397 } 398 399 final Iterator<String> argIterator = trailingArgs.iterator(); 400 final String ava = argIterator.next(); 401 if (ava.indexOf(':') < 1) 402 { 403 throw new ArgumentException("The first trailing argument value must " + 404 "specify the assertion criteria in the form 'attr:value'."); 405 } 406 407 while (argIterator.hasNext()) 408 { 409 final String arg = argIterator.next(); 410 try 411 { 412 new DN(arg); 413 } 414 catch (final Exception e) 415 { 416 Debug.debugException(e); 417 throw new ArgumentException( 418 "Unable to parse trailing argument '" + arg + "' as a valid DN.", 419 e); 420 } 421 } 422 } 423 424 425 426 /** 427 * {@inheritDoc} 428 */ 429 @Override() 430 protected List<Control> getBindControls() 431 { 432 return bindControls.getValues(); 433 } 434 435 436 437 /** 438 * Performs the actual processing for this tool. In this case, it gets a 439 * connection to the directory server and uses it to perform the requested 440 * comparisons. 441 * 442 * @return The result code for the processing that was performed. 443 */ 444 @Override() 445 public ResultCode doToolProcessing() 446 { 447 // Make sure that at least two trailing arguments were provided, which will 448 // be the attribute value assertion and at least one entry DN. 449 final List<String> trailingArguments = parser.getTrailingArguments(); 450 if (trailingArguments.isEmpty()) 451 { 452 err("No attribute value assertion was provided."); 453 err(); 454 err(parser.getUsageString(StaticUtils.TERMINAL_WIDTH_COLUMNS - 1)); 455 return ResultCode.PARAM_ERROR; 456 } 457 else if (trailingArguments.size() == 1) 458 { 459 err("No target entry DNs were provided."); 460 err(); 461 err(parser.getUsageString(StaticUtils.TERMINAL_WIDTH_COLUMNS - 1)); 462 return ResultCode.PARAM_ERROR; 463 } 464 465 466 // Parse the attribute value assertion. 467 final String avaString = trailingArguments.get(0); 468 final int colonPos = avaString.indexOf(':'); 469 if (colonPos <= 0) 470 { 471 err("Malformed attribute value assertion."); 472 err(); 473 err(parser.getUsageString(StaticUtils.TERMINAL_WIDTH_COLUMNS - 1)); 474 return ResultCode.PARAM_ERROR; 475 } 476 477 final String attributeName = avaString.substring(0, colonPos); 478 final byte[] assertionValueBytes; 479 final int doubleColonPos = avaString.indexOf("::"); 480 if (doubleColonPos == colonPos) 481 { 482 // There are two colons, so it's a base64-encoded assertion value. 483 try 484 { 485 assertionValueBytes = Base64.decode(avaString.substring(colonPos+2)); 486 } 487 catch (ParseException pe) 488 { 489 err("Unable to base64-decode the assertion value: ", 490 pe.getMessage()); 491 err(); 492 err(parser.getUsageString(StaticUtils.TERMINAL_WIDTH_COLUMNS - 1)); 493 return ResultCode.PARAM_ERROR; 494 } 495 } 496 else 497 { 498 // There is only a single colon, so it's a simple UTF-8 string. 499 assertionValueBytes = 500 StaticUtils.getBytes(avaString.substring(colonPos+1)); 501 } 502 503 504 // Get the connection to the directory server. 505 final LDAPConnection connection; 506 try 507 { 508 connection = getConnection(); 509 out("Connected to ", connection.getConnectedAddress(), ':', 510 connection.getConnectedPort()); 511 } 512 catch (LDAPException le) 513 { 514 err("Error connecting to the directory server: ", le.getMessage()); 515 return le.getResultCode(); 516 } 517 518 519 // For each of the target entry DNs, process the compare. 520 ResultCode resultCode = ResultCode.SUCCESS; 521 CompareRequest compareRequest = null; 522 for (int i=1; i < trailingArguments.size(); i++) 523 { 524 final String targetDN = trailingArguments.get(i); 525 if (compareRequest == null) 526 { 527 compareRequest = new CompareRequest(targetDN, attributeName, 528 assertionValueBytes); 529 compareRequest.setControls(compareControls.getValues()); 530 } 531 else 532 { 533 compareRequest.setDN(targetDN); 534 } 535 536 try 537 { 538 out("Processing compare request for entry ", targetDN); 539 final CompareResult result = connection.compare(compareRequest); 540 if (result.compareMatched()) 541 { 542 out("The compare operation matched."); 543 } 544 else 545 { 546 out("The compare operation did not match."); 547 } 548 } 549 catch (LDAPException le) 550 { 551 resultCode = le.getResultCode(); 552 err("An error occurred while processing the request: ", 553 le.getMessage()); 554 err("Result Code: ", le.getResultCode().intValue(), " (", 555 le.getResultCode().getName(), ')'); 556 if (le.getMatchedDN() != null) 557 { 558 err("Matched DN: ", le.getMatchedDN()); 559 } 560 if (le.getReferralURLs() != null) 561 { 562 for (final String url : le.getReferralURLs()) 563 { 564 err("Referral URL: ", url); 565 } 566 } 567 } 568 out(); 569 } 570 571 572 // Close the connection to the directory server and exit. 573 connection.close(); 574 out(); 575 out("Disconnected from the server"); 576 return resultCode; 577 } 578 579 580 581 /** 582 * {@inheritDoc} 583 */ 584 @Override() 585 public LinkedHashMap<String[],String> getExampleUsages() 586 { 587 final LinkedHashMap<String[],String> examples = 588 new LinkedHashMap<String[],String>(); 589 590 final String[] args = 591 { 592 "--hostname", "server.example.com", 593 "--port", "389", 594 "--bindDN", "uid=admin,dc=example,dc=com", 595 "--bindPassword", "password", 596 "givenName:John", 597 "uid=jdoe,ou=People,dc=example,dc=com" 598 }; 599 final String description = 600 "Attempt to determine whether the entry for user " + 601 "'uid=jdoe,ou=People,dc=example,dc=com' has a value of 'John' for " + 602 "the givenName attribute."; 603 examples.put(args, description); 604 605 return examples; 606 } 607}