Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 171a723c authored by Ember Rose's avatar Ember Rose
Browse files

Code generation for @InspectableProperty

This does not include the annotation processing needed to build the
property model the generator consumes or support for IntEnumMapping or
IntFlagMapping. Support will be added in subsequent CLs.

Bug: 117616612
Test: atest --host view-inspector-annotation-processor-test
Change-Id: I1d7829a12c7243645a96a32c8fc22b13c70c64e1
parent 9943de9f
Loading
Loading
Loading
Loading
+3 −0
Original line number Diff line number Diff line
alanv@google.com
ashleyrose@google.com
aurimas@google.com
 No newline at end of file
+151 −2
Original line number Diff line number Diff line
@@ -18,16 +18,20 @@ package android.processor.view.inspector;

import com.squareup.javapoet.ClassName;

import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;

/**
 * Model of an inspectable class derived from annotations.
 *
 * This class does not use any {javax.lang.model} objects to facilitate building models for testing
 * {@link InspectionCompanionGenerator}.
 * This class does not use any {@code javax.lang.model} objects to facilitate building models for
 * testing {@link InspectionCompanionGenerator}.
 */
public final class InspectableClassModel {
    private final ClassName mClassName;
    private final Map<String, Property> mPropertyMap;
    private Optional<String> mNodeName = Optional.empty();

    /**
@@ -35,6 +39,7 @@ public final class InspectableClassModel {
     */
    public InspectableClassModel(ClassName className) {
        mClassName = className;
        mPropertyMap = new HashMap<>();
    }

    public ClassName getClassName() {
@@ -48,4 +53,148 @@ public final class InspectableClassModel {
    public void setNodeName(Optional<String> nodeName) {
        mNodeName = nodeName;
    }

    /**
     * Add a property to the model, replacing an existing property of the same name.
     *
     * @param property The property to add or replace
     */
    public void putProperty(Property property) {
        mPropertyMap.put(property.getName(), property);
    }

    /**
     * Get a property by name.
     *
     * @param name The name of the property
     * @return The property or an empty optional
     */
    public Optional<Property> getProperty(String name) {
        return Optional.of(mPropertyMap.get(name));
    }

    /**
     * Get all the properties defined on this model.
     *
     * @return An un-ordered collection of properties
     */
    public Collection<Property> getAllProperties() {
        return mPropertyMap.values();
    }

    /**
     * Model an inspectable property
     */
    public static final class Property {
        private final String mName;
        private String mGetter;
        private Type mType;
        private boolean mAttributeIdInferrableFromR = true;
        private int mAttributeId = 0;

        public Property(String name) {
            mName = name;
        }

        public int getAttributeId() {
            return mAttributeId;
        }

        /**
         * Set the attribute ID, and mark the attribute ID as non-inferrable.
         *
         * @param attributeId The attribute ID for this property
         */
        public void setAttributeId(int attributeId) {
            mAttributeIdInferrableFromR = false;
            mAttributeId = attributeId;
        }

        public boolean isAttributeIdInferrableFromR() {
            return mAttributeIdInferrableFromR;
        }

        public void setAttributeIdInferrableFromR(boolean attributeIdInferrableFromR) {
            mAttributeIdInferrableFromR = attributeIdInferrableFromR;
        }

        public String getName() {
            return mName;
        }

        public String getGetter() {
            return mGetter;
        }

        public void setGetter(String getter) {
            mGetter = getter;
        }

        public Type getType() {
            return mType;
        }

        public void setType(Type type) {
            mType = type;
        }

        public enum Type {
            /** Primitive or boxed {@code boolean} */
            BOOLEAN,

            /** Primitive or boxed {@code byte} */
            BYTE,

            /** Primitive or boxed {@code char} */
            CHAR,

            /** Primitive or boxed {@code double} */
            DOUBLE,

            /** Primitive or boxed {@code float} */
            FLOAT,

            /** Primitive or boxed {@code int} */
            INT,

            /** Primitive or boxed {@code long} */
            LONG,

            /** Primitive or boxed {@code short} */
            SHORT,

            /** Any other object */
            OBJECT,

            /**
             * A color object or packed color {@code int} or {@code long}.
             *
             * @see android.graphics.Color
             * @see android.annotation.ColorInt
             * @see android.annotation.ColorLong
             */
            COLOR,

            /**
             * An {@code int} packed with a gravity specification
             *
             * @see android.view.Gravity
             */
            GRAVITY,

            /**
             * An enumeration packed into an {@code int}.
             *
             * @see android.view.inspector.IntEnumMapping
             */
            INT_ENUM,

            /**
             * Non-exclusive or partially-exclusive flags packed into an {@code int}.
             *
             * @see android.view.inspector.IntFlagMapping
             */
            INT_FLAG
        }
    }
}
+256 −31
Original line number Diff line number Diff line
@@ -16,13 +16,23 @@

package android.processor.view.inspector;

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;
@@ -35,11 +45,63 @@ 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.content.res.ResourceId}.
     */
    private static final ClassName RESOURCE_ID_CLASS_NAME = ClassName.get(
            "android.content.res", "ResourceId");

