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.loganalysis.util.config; 18 19 import com.android.loganalysis.util.ArrayUtil; 20 import com.google.common.base.Objects; 21 22 import java.io.File; 23 import java.lang.reflect.Field; 24 import java.lang.reflect.Modifier; 25 import java.lang.reflect.ParameterizedType; 26 import java.lang.reflect.Type; 27 import java.util.ArrayList; 28 import java.util.Arrays; 29 import java.util.Collection; 30 import java.util.HashMap; 31 import java.util.HashSet; 32 import java.util.Iterator; 33 import java.util.Locale; 34 import java.util.Map; 35 36 /** 37 * Populates {@link Option} fields. 38 * <p/> 39 * Setting of numeric fields such byte, short, int, long, float, and double fields is supported. 40 * This includes both unboxed and boxed versions (e.g. int vs Integer). If there is a problem 41 * setting the argument to match the desired type, a {@link ConfigurationException} is thrown. 42 * <p/> 43 * File option fields are supported by simply wrapping the string argument in a File object without 44 * testing for the existence of the file. 45 * <p/> 46 * Parameterized Collection fields such as List<File> and Set<String> are supported as long as the 47 * parameter type is otherwise supported by the option setter. The collection field should be 48 * initialized with an appropriate collection instance. 49 * <p/> 50 * All fields will be processed, including public, protected, default (package) access, private and 51 * inherited fields. 52 * <p/> 53 * 54 * ported from dalvik.runner.OptionParser 55 * @see {@link ArgsOptionParser} 56 */ 57 //TODO: Use libTF once this is copied over. 58 @SuppressWarnings("rawtypes") 59 public class OptionSetter { 60 61 static final String BOOL_FALSE_PREFIX = "no-"; 62 private static final HashMap<Class<?>, Handler> handlers = new HashMap<Class<?>, Handler>(); 63 static final char NAMESPACE_SEPARATOR = ':'; 64 65 static { 66 handlers.put(boolean.class, new BooleanHandler()); 67 handlers.put(Boolean.class, new BooleanHandler()); 68 69 handlers.put(byte.class, new ByteHandler()); 70 handlers.put(Byte.class, new ByteHandler()); 71 handlers.put(short.class, new ShortHandler()); 72 handlers.put(Short.class, new ShortHandler()); 73 handlers.put(int.class, new IntegerHandler()); 74 handlers.put(Integer.class, new IntegerHandler()); 75 handlers.put(long.class, new LongHandler()); 76 handlers.put(Long.class, new LongHandler()); 77 78 handlers.put(float.class, new FloatHandler()); 79 handlers.put(Float.class, new FloatHandler()); 80 handlers.put(double.class, new DoubleHandler()); 81 handlers.put(Double.class, new DoubleHandler()); 82 83 handlers.put(String.class, new StringHandler()); 84 handlers.put(File.class, new FileHandler()); 85 } 86 87 private static Handler getHandler(Type type) throws ConfigurationException { 88 if (type instanceof ParameterizedType) { 89 ParameterizedType parameterizedType = (ParameterizedType) type; 90 Class<?> rawClass = (Class<?>) parameterizedType.getRawType(); 91 if (Collection.class.isAssignableFrom(rawClass)) { 92 // handle Collection 93 Type actualType = parameterizedType.getActualTypeArguments()[0]; 94 if (!(actualType instanceof Class)) { 95 throw new ConfigurationException( 96 "cannot handle nested parameterized type " + type); 97 } 98 return getHandler(actualType); 99 } else if (Map.class.isAssignableFrom(rawClass)) { 100 // handle Map 101 Type keyType = parameterizedType.getActualTypeArguments()[0]; 102 Type valueType = parameterizedType.getActualTypeArguments()[1]; 103 if (!(keyType instanceof Class)) { 104 throw new ConfigurationException( 105 "cannot handle nested parameterized type " + keyType); 106 } else if (!(valueType instanceof Class)) { 107 throw new ConfigurationException( 108 "cannot handle nested parameterized type " + valueType); 109 } 110 111 return new MapHandler(getHandler(keyType), getHandler(valueType)); 112 } else { 113 throw new ConfigurationException(String.format( 114 "can't handle parameterized type %s; only Collection and Map are supported", 115 type)); 116 } 117 } 118 if (type instanceof Class) { 119 Class<?> cType = (Class<?>) type; 120 121 if (cType.isEnum()) { 122 return new EnumHandler(cType); 123 } else if (Collection.class.isAssignableFrom(cType)) { 124 // could handle by just having a default of treating 125 // contents as String but consciously decided this 126 // should be an error 127 throw new ConfigurationException(String.format( 128 "Cannot handle non-parameterized collection %s. Use a generic Collection " 129 + "to specify a desired element type.", type)); 130 } else if (Map.class.isAssignableFrom(cType)) { 131 // could handle by just having a default of treating 132 // contents as String but consciously decided this 133 // should be an error 134 throw new ConfigurationException(String.format( 135 "Cannot handle non-parameterized map %s. Use a generic Map to specify " 136 + "desired element types.", type)); 137 } 138 return handlers.get(cType); 139 } 140 throw new ConfigurationException(String.format("cannot handle unknown field type %s", 141 type)); 142 } 143 144 private final Collection<Object> mOptionSources; 145 private final Map<String, OptionFieldsForName> mOptionMap; 146 147 /** 148 * Container for the list of option fields with given name. 149 * <p/> 150 * Used to enforce constraint that fields with same name can exist in different option sources, 151 * but not the same option source 152 */ 153 private class OptionFieldsForName implements Iterable<Map.Entry<Object, Field>> { 154 155 private Map<Object, Field> mSourceFieldMap = new HashMap<Object, Field>(); 156 157 void addField(String name, Object source, Field field) throws ConfigurationException { 158 if (size() > 0) { 159 Handler existingFieldHandler = getHandler(getFirstField().getGenericType()); 160 Handler newFieldHandler = getHandler(field.getGenericType()); 161 if (!existingFieldHandler.equals(newFieldHandler)) { 162 throw new ConfigurationException(String.format( 163 "@Option field with name '%s' in class '%s' is defined with a " + 164 "different type than same option in class '%s'", 165 name, source.getClass().getName(), 166 getFirstObject().getClass().getName())); 167 } 168 } 169 if (mSourceFieldMap.put(source, field) != null) { 170 throw new ConfigurationException(String.format( 171 "@Option field with name '%s' is defined more than once in class '%s'", 172 name, source.getClass().getName())); 173 } 174 } 175 176 public int size() { 177 return mSourceFieldMap.size(); 178 } 179 180 public Field getFirstField() throws ConfigurationException { 181 if (size() <= 0) { 182 // should never happen 183 throw new ConfigurationException("no option fields found"); 184 } 185 return mSourceFieldMap.values().iterator().next(); 186 } 187 188 public Object getFirstObject() throws ConfigurationException { 189 if (size() <= 0) { 190 // should never happen 191 throw new ConfigurationException("no option fields found"); 192 } 193 return mSourceFieldMap.keySet().iterator().next(); 194 } 195 196 @Override 197 public Iterator<Map.Entry<Object, Field>> iterator() { 198 return mSourceFieldMap.entrySet().iterator(); 199 } 200 } 201 202 /** 203 * Constructs a new OptionParser for setting the @Option fields of 'optionSources'. 204 * @throws ConfigurationException 205 */ 206 public OptionSetter(Object... optionSources) throws ConfigurationException { 207 this(Arrays.asList(optionSources)); 208 } 209 210 /** 211 * Constructs a new OptionParser for setting the @Option fields of 'optionSources'. 212 * @throws ConfigurationException 213 */ 214 public OptionSetter(Collection<Object> optionSources) throws ConfigurationException { 215 mOptionSources = optionSources; 216 mOptionMap = makeOptionMap(); 217 } 218 219 private OptionFieldsForName fieldsForArg(String name) throws ConfigurationException { 220 OptionFieldsForName fields = mOptionMap.get(name); 221 if (fields == null || fields.size() == 0) { 222 throw new ConfigurationException(String.format("Could not find option with name %s", 223 name)); 224 } 225 return fields; 226 } 227 228 /** 229 * Returns a string describing the type of the field with given name. 230 * 231 * @param name the {@link Option} field name 232 * @return a {@link String} describing the field's type 233 * @throws ConfigurationException if field could not be found 234 */ 235 public String getTypeForOption(String name) throws ConfigurationException { 236 return fieldsForArg(name).getFirstField().getType().getSimpleName().toLowerCase(); 237 } 238 239 /** 240 * Sets the value for an option. 241 * @param optionName the name of Option to set 242 * @param valueText the value 243 * @throws ConfigurationException if Option cannot be found or valueText is wrong type 244 */ 245 public void setOptionValue(String optionName, String valueText) throws ConfigurationException { 246 OptionFieldsForName optionFields = fieldsForArg(optionName); 247 for (Map.Entry<Object, Field> fieldEntry : optionFields) { 248 249 Object optionSource = fieldEntry.getKey(); 250 Field field = fieldEntry.getValue(); 251 Handler handler = getHandler(field.getGenericType()); 252 Object value = handler.translate(valueText); 253 if (value == null) { 254 final String type = field.getType().getSimpleName(); 255 throw new ConfigurationException( 256 String.format("Couldn't convert '%s' to a %s for option '%s'", valueText, 257 type, optionName)); 258 } 259 setFieldValue(optionName, optionSource, field, value); 260 } 261 } 262 263 /** 264 * Sets the given {@link Option} fields value. 265 * 266 * @param optionName the {@link Option#name()} 267 * @param optionSource the {@link Object} to set 268 * @param field the {@link Field} 269 * @param value the value to set 270 * @throws ConfigurationException 271 */ 272 @SuppressWarnings("unchecked") 273 static void setFieldValue(String optionName, Object optionSource, Field field, Object value) 274 throws ConfigurationException { 275 try { 276 field.setAccessible(true); 277 if (Collection.class.isAssignableFrom(field.getType())) { 278 Collection collection = (Collection)field.get(optionSource); 279 if (collection == null) { 280 throw new ConfigurationException(String.format( 281 "internal error: no storage allocated for field '%s' (used for " + 282 "option '%s') in class '%s'", 283 field.getName(), optionName, optionSource.getClass().getName())); 284 } 285 if (value instanceof Collection) { 286 collection.addAll((Collection)value); 287 } else { 288 collection.add(value); 289 } 290 } else if (Map.class.isAssignableFrom(field.getType())) { 291 Map map = (Map)field.get(optionSource); 292 if (map == null) { 293 throw new ConfigurationException(String.format( 294 "internal error: no storage allocated for field '%s' (used for " + 295 "option '%s') in class '%s'", 296 field.getName(), optionName, optionSource.getClass().getName())); 297 } 298 if (value instanceof Map) { 299 map.putAll((Map)value); 300 } else { 301 throw new ConfigurationException(String.format( 302 "internal error: value provided for field '%s' is not a map (used " + 303 "for option '%s') in class '%s'", 304 field.getName(), optionName, optionSource.getClass().getName())); 305 } 306 } else { 307 final Option option = field.getAnnotation(Option.class); 308 if (option == null) { 309 // By virtue of us having gotten here, this should never happen. But better 310 // safe than sorry 311 throw new ConfigurationException(String.format( 312 "internal error: @Option annotation for field %s in class %s was " + 313 "unexpectedly null", 314 field.getName(), optionSource.getClass().getName())); 315 } 316 OptionUpdateRule rule = option.updateRule(); 317 field.set(optionSource, rule.update(optionName, optionSource, field, value)); 318 } 319 } catch (IllegalAccessException e) { 320 throw new ConfigurationException(String.format( 321 "internal error when setting option '%s'", optionName), e); 322 } catch (IllegalArgumentException e) { 323 throw new ConfigurationException(String.format( 324 "internal error when setting option '%s'", optionName), e); 325 } 326 } 327 328 /** 329 * Sets the key and value for a Map option. 330 * @param optionName the name of Option to set 331 * @param keyText the key, if applicable. Will be ignored for non-Map fields 332 * @param valueText the value 333 * @throws ConfigurationException if Option cannot be found or valueText is wrong type 334 */ 335 @SuppressWarnings("unchecked") 336 public void setOptionMapValue(String optionName, String keyText, String valueText) 337 throws ConfigurationException { 338 // FIXME: try to unify code paths with setOptionValue 339 OptionFieldsForName optionFields = fieldsForArg(optionName); 340 for (Map.Entry<Object, Field> fieldEntry : optionFields) { 341 342 Object optionSource = fieldEntry.getKey(); 343 Field field = fieldEntry.getValue(); 344 Handler handler = getHandler(field.getGenericType()); 345 if (handler == null || !(handler instanceof MapHandler)) { 346 throw new ConfigurationException("Not a map!"); 347 } 348 349 MapEntry pair = null; 350 try { 351 pair = ((MapHandler) handler).translate(keyText, valueText); 352 if (pair == null) { 353 throw new IllegalArgumentException(); 354 } 355 } catch (IllegalArgumentException e) { 356 ParameterizedType pType = (ParameterizedType) field.getGenericType(); 357 Type keyType = pType.getActualTypeArguments()[0]; 358 Type valueType = pType.getActualTypeArguments()[1]; 359 360 String keyTypeName = ((Class<?>)keyType).getSimpleName().toLowerCase(); 361 String valueTypeName = ((Class<?>)valueType).getSimpleName().toLowerCase(); 362 363 String message = ""; 364 if (e.getMessage().contains("key")) { 365 message = String.format( 366 "Couldn't convert '%s' to a %s for the key of mapoption '%s'", 367 keyText, keyTypeName, optionName); 368 } else if (e.getMessage().contains("value")) { 369 message = String.format( 370 "Couldn't convert '%s' to a %s for the value of mapoption '%s'", 371 valueText, valueTypeName, optionName); 372 } else { 373 message = String.format("Failed to convert key '%s' to type %s and/or " + 374 "value '%s' to type %s for mapoption '%s'", 375 keyText, keyTypeName, valueText, valueTypeName, optionName); 376 } 377 throw new ConfigurationException(message); 378 } 379 try { 380 field.setAccessible(true); 381 if (!Map.class.isAssignableFrom(field.getType())) { 382 throw new ConfigurationException(String.format( 383 "internal error: not a map field!")); 384 } 385 Map map = (Map)field.get(optionSource); 386 if (map == null) { 387 throw new ConfigurationException(String.format( 388 "internal error: no storage allocated for field '%s' (used for " + 389 "option '%s') in class '%s'", 390 field.getName(), optionName, optionSource.getClass().getName())); 391 } 392 map.put(pair.mKey, pair.mValue); 393 } catch (IllegalAccessException e) { 394 throw new ConfigurationException(String.format( 395 "internal error when setting option '%s'", optionName), e); 396 } 397 } 398 } 399 400 /** 401 * Cache the available options and report any problems with the options themselves right away. 402 * 403 * @return a {@link Map} of {@link Option} field name to {@link OptionField}s 404 * @throws ConfigurationException if any {@link Option} are incorrectly specified 405 */ 406 private Map<String, OptionFieldsForName> makeOptionMap() throws ConfigurationException { 407 final Map<String, Integer> freqMap = new HashMap<String, Integer>(mOptionSources.size()); 408 final Map<String, OptionFieldsForName> optionMap = 409 new HashMap<String, OptionFieldsForName>(); 410 for (Object objectSource : mOptionSources) { 411 final String className = objectSource.getClass().getName(); 412 413 // Keep track of how many times we've seen this className. This assumes that we 414 // maintain the optionSources in a universally-knowable order internally (which we do -- 415 // they remain in the order in which they were passed to the constructor). Thus, the 416 // index can serve as a unique identifier for each instance of className as long as 417 // other upstream classes use the same 1-based ordered numbering scheme. 418 Integer index = freqMap.get(className); 419 index = index == null ? 1 : index + 1; 420 freqMap.put(className, index); 421 422 addOptionsForObject(objectSource, optionMap, index); 423 } 424 return optionMap; 425 } 426 427 /** 428 * Adds all option fields (both declared and inherited) to the <var>optionMap</var> for 429 * provided <var>optionClass</var>. 430 * 431 * @param optionSource 432 * @param optionMap 433 * @param index The unique index of this instance of the optionSource class. Should equal the 434 * number of instances of this class that we've already seen, plus 1. 435 * @throws ConfigurationException 436 */ 437 private void addOptionsForObject(Object optionSource, 438 Map<String, OptionFieldsForName> optionMap, Integer index) 439 throws ConfigurationException { 440 Collection<Field> optionFields = getOptionFieldsForClass(optionSource.getClass()); 441 for (Field field : optionFields) { 442 final Option option = field.getAnnotation(Option.class); 443 if (option.name().indexOf(NAMESPACE_SEPARATOR) != -1) { 444 throw new ConfigurationException(String.format( 445 "Option name '%s' in class '%s' is invalid. " + 446 "Option names cannot contain the namespace separator character '%c'", 447 option.name(), optionSource.getClass().getName(), NAMESPACE_SEPARATOR)); 448 } 449 450 // Make sure the source doesn't use GREATEST or LEAST for a non-Comparable field. 451 final Type type = field.getGenericType(); 452 if ((type instanceof Class) && !(type instanceof ParameterizedType)) { 453 // Not a parameterized type 454 if ((option.updateRule() == OptionUpdateRule.GREATEST) || 455 (option.updateRule() == OptionUpdateRule.LEAST)) { 456 Class cType = (Class) type; 457 if (!(Comparable.class.isAssignableFrom(cType))) { 458 throw new ConfigurationException(String.format( 459 "Option '%s' in class '%s' attempts to use updateRule %s with " + 460 "non-Comparable type '%s'.", option.name(), 461 optionSource.getClass().getName(), option.updateRule(), 462 field.getGenericType())); 463 } 464 } 465 466 // don't allow 'final' for non-Collections 467 if ((field.getModifiers() & Modifier.FINAL) != 0) { 468 throw new ConfigurationException(String.format( 469 "Option '%s' in class '%s' is final and cannot be set", option.name(), 470 optionSource.getClass().getName())); 471 } 472 } 473 474 // Allow classes to opt out of the global Option namespace 475 boolean addToGlobalNamespace = true; 476 if (optionSource.getClass().isAnnotationPresent(OptionClass.class)) { 477 final OptionClass classAnnotation = optionSource.getClass().getAnnotation( 478 OptionClass.class); 479 addToGlobalNamespace = classAnnotation.global_namespace(); 480 } 481 482 if (addToGlobalNamespace) { 483 addNameToMap(optionMap, optionSource, option.name(), field); 484 } 485 addNamespacedOptionToMap(optionMap, optionSource, option.name(), field, index); 486 if (option.shortName() != Option.NO_SHORT_NAME) { 487 if (addToGlobalNamespace) { 488 addNameToMap(optionMap, optionSource, String.valueOf(option.shortName()), 489 field); 490 } 491 addNamespacedOptionToMap(optionMap, optionSource, 492 String.valueOf(option.shortName()), field, index); 493 } 494 if (isBooleanField(field)) { 495 // add the corresponding "no" option to make boolean false 496 if (addToGlobalNamespace) { 497 addNameToMap(optionMap, optionSource, BOOL_FALSE_PREFIX + option.name(), field); 498 } 499 addNamespacedOptionToMap(optionMap, optionSource, BOOL_FALSE_PREFIX + option.name(), 500 field, index); 501 } 502 } 503 } 504 505 /** 506 * Returns the names of all of the {@link Option}s that are marked as {@code mandatory} but 507 * remain unset. 508 * 509 * @return A {@link Collection} of {@link String}s containing the (unqualified) names of unset 510 * mandatory options. 511 * @throws ConfigurationException if a field to be checked is inaccessible 512 */ 513 protected Collection<String> getUnsetMandatoryOptions() throws ConfigurationException { 514 Collection<String> unsetOptions = new HashSet<String>(); 515 for (Map.Entry<String, OptionFieldsForName> optionPair : mOptionMap.entrySet()) { 516 final String optName = optionPair.getKey(); 517 final OptionFieldsForName optionFields = optionPair.getValue(); 518 if (optName.indexOf(NAMESPACE_SEPARATOR) >= 0) { 519 // Only return unqualified option names 520 continue; 521 } 522 523 for (Map.Entry<Object, Field> fieldEntry : optionFields) { 524 final Object obj = fieldEntry.getKey(); 525 final Field field = fieldEntry.getValue(); 526 final Option option = field.getAnnotation(Option.class); 527 if (option == null) { 528 continue; 529 } else if (!option.mandatory()) { 530 continue; 531 } 532 533 // At this point, we know this is a mandatory field; make sure it's set 534 field.setAccessible(true); 535 final Object value; 536 try { 537 value = field.get(obj); 538 } catch (IllegalAccessException e) { 539 throw new ConfigurationException(String.format("internal error: %s", 540 e.getMessage())); 541 } 542 543 final String realOptName = String.format("--%s", option.name()); 544 if (value == null) { 545 unsetOptions.add(realOptName); 546 } else if (value instanceof Collection) { 547 Collection c = (Collection) value; 548 if (c.isEmpty()) { 549 unsetOptions.add(realOptName); 550 } 551 } else if (value instanceof Map) { 552 Map m = (Map) value; 553 if (m.isEmpty()) { 554 unsetOptions.add(realOptName); 555 } 556 } 557 } 558 } 559 return unsetOptions; 560 } 561 562 /** 563 * Gets a list of all {@link Option} fields (both declared and inherited) for given class. 564 * 565 * @param optionClass the {@link Class} to search 566 * @return a {@link Collection} of fields annotated with {@link Option} 567 */ 568 static Collection<Field> getOptionFieldsForClass(final Class<?> optionClass) { 569 Collection<Field> fieldList = new ArrayList<Field>(); 570 buildOptionFieldsForClass(optionClass, fieldList); 571 return fieldList; 572 } 573 574 /** 575 * Recursive method that adds all option fields (both declared and inherited) to the 576 * <var>optionFields</var> for provided <var>optionClass</var> 577 * 578 * @param optionClass 579 * @param optionFields 580 */ 581 private static void buildOptionFieldsForClass(final Class<?> optionClass, 582 Collection<Field> optionFields) { 583 for (Field field : optionClass.getDeclaredFields()) { 584 if (field.isAnnotationPresent(Option.class)) { 585 optionFields.add(field); 586 } 587 } 588 Class<?> superClass = optionClass.getSuperclass(); 589 if (superClass != null) { 590 buildOptionFieldsForClass(superClass, optionFields); 591 } 592 } 593 594 /** 595 * Return the given {@link Field}'s value as a {@link String}. 596 * 597 * @param field the {@link Field} 598 * @param optionObject the {@link Object} to get field's value from. 599 * @return the field's value as a {@link String}, or <code>null</code> if field is not set or is 600 * empty (in case of {@link Collection}s 601 */ 602 static String getFieldValueAsString(Field field, Object optionObject) { 603 Object fieldValue = getFieldValue(field, optionObject); 604 if (fieldValue == null) { 605 return null; 606 } 607 if (fieldValue instanceof Collection) { 608 Collection collection = (Collection)fieldValue; 609 if (collection.isEmpty()) { 610 return null; 611 } 612 } else if (fieldValue instanceof Map) { 613 Map map = (Map)fieldValue; 614 if (map.isEmpty()) { 615 return null; 616 } 617 } 618 return fieldValue.toString(); 619 } 620 621 /** 622 * Return the given {@link Field}'s value, handling any exceptions. 623 * 624 * @param field the {@link Field} 625 * @param optionObject the {@link Object} to get field's value from. 626 * @return the field's value as a {@link Object}, or <code>null</code> 627 */ 628 static Object getFieldValue(Field field, Object optionObject) { 629 try { 630 field.setAccessible(true); 631 return field.get(optionObject); 632 } catch (IllegalArgumentException e) { 633 return null; 634 } catch (IllegalAccessException e) { 635 return null; 636 } 637 } 638 639 /** 640 * Returns the help text describing the valid values for the Enum field. 641 * 642 * @param field the {@link Field} to get values for 643 * @return the appropriate help text, or an empty {@link String} if the field is not an Enum. 644 */ 645 static String getEnumFieldValuesAsString(Field field) { 646 Class<?> type = field.getType(); 647 Object[] vals = type.getEnumConstants(); 648 if (vals == null) { 649 return ""; 650 } 651 652 StringBuilder sb = new StringBuilder(" Valid values: ["); 653 sb.append(ArrayUtil.join(", ", vals)); 654 sb.append("]"); 655 return sb.toString(); 656 } 657 658 public boolean isBooleanOption(String name) throws ConfigurationException { 659 Field field = fieldsForArg(name).getFirstField(); 660 return isBooleanField(field); 661 } 662 663 static boolean isBooleanField(Field field) throws ConfigurationException { 664 return getHandler(field.getGenericType()).isBoolean(); 665 } 666 667 public boolean isMapOption(String name) throws ConfigurationException { 668 Field field = fieldsForArg(name).getFirstField(); 669 return isMapField(field); 670 } 671 672 static boolean isMapField(Field field) throws ConfigurationException { 673 return getHandler(field.getGenericType()).isMap(); 674 } 675 676 private void addNameToMap(Map<String, OptionFieldsForName> optionMap, Object optionSource, 677 String name, Field field) throws ConfigurationException { 678 OptionFieldsForName fields = optionMap.get(name); 679 if (fields == null) { 680 fields = new OptionFieldsForName(); 681 optionMap.put(name, fields); 682 } 683 684 fields.addField(name, optionSource, field); 685 if (getHandler(field.getGenericType()) == null) { 686 throw new ConfigurationException(String.format( 687 "Option name '%s' in class '%s' is invalid. Unsupported @Option field type '%s'", 688 name, optionSource.getClass().getName(), field.getType())); 689 } 690 } 691 692 /** 693 * Adds the namespaced versions of the option to the map 694 * 695 * @see {@link #makeOptionMap()} for details on the enumeration scheme 696 */ 697 private void addNamespacedOptionToMap(Map<String, OptionFieldsForName> optionMap, 698 Object optionSource, String name, Field field, int index) 699 throws ConfigurationException { 700 final String className = optionSource.getClass().getName(); 701 702 if (optionSource.getClass().isAnnotationPresent(OptionClass.class)) { 703 final OptionClass classAnnotation = optionSource.getClass().getAnnotation( 704 OptionClass.class); 705 addNameToMap(optionMap, optionSource, String.format("%s%c%s", classAnnotation.alias(), 706 NAMESPACE_SEPARATOR, name), field); 707 708 // Allows use of an enumerated namespace, to enable options to map to specific instances 709 // of a class alias, rather than just to all instances of that particular alias. 710 // Example option name: alias:2:option-name 711 addNameToMap(optionMap, optionSource, String.format("%s%c%d%c%s", 712 classAnnotation.alias(), NAMESPACE_SEPARATOR, index, NAMESPACE_SEPARATOR, name), 713 field); 714 } 715 716 // Allows use of a className-delimited namespace. 717 // Example option name: com.fully.qualified.ClassName:option-name 718 addNameToMap(optionMap, optionSource, String.format("%s%c%s", 719 className, NAMESPACE_SEPARATOR, name), field); 720 721 // Allows use of an enumerated namespace, to enable options to map to specific instances of 722 // a className, rather than just to all instances of that particular className. 723 // Example option name: com.fully.qualified.ClassName:2:option-name 724 addNameToMap(optionMap, optionSource, String.format("%s%c%d%c%s", 725 className, NAMESPACE_SEPARATOR, index, NAMESPACE_SEPARATOR, name), field); 726 } 727 728 private abstract static class Handler { 729 // Only BooleanHandler should ever override this. 730 boolean isBoolean() { 731 return false; 732 } 733 734 // Only MapHandler should ever override this. 735 boolean isMap() { 736 return false; 737 } 738 739 /** 740 * Returns an object of appropriate type for the given Handle, corresponding to 'valueText'. 741 * Returns null on failure. 742 */ 743 abstract Object translate(String valueText); 744 } 745 746 private static class BooleanHandler extends Handler { 747 @Override boolean isBoolean() { 748 return true; 749 } 750 751 @Override 752 Object translate(String valueText) { 753 if (valueText.equalsIgnoreCase("true") || valueText.equalsIgnoreCase("yes")) { 754 return Boolean.TRUE; 755 } else if (valueText.equalsIgnoreCase("false") || valueText.equalsIgnoreCase("no")) { 756 return Boolean.FALSE; 757 } 758 return null; 759 } 760 } 761 762 private static class ByteHandler extends Handler { 763 @Override 764 Object translate(String valueText) { 765 try { 766 return Byte.parseByte(valueText); 767 } catch (NumberFormatException ex) { 768 return null; 769 } 770 } 771 } 772 773 private static class ShortHandler extends Handler { 774 @Override 775 Object translate(String valueText) { 776 try { 777 return Short.parseShort(valueText); 778 } catch (NumberFormatException ex) { 779 return null; 780 } 781 } 782 } 783 784 private static class IntegerHandler extends Handler { 785 @Override 786 Object translate(String valueText) { 787 try { 788 return Integer.parseInt(valueText); 789 } catch (NumberFormatException ex) { 790 return null; 791 } 792 } 793 } 794 795 private static class LongHandler extends Handler { 796 @Override 797 Object translate(String valueText) { 798 try { 799 return Long.parseLong(valueText); 800 } catch (NumberFormatException ex) { 801 return null; 802 } 803 } 804 } 805 806 private static class FloatHandler extends Handler { 807 @Override 808 Object translate(String valueText) { 809 try { 810 return Float.parseFloat(valueText); 811 } catch (NumberFormatException ex) { 812 return null; 813 } 814 } 815 } 816 817 private static class DoubleHandler extends Handler { 818 @Override 819 Object translate(String valueText) { 820 try { 821 return Double.parseDouble(valueText); 822 } catch (NumberFormatException ex) { 823 return null; 824 } 825 } 826 } 827 828 private static class StringHandler extends Handler { 829 @Override 830 Object translate(String valueText) { 831 return valueText; 832 } 833 } 834 835 private static class FileHandler extends Handler { 836 @Override 837 Object translate(String valueText) { 838 return new File(valueText); 839 } 840 } 841 842 private static class MapEntry { 843 public Object mKey = null; 844 public Object mValue = null; 845 846 /** 847 * Convenience constructor 848 */ 849 MapEntry(Object key, Object value) { 850 mKey = key; 851 mValue = value; 852 } 853 } 854 855 /** 856 * A {@see Handler} to handle values for Map fields. The {@code Object} returned is a 857 * MapEntry 858 */ 859 private static class MapHandler extends Handler { 860 private Handler mKeyHandler; 861 private Handler mValueHandler; 862 863 MapHandler(Handler keyHandler, Handler valueHandler) { 864 if (keyHandler == null || valueHandler == null) { 865 throw new NullPointerException(); 866 } 867 868 mKeyHandler = keyHandler; 869 mValueHandler = valueHandler; 870 } 871 872 Handler getKeyHandler() { 873 return mKeyHandler; 874 } 875 876 Handler getValueHandler() { 877 return mValueHandler; 878 } 879 880 /** 881 * {@inheritDoc} 882 */ 883 @Override 884 boolean isMap() { 885 return true; 886 } 887 888 /** 889 * {@inheritDoc} 890 */ 891 @Override 892 public int hashCode() { 893 return Objects.hashCode(MapHandler.class, mKeyHandler, mValueHandler); 894 } 895 896 /** 897 * Define two {@link MapHandler}s as equivalent if their key and value Handlers are 898 * respectively equivalent. 899 * <p /> 900 * {@inheritDoc} 901 */ 902 @Override 903 public boolean equals(Object otherObj) { 904 if ((otherObj != null) && (otherObj instanceof MapHandler)) { 905 MapHandler other = (MapHandler) otherObj; 906 Handler otherKeyHandler = other.getKeyHandler(); 907 Handler otherValueHandler = other.getValueHandler(); 908 909 return mKeyHandler.equals(otherKeyHandler) 910 && mValueHandler.equals(otherValueHandler); 911 } 912 913 return false; 914 } 915 916 /** 917 * {@inheritDoc} 918 */ 919 @Override 920 Object translate(String valueText) { 921 return null; 922 } 923 924 MapEntry translate(String keyText, String valueText) { 925 Object key = mKeyHandler.translate(keyText); 926 Object value = mValueHandler.translate(valueText); 927 if (key == null) { 928 throw new IllegalArgumentException("Failed to parse key"); 929 } else if (value == null) { 930 throw new IllegalArgumentException("Failed to parse value"); 931 } 932 933 return new MapEntry(key, value); 934 } 935 } 936 937 /** 938 * A {@link Handler} to handle values for {@link Enum} fields. 939 */ 940 private static class EnumHandler extends Handler { 941 private final Class mEnumType; 942 943 EnumHandler(Class<?> enumType) { 944 mEnumType = enumType; 945 } 946 947 Class<?> getEnumType() { 948 return mEnumType; 949 } 950 951 /** 952 * {@inheritDoc} 953 */ 954 @Override 955 public int hashCode() { 956 return Objects.hashCode(EnumHandler.class, mEnumType); 957 } 958 959 /** 960 * Define two EnumHandlers as equivalent if their EnumTypes are mutually assignable 961 * <p /> 962 * {@inheritDoc} 963 */ 964 @SuppressWarnings("unchecked") 965 @Override 966 public boolean equals(Object otherObj) { 967 if ((otherObj != null) && (otherObj instanceof EnumHandler)) { 968 EnumHandler other = (EnumHandler) otherObj; 969 Class<?> otherType = other.getEnumType(); 970 971 return mEnumType.isAssignableFrom(otherType) 972 && otherType.isAssignableFrom(mEnumType); 973 } 974 975 return false; 976 } 977 978 /** 979 * {@inheritDoc} 980 */ 981 @Override 982 Object translate(String valueText) { 983 return translate(valueText, true); 984 } 985 986 @SuppressWarnings("unchecked") 987 Object translate(String valueText, boolean shouldTryUpperCase) { 988 try { 989 return Enum.valueOf(mEnumType, valueText); 990 } catch (IllegalArgumentException e) { 991 // Will be thrown if the value can't be mapped back to the enum 992 if (shouldTryUpperCase) { 993 // Try to automatically map variable-case strings to uppercase. This is 994 // reasonable since most Enum constants tend to be uppercase by convention. 995 return translate(valueText.toUpperCase(Locale.ENGLISH), false); 996 } else { 997 return null; 998 } 999 } 1000 } 1001 } 1002 } 1003