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