Implement the map from schema names to AppSearch document classes
The map is implemented as shown in the design doc go/appsearch-polydoc-deserialization to properly handle GenericDocument's polymorphic deserialization issue.
Bug: 290389974
Test: Presubmit
Relnote: Implement the map from schema names to AppSearch document classes
Change-Id: I0a0bf7eac328d10858b97847707e5d5c67be24d8
diff --git a/appsearch/appsearch/api/current.txt b/appsearch/appsearch/api/current.txt
index 96049d0..e6eb190 100644
--- a/appsearch/appsearch/api/current.txt
+++ b/appsearch/appsearch/api/current.txt
@@ -92,6 +92,11 @@
method public androidx.appsearch.app.AppSearchBatchResult.Builder<KeyType!,ValueType!> setSuccess(KeyType, ValueType?);
}
+ @AnyThread public abstract class AppSearchDocumentClassMap {
+ ctor public AppSearchDocumentClassMap();
+ method protected abstract java.util.Map<java.lang.String!,java.util.List<java.lang.String!>!> getMap();
+ }
+
public final class AppSearchResult<ValueType> {
method public String? getErrorMessage();
method public int getResultCode();
diff --git a/appsearch/appsearch/api/restricted_current.txt b/appsearch/appsearch/api/restricted_current.txt
index 96049d0..e6eb190 100644
--- a/appsearch/appsearch/api/restricted_current.txt
+++ b/appsearch/appsearch/api/restricted_current.txt
@@ -92,6 +92,11 @@
method public androidx.appsearch.app.AppSearchBatchResult.Builder<KeyType!,ValueType!> setSuccess(KeyType, ValueType?);
}
+ @AnyThread public abstract class AppSearchDocumentClassMap {
+ ctor public AppSearchDocumentClassMap();
+ method protected abstract java.util.Map<java.lang.String!,java.util.List<java.lang.String!>!> getMap();
+ }
+
public final class AppSearchResult<ValueType> {
method public String? getErrorMessage();
method public int getResultCode();
diff --git a/appsearch/appsearch/build.gradle b/appsearch/appsearch/build.gradle
index 1fd3fdb..a940b11 100644
--- a/appsearch/appsearch/build.gradle
+++ b/appsearch/appsearch/build.gradle
@@ -34,6 +34,7 @@
dependencies {
api('androidx.annotation:annotation:1.1.0')
api(libs.guavaListenableFuture)
+ api(libs.autoServiceAnnotations)
implementation('androidx.collection:collection:1.2.0')
implementation('androidx.concurrent:concurrent-futures:1.0.0')
diff --git a/appsearch/appsearch/proguard-rules.pro b/appsearch/appsearch/proguard-rules.pro
index a7af3cc..c344815 100644
--- a/appsearch/appsearch/proguard-rules.pro
+++ b/appsearch/appsearch/proguard-rules.pro
@@ -12,5 +12,6 @@
# See the License for the specific language governing permissions and
# limitations under the License.
-keep class ** implements androidx.appsearch.app.DocumentClassFactory { *; }
+-keep class ** implements androidx.appsearch.app.AppSearchDocumentClassMap { *; }
-keep @androidx.appsearch.annotation.Document class *
diff --git a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/AnnotationProcessorTestBase.java b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/AnnotationProcessorTestBase.java
index 690eb0f..5abb9d0 100644
--- a/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/AnnotationProcessorTestBase.java
+++ b/appsearch/appsearch/src/androidTest/java/androidx/appsearch/app/AnnotationProcessorTestBase.java
@@ -49,7 +49,9 @@
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
+import java.util.HashMap;
import java.util.List;
+import java.util.Map;
public abstract class AnnotationProcessorTestBase {
private AppSearchSession mSession;
@@ -1425,4 +1427,123 @@
documents = convertSearchResultsToDocuments(searchResults);
assertThat(documents).containsExactly(businessGeneric);
}
+
+ @Test
+ public void testAppSearchDocumentClassMap() throws Exception {
+ // Before this test, AppSearch's annotation processor has already generated the maps for
+ // each module at compile time for all document classes available in the current JVM
+ // environment.
+ Map<String, List<String>> expectedDocumentMap = new HashMap<>();
+ // The following classes come from androidx.appsearch.builtintypes.
+ expectedDocumentMap.put("builtin:StopwatchLap",
+ Arrays.asList("androidx.appsearch.builtintypes.StopwatchLap"));
+ expectedDocumentMap.put("builtin:Thing",
+ Arrays.asList("androidx.appsearch.builtintypes.Thing"));
+ expectedDocumentMap.put("builtin:ContactPoint",
+ Arrays.asList("androidx.appsearch.builtintypes.ContactPoint"));
+ expectedDocumentMap.put("builtin:Person",
+ Arrays.asList("androidx.appsearch.builtintypes.Person"));
+ expectedDocumentMap.put("builtin:AlarmInstance",
+ Arrays.asList("androidx.appsearch.builtintypes.AlarmInstance"));
+ expectedDocumentMap.put("Keyword",
+ Arrays.asList("androidx.appsearch.builtintypes.properties.Keyword"));
+ expectedDocumentMap.put("builtin:Alarm",
+ Arrays.asList("androidx.appsearch.builtintypes.Alarm"));
+ expectedDocumentMap.put("builtin:Timer",
+ Arrays.asList("androidx.appsearch.builtintypes.Timer"));
+ expectedDocumentMap.put("builtin:ImageObject",
+ Arrays.asList("androidx.appsearch.builtintypes.ImageObject"));
+ expectedDocumentMap.put("builtin:PotentialAction",
+ Arrays.asList("androidx.appsearch.builtintypes.PotentialAction"));
+ expectedDocumentMap.put("builtin:Stopwatch",
+ Arrays.asList("androidx.appsearch.builtintypes.Stopwatch"));
+ // The following classes come from all test files in androidx.appsearch.cts and
+ // androidx.appsearch.app.
+ expectedDocumentMap.put("Artist",
+ Arrays.asList("androidx.appsearch.cts.app.SetSchemaRequestCtsTest$Artist"));
+ expectedDocumentMap.put("Organization",
+ Arrays.asList("androidx.appsearch.app.AnnotationProcessorTestBase$Organization",
+ "androidx.appsearch.cts.app.SetSchemaRequestCtsTest$Organization"));
+ expectedDocumentMap.put("Email",
+ Arrays.asList("androidx.appsearch.app.AnnotationProcessorTestBase$Email",
+ "androidx.appsearch.cts.app.SetSchemaRequestCtsTest$Email"));
+ expectedDocumentMap.put("Message",
+ Arrays.asList("androidx.appsearch.app.AnnotationProcessorTestBase$Message",
+ "androidx.appsearch.cts.app.SetSchemaRequestCtsTest$Message"));
+ expectedDocumentMap.put("Parent",
+ Arrays.asList("androidx.appsearch.cts.app.SetSchemaRequestCtsTest$Parent"));
+ expectedDocumentMap.put("Outer",
+ Arrays.asList("androidx.appsearch.app.AnnotationProcessorTestBase$Outer",
+ "androidx.appsearch.cts.app.SetSchemaRequestCtsTest$Outer"));
+ expectedDocumentMap.put("BusinessImpl",
+ Arrays.asList("androidx.appsearch.app.AnnotationProcessorTestBase$BusinessImpl"));
+ expectedDocumentMap.put("Inner",
+ Arrays.asList("androidx.appsearch.app.AnnotationProcessorTestBase$Inner",
+ "androidx.appsearch.cts.app.SetSchemaRequestCtsTest$Inner"));
+ expectedDocumentMap.put("King",
+ Arrays.asList("androidx.appsearch.cts.app.SetSchemaRequestCtsTest$King",
+ "androidx.appsearch.cts.app.SearchSpecCtsTest$King",
+ "androidx.appsearch.cts.observer.ObserverSpecCtsTest$King"));
+ expectedDocumentMap.put("ArtType",
+ Arrays.asList("androidx.appsearch.cts.app.SetSchemaRequestCtsTest$ArtType"));
+ expectedDocumentMap.put("Pineapple",
+ Arrays.asList("androidx.appsearch.app.AnnotationProcessorTestBase$Pineapple",
+ "androidx.appsearch.app.AnnotationProcessorTestBase$CoolPineapple"));
+ expectedDocumentMap.put("Jack",
+ Arrays.asList("androidx.appsearch.cts.observer.ObserverSpecCtsTest$Jack"));
+ expectedDocumentMap.put("ClassA",
+ Arrays.asList("androidx.appsearch.cts.app.SetSchemaRequestCtsTest$ClassA"));
+ expectedDocumentMap.put("ClassB",
+ Arrays.asList("androidx.appsearch.cts.app.SetSchemaRequestCtsTest$ClassB"));
+ expectedDocumentMap.put("Thing",
+ Arrays.asList("androidx.appsearch.cts.app.SetSchemaRequestCtsTest$Thing"));
+ expectedDocumentMap.put("Business",
+ Arrays.asList("androidx.appsearch.app.AnnotationProcessorTestBase$Business"));
+ expectedDocumentMap.put("Ace",
+ Arrays.asList("androidx.appsearch.cts.observer.ObserverSpecCtsTest$Ace"));
+ expectedDocumentMap.put("EmailMessage",
+ Arrays.asList("androidx.appsearch.app.AnnotationProcessorTestBase$EmailMessage",
+ "androidx.appsearch.cts.app.SetSchemaRequestCtsTest$EmailMessage"));
+ expectedDocumentMap.put("FakeMessage",
+ Arrays.asList("androidx.appsearch.app.AnnotationProcessorTestBase$FakeMessage"));
+ expectedDocumentMap.put("Root",
+ Arrays.asList("androidx.appsearch.app.AnnotationProcessorTestBase$Root"));
+ expectedDocumentMap.put("Queen",
+ Arrays.asList("androidx.appsearch.cts.app.SetSchemaRequestCtsTest$Queen",
+ "androidx.appsearch.cts.observer.ObserverSpecCtsTest$Queen"));
+ expectedDocumentMap.put("EmailDocument",
+ Arrays.asList("androidx.appsearch.cts.app.customer.EmailDocument"));
+ expectedDocumentMap.put("Middle",
+ Arrays.asList("androidx.appsearch.app.AnnotationProcessorTestBase$Middle",
+ "androidx.appsearch.cts.app.SetSchemaRequestCtsTest$Middle"));
+ expectedDocumentMap.put("Common",
+ Arrays.asList("androidx.appsearch.cts.app.SetSchemaRequestCtsTest$Common"));
+ expectedDocumentMap.put("Card",
+ Arrays.asList("androidx.appsearch.app.AnnotationProcessorTestBase$Card",
+ "androidx.appsearch.cts.app.SetSchemaRequestCtsTest$Card",
+ "androidx.appsearch.cts.app.PutDocumentsRequestCtsTest$Card"));
+ expectedDocumentMap.put("Gift",
+ Arrays.asList("androidx.appsearch.app.AnnotationProcessorTestBase$Gift"));
+ expectedDocumentMap.put("CardAction",
+ Arrays.asList("androidx.appsearch.app.AnnotationProcessorTestBase$CardAction"));
+ expectedDocumentMap.put("InterfaceRoot",
+ Arrays.asList("androidx.appsearch.app.AnnotationProcessorTestBase$InterfaceRoot"));
+ expectedDocumentMap.put("Person",
+ Arrays.asList("androidx.appsearch.app.AnnotationProcessorTestBase$Person",
+ "androidx.appsearch.cts.app.SetSchemaRequestCtsTest$Person"));
+ expectedDocumentMap.put("Place",
+ Arrays.asList("androidx.appsearch.app.AnnotationProcessorTestBase$Place"));
+ expectedDocumentMap.put("LongDoc",
+ Arrays.asList("androidx.appsearch.app.AnnotationProcessorTestBase$LongDoc"));
+ expectedDocumentMap.put("SampleAutoValue", Arrays.asList(
+ "androidx.appsearch.app.AnnotationProcessorTestBase$SampleAutoValue"));
+
+ Map<String, List<String>> actualDocumentMap = AppSearchDocumentClassMap.getMergedMap();
+ assertThat(actualDocumentMap.keySet()).containsAtLeastElementsIn(
+ expectedDocumentMap.keySet());
+ for (String key : expectedDocumentMap.keySet()) {
+ assertThat(actualDocumentMap.get(key)).containsAtLeastElementsIn(
+ expectedDocumentMap.get(key));
+ }
+ }
}
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/app/AppSearchDocumentClassMap.java b/appsearch/appsearch/src/main/java/androidx/appsearch/app/AppSearchDocumentClassMap.java
new file mode 100644
index 0000000..2021b26
--- /dev/null
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/app/AppSearchDocumentClassMap.java
@@ -0,0 +1,103 @@
+/*
+ * Copyright 2023 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.
+ */
+// @exportToFramework:skipFile()
+package androidx.appsearch.app;
+
+import androidx.annotation.AnyThread;
+import androidx.annotation.GuardedBy;
+import androidx.annotation.NonNull;
+import androidx.annotation.RestrictTo;
+import androidx.collection.ArrayMap;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.ServiceLoader;
+
+/**
+ * A class that maintains the map from schema type names to the fully qualified names of the
+ * corresponding document classes.
+ *
+ * <p>Do not extend this class directly. AppSearch's annotation processor will automatically
+ * generate necessary subclasses to hold the map.
+ */
+@AnyThread
+public abstract class AppSearchDocumentClassMap {
+
+ /**
+ * The cached value of {@link #getMergedMap()}.
+ */
+ private static volatile Map<String, List<String>> sMergedMap = null;
+
+ /**
+ * Collects all of the instances of the generated {@link AppSearchDocumentClassMap} classes
+ * available in the current JVM environment, and calls the {@link #getMap()} method from them to
+ * build, cache and return the merged map. The keys are schema type names, and the values are
+ * the lists of the corresponding document classes.
+ */
+ @NonNull
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ public static Map<String, List<String>> getMergedMap() {
+ if (sMergedMap == null) {
+ synchronized (AppSearchDocumentClassMap.class) {
+ if (sMergedMap == null) {
+ sMergedMap = buildMergedMapLocked();
+ }
+ }
+ }
+ return sMergedMap;
+ }
+
+ /**
+ * Returns the map from schema type names to the list of the fully qualified names of the
+ * corresponding document classes.
+ */
+ @NonNull
+ protected abstract Map<String, List<String>> getMap();
+
+ @NonNull
+ @GuardedBy("AppSearchDocumentClassMap.class")
+ private static Map<String, List<String>> buildMergedMapLocked() {
+ ServiceLoader<AppSearchDocumentClassMap> loader = ServiceLoader.load(
+ AppSearchDocumentClassMap.class);
+ Map<String, List<String>> result = new ArrayMap<>();
+ for (AppSearchDocumentClassMap appSearchDocumentClassMap : loader) {
+ Map<String, List<String>> documentClassMap = appSearchDocumentClassMap.getMap();
+ for (Map.Entry<String, List<String>> entry : documentClassMap.entrySet()) {
+ String schemaName = entry.getKey();
+ // A single schema name can be mapped to more than one document classes because
+ // document classes can choose to have arbitrary schema names. The most common
+ // case is when there are multiple AppSearch packages that define the same schema
+ // name. It is necessary to keep track all of the mapped document classes to prevent
+ // from losing any information.
+ List<String> documentClassNames = result.get(schemaName);
+ if (documentClassNames == null) {
+ documentClassNames = new ArrayList<>();
+ result.put(schemaName, documentClassNames);
+ }
+ documentClassNames.addAll(entry.getValue());
+ }
+ }
+
+ for (String schemaName : result.keySet()) {
+ result.put(schemaName,
+ Collections.unmodifiableList(Objects.requireNonNull(result.get(schemaName))));
+ }
+ return Collections.unmodifiableMap(result);
+ }
+}
diff --git a/appsearch/compiler/build.gradle b/appsearch/compiler/build.gradle
index 57511cb..8892047 100644
--- a/appsearch/compiler/build.gradle
+++ b/appsearch/compiler/build.gradle
@@ -27,6 +27,8 @@
api('androidx.annotation:annotation:1.1.0')
api(libs.jsr250)
implementation(libs.autoCommon)
+ implementation(libs.autoService)
+ implementation(libs.autoServiceAnnotations)
implementation(libs.autoValue)
implementation(libs.autoValueAnnotations)
implementation(libs.guava)
diff --git a/appsearch/compiler/src/main/java/androidx/appsearch/compiler/AppSearchCompiler.java b/appsearch/compiler/src/main/java/androidx/appsearch/compiler/AppSearchCompiler.java
index 4ea0f1d6..998ce5c 100644
--- a/appsearch/compiler/src/main/java/androidx/appsearch/compiler/AppSearchCompiler.java
+++ b/appsearch/compiler/src/main/java/androidx/appsearch/compiler/AppSearchCompiler.java
@@ -17,6 +17,7 @@
import static androidx.appsearch.compiler.IntrospectionHelper.APPSEARCH_ANNOTATION_PKG;
import static androidx.appsearch.compiler.IntrospectionHelper.DOCUMENT_ANNOTATION_SIMPLE_CLASS_NAME;
+
import static javax.lang.model.util.ElementFilter.typesIn;
import androidx.annotation.NonNull;
@@ -28,9 +29,19 @@
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableSetMultimap;
+import com.squareup.javapoet.JavaFile;
import java.io.File;
import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
import java.util.Set;
import javax.annotation.processing.Messager;
@@ -74,10 +85,16 @@
private static final class AppSearchCompileStep implements Step {
private final ProcessingEnvironment mProcessingEnv;
private final Messager mMessager;
+ private final Map<String, List<String>> mDocumentClassMap;
+ // Annotation processing can be run in multiple rounds. This tracks the index of current
+ // round starting from 0.
+ private int mRoundIndex;
AppSearchCompileStep(ProcessingEnvironment processingEnv) {
mProcessingEnv = processingEnv;
mMessager = processingEnv.getMessager();
+ mDocumentClassMap = new HashMap<>();
+ mRoundIndex = -1;
}
@Override
@@ -88,11 +105,16 @@
@Override
public ImmutableSet<Element> process(
ImmutableSetMultimap<String, Element> elementsByAnnotation) {
+ mDocumentClassMap.clear();
+ mRoundIndex += 1;
+
Set<TypeElement> documentElements =
typesIn(elementsByAnnotation.get(
IntrospectionHelper.DOCUMENT_ANNOTATION_CLASS.canonicalName()));
ImmutableSet.Builder<Element> nextRound = new ImmutableSet.Builder<>();
+ String documentMapClassPackage = null;
+ Set<String> classNames = new HashSet<>();
for (TypeElement document : documentElements) {
try {
processDocument(document);
@@ -104,11 +126,58 @@
// Prints error message.
e.printDiagnostic(mMessager);
}
+ classNames.add(document.getQualifiedName().toString());
+ String packageName =
+ mProcessingEnv.getElementUtils().getPackageOf(document).toString();
+ // We must choose a deterministic package to place the generated document map
+ // class. Given multiple packages, we have no real preference between them. So
+ // for the sake of making a deterministic selection, we always choose to generate
+ // the map in the lexicographically smallest package.
+ if (documentMapClassPackage == null || packageName.compareTo(
+ documentMapClassPackage) < 0) {
+ documentMapClassPackage = packageName;
+ }
}
+
+ try {
+ if (!classNames.isEmpty()) {
+ // Append the hash code of classNames and the index of the current round as a
+ // suffix to the name of the generated document map class. This will prevent
+ // the generation of two classes with the same name, which could otherwise
+ // happen when there are two Java modules that contain classes in the same
+ // package name, or there are multiple rounds of annotation processing for some
+ // module.
+ String classSuffix = generateStringSetHash(
+ classNames, /* delimiter= */ ",") + "_" + mRoundIndex;
+ writeJavaFile(DocumentMapGenerator.generate(mProcessingEnv,
+ documentMapClassPackage, classSuffix, mDocumentClassMap));
+ }
+ } catch (NoSuchAlgorithmException | IOException e) {
+ mProcessingEnv.getMessager().printMessage(Kind.ERROR,
+ "Failed to create the AppSearch document map class: " + e);
+ }
+
// Pass elements to next round of processing.
return nextRound.build();
}
+ private void writeJavaFile(JavaFile javaFile) throws IOException {
+ String outputDir = mProcessingEnv.getOptions().get(OUTPUT_DIR_OPTION);
+ if (outputDir == null || outputDir.isEmpty()) {
+ javaFile.writeTo(mProcessingEnv.getFiler());
+ } else {
+ mMessager.printMessage(
+ Kind.NOTE,
+ "Writing output to \"" + outputDir
+ + "\" due to the presence of -A" + OUTPUT_DIR_OPTION);
+ javaFile.writeTo(new File(outputDir));
+ }
+ }
+
+ /**
+ * Process the document class by generating a factory class for it and properly update
+ * {@link #mDocumentClassMap}.
+ */
private void processDocument(@NonNull TypeElement element)
throws ProcessingException, MissingTypeException {
if (element.getKind() != ElementKind.CLASS
@@ -137,23 +206,19 @@
model = DocumentModel.createPojoModel(mProcessingEnv, element);
}
CodeGenerator generator = CodeGenerator.generate(mProcessingEnv, model);
- String outputDir = mProcessingEnv.getOptions().get(OUTPUT_DIR_OPTION);
try {
- if (outputDir == null || outputDir.isEmpty()) {
- generator.writeToFiler();
- } else {
- mMessager.printMessage(
- Kind.NOTE,
- "Writing output to \"" + outputDir
- + "\" due to the presence of -A" + OUTPUT_DIR_OPTION);
- generator.writeToFolder(new File(outputDir));
- }
+ writeJavaFile(generator.createJavaFile());
} catch (IOException e) {
ProcessingException pe =
new ProcessingException("Failed to write output", model.getClassElement());
pe.initCause(e);
throw pe;
}
+
+ List<String> documentClassList = mDocumentClassMap.computeIfAbsent(
+ model.getSchemaName(), k -> new ArrayList<>());
+ documentClassList.add(
+ mProcessingEnv.getElementUtils().getBinaryName(element).toString());
}
/**
@@ -172,5 +237,34 @@
String dot = pkg.isEmpty() ? "" : ".";
return pkg + dot + "AutoValue_" + name;
}
+
+ /**
+ * Generate a SHA-256 hash for a given string set.
+ *
+ * @param set The set of the strings.
+ * @param delimiter The delimiter used to separate the strings, which should not have
+ * appeared in any of the strings in the set.
+ */
+ @NonNull
+ private static String generateStringSetHash(@NonNull Set<String> set,
+ @NonNull String delimiter) throws NoSuchAlgorithmException {
+ List<String> sortedList = new ArrayList<>(set);
+ Collections.sort(sortedList);
+
+ MessageDigest md = MessageDigest.getInstance("SHA-256");
+ for (String s : sortedList) {
+ md.update(s.getBytes(StandardCharsets.UTF_8));
+ md.update(delimiter.getBytes(StandardCharsets.UTF_8));
+ }
+ StringBuilder result = new StringBuilder();
+ for (byte b : md.digest()) {
+ String hex = Integer.toHexString(0xFF & b);
+ if (hex.length() == 1) {
+ result.append('0');
+ }
+ result.append(hex);
+ }
+ return result.toString();
+ }
}
}
diff --git a/appsearch/compiler/src/main/java/androidx/appsearch/compiler/CodeGenerator.java b/appsearch/compiler/src/main/java/androidx/appsearch/compiler/CodeGenerator.java
index 44cc1f0..caa2e58 100644
--- a/appsearch/compiler/src/main/java/androidx/appsearch/compiler/CodeGenerator.java
+++ b/appsearch/compiler/src/main/java/androidx/appsearch/compiler/CodeGenerator.java
@@ -25,7 +25,6 @@
import com.squareup.javapoet.TypeName;
import com.squareup.javapoet.TypeSpec;
-import java.io.File;
import java.io.IOException;
import javax.annotation.processing.ProcessingEnvironment;
@@ -62,12 +61,8 @@
mOutputClass = createClass();
}
- public void writeToFiler() throws IOException {
- JavaFile.builder(mOutputPackage, mOutputClass).build().writeTo(mEnv.getFiler());
- }
-
- public void writeToFolder(@NonNull File folder) throws IOException {
- JavaFile.builder(mOutputPackage, mOutputClass).build().writeTo(folder);
+ public JavaFile createJavaFile() throws IOException {
+ return JavaFile.builder(mOutputPackage, mOutputClass).build();
}
/**
diff --git a/appsearch/compiler/src/main/java/androidx/appsearch/compiler/DocumentMapGenerator.java b/appsearch/compiler/src/main/java/androidx/appsearch/compiler/DocumentMapGenerator.java
new file mode 100644
index 0000000..59b2c54
--- /dev/null
+++ b/appsearch/compiler/src/main/java/androidx/appsearch/compiler/DocumentMapGenerator.java
@@ -0,0 +1,108 @@
+/*
+ * Copyright 2023 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 androidx.appsearch.compiler;
+
+import androidx.annotation.NonNull;
+
+import com.google.auto.common.GeneratedAnnotationSpecs;
+import com.google.auto.service.AutoService;
+import com.squareup.javapoet.AnnotationSpec;
+import com.squareup.javapoet.ClassName;
+import com.squareup.javapoet.CodeBlock;
+import com.squareup.javapoet.JavaFile;
+import com.squareup.javapoet.MethodSpec;
+import com.squareup.javapoet.ParameterizedTypeName;
+import com.squareup.javapoet.TypeName;
+import com.squareup.javapoet.TypeSpec;
+
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+import javax.annotation.processing.ProcessingEnvironment;
+import javax.lang.model.element.Modifier;
+
+/**
+ * A class that wraps a static method that generates a java class of
+ * {@link androidx.appsearch.app.AppSearchDocumentClassMap}.
+ */
+public class DocumentMapGenerator {
+ /**
+ * Returns the generated {@link androidx.appsearch.app.AppSearchDocumentClassMap}, based on
+ * the provided document class map.
+ *
+ * @param documentClassMap The map from schema type names to the list of the fully qualified
+ * names of the corresponding document classes, so that the
+ * {@code getMap} method of the generated class can return this map.
+ */
+ @NonNull
+ public static JavaFile generate(@NonNull ProcessingEnvironment processingEnv,
+ @NonNull String packageName, @NonNull String classSuffix,
+ @NonNull Map<String, List<String>> documentClassMap) {
+ ClassName superClassName = ClassName.get(
+ IntrospectionHelper.APPSEARCH_PKG, "AppSearchDocumentClassMap");
+ TypeSpec.Builder genClass = TypeSpec
+ .classBuilder(IntrospectionHelper.GEN_CLASS_PREFIX + "DocumentClassMap" + "_"
+ + classSuffix)
+ .addModifiers(Modifier.PUBLIC, Modifier.FINAL)
+ .superclass(superClassName)
+ .addAnnotation(AnnotationSpec.builder(AutoService.class)
+ .addMember("value", "$T.class", superClassName)
+ .build());
+
+ // Add the @Generated annotation to avoid static analysis running on these files
+ GeneratedAnnotationSpecs.generatedAnnotationSpec(
+ processingEnv.getElementUtils(),
+ processingEnv.getSourceVersion(),
+ AppSearchCompiler.class
+ ).ifPresent(genClass::addAnnotation);
+
+ // The type of the map is Map<String, List<String>>.
+ TypeName returnType = ParameterizedTypeName.get(ClassName.get(Map.class),
+ ClassName.get(String.class),
+ ParameterizedTypeName.get(ClassName.get(List.class), ClassName.get(String.class)));
+
+ genClass.addMethod(MethodSpec.methodBuilder("getMap")
+ .addModifiers(Modifier.PROTECTED)
+ .returns(returnType)
+ .addAnnotation(NonNull.class)
+ .addAnnotation(Override.class)
+ .addStatement("$T result = new $T<>()", returnType,
+ ClassName.get(HashMap.class))
+ .addCode(getMapConstructionCode(documentClassMap))
+ .addStatement("return result")
+ .build());
+
+ return JavaFile.builder(packageName, genClass.build()).build();
+ }
+
+ private static CodeBlock getMapConstructionCode(
+ @NonNull Map<String, List<String>> documentClassMap) {
+ CodeBlock.Builder mapContentBuilder = CodeBlock.builder();
+ for (Map.Entry<String, List<String>> entry : documentClassMap.entrySet()) {
+ String valueString = entry.getValue().stream().map(
+ value -> "\"" + value + "\"").collect(Collectors.joining(", "));
+ mapContentBuilder.addStatement("result.put($S, $T.asList($L))", entry.getKey(),
+ ClassName.get(Arrays.class), valueString);
+ }
+ return mapContentBuilder.build();
+ }
+
+ private DocumentMapGenerator() {}
+}
diff --git a/appsearch/compiler/src/test/java/androidx/appsearch/compiler/AppSearchCompilerTest.java b/appsearch/compiler/src/test/java/androidx/appsearch/compiler/AppSearchCompilerTest.java
index e14f777..e2b4532 100644
--- a/appsearch/compiler/src/test/java/androidx/appsearch/compiler/AppSearchCompilerTest.java
+++ b/appsearch/compiler/src/test/java/androidx/appsearch/compiler/AppSearchCompilerTest.java
@@ -1491,6 +1491,43 @@
assertThat(compilation).succeededWithoutWarnings();
checkEqualsGolden("AutoValue_Gift.java");
+ checkDocumentMapEqualsGolden(/* roundIndex= */0);
+ // The number of rounds that the annotation processor takes can vary from setup to setup.
+ // In this test case, AutoValue documents are processed in the second round because their
+ // generated classes are not available in the first turn.
+ checkDocumentMapEqualsGolden(/* roundIndex= */1);
+ }
+
+ @Test
+ public void testAutoValueDocumentWithNormalDocument() throws IOException {
+ Compilation compilation = compile(
+ "import com.google.auto.value.AutoValue;\n"
+ + "import com.google.auto.value.AutoValue.*;\n"
+ + "@Document\n"
+ + "class Person {\n"
+ + " @Document.Namespace String namespace;\n"
+ + " @Document.Id String id;\n"
+ + "}\n"
+ + "@Document\n"
+ + "@AutoValue\n"
+ + "public abstract class Gift {\n"
+ + " @CopyAnnotations @Document.Id abstract String id();\n"
+ + " @CopyAnnotations @Document.Namespace abstract String namespace();\n"
+ + " @CopyAnnotations\n"
+ + " @Document.StringProperty abstract String property();\n"
+ + " public static Gift create(String id, String namespace, String"
+ + " property) {\n"
+ + " return new AutoValue_Gift(id, namespace, property);\n"
+ + " }\n"
+ + "}\n");
+
+ assertThat(compilation).succeededWithoutWarnings();
+ checkEqualsGolden("AutoValue_Gift.java");
+ checkDocumentMapEqualsGolden(/* roundIndex= */0);
+ // The number of rounds that the annotation processor takes can vary from setup to setup.
+ // In this test case, AutoValue documents are processed in the second round because their
+ // generated classes are not available in the first turn.
+ checkDocumentMapEqualsGolden(/* roundIndex= */1);
}
@Test
@@ -1639,6 +1676,7 @@
checkResultContains("Gift.java", "addParentType($$__AppSearch__Parent2.SCHEMA_NAME)");
checkEqualsGolden("Gift.java");
+ checkDocumentMapEqualsGolden(/* roundIndex= */0);
}
@Test
@@ -1998,6 +2036,7 @@
checkResultContains("Gift.java", "document.getStr2()");
checkResultContains("Gift.java", "document.getPrice()");
checkEqualsGolden("Gift.java");
+ checkDocumentMapEqualsGolden(/* roundIndex= */0);
}
@Test
@@ -2771,6 +2810,28 @@
"Method cannot be used to create a document class: abstract constructor");
}
+ @Test
+ public void testDocumentClassesWithDuplicatedNames() throws Exception {
+ Compilation compilation = compile(
+ "@Document(name=\"A\")\n"
+ + "class MyClass1 {\n"
+ + " @Document.Namespace String namespace;\n"
+ + " @Document.Id String id;\n"
+ + "}\n"
+ + "@Document(name=\"A\")\n"
+ + "class MyClass2 {\n"
+ + " @Document.Namespace String namespace;\n"
+ + " @Document.Id String id;\n"
+ + "}\n"
+ + "@Document(name=\"B\")\n"
+ + "class MyClass3 {\n"
+ + " @Document.Namespace String namespace;\n"
+ + " @Document.Id String id;\n"
+ + "}\n");
+ assertThat(compilation).succeededWithoutWarnings();
+ checkDocumentMapEqualsGolden(/* roundIndex= */0);
+ }
+
private Compilation compile(String classBody) {
return compile("Gift", classBody);
}
@@ -2797,8 +2858,27 @@
}
private void checkEqualsGolden(String className) throws IOException {
- // Get the expected file contents
String goldenResPath = "goldens/" + mTestName.getMethodName() + ".JAVA";
+ File actualPackageDir = new File(mGenFilesDir, "com/example/appsearch");
+ File actualPath =
+ new File(actualPackageDir, IntrospectionHelper.GEN_CLASS_PREFIX + className);
+ checkEqualsGoldenHelper(goldenResPath, actualPath);
+ }
+
+ private void checkDocumentMapEqualsGolden(int roundIndex) throws IOException {
+ String goldenResPath =
+ "goldens/" + mTestName.getMethodName() + "DocumentMap_" + roundIndex + ".JAVA";
+ File actualPackageDir = new File(mGenFilesDir, "com/example/appsearch");
+ File[] files = actualPackageDir.listFiles((dir, name) ->
+ name.startsWith(IntrospectionHelper.GEN_CLASS_PREFIX + "DocumentClassMap")
+ && name.endsWith("_" + roundIndex + ".java"));
+ Truth.assertThat(files).isNotNull();
+ Truth.assertThat(files).hasLength(1);
+ checkEqualsGoldenHelper(goldenResPath, files[0]);
+ }
+
+ private void checkEqualsGoldenHelper(String goldenResPath, File actualPath) throws IOException {
+ // Get the expected file contents
String expected = "";
try (InputStream is = getClass().getResourceAsStream(goldenResPath)) {
if (is == null) {
@@ -2810,9 +2890,6 @@
}
// Get the actual file contents
- File actualPackageDir = new File(mGenFilesDir, "com/example/appsearch");
- File actualPath =
- new File(actualPackageDir, IntrospectionHelper.GEN_CLASS_PREFIX + className);
Truth.assertWithMessage("Path " + actualPath + " is not a file")
.that(actualPath.isFile()).isTrue();
String actual = Files.asCharSource(actualPath, StandardCharsets.UTF_8).read();
diff --git a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testAutoValueDocumentDocumentMap_0.JAVA b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testAutoValueDocumentDocumentMap_0.JAVA
new file mode 100644
index 0000000..36b4fc9
--- /dev/null
+++ b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testAutoValueDocumentDocumentMap_0.JAVA
@@ -0,0 +1,22 @@
+package com.example.appsearch;
+
+import androidx.annotation.NonNull;
+import androidx.appsearch.app.AppSearchDocumentClassMap;
+import com.google.auto.service.AutoService;
+import java.lang.Override;
+import java.lang.String;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import javax.annotation.processing.Generated;
+
+@AutoService(AppSearchDocumentClassMap.class)
+@Generated("androidx.appsearch.compiler.AppSearchCompiler")
+public final class $$__AppSearch__DocumentClassMap_71ecb22a3f48746c4261bd34b4ba3c2861632443de7f7c577156ec583523e7f6_0 extends AppSearchDocumentClassMap {
+ @NonNull
+ @Override
+ protected Map<String, List<String>> getMap() {
+ Map<String, List<String>> result = new HashMap<>();
+ return result;
+ }
+}
diff --git a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testAutoValueDocumentDocumentMap_1.JAVA b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testAutoValueDocumentDocumentMap_1.JAVA
new file mode 100644
index 0000000..4e88fc3
--- /dev/null
+++ b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testAutoValueDocumentDocumentMap_1.JAVA
@@ -0,0 +1,24 @@
+package com.example.appsearch;
+
+import androidx.annotation.NonNull;
+import androidx.appsearch.app.AppSearchDocumentClassMap;
+import com.google.auto.service.AutoService;
+import java.lang.Override;
+import java.lang.String;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import javax.annotation.processing.Generated;
+
+@AutoService(AppSearchDocumentClassMap.class)
+@Generated("androidx.appsearch.compiler.AppSearchCompiler")
+public final class $$__AppSearch__DocumentClassMap_71ecb22a3f48746c4261bd34b4ba3c2861632443de7f7c577156ec583523e7f6_1 extends AppSearchDocumentClassMap {
+ @NonNull
+ @Override
+ protected Map<String, List<String>> getMap() {
+ Map<String, List<String>> result = new HashMap<>();
+ result.put("Gift", Arrays.asList("com.example.appsearch.Gift"));
+ return result;
+ }
+}
diff --git a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testAutoValueDocumentWithNormalDocument.JAVA b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testAutoValueDocumentWithNormalDocument.JAVA
new file mode 100644
index 0000000..1c28dfb
--- /dev/null
+++ b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testAutoValueDocumentWithNormalDocument.JAVA
@@ -0,0 +1,63 @@
+package com.example.appsearch;
+
+import androidx.appsearch.app.AppSearchSchema;
+import androidx.appsearch.app.DocumentClassFactory;
+import androidx.appsearch.app.GenericDocument;
+import androidx.appsearch.exceptions.AppSearchException;
+import java.lang.Class;
+import java.lang.Override;
+import java.lang.String;
+import java.util.Collections;
+import java.util.List;
+import javax.annotation.processing.Generated;
+
+@Generated("androidx.appsearch.compiler.AppSearchCompiler")
+public final class $$__AppSearch__AutoValue_Gift implements DocumentClassFactory<Gift> {
+ public static final String SCHEMA_NAME = "Gift";
+
+ @Override
+ public String getSchemaName() {
+ return SCHEMA_NAME;
+ }
+
+ @Override
+ public AppSearchSchema getSchema() throws AppSearchException {
+ return new AppSearchSchema.Builder(SCHEMA_NAME)
+ .addProperty(new AppSearchSchema.StringPropertyConfig.Builder("property")
+ .setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
+ .setTokenizerType(AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_NONE)
+ .setIndexingType(AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_NONE)
+ .setJoinableValueType(AppSearchSchema.StringPropertyConfig.JOINABLE_VALUE_TYPE_NONE)
+ .build())
+ .build();
+ }
+
+ @Override
+ public List<Class<?>> getDependencyDocumentClasses() throws AppSearchException {
+ return Collections.emptyList();
+ }
+
+ @Override
+ public GenericDocument toGenericDocument(Gift document) throws AppSearchException {
+ GenericDocument.Builder<?> builder =
+ new GenericDocument.Builder<>(document.namespace(), document.id(), SCHEMA_NAME);
+ String propertyCopy = document.property();
+ if (propertyCopy != null) {
+ builder.setPropertyString("property", propertyCopy);
+ }
+ return builder.build();
+ }
+
+ @Override
+ public Gift fromGenericDocument(GenericDocument genericDoc) throws AppSearchException {
+ String namespaceConv = genericDoc.getNamespace();
+ String idConv = genericDoc.getId();
+ String[] propertyCopy = genericDoc.getPropertyStringArray("property");
+ String propertyConv = null;
+ if (propertyCopy != null && propertyCopy.length != 0) {
+ propertyConv = propertyCopy[0];
+ }
+ Gift document = Gift.create(idConv, namespaceConv, propertyConv);
+ return document;
+ }
+}
diff --git a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testAutoValueDocumentWithNormalDocumentDocumentMap_0.JAVA b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testAutoValueDocumentWithNormalDocumentDocumentMap_0.JAVA
new file mode 100644
index 0000000..4b302b6
--- /dev/null
+++ b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testAutoValueDocumentWithNormalDocumentDocumentMap_0.JAVA
@@ -0,0 +1,24 @@
+package com.example.appsearch;
+
+import androidx.annotation.NonNull;
+import androidx.appsearch.app.AppSearchDocumentClassMap;
+import com.google.auto.service.AutoService;
+import java.lang.Override;
+import java.lang.String;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import javax.annotation.processing.Generated;
+
+@AutoService(AppSearchDocumentClassMap.class)
+@Generated("androidx.appsearch.compiler.AppSearchCompiler")
+public final class $$__AppSearch__DocumentClassMap_8bbdfb6f96b48bfad89bc598cab2960300d30e5388d5aaa970ab2db67993d889_0 extends AppSearchDocumentClassMap {
+ @NonNull
+ @Override
+ protected Map<String, List<String>> getMap() {
+ Map<String, List<String>> result = new HashMap<>();
+ result.put("Person", Arrays.asList("com.example.appsearch.Person"));
+ return result;
+ }
+}
diff --git a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testAutoValueDocumentWithNormalDocumentDocumentMap_1.JAVA b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testAutoValueDocumentWithNormalDocumentDocumentMap_1.JAVA
new file mode 100644
index 0000000..4e88fc3
--- /dev/null
+++ b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testAutoValueDocumentWithNormalDocumentDocumentMap_1.JAVA
@@ -0,0 +1,24 @@
+package com.example.appsearch;
+
+import androidx.annotation.NonNull;
+import androidx.appsearch.app.AppSearchDocumentClassMap;
+import com.google.auto.service.AutoService;
+import java.lang.Override;
+import java.lang.String;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import javax.annotation.processing.Generated;
+
+@AutoService(AppSearchDocumentClassMap.class)
+@Generated("androidx.appsearch.compiler.AppSearchCompiler")
+public final class $$__AppSearch__DocumentClassMap_71ecb22a3f48746c4261bd34b4ba3c2861632443de7f7c577156ec583523e7f6_1 extends AppSearchDocumentClassMap {
+ @NonNull
+ @Override
+ protected Map<String, List<String>> getMap() {
+ Map<String, List<String>> result = new HashMap<>();
+ result.put("Gift", Arrays.asList("com.example.appsearch.Gift"));
+ return result;
+ }
+}
diff --git a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testDocumentClassesWithDuplicatedNamesDocumentMap_0.JAVA b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testDocumentClassesWithDuplicatedNamesDocumentMap_0.JAVA
new file mode 100644
index 0000000..72c5bc0
--- /dev/null
+++ b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testDocumentClassesWithDuplicatedNamesDocumentMap_0.JAVA
@@ -0,0 +1,25 @@
+package com.example.appsearch;
+
+import androidx.annotation.NonNull;
+import androidx.appsearch.app.AppSearchDocumentClassMap;
+import com.google.auto.service.AutoService;
+import java.lang.Override;
+import java.lang.String;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import javax.annotation.processing.Generated;
+
+@AutoService(AppSearchDocumentClassMap.class)
+@Generated("androidx.appsearch.compiler.AppSearchCompiler")
+public final class $$__AppSearch__DocumentClassMap_be3cf5fe3ee0964354144ba564257ea30a4560a4f416181f226daea685c5fde5_0 extends AppSearchDocumentClassMap {
+ @NonNull
+ @Override
+ protected Map<String, List<String>> getMap() {
+ Map<String, List<String>> result = new HashMap<>();
+ result.put("A", Arrays.asList("com.example.appsearch.MyClass1", "com.example.appsearch.MyClass2"));
+ result.put("B", Arrays.asList("com.example.appsearch.MyClass3"));
+ return result;
+ }
+}
diff --git a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testInterfaceImplementingParentsDocumentMap_0.JAVA b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testInterfaceImplementingParentsDocumentMap_0.JAVA
new file mode 100644
index 0000000..53c51e5
--- /dev/null
+++ b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testInterfaceImplementingParentsDocumentMap_0.JAVA
@@ -0,0 +1,27 @@
+package com.example.appsearch;
+
+import androidx.annotation.NonNull;
+import androidx.appsearch.app.AppSearchDocumentClassMap;
+import com.google.auto.service.AutoService;
+import java.lang.Override;
+import java.lang.String;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import javax.annotation.processing.Generated;
+
+@AutoService(AppSearchDocumentClassMap.class)
+@Generated("androidx.appsearch.compiler.AppSearchCompiler")
+public final class $$__AppSearch__DocumentClassMap_fc605c65514dc62366bce05ca8e3a0a22e13d867acb0e63a098ee18dbd2de01e_0 extends AppSearchDocumentClassMap {
+ @NonNull
+ @Override
+ protected Map<String, List<String>> getMap() {
+ Map<String, List<String>> result = new HashMap<>();
+ result.put("Gift", Arrays.asList("com.example.appsearch.Gift"));
+ result.put("Root", Arrays.asList("com.example.appsearch.Root"));
+ result.put("Parent2", Arrays.asList("com.example.appsearch.Parent2"));
+ result.put("Parent1", Arrays.asList("com.example.appsearch.Parent1"));
+ return result;
+ }
+}
diff --git a/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testPolymorphismDocumentMap_0.JAVA b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testPolymorphismDocumentMap_0.JAVA
new file mode 100644
index 0000000..1bfb085
--- /dev/null
+++ b/appsearch/compiler/src/test/resources/androidx/appsearch/compiler/goldens/testPolymorphismDocumentMap_0.JAVA
@@ -0,0 +1,26 @@
+package com.example.appsearch;
+
+import androidx.annotation.NonNull;
+import androidx.appsearch.app.AppSearchDocumentClassMap;
+import com.google.auto.service.AutoService;
+import java.lang.Override;
+import java.lang.String;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import javax.annotation.processing.Generated;
+
+@AutoService(AppSearchDocumentClassMap.class)
+@Generated("androidx.appsearch.compiler.AppSearchCompiler")
+public final class $$__AppSearch__DocumentClassMap_3564af6e02075934601ae45e21da1e65bb69157de5fa8378ab5beb7f70e61d84_0 extends AppSearchDocumentClassMap {
+ @NonNull
+ @Override
+ protected Map<String, List<String>> getMap() {
+ Map<String, List<String>> result = new HashMap<>();
+ result.put("Gift", Arrays.asList("com.example.appsearch.Gift"));
+ result.put("Parent2", Arrays.asList("com.example.appsearch.Parent2"));
+ result.put("Parent1", Arrays.asList("com.example.appsearch.Parent1"));
+ return result;
+ }
+}