| /* |
| * 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 androidx.annotation.NonNull; |
| |
| import com.squareup.javapoet.ClassName; |
| import com.squareup.javapoet.CodeBlock; |
| import com.squareup.javapoet.FieldSpec; |
| import com.squareup.javapoet.JavaFile; |
| import com.squareup.javapoet.MethodSpec; |
| import com.squareup.javapoet.NameAllocator; |
| import com.squareup.javapoet.ParameterizedTypeName; |
| import com.squareup.javapoet.TypeName; |
| import com.squareup.javapoet.TypeSpec; |
| |
| import java.io.IOException; |
| import java.util.ArrayList; |
| import java.util.Comparator; |
| import java.util.HashMap; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.NoSuchElementException; |
| import java.util.stream.Collectors; |
| |
| import javax.annotation.processing.Filer; |
| import javax.lang.model.element.Modifier; |
| |
| /** |
| * Generates a source file defining a {@link android.view.inspector.InspectionCompanion}. |
| */ |
| public final class InspectionCompanionGenerator { |
| private final @NonNull Filer mFiler; |
| private final @NonNull Class mRequestingClass; |
| |
| /** |
| * The class name for {@code R.java}. |
| */ |
| private static final ClassName R_CLASS_NAME = ClassName.get("android", "R"); |
| |
| /** |
| * The class name of {@link android.view.inspector.InspectionCompanion}. |
| */ |
| private static final ClassName INSPECTION_COMPANION = ClassName.get( |
| "android.view.inspector", "InspectionCompanion"); |
| |
| /** |
| * The class name of {@link android.view.inspector.PropertyMapper}. |
| */ |
| private static final ClassName PROPERTY_MAPPER = ClassName.get( |
| "android.view.inspector", "PropertyMapper"); |
| |
| /** |
| * The class name of {@link android.view.inspector.PropertyReader}. |
| */ |
| private static final ClassName PROPERTY_READER = ClassName.get( |
| "android.view.inspector", "PropertyReader"); |
| |
| /** |
| * The class name of {@link android.util.SparseArray}. |
| */ |
| private static final ClassName SPARSE_ARRAY = ClassName.get("android.util", "SparseArray"); |
| |
| /** |
| * The class name of {@link android.view.inspector.IntFlagMapping}. |
| */ |
| private static final ClassName INT_FLAG_MAPPING = ClassName.get( |
| "android.view.inspector", "IntFlagMapping"); |
| |
| /** |
| * The suffix of the generated class name after the class's binary name. |
| */ |
| private static final String GENERATED_CLASS_SUFFIX = "$InspectionCompanion"; |
| |
| /** |
| * The null resource ID, copied to avoid a host dependency on platform code. |
| * |
| * @see android.content.res.Resources#ID_NULL |
| */ |
| private static final int ID_NULL = 0; |
| |
| /** |
| * @param filer A filer to write the generated source to |
| * @param requestingClass A class object representing the class that invoked the generator |
| */ |
| public InspectionCompanionGenerator(@NonNull Filer filer, @NonNull Class requestingClass) { |
| mFiler = filer; |
| mRequestingClass = requestingClass; |
| } |
| |
| /** |
| * Generate and write an inspection companion. |
| * |
| * @param model The model to generated |
| * @throws IOException From the Filer |
| */ |
| public void generate(@NonNull InspectableClassModel model) throws IOException { |
| generateFile(model).writeTo(mFiler); |
| } |
| |
| /** |
| * Generate a {@link JavaFile} from a model. |
| * |
| * This is package-public for testing. |
| * |
| * @param model The model to generate from |
| * @return A generated file of an {@link android.view.inspector.InspectionCompanion} |
| */ |
| @NonNull |
| JavaFile generateFile(@NonNull InspectableClassModel model) { |
| return JavaFile |
| .builder(model.getClassName().packageName(), generateTypeSpec(model)) |
| .indent(" ") |
| .build(); |
| } |
| |
| /** |
| * Generate a {@link TypeSpec} for the {@link android.view.inspector.InspectionCompanion} |
| * for the supplied model. |
| * |
| * @param model The model to generate from |
| * @return A TypeSpec of the inspection companion |
| */ |
| @NonNull |
| private TypeSpec generateTypeSpec(@NonNull InspectableClassModel model) { |
| final List<Property> properties = new ArrayList<>(model.getAllProperties()); |
| properties.sort(Comparator.comparing(Property::getName)); |
| |
| final Map<Property, FieldSpec> fields = generateIdFieldSpecs(properties); |
| |
| TypeSpec.Builder builder = TypeSpec |
| .classBuilder(generateClassName(model)) |
| .addModifiers(Modifier.PUBLIC, Modifier.FINAL) |
| .addSuperinterface(ParameterizedTypeName.get( |
| INSPECTION_COMPANION, model.getClassName())) |
| .addJavadoc("Inspection companion for {@link $T}.\n\n", model.getClassName()) |
| .addJavadoc("Generated by {@link $T}\n", getClass()) |
| .addJavadoc("on behalf of {@link $T}.\n", mRequestingClass) |
| .addField(FieldSpec |
| .builder(TypeName.BOOLEAN, "mPropertiesMapped", Modifier.PRIVATE) |
| .initializer("false") |
| .addJavadoc("Guards against reading properties before mapping them.\n") |
| .build()) |
| .addFields(properties.stream().map(fields::get).collect(Collectors.toList())) |
| .addMethod(generateMapProperties(properties, fields)) |
| .addMethod(generateReadProperties(properties, fields, model.getClassName())); |
| |
| return builder.build(); |
| } |
| |
| /** |
| * Map properties to fields to store the mapping IDs in the generated inspection companion. |
| * |
| * @param properties A list of property models |
| * @return A map of properties to their {@link FieldSpec} |
| */ |
| @NonNull |
| private Map<Property, FieldSpec> generateIdFieldSpecs(@NonNull List<Property> properties) { |
| final Map<Property, FieldSpec> fields = new HashMap<>(); |
| final NameAllocator fieldNames = new NameAllocator(); |
| fieldNames.newName("mPropertiesMapped"); |
| |
| for (Property property : properties) { |
| final String memberName = fieldNames.newName(String.format( |
| "m%s%sId", |
| property.getName().substring(0, 1).toUpperCase(), |
| property.getName().substring(1))); |
| |
| fields.put(property, FieldSpec |
| .builder(TypeName.INT, memberName, Modifier.PRIVATE) |
| .addJavadoc("Property ID of {@code $L}.\n", property.getName()) |
| .build()); |
| } |
| |
| return fields; |
| } |
| |
| /** |
| * Generates an implementation of |
| * {@link android.view.inspector.InspectionCompanion#mapProperties( |
| * android.view.inspector.PropertyMapper)}. |
| * |
| * Example: |
| * <pre> |
| * @Override |
| * public void mapProperties(PropertyMapper propertyMapper) { |
| * mValueId = propertyMapper.mapInt("value", R.attr.value); |
| * mPropertiesMapped = true; |
| * } |
| * </pre> |
| * |
| * @param properties A sorted list of property models |
| * @param fields A map of properties to their ID field specs |
| * @return A method definition |
| */ |
| @NonNull |
| private MethodSpec generateMapProperties( |
| @NonNull List<Property> properties, |
| @NonNull Map<Property, FieldSpec> fields) { |
| final NameAllocator mappingVariables = new NameAllocator(); |
| |
| final MethodSpec.Builder builder = MethodSpec.methodBuilder("mapProperties") |
| .addAnnotation(Override.class) |
| .addModifiers(Modifier.PUBLIC) |
| .addParameter(PROPERTY_MAPPER, "propertyMapper"); |
| |
| // Reserve existing names |
| mappingVariables.newName("mPropertiesMapped"); |
| mappingVariables.newName("propertyMapper"); |
| properties.forEach(p -> mappingVariables.newName(fields.get(p).name)); |
| |
| for (Property property : properties) { |
| final FieldSpec field = fields.get(property); |
| switch (property.getType()) { |
| case INT_ENUM: |
| builder.addCode(generateIntEnumPropertyMapperInvocation( |
| property, |
| field, |
| mappingVariables.newName(property.getName() + "EnumMapping"))); |
| break; |
| case INT_FLAG: |
| builder.addCode(generateIntFlagPropertyMapperInvocation( |
| property, |
| field, |
| mappingVariables.newName(property.getName() + "FlagMapping"))); |
| break; |
| default: |
| builder.addCode(generateSimplePropertyMapperInvocation(property, field)); |
| } |
| } |
| |
| builder.addStatement("mPropertiesMapped = true"); |
| |
| return builder.build(); |
| } |
| |
| /** |
| * Generate a {@link android.view.inspector.PropertyMapper} invocation. |
| * |
| * Example: |
| * <pre> |
| * mValueId = propertyMapper.mapInt("value", R.attr.value); |
| * </pre> |
| * |
| * @param property A property model to map |
| * @param field The property ID field for the property |
| * @return A code block containing a statement |
| */ |
| @NonNull |
| private CodeBlock generateSimplePropertyMapperInvocation( |
| @NonNull Property property, |
| @NonNull FieldSpec field) { |
| return CodeBlock |
| .builder() |
| .addStatement( |
| "$N = propertyMapper.map$L($S, $L)", |
| field, |
| methodSuffixForPropertyType(property.getType()), |
| property.getName(), |
| generateAttributeId(property)) |
| .build(); |
| } |
| |
| /** |
| * Generate a {@link android.view.inspector.PropertyMapper} invocation for an int enum. |
| * |
| * Example: |
| * <pre> |
| * final SparseArray<String> valueEnumMapping = new SparseArray<>(); |
| * valueEnumMapping.put(1, "ONE"); |
| * valueEnumMapping.put(2, "TWO"); |
| * mValueId = propertyMapper.mapIntEnum("value", R.attr.value, valueEnumMapping::get); |
| * </pre> |
| * |
| * @param property A property model to map |
| * @param field The property ID field for the property |
| * @param variable The name of a local variable to use to store the mapping in |
| * @return A code block containing a series of statements |
| */ |
| @NonNull |
| private CodeBlock generateIntEnumPropertyMapperInvocation( |
| @NonNull Property property, |
| @NonNull FieldSpec field, |
| @NonNull String variable) { |
| final CodeBlock.Builder builder = CodeBlock.builder(); |
| |
| final List<IntEnumEntry> enumEntries = property.getIntEnumEntries(); |
| enumEntries.sort(Comparator.comparing(IntEnumEntry::getValue)); |
| |
| builder.addStatement( |
| "final $1T<$2T> $3N = new $1T<>()", |
| SPARSE_ARRAY, |
| String.class, |
| variable); |
| |
| for (IntEnumEntry enumEntry : enumEntries) { |
| builder.addStatement( |
| "$N.put($L, $S)", |
| variable, |
| enumEntry.getValue(), |
| enumEntry.getName()); |
| } |
| |
| builder.addStatement( |
| "$N = propertyMapper.mapIntEnum($S, $L, $N::get)", |
| field, |
| property.getName(), |
| generateAttributeId(property), |
| variable); |
| |
| return builder.build(); |
| } |
| |
| /** |
| * Generate a {@link android.view.inspector.PropertyMapper} invocation for an int flag. |
| * |
| * Example: |
| * <pre> |
| * final IntFlagMapping valueFlagMapping = new IntFlagMapping(); |
| * valueFlagMapping.add(0x00000003, 0x00000001, "ONE"); |
| * valueFlagMapping.add(0x00000003, 0x00000002, "TWO"); |
| * mValueId = propertyMapper.mapIntFlag("value", R.attr.value, valueFlagMapping::get); |
| * </pre> |
| * |
| * @param property A property model to map |
| * @param field The property ID field for the property |
| * @param variable The name of a local variable to use to store the mapping in |
| * @return A code block containing a series of statements |
| */ |
| @NonNull |
| private CodeBlock generateIntFlagPropertyMapperInvocation( |
| @NonNull Property property, |
| @NonNull FieldSpec field, |
| @NonNull String variable) { |
| final CodeBlock.Builder builder = CodeBlock.builder(); |
| |
| final List<IntFlagEntry> flagEntries = property.getIntFlagEntries(); |
| flagEntries.sort(Comparator.comparing(IntFlagEntry::getName)); |
| |
| builder.addStatement( |
| "final $1T $2N = new $1T()", |
| INT_FLAG_MAPPING, |
| variable); |
| |
| for (IntFlagEntry flagEntry : flagEntries) { |
| builder.addStatement( |
| "$N.add($L, $L, $S)", |
| variable, |
| hexLiteral(flagEntry.getMask()), |
| hexLiteral(flagEntry.getTarget()), |
| flagEntry.getName()); |
| } |
| |
| builder.addStatement( |
| "$N = propertyMapper.mapIntFlag($S, $L, $N::get)", |
| field, |
| property.getName(), |
| generateAttributeId(property), |
| variable); |
| |
| return builder.build(); |
| } |
| |
| /** |
| * Generate a literal attribute ID or reference to {@link android.R.attr}. |
| * |
| * Example: {@code R.attr.value} or {@code 0xdecafbad}. |
| * |
| * @param property A property model |
| * @return A code block containing the attribute ID |
| */ |
| @NonNull |
| private CodeBlock generateAttributeId(@NonNull Property property) { |
| if (property.isAttributeIdInferrableFromR()) { |
| return CodeBlock.of("$T.attr.$L", R_CLASS_NAME, property.getName()); |
| } else { |
| if (property.getAttributeId() == ID_NULL) { |
| return CodeBlock.of("$L", ID_NULL); |
| } else { |
| return CodeBlock.of("$L", hexLiteral(property.getAttributeId())); |
| } |
| } |
| } |
| |
| /** |
| * Generate an implementation of |
| * {@link android.view.inspector.InspectionCompanion#readProperties(Object, |
| * android.view.inspector.PropertyReader)}. |
| * |
| * Example: |
| * <pre> |
| * @Override |
| * public void readProperties(MyNode node, PropertyReader propertyReader) { |
| * if (!mPropertiesMapped) { |
| * throw new InspectionCompanion.UninitializedPropertyMapException(); |
| * } |
| * propertyReader.readInt(mValueId, node.getValue()); |
| * } |
| * </pre> |
| * |
| * @param properties An ordered list of property models |
| * @param fields A map from properties to their field specs |
| * @param nodeClass The class of the node, used for the parameter type |
| * @return A method definition |
| */ |
| @NonNull |
| private MethodSpec generateReadProperties( |
| @NonNull List<Property> properties, |
| @NonNull Map<Property, FieldSpec> fields, |
| @NonNull ClassName nodeClass) { |
| final MethodSpec.Builder builder = MethodSpec.methodBuilder("readProperties") |
| .addAnnotation(Override.class) |
| .addModifiers(Modifier.PUBLIC) |
| .addParameter(nodeClass, "node") |
| .addParameter(PROPERTY_READER, "propertyReader") |
| .beginControlFlow("if (!mPropertiesMapped)") |
| .addStatement( |
| "throw new $T()", |
| INSPECTION_COMPANION.nestedClass("UninitializedPropertyMapException")) |
| .endControlFlow(); |
| |
| for (Property property : properties) { |
| builder.addStatement( |
| "propertyReader.read$L($N, node.$L)", |
| methodSuffixForPropertyType(property.getType()), |
| fields.get(property), |
| property.getAccessor().invocation()); |
| } |
| |
| return builder.build(); |
| } |
| |
| /** |
| * Generate the final class name for the inspection companion from the model's class name. |
| * |
| * The generated class is added to the same package as the source class. If the class in the |
| * model is a nested class, the nested class names are joined with {@code "$"}. The suffix |
| * {@code "$InspectionCompanion"} is always added to the generated name. E.g.: For modeled |
| * class {@code com.example.Outer.Inner}, the generated class name will be |
| * {@code com.example.Outer$Inner$InspectionCompanion}. |
| * |
| * @param model The model to generate from |
| * @return A class name for the generated inspection companion class |
| */ |
| @NonNull |
| private static ClassName generateClassName(@NonNull InspectableClassModel model) { |
| final ClassName className = model.getClassName(); |
| |
| return ClassName.get( |
| className.packageName(), |
| String.join("$", className.simpleNames()) + GENERATED_CLASS_SUFFIX); |
| } |
| |
| /** |
| * Get the suffix for a {@code map} or {@code read} method for a property type. |
| * |
| * @param type The requested property type |
| * @return A method suffix |
| */ |
| @NonNull |
| private static String methodSuffixForPropertyType(@NonNull Property.Type type) { |
| switch (type) { |
| case BOOLEAN: |
| return "Boolean"; |
| case BYTE: |
| return "Byte"; |
| case CHAR: |
| return "Char"; |
| case DOUBLE: |
| return "Double"; |
| case FLOAT: |
| return "Float"; |
| case INT: |
| return "Int"; |
| case LONG: |
| return "Long"; |
| case SHORT: |
| return "Short"; |
| case OBJECT: |
| return "Object"; |
| case COLOR: |
| return "Color"; |
| case GRAVITY: |
| return "Gravity"; |
| case INT_ENUM: |
| return "IntEnum"; |
| case INT_FLAG: |
| return "IntFlag"; |
| case RESOURCE_ID: |
| return "ResourceId"; |
| default: |
| throw new NoSuchElementException(String.format("No such property type, %s", type)); |
| } |
| } |
| |
| /** |
| * Format an int as an 8 digit hex literal |
| * |
| * @param value The value to format |
| * @return A string representation of the hex literal |
| */ |
| @NonNull |
| private static String hexLiteral(int value) { |
| return String.format("0x%08x", value); |
| } |
| } |