summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAshley Rose <ashleyrose@google.com>2018-12-07 17:20:25 -0500
committerAshley Rose <ashleyrose@google.com>2018-12-10 23:30:43 +0000
commitde080eb7b0374882cfe3dbea01540f477ff59e5d (patch)
tree328ec34832a38f40de0a9a8c037062b76d77fb6c
parent52fe5dd97fb749aad4f570914a22aebf8d0de1c1 (diff)
Annotation processor for @InspectableNodeName
Bug: 117616612 Test: atest --host view-inspector-annotation-processor-test Change-Id: I48f62544655adbc33e3ccdd3301d6dc471fe4163
-rw-r--r--Android.bp12
-rw-r--r--tools/processors/view_inspector/Android.bp27
-rw-r--r--tools/processors/view_inspector/TEST_MAPPING7
-rw-r--r--tools/processors/view_inspector/src/java/android/processor/view/inspector/AnnotationUtils.java117
-rw-r--r--tools/processors/view_inspector/src/java/android/processor/view/inspector/InspectableClassModel.java51
-rw-r--r--tools/processors/view_inspector/src/java/android/processor/view/inspector/InspectableNodeNameProcessor.java76
-rw-r--r--tools/processors/view_inspector/src/java/android/processor/view/inspector/InspectionCompanionGenerator.java181
-rw-r--r--tools/processors/view_inspector/src/java/android/processor/view/inspector/ModelProcessor.java32
-rw-r--r--tools/processors/view_inspector/src/java/android/processor/view/inspector/PlatformInspectableProcessor.java161
-rw-r--r--tools/processors/view_inspector/src/java/android/processor/view/inspector/ProcessingException.java78
-rw-r--r--tools/processors/view_inspector/src/resources/META-INF/services/javax.annotation.processing.Processor1
-rw-r--r--tools/processors/view_inspector/test/java/android/processor/view/inspector/InspectionCompanionGeneratorTest.java80
-rw-r--r--tools/processors/view_inspector/test/resources/android/processor/view/inspector/InspectionCompanionGeneratorTest/NestedClass.java.txt22
-rw-r--r--tools/processors/view_inspector/test/resources/android/processor/view/inspector/InspectionCompanionGeneratorTest/NodeName.java.txt28
14 files changed, 861 insertions, 12 deletions
diff --git a/Android.bp b/Android.bp
index 0210bb3c1323..4762cba7ecf9 100644
--- a/Android.bp
+++ b/Android.bp
@@ -784,18 +784,6 @@ java_library {
],
}
-// A host library containing the inspector annotations for inspector-annotation-processor.
-java_library_host {
- name: "inspector-annotation",
- srcs: [
- "core/java/android/view/inspector/InspectableNodeName.java",
- "core/java/android/view/inspector/InspectableProperty.java",
- // Needed for the ResourceId.ID_NULL constant
- "core/java/android/content/res/ResourceId.java",
- "core/java/android/annotation/AnyRes.java",
- ],
-}
-
// A host library including just UnsupportedAppUsage.java so that the annotation
// processor can also use this annotation.
java_library_host {
diff --git a/tools/processors/view_inspector/Android.bp b/tools/processors/view_inspector/Android.bp
new file mode 100644
index 000000000000..ca6b3c4572f5
--- /dev/null
+++ b/tools/processors/view_inspector/Android.bp
@@ -0,0 +1,27 @@
+java_library_host {
+ name: "view-inspector-annotation-processor",
+
+ srcs: ["src/java/**/*.java"],
+ java_resource_dirs: ["src/resources"],
+
+ static_libs: [
+ "javapoet",
+ ],
+
+ use_tools_jar: true,
+}
+
+java_test_host {
+ name: "view-inspector-annotation-processor-test",
+
+ srcs: ["test/java/**/*.java"],
+ java_resource_dirs: ["test/resources"],
+
+ static_libs: [
+ "guava",
+ "junit",
+ "view-inspector-annotation-processor",
+ ],
+
+ test_suites: ["general-tests"],
+}
diff --git a/tools/processors/view_inspector/TEST_MAPPING b/tools/processors/view_inspector/TEST_MAPPING
new file mode 100644
index 000000000000..a91b5b452c39
--- /dev/null
+++ b/tools/processors/view_inspector/TEST_MAPPING
@@ -0,0 +1,7 @@
+{
+ "presubmit": [
+ {
+ "name": "view-inspector-annotation-processor-test"
+ }
+ ]
+}
diff --git a/tools/processors/view_inspector/src/java/android/processor/view/inspector/AnnotationUtils.java b/tools/processors/view_inspector/src/java/android/processor/view/inspector/AnnotationUtils.java
new file mode 100644
index 000000000000..f157949f4d1b
--- /dev/null
+++ b/tools/processors/view_inspector/src/java/android/processor/view/inspector/AnnotationUtils.java
@@ -0,0 +1,117 @@
+/*
+ * Copyright (C) 2018 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 java.util.Map;
+import java.util.Optional;
+
+import javax.annotation.processing.ProcessingEnvironment;
+import javax.lang.model.element.AnnotationMirror;
+import javax.lang.model.element.AnnotationValue;
+import javax.lang.model.element.Element;
+import javax.lang.model.element.ExecutableElement;
+import javax.lang.model.type.TypeMirror;
+import javax.lang.model.util.Elements;
+import javax.lang.model.util.Types;
+
+/**
+ * Utilities for working with {@link AnnotationMirror}.
+ */
+final class AnnotationUtils {
+ private final Elements mElementUtils;
+ private final Types mTypeUtils;
+
+ AnnotationUtils(ProcessingEnvironment processingEnv) {
+ mElementUtils = processingEnv.getElementUtils();
+ mTypeUtils = processingEnv.getTypeUtils();
+ }
+
+ /**
+ * Get a {@link AnnotationMirror} specified by name from an {@link Element}.
+ *
+ * @param qualifiedName The fully qualified name of the annotation to search for
+ * @param element The element to search for annotations on
+ * @return The mirror of the requested annotation
+ * @throws ProcessingException If there is not exactly one of the requested annotation.
+ */
+ AnnotationMirror exactlyOneMirror(String qualifiedName, Element element) {
+ final Element targetTypeElment = mElementUtils.getTypeElement(qualifiedName);
+ final TypeMirror targetType = targetTypeElment.asType();
+ AnnotationMirror result = null;
+
+ for (AnnotationMirror annotation : element.getAnnotationMirrors()) {
+ final TypeMirror annotationType = annotation.getAnnotationType().asElement().asType();
+ if (mTypeUtils.isSameType(annotationType, targetType)) {
+ if (result == null) {
+ result = annotation;
+ } else {
+ final String message = String.format(
+ "Element had multiple instances of @%s, expected exactly one",
+ targetTypeElment.getSimpleName());
+
+ throw new ProcessingException(message, element, annotation);
+ }
+ }
+ }
+
+ if (result == null) {
+ final String message = String.format(
+ "Expected an @%s annotation, found none", targetTypeElment.getSimpleName());
+ throw new ProcessingException(message, element);
+ } else {
+ return result;
+ }
+ }
+
+ /**
+ * Extract a string-valued property from an {@link AnnotationMirror}.
+ *
+ * @param propertyName The name of the requested property
+ * @param annotationMirror The mirror to search for the property
+ * @return The String value of the annotation or null
+ */
+ Optional<String> stringProperty(String propertyName, AnnotationMirror annotationMirror) {
+ final AnnotationValue value = valueByName(propertyName, annotationMirror);
+ if (value != null) {
+ return Optional.of((String) value.getValue());
+ } else {
+ return Optional.empty();
+ }
+ }
+
+
+ /**
+ * Extract a {@link AnnotationValue} from a mirror by string property name.
+ *
+ * @param propertyName The name of the property requested property
+ * @param annotationMirror
+ * @return
+ */
+ AnnotationValue valueByName(String propertyName, AnnotationMirror annotationMirror) {
+ final Map<? extends ExecutableElement, ? extends AnnotationValue> valueMap =
+ annotationMirror.getElementValues();
+
+ for (ExecutableElement method : valueMap.keySet()) {
+ if (method.getSimpleName().contentEquals(propertyName)) {
+ return valueMap.get(method);
+ }
+ }
+
+ return null;
+ }
+
+}
diff --git a/tools/processors/view_inspector/src/java/android/processor/view/inspector/InspectableClassModel.java b/tools/processors/view_inspector/src/java/android/processor/view/inspector/InspectableClassModel.java
new file mode 100644
index 000000000000..f0b0ff6b979f
--- /dev/null
+++ b/tools/processors/view_inspector/src/java/android/processor/view/inspector/InspectableClassModel.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2018 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 com.squareup.javapoet.ClassName;
+
+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}.
+ */
+public final class InspectableClassModel {
+ private final ClassName mClassName;
+ private Optional<String> mNodeName = Optional.empty();
+
+ /**
+ * @param className The name of the modeled class
+ */
+ public InspectableClassModel(ClassName className) {
+ mClassName = className;
+ }
+
+ public ClassName getClassName() {
+ return mClassName;
+ }
+
+ public Optional<String> getNodeName() {
+ return mNodeName;
+ }
+
+ public void setNodeName(Optional<String> nodeName) {
+ mNodeName = nodeName;
+ }
+}
diff --git a/tools/processors/view_inspector/src/java/android/processor/view/inspector/InspectableNodeNameProcessor.java b/tools/processors/view_inspector/src/java/android/processor/view/inspector/InspectableNodeNameProcessor.java
new file mode 100644
index 000000000000..a186a82af160
--- /dev/null
+++ b/tools/processors/view_inspector/src/java/android/processor/view/inspector/InspectableNodeNameProcessor.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright (C) 2018 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 java.util.Optional;
+
+import javax.annotation.processing.ProcessingEnvironment;
+import javax.lang.model.element.AnnotationMirror;
+import javax.lang.model.element.Element;
+
+/**
+ * Process {InspectableNodeName} annotations
+ *
+ * @see android.view.inspector.InspectableNodeName
+ */
+public final class InspectableNodeNameProcessor implements ModelProcessor {
+ private final String mQualifiedName;
+ private final ProcessingEnvironment mProcessingEnv;
+ private final AnnotationUtils mAnnotationUtils;
+
+ /**
+ * @param annotationQualifiedName The qualified name of the annotation to process
+ * @param processingEnv The processing environment from the parent processor
+ */
+ public InspectableNodeNameProcessor(
+ String annotationQualifiedName,
+ ProcessingEnvironment processingEnv) {
+ mQualifiedName = annotationQualifiedName;
+ mProcessingEnv = processingEnv;
+ mAnnotationUtils = new AnnotationUtils(processingEnv);
+ }
+
+ /**
+ * Set the node name on the model if one is supplied.
+ *
+ * If the model already has a different node name, the node name will not be updated, and
+ * the processor will print an error the the messager.
+ *
+ * @param element The annotated element to operate on
+ * @param model The model this element should be merged into
+ */
+ @Override
+ public void process(Element element, InspectableClassModel model) {
+ try {
+ final AnnotationMirror mirror =
+ mAnnotationUtils.exactlyOneMirror(mQualifiedName, element);
+ final Optional<String> nodeName = mAnnotationUtils.stringProperty("value", mirror);
+
+ if (!model.getNodeName().isPresent() || model.getNodeName().equals(nodeName)) {
+ model.setNodeName(nodeName);
+ } else {
+ final String message = String.format(
+ "Node name was already set to \"%s\", refusing to change it to \"%s\".",
+ model.getNodeName().get(),
+ nodeName);
+ throw new ProcessingException(message, element, mirror);
+ }
+ } catch (ProcessingException processingException) {
+ processingException.print(mProcessingEnv.getMessager());
+ }
+ }
+}
diff --git a/tools/processors/view_inspector/src/java/android/processor/view/inspector/InspectionCompanionGenerator.java b/tools/processors/view_inspector/src/java/android/processor/view/inspector/InspectionCompanionGenerator.java
new file mode 100644
index 000000000000..fe0153d7f9af
--- /dev/null
+++ b/tools/processors/view_inspector/src/java/android/processor/view/inspector/InspectionCompanionGenerator.java
@@ -0,0 +1,181 @@
+/*
+ * Copyright (C) 2018 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 com.squareup.javapoet.ClassName;
+import com.squareup.javapoet.JavaFile;
+import com.squareup.javapoet.MethodSpec;
+import com.squareup.javapoet.ParameterizedTypeName;
+import com.squareup.javapoet.TypeSpec;
+
+import java.io.IOException;
+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;
+
+ /**
+ * @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) {
+ 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) {
+ TypeSpec.Builder builder = TypeSpec
+ .classBuilder(generateClassName(model))
+ .addModifiers(Modifier.PUBLIC, Modifier.FINAL)
+ .addSuperinterface(ParameterizedTypeName.get(
+ ClassName.get("android.view.inspector", "InspectionCompanion"),
+ 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));
+
+ generateGetNodeName(model).ifPresent(builder::addMethod);
+
+ return builder.build();
+ }
+
+ /**
+ * 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)}.
+ *
+ * TODO: implement
+ *
+ * @param model The model to generate from
+ * @return The method definition
+ */
+ private MethodSpec generateMapProperties(InspectableClassModel model) {
+ final ClassName propertyMapper = ClassName.get(
+ "android.view.inspector", "PropertyMapper");
+
+ return MethodSpec.methodBuilder("mapProperties")
+ .addAnnotation(Override.class)
+ .addModifiers(Modifier.PUBLIC)
+ .addParameter(propertyMapper, "propertyMapper")
+ // TODO: add method body
+ .build();
+ }
+
+ /**
+ * Generate a method definition for
+ * {@link android.view.inspector.InspectionCompanion#readProperties(
+ * Object, android.view.inspector.PropertyReader)}.
+ *
+ * TODO: implement
+ *
+ * @param model The model to generate from
+ * @return The method definition
+ */
+ private MethodSpec generateReadProperties(InspectableClassModel model) {
+ final ClassName propertyReader = ClassName.get(
+ "android.view.inspector", "PropertyReader");
+
+ return MethodSpec.methodBuilder("readProperties")
+ .addAnnotation(Override.class)
+ .addModifiers(Modifier.PUBLIC)
+ .addParameter(model.getClassName(), "inspectable")
+ .addParameter(propertyReader, "propertyReader")
+ // TODO: add method body
+ .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 {"$"}. 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}.
+ *
+ * @param model The model to generate from
+ * @return A class name for the generated inspection companion class
+ */
+ private ClassName generateClassName(final InspectableClassModel model) {
+ final ClassName className = model.getClassName();
+
+ return ClassName.get(
+ className.packageName(),
+ String.join("$", className.simpleNames()) + "$$InspectionCompanion");
+ }
+}
diff --git a/tools/processors/view_inspector/src/java/android/processor/view/inspector/ModelProcessor.java b/tools/processors/view_inspector/src/java/android/processor/view/inspector/ModelProcessor.java
new file mode 100644
index 000000000000..3ffcff8a87d3
--- /dev/null
+++ b/tools/processors/view_inspector/src/java/android/processor/view/inspector/ModelProcessor.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2018 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 javax.lang.model.element.Element;
+
+/**
+ * An interface for annotation processors that operate on a single element and a class model.
+ */
+public interface ModelProcessor {
+ /**
+ * Process the supplied element, mutating the model as needed.
+ *
+ * @param element The annotated element to operate on
+ * @param model The model this element should be merged into
+ */
+ void process(Element element, InspectableClassModel model);
+}
diff --git a/tools/processors/view_inspector/src/java/android/processor/view/inspector/PlatformInspectableProcessor.java b/tools/processors/view_inspector/src/java/android/processor/view/inspector/PlatformInspectableProcessor.java
new file mode 100644
index 000000000000..e531b67d9ea2
--- /dev/null
+++ b/tools/processors/view_inspector/src/java/android/processor/view/inspector/PlatformInspectableProcessor.java
@@ -0,0 +1,161 @@
+/*
+ * Copyright (C) 2018 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 static javax.tools.Diagnostic.Kind.ERROR;
+
+import com.squareup.javapoet.ClassName;
+
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+
+import javax.annotation.processing.AbstractProcessor;
+import javax.annotation.processing.RoundEnvironment;
+import javax.annotation.processing.SupportedAnnotationTypes;
+import javax.lang.model.SourceVersion;
+import javax.lang.model.element.Element;
+import javax.lang.model.element.ElementKind;
+import javax.lang.model.element.TypeElement;
+
+
+/**
+ * An annotation processor for the platform inspectable annotations.
+ *
+ * It mostly delegates to {@link ModelProcessor} and {@link InspectionCompanionGenerator}. This
+ * modular architecture allows the core generation code to be reused for comparable annotations
+ * outside the platform, such as in AndroidX.
+ *
+ * @see android.view.inspector.InspectableNodeName
+ * @see android.view.inspector.InspectableProperty
+ */
+@SupportedAnnotationTypes({
+ PlatformInspectableProcessor.NODE_NAME_QUALIFIED_NAME
+})
+public final class PlatformInspectableProcessor extends AbstractProcessor {
+ static final String NODE_NAME_QUALIFIED_NAME =
+ "android.view.inspector.InspectableNodeName";
+
+ @Override
+ public SourceVersion getSupportedSourceVersion() {
+ return SourceVersion.latest();
+ }
+
+ @Override
+ public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
+ final Map<String, InspectableClassModel> modelMap = new HashMap<>();
+
+ for (TypeElement annotation : annotations) {
+ if (annotation.getQualifiedName().contentEquals(NODE_NAME_QUALIFIED_NAME)) {
+ runModelProcessor(
+ roundEnv.getElementsAnnotatedWith(annotation),
+ new InspectableNodeNameProcessor(NODE_NAME_QUALIFIED_NAME, processingEnv),
+ modelMap);
+
+
+ } else {
+ fail("Unexpected annotation type", annotation);
+ }
+ }
+
+ final InspectionCompanionGenerator generator =
+ new InspectionCompanionGenerator(processingEnv.getFiler(), getClass());
+
+ for (InspectableClassModel model : modelMap.values()) {
+ try {
+ generator.generate(model);
+ } catch (IOException ioException) {
+ fail(String.format(
+ "Unable to generate inspection companion for %s due to %s",
+ model.getClassName().toString(),
+ ioException.getMessage()));
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * Run a {@link ModelProcessor} for a set of elements
+ *
+ * @param elements Elements to process, should be annotated correctly
+ * @param processor The processor to use
+ * @param modelMap A map of qualified class names to models
+ */
+ private void runModelProcessor(
+ Set<? extends Element> elements,
+ ModelProcessor processor,
+ Map<String, InspectableClassModel> modelMap) {
+ for (Element element : elements) {
+ final Optional<TypeElement> classElement = enclosingClassElement(element);
+
+ if (!classElement.isPresent()) {
+ fail("Element not contained in a class", element);
+ break;
+ }
+
+ final InspectableClassModel model = modelMap.computeIfAbsent(
+ classElement.get().getQualifiedName().toString(),
+ k -> new InspectableClassModel(ClassName.get(classElement.get())));
+
+ processor.process(element, model);
+ }
+ }
+
+ /**
+ * Get the nearest enclosing class if there is one.
+ *
+ * If {@param element} represents a class, it will be returned wrapped in an optional.
+ *
+ * @param element An element to search from
+ * @return A TypeElement of the nearest enclosing class or an empty optional
+ */
+ private static Optional<TypeElement> enclosingClassElement(Element element) {
+ Element cursor = element;
+
+ while (cursor != null) {
+ if (cursor.getKind() == ElementKind.CLASS) {
+ return Optional.of((TypeElement) cursor);
+ }
+
+ cursor = cursor.getEnclosingElement();
+ }
+
+ return Optional.empty();
+ }
+
+ /**
+ * Print message and fail the build.
+ *
+ * @param message Message to print
+ */
+ private void fail(String message) {
+ processingEnv.getMessager().printMessage(ERROR, message);
+ }
+
+ /**
+ * Print message and fail the build.
+ *
+ * @param message Message to print
+ * @param element The element that failed
+ */
+ private void fail(String message, Element element) {
+ processingEnv.getMessager().printMessage(ERROR, message, element);
+ }
+}
diff --git a/tools/processors/view_inspector/src/java/android/processor/view/inspector/ProcessingException.java b/tools/processors/view_inspector/src/java/android/processor/view/inspector/ProcessingException.java
new file mode 100644
index 000000000000..6360e0a2de39
--- /dev/null
+++ b/tools/processors/view_inspector/src/java/android/processor/view/inspector/ProcessingException.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 2018 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 static javax.tools.Diagnostic.Kind.ERROR;
+
+import javax.annotation.processing.Messager;
+import javax.lang.model.element.AnnotationMirror;
+import javax.lang.model.element.AnnotationValue;
+import javax.lang.model.element.Element;
+
+/**
+ * Internal exception used to signal an error processing an annotation.
+ */
+final class ProcessingException extends RuntimeException {
+ private final Element mElement;
+ private final AnnotationMirror mAnnotationMirror;
+ private final AnnotationValue mAnnotationValue;
+
+ ProcessingException(String message) {
+ this(message, null, null, null);
+ }
+
+ ProcessingException(String message, Element element) {
+ this(message, element, null, null);
+ }
+
+ ProcessingException(String message, Element element, AnnotationMirror annotationMirror) {
+ this(message, element, annotationMirror, null);
+ }
+
+ ProcessingException(
+ String message,
+ Element element,
+ AnnotationMirror annotationMirror,
+ AnnotationValue annotationValue) {
+ super(message);
+ mElement = element;
+ mAnnotationMirror = annotationMirror;
+ mAnnotationValue = annotationValue;
+ }
+
+ /**
+ * Prints the exception to a Messager.
+ *
+ * @param messager A Messager to print to
+ */
+ void print(Messager messager) {
+ if (mElement != null) {
+ if (mAnnotationMirror != null) {
+ if (mAnnotationValue != null) {
+ messager.printMessage(
+ ERROR, getMessage(), mElement, mAnnotationMirror, mAnnotationValue);
+ } else {
+ messager.printMessage(ERROR, getMessage(), mElement, mAnnotationMirror);
+ }
+ } else {
+ messager.printMessage(ERROR, getMessage(), mElement);
+ }
+ } else {
+ messager.printMessage(ERROR, getMessage());
+ }
+ }
+}
diff --git a/tools/processors/view_inspector/src/resources/META-INF/services/javax.annotation.processing.Processor b/tools/processors/view_inspector/src/resources/META-INF/services/javax.annotation.processing.Processor
new file mode 100644
index 000000000000..fa4f71ffd0fa
--- /dev/null
+++ b/tools/processors/view_inspector/src/resources/META-INF/services/javax.annotation.processing.Processor
@@ -0,0 +1 @@
+android.processor.inspector.view.PlatformInspectableProcessor
diff --git a/tools/processors/view_inspector/test/java/android/processor/view/inspector/InspectionCompanionGeneratorTest.java b/tools/processors/view_inspector/test/java/android/processor/view/inspector/InspectionCompanionGeneratorTest.java
new file mode 100644
index 000000000000..c02b0bdba1cf
--- /dev/null
+++ b/tools/processors/view_inspector/test/java/android/processor/view/inspector/InspectionCompanionGeneratorTest.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright (C) 2018 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 static junit.framework.Assert.assertEquals;
+import static junit.framework.Assert.assertNotNull;
+import static junit.framework.TestCase.fail;
+
+import com.google.common.base.Charsets;
+import com.google.common.io.Resources;
+import com.squareup.javapoet.ClassName;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import java.io.IOException;
+import java.net.URL;
+import java.util.Optional;
+
+/**
+ * Tests for {@link InspectionCompanionGenerator}
+ */
+public class InspectionCompanionGeneratorTest {
+ private static final String RESOURCE_PATH_TEMPLATE =
+ "android/processor/view/inspector/InspectionCompanionGeneratorTest/%s.java.txt";
+ private static final ClassName TEST_CLASS_NAME =
+ ClassName.get("com.android.inspectable", "TestInspectable");
+ private InspectableClassModel mModel;
+ private InspectionCompanionGenerator mGenerator;
+
+ @Before
+ public void setup() {
+ mModel = new InspectableClassModel(TEST_CLASS_NAME);
+ mGenerator = new InspectionCompanionGenerator(null, getClass());
+ }
+
+ @Test
+ public void testNodeName() {
+ mModel.setNodeName(Optional.of("NodeName"));
+ assertGeneratedFileEquals("NodeName");
+ }
+
+ @Test
+ public void testNestedClass() {
+ mModel = new InspectableClassModel(
+ ClassName.get("com.android.inspectable", "Outer", "Inner"));
+ assertGeneratedFileEquals("NestedClass");
+ }
+
+ private void assertGeneratedFileEquals(String fileName) {
+ assertEquals(
+ loadTextResource(String.format(RESOURCE_PATH_TEMPLATE, fileName)),
+ mGenerator.generateFile(mModel).toString());
+ }
+
+ private String loadTextResource(String path) {
+ try {
+ final URL url = Resources.getResource(path);
+ assertNotNull(String.format("Resource file not found: %s", path), url);
+ return Resources.toString(url, Charsets.UTF_8);
+ } catch (IOException e) {
+ fail(e.getMessage());
+ return null;
+ }
+ }
+}
diff --git a/tools/processors/view_inspector/test/resources/android/processor/view/inspector/InspectionCompanionGeneratorTest/NestedClass.java.txt b/tools/processors/view_inspector/test/resources/android/processor/view/inspector/InspectionCompanionGeneratorTest/NestedClass.java.txt
new file mode 100644
index 000000000000..e5fb6a2129f5
--- /dev/null
+++ b/tools/processors/view_inspector/test/resources/android/processor/view/inspector/InspectionCompanionGeneratorTest/NestedClass.java.txt
@@ -0,0 +1,22 @@
+package com.android.inspectable;
+
+import android.view.inspector.InspectionCompanion;
+import android.view.inspector.PropertyMapper;
+import android.view.inspector.PropertyReader;
+import java.lang.Override;
+
+/**
+ * Inspection companion for {@link Outer.Inner}.
+ *
+ * Generated by {@link android.processor.view.inspector.InspectionCompanionGenerator}
+ * on behalf of {@link android.processor.view.inspector.InspectionCompanionGeneratorTest}.
+ */
+public final class Outer$Inner$$InspectionCompanion implements InspectionCompanion<Outer.Inner> {
+ @Override
+ public void mapProperties(PropertyMapper propertyMapper) {
+ }
+
+ @Override
+ public void readProperties(Outer.Inner inspectable, PropertyReader propertyReader) {
+ }
+}
diff --git a/tools/processors/view_inspector/test/resources/android/processor/view/inspector/InspectionCompanionGeneratorTest/NodeName.java.txt b/tools/processors/view_inspector/test/resources/android/processor/view/inspector/InspectionCompanionGeneratorTest/NodeName.java.txt
new file mode 100644
index 000000000000..a334f50bbdf5
--- /dev/null
+++ b/tools/processors/view_inspector/test/resources/android/processor/view/inspector/InspectionCompanionGeneratorTest/NodeName.java.txt
@@ -0,0 +1,28 @@
+package com.android.inspectable;
+
+import android.view.inspector.InspectionCompanion;
+import android.view.inspector.PropertyMapper;
+import android.view.inspector.PropertyReader;
+import java.lang.Override;
+import java.lang.String;
+
+/**
+ * Inspection companion for {@link TestInspectable}.
+ *
+ * Generated by {@link android.processor.view.inspector.InspectionCompanionGenerator}
+ * on behalf of {@link android.processor.view.inspector.InspectionCompanionGeneratorTest}.
+ */
+public final class TestInspectable$$InspectionCompanion implements InspectionCompanion<TestInspectable> {
+ @Override
+ public void mapProperties(PropertyMapper propertyMapper) {
+ }
+
+ @Override
+ public void readProperties(TestInspectable inspectable, PropertyReader propertyReader) {
+ }
+
+ @Override
+ public String getNodeName() {
+ return "NodeName";
+ }
+}