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;
+  }
+}