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