| /* |
| * 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<File> and Set<String> 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; |
| } |
| } |
| } |
| } |
| } |