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