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