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