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