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}