001    /*
002     * Copyright 2008-2016 UnboundID Corp.
003     * All Rights Reserved.
004     */
005    /*
006     * Copyright (C) 2008-2016 UnboundID Corp.
007     *
008     * This program is free software; you can redistribute it and/or modify
009     * it under the terms of the GNU General Public License (GPLv2 only)
010     * or the terms of the GNU Lesser General Public License (LGPLv2.1 only)
011     * as published by the Free Software Foundation.
012     *
013     * This program is distributed in the hope that it will be useful,
014     * but WITHOUT ANY WARRANTY; without even the implied warranty of
015     * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
016     * GNU General Public License for more details.
017     *
018     * You should have received a copy of the GNU General Public License
019     * along with this program; if not, see <http://www.gnu.org/licenses>.
020     */
021    package com.unboundid.util;
022    
023    
024    
025    import java.io.File;
026    import java.io.FileOutputStream;
027    import java.io.OutputStream;
028    import java.io.PrintStream;
029    import java.util.Collections;
030    import java.util.Iterator;
031    import java.util.LinkedHashMap;
032    import java.util.LinkedHashSet;
033    import java.util.List;
034    import java.util.Map;
035    import java.util.Set;
036    import java.util.TreeMap;
037    import java.util.concurrent.atomic.AtomicReference;
038    
039    import com.unboundid.ldap.sdk.LDAPException;
040    import com.unboundid.ldap.sdk.ResultCode;
041    import com.unboundid.util.args.ArgumentException;
042    import com.unboundid.util.args.ArgumentParser;
043    import com.unboundid.util.args.BooleanArgument;
044    import com.unboundid.util.args.FileArgument;
045    import com.unboundid.util.args.SubCommand;
046    
047    import static com.unboundid.util.Debug.*;
048    import static com.unboundid.util.StaticUtils.*;
049    import 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)
094    public 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    }