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.util;
022
023
024
025import java.io.OutputStream;
026import java.io.PrintStream;
027import java.util.LinkedHashMap;
028import java.util.List;
029import java.util.Map;
030import java.util.concurrent.atomic.AtomicReference;
031
032import com.unboundid.ldap.sdk.ResultCode;
033import com.unboundid.util.args.ArgumentException;
034import com.unboundid.util.args.ArgumentParser;
035import com.unboundid.util.args.BooleanArgument;
036
037import static com.unboundid.util.Debug.*;
038import static com.unboundid.util.StaticUtils.*;
039import 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)
084public 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}