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