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