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