001 /*
002 * Copyright 2008-2016 UnboundID Corp.
003 * All Rights Reserved.
004 */
005 /*
006 * Copyright (C) 2008-2016 UnboundID Corp.
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 */
021 package com.unboundid.ldap.sdk.examples;
022
023
024
025 import java.io.IOException;
026 import java.io.OutputStream;
027 import java.io.Serializable;
028 import java.util.LinkedHashMap;
029 import java.util.List;
030
031 import com.unboundid.ldap.sdk.Control;
032 import com.unboundid.ldap.sdk.LDAPConnection;
033 import com.unboundid.ldap.sdk.LDAPException;
034 import com.unboundid.ldap.sdk.ResultCode;
035 import com.unboundid.ldap.sdk.Version;
036 import com.unboundid.ldif.LDIFChangeRecord;
037 import com.unboundid.ldif.LDIFException;
038 import com.unboundid.ldif.LDIFReader;
039 import com.unboundid.util.LDAPCommandLineTool;
040 import com.unboundid.util.ThreadSafety;
041 import com.unboundid.util.ThreadSafetyLevel;
042 import com.unboundid.util.args.ArgumentException;
043 import com.unboundid.util.args.ArgumentParser;
044 import com.unboundid.util.args.BooleanArgument;
045 import com.unboundid.util.args.ControlArgument;
046 import 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)
085 public 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 * Adds the arguments used by this program that aren't already provided by the
319 * generic {@code LDAPCommandLineTool} framework.
320 *
321 * @param parser The argument parser to which the arguments should be added.
322 *
323 * @throws ArgumentException If a problem occurs while adding the arguments.
324 */
325 @Override()
326 public void addNonLDAPArguments(final ArgumentParser parser)
327 throws ArgumentException
328 {
329 String description = "Treat LDIF records that do not contain a " +
330 "changetype as add records.";
331 defaultAdd = new BooleanArgument('a', "defaultAdd", description);
332 defaultAdd.addLongIdentifier("default-add");
333 parser.addArgument(defaultAdd);
334
335
336 description = "Attempt to continue processing additional changes if " +
337 "an error occurs.";
338 continueOnError = new BooleanArgument('c', "continueOnError",
339 description);
340 continueOnError.addLongIdentifier("continue-on-error");
341 parser.addArgument(continueOnError);
342
343
344 description = "The path to the LDIF file containing the changes. If " +
345 "this is not provided, then the changes will be read from " +
346 "standard input.";
347 ldifFile = new FileArgument('f', "ldifFile", false, 1, "{path}",
348 description, true, false, true, false);
349 ldifFile.addLongIdentifier("ldif-file");
350 parser.addArgument(ldifFile);
351
352
353 description = "Information about a control to include in the bind request.";
354 bindControls = new ControlArgument(null, "bindControl", false, 0, null,
355 description);
356 bindControls.addLongIdentifier("bind-control");
357 parser.addArgument(bindControls);
358 }
359
360
361
362 /**
363 * {@inheritDoc}
364 */
365 @Override()
366 protected List<Control> getBindControls()
367 {
368 return bindControls.getValues();
369 }
370
371
372
373 /**
374 * Performs the actual processing for this tool. In this case, it gets a
375 * connection to the directory server and uses it to perform the requested
376 * operations.
377 *
378 * @return The result code for the processing that was performed.
379 */
380 @Override()
381 public ResultCode doToolProcessing()
382 {
383 // Set up the LDIF reader that will be used to read the changes to apply.
384 final LDIFReader ldifReader;
385 try
386 {
387 if (ldifFile.isPresent())
388 {
389 // An LDIF file was specified on the command line, so we will use it.
390 ldifReader = new LDIFReader(ldifFile.getValue());
391 }
392 else
393 {
394 // No LDIF file was specified, so we will read from standard input.
395 ldifReader = new LDIFReader(System.in);
396 }
397 }
398 catch (IOException ioe)
399 {
400 err("I/O error creating the LDIF reader: ", ioe.getMessage());
401 return ResultCode.LOCAL_ERROR;
402 }
403
404
405 // Get the connection to the directory server.
406 final LDAPConnection connection;
407 try
408 {
409 connection = getConnection();
410 out("Connected to ", connection.getConnectedAddress(), ':',
411 connection.getConnectedPort());
412 }
413 catch (LDAPException le)
414 {
415 err("Error connecting to the directory server: ", le.getMessage());
416 return le.getResultCode();
417 }
418
419
420 // Attempt to process and apply the changes to the server.
421 ResultCode resultCode = ResultCode.SUCCESS;
422 while (true)
423 {
424 // Read the next change to process.
425 final LDIFChangeRecord changeRecord;
426 try
427 {
428 changeRecord = ldifReader.readChangeRecord(defaultAdd.isPresent());
429 }
430 catch (LDIFException le)
431 {
432 err("Malformed change record: ", le.getMessage());
433 if (! le.mayContinueReading())
434 {
435 err("Unable to continue processing the LDIF content.");
436 resultCode = ResultCode.DECODING_ERROR;
437 break;
438 }
439 else if (! continueOnError.isPresent())
440 {
441 resultCode = ResultCode.DECODING_ERROR;
442 break;
443 }
444 else
445 {
446 // We can try to keep processing, so do so.
447 continue;
448 }
449 }
450 catch (IOException ioe)
451 {
452 err("I/O error encountered while reading a change record: ",
453 ioe.getMessage());
454 resultCode = ResultCode.LOCAL_ERROR;
455 break;
456 }
457
458
459 // If the change record was null, then it means there are no more changes
460 // to be processed.
461 if (changeRecord == null)
462 {
463 break;
464 }
465
466
467 // Apply the target change to the server.
468 try
469 {
470 out("Processing ", changeRecord.getChangeType().toString(),
471 " operation for ", changeRecord.getDN());
472 changeRecord.processChange(connection);
473 out("Success");
474 out();
475 }
476 catch (LDAPException le)
477 {
478 err("Error: ", le.getMessage());
479 err("Result Code: ", le.getResultCode().intValue(), " (",
480 le.getResultCode().getName(), ')');
481 if (le.getMatchedDN() != null)
482 {
483 err("Matched DN: ", le.getMatchedDN());
484 }
485
486 if (le.getReferralURLs() != null)
487 {
488 for (final String url : le.getReferralURLs())
489 {
490 err("Referral URL: ", url);
491 }
492 }
493
494 err();
495 if (! continueOnError.isPresent())
496 {
497 resultCode = le.getResultCode();
498 break;
499 }
500 }
501 }
502
503
504 // Close the connection to the directory server and exit.
505 connection.close();
506 out("Disconnected from the server");
507 return resultCode;
508 }
509
510
511
512 /**
513 * {@inheritDoc}
514 */
515 @Override()
516 public LinkedHashMap<String[],String> getExampleUsages()
517 {
518 final LinkedHashMap<String[],String> examples =
519 new LinkedHashMap<String[],String>();
520
521 String[] args =
522 {
523 "--hostname", "server.example.com",
524 "--port", "389",
525 "--bindDN", "uid=admin,dc=example,dc=com",
526 "--bindPassword", "password",
527 "--ldifFile", "changes.ldif"
528 };
529 String description =
530 "Attempt to apply the add, delete, modify, and/or modify DN " +
531 "operations contained in the 'changes.ldif' file against the " +
532 "specified directory server.";
533 examples.put(args, description);
534
535 args = new String[]
536 {
537 "--hostname", "server.example.com",
538 "--port", "389",
539 "--bindDN", "uid=admin,dc=example,dc=com",
540 "--bindPassword", "password",
541 "--continueOnError",
542 "--defaultAdd"
543 };
544 description =
545 "Establish a connection to the specified directory server and then " +
546 "wait for information about the add, delete, modify, and/or modify " +
547 "DN operations to perform to be provided via standard input. If " +
548 "any invalid operations are requested, then the tool will display " +
549 "an error message but will continue running. Any LDIF record " +
550 "provided which does not include a 'changeType' line will be " +
551 "treated as an add request.";
552 examples.put(args, description);
553
554 return examples;
555 }
556 }