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