SubCommandArgumentParser.java revision 0d6977f00b84c8248c025728305f8268e3ebbb37
/*
* CDDL HEADER START
*
* The contents of this file are subject to the terms of the
* Common Development and Distribution License, Version 1.0 only
* (the "License"). You may not use this file except in compliance
* with the License.
*
* You can obtain a copy of the license at
* trunk/opends/resource/legal-notices/OpenDS.LICENSE
* or https://OpenDS.dev.java.net/OpenDS.LICENSE.
* See the License for the specific language governing permissions
* and limitations under the License.
*
* When distributing Covered Code, include this CDDL HEADER in each
* file and include the License file at
* trunk/opends/resource/legal-notices/OpenDS.LICENSE. If applicable,
* add the following below this CDDL HEADER, with the fields enclosed
* by brackets "[]" replaced with your own identifying information:
* Portions Copyright [yyyy] [name of copyright owner]
*
* CDDL HEADER END
*
*
* Portions Copyright 2006-2007 Sun Microsystems, Inc.
*/
package org.opends.server.util.args;
import org.opends.messages.Message;
import org.opends.messages.MessageBuilder;
import static org.opends.messages.ToolMessages.*;
import static org.opends.messages.UtilityMessages.*;
import static org.opends.server.tools.ToolConstants.*;
import static org.opends.server.util.ServerConstants.*;
import static org.opends.server.util.StaticUtils.*;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.Map;
import java.util.Properties;
import java.util.SortedMap;
import java.util.TreeMap;
import org.opends.server.core.DirectoryServer;
/**
* This class defines a variant of the argument parser that can be used with
* applications that use subcommands to customize their behavior and that have a
* different set of options per subcommand (e.g, "cvs checkout" takes different
* options than "cvs commit"). This parser also has the ability to use global
* options that will always be applicable regardless of the subcommand in
* addition to the subcommand-specific arguments. There must not be any
* conflicts between the global options and the option for any subcommand, but
* it is allowed to re-use subcommand-specific options for different purposes
* between different subcommands.
*/
public class SubCommandArgumentParser extends ArgumentParser
{
// The argument that will be used to trigger the display of usage information.
private Argument usageArgument;
// The arguments that will be used to trigger the display of usage
// information for groups of sub-commands.
private Map<Argument, Collection<SubCommand>> usageGroupArguments;
// The set of unnamed trailing arguments that were provided for this parser.
private ArrayList<String> trailingArguments;
// Indicates whether subcommand and long argument names should be treated in a
// case-sensitive manner.
private boolean longArgumentsCaseSensitive;
// Indicates whether the usage information has been displayed.
private boolean usageOrVersionDisplayed;
// The set of global arguments defined for this parser, referenced by short
// ID.
private HashMap<Character,Argument> globalShortIDMap;
// The set of global arguments defined for this parser, referenced by
// argument name.
private HashMap<String,Argument> globalArgumentMap;
// The set of global arguments defined for this parser, referenced by long
// ID.
private HashMap<String,Argument> globalLongIDMap;
// The set of subcommands defined for this parser, referenced by subcommand
// name.
private SortedMap<String,SubCommand> subCommands;
// The total set of global arguments defined for this parser.
private LinkedList<Argument> globalArgumentList;
// The output stream to which usage information should be printed.
private OutputStream usageOutputStream;
// The fully-qualified name of the Java class that should be invoked to launch
// the program with which this argument parser is associated.
private String mainClassName;
// A human-readable description for the tool, which will be included when
// displaying usage information.
private Message toolDescription;
// The raw set of command-line arguments that were provided.
private String[] rawArguments;
// The subcommand requested by the user as part of the command-line arguments.
private SubCommand subCommand;
/**
* Creates a new instance of this subcommand argument parser with no
* arguments.
*
* @param mainClassName The fully-qualified name of the Java
* class that should be invoked to launch
* the program with which this argument
* parser is associated.
* @param toolDescription A human-readable description for the
* tool, which will be included when
* displaying usage information.
* @param longArgumentsCaseSensitive Indicates whether subcommand and long
* argument names should be treated in a
* case-sensitive manner.
*/
public SubCommandArgumentParser(String mainClassName, Message toolDescription,
boolean longArgumentsCaseSensitive)
{
super(mainClassName, toolDescription, longArgumentsCaseSensitive);
this.mainClassName = mainClassName;
this.toolDescription = toolDescription;
this.longArgumentsCaseSensitive = longArgumentsCaseSensitive;
trailingArguments = new ArrayList<String>();
globalArgumentList = new LinkedList<Argument>();
globalArgumentMap = new HashMap<String,Argument>();
globalShortIDMap = new HashMap<Character,Argument>();
globalLongIDMap = new HashMap<String,Argument>();
usageGroupArguments = new HashMap<Argument, Collection<SubCommand>>();
subCommands = new TreeMap<String,SubCommand>();
usageOrVersionDisplayed = false;
rawArguments = null;
subCommand = null;
usageArgument = null;
usageOutputStream = null;
}
/**
* Retrieves the fully-qualified name of the Java class that should be invoked
* to launch the program with which this argument parser is associated.
*
* @return The fully-qualified name of the Java class that should be invoked
* to launch the program with which this argument parser is
* associated.
*/
public String getMainClassName()
{
return mainClassName;
}
/**
* Retrieves a human-readable description for this tool, which should be
* included at the top of the command-line usage information.
*
* @return A human-readable description for this tool, or {@code null} if
* none is available.
*/
public Message getToolDescription()
{
return toolDescription;
}
/**
* Indicates whether subcommand names and long argument strings should be
* treated in a case-sensitive manner.
*
* @return <CODE>true</CODE> if subcommand names and long argument strings
* should be treated in a case-sensitive manner, or
* <CODE>false</CODE> if they should not.
*/
public boolean longArgumentsCaseSensitive()
{
return longArgumentsCaseSensitive;
}
/**
* Retrieves the list of all global arguments that have been defined for this
* argument parser.
*
* @return The list of all global arguments that have been defined for this
* argument parser.
*/
public LinkedList<Argument> getGlobalArgumentList()
{
return globalArgumentList;
}
/**
* Indicates whether this argument parser contains a global argument with the
* specified name.
*
* @param argumentName The name for which to make the determination.
*
* @return <CODE>true</CODE> if a global argument exists with the specified
* name, or <CODE>false</CODE> if not.
*/
public boolean hasGlobalArgument(String argumentName)
{
return globalArgumentMap.containsKey(argumentName);
}
/**
* Retrieves the global argument with the specified name.
*
* @param name The name of the global argument to retrieve.
*
* @return The global argument with the specified name, or <CODE>null</CODE>
* if there is no such argument.
*/
public Argument getGlobalArgument(String name)
{
return globalArgumentMap.get(name);
}
/**
* Retrieves the set of global arguments mapped by the short identifier that
* may be used to reference them. Note that arguments that do not have a
* short identifier will not be present in this list.
*
* @return The set of global arguments mapped by the short identifier that
* may be used to reference them.
*/
public HashMap<Character,Argument> getGlobalArgumentsByShortID()
{
return globalShortIDMap;
}
/**
* Indicates whether this argument parser has a global argument with the
* specified short ID.
*
* @param shortID The short ID character for which to make the
* determination.
*
* @return <CODE>true</CODE> if a global argument exists with the specified
* short ID, or <CODE>false</CODE> if not.
*/
public boolean hasGlobalArgumentWithShortID(Character shortID)
{
return globalShortIDMap.containsKey(shortID);
}
/**
* Retrieves the global argument with the specified short identifier.
*
* @param shortID The short identifier for the global argument to retrieve.
*
* @return The global argument with the specified short identifier, or
* <CODE>null</CODE> if there is no such argument.
*/
public Argument getGlobalArgumentForShortID(Character shortID)
{
return globalShortIDMap.get(shortID);
}
/**
* Retrieves the set of global arguments mapped by the long identifier that
* may be used to reference them. Note that arguments that do not have a long
* identifier will not be present in this list.
*
* @return The set of global arguments mapped by the long identifier that may
* be used to reference them.
*/
public HashMap<String,Argument> getGlobalArgumentsByLongID()
{
return globalLongIDMap;
}
/**
* Indicates whether this argument parser has a global argument with the
* specified long ID.
*
* @param longID The long ID string for which to make the determination.
*
* @return <CODE>true</CODE> if a global argument exists with the specified
* long ID, or <CODE>false</CODE> if not.
*/
public boolean hasGlobalArgumentWithLongID(String longID)
{
return globalLongIDMap.containsKey(longID);
}
/**
* Retrieves the global argument with the specified long identifier.
*
* @param longID The long identifier for the global argument to retrieve.
*
* @return The global argument with the specified long identifier, or
* <CODE>null</CODE> if there is no such argument.
*/
public Argument getGlobalArgumentForLongID(String longID)
{
return globalLongIDMap.get(longID);
}
/**
* Retrieves the set of subcommands defined for this argument parser,
* referenced by subcommand name.
*
* @return The set of subcommands defined for this argument parser,
* referenced by subcommand name.
*/
public SortedMap<String,SubCommand> getSubCommands()
{
return subCommands;
}
/**
* Indicates whether this argument parser has a subcommand with the specified
* name.
*
* @param name The subcommand name for which to make the determination.
*
* @return <CODE>true</CODE> if this argument parser has a subcommand with
* the specified name, or <CODE>false</CODE> if it does not.
*/
public boolean hasSubCommand(String name)
{
return subCommands.containsKey(name);
}
/**
* Retrieves the subcommand with the specified name.
*
* @param name The name of the subcommand to retrieve.
*
* @return The subcommand with the specified name, or <CODE>null</CODE> if no
* such subcommand is defined.
*/
public SubCommand getSubCommand(String name)
{
return subCommands.get(name);
}
/**
* Retrieves the subcommand that was selected in the set of command-line
* arguments.
*
* @return The subcommand that was selected in the set of command-line
* arguments, or <CODE>null</CODE> if none was selected.
*/
public SubCommand getSubCommand()
{
return subCommand;
}
/**
* Retrieves the raw set of arguments that were provided.
*
* @return The raw set of arguments that were provided, or <CODE>null</CODE>
* if the argument list has not yet been parsed.
*/
public String[] getRawArguments()
{
return rawArguments;
}
/**
* Adds the provided argument to the set of global arguments handled by this
* parser.
*
* @param argument The argument to be added.
*
* @throws ArgumentException If the provided argument conflicts with another
* global or subcommand argument that has already
* been defined.
*/
public void addGlobalArgument(Argument argument)
throws ArgumentException
{
String argumentName = argument.getName();
if (globalArgumentMap.containsKey(argumentName))
{
Message message =
ERR_SUBCMDPARSER_DUPLICATE_GLOBAL_ARG_NAME.get(argumentName);
throw new ArgumentException(message);
}
for (SubCommand s : subCommands.values())
{
if (s.getArgumentForName(argumentName) != null)
{
Message message = ERR_SUBCMDPARSER_GLOBAL_ARG_NAME_SUBCMD_CONFLICT.get(
argumentName, s.getName());
throw new ArgumentException(message);
}
}
Character shortID = argument.getShortIdentifier();
if (shortID != null)
{
if (globalShortIDMap.containsKey(shortID))
{
String name = globalShortIDMap.get(shortID).getName();
Message message = ERR_SUBCMDPARSER_DUPLICATE_GLOBAL_ARG_SHORT_ID.get(
String.valueOf(shortID), argumentName, name);
throw new ArgumentException(message);
}
for (SubCommand s : subCommands.values())
{
if (s.getArgument(shortID) != null)
{
String cmdName = s.getName();
String name = s.getArgument(shortID).getName();
Message message = ERR_SUBCMDPARSER_GLOBAL_ARG_SHORT_ID_CONFLICT.get(
String.valueOf(shortID), argumentName, name, cmdName);
throw new ArgumentException(message);
}
}
}
String longID = argument.getLongIdentifier();
if (longID != null)
{
if (! longArgumentsCaseSensitive)
{
longID = toLowerCase(longID);
}
if (globalLongIDMap.containsKey(longID))
{
String name = globalLongIDMap.get(longID).getName();
Message message = ERR_SUBCMDPARSER_DUPLICATE_GLOBAL_ARG_LONG_ID.get(
longID, argumentName, name);
throw new ArgumentException(message);
}
for (SubCommand s : subCommands.values())
{
if (s.getArgument(longID) != null)
{
String cmdName = s.getName();
String name = s.getArgument(longID).getName();
Message message = ERR_SUBCMDPARSER_GLOBAL_ARG_LONG_ID_CONFLICT.get(
longID, argumentName, name, cmdName);
throw new ArgumentException(message);
}
}
}
if (shortID != null)
{
globalShortIDMap.put(shortID, argument);
}
if (longID != null)
{
globalLongIDMap.put(longID, argument);
}
globalArgumentList.add(argument);
}
/**
* Removes the provided argument from the set of global arguments handled by
* this parser.
*
* @param argument The argument to be removed.
*/
protected void removeGlobalArgument(Argument argument)
{
String argumentName = argument.getName();
globalArgumentMap.remove(argumentName);
Character shortID = argument.getShortIdentifier();
if (shortID != null)
{
globalShortIDMap.remove(shortID);
}
String longID = argument.getLongIdentifier();
if (longID != null)
{
if (! longArgumentsCaseSensitive)
{
longID = toLowerCase(longID);
}
globalLongIDMap.remove(longID);
}
globalArgumentList.remove(argument);
}
/**
* Sets the provided argument as one which will automatically
* trigger the output of full usage information if it is provided on
* the command line and no further argument validation will be
* performed.
* <p>
* If sub-command groups are defined using the
* {@link #setUsageGroupArgument(Argument, Collection)} method, then
* this usage argument, when specified, will result in usage
* information being displayed which does not include information on
* sub-commands.
* <p>
* Note that the caller will still need to add this argument to the
* parser with the {@link #addGlobalArgument(Argument)} method, and
* the argument should not be required and should not take a value.
* Also, the caller will still need to check for the presence of the
* usage argument after calling {@link #parseArguments(String[])} to
* know that no further processing will be required.
*
* @param argument
* The argument whose presence should automatically trigger
* the display of full usage information.
* @param outputStream
* The output stream to which the usage information should
* be written.
*/
public void setUsageArgument(Argument argument, OutputStream outputStream) {
usageArgument = argument;
usageOutputStream = outputStream;
usageGroupArguments.put(argument, Collections.<SubCommand>emptySet());
}
/**
* Sets the provided argument as one which will automatically
* trigger the output of partial usage information if it is provided
* on the command line and no further argument validation will be
* performed.
* <p>
* Partial usage information will include a usage synopsis, a
* summary of each of the sub-commands listed in the provided
* sub-commands collection, and a summary of the global options.
* <p>
* Note that the caller will still need to add this argument to the
* parser with the {@link #addGlobalArgument(Argument)} method, and
* the argument should not be required and should not take a value.
* Also, the caller will still need to check for the presence of the
* usage argument after calling {@link #parseArguments(String[])} to
* know that no further processing will be required.
*
* @param argument
* The argument whose presence should automatically trigger
* the display of partial usage information.
* @param subCommands
* The list of sub-commands which should have their usage
* displayed.
*/
public void setUsageGroupArgument(Argument argument,
Collection<SubCommand> subCommands) {
usageGroupArguments.put(argument, subCommands);
}
/**
* Parses the provided set of arguments and updates the information associated
* with this parser accordingly.
*
* @param rawArguments The raw set of arguments to parse.
*
* @throws ArgumentException If a problem was encountered while parsing the
* provided arguments.
*/
public void parseArguments(String[] rawArguments)
throws ArgumentException
{
parseArguments(rawArguments, null);
}
/**
* Parses the provided set of arguments and updates the information associated
* with this parser accordingly. Default values for unspecified arguments
* may be read from the specified properties file.
*
* @param rawArguments The set of raw arguments to parse.
* @param propertiesFile The path to the properties file to use to
* obtain default values for unspecified
* properties.
* @param requirePropertiesFile Indicates whether the parsing should fail if
* the provided properties file does not exist
* or is not accessible.
*
* @throws ArgumentException If a problem was encountered while parsing the
* provided arguments or interacting with the
* properties file.
*/
public void parseArguments(String[] rawArguments, String propertiesFile,
boolean requirePropertiesFile)
throws ArgumentException
{
this.rawArguments = rawArguments;
Properties argumentProperties = null;
try
{
Properties p = new Properties();
FileInputStream fis = new FileInputStream(propertiesFile);
p.load(fis);
fis.close();
argumentProperties = p;
}
catch (Exception e)
{
if (requirePropertiesFile)
{
Message message = ERR_SUBCMDPARSER_CANNOT_READ_PROPERTIES_FILE.get(
String.valueOf(propertiesFile), getExceptionMessage(e));
throw new ArgumentException(message, e);
}
}
parseArguments(rawArguments, argumentProperties);
}
/**
* Parses the provided set of arguments and updates the information associated
* with this parser accordingly. Default values for unspecified arguments may
* be read from the specified properties if any are provided.
*
* @param rawArguments The set of raw arguments to parse.
* @param argumentProperties A set of properties that may be used to provide
* default values for arguments not included in
* the given raw arguments.
*
* @throws ArgumentException If a problem was encountered while parsing the
* provided arguments.
*/
public void parseArguments(String[] rawArguments,
Properties argumentProperties)
throws ArgumentException
{
this.rawArguments = rawArguments;
this.subCommand = null;
this.trailingArguments = new ArrayList<String>();
this.usageOrVersionDisplayed = false;
boolean inTrailingArgs = false;
int numArguments = rawArguments.length;
for (int i=0; i < numArguments; i++)
{
String arg = rawArguments[i];
if (inTrailingArgs)
{
trailingArguments.add(arg);
if ((subCommand.getMaxTrailingArguments() > 0) &&
(trailingArguments.size() > subCommand.getMaxTrailingArguments()))
{
Message message = ERR_ARGPARSER_TOO_MANY_TRAILING_ARGS.get(
subCommand.getMaxTrailingArguments());
throw new ArgumentException(message);
}
continue;
}
if (arg.equals("--"))
{
inTrailingArgs = true;
}
else if (arg.startsWith("--"))
{
// This indicates that we are using the long name to reference the
// argument. It may be in any of the following forms:
// --name
// --name value
// --name=value
String argName = arg.substring(2);
String argValue = null;
int equalPos = argName.indexOf('=');
if (equalPos < 0)
{
// This is fine. The value is not part of the argument name token.
}
else if (equalPos == 0)
{
// The argument starts with "--=", which is not acceptable.
Message message = ERR_SUBCMDPARSER_LONG_ARG_WITHOUT_NAME.get(arg);
throw new ArgumentException(message);
}
else
{
// The argument is in the form --name=value, so parse them both out.
argValue = argName.substring(equalPos+1);
argName = argName.substring(0, equalPos);
}
// If we're not case-sensitive, then convert the name to lowercase.
if (! longArgumentsCaseSensitive)
{
argName = toLowerCase(argName);
}
// See if the specified name references a global argument. If not, then
// see if it references a subcommand argument.
Argument a = globalLongIDMap.get(argName);
if (a == null)
{
if (subCommand == null)
{
if (argName.equals("help"))
{
// "--help" will always be interpreted as requesting usage
// information.
try
{
getUsage(usageOutputStream);
} catch (Exception e) {}
return;
}
else
if (argName.equals(OPTION_LONG_PRODUCT_VERSION))
{
// "--version" will always be interpreted as requesting usage
// information.
try
{
DirectoryServer.printVersion(usageOutputStream);
usageOrVersionDisplayed = true ;
} catch (Exception e) {}
return;
}
else
{
// There is no such global argument.
Message message =
ERR_SUBCMDPARSER_NO_GLOBAL_ARGUMENT_FOR_LONG_ID.get(argName);
throw new ArgumentException(message);
}
}
else
{
a = subCommand.getArgument(argName);
if (a == null)
{
if (argName.equals("help"))
{
// "--help" will always be interpreted as requesting usage
// information.
try
{
getUsage(usageOutputStream);
} catch (Exception e) {}
return;
}
else
if (argName.equals(OPTION_LONG_PRODUCT_VERSION))
{
// "--version" will always be interpreted as requesting usage
// information.
try
{
DirectoryServer.printVersion(usageOutputStream);
usageOrVersionDisplayed = true ;
} catch (Exception e) {}
return;
}
else
{
// There is no such global or subcommand argument.
Message message =
ERR_SUBCMDPARSER_NO_ARGUMENT_FOR_LONG_ID.get(argName);
throw new ArgumentException(message);
}
}
}
}
a.setPresent(true);
// If this is a usage argument, then immediately stop and print
// usage information.
if (usageGroupArguments.containsKey(a))
{
try
{
getUsage(a, usageOutputStream);
} catch (Exception e) {}
return;
}
// See if the argument takes a value. If so, then make sure one was
// provided. If not, then make sure none was provided.
if (a.needsValue())
{
if (argValue == null)
{
if ((i+1) == numArguments)
{
Message message =
ERR_SUBCMDPARSER_NO_VALUE_FOR_ARGUMENT_WITH_LONG_ID.
get(argName);
throw new ArgumentException(message);
}
argValue = rawArguments[++i];
}
MessageBuilder invalidReason = new MessageBuilder();
if (! a.valueIsAcceptable(argValue, invalidReason))
{
Message message = ERR_SUBCMDPARSER_VALUE_UNACCEPTABLE_FOR_LONG_ID.
get(argValue, argName, invalidReason.toString());
throw new ArgumentException(message);
}
// If the argument already has a value, then make sure it is
// acceptable to have more than one.
if (a.hasValue() && (! a.isMultiValued()))
{
Message message =
ERR_SUBCMDPARSER_NOT_MULTIVALUED_FOR_LONG_ID.get(argName);
throw new ArgumentException(message);
}
a.addValue(argValue);
}
else
{
if (argValue != null)
{
Message message =
ERR_SUBCMDPARSER_ARG_FOR_LONG_ID_DOESNT_TAKE_VALUE.get(argName);
throw new ArgumentException(message);
}
}
}
else if (arg.startsWith("-"))
{
// This indicates that we are using the 1-character name to reference
// the argument. It may be in any of the following forms:
// -n
// -nvalue
// -n value
if (arg.equals("-"))
{
Message message = ERR_SUBCMDPARSER_INVALID_DASH_AS_ARGUMENT.get();
throw new ArgumentException(message);
}
char argCharacter = arg.charAt(1);
String argValue;
if (arg.length() > 2)
{
argValue = arg.substring(2);
}
else
{
argValue = null;
}
// Get the argument with the specified short ID. It may be either a
// global argument or a subcommand-specific argument.
Argument a = globalShortIDMap.get(argCharacter);
if (a == null)
{
if (subCommand == null)
{
if (argCharacter == '?')
{
// "-?" will always be interpreted as requesting usage.
try
{
getUsage(usageOutputStream);
} catch (Exception e) {}
return;
}
else
if (argCharacter == OPTION_SHORT_PRODUCT_VERSION)
{
// "-V" will always be interpreted as requesting
// version information except if it's already defined.
boolean dashVAccepted = true;
if (globalShortIDMap.containsKey(OPTION_SHORT_PRODUCT_VERSION))
{
dashVAccepted = false;
}
else
{
for (SubCommand subCmd : subCommands.values())
{
if (subCmd.getArgument(OPTION_SHORT_PRODUCT_VERSION) != null)
{
dashVAccepted = false;
break;
}
}
}
if (dashVAccepted)
{
usageOrVersionDisplayed = true;
try
{
DirectoryServer.printVersion(usageOutputStream);
}
catch (Exception e)
{
}
return;
}
else
{
// -V is defined in another suncommand, so we can
// accepted it as the version information argument
Message message =
ERR_SUBCMDPARSER_NO_GLOBAL_ARGUMENT_FOR_SHORT_ID.
get(String.valueOf(argCharacter));
throw new ArgumentException(message);
}
}
else
{
// There is no such argument registered.
Message message =
ERR_SUBCMDPARSER_NO_GLOBAL_ARGUMENT_FOR_SHORT_ID.
get(String.valueOf(argCharacter));
throw new ArgumentException(message);
}
}
else
{
a = subCommand.getArgument(argCharacter);
if (a == null)
{
if (argCharacter == '?')
{
// "-?" will always be interpreted as requesting usage.
try
{
getUsage(usageOutputStream);
} catch (Exception e) {}
return;
}
else
if (argCharacter == OPTION_SHORT_PRODUCT_VERSION)
{
// "-V" will always be interpreted as requesting
// version information except if it's already defined.
boolean dashVAccepted = true;
if (globalShortIDMap.containsKey(OPTION_SHORT_PRODUCT_VERSION))
{
dashVAccepted = false;
}
else
{
for (SubCommand subCmd : subCommands.values())
{
if (subCmd.getArgument(OPTION_SHORT_PRODUCT_VERSION)!=null)
{
dashVAccepted = false;
break;
}
}
}
if (dashVAccepted)
{
usageOrVersionDisplayed = true;
try
{
DirectoryServer.printVersion(usageOutputStream);
}
catch (Exception e)
{
}
return;
}
}
else
{
// There is no such argument registered.
Message message = ERR_SUBCMDPARSER_NO_ARGUMENT_FOR_SHORT_ID.get(
String.valueOf(argCharacter));
throw new ArgumentException(message);
}
}
}
}
a.setPresent(true);
// If this is the usage argument, then immediately stop and print
// usage information.
if (usageGroupArguments.containsKey(a))
{
try
{
getUsage(a, usageOutputStream);
} catch (Exception e) {}
return;
}
// See if the argument takes a value. If so, then make sure one was
// provided. If not, then make sure none was provided.
if (a.needsValue())
{
if (argValue == null)
{
if ((i+1) == numArguments)
{
Message message =
ERR_SUBCMDPARSER_NO_VALUE_FOR_ARGUMENT_WITH_SHORT_ID.
get(String.valueOf(argCharacter));
throw new ArgumentException(message);
}
argValue = rawArguments[++i];
}
MessageBuilder invalidReason = new MessageBuilder();
if (! a.valueIsAcceptable(argValue, invalidReason))
{
Message message = ERR_SUBCMDPARSER_VALUE_UNACCEPTABLE_FOR_SHORT_ID.
get(argValue, String.valueOf(argCharacter),
invalidReason.toString());
throw new ArgumentException(message);
}
// If the argument already has a value, then make sure it is
// acceptable to have more than one.
if (a.hasValue() && (! a.isMultiValued()))
{
Message message = ERR_SUBCMDPARSER_NOT_MULTIVALUED_FOR_SHORT_ID.get(
String.valueOf(argCharacter));
throw new ArgumentException(message);
}
a.addValue(argValue);
}
else
{
if (argValue != null)
{
// If we've gotten here, then it means that we're in a scenario like
// "-abc" where "a" is a valid argument that doesn't take a value.
// However, this could still be valid if all remaining characters in
// the value are also valid argument characters that don't take
// values.
int valueLength = argValue.length();
for (int j=0; j < valueLength; j++)
{
char c = argValue.charAt(j);
Argument b = globalShortIDMap.get(c);
if (b == null)
{
if (subCommand == null)
{
Message message =
ERR_SUBCMDPARSER_NO_GLOBAL_ARGUMENT_FOR_SHORT_ID.
get(String.valueOf(argCharacter));
throw new ArgumentException(message);
}
else
{
b = subCommand.getArgument(c);
if (b == null)
{
Message message = ERR_SUBCMDPARSER_NO_ARGUMENT_FOR_SHORT_ID.
get(String.valueOf(argCharacter));
throw new ArgumentException(message);
}
}
}
if (b.needsValue())
{
// This means we're in a scenario like "-abc" where b is a
// valid argument that takes a value. We don't support that.
Message message = ERR_SUBCMDPARSER_CANT_MIX_ARGS_WITH_VALUES.
get(String.valueOf(argCharacter), argValue,
String.valueOf(c));
throw new ArgumentException(message);
}
else
{
b.setPresent(true);
// If this is the usage argument, then immediately stop and
// print usage information.
if (usageGroupArguments.containsKey(b))
{
try
{
getUsage(b, usageOutputStream);
} catch (Exception e) {}
return;
}
}
}
}
}
}
else if (subCommand != null)
{
// It's not a short or long identifier and the sub-command has
// already been specified, so it must be the first trailing argument.
if (subCommand.allowsTrailingArguments())
{
trailingArguments.add(arg);
inTrailingArgs = true;
}
else
{
// Trailing arguments are not allowed for this sub-command.
Message message = ERR_ARGPARSER_DISALLOWED_TRAILING_ARGUMENT.get(arg);
throw new ArgumentException(message);
}
}
else
{
// It must be the sub-command.
String nameToCheck = arg;
if (! longArgumentsCaseSensitive)
{
nameToCheck = toLowerCase(arg);
}
SubCommand sc = subCommands.get(nameToCheck);
if (sc == null)
{
Message message = ERR_SUBCMDPARSER_INVALID_ARGUMENT.get(arg);
throw new ArgumentException(message);
}
else
{
subCommand = sc;
}
}
}
// If we have a sub-command and it allows trailing arguments and
// there is a minimum number, then make sure at least that many
// were provided.
if (subCommand != null)
{
int minTrailingArguments = subCommand.getMinTrailingArguments();
if (subCommand.allowsTrailingArguments() && (minTrailingArguments > 0))
{
if (trailingArguments.size() < minTrailingArguments)
{
Message message = ERR_ARGPARSER_TOO_FEW_TRAILING_ARGUMENTS.get(
minTrailingArguments);
throw new ArgumentException(message);
}
}
}
// If we don't have the argumentProperties, try to load a properties file.
if (argumentProperties == null)
{
argumentProperties = checkExternalProperties();
}
// Iterate through all the global arguments and make sure that they have
// values or a suitable default is available.
for (Argument a : globalArgumentList)
{
if (! a.isPresent())
{
// See if there is a value in the properties that can be used
if ((argumentProperties != null) && (a.getPropertyName() != null))
{
String value = argumentProperties.getProperty(a.getPropertyName()
.toLowerCase());
if (value != null)
{
a.addValue(value);
if (a.needsValue())
{
a.setPresent(true);
}
a.setValueSetByProperty(true);
}
}
}
if ((! a.isPresent()) && a.needsValue())
{
// ISee if the argument defines a default.
if (a.getDefaultValue() != null)
{
a.addValue(a.getDefaultValue());
}
// If there is still no value and the argument is required, then that's
// a problem.
if ((! a.hasValue()) && a.isRequired())
{
Message message =
ERR_SUBCMDPARSER_NO_VALUE_FOR_REQUIRED_ARG.get(a.getName());
throw new ArgumentException(message);
}
}
}
// Iterate through all the subcommand-specific arguments and make sure that
// they have values or a suitable default is available.
if (subCommand != null)
{
for (Argument a : subCommand.getArguments())
{
if (! a.isPresent())
{
// See if there is a value in the properties that can be used
if ((argumentProperties != null) && (a.getPropertyName() != null))
{
String value = argumentProperties.getProperty(a.getPropertyName()
.toLowerCase());
if (value != null)
{
a.addValue(value);
if (a.needsValue())
{
a.setPresent(true);
}
a.setValueSetByProperty(true);
}
}
}
if ((! a.isPresent()) && a.needsValue())
{
// See if the argument defines a default.
if (a.getDefaultValue() != null)
{
a.addValue(a.getDefaultValue());
}
// If there is still no value and the argument is required, then
// that's a problem.
if ((! a.hasValue()) && a.isRequired())
{
Message message =
ERR_SUBCMDPARSER_NO_VALUE_FOR_REQUIRED_ARG.get(a.getName());
throw new ArgumentException(message);
}
}
}
}
}
/**
* Appends usage information for the specified subcommand to the provided
* buffer.
*
* @param buffer The buffer to which the usage information should be
* appended.
* @param subCommand The subcommand for which to display the usage
* information.
*/
public void getSubCommandUsage(MessageBuilder buffer, SubCommand subCommand)
{
usageOrVersionDisplayed = true;
String scriptName = System.getProperty(PROPERTY_SCRIPT_NAME);
if ((scriptName == null) || (scriptName.length() == 0))
{
scriptName = "java " + mainClassName;
}
buffer.append(INFO_ARGPARSER_USAGE.get());
buffer.append(" ");
buffer.append(scriptName);
buffer.append(" ");
buffer.append(subCommand.getName());
buffer.append(" ").append(INFO_SUBCMDPARSER_OPTIONS.get());
if (subCommand.allowsTrailingArguments()) {
buffer.append(' ');
buffer.append(subCommand.getTrailingArgumentsDisplayName());
}
buffer.append(EOL);
buffer.append(subCommand.getDescription());
buffer.append(EOL);
if ( ! globalArgumentList.isEmpty())
{
buffer.append(EOL);
buffer.append(INFO_GLOBAL_OPTIONS.get());
buffer.append(EOL);
buffer.append(" ");
buffer.append(INFO_GLOBAL_OPTIONS_REFERENCE.get(scriptName));
buffer.append(EOL);
}
if ( ! subCommand.getArguments().isEmpty() )
{
buffer.append(EOL);
buffer.append(INFO_SUBCMD_OPTIONS.get());
buffer.append(EOL);
}
for (Argument a : subCommand.getArguments())
{
// If this argument is hidden, then skip it.
if (a.isHidden())
{
continue;
}
// Write a line with the short and/or long identifiers that may be used
// for the argument.
Character shortID = a.getShortIdentifier();
String longID = a.getLongIdentifier();
if (shortID != null)
{
int currentLength = buffer.length();
if (a.equals(usageArgument))
{
buffer.append("-?, ");
}
buffer.append("-");
buffer.append(shortID.charValue());
if (a.needsValue() && longID == null)
{
buffer.append(" ");
buffer.append(a.getValuePlaceholder());
}
if (longID != null)
{
StringBuilder newBuffer = new StringBuilder();
newBuffer.append(", --");
newBuffer.append(longID);
if (a.needsValue())
{
newBuffer.append(" ");
newBuffer.append(a.getValuePlaceholder());
}
int lineLength = (buffer.length() - currentLength) +
newBuffer.length();
if (lineLength > 80)
{
buffer.append(EOL);
buffer.append(newBuffer.toString());
}
else
{
buffer.append(newBuffer.toString());
}
}
buffer.append(EOL);
}
else
{
if (longID != null)
{
if (a.equals(usageArgument))
{
buffer.append("-?, ");
}
buffer.append("--");
buffer.append(longID);
if (a.needsValue())
{
buffer.append(" ");
buffer.append(a.getValuePlaceholder());
}
buffer.append(EOL);
}
}
// Write one or more lines with the description of the argument. We will
// indent the description five characters and try our best to wrap at or
// before column 79 so it will be friendly to 80-column displays.
Message description = a.getDescription();
if (description.length() <= 75)
{
buffer.append(" ");
buffer.append(description);
buffer.append(EOL);
}
else
{
String s = description.toString();
while (s.length() > 75)
{
int spacePos = s.lastIndexOf(' ', 75);
if (spacePos > 0)
{
buffer.append(" ");
buffer.append(s.substring(0, spacePos).trim());
s = s.substring(spacePos+1).trim();
buffer.append(EOL);
}
else
{
// There are no spaces in the first 74 columns. See if there is one
// after that point. If so, then break there. If not, then don't
// break at all.
spacePos = s.indexOf(' ');
if (spacePos > 0)
{
buffer.append(" ");
buffer.append(s.substring(0, spacePos).trim());
s = s.substring(spacePos+1).trim();
buffer.append(EOL);
}
else
{
buffer.append(" ");
buffer.append(s);
s = "";
buffer.append(EOL);
}
}
}
if (s.length() > 0)
{
buffer.append(" ");
buffer.append(s);
buffer.append(EOL);
}
}
}
}
/**
* Retrieves a string containing usage information based on the defined
* arguments.
*
* @return A string containing usage information based on the defined
* arguments.
*/
public String getUsage()
{
MessageBuilder buffer = new MessageBuilder();
if (subCommand == null) {
if (usageGroupArguments.size() > 1) {
// We have sub-command groups, so don't display any
// sub-commands by default.
getFullUsage(Collections.<SubCommand> emptySet(), true, buffer);
} else {
// No grouping, so display all sub-commands.
getFullUsage(subCommands.values(), true, buffer);
}
} else {
getSubCommandUsage(buffer, subCommand);
}
return buffer.toMessage().toString();
}
/**
* Retrieves a string describing how the user can get more help.
*
* @return A string describing how the user can get more help.
*/
public Message getHelpUsageReference()
{
usageOrVersionDisplayed = true;
String scriptName = System.getProperty(PROPERTY_SCRIPT_NAME);
if ((scriptName == null) || (scriptName.length() == 0))
{
scriptName = "java " + mainClassName;
}
MessageBuilder buffer = new MessageBuilder();
buffer.append(INFO_GLOBAL_HELP_REFERENCE.get(scriptName));
buffer.append(EOL);
return buffer.toMessage();
}
/**
* Retrieves the set of unnamed trailing arguments that were
* provided on the command line.
*
* @return The set of unnamed trailing arguments that were provided
* on the command line.
*/
public ArrayList<String> getTrailingArguments()
{
return trailingArguments;
}
/**
* Indicates whether the usage information has been displayed to the end user
* either by an explicit argument like "-H" or "--help", or by a built-in
* argument like "-?".
*
* @return {@code true} if the usage information has been displayed, or
* {@code false} if not.
*/
public boolean usageOrVersionDisplayed()
{
return usageOrVersionDisplayed;
}
/**
* Adds the provided subcommand to this argument parser. This is only
* intended for use by the <CODE>SubCommand</CODE> constructor and does not
* do any validation of its own to ensure that there are no conflicts with the
* subcommand or any of its arguments.
*
* @param subCommand The subcommand to add to this argument parser.
*/
void addSubCommand(SubCommand subCommand)
{
subCommands.put(toLowerCase(subCommand.getName()), subCommand);
}
// Get usage for a specific usage argument.
private void getUsage(Argument a, OutputStream outputStream)
throws IOException {
MessageBuilder buffer = new MessageBuilder();
if (a.equals(usageArgument) && subCommand != null) {
getSubCommandUsage(buffer, subCommand);
} else if (a.equals(usageArgument) && usageGroupArguments.size() <= 1) {
// No groups - so display all sub-commands.
getFullUsage(subCommands.values(), true, buffer);
} else if (a.equals(usageArgument)) {
// Using groups - so display all sub-commands group help.
getFullUsage(Collections.<SubCommand> emptySet(), true, buffer);
} else {
// Requested help on specific group - don't display global
// options.
getFullUsage(usageGroupArguments.get(a), false, buffer);
}
outputStream.write(getBytes(buffer.toString()));
}
/**
* {@inheritDoc}
*/
public void getUsage(OutputStream outputStream)
throws IOException {
outputStream.write(getBytes(String.valueOf(getUsage())));
}
// Appends complete usage information for the specified set of
// sub-commands.
private void getFullUsage(Collection<SubCommand> c,
boolean showGlobalOptions, MessageBuilder buffer) {
usageOrVersionDisplayed = true;
if ((toolDescription != null) && (toolDescription.length() > 0))
{
buffer.append(wrapText(toolDescription, 79));
buffer.append(EOL);
}
String scriptName = System.getProperty(PROPERTY_SCRIPT_NAME);
if ((scriptName == null) || (scriptName.length() == 0))
{
scriptName = "java " + mainClassName;
}
buffer.append(INFO_ARGPARSER_USAGE.get());
buffer.append(" ");
buffer.append(scriptName);
if (subCommands.isEmpty())
{
buffer.append(" "+INFO_SUBCMDPARSER_OPTIONS.get());
}
else
{
buffer.append(" "+INFO_SUBCMDPARSER_SUBCMD_AND_OPTIONS.get());
}
if (!subCommands.isEmpty())
{
buffer.append(EOL);
buffer.append(EOL);
if (c.isEmpty())
{
buffer.append(INFO_SUBCMDPARSER_SUBCMD_HELP_HEADING.get());
}
else
{
buffer.append(INFO_SUBCMDPARSER_SUBCMD_HEADING.get());
}
buffer.append(EOL);
}
if (c.isEmpty()) {
// Display usage arguments (except the default one).
for (Argument a : globalArgumentList) {
if (a.isHidden()) {
continue;
}
if (usageGroupArguments.containsKey(a)) {
if (!a.equals(usageArgument)) {
printArgumentUsage(a, buffer);
}
}
}
} else {
int indentNb = 0;
for (SubCommand sc : c) {
if (sc.isHidden()) {
continue;
}
if (sc.getName().length() > indentNb) {
indentNb = sc.getName().length();
}
}
indentNb++;
boolean isFirst = true;
for (SubCommand sc : c) {
if (sc.isHidden()) {
continue;
}
if (!isFirst)
{
buffer.append(EOL);
}
buffer.append(" " + sc.getName());
for (int i = 0; i < indentNb - sc.getName().length(); i++) {
buffer.append(" ");
}
buffer.append(sc.getDescription());
buffer.append(EOL);
isFirst = false;
}
}
buffer.append(EOL);
if (showGlobalOptions) {
if (subCommands.isEmpty())
{
buffer.append(INFO_SUBCMDPARSER_WHERE_OPTIONS_INCLUDE.get());
buffer.append(EOL);
}
else
{
buffer.append(INFO_SUBCMDPARSER_GLOBAL_HEADING.get());
buffer.append(EOL);
}
// --version is a builtin option
boolean dashVAccepted = true;
if (globalShortIDMap.containsKey(OPTION_SHORT_PRODUCT_VERSION))
{
dashVAccepted = false;
}
else
{
for (SubCommand subCmd : subCommands.values())
{
if (subCmd.getArgument(OPTION_SHORT_PRODUCT_VERSION) != null)
{
dashVAccepted = false;
break;
}
}
}
if (dashVAccepted)
{
buffer.append("-" + OPTION_SHORT_PRODUCT_VERSION + ", ");
}
buffer.append("--" + OPTION_LONG_PRODUCT_VERSION);
buffer.append(EOL);
buffer.append(" ");
buffer.append( INFO_DESCRIPTION_PRODUCT_VERSION.get());
buffer.append(EOL);
// Display non-usage arguments.
for (Argument a : globalArgumentList) {
if (a.isHidden()) {
continue;
}
if (!usageGroupArguments.containsKey(a)) {
printArgumentUsage(a, buffer);
}
}
// Finally print default usage argument.
if (usageArgument != null) {
printArgumentUsage(usageArgument, buffer);
} else {
buffer.append("-?");
}
buffer.append(EOL);
}
}
/**
* Appends argument usage information to the provided buffer.
*
* @param a
* The argument to handle.
* @param buffer
* The buffer to which the usage information should be
* appended.
*/
private void printArgumentUsage(Argument a, MessageBuilder buffer) {
String value;
if (a.needsValue())
{
String valuePlaceholder = a.getValuePlaceholder();
if (valuePlaceholder == null)
{
value = " {value}";
}
else
{
value = " " + valuePlaceholder;
}
}
else
{
value = "";
}
Character shortIDChar = a.getShortIdentifier();
if (shortIDChar != null)
{
if (a.equals(usageArgument))
{
buffer.append("-?, ");
}
buffer.append("-");
buffer.append(shortIDChar);
String longIDString = a.getLongIdentifier();
if (longIDString != null)
{
buffer.append(", --");
buffer.append(longIDString);
}
buffer.append(value);
}
else
{
String longIDString = a.getLongIdentifier();
if (longIDString != null)
{
if (a.equals(usageArgument))
{
buffer.append("-?, ");
}
buffer.append("--");
buffer.append(longIDString);
buffer.append(value);
}
}
buffer.append(EOL);
indentAndWrap(Message.raw(" "), a.getDescription(), buffer);
}
/**
* Write one or more lines with the description of the argument. We will
* indent the description five characters and try our best to wrap at or
* before column 79 so it will be friendly to 80-column displays.
*/
private void indentAndWrap(Message indent, Message text,
MessageBuilder buffer)
{
int actualSize = 80 - indent.length();
if (text.length() <= actualSize)
{
buffer.append(indent);
buffer.append(text);
buffer.append(EOL);
}
else
{
String s = text.toString();
while (s.length() > actualSize)
{
int spacePos = s.lastIndexOf(' ', actualSize);
if (spacePos > 0)
{
buffer.append(indent);
buffer.append(s.substring(0, spacePos).trim());
s = s.substring(spacePos + 1).trim();
buffer.append(EOL);
}
else
{
// There are no spaces in the first actualSize -1 columns. See
// if there is one after that point. If so, then break there.
// If not, then don't break at all.
spacePos = s.indexOf(' ');
if (spacePos > 0)
{
buffer.append(indent);
buffer.append(s.substring(0, spacePos).trim());
s = s.substring(spacePos + 1).trim();
buffer.append(EOL);
}
else
{
buffer.append(indent);
buffer.append(s);
s = "";
buffer.append(EOL);
}
}
}
if (s.length() > 0)
{
buffer.append(indent);
buffer.append(s);
buffer.append(EOL);
}
}
}
}