/*
* $Id$
*
* 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-2010 Sun Microsystems, Inc.
* Portions Copyright 2011 ForgeRock AS
* Portions Copyright 2013 Jens Elkner
*/
package org.opends.server.util.args;
import org.opends.messages.Message;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.Properties;
import java.util.TreeSet;
import java.util.Set;
import org.opends.server.core.DirectoryServer;
import org.opends.server.util.SetupUtils;
import static org.opends.messages.UtilityMessages.*;
import static org.opends.server.util.ServerConstants.*;
import static org.opends.server.util.StaticUtils.*;
import static org.opends.server.tools.ToolConstants.*;
import static org.opends.messages.ToolMessages.*;
import org.opends.messages.MessageBuilder;
/**
* This class defines a utility that can be used to deal with command-line
* arguments for applications in a CLIP-compliant manner using either short
* one-character or longer word-based arguments. It is also integrated with the
* Directory Server message catalog so that it can display messages in an
* internationalizeable format, can automatically generate usage information,
* can detect conflicts between arguments, and can interact with a properties
* file to obtain default values for arguments there if they are not specified
* on the command-line.
*/
public class ArgumentParser
{
/**
* The argument that will be used to indicate the file properties.
*/
private StringArgument filePropertiesPathArgument;
/**
* The argument that will be used to indicate that we'll not look for
* default properties file.
*/
private BooleanArgument noPropertiesFileArgument;
// The argument that will be used to trigger the display of usage information.
private Argument usageArgument;
// The argument that will be used to trigger the display of the OpenDS
// version.
private Argument versionArgument;
// The set of unnamed trailing arguments that were provided for this parser.
private ArrayList<String> trailingArguments;
// Indicates whether this parser will allow additional unnamed arguments at
// the end of the list.
private boolean allowsTrailingArguments;
// Indicates whether long arguments should be treated in a case-sensitive
// manner.
private boolean longArgumentsCaseSensitive;
// Indicates whether the usage or version information has been displayed.
private boolean usageOrVersionDisplayed;
// Indicates whether the version argument was provided.
private boolean versionPresent;
// The set of arguments defined for this parser, referenced by short ID.
private HashMap<Character,Argument> shortIDMap;
// The set of arguments defined for this parser, referenced by argument name.
private HashMap<String,Argument> argumentMap;
// The set of arguments defined for this parser, referenced by long ID.
private HashMap<String,Argument> longIDMap;
// The maximum number of unnamed trailing arguments that may be provided.
private int maxTrailingArguments;
// The minimum number of unnamed trailing arguments that may be provided.
private int minTrailingArguments;
// The total set of arguments defined for this parser.
private LinkedList<Argument> argumentList;
// 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 display name that will be used for the trailing arguments in the usage
// information.
private String trailingArgsDisplayName;
// The raw set of command-line arguments that were provided.
private String[] rawArguments;
/** Set of argument groups. */
protected Set<ArgumentGroup> argumentGroups;
/**
* Group for arguments that have not been explicitly grouped.
* These will appear at the top of the usage statement without
* a header.
*/
protected ArgumentGroup defaultArgGroup = new ArgumentGroup(
Message.EMPTY, Integer.MAX_VALUE);
/**
* Group for arguments that are related to connection through LDAP. This
* includes options like the bind DN, the port, etc.
*/
protected ArgumentGroup ldapArgGroup = new ArgumentGroup(
INFO_DESCRIPTION_LDAP_CONNECTION_ARGS.get(), Integer.MIN_VALUE + 2);
/**
* Group for arguments that are related to utility input/output like
* properties file, no-prompt etc. These will appear toward the bottom
* of the usage statement.
*/
protected ArgumentGroup ioArgGroup = new ArgumentGroup(
INFO_DESCRIPTION_IO_ARGS.get(), Integer.MIN_VALUE + 1);
/**
* Group for arguments that are general like help, version etc.
* These will appear at the end of the usage statement.
*/
protected ArgumentGroup generalArgGroup = new ArgumentGroup(
INFO_DESCRIPTION_GENERAL_ARGS.get(), Integer.MIN_VALUE);
private final static String INDENT = " ";
private final static int MAX_LENGTH = SetupUtils.isWindows() ? 79 : 80;
/**
* Creates a new instance of this argument parser with no arguments.
* Unnamed trailing arguments will not be allowed.
*
* @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 long arguments should
* be treated in a case-sensitive manner.
*/
public ArgumentParser(String mainClassName, Message toolDescription,
boolean longArgumentsCaseSensitive)
{
this.mainClassName = mainClassName;
this.toolDescription = toolDescription;
this.longArgumentsCaseSensitive = longArgumentsCaseSensitive;
argumentList = new LinkedList<Argument>();
argumentMap = new HashMap<String,Argument>();
shortIDMap = new HashMap<Character,Argument>();
longIDMap = new HashMap<String,Argument>();
allowsTrailingArguments = false;
usageOrVersionDisplayed = false;
versionPresent = false;
trailingArgsDisplayName = null;
maxTrailingArguments = 0;
minTrailingArguments = 0;
trailingArguments = new ArrayList<String>();
rawArguments = null;
usageArgument = null;
filePropertiesPathArgument = null;
noPropertiesFileArgument = null;
usageOutputStream = System.out;
initGroups();
}
/**
* Creates a new instance of this argument parser with no arguments that may
* or may not be allowed to have unnamed trailing 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 long arguments should
* be treated in a case-sensitive manner.
* @param allowsTrailingArguments Indicates whether this parser allows
* unnamed trailing arguments to be
* provided.
* @param minTrailingArguments The minimum number of unnamed trailing
* arguments that must be provided. A
* value less than or equal to zero
* indicates that no minimum will be
* enforced.
* @param maxTrailingArguments The maximum number of unnamed trailing
* arguments that may be provided. A
* value less than or equal to zero
* indicates that no maximum will be
* enforced.
* @param trailingArgsDisplayName The display name that should be used
* as a placeholder for unnamed trailing
* arguments in the generated usage
* information.
*/
public ArgumentParser(String mainClassName, Message toolDescription,
boolean longArgumentsCaseSensitive,
boolean allowsTrailingArguments,
int minTrailingArguments, int maxTrailingArguments,
String trailingArgsDisplayName)
{
this.mainClassName = mainClassName;
this.toolDescription = toolDescription;
this.longArgumentsCaseSensitive = longArgumentsCaseSensitive;
this.allowsTrailingArguments = allowsTrailingArguments;
this.minTrailingArguments = minTrailingArguments;
this.maxTrailingArguments = maxTrailingArguments;
this.trailingArgsDisplayName = trailingArgsDisplayName;
argumentList = new LinkedList<Argument>();
argumentMap = new HashMap<String,Argument>();
shortIDMap = new HashMap<Character,Argument>();
longIDMap = new HashMap<String,Argument>();
trailingArguments = new ArrayList<String>();
usageOrVersionDisplayed = false;
versionPresent = false;
rawArguments = null;
usageArgument = null;
usageOutputStream = System.out;
initGroups();
}
/**
* 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 this parser will allow unnamed trailing arguments. These
* will be arguments at the end of the list that are not preceded by either a
* long or short identifier and will need to be manually parsed by the
* application using this parser. Note that once an unnamed trailing argument
* has been identified, all remaining arguments will be classified as such.
*
* @return <CODE>true</CODE> if this parser allows unnamed trailing
* arguments, or <CODE>false</CODE> if it does not.
*/
public boolean allowsTrailingArguments()
{
return allowsTrailingArguments;
}
/**
* Retrieves the minimum number of unnamed trailing arguments that must be
* provided.
*
* @return The minimum number of unnamed trailing arguments that must be
* provided, or a value less than or equal to zero if no minimum will
* be enforced.
*/
public int getMinTrailingArguments()
{
return minTrailingArguments;
}
/**
* Retrieves the maximum number of unnamed trailing arguments that may be
* provided.
*
* @return The maximum number of unnamed trailing arguments that may be
* provided, or a value less than or equal to zero if no maximum will
* be enforced.
*/
public int getMaxTrailingArguments()
{
return maxTrailingArguments;
}
/**
* Retrieves the list of all arguments that have been defined for this
* argument parser.
*
* @return The list of all arguments that have been defined for this argument
* parser.
*/
public LinkedList<Argument> getArgumentList()
{
return argumentList;
}
/**
* Retrieves the argument with the specified name.
*
* @param name The name of the argument to retrieve.
*
* @return The argument with the specified name, or <CODE>null</CODE> if
* there is no such argument.
*/
public Argument getArgument(String name)
{
return argumentMap.get(name);
}
/**
* Retrieves the set of 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 arguments mapped by the short identifier that may be
* used to reference them.
*/
public HashMap<Character,Argument> getArgumentsByShortID()
{
return shortIDMap;
}
/**
* Retrieves the argument with the specified short identifier.
*
* @param shortID The short ID for the argument to retrieve.
*
* @return The argument with the specified short identifier, or
* <CODE>null</CODE> if there is no such argument.
*/
public Argument getArgumentForShortID(Character shortID)
{
return shortIDMap.get(shortID);
}
/**
* Retrieves the set of 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 arguments mapped by the long identifier that may be
* used to reference them.
*/
public HashMap<String,Argument> getArgumentsByLongID()
{
return longIDMap;
}
/**
* Retrieves the argument with the specified long identifier.
*
* @param longID The long identifier of the argument to retrieve.
*
* @return The argument with the specified long identifier, or
* <CODE>null</CODE> if there is no such argument.
*/
public Argument getArgumentForLongID(String longID)
{
return longIDMap.get(longID);
}
/**
* 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;
}
/**
* 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;
}
/**
* Sets the usage group description for the default argument group.
*
* @param description for the default group
*/
public void setDefaultArgumentGroupDescription(Message description)
{
this.defaultArgGroup.setDescription(description);
}
/**
* Sets the usage group description for the LDAP argument group.
*
* @param description for the LDAP group
*/
public void setLdapArgumentGroupDescription(Message description)
{
this.ldapArgGroup.setDescription(description);
}
/**
* Sets the usage group description for the input/output argument group.
*
* @param description for the input/output group
*/
public void setInputOutputArgumentGroupDescription(Message description)
{
this.ioArgGroup.setDescription(description);
}
/**
* Sets the usage group description for the general argument group.
*
* @param description for the general group
*/
public void setGeneralArgumentGroupDescription(Message description)
{
this.generalArgGroup.setDescription(description);
}
/**
* Adds the provided argument to the set of arguments handled by this parser.
*
* @param argument The argument to be added.
*
* @throws ArgumentException If the provided argument conflicts with another
* argument that has already been defined.
*/
public void addArgument(Argument argument)
throws ArgumentException
{
addArgument(argument, null);
}
/**
* Adds the provided argument to the set of arguments handled by this parser
* and puts the argument in the default group.
*
* @param argument The argument to be added.
*
* @throws ArgumentException If the provided argument conflicts with another
* argument that has already been defined.
*/
public void addDefaultArgument(Argument argument)
throws ArgumentException
{
addArgument(argument, defaultArgGroup);
}
/**
* Adds the provided argument to the set of arguments handled by this parser
* and puts the argument in the LDAP connection group.
*
* @param argument The argument to be added.
*
* @throws ArgumentException If the provided argument conflicts with another
* argument that has already been defined.
*/
public void addLdapConnectionArgument(Argument argument)
throws ArgumentException
{
addArgument(argument, ldapArgGroup);
}
/**
* Adds the provided argument to the set of arguments handled by this parser
* and puts the argument in the input/output group.
*
* @param argument The argument to be added.
*
* @throws ArgumentException If the provided argument conflicts with another
* argument that has already been defined.
*/
public void addInputOutputArgument(Argument argument)
throws ArgumentException
{
addArgument(argument, ioArgGroup);
}
/**
* Adds the provided argument to the set of arguments handled by this parser
* and puts the argument in the general group.
*
* @param argument The argument to be added.
*
* @throws ArgumentException If the provided argument conflicts with another
* argument that has already been defined.
*/
public void addGeneralArgument(Argument argument)
throws ArgumentException
{
addArgument(argument, generalArgGroup);
}
/**
* Adds the provided argument to the set of 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
* argument that has already been defined.
*/
public void addArgument(Argument argument, ArgumentGroup group)
throws ArgumentException
{
Character shortID = argument.getShortIdentifier();
if ((shortID != null) && shortIDMap.containsKey(shortID))
{
String conflictingName = shortIDMap.get(shortID).getName();
Message message = ERR_ARGPARSER_DUPLICATE_SHORT_ID.get(
argument.getName(), String.valueOf(shortID), conflictingName);
throw new ArgumentException(message);
}
if ((versionArgument != null) && (shortID != null))
{
if (shortID.equals(versionArgument.getShortIdentifier()))
{
// Update the version argument to not display its short identifier.
try {
versionArgument = new BooleanArgument(
OPTION_LONG_PRODUCT_VERSION,
null,
OPTION_LONG_PRODUCT_VERSION,
INFO_DESCRIPTION_PRODUCT_VERSION.get());
this.generalArgGroup.addArgument(versionArgument);
} catch (ArgumentException e) {
// ignore
}
}
}
String longID = argument.getLongIdentifier();
if (longID != null)
{
if (! longArgumentsCaseSensitive)
{
longID = toLowerCase(longID);
}
if (longIDMap.containsKey(longID))
{
String conflictingName = longIDMap.get(longID).getName();
Message message = ERR_ARGPARSER_DUPLICATE_LONG_ID.get(
argument.getName(), argument.getLongIdentifier(), conflictingName);
throw new ArgumentException(message);
}
}
if (shortID != null)
{
shortIDMap.put(shortID, argument);
}
if (longID != null)
{
longIDMap.put(longID, argument);
}
argumentList.add(argument);
if (group == null) {
group = getStandardGroup(argument);
}
group.addArgument(argument);
argumentGroups.add(group);
}
/**
* Sets the provided argument as one which will automatically trigger the
* output of usage information if it is provided on the command line and no
* further argument validation will be performed. Note that the caller will
* still need to add this argument to the parser with the
* <CODE>addArgument</CODE> 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
* <CODE>parseArguments</CODE> to know that no further processing will be
* required.
*
* @param argument The argument whose presence should automatically
* trigger the display of usage information.
*/
public void setUsageArgument(Argument argument)
{
usageArgument = argument;
usageOutputStream = System.out;
}
/**
* Sets the provided argument as one which will automatically trigger the
* output of usage information if it is provided on the command line and no
* further argument validation will be performed. Note that the caller will
* still need to add this argument to the parser with the
* <CODE>addArgument</CODE> 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
* <CODE>parseArguments</CODE> to know that no further processing will be
* required.
*
* @param argument The argument whose presence should automatically
* trigger the display of 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;
}
/**
* Sets the provided argument which will be used to identify the
* file properties.
*
* @param argument
* The argument which will be used to identify the file
* properties.
*/
public void setFilePropertiesArgument(StringArgument argument)
{
filePropertiesPathArgument= argument;
}
/**
* Sets the provided argument which will be used to identify the
* file properties.
*
* @param argument
* The argument which will be used to indicate if we have to
* look for properties file.
*/
public void setNoPropertiesFileArgument(BooleanArgument argument)
{
noPropertiesFileArgument = argument;
}
/**
* 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_ARGPARSER_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;
boolean inTrailingArgs = false;
int numArguments = rawArguments.length;
for (int i=0; i < numArguments; i++)
{
String arg = rawArguments[i];
if (inTrailingArgs)
{
trailingArguments.add(arg);
if ((maxTrailingArguments > 0) &&
(trailingArguments.size() > maxTrailingArguments))
{
Message message =
ERR_ARGPARSER_TOO_MANY_TRAILING_ARGS.get(maxTrailingArguments);
throw new ArgumentException(message);
}
continue;
}
if (arg.equals("--"))
{
// This is a special indicator that we have reached the end of the named
// arguments and that everything that follows after this should be
// considered trailing arguments.
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_ARGPARSER_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);
}
// Get the argument with the specified name.
Argument a = longIDMap.get(argName);
if (a == null)
{
if (argName.equals(OPTION_LONG_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 version
// information.
usageOrVersionDisplayed = true;
versionPresent = true;
try
{
DirectoryServer.printVersion(usageOutputStream);
} catch (Exception e) {}
return;
}
else
{
// There is no such argument registered.
Message message =
ERR_ARGPARSER_NO_ARGUMENT_WITH_LONG_ID.get(origArgName);
throw new ArgumentException(message);
}
}
else
{
a.setPresent(true);
// If this is the usage argument, then immediately stop and print
// usage information.
if ((usageArgument != null) &&
usageArgument.getName().equals(a.getName()))
{
try
{
getUsage(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_ARGPARSER_NO_VALUE_FOR_ARGUMENT_WITH_LONG_ID.get(
origArgName);
throw new ArgumentException(message);
}
argValue = rawArguments[++i];
}
MessageBuilder invalidReason = new MessageBuilder();
if (! a.valueIsAcceptable(argValue, invalidReason))
{
Message message = ERR_ARGPARSER_VALUE_UNACCEPTABLE_FOR_LONG_ID.get(
argValue, origArgName, 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_ARGPARSER_NOT_MULTIVALUED_FOR_LONG_ID.get(origArgName);
throw new ArgumentException(message);
}
a.addValue(argValue);
}
else
{
if (argValue != null)
{
Message message =
ERR_ARGPARSER_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_ARGPARSER_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.
Argument a = shortIDMap.get(argCharacter);
if (a == null)
{
if (argCharacter == '?')
{
// "-?" will always be interpreted as requesting usage information.
try
{
getUsage(usageOutputStream);
} catch (Exception e) {}
return;
}
else
if ( (argCharacter == OPTION_SHORT_PRODUCT_VERSION)
&&
( ! shortIDMap.containsKey(OPTION_SHORT_PRODUCT_VERSION)))
{
// "-V" will always be interpreted as requesting
// version information except if it's already defined (e.g in
// ldap tools).
usageOrVersionDisplayed = true ;
versionPresent = true;
try
{
DirectoryServer.printVersion(usageOutputStream);
} catch (Exception e) {}
return;
}
else
{
// There is no such argument registered.
Message message = ERR_ARGPARSER_NO_ARGUMENT_WITH_SHORT_ID.get(
String.valueOf(argCharacter));
throw new ArgumentException(message);
}
}
else
{
a.setPresent(true);
// If this is the usage argument, then immediately stop and print
// usage information.
if ((usageArgument != null) &&
usageArgument.getName().equals(a.getName()))
{
try
{
getUsage(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_ARGPARSER_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_ARGPARSER_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_ARGPARSER_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 = shortIDMap.get(c);
if (b == null)
{
// There is no such argument registered.
Message message = ERR_ARGPARSER_NO_ARGUMENT_WITH_SHORT_ID.get(
String.valueOf(argCharacter));
throw new ArgumentException(message);
}
else 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_ARGPARSER_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 ((usageArgument != null) &&
usageArgument.getName().equals(b.getName()))
{
try
{
getUsage(usageOutputStream);
} catch (Exception e) {}
return;
}
}
}
}
}
}
else if (allowsTrailingArguments)
{
// It doesn't start with a dash, so it must be a trailing argument if
// that is acceptable.
inTrailingArgs = true;
trailingArguments.add(arg);
}
else
{
// It doesn't start with a dash and we don't allow trailing arguments,
// so this is illegal.
Message message = ERR_ARGPARSER_DISALLOWED_TRAILING_ARGUMENT.get(arg);
throw new ArgumentException(message);
}
}
// If we allow trailing arguments and there is a minimum number, then make
// sure at least that many were provided.
if (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 of the arguments. For any that were not provided on
// the command line, see if there is an alternate default that can be used.
// For cases where there is not, see that argument is required.
for (Argument a : argumentList)
{
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_ARGPARSER_NO_VALUE_FOR_REQUIRED_ARG.get(a.getName());
throw new ArgumentException(message);
}
}
}
}
/**
* Check if we have a properties file.
*
* @return The properties found in the properties file or null.
* @throws ArgumentException
* If a problem was encountered while parsing the provided
* arguments.
*/
protected Properties checkExternalProperties()
throws ArgumentException
{
// We don't look for properties file.
if ((noPropertiesFileArgument != null)
&& (noPropertiesFileArgument.isPresent()))
{
return null;
}
// Check if we have a properties file argument
if (filePropertiesPathArgument == null)
{
return null;
}
// check if the properties file argument has been set. If not
// look for default location.
String propertiesFilePath = null;
if (filePropertiesPathArgument.isPresent())
{
propertiesFilePath = filePropertiesPathArgument.getValue();
}
else
{
// Check in ~/.opendj directory of the OpenDJ admin first
String userDir = System.getenv("HOME");
if (userDir == null || userDir.length() == 0) {
userDir = System.getProperty("user.home");
}
propertiesFilePath = findPropertiesFile(userDir + File.separator
+ DEFAULT_OPENDS_CONFIG_DIR);
if (propertiesFilePath == null)
{
// check "Opends instance"/config directory
String instanceDir = DirectoryServer.getInstanceRoot();
propertiesFilePath = findPropertiesFile(instanceDir+ File.separator
+ "config");
}
}
// We don't have a properties file location
if (propertiesFilePath == null)
{
return null;
}
// We have a location for the properties file.
Properties argumentProperties = new Properties();
String scriptName = System.getProperty(PROPERTY_SCRIPT_NAME);
try
{
Properties p = new Properties();
FileInputStream fis = new FileInputStream(propertiesFilePath);
p.load(fis);
fis.close();
for (Enumeration<?> e = p.propertyNames(); e.hasMoreElements();)
{
String currentPropertyName = (String) e.nextElement();
String propertyName = currentPropertyName;
// Property name form <script name>.<property name> has the
// precedence to <property name>
if (scriptName != null)
{
if (currentPropertyName.startsWith(scriptName))
{
propertyName = currentPropertyName
.substring(scriptName.length() + 1);
}
else
{
if (p.containsKey(scriptName + "." + currentPropertyName ))
{
continue;
}
}
}
argumentProperties.setProperty(propertyName.toLowerCase(), p
.getProperty(currentPropertyName));
}
}
catch (Exception e)
{
Message message = ERR_ARGPARSER_CANNOT_READ_PROPERTIES_FILE.get(String
.valueOf(propertiesFilePath), getExceptionMessage(e));
throw new ArgumentException(message, e);
}
return argumentProperties;
}
/**
* Get the absolute path of the properties file.
*
* @param directory
* The location in which we should look for properties file
* @return The absolute path of the properties file or null
*/
private String findPropertiesFile(String directory)
{
// Look for the tools properties file
File f = new File(directory,DEFAULT_OPENDS_PROPERTIES_FILE_NAME
+ DEFAULT_OPENDS_PROPERTIES_FILE_EXTENSION);
if (f.exists() && f.canRead())
{
return f.getAbsolutePath();
}
else
{
return null;
}
}
/**
* Appends usage information based on the defined arguments to the
* provided buffer.
*
* @param buffer
* The buffer to which the usage information should be
* appended.
*/
public void getUsage(StringBuilder buffer)
{
usageOrVersionDisplayed = true;
if ((toolDescription != null) && (toolDescription.length() > 0))
{
buffer.append(wrapText(toolDescription.toString(), MAX_LENGTH - 1));
buffer.append(EOL);
buffer.append(EOL);
}
String scriptName = System.getProperty(PROPERTY_SCRIPT_NAME);
if ((scriptName == null) || (scriptName.length() == 0))
{
buffer.append(INFO_ARGPARSER_USAGE_JAVA_CLASSNAME.get(mainClassName));
}
else
{
buffer.append(INFO_ARGPARSER_USAGE_JAVA_SCRIPTNAME.get(scriptName));
}
if (allowsTrailingArguments)
{
if (trailingArgsDisplayName == null)
{
buffer.append(" ");
buffer.append(INFO_ARGPARSER_USAGE_TRAILINGARGS.get());
}
else
{
buffer.append(" ");
buffer.append(trailingArgsDisplayName);
}
}
buffer.append(EOL);
buffer.append(INFO_SUBCMDPARSER_WHERE_OPTIONS_INCLUDE.get());
buffer.append(EOL);
buffer.append(EOL);
Argument helpArgument = null ;
boolean printHeaders = printUsageGroupHeaders();
for (ArgumentGroup argGroup : argumentGroups)
{
if (argGroup.containsArguments() && printHeaders)
{
// 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 this argument is hidden, then skip it.
if (a.isHidden())
{
continue;
}
// Help argument should be printed at the end
if ((usageArgument != null) &&
usageArgument.getName().equals(a.getName()))
{
helpArgument = a ;
continue ;
}
printArgumentUsage(a, buffer);
}
}
if (helpArgument != null)
{
printArgumentUsage(helpArgument, buffer);
}
else
{
buffer.append(EOL);
buffer.append("-?");
buffer.append(EOL);
}
}
/**
* Retrieves a message containing usage information based on the defined
* arguments.
*
* @return A string containing usage information based on the defined
* arguments.
*/
public Message getUsageMessage()
{
StringBuilder buffer = new StringBuilder();
getUsage(buffer);
// TODO: rework getUsage(OutputStream) to work with messages framework
return Message.raw(buffer.toString());
}
/**
* 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()
{
StringBuilder buffer = new StringBuilder();
getUsage(buffer);
return buffer.toString();
}
/**
* Writes usage information based on the defined arguments to the provided
* output stream.
*
* @param outputStream The output stream to which the usage information
* should be written.
*
* @throws IOException If a problem occurs while attempting to write the
* usage information to the provided output stream.
*/
public void getUsage(OutputStream outputStream)
throws IOException
{
StringBuilder buffer = new StringBuilder();
getUsage(buffer);
outputStream.write(buffer.toString().getBytes());
}
/**
* Indicates whether the version or 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;
}
/**
* 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, StringBuilder buffer)
{
// Write a line with the short and/or long identifiers that may be
// used
// for the argument.
final int indentLength = INDENT.length();
Character shortID = a.getShortIdentifier();
String longID = a.getLongIdentifier();
if (shortID != null)
{
int currentLength = buffer.length();
if (usageArgument.getName().equals(a.getName()))
{
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 (usageArgument.getName().equals(a.getName()))
{
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 descMaxLength = MAX_LENGTH - indentLength - 1;
if (description.length() <= descMaxLength)
{
buffer.append(INDENT);
buffer.append(description);
buffer.append(EOL);
}
else
{
String s = description.toString();
while (s.length() > descMaxLength)
{
int spacePos = s.lastIndexOf(' ', descMaxLength);
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(INDENT);
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);
}
}
/**
* Given an argument, returns an appropriate group. Arguments may
* be part of one of the special groups or the default group.
*
* @param argument for which a group is requested
* @return argument group appropriate for <code>argument</code>
*/
protected ArgumentGroup getStandardGroup(Argument argument) {
ArgumentGroup group;
if (isInputOutputArgument(argument)) {
group = ioArgGroup;
} else if (isGeneralArgument(argument)) {
group = generalArgGroup;
} else if (isLdapConnectionArgument(argument)) {
group = ldapArgGroup;
} else {
group = defaultArgGroup;
}
return group;
}
/**
* Indicates whether or not argument group description headers
* should be printed.
*
* @return boolean where true means print the descriptions
*/
protected boolean printUsageGroupHeaders() {
// If there is only a single group then we won't print them.
int groupsContainingArgs = 0;
for (ArgumentGroup argGroup : argumentGroups)
{
if (argGroup.containsNonHiddenArguments())
{
groupsContainingArgs++;
}
}
return groupsContainingArgs > 1;
}
private void initGroups() {
this.argumentGroups = new TreeSet<ArgumentGroup>();
this.argumentGroups.add(defaultArgGroup);
this.argumentGroups.add(ldapArgGroup);
this.argumentGroups.add(generalArgGroup);
this.argumentGroups.add(ioArgGroup);
try {
versionArgument = new BooleanArgument(
OPTION_LONG_PRODUCT_VERSION,
OPTION_SHORT_PRODUCT_VERSION,
OPTION_LONG_PRODUCT_VERSION,
INFO_DESCRIPTION_PRODUCT_VERSION.get());
this.generalArgGroup.addArgument(versionArgument);
} catch (ArgumentException e) {
// ignore
}
}
private boolean isInputOutputArgument(Argument arg) {
boolean io = false;
if (arg != null) {
String longId = arg.getLongIdentifier();
io = OPTION_LONG_VERBOSE.equals(longId) ||
OPTION_LONG_QUIET.equals(longId) ||
OPTION_LONG_NO_PROMPT.equals(longId) ||
OPTION_LONG_PROP_FILE_PATH.equals(longId) ||
OPTION_LONG_NO_PROP_FILE.equals(longId) ||
OPTION_LONG_SCRIPT_FRIENDLY.equals(longId) ||
OPTION_LONG_DONT_WRAP.equals(longId) ||
OPTION_LONG_ENCODING.equals(longId) ||
OPTION_DSCFG_LONG_DISPLAY_EQUIVALENT.equals(longId) ||
OPTION_LONG_EQUIVALENT_COMMAND_FILE_PATH.equals(longId) ||
OPTION_LONG_BATCH_FILE_PATH.equals(longId);
}
return io;
}
private boolean isLdapConnectionArgument(Argument arg) {
boolean ldap = false;
if (arg != null) {
String longId = arg.getLongIdentifier();
ldap = OPTION_LONG_USE_SSL.equals(longId) ||
OPTION_LONG_START_TLS.equals(longId) ||
OPTION_LONG_HOST.equals(longId) ||
OPTION_LONG_PORT.equals(longId) ||
OPTION_LONG_BINDDN.equals(longId) ||
OPTION_LONG_BINDPWD.equals(longId) ||
OPTION_LONG_BINDPWD_FILE.equals(longId) ||
OPTION_LONG_SASLOPTION.equals(longId) ||
OPTION_LONG_TRUSTALL.equals(longId) ||
OPTION_LONG_TRUSTSTOREPATH.equals(longId) ||
OPTION_LONG_TRUSTSTORE_PWD.equals(longId) ||
OPTION_LONG_TRUSTSTORE_PWD_FILE.equals(longId) ||
OPTION_LONG_KEYSTOREPATH.equals(longId) ||
OPTION_LONG_KEYSTORE_PWD.equals(longId) ||
OPTION_LONG_KEYSTORE_PWD_FILE.equals(longId) ||
OPTION_LONG_CERT_NICKNAME.equals(longId) ||
OPTION_LONG_REFERENCED_HOST_NAME.equals(longId) ||
OPTION_LONG_ADMIN_UID.equals(longId) ||
OPTION_LONG_ADMIN_PWD.equals(longId) ||
OPTION_LONG_ADMIN_PWD_FILE.equals(longId) ||
OPTION_LONG_REPORT_AUTHZ_ID.equals(longId) ||
OPTION_LONG_USE_PW_POLICY_CTL.equals(longId) ||
OPTION_LONG_USE_SASL_EXTERNAL.equals(longId) ||
OPTION_LONG_PROTOCOL_VERSION.equals(longId) ||
OPTION_LONG_CONNECT_TIMEOUT.equals(longId);
}
return ldap;
}
private boolean isGeneralArgument(Argument arg) {
boolean general = false;
if (arg != null) {
String longId = arg.getLongIdentifier();
general = OPTION_LONG_HELP.equals(longId) ||
OPTION_LONG_PRODUCT_VERSION.equals(longId);
}
return general;
}
/**
* Returns whether the usage argument was provided or not. This method
* should be called after a call to parseArguments.
* @return <CODE>true</CODE> if the usage argument was provided and
* <CODE>false</CODE> 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 <CODE>true</CODE> if the version argument was provided and
* <CODE>false</CODE> otherwise.
*/
public boolean isVersionArgumentPresent()
{
return versionPresent;
}
/**
* Get the password which has to be used for the command without prompting
* the user. If no password was specified, return null.
*
* @param clearArg
* The password StringArgument argument.
* @param fileArg
* The password FileBased argument.
* @return The password stored into the specified file on by the
* command line argument, or null it if not specified.
*/
public static String getBindPassword(StringArgument clearArg,
FileBasedArgument fileArg)
{
String pwd;
if (clearArg.isPresent())
{
pwd = clearArg.getValue();
}
else
if (fileArg.isPresent())
{
pwd = fileArg.getValue();
}
else
{
pwd = null;
}
return pwd;
}
}