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