001/*
002 * Copyright 2008-2017 Ping Identity Corporation
003 * All Rights Reserved.
004 */
005/*
006 * Copyright (C) 2008-2017 Ping Identity Corporation
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.File;
026import java.io.FileOutputStream;
027import java.io.OutputStream;
028import java.io.PrintStream;
029import java.util.Collections;
030import java.util.Iterator;
031import java.util.LinkedHashMap;
032import java.util.LinkedHashSet;
033import java.util.List;
034import java.util.Map;
035import java.util.Set;
036import java.util.TreeMap;
037import java.util.concurrent.atomic.AtomicReference;
038
039import com.unboundid.ldap.sdk.LDAPException;
040import com.unboundid.ldap.sdk.ResultCode;
041import com.unboundid.util.args.ArgumentException;
042import com.unboundid.util.args.ArgumentParser;
043import com.unboundid.util.args.BooleanArgument;
044import com.unboundid.util.args.FileArgument;
045import com.unboundid.util.args.SubCommand;
046
047import static com.unboundid.util.Debug.*;
048import static com.unboundid.util.StaticUtils.*;
049import static com.unboundid.util.UtilityMessages.*;
050
051
052
053/**
054 * This class provides a framework for developing command-line tools that use
055 * the argument parser provided as part of the UnboundID LDAP SDK for Java.
056 * This tool adds a "-H" or "--help" option, which can be used to display usage
057 * information for the program, and may also add a "-V" or "--version" option,
058 * which can display the tool version.
059 * <BR><BR>
060 * Subclasses should include their own {@code main} method that creates an
061 * instance of a {@code CommandLineTool} and should invoke the
062 * {@link CommandLineTool#runTool} method with the provided arguments.  For
063 * example:
064 * <PRE>
065 *   public class ExampleCommandLineTool
066 *          extends CommandLineTool
067 *   {
068 *     public static void main(String[] args)
069 *     {
070 *       ExampleCommandLineTool tool = new ExampleCommandLineTool();
071 *       ResultCode resultCode = tool.runTool(args);
072 *       if (resultCode != ResultCode.SUCCESS)
073 *       {
074 *         System.exit(resultCode.intValue());
075 *       }
076 *     |
077 *
078 *     public ExampleCommandLineTool()
079 *     {
080 *       super(System.out, System.err);
081 *     }
082 *
083 *     // The rest of the tool implementation goes here.
084 *     ...
085 *   }
086 * </PRE>.
087 * <BR><BR>
088 * Note that in general, methods in this class are not threadsafe.  However, the
089 * {@link #out(Object...)} and {@link #err(Object...)} methods may be invoked
090 * concurrently by any number of threads.
091 */
092@Extensible()
093@ThreadSafety(level=ThreadSafetyLevel.INTERFACE_NOT_THREADSAFE)
094public abstract class CommandLineTool
095{
096  // The print stream that was originally used for standard output.  It may not
097  // be the current standard output stream if an output file has been
098  // configured.
099  private final PrintStream originalOut;
100
101  // The print stream that was originally used for standard error.  It may not
102  // be the current standard error stream if an output file has been configured.
103  private final PrintStream originalErr;
104
105  // The print stream to use for messages written to standard output.
106  private volatile PrintStream out;
107
108  // The print stream to use for messages written to standard error.
109  private volatile PrintStream err;
110
111  // The argument used to indicate that the tool should append to the output
112  // file rather than overwrite it.
113  private BooleanArgument appendToOutputFileArgument = null;
114
115  // The argument used to request tool help.
116  private BooleanArgument helpArgument = null;
117
118  // The argument used to request help about SASL authentication.
119  private BooleanArgument helpSASLArgument = null;
120
121  // The argument used to request help information about all of the subcommands.
122  private BooleanArgument helpSubcommandsArgument = null;
123
124  // The argument used to request interactive mode.
125  private BooleanArgument interactiveArgument = null;
126
127  // The argument used to indicate that output should be written to standard out
128  // as well as the specified output file.
129  private BooleanArgument teeOutputArgument = null;
130
131  // The argument used to request the tool version.
132  private BooleanArgument versionArgument = null;
133
134  // The argument used to specify the output file for standard output and
135  // standard error.
136  private FileArgument outputFileArgument = null;
137
138
139
140  /**
141   * Creates a new instance of this command-line tool with the provided
142   * information.
143   *
144   * @param  outStream  The output stream to use for standard output.  It may be
145   *                    {@code System.out} for the JVM's default standard output
146   *                    stream, {@code null} if no output should be generated,
147   *                    or a custom output stream if the output should be sent
148   *                    to an alternate location.
149   * @param  errStream  The output stream to use for standard error.  It may be
150   *                    {@code System.err} for the JVM's default standard error
151   *                    stream, {@code null} if no output should be generated,
152   *                    or a custom output stream if the output should be sent
153   *                    to an alternate location.
154   */
155  public CommandLineTool(final OutputStream outStream,
156                         final OutputStream errStream)
157  {
158    if (outStream == null)
159    {
160      out = NullOutputStream.getPrintStream();
161    }
162    else
163    {
164      out = new PrintStream(outStream);
165    }
166
167    if (errStream == null)
168    {
169      err = NullOutputStream.getPrintStream();
170    }
171    else
172    {
173      err = new PrintStream(errStream);
174    }
175
176    originalOut = out;
177    originalErr = err;
178  }
179
180
181
182  /**
183   * Performs all processing for this command-line tool.  This includes:
184   * <UL>
185   *   <LI>Creating the argument parser and populating it using the
186   *       {@link #addToolArguments} method.</LI>
187   *   <LI>Parsing the provided set of command line arguments, including any
188   *       additional validation using the {@link #doExtendedArgumentValidation}
189   *       method.</LI>
190   *   <LI>Invoking the {@link #doToolProcessing} method to do the appropriate
191   *       work for this tool.</LI>
192   * </UL>
193   *
194   * @param  args  The command-line arguments provided to this program.
195   *
196   * @return  The result of processing this tool.  It should be
197   *          {@link ResultCode#SUCCESS} if the tool completed its work
198   *          successfully, or some other result if a problem occurred.
199   */
200  public final ResultCode runTool(final String... args)
201  {
202    final ArgumentParser parser;
203    try
204    {
205      parser = createArgumentParser();
206      if (supportsInteractiveMode() && defaultsToInteractiveMode() &&
207          ((args == null) || (args.length == 0)))
208      {
209        // We'll go ahead and perform argument parsing even though no arguments
210        // were provided because there might be a properties file that should
211        // prevent running in interactive mode.  But we'll ignore any exception
212        // thrown during argument parsing because the tool might require
213        // arguments when run non-interactively.
214        try
215        {
216          parser.parse(args);
217        }
218        catch (final Exception e)
219        {
220          debugException(e);
221        }
222      }
223      else
224      {
225        parser.parse(args);
226      }
227
228      final File generatedPropertiesFile = parser.getGeneratedPropertiesFile();
229      if (supportsPropertiesFile() && (generatedPropertiesFile != null))
230      {
231        wrapOut(0, StaticUtils.TERMINAL_WIDTH_COLUMNS - 1,
232             INFO_CL_TOOL_WROTE_PROPERTIES_FILE.get(
233                  generatedPropertiesFile.getAbsolutePath()));
234        return ResultCode.SUCCESS;
235      }
236
237      if (helpArgument.isPresent())
238      {
239        out(parser.getUsageString(StaticUtils.TERMINAL_WIDTH_COLUMNS - 1));
240        displayExampleUsages(parser);
241        return ResultCode.SUCCESS;
242      }
243
244      if ((helpSASLArgument != null) && helpSASLArgument.isPresent())
245      {
246        out(SASLUtils.getUsageString(StaticUtils.TERMINAL_WIDTH_COLUMNS - 1));
247        return ResultCode.SUCCESS;
248      }
249
250      if ((helpSubcommandsArgument != null) &&
251          helpSubcommandsArgument.isPresent())
252      {
253        final TreeMap<String,SubCommand> subCommands =
254             getSortedSubCommands(parser);
255        for (final SubCommand sc : subCommands.values())
256        {
257          final StringBuilder nameBuffer = new StringBuilder();
258
259          final Iterator<String> nameIterator = sc.getNames().iterator();
260          while (nameIterator.hasNext())
261          {
262            nameBuffer.append(nameIterator.next());
263            if (nameIterator.hasNext())
264            {
265              nameBuffer.append(", ");
266            }
267          }
268          out(nameBuffer.toString());
269
270          for (final String descriptionLine :
271               wrapLine(sc.getDescription(),
272                    (StaticUtils.TERMINAL_WIDTH_COLUMNS - 3)))
273          {
274            out("  " + descriptionLine);
275          }
276          out();
277        }
278
279        wrapOut(0, (StaticUtils.TERMINAL_WIDTH_COLUMNS - 1),
280             INFO_CL_TOOL_USE_SUBCOMMAND_HELP.get(getToolName()));
281        return ResultCode.SUCCESS;
282      }
283
284      if ((versionArgument != null) && versionArgument.isPresent())
285      {
286        out(getToolVersion());
287        return ResultCode.SUCCESS;
288      }
289
290      boolean extendedValidationDone = false;
291      if (interactiveArgument != null)
292      {
293        if (interactiveArgument.isPresent() ||
294            (defaultsToInteractiveMode() &&
295             ((args == null) || (args.length == 0)) &&
296             parser.getArgumentsSetFromPropertiesFile().isEmpty()))
297        {
298          final CommandLineToolInteractiveModeProcessor interactiveProcessor =
299               new CommandLineToolInteractiveModeProcessor(this, parser);
300          try
301          {
302            interactiveProcessor.doInteractiveModeProcessing();
303            extendedValidationDone = true;
304          }
305          catch (final LDAPException le)
306          {
307            debugException(le);
308
309            final String message = le.getMessage();
310            if ((message != null) && (message.length() > 0))
311            {
312              err(message);
313            }
314
315            return le.getResultCode();
316          }
317        }
318      }
319
320      if (! extendedValidationDone)
321      {
322        doExtendedArgumentValidation();
323      }
324    }
325    catch (final ArgumentException ae)
326    {
327      debugException(ae);
328      err(ae.getMessage());
329      return ResultCode.PARAM_ERROR;
330    }
331
332    if ((outputFileArgument != null) && outputFileArgument.isPresent())
333    {
334      final File outputFile = outputFileArgument.getValue();
335      final boolean append = ((appendToOutputFileArgument != null) &&
336           appendToOutputFileArgument.isPresent());
337
338      final PrintStream outputFileStream;
339      try
340      {
341        final FileOutputStream fos = new FileOutputStream(outputFile, append);
342        outputFileStream = new PrintStream(fos, true, "UTF-8");
343      }
344      catch (final Exception e)
345      {
346        debugException(e);
347        err(ERR_CL_TOOL_ERROR_CREATING_OUTPUT_FILE.get(
348             outputFile.getAbsolutePath(), getExceptionMessage(e)));
349        return ResultCode.LOCAL_ERROR;
350      }
351
352      if ((teeOutputArgument != null) && teeOutputArgument.isPresent())
353      {
354        out = new PrintStream(new TeeOutputStream(out, outputFileStream));
355        err = new PrintStream(new TeeOutputStream(err, outputFileStream));
356      }
357      else
358      {
359        out = outputFileStream;
360        err = outputFileStream;
361      }
362    }
363
364
365    // If any values were selected using a properties file, then display
366    // information about them.
367    final List<String> argsSetFromPropertiesFiles =
368         parser.getArgumentsSetFromPropertiesFile();
369    if ((! argsSetFromPropertiesFiles.isEmpty()) &&
370        (! parser.suppressPropertiesFileComment()))
371    {
372      for (final String line :
373           wrapLine(
374                INFO_CL_TOOL_ARGS_FROM_PROPERTIES_FILE.get(
375                     parser.getPropertiesFileUsed().getPath()),
376                (TERMINAL_WIDTH_COLUMNS - 3)))
377      {
378        out("# ", line);
379      }
380
381      final StringBuilder buffer = new StringBuilder();
382      for (final String s : argsSetFromPropertiesFiles)
383      {
384        if (s.startsWith("-"))
385        {
386          if (buffer.length() > 0)
387          {
388            out(buffer);
389            buffer.setLength(0);
390          }
391
392          buffer.append("#      ");
393          buffer.append(s);
394        }
395        else
396        {
397          if (buffer.length() == 0)
398          {
399            // This should never happen.
400            buffer.append("#      ");
401          }
402          else
403          {
404            buffer.append(' ');
405          }
406
407          buffer.append(StaticUtils.cleanExampleCommandLineArgument(s));
408        }
409      }
410
411      if (buffer.length() > 0)
412      {
413        out(buffer);
414      }
415
416      out();
417    }
418
419
420    CommandLineToolShutdownHook shutdownHook = null;
421    final AtomicReference<ResultCode> exitCode =
422         new AtomicReference<ResultCode>();
423    if (registerShutdownHook())
424    {
425      shutdownHook = new CommandLineToolShutdownHook(this, exitCode);
426      Runtime.getRuntime().addShutdownHook(shutdownHook);
427    }
428
429    try
430    {
431      exitCode.set(doToolProcessing());
432    }
433    catch (final Exception e)
434    {
435      debugException(e);
436      err(getExceptionMessage(e));
437      exitCode.set(ResultCode.LOCAL_ERROR);
438    }
439    finally
440    {
441      if (shutdownHook != null)
442      {
443        Runtime.getRuntime().removeShutdownHook(shutdownHook);
444      }
445    }
446
447    return exitCode.get();
448  }
449
450
451
452  /**
453   * Retrieves a sorted map of subcommands for the provided argument parser,
454   * alphabetized by primary name.
455   *
456   * @param  parser  The argument parser for which to get the sorted
457   *                 subcommands.
458   *
459   * @return  The sorted map of subcommands.
460   */
461  private static TreeMap<String,SubCommand> getSortedSubCommands(
462                                                 final ArgumentParser parser)
463  {
464    final TreeMap<String,SubCommand> m = new TreeMap<String,SubCommand>();
465    for (final SubCommand sc : parser.getSubCommands())
466    {
467      m.put(sc.getPrimaryName(), sc);
468    }
469    return m;
470  }
471
472
473
474  /**
475   * Writes example usage information for this tool to the standard output
476   * stream.
477   *
478   * @param  parser  The argument parser used to process the provided set of
479   *                 command-line arguments.
480   */
481  private void displayExampleUsages(final ArgumentParser parser)
482  {
483    final LinkedHashMap<String[],String> examples;
484    if ((parser != null) && (parser.getSelectedSubCommand() != null))
485    {
486      examples = parser.getSelectedSubCommand().getExampleUsages();
487    }
488    else
489    {
490      examples = getExampleUsages();
491    }
492
493    if ((examples == null) || examples.isEmpty())
494    {
495      return;
496    }
497
498    out(INFO_CL_TOOL_LABEL_EXAMPLES);
499
500    final int wrapWidth = StaticUtils.TERMINAL_WIDTH_COLUMNS - 1;
501    for (final Map.Entry<String[],String> e : examples.entrySet())
502    {
503      out();
504      wrapOut(2, wrapWidth, e.getValue());
505      out();
506
507      final StringBuilder buffer = new StringBuilder();
508      buffer.append("    ");
509      buffer.append(getToolName());
510
511      final String[] args = e.getKey();
512      for (int i=0; i < args.length; i++)
513      {
514        buffer.append(' ');
515
516        // If the argument has a value, then make sure to keep it on the same
517        // line as the argument name.  This may introduce false positives due to
518        // unnamed trailing arguments, but the worst that will happen that case
519        // is that the output may be wrapped earlier than necessary one time.
520        String arg = args[i];
521        if (arg.startsWith("-"))
522        {
523          if ((i < (args.length - 1)) && (! args[i+1].startsWith("-")))
524          {
525            final ExampleCommandLineArgument cleanArg =
526                ExampleCommandLineArgument.getCleanArgument(args[i+1]);
527            arg += ' ' + cleanArg.getLocalForm();
528            i++;
529          }
530        }
531        else
532        {
533          final ExampleCommandLineArgument cleanArg =
534              ExampleCommandLineArgument.getCleanArgument(arg);
535          arg = cleanArg.getLocalForm();
536        }
537
538        if ((buffer.length() + arg.length() + 2) < wrapWidth)
539        {
540          buffer.append(arg);
541        }
542        else
543        {
544          buffer.append('\\');
545          out(buffer.toString());
546          buffer.setLength(0);
547          buffer.append("         ");
548          buffer.append(arg);
549        }
550      }
551
552      out(buffer.toString());
553    }
554  }
555
556
557
558  /**
559   * Retrieves the name of this tool.  It should be the name of the command used
560   * to invoke this tool.
561   *
562   * @return  The name for this tool.
563   */
564  public abstract String getToolName();
565
566
567
568  /**
569   * Retrieves a human-readable description for this tool.
570   *
571   * @return  A human-readable description for this tool.
572   */
573  public abstract String getToolDescription();
574
575
576
577  /**
578   * Retrieves a version string for this tool, if available.
579   *
580   * @return  A version string for this tool, or {@code null} if none is
581   *          available.
582   */
583  public String getToolVersion()
584  {
585    return null;
586  }
587
588
589
590  /**
591   * Retrieves the minimum number of unnamed trailing arguments that must be
592   * provided for this tool.  If a tool requires the use of trailing arguments,
593   * then it must override this method and the {@link #getMaxTrailingArguments}
594   * arguments to return nonzero values, and it must also override the
595   * {@link #getTrailingArgumentsPlaceholder} method to return a
596   * non-{@code null} value.
597   *
598   * @return  The minimum number of unnamed trailing arguments that may be
599   *          provided for this tool.  A value of zero indicates that the tool
600   *          may be invoked without any trailing arguments.
601   */
602  public int getMinTrailingArguments()
603  {
604    return 0;
605  }
606
607
608
609  /**
610   * Retrieves the maximum number of unnamed trailing arguments that may be
611   * provided for this tool.  If a tool supports trailing arguments, then it
612   * must override this method to return a nonzero value, and must also override
613   * the {@link CommandLineTool#getTrailingArgumentsPlaceholder} method to
614   * return a non-{@code null} value.
615   *
616   * @return  The maximum number of unnamed trailing arguments that may be
617   *          provided for this tool.  A value of zero indicates that trailing
618   *          arguments are not allowed.  A negative value indicates that there
619   *          should be no limit on the number of trailing arguments.
620   */
621  public int getMaxTrailingArguments()
622  {
623    return 0;
624  }
625
626
627
628  /**
629   * Retrieves a placeholder string that should be used for trailing arguments
630   * in the usage information for this tool.
631   *
632   * @return  A placeholder string that should be used for trailing arguments in
633   *          the usage information for this tool, or {@code null} if trailing
634   *          arguments are not supported.
635   */
636  public String getTrailingArgumentsPlaceholder()
637  {
638    return null;
639  }
640
641
642
643  /**
644   * Indicates whether this tool should provide support for an interactive mode,
645   * in which the tool offers a mode in which the arguments can be provided in
646   * a text-driven menu rather than requiring them to be given on the command
647   * line.  If interactive mode is supported, it may be invoked using the
648   * "--interactive" argument.  Alternately, if interactive mode is supported
649   * and {@link #defaultsToInteractiveMode()} returns {@code true}, then
650   * interactive mode may be invoked by simply launching the tool without any
651   * arguments.
652   *
653   * @return  {@code true} if this tool supports interactive mode, or
654   *          {@code false} if not.
655   */
656  public boolean supportsInteractiveMode()
657  {
658    return false;
659  }
660
661
662
663  /**
664   * Indicates whether this tool defaults to launching in interactive mode if
665   * the tool is invoked without any command-line arguments.  This will only be
666   * used if {@link #supportsInteractiveMode()} returns {@code true}.
667   *
668   * @return  {@code true} if this tool defaults to using interactive mode if
669   *          launched without any command-line arguments, or {@code false} if
670   *          not.
671   */
672  public boolean defaultsToInteractiveMode()
673  {
674    return false;
675  }
676
677
678
679  /**
680   * Indicates whether this tool supports the use of a properties file for
681   * specifying default values for arguments that aren't specified on the
682   * command line.
683   *
684   * @return  {@code true} if this tool supports the use of a properties file
685   *          for specifying default values for arguments that aren't specified
686   *          on the command line, or {@code false} if not.
687   */
688  public boolean supportsPropertiesFile()
689  {
690    return false;
691  }
692
693
694
695  /**
696   * Indicates whether this tool should provide arguments for redirecting output
697   * to a file.  If this method returns {@code true}, then the tool will offer
698   * an "--outputFile" argument that will specify the path to a file to which
699   * all standard output and standard error content will be written, and it will
700   * also offer a "--teeToStandardOut" argument that can only be used if the
701   * "--outputFile" argument is present and will cause all output to be written
702   * to both the specified output file and to standard output.
703   *
704   * @return  {@code true} if this tool should provide arguments for redirecting
705   *          output to a file, or {@code false} if not.
706   */
707  protected boolean supportsOutputFile()
708  {
709    return false;
710  }
711
712
713
714  /**
715   * Creates a parser that can be used to to parse arguments accepted by
716   * this tool.
717   *
718   * @return ArgumentParser that can be used to parse arguments for this
719   *         tool.
720   *
721   * @throws ArgumentException  If there was a problem initializing the
722   *                            parser for this tool.
723   */
724  public final ArgumentParser createArgumentParser()
725         throws ArgumentException
726  {
727    final ArgumentParser parser = new ArgumentParser(getToolName(),
728         getToolDescription(), getMinTrailingArguments(),
729         getMaxTrailingArguments(), getTrailingArgumentsPlaceholder());
730
731    addToolArguments(parser);
732
733    if (supportsInteractiveMode())
734    {
735      interactiveArgument = new BooleanArgument(null, "interactive",
736           INFO_CL_TOOL_DESCRIPTION_INTERACTIVE.get());
737      interactiveArgument.setUsageArgument(true);
738      parser.addArgument(interactiveArgument);
739    }
740
741    if (supportsOutputFile())
742    {
743      outputFileArgument = new FileArgument(null, "outputFile", false, 1, null,
744           INFO_CL_TOOL_DESCRIPTION_OUTPUT_FILE.get(), false, true, true,
745           false);
746      outputFileArgument.addLongIdentifier("output-file");
747      outputFileArgument.setUsageArgument(true);
748      parser.addArgument(outputFileArgument);
749
750      appendToOutputFileArgument = new BooleanArgument(null,
751           "appendToOutputFile", 1,
752           INFO_CL_TOOL_DESCRIPTION_APPEND_TO_OUTPUT_FILE.get(
753                outputFileArgument.getIdentifierString()));
754      appendToOutputFileArgument.addLongIdentifier("append-to-output-file");
755      appendToOutputFileArgument.setUsageArgument(true);
756      parser.addArgument(appendToOutputFileArgument);
757
758      teeOutputArgument = new BooleanArgument(null, "teeOutput", 1,
759           INFO_CL_TOOL_DESCRIPTION_TEE_OUTPUT.get(
760                outputFileArgument.getIdentifierString()));
761      teeOutputArgument.addLongIdentifier("tee-output");
762      teeOutputArgument.setUsageArgument(true);
763      parser.addArgument(teeOutputArgument);
764
765      parser.addDependentArgumentSet(appendToOutputFileArgument,
766           outputFileArgument);
767      parser.addDependentArgumentSet(teeOutputArgument,
768           outputFileArgument);
769    }
770
771    helpArgument = new BooleanArgument('H', "help",
772         INFO_CL_TOOL_DESCRIPTION_HELP.get());
773    helpArgument.addShortIdentifier('?');
774    helpArgument.setUsageArgument(true);
775    parser.addArgument(helpArgument);
776
777    if (! parser.getSubCommands().isEmpty())
778    {
779      helpSubcommandsArgument = new BooleanArgument(null, "helpSubcommands", 1,
780           INFO_CL_TOOL_DESCRIPTION_HELP_SUBCOMMANDS.get());
781      helpSubcommandsArgument.addLongIdentifier("help-subcommands");
782      helpSubcommandsArgument.setUsageArgument(true);
783      parser.addArgument(helpSubcommandsArgument);
784    }
785
786    final String version = getToolVersion();
787    if ((version != null) && (version.length() > 0) &&
788        (parser.getNamedArgument("version") == null))
789    {
790      final Character shortIdentifier;
791      if (parser.getNamedArgument('V') == null)
792      {
793        shortIdentifier = 'V';
794      }
795      else
796      {
797        shortIdentifier = null;
798      }
799
800      versionArgument = new BooleanArgument(shortIdentifier, "version",
801           INFO_CL_TOOL_DESCRIPTION_VERSION.get());
802      versionArgument.setUsageArgument(true);
803      parser.addArgument(versionArgument);
804    }
805
806    if (supportsPropertiesFile())
807    {
808      parser.enablePropertiesFileSupport();
809    }
810
811    return parser;
812  }
813
814
815
816  /**
817   * Specifies the argument that is used to retrieve usage information about
818   * SASL authentication.
819   *
820   * @param  helpSASLArgument  The argument that is used to retrieve usage
821   *                           information about SASL authentication.
822   */
823  void setHelpSASLArgument(final BooleanArgument helpSASLArgument)
824  {
825    this.helpSASLArgument = helpSASLArgument;
826  }
827
828
829
830  /**
831   * Retrieves a set containing the long identifiers used for usage arguments
832   * injected by this class.
833   *
834   * @param  tool  The tool to use to help make the determination.
835   *
836   * @return  A set containing the long identifiers used for usage arguments
837   *          injected by this class.
838   */
839  static Set<String> getUsageArgumentIdentifiers(final CommandLineTool tool)
840  {
841    final LinkedHashSet<String> ids = new LinkedHashSet<String>(9);
842
843    ids.add("help");
844    ids.add("version");
845    ids.add("helpSubcommands");
846
847    if (tool.supportsInteractiveMode())
848    {
849      ids.add("interactive");
850    }
851
852    if (tool.supportsPropertiesFile())
853    {
854      ids.add("propertiesFilePath");
855      ids.add("generatePropertiesFile");
856      ids.add("noPropertiesFile");
857      ids.add("suppressPropertiesFileComment");
858    }
859
860    if (tool.supportsOutputFile())
861    {
862      ids.add("outputFile");
863      ids.add("appendToOutputFile");
864      ids.add("teeOutput");
865    }
866
867    return Collections.unmodifiableSet(ids);
868  }
869
870
871
872  /**
873   * Adds the command-line arguments supported for use with this tool to the
874   * provided argument parser.  The tool may need to retain references to the
875   * arguments (and/or the argument parser, if trailing arguments are allowed)
876   * to it in order to obtain their values for use in later processing.
877   *
878   * @param  parser  The argument parser to which the arguments are to be added.
879   *
880   * @throws  ArgumentException  If a problem occurs while adding any of the
881   *                             tool-specific arguments to the provided
882   *                             argument parser.
883   */
884  public abstract void addToolArguments(ArgumentParser parser)
885         throws ArgumentException;
886
887
888
889  /**
890   * Performs any necessary processing that should be done to ensure that the
891   * provided set of command-line arguments were valid.  This method will be
892   * called after the basic argument parsing has been performed and immediately
893   * before the {@link CommandLineTool#doToolProcessing} method is invoked.
894   * Note that if the tool supports interactive mode, then this method may be
895   * invoked multiple times to allow the user to interactively fix validation
896   * errors.
897   *
898   * @throws  ArgumentException  If there was a problem with the command-line
899   *                             arguments provided to this program.
900   */
901  public void doExtendedArgumentValidation()
902         throws ArgumentException
903  {
904    // No processing will be performed by default.
905  }
906
907
908
909  /**
910   * Performs the core set of processing for this tool.
911   *
912   * @return  A result code that indicates whether the processing completed
913   *          successfully.
914   */
915  public abstract ResultCode doToolProcessing();
916
917
918
919  /**
920   * Indicates whether this tool should register a shutdown hook with the JVM.
921   * Shutdown hooks allow for a best-effort attempt to perform a specified set
922   * of processing when the JVM is shutting down under various conditions,
923   * including:
924   * <UL>
925   *   <LI>When all non-daemon threads have stopped running (i.e., the tool has
926   *       completed processing).</LI>
927   *   <LI>When {@code System.exit()} or {@code Runtime.exit()} is called.</LI>
928   *   <LI>When the JVM receives an external kill signal (e.g., via the use of
929   *       the kill tool or interrupting the JVM with Ctrl+C).</LI>
930   * </UL>
931   * Shutdown hooks may not be invoked if the process is forcefully killed
932   * (e.g., using "kill -9", or the {@code System.halt()} or
933   * {@code Runtime.halt()} methods).
934   * <BR><BR>
935   * If this method is overridden to return {@code true}, then the
936   * {@link #doShutdownHookProcessing(ResultCode)} method should also be
937   * overridden to contain the logic that will be invoked when the JVM is
938   * shutting down in a manner that calls shutdown hooks.
939   *
940   * @return  {@code true} if this tool should register a shutdown hook, or
941   *          {@code false} if not.
942   */
943  protected boolean registerShutdownHook()
944  {
945    return false;
946  }
947
948
949
950  /**
951   * Performs any processing that may be needed when the JVM is shutting down,
952   * whether because tool processing has completed or because it has been
953   * interrupted (e.g., by a kill or break signal).
954   * <BR><BR>
955   * Note that because shutdown hooks run at a delicate time in the life of the
956   * JVM, they should complete quickly and minimize access to external
957   * resources.  See the documentation for the
958   * {@code java.lang.Runtime.addShutdownHook} method for recommendations and
959   * restrictions about writing shutdown hooks.
960   *
961   * @param  resultCode  The result code returned by the tool.  It may be
962   *                     {@code null} if the tool was interrupted before it
963   *                     completed processing.
964   */
965  protected void doShutdownHookProcessing(final ResultCode resultCode)
966  {
967    throw new LDAPSDKUsageException(
968         ERR_COMMAND_LINE_TOOL_SHUTDOWN_HOOK_NOT_IMPLEMENTED.get(
969              getToolName()));
970  }
971
972
973
974  /**
975   * Retrieves a set of information that may be used to generate example usage
976   * information.  Each element in the returned map should consist of a map
977   * between an example set of arguments and a string that describes the
978   * behavior of the tool when invoked with that set of arguments.
979   *
980   * @return  A set of information that may be used to generate example usage
981   *          information.  It may be {@code null} or empty if no example usage
982   *          information is available.
983   */
984  @ThreadSafety(level=ThreadSafetyLevel.METHOD_THREADSAFE)
985  public LinkedHashMap<String[],String> getExampleUsages()
986  {
987    return null;
988  }
989
990
991
992  /**
993   * Retrieves the print stream that will be used for standard output.
994   *
995   * @return  The print stream that will be used for standard output.
996   */
997  public final PrintStream getOut()
998  {
999    return out;
1000  }
1001
1002
1003
1004  /**
1005   * Retrieves the print stream that may be used to write to the original
1006   * standard output.  This may be different from the current standard output
1007   * stream if an output file has been configured.
1008   *
1009   * @return  The print stream that may be used to write to the original
1010   *          standard output.
1011   */
1012  public final PrintStream getOriginalOut()
1013  {
1014    return originalOut;
1015  }
1016
1017
1018
1019  /**
1020   * Writes the provided message to the standard output stream for this tool.
1021   * <BR><BR>
1022   * This method is completely threadsafe and my be invoked concurrently by any
1023   * number of threads.
1024   *
1025   * @param  msg  The message components that will be written to the standard
1026   *              output stream.  They will be concatenated together on the same
1027   *              line, and that line will be followed by an end-of-line
1028   *              sequence.
1029   */
1030  @ThreadSafety(level=ThreadSafetyLevel.METHOD_THREADSAFE)
1031  public final synchronized void out(final Object... msg)
1032  {
1033    write(out, 0, 0, msg);
1034  }
1035
1036
1037
1038  /**
1039   * Writes the provided message to the standard output stream for this tool,
1040   * optionally wrapping and/or indenting the text in the process.
1041   * <BR><BR>
1042   * This method is completely threadsafe and my be invoked concurrently by any
1043   * number of threads.
1044   *
1045   * @param  indent      The number of spaces each line should be indented.  A
1046   *                     value less than or equal to zero indicates that no
1047   *                     indent should be used.
1048   * @param  wrapColumn  The column at which to wrap long lines.  A value less
1049   *                     than or equal to two indicates that no wrapping should
1050   *                     be performed.  If both an indent and a wrap column are
1051   *                     to be used, then the wrap column must be greater than
1052   *                     the indent.
1053   * @param  msg         The message components that will be written to the
1054   *                     standard output stream.  They will be concatenated
1055   *                     together on the same line, and that line will be
1056   *                     followed by an end-of-line sequence.
1057   */
1058  @ThreadSafety(level=ThreadSafetyLevel.METHOD_THREADSAFE)
1059  public final synchronized void wrapOut(final int indent, final int wrapColumn,
1060                                         final Object... msg)
1061  {
1062    write(out, indent, wrapColumn, msg);
1063  }
1064
1065
1066
1067  /**
1068   * Writes the provided message to the standard output stream for this tool,
1069   * optionally wrapping and/or indenting the text in the process.
1070   * <BR><BR>
1071   * This method is completely threadsafe and my be invoked concurrently by any
1072   * number of threads.
1073   *
1074   * @param  firstLineIndent       The number of spaces the first line should be
1075   *                               indented.  A value less than or equal to zero
1076   *                               indicates that no indent should be used.
1077   * @param  subsequentLineIndent  The number of spaces each line except the
1078   *                               first should be indented.  A value less than
1079   *                               or equal to zero indicates that no indent
1080   *                               should be used.
1081   * @param  wrapColumn            The column at which to wrap long lines.  A
1082   *                               value less than or equal to two indicates
1083   *                               that no wrapping should be performed.  If
1084   *                               both an indent and a wrap column are to be
1085   *                               used, then the wrap column must be greater
1086   *                               than the indent.
1087   * @param  endWithNewline        Indicates whether a newline sequence should
1088   *                               follow the last line that is printed.
1089   * @param  msg                   The message components that will be written
1090   *                               to the standard output stream.  They will be
1091   *                               concatenated together on the same line, and
1092   *                               that line will be followed by an end-of-line
1093   *                               sequence.
1094   */
1095  final synchronized void wrapStandardOut(final int firstLineIndent,
1096                                          final int subsequentLineIndent,
1097                                          final int wrapColumn,
1098                                          final boolean endWithNewline,
1099                                          final Object... msg)
1100  {
1101    write(out, firstLineIndent, subsequentLineIndent, wrapColumn,
1102         endWithNewline, msg);
1103  }
1104
1105
1106
1107  /**
1108   * Retrieves the print stream that will be used for standard error.
1109   *
1110   * @return  The print stream that will be used for standard error.
1111   */
1112  public final PrintStream getErr()
1113  {
1114    return err;
1115  }
1116
1117
1118
1119  /**
1120   * Retrieves the print stream that may be used to write to the original
1121   * standard error.  This may be different from the current standard error
1122   * stream if an output file has been configured.
1123   *
1124   * @return  The print stream that may be used to write to the original
1125   *          standard error.
1126   */
1127  public final PrintStream getOriginalErr()
1128  {
1129    return originalErr;
1130  }
1131
1132
1133
1134  /**
1135   * Writes the provided message to the standard error stream for this tool.
1136   * <BR><BR>
1137   * This method is completely threadsafe and my be invoked concurrently by any
1138   * number of threads.
1139   *
1140   * @param  msg  The message components that will be written to the standard
1141   *              error stream.  They will be concatenated together on the same
1142   *              line, and that line will be followed by an end-of-line
1143   *              sequence.
1144   */
1145  @ThreadSafety(level=ThreadSafetyLevel.METHOD_THREADSAFE)
1146  public final synchronized void err(final Object... msg)
1147  {
1148    write(err, 0, 0, msg);
1149  }
1150
1151
1152
1153  /**
1154   * Writes the provided message to the standard error stream for this tool,
1155   * optionally wrapping and/or indenting the text in the process.
1156   * <BR><BR>
1157   * This method is completely threadsafe and my be invoked concurrently by any
1158   * number of threads.
1159   *
1160   * @param  indent      The number of spaces each line should be indented.  A
1161   *                     value less than or equal to zero indicates that no
1162   *                     indent should be used.
1163   * @param  wrapColumn  The column at which to wrap long lines.  A value less
1164   *                     than or equal to two indicates that no wrapping should
1165   *                     be performed.  If both an indent and a wrap column are
1166   *                     to be used, then the wrap column must be greater than
1167   *                     the indent.
1168   * @param  msg         The message components that will be written to the
1169   *                     standard output stream.  They will be concatenated
1170   *                     together on the same line, and that line will be
1171   *                     followed by an end-of-line sequence.
1172   */
1173  @ThreadSafety(level=ThreadSafetyLevel.METHOD_THREADSAFE)
1174  public final synchronized void wrapErr(final int indent, final int wrapColumn,
1175                                         final Object... msg)
1176  {
1177    write(err, indent, wrapColumn, msg);
1178  }
1179
1180
1181
1182  /**
1183   * Writes the provided message to the given print stream, optionally wrapping
1184   * and/or indenting the text in the process.
1185   *
1186   * @param  stream      The stream to which the message should be written.
1187   * @param  indent      The number of spaces each line should be indented.  A
1188   *                     value less than or equal to zero indicates that no
1189   *                     indent should be used.
1190   * @param  wrapColumn  The column at which to wrap long lines.  A value less
1191   *                     than or equal to two indicates that no wrapping should
1192   *                     be performed.  If both an indent and a wrap column are
1193   *                     to be used, then the wrap column must be greater than
1194   *                     the indent.
1195   * @param  msg         The message components that will be written to the
1196   *                     standard output stream.  They will be concatenated
1197   *                     together on the same line, and that line will be
1198   *                     followed by an end-of-line sequence.
1199   */
1200  private static void write(final PrintStream stream, final int indent,
1201                            final int wrapColumn, final Object... msg)
1202  {
1203    write(stream, indent, indent, wrapColumn, true, msg);
1204  }
1205
1206
1207
1208  /**
1209   * Writes the provided message to the given print stream, optionally wrapping
1210   * and/or indenting the text in the process.
1211   *
1212   * @param  stream                The stream to which the message should be
1213   *                               written.
1214   * @param  firstLineIndent       The number of spaces the first line should be
1215   *                               indented.  A value less than or equal to zero
1216   *                               indicates that no indent should be used.
1217   * @param  subsequentLineIndent  The number of spaces all lines after the
1218   *                               first should be indented.  A value less than
1219   *                               or equal to zero indicates that no indent
1220   *                               should be used.
1221   * @param  wrapColumn            The column at which to wrap long lines.  A
1222   *                               value less than or equal to two indicates
1223   *                               that no wrapping should be performed.  If
1224   *                               both an indent and a wrap column are to be
1225   *                               used, then the wrap column must be greater
1226   *                               than the indent.
1227   * @param  endWithNewline        Indicates whether a newline sequence should
1228   *                               follow the last line that is printed.
1229   * @param  msg                   The message components that will be written
1230   *                               to the standard output stream.  They will be
1231   *                               concatenated together on the same line, and
1232   *                               that line will be followed by an end-of-line
1233   *                               sequence.
1234   */
1235  private static void write(final PrintStream stream, final int firstLineIndent,
1236                            final int subsequentLineIndent,
1237                            final int wrapColumn,
1238                            final boolean endWithNewline, final Object... msg)
1239  {
1240    final StringBuilder buffer = new StringBuilder();
1241    for (final Object o : msg)
1242    {
1243      buffer.append(o);
1244    }
1245
1246    if (wrapColumn > 2)
1247    {
1248      boolean firstLine = true;
1249      for (final String line :
1250           wrapLine(buffer.toString(), (wrapColumn - firstLineIndent),
1251                (wrapColumn - subsequentLineIndent)))
1252      {
1253        final int indent;
1254        if (firstLine)
1255        {
1256          indent = firstLineIndent;
1257          firstLine = false;
1258        }
1259        else
1260        {
1261          stream.println();
1262          indent = subsequentLineIndent;
1263        }
1264
1265        if (indent > 0)
1266        {
1267          for (int i=0; i < indent; i++)
1268          {
1269            stream.print(' ');
1270          }
1271        }
1272        stream.print(line);
1273      }
1274    }
1275    else
1276    {
1277      if (firstLineIndent > 0)
1278      {
1279        for (int i=0; i < firstLineIndent; i++)
1280        {
1281          stream.print(' ');
1282        }
1283      }
1284      stream.print(buffer.toString());
1285    }
1286
1287    if (endWithNewline)
1288    {
1289      stream.println();
1290    }
1291    stream.flush();
1292  }
1293}