blob: 6f6c1aa485acd2ab8678a298723f980bffdc141a [file] [log] [blame]
/*
* 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;
}
}
}