001/*
002 * Copyright 2008-2016 UnboundID Corp.
003 * All Rights Reserved.
004 */
005/*
006 * Copyright (C) 2008-2016 UnboundID Corp.
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.OutputStream;
027import java.io.PrintStream;
028import java.util.Collections;
029import java.util.LinkedHashMap;
030import java.util.LinkedHashSet;
031import java.util.Map;
032import java.util.Set;
033import java.util.concurrent.atomic.AtomicReference;
034
035import com.unboundid.ldap.sdk.LDAPException;
036import com.unboundid.ldap.sdk.ResultCode;
037import com.unboundid.util.args.ArgumentException;
038import com.unboundid.util.args.ArgumentParser;
039import com.unboundid.util.args.BooleanArgument;
040
041import static com.unboundid.util.Debug.*;
042import static com.unboundid.util.StaticUtils.*;
043import static com.unboundid.util.UtilityMessages.*;
044
045
046
047/**
048 * This class provides a framework for developing command-line tools that use
049 * the argument parser provided as part of the UnboundID LDAP SDK for Java.
050 * This tool adds a "-H" or "--help" option, which can be used to display usage
051 * information for the program, and may also add a "-V" or "--version" option,
052 * which can display the tool version.
053 * <BR><BR>
054 * Subclasses should include their own {@code main} method that creates an
055 * instance of a {@code CommandLineTool} and should invoke the
056 * {@link CommandLineTool#runTool} method with the provided arguments.  For
057 * example:
058 * <PRE>
059 *   public class ExampleCommandLineTool
060 *          extends CommandLineTool
061 *   {
062 *     public static void main(String[] args)
063 *     {
064 *       ExampleCommandLineTool tool = new ExampleCommandLineTool();
065 *       ResultCode resultCode = tool.runTool(args);
066 *       if (resultCode != ResultCode.SUCCESS)
067 *       {
068 *         System.exit(resultCode.intValue());
069 *       }
070 *     |
071 *
072 *     public ExampleCommandLineTool()
073 *     {
074 *       super(System.out, System.err);
075 *     }
076 *
077 *     // The rest of the tool implementation goes here.
078 *     ...
079 *   }
080 * </PRE>.
081 * <BR><BR>
082 * Note that in general, methods in this class are not threadsafe.  However, the
083 * {@link #out(Object...)} and {@link #err(Object...)} methods may be invoked
084 * concurrently by any number of threads.
085 */
086@Extensible()
087@ThreadSafety(level=ThreadSafetyLevel.INTERFACE_NOT_THREADSAFE)
088public abstract class CommandLineTool
089{
090  // The print stream to use for messages written to standard output.
091  private final PrintStream out;
092
093  // The print stream to use for messages written to standard error.
094  private final PrintStream err;
095
096  // The argument used to request tool help.
097  private BooleanArgument helpArgument = null;
098
099  // The argument used to request help about SASL authentication.
100  private BooleanArgument helpSASLArgument = null;
101
102  // The argument used to request interactive mode.
103  private BooleanArgument interactiveArgument = null;
104
105  // The argument used to request the tool version.
106  private BooleanArgument versionArgument = null;
107
108
109
110  /**
111   * Creates a new instance of this command-line tool with the provided
112   * information.
113   *
114   * @param  outStream  The output stream to use for standard output.  It may be
115   *                    {@code System.out} for the JVM's default standard output
116   *                    stream, {@code null} if no output should be generated,
117   *                    or a custom output stream if the output should be sent
118   *                    to an alternate location.
119   * @param  errStream  The output stream to use for standard error.  It may be
120   *                    {@code System.err} for the JVM's default standard error
121   *                    stream, {@code null} if no output should be generated,
122   *                    or a custom output stream if the output should be sent
123   *                    to an alternate location.
124   */
125  public CommandLineTool(final OutputStream outStream,
126                         final OutputStream errStream)
127  {
128    if (outStream == null)
129    {
130      out = NullOutputStream.getPrintStream();
131    }
132    else
133    {
134      out = new PrintStream(outStream);
135    }
136
137    if (errStream == null)
138    {
139      err = NullOutputStream.getPrintStream();
140    }
141    else
142    {
143      err = new PrintStream(errStream);
144    }
145  }
146
147
148
149  /**
150   * Performs all processing for this command-line tool.  This includes:
151   * <UL>
152   *   <LI>Creating the argument parser and populating it using the
153   *       {@link #addToolArguments} method.</LI>
154   *   <LI>Parsing the provided set of command line arguments, including any
155   *       additional validation using the {@link #doExtendedArgumentValidation}
156   *       method.</LI>
157   *   <LI>Invoking the {@link #doToolProcessing} method to do the appropriate
158   *       work for this tool.</LI>
159   * </UL>
160   *
161   * @param  args  The command-line arguments provided to this program.
162   *
163   * @return  The result of processing this tool.  It should be
164   *          {@link ResultCode#SUCCESS} if the tool completed its work
165   *          successfully, or some other result if a problem occurred.
166   */
167  public final ResultCode runTool(final String... args)
168  {
169    try
170    {
171      final ArgumentParser parser = createArgumentParser();
172      if (supportsInteractiveMode() && defaultsToInteractiveMode() &&
173          ((args == null) || (args.length == 0)))
174      {
175        // We'll skip argument parsing in this case because no arguments were
176        // provided, and the tool may not allow no arguments to be provided in
177        // non-interactive mode.
178      }
179      else
180      {
181        parser.parse(args);
182      }
183
184      final File generatedPropertiesFile = parser.getGeneratedPropertiesFile();
185      if (supportsPropertiesFile() && (generatedPropertiesFile != null))
186      {
187        wrapOut(0, StaticUtils.TERMINAL_WIDTH_COLUMNS - 1,
188             INFO_CL_TOOL_WROTE_PROPERTIES_FILE.get(
189                  generatedPropertiesFile.getAbsolutePath()));
190        return ResultCode.SUCCESS;
191      }
192
193      if (helpArgument.isPresent())
194      {
195        out(parser.getUsageString(StaticUtils.TERMINAL_WIDTH_COLUMNS - 1));
196        displayExampleUsages();
197        return ResultCode.SUCCESS;
198      }
199
200      if ((helpSASLArgument != null) && helpSASLArgument.isPresent())
201      {
202        out(SASLUtils.getUsageString(StaticUtils.TERMINAL_WIDTH_COLUMNS - 1));
203        return ResultCode.SUCCESS;
204      }
205
206      if ((versionArgument != null) && versionArgument.isPresent())
207      {
208        out(getToolVersion());
209        return ResultCode.SUCCESS;
210      }
211
212      boolean extendedValidationDone = false;
213      if (interactiveArgument != null)
214      {
215        if (interactiveArgument.isPresent() ||
216            (defaultsToInteractiveMode() &&
217             ((args == null) || (args.length == 0))))
218        {
219          final CommandLineToolInteractiveModeProcessor interactiveProcessor =
220               new CommandLineToolInteractiveModeProcessor(this, parser);
221          try
222          {
223            interactiveProcessor.doInteractiveModeProcessing();
224            extendedValidationDone = true;
225          }
226          catch (final LDAPException le)
227          {
228            debugException(le);
229
230            final String message = le.getMessage();
231            if ((message != null) && (message.length() > 0))
232            {
233              err(message);
234            }
235
236            return le.getResultCode();
237          }
238        }
239      }
240
241      if (! extendedValidationDone)
242      {
243        doExtendedArgumentValidation();
244      }
245    }
246    catch (ArgumentException ae)
247    {
248      debugException(ae);
249      err(ae.getMessage());
250      return ResultCode.PARAM_ERROR;
251    }
252
253
254    final AtomicReference<ResultCode> exitCode =
255         new AtomicReference<ResultCode>();
256    if (registerShutdownHook())
257    {
258      final CommandLineToolShutdownHook shutdownHook =
259           new CommandLineToolShutdownHook(this, exitCode);
260      Runtime.getRuntime().addShutdownHook(shutdownHook);
261    }
262
263    try
264    {
265      exitCode.set(doToolProcessing());
266    }
267    catch (Exception e)
268    {
269      debugException(e);
270      err(getExceptionMessage(e));
271      exitCode.set(ResultCode.LOCAL_ERROR);
272    }
273
274    return exitCode.get();
275  }
276
277
278
279  /**
280   * Writes example usage information for this tool to the standard output
281   * stream.
282   */
283  private void displayExampleUsages()
284  {
285    final LinkedHashMap<String[],String> examples = getExampleUsages();
286    if ((examples == null) || examples.isEmpty())
287    {
288      return;
289    }
290
291    out(INFO_CL_TOOL_LABEL_EXAMPLES);
292
293    final int wrapWidth = StaticUtils.TERMINAL_WIDTH_COLUMNS - 1;
294    for (final Map.Entry<String[],String> e : examples.entrySet())
295    {
296      out();
297      wrapOut(2, wrapWidth, e.getValue());
298      out();
299
300      final StringBuilder buffer = new StringBuilder();
301      buffer.append("    ");
302      buffer.append(getToolName());
303
304      final String[] args = e.getKey();
305      for (int i=0; i < args.length; i++)
306      {
307        buffer.append(' ');
308
309        // If the argument has a value, then make sure to keep it on the same
310        // line as the argument name.  This may introduce false positives due to
311        // unnamed trailing arguments, but the worst that will happen that case
312        // is that the output may be wrapped earlier than necessary one time.
313        String arg = args[i];
314        if (arg.startsWith("-"))
315        {
316          if ((i < (args.length - 1)) && (! args[i+1].startsWith("-")))
317          {
318            ExampleCommandLineArgument cleanArg =
319                ExampleCommandLineArgument.getCleanArgument(args[i+1]);
320            arg += ' ' + cleanArg.getLocalForm();
321            i++;
322          }
323        }
324        else
325        {
326          ExampleCommandLineArgument cleanArg =
327              ExampleCommandLineArgument.getCleanArgument(arg);
328          arg = cleanArg.getLocalForm();
329        }
330
331        if ((buffer.length() + arg.length() + 2) < wrapWidth)
332        {
333          buffer.append(arg);
334        }
335        else
336        {
337          buffer.append('\\');
338          out(buffer.toString());
339          buffer.setLength(0);
340          buffer.append("         ");
341          buffer.append(arg);
342        }
343      }
344
345      out(buffer.toString());
346    }
347  }
348
349
350
351  /**
352   * Retrieves the name of this tool.  It should be the name of the command used
353   * to invoke this tool.
354   *
355   * @return  The name for this tool.
356   */
357  public abstract String getToolName();
358
359
360
361  /**
362   * Retrieves a human-readable description for this tool.
363   *
364   * @return  A human-readable description for this tool.
365   */
366  public abstract String getToolDescription();
367
368
369
370  /**
371   * Retrieves a version string for this tool, if available.
372   *
373   * @return  A version string for this tool, or {@code null} if none is
374   *          available.
375   */
376  public String getToolVersion()
377  {
378    return null;
379  }
380
381
382
383  /**
384   * Retrieves the minimum number of unnamed trailing arguments that must be
385   * provided for this tool.  If a tool requires the use of trailing arguments,
386   * then it must override this method and the {@link #getMaxTrailingArguments}
387   * arguments to return nonzero values, and it must also override the
388   * {@link #getTrailingArgumentsPlaceholder} method to return a
389   * non-{@code null} value.
390   *
391   * @return  The minimum number of unnamed trailing arguments that may be
392   *          provided for this tool.  A value of zero indicates that the tool
393   *          may be invoked without any trailing arguments.
394   */
395  public int getMinTrailingArguments()
396  {
397    return 0;
398  }
399
400
401
402  /**
403   * Retrieves the maximum number of unnamed trailing arguments that may be
404   * provided for this tool.  If a tool supports trailing arguments, then it
405   * must override this method to return a nonzero value, and must also override
406   * the {@link CommandLineTool#getTrailingArgumentsPlaceholder} method to
407   * return a non-{@code null} value.
408   *
409   * @return  The maximum number of unnamed trailing arguments that may be
410   *          provided for this tool.  A value of zero indicates that trailing
411   *          arguments are not allowed.  A negative value indicates that there
412   *          should be no limit on the number of trailing arguments.
413   */
414  public int getMaxTrailingArguments()
415  {
416    return 0;
417  }
418
419
420
421  /**
422   * Retrieves a placeholder string that should be used for trailing arguments
423   * in the usage information for this tool.
424   *
425   * @return  A placeholder string that should be used for trailing arguments in
426   *          the usage information for this tool, or {@code null} if trailing
427   *          arguments are not supported.
428   */
429  public String getTrailingArgumentsPlaceholder()
430  {
431    return null;
432  }
433
434
435
436  /**
437   * Indicates whether this tool should provide support for an interactive mode,
438   * in which the tool offers a mode in which the arguments can be provided in
439   * a text-driven menu rather than requiring them to be given on the command
440   * line.  If interactive mode is supported, it may be invoked using the
441   * "--interactive" argument.  Alternately, if interactive mode is supported
442   * and {@link #defaultsToInteractiveMode()} returns {@code true}, then
443   * interactive mode may be invoked by simply launching the tool without any
444   * arguments.
445   *
446   * @return  {@code true} if this tool supports interactive mode, or
447   *          {@code false} if not.
448   */
449  public boolean supportsInteractiveMode()
450  {
451    return false;
452  }
453
454
455
456  /**
457   * Indicates whether this tool defaults to launching in interactive mode if
458   * the tool is invoked without any command-line arguments.  This will only be
459   * used if {@link #supportsInteractiveMode()} returns {@code true}.
460   *
461   * @return  {@code true} if this tool defaults to using interactive mode if
462   *          launched without any command-line arguments, or {@code false} if
463   *          not.
464   */
465  public boolean defaultsToInteractiveMode()
466  {
467    return false;
468  }
469
470
471
472  /**
473   * Indicates whether this tool supports the use of a properties file for
474   * specifying default values for arguments that aren't specified on the
475   * command line.
476   *
477   * @return  {@code true} if this tool supports the use of a properties file
478   *          for specifying default values for arguments that aren't specified
479   *          on the command line, or {@code false} if not.
480   */
481  public boolean supportsPropertiesFile()
482  {
483    return false;
484  }
485
486
487
488  /**
489   * Creates a parser that can be used to to parse arguments accepted by
490   * this tool.
491   *
492   * @return ArgumentParser that can be used to parse arguments for this
493   *         tool.
494   *
495   * @throws ArgumentException  If there was a problem initializing the
496   *                            parser for this tool.
497   */
498  public final ArgumentParser createArgumentParser()
499         throws ArgumentException
500  {
501    final ArgumentParser parser = new ArgumentParser(getToolName(),
502         getToolDescription(), getMinTrailingArguments(),
503         getMaxTrailingArguments(), getTrailingArgumentsPlaceholder());
504
505    addToolArguments(parser);
506
507    if (supportsInteractiveMode())
508    {
509      interactiveArgument = new BooleanArgument(null, "interactive",
510           INFO_CL_TOOL_DESCRIPTION_INTERACTIVE.get());
511      interactiveArgument.setUsageArgument(true);
512      parser.addArgument(interactiveArgument);
513    }
514
515    helpArgument = new BooleanArgument('H', "help",
516         INFO_CL_TOOL_DESCRIPTION_HELP.get());
517    helpArgument.addShortIdentifier('?');
518    helpArgument.setUsageArgument(true);
519    parser.addArgument(helpArgument);
520
521    final String version = getToolVersion();
522    if ((version != null) && (version.length() > 0) &&
523        (parser.getNamedArgument("version") == null))
524    {
525      final Character shortIdentifier;
526      if (parser.getNamedArgument('V') == null)
527      {
528        shortIdentifier = 'V';
529      }
530      else
531      {
532        shortIdentifier = null;
533      }
534
535      versionArgument = new BooleanArgument(shortIdentifier, "version",
536           INFO_CL_TOOL_DESCRIPTION_VERSION.get());
537      versionArgument.setUsageArgument(true);
538      parser.addArgument(versionArgument);
539    }
540
541    if (supportsPropertiesFile())
542    {
543      parser.enablePropertiesFileSupport();
544    }
545
546    return parser;
547  }
548
549
550
551  /**
552   * Specifies the argument that is used to retrieve usage information about
553   * SASL authentication.
554   *
555   * @param  helpSASLArgument  The argument that is used to retrieve usage
556   *                           information about SASL authentication.
557   */
558  void setHelpSASLArgument(final BooleanArgument helpSASLArgument)
559  {
560    this.helpSASLArgument = helpSASLArgument;
561  }
562
563
564
565  /**
566   * Retrieves a set containing the long identifiers used for LDAP-related
567   * arguments injected by this class.
568   *
569   * @return  A set containing the long identifiers used for LDAP-related
570   *          arguments injected by this class.
571   */
572  static Set<String> getUsageArgumentIdentifiers()
573  {
574    final LinkedHashSet<String> ids = new LinkedHashSet<String>(21);
575
576    ids.add("interactive");
577    ids.add("help");
578    ids.add("version");
579
580    return Collections.unmodifiableSet(ids);
581  }
582
583
584
585  /**
586   * Adds the command-line arguments supported for use with this tool to the
587   * provided argument parser.  The tool may need to retain references to the
588   * arguments (and/or the argument parser, if trailing arguments are allowed)
589   * to it in order to obtain their values for use in later processing.
590   *
591   * @param  parser  The argument parser to which the arguments are to be added.
592   *
593   * @throws  ArgumentException  If a problem occurs while adding any of the
594   *                             tool-specific arguments to the provided
595   *                             argument parser.
596   */
597  public abstract void addToolArguments(final ArgumentParser parser)
598         throws ArgumentException;
599
600
601
602  /**
603   * Performs any necessary processing that should be done to ensure that the
604   * provided set of command-line arguments were valid.  This method will be
605   * called after the basic argument parsing has been performed and immediately
606   * before the {@link CommandLineTool#doToolProcessing} method is invoked.
607   * Note that if the tool supports interactive mode, then this method may be
608   * invoked multiple times to allow the user to interactively fix validation
609   * errors.
610   *
611   * @throws  ArgumentException  If there was a problem with the command-line
612   *                             arguments provided to this program.
613   */
614  public void doExtendedArgumentValidation()
615         throws ArgumentException
616  {
617    // No processing will be performed by default.
618  }
619
620
621
622  /**
623   * Performs the core set of processing for this tool.
624   *
625   * @return  A result code that indicates whether the processing completed
626   *          successfully.
627   */
628  public abstract ResultCode doToolProcessing();
629
630
631
632  /**
633   * Indicates whether this tool should register a shutdown hook with the JVM.
634   * Shutdown hooks allow for a best-effort attempt to perform a specified set
635   * of processing when the JVM is shutting down under various conditions,
636   * including:
637   * <UL>
638   *   <LI>When all non-daemon threads have stopped running (i.e., the tool has
639   *       completed processing).</LI>
640   *   <LI>When {@code System.exit()} or {@code Runtime.exit()} is called.</LI>
641   *   <LI>When the JVM receives an external kill signal (e.g., via the use of
642   *       the kill tool or interrupting the JVM with Ctrl+C).</LI>
643   * </UL>
644   * Shutdown hooks may not be invoked if the process is forcefully killed
645   * (e.g., using "kill -9", or the {@code System.halt()} or
646   * {@code Runtime.halt()} methods).
647   * <BR><BR>
648   * If this method is overridden to return {@code true}, then the
649   * {@link #doShutdownHookProcessing(ResultCode)} method should also be
650   * overridden to contain the logic that will be invoked when the JVM is
651   * shutting down in a manner that calls shutdown hooks.
652   *
653   * @return  {@code true} if this tool should register a shutdown hook, or
654   *          {@code false} if not.
655   */
656  protected boolean registerShutdownHook()
657  {
658    return false;
659  }
660
661
662
663  /**
664   * Performs any processing that may be needed when the JVM is shutting down,
665   * whether because tool processing has completed or because it has been
666   * interrupted (e.g., by a kill or break signal).
667   * <BR><BR>
668   * Note that because shutdown hooks run at a delicate time in the life of the
669   * JVM, they should complete quickly and minimize access to external
670   * resources.  See the documentation for the
671   * {@code java.lang.Runtime.addShutdownHook} method for recommendations and
672   * restrictions about writing shutdown hooks.
673   *
674   * @param  resultCode  The result code returned by the tool.  It may be
675   *                     {@code null} if the tool was interrupted before it
676   *                     completed processing.
677   */
678  protected void doShutdownHookProcessing(final ResultCode resultCode)
679  {
680    throw new LDAPSDKUsageException(
681         ERR_COMMAND_LINE_TOOL_SHUTDOWN_HOOK_NOT_IMPLEMENTED.get(
682              getToolName()));
683  }
684
685
686
687  /**
688   * Retrieves a set of information that may be used to generate example usage
689   * information.  Each element in the returned map should consist of a map
690   * between an example set of arguments and a string that describes the
691   * behavior of the tool when invoked with that set of arguments.
692   *
693   * @return  A set of information that may be used to generate example usage
694   *          information.  It may be {@code null} or empty if no example usage
695   *          information is available.
696   */
697  @ThreadSafety(level=ThreadSafetyLevel.METHOD_THREADSAFE)
698  public LinkedHashMap<String[],String> getExampleUsages()
699  {
700    return null;
701  }
702
703
704
705  /**
706   * Retrieves the print writer that will be used for standard output.
707   *
708   * @return  The print writer that will be used for standard output.
709   */
710  public final PrintStream getOut()
711  {
712    return out;
713  }
714
715
716
717  /**
718   * Writes the provided message to the standard output stream for this tool.
719   * <BR><BR>
720   * This method is completely threadsafe and my be invoked concurrently by any
721   * number of threads.
722   *
723   * @param  msg  The message components that will be written to the standard
724   *              output stream.  They will be concatenated together on the same
725   *              line, and that line will be followed by an end-of-line
726   *              sequence.
727   */
728  @ThreadSafety(level=ThreadSafetyLevel.METHOD_THREADSAFE)
729  public final synchronized void out(final Object... msg)
730  {
731    write(out, 0, 0, msg);
732  }
733
734
735
736  /**
737   * Writes the provided message to the standard output stream for this tool,
738   * optionally wrapping and/or indenting the text in the process.
739   * <BR><BR>
740   * This method is completely threadsafe and my be invoked concurrently by any
741   * number of threads.
742   *
743   * @param  indent      The number of spaces each line should be indented.  A
744   *                     value less than or equal to zero indicates that no
745   *                     indent should be used.
746   * @param  wrapColumn  The column at which to wrap long lines.  A value less
747   *                     than or equal to two indicates that no wrapping should
748   *                     be performed.  If both an indent and a wrap column are
749   *                     to be used, then the wrap column must be greater than
750   *                     the indent.
751   * @param  msg         The message components that will be written to the
752   *                     standard output stream.  They will be concatenated
753   *                     together on the same line, and that line will be
754   *                     followed by an end-of-line sequence.
755   */
756  @ThreadSafety(level=ThreadSafetyLevel.METHOD_THREADSAFE)
757  public final synchronized void wrapOut(final int indent, final int wrapColumn,
758                                         final Object... msg)
759  {
760    write(out, indent, wrapColumn, msg);
761  }
762
763
764
765  /**
766   * Writes the provided message to the standard output stream for this tool,
767   * optionally wrapping and/or indenting the text in the process.
768   * <BR><BR>
769   * This method is completely threadsafe and my be invoked concurrently by any
770   * number of threads.
771   *
772   * @param  firstLineIndent       The number of spaces the first line should be
773   *                               indented.  A value less than or equal to zero
774   *                               indicates that no indent should be used.
775   * @param  subsequentLineIndent  The number of spaces each line except the
776   *                               first should be indented.  A value less than
777   *                               or equal to zero indicates that no indent
778   *                               should be used.
779   * @param  wrapColumn            The column at which to wrap long lines.  A
780   *                               value less than or equal to two indicates
781   *                               that no wrapping should be performed.  If
782   *                               both an indent and a wrap column are to be
783   *                               used, then the wrap column must be greater
784   *                               than the indent.
785   * @param  endWithNewline        Indicates whether a newline sequence should
786   *                               follow the last line that is printed.
787   * @param  msg                   The message components that will be written
788   *                               to the standard output stream.  They will be
789   *                               concatenated together on the same line, and
790   *                               that line will be followed by an end-of-line
791   *                               sequence.
792   */
793  final synchronized void wrapStandardOut(final int firstLineIndent,
794                                          final int subsequentLineIndent,
795                                          final int wrapColumn,
796                                          final boolean endWithNewline,
797                                          final Object... msg)
798  {
799    write(out, firstLineIndent, subsequentLineIndent, wrapColumn,
800         endWithNewline, msg);
801  }
802
803
804
805  /**
806   * Retrieves the print writer that will be used for standard error.
807   *
808   * @return  The print writer that will be used for standard error.
809   */
810  public final PrintStream getErr()
811  {
812    return err;
813  }
814
815
816
817  /**
818   * Writes the provided message to the standard error stream for this tool.
819   * <BR><BR>
820   * This method is completely threadsafe and my be invoked concurrently by any
821   * number of threads.
822   *
823   * @param  msg  The message components that will be written to the standard
824   *              error stream.  They will be concatenated together on the same
825   *              line, and that line will be followed by an end-of-line
826   *              sequence.
827   */
828  @ThreadSafety(level=ThreadSafetyLevel.METHOD_THREADSAFE)
829  public final synchronized void err(final Object... msg)
830  {
831    write(err, 0, 0, msg);
832  }
833
834
835
836  /**
837   * Writes the provided message to the standard error stream for this tool,
838   * optionally wrapping and/or indenting the text in the process.
839   * <BR><BR>
840   * This method is completely threadsafe and my be invoked concurrently by any
841   * number of threads.
842   *
843   * @param  indent      The number of spaces each line should be indented.  A
844   *                     value less than or equal to zero indicates that no
845   *                     indent should be used.
846   * @param  wrapColumn  The column at which to wrap long lines.  A value less
847   *                     than or equal to two indicates that no wrapping should
848   *                     be performed.  If both an indent and a wrap column are
849   *                     to be used, then the wrap column must be greater than
850   *                     the indent.
851   * @param  msg         The message components that will be written to the
852   *                     standard output stream.  They will be concatenated
853   *                     together on the same line, and that line will be
854   *                     followed by an end-of-line sequence.
855   */
856  @ThreadSafety(level=ThreadSafetyLevel.METHOD_THREADSAFE)
857  public final synchronized void wrapErr(final int indent, final int wrapColumn,
858                                         final Object... msg)
859  {
860    write(err, indent, wrapColumn, msg);
861  }
862
863
864
865  /**
866   * Writes the provided message to the given print stream, optionally wrapping
867   * and/or indenting the text in the process.
868   *
869   * @param  stream      The stream to which the message should be written.
870   * @param  indent      The number of spaces each line should be indented.  A
871   *                     value less than or equal to zero indicates that no
872   *                     indent should be used.
873   * @param  wrapColumn  The column at which to wrap long lines.  A value less
874   *                     than or equal to two indicates that no wrapping should
875   *                     be performed.  If both an indent and a wrap column are
876   *                     to be used, then the wrap column must be greater than
877   *                     the indent.
878   * @param  msg         The message components that will be written to the
879   *                     standard output stream.  They will be concatenated
880   *                     together on the same line, and that line will be
881   *                     followed by an end-of-line sequence.
882   */
883  private static void write(final PrintStream stream, final int indent,
884                            final int wrapColumn, final Object... msg)
885  {
886    write(stream, indent, indent, wrapColumn, true, msg);
887  }
888
889
890
891  /**
892   * Writes the provided message to the given print stream, optionally wrapping
893   * and/or indenting the text in the process.
894   *
895   * @param  stream                The stream to which the message should be
896   *                               written.
897   * @param  firstLineIndent       The number of spaces the first line should be
898   *                               indented.  A value less than or equal to zero
899   *                               indicates that no indent should be used.
900   * @param  subsequentLineIndent  The number of spaces all lines after the
901   *                               first should be indented.  A value less than
902   *                               or equal to zero indicates that no indent
903   *                               should be used.
904   * @param  wrapColumn            The column at which to wrap long lines.  A
905   *                               value less than or equal to two indicates
906   *                               that no wrapping should be performed.  If
907   *                               both an indent and a wrap column are to be
908   *                               used, then the wrap column must be greater
909   *                               than the indent.
910   * @param  endWithNewline        Indicates whether a newline sequence should
911   *                               follow the last line that is printed.
912   * @param  msg                   The message components that will be written
913   *                               to the standard output stream.  They will be
914   *                               concatenated together on the same line, and
915   *                               that line will be followed by an end-of-line
916   *                               sequence.
917   */
918  private static void write(final PrintStream stream, final int firstLineIndent,
919                            final int subsequentLineIndent,
920                            final int wrapColumn,
921                            final boolean endWithNewline, final Object... msg)
922  {
923    final StringBuilder buffer = new StringBuilder();
924    for (final Object o : msg)
925    {
926      buffer.append(o);
927    }
928
929    if (wrapColumn > 2)
930    {
931      boolean firstLine = true;
932      for (final String line :
933           wrapLine(buffer.toString(), (wrapColumn - firstLineIndent),
934                (wrapColumn - subsequentLineIndent)))
935      {
936        final int indent;
937        if (firstLine)
938        {
939          indent = firstLineIndent;
940          firstLine = false;
941        }
942        else
943        {
944          stream.println();
945          indent = subsequentLineIndent;
946        }
947
948        if (indent > 0)
949        {
950          for (int i=0; i < indent; i++)
951          {
952            stream.print(' ');
953          }
954        }
955        stream.print(line);
956      }
957    }
958    else
959    {
960      if (firstLineIndent > 0)
961      {
962        for (int i=0; i < firstLineIndent; i++)
963        {
964          stream.print(' ');
965        }
966      }
967      stream.print(buffer.toString());
968    }
969
970    if (endWithNewline)
971    {
972      stream.println();
973    }
974    stream.flush();
975  }
976}