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