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