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