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