001/*
002 * Copyright 2008-2014 UnboundID Corp.
003 * All Rights Reserved.
004 */
005/*
006 * Copyright (C) 2008-2014 UnboundID Corp.
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;
029
030import com.unboundid.ldap.sdk.LDAPConnection;
031import com.unboundid.ldap.sdk.LDAPException;
032import com.unboundid.ldap.sdk.ResultCode;
033import com.unboundid.ldap.sdk.Version;
034import com.unboundid.ldif.LDIFChangeRecord;
035import com.unboundid.ldif.LDIFException;
036import com.unboundid.ldif.LDIFReader;
037import com.unboundid.util.LDAPCommandLineTool;
038import com.unboundid.util.ThreadSafety;
039import com.unboundid.util.ThreadSafetyLevel;
040import com.unboundid.util.args.ArgumentException;
041import com.unboundid.util.args.ArgumentParser;
042import com.unboundid.util.args.BooleanArgument;
043import com.unboundid.util.args.FileArgument;
044
045
046
047/**
048 * This class provides a simple tool that can be used to perform add, delete,
049 * modify, and modify DN operations against an LDAP directory server.  The
050 * changes to apply can be read either from standard input or from an LDIF file.
051 * <BR><BR>
052 * Some of the APIs demonstrated by this example include:
053 * <UL>
054 *   <LI>Argument Parsing (from the {@code com.unboundid.util.args}
055 *       package)</LI>
056 *   <LI>LDAP Command-Line Tool (from the {@code com.unboundid.util}
057 *       package)</LI>
058 *   <LI>LDIF Processing (from the {@code com.unboundid.ldif} package)</LI>
059 * </UL>
060 * <BR><BR>
061 * The behavior of this utility is controlled by command line arguments.
062 * Supported arguments include those allowed by the {@link LDAPCommandLineTool}
063 * class, as well as the following additional arguments:
064 * <UL>
065 *   <LI>"-f {path}" or "--ldifFile {path}" -- specifies the path to the LDIF
066 *       file containing the changes to apply.  If this is not provided, then
067 *       changes will be read from standard input.</LI>
068 *   <LI>"-a" or "--defaultAdd" -- indicates that any LDIF records encountered
069 *       that do not include a changetype should be treated as add change
070 *       records.  If this is not provided, then such records will be
071 *       rejected.</LI>
072 *   <LI>"-c" or "--continueOnError" -- indicates that processing should
073 *       continue if an error occurs while processing an earlier change.  If
074 *       this is not provided, then the command will exit on the first error
075 *       that occurs.</LI>
076 * </UL>
077 */
078@ThreadSafety(level=ThreadSafetyLevel.NOT_THREADSAFE)
079public final class LDAPModify
080       extends LDAPCommandLineTool
081       implements Serializable
082{
083  /**
084   * The serial version UID for this serializable class.
085   */
086  private static final long serialVersionUID = -2602159836108416722L;
087
088
089
090  // Indicates whether processing should continue even if an error has occurred.
091  private BooleanArgument continueOnError;
092
093  // Indicates whether LDIF records without a changetype should be considered
094  // add records.
095  private BooleanArgument defaultAdd;
096
097  // The LDIF file to be processed.
098  private FileArgument ldifFile;
099
100
101
102  /**
103   * Parse the provided command line arguments and make the appropriate set of
104   * changes.
105   *
106   * @param  args  The command line arguments provided to this program.
107   */
108  public static void main(final String[] args)
109  {
110    final ResultCode resultCode = main(args, System.out, System.err);
111    if (resultCode != ResultCode.SUCCESS)
112    {
113      System.exit(resultCode.intValue());
114    }
115  }
116
117
118
119  /**
120   * Parse the provided command line arguments and make the appropriate set of
121   * changes.
122   *
123   * @param  args       The command line arguments provided to this program.
124   * @param  outStream  The output stream to which standard out should be
125   *                    written.  It may be {@code null} if output should be
126   *                    suppressed.
127   * @param  errStream  The output stream to which standard error should be
128   *                    written.  It may be {@code null} if error messages
129   *                    should be suppressed.
130   *
131   * @return  A result code indicating whether the processing was successful.
132   */
133  public static ResultCode main(final String[] args,
134                                final OutputStream outStream,
135                                final OutputStream errStream)
136  {
137    final LDAPModify ldapModify = new LDAPModify(outStream, errStream);
138    return ldapModify.runTool(args);
139  }
140
141
142
143  /**
144   * Creates a new instance of this tool.
145   *
146   * @param  outStream  The output stream to which standard out should be
147   *                    written.  It may be {@code null} if output should be
148   *                    suppressed.
149   * @param  errStream  The output stream to which standard error should be
150   *                    written.  It may be {@code null} if error messages
151   *                    should be suppressed.
152   */
153  public LDAPModify(final OutputStream outStream, final OutputStream errStream)
154  {
155    super(outStream, errStream);
156  }
157
158
159
160  /**
161   * Retrieves the name for this tool.
162   *
163   * @return  The name for this tool.
164   */
165  @Override()
166  public String getToolName()
167  {
168    return "ldapmodify";
169  }
170
171
172
173  /**
174   * Retrieves the description for this tool.
175   *
176   * @return  The description for this tool.
177   */
178  @Override()
179  public String getToolDescription()
180  {
181    return "Perform add, delete, modify, and modify " +
182           "DN operations in an LDAP directory server.";
183  }
184
185
186
187  /**
188   * Retrieves the version string for this tool.
189   *
190   * @return  The version string for this tool.
191   */
192  @Override()
193  public String getToolVersion()
194  {
195    return Version.NUMERIC_VERSION_STRING;
196  }
197
198
199
200  /**
201   * Adds the arguments used by this program that aren't already provided by the
202   * generic {@code LDAPCommandLineTool} framework.
203   *
204   * @param  parser  The argument parser to which the arguments should be added.
205   *
206   * @throws  ArgumentException  If a problem occurs while adding the arguments.
207   */
208  @Override()
209  public void addNonLDAPArguments(final ArgumentParser parser)
210         throws ArgumentException
211  {
212    String description = "Treat LDIF records that do not contain a " +
213                         "changetype as add records.";
214    defaultAdd = new BooleanArgument('a', "defaultAdd", description);
215    parser.addArgument(defaultAdd);
216
217
218    description = "Attempt to continue processing additional changes if " +
219                  "an error occurs.";
220    continueOnError = new BooleanArgument('c', "continueOnError",
221                                          description);
222    parser.addArgument(continueOnError);
223
224
225    description = "The path to the LDIF file containing the changes.  If " +
226                  "this is not provided, then the changes will be read from " +
227                  "standard input.";
228    ldifFile = new FileArgument('f', "ldifFile", false, 1, "{path}",
229                                description, true, false, true, false);
230    parser.addArgument(ldifFile);
231  }
232
233
234
235  /**
236   * Performs the actual processing for this tool.  In this case, it gets a
237   * connection to the directory server and uses it to perform the requested
238   * operations.
239   *
240   * @return  The result code for the processing that was performed.
241   */
242  @Override()
243  public ResultCode doToolProcessing()
244  {
245    // Set up the LDIF reader that will be used to read the changes to apply.
246    final LDIFReader ldifReader;
247    try
248    {
249      if (ldifFile.isPresent())
250      {
251        // An LDIF file was specified on the command line, so we will use it.
252        ldifReader = new LDIFReader(ldifFile.getValue());
253      }
254      else
255      {
256        // No LDIF file was specified, so we will read from standard input.
257        ldifReader = new LDIFReader(System.in);
258      }
259    }
260    catch (IOException ioe)
261    {
262      err("I/O error creating the LDIF reader:  ", ioe.getMessage());
263      return ResultCode.LOCAL_ERROR;
264    }
265
266
267    // Get the connection to the directory server.
268    final LDAPConnection connection;
269    try
270    {
271      connection = getConnection();
272      out("Connected to ", connection.getConnectedAddress(), ':',
273          connection.getConnectedPort());
274    }
275    catch (LDAPException le)
276    {
277      err("Error connecting to the directory server:  ", le.getMessage());
278      return le.getResultCode();
279    }
280
281
282    // Attempt to process and apply the changes to the server.
283    ResultCode resultCode = ResultCode.SUCCESS;
284    while (true)
285    {
286      // Read the next change to process.
287      final LDIFChangeRecord changeRecord;
288      try
289      {
290        changeRecord = ldifReader.readChangeRecord(defaultAdd.isPresent());
291      }
292      catch (LDIFException le)
293      {
294        err("Malformed change record:  ", le.getMessage());
295        if (! le.mayContinueReading())
296        {
297          err("Unable to continue processing the LDIF content.");
298          resultCode = ResultCode.DECODING_ERROR;
299          break;
300        }
301        else if (! continueOnError.isPresent())
302        {
303          resultCode = ResultCode.DECODING_ERROR;
304          break;
305        }
306        else
307        {
308          // We can try to keep processing, so do so.
309          continue;
310        }
311      }
312      catch (IOException ioe)
313      {
314        err("I/O error encountered while reading a change record:  ",
315            ioe.getMessage());
316        resultCode = ResultCode.LOCAL_ERROR;
317        break;
318      }
319
320
321      // If the change record was null, then it means there are no more changes
322      // to be processed.
323      if (changeRecord == null)
324      {
325        break;
326      }
327
328
329      // Apply the target change to the server.
330      try
331      {
332        out("Processing ", changeRecord.getChangeType().toString(),
333            " operation for ", changeRecord.getDN());
334        changeRecord.processChange(connection);
335        out("Success");
336        out();
337      }
338      catch (LDAPException le)
339      {
340        err("Error:  ", le.getMessage());
341        err("Result Code:  ", le.getResultCode().intValue(), " (",
342            le.getResultCode().getName(), ')');
343        if (le.getMatchedDN() != null)
344        {
345          err("Matched DN:  ", le.getMatchedDN());
346        }
347
348        if (le.getReferralURLs() != null)
349        {
350          for (final String url : le.getReferralURLs())
351          {
352            err("Referral URL:  ", url);
353          }
354        }
355
356        err();
357        if (! continueOnError.isPresent())
358        {
359          resultCode = le.getResultCode();
360          break;
361        }
362      }
363    }
364
365
366    // Close the connection to the directory server and exit.
367    connection.close();
368    out("Disconnected from the server");
369    return resultCode;
370  }
371
372
373
374  /**
375   * {@inheritDoc}
376   */
377  @Override()
378  public LinkedHashMap<String[],String> getExampleUsages()
379  {
380    final LinkedHashMap<String[],String> examples =
381         new LinkedHashMap<String[],String>();
382
383    String[] args =
384    {
385      "--hostname", "server.example.com",
386      "--port", "389",
387      "--bindDN", "uid=admin,dc=example,dc=com",
388      "--bindPassword", "password",
389      "--ldifFile", "changes.ldif"
390    };
391    String description =
392         "Attempt to apply the add, delete, modify, and/or modify DN " +
393         "operations contained in the 'changes.ldif' file against the " +
394         "specified directory server.";
395    examples.put(args, description);
396
397    args = new String[]
398    {
399      "--hostname", "server.example.com",
400      "--port", "389",
401      "--bindDN", "uid=admin,dc=example,dc=com",
402      "--bindPassword", "password",
403      "--continueOnError",
404      "--defaultAdd"
405    };
406    description =
407         "Establish a connection to the specified directory server and then " +
408         "wait for information about the add, delete, modify, and/or modify " +
409         "DN operations to perform to be provided via standard input.  If " +
410         "any invalid operations are requested, then the tool will display " +
411         "an error message but will continue running.  Any LDIF record " +
412         "provided which does not include a 'changeType' line will be " +
413         "treated as an add request.";
414    examples.put(args, description);
415
416    return examples;
417  }
418}