// Copyright 2014 Google Inc. All rights reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package com.google.devtools.common.options; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.Map; import javax.annotation.concurrent.Immutable; /** * An immutable selection of options data corresponding to a set of options * classes. The data is collected using reflection, which can be expensive. * Therefore this class can be used internally to cache the results. */ @Immutable final class OptionsData { /** * These are the options-declaring classes which are annotated with * {@link Option} annotations. */ private final Map, Constructor> optionsClasses; /** Maps option name to Option-annotated Field. */ private final Map nameToField; /** Maps option abbreviation to Option-annotated Field. */ private final Map abbrevToField; /** * For each options class, contains a list of all Option-annotated fields in * that class. */ private final Map, List> allOptionsFields; /** * Mapping from each Option-annotated field to the default value for that * field. */ private final Map optionDefaults; /** * Mapping from each Option-annotated field to the proper converter. * * @see OptionsParserImpl#findConverter */ private final Map> converters; private OptionsData(Map, Constructor> optionsClasses, Map nameToField, Map abbrevToField, Map, List> allOptionsFields, Map optionDefaults, Map> converters) { this.optionsClasses = ImmutableMap.copyOf(optionsClasses); this.allOptionsFields = ImmutableMap.copyOf(allOptionsFields); this.nameToField = ImmutableMap.copyOf(nameToField); this.abbrevToField = ImmutableMap.copyOf(abbrevToField); // Can't use an ImmutableMap here because of null values. this.optionDefaults = Collections.unmodifiableMap(optionDefaults); this.converters = ImmutableMap.copyOf(converters); } public Collection> getOptionsClasses() { return optionsClasses.keySet(); } @SuppressWarnings("unchecked") // The construction ensures that the case is always valid. public Constructor getConstructor(Class clazz) { return (Constructor) optionsClasses.get(clazz); } public Field getFieldFromName(String name) { return nameToField.get(name); } public Iterable> getAllNamedFields() { return nameToField.entrySet(); } public Field getFieldForAbbrev(char abbrev) { return abbrevToField.get(abbrev); } public List getFieldsForClass(Class optionsClass) { return allOptionsFields.get(optionsClass); } public Object getDefaultValue(Field field) { return optionDefaults.get(field); } public Converter getConverter(Field field) { return converters.get(field); } private static List getAllAnnotatedFields(Class optionsClass) { List allFields = Lists.newArrayList(); for (Field field : optionsClass.getFields()) { if (field.isAnnotationPresent(Option.class)) { allFields.add(field); } } if (allFields.isEmpty()) { throw new IllegalStateException(optionsClass + " has no public @Option-annotated fields"); } return ImmutableList.copyOf(allFields); } private static Object retrieveDefaultFromAnnotation(Field optionField) { Option annotation = optionField.getAnnotation(Option.class); // If an option can be specified multiple times, its default value is a new empty list. if (annotation.allowMultiple()) { return Collections.emptyList(); } String defaultValueString = OptionsParserImpl.getDefaultOptionString(optionField); try { return OptionsParserImpl.isSpecialNullDefault(defaultValueString, optionField) ? null : OptionsParserImpl.findConverter(optionField).convert(defaultValueString); } catch (OptionsParsingException e) { throw new IllegalStateException("OptionsParsingException while " + "retrieving default for " + optionField.getName() + ": " + e.getMessage()); } } static OptionsData of(Collection> classes) { Map, Constructor> constructorBuilder = Maps.newHashMap(); Map, List> allOptionsFieldsBuilder = Maps.newHashMap(); Map nameToFieldBuilder = Maps.newHashMap(); Map abbrevToFieldBuilder = Maps.newHashMap(); Map optionDefaultsBuilder = Maps.newHashMap(); Map> convertersBuilder = Maps.newHashMap(); // Read all Option annotations: for (Class parsedOptionsClass : classes) { try { Constructor constructor = parsedOptionsClass.getConstructor(new Class[0]); constructorBuilder.put(parsedOptionsClass, constructor); } catch (NoSuchMethodException e) { throw new IllegalArgumentException(parsedOptionsClass + " lacks an accessible default constructor"); } List fields = getAllAnnotatedFields(parsedOptionsClass); allOptionsFieldsBuilder.put(parsedOptionsClass, fields); for (Field field : fields) { Option annotation = field.getAnnotation(Option.class); // Check that the field type is a List, and that the converter // type matches the element type of the list. Type fieldType = field.getGenericType(); if (annotation.allowMultiple()) { if (!(fieldType instanceof ParameterizedType)) { throw new AssertionError("Type of multiple occurrence option must be a List<...>"); } ParameterizedType pfieldType = (ParameterizedType) fieldType; if (pfieldType.getRawType() != List.class) { // Throw an assertion, because this indicates an undetected type // error in the code. throw new AssertionError("Type of multiple occurrence option must be a List<...>"); } fieldType = pfieldType.getActualTypeArguments()[0]; } // Get the converter return type. @SuppressWarnings("rawtypes") Class converter = annotation.converter(); if (converter == Converter.class) { Converter actualConverter = OptionsParserImpl.DEFAULT_CONVERTERS.get(fieldType); if (actualConverter == null) { throw new AssertionError("Cannot find converter for field of type " + field.getType() + " named " + field.getName() + " in class " + field.getDeclaringClass().getName()); } converter = actualConverter.getClass(); } if (Modifier.isAbstract(converter.getModifiers())) { throw new AssertionError("The converter type (" + converter + ") must be a concrete type"); } Type converterResultType; try { Method convertMethod = converter.getMethod("convert", String.class); converterResultType = GenericTypeHelper.getActualReturnType(converter, convertMethod); } catch (NoSuchMethodException e) { throw new AssertionError("A known converter object doesn't implement the convert" + " method"); } if (annotation.allowMultiple()) { if (GenericTypeHelper.getRawType(converterResultType) == List.class) { Type elementType = ((ParameterizedType) converterResultType).getActualTypeArguments()[0]; if (!GenericTypeHelper.isAssignableFrom(fieldType, elementType)) { throw new AssertionError("If the converter return type of a multiple occurance " + "option is a list, then the type of list elements (" + fieldType + ") must be " + "assignable from the converter list element type (" + elementType + ")"); } } else { if (!GenericTypeHelper.isAssignableFrom(fieldType, converterResultType)) { throw new AssertionError("Type of list elements (" + fieldType + ") for multiple occurrence option must be assignable from the converter " + "return type (" + converterResultType + ")"); } } } else { if (!GenericTypeHelper.isAssignableFrom(fieldType, converterResultType)) { throw new AssertionError("Type of field (" + fieldType + ") must be assignable from the converter " + "return type (" + converterResultType + ")"); } } if (annotation.name() == null) { throw new AssertionError( "Option cannot have a null name"); } if (nameToFieldBuilder.put(annotation.name(), field) != null) { throw new DuplicateOptionDeclarationException( "Duplicate option name: --" + annotation.name()); } if (annotation.abbrev() != '\0') { if (abbrevToFieldBuilder.put(annotation.abbrev(), field) != null) { throw new DuplicateOptionDeclarationException( "Duplicate option abbrev: -" + annotation.abbrev()); } } optionDefaultsBuilder.put(field, retrieveDefaultFromAnnotation(field)); convertersBuilder.put(field, OptionsParserImpl.findConverter(field)); } } return new OptionsData(constructorBuilder, nameToFieldBuilder, abbrevToFieldBuilder, allOptionsFieldsBuilder, optionDefaultsBuilder, convertersBuilder); } }