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