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.tradefed.config;
18 
19 import com.android.tradefed.build.BuildRetrievalError;
20 import com.android.tradefed.invoker.tracing.CloseableTraceScope;
21 import com.android.tradefed.log.LogUtil.CLog;
22 import com.android.tradefed.result.error.InfraErrorIdentifier;
23 import com.android.tradefed.util.ArrayUtil;
24 import com.android.tradefed.util.MultiMap;
25 import com.android.tradefed.util.TimeVal;
26 import com.android.tradefed.util.keystore.IKeyStoreClient;
27 
28 import com.google.common.base.Objects;
29 
30 import java.io.File;
31 import java.lang.reflect.Field;
32 import java.lang.reflect.Modifier;
33 import java.lang.reflect.ParameterizedType;
34 import java.lang.reflect.Type;
35 import java.time.Duration;
36 import java.util.ArrayList;
37 import java.util.Arrays;
38 import java.util.Collection;
39 import java.util.HashMap;
40 import java.util.HashSet;
41 import java.util.Iterator;
42 import java.util.LinkedHashMap;
43 import java.util.List;
44 import java.util.Locale;
45 import java.util.Map;
46 import java.util.Set;
47 import java.util.regex.Pattern;
48 import java.util.regex.PatternSyntaxException;
49 
50 /**
51  * Populates {@link Option} fields.
52  * <p/>
53  * Setting of numeric fields such byte, short, int, long, float, and double fields is supported.
54  * This includes both unboxed and boxed versions (e.g. int vs Integer). If there is a problem
55  * setting the argument to match the desired type, a {@link ConfigurationException} is thrown.
56  * <p/>
57  * File option fields are supported by simply wrapping the string argument in a File object without
58  * testing for the existence of the file.
59  * <p/>
60  * Parameterized Collection fields such as List&lt;File&gt; and Set&lt;String&gt; are supported as
61  * long as the parameter type is otherwise supported by the option setter. The collection field
62  * should be initialized with an appropriate collection instance.
63  * <p/>
64  * All fields will be processed, including public, protected, default (package) access, private and
65  * inherited fields.
66  * <p/>
67  *
68  * ported from dalvik.runner.OptionParser
69  * @see ArgsOptionParser
70  */
71 @SuppressWarnings("rawtypes")
72 public class OptionSetter {
73     static final String BOOL_FALSE_PREFIX = "no-";
74     private static final HashMap<Class<?>, Handler> handlers = new HashMap<Class<?>, Handler>();
75     public static final char NAMESPACE_SEPARATOR = ':';
76     static final Pattern USE_KEYSTORE_REGEX = Pattern.compile("USE_KEYSTORE@(.*)");
77     private IKeyStoreClient mKeyStoreClient = null;
78 
79     static {
handlers.put(boolean.class, new BooleanHandler())80         handlers.put(boolean.class, new BooleanHandler());
handlers.put(Boolean.class, new BooleanHandler())81         handlers.put(Boolean.class, new BooleanHandler());
82 
handlers.put(byte.class, new ByteHandler())83         handlers.put(byte.class, new ByteHandler());
handlers.put(Byte.class, new ByteHandler())84         handlers.put(Byte.class, new ByteHandler());
handlers.put(short.class, new ShortHandler())85         handlers.put(short.class, new ShortHandler());
handlers.put(Short.class, new ShortHandler())86         handlers.put(Short.class, new ShortHandler());
handlers.put(int.class, new IntegerHandler())87         handlers.put(int.class, new IntegerHandler());
handlers.put(Integer.class, new IntegerHandler())88         handlers.put(Integer.class, new IntegerHandler());
handlers.put(long.class, new LongHandler())89         handlers.put(long.class, new LongHandler());
handlers.put(Long.class, new LongHandler())90         handlers.put(Long.class, new LongHandler());
91 
handlers.put(float.class, new FloatHandler())92         handlers.put(float.class, new FloatHandler());
handlers.put(Float.class, new FloatHandler())93         handlers.put(Float.class, new FloatHandler());
handlers.put(double.class, new DoubleHandler())94         handlers.put(double.class, new DoubleHandler());
handlers.put(Double.class, new DoubleHandler())95         handlers.put(Double.class, new DoubleHandler());
96 
handlers.put(String.class, new StringHandler())97         handlers.put(String.class, new StringHandler());
handlers.put(File.class, new FileHandler())98         handlers.put(File.class, new FileHandler());
handlers.put(TimeVal.class, new TimeValHandler())99         handlers.put(TimeVal.class, new TimeValHandler());
handlers.put(Pattern.class, new PatternHandler())100         handlers.put(Pattern.class, new PatternHandler());
handlers.put(Duration.class, new DurationHandler())101         handlers.put(Duration.class, new DurationHandler());
102     }
103 
104 
105     static class FieldDef {
106         Object object;
107         Field field;
108         Object key;
109 
FieldDef(Object object, Field field, Object key)110         FieldDef(Object object, Field field, Object key) {
111             this.object = object;
112             this.field = field;
113             this.key = key;
114         }
115 
116         @Override
equals(Object obj)117         public boolean equals(Object obj) {
118             if (obj == this) {
119                 return true;
120             }
121 
122             if (obj instanceof FieldDef) {
123                 FieldDef other = (FieldDef)obj;
124                 return Objects.equal(this.object, other.object) &&
125                         Objects.equal(this.field, other.field) &&
126                         Objects.equal(this.key, other.key);
127             }
128 
129             return false;
130         }
131 
132         @Override
hashCode()133         public int hashCode() {
134             return Objects.hashCode(object, field, key);
135         }
136     }
137 
138 
getHandler(Type type)139     private static Handler getHandler(Type type) throws ConfigurationException {
140         if (type instanceof ParameterizedType) {
141             ParameterizedType parameterizedType = (ParameterizedType) type;
142             Class<?> rawClass = (Class<?>) parameterizedType.getRawType();
143             if (Collection.class.isAssignableFrom(rawClass)) {
144                 // handle Collection
145                 Type actualType = parameterizedType.getActualTypeArguments()[0];
146                 if (!(actualType instanceof Class)) {
147                     throw new ConfigurationException(
148                             "cannot handle nested parameterized type " + type);
149                 }
150                 return getHandler(actualType);
151             } else if (Map.class.isAssignableFrom(rawClass) ||
152                     MultiMap.class.isAssignableFrom(rawClass)) {
153                 // handle Map
154                 Type keyType = parameterizedType.getActualTypeArguments()[0];
155                 Type valueType = parameterizedType.getActualTypeArguments()[1];
156                 if (!(keyType instanceof Class)) {
157                     throw new ConfigurationException(
158                             "cannot handle nested parameterized type " + keyType);
159                 } else if (!(valueType instanceof Class)) {
160                     throw new ConfigurationException(
161                             "cannot handle nested parameterized type " + valueType);
162                 }
163 
164                 return new MapHandler(getHandler(keyType), getHandler(valueType));
165             } else {
166                 throw new ConfigurationException(String.format(
167                         "can't handle parameterized type %s; only Collection, Map, and MultiMap "
168                         + "are supported", type));
169             }
170         }
171         if (type instanceof Class) {
172             Class<?> cType = (Class<?>) type;
173 
174             if (cType.isEnum()) {
175                 return new EnumHandler(cType);
176             } else if (Collection.class.isAssignableFrom(cType)) {
177                 // could handle by just having a default of treating
178                 // contents as String but consciously decided this
179                 // should be an error
180                 throw new ConfigurationException(String.format(
181                         "Cannot handle non-parameterized collection %s.  Use a generic Collection "
182                         + "to specify a desired element type.", type));
183             } else if (Map.class.isAssignableFrom(cType)) {
184                 // could handle by just having a default of treating
185                 // contents as String but consciously decided this
186                 // should be an error
187                 throw new ConfigurationException(String.format(
188                         "Cannot handle non-parameterized map %s.  Use a generic Map to specify "
189                         + "desired element types.", type));
190             } else if (MultiMap.class.isAssignableFrom(cType)) {
191                 // could handle by just having a default of treating
192                 // contents as String but consciously decided this
193                 // should be an error
194                 throw new ConfigurationException(String.format(
195                         "Cannot handle non-parameterized multimap %s.  Use a generic MultiMap to "
196                         + "specify desired element types.", type));
197             }
198             return handlers.get(cType);
199         }
200         throw new ConfigurationException(String.format("cannot handle unknown field type %s",
201                 type));
202     }
203 
204     /**
205      * Does some magic to distinguish TimeVal long field from normal long fields, then calls
206      * {@link #getHandler(Type)} in the appropriate manner.
207      */
getHandlerOrTimeVal(Field field, Object optionSource)208     private Handler getHandlerOrTimeVal(Field field, Object optionSource)
209             throws ConfigurationException {
210         // Do some magic to distinguish TimeVal long fields from normal long fields
211         final Option option = field.getAnnotation(Option.class);
212         if (option == null) {
213             // Shouldn't happen, but better to check.
214             throw new ConfigurationException(String.format(
215                     "internal error: @Option annotation for field %s in class %s was " +
216                     "unexpectedly null",
217                     field.getName(), optionSource.getClass().getName()));
218         }
219 
220         final Type type = field.getGenericType();
221         if (option.isTimeVal()) {
222             // We've got a field that marks itself as a time value.  First off, verify that it's
223             // a compatible type
224             if (type instanceof Class) {
225                 final Class<?> cType = (Class<?>) type;
226                 if (long.class.equals(cType) || Long.class.equals(cType)) {
227                     // Parse time value and return a Long
228                     return new TimeValLongHandler();
229 
230                 } else if (TimeVal.class.equals(cType)) {
231                     // Parse time value and return a TimeVal object
232                     return new TimeValHandler();
233                 }
234             }
235 
236             throw new ConfigurationException(String.format("Only fields of type long, " +
237                     "Long, or TimeVal may be declared as isTimeVal.  Field %s has " +
238                     "incompatible type %s.", field.getName(), field.getGenericType()));
239 
240         } else {
241             // Note that fields declared as TimeVal (or Generic types with TimeVal parameters) will
242             // follow this branch, but will still work as expected.
243             return getHandler(type);
244         }
245     }
246 
247 
248     private final Collection<Object> mOptionSources;
249     private final Map<String, OptionFieldsForName> mOptionMap;
250 
251     /**
252      * Container for the list of option fields with given name.
253      *
254      * <p>Used to enforce constraint that fields with same name can exist in different option
255      * sources, but not the same option source
256      */
257     protected class OptionFieldsForName implements Iterable<Map.Entry<Object, Field>> {
258 
259         private Map<Object, Field> mSourceFieldMap = new LinkedHashMap<Object, Field>();
260 
addField(String name, Object source, Field field)261         void addField(String name, Object source, Field field) throws ConfigurationException {
262             if (size() > 0) {
263                 Handler existingFieldHandler = getHandler(getFirstField().getGenericType());
264                 Handler newFieldHandler = getHandler(field.getGenericType());
265                 if (existingFieldHandler == null || newFieldHandler == null ||
266                         !existingFieldHandler.getClass().equals(newFieldHandler.getClass())) {
267                     throw new ConfigurationException(String.format(
268                             "@Option field with name '%s' in class '%s' is defined with a " +
269                             "different type than same option in class '%s'",
270                             name, source.getClass().getName(),
271                             getFirstObject().getClass().getName()));
272                 }
273             }
274             if (mSourceFieldMap.put(source, field) != null) {
275                 throw new ConfigurationException(String.format(
276                         "@Option field with name '%s' is defined more than once in class '%s'",
277                         name, source.getClass().getName()));
278             }
279         }
280 
size()281         public int size() {
282             return mSourceFieldMap.size();
283         }
284 
getFirstField()285         public Field getFirstField() throws ConfigurationException {
286             if (size() <= 0) {
287                 // should never happen
288                 throw new ConfigurationException("no option fields found");
289             }
290             return mSourceFieldMap.values().iterator().next();
291         }
292 
getFirstObject()293         public Object getFirstObject() throws ConfigurationException {
294             if (size() <= 0) {
295                 // should never happen
296                 throw new ConfigurationException("no option fields found");
297             }
298             return mSourceFieldMap.keySet().iterator().next();
299         }
300 
301         @Override
iterator()302         public Iterator<Map.Entry<Object, Field>> iterator() {
303             return mSourceFieldMap.entrySet().iterator();
304         }
305     }
306 
307     /**
308      * Constructs a new OptionParser for setting the @Option fields of 'optionSources'.
309      * @throws ConfigurationException
310      */
OptionSetter(Object... optionSources)311     public OptionSetter(Object... optionSources) throws ConfigurationException {
312         this(Arrays.asList(optionSources));
313     }
314 
315     /**
316      * Constructs a new OptionParser for setting the @Option fields of 'optionSources'.
317      * @throws ConfigurationException
318      */
OptionSetter(Collection<Object> optionSources)319     public OptionSetter(Collection<Object> optionSources) throws ConfigurationException {
320         mOptionSources = optionSources;
321         mOptionMap = makeOptionMap();
322     }
323 
setKeyStore(IKeyStoreClient keyStore)324     public void setKeyStore(IKeyStoreClient keyStore) {
325         mKeyStoreClient = keyStore;
326     }
327 
getKeyStore()328     public IKeyStoreClient getKeyStore() {
329         return mKeyStoreClient;
330     }
331 
fieldsForArg(String name)332     private OptionFieldsForName fieldsForArg(String name) throws ConfigurationException {
333         OptionFieldsForName fields = fieldsForArgNoThrow(name);
334         if (fields == null) {
335             throw new ConfigurationException(
336                     String.format("Could not find option with name '%s'", name),
337                     InfraErrorIdentifier.OPTION_CONFIGURATION_ERROR);
338         }
339         return fields;
340     }
341 
fieldsForArgNoThrow(String name)342     OptionFieldsForName fieldsForArgNoThrow(String name) {
343         OptionFieldsForName fields = mOptionMap.get(name);
344         if (fields == null || fields.size() == 0) {
345             return null;
346         }
347         return fields;
348     }
349 
350     /**
351      * Returns a string describing the type of the field with given name.
352      *
353      * @param name the {@link Option} field name
354      * @return a {@link String} describing the field's type
355      * @throws ConfigurationException if field could not be found
356      */
getTypeForOption(String name)357     public String getTypeForOption(String name) throws ConfigurationException {
358         return fieldsForArg(name).getFirstField().getType().getSimpleName().toLowerCase();
359     }
360 
361     /**
362      * Sets the value for a non-map option.
363      *
364      * @param optionName the name of Option to set
365      * @param valueText the value
366      * @return A list of {@link FieldDef}s corresponding to each object field that was modified.
367      * @throws ConfigurationException if Option cannot be found or valueText is wrong type
368      */
setOptionValue(String optionName, String valueText)369     public List<FieldDef> setOptionValue(String optionName, String valueText)
370             throws ConfigurationException {
371         return setOptionValue(optionName, null, valueText);
372     }
373 
374     /**
375      * Sets the value for an option.
376      *
377      * @param optionName the name of Option to set
378      * @param keyText the key for Map options, or null.
379      * @param valueText the value
380      * @return A list of {@link FieldDef}s corresponding to each object field that was modified.
381      * @throws ConfigurationException if Option cannot be found or valueText is wrong type
382      */
setOptionValue(String optionName, String keyText, String valueText)383     public List<FieldDef> setOptionValue(String optionName, String keyText, String valueText)
384             throws ConfigurationException {
385 
386         List<FieldDef> ret = new ArrayList<>();
387 
388         // For each of the applicable object fields
389         final OptionFieldsForName optionFields = fieldsForArg(optionName);
390         for (Map.Entry<Object, Field> fieldEntry : optionFields) {
391 
392             // Retrieve an appropriate handler for this field's type
393             final Object optionSource = fieldEntry.getKey();
394             final Field field = fieldEntry.getValue();
395             final Handler handler = getHandlerOrTimeVal(field, optionSource);
396 
397             // Translate the string value to the actual type of the field
398             Object value = handler.translate(valueText);
399             if (value == null) {
400                 String type = field.getType().getSimpleName();
401                 if (handler.isMap()) {
402                     ParameterizedType pType = (ParameterizedType) field.getGenericType();
403                     Type valueType = pType.getActualTypeArguments()[1];
404                     type = ((Class<?>)valueType).getSimpleName().toLowerCase();
405                 }
406                 throw new ConfigurationException(
407                         String.format(
408                                 "Couldn't convert value '%s' to a %s for option '%s'",
409                                 valueText, type, optionName),
410                         InfraErrorIdentifier.OPTION_CONFIGURATION_ERROR);
411             }
412 
413             // For maps, also translate the key value
414             Object key = null;
415             if (handler.isMap()) {
416                 key = ((MapHandler)handler).translateKey(keyText);
417                 if (key == null) {
418                     ParameterizedType pType = (ParameterizedType) field.getGenericType();
419                     Type keyType = pType.getActualTypeArguments()[0];
420                     String type = ((Class<?>)keyType).getSimpleName().toLowerCase();
421                     throw new ConfigurationException(
422                             String.format(
423                                     "Couldn't convert key '%s' to a %s for option '%s'",
424                                     keyText, type, optionName),
425                             InfraErrorIdentifier.OPTION_CONFIGURATION_ERROR);
426                 }
427             }
428 
429             // Actually set the field value
430             if (setFieldValue(optionName, optionSource, field, key, value)) {
431                 ret.add(new FieldDef(optionSource, field, key));
432             }
433         }
434 
435         return ret;
436     }
437 
438 
439     /**
440      * Sets the given {@link Option} field's value.
441      *
442      * @param optionName the name specified in {@link Option}
443      * @param optionSource the {@link Object} to set
444      * @param field the {@link Field}
445      * @param key the key to an entry in a {@link Map} or {@link MultiMap} field or null.
446      * @param value the value to set
447      * @return Whether the field was set.
448      * @throws ConfigurationException
449      * @see OptionUpdateRule
450      */
451     @SuppressWarnings("unchecked")
setFieldValue(String optionName, Object optionSource, Field field, Object key, Object value)452     static boolean setFieldValue(String optionName, Object optionSource, Field field, Object key,
453             Object value) throws ConfigurationException {
454 
455         boolean fieldWasSet = true;
456 
457         try {
458             field.setAccessible(true);
459 
460             if (Collection.class.isAssignableFrom(field.getType())) {
461                 if (key != null) {
462                     throw new ConfigurationException(String.format(
463                             "key not applicable for Collection field '%s'", field.getName()));
464                 }
465                 Collection collection = (Collection)field.get(optionSource);
466                 if (collection == null) {
467                     throw new ConfigurationException(String.format(
468                             "Unable to add value to field '%s'. Field is null.", field.getName()));
469                 }
470                 ParameterizedType pType = (ParameterizedType) field.getGenericType();
471                 Type fieldType = pType.getActualTypeArguments()[0];
472                 if (value instanceof Collection) {
473                     collection.addAll((Collection)value);
474                 } else if (!((Class<?>) fieldType).isInstance(value)) {
475                     // Ensure that the value being copied is of the right type for the collection.
476                     throw new ConfigurationException(
477                             String.format(
478                                     "Value '%s' is not of type '%s' like the Collection.",
479                                     value, fieldType));
480                 } else {
481                     collection.add(value);
482                 }
483             } else if (Map.class.isAssignableFrom(field.getType())) {
484                 // TODO: check if type of the value can be added safely to the Map.
485                 Map map = (Map) field.get(optionSource);
486                 if (map == null) {
487                     throw new ConfigurationException(String.format(
488                             "Unable to add value to field '%s'. Field is null.", field.getName()));
489                 }
490                 if (value instanceof Map) {
491                     if (key != null) {
492                         throw new ConfigurationException(String.format(
493                                 "Key not applicable when setting Map field '%s' from map value",
494                                 field.getName()));
495                     }
496                     map.putAll((Map)value);
497                 } else {
498                     if (key == null) {
499                         throw new ConfigurationException(String.format(
500                                 "Unable to add value to map field '%s'. Key is null.",
501                                 field.getName()));
502                     }
503                     Object o = map.put(key, value);
504                     if (o != null) {
505                         CLog.d(
506                                 "Overridden option value '%s' in map for option '%s' and key '%s'",
507                                 o, optionName, key);
508                     }
509                 }
510             } else if (MultiMap.class.isAssignableFrom(field.getType())) {
511                 // TODO: see if we can combine this with Map logic above
512                 MultiMap map = (MultiMap)field.get(optionSource);
513                 if (map == null) {
514                     throw new ConfigurationException(String.format(
515                             "Unable to add value to field '%s'. Field is null.", field.getName()));
516                 }
517                 if (value instanceof MultiMap) {
518                     if (key != null) {
519                         throw new ConfigurationException(String.format(
520                                 "Key not applicable when setting Map field '%s' from map value",
521                                 field.getName()));
522                     }
523                     map.putAll((MultiMap)value);
524                 } else {
525                     if (key == null) {
526                         throw new ConfigurationException(String.format(
527                                 "Unable to add value to map field '%s'. Key is null.",
528                                 field.getName()));
529                     }
530                     map.put(key, value);
531                 }
532             } else {
533                 if (key != null) {
534                     throw new ConfigurationException(String.format(
535                             "Key not applicable when setting non-map field '%s'", field.getName()));
536                 }
537                 final Option option = field.getAnnotation(Option.class);
538                 if (option == null) {
539                     // By virtue of us having gotten here, this should never happen.  But better
540                     // safe than sorry
541                     throw new ConfigurationException(String.format(
542                             "internal error: @Option annotation for field %s in class %s was " +
543                             "unexpectedly null",
544                             field.getName(), optionSource.getClass().getName()));
545                 }
546                 OptionUpdateRule rule = option.updateRule();
547                 if (rule.shouldUpdate(optionName, optionSource, field, value)) {
548                     Object curValue = field.get(optionSource);
549                     if (value == null || value.equals(curValue)) {
550                         fieldWasSet = false;
551                     } else {
552                         field.set(optionSource, value);
553                     }
554                 } else {
555                     fieldWasSet = false;
556                 }
557             }
558         } catch (IllegalAccessException | IllegalArgumentException e) {
559             throw new ConfigurationException(String.format(
560                     "internal error when setting option '%s'", optionName), e);
561 
562         }
563         return fieldWasSet;
564     }
565 
566     /**
567      * Sets the given {@link Option} fields value.
568      *
569      * @param optionName the name specified in {@link Option}
570      * @param optionSource the {@link Object} to set
571      * @param field the {@link Field}
572      * @param value the value to set
573      * @throws ConfigurationException
574      */
setFieldValue(String optionName, Object optionSource, Field field, Object value)575     static void setFieldValue(String optionName, Object optionSource, Field field, Object value)
576             throws ConfigurationException {
577 
578         setFieldValue(optionName, optionSource, field, null, value);
579     }
580 
581     /**
582      * Cache the available options and report any problems with the options themselves right away.
583      *
584      * @return a {@link Map} of {@link Option} field name to {@link OptionFieldsForName}s
585      * @throws ConfigurationException if any {@link Option} are incorrectly specified
586      */
makeOptionMap()587     private Map<String, OptionFieldsForName> makeOptionMap() throws ConfigurationException {
588         try (CloseableTraceScope m = new CloseableTraceScope("makeOptionMap")) {
589             final Map<String, Integer> freqMap = new HashMap<String, Integer>(mOptionSources.size());
590             final Map<String, OptionFieldsForName> optionMap =
591                     new LinkedHashMap<String, OptionFieldsForName>();
592             for (Object objectSource : mOptionSources) {
593                 final String className = objectSource.getClass().getName();
594 
595                 // Keep track of how many times we've seen this className.  This assumes that
596                 // we maintain the optionSources in a universally-knowable order internally
597                 // (which we do, they remain in the order in which they were passed to the
598                 // constructor).  Thus, the index can serve as a unique identifier for each
599                 // instance of className as long as other upstream classes use the same
600                 // 1-based ordered numbering scheme.
601                 Integer index = freqMap.get(className);
602                 index = index == null ? 1 : index + 1;
603                 freqMap.put(className, index);
604                 addOptionsForObject(objectSource, optionMap, index, null);
605 
606                 if (objectSource instanceof IDeviceConfiguration) {
607                     for (Object deviceObject :
608                                 ((IDeviceConfiguration)objectSource).getAllObjects()) {
609                         index = freqMap.get(deviceObject.getClass().getName());
610                         index = index == null ? 1 : index + 1;
611                         freqMap.put(deviceObject.getClass().getName(), index);
612                         Integer tracked = ((IDeviceConfiguration) objectSource)
613                                  .getFrequency(deviceObject);
614                         if (tracked != null && !index.equals(tracked)) {
615                             index = tracked;
616                         }
617                         addOptionsForObject(deviceObject, optionMap, index,
618                                 ((IDeviceConfiguration)objectSource).getDeviceName());
619                     }
620                 }
621             }
622             return optionMap;
623         }
624     }
625 
626     /**
627      * Adds all option fields (both declared and inherited) to the <var>optionMap</var> for provided
628      * <var>optionClass</var>.
629      *
630      * <p>Also adds option fields with all the alias namespaced from the class they are found in, and
631      * their child classes.
632      *
633      * <p>For example: if class1(@alias1) extends class2(@alias2), all the option from class2 will be
634      * available with the alias1 and alias2. All the option from class1 are available with alias1
635      * only.
636      *
637      * @param optionSource
638      * @param optionMap
639      * @param index The unique index of this instance of the optionSource class. Should equal the
640      *     number of instances of this class that we've already seen, plus 1.
641      * @param deviceName the Configuration Device Name that this attributes belong to. can be null.
642      * @throws ConfigurationException
643      */
addOptionsForObject( Object optionSource, Map<String, OptionFieldsForName> optionMap, int index, String deviceName)644     private void addOptionsForObject(
645         Object optionSource, Map<String, OptionFieldsForName> optionMap, int index, String deviceName)
646       throws ConfigurationException {
647         Collection<Field> optionFields = getOptionFieldsForClass(optionSource.getClass());
648         for (Field field : optionFields) {
649             final Option option = field.getAnnotation(Option.class);
650             if (option.name().indexOf(NAMESPACE_SEPARATOR) != -1) {
651                 throw new ConfigurationException(String.format(
652                         "Option name '%s' in class '%s' is invalid. " +
653                         "Option names cannot contain the namespace separator character '%c'",
654                         option.name(), optionSource.getClass().getName(), NAMESPACE_SEPARATOR));
655             }
656 
657             // Make sure the source doesn't use GREATEST or LEAST for a non-Comparable field.
658             final Type type = field.getGenericType();
659             if ((type instanceof Class) && !(type instanceof ParameterizedType)) {
660                 // Not a parameterized type
661                 if ((option.updateRule() == OptionUpdateRule.GREATEST) ||
662                         (option.updateRule() == OptionUpdateRule.LEAST)) {
663                     Class cType = (Class) type;
664                     if (!Comparable.class.isAssignableFrom(cType)) {
665                         throw new ConfigurationException(String.format(
666                                 "Option '%s' in class '%s' attempts to use updateRule %s with " +
667                                 "non-Comparable type '%s'.", option.name(),
668                                 optionSource.getClass().getName(), option.updateRule(),
669                                 field.getGenericType()));
670                     }
671                 }
672 
673                 // don't allow 'final' for non-Collections
674                 if ((field.getModifiers() & Modifier.FINAL) != 0) {
675                     throw new ConfigurationException(String.format(
676                             "Option '%s' in class '%s' is final and cannot be set", option.name(),
677                             optionSource.getClass().getName()));
678                 }
679             }
680 
681             // Allow classes to opt out of the global Option namespace
682             boolean addToGlobalNamespace = true;
683             if (optionSource.getClass().isAnnotationPresent(OptionClass.class)) {
684                 final OptionClass classAnnotation = optionSource.getClass().getAnnotation(
685                         OptionClass.class);
686                 addToGlobalNamespace = classAnnotation.global_namespace();
687             }
688 
689             if (addToGlobalNamespace) {
690                 addNameToMap(optionMap, optionSource, option.name(), field);
691                 if (deviceName != null) {
692                     addNameToMap(optionMap, optionSource,
693                             String.format("{%s}%s", deviceName, option.name()), field);
694                 }
695             }
696             addNamespacedOptionToMap(optionMap, optionSource, option.name(), field, index,
697                     deviceName);
698             if (option.shortName() != Option.NO_SHORT_NAME) {
699                 if (addToGlobalNamespace) {
700                     // Note that shortName is not supported with device specified, full name needs
701                     // to be use
702                     addNameToMap(optionMap, optionSource, String.valueOf(option.shortName()),
703                             field);
704                 }
705                 addNamespacedOptionToMap(optionMap, optionSource,
706                         String.valueOf(option.shortName()), field, index, deviceName);
707             }
708             if (isBooleanField(field)) {
709                 // add the corresponding "no" option to make boolean false
710                 if (addToGlobalNamespace) {
711                     addNameToMap(optionMap, optionSource, BOOL_FALSE_PREFIX + option.name(), field);
712                     if (deviceName != null) {
713                         addNameToMap(optionMap, optionSource, String.format("{%s}%s", deviceName,
714                                         BOOL_FALSE_PREFIX + option.name()), field);
715                     }
716                 }
717                 addNamespacedOptionToMap(optionMap, optionSource, BOOL_FALSE_PREFIX + option.name(),
718                         field, index, deviceName);
719             }
720         }
721     }
722 
723     /**
724      * Returns the names of all of the {@link Option}s that are marked as {@code mandatory} but
725      * remain unset.
726      *
727      * @return A {@link Collection} of {@link String}s containing the (unqualified) names of unset
728      *         mandatory options.
729      * @throws ConfigurationException if a field to be checked is inaccessible
730      */
getUnsetMandatoryOptions()731     protected Collection<String> getUnsetMandatoryOptions() throws ConfigurationException {
732         Collection<String> unsetOptions = new HashSet<String>();
733         for (Map.Entry<String, OptionFieldsForName> optionPair : mOptionMap.entrySet()) {
734             final String optName = optionPair.getKey();
735             final OptionFieldsForName optionFields = optionPair.getValue();
736             if (optName.indexOf(NAMESPACE_SEPARATOR) >= 0) {
737                 // Only return unqualified option names
738                 continue;
739             }
740 
741             for (Map.Entry<Object, Field> fieldEntry : optionFields) {
742                 final Object obj = fieldEntry.getKey();
743                 final Field field = fieldEntry.getValue();
744                 final Option option = field.getAnnotation(Option.class);
745                 if (option == null) {
746                     continue;
747                 } else if (!option.mandatory()) {
748                     continue;
749                 }
750 
751                 // At this point, we know this is a mandatory field; make sure it's set
752                 field.setAccessible(true);
753                 final Object value;
754                 try {
755                     value = field.get(obj);
756                 } catch (IllegalAccessException e) {
757                     throw new ConfigurationException(String.format("internal error: %s",
758                             e.getMessage()));
759                 }
760 
761                 final String realOptName = String.format("--%s", option.name());
762                 if (value == null) {
763                     unsetOptions.add(realOptName);
764                 } else if (value instanceof Collection) {
765                     Collection c = (Collection) value;
766                     if (c.isEmpty()) {
767                         unsetOptions.add(realOptName);
768                     }
769                 } else if (value instanceof Map) {
770                     Map m = (Map) value;
771                     if (m.isEmpty()) {
772                         unsetOptions.add(realOptName);
773                     }
774                 } else if (value instanceof MultiMap) {
775                     MultiMap m = (MultiMap) value;
776                     if (m.isEmpty()) {
777                         unsetOptions.add(realOptName);
778                     }
779                 }
780             }
781         }
782         return unsetOptions;
783     }
784 
785     /**
786      * Runs through all the {@link File} option type and check if their path should be resolved.
787      *
788      * @param resolver The {@link DynamicRemoteFileResolver} to use to resolve the files.
789      * @return The list of {@link File} that was resolved that way.
790      * @throws BuildRetrievalError
791      */
validateRemoteFilePath(DynamicRemoteFileResolver resolver)792     public final Set<File> validateRemoteFilePath(DynamicRemoteFileResolver resolver)
793             throws BuildRetrievalError {
794         resolver.setOptionMap(mOptionMap);
795         return resolver.validateRemoteFilePath();
796     }
797 
798     /**
799      * Gets a list of all {@link Option} fields (both declared and inherited) for given class.
800      *
801      * @param optionClass the {@link Class} to search
802      * @return a {@link Collection} of fields annotated with {@link Option}
803      */
getOptionFieldsForClass(final Class<?> optionClass)804     public static Collection<Field> getOptionFieldsForClass(final Class<?> optionClass) {
805         Collection<Field> fieldList = new ArrayList<Field>();
806         buildOptionFieldsForClass(optionClass, fieldList);
807         return fieldList;
808     }
809 
810     /**
811      * Recursive method that adds all option fields (both declared and inherited) to the
812      * <var>optionFields</var> for provided <var>optionClass</var>
813      *
814      * @param optionClass
815      * @param optionFields
816      */
buildOptionFieldsForClass(final Class<?> optionClass, Collection<Field> optionFields)817     private static void buildOptionFieldsForClass(final Class<?> optionClass,
818             Collection<Field> optionFields) {
819         for (Field field : optionClass.getDeclaredFields()) {
820             if (field.isAnnotationPresent(Option.class)) {
821                 optionFields.add(field);
822             }
823         }
824         Class<?> superClass = optionClass.getSuperclass();
825         if (superClass != null) {
826             buildOptionFieldsForClass(superClass, optionFields);
827         }
828     }
829 
830     /**
831      * Return the given {@link Field}'s value as a {@link String}.
832      *
833      * @param field the {@link Field}
834      * @param optionObject the {@link Object} to get field's value from.
835      * @return the field's value as a {@link String}, or <code>null</code> if field is not set or is
836      *         empty (in case of {@link Collection}s
837      */
getFieldValueAsString(Field field, Object optionObject)838     static String getFieldValueAsString(Field field, Object optionObject) {
839         Object fieldValue = getFieldValue(field, optionObject);
840         if (fieldValue == null) {
841             return null;
842         }
843         if (fieldValue instanceof Collection) {
844             Collection collection = (Collection)fieldValue;
845             if (collection.isEmpty()) {
846                 return null;
847             }
848         } else if (fieldValue instanceof Map) {
849             Map map = (Map)fieldValue;
850             if (map.isEmpty()) {
851                 return null;
852             }
853         } else if (fieldValue instanceof MultiMap) {
854             MultiMap multimap = (MultiMap)fieldValue;
855             if (multimap.isEmpty()) {
856                 return null;
857             }
858         }
859         return fieldValue.toString();
860     }
861 
862     /**
863      * Return the given {@link Field}'s value, handling any exceptions.
864      *
865      * @param field the {@link Field}
866      * @param optionObject the {@link Object} to get field's value from.
867      * @return the field's value as a {@link Object}, or <code>null</code>
868      */
getFieldValue(Field field, Object optionObject)869     public static Object getFieldValue(Field field, Object optionObject) {
870         try {
871             field.setAccessible(true);
872             return field.get(optionObject);
873         } catch (IllegalArgumentException e) {
874             CLog.w("Could not read value for field %s in class %s. Reason: %s", field.getName(),
875                     optionObject.getClass().getName(), e);
876             return null;
877         } catch (IllegalAccessException e) {
878             CLog.w("Could not read value for field %s in class %s. Reason: %s", field.getName(),
879                     optionObject.getClass().getName(), e);
880             return null;
881         }
882     }
883 
884     /**
885      * Returns the help text describing the valid values for the Enum field.
886      *
887      * @param field the {@link Field} to get values for
888      * @return the appropriate help text, or an empty {@link String} if the field is not an Enum.
889      */
getEnumFieldValuesAsString(Field field)890     static String getEnumFieldValuesAsString(Field field) {
891         Class<?> type = field.getType();
892         Object[] vals = type.getEnumConstants();
893         if (vals == null) {
894             return "";
895         }
896 
897         StringBuilder sb = new StringBuilder(" Valid values: [");
898         sb.append(ArrayUtil.join(", ", vals));
899         sb.append("]");
900         return sb.toString();
901     }
902 
isBooleanOption(String name)903     public boolean isBooleanOption(String name) throws ConfigurationException {
904         Field field = fieldsForArg(name).getFirstField();
905         return isBooleanField(field);
906     }
907 
isBooleanField(Field field)908     static boolean isBooleanField(Field field) throws ConfigurationException {
909         return getHandler(field.getGenericType()).isBoolean();
910     }
911 
isMapOption(String name)912     public boolean isMapOption(String name) throws ConfigurationException {
913         Field field = fieldsForArg(name).getFirstField();
914         return isMapField(field);
915     }
916 
isMapField(Field field)917     static boolean isMapField(Field field) throws ConfigurationException {
918         return getHandler(field.getGenericType()).isMap();
919     }
920 
addNameToMap(Map<String, OptionFieldsForName> optionMap, Object optionSource, String name, Field field)921     private void addNameToMap(Map<String, OptionFieldsForName> optionMap, Object optionSource,
922             String name, Field field) throws ConfigurationException {
923         OptionFieldsForName fields = optionMap.get(name);
924         if (fields == null) {
925             fields = new OptionFieldsForName();
926             optionMap.put(name, fields);
927         }
928 
929         fields.addField(name, optionSource, field);
930         if (getHandler(field.getGenericType()) == null) {
931             throw new ConfigurationException(String.format(
932                     "Option name '%s' in class '%s' is invalid. Unsupported @Option field type "
933                     + "'%s'", name, optionSource.getClass().getName(), field.getType()));
934         }
935     }
936 
937     /**
938      * Adds the namespaced versions of the option to the map
939      *
940      * See {@link #makeOptionMap()} for details on the enumeration scheme
941      */
addNamespacedOptionToMap(Map<String, OptionFieldsForName> optionMap, Object optionSource, String name, Field field, int index, String deviceName)942     private void addNamespacedOptionToMap(Map<String, OptionFieldsForName> optionMap,
943             Object optionSource, String name, Field field, int index, String deviceName)
944             throws ConfigurationException {
945         final String className = optionSource.getClass().getName();
946 
947         if (optionSource.getClass().isAnnotationPresent(OptionClass.class)) {
948             final OptionClass classAnnotation = optionSource.getClass().getAnnotation(
949                     OptionClass.class);
950             addNamespacedAliasOptionToMap(optionMap, optionSource, name, field, index, deviceName,
951                     classAnnotation.alias());
952         }
953 
954         // Allows use of a className-delimited namespace.
955         // Example option name: com.fully.qualified.ClassName:option-name
956         addNameToMap(optionMap, optionSource, String.format("%s%c%s",
957                 className, NAMESPACE_SEPARATOR, name), field);
958 
959         // Allows use of an enumerated namespace, to enable options to map to specific instances of
960         // a className, rather than just to all instances of that particular className.
961         // Example option name: com.fully.qualified.ClassName:2:option-name
962         addNameToMap(optionMap, optionSource, String.format("%s%c%d%c%s",
963                 className, NAMESPACE_SEPARATOR, index, NAMESPACE_SEPARATOR, name), field);
964 
965         if (deviceName != null) {
966             // Example option name: {device1}com.fully.qualified.ClassName:option-name
967             addNameToMap(optionMap, optionSource, String.format("{%s}%s%c%s",
968                     deviceName, className, NAMESPACE_SEPARATOR, name), field);
969 
970             // Allows use of an enumerated namespace, to enable options to map to specific
971             // instances of a className inside a device configuration holder,
972             // rather than just to all instances of that particular className.
973             // Example option name: {device1}com.fully.qualified.ClassName:2:option-name
974             addNameToMap(optionMap, optionSource, String.format("{%s}%s%c%d%c%s",
975                     deviceName, className, NAMESPACE_SEPARATOR, index, NAMESPACE_SEPARATOR, name),
976                     field);
977         }
978     }
979 
980     /**
981      * Adds the alias namespaced versions of the option to the map
982      *
983      * See {@link #makeOptionMap()} for details on the enumeration scheme
984      */
addNamespacedAliasOptionToMap(Map<String, OptionFieldsForName> optionMap, Object optionSource, String name, Field field, int index, String deviceName, String alias)985     private void addNamespacedAliasOptionToMap(Map<String, OptionFieldsForName> optionMap,
986             Object optionSource, String name, Field field, int index, String deviceName,
987             String alias) throws ConfigurationException {
988         addNameToMap(optionMap, optionSource, String.format("%s%c%s", alias,
989                 NAMESPACE_SEPARATOR, name), field);
990 
991         // Allows use of an enumerated namespace, to enable options to map to specific instances
992         // of a class alias, rather than just to all instances of that particular alias.
993         // Example option name: alias:2:option-name
994         addNameToMap(optionMap, optionSource, String.format("%s%c%d%c%s",
995                 alias, NAMESPACE_SEPARATOR, index, NAMESPACE_SEPARATOR, name),
996                 field);
997 
998         if (deviceName != null) {
999             addNameToMap(optionMap, optionSource, String.format("{%s}%s%c%s", deviceName,
1000                     alias, NAMESPACE_SEPARATOR, name), field);
1001             // Allows use of an enumerated namespace, to enable options to map to specific
1002             // instances of a class alias inside a device configuration holder,
1003             // rather than just to all instances of that particular alias.
1004             // Example option name: {device1}alias:2:option-name
1005             addNameToMap(optionMap, optionSource, String.format("{%s}%s%c%d%c%s",
1006                     deviceName, alias, NAMESPACE_SEPARATOR, index,
1007                     NAMESPACE_SEPARATOR, name), field);
1008         }
1009     }
1010 
1011     private abstract static class Handler<T> {
1012         // Only BooleanHandler should ever override this.
isBoolean()1013         boolean isBoolean() {
1014             return false;
1015         }
1016 
1017         // Only MapHandler should ever override this.
isMap()1018         boolean isMap() {
1019             return false;
1020         }
1021 
1022         /**
1023          * Returns an object of appropriate type for the given Handle, corresponding to 'valueText'.
1024          * Returns null on failure.
1025          */
translate(String valueText)1026         abstract T translate(String valueText);
1027     }
1028 
1029     private static class BooleanHandler extends Handler<Boolean> {
isBoolean()1030         @Override boolean isBoolean() {
1031             return true;
1032         }
1033 
1034         @Override
translate(String valueText)1035         Boolean translate(String valueText) {
1036             if (valueText.equalsIgnoreCase("true") || valueText.equalsIgnoreCase("yes")) {
1037                 return Boolean.TRUE;
1038             } else if (valueText.equalsIgnoreCase("false") || valueText.equalsIgnoreCase("no")) {
1039                 return Boolean.FALSE;
1040             }
1041             return null;
1042         }
1043     }
1044 
1045     private static class ByteHandler extends Handler<Byte> {
1046         @Override
translate(String valueText)1047         Byte translate(String valueText) {
1048             try {
1049                 return Byte.parseByte(valueText);
1050             } catch (NumberFormatException ex) {
1051                 return null;
1052             }
1053         }
1054     }
1055 
1056     private static class ShortHandler extends Handler<Short> {
1057         @Override
translate(String valueText)1058         Short translate(String valueText) {
1059             try {
1060                 return Short.parseShort(valueText);
1061             } catch (NumberFormatException ex) {
1062                 return null;
1063             }
1064         }
1065     }
1066 
1067     private static class IntegerHandler extends Handler<Integer> {
1068         @Override
translate(String valueText)1069         Integer translate(String valueText) {
1070             try {
1071                 return Integer.parseInt(valueText);
1072             } catch (NumberFormatException ex) {
1073                 return null;
1074             }
1075         }
1076     }
1077 
1078     private static class LongHandler extends Handler<Long> {
1079         @Override
translate(String valueText)1080         Long translate(String valueText) {
1081             try {
1082                 return Long.parseLong(valueText);
1083             } catch (NumberFormatException ex) {
1084                 return null;
1085             }
1086         }
1087     }
1088 
1089     private static class TimeValLongHandler extends Handler<Long> {
1090         /** We parse the string as a time value, and return a {@code long} */
1091         @Override
translate(String valueText)1092         Long translate(String valueText) {
1093             try {
1094                 return TimeVal.fromString(valueText);
1095 
1096             } catch (NumberFormatException ex) {
1097                 return null;
1098             }
1099         }
1100     }
1101 
1102     private static class TimeValHandler extends Handler<TimeVal> {
1103         /** We parse the string as a time value, and return a {@code TimeVal} */
1104         @Override
translate(String valueText)1105         TimeVal translate(String valueText) {
1106             try {
1107                 return new TimeVal(valueText);
1108 
1109             } catch (NumberFormatException ex) {
1110                 return null;
1111             }
1112         }
1113     }
1114 
1115     private static class DurationHandler extends Handler<Duration> {
1116         /**
1117          * We parse the string as a time value, and return a {@code Duration}.
1118          *
1119          * <p>Both the {@link TimeVal} and {@link Duration#parse(CharSequence)} formats are
1120          * supported.
1121          */
1122         @Override
translate(String valueText)1123         Duration translate(String valueText) {
1124             try {
1125                 return Duration.ofMillis(TimeVal.fromString(valueText));
1126             } catch (NumberFormatException e) {
1127 
1128             }
1129             return Duration.parse(valueText);
1130         }
1131     }
1132 
1133     private static class PatternHandler extends Handler<Pattern> {
1134         /** We parse the string as a regex pattern, and return a {@code Pattern} */
1135         @Override
translate(String valueText)1136         Pattern translate(String valueText) {
1137             try {
1138                 return Pattern.compile(valueText);
1139             } catch (PatternSyntaxException ex) {
1140                 return null;
1141             }
1142         }
1143     }
1144 
1145     private static class FloatHandler extends Handler<Float> {
1146         @Override
translate(String valueText)1147         Float translate(String valueText) {
1148             try {
1149                 return Float.parseFloat(valueText);
1150             } catch (NumberFormatException ex) {
1151                 return null;
1152             }
1153         }
1154     }
1155 
1156     private static class DoubleHandler extends Handler<Double> {
1157         @Override
translate(String valueText)1158         Double translate(String valueText) {
1159             try {
1160                 return Double.parseDouble(valueText);
1161             } catch (NumberFormatException ex) {
1162                 return null;
1163             }
1164         }
1165     }
1166 
1167     private static class StringHandler extends Handler<String> {
1168         @Override
translate(String valueText)1169         String translate(String valueText) {
1170             return valueText;
1171         }
1172     }
1173 
1174     private static class FileHandler extends Handler<File> {
1175         @Override
translate(String valueText)1176         File translate(String valueText) {
1177             return new File(valueText);
1178         }
1179     }
1180 
1181     /**
1182      * A {@link Handler} to handle values for Map fields.  The {@code Object} returned is a
1183      * MapEntry
1184      */
1185     private static class MapHandler extends Handler {
1186         private Handler mKeyHandler;
1187         private Handler mValueHandler;
1188 
MapHandler(Handler keyHandler, Handler valueHandler)1189         MapHandler(Handler keyHandler, Handler valueHandler) {
1190             if (keyHandler == null || valueHandler == null) {
1191                 throw new NullPointerException();
1192             }
1193 
1194             mKeyHandler = keyHandler;
1195             mValueHandler = valueHandler;
1196         }
1197 
getKeyHandler()1198         Handler getKeyHandler() {
1199             return mKeyHandler;
1200         }
1201 
getValueHandler()1202         Handler getValueHandler() {
1203             return mValueHandler;
1204         }
1205 
1206         /**
1207          * {@inheritDoc}
1208          */
1209         @Override
isMap()1210         boolean isMap() {
1211             return true;
1212         }
1213 
1214         /**
1215          * {@inheritDoc}
1216          */
1217         @Override
hashCode()1218         public int hashCode() {
1219             return Objects.hashCode(MapHandler.class, mKeyHandler, mValueHandler);
1220         }
1221 
1222         /**
1223          * Define two {@link MapHandler}s as equivalent if their key and value Handlers are
1224          * respectively equivalent.
1225          * <p />
1226          * {@inheritDoc}
1227          */
1228         @Override
equals(Object otherObj)1229         public boolean equals(Object otherObj) {
1230             if ((otherObj != null) && (otherObj instanceof MapHandler)) {
1231                 MapHandler other = (MapHandler) otherObj;
1232                 Handler otherKeyHandler = other.getKeyHandler();
1233                 Handler otherValueHandler = other.getValueHandler();
1234 
1235                 return mKeyHandler.equals(otherKeyHandler)
1236                         && mValueHandler.equals(otherValueHandler);
1237             }
1238 
1239             return false;
1240         }
1241 
1242         /**
1243          * {@inheritDoc}
1244          */
1245         @Override
translate(String valueText)1246         Object translate(String valueText) {
1247             return mValueHandler.translate(valueText);
1248         }
1249 
translateKey(String keyText)1250         Object translateKey(String keyText) {
1251             return mKeyHandler.translate(keyText);
1252         }
1253     }
1254 
1255     /**
1256      * A {@link Handler} to handle values for {@link Enum} fields.
1257      */
1258     private static class EnumHandler extends Handler {
1259         private final Class mEnumType;
1260 
EnumHandler(Class<?> enumType)1261         EnumHandler(Class<?> enumType) {
1262             mEnumType = enumType;
1263         }
1264 
getEnumType()1265         Class<?> getEnumType() {
1266             return mEnumType;
1267         }
1268 
1269         /**
1270          * {@inheritDoc}
1271          */
1272         @Override
hashCode()1273         public int hashCode() {
1274             return Objects.hashCode(EnumHandler.class, mEnumType);
1275         }
1276 
1277         /**
1278          * Define two EnumHandlers as equivalent if their EnumTypes are mutually assignable
1279          * <p />
1280          * {@inheritDoc}
1281          */
1282         @SuppressWarnings("unchecked")
1283         @Override
equals(Object otherObj)1284         public boolean equals(Object otherObj) {
1285             if ((otherObj != null) && (otherObj instanceof EnumHandler)) {
1286                 EnumHandler other = (EnumHandler) otherObj;
1287                 Class<?> otherType = other.getEnumType();
1288 
1289                 return mEnumType.isAssignableFrom(otherType)
1290                         && otherType.isAssignableFrom(mEnumType);
1291             }
1292 
1293             return false;
1294         }
1295 
1296         /**
1297          * {@inheritDoc}
1298          */
1299         @Override
translate(String valueText)1300         Object translate(String valueText) {
1301             return translate(valueText, true);
1302         }
1303 
1304         @SuppressWarnings("unchecked")
translate(String valueText, boolean shouldTryUpperCase)1305         Object translate(String valueText, boolean shouldTryUpperCase) {
1306             try {
1307                 return Enum.valueOf(mEnumType, valueText);
1308             } catch (IllegalArgumentException e) {
1309                 // Will be thrown if the value can't be mapped back to the enum
1310                 if (shouldTryUpperCase) {
1311                     // Try to automatically map variable-case strings to uppercase.  This is
1312                     // reasonable since most Enum constants tend to be uppercase by convention.
1313                     return translate(valueText.toUpperCase(Locale.ENGLISH), false);
1314                 } else {
1315                     return null;
1316                 }
1317             }
1318         }
1319     }
1320 }
1321