Home | History | Annotate | Download | only in config
      1 /*
      2  * Copyright (C) 2010 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.android.loganalysis.util.config;
     18 
     19 import com.android.loganalysis.util.ArrayUtil;
     20 import com.google.common.base.Objects;
     21 
     22 import java.io.File;
     23 import java.lang.reflect.Field;
     24 import java.lang.reflect.Modifier;
     25 import java.lang.reflect.ParameterizedType;
     26 import java.lang.reflect.Type;
     27 import java.util.ArrayList;
     28 import java.util.Arrays;
     29 import java.util.Collection;
     30 import java.util.HashMap;
     31 import java.util.HashSet;
     32 import java.util.Iterator;
     33 import java.util.Locale;
     34 import java.util.Map;
     35 
     36 /**
     37  * Populates {@link Option} fields.
     38  * <p/>
     39  * Setting of numeric fields such byte, short, int, long, float, and double fields is supported.
     40  * This includes both unboxed and boxed versions (e.g. int vs Integer). If there is a problem
     41  * setting the argument to match the desired type, a {@link ConfigurationException} is thrown.
     42  * <p/>
     43  * File option fields are supported by simply wrapping the string argument in a File object without
     44  * testing for the existence of the file.
     45  * <p/>
     46  * Parameterized Collection fields such as List<File> and Set<String> are supported as long as the
     47  * parameter type is otherwise supported by the option setter. The collection field should be
     48  * initialized with an appropriate collection instance.
     49  * <p/>
     50  * All fields will be processed, including public, protected, default (package) access, private and
     51  * inherited fields.
     52  * <p/>
     53  *
     54  * ported from dalvik.runner.OptionParser
     55  * @see {@link ArgsOptionParser}
     56  */
     57 //TODO: Use libTF once this is copied over.
     58 @SuppressWarnings("rawtypes")
     59 public class OptionSetter {
     60 
     61     static final String BOOL_FALSE_PREFIX = "no-";
     62     private static final HashMap<Class<?>, Handler> handlers = new HashMap<Class<?>, Handler>();
     63     static final char NAMESPACE_SEPARATOR = ':';
     64 
     65     static {
     66         handlers.put(boolean.class, new BooleanHandler());
     67         handlers.put(Boolean.class, new BooleanHandler());
     68 
     69         handlers.put(byte.class, new ByteHandler());
     70         handlers.put(Byte.class, new ByteHandler());
     71         handlers.put(short.class, new ShortHandler());
     72         handlers.put(Short.class, new ShortHandler());
     73         handlers.put(int.class, new IntegerHandler());
     74         handlers.put(Integer.class, new IntegerHandler());
     75         handlers.put(long.class, new LongHandler());
     76         handlers.put(Long.class, new LongHandler());
     77 
     78         handlers.put(float.class, new FloatHandler());
     79         handlers.put(Float.class, new FloatHandler());
     80         handlers.put(double.class, new DoubleHandler());
     81         handlers.put(Double.class, new DoubleHandler());
     82 
     83         handlers.put(String.class, new StringHandler());
     84         handlers.put(File.class, new FileHandler());
     85     }
     86 
     87     private static Handler getHandler(Type type) throws ConfigurationException {
     88         if (type instanceof ParameterizedType) {
     89             ParameterizedType parameterizedType = (ParameterizedType) type;
     90             Class<?> rawClass = (Class<?>) parameterizedType.getRawType();
     91             if (Collection.class.isAssignableFrom(rawClass)) {
     92                 // handle Collection
     93                 Type actualType = parameterizedType.getActualTypeArguments()[0];
     94                 if (!(actualType instanceof Class)) {
     95                     throw new ConfigurationException(
     96                             "cannot handle nested parameterized type " + type);
     97                 }
     98                 return getHandler(actualType);
     99             } else if (Map.class.isAssignableFrom(rawClass)) {
    100                 // handle Map
    101                 Type keyType = parameterizedType.getActualTypeArguments()[0];
    102                 Type valueType = parameterizedType.getActualTypeArguments()[1];
    103                 if (!(keyType instanceof Class)) {
    104                     throw new ConfigurationException(
    105                             "cannot handle nested parameterized type " + keyType);
    106                 } else if (!(valueType instanceof Class)) {
    107                     throw new ConfigurationException(
    108                             "cannot handle nested parameterized type " + valueType);
    109                 }
    110 
    111                 return new MapHandler(getHandler(keyType), getHandler(valueType));
    112             } else {
    113                 throw new ConfigurationException(String.format(
    114                         "can't handle parameterized type %s; only Collection and Map are supported",
    115                         type));
    116             }
    117         }
    118         if (type instanceof Class) {
    119             Class<?> cType = (Class<?>) type;
    120 
    121             if (cType.isEnum()) {
    122                 return new EnumHandler(cType);
    123             } else if (Collection.class.isAssignableFrom(cType)) {
    124                 // could handle by just having a default of treating
    125                 // contents as String but consciously decided this
    126                 // should be an error
    127                 throw new ConfigurationException(String.format(
    128                         "Cannot handle non-parameterized collection %s.  Use a generic Collection "
    129                         + "to specify a desired element type.", type));
    130             } else if (Map.class.isAssignableFrom(cType)) {
    131                 // could handle by just having a default of treating
    132                 // contents as String but consciously decided this
    133                 // should be an error
    134                 throw new ConfigurationException(String.format(
    135                         "Cannot handle non-parameterized map %s.  Use a generic Map to specify "
    136                         + "desired element types.", type));
    137             }
    138             return handlers.get(cType);
    139         }
    140         throw new ConfigurationException(String.format("cannot handle unknown field type %s",
    141                 type));
    142     }
    143 
    144     private final Collection<Object> mOptionSources;
    145     private final Map<String, OptionFieldsForName> mOptionMap;
    146 
    147     /**
    148      * Container for the list of option fields with given name.
    149      * <p/>
    150      * Used to enforce constraint that fields with same name can exist in different option sources,
    151      * but not the same option source
    152      */
    153     private class OptionFieldsForName implements Iterable<Map.Entry<Object, Field>> {
    154 
    155         private Map<Object, Field> mSourceFieldMap = new HashMap<Object, Field>();
    156 
    157         void addField(String name, Object source, Field field) throws ConfigurationException {
    158             if (size() > 0) {
    159                 Handler existingFieldHandler = getHandler(getFirstField().getGenericType());
    160                 Handler newFieldHandler = getHandler(field.getGenericType());
    161                 if (!existingFieldHandler.equals(newFieldHandler)) {
    162                     throw new ConfigurationException(String.format(
    163                             "@Option field with name '%s' in class '%s' is defined with a " +
    164                             "different type than same option in class '%s'",
    165                             name, source.getClass().getName(),
    166                             getFirstObject().getClass().getName()));
    167                 }
    168             }
    169             if (mSourceFieldMap.put(source, field) != null) {
    170                 throw new ConfigurationException(String.format(
    171                         "@Option field with name '%s' is defined more than once in class '%s'",
    172                         name, source.getClass().getName()));
    173             }
    174         }
    175 
    176         public int size() {
    177             return mSourceFieldMap.size();
    178         }
    179 
    180         public Field getFirstField() throws ConfigurationException {
    181             if (size() <= 0) {
    182                 // should never happen
    183                 throw new ConfigurationException("no option fields found");
    184             }
    185             return mSourceFieldMap.values().iterator().next();
    186         }
    187 
    188         public Object getFirstObject() throws ConfigurationException {
    189             if (size() <= 0) {
    190                 // should never happen
    191                 throw new ConfigurationException("no option fields found");
    192             }
    193             return mSourceFieldMap.keySet().iterator().next();
    194         }
    195 
    196         @Override
    197         public Iterator<Map.Entry<Object, Field>> iterator() {
    198             return mSourceFieldMap.entrySet().iterator();
    199         }
    200     }
    201 
    202     /**
    203      * Constructs a new OptionParser for setting the @Option fields of 'optionSources'.
    204      * @throws ConfigurationException
    205      */
    206     public OptionSetter(Object... optionSources) throws ConfigurationException {
    207         this(Arrays.asList(optionSources));
    208     }
    209 
    210     /**
    211      * Constructs a new OptionParser for setting the @Option fields of 'optionSources'.
    212      * @throws ConfigurationException
    213      */
    214     public OptionSetter(Collection<Object> optionSources) throws ConfigurationException {
    215         mOptionSources = optionSources;
    216         mOptionMap = makeOptionMap();
    217     }
    218 
    219     private OptionFieldsForName fieldsForArg(String name) throws ConfigurationException {
    220         OptionFieldsForName fields = mOptionMap.get(name);
    221         if (fields == null || fields.size() == 0) {
    222             throw new ConfigurationException(String.format("Could not find option with name %s",
    223                     name));
    224         }
    225         return fields;
    226     }
    227 
    228     /**
    229      * Returns a string describing the type of the field with given name.
    230      *
    231      * @param name the {@link Option} field name
    232      * @return a {@link String} describing the field's type
    233      * @throws ConfigurationException if field could not be found
    234      */
    235     public String getTypeForOption(String name) throws ConfigurationException {
    236         return fieldsForArg(name).getFirstField().getType().getSimpleName().toLowerCase();
    237     }
    238 
    239     /**
    240      * Sets the value for an option.
    241      * @param optionName the name of Option to set
    242      * @param valueText the value
    243      * @throws ConfigurationException if Option cannot be found or valueText is wrong type
    244      */
    245     public void setOptionValue(String optionName, String valueText) throws ConfigurationException {
    246         OptionFieldsForName optionFields = fieldsForArg(optionName);
    247         for (Map.Entry<Object, Field> fieldEntry : optionFields) {
    248 
    249             Object optionSource = fieldEntry.getKey();
    250             Field field = fieldEntry.getValue();
    251             Handler handler = getHandler(field.getGenericType());
    252             Object value = handler.translate(valueText);
    253             if (value == null) {
    254                 final String type = field.getType().getSimpleName();
    255                 throw new ConfigurationException(
    256                         String.format("Couldn't convert '%s' to a %s for option '%s'", valueText,
    257                                 type, optionName));
    258             }
    259             setFieldValue(optionName, optionSource, field, value);
    260         }
    261     }
    262 
    263     /**
    264      * Sets the given {@link Option} fields value.
    265      *
    266      * @param optionName the {@link Option#name()}
    267      * @param optionSource the {@link Object} to set
    268      * @param field the {@link Field}
    269      * @param value the value to set
    270      * @throws ConfigurationException
    271      */
    272     @SuppressWarnings("unchecked")
    273     static void setFieldValue(String optionName, Object optionSource, Field field, Object value)
    274             throws ConfigurationException {
    275         try {
    276             field.setAccessible(true);
    277             if (Collection.class.isAssignableFrom(field.getType())) {
    278                 Collection collection = (Collection)field.get(optionSource);
    279                 if (collection == null) {
    280                     throw new ConfigurationException(String.format(
    281                             "internal error: no storage allocated for field '%s' (used for " +
    282                             "option '%s') in class '%s'",
    283                             field.getName(), optionName, optionSource.getClass().getName()));
    284                 }
    285                 if (value instanceof Collection) {
    286                     collection.addAll((Collection)value);
    287                 } else {
    288                     collection.add(value);
    289                 }
    290             } else if (Map.class.isAssignableFrom(field.getType())) {
    291                 Map map = (Map)field.get(optionSource);
    292                 if (map == null) {
    293                     throw new ConfigurationException(String.format(
    294                             "internal error: no storage allocated for field '%s' (used for " +
    295                             "option '%s') in class '%s'",
    296                             field.getName(), optionName, optionSource.getClass().getName()));
    297                 }
    298                 if (value instanceof Map) {
    299                     map.putAll((Map)value);
    300                 } else {
    301                     throw new ConfigurationException(String.format(
    302                             "internal error: value provided for field '%s' is not a map (used " +
    303                             "for option '%s') in class '%s'",
    304                             field.getName(), optionName, optionSource.getClass().getName()));
    305                 }
    306             } else {
    307                 final Option option = field.getAnnotation(Option.class);
    308                 if (option == null) {
    309                     // By virtue of us having gotten here, this should never happen.  But better
    310                     // safe than sorry
    311                     throw new ConfigurationException(String.format(
    312                             "internal error: @Option annotation for field %s in class %s was " +
    313                             "unexpectedly null",
    314                             field.getName(), optionSource.getClass().getName()));
    315                 }
    316                 OptionUpdateRule rule = option.updateRule();
    317                 field.set(optionSource, rule.update(optionName, optionSource, field, value));
    318             }
    319         } catch (IllegalAccessException e) {
    320             throw new ConfigurationException(String.format(
    321                     "internal error when setting option '%s'", optionName), e);
    322         } catch (IllegalArgumentException e) {
    323             throw new ConfigurationException(String.format(
    324                     "internal error when setting option '%s'", optionName), e);
    325         }
    326     }
    327 
    328     /**
    329      * Sets the key and value for a Map option.
    330      * @param optionName the name of Option to set
    331      * @param keyText the key, if applicable.  Will be ignored for non-Map fields
    332      * @param valueText the value
    333      * @throws ConfigurationException if Option cannot be found or valueText is wrong type
    334      */
    335     @SuppressWarnings("unchecked")
    336     public void setOptionMapValue(String optionName, String keyText, String valueText)
    337             throws ConfigurationException {
    338         // FIXME: try to unify code paths with setOptionValue
    339         OptionFieldsForName optionFields = fieldsForArg(optionName);
    340         for (Map.Entry<Object, Field> fieldEntry : optionFields) {
    341 
    342             Object optionSource = fieldEntry.getKey();
    343             Field field = fieldEntry.getValue();
    344             Handler handler = getHandler(field.getGenericType());
    345             if (handler == null || !(handler instanceof MapHandler)) {
    346                 throw new ConfigurationException("Not a map!");
    347             }
    348 
    349             MapEntry pair = null;
    350             try {
    351                 pair = ((MapHandler) handler).translate(keyText, valueText);
    352                 if (pair == null) {
    353                     throw new IllegalArgumentException();
    354                 }
    355             } catch (IllegalArgumentException e) {
    356                 ParameterizedType pType = (ParameterizedType) field.getGenericType();
    357                 Type keyType = pType.getActualTypeArguments()[0];
    358                 Type valueType = pType.getActualTypeArguments()[1];
    359 
    360                 String keyTypeName = ((Class<?>)keyType).getSimpleName().toLowerCase();
    361                 String valueTypeName = ((Class<?>)valueType).getSimpleName().toLowerCase();
    362 
    363                 String message = "";
    364                 if (e.getMessage().contains("key")) {
    365                     message = String.format(
    366                             "Couldn't convert '%s' to a %s for the key of mapoption '%s'",
    367                             keyText, keyTypeName, optionName);
    368                 } else if (e.getMessage().contains("value")) {
    369                     message = String.format(
    370                             "Couldn't convert '%s' to a %s for the value of mapoption '%s'",
    371                             valueText, valueTypeName, optionName);
    372                 } else {
    373                     message = String.format("Failed to convert key '%s' to type %s and/or " +
    374                             "value '%s' to type %s for mapoption '%s'",
    375                             keyText, keyTypeName, valueText, valueTypeName, optionName);
    376                 }
    377                 throw new ConfigurationException(message);
    378             }
    379             try {
    380                 field.setAccessible(true);
    381                 if (!Map.class.isAssignableFrom(field.getType())) {
    382                     throw new ConfigurationException(String.format(
    383                             "internal error: not a map field!"));
    384                 }
    385                 Map map = (Map)field.get(optionSource);
    386                 if (map == null) {
    387                     throw new ConfigurationException(String.format(
    388                             "internal error: no storage allocated for field '%s' (used for " +
    389                             "option '%s') in class '%s'",
    390                             field.getName(), optionName, optionSource.getClass().getName()));
    391                 }
    392                 map.put(pair.mKey, pair.mValue);
    393             } catch (IllegalAccessException e) {
    394                 throw new ConfigurationException(String.format(
    395                         "internal error when setting option '%s'", optionName), e);
    396             }
    397         }
    398     }
    399 
    400     /**
    401      * Cache the available options and report any problems with the options themselves right away.
    402      *
    403      * @return a {@link Map} of {@link Option} field name to {@link OptionField}s
    404      * @throws ConfigurationException if any {@link Option} are incorrectly specified
    405      */
    406     private Map<String, OptionFieldsForName> makeOptionMap() throws ConfigurationException {
    407         final Map<String, Integer> freqMap = new HashMap<String, Integer>(mOptionSources.size());
    408         final Map<String, OptionFieldsForName> optionMap =
    409                 new HashMap<String, OptionFieldsForName>();
    410         for (Object objectSource : mOptionSources) {
    411             final String className = objectSource.getClass().getName();
    412 
    413             // Keep track of how many times we've seen this className.  This assumes that we
    414             // maintain the optionSources in a universally-knowable order internally (which we do --
    415             // they remain in the order in which they were passed to the constructor).  Thus, the
    416             // index can serve as a unique identifier for each instance of className as long as
    417             // other upstream classes use the same 1-based ordered numbering scheme.
    418             Integer index = freqMap.get(className);
    419             index = index == null ? 1 : index + 1;
    420             freqMap.put(className, index);
    421 
    422             addOptionsForObject(objectSource, optionMap, index);
    423         }
    424         return optionMap;
    425     }
    426 
    427     /**
    428      * Adds all option fields (both declared and inherited) to the <var>optionMap</var> for
    429      * provided <var>optionClass</var>.
    430      *
    431      * @param optionSource
    432      * @param optionMap
    433      * @param index The unique index of this instance of the optionSource class.  Should equal the
    434      *              number of instances of this class that we've already seen, plus 1.
    435      * @throws ConfigurationException
    436      */
    437     private void addOptionsForObject(Object optionSource,
    438             Map<String, OptionFieldsForName> optionMap, Integer index)
    439             throws ConfigurationException {
    440         Collection<Field> optionFields = getOptionFieldsForClass(optionSource.getClass());
    441         for (Field field : optionFields) {
    442             final Option option = field.getAnnotation(Option.class);
    443             if (option.name().indexOf(NAMESPACE_SEPARATOR) != -1) {
    444                 throw new ConfigurationException(String.format(
    445                         "Option name '%s' in class '%s' is invalid. " +
    446                         "Option names cannot contain the namespace separator character '%c'",
    447                         option.name(), optionSource.getClass().getName(), NAMESPACE_SEPARATOR));
    448             }
    449 
    450             // Make sure the source doesn't use GREATEST or LEAST for a non-Comparable field.
    451             final Type type = field.getGenericType();
    452             if ((type instanceof Class) && !(type instanceof ParameterizedType)) {
    453                 // Not a parameterized type
    454                 if ((option.updateRule() == OptionUpdateRule.GREATEST) ||
    455                         (option.updateRule() == OptionUpdateRule.LEAST)) {
    456                     Class cType = (Class) type;
    457                     if (!(Comparable.class.isAssignableFrom(cType))) {
    458                         throw new ConfigurationException(String.format(
    459                                 "Option '%s' in class '%s' attempts to use updateRule %s with " +
    460                                 "non-Comparable type '%s'.", option.name(),
    461                                 optionSource.getClass().getName(), option.updateRule(),
    462                                 field.getGenericType()));
    463                     }
    464                 }
    465 
    466                 // don't allow 'final' for non-Collections
    467                 if ((field.getModifiers() & Modifier.FINAL) != 0) {
    468                     throw new ConfigurationException(String.format(
    469                             "Option '%s' in class '%s' is final and cannot be set", option.name(),
    470                             optionSource.getClass().getName()));
    471                 }
    472             }
    473 
    474             // Allow classes to opt out of the global Option namespace
    475             boolean addToGlobalNamespace = true;
    476             if (optionSource.getClass().isAnnotationPresent(OptionClass.class)) {
    477                 final OptionClass classAnnotation = optionSource.getClass().getAnnotation(
    478                         OptionClass.class);
    479                 addToGlobalNamespace = classAnnotation.global_namespace();
    480             }
    481 
    482             if (addToGlobalNamespace) {
    483                 addNameToMap(optionMap, optionSource, option.name(), field);
    484             }
    485             addNamespacedOptionToMap(optionMap, optionSource, option.name(), field, index);
    486             if (option.shortName() != Option.NO_SHORT_NAME) {
    487                 if (addToGlobalNamespace) {
    488                     addNameToMap(optionMap, optionSource, String.valueOf(option.shortName()),
    489                             field);
    490                 }
    491                 addNamespacedOptionToMap(optionMap, optionSource,
    492                         String.valueOf(option.shortName()), field, index);
    493             }
    494             if (isBooleanField(field)) {
    495                 // add the corresponding "no" option to make boolean false
    496                 if (addToGlobalNamespace) {
    497                     addNameToMap(optionMap, optionSource, BOOL_FALSE_PREFIX + option.name(), field);
    498                 }
    499                 addNamespacedOptionToMap(optionMap, optionSource, BOOL_FALSE_PREFIX + option.name(),
    500                         field, index);
    501             }
    502         }
    503     }
    504 
    505     /**
    506      * Returns the names of all of the {@link Option}s that are marked as {@code mandatory} but
    507      * remain unset.
    508      *
    509      * @return A {@link Collection} of {@link String}s containing the (unqualified) names of unset
    510      *         mandatory options.
    511      * @throws ConfigurationException if a field to be checked is inaccessible
    512      */
    513     protected Collection<String> getUnsetMandatoryOptions() throws ConfigurationException {
    514         Collection<String> unsetOptions = new HashSet<String>();
    515         for (Map.Entry<String, OptionFieldsForName> optionPair : mOptionMap.entrySet()) {
    516             final String optName = optionPair.getKey();
    517             final OptionFieldsForName optionFields = optionPair.getValue();
    518             if (optName.indexOf(NAMESPACE_SEPARATOR) >= 0) {
    519                 // Only return unqualified option names
    520                 continue;
    521             }
    522 
    523             for (Map.Entry<Object, Field> fieldEntry : optionFields) {
    524                 final Object obj = fieldEntry.getKey();
    525                 final Field field = fieldEntry.getValue();
    526                 final Option option = field.getAnnotation(Option.class);
    527                 if (option == null) {
    528                     continue;
    529                 } else if (!option.mandatory()) {
    530                     continue;
    531                 }
    532 
    533                 // At this point, we know this is a mandatory field; make sure it's set
    534                 field.setAccessible(true);
    535                 final Object value;
    536                 try {
    537                     value = field.get(obj);
    538                 } catch (IllegalAccessException e) {
    539                     throw new ConfigurationException(String.format("internal error: %s",
    540                             e.getMessage()));
    541                 }
    542 
    543                 final String realOptName = String.format("--%s", option.name());
    544                 if (value == null) {
    545                     unsetOptions.add(realOptName);
    546                 } else if (value instanceof Collection) {
    547                     Collection c = (Collection) value;
    548                     if (c.isEmpty()) {
    549                         unsetOptions.add(realOptName);
    550                     }
    551                 } else if (value instanceof Map) {
    552                     Map m = (Map) value;
    553                     if (m.isEmpty()) {
    554                         unsetOptions.add(realOptName);
    555                     }
    556                 }
    557             }
    558         }
    559         return unsetOptions;
    560     }
    561 
    562     /**
    563      * Gets a list of all {@link Option} fields (both declared and inherited) for given class.
    564      *
    565      * @param optionClass the {@link Class} to search
    566      * @return a {@link Collection} of fields annotated with {@link Option}
    567      */
    568     static Collection<Field> getOptionFieldsForClass(final Class<?> optionClass) {
    569         Collection<Field> fieldList = new ArrayList<Field>();
    570         buildOptionFieldsForClass(optionClass, fieldList);
    571         return fieldList;
    572     }
    573 
    574     /**
    575      * Recursive method that adds all option fields (both declared and inherited) to the
    576      * <var>optionFields</var> for provided <var>optionClass</var>
    577      *
    578      * @param optionClass
    579      * @param optionFields
    580      */
    581     private static void buildOptionFieldsForClass(final Class<?> optionClass,
    582             Collection<Field> optionFields) {
    583         for (Field field : optionClass.getDeclaredFields()) {
    584             if (field.isAnnotationPresent(Option.class)) {
    585                 optionFields.add(field);
    586             }
    587         }
    588         Class<?> superClass = optionClass.getSuperclass();
    589         if (superClass != null) {
    590             buildOptionFieldsForClass(superClass, optionFields);
    591         }
    592     }
    593 
    594     /**
    595      * Return the given {@link Field}'s value as a {@link String}.
    596      *
    597      * @param field the {@link Field}
    598      * @param optionObject the {@link Object} to get field's value from.
    599      * @return the field's value as a {@link String}, or <code>null</code> if field is not set or is
    600      *         empty (in case of {@link Collection}s
    601      */
    602     static String getFieldValueAsString(Field field, Object optionObject) {
    603         Object fieldValue = getFieldValue(field, optionObject);
    604         if (fieldValue == null) {
    605             return null;
    606         }
    607         if (fieldValue instanceof Collection) {
    608             Collection collection = (Collection)fieldValue;
    609             if (collection.isEmpty()) {
    610                 return null;
    611             }
    612         } else if (fieldValue instanceof Map) {
    613             Map map = (Map)fieldValue;
    614             if (map.isEmpty()) {
    615                 return null;
    616             }
    617         }
    618         return fieldValue.toString();
    619     }
    620 
    621     /**
    622      * Return the given {@link Field}'s value, handling any exceptions.
    623      *
    624      * @param field the {@link Field}
    625      * @param optionObject the {@link Object} to get field's value from.
    626      * @return the field's value as a {@link Object}, or <code>null</code>
    627      */
    628     static Object getFieldValue(Field field, Object optionObject) {
    629         try {
    630             field.setAccessible(true);
    631             return field.get(optionObject);
    632         } catch (IllegalArgumentException e) {
    633             return null;
    634         } catch (IllegalAccessException e) {
    635             return null;
    636         }
    637     }
    638 
    639     /**
    640      * Returns the help text describing the valid values for the Enum field.
    641      *
    642      * @param field the {@link Field} to get values for
    643      * @return the appropriate help text, or an empty {@link String} if the field is not an Enum.
    644      */
    645     static String getEnumFieldValuesAsString(Field field) {
    646         Class<?> type = field.getType();
    647         Object[] vals = type.getEnumConstants();
    648         if (vals == null) {
    649             return "";
    650         }
    651 
    652         StringBuilder sb = new StringBuilder(" Valid values: [");
    653         sb.append(ArrayUtil.join(", ", vals));
    654         sb.append("]");
    655         return sb.toString();
    656     }
    657 
    658     public boolean isBooleanOption(String name) throws ConfigurationException {
    659         Field field = fieldsForArg(name).getFirstField();
    660         return isBooleanField(field);
    661     }
    662 
    663     static boolean isBooleanField(Field field) throws ConfigurationException {
    664         return getHandler(field.getGenericType()).isBoolean();
    665     }
    666 
    667     public boolean isMapOption(String name) throws ConfigurationException {
    668         Field field = fieldsForArg(name).getFirstField();
    669         return isMapField(field);
    670     }
    671 
    672     static boolean isMapField(Field field) throws ConfigurationException {
    673         return getHandler(field.getGenericType()).isMap();
    674     }
    675 
    676     private void addNameToMap(Map<String, OptionFieldsForName> optionMap, Object optionSource,
    677             String name, Field field) throws ConfigurationException {
    678         OptionFieldsForName fields = optionMap.get(name);
    679         if (fields == null) {
    680             fields = new OptionFieldsForName();
    681             optionMap.put(name, fields);
    682         }
    683 
    684         fields.addField(name, optionSource, field);
    685         if (getHandler(field.getGenericType()) == null) {
    686             throw new ConfigurationException(String.format(
    687                     "Option name '%s' in class '%s' is invalid. Unsupported @Option field type '%s'",
    688                     name, optionSource.getClass().getName(), field.getType()));
    689         }
    690     }
    691 
    692     /**
    693      * Adds the namespaced versions of the option to the map
    694      *
    695      * @see {@link #makeOptionMap()} for details on the enumeration scheme
    696      */
    697     private void addNamespacedOptionToMap(Map<String, OptionFieldsForName> optionMap,
    698             Object optionSource, String name, Field field, int index)
    699             throws ConfigurationException {
    700         final String className = optionSource.getClass().getName();
    701 
    702         if (optionSource.getClass().isAnnotationPresent(OptionClass.class)) {
    703             final OptionClass classAnnotation = optionSource.getClass().getAnnotation(
    704                     OptionClass.class);
    705             addNameToMap(optionMap, optionSource, String.format("%s%c%s", classAnnotation.alias(),
    706                     NAMESPACE_SEPARATOR, name), field);
    707 
    708             // Allows use of an enumerated namespace, to enable options to map to specific instances
    709             // of a class alias, rather than just to all instances of that particular alias.
    710             // Example option name: alias:2:option-name
    711             addNameToMap(optionMap, optionSource, String.format("%s%c%d%c%s",
    712                     classAnnotation.alias(), NAMESPACE_SEPARATOR, index, NAMESPACE_SEPARATOR, name),
    713                     field);
    714         }
    715 
    716         // Allows use of a className-delimited namespace.
    717         // Example option name: com.fully.qualified.ClassName:option-name
    718         addNameToMap(optionMap, optionSource, String.format("%s%c%s",
    719                 className, NAMESPACE_SEPARATOR, name), field);
    720 
    721         // Allows use of an enumerated namespace, to enable options to map to specific instances of
    722         // a className, rather than just to all instances of that particular className.
    723         // Example option name: com.fully.qualified.ClassName:2:option-name
    724         addNameToMap(optionMap, optionSource, String.format("%s%c%d%c%s",
    725                 className, NAMESPACE_SEPARATOR, index, NAMESPACE_SEPARATOR, name), field);
    726     }
    727 
    728     private abstract static class Handler {
    729         // Only BooleanHandler should ever override this.
    730         boolean isBoolean() {
    731             return false;
    732         }
    733 
    734         // Only MapHandler should ever override this.
    735         boolean isMap() {
    736             return false;
    737         }
    738 
    739         /**
    740          * Returns an object of appropriate type for the given Handle, corresponding to 'valueText'.
    741          * Returns null on failure.
    742          */
    743         abstract Object translate(String valueText);
    744     }
    745 
    746     private static class BooleanHandler extends Handler {
    747         @Override boolean isBoolean() {
    748             return true;
    749         }
    750 
    751         @Override
    752         Object translate(String valueText) {
    753             if (valueText.equalsIgnoreCase("true") || valueText.equalsIgnoreCase("yes")) {
    754                 return Boolean.TRUE;
    755             } else if (valueText.equalsIgnoreCase("false") || valueText.equalsIgnoreCase("no")) {
    756                 return Boolean.FALSE;
    757             }
    758             return null;
    759         }
    760     }
    761 
    762     private static class ByteHandler extends Handler {
    763         @Override
    764         Object translate(String valueText) {
    765             try {
    766                 return Byte.parseByte(valueText);
    767             } catch (NumberFormatException ex) {
    768                 return null;
    769             }
    770         }
    771     }
    772 
    773     private static class ShortHandler extends Handler {
    774         @Override
    775         Object translate(String valueText) {
    776             try {
    777                 return Short.parseShort(valueText);
    778             } catch (NumberFormatException ex) {
    779                 return null;
    780             }
    781         }
    782     }
    783 
    784     private static class IntegerHandler extends Handler {
    785         @Override
    786         Object translate(String valueText) {
    787             try {
    788                 return Integer.parseInt(valueText);
    789             } catch (NumberFormatException ex) {
    790                 return null;
    791             }
    792         }
    793     }
    794 
    795     private static class LongHandler extends Handler {
    796         @Override
    797         Object translate(String valueText) {
    798             try {
    799                 return Long.parseLong(valueText);
    800             } catch (NumberFormatException ex) {
    801                 return null;
    802             }
    803         }
    804     }
    805 
    806     private static class FloatHandler extends Handler {
    807         @Override
    808         Object translate(String valueText) {
    809             try {
    810                 return Float.parseFloat(valueText);
    811             } catch (NumberFormatException ex) {
    812                 return null;
    813             }
    814         }
    815     }
    816 
    817     private static class DoubleHandler extends Handler {
    818         @Override
    819         Object translate(String valueText) {
    820             try {
    821                 return Double.parseDouble(valueText);
    822             } catch (NumberFormatException ex) {
    823                 return null;
    824             }
    825         }
    826     }
    827 
    828     private static class StringHandler extends Handler {
    829         @Override
    830         Object translate(String valueText) {
    831             return valueText;
    832         }
    833     }
    834 
    835     private static class FileHandler extends Handler {
    836         @Override
    837         Object translate(String valueText) {
    838             return new File(valueText);
    839         }
    840     }
    841 
    842     private static class MapEntry {
    843         public Object mKey = null;
    844         public Object mValue = null;
    845 
    846         /**
    847          * Convenience constructor
    848          */
    849         MapEntry(Object key, Object value) {
    850             mKey = key;
    851             mValue = value;
    852         }
    853     }
    854 
    855     /**
    856      * A {@see Handler} to handle values for Map fields.  The {@code Object} returned is a
    857      * MapEntry
    858      */
    859     private static class MapHandler extends Handler {
    860         private Handler mKeyHandler;
    861         private Handler mValueHandler;
    862 
    863         MapHandler(Handler keyHandler, Handler valueHandler) {
    864             if (keyHandler == null || valueHandler == null) {
    865                 throw new NullPointerException();
    866             }
    867 
    868             mKeyHandler = keyHandler;
    869             mValueHandler = valueHandler;
    870         }
    871 
    872         Handler getKeyHandler() {
    873             return mKeyHandler;
    874         }
    875 
    876         Handler getValueHandler() {
    877             return mValueHandler;
    878         }
    879 
    880         /**
    881          * {@inheritDoc}
    882          */
    883         @Override
    884         boolean isMap() {
    885             return true;
    886         }
    887 
    888         /**
    889          * {@inheritDoc}
    890          */
    891         @Override
    892         public int hashCode() {
    893             return Objects.hashCode(MapHandler.class, mKeyHandler, mValueHandler);
    894         }
    895 
    896         /**
    897          * Define two {@link MapHandler}s as equivalent if their key and value Handlers are
    898          * respectively equivalent.
    899          * <p />
    900          * {@inheritDoc}
    901          */
    902         @Override
    903         public boolean equals(Object otherObj) {
    904             if ((otherObj != null) && (otherObj instanceof MapHandler)) {
    905                 MapHandler other = (MapHandler) otherObj;
    906                 Handler otherKeyHandler = other.getKeyHandler();
    907                 Handler otherValueHandler = other.getValueHandler();
    908 
    909                 return mKeyHandler.equals(otherKeyHandler)
    910                         && mValueHandler.equals(otherValueHandler);
    911             }
    912 
    913             return false;
    914         }
    915 
    916         /**
    917          * {@inheritDoc}
    918          */
    919         @Override
    920         Object translate(String valueText) {
    921             return null;
    922         }
    923 
    924         MapEntry translate(String keyText, String valueText) {
    925             Object key = mKeyHandler.translate(keyText);
    926             Object value = mValueHandler.translate(valueText);
    927             if (key == null) {
    928                 throw new IllegalArgumentException("Failed to parse key");
    929             } else if (value == null) {
    930                 throw new IllegalArgumentException("Failed to parse value");
    931             }
    932 
    933             return new MapEntry(key, value);
    934         }
    935     }
    936 
    937     /**
    938      * A {@link Handler} to handle values for {@link Enum} fields.
    939      */
    940     private static class EnumHandler extends Handler {
    941         private final Class mEnumType;
    942 
    943         EnumHandler(Class<?> enumType) {
    944             mEnumType = enumType;
    945         }
    946 
    947         Class<?> getEnumType() {
    948             return mEnumType;
    949         }
    950 
    951         /**
    952          * {@inheritDoc}
    953          */
    954         @Override
    955         public int hashCode() {
    956             return Objects.hashCode(EnumHandler.class, mEnumType);
    957         }
    958 
    959         /**
    960          * Define two EnumHandlers as equivalent if their EnumTypes are mutually assignable
    961          * <p />
    962          * {@inheritDoc}
    963          */
    964         @SuppressWarnings("unchecked")
    965         @Override
    966         public boolean equals(Object otherObj) {
    967             if ((otherObj != null) && (otherObj instanceof EnumHandler)) {
    968                 EnumHandler other = (EnumHandler) otherObj;
    969                 Class<?> otherType = other.getEnumType();
    970 
    971                 return mEnumType.isAssignableFrom(otherType)
    972                         && otherType.isAssignableFrom(mEnumType);
    973             }
    974 
    975             return false;
    976         }
    977 
    978         /**
    979          * {@inheritDoc}
    980          */
    981         @Override
    982         Object translate(String valueText) {
    983             return translate(valueText, true);
    984         }
    985 
    986         @SuppressWarnings("unchecked")
    987         Object translate(String valueText, boolean shouldTryUpperCase) {
    988             try {
    989                 return Enum.valueOf(mEnumType, valueText);
    990             } catch (IllegalArgumentException e) {
    991                 // Will be thrown if the value can't be mapped back to the enum
    992                 if (shouldTryUpperCase) {
    993                     // Try to automatically map variable-case strings to uppercase.  This is
    994                     // reasonable since most Enum constants tend to be uppercase by convention.
    995                     return translate(valueText.toUpperCase(Locale.ENGLISH), false);
    996                 } else {
    997                     return null;
    998                 }
    999             }
   1000         }
   1001     }
   1002 }
   1003