blob: 83e90e230eaafb87241f8f66a9212cb6567be465 [file] [log] [blame]
/*
* Copyright (C) 2010 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.tradefed.config;
import com.android.tradefed.log.LogUtil.CLog;
import com.android.tradefed.util.ArrayUtil;
import com.android.tradefed.util.MultiMap;
import com.android.tradefed.util.TimeVal;
import com.google.common.base.Objects;
import java.io.File;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
/**
* Populates {@link Option} fields.
* <p/>
* Setting of numeric fields such byte, short, int, long, float, and double fields is supported.
* This includes both unboxed and boxed versions (e.g. int vs Integer). If there is a problem
* setting the argument to match the desired type, a {@link ConfigurationException} is thrown.
* <p/>
* File option fields are supported by simply wrapping the string argument in a File object without
* testing for the existence of the file.
* <p/>
* Parameterized Collection fields such as List&lt;File&gt; and Set&lt;String&gt; are supported as
* long as the parameter type is otherwise supported by the option setter. The collection field
* should be initialized with an appropriate collection instance.
* <p/>
* All fields will be processed, including public, protected, default (package) access, private and
* inherited fields.
* <p/>
*
* ported from dalvik.runner.OptionParser
* @see {@link ArgsOptionParser}
*/
@SuppressWarnings("rawtypes")
public class OptionSetter {
static final String BOOL_FALSE_PREFIX = "no-";
private static final HashMap<Class<?>, Handler> handlers = new HashMap<Class<?>, Handler>();
static final char NAMESPACE_SEPARATOR = ':';
static {
handlers.put(boolean.class, new BooleanHandler());
handlers.put(Boolean.class, new BooleanHandler());
handlers.put(byte.class, new ByteHandler());
handlers.put(Byte.class, new ByteHandler());
handlers.put(short.class, new ShortHandler());
handlers.put(Short.class, new ShortHandler());
handlers.put(int.class, new IntegerHandler());
handlers.put(Integer.class, new IntegerHandler());
handlers.put(long.class, new LongHandler());
handlers.put(Long.class, new LongHandler());
handlers.put(float.class, new FloatHandler());
handlers.put(Float.class, new FloatHandler());
handlers.put(double.class, new DoubleHandler());
handlers.put(Double.class, new DoubleHandler());
handlers.put(String.class, new StringHandler());
handlers.put(File.class, new FileHandler());
handlers.put(TimeVal.class, new TimeValHandler());
}
static class FieldDef {
Object object;
Field field;
Object key;
FieldDef(Object object, Field field, Object key) {
this.object = object;
this.field = field;
this.key = key;
}
public boolean equals(Object obj) {
if (obj == this) {
return true;
}
if (obj instanceof FieldDef) {
FieldDef other = (FieldDef)obj;
return Objects.equal(this.object, other.object) &&
Objects.equal(this.field, other.field) &&
Objects.equal(this.key, other.key);
}
return false;
}
public int hashCode() {
return Objects.hashCode(object, field, key);
}
}
private static Handler getHandler(Type type) throws ConfigurationException {
if (type instanceof ParameterizedType) {
ParameterizedType parameterizedType = (ParameterizedType) type;
Class<?> rawClass = (Class<?>) parameterizedType.getRawType();
if (Collection.class.isAssignableFrom(rawClass)) {
// handle Collection
Type actualType = parameterizedType.getActualTypeArguments()[0];
if (!(actualType instanceof Class)) {
throw new ConfigurationException(
"cannot handle nested parameterized type " + type);
}
return getHandler(actualType);
} else if (Map.class.isAssignableFrom(rawClass) ||
MultiMap.class.isAssignableFrom(rawClass)) {
// handle Map
Type keyType = parameterizedType.getActualTypeArguments()[0];
Type valueType = parameterizedType.getActualTypeArguments()[1];
if (!(keyType instanceof Class)) {
throw new ConfigurationException(
"cannot handle nested parameterized type " + keyType);
} else if (!(valueType instanceof Class)) {
throw new ConfigurationException(
"cannot handle nested parameterized type " + valueType);
}
return new MapHandler(getHandler(keyType), getHandler(valueType));
} else {
throw new ConfigurationException(String.format(
"can't handle parameterized type %s; only Collection, Map, and MultiMap "
+ "are supported", type));
}
}
if (type instanceof Class) {
Class<?> cType = (Class<?>) type;
if (cType.isEnum()) {
return new EnumHandler(cType);
} else if (Collection.class.isAssignableFrom(cType)) {
// could handle by just having a default of treating
// contents as String but consciously decided this
// should be an error
throw new ConfigurationException(String.format(
"Cannot handle non-parameterized collection %s. Use a generic Collection "
+ "to specify a desired element type.", type));
} else if (Map.class.isAssignableFrom(cType)) {
// could handle by just having a default of treating
// contents as String but consciously decided this
// should be an error
throw new ConfigurationException(String.format(
"Cannot handle non-parameterized map %s. Use a generic Map to specify "
+ "desired element types.", type));
} else if (MultiMap.class.isAssignableFrom(cType)) {
// could handle by just having a default of treating
// contents as String but consciously decided this
// should be an error
throw new ConfigurationException(String.format(
"Cannot handle non-parameterized multimap %s. Use a generic MultiMap to "
+ "specify desired element types.", type));
}
return handlers.get(cType);
}
throw new ConfigurationException(String.format("cannot handle unknown field type %s",
type));
}
/**
* Does some magic to distinguish TimeVal long field from normal long fields, then calls
* {@see #getHandler(Type)} in the appropriate manner.
*/
private Handler getHandlerOrTimeVal(Field field, Object optionSource)
throws ConfigurationException {
// Do some magic to distinguish TimeVal long fields from normal long fields
final Option option = field.getAnnotation(Option.class);
if (option == null) {
// Shouldn't happen, but better to check.
throw new ConfigurationException(String.format(
"internal error: @Option annotation for field %s in class %s was " +
"unexpectedly null",
field.getName(), optionSource.getClass().getName()));
}
final Type type = field.getGenericType();
if (option.isTimeVal()) {
// We've got a field that marks itself as a time value. First off, verify that it's
// a compatible type
if (type instanceof Class) {
final Class<?> cType = (Class<?>) type;
if (long.class.equals(cType) || Long.class.equals(cType)) {
// Parse time value and return a Long
return new TimeValLongHandler();
} else if (TimeVal.class.equals(cType)) {
// Parse time value and return a TimeVal object
return new TimeValHandler();
}
}
throw new ConfigurationException(String.format("Only fields of type long, " +
"Long, or TimeVal may be declared as isTimeVal. Field %s has " +
"incompatible type %s.", field.getName(), field.getGenericType()));
} else {
// Note that fields declared as TimeVal (or Generic types with TimeVal parameters) will
// follow this branch, but will still work as expected.
return getHandler(type);
}
}
private final Collection<Object> mOptionSources;
private final Map<String, OptionFieldsForName> mOptionMap;
/**
* Container for the list of option fields with given name.
* <p/>
* Used to enforce constraint that fields with same name can exist in different option sources,
* but not the same option source
*/
private class OptionFieldsForName implements Iterable<Map.Entry<Object, Field>> {
private Map<Object, Field> mSourceFieldMap = new HashMap<Object, Field>();
void addField(String name, Object source, Field field) throws ConfigurationException {
if (size() > 0) {
Handler existingFieldHandler = getHandler(getFirstField().getGenericType());
Handler newFieldHandler = getHandler(field.getGenericType());
if (!existingFieldHandler.equals(newFieldHandler)) {
throw new ConfigurationException(String.format(
"@Option field with name '%s' in class '%s' is defined with a " +
"different type than same option in class '%s'",
name, source.getClass().getName(),
getFirstObject().getClass().getName()));
}
}
if (mSourceFieldMap.put(source, field) != null) {
throw new ConfigurationException(String.format(
"@Option field with name '%s' is defined more than once in class '%s'",
name, source.getClass().getName()));
}
}
public int size() {
return mSourceFieldMap.size();
}
public Field getFirstField() throws ConfigurationException {
if (size() <= 0) {
// should never happen
throw new ConfigurationException("no option fields found");
}
return mSourceFieldMap.values().iterator().next();
}
public Object getFirstObject() throws ConfigurationException {
if (size() <= 0) {
// should never happen
throw new ConfigurationException("no option fields found");
}
return mSourceFieldMap.keySet().iterator().next();
}
@Override
public Iterator<Map.Entry<Object, Field>> iterator() {
return mSourceFieldMap.entrySet().iterator();
}
}
/**
* Constructs a new OptionParser for setting the @Option fields of 'optionSources'.
* @throws ConfigurationException
*/
public OptionSetter(Object... optionSources) throws ConfigurationException {
this(Arrays.asList(optionSources));
}
/**
* Constructs a new OptionParser for setting the @Option fields of 'optionSources'.
* @throws ConfigurationException
*/
public OptionSetter(Collection<Object> optionSources) throws ConfigurationException {
mOptionSources = optionSources;
mOptionMap = makeOptionMap();
}
private OptionFieldsForName fieldsForArg(String name) throws ConfigurationException {
OptionFieldsForName fields = mOptionMap.get(name);
if (fields == null || fields.size() == 0) {
throw new ConfigurationException(String.format("Could not find option with name %s",
name));
}
return fields;
}
/**
* Returns a string describing the type of the field with given name.
*
* @param name the {@link Option} field name
* @return a {@link String} describing the field's type
* @throws ConfigurationException if field could not be found
*/
public String getTypeForOption(String name) throws ConfigurationException {
return fieldsForArg(name).getFirstField().getType().getSimpleName().toLowerCase();
}
/**
* Sets the value for a non-map option.
*
* @param optionName the name of Option to set
* @param valueText the value
* @return A list of {@link FieldDef}s corresponding to each object field that was modified.
* @throws ConfigurationException if Option cannot be found or valueText is wrong type
*/
public List<FieldDef> setOptionValue(String optionName, String valueText)
throws ConfigurationException {
return setOptionValue(optionName, null, valueText);
}
/**
* Sets the value for an option.
*
* @param optionName the name of Option to set
* @param keyText the key for Map options, or null.
* @param valueText the value
* @return A list of {@link FieldDef}s corresponding to each object field that was modified.
* @throws ConfigurationException if Option cannot be found or valueText is wrong type
*/
public List<FieldDef> setOptionValue(String optionName, String keyText, String valueText)
throws ConfigurationException {
List<FieldDef> ret = new ArrayList<>();
// For each of the applicable object fields
final OptionFieldsForName optionFields = fieldsForArg(optionName);
for (Map.Entry<Object, Field> fieldEntry : optionFields) {
// Retrieve an appropriate handler for this field's type
final Object optionSource = fieldEntry.getKey();
final Field field = fieldEntry.getValue();
final Handler handler = getHandlerOrTimeVal(field, optionSource);
// Translate the string value to the actual type of the field
Object value = handler.translate(valueText);
if (value == null) {
String type = field.getType().getSimpleName();
if (handler.isMap()) {
ParameterizedType pType = (ParameterizedType) field.getGenericType();
Type valueType = pType.getActualTypeArguments()[1];
type = ((Class<?>)valueType).getSimpleName().toLowerCase();
}
throw new ConfigurationException(String.format(
"Couldn't convert value '%s' to a %s for option '%s'", valueText, type,
optionName));
}
// For maps, also translate the key value
Object key = null;
if (handler.isMap()) {
key = ((MapHandler)handler).translateKey(keyText);
if (key == null) {
ParameterizedType pType = (ParameterizedType) field.getGenericType();
Type keyType = pType.getActualTypeArguments()[0];
String type = ((Class<?>)keyType).getSimpleName().toLowerCase();
throw new ConfigurationException(String.format(
"Couldn't convert key '%s' to a %s for option '%s'", keyText, type,
optionName));
}
}
// Actually set the field value
if (setFieldValue(optionName, optionSource, field, key, value)) {
ret.add(new FieldDef(optionSource, field, key));
}
}
return ret;
}
/**
* Sets the given {@link Option} field's value.
*
* @param optionName the {@link Option#name()}
* @param optionSource the {@link Object} to set
* @param field the {@link Field}
* @param key the key to an entry in a {@link Map} or {@link MultiMap} field or null.
* @param value the value to set
* @return Whether the field was set.
* @throws ConfigurationException
* @see OptionUpdateRule
*/
@SuppressWarnings("unchecked")
static boolean setFieldValue(String optionName, Object optionSource, Field field, Object key,
Object value) throws ConfigurationException {
boolean fieldWasSet = true;
try {
field.setAccessible(true);
if (Collection.class.isAssignableFrom(field.getType())) {
if (key != null) {
throw new ConfigurationException(String.format(
"key not applicable for Collection field '%s'", field.getName()));
}
Collection collection = (Collection)field.get(optionSource);
if (collection == null) {
throw new ConfigurationException(String.format(
"Unable to add value to field '%s'. Field is null.", field.getName()));
}
if (value instanceof Collection) {
collection.addAll((Collection)value);
} else {
collection.add(value);
}
} else if (Map.class.isAssignableFrom(field.getType())) {
Map map = (Map)field.get(optionSource);
if (map == null) {
throw new ConfigurationException(String.format(
"Unable to add value to field '%s'. Field is null.", field.getName()));
}
if (value instanceof Map) {
if (key != null) {
throw new ConfigurationException(String.format(
"Key not applicable when setting Map field '%s' from map value",
field.getName()));
}
map.putAll((Map)value);
} else {
if (key == null) {
throw new ConfigurationException(String.format(
"Unable to add value to map field '%s'. Key is null.",
field.getName()));
}
map.put(key, value);
}
} else if (MultiMap.class.isAssignableFrom(field.getType())) {
// TODO: see if we can combine this with Map logic above
MultiMap map = (MultiMap)field.get(optionSource);
if (map == null) {
throw new ConfigurationException(String.format(
"Unable to add value to field '%s'. Field is null.", field.getName()));
}
if (value instanceof MultiMap) {
if (key != null) {
throw new ConfigurationException(String.format(
"Key not applicable when setting Map field '%s' from map value",
field.getName()));
}
map.putAll((MultiMap)value);
} else {
if (key == null) {
throw new ConfigurationException(String.format(
"Unable to add value to map field '%s'. Key is null.",
field.getName()));
}
map.put(key, value);
}
} else {
if (key != null) {
throw new ConfigurationException(String.format(
"Key not applicable when setting non-map field '%s'", field.getName()));
}
final Option option = field.getAnnotation(Option.class);
if (option == null) {
// By virtue of us having gotten here, this should never happen. But better
// safe than sorry
throw new ConfigurationException(String.format(
"internal error: @Option annotation for field %s in class %s was " +
"unexpectedly null",
field.getName(), optionSource.getClass().getName()));
}
OptionUpdateRule rule = option.updateRule();
if (rule.shouldUpdate(optionName, optionSource, field, value)) {
field.set(optionSource, value);
} else {
fieldWasSet = false;
}
}
} catch (IllegalAccessException | IllegalArgumentException e) {
throw new ConfigurationException(String.format(
"internal error when setting option '%s'", optionName), e);
}
return fieldWasSet;
}
/**
* Sets the given {@link Option} fields value.
*
* @param optionName the {@link Option#name()}
* @param optionSource the {@link Object} to set
* @param field the {@link Field}
* @param value the value to set
* @throws ConfigurationException
*/
static void setFieldValue(String optionName, Object optionSource, Field field, Object value)
throws ConfigurationException {
setFieldValue(optionName, optionSource, field, null, value);
}
/**
* Cache the available options and report any problems with the options themselves right away.
*
* @return a {@link Map} of {@link Option} field name to {@link OptionField}s
* @throws ConfigurationException if any {@link Option} are incorrectly specified
*/
private Map<String, OptionFieldsForName> makeOptionMap() throws ConfigurationException {
final Map<String, Integer> freqMap = new HashMap<String, Integer>(mOptionSources.size());
final Map<String, OptionFieldsForName> optionMap =
new HashMap<String, OptionFieldsForName>();
for (Object objectSource : mOptionSources) {
final String className = objectSource.getClass().getName();
// Keep track of how many times we've seen this className. This assumes that we
// maintain the optionSources in a universally-knowable order internally (which we do --
// they remain in the order in which they were passed to the constructor). Thus, the
// index can serve as a unique identifier for each instance of className as long as
// other upstream classes use the same 1-based ordered numbering scheme.
Integer index = freqMap.get(className);
index = index == null ? 1 : index + 1;
freqMap.put(className, index);
addOptionsForObject(objectSource, optionMap, index);
}
return optionMap;
}
/**
* Adds all option fields (both declared and inherited) to the <var>optionMap</var> for
* provided <var>optionClass</var>.
*
* @param optionSource
* @param optionMap
* @param index The unique index of this instance of the optionSource class. Should equal the
* number of instances of this class that we've already seen, plus 1.
* @throws ConfigurationException
*/
private void addOptionsForObject(Object optionSource,
Map<String, OptionFieldsForName> optionMap, Integer index)
throws ConfigurationException {
Collection<Field> optionFields = getOptionFieldsForClass(optionSource.getClass());
for (Field field : optionFields) {
final Option option = field.getAnnotation(Option.class);
if (option.name().indexOf(NAMESPACE_SEPARATOR) != -1) {
throw new ConfigurationException(String.format(
"Option name '%s' in class '%s' is invalid. " +
"Option names cannot contain the namespace separator character '%c'",
option.name(), optionSource.getClass().getName(), NAMESPACE_SEPARATOR));
}
// Make sure the source doesn't use GREATEST or LEAST for a non-Comparable field.
final Type type = field.getGenericType();
if ((type instanceof Class) && !(type instanceof ParameterizedType)) {
// Not a parameterized type
if ((option.updateRule() == OptionUpdateRule.GREATEST) ||
(option.updateRule() == OptionUpdateRule.LEAST)) {
Class cType = (Class) type;
if (!(Comparable.class.isAssignableFrom(cType))) {
throw new ConfigurationException(String.format(
"Option '%s' in class '%s' attempts to use updateRule %s with " +
"non-Comparable type '%s'.", option.name(),
optionSource.getClass().getName(), option.updateRule(),
field.getGenericType()));
}
}
// don't allow 'final' for non-Collections
if ((field.getModifiers() & Modifier.FINAL) != 0) {
throw new ConfigurationException(String.format(
"Option '%s' in class '%s' is final and cannot be set", option.name(),
optionSource.getClass().getName()));
}
}
// Allow classes to opt out of the global Option namespace
boolean addToGlobalNamespace = true;
if (optionSource.getClass().isAnnotationPresent(OptionClass.class)) {
final OptionClass classAnnotation = optionSource.getClass().getAnnotation(
OptionClass.class);
addToGlobalNamespace = classAnnotation.global_namespace();
}
if (addToGlobalNamespace) {
addNameToMap(optionMap, optionSource, option.name(), field);
}
addNamespacedOptionToMap(optionMap, optionSource, option.name(), field, index);
if (option.shortName() != Option.NO_SHORT_NAME) {
if (addToGlobalNamespace) {
addNameToMap(optionMap, optionSource, String.valueOf(option.shortName()),
field);
}
addNamespacedOptionToMap(optionMap, optionSource,
String.valueOf(option.shortName()), field, index);
}
if (isBooleanField(field)) {
// add the corresponding "no" option to make boolean false
if (addToGlobalNamespace) {
addNameToMap(optionMap, optionSource, BOOL_FALSE_PREFIX + option.name(), field);
}
addNamespacedOptionToMap(optionMap, optionSource, BOOL_FALSE_PREFIX + option.name(),
field, index);
}
}
}
/**
* Returns the names of all of the {@link Option}s that are marked as {@code mandatory} but
* remain unset.
*
* @return A {@link Collection} of {@link String}s containing the (unqualified) names of unset
* mandatory options.
* @throws ConfigurationException if a field to be checked is inaccessible
*/
protected Collection<String> getUnsetMandatoryOptions() throws ConfigurationException {
Collection<String> unsetOptions = new HashSet<String>();
for (Map.Entry<String, OptionFieldsForName> optionPair : mOptionMap.entrySet()) {
final String optName = optionPair.getKey();
final OptionFieldsForName optionFields = optionPair.getValue();
if (optName.indexOf(NAMESPACE_SEPARATOR) >= 0) {
// Only return unqualified option names
continue;
}
for (Map.Entry<Object, Field> fieldEntry : optionFields) {
final Object obj = fieldEntry.getKey();
final Field field = fieldEntry.getValue();
final Option option = field.getAnnotation(Option.class);
if (option == null) {
continue;
} else if (!option.mandatory()) {
continue;
}
// At this point, we know this is a mandatory field; make sure it's set
field.setAccessible(true);
final Object value;
try {
value = field.get(obj);
} catch (IllegalAccessException e) {
throw new ConfigurationException(String.format("internal error: %s",
e.getMessage()));
}
final String realOptName = String.format("--%s", option.name());
if (value == null) {
unsetOptions.add(realOptName);
} else if (value instanceof Collection) {
Collection c = (Collection) value;
if (c.isEmpty()) {
unsetOptions.add(realOptName);
}
} else if (value instanceof Map) {
Map m = (Map) value;
if (m.isEmpty()) {
unsetOptions.add(realOptName);
}
} else if (value instanceof MultiMap) {
MultiMap m = (MultiMap) value;
if (m.isEmpty()) {
unsetOptions.add(realOptName);
}
}
}
}
return unsetOptions;
}
/**
* Gets a list of all {@link Option} fields (both declared and inherited) for given class.
*
* @param optionClass the {@link Class} to search
* @return a {@link Collection} of fields annotated with {@link Option}
*/
static Collection<Field> getOptionFieldsForClass(final Class<?> optionClass) {
Collection<Field> fieldList = new ArrayList<Field>();
buildOptionFieldsForClass(optionClass, fieldList);
return fieldList;
}
/**
* Recursive method that adds all option fields (both declared and inherited) to the
* <var>optionFields</var> for provided <var>optionClass</var>
*
* @param optionClass
* @param optionFields
*/
private static void buildOptionFieldsForClass(final Class<?> optionClass,
Collection<Field> optionFields) {
for (Field field : optionClass.getDeclaredFields()) {
if (field.isAnnotationPresent(Option.class)) {
optionFields.add(field);
}
}
Class<?> superClass = optionClass.getSuperclass();
if (superClass != null) {
buildOptionFieldsForClass(superClass, optionFields);
}
}
/**
* Return the given {@link Field}'s value as a {@link String}.
*
* @param field the {@link Field}
* @param optionObject the {@link Object} to get field's value from.
* @return the field's value as a {@link String}, or <code>null</code> if field is not set or is
* empty (in case of {@link Collection}s
*/
static String getFieldValueAsString(Field field, Object optionObject) {
Object fieldValue = getFieldValue(field, optionObject);
if (fieldValue == null) {
return null;
}
if (fieldValue instanceof Collection) {
Collection collection = (Collection)fieldValue;
if (collection.isEmpty()) {
return null;
}
} else if (fieldValue instanceof Map) {
Map map = (Map)fieldValue;
if (map.isEmpty()) {
return null;
}
} else if (fieldValue instanceof MultiMap) {
MultiMap multimap = (MultiMap)fieldValue;
if (multimap.isEmpty()) {
return null;
}
}
return fieldValue.toString();
}
/**
* Return the given {@link Field}'s value, handling any exceptions.
*
* @param field the {@link Field}
* @param optionObject the {@link Object} to get field's value from.
* @return the field's value as a {@link Object}, or <code>null</code>
*/
static Object getFieldValue(Field field, Object optionObject) {
try {
field.setAccessible(true);
return field.get(optionObject);
} catch (IllegalArgumentException e) {
CLog.w("Could not read value for field %s in class %s. Reason: %s", field.getName(),
optionObject.getClass().getName(), e);
return null;
} catch (IllegalAccessException e) {
CLog.w("Could not read value for field %s in class %s. Reason: %s", field.getName(),
optionObject.getClass().getName(), e);
return null;
}
}
/**
* Returns the help text describing the valid values for the Enum field.
*
* @param field the {@link Field} to get values for
* @return the appropriate help text, or an empty {@link String} if the field is not an Enum.
*/
static String getEnumFieldValuesAsString(Field field) {
Class<?> type = field.getType();
Object[] vals = type.getEnumConstants();
if (vals == null) {
return "";
}
StringBuilder sb = new StringBuilder(" Valid values: [");
sb.append(ArrayUtil.join(", ", vals));
sb.append("]");
return sb.toString();
}
public boolean isBooleanOption(String name) throws ConfigurationException {
Field field = fieldsForArg(name).getFirstField();
return isBooleanField(field);
}
static boolean isBooleanField(Field field) throws ConfigurationException {
return getHandler(field.getGenericType()).isBoolean();
}
public boolean isMapOption(String name) throws ConfigurationException {
Field field = fieldsForArg(name).getFirstField();
return isMapField(field);
}
static boolean isMapField(Field field) throws ConfigurationException {
return getHandler(field.getGenericType()).isMap();
}
private void addNameToMap(Map<String, OptionFieldsForName> optionMap, Object optionSource,
String name, Field field) throws ConfigurationException {
OptionFieldsForName fields = optionMap.get(name);
if (fields == null) {
fields = new OptionFieldsForName();
optionMap.put(name, fields);
}
fields.addField(name, optionSource, field);
if (getHandler(field.getGenericType()) == null) {
throw new ConfigurationException(String.format(
"Option name '%s' in class '%s' is invalid. Unsupported @Option field type '%s'",
name, optionSource.getClass().getName(), field.getType()));
}
}
/**
* Adds the namespaced versions of the option to the map
*
* @see {@link #makeOptionMap()} for details on the enumeration scheme
*/
private void addNamespacedOptionToMap(Map<String, OptionFieldsForName> optionMap,
Object optionSource, String name, Field field, int index)
throws ConfigurationException {
final String className = optionSource.getClass().getName();
if (optionSource.getClass().isAnnotationPresent(OptionClass.class)) {
final OptionClass classAnnotation = optionSource.getClass().getAnnotation(
OptionClass.class);
addNameToMap(optionMap, optionSource, String.format("%s%c%s", classAnnotation.alias(),
NAMESPACE_SEPARATOR, name), field);
// Allows use of an enumerated namespace, to enable options to map to specific instances
// of a class alias, rather than just to all instances of that particular alias.
// Example option name: alias:2:option-name
addNameToMap(optionMap, optionSource, String.format("%s%c%d%c%s",
classAnnotation.alias(), NAMESPACE_SEPARATOR, index, NAMESPACE_SEPARATOR, name),
field);
}
// Allows use of a className-delimited namespace.
// Example option name: com.fully.qualified.ClassName:option-name
addNameToMap(optionMap, optionSource, String.format("%s%c%s",
className, NAMESPACE_SEPARATOR, name), field);
// Allows use of an enumerated namespace, to enable options to map to specific instances of
// a className, rather than just to all instances of that particular className.
// Example option name: com.fully.qualified.ClassName:2:option-name
addNameToMap(optionMap, optionSource, String.format("%s%c%d%c%s",
className, NAMESPACE_SEPARATOR, index, NAMESPACE_SEPARATOR, name), field);
}
private abstract static class Handler {
// Only BooleanHandler should ever override this.
boolean isBoolean() {
return false;
}
// Only MapHandler should ever override this.
boolean isMap() {
return false;
}
/**
* Returns an object of appropriate type for the given Handle, corresponding to 'valueText'.
* Returns null on failure.
*/
abstract Object translate(String valueText);
}
private static class BooleanHandler extends Handler {
@Override boolean isBoolean() {
return true;
}
@Override
Object translate(String valueText) {
if (valueText.equalsIgnoreCase("true") || valueText.equalsIgnoreCase("yes")) {
return Boolean.TRUE;
} else if (valueText.equalsIgnoreCase("false") || valueText.equalsIgnoreCase("no")) {
return Boolean.FALSE;
}
return null;
}
}
private static class ByteHandler extends Handler {
@Override
Object translate(String valueText) {
try {
return Byte.parseByte(valueText);
} catch (NumberFormatException ex) {
return null;
}
}
}
private static class ShortHandler extends Handler {
@Override
Object translate(String valueText) {
try {
return Short.parseShort(valueText);
} catch (NumberFormatException ex) {
return null;
}
}
}
private static class IntegerHandler extends Handler {
@Override
Object translate(String valueText) {
try {
return Integer.parseInt(valueText);
} catch (NumberFormatException ex) {
return null;
}
}
}
private static class LongHandler extends Handler {
@Override
Object translate(String valueText) {
try {
return Long.parseLong(valueText);
} catch (NumberFormatException ex) {
return null;
}
}
}
private static class TimeValLongHandler extends Handler {
/**
* We parse the string as a time value, and return a {@code long}
*/
@Override
Object translate(String valueText) {
try {
return TimeVal.fromString(valueText);
} catch (NumberFormatException ex) {
return null;
}
}
}
private static class TimeValHandler extends Handler {
/**
* We parse the string as a time value, and return a {@code TimeVal}
*/
@Override
Object translate(String valueText) {
try {
return new TimeVal(valueText);
} catch (NumberFormatException ex) {
return null;
}
}
}
private static class FloatHandler extends Handler {
@Override
Object translate(String valueText) {
try {
return Float.parseFloat(valueText);
} catch (NumberFormatException ex) {
return null;
}
}
}
private static class DoubleHandler extends Handler {
@Override
Object translate(String valueText) {
try {
return Double.parseDouble(valueText);
} catch (NumberFormatException ex) {
return null;
}
}
}
private static class StringHandler extends Handler {
@Override
Object translate(String valueText) {
return valueText;
}
}
private static class FileHandler extends Handler {
@Override
Object translate(String valueText) {
return new File(valueText);
}
}
/**
* A {@see Handler} to handle values for Map fields. The {@code Object} returned is a
* MapEntry
*/
private static class MapHandler extends Handler {
private Handler mKeyHandler;
private Handler mValueHandler;
MapHandler(Handler keyHandler, Handler valueHandler) {
if (keyHandler == null || valueHandler == null) {
throw new NullPointerException();
}
mKeyHandler = keyHandler;
mValueHandler = valueHandler;
}
Handler getKeyHandler() {
return mKeyHandler;
}
Handler getValueHandler() {
return mValueHandler;
}
/**
* {@inheritDoc}
*/
@Override
boolean isMap() {
return true;
}
/**
* {@inheritDoc}
*/
@Override
public int hashCode() {
return Objects.hashCode(MapHandler.class, mKeyHandler, mValueHandler);
}
/**
* Define two {@link MapHandler}s as equivalent if their key and value Handlers are
* respectively equivalent.
* <p />
* {@inheritDoc}
*/
@Override
public boolean equals(Object otherObj) {
if ((otherObj != null) && (otherObj instanceof MapHandler)) {
MapHandler other = (MapHandler) otherObj;
Handler otherKeyHandler = other.getKeyHandler();
Handler otherValueHandler = other.getValueHandler();
return mKeyHandler.equals(otherKeyHandler)
&& mValueHandler.equals(otherValueHandler);
}
return false;
}
/**
* {@inheritDoc}
*/
@Override
Object translate(String valueText) {
return mValueHandler.translate(valueText);
}
Object translateKey(String keyText) {
return mKeyHandler.translate(keyText);
}
}
/**
* A {@link Handler} to handle values for {@link Enum} fields.
*/
private static class EnumHandler extends Handler {
private final Class mEnumType;
EnumHandler(Class<?> enumType) {
mEnumType = enumType;
}
Class<?> getEnumType() {
return mEnumType;
}
/**
* {@inheritDoc}
*/
@Override
public int hashCode() {
return Objects.hashCode(EnumHandler.class, mEnumType);
}
/**
* Define two EnumHandlers as equivalent if their EnumTypes are mutually assignable
* <p />
* {@inheritDoc}
*/
@SuppressWarnings("unchecked")
@Override
public boolean equals(Object otherObj) {
if ((otherObj != null) && (otherObj instanceof EnumHandler)) {
EnumHandler other = (EnumHandler) otherObj;
Class<?> otherType = other.getEnumType();
return mEnumType.isAssignableFrom(otherType)
&& otherType.isAssignableFrom(mEnumType);
}
return false;
}
/**
* {@inheritDoc}
*/
@Override
Object translate(String valueText) {
return translate(valueText, true);
}
@SuppressWarnings("unchecked")
Object translate(String valueText, boolean shouldTryUpperCase) {
try {
return Enum.valueOf(mEnumType, valueText);
} catch (IllegalArgumentException e) {
// Will be thrown if the value can't be mapped back to the enum
if (shouldTryUpperCase) {
// Try to automatically map variable-case strings to uppercase. This is
// reasonable since most Enum constants tend to be uppercase by convention.
return translate(valueText.toUpperCase(Locale.ENGLISH), false);
} else {
return null;
}
}
}
}
}