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