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