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 * {@inheritDoc} 322 */ 323 @Override() 324 protected boolean logToolInvocationByDefault() 325 { 326 return true; 327 } 328 329 330 331 /** 332 * Adds the arguments used by this program that aren't already provided by the 333 * generic {@code LDAPCommandLineTool} framework. 334 * 335 * @param parser The argument parser to which the arguments should be added. 336 * 337 * @throws ArgumentException If a problem occurs while adding the arguments. 338 */ 339 @Override() 340 public void addNonLDAPArguments(final ArgumentParser parser) 341 throws ArgumentException 342 { 343 String description = "Treat LDIF records that do not contain a " + 344 "changetype as add records."; 345 defaultAdd = new BooleanArgument('a', "defaultAdd", description); 346 defaultAdd.addLongIdentifier("default-add", true); 347 parser.addArgument(defaultAdd); 348 349 350 description = "Attempt to continue processing additional changes if " + 351 "an error occurs."; 352 continueOnError = new BooleanArgument('c', "continueOnError", 353 description); 354 continueOnError.addLongIdentifier("continue-on-error", true); 355 parser.addArgument(continueOnError); 356 357 358 description = "The path to the LDIF file containing the changes. If " + 359 "this is not provided, then the changes will be read from " + 360 "standard input."; 361 ldifFile = new FileArgument('f', "ldifFile", false, 1, "{path}", 362 description, true, false, true, false); 363 ldifFile.addLongIdentifier("ldif-file", true); 364 parser.addArgument(ldifFile); 365 366 367 description = "Information about a control to include in the bind request."; 368 bindControls = new ControlArgument(null, "bindControl", false, 0, null, 369 description); 370 bindControls.addLongIdentifier("bind-control", true); 371 parser.addArgument(bindControls); 372 } 373 374 375 376 /** 377 * {@inheritDoc} 378 */ 379 @Override() 380 protected List<Control> getBindControls() 381 { 382 return bindControls.getValues(); 383 } 384 385 386 387 /** 388 * Performs the actual processing for this tool. In this case, it gets a 389 * connection to the directory server and uses it to perform the requested 390 * operations. 391 * 392 * @return The result code for the processing that was performed. 393 */ 394 @Override() 395 public ResultCode doToolProcessing() 396 { 397 // Set up the LDIF reader that will be used to read the changes to apply. 398 final LDIFReader ldifReader; 399 try 400 { 401 if (ldifFile.isPresent()) 402 { 403 // An LDIF file was specified on the command line, so we will use it. 404 ldifReader = new LDIFReader(ldifFile.getValue()); 405 } 406 else 407 { 408 // No LDIF file was specified, so we will read from standard input. 409 ldifReader = new LDIFReader(System.in); 410 } 411 } 412 catch (final IOException ioe) 413 { 414 err("I/O error creating the LDIF reader: ", ioe.getMessage()); 415 return ResultCode.LOCAL_ERROR; 416 } 417 418 419 // Get the connection to the directory server. 420 final LDAPConnection connection; 421 try 422 { 423 connection = getConnection(); 424 out("Connected to ", connection.getConnectedAddress(), ':', 425 connection.getConnectedPort()); 426 } 427 catch (final LDAPException le) 428 { 429 err("Error connecting to the directory server: ", le.getMessage()); 430 return le.getResultCode(); 431 } 432 433 434 // Attempt to process and apply the changes to the server. 435 ResultCode resultCode = ResultCode.SUCCESS; 436 while (true) 437 { 438 // Read the next change to process. 439 final LDIFChangeRecord changeRecord; 440 try 441 { 442 changeRecord = ldifReader.readChangeRecord(defaultAdd.isPresent()); 443 } 444 catch (final LDIFException le) 445 { 446 err("Malformed change record: ", le.getMessage()); 447 if (! le.mayContinueReading()) 448 { 449 err("Unable to continue processing the LDIF content."); 450 resultCode = ResultCode.DECODING_ERROR; 451 break; 452 } 453 else if (! continueOnError.isPresent()) 454 { 455 resultCode = ResultCode.DECODING_ERROR; 456 break; 457 } 458 else 459 { 460 // We can try to keep processing, so do so. 461 continue; 462 } 463 } 464 catch (final IOException ioe) 465 { 466 err("I/O error encountered while reading a change record: ", 467 ioe.getMessage()); 468 resultCode = ResultCode.LOCAL_ERROR; 469 break; 470 } 471 472 473 // If the change record was null, then it means there are no more changes 474 // to be processed. 475 if (changeRecord == null) 476 { 477 break; 478 } 479 480 481 // Apply the target change to the server. 482 try 483 { 484 out("Processing ", changeRecord.getChangeType().toString(), 485 " operation for ", changeRecord.getDN()); 486 changeRecord.processChange(connection); 487 out("Success"); 488 out(); 489 } 490 catch (final LDAPException le) 491 { 492 err("Error: ", le.getMessage()); 493 err("Result Code: ", le.getResultCode().intValue(), " (", 494 le.getResultCode().getName(), ')'); 495 if (le.getMatchedDN() != null) 496 { 497 err("Matched DN: ", le.getMatchedDN()); 498 } 499 500 if (le.getReferralURLs() != null) 501 { 502 for (final String url : le.getReferralURLs()) 503 { 504 err("Referral URL: ", url); 505 } 506 } 507 508 err(); 509 if (! continueOnError.isPresent()) 510 { 511 resultCode = le.getResultCode(); 512 break; 513 } 514 } 515 } 516 517 518 // Close the connection to the directory server and exit. 519 connection.close(); 520 out("Disconnected from the server"); 521 return resultCode; 522 } 523 524 525 526 /** 527 * {@inheritDoc} 528 */ 529 @Override() 530 public LinkedHashMap<String[],String> getExampleUsages() 531 { 532 final LinkedHashMap<String[],String> examples = 533 new LinkedHashMap<>(StaticUtils.computeMapCapacity(2)); 534 535 String[] args = 536 { 537 "--hostname", "server.example.com", 538 "--port", "389", 539 "--bindDN", "uid=admin,dc=example,dc=com", 540 "--bindPassword", "password", 541 "--ldifFile", "changes.ldif" 542 }; 543 String description = 544 "Attempt to apply the add, delete, modify, and/or modify DN " + 545 "operations contained in the 'changes.ldif' file against the " + 546 "specified directory server."; 547 examples.put(args, description); 548 549 args = new String[] 550 { 551 "--hostname", "server.example.com", 552 "--port", "389", 553 "--bindDN", "uid=admin,dc=example,dc=com", 554 "--bindPassword", "password", 555 "--continueOnError", 556 "--defaultAdd" 557 }; 558 description = 559 "Establish a connection to the specified directory server and then " + 560 "wait for information about the add, delete, modify, and/or modify " + 561 "DN operations to perform to be provided via standard input. If " + 562 "any invalid operations are requested, then the tool will display " + 563 "an error message but will continue running. Any LDIF record " + 564 "provided which does not include a 'changeType' line will be " + 565 "treated as an add request."; 566 examples.put(args, description); 567 568 return examples; 569 } 570}