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