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