001/*
002 * Copyright 2008-2019 Ping Identity Corporation
003 * All Rights Reserved.
004 */
005/*
006 * Copyright (C) 2008-2019 Ping Identity Corporation
007 *
008 * This program is free software; you can redistribute it and/or modify
009 * it under the terms of the GNU General Public License (GPLv2 only)
010 * or the terms of the GNU Lesser General Public License (LGPLv2.1 only)
011 * as published by the Free Software Foundation.
012 *
013 * This program is distributed in the hope that it will be useful,
014 * but WITHOUT ANY WARRANTY; without even the implied warranty of
015 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
016 * GNU General Public License for more details.
017 *
018 * You should have received a copy of the GNU General Public License
019 * along with this program; if not, see <http://www.gnu.org/licenses>.
020 */
021package com.unboundid.util;
022
023
024
025import java.io.File;
026import java.io.FileOutputStream;
027import java.io.OutputStream;
028import java.io.PrintStream;
029import java.util.ArrayList;
030import java.util.Collections;
031import java.util.HashSet;
032import java.util.Iterator;
033import java.util.LinkedHashMap;
034import java.util.LinkedHashSet;
035import java.util.List;
036import java.util.Map;
037import java.util.Set;
038import java.util.TreeMap;
039import java.util.concurrent.atomic.AtomicReference;
040
041import com.unboundid.ldap.sdk.LDAPException;
042import com.unboundid.ldap.sdk.ResultCode;
043import com.unboundid.util.args.Argument;
044import com.unboundid.util.args.ArgumentException;
045import com.unboundid.util.args.ArgumentHelper;
046import com.unboundid.util.args.ArgumentParser;
047import com.unboundid.util.args.BooleanArgument;
048import com.unboundid.util.args.FileArgument;
049import com.unboundid.util.args.SubCommand;
050import com.unboundid.ldap.sdk.unboundidds.tools.ToolInvocationLogger;
051import com.unboundid.ldap.sdk.unboundidds.tools.ToolInvocationLogDetails;
052import com.unboundid.ldap.sdk.unboundidds.tools.ToolInvocationLogShutdownHook;
053
054import static com.unboundid.util.UtilityMessages.*;
055
056
057
058/**
059 * This class provides a framework for developing command-line tools that use
060 * the argument parser provided as part of the UnboundID LDAP SDK for Java.
061 * This tool adds a "-H" or "--help" option, which can be used to display usage
062 * information for the program, and may also add a "-V" or "--version" option,
063 * which can display the tool version.
064 * <BR><BR>
065 * Subclasses should include their own {@code main} method that creates an
066 * instance of a {@code CommandLineTool} and should invoke the
067 * {@link CommandLineTool#runTool} method with the provided arguments.  For
068 * example:
069 * <PRE>
070 *   public class ExampleCommandLineTool
071 *          extends CommandLineTool
072 *   {
073 *     public static void main(String[] args)
074 *     {
075 *       ExampleCommandLineTool tool = new ExampleCommandLineTool();
076 *       ResultCode resultCode = tool.runTool(args);
077 *       if (resultCode != ResultCode.SUCCESS)
078 *       {
079 *         System.exit(resultCode.intValue());
080 *       }
081 *     }
082 *
083 *     public ExampleCommandLineTool()
084 *     {
085 *       super(System.out, System.err);
086 *     }
087 *
088 *     // The rest of the tool implementation goes here.
089 *     ...
090 *   }
091 * </PRE>.
092 * <BR><BR>
093 * Note that in general, methods in this class are not threadsafe.  However, the
094 * {@link #out(Object...)} and {@link #err(Object...)} methods may be invoked
095 * concurrently by any number of threads.
096 */
097@Extensible()
098@ThreadSafety(level=ThreadSafetyLevel.INTERFACE_NOT_THREADSAFE)
099public abstract class CommandLineTool
100{
101  // The argument used to indicate that the tool should append to the output
102  // file rather than overwrite it.
103  private BooleanArgument appendToOutputFileArgument = null;
104
105  // The argument used to request tool help.
106  private BooleanArgument helpArgument = null;
107
108  // The argument used to request help about SASL authentication.
109  private BooleanArgument helpSASLArgument = null;
110
111  // The argument used to request help information about all of the subcommands.
112  private BooleanArgument helpSubcommandsArgument = null;
113
114  // The argument used to request interactive mode.
115  private BooleanArgument interactiveArgument = null;
116
117  // The argument used to indicate that output should be written to standard out
118  // as well as the specified output file.
119  private BooleanArgument teeOutputArgument = null;
120
121  // The argument used to request the tool version.
122  private BooleanArgument versionArgument = null;
123
124  // The argument used to specify the output file for standard output and
125  // standard error.
126  private FileArgument outputFileArgument = null;
127
128  // A list of arguments that can be used to enable SSL/TLS debugging.
129  private final List<BooleanArgument> enableSSLDebuggingArguments;
130
131  // The password file reader for this tool.
132  private final PasswordFileReader passwordFileReader;
133
134  // The print stream that was originally used for standard output.  It may not
135  // be the current standard output stream if an output file has been
136  // configured.
137  private final PrintStream originalOut;
138
139  // The print stream that was originally used for standard error.  It may not
140  // be the current standard error stream if an output file has been configured.
141  private final PrintStream originalErr;
142
143  // The print stream to use for messages written to standard output.
144  private volatile PrintStream out;
145
146  // The print stream to use for messages written to standard error.
147  private volatile PrintStream err;
148
149
150
151  /**
152   * Creates a new instance of this command-line tool with the provided
153   * information.
154   *
155   * @param  outStream  The output stream to use for standard output.  It may be
156   *                    {@code System.out} for the JVM's default standard output
157   *                    stream, {@code null} if no output should be generated,
158   *                    or a custom output stream if the output should be sent
159   *                    to an alternate location.
160   * @param  errStream  The output stream to use for standard error.  It may be
161   *                    {@code System.err} for the JVM's default standard error
162   *                    stream, {@code null} if no output should be generated,
163   *                    or a custom output stream if the output should be sent
164   *                    to an alternate location.
165   */
166  public CommandLineTool(final OutputStream outStream,
167                         final OutputStream errStream)
168  {
169    if (outStream == null)
170    {
171      out = NullOutputStream.getPrintStream();
172    }
173    else
174    {
175      out = new PrintStream(outStream);
176    }
177
178    if (errStream == null)
179    {
180      err = NullOutputStream.getPrintStream();
181    }
182    else
183    {
184      err = new PrintStream(errStream);
185    }
186
187    originalOut = out;
188    originalErr = err;
189
190    passwordFileReader = new PasswordFileReader(out, err);
191    enableSSLDebuggingArguments = new ArrayList<>(1);
192  }
193
194
195
196  /**
197   * Performs all processing for this command-line tool.  This includes:
198   * <UL>
199   *   <LI>Creating the argument parser and populating it using the
200   *       {@link #addToolArguments} method.</LI>
201   *   <LI>Parsing the provided set of command line arguments, including any
202   *       additional validation using the {@link #doExtendedArgumentValidation}
203   *       method.</LI>
204   *   <LI>Invoking the {@link #doToolProcessing} method to do the appropriate
205   *       work for this tool.</LI>
206   * </UL>
207   *
208   * @param  args  The command-line arguments provided to this program.
209   *
210   * @return  The result of processing this tool.  It should be
211   *          {@link ResultCode#SUCCESS} if the tool completed its work
212   *          successfully, or some other result if a problem occurred.
213   */
214  public final ResultCode runTool(final String... args)
215  {
216    final ArgumentParser parser;
217    try
218    {
219      parser = createArgumentParser();
220      boolean exceptionFromParsingWithNoArgumentsExplicitlyProvided = false;
221      if (supportsInteractiveMode() && defaultsToInteractiveMode() &&
222          ((args == null) || (args.length == 0)))
223      {
224        // We'll go ahead and perform argument parsing even though no arguments
225        // were provided because there might be a properties file that should
226        // prevent running in interactive mode.  But we'll ignore any exception
227        // thrown during argument parsing because the tool might require
228        // arguments when run non-interactively.
229        try
230        {
231          parser.parse(args);
232        }
233        catch (final Exception e)
234        {
235          Debug.debugException(e);
236          exceptionFromParsingWithNoArgumentsExplicitlyProvided = true;
237        }
238      }
239      else
240      {
241        parser.parse(args);
242      }
243
244      final File generatedPropertiesFile = parser.getGeneratedPropertiesFile();
245      if (supportsPropertiesFile() && (generatedPropertiesFile != null))
246      {
247        wrapOut(0, StaticUtils.TERMINAL_WIDTH_COLUMNS - 1,
248             INFO_CL_TOOL_WROTE_PROPERTIES_FILE.get(
249                  generatedPropertiesFile.getAbsolutePath()));
250        return ResultCode.SUCCESS;
251      }
252
253      if (helpArgument.isPresent())
254      {
255        out(parser.getUsageString(StaticUtils.TERMINAL_WIDTH_COLUMNS - 1));
256        displayExampleUsages(parser);
257        return ResultCode.SUCCESS;
258      }
259
260      if ((helpSASLArgument != null) && helpSASLArgument.isPresent())
261      {
262        out(SASLUtils.getUsageString(StaticUtils.TERMINAL_WIDTH_COLUMNS - 1));
263        return ResultCode.SUCCESS;
264      }
265
266      if ((helpSubcommandsArgument != null) &&
267          helpSubcommandsArgument.isPresent())
268      {
269        final TreeMap<String,SubCommand> subCommands =
270             getSortedSubCommands(parser);
271        for (final SubCommand sc : subCommands.values())
272        {
273          final StringBuilder nameBuffer = new StringBuilder();
274
275          final Iterator<String> nameIterator = sc.getNames(false).iterator();
276          while (nameIterator.hasNext())
277          {
278            nameBuffer.append(nameIterator.next());
279            if (nameIterator.hasNext())
280            {
281              nameBuffer.append(", ");
282            }
283          }
284          out(nameBuffer.toString());
285
286          for (final String descriptionLine :
287               StaticUtils.wrapLine(sc.getDescription(),
288                    (StaticUtils.TERMINAL_WIDTH_COLUMNS - 3)))
289          {
290            out("  " + descriptionLine);
291          }
292          out();
293        }
294
295        wrapOut(0, (StaticUtils.TERMINAL_WIDTH_COLUMNS - 1),
296             INFO_CL_TOOL_USE_SUBCOMMAND_HELP.get(getToolName()));
297        return ResultCode.SUCCESS;
298      }
299
300      if ((versionArgument != null) && versionArgument.isPresent())
301      {
302        out(getToolVersion());
303        return ResultCode.SUCCESS;
304      }
305
306      // If we should enable SSL/TLS debugging, then do that now.  Do it before
307      // any kind of user-defined validation is performed.  Java is really
308      // touchy about when this is done, and we need to do it before any
309      // connection attempt is made.
310      for (final BooleanArgument a : enableSSLDebuggingArguments)
311      {
312        if (a.isPresent())
313        {
314          StaticUtils.setSystemProperty("javax.net.debug", "all");
315        }
316      }
317
318      boolean extendedValidationDone = false;
319      if (interactiveArgument != null)
320      {
321        if (interactiveArgument.isPresent() ||
322            (defaultsToInteractiveMode() &&
323             ((args == null) || (args.length == 0)) &&
324             (parser.getArgumentsSetFromPropertiesFile().isEmpty() ||
325                  exceptionFromParsingWithNoArgumentsExplicitlyProvided)))
326        {
327          try
328          {
329            final List<String> interactiveArgs =
330                 requestToolArgumentsInteractively(parser);
331            if (interactiveArgs == null)
332            {
333              final CommandLineToolInteractiveModeProcessor processor =
334                   new CommandLineToolInteractiveModeProcessor(this, parser);
335              processor.doInteractiveModeProcessing();
336              extendedValidationDone = true;
337            }
338            else
339            {
340              ArgumentHelper.reset(parser);
341              parser.parse(StaticUtils.toArray(interactiveArgs, String.class));
342            }
343          }
344          catch (final LDAPException le)
345          {
346            Debug.debugException(le);
347
348            final String message = le.getMessage();
349            if ((message != null) && (! message.isEmpty()))
350            {
351              err(message);
352            }
353
354            return le.getResultCode();
355          }
356        }
357      }
358
359      if (! extendedValidationDone)
360      {
361        doExtendedArgumentValidation();
362      }
363    }
364    catch (final ArgumentException ae)
365    {
366      Debug.debugException(ae);
367      err(ae.getMessage());
368      return ResultCode.PARAM_ERROR;
369    }
370
371    if ((outputFileArgument != null) && outputFileArgument.isPresent())
372    {
373      final File outputFile = outputFileArgument.getValue();
374      final boolean append = ((appendToOutputFileArgument != null) &&
375           appendToOutputFileArgument.isPresent());
376
377      final PrintStream outputFileStream;
378      try
379      {
380        final FileOutputStream fos = new FileOutputStream(outputFile, append);
381        outputFileStream = new PrintStream(fos, true, "UTF-8");
382      }
383      catch (final Exception e)
384      {
385        Debug.debugException(e);
386        err(ERR_CL_TOOL_ERROR_CREATING_OUTPUT_FILE.get(
387             outputFile.getAbsolutePath(), StaticUtils.getExceptionMessage(e)));
388        return ResultCode.LOCAL_ERROR;
389      }
390
391      if ((teeOutputArgument != null) && teeOutputArgument.isPresent())
392      {
393        out = new PrintStream(new TeeOutputStream(out, outputFileStream));
394        err = new PrintStream(new TeeOutputStream(err, outputFileStream));
395      }
396      else
397      {
398        out = outputFileStream;
399        err = outputFileStream;
400      }
401    }
402
403
404    // If any values were selected using a properties file, then display
405    // information about them.
406    final List<String> argsSetFromPropertiesFiles =
407         parser.getArgumentsSetFromPropertiesFile();
408    if ((! argsSetFromPropertiesFiles.isEmpty()) &&
409        (! parser.suppressPropertiesFileComment()))
410    {
411      for (final String line :
412           StaticUtils.wrapLine(
413                INFO_CL_TOOL_ARGS_FROM_PROPERTIES_FILE.get(
414                     parser.getPropertiesFileUsed().getPath()),
415                (StaticUtils.TERMINAL_WIDTH_COLUMNS - 3)))
416      {
417        out("# ", line);
418      }
419
420      final StringBuilder buffer = new StringBuilder();
421      for (final String s : argsSetFromPropertiesFiles)
422      {
423        if (s.startsWith("-"))
424        {
425          if (buffer.length() > 0)
426          {
427            out(buffer);
428            buffer.setLength(0);
429          }
430
431          buffer.append("#      ");
432          buffer.append(s);
433        }
434        else
435        {
436          if (buffer.length() == 0)
437          {
438            // This should never happen.
439            buffer.append("#      ");
440          }
441          else
442          {
443            buffer.append(' ');
444          }
445
446          buffer.append(StaticUtils.cleanExampleCommandLineArgument(s));
447        }
448      }
449
450      if (buffer.length() > 0)
451      {
452        out(buffer);
453      }
454
455      out();
456    }
457
458
459    CommandLineToolShutdownHook shutdownHook = null;
460    final AtomicReference<ResultCode> exitCode = new AtomicReference<>();
461    if (registerShutdownHook())
462    {
463      shutdownHook = new CommandLineToolShutdownHook(this, exitCode);
464      Runtime.getRuntime().addShutdownHook(shutdownHook);
465    }
466
467    final ToolInvocationLogDetails logDetails =
468            ToolInvocationLogger.getLogMessageDetails(
469                    getToolName(), logToolInvocationByDefault(), getErr());
470    ToolInvocationLogShutdownHook logShutdownHook = null;
471
472    if (logDetails.logInvocation())
473    {
474      final HashSet<Argument> argumentsSetFromPropertiesFile =
475           new HashSet<>(StaticUtils.computeMapCapacity(10));
476      final ArrayList<ObjectPair<String,String>> propertiesFileArgList =
477           new ArrayList<>(10);
478      getToolInvocationPropertiesFileArguments(parser,
479           argumentsSetFromPropertiesFile, propertiesFileArgList);
480
481      final ArrayList<ObjectPair<String,String>> providedArgList =
482           new ArrayList<>(10);
483      getToolInvocationProvidedArguments(parser,
484           argumentsSetFromPropertiesFile, providedArgList);
485
486      logShutdownHook = new ToolInvocationLogShutdownHook(logDetails);
487      Runtime.getRuntime().addShutdownHook(logShutdownHook);
488
489      final String propertiesFilePath;
490      if (propertiesFileArgList.isEmpty())
491      {
492        propertiesFilePath = "";
493      }
494      else
495      {
496        final File propertiesFile = parser.getPropertiesFileUsed();
497        if (propertiesFile == null)
498        {
499          propertiesFilePath = "";
500        }
501        else
502        {
503          propertiesFilePath = propertiesFile.getAbsolutePath();
504        }
505      }
506
507      ToolInvocationLogger.logLaunchMessage(logDetails, providedArgList,
508              propertiesFileArgList, propertiesFilePath);
509    }
510
511    try
512    {
513      exitCode.set(doToolProcessing());
514    }
515    catch (final Exception e)
516    {
517      Debug.debugException(e);
518      err(StaticUtils.getExceptionMessage(e));
519      exitCode.set(ResultCode.LOCAL_ERROR);
520    }
521    finally
522    {
523      if (logShutdownHook != null)
524      {
525        Runtime.getRuntime().removeShutdownHook(logShutdownHook);
526
527        String completionMessage = getToolCompletionMessage();
528        if (completionMessage == null)
529        {
530          completionMessage = exitCode.get().getName();
531        }
532
533        ToolInvocationLogger.logCompletionMessage(
534                logDetails, exitCode.get().intValue(), completionMessage);
535      }
536      if (shutdownHook != null)
537      {
538        Runtime.getRuntime().removeShutdownHook(shutdownHook);
539      }
540    }
541
542    return exitCode.get();
543  }
544
545
546
547  /**
548   * Updates the provided argument list with object pairs that comprise the
549   * set of arguments actually provided to this tool on the command line.
550   *
551   * @param  parser                          The argument parser for this tool.
552   *                                         It must not be {@code null}.
553   * @param  argumentsSetFromPropertiesFile  A set that includes all arguments
554   *                                         set from the properties file.
555   * @param  argList                         The list to which the argument
556   *                                         information should be added.  It
557   *                                         must not be {@code null}.  The
558   *                                         first element of each object pair
559   *                                         that is added must be
560   *                                         non-{@code null}.  The second
561   *                                         element in any given pair may be
562   *                                         {@code null} if the first element
563   *                                         represents the name of an argument
564   *                                         that doesn't take any values, the
565   *                                         name of the selected subcommand, or
566   *                                         an unnamed trailing argument.
567   */
568  private static void getToolInvocationProvidedArguments(
569                           final ArgumentParser parser,
570                           final Set<Argument> argumentsSetFromPropertiesFile,
571                           final List<ObjectPair<String,String>> argList)
572  {
573    final String noValue = null;
574    final SubCommand subCommand = parser.getSelectedSubCommand();
575    if (subCommand != null)
576    {
577      argList.add(new ObjectPair<>(subCommand.getPrimaryName(), noValue));
578    }
579
580    for (final Argument arg : parser.getNamedArguments())
581    {
582      // Exclude arguments that weren't provided.
583      if (! arg.isPresent())
584      {
585        continue;
586      }
587
588      // Exclude arguments that were set from the properties file.
589      if (argumentsSetFromPropertiesFile.contains(arg))
590      {
591        continue;
592      }
593
594      if (arg.takesValue())
595      {
596        for (final String value : arg.getValueStringRepresentations(false))
597        {
598          if (arg.isSensitive())
599          {
600            argList.add(new ObjectPair<>(arg.getIdentifierString(),
601                 "*****REDACTED*****"));
602          }
603          else
604          {
605            argList.add(new ObjectPair<>(arg.getIdentifierString(), value));
606          }
607        }
608      }
609      else
610      {
611        argList.add(new ObjectPair<>(arg.getIdentifierString(), noValue));
612      }
613    }
614
615    if (subCommand != null)
616    {
617      getToolInvocationProvidedArguments(subCommand.getArgumentParser(),
618           argumentsSetFromPropertiesFile, argList);
619    }
620
621    for (final String trailingArgument : parser.getTrailingArguments())
622    {
623      argList.add(new ObjectPair<>(trailingArgument, noValue));
624    }
625  }
626
627
628
629  /**
630   * Updates the provided argument list with object pairs that comprise the
631   * set of tool arguments set from a properties file.
632   *
633   * @param  parser                          The argument parser for this tool.
634   *                                         It must not be {@code null}.
635   * @param  argumentsSetFromPropertiesFile  A set that should be updated with
636   *                                         each argument set from the
637   *                                         properties file.
638   * @param  argList                         The list to which the argument
639   *                                         information should be added.  It
640   *                                         must not be {@code null}.  The
641   *                                         first element of each object pair
642   *                                         that is added must be
643   *                                         non-{@code null}.  The second
644   *                                         element in any given pair may be
645   *                                         {@code null} if the first element
646   *                                         represents the name of an argument
647   *                                         that doesn't take any values, the
648   *                                         name of the selected subcommand, or
649   *                                         an unnamed trailing argument.
650   */
651  private static void getToolInvocationPropertiesFileArguments(
652                          final ArgumentParser parser,
653                          final Set<Argument> argumentsSetFromPropertiesFile,
654                          final List<ObjectPair<String,String>> argList)
655  {
656    final ArgumentParser subCommandParser;
657    final SubCommand subCommand = parser.getSelectedSubCommand();
658    if (subCommand == null)
659    {
660      subCommandParser = null;
661    }
662    else
663    {
664      subCommandParser = subCommand.getArgumentParser();
665    }
666
667    final String noValue = null;
668
669    final Iterator<String> iterator =
670            parser.getArgumentsSetFromPropertiesFile().iterator();
671    while (iterator.hasNext())
672    {
673      final String arg = iterator.next();
674      if (arg.startsWith("-"))
675      {
676        Argument a;
677        if (arg.startsWith("--"))
678        {
679          final String longIdentifier = arg.substring(2);
680          a = parser.getNamedArgument(longIdentifier);
681          if ((a == null) && (subCommandParser != null))
682          {
683            a = subCommandParser.getNamedArgument(longIdentifier);
684          }
685        }
686        else
687        {
688          final char shortIdentifier = arg.charAt(1);
689          a = parser.getNamedArgument(shortIdentifier);
690          if ((a == null) && (subCommandParser != null))
691          {
692            a = subCommandParser.getNamedArgument(shortIdentifier);
693          }
694        }
695
696        if (a != null)
697        {
698          argumentsSetFromPropertiesFile.add(a);
699
700          if (a.takesValue())
701          {
702            final String value = iterator.next();
703            if (a.isSensitive())
704            {
705              argList.add(new ObjectPair<>(a.getIdentifierString(), noValue));
706            }
707            else
708            {
709              argList.add(new ObjectPair<>(a.getIdentifierString(), value));
710            }
711          }
712          else
713          {
714            argList.add(new ObjectPair<>(a.getIdentifierString(), noValue));
715          }
716        }
717      }
718      else
719      {
720        argList.add(new ObjectPair<>(arg, noValue));
721      }
722    }
723  }
724
725
726
727  /**
728   * Retrieves a sorted map of subcommands for the provided argument parser,
729   * alphabetized by primary name.
730   *
731   * @param  parser  The argument parser for which to get the sorted
732   *                 subcommands.
733   *
734   * @return  The sorted map of subcommands.
735   */
736  private static TreeMap<String,SubCommand> getSortedSubCommands(
737                                                 final ArgumentParser parser)
738  {
739    final TreeMap<String,SubCommand> m = new TreeMap<>();
740    for (final SubCommand sc : parser.getSubCommands())
741    {
742      m.put(sc.getPrimaryName(), sc);
743    }
744    return m;
745  }
746
747
748
749  /**
750   * Writes example usage information for this tool to the standard output
751   * stream.
752   *
753   * @param  parser  The argument parser used to process the provided set of
754   *                 command-line arguments.
755   */
756  private void displayExampleUsages(final ArgumentParser parser)
757  {
758    final LinkedHashMap<String[],String> examples;
759    if ((parser != null) && (parser.getSelectedSubCommand() != null))
760    {
761      examples = parser.getSelectedSubCommand().getExampleUsages();
762    }
763    else
764    {
765      examples = getExampleUsages();
766    }
767
768    if ((examples == null) || examples.isEmpty())
769    {
770      return;
771    }
772
773    out(INFO_CL_TOOL_LABEL_EXAMPLES);
774
775    final int wrapWidth = StaticUtils.TERMINAL_WIDTH_COLUMNS - 1;
776    for (final Map.Entry<String[],String> e : examples.entrySet())
777    {
778      out();
779      wrapOut(2, wrapWidth, e.getValue());
780      out();
781
782      final StringBuilder buffer = new StringBuilder();
783      buffer.append("    ");
784      buffer.append(getToolName());
785
786      final String[] args = e.getKey();
787      for (int i=0; i < args.length; i++)
788      {
789        buffer.append(' ');
790
791        // If the argument has a value, then make sure to keep it on the same
792        // line as the argument name.  This may introduce false positives due to
793        // unnamed trailing arguments, but the worst that will happen that case
794        // is that the output may be wrapped earlier than necessary one time.
795        String arg = args[i];
796        if (arg.startsWith("-"))
797        {
798          if ((i < (args.length - 1)) && (! args[i+1].startsWith("-")))
799          {
800            final ExampleCommandLineArgument cleanArg =
801                ExampleCommandLineArgument.getCleanArgument(args[i+1]);
802            arg += ' ' + cleanArg.getLocalForm();
803            i++;
804          }
805        }
806        else
807        {
808          final ExampleCommandLineArgument cleanArg =
809              ExampleCommandLineArgument.getCleanArgument(arg);
810          arg = cleanArg.getLocalForm();
811        }
812
813        if ((buffer.length() + arg.length() + 2) < wrapWidth)
814        {
815          buffer.append(arg);
816        }
817        else
818        {
819          buffer.append('\\');
820          out(buffer.toString());
821          buffer.setLength(0);
822          buffer.append("         ");
823          buffer.append(arg);
824        }
825      }
826
827      out(buffer.toString());
828    }
829  }
830
831
832
833  /**
834   * Retrieves the name of this tool.  It should be the name of the command used
835   * to invoke this tool.
836   *
837   * @return  The name for this tool.
838   */
839  public abstract String getToolName();
840
841
842
843  /**
844   * Retrieves a human-readable description for this tool.  If the description
845   * should include multiple paragraphs, then this method should return the text
846   * for the first paragraph, and the
847   * {@link #getAdditionalDescriptionParagraphs()} method should be used to
848   * return the text for the subsequent paragraphs.
849   *
850   * @return  A human-readable description for this tool.
851   */
852  public abstract String getToolDescription();
853
854
855
856  /**
857   * Retrieves additional paragraphs that should be included in the description
858   * for this tool.  If the tool description should include multiple paragraphs,
859   * then the {@link #getToolDescription()} method should return the text of the
860   * first paragraph, and each item in the list returned by this method should
861   * be the text for each subsequent paragraph.  If the tool description should
862   * only have a single paragraph, then this method may return {@code null} or
863   * an empty list.
864   *
865   * @return  Additional paragraphs that should be included in the description
866   *          for this tool, or {@code null} or an empty list if only a single
867   *          description paragraph (whose text is returned by the
868   *          {@code getToolDescription} method) is needed.
869   */
870  public List<String> getAdditionalDescriptionParagraphs()
871  {
872    return Collections.emptyList();
873  }
874
875
876
877  /**
878   * Retrieves a version string for this tool, if available.
879   *
880   * @return  A version string for this tool, or {@code null} if none is
881   *          available.
882   */
883  public String getToolVersion()
884  {
885    return null;
886  }
887
888
889
890  /**
891   * Retrieves the minimum number of unnamed trailing arguments that must be
892   * provided for this tool.  If a tool requires the use of trailing arguments,
893   * then it must override this method and the {@link #getMaxTrailingArguments}
894   * arguments to return nonzero values, and it must also override the
895   * {@link #getTrailingArgumentsPlaceholder} method to return a
896   * non-{@code null} value.
897   *
898   * @return  The minimum number of unnamed trailing arguments that may be
899   *          provided for this tool.  A value of zero indicates that the tool
900   *          may be invoked without any trailing arguments.
901   */
902  public int getMinTrailingArguments()
903  {
904    return 0;
905  }
906
907
908
909  /**
910   * Retrieves the maximum number of unnamed trailing arguments that may be
911   * provided for this tool.  If a tool supports trailing arguments, then it
912   * must override this method to return a nonzero value, and must also override
913   * the {@link CommandLineTool#getTrailingArgumentsPlaceholder} method to
914   * return a non-{@code null} value.
915   *
916   * @return  The maximum number of unnamed trailing arguments that may be
917   *          provided for this tool.  A value of zero indicates that trailing
918   *          arguments are not allowed.  A negative value indicates that there
919   *          should be no limit on the number of trailing arguments.
920   */
921  public int getMaxTrailingArguments()
922  {
923    return 0;
924  }
925
926
927
928  /**
929   * Retrieves a placeholder string that should be used for trailing arguments
930   * in the usage information for this tool.
931   *
932   * @return  A placeholder string that should be used for trailing arguments in
933   *          the usage information for this tool, or {@code null} if trailing
934   *          arguments are not supported.
935   */
936  public String getTrailingArgumentsPlaceholder()
937  {
938    return null;
939  }
940
941
942
943  /**
944   * Indicates whether this tool should provide support for an interactive mode,
945   * in which the tool offers a mode in which the arguments can be provided in
946   * a text-driven menu rather than requiring them to be given on the command
947   * line.  If interactive mode is supported, it may be invoked using the
948   * "--interactive" argument.  Alternately, if interactive mode is supported
949   * and {@link #defaultsToInteractiveMode()} returns {@code true}, then
950   * interactive mode may be invoked by simply launching the tool without any
951   * arguments.
952   *
953   * @return  {@code true} if this tool supports interactive mode, or
954   *          {@code false} if not.
955   */
956  public boolean supportsInteractiveMode()
957  {
958    return false;
959  }
960
961
962
963  /**
964   * Indicates whether this tool defaults to launching in interactive mode if
965   * the tool is invoked without any command-line arguments.  This will only be
966   * used if {@link #supportsInteractiveMode()} returns {@code true}.
967   *
968   * @return  {@code true} if this tool defaults to using interactive mode if
969   *          launched without any command-line arguments, or {@code false} if
970   *          not.
971   */
972  public boolean defaultsToInteractiveMode()
973  {
974    return false;
975  }
976
977
978
979  /**
980   * Interactively prompts the user for information needed to invoke this tool
981   * and returns an appropriate list of arguments that should be used to run it.
982   * <BR><BR>
983   * This method will only be invoked if {@link #supportsInteractiveMode()}
984   * returns {@code true}, and if one of the following conditions is satisfied:
985   * <UL>
986   *   <LI>The {@code --interactive} argument is explicitly provided on the
987   *       command line.</LI>
988   *   <LI>The tool was invoked without any command-line arguments and
989   *       {@link #defaultsToInteractiveMode()} returns {@code true}.</LI>
990   * </UL>
991   * If this method is invoked and returns {@code null}, then the LDAP SDK's
992   * default interactive mode processing will be performed.  Otherwise, the tool
993   * will be invoked with only the arguments in the list that is returned.
994   *
995   * @param  parser  The argument parser that has been used to parse any
996   *                 command-line arguments that were provided before the
997   *                 interactive mode processing was invoked.  If this method
998   *                 returns a non-{@code null} value, then this parser will be
999   *                 reset before parsing the new set of arguments.
1000   *
1001   * @return  Retrieves a list of command-line arguments that may be used to
1002   *          invoke this tool, or {@code null} if the LDAP SDK's default
1003   *          interactive mode processing should be performed.
1004   *
1005   * @throws  LDAPException  If a problem is encountered while interactively
1006   *                         obtaining the arguments that should be used to
1007   *                         run the tool.
1008   */
1009  protected List<String> requestToolArgumentsInteractively(
1010                              final ArgumentParser parser)
1011            throws LDAPException
1012  {
1013    // Fall back to using the LDAP SDK's default interactive mode processor.
1014    return null;
1015  }
1016
1017
1018
1019  /**
1020   * Indicates whether this tool supports the use of a properties file for
1021   * specifying default values for arguments that aren't specified on the
1022   * command line.
1023   *
1024   * @return  {@code true} if this tool supports the use of a properties file
1025   *          for specifying default values for arguments that aren't specified
1026   *          on the command line, or {@code false} if not.
1027   */
1028  public boolean supportsPropertiesFile()
1029  {
1030    return false;
1031  }
1032
1033
1034
1035  /**
1036   * Indicates whether this tool should provide arguments for redirecting output
1037   * to a file.  If this method returns {@code true}, then the tool will offer
1038   * an "--outputFile" argument that will specify the path to a file to which
1039   * all standard output and standard error content will be written, and it will
1040   * also offer a "--teeToStandardOut" argument that can only be used if the
1041   * "--outputFile" argument is present and will cause all output to be written
1042   * to both the specified output file and to standard output.
1043   *
1044   * @return  {@code true} if this tool should provide arguments for redirecting
1045   *          output to a file, or {@code false} if not.
1046   */
1047  protected boolean supportsOutputFile()
1048  {
1049    return false;
1050  }
1051
1052
1053
1054  /**
1055   * Indicates whether to log messages about the launch and completion of this
1056   * tool into the invocation log of Ping Identity server products that may
1057   * include it.  This method is not needed for tools that are not expected to
1058   * be part of the Ping Identity server products suite.  Further, this value
1059   * may be overridden by settings in the server's
1060   * tool-invocation-logging.properties file.
1061   * <BR><BR>
1062   * This method should generally return {@code true} for tools that may alter
1063   * the server configuration, data, or other state information, and
1064   * {@code false} for tools that do not make any changes.
1065   *
1066   * @return  {@code true} if Ping Identity server products should include
1067   *          messages about the launch and completion of this tool in tool
1068   *          invocation log files by default, or {@code false} if not.
1069   */
1070  protected boolean logToolInvocationByDefault()
1071  {
1072    return false;
1073  }
1074
1075
1076
1077  /**
1078   * Retrieves an optional message that may provide additional information about
1079   * the way that the tool completed its processing.  For example if the tool
1080   * exited with an error message, it may be useful for this method to return
1081   * that error message.
1082   * <BR><BR>
1083   * The message returned by this method is intended for purposes and is not
1084   * meant to be parsed or programmatically interpreted.
1085   *
1086   * @return  An optional message that may provide additional information about
1087   *          the completion state for this tool, or {@code null} if no
1088   *          completion message is available.
1089   */
1090  protected String getToolCompletionMessage()
1091  {
1092    return null;
1093  }
1094
1095
1096
1097  /**
1098   * Creates a parser that can be used to to parse arguments accepted by
1099   * this tool.
1100   *
1101   * @return ArgumentParser that can be used to parse arguments for this
1102   *         tool.
1103   *
1104   * @throws ArgumentException  If there was a problem initializing the
1105   *                            parser for this tool.
1106   */
1107  public final ArgumentParser createArgumentParser()
1108         throws ArgumentException
1109  {
1110    final ArgumentParser parser = new ArgumentParser(getToolName(),
1111         getToolDescription(), getAdditionalDescriptionParagraphs(),
1112         getMinTrailingArguments(), getMaxTrailingArguments(),
1113         getTrailingArgumentsPlaceholder());
1114    parser.setCommandLineTool(this);
1115
1116    addToolArguments(parser);
1117
1118    if (supportsInteractiveMode())
1119    {
1120      interactiveArgument = new BooleanArgument(null, "interactive",
1121           INFO_CL_TOOL_DESCRIPTION_INTERACTIVE.get());
1122      interactiveArgument.setUsageArgument(true);
1123      parser.addArgument(interactiveArgument);
1124    }
1125
1126    if (supportsOutputFile())
1127    {
1128      outputFileArgument = new FileArgument(null, "outputFile", false, 1, null,
1129           INFO_CL_TOOL_DESCRIPTION_OUTPUT_FILE.get(), false, true, true,
1130           false);
1131      outputFileArgument.addLongIdentifier("output-file", true);
1132      outputFileArgument.setUsageArgument(true);
1133      parser.addArgument(outputFileArgument);
1134
1135      appendToOutputFileArgument = new BooleanArgument(null,
1136           "appendToOutputFile", 1,
1137           INFO_CL_TOOL_DESCRIPTION_APPEND_TO_OUTPUT_FILE.get(
1138                outputFileArgument.getIdentifierString()));
1139      appendToOutputFileArgument.addLongIdentifier("append-to-output-file",
1140           true);
1141      appendToOutputFileArgument.setUsageArgument(true);
1142      parser.addArgument(appendToOutputFileArgument);
1143
1144      teeOutputArgument = new BooleanArgument(null, "teeOutput", 1,
1145           INFO_CL_TOOL_DESCRIPTION_TEE_OUTPUT.get(
1146                outputFileArgument.getIdentifierString()));
1147      teeOutputArgument.addLongIdentifier("tee-output", true);
1148      teeOutputArgument.setUsageArgument(true);
1149      parser.addArgument(teeOutputArgument);
1150
1151      parser.addDependentArgumentSet(appendToOutputFileArgument,
1152           outputFileArgument);
1153      parser.addDependentArgumentSet(teeOutputArgument,
1154           outputFileArgument);
1155    }
1156
1157    helpArgument = new BooleanArgument('H', "help",
1158         INFO_CL_TOOL_DESCRIPTION_HELP.get());
1159    helpArgument.addShortIdentifier('?', true);
1160    helpArgument.setUsageArgument(true);
1161    parser.addArgument(helpArgument);
1162
1163    if (! parser.getSubCommands().isEmpty())
1164    {
1165      helpSubcommandsArgument = new BooleanArgument(null, "helpSubcommands", 1,
1166           INFO_CL_TOOL_DESCRIPTION_HELP_SUBCOMMANDS.get());
1167      helpSubcommandsArgument.addLongIdentifier("helpSubcommand", true);
1168      helpSubcommandsArgument.addLongIdentifier("help-subcommands", true);
1169      helpSubcommandsArgument.addLongIdentifier("help-subcommand", true);
1170      helpSubcommandsArgument.setUsageArgument(true);
1171      parser.addArgument(helpSubcommandsArgument);
1172    }
1173
1174    final String version = getToolVersion();
1175    if ((version != null) && (! version.isEmpty()) &&
1176        (parser.getNamedArgument("version") == null))
1177    {
1178      final Character shortIdentifier;
1179      if (parser.getNamedArgument('V') == null)
1180      {
1181        shortIdentifier = 'V';
1182      }
1183      else
1184      {
1185        shortIdentifier = null;
1186      }
1187
1188      versionArgument = new BooleanArgument(shortIdentifier, "version",
1189           INFO_CL_TOOL_DESCRIPTION_VERSION.get());
1190      versionArgument.setUsageArgument(true);
1191      parser.addArgument(versionArgument);
1192    }
1193
1194    if (supportsPropertiesFile())
1195    {
1196      parser.enablePropertiesFileSupport();
1197    }
1198
1199    return parser;
1200  }
1201
1202
1203
1204  /**
1205   * Specifies the argument that is used to retrieve usage information about
1206   * SASL authentication.
1207   *
1208   * @param  helpSASLArgument  The argument that is used to retrieve usage
1209   *                           information about SASL authentication.
1210   */
1211  void setHelpSASLArgument(final BooleanArgument helpSASLArgument)
1212  {
1213    this.helpSASLArgument = helpSASLArgument;
1214  }
1215
1216
1217
1218  /**
1219   * Adds the provided argument to the set of arguments that may be used to
1220   * enable JVM SSL/TLS debugging.
1221   *
1222   * @param  enableSSLDebuggingArgument  The argument to add to the set of
1223   *                                     arguments that may be used to enable
1224   *                                     JVM SSL/TLS debugging.
1225   */
1226  protected void addEnableSSLDebuggingArgument(
1227                      final BooleanArgument enableSSLDebuggingArgument)
1228  {
1229    enableSSLDebuggingArguments.add(enableSSLDebuggingArgument);
1230  }
1231
1232
1233
1234  /**
1235   * Retrieves a set containing the long identifiers used for usage arguments
1236   * injected by this class.
1237   *
1238   * @param  tool  The tool to use to help make the determination.
1239   *
1240   * @return  A set containing the long identifiers used for usage arguments
1241   *          injected by this class.
1242   */
1243  static Set<String> getUsageArgumentIdentifiers(final CommandLineTool tool)
1244  {
1245    final LinkedHashSet<String> ids =
1246         new LinkedHashSet<>(StaticUtils.computeMapCapacity(9));
1247
1248    ids.add("help");
1249    ids.add("version");
1250    ids.add("helpSubcommands");
1251
1252    if (tool.supportsInteractiveMode())
1253    {
1254      ids.add("interactive");
1255    }
1256
1257    if (tool.supportsPropertiesFile())
1258    {
1259      ids.add("propertiesFilePath");
1260      ids.add("generatePropertiesFile");
1261      ids.add("noPropertiesFile");
1262      ids.add("suppressPropertiesFileComment");
1263    }
1264
1265    if (tool.supportsOutputFile())
1266    {
1267      ids.add("outputFile");
1268      ids.add("appendToOutputFile");
1269      ids.add("teeOutput");
1270    }
1271
1272    return Collections.unmodifiableSet(ids);
1273  }
1274
1275
1276
1277  /**
1278   * Adds the command-line arguments supported for use with this tool to the
1279   * provided argument parser.  The tool may need to retain references to the
1280   * arguments (and/or the argument parser, if trailing arguments are allowed)
1281   * to it in order to obtain their values for use in later processing.
1282   *
1283   * @param  parser  The argument parser to which the arguments are to be added.
1284   *
1285   * @throws  ArgumentException  If a problem occurs while adding any of the
1286   *                             tool-specific arguments to the provided
1287   *                             argument parser.
1288   */
1289  public abstract void addToolArguments(ArgumentParser parser)
1290         throws ArgumentException;
1291
1292
1293
1294  /**
1295   * Performs any necessary processing that should be done to ensure that the
1296   * provided set of command-line arguments were valid.  This method will be
1297   * called after the basic argument parsing has been performed and immediately
1298   * before the {@link CommandLineTool#doToolProcessing} method is invoked.
1299   * Note that if the tool supports interactive mode, then this method may be
1300   * invoked multiple times to allow the user to interactively fix validation
1301   * errors.
1302   *
1303   * @throws  ArgumentException  If there was a problem with the command-line
1304   *                             arguments provided to this program.
1305   */
1306  public void doExtendedArgumentValidation()
1307         throws ArgumentException
1308  {
1309    // No processing will be performed by default.
1310  }
1311
1312
1313
1314  /**
1315   * Performs the core set of processing for this tool.
1316   *
1317   * @return  A result code that indicates whether the processing completed
1318   *          successfully.
1319   */
1320  public abstract ResultCode doToolProcessing();
1321
1322
1323
1324  /**
1325   * Indicates whether this tool should register a shutdown hook with the JVM.
1326   * Shutdown hooks allow for a best-effort attempt to perform a specified set
1327   * of processing when the JVM is shutting down under various conditions,
1328   * including:
1329   * <UL>
1330   *   <LI>When all non-daemon threads have stopped running (i.e., the tool has
1331   *       completed processing).</LI>
1332   *   <LI>When {@code System.exit()} or {@code Runtime.exit()} is called.</LI>
1333   *   <LI>When the JVM receives an external kill signal (e.g., via the use of
1334   *       the kill tool or interrupting the JVM with Ctrl+C).</LI>
1335   * </UL>
1336   * Shutdown hooks may not be invoked if the process is forcefully killed
1337   * (e.g., using "kill -9", or the {@code System.halt()} or
1338   * {@code Runtime.halt()} methods).
1339   * <BR><BR>
1340   * If this method is overridden to return {@code true}, then the
1341   * {@link #doShutdownHookProcessing(ResultCode)} method should also be
1342   * overridden to contain the logic that will be invoked when the JVM is
1343   * shutting down in a manner that calls shutdown hooks.
1344   *
1345   * @return  {@code true} if this tool should register a shutdown hook, or
1346   *          {@code false} if not.
1347   */
1348  protected boolean registerShutdownHook()
1349  {
1350    return false;
1351  }
1352
1353
1354
1355  /**
1356   * Performs any processing that may be needed when the JVM is shutting down,
1357   * whether because tool processing has completed or because it has been
1358   * interrupted (e.g., by a kill or break signal).
1359   * <BR><BR>
1360   * Note that because shutdown hooks run at a delicate time in the life of the
1361   * JVM, they should complete quickly and minimize access to external
1362   * resources.  See the documentation for the
1363   * {@code java.lang.Runtime.addShutdownHook} method for recommendations and
1364   * restrictions about writing shutdown hooks.
1365   *
1366   * @param  resultCode  The result code returned by the tool.  It may be
1367   *                     {@code null} if the tool was interrupted before it
1368   *                     completed processing.
1369   */
1370  protected void doShutdownHookProcessing(final ResultCode resultCode)
1371  {
1372    throw new LDAPSDKUsageException(
1373         ERR_COMMAND_LINE_TOOL_SHUTDOWN_HOOK_NOT_IMPLEMENTED.get(
1374              getToolName()));
1375  }
1376
1377
1378
1379  /**
1380   * Retrieves a set of information that may be used to generate example usage
1381   * information.  Each element in the returned map should consist of a map
1382   * between an example set of arguments and a string that describes the
1383   * behavior of the tool when invoked with that set of arguments.
1384   *
1385   * @return  A set of information that may be used to generate example usage
1386   *          information.  It may be {@code null} or empty if no example usage
1387   *          information is available.
1388   */
1389  @ThreadSafety(level=ThreadSafetyLevel.METHOD_THREADSAFE)
1390  public LinkedHashMap<String[],String> getExampleUsages()
1391  {
1392    return null;
1393  }
1394
1395
1396
1397  /**
1398   * Retrieves the password file reader for this tool, which may be used to
1399   * read passwords from (optionally compressed and encrypted) files.
1400   *
1401   * @return  The password file reader for this tool.
1402   */
1403  public final PasswordFileReader getPasswordFileReader()
1404  {
1405    return passwordFileReader;
1406  }
1407
1408
1409
1410  /**
1411   * Retrieves the print stream that will be used for standard output.
1412   *
1413   * @return  The print stream that will be used for standard output.
1414   */
1415  public final PrintStream getOut()
1416  {
1417    return out;
1418  }
1419
1420
1421
1422  /**
1423   * Retrieves the print stream that may be used to write to the original
1424   * standard output.  This may be different from the current standard output
1425   * stream if an output file has been configured.
1426   *
1427   * @return  The print stream that may be used to write to the original
1428   *          standard output.
1429   */
1430  public final PrintStream getOriginalOut()
1431  {
1432    return originalOut;
1433  }
1434
1435
1436
1437  /**
1438   * Writes the provided message to the standard output stream for this tool.
1439   * <BR><BR>
1440   * This method is completely threadsafe and my be invoked concurrently by any
1441   * number of threads.
1442   *
1443   * @param  msg  The message components that will be written to the standard
1444   *              output stream.  They will be concatenated together on the same
1445   *              line, and that line will be followed by an end-of-line
1446   *              sequence.
1447   */
1448  @ThreadSafety(level=ThreadSafetyLevel.METHOD_THREADSAFE)
1449  public final synchronized void out(final Object... msg)
1450  {
1451    write(out, 0, 0, msg);
1452  }
1453
1454
1455
1456  /**
1457   * Writes the provided message to the standard output stream for this tool,
1458   * optionally wrapping and/or indenting the text in the process.
1459   * <BR><BR>
1460   * This method is completely threadsafe and my be invoked concurrently by any
1461   * number of threads.
1462   *
1463   * @param  indent      The number of spaces each line should be indented.  A
1464   *                     value less than or equal to zero indicates that no
1465   *                     indent should be used.
1466   * @param  wrapColumn  The column at which to wrap long lines.  A value less
1467   *                     than or equal to two indicates that no wrapping should
1468   *                     be performed.  If both an indent and a wrap column are
1469   *                     to be used, then the wrap column must be greater than
1470   *                     the indent.
1471   * @param  msg         The message components that will be written to the
1472   *                     standard output stream.  They will be concatenated
1473   *                     together on the same line, and that line will be
1474   *                     followed by an end-of-line sequence.
1475   */
1476  @ThreadSafety(level=ThreadSafetyLevel.METHOD_THREADSAFE)
1477  public final synchronized void wrapOut(final int indent, final int wrapColumn,
1478                                         final Object... msg)
1479  {
1480    write(out, indent, wrapColumn, msg);
1481  }
1482
1483
1484
1485  /**
1486   * Writes the provided message to the standard output stream for this tool,
1487   * optionally wrapping and/or indenting the text in the process.
1488   * <BR><BR>
1489   * This method is completely threadsafe and my be invoked concurrently by any
1490   * number of threads.
1491   *
1492   * @param  firstLineIndent       The number of spaces the first line should be
1493   *                               indented.  A value less than or equal to zero
1494   *                               indicates that no indent should be used.
1495   * @param  subsequentLineIndent  The number of spaces each line except the
1496   *                               first should be indented.  A value less than
1497   *                               or equal to zero indicates that no indent
1498   *                               should be used.
1499   * @param  wrapColumn            The column at which to wrap long lines.  A
1500   *                               value less than or equal to two indicates
1501   *                               that no wrapping should be performed.  If
1502   *                               both an indent and a wrap column are to be
1503   *                               used, then the wrap column must be greater
1504   *                               than the indent.
1505   * @param  endWithNewline        Indicates whether a newline sequence should
1506   *                               follow the last line that is printed.
1507   * @param  msg                   The message components that will be written
1508   *                               to the standard output stream.  They will be
1509   *                               concatenated together on the same line, and
1510   *                               that line will be followed by an end-of-line
1511   *                               sequence.
1512   */
1513  final synchronized void wrapStandardOut(final int firstLineIndent,
1514                                          final int subsequentLineIndent,
1515                                          final int wrapColumn,
1516                                          final boolean endWithNewline,
1517                                          final Object... msg)
1518  {
1519    write(out, firstLineIndent, subsequentLineIndent, wrapColumn,
1520         endWithNewline, msg);
1521  }
1522
1523
1524
1525  /**
1526   * Retrieves the print stream that will be used for standard error.
1527   *
1528   * @return  The print stream that will be used for standard error.
1529   */
1530  public final PrintStream getErr()
1531  {
1532    return err;
1533  }
1534
1535
1536
1537  /**
1538   * Retrieves the print stream that may be used to write to the original
1539   * standard error.  This may be different from the current standard error
1540   * stream if an output file has been configured.
1541   *
1542   * @return  The print stream that may be used to write to the original
1543   *          standard error.
1544   */
1545  public final PrintStream getOriginalErr()
1546  {
1547    return originalErr;
1548  }
1549
1550
1551
1552  /**
1553   * Writes the provided message to the standard error stream for this tool.
1554   * <BR><BR>
1555   * This method is completely threadsafe and my be invoked concurrently by any
1556   * number of threads.
1557   *
1558   * @param  msg  The message components that will be written to the standard
1559   *              error stream.  They will be concatenated together on the same
1560   *              line, and that line will be followed by an end-of-line
1561   *              sequence.
1562   */
1563  @ThreadSafety(level=ThreadSafetyLevel.METHOD_THREADSAFE)
1564  public final synchronized void err(final Object... msg)
1565  {
1566    write(err, 0, 0, msg);
1567  }
1568
1569
1570
1571  /**
1572   * Writes the provided message to the standard error stream for this tool,
1573   * optionally wrapping and/or indenting the text in the process.
1574   * <BR><BR>
1575   * This method is completely threadsafe and my be invoked concurrently by any
1576   * number of threads.
1577   *
1578   * @param  indent      The number of spaces each line should be indented.  A
1579   *                     value less than or equal to zero indicates that no
1580   *                     indent should be used.
1581   * @param  wrapColumn  The column at which to wrap long lines.  A value less
1582   *                     than or equal to two indicates that no wrapping should
1583   *                     be performed.  If both an indent and a wrap column are
1584   *                     to be used, then the wrap column must be greater than
1585   *                     the indent.
1586   * @param  msg         The message components that will be written to the
1587   *                     standard output stream.  They will be concatenated
1588   *                     together on the same line, and that line will be
1589   *                     followed by an end-of-line sequence.
1590   */
1591  @ThreadSafety(level=ThreadSafetyLevel.METHOD_THREADSAFE)
1592  public final synchronized void wrapErr(final int indent, final int wrapColumn,
1593                                         final Object... msg)
1594  {
1595    write(err, indent, wrapColumn, msg);
1596  }
1597
1598
1599
1600  /**
1601   * Writes the provided message to the given print stream, optionally wrapping
1602   * and/or indenting the text in the process.
1603   *
1604   * @param  stream      The stream to which the message should be written.
1605   * @param  indent      The number of spaces each line should be indented.  A
1606   *                     value less than or equal to zero indicates that no
1607   *                     indent should be used.
1608   * @param  wrapColumn  The column at which to wrap long lines.  A value less
1609   *                     than or equal to two indicates that no wrapping should
1610   *                     be performed.  If both an indent and a wrap column are
1611   *                     to be used, then the wrap column must be greater than
1612   *                     the indent.
1613   * @param  msg         The message components that will be written to the
1614   *                     standard output stream.  They will be concatenated
1615   *                     together on the same line, and that line will be
1616   *                     followed by an end-of-line sequence.
1617   */
1618  private static void write(final PrintStream stream, final int indent,
1619                            final int wrapColumn, final Object... msg)
1620  {
1621    write(stream, indent, indent, wrapColumn, true, msg);
1622  }
1623
1624
1625
1626  /**
1627   * Writes the provided message to the given print stream, optionally wrapping
1628   * and/or indenting the text in the process.
1629   *
1630   * @param  stream                The stream to which the message should be
1631   *                               written.
1632   * @param  firstLineIndent       The number of spaces the first line should be
1633   *                               indented.  A value less than or equal to zero
1634   *                               indicates that no indent should be used.
1635   * @param  subsequentLineIndent  The number of spaces all lines after the
1636   *                               first should be indented.  A value less than
1637   *                               or equal to zero indicates that no indent
1638   *                               should be used.
1639   * @param  wrapColumn            The column at which to wrap long lines.  A
1640   *                               value less than or equal to two indicates
1641   *                               that no wrapping should be performed.  If
1642   *                               both an indent and a wrap column are to be
1643   *                               used, then the wrap column must be greater
1644   *                               than the indent.
1645   * @param  endWithNewline        Indicates whether a newline sequence should
1646   *                               follow the last line that is printed.
1647   * @param  msg                   The message components that will be written
1648   *                               to the standard output stream.  They will be
1649   *                               concatenated together on the same line, and
1650   *                               that line will be followed by an end-of-line
1651   *                               sequence.
1652   */
1653  private static void write(final PrintStream stream, final int firstLineIndent,
1654                            final int subsequentLineIndent,
1655                            final int wrapColumn,
1656                            final boolean endWithNewline, final Object... msg)
1657  {
1658    final StringBuilder buffer = new StringBuilder();
1659    for (final Object o : msg)
1660    {
1661      buffer.append(o);
1662    }
1663
1664    if (wrapColumn > 2)
1665    {
1666      boolean firstLine = true;
1667      for (final String line :
1668           StaticUtils.wrapLine(buffer.toString(),
1669                (wrapColumn - firstLineIndent),
1670                (wrapColumn - subsequentLineIndent)))
1671      {
1672        final int indent;
1673        if (firstLine)
1674        {
1675          indent = firstLineIndent;
1676          firstLine = false;
1677        }
1678        else
1679        {
1680          stream.println();
1681          indent = subsequentLineIndent;
1682        }
1683
1684        if (indent > 0)
1685        {
1686          for (int i=0; i < indent; i++)
1687          {
1688            stream.print(' ');
1689          }
1690        }
1691        stream.print(line);
1692      }
1693    }
1694    else
1695    {
1696      if (firstLineIndent > 0)
1697      {
1698        for (int i=0; i < firstLineIndent; i++)
1699        {
1700          stream.print(' ');
1701        }
1702      }
1703      stream.print(buffer.toString());
1704    }
1705
1706    if (endWithNewline)
1707    {
1708      stream.println();
1709    }
1710    stream.flush();
1711  }
1712}