    /**
     * 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 {@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.
     *
     * @see android.content.res.ResourceId#ID_NULL
     */
    private static final int NO_ID = 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(final Filer filer, final Class requestingClass) {
    public InspectionCompanionGenerator(Filer filer, Class requestingClass) {
        mFiler = filer;
        mRequestingClass = requestingClass;
    }
@@ -77,23 +139,66 @@ public final class InspectionCompanionGenerator {
     * @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(
                        ClassName.get("android.view.inspector", "InspectionCompanion"),
                        model.getClassName()))
                        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)
                .addMethod(generateMapProperties(model))
                .addMethod(generateReadProperties(model));
                .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.
@@ -119,21 +224,19 @@ public final class InspectionCompanionGenerator {
     * {@link android.view.inspector.InspectionCompanion#mapProperties(
     * android.view.inspector.PropertyMapper)}.
     *
     * TODO: implement
     *
     * @param model The model to generate from
     * @param propertyIdFields A list of properties to map to ID fields
     * @return The method definition
     */
    private MethodSpec generateMapProperties(InspectableClassModel model) {
        final ClassName propertyMapper = ClassName.get(
                "android.view.inspector", "PropertyMapper");

        return MethodSpec.methodBuilder("mapProperties")
    private MethodSpec generateMapProperties(List<PropertyIdField> propertyIdFields) {
        final MethodSpec.Builder builder = MethodSpec.methodBuilder("mapProperties")
                .addAnnotation(Override.class)
                .addModifiers(Modifier.PUBLIC)
                .addParameter(propertyMapper, "propertyMapper")
                // TODO: add method body
                .build();
                .addParameter(PROPERTY_MAPPER, "propertyMapper");

        propertyIdFields.forEach(p -> builder.addStatement(generatePropertyMapperInvocation(p)));
        builder.addStatement("$N = true", M_PROPERTIES_MAPPED);

        return builder.build();
    }

    /**
@@ -141,21 +244,91 @@ public final class InspectionCompanionGenerator {
     * {@link android.view.inspector.InspectionCompanion#readProperties(
     * Object, android.view.inspector.PropertyReader)}.
     *
     * TODO: implement
     *
     * @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) {
        final ClassName propertyReader = ClassName.get(
                "android.view.inspector", "PropertyReader");

        return MethodSpec.methodBuilder("readProperties")
    private MethodSpec generateReadProperties(
            InspectableClassModel model,
            List<PropertyIdField> propertyIdFields) {
        final MethodSpec.Builder builder =  MethodSpec.methodBuilder("readProperties")
                .addAnnotation(Override.class)
                .addModifiers(Modifier.PUBLIC)
                .addParameter(model.getClassName(), "inspectable")
                .addParameter(propertyReader, "propertyReader")
                // TODO: add method body
                .addParameter(PROPERTY_READER, "propertyReader")
                .addCode(generatePropertyMapInitializationCheck());

        for (PropertyIdField propertyIdField : propertyIdFields) {
            builder.addStatement(
                    "propertyReader.read$L($N, inspectable.$L())",
                    methodSuffixForPropertyType(propertyIdField.mProperty.getType()),
                    propertyIdField.mFieldSpec,
                    propertyIdField.mProperty.getGetter());
        }

        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() == NO_ID) {
                builder.add("$T.ID_NULL", RESOURCE_ID_CLASS_NAME);
            } else {
                builder.add("$L", String.format("0x%08x", property.getAttributeId()));
            }
        }

        switch (property.getType()) {
            case INT_ENUM:
                throw new RuntimeException("IntEnumMapping generation not implemented");
            case INT_FLAG:
                throw new RuntimeException("IntFlagMapping generation not implemented");
            default:
                builder.add(")");
                break;
        }

        return builder.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();
    }

@@ -163,19 +336,71 @@ public final class InspectionCompanionGenerator {
     * 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 {"$"}. The suffix
     * {"$$InspectionCompanion"} is always added the the generated name. E.g.: For modeled class
     * {com.example.Outer.Inner}, the generated class name will be
     * {com.example.Outer$Inner$$InspectionCompanion}.
     * 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 ClassName generateClassName(final InspectableClassModel model) {
    private static ClassName generateClassName(InspectableClassModel model) {
        final ClassName className = model.getClassName();

        return ClassName.get(
                className.packageName(),
                String.join("$", className.simpleNames()) + "$$InspectionCompanion");
                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";
            default:
                throw new NoSuchElementException(String.format("No such property type, %s", type));
        }
    }

    /**
     * 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;
        }
    }
}
+50 −0
Original line number Diff line number Diff line
@@ -16,6 +16,8 @@

package android.processor.view.inspector;

import android.processor.view.inspector.InspectableClassModel.Property;

import static junit.framework.Assert.assertEquals;
import static junit.framework.Assert.assertNotNull;
import static junit.framework.TestCase.fail;
@@ -61,6 +63,54 @@ public class InspectionCompanionGeneratorTest {
        assertGeneratedFileEquals("NestedClass");
    }

    @Test
    public void testSimpleProperties() {
        addProperty("boolean", Property.Type.BOOLEAN, "getBoolean");
        addProperty("byte", Property.Type.BYTE, "getByte");
        addProperty("char", Property.Type.CHAR, "getChar");
        addProperty("double", Property.Type.DOUBLE, "getDouble");
        addProperty("float", Property.Type.FLOAT, "getFloat");
        addProperty("int", Property.Type.INT, "getInt");
        addProperty("long", Property.Type.LONG, "getLong");
        addProperty("short", Property.Type.SHORT, "getShort");

        addProperty("object", Property.Type.OBJECT, "getObject");
        addProperty("color", Property.Type.COLOR, "getColor");
        addProperty("gravity", Property.Type.GRAVITY, "getGravity");

        assertGeneratedFileEquals("SimpleProperties");
    }

    @Test
    public void testNoAttributeId() {
        final Property property = new Property("noAttributeProperty");
        property.setType(Property.Type.INT);
        property.setGetter("getNoAttributeProperty");
        property.setAttributeIdInferrableFromR(false);
        mModel.putProperty(property);

        assertGeneratedFileEquals("NoAttributeId");
    }

    @Test
    public void testSuppliedAttributeId() {
        final Property property = new Property("suppliedAttributeProperty");
        property.setType(Property.Type.INT);
        property.setGetter("getSuppliedAttributeProperty");
        property.setAttributeId(0xdecafbad);
        mModel.putProperty(property);

        assertGeneratedFileEquals("SuppliedAttributeId");
    }

    private Property addProperty(String name, Property.Type type, String getter) {
        final Property property = new Property(name);
        property.setType(type);
        property.setGetter(getter);
        mModel.putProperty(property);
        return property;
    }

    private void assertGeneratedFileEquals(String fileName) {
        assertEquals(
                loadTextResource(String.format(RESOURCE_PATH_TEMPLATE, fileName)),
+9 −0
Original line number Diff line number Diff line
@@ -12,11 +12,20 @@ import java.lang.Override;
 * on behalf of {@link android.processor.view.inspector.InspectionCompanionGeneratorTest}.
 */
public final class Outer$Inner$$InspectionCompanion implements InspectionCompanion<Outer.Inner> {
    /**
     * Set by {@link #mapProperties(PropertyMapper)} once properties have been mapped.
     */
    private boolean mPropertiesMapped = false;

    @Override
    public void mapProperties(PropertyMapper propertyMapper) {
        mPropertiesMapped = true;
    }

    @Override
    public void readProperties(Outer.Inner inspectable, PropertyReader propertyReader) {
        if (!mPropertiesMapped) {
            throw new InspectionCompanion.UninitializedPropertyMapException();
        }
    }
}
Loading