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