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