/* * 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 * * * Copyright 2006-2009 Sun Microsystems, Inc. */ package org.opends.server.util.args; import org.opends.messages.Message; import org.opends.messages.MessageBuilder; 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; import org.opends.server.util.SetupUtils; /** * 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> usageGroupArguments; // The set of unnamed trailing arguments that were provided for this parser. private ArrayList 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 globalShortIDMap; // The set of global arguments defined for this parser, referenced by // argument name. private HashMap globalArgumentMap; // The set of global arguments defined for this parser, referenced by long // ID. private HashMap globalLongIDMap; // The set of subcommands defined for this parser, referenced by subcommand // name. private SortedMap subCommands; // The total set of global arguments defined for this parser. private LinkedList 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; //Indicates whether the version argument was provided. private boolean versionPresent; private final static String INDENT = " "; private final static int MAX_LENGTH = SetupUtils.isWindows() ? 79 : 80; /** * 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(); globalArgumentList = new LinkedList(); globalArgumentMap = new HashMap(); globalShortIDMap = new HashMap(); globalLongIDMap = new HashMap(); usageGroupArguments = new HashMap>(); subCommands = new TreeMap(); 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 true if subcommand names and long argument strings * should be treated in a case-sensitive manner, or * false 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 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 true if a global argument exists with the specified * name, or false 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 null * 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 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 true if a global argument exists with the specified * short ID, or false 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 * null 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 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 true if a global argument exists with the specified * long ID, or false 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 * null 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 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 true if this argument parser has a subcommand with * the specified name, or false 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 null 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 null 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 null * 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 { addGlobalArgument(argument, null); } /** * Adds the provided argument to the set of global arguments handled by this * parser. * * @param argument The argument to be added. * @param group The argument group to which the argument belongs. * @throws ArgumentException If the provided argument conflicts with another * global or subcommand argument that has already * been defined. */ public void addGlobalArgument(Argument argument, ArgumentGroup group) 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( argument.getLongIdentifier(), 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( argument.getLongIdentifier(), argumentName, name, cmdName); throw new ArgumentException(message); } } } if (shortID != null) { globalShortIDMap.put(shortID, argument); } if (longID != null) { globalLongIDMap.put(longID, argument); } globalArgumentList.add(argument); if (group == null) { group = getStandardGroup(argument); } group.addArgument(argument); argumentGroups.add(group); } /** * 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. *

* 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. *

* 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.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. *

* 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. *

* 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 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(); 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. String origArgName = argName; 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 { versionPresent = true; 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( origArgName); 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 { versionPresent = true; 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(origArgName); 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(origArgName); throw new ArgumentException(message); } a.addValue(argValue); } else { if (argValue != null) { Message message = ERR_SUBCMDPARSER_ARG_FOR_LONG_ID_DOESNT_TAKE_VALUE.get( origArgName); 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); if (usageArgument != null) { usageArgument.setPresent(true); } } 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; versionPresent = 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; versionPresent = 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()); MessageBuilder invalidReason = new MessageBuilder(); if (value != null) { Boolean addValue = true; if (!( a instanceof BooleanArgument)) { addValue = a.valueIsAcceptable(value, invalidReason); } if (addValue) { 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()); MessageBuilder invalidReason = new MessageBuilder(); if (value != null) { Boolean addValue = true; if (!( a instanceof BooleanArgument)) { addValue = a.valueIsAcceptable(value, invalidReason); } if (addValue) { 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 > MAX_LENGTH) { 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(); int maxLength = MAX_LENGTH - INDENT.length() - 1; if (description.length() <= maxLength) { buffer.append(INDENT); buffer.append(description); buffer.append(EOL); } else { String s = description.toString(); while (s.length() > maxLength) { int spacePos = s.lastIndexOf(' ', maxLength); 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 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(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(" "); buffer.append(s); buffer.append(EOL); } } if (a.needsValue() && (a.getDefaultValue() != null) && (a.getDefaultValue().length() > 0)) { buffer.append(INDENT); buffer.append(INFO_ARGPARSER_USAGE_DEFAULT_VALUE.get( a.getDefaultValue()).toString()); 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. 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 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 SubCommand 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. 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 c, boolean showGlobalOptions, MessageBuilder buffer) { usageOrVersionDisplayed = true; if ((toolDescription != null) && (toolDescription.length() > 0)) { buffer.append(wrapText(toolDescription, MAX_LENGTH - 1)); buffer.append(EOL); 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 { boolean isFirst = true; for (SubCommand sc : c) { if (sc.isHidden()) { continue; } if (isFirst) { buffer.append(EOL); } buffer.append(sc.getName()); buffer.append(EOL); indentAndWrap(Message.raw(INDENT), sc.getDescription(), buffer); 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); } buffer.append(EOL); boolean printGroupHeaders = printUsageGroupHeaders(); // Display non-usage arguments. for (ArgumentGroup argGroup : argumentGroups) { if (argGroup.containsArguments() && printGroupHeaders) { // Print the groups description if any Message groupDesc = argGroup.getDescription(); if (groupDesc != null && !Message.EMPTY.equals(groupDesc)) { buffer.append(EOL); buffer.append(wrapText(groupDesc.toString(), MAX_LENGTH - 1)); buffer.append(EOL); buffer.append(EOL); } } for (Argument a : argGroup.getArguments()) { 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()) { Message pHolder = a.getValuePlaceholder(); if (pHolder == null) { value = " {value}"; } else { value = " " + pHolder; } } 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(INDENT), a.getDescription(), buffer); if (a.needsValue() && (a.getDefaultValue() != null) && (a.getDefaultValue().length() > 0)) { indentAndWrap(Message.raw(INDENT), INFO_ARGPARSER_USAGE_DEFAULT_VALUE.get(a.getDefaultValue()), 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 = MAX_LENGTH - 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); } } } /** * Returns whether the usage argument was provided or not. This method * should be called after a call to parseArguments. * @return true if the usage argument was provided and * false otherwise. */ public boolean isUsageArgumentPresent() { boolean isUsageArgumentPresent = false; if (usageArgument != null) { isUsageArgumentPresent = usageArgument.isPresent(); } return isUsageArgumentPresent; } /** * Returns whether the version argument was provided or not. This method * should be called after a call to parseArguments. * @return true if the version argument was provided and * false otherwise. */ public boolean isVersionArgumentPresent() { boolean isPresent; if (!super.isVersionArgumentPresent()) { isPresent = versionPresent; } else { isPresent = true; } return isPresent; } }