001    /*
002     * Copyright 2008-2012 UnboundID Corp.
003     * All Rights Reserved.
004     */
005    /*
006     * Copyright (C) 2008-2012 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.util;
022    
023    
024    
025    import java.io.OutputStream;
026    import java.io.PrintStream;
027    import java.util.LinkedHashMap;
028    import java.util.List;
029    import java.util.Map;
030    
031    import com.unboundid.ldap.sdk.ResultCode;
032    import com.unboundid.util.args.ArgumentException;
033    import com.unboundid.util.args.ArgumentParser;
034    import com.unboundid.util.args.BooleanArgument;
035    
036    import static com.unboundid.util.Debug.*;
037    import static com.unboundid.util.StaticUtils.*;
038    import static com.unboundid.util.UtilityMessages.*;
039    
040    
041    
042    /**
043     * This class provides a framework for developing command-line tools that use
044     * the argument parser provided as part of the UnboundID LDAP SDK for Java.
045     * This tool adds a "-H" or "--help" option, which can be used to display usage
046     * information for the program, and may also add a "-V" or "--version" option,
047     * which can display the tool version.
048     * <BR><BR>
049     * Subclasses should include their own {@code main} method that creates an
050     * instance of a {@code CommandLineTool} and should invoke the
051     * {@link CommandLineTool#runTool} method with the provided arguments.  For
052     * example:
053     * <PRE>
054     *   public class ExampleCommandLineTool
055     *          extends CommandLineTool
056     *   {
057     *     public static void main(String[] args)
058     *     {
059     *       ExampleCommandLineTool tool = new ExampleCommandLineTool();
060     *       ResultCode resultCode = tool.runTool(args);
061     *       if (resultCode != ResultCode.SUCCESS)
062     *       {
063     *         System.exit(resultCode.intValue());
064     *       }
065     *     |
066     *
067     *     public ExampleCommandLineTool()
068     *     {
069     *       super(System.out, System.err);
070     *     }
071     *
072     *     // The rest of the tool implementation goes here.
073     *     ...
074     *   }
075     * </PRE>.
076     * <BR><BR>
077     * Note that in general, methods in this class are not threadsafe.  However, the
078     * {@link #out(Object...)} and {@link #err(Object...)} methods may be invoked
079     * concurrently by any number of threads.
080     */
081    @Extensible()
082    @ThreadSafety(level=ThreadSafetyLevel.INTERFACE_NOT_THREADSAFE)
083    public abstract class CommandLineTool
084    {
085      // The print stream to use for messages written to standard output.
086      private final PrintStream out;
087    
088      // The print stream to use for messages written to standard error.
089      private final PrintStream err;
090    
091      // The argument used to request tool help.
092      private BooleanArgument helpArgument = null;
093    
094      // The argument used to request the tool version.
095      private BooleanArgument versionArgument = null;
096    
097    
098    
099      /**
100       * Creates a new instance of this command-line tool with the provided
101       * information.
102       *
103       * @param  outStream  The output stream to use for standard output.  It may be
104       *                    {@code System.out} for the JVM's default standard output
105       *                    stream, {@code null} if no output should be generated,
106       *                    or a custom output stream if the output should be sent
107       *                    to an alternate location.
108       * @param  errStream  The output stream to use for standard error.  It may be
109       *                    {@code System.err} for the JVM's default standard error
110       *                    stream, {@code null} if no output should be generated,
111       *                    or a custom output stream if the output should be sent
112       *                    to an alternate location.
113       */
114      public CommandLineTool(final OutputStream outStream,
115                             final OutputStream errStream)
116      {
117        if (outStream == null)
118        {
119          out = NullOutputStream.getPrintStream();
120        }
121        else
122        {
123          out = new PrintStream(outStream);
124        }
125    
126        if (errStream == null)
127        {
128          err = NullOutputStream.getPrintStream();
129        }
130        else
131        {
132          err = new PrintStream(errStream);
133        }
134      }
135    
136    
137    
138      /**
139       * Performs all processing for this command-line tool.  This includes:
140       * <UL>
141       *   <LI>Creating the argument parser and populating it using the
142       *       {@link #addToolArguments} method.</LI>
143       *   <LI>Parsing the provided set of command line arguments, including any
144       *       additional validation using the {@link #doExtendedArgumentValidation}
145       *       method.</LI>
146       *   <LI>Invoking the {@link #doToolProcessing} method to do the appropriate
147       *       work for this tool.</LI>
148       * </UL>
149       *
150       * @param  args  The command-line arguments provided to this program.
151       *
152       * @return  The result of processing this tool.  It should be
153       *          {@link ResultCode#SUCCESS} if the tool completed its work
154       *          successfully, or some other result if a problem occurred.
155       */
156      public final ResultCode runTool(final String... args)
157      {
158        try
159        {
160          final ArgumentParser parser = createArgumentParser();
161          parser.parse(args);
162    
163          if (helpArgument.isPresent())
164          {
165            out(parser.getUsageString(79));
166            displayExampleUsages();
167            return ResultCode.SUCCESS;
168          }
169    
170          if ((versionArgument != null) && versionArgument.isPresent())
171          {
172            out(getToolVersion());
173            return ResultCode.SUCCESS;
174          }
175    
176          doExtendedArgumentValidation();
177        }
178        catch (ArgumentException ae)
179        {
180          debugException(ae);
181          err(ae.getMessage());
182          return ResultCode.PARAM_ERROR;
183        }
184    
185        try
186        {
187          return doToolProcessing();
188        }
189        catch (Exception e)
190        {
191          debugException(e);
192          err(getExceptionMessage(e));
193          return ResultCode.LOCAL_ERROR;
194        }
195      }
196    
197    
198    
199      /**
200       * Writes example usage information for this tool to the standard output
201       * stream.
202       */
203      private void displayExampleUsages()
204      {
205        final LinkedHashMap<String[],String> examples = getExampleUsages();
206        if ((examples == null) || examples.isEmpty())
207        {
208          return;
209        }
210    
211        out(INFO_CL_TOOL_LABEL_EXAMPLES);
212    
213        for (final Map.Entry<String[],String> e : examples.entrySet())
214        {
215          out();
216          wrapOut(2, 79, e.getValue());
217          out();
218    
219          final StringBuilder buffer = new StringBuilder();
220          buffer.append("    ");
221          buffer.append(getToolName());
222    
223          final String[] args = e.getKey();
224          for (int i=0; i < args.length; i++)
225          {
226            buffer.append(' ');
227    
228            // If the argument has a value, then make sure to keep it on the same
229            // line as the argument name.  This may introduce false positives due to
230            // unnamed trailing arguments, but the worst that will happen that case
231            // is that the output may be wrapped earlier than necessary one time.
232            String arg = args[i];
233            if (arg.startsWith("-"))
234            {
235              if ((i < (args.length - 1)) && (! args[i+1].startsWith("-")))
236              {
237                ExampleCommandLineArgument cleanArg =
238                    ExampleCommandLineArgument.getCleanArgument(args[i+1]);
239                arg += ' ' + cleanArg.getLocalForm();
240                i++;
241              }
242            }
243            else
244            {
245              ExampleCommandLineArgument cleanArg =
246                  ExampleCommandLineArgument.getCleanArgument(arg);
247              arg = cleanArg.getLocalForm();
248            }
249    
250            if ((buffer.length() + arg.length() + 2) < 79)
251            {
252              buffer.append(arg);
253            }
254            else
255            {
256              buffer.append('\\');
257              out(buffer.toString());
258              buffer.setLength(0);
259              buffer.append("         ");
260              buffer.append(arg);
261            }
262          }
263    
264          out(buffer.toString());
265        }
266      }
267    
268    
269    
270      /**
271       * Retrieves the name of this tool.  It should be the name of the command used
272       * to invoke this tool.
273       *
274       * @return  The name for this tool.
275       */
276      public abstract String getToolName();
277    
278    
279    
280      /**
281       * Retrieves a human-readable description for this tool.
282       *
283       * @return  A human-readable description for this tool.
284       */
285      public abstract String getToolDescription();
286    
287    
288    
289      /**
290       * Retrieves a version string for this tool, if available.
291       *
292       * @return  A version string for this tool, or {@code null} if none is
293       *          available.
294       */
295      public String getToolVersion()
296      {
297        return null;
298      }
299    
300    
301    
302      /**
303       * Retrieves the maximum number of unnamed trailing arguments that may be
304       * provided for this tool.  If a tool supports trailing arguments, then it
305       * must override this method to return a nonzero value, and must also override
306       * the {@link CommandLineTool#getTrailingArgumentsPlaceholder} method to
307       * return a non-{@code null} value.
308       *
309       * @return  The maximum number of unnamed trailing arguments that may be
310       *          provided for this tool.  A value of zero indicates that trailing
311       *          arguments are not allowed.  A negative value indicates that there
312       *          should be no limit on the number of trailing arguments.
313       */
314      public int getMaxTrailingArguments()
315      {
316        return 0;
317      }
318    
319    
320    
321      /**
322       * Retrieves a placeholder string that should be used for trailing arguments
323       * in the usage information for this tool.
324       *
325       * @return  A placeholder string that should be used for trailing arguments in
326       *          the usage information for this tool, or {@code null} if trailing
327       *          arguments are not supported.
328       */
329      public String getTrailingArgumentsPlaceholder()
330      {
331        return null;
332      }
333    
334    
335    
336      /**
337       * Creates a parser that can be used to to parse arguments accepted by
338       * this tool.
339       *
340       * @return ArgumentParser that can be used to parse arguments for this
341       *         tool.
342       *
343       * @throws ArgumentException  If there was a problem initializing the
344       *                            parser for this tool.
345       */
346      public final ArgumentParser createArgumentParser()
347             throws ArgumentException
348      {
349        final ArgumentParser parser = new ArgumentParser(getToolName(),
350             getToolDescription(), getMaxTrailingArguments(),
351             getTrailingArgumentsPlaceholder());
352    
353        addToolArguments(parser);
354    
355        helpArgument = new BooleanArgument('H', "help",
356             INFO_CL_TOOL_DESCRIPTION_HELP.get());
357        helpArgument.addShortIdentifier('?');
358        helpArgument.setUsageArgument(true);
359        parser.addArgument(helpArgument);
360    
361        final String version = getToolVersion();
362        if ((version != null) && (version.length() > 0) &&
363            (parser.getNamedArgument("version") == null))
364        {
365          final Character shortIdentifier;
366          if (parser.getNamedArgument('V') == null)
367          {
368            shortIdentifier = 'V';
369          }
370          else
371          {
372            shortIdentifier = null;
373          }
374    
375          versionArgument = new BooleanArgument(shortIdentifier, "version",
376               INFO_CL_TOOL_DESCRIPTION_VERSION.get());
377          versionArgument.setUsageArgument(true);
378          parser.addArgument(versionArgument);
379        }
380    
381        return parser;
382      }
383    
384    
385    
386      /**
387       * Adds the command-line arguments supported for use with this tool to the
388       * provided argument parser.  The tool may need to retain references to the
389       * arguments (and/or the argument parser, if trailing arguments are allowed)
390       * to it in order to obtain their values for use in later processing.
391       *
392       * @param  parser  The argument parser to which the arguments are to be added.
393       *
394       * @throws  ArgumentException  If a problem occurs while adding any of the
395       *                             tool-specific arguments to the provided
396       *                             argument parser.
397       */
398      public abstract void addToolArguments(final ArgumentParser parser)
399             throws ArgumentException;
400    
401    
402    
403      /**
404       * Performs any necessary processing that should be done to ensure that the
405       * provided set of command-line arguments were valid.  This method will be
406       * called after the basic argument parsing has been performed and immediately
407       * before the {@link CommandLineTool#doToolProcessing} method is invoked.
408       *
409       * @throws  ArgumentException  If there was a problem with the command-line
410       *                             arguments provided to this program.
411       */
412      public void doExtendedArgumentValidation()
413             throws ArgumentException
414      {
415        // No processing will be performed by default.
416      }
417    
418    
419    
420      /**
421       * Performs the core set of processing for this tool.
422       *
423       * @return  A result code that indicates whether the processing completed
424       *          successfully.
425       */
426      public abstract ResultCode doToolProcessing();
427    
428    
429    
430      /**
431       * Retrieves a set of information that may be used to generate example usage
432       * information.  Each element in the returned map should consist of a map
433       * between an example set of arguments and a string that describes the
434       * behavior of the tool when invoked with that set of arguments.
435       *
436       * @return  A set of information that may be used to generate example usage
437       *          information.  It may be {@code null} or empty if no example usage
438       *          information is available.
439       */
440      @ThreadSafety(level=ThreadSafetyLevel.METHOD_THREADSAFE)
441      public LinkedHashMap<String[],String> getExampleUsages()
442      {
443        return null;
444      }
445    
446    
447    
448      /**
449       * Retrieves the print writer that will be used for standard output.
450       *
451       * @return  The print writer that will be used for standard output.
452       */
453      public final PrintStream getOut()
454      {
455        return out;
456      }
457    
458    
459    
460      /**
461       * Writes the provided message to the standard output stream for this tool.
462       * <BR><BR>
463       * This method is completely threadsafe and my be invoked concurrently by any
464       * number of threads.
465       *
466       * @param  msg  The message components that will be written to the standard
467       *              output stream.  They will be concatenated together on the same
468       *              line, and that line will be followed by an end-of-line
469       *              sequence.
470       */
471      @ThreadSafety(level=ThreadSafetyLevel.METHOD_THREADSAFE)
472      public final synchronized void out(final Object... msg)
473      {
474        write(out, 0, 0, msg);
475      }
476    
477    
478    
479      /**
480       * Writes the provided message to the standard output stream for this tool,
481       * optionally wrapping and/or indenting the text in the process.
482       * <BR><BR>
483       * This method is completely threadsafe and my be invoked concurrently by any
484       * number of threads.
485       *
486       * @param  indent      The number of spaces each line should be indented.  A
487       *                     value less than or equal to zero indicates that no
488       *                     indent should be used.
489       * @param  wrapColumn  The column at which to wrap long lines.  A value less
490       *                     than or equal to two indicates that no wrapping should
491       *                     be performed.  If both an indent and a wrap column are
492       *                     to be used, then the wrap column must be greater than
493       *                     the indent.
494       * @param  msg         The message components that will be written to the
495       *                     standard output stream.  They will be concatenated
496       *                     together on the same line, and that line will be
497       *                     followed by an end-of-line sequence.
498       */
499      @ThreadSafety(level=ThreadSafetyLevel.METHOD_THREADSAFE)
500      public final synchronized void wrapOut(final int indent, final int wrapColumn,
501                                             final Object... msg)
502      {
503        write(out, indent, wrapColumn, msg);
504      }
505    
506    
507    
508      /**
509       * Retrieves the print writer that will be used for standard error.
510       *
511       * @return  The print writer that will be used for standard error.
512       */
513      public final PrintStream getErr()
514      {
515        return err;
516      }
517    
518    
519    
520      /**
521       * Writes the provided message to the standard error stream for this tool.
522       * <BR><BR>
523       * This method is completely threadsafe and my be invoked concurrently by any
524       * number of threads.
525       *
526       * @param  msg  The message components that will be written to the standard
527       *              error stream.  They will be concatenated together on the same
528       *              line, and that line will be followed by an end-of-line
529       *              sequence.
530       */
531      @ThreadSafety(level=ThreadSafetyLevel.METHOD_THREADSAFE)
532      public final synchronized void err(final Object... msg)
533      {
534        write(err, 0, 0, msg);
535      }
536    
537    
538    
539      /**
540       * Writes the provided message to the standard error stream for this tool,
541       * optionally wrapping and/or indenting the text in the process.
542       * <BR><BR>
543       * This method is completely threadsafe and my be invoked concurrently by any
544       * number of threads.
545       *
546       * @param  indent      The number of spaces each line should be indented.  A
547       *                     value less than or equal to zero indicates that no
548       *                     indent should be used.
549       * @param  wrapColumn  The column at which to wrap long lines.  A value less
550       *                     than or equal to two indicates that no wrapping should
551       *                     be performed.  If both an indent and a wrap column are
552       *                     to be used, then the wrap column must be greater than
553       *                     the indent.
554       * @param  msg         The message components that will be written to the
555       *                     standard output stream.  They will be concatenated
556       *                     together on the same line, and that line will be
557       *                     followed by an end-of-line sequence.
558       */
559      @ThreadSafety(level=ThreadSafetyLevel.METHOD_THREADSAFE)
560      public final synchronized void wrapErr(final int indent, final int wrapColumn,
561                                             final Object... msg)
562      {
563        write(err, indent, wrapColumn, msg);
564      }
565    
566    
567    
568      /**
569       * Writes the provided message to the given print stream, optionally wrapping
570       * and/or indenting the text in the process.
571       *
572       * @param  stream      The stream to which the message should be written.
573       * @param  indent      The number of spaces each line should be indented.  A
574       *                     value less than or equal to zero indicates that no
575       *                     indent should be used.
576       * @param  wrapColumn  The column at which to wrap long lines.  A value less
577       *                     than or equal to two indicates that no wrapping should
578       *                     be performed.  If both an indent and a wrap column are
579       *                     to be used, then the wrap column must be greater than
580       *                     the indent.
581       * @param  msg         The message components that will be written to the
582       *                     standard output stream.  They will be concatenated
583       *                     together on the same line, and that line will be
584       *                     followed by an end-of-line sequence.
585       */
586      private static void write(final PrintStream stream, final int indent,
587                                final int wrapColumn, final Object... msg)
588      {
589        final StringBuilder buffer = new StringBuilder();
590        for (final Object o : msg)
591        {
592          buffer.append(o);
593        }
594    
595        if (wrapColumn > 2)
596        {
597          final List<String> lines;
598          if (indent > 0)
599          {
600            for (final String line :
601                 wrapLine(buffer.toString(), (wrapColumn - indent)))
602            {
603              for (int i=0; i < indent; i++)
604              {
605                stream.print(' ');
606              }
607              stream.println(line);
608            }
609          }
610          else
611          {
612            for (final String line : wrapLine(buffer.toString(), wrapColumn))
613            {
614              stream.println(line);
615            }
616          }
617        }
618        else
619        {
620          if (indent > 0)
621          {
622            for (int i=0; i < indent; i++)
623            {
624              stream.print(' ');
625            }
626          }
627          stream.println(buffer.toString());
628        }
629      }
630    }