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