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