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     */
021    package com.unboundid.ldap.sdk.examples;
022    
023    
024    
025    import java.io.IOException;
026    import java.io.OutputStream;
027    import java.io.Serializable;
028    import java.util.LinkedHashMap;
029    
030    import com.unboundid.ldap.sdk.LDAPConnection;
031    import com.unboundid.ldap.sdk.LDAPException;
032    import com.unboundid.ldap.sdk.ResultCode;
033    import com.unboundid.ldap.sdk.Version;
034    import com.unboundid.ldif.LDIFChangeRecord;
035    import com.unboundid.ldif.LDIFException;
036    import com.unboundid.ldif.LDIFReader;
037    import com.unboundid.util.LDAPCommandLineTool;
038    import com.unboundid.util.ThreadSafety;
039    import com.unboundid.util.ThreadSafetyLevel;
040    import com.unboundid.util.args.ArgumentException;
041    import com.unboundid.util.args.ArgumentParser;
042    import com.unboundid.util.args.BooleanArgument;
043    import 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)
079    public 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    }