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