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