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     */
021    package com.unboundid.util;
022    
023    
024    
025    import java.io.File;
026    import java.io.OutputStream;
027    import java.io.PrintStream;
028    import java.util.Collections;
029    import java.util.LinkedHashMap;
030    import java.util.LinkedHashSet;
031    import java.util.Map;
032    import java.util.Set;
033    import java.util.concurrent.atomic.AtomicReference;
034    
035    import com.unboundid.ldap.sdk.LDAPException;
036    import com.unboundid.ldap.sdk.ResultCode;
037    import com.unboundid.util.args.ArgumentException;
038    import com.unboundid.util.args.ArgumentParser;
039    import com.unboundid.util.args.BooleanArgument;
040    
041    import static com.unboundid.util.Debug.*;
042    import static com.unboundid.util.StaticUtils.*;
043    import 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)
088    public 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    }