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

Commit cc50ce30 authored by TreeHugger Robot's avatar TreeHugger Robot Committed by Android (Google) Code Review
Browse files

Merge "Code generation for @InspectableProperty"

parents 02e7db9b 171a723c
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