001/* 002 * Copyright 2008-2019 Ping Identity Corporation 003 * All Rights Reserved. 004 */ 005/* 006 * Copyright (C) 2008-2019 Ping Identity Corporation 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.IOException; 026import java.io.OutputStream; 027import java.io.Serializable; 028import java.util.LinkedHashMap; 029import java.util.List; 030 031import com.unboundid.ldap.sdk.Control; 032import com.unboundid.ldap.sdk.LDAPConnection; 033import com.unboundid.ldap.sdk.LDAPException; 034import com.unboundid.ldap.sdk.ResultCode; 035import com.unboundid.ldap.sdk.Version; 036import com.unboundid.ldif.LDIFChangeRecord; 037import com.unboundid.ldif.LDIFException; 038import com.unboundid.ldif.LDIFReader; 039import com.unboundid.util.LDAPCommandLineTool; 040import com.unboundid.util.StaticUtils; 041import com.unboundid.util.ThreadSafety; 042import com.unboundid.util.ThreadSafetyLevel; 043import com.unboundid.util.args.ArgumentException; 044import com.unboundid.util.args.ArgumentParser; 045import com.unboundid.util.args.BooleanArgument; 046import com.unboundid.util.args.ControlArgument; 047import com.unboundid.util.args.FileArgument; 048 049 050 051/** 052 * This class provides a simple tool that can be used to perform add, delete, 053 * modify, and modify DN operations against an LDAP directory server. The 054 * changes to apply can be read either from standard input or from an LDIF file. 055 * <BR><BR> 056 * Some of the APIs demonstrated by this example include: 057 * <UL> 058 * <LI>Argument Parsing (from the {@code com.unboundid.util.args} 059 * package)</LI> 060 * <LI>LDAP Command-Line Tool (from the {@code com.unboundid.util} 061 * package)</LI> 062 * <LI>LDIF Processing (from the {@code com.unboundid.ldif} package)</LI> 063 * </UL> 064 * <BR><BR> 065 * The behavior of this utility is controlled by command line arguments. 066 * Supported arguments include those allowed by the {@link LDAPCommandLineTool} 067 * class, as well as the following additional arguments: 068 * <UL> 069 * <LI>"-f {path}" or "--ldifFile {path}" -- specifies the path to the LDIF 070 * file containing the changes to apply. If this is not provided, then 071 * changes will be read from standard input.</LI> 072 * <LI>"-a" or "--defaultAdd" -- indicates that any LDIF records encountered 073 * that do not include a changetype should be treated as add change 074 * records. If this is not provided, then such records will be 075 * rejected.</LI> 076 * <LI>"-c" or "--continueOnError" -- indicates that processing should 077 * continue if an error occurs while processing an earlier change. If 078 * this is not provided, then the command will exit on the first error 079 * that occurs.</LI> 080 * <LI>"--bindControl {control}" -- specifies a control that should be 081 * included in the bind request sent by this tool before performing any 082 * update operations.</LI> 083 * </UL> 084 * 085 * @see com.unboundid.ldap.sdk.unboundidds.tools.LDAPModify 086 */ 087@ThreadSafety(level=ThreadSafetyLevel.NOT_THREADSAFE) 088public final class LDAPModify 089 extends LDAPCommandLineTool 090 implements Serializable 091{ 092 /** 093 * The serial version UID for this serializable class. 094 */ 095 private static final long serialVersionUID = -2602159836108416722L; 096 097 098 099 // Indicates whether processing should continue even if an error has occurred. 100 private BooleanArgument continueOnError; 101 102 // Indicates whether LDIF records without a changetype should be considered 103 // add records. 104 private BooleanArgument defaultAdd; 105 106 // The argument used to specify any bind controls that should be used. 107 private ControlArgument bindControls; 108 109 // The LDIF file to be processed. 110 private FileArgument ldifFile; 111 112 113 114 /** 115 * Parse the provided command line arguments and make the appropriate set of 116 * changes. 117 * 118 * @param args The command line arguments provided to this program. 119 */ 120 public static void main(final String[] args) 121 { 122 final ResultCode resultCode = main(args, System.out, System.err); 123 if (resultCode != ResultCode.SUCCESS) 124 { 125 System.exit(resultCode.intValue()); 126 } 127 } 128 129 130 131 /** 132 * Parse the provided command line arguments and make the appropriate set of 133 * changes. 134 * 135 * @param args The command line arguments provided to this program. 136 * @param outStream The output stream to which standard out should be 137 * written. It may be {@code null} if output should be 138 * suppressed. 139 * @param errStream The output stream to which standard error should be 140 * written. It may be {@code null} if error messages 141 * should be suppressed. 142 * 143 * @return A result code indicating whether the processing was successful. 144 */ 145 public static ResultCode main(final String[] args, 146 final OutputStream outStream, 147 final OutputStream errStream) 148 { 149 final LDAPModify ldapModify = new LDAPModify(outStream, errStream); 150 return ldapModify.runTool(args); 151 } 152 153 154 155 /** 156 * Creates a new instance of this tool. 157 * 158 * @param outStream The output stream to which standard out should be 159 * written. It may be {@code null} if output should be 160 * suppressed. 161 * @param errStream The output stream to which standard error should be 162 * written. It may be {@code null} if error messages 163 * should be suppressed. 164 */ 165 public LDAPModify(final OutputStream outStream, final OutputStream errStream) 166 { 167 super(outStream, errStream); 168 } 169 170 171 172 /** 173 * Retrieves the name for this tool. 174 * 175 * @return The name for this tool. 176 */ 177 @Override() 178 public String getToolName() 179 { 180 return "ldapmodify"; 181 } 182 183 184 185 /** 186 * Retrieves the description for this tool. 187 * 188 * @return The description for this tool. 189 */ 190 @Override() 191 public String getToolDescription() 192 { 193 return "Perform add, delete, modify, and modify " + 194 "DN operations in an LDAP directory server."; 195 } 196 197 198 199 /** 200 * Retrieves the version string for this tool. 201 * 202 * @return The version string for this tool. 203 */ 204 @Override() 205 public String getToolVersion() 206 { 207 return Version.NUMERIC_VERSION_STRING; 208 } 209 210 211 212 /** 213 * Indicates whether this tool should provide support for an interactive mode, 214 * in which the tool offers a mode in which the arguments can be provided in 215 * a text-driven menu rather than requiring them to be given on the command 216 * line. If interactive mode is supported, it may be invoked using the 217 * "--interactive" argument. Alternately, if interactive mode is supported 218 * and {@link #defaultsToInteractiveMode()} returns {@code true}, then 219 * interactive mode may be invoked by simply launching the tool without any 220 * arguments. 221 * 222 * @return {@code true} if this tool supports interactive mode, or 223 * {@code false} if not. 224 */ 225 @Override() 226 public boolean supportsInteractiveMode() 227 { 228 return true; 229 } 230 231 232 233 /** 234 * Indicates whether this tool defaults to launching in interactive mode if 235 * the tool is invoked without any command-line arguments. This will only be 236 * used if {@link #supportsInteractiveMode()} returns {@code true}. 237 * 238 * @return {@code true} if this tool defaults to using interactive mode if 239 * launched without any command-line arguments, or {@code false} if 240 * not. 241 */ 242 @Override() 243 public boolean defaultsToInteractiveMode() 244 { 245 return true; 246 } 247 248 249 250 /** 251 * Indicates whether this tool should provide arguments for redirecting output 252 * to a file. If this method returns {@code true}, then the tool will offer 253 * an "--outputFile" argument that will specify the path to a file to which 254 * all standard output and standard error content will be written, and it will 255 * also offer a "--teeToStandardOut" argument that can only be used if the 256 * "--outputFile" argument is present and will cause all output to be written 257 * to both the specified output file and to standard output. 258 * 259 * @return {@code true} if this tool should provide arguments for redirecting 260 * output to a file, or {@code false} if not. 261 */ 262 @Override() 263 protected boolean supportsOutputFile() 264 { 265 return true; 266 } 267 268 269 270 /** 271 * Indicates whether this tool should default to interactively prompting for 272 * the bind password if a password is required but no argument was provided 273 * to indicate how to get the password. 274 * 275 * @return {@code true} if this tool should default to interactively 276 * prompting for the bind password, or {@code false} if not. 277 */ 278 @Override() 279 protected boolean defaultToPromptForBindPassword() 280 { 281 return true; 282 } 283 284 285 286 /** 287 * Indicates whether this tool supports the use of a properties file for 288 * specifying default values for arguments that aren't specified on the 289 * command line. 290 * 291 * @return {@code true} if this tool supports the use of a properties file 292 * for specifying default values for arguments that aren't specified 293 * on the command line, or {@code false} if not. 294 */ 295 @Override() 296 public boolean supportsPropertiesFile() 297 { 298 return true; 299 } 300 301 302 303 /** 304 * Indicates whether the LDAP-specific arguments should include alternate 305 * versions of all long identifiers that consist of multiple words so that 306 * they are available in both camelCase and dash-separated versions. 307 * 308 * @return {@code true} if this tool should provide multiple versions of 309 * long identifiers for LDAP-specific arguments, or {@code false} if 310 * not. 311 */ 312 @Override() 313 protected boolean includeAlternateLongIdentifiers() 314 { 315 return true; 316 } 317 318 319 320 /** 321 * Indicates whether this tool should provide a command-line argument that 322 * allows for low-level SSL debugging. If this returns {@code true}, then an 323 * "--enableSSLDebugging}" argument will be added that sets the 324 * "javax.net.debug" system property to "all" before attempting any 325 * communication. 326 * 327 * @return {@code true} if this tool should offer an "--enableSSLDebugging" 328 * argument, or {@code false} if not. 329 */ 330 @Override() 331 protected boolean supportsSSLDebugging() 332 { 333 return true; 334 } 335 336 337 338 /** 339 * {@inheritDoc} 340 */ 341 @Override() 342 protected boolean logToolInvocationByDefault() 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 String description = "Treat LDIF records that do not contain a " + 362 "changetype as add records."; 363 defaultAdd = new BooleanArgument('a', "defaultAdd", description); 364 defaultAdd.addLongIdentifier("default-add", true); 365 parser.addArgument(defaultAdd); 366 367 368 description = "Attempt to continue processing additional changes if " + 369 "an error occurs."; 370 continueOnError = new BooleanArgument('c', "continueOnError", 371 description); 372 continueOnError.addLongIdentifier("continue-on-error", true); 373 parser.addArgument(continueOnError); 374 375 376 description = "The path to the LDIF file containing the changes. If " + 377 "this is not provided, then the changes will be read from " + 378 "standard input."; 379 ldifFile = new FileArgument('f', "ldifFile", false, 1, "{path}", 380 description, true, false, true, false); 381 ldifFile.addLongIdentifier("ldif-file", true); 382 parser.addArgument(ldifFile); 383 384 385 description = "Information about a control to include in the bind request."; 386 bindControls = new ControlArgument(null, "bindControl", false, 0, null, 387 description); 388 bindControls.addLongIdentifier("bind-control", true); 389 parser.addArgument(bindControls); 390 } 391 392 393 394 /** 395 * {@inheritDoc} 396 */ 397 @Override() 398 protected List<Control> getBindControls() 399 { 400 return bindControls.getValues(); 401 } 402 403 404 405 /** 406 * Performs the actual processing for this tool. In this case, it gets a 407 * connection to the directory server and uses it to perform the requested 408 * operations. 409 * 410 * @return The result code for the processing that was performed. 411 */ 412 @Override() 413 public ResultCode doToolProcessing() 414 { 415 // Set up the LDIF reader that will be used to read the changes to apply. 416 final LDIFReader ldifReader; 417 try 418 { 419 if (ldifFile.isPresent()) 420 { 421 // An LDIF file was specified on the command line, so we will use it. 422 ldifReader = new LDIFReader(ldifFile.getValue()); 423 } 424 else 425 { 426 // No LDIF file was specified, so we will read from standard input. 427 ldifReader = new LDIFReader(System.in); 428 } 429 } 430 catch (final IOException ioe) 431 { 432 err("I/O error creating the LDIF reader: ", ioe.getMessage()); 433 return ResultCode.LOCAL_ERROR; 434 } 435 436 437 // Get the connection to the directory server. 438 final LDAPConnection connection; 439 try 440 { 441 connection = getConnection(); 442 out("Connected to ", connection.getConnectedAddress(), ':', 443 connection.getConnectedPort()); 444 } 445 catch (final LDAPException le) 446 { 447 err("Error connecting to the directory server: ", le.getMessage()); 448 return le.getResultCode(); 449 } 450 451 452 // Attempt to process and apply the changes to the server. 453 ResultCode resultCode = ResultCode.SUCCESS; 454 while (true) 455 { 456 // Read the next change to process. 457 final LDIFChangeRecord changeRecord; 458 try 459 { 460 changeRecord = ldifReader.readChangeRecord(defaultAdd.isPresent()); 461 } 462 catch (final LDIFException le) 463 { 464 err("Malformed change record: ", le.getMessage()); 465 if (! le.mayContinueReading()) 466 { 467 err("Unable to continue processing the LDIF content."); 468 resultCode = ResultCode.DECODING_ERROR; 469 break; 470 } 471 else if (! continueOnError.isPresent()) 472 { 473 resultCode = ResultCode.DECODING_ERROR; 474 break; 475 } 476 else 477 { 478 // We can try to keep processing, so do so. 479 continue; 480 } 481 } 482 catch (final IOException ioe) 483 { 484 err("I/O error encountered while reading a change record: ", 485 ioe.getMessage()); 486 resultCode = ResultCode.LOCAL_ERROR; 487 break; 488 } 489 490 491 // If the change record was null, then it means there are no more changes 492 // to be processed. 493 if (changeRecord == null) 494 { 495 break; 496 } 497 498 499 // Apply the target change to the server. 500 try 501 { 502 out("Processing ", changeRecord.getChangeType().toString(), 503 " operation for ", changeRecord.getDN()); 504 changeRecord.processChange(connection); 505 out("Success"); 506 out(); 507 } 508 catch (final LDAPException le) 509 { 510 err("Error: ", le.getMessage()); 511 err("Result Code: ", le.getResultCode().intValue(), " (", 512 le.getResultCode().getName(), ')'); 513 if (le.getMatchedDN() != null) 514 { 515 err("Matched DN: ", le.getMatchedDN()); 516 } 517 518 if (le.getReferralURLs() != null) 519 { 520 for (final String url : le.getReferralURLs()) 521 { 522 err("Referral URL: ", url); 523 } 524 } 525 526 err(); 527 if (! continueOnError.isPresent()) 528 { 529 resultCode = le.getResultCode(); 530 break; 531 } 532 } 533 } 534 535 536 // Close the connection to the directory server and exit. 537 connection.close(); 538 out("Disconnected from the server"); 539 return resultCode; 540 } 541 542 543 544 /** 545 * {@inheritDoc} 546 */ 547 @Override() 548 public LinkedHashMap<String[],String> getExampleUsages() 549 { 550 final LinkedHashMap<String[],String> examples = 551 new LinkedHashMap<>(StaticUtils.computeMapCapacity(2)); 552 553 String[] args = 554 { 555 "--hostname", "server.example.com", 556 "--port", "389", 557 "--bindDN", "uid=admin,dc=example,dc=com", 558 "--bindPassword", "password", 559 "--ldifFile", "changes.ldif" 560 }; 561 String description = 562 "Attempt to apply the add, delete, modify, and/or modify DN " + 563 "operations contained in the 'changes.ldif' file against the " + 564 "specified directory server."; 565 examples.put(args, description); 566 567 args = new String[] 568 { 569 "--hostname", "server.example.com", 570 "--port", "389", 571 "--bindDN", "uid=admin,dc=example,dc=com", 572 "--bindPassword", "password", 573 "--continueOnError", 574 "--defaultAdd" 575 }; 576 description = 577 "Establish a connection to the specified directory server and then " + 578 "wait for information about the add, delete, modify, and/or modify " + 579 "DN operations to perform to be provided via standard input. If " + 580 "any invalid operations are requested, then the tool will display " + 581 "an error message but will continue running. Any LDIF record " + 582 "provided which does not include a 'changeType' line will be " + 583 "treated as an add request."; 584 examples.put(args, description); 585 586 return examples; 587 } 588}