| /* |
| * Copyright 2019 The Android Open Source Project |
| * |
| * 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 android.processor.view.inspector; |
| |
| import android.processor.view.inspector.InspectableClassModel.IntEnumEntry; |
| import android.processor.view.inspector.InspectableClassModel.IntFlagEntry; |
| import android.processor.view.inspector.InspectableClassModel.Property; |
| |
| import java.util.ArrayList; |
| import java.util.List; |
| import java.util.Optional; |
| import java.util.Set; |
| import java.util.regex.Pattern; |
| |
| import javax.annotation.processing.ProcessingEnvironment; |
| import javax.lang.model.element.AnnotationMirror; |
| import javax.lang.model.element.AnnotationValue; |
| import javax.lang.model.element.Element; |
| import javax.lang.model.element.ElementKind; |
| import javax.lang.model.element.ExecutableElement; |
| import javax.lang.model.element.Modifier; |
| import javax.lang.model.element.TypeElement; |
| import javax.lang.model.type.NoType; |
| import javax.lang.model.type.TypeKind; |
| import javax.lang.model.type.TypeMirror; |
| |
| /** |
| * Process {@code @InspectableProperty} annotations. |
| * |
| * @see android.view.inspector.InspectableProperty |
| */ |
| public final class InspectablePropertyProcessor implements ModelProcessor { |
| private final String mQualifiedName; |
| private final ProcessingEnvironment mProcessingEnv; |
| private final AnnotationUtils mAnnotationUtils; |
| |
| /** |
| * Regex that matches methods names of the form {@code #getValue()}. |
| */ |
| private static final Pattern GETTER_GET_PREFIX = Pattern.compile("\\Aget[A-Z]"); |
| |
| /** |
| * Regex that matches method name of the form {@code #isPredicate()}. |
| */ |
| private static final Pattern GETTER_IS_PREFIX = Pattern.compile("\\Ais[A-Z]"); |
| |
| /** |
| * Set of android and androidx annotation qualified names for colors packed into {@code int}. |
| * |
| * @see android.annotation.ColorInt |
| */ |
| private static final String[] COLOR_INT_ANNOTATION_NAMES = { |
| "android.annotation.ColorInt", |
| "androidx.annotation.ColorInt"}; |
| |
| /** |
| * Set of android and androidx annotation qualified names for colors packed into {@code long}. |
| * |
| * @see android.annotation.ColorLong |
| */ |
| private static final String[] COLOR_LONG_ANNOTATION_NAMES = { |
| "android.annotation.ColorLong", |
| "androidx.annotation.ColorLong"}; |
| |
| /** |
| * @param annotationQualifiedName The qualified name of the annotation to process |
| * @param processingEnv The processing environment from the parent processor |
| */ |
| public InspectablePropertyProcessor( |
| String annotationQualifiedName, |
| ProcessingEnvironment processingEnv) { |
| mQualifiedName = annotationQualifiedName; |
| mProcessingEnv = processingEnv; |
| mAnnotationUtils = new AnnotationUtils(processingEnv); |
| } |
| |
| @Override |
| public void process(Element element, InspectableClassModel model) { |
| try { |
| final AnnotationMirror annotation = |
| mAnnotationUtils.exactlyOneMirror(mQualifiedName, element); |
| final ExecutableElement getter = ensureGetter(element); |
| final Property property = buildProperty(getter, annotation); |
| |
| model.getProperty(property.getName()).ifPresent(p -> { |
| throw new ProcessingException( |
| String.format( |
| "Property \"%s\" is already defined on #%s().", |
| p.getName(), |
| p.getGetter()), |
| getter, |
| annotation); |
| }); |
| |
| model.putProperty(property); |
| } catch (ProcessingException processingException) { |
| processingException.print(mProcessingEnv.getMessager()); |
| } |
| } |
| |
| /** |
| * Check that an element is shaped like a getter. |
| * |
| * @param element An element that hopefully represents a getter |
| * @return An {@link ExecutableElement} that represents a getter method. |
| * @throws ProcessingException if the element isn't a getter |
| */ |
| private ExecutableElement ensureGetter(Element element) { |
| if (element.getKind() != ElementKind.METHOD) { |
| throw new ProcessingException( |
| String.format("Expected a method, got a %s", element.getKind()), |
| element); |
| } |
| |
| final ExecutableElement method = (ExecutableElement) element; |
| final Set<Modifier> modifiers = method.getModifiers(); |
| |
| if (modifiers.contains(Modifier.PRIVATE)) { |
| throw new ProcessingException( |
| "Property getter methods must not be private.", |
| element); |
| } |
| |
| if (modifiers.contains(Modifier.ABSTRACT)) { |
| throw new ProcessingException( |
| "Property getter methods must not be abstract.", |
| element); |
| } |
| |
| if (modifiers.contains(Modifier.STATIC)) { |
| throw new ProcessingException( |
| "Property getter methods must not be static.", |
| element); |
| } |
| |
| if (!method.getParameters().isEmpty()) { |
| throw new ProcessingException( |
| String.format( |
| "Expected a getter method to take no parameters, " |
| + "but got %d parameters.", |
| method.getParameters().size()), |
| element); |
| } |
| |
| if (method.isVarArgs()) { |
| throw new ProcessingException( |
| "Expected a getter method to take no arguments, but got a var args method.", |
| element); |
| } |
| |
| if (method.getReturnType() instanceof NoType) { |
| throw new ProcessingException( |
| "Expected a getter to have a return type, got void.", |
| element); |
| } |
| |
| return method; |
| } |
| |
| /** |
| * Build a {@link Property} from a getter and an inspectable property annotation. |
| * |
| * @param getter An element representing the getter to build from |
| * @param annotation A mirror of an inspectable property-shaped annotation |
| * @return A property for the getter and annotation |
| * @throws ProcessingException If the supplied data is invalid and a property cannot be modeled |
| */ |
| private Property buildProperty(ExecutableElement getter, AnnotationMirror annotation) { |
| final String name = mAnnotationUtils |
| .typedValueByName("name", String.class, getter, annotation) |
| .orElseGet(() -> inferPropertyNameFromGetter(getter)); |
| |
| final Property property = new Property( |
| name, |
| getter.getSimpleName().toString(), |
| determinePropertyType(getter, annotation)); |
| |
| mAnnotationUtils |
| .typedValueByName("hasAttributeId", Boolean.class, getter, annotation) |
| .ifPresent(property::setAttributeIdInferrableFromR); |
| |
| mAnnotationUtils |
| .typedValueByName("attributeId", Integer.class, getter, annotation) |
| .ifPresent(property::setAttributeId); |
| |
| switch (property.getType()) { |
| case INT_ENUM: |
| property.setIntEnumEntries(processEnumMapping(getter, annotation)); |
| break; |
| case INT_FLAG: |
| property.setIntFlagEntries(processFlagMapping(getter, annotation)); |
| break; |
| } |
| |
| return property; |
| } |
| |
| /** |
| * Determine the property type from the annotation, return type, or context clues. |
| * |
| * @param getter An element representing the getter to build from |
| * @param annotation A mirror of an inspectable property-shaped annotation |
| * @return The resolved property type |
| * @throws ProcessingException If the property type cannot be resolved or is invalid |
| * @see android.view.inspector.InspectableProperty#valueType() |
| */ |
| private Property.Type determinePropertyType( |
| ExecutableElement getter, |
| AnnotationMirror annotation) { |
| |
| final String valueType = mAnnotationUtils |
| .untypedValueByName("valueType", getter, annotation) |
| .map(Object::toString) |
| .orElse("INFERRED"); |
| |
| final Property.Type returnType = convertReturnTypeToPropertyType(getter); |
| |
| final boolean hasColor = hasColorAnnotation(getter); |
| final Optional<AnnotationValue> enumMapping = |
| mAnnotationUtils.valueByName("enumMapping", annotation); |
| final Optional<AnnotationValue> flagMapping = |
| mAnnotationUtils.valueByName("flagMapping", annotation); |
| |
| if (returnType != Property.Type.INT) { |
| enumMapping.ifPresent(value -> { |
| throw new ProcessingException( |
| String.format( |
| "Can only use enumMapping on int types, got %s.", |
| returnType.toString().toLowerCase()), |
| getter, |
| annotation, |
| value); |
| }); |
| flagMapping.ifPresent(value -> { |
| throw new ProcessingException( |
| String.format( |
| "Can only use flagMapping on int types, got %s.", |
| returnType.toString().toLowerCase()), |
| getter, |
| annotation, |
| value); |
| }); |
| } |
| |
| switch (valueType) { |
| case "INFERRED": |
| if (hasColor) { |
| enumMapping.ifPresent(value -> { |
| throw new ProcessingException( |
| "Cannot use enumMapping on a color type.", |
| getter, |
| annotation, |
| value); |
| }); |
| flagMapping.ifPresent(value -> { |
| throw new ProcessingException( |
| "Cannot use flagMapping on a color type.", |
| getter, |
| annotation, |
| value); |
| }); |
| return Property.Type.COLOR; |
| } else if (enumMapping.isPresent()) { |
| flagMapping.ifPresent(value -> { |
| throw new ProcessingException( |
| "Cannot use flagMapping and enumMapping simultaneously.", |
| getter, |
| annotation, |
| value); |
| }); |
| return Property.Type.INT_ENUM; |
| } else if (flagMapping.isPresent()) { |
| return Property.Type.INT_FLAG; |
| } else { |
| return returnType; |
| } |
| case "NONE": |
| return returnType; |
| case "COLOR": |
| switch (returnType) { |
| case COLOR: |
| case INT: |
| case LONG: |
| return Property.Type.COLOR; |
| default: |
| throw new ProcessingException( |
| "Color must be a long, integer, or android.graphics.Color", |
| getter, |
| annotation); |
| } |
| case "GRAVITY": |
| requirePackedIntToReturnInt("Gravity", returnType, getter, annotation); |
| return Property.Type.GRAVITY; |
| case "INT_ENUM": |
| requirePackedIntToReturnInt("IntEnum", returnType, getter, annotation); |
| return Property.Type.INT_ENUM; |
| case "INT_FLAG": |
| requirePackedIntToReturnInt("IntFlag", returnType, getter, annotation); |
| return Property.Type.INT_FLAG; |
| default: |
| throw new ProcessingException( |
| String.format("Unknown value type enumeration value: %s", valueType), |
| getter, |
| annotation); |
| } |
| } |
| |
| /** |
| * Get a property type from the return type of a getter. |
| * |
| * @param getter The getter to extract the return type of |
| * @return The property type returned by the getter |
| * @throws ProcessingException If the return type is not a primitive or an object |
| */ |
| private Property.Type convertReturnTypeToPropertyType(ExecutableElement getter) { |
| final TypeMirror returnType = getter.getReturnType(); |
| |
| switch (unboxType(returnType)) { |
| case BOOLEAN: |
| return Property.Type.BOOLEAN; |
| case BYTE: |
| return Property.Type.BYTE; |
| case CHAR: |
| return Property.Type.CHAR; |
| case DOUBLE: |
| return Property.Type.DOUBLE; |
| case FLOAT: |
| return Property.Type.FLOAT; |
| case INT: |
| return Property.Type.INT; |
| case LONG: |
| return Property.Type.LONG; |
| case SHORT: |
| return Property.Type.SHORT; |
| case DECLARED: |
| if (isColorType(returnType)) { |
| return Property.Type.COLOR; |
| } else { |
| return Property.Type.OBJECT; |
| } |
| case ARRAY: |
| return Property.Type.OBJECT; |
| default: |
| throw new ProcessingException( |
| String.format("Unsupported return type %s.", returnType), |
| getter); |
| } |
| } |
| |
| /** |
| * Require that a value type packed into an integer be on a getter that returns an int. |
| * |
| * @param typeName The name of the type to use in the exception |
| * @param returnType The return type of the getter to check |
| * @param getter The getter, to use in the exception |
| * @param annotation The annotation, to use in the exception |
| * @throws ProcessingException If the return type is not an int |
| */ |
| private static void requirePackedIntToReturnInt( |
| String typeName, |
| Property.Type returnType, |
| ExecutableElement getter, |
| AnnotationMirror annotation) { |
| if (returnType != Property.Type.INT) { |
| throw new ProcessingException( |
| String.format( |
| "%s can only be defined on a method that returns int, got %s.", |
| typeName, |
| returnType.toString().toLowerCase()), |
| getter, |
| annotation); |
| } |
| } |
| |
| /** |
| * Determine if a getter is annotated with color annotation matching its return type. |
| * |
| * Note that an {@code int} return value annotated with {@link android.annotation.ColorLong} is |
| * not considered to be annotated, nor is a {@code long} annotated with |
| * {@link android.annotation.ColorInt}. |
| * |
| * @param getter The getter to query |
| * @return True if the getter has a color annotation, false otherwise |
| */ |
| private boolean hasColorAnnotation(ExecutableElement getter) { |
| switch (unboxType(getter.getReturnType())) { |
| case INT: |
| for (String name : COLOR_INT_ANNOTATION_NAMES) { |
| if (mAnnotationUtils.hasAnnotation(getter, name)) { |
| return true; |
| } |
| } |
| return false; |
| case LONG: |
| for (String name : COLOR_LONG_ANNOTATION_NAMES) { |
| if (mAnnotationUtils.hasAnnotation(getter, name)) { |
| return true; |
| } |
| } |
| return false; |
| default: |
| return false; |
| } |
| } |
| |
| /** |
| * Infer a property name from a getter method. |
| * |
| * If the method is prefixed with {@code get}, the prefix will be stripped, and the |
| * capitalization fixed. E.g.: {@code getSomeProperty} to {@code someProperty}. |
| * |
| * Additionally, if the method's return type is a boolean, an {@code is} prefix will also be |
| * stripped. E.g.: {@code isPropertyEnabled} to {@code propertyEnabled}. |
| * |
| * Failing that, this method will just return the full name of the getter. |
| * |
| * @param getter An element representing a getter |
| * @return A string property name |
| */ |
| private String inferPropertyNameFromGetter(ExecutableElement getter) { |
| final String name = getter.getSimpleName().toString(); |
| |
| if (GETTER_GET_PREFIX.matcher(name).find()) { |
| return name.substring(3, 4).toLowerCase() + name.substring(4); |
| } else if (isBoolean(getter.getReturnType()) && GETTER_IS_PREFIX.matcher(name).find()) { |
| return name.substring(2, 3).toLowerCase() + name.substring(3); |
| } else { |
| return name; |
| } |
| } |
| |
| /** |
| * Build a model of an {@code int} enumeration mapping from annotation values. |
| * |
| * This method only handles the one-to-one mapping of mirrors of |
| * {@link android.view.inspector.InspectableProperty.EnumMap} annotations into |
| * {@link IntEnumEntry} objects. Further validation should be handled elsewhere |
| * |
| * @see android.view.inspector.IntEnumMapping |
| * @see android.view.inspector.InspectableProperty#enumMapping() |
| * @param getter The getter of the property, used for exceptions |
| * @param annotation The {@link android.view.inspector.InspectableProperty} annotation to |
| * extract enum mapping values from. |
| * @return A list of int enum entries, in the order specified in source |
| * @throws ProcessingException if mapping doesn't exist or is invalid |
| */ |
| private List<IntEnumEntry> processEnumMapping( |
| ExecutableElement getter, |
| AnnotationMirror annotation) { |
| List<AnnotationMirror> enumAnnotations = mAnnotationUtils.typedArrayValuesByName( |
| "enumMapping", AnnotationMirror.class, getter, annotation); |
| List<IntEnumEntry> enumEntries = new ArrayList<>(enumAnnotations.size()); |
| |
| if (enumAnnotations.isEmpty()) { |
| throw new ProcessingException( |
| "Encountered an empty array for enumMapping", getter, annotation); |
| } |
| |
| for (AnnotationMirror enumAnnotation : enumAnnotations) { |
| final String name = mAnnotationUtils.typedValueByName( |
| "name", String.class, getter, enumAnnotation) |
| .orElseThrow(() -> { |
| throw new ProcessingException( |
| "Name is required for @EnumMap", |
| getter, |
| enumAnnotation); |
| }); |
| |
| final int value = mAnnotationUtils.typedValueByName( |
| "value", Integer.class, getter, enumAnnotation) |
| .orElseThrow(() -> { |
| throw new ProcessingException( |
| "Value is required for @EnumMap", |
| getter, |
| enumAnnotation); |
| }); |
| |
| enumEntries.add(new IntEnumEntry(name, value)); |
| } |
| |
| return enumEntries; |
| } |
| |
| /** |
| * Build a model of an {@code int} flag mapping from annotation values. |
| * |
| * This method only handles the one-to-one mapping of mirrors of |
| * {@link android.view.inspector.InspectableProperty.FlagMap} annotations into |
| * {@link IntFlagEntry} objects. Further validation should be handled elsewhere |
| * |
| * @see android.view.inspector.IntFlagMapping |
| * @see android.view.inspector.InspectableProperty#flagMapping() |
| * @param getter The getter of the property, used for exceptions |
| * @param annotation The {@link android.view.inspector.InspectableProperty} annotation to |
| * extract flag mapping values from. |
| * @return A list of int flags entries, in the order specified in source |
| * @throws ProcessingException if mapping doesn't exist or is invalid |
| */ |
| private List<IntFlagEntry> processFlagMapping( |
| ExecutableElement getter, |
| AnnotationMirror annotation) { |
| List<AnnotationMirror> flagAnnotations = mAnnotationUtils.typedArrayValuesByName( |
| "flagMapping", AnnotationMirror.class, getter, annotation); |
| List<IntFlagEntry> flagEntries = new ArrayList<>(flagAnnotations.size()); |
| |
| if (flagAnnotations.isEmpty()) { |
| throw new ProcessingException( |
| "Encountered an empty array for flagMapping", getter, annotation); |
| } |
| |
| for (AnnotationMirror flagAnnotation : flagAnnotations) { |
| final String name = mAnnotationUtils.typedValueByName( |
| "name", String.class, getter, flagAnnotation) |
| .orElseThrow(() -> { |
| throw new ProcessingException( |
| "Name is required for @FlagMap", |
| getter, |
| flagAnnotation); |
| }); |
| |
| final int target = mAnnotationUtils.typedValueByName( |
| "target", Integer.class, getter, flagAnnotation) |
| .orElseThrow(() -> { |
| throw new ProcessingException( |
| "Target is required for @FlagMap", |
| getter, |
| flagAnnotation); |
| }); |
| |
| final Optional<Integer> mask = mAnnotationUtils.typedValueByName( |
| "mask", Integer.class, getter, flagAnnotation); |
| |
| if (mask.isPresent()) { |
| flagEntries.add(new IntFlagEntry(name, target, mask.get())); |
| } else { |
| flagEntries.add(new IntFlagEntry(name, target)); |
| } |
| } |
| |
| return flagEntries; |
| } |
| |
| /** |
| * Determine if a {@link TypeMirror} is a boxed or unboxed boolean. |
| * |
| * @param type The type mirror to check |
| * @return True if the type is a boolean |
| */ |
| private boolean isBoolean(TypeMirror type) { |
| if (type.getKind() == TypeKind.DECLARED) { |
| return mProcessingEnv.getTypeUtils().unboxedType(type).getKind() == TypeKind.BOOLEAN; |
| } else { |
| return type.getKind() == TypeKind.BOOLEAN; |
| } |
| } |
| |
| /** |
| * Unbox a type mirror if it represents a boxed type, otherwise pass it through. |
| * |
| * @param typeMirror The type mirror to unbox |
| * @return The same type mirror, or an unboxed primitive version |
| */ |
| private TypeKind unboxType(TypeMirror typeMirror) { |
| final TypeKind typeKind = typeMirror.getKind(); |
| |
| if (typeKind.isPrimitive()) { |
| return typeKind; |
| } else if (typeKind == TypeKind.DECLARED) { |
| try { |
| return mProcessingEnv.getTypeUtils().unboxedType(typeMirror).getKind(); |
| } catch (IllegalArgumentException e) { |
| return typeKind; |
| } |
| } else { |
| return typeKind; |
| } |
| } |
| |
| /** |
| * Determine if a type mirror represents a subtype of {@link android.graphics.Color}. |
| * |
| * @param typeMirror The type mirror to test |
| * @return True if it represents a subclass of color, false otherwise |
| */ |
| private boolean isColorType(TypeMirror typeMirror) { |
| final TypeElement colorType = mProcessingEnv |
| .getElementUtils() |
| .getTypeElement("android.graphics.Color"); |
| |
| if (colorType == null) { |
| return false; |
| } else { |
| return mProcessingEnv.getTypeUtils().isSubtype(typeMirror, colorType.asType()); |
| } |
| } |
| } |