![JAR search and dependency download from the Maven repository](/logo.png)
com.unboundid.util.CommandLineTool Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of unboundid-ldapsdk Show documentation
Show all versions of unboundid-ldapsdk Show documentation
The UnboundID LDAP SDK for Java is a fast, comprehensive, and easy-to-use
Java API for communicating with LDAP directory servers and performing
related tasks like reading and writing LDIF, encoding and decoding data
using base64 and ASN.1 BER, and performing secure communication. This
package contains the Standard Edition of the LDAP SDK, which is a
complete, general-purpose library for communicating with LDAPv3 directory
servers.
/*
* Copyright 2008-2018 Ping Identity Corporation
* All Rights Reserved.
*/
/*
* Copyright (C) 2008-2018 Ping Identity Corporation
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License (GPLv2 only)
* or the terms of the GNU Lesser General Public License (LGPLv2.1 only)
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, see .
*/
package com.unboundid.util;
import java.io.File;
import java.io.FileOutputStream;
import java.io.OutputStream;
import java.io.PrintStream;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.concurrent.atomic.AtomicReference;
import com.unboundid.ldap.sdk.LDAPException;
import com.unboundid.ldap.sdk.ResultCode;
import com.unboundid.util.args.Argument;
import com.unboundid.util.args.ArgumentException;
import com.unboundid.util.args.ArgumentParser;
import com.unboundid.util.args.BooleanArgument;
import com.unboundid.util.args.FileArgument;
import com.unboundid.util.args.SubCommand;
import com.unboundid.ldap.sdk.unboundidds.tools.ToolInvocationLogger;
import com.unboundid.ldap.sdk.unboundidds.tools.ToolInvocationLogDetails;
import com.unboundid.ldap.sdk.unboundidds.tools.ToolInvocationLogShutdownHook;
import static com.unboundid.util.UtilityMessages.*;
/**
* This class provides a framework for developing command-line tools that use
* the argument parser provided as part of the UnboundID LDAP SDK for Java.
* This tool adds a "-H" or "--help" option, which can be used to display usage
* information for the program, and may also add a "-V" or "--version" option,
* which can display the tool version.
*
* Subclasses should include their own {@code main} method that creates an
* instance of a {@code CommandLineTool} and should invoke the
* {@link CommandLineTool#runTool} method with the provided arguments. For
* example:
*
* public class ExampleCommandLineTool
* extends CommandLineTool
* {
* public static void main(String[] args)
* {
* ExampleCommandLineTool tool = new ExampleCommandLineTool();
* ResultCode resultCode = tool.runTool(args);
* if (resultCode != ResultCode.SUCCESS)
* {
* System.exit(resultCode.intValue());
* }
* }
*
* public ExampleCommandLineTool()
* {
* super(System.out, System.err);
* }
*
* // The rest of the tool implementation goes here.
* ...
* }
*
.
*
* Note that in general, methods in this class are not threadsafe. However, the
* {@link #out(Object...)} and {@link #err(Object...)} methods may be invoked
* concurrently by any number of threads.
*/
@Extensible()
@ThreadSafety(level=ThreadSafetyLevel.INTERFACE_NOT_THREADSAFE)
public abstract class CommandLineTool
{
// The print stream that was originally used for standard output. It may not
// be the current standard output stream if an output file has been
// configured.
private final PrintStream originalOut;
// The print stream that was originally used for standard error. It may not
// be the current standard error stream if an output file has been configured.
private final PrintStream originalErr;
// The print stream to use for messages written to standard output.
private volatile PrintStream out;
// The print stream to use for messages written to standard error.
private volatile PrintStream err;
// The argument used to indicate that the tool should append to the output
// file rather than overwrite it.
private BooleanArgument appendToOutputFileArgument = null;
// The argument used to request tool help.
private BooleanArgument helpArgument = null;
// The argument used to request help about SASL authentication.
private BooleanArgument helpSASLArgument = null;
// The argument used to request help information about all of the subcommands.
private BooleanArgument helpSubcommandsArgument = null;
// The argument used to request interactive mode.
private BooleanArgument interactiveArgument = null;
// The argument used to indicate that output should be written to standard out
// as well as the specified output file.
private BooleanArgument teeOutputArgument = null;
// The argument used to request the tool version.
private BooleanArgument versionArgument = null;
// The argument used to specify the output file for standard output and
// standard error.
private FileArgument outputFileArgument = null;
/**
* Creates a new instance of this command-line tool with the provided
* information.
*
* @param outStream The output stream to use for standard output. It may be
* {@code System.out} for the JVM's default standard output
* stream, {@code null} if no output should be generated,
* or a custom output stream if the output should be sent
* to an alternate location.
* @param errStream The output stream to use for standard error. It may be
* {@code System.err} for the JVM's default standard error
* stream, {@code null} if no output should be generated,
* or a custom output stream if the output should be sent
* to an alternate location.
*/
public CommandLineTool(final OutputStream outStream,
final OutputStream errStream)
{
if (outStream == null)
{
out = NullOutputStream.getPrintStream();
}
else
{
out = new PrintStream(outStream);
}
if (errStream == null)
{
err = NullOutputStream.getPrintStream();
}
else
{
err = new PrintStream(errStream);
}
originalOut = out;
originalErr = err;
}
/**
* Performs all processing for this command-line tool. This includes:
*
* - Creating the argument parser and populating it using the
* {@link #addToolArguments} method.
* - Parsing the provided set of command line arguments, including any
* additional validation using the {@link #doExtendedArgumentValidation}
* method.
* - Invoking the {@link #doToolProcessing} method to do the appropriate
* work for this tool.
*
*
* @param args The command-line arguments provided to this program.
*
* @return The result of processing this tool. It should be
* {@link ResultCode#SUCCESS} if the tool completed its work
* successfully, or some other result if a problem occurred.
*/
public final ResultCode runTool(final String... args)
{
final ArgumentParser parser;
try
{
parser = createArgumentParser();
boolean exceptionFromParsingWithNoArgumentsExplicitlyProvided = false;
if (supportsInteractiveMode() && defaultsToInteractiveMode() &&
((args == null) || (args.length == 0)))
{
// We'll go ahead and perform argument parsing even though no arguments
// were provided because there might be a properties file that should
// prevent running in interactive mode. But we'll ignore any exception
// thrown during argument parsing because the tool might require
// arguments when run non-interactively.
try
{
parser.parse(args);
}
catch (final Exception e)
{
Debug.debugException(e);
exceptionFromParsingWithNoArgumentsExplicitlyProvided = true;
}
}
else
{
parser.parse(args);
}
final File generatedPropertiesFile = parser.getGeneratedPropertiesFile();
if (supportsPropertiesFile() && (generatedPropertiesFile != null))
{
wrapOut(0, StaticUtils.TERMINAL_WIDTH_COLUMNS - 1,
INFO_CL_TOOL_WROTE_PROPERTIES_FILE.get(
generatedPropertiesFile.getAbsolutePath()));
return ResultCode.SUCCESS;
}
if (helpArgument.isPresent())
{
out(parser.getUsageString(StaticUtils.TERMINAL_WIDTH_COLUMNS - 1));
displayExampleUsages(parser);
return ResultCode.SUCCESS;
}
if ((helpSASLArgument != null) && helpSASLArgument.isPresent())
{
out(SASLUtils.getUsageString(StaticUtils.TERMINAL_WIDTH_COLUMNS - 1));
return ResultCode.SUCCESS;
}
if ((helpSubcommandsArgument != null) &&
helpSubcommandsArgument.isPresent())
{
final TreeMap subCommands =
getSortedSubCommands(parser);
for (final SubCommand sc : subCommands.values())
{
final StringBuilder nameBuffer = new StringBuilder();
final Iterator nameIterator = sc.getNames(false).iterator();
while (nameIterator.hasNext())
{
nameBuffer.append(nameIterator.next());
if (nameIterator.hasNext())
{
nameBuffer.append(", ");
}
}
out(nameBuffer.toString());
for (final String descriptionLine :
StaticUtils.wrapLine(sc.getDescription(),
(StaticUtils.TERMINAL_WIDTH_COLUMNS - 3)))
{
out(" " + descriptionLine);
}
out();
}
wrapOut(0, (StaticUtils.TERMINAL_WIDTH_COLUMNS - 1),
INFO_CL_TOOL_USE_SUBCOMMAND_HELP.get(getToolName()));
return ResultCode.SUCCESS;
}
if ((versionArgument != null) && versionArgument.isPresent())
{
out(getToolVersion());
return ResultCode.SUCCESS;
}
boolean extendedValidationDone = false;
if (interactiveArgument != null)
{
if (interactiveArgument.isPresent() ||
(defaultsToInteractiveMode() &&
((args == null) || (args.length == 0)) &&
(parser.getArgumentsSetFromPropertiesFile().isEmpty() ||
exceptionFromParsingWithNoArgumentsExplicitlyProvided)))
{
final CommandLineToolInteractiveModeProcessor interactiveProcessor =
new CommandLineToolInteractiveModeProcessor(this, parser);
try
{
interactiveProcessor.doInteractiveModeProcessing();
extendedValidationDone = true;
}
catch (final LDAPException le)
{
Debug.debugException(le);
final String message = le.getMessage();
if ((message != null) && (! message.isEmpty()))
{
err(message);
}
return le.getResultCode();
}
}
}
if (! extendedValidationDone)
{
doExtendedArgumentValidation();
}
}
catch (final ArgumentException ae)
{
Debug.debugException(ae);
err(ae.getMessage());
return ResultCode.PARAM_ERROR;
}
if ((outputFileArgument != null) && outputFileArgument.isPresent())
{
final File outputFile = outputFileArgument.getValue();
final boolean append = ((appendToOutputFileArgument != null) &&
appendToOutputFileArgument.isPresent());
final PrintStream outputFileStream;
try
{
final FileOutputStream fos = new FileOutputStream(outputFile, append);
outputFileStream = new PrintStream(fos, true, "UTF-8");
}
catch (final Exception e)
{
Debug.debugException(e);
err(ERR_CL_TOOL_ERROR_CREATING_OUTPUT_FILE.get(
outputFile.getAbsolutePath(), StaticUtils.getExceptionMessage(e)));
return ResultCode.LOCAL_ERROR;
}
if ((teeOutputArgument != null) && teeOutputArgument.isPresent())
{
out = new PrintStream(new TeeOutputStream(out, outputFileStream));
err = new PrintStream(new TeeOutputStream(err, outputFileStream));
}
else
{
out = outputFileStream;
err = outputFileStream;
}
}
// If any values were selected using a properties file, then display
// information about them.
final List argsSetFromPropertiesFiles =
parser.getArgumentsSetFromPropertiesFile();
if ((! argsSetFromPropertiesFiles.isEmpty()) &&
(! parser.suppressPropertiesFileComment()))
{
for (final String line :
StaticUtils.wrapLine(
INFO_CL_TOOL_ARGS_FROM_PROPERTIES_FILE.get(
parser.getPropertiesFileUsed().getPath()),
(StaticUtils.TERMINAL_WIDTH_COLUMNS - 3)))
{
out("# ", line);
}
final StringBuilder buffer = new StringBuilder();
for (final String s : argsSetFromPropertiesFiles)
{
if (s.startsWith("-"))
{
if (buffer.length() > 0)
{
out(buffer);
buffer.setLength(0);
}
buffer.append("# ");
buffer.append(s);
}
else
{
if (buffer.length() == 0)
{
// This should never happen.
buffer.append("# ");
}
else
{
buffer.append(' ');
}
buffer.append(StaticUtils.cleanExampleCommandLineArgument(s));
}
}
if (buffer.length() > 0)
{
out(buffer);
}
out();
}
CommandLineToolShutdownHook shutdownHook = null;
final AtomicReference exitCode = new AtomicReference<>();
if (registerShutdownHook())
{
shutdownHook = new CommandLineToolShutdownHook(this, exitCode);
Runtime.getRuntime().addShutdownHook(shutdownHook);
}
final ToolInvocationLogDetails logDetails =
ToolInvocationLogger.getLogMessageDetails(
getToolName(), logToolInvocationByDefault(), getErr());
ToolInvocationLogShutdownHook logShutdownHook = null;
if (logDetails.logInvocation())
{
final HashSet argumentsSetFromPropertiesFile =
new HashSet<>(StaticUtils.computeMapCapacity(10));
final ArrayList> propertiesFileArgList =
new ArrayList<>(10);
getToolInvocationPropertiesFileArguments(parser,
argumentsSetFromPropertiesFile, propertiesFileArgList);
final ArrayList> providedArgList =
new ArrayList<>(10);
getToolInvocationProvidedArguments(parser,
argumentsSetFromPropertiesFile, providedArgList);
logShutdownHook = new ToolInvocationLogShutdownHook(logDetails);
Runtime.getRuntime().addShutdownHook(logShutdownHook);
final String propertiesFilePath;
if (propertiesFileArgList.isEmpty())
{
propertiesFilePath = "";
}
else
{
final File propertiesFile = parser.getPropertiesFileUsed();
if (propertiesFile == null)
{
propertiesFilePath = "";
}
else
{
propertiesFilePath = propertiesFile.getAbsolutePath();
}
}
ToolInvocationLogger.logLaunchMessage(logDetails, providedArgList,
propertiesFileArgList, propertiesFilePath);
}
try
{
exitCode.set(doToolProcessing());
}
catch (final Exception e)
{
Debug.debugException(e);
err(StaticUtils.getExceptionMessage(e));
exitCode.set(ResultCode.LOCAL_ERROR);
}
finally
{
if (logShutdownHook != null)
{
Runtime.getRuntime().removeShutdownHook(logShutdownHook);
String completionMessage = getToolCompletionMessage();
if (completionMessage == null)
{
completionMessage = exitCode.get().getName();
}
ToolInvocationLogger.logCompletionMessage(
logDetails, exitCode.get().intValue(), completionMessage);
}
if (shutdownHook != null)
{
Runtime.getRuntime().removeShutdownHook(shutdownHook);
}
}
return exitCode.get();
}
/**
* Updates the provided argument list with object pairs that comprise the
* set of arguments actually provided to this tool on the command line.
*
* @param parser The argument parser for this tool.
* It must not be {@code null}.
* @param argumentsSetFromPropertiesFile A set that includes all arguments
* set from the properties file.
* @param argList The list to which the argument
* information should be added. It
* must not be {@code null}. The
* first element of each object pair
* that is added must be
* non-{@code null}. The second
* element in any given pair may be
* {@code null} if the first element
* represents the name of an argument
* that doesn't take any values, the
* name of the selected subcommand, or
* an unnamed trailing argument.
*/
private static void getToolInvocationProvidedArguments(
final ArgumentParser parser,
final Set argumentsSetFromPropertiesFile,
final List> argList)
{
final String noValue = null;
final SubCommand subCommand = parser.getSelectedSubCommand();
if (subCommand != null)
{
argList.add(new ObjectPair<>(subCommand.getPrimaryName(), noValue));
}
for (final Argument arg : parser.getNamedArguments())
{
// Exclude arguments that weren't provided.
if (! arg.isPresent())
{
continue;
}
// Exclude arguments that were set from the properties file.
if (argumentsSetFromPropertiesFile.contains(arg))
{
continue;
}
if (arg.takesValue())
{
for (final String value : arg.getValueStringRepresentations(false))
{
if (arg.isSensitive())
{
argList.add(new ObjectPair<>(arg.getIdentifierString(),
"*****REDACTED*****"));
}
else
{
argList.add(new ObjectPair<>(arg.getIdentifierString(), value));
}
}
}
else
{
argList.add(new ObjectPair<>(arg.getIdentifierString(), noValue));
}
}
if (subCommand != null)
{
getToolInvocationProvidedArguments(subCommand.getArgumentParser(),
argumentsSetFromPropertiesFile, argList);
}
for (final String trailingArgument : parser.getTrailingArguments())
{
argList.add(new ObjectPair<>(trailingArgument, noValue));
}
}
/**
* Updates the provided argument list with object pairs that comprise the
* set of tool arguments set from a properties file.
*
* @param parser The argument parser for this tool.
* It must not be {@code null}.
* @param argumentsSetFromPropertiesFile A set that should be updated with
* each argument set from the
* properties file.
* @param argList The list to which the argument
* information should be added. It
* must not be {@code null}. The
* first element of each object pair
* that is added must be
* non-{@code null}. The second
* element in any given pair may be
* {@code null} if the first element
* represents the name of an argument
* that doesn't take any values, the
* name of the selected subcommand, or
* an unnamed trailing argument.
*/
private static void getToolInvocationPropertiesFileArguments(
final ArgumentParser parser,
final Set argumentsSetFromPropertiesFile,
final List> argList)
{
final ArgumentParser subCommandParser;
final SubCommand subCommand = parser.getSelectedSubCommand();
if (subCommand == null)
{
subCommandParser = null;
}
else
{
subCommandParser = subCommand.getArgumentParser();
}
final String noValue = null;
final Iterator iterator =
parser.getArgumentsSetFromPropertiesFile().iterator();
while (iterator.hasNext())
{
final String arg = iterator.next();
if (arg.startsWith("-"))
{
Argument a;
if (arg.startsWith("--"))
{
final String longIdentifier = arg.substring(2);
a = parser.getNamedArgument(longIdentifier);
if ((a == null) && (subCommandParser != null))
{
a = subCommandParser.getNamedArgument(longIdentifier);
}
}
else
{
final char shortIdentifier = arg.charAt(1);
a = parser.getNamedArgument(shortIdentifier);
if ((a == null) && (subCommandParser != null))
{
a = subCommandParser.getNamedArgument(shortIdentifier);
}
}
if (a != null)
{
argumentsSetFromPropertiesFile.add(a);
if (a.takesValue())
{
final String value = iterator.next();
if (a.isSensitive())
{
argList.add(new ObjectPair<>(a.getIdentifierString(), noValue));
}
else
{
argList.add(new ObjectPair<>(a.getIdentifierString(), value));
}
}
else
{
argList.add(new ObjectPair<>(a.getIdentifierString(), noValue));
}
}
}
else
{
argList.add(new ObjectPair<>(arg, noValue));
}
}
}
/**
* Retrieves a sorted map of subcommands for the provided argument parser,
* alphabetized by primary name.
*
* @param parser The argument parser for which to get the sorted
* subcommands.
*
* @return The sorted map of subcommands.
*/
private static TreeMap getSortedSubCommands(
final ArgumentParser parser)
{
final TreeMap m = new TreeMap<>();
for (final SubCommand sc : parser.getSubCommands())
{
m.put(sc.getPrimaryName(), sc);
}
return m;
}
/**
* Writes example usage information for this tool to the standard output
* stream.
*
* @param parser The argument parser used to process the provided set of
* command-line arguments.
*/
private void displayExampleUsages(final ArgumentParser parser)
{
final LinkedHashMap examples;
if ((parser != null) && (parser.getSelectedSubCommand() != null))
{
examples = parser.getSelectedSubCommand().getExampleUsages();
}
else
{
examples = getExampleUsages();
}
if ((examples == null) || examples.isEmpty())
{
return;
}
out(INFO_CL_TOOL_LABEL_EXAMPLES);
final int wrapWidth = StaticUtils.TERMINAL_WIDTH_COLUMNS - 1;
for (final Map.Entry e : examples.entrySet())
{
out();
wrapOut(2, wrapWidth, e.getValue());
out();
final StringBuilder buffer = new StringBuilder();
buffer.append(" ");
buffer.append(getToolName());
final String[] args = e.getKey();
for (int i=0; i < args.length; i++)
{
buffer.append(' ');
// If the argument has a value, then make sure to keep it on the same
// line as the argument name. This may introduce false positives due to
// unnamed trailing arguments, but the worst that will happen that case
// is that the output may be wrapped earlier than necessary one time.
String arg = args[i];
if (arg.startsWith("-"))
{
if ((i < (args.length - 1)) && (! args[i+1].startsWith("-")))
{
final ExampleCommandLineArgument cleanArg =
ExampleCommandLineArgument.getCleanArgument(args[i+1]);
arg += ' ' + cleanArg.getLocalForm();
i++;
}
}
else
{
final ExampleCommandLineArgument cleanArg =
ExampleCommandLineArgument.getCleanArgument(arg);
arg = cleanArg.getLocalForm();
}
if ((buffer.length() + arg.length() + 2) < wrapWidth)
{
buffer.append(arg);
}
else
{
buffer.append('\\');
out(buffer.toString());
buffer.setLength(0);
buffer.append(" ");
buffer.append(arg);
}
}
out(buffer.toString());
}
}
/**
* Retrieves the name of this tool. It should be the name of the command used
* to invoke this tool.
*
* @return The name for this tool.
*/
public abstract String getToolName();
/**
* Retrieves a human-readable description for this tool. If the description
* should include multiple paragraphs, then this method should return the text
* for the first paragraph, and the
* {@link #getAdditionalDescriptionParagraphs()} method should be used to
* return the text for the subsequent paragraphs.
*
* @return A human-readable description for this tool.
*/
public abstract String getToolDescription();
/**
* Retrieves additional paragraphs that should be included in the description
* for this tool. If the tool description should include multiple paragraphs,
* then the {@link #getToolDescription()} method should return the text of the
* first paragraph, and each item in the list returned by this method should
* be the text for each subsequent paragraph. If the tool description should
* only have a single paragraph, then this method may return {@code null} or
* an empty list.
*
* @return Additional paragraphs that should be included in the description
* for this tool, or {@code null} or an empty list if only a single
* description paragraph (whose text is returned by the
* {@code getToolDescription} method) is needed.
*/
public List getAdditionalDescriptionParagraphs()
{
return Collections.emptyList();
}
/**
* Retrieves a version string for this tool, if available.
*
* @return A version string for this tool, or {@code null} if none is
* available.
*/
public String getToolVersion()
{
return null;
}
/**
* Retrieves the minimum number of unnamed trailing arguments that must be
* provided for this tool. If a tool requires the use of trailing arguments,
* then it must override this method and the {@link #getMaxTrailingArguments}
* arguments to return nonzero values, and it must also override the
* {@link #getTrailingArgumentsPlaceholder} method to return a
* non-{@code null} value.
*
* @return The minimum number of unnamed trailing arguments that may be
* provided for this tool. A value of zero indicates that the tool
* may be invoked without any trailing arguments.
*/
public int getMinTrailingArguments()
{
return 0;
}
/**
* Retrieves the maximum number of unnamed trailing arguments that may be
* provided for this tool. If a tool supports trailing arguments, then it
* must override this method to return a nonzero value, and must also override
* the {@link CommandLineTool#getTrailingArgumentsPlaceholder} method to
* return a non-{@code null} value.
*
* @return The maximum number of unnamed trailing arguments that may be
* provided for this tool. A value of zero indicates that trailing
* arguments are not allowed. A negative value indicates that there
* should be no limit on the number of trailing arguments.
*/
public int getMaxTrailingArguments()
{
return 0;
}
/**
* Retrieves a placeholder string that should be used for trailing arguments
* in the usage information for this tool.
*
* @return A placeholder string that should be used for trailing arguments in
* the usage information for this tool, or {@code null} if trailing
* arguments are not supported.
*/
public String getTrailingArgumentsPlaceholder()
{
return null;
}
/**
* Indicates whether this tool should provide support for an interactive mode,
* in which the tool offers a mode in which the arguments can be provided in
* a text-driven menu rather than requiring them to be given on the command
* line. If interactive mode is supported, it may be invoked using the
* "--interactive" argument. Alternately, if interactive mode is supported
* and {@link #defaultsToInteractiveMode()} returns {@code true}, then
* interactive mode may be invoked by simply launching the tool without any
* arguments.
*
* @return {@code true} if this tool supports interactive mode, or
* {@code false} if not.
*/
public boolean supportsInteractiveMode()
{
return false;
}
/**
* Indicates whether this tool defaults to launching in interactive mode if
* the tool is invoked without any command-line arguments. This will only be
* used if {@link #supportsInteractiveMode()} returns {@code true}.
*
* @return {@code true} if this tool defaults to using interactive mode if
* launched without any command-line arguments, or {@code false} if
* not.
*/
public boolean defaultsToInteractiveMode()
{
return false;
}
/**
* Indicates whether this tool supports the use of a properties file for
* specifying default values for arguments that aren't specified on the
* command line.
*
* @return {@code true} if this tool supports the use of a properties file
* for specifying default values for arguments that aren't specified
* on the command line, or {@code false} if not.
*/
public boolean supportsPropertiesFile()
{
return false;
}
/**
* Indicates whether this tool should provide arguments for redirecting output
* to a file. If this method returns {@code true}, then the tool will offer
* an "--outputFile" argument that will specify the path to a file to which
* all standard output and standard error content will be written, and it will
* also offer a "--teeToStandardOut" argument that can only be used if the
* "--outputFile" argument is present and will cause all output to be written
* to both the specified output file and to standard output.
*
* @return {@code true} if this tool should provide arguments for redirecting
* output to a file, or {@code false} if not.
*/
protected boolean supportsOutputFile()
{
return false;
}
/**
* Indicates whether to log messages about the launch and completion of this
* tool into the invocation log of Ping Identity server products that may
* include it. This method is not needed for tools that are not expected to
* be part of the Ping Identity server products suite. Further, this value
* may be overridden by settings in the server's
* tool-invocation-logging.properties file.
*
* This method should generally return {@code true} for tools that may alter
* the server configuration, data, or other state information, and
* {@code false} for tools that do not make any changes.
*
* @return {@code true} if Ping Identity server products should include
* messages about the launch and completion of this tool in tool
* invocation log files by default, or {@code false} if not.
*/
protected boolean logToolInvocationByDefault()
{
return false;
}
/**
* Retrieves an optional message that may provide additional information about
* the way that the tool completed its processing. For example if the tool
* exited with an error message, it may be useful for this method to return
* that error message.
*
* The message returned by this method is intended for purposes and is not
* meant to be parsed or programmatically interpreted.
*
* @return An optional message that may provide additional information about
* the completion state for this tool, or {@code null} if no
* completion message is available.
*/
protected String getToolCompletionMessage()
{
return null;
}
/**
* Creates a parser that can be used to to parse arguments accepted by
* this tool.
*
* @return ArgumentParser that can be used to parse arguments for this
* tool.
*
* @throws ArgumentException If there was a problem initializing the
* parser for this tool.
*/
public final ArgumentParser createArgumentParser()
throws ArgumentException
{
final ArgumentParser parser = new ArgumentParser(getToolName(),
getToolDescription(), getAdditionalDescriptionParagraphs(),
getMinTrailingArguments(), getMaxTrailingArguments(),
getTrailingArgumentsPlaceholder());
addToolArguments(parser);
if (supportsInteractiveMode())
{
interactiveArgument = new BooleanArgument(null, "interactive",
INFO_CL_TOOL_DESCRIPTION_INTERACTIVE.get());
interactiveArgument.setUsageArgument(true);
parser.addArgument(interactiveArgument);
}
if (supportsOutputFile())
{
outputFileArgument = new FileArgument(null, "outputFile", false, 1, null,
INFO_CL_TOOL_DESCRIPTION_OUTPUT_FILE.get(), false, true, true,
false);
outputFileArgument.addLongIdentifier("output-file", true);
outputFileArgument.setUsageArgument(true);
parser.addArgument(outputFileArgument);
appendToOutputFileArgument = new BooleanArgument(null,
"appendToOutputFile", 1,
INFO_CL_TOOL_DESCRIPTION_APPEND_TO_OUTPUT_FILE.get(
outputFileArgument.getIdentifierString()));
appendToOutputFileArgument.addLongIdentifier("append-to-output-file",
true);
appendToOutputFileArgument.setUsageArgument(true);
parser.addArgument(appendToOutputFileArgument);
teeOutputArgument = new BooleanArgument(null, "teeOutput", 1,
INFO_CL_TOOL_DESCRIPTION_TEE_OUTPUT.get(
outputFileArgument.getIdentifierString()));
teeOutputArgument.addLongIdentifier("tee-output", true);
teeOutputArgument.setUsageArgument(true);
parser.addArgument(teeOutputArgument);
parser.addDependentArgumentSet(appendToOutputFileArgument,
outputFileArgument);
parser.addDependentArgumentSet(teeOutputArgument,
outputFileArgument);
}
helpArgument = new BooleanArgument('H', "help",
INFO_CL_TOOL_DESCRIPTION_HELP.get());
helpArgument.addShortIdentifier('?', true);
helpArgument.setUsageArgument(true);
parser.addArgument(helpArgument);
if (! parser.getSubCommands().isEmpty())
{
helpSubcommandsArgument = new BooleanArgument(null, "helpSubcommands", 1,
INFO_CL_TOOL_DESCRIPTION_HELP_SUBCOMMANDS.get());
helpSubcommandsArgument.addLongIdentifier("helpSubcommand", true);
helpSubcommandsArgument.addLongIdentifier("help-subcommands", true);
helpSubcommandsArgument.addLongIdentifier("help-subcommand", true);
helpSubcommandsArgument.setUsageArgument(true);
parser.addArgument(helpSubcommandsArgument);
}
final String version = getToolVersion();
if ((version != null) && (! version.isEmpty()) &&
(parser.getNamedArgument("version") == null))
{
final Character shortIdentifier;
if (parser.getNamedArgument('V') == null)
{
shortIdentifier = 'V';
}
else
{
shortIdentifier = null;
}
versionArgument = new BooleanArgument(shortIdentifier, "version",
INFO_CL_TOOL_DESCRIPTION_VERSION.get());
versionArgument.setUsageArgument(true);
parser.addArgument(versionArgument);
}
if (supportsPropertiesFile())
{
parser.enablePropertiesFileSupport();
}
return parser;
}
/**
* Specifies the argument that is used to retrieve usage information about
* SASL authentication.
*
* @param helpSASLArgument The argument that is used to retrieve usage
* information about SASL authentication.
*/
void setHelpSASLArgument(final BooleanArgument helpSASLArgument)
{
this.helpSASLArgument = helpSASLArgument;
}
/**
* Retrieves a set containing the long identifiers used for usage arguments
* injected by this class.
*
* @param tool The tool to use to help make the determination.
*
* @return A set containing the long identifiers used for usage arguments
* injected by this class.
*/
static Set getUsageArgumentIdentifiers(final CommandLineTool tool)
{
final LinkedHashSet ids =
new LinkedHashSet<>(StaticUtils.computeMapCapacity(9));
ids.add("help");
ids.add("version");
ids.add("helpSubcommands");
if (tool.supportsInteractiveMode())
{
ids.add("interactive");
}
if (tool.supportsPropertiesFile())
{
ids.add("propertiesFilePath");
ids.add("generatePropertiesFile");
ids.add("noPropertiesFile");
ids.add("suppressPropertiesFileComment");
}
if (tool.supportsOutputFile())
{
ids.add("outputFile");
ids.add("appendToOutputFile");
ids.add("teeOutput");
}
return Collections.unmodifiableSet(ids);
}
/**
* Adds the command-line arguments supported for use with this tool to the
* provided argument parser. The tool may need to retain references to the
* arguments (and/or the argument parser, if trailing arguments are allowed)
* to it in order to obtain their values for use in later processing.
*
* @param parser The argument parser to which the arguments are to be added.
*
* @throws ArgumentException If a problem occurs while adding any of the
* tool-specific arguments to the provided
* argument parser.
*/
public abstract void addToolArguments(ArgumentParser parser)
throws ArgumentException;
/**
* Performs any necessary processing that should be done to ensure that the
* provided set of command-line arguments were valid. This method will be
* called after the basic argument parsing has been performed and immediately
* before the {@link CommandLineTool#doToolProcessing} method is invoked.
* Note that if the tool supports interactive mode, then this method may be
* invoked multiple times to allow the user to interactively fix validation
* errors.
*
* @throws ArgumentException If there was a problem with the command-line
* arguments provided to this program.
*/
public void doExtendedArgumentValidation()
throws ArgumentException
{
// No processing will be performed by default.
}
/**
* Performs the core set of processing for this tool.
*
* @return A result code that indicates whether the processing completed
* successfully.
*/
public abstract ResultCode doToolProcessing();
/**
* Indicates whether this tool should register a shutdown hook with the JVM.
* Shutdown hooks allow for a best-effort attempt to perform a specified set
* of processing when the JVM is shutting down under various conditions,
* including:
*
* - When all non-daemon threads have stopped running (i.e., the tool has
* completed processing).
* - When {@code System.exit()} or {@code Runtime.exit()} is called.
* - When the JVM receives an external kill signal (e.g., via the use of
* the kill tool or interrupting the JVM with Ctrl+C).
*
* Shutdown hooks may not be invoked if the process is forcefully killed
* (e.g., using "kill -9", or the {@code System.halt()} or
* {@code Runtime.halt()} methods).
*
* If this method is overridden to return {@code true}, then the
* {@link #doShutdownHookProcessing(ResultCode)} method should also be
* overridden to contain the logic that will be invoked when the JVM is
* shutting down in a manner that calls shutdown hooks.
*
* @return {@code true} if this tool should register a shutdown hook, or
* {@code false} if not.
*/
protected boolean registerShutdownHook()
{
return false;
}
/**
* Performs any processing that may be needed when the JVM is shutting down,
* whether because tool processing has completed or because it has been
* interrupted (e.g., by a kill or break signal).
*
* Note that because shutdown hooks run at a delicate time in the life of the
* JVM, they should complete quickly and minimize access to external
* resources. See the documentation for the
* {@code java.lang.Runtime.addShutdownHook} method for recommendations and
* restrictions about writing shutdown hooks.
*
* @param resultCode The result code returned by the tool. It may be
* {@code null} if the tool was interrupted before it
* completed processing.
*/
protected void doShutdownHookProcessing(final ResultCode resultCode)
{
throw new LDAPSDKUsageException(
ERR_COMMAND_LINE_TOOL_SHUTDOWN_HOOK_NOT_IMPLEMENTED.get(
getToolName()));
}
/**
* Retrieves a set of information that may be used to generate example usage
* information. Each element in the returned map should consist of a map
* between an example set of arguments and a string that describes the
* behavior of the tool when invoked with that set of arguments.
*
* @return A set of information that may be used to generate example usage
* information. It may be {@code null} or empty if no example usage
* information is available.
*/
@ThreadSafety(level=ThreadSafetyLevel.METHOD_THREADSAFE)
public LinkedHashMap getExampleUsages()
{
return null;
}
/**
* Retrieves the print stream that will be used for standard output.
*
* @return The print stream that will be used for standard output.
*/
public final PrintStream getOut()
{
return out;
}
/**
* Retrieves the print stream that may be used to write to the original
* standard output. This may be different from the current standard output
* stream if an output file has been configured.
*
* @return The print stream that may be used to write to the original
* standard output.
*/
public final PrintStream getOriginalOut()
{
return originalOut;
}
/**
* Writes the provided message to the standard output stream for this tool.
*
* This method is completely threadsafe and my be invoked concurrently by any
* number of threads.
*
* @param msg The message components that will be written to the standard
* output stream. They will be concatenated together on the same
* line, and that line will be followed by an end-of-line
* sequence.
*/
@ThreadSafety(level=ThreadSafetyLevel.METHOD_THREADSAFE)
public final synchronized void out(final Object... msg)
{
write(out, 0, 0, msg);
}
/**
* Writes the provided message to the standard output stream for this tool,
* optionally wrapping and/or indenting the text in the process.
*
* This method is completely threadsafe and my be invoked concurrently by any
* number of threads.
*
* @param indent The number of spaces each line should be indented. A
* value less than or equal to zero indicates that no
* indent should be used.
* @param wrapColumn The column at which to wrap long lines. A value less
* than or equal to two indicates that no wrapping should
* be performed. If both an indent and a wrap column are
* to be used, then the wrap column must be greater than
* the indent.
* @param msg The message components that will be written to the
* standard output stream. They will be concatenated
* together on the same line, and that line will be
* followed by an end-of-line sequence.
*/
@ThreadSafety(level=ThreadSafetyLevel.METHOD_THREADSAFE)
public final synchronized void wrapOut(final int indent, final int wrapColumn,
final Object... msg)
{
write(out, indent, wrapColumn, msg);
}
/**
* Writes the provided message to the standard output stream for this tool,
* optionally wrapping and/or indenting the text in the process.
*
* This method is completely threadsafe and my be invoked concurrently by any
* number of threads.
*
* @param firstLineIndent The number of spaces the first line should be
* indented. A value less than or equal to zero
* indicates that no indent should be used.
* @param subsequentLineIndent The number of spaces each line except the
* first should be indented. A value less than
* or equal to zero indicates that no indent
* should be used.
* @param wrapColumn The column at which to wrap long lines. A
* value less than or equal to two indicates
* that no wrapping should be performed. If
* both an indent and a wrap column are to be
* used, then the wrap column must be greater
* than the indent.
* @param endWithNewline Indicates whether a newline sequence should
* follow the last line that is printed.
* @param msg The message components that will be written
* to the standard output stream. They will be
* concatenated together on the same line, and
* that line will be followed by an end-of-line
* sequence.
*/
final synchronized void wrapStandardOut(final int firstLineIndent,
final int subsequentLineIndent,
final int wrapColumn,
final boolean endWithNewline,
final Object... msg)
{
write(out, firstLineIndent, subsequentLineIndent, wrapColumn,
endWithNewline, msg);
}
/**
* Retrieves the print stream that will be used for standard error.
*
* @return The print stream that will be used for standard error.
*/
public final PrintStream getErr()
{
return err;
}
/**
* Retrieves the print stream that may be used to write to the original
* standard error. This may be different from the current standard error
* stream if an output file has been configured.
*
* @return The print stream that may be used to write to the original
* standard error.
*/
public final PrintStream getOriginalErr()
{
return originalErr;
}
/**
* Writes the provided message to the standard error stream for this tool.
*
* This method is completely threadsafe and my be invoked concurrently by any
* number of threads.
*
* @param msg The message components that will be written to the standard
* error stream. They will be concatenated together on the same
* line, and that line will be followed by an end-of-line
* sequence.
*/
@ThreadSafety(level=ThreadSafetyLevel.METHOD_THREADSAFE)
public final synchronized void err(final Object... msg)
{
write(err, 0, 0, msg);
}
/**
* Writes the provided message to the standard error stream for this tool,
* optionally wrapping and/or indenting the text in the process.
*
* This method is completely threadsafe and my be invoked concurrently by any
* number of threads.
*
* @param indent The number of spaces each line should be indented. A
* value less than or equal to zero indicates that no
* indent should be used.
* @param wrapColumn The column at which to wrap long lines. A value less
* than or equal to two indicates that no wrapping should
* be performed. If both an indent and a wrap column are
* to be used, then the wrap column must be greater than
* the indent.
* @param msg The message components that will be written to the
* standard output stream. They will be concatenated
* together on the same line, and that line will be
* followed by an end-of-line sequence.
*/
@ThreadSafety(level=ThreadSafetyLevel.METHOD_THREADSAFE)
public final synchronized void wrapErr(final int indent, final int wrapColumn,
final Object... msg)
{
write(err, indent, wrapColumn, msg);
}
/**
* Writes the provided message to the given print stream, optionally wrapping
* and/or indenting the text in the process.
*
* @param stream The stream to which the message should be written.
* @param indent The number of spaces each line should be indented. A
* value less than or equal to zero indicates that no
* indent should be used.
* @param wrapColumn The column at which to wrap long lines. A value less
* than or equal to two indicates that no wrapping should
* be performed. If both an indent and a wrap column are
* to be used, then the wrap column must be greater than
* the indent.
* @param msg The message components that will be written to the
* standard output stream. They will be concatenated
* together on the same line, and that line will be
* followed by an end-of-line sequence.
*/
private static void write(final PrintStream stream, final int indent,
final int wrapColumn, final Object... msg)
{
write(stream, indent, indent, wrapColumn, true, msg);
}
/**
* Writes the provided message to the given print stream, optionally wrapping
* and/or indenting the text in the process.
*
* @param stream The stream to which the message should be
* written.
* @param firstLineIndent The number of spaces the first line should be
* indented. A value less than or equal to zero
* indicates that no indent should be used.
* @param subsequentLineIndent The number of spaces all lines after the
* first should be indented. A value less than
* or equal to zero indicates that no indent
* should be used.
* @param wrapColumn The column at which to wrap long lines. A
* value less than or equal to two indicates
* that no wrapping should be performed. If
* both an indent and a wrap column are to be
* used, then the wrap column must be greater
* than the indent.
* @param endWithNewline Indicates whether a newline sequence should
* follow the last line that is printed.
* @param msg The message components that will be written
* to the standard output stream. They will be
* concatenated together on the same line, and
* that line will be followed by an end-of-line
* sequence.
*/
private static void write(final PrintStream stream, final int firstLineIndent,
final int subsequentLineIndent,
final int wrapColumn,
final boolean endWithNewline, final Object... msg)
{
final StringBuilder buffer = new StringBuilder();
for (final Object o : msg)
{
buffer.append(o);
}
if (wrapColumn > 2)
{
boolean firstLine = true;
for (final String line :
StaticUtils.wrapLine(buffer.toString(),
(wrapColumn - firstLineIndent),
(wrapColumn - subsequentLineIndent)))
{
final int indent;
if (firstLine)
{
indent = firstLineIndent;
firstLine = false;
}
else
{
stream.println();
indent = subsequentLineIndent;
}
if (indent > 0)
{
for (int i=0; i < indent; i++)
{
stream.print(' ');
}
}
stream.print(line);
}
}
else
{
if (firstLineIndent > 0)
{
for (int i=0; i < firstLineIndent; i++)
{
stream.print(' ');
}
}
stream.print(buffer.toString());
}
if (endWithNewline)
{
stream.println();
}
stream.flush();
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy