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