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