001/* 002 * Copyright 2008-2016 UnboundID Corp. 003 * All Rights Reserved. 004 */ 005/* 006 * Copyright (C) 2008-2016 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 supports the use of a properties file for 281 * specifying default values for arguments that aren't specified on the 282 * command line. 283 * 284 * @return {@code true} if this tool supports the use of a properties file 285 * for specifying default values for arguments that aren't specified 286 * on the command line, or {@code false} if not. 287 */ 288 @Override() 289 public boolean supportsPropertiesFile() 290 { 291 return true; 292 } 293 294 295 296 /** 297 * Indicates whether the LDAP-specific arguments should include alternate 298 * versions of all long identifiers that consist of multiple words so that 299 * they are available in both camelCase and dash-separated versions. 300 * 301 * @return {@code true} if this tool should provide multiple versions of 302 * long identifiers for LDAP-specific arguments, or {@code false} if 303 * not. 304 */ 305 @Override() 306 protected boolean includeAlternateLongIdentifiers() 307 { 308 return true; 309 } 310 311 312 313 /** 314 * Adds the arguments used by this program that aren't already provided by the 315 * generic {@code LDAPCommandLineTool} framework. 316 * 317 * @param parser The argument parser to which the arguments should be added. 318 * 319 * @throws ArgumentException If a problem occurs while adding the arguments. 320 */ 321 @Override() 322 public void addNonLDAPArguments(final ArgumentParser parser) 323 throws ArgumentException 324 { 325 // Save a reference to the argument parser. 326 this.parser = parser; 327 328 String description = 329 "Information about a control to include in the bind request."; 330 bindControls = new ControlArgument(null, "bindControl", false, 0, null, 331 description); 332 bindControls.addLongIdentifier("bind-control"); 333 parser.addArgument(bindControls); 334 335 336 description = "Information about a control to include in compare requests."; 337 compareControls = new ControlArgument('J', "control", false, 0, null, 338 description); 339 parser.addArgument(compareControls); 340 } 341 342 343 344 /** 345 * {@inheritDoc} 346 */ 347 @Override() 348 public void doExtendedNonLDAPArgumentValidation() 349 throws ArgumentException 350 { 351 // There must have been at least two trailing arguments provided. The first 352 // must be in the form "attr:value". All subsequent trailing arguments 353 // must be parsable as valid DNs. 354 final List<String> trailingArgs = parser.getTrailingArguments(); 355 if (trailingArgs.size() < 2) 356 { 357 throw new ArgumentException("At least two trailing argument must be " + 358 "provided to specify the assertion criteria in the form " + 359 "'attr:value'. All additional trailing arguments must be the " + 360 "DNs of the entries against which to perform the compare."); 361 } 362 363 final Iterator<String> argIterator = trailingArgs.iterator(); 364 final String ava = argIterator.next(); 365 if (ava.indexOf(':') < 1) 366 { 367 throw new ArgumentException("The first trailing argument value must " + 368 "specify the assertion criteria in the form 'attr:value'."); 369 } 370 371 while (argIterator.hasNext()) 372 { 373 final String arg = argIterator.next(); 374 try 375 { 376 new DN(arg); 377 } 378 catch (final Exception e) 379 { 380 Debug.debugException(e); 381 throw new ArgumentException( 382 "Unable to parse trailing argument '" + arg + "' as a valid DN.", 383 e); 384 } 385 } 386 } 387 388 389 390 /** 391 * {@inheritDoc} 392 */ 393 @Override() 394 protected List<Control> getBindControls() 395 { 396 return bindControls.getValues(); 397 } 398 399 400 401 /** 402 * Performs the actual processing for this tool. In this case, it gets a 403 * connection to the directory server and uses it to perform the requested 404 * comparisons. 405 * 406 * @return The result code for the processing that was performed. 407 */ 408 @Override() 409 public ResultCode doToolProcessing() 410 { 411 // Make sure that at least two trailing arguments were provided, which will 412 // be the attribute value assertion and at least one entry DN. 413 final List<String> trailingArguments = parser.getTrailingArguments(); 414 if (trailingArguments.isEmpty()) 415 { 416 err("No attribute value assertion was provided."); 417 err(); 418 err(parser.getUsageString(StaticUtils.TERMINAL_WIDTH_COLUMNS - 1)); 419 return ResultCode.PARAM_ERROR; 420 } 421 else if (trailingArguments.size() == 1) 422 { 423 err("No target entry DNs were provided."); 424 err(); 425 err(parser.getUsageString(StaticUtils.TERMINAL_WIDTH_COLUMNS - 1)); 426 return ResultCode.PARAM_ERROR; 427 } 428 429 430 // Parse the attribute value assertion. 431 final String avaString = trailingArguments.get(0); 432 final int colonPos = avaString.indexOf(':'); 433 if (colonPos <= 0) 434 { 435 err("Malformed attribute value assertion."); 436 err(); 437 err(parser.getUsageString(StaticUtils.TERMINAL_WIDTH_COLUMNS - 1)); 438 return ResultCode.PARAM_ERROR; 439 } 440 441 final String attributeName = avaString.substring(0, colonPos); 442 final byte[] assertionValueBytes; 443 final int doubleColonPos = avaString.indexOf("::"); 444 if (doubleColonPos == colonPos) 445 { 446 // There are two colons, so it's a base64-encoded assertion value. 447 try 448 { 449 assertionValueBytes = Base64.decode(avaString.substring(colonPos+2)); 450 } 451 catch (ParseException pe) 452 { 453 err("Unable to base64-decode the assertion value: ", 454 pe.getMessage()); 455 err(); 456 err(parser.getUsageString(StaticUtils.TERMINAL_WIDTH_COLUMNS - 1)); 457 return ResultCode.PARAM_ERROR; 458 } 459 } 460 else 461 { 462 // There is only a single colon, so it's a simple UTF-8 string. 463 assertionValueBytes = 464 StaticUtils.getBytes(avaString.substring(colonPos+1)); 465 } 466 467 468 // Get the connection to the directory server. 469 final LDAPConnection connection; 470 try 471 { 472 connection = getConnection(); 473 out("Connected to ", connection.getConnectedAddress(), ':', 474 connection.getConnectedPort()); 475 } 476 catch (LDAPException le) 477 { 478 err("Error connecting to the directory server: ", le.getMessage()); 479 return le.getResultCode(); 480 } 481 482 483 // For each of the target entry DNs, process the compare. 484 ResultCode resultCode = ResultCode.SUCCESS; 485 CompareRequest compareRequest = null; 486 for (int i=1; i < trailingArguments.size(); i++) 487 { 488 final String targetDN = trailingArguments.get(i); 489 if (compareRequest == null) 490 { 491 compareRequest = new CompareRequest(targetDN, attributeName, 492 assertionValueBytes); 493 compareRequest.setControls(compareControls.getValues()); 494 } 495 else 496 { 497 compareRequest.setDN(targetDN); 498 } 499 500 try 501 { 502 out("Processing compare request for entry ", targetDN); 503 final CompareResult result = connection.compare(compareRequest); 504 if (result.compareMatched()) 505 { 506 out("The compare operation matched."); 507 } 508 else 509 { 510 out("The compare operation did not match."); 511 } 512 } 513 catch (LDAPException le) 514 { 515 resultCode = le.getResultCode(); 516 err("An error occurred while processing the request: ", 517 le.getMessage()); 518 err("Result Code: ", le.getResultCode().intValue(), " (", 519 le.getResultCode().getName(), ')'); 520 if (le.getMatchedDN() != null) 521 { 522 err("Matched DN: ", le.getMatchedDN()); 523 } 524 if (le.getReferralURLs() != null) 525 { 526 for (final String url : le.getReferralURLs()) 527 { 528 err("Referral URL: ", url); 529 } 530 } 531 } 532 out(); 533 } 534 535 536 // Close the connection to the directory server and exit. 537 connection.close(); 538 out(); 539 out("Disconnected from the server"); 540 return resultCode; 541 } 542 543 544 545 /** 546 * {@inheritDoc} 547 */ 548 @Override() 549 public LinkedHashMap<String[],String> getExampleUsages() 550 { 551 final LinkedHashMap<String[],String> examples = 552 new LinkedHashMap<String[],String>(); 553 554 final String[] args = 555 { 556 "--hostname", "server.example.com", 557 "--port", "389", 558 "--bindDN", "uid=admin,dc=example,dc=com", 559 "--bindPassword", "password", 560 "givenName:John", 561 "uid=jdoe,ou=People,dc=example,dc=com" 562 }; 563 final String description = 564 "Attempt to determine whether the entry for user " + 565 "'uid=jdoe,ou=People,dc=example,dc=com' has a value of 'John' for " + 566 "the givenName attribute."; 567 examples.put(args, description); 568 569 return examples; 570 } 571}