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