| /* |
| * 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 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.List; |
| import java.util.NoSuchElementException; |
| import java.util.Optional; |
| |
| 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 Filer mFiler; |
| private final 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.view.inspector.IntEnumMapping}. |
| */ |
| private static final ClassName INT_ENUM_MAPPING = ClassName.get( |
| "android.view.inspector", "IntEnumMapping"); |
| |
| /** |
| * The class name of {@link android.view.inspector.IntFlagMapping}. |
| */ |
| private static final ClassName INT_FLAG_MAPPING = ClassName.get( |
| "android.view.inspector", "IntFlagMapping"); |
| |
| /** |
| * The {@code mPropertiesMapped} field. |
| */ |
| private static final FieldSpec M_PROPERTIES_MAPPED = FieldSpec |
| .builder(TypeName.BOOLEAN, "mPropertiesMapped", Modifier.PRIVATE) |
| .initializer("false") |
| .addJavadoc( |
| "Set by {@link #mapProperties($T)} once properties have been mapped.\n", |
| PROPERTY_MAPPER) |
| .build(); |
| |
| /** |
| * 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(Filer filer, 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(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} |
| */ |
| JavaFile generateFile(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 |
| */ |
| private TypeSpec generateTypeSpec(InspectableClassModel model) { |
| final List<PropertyIdField> propertyIdFields = generatePropertyIdFields(model); |
| |
| 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(M_PROPERTIES_MAPPED); |
| |
| for (PropertyIdField propertyIdField : propertyIdFields) { |
| builder.addField(propertyIdField.mFieldSpec); |
| } |
| |
| builder.addMethod(generateMapProperties(propertyIdFields)) |
| .addMethod(generateReadProperties(model, propertyIdFields)); |
| |
| generateGetNodeName(model).ifPresent(builder::addMethod); |
| |
| return builder.build(); |
| } |
| |
| /** |
| * Build a list of {@link PropertyIdField}'s for a model. |
| * |
| * To insure idempotency of the generated code, this method sorts the list of properties |
| * alphabetically by name. |
| * |
| * A {@link NameAllocator} is used to ensure that the field names are valid Java identifiers, |
| * and it prevents overlaps in names by suffixing them as needed. |
| * |
| * @param model The model to get properties from |
| * @return A list of properties and fields |
| */ |
| private List<PropertyIdField> generatePropertyIdFields(InspectableClassModel model) { |
| final NameAllocator nameAllocator = new NameAllocator(); |
| final List<Property> sortedProperties = new ArrayList<>(model.getAllProperties()); |
| final List<PropertyIdField> propertyIdFields = new ArrayList<>(sortedProperties.size()); |
| |
| sortedProperties.sort(Comparator.comparing(Property::getName)); |
| |
| for (Property property : sortedProperties) { |
| // Format a property to a member field name like "someProperty" -> "mSomePropertyId" |
| final String memberName = String.format( |
| "m%s%sId", |
| property.getName().substring(0, 1).toUpperCase(), |
| property.getName().substring(1)); |
| final FieldSpec fieldSpec = FieldSpec |
| .builder(TypeName.INT, nameAllocator.newName(memberName), Modifier.PRIVATE) |
| .addJavadoc("Property ID of {@code $L}.\n", property.getName()) |
| .build(); |
| |
| propertyIdFields.add(new PropertyIdField(fieldSpec, property)); |
| } |
| |
| return propertyIdFields; |
| } |
| |
| /** |
| * Generate a method definition for |
| * {@link android.view.inspector.InspectionCompanion#getNodeName()}, if needed. |
| * |
| * If {@link InspectableClassModel#getNodeName()} is empty, This method returns an empty |
| * optional, otherwise, it generates a simple method that returns the string value of the |
| * node name. |
| * |
| * @param model The model to generate from |
| * @return The method definition or an empty Optional |
| */ |
| private Optional<MethodSpec> generateGetNodeName(InspectableClassModel model) { |
| return model.getNodeName().map(nodeName -> MethodSpec.methodBuilder("getNodeName") |
| .addAnnotation(Override.class) |
| .addModifiers(Modifier.PUBLIC) |
| .returns(String.class) |
| .addStatement("return $S", nodeName) |
| .build()); |
| } |
| |
| /** |
| * Generate a method definition for |
| * {@link android.view.inspector.InspectionCompanion#mapProperties( |
| * android.view.inspector.PropertyMapper)}. |
| * |
| * @param propertyIdFields A list of properties to map to ID fields |
| * @return The method definition |
| */ |
| private MethodSpec generateMapProperties(List<PropertyIdField> propertyIdFields) { |
| final MethodSpec.Builder builder = MethodSpec.methodBuilder("mapProperties") |
| .addAnnotation(Override.class) |
| .addModifiers(Modifier.PUBLIC) |
| .addParameter(PROPERTY_MAPPER, "propertyMapper"); |
| |
| propertyIdFields.forEach(p -> builder.addStatement(generatePropertyMapperInvocation(p))); |
| builder.addStatement("$N = true", M_PROPERTIES_MAPPED); |
| |
| return builder.build(); |
| } |
| |
| /** |
| * Generate a method definition for |
| * {@link android.view.inspector.InspectionCompanion#readProperties( |
| * Object, android.view.inspector.PropertyReader)}. |
| * |
| * @param model The model to generate from |
| * @param propertyIdFields A list of properties and ID fields to read from |
| * @return The method definition |
| */ |
| private MethodSpec generateReadProperties( |
| InspectableClassModel model, |
| List<PropertyIdField> propertyIdFields) { |
| final MethodSpec.Builder builder = MethodSpec.methodBuilder("readProperties") |
| .addAnnotation(Override.class) |
| .addModifiers(Modifier.PUBLIC) |
| .addParameter(model.getClassName(), "node") |
| .addParameter(PROPERTY_READER, "propertyReader") |
| .addCode(generatePropertyMapInitializationCheck()); |
| |
| for (PropertyIdField propertyIdField : propertyIdFields) { |
| builder.addStatement( |
| "propertyReader.read$L($N, node.$L)", |
| methodSuffixForPropertyType(propertyIdField.mProperty.getType()), |
| propertyIdField.mFieldSpec, |
| propertyIdField.mProperty.getAccessor().invocation()); |
| } |
| |
| return builder.build(); |
| } |
| |
| /** |
| * Generate a statement maps a property with a {@link android.view.inspector.PropertyMapper}. |
| * |
| * @param propertyIdField The property model and ID field to generate from |
| * @return A statement that invokes property mapper method |
| */ |
| private CodeBlock generatePropertyMapperInvocation(PropertyIdField propertyIdField) { |
| final CodeBlock.Builder builder = CodeBlock.builder(); |
| final Property property = propertyIdField.mProperty; |
| final FieldSpec fieldSpec = propertyIdField.mFieldSpec; |
| |
| builder.add( |
| "$N = propertyMapper.map$L($S,$W", |
| fieldSpec, |
| methodSuffixForPropertyType(property.getType()), |
| property.getName()); |
| |
| if (property.isAttributeIdInferrableFromR()) { |
| builder.add("$T.attr.$L", R_CLASS_NAME, property.getName()); |
| } else { |
| if (property.getAttributeId() == ID_NULL) { |
| builder.add("$L", ID_NULL); |
| } else { |
| builder.add("$L", hexLiteral(property.getAttributeId())); |
| } |
| } |
| |
| switch (property.getType()) { |
| case INT_ENUM: |
| builder.add(",$W"); |
| builder.add(generateIntEnumMappingBuilder(property.getIntEnumEntries())); |
| break; |
| case INT_FLAG: |
| builder.add(",$W"); |
| builder.add(generateIntFlagMappingBuilder(property.getIntFlagEntries())); |
| break; |
| } |
| |
| return builder.add(")").build(); |
| } |
| |
| /** |
| * Generate a check that throws |
| * {@link android.view.inspector.InspectionCompanion.UninitializedPropertyMapException} |
| * if the properties haven't been initialized. |
| * |
| * <pre> |
| * if (!mPropertiesMapped) { |
| * throw new InspectionCompanion.UninitializedPropertyMapException(); |
| * } |
| * </pre> |
| * |
| * @return A codeblock containing the property map initialization check |
| */ |
| private CodeBlock generatePropertyMapInitializationCheck() { |
| return CodeBlock.builder() |
| .beginControlFlow("if (!$N)", M_PROPERTIES_MAPPED) |
| .addStatement( |
| "throw new $T()", |
| INSPECTION_COMPANION.nestedClass("UninitializedPropertyMapException")) |
| .endControlFlow() |
| .build(); |
| } |
| |
| /** |
| * Generate an invocation of {@link android.view.inspector.IntEnumMapping.Builder}. |
| * |
| * <pre> |
| * new IntEnumMapping.Builder() |
| * .addValue("ONE", 1) |
| * .build() |
| * </pre> |
| * |
| * @return A codeblock containing the an int enum mapping builder |
| */ |
| private CodeBlock generateIntEnumMappingBuilder(List<IntEnumEntry> intEnumEntries) { |
| final ArrayList<IntEnumEntry> sortedEntries = new ArrayList<>(intEnumEntries); |
| sortedEntries.sort(Comparator.comparing(IntEnumEntry::getValue)); |
| |
| final CodeBlock.Builder builder = CodeBlock.builder() |
| .add("new $T()$>", INT_ENUM_MAPPING.nestedClass("Builder")); |
| |
| for (IntEnumEntry entry : sortedEntries) { |
| builder.add("\n.addValue($S, $L)", entry.getName(), entry.getValue()); |
| } |
| |
| return builder.add("\n.build()$<").build(); |
| } |
| |
| private CodeBlock generateIntFlagMappingBuilder(List<IntFlagEntry> intFlagEntries) { |
| final ArrayList<IntFlagEntry> sortedEntries = new ArrayList<>(intFlagEntries); |
| sortedEntries.sort(Comparator.comparing(IntFlagEntry::getName)); |
| |
| final CodeBlock.Builder builder = CodeBlock.builder() |
| .add("new $T()$>", INT_FLAG_MAPPING.nestedClass("Builder")); |
| |
| for (IntFlagEntry entry : sortedEntries) { |
| if (entry.hasMask()) { |
| builder.add( |
| "\n.addFlag($S, $L, $L)", |
| entry.getName(), |
| hexLiteral(entry.getTarget()), |
| hexLiteral(entry.getMask())); |
| } else { |
| builder.add( |
| "\n.addFlag($S, $L)", |
| entry.getName(), |
| hexLiteral(entry.getTarget())); |
| } |
| } |
| |
| return builder.add("\n.build()$<").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 the 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 |
| */ |
| private static ClassName generateClassName(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 |
| */ |
| private static String methodSuffixForPropertyType(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)); |
| } |
| } |
| |
| private static String hexLiteral(int value) { |
| return String.format("0x%08x", value); |
| } |
| |
| /** |
| * Value class that holds a {@link Property} and a {@link FieldSpec} for that property. |
| */ |
| private static final class PropertyIdField { |
| private final FieldSpec mFieldSpec; |
| private final Property mProperty; |
| |
| private PropertyIdField(FieldSpec fieldSpec, Property property) { |
| mFieldSpec = fieldSpec; |
| mProperty = property; |
| } |
| } |
| } |