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