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