[go: nahoru, domu]

Support namespaces in the annotation processor.

Bug: 156296904
Test: presubmit
Change-Id: I17bee84ff253cee3226de86165b0f018e0cb6a8e
diff --git a/appsearch/annotation/src/main/java/androidx/appsearch/compiler/AppSearchDocumentModel.java b/appsearch/annotation/src/main/java/androidx/appsearch/compiler/AppSearchDocumentModel.java
index 38fb581..76578db 100644
--- a/appsearch/annotation/src/main/java/androidx/appsearch/compiler/AppSearchDocumentModel.java
+++ b/appsearch/annotation/src/main/java/androidx/appsearch/compiler/AppSearchDocumentModel.java
@@ -47,7 +47,7 @@
 class AppSearchDocumentModel {
 
     /** Enumeration of fields that must be handled specially (i.e. are not properties) */
-    enum SpecialField { URI, CREATION_TIMESTAMP_MILLIS, TTL_MILLIS, SCORE }
+    enum SpecialField { URI, NAMESPACE, CREATION_TIMESTAMP_MILLIS, TTL_MILLIS, SCORE }
     /** Determines how the annotation processor has decided to read the value of a field. */
     enum ReadKind { FIELD, GETTER }
     /** Determines how the annotation processor has decided to write the value of a field. */
@@ -162,6 +162,7 @@
 
     private void scanFields() throws ProcessingException {
         Element uriField = null;
+        Element namespaceField = null;
         Element creationTimestampField = null;
         Element ttlField = null;
         Element scoreField = null;
@@ -180,6 +181,14 @@
                     uriField = child;
                     mSpecialFieldNames.put(SpecialField.URI, fieldName);
 
+                } else if (IntrospectionHelper.NAMESPACE_CLASS.equals(annotationFq)) {
+                    if (namespaceField != null) {
+                        throw new ProcessingException(
+                                "Class contains multiple fields annotated @Namespace", child);
+                    }
+                    namespaceField = child;
+                    mSpecialFieldNames.put(SpecialField.NAMESPACE, fieldName);
+
                 } else if (
                         IntrospectionHelper.CREATION_TIMESTAMP_MILLIS_CLASS.equals(annotationFq)) {
                     if (creationTimestampField != null) {
diff --git a/appsearch/annotation/src/main/java/androidx/appsearch/compiler/FromGenericDocumentCodeGenerator.java b/appsearch/annotation/src/main/java/androidx/appsearch/compiler/FromGenericDocumentCodeGenerator.java
index 7cdea01..fe33e97 100644
--- a/appsearch/annotation/src/main/java/androidx/appsearch/compiler/FromGenericDocumentCodeGenerator.java
+++ b/appsearch/annotation/src/main/java/androidx/appsearch/compiler/FromGenericDocumentCodeGenerator.java
@@ -598,6 +598,9 @@
                 case URI:
                     method.addStatement("String $NConv = genericDoc.getUri()", fieldName);
                     break;
+                case NAMESPACE:
+                    method.addStatement("String $NConv = genericDoc.getNamespace()", fieldName);
+                    break;
                 case CREATION_TIMESTAMP_MILLIS:
                     method.addStatement(
                             "long $NConv = genericDoc.getCreationTimestampMillis()", fieldName);
diff --git a/appsearch/annotation/src/main/java/androidx/appsearch/compiler/IntrospectionHelper.java b/appsearch/annotation/src/main/java/androidx/appsearch/compiler/IntrospectionHelper.java
index b489a3c..6986572 100644
--- a/appsearch/annotation/src/main/java/androidx/appsearch/compiler/IntrospectionHelper.java
+++ b/appsearch/annotation/src/main/java/androidx/appsearch/compiler/IntrospectionHelper.java
@@ -46,6 +46,8 @@
             "androidx.appsearch.annotation.AppSearchDocument";
     static final String URI_CLASS =
             "androidx.appsearch.annotation.AppSearchDocument.Uri";
+    static final String NAMESPACE_CLASS =
+            "androidx.appsearch.annotation.AppSearchDocument.Namespace";
     static final String CREATION_TIMESTAMP_MILLIS_CLASS =
             "androidx.appsearch.annotation.AppSearchDocument.CreationTimestampMillis";
     static final String TTL_MILLIS_CLASS =
diff --git a/appsearch/annotation/src/main/java/androidx/appsearch/compiler/ToGenericDocumentCodeGenerator.java b/appsearch/annotation/src/main/java/androidx/appsearch/compiler/ToGenericDocumentCodeGenerator.java
index 052c0e7..d68af02 100644
--- a/appsearch/annotation/src/main/java/androidx/appsearch/compiler/ToGenericDocumentCodeGenerator.java
+++ b/appsearch/annotation/src/main/java/androidx/appsearch/compiler/ToGenericDocumentCodeGenerator.java
@@ -522,6 +522,16 @@
             switch (specialField) {
                 case URI:
                     break;  // Always provided to builder constructor; cannot be set separately.
+                case NAMESPACE:
+                    method.addCode(CodeBlock.builder()
+                            .addStatement(
+                                    "String $NCopy = $L",
+                                    fieldName, createAppSearchFieldRead(fieldName))
+                            .add("if ($NCopy != null) {\n", fieldName).indent()
+                            .addStatement("builder.setNamespace($NCopy)", fieldName)
+                            .unindent().add("}\n")
+                            .build());
+                    break;
                 case CREATION_TIMESTAMP_MILLIS:
                     method.addStatement(
                             "builder.setCreationTimestampMillis($L)",
diff --git a/appsearch/annotation/src/test/java/androidx/appsearch/compiler/DocumentProcessorTest.java b/appsearch/annotation/src/test/java/androidx/appsearch/compiler/DocumentProcessorTest.java
index 14b1eb5..a5a15a5 100644
--- a/appsearch/annotation/src/test/java/androidx/appsearch/compiler/DocumentProcessorTest.java
+++ b/appsearch/annotation/src/test/java/androidx/appsearch/compiler/DocumentProcessorTest.java
@@ -111,6 +111,19 @@
     }
 
     @Test
+    public void testManyNamespace() {
+        Compilation compilation = compile(
+                "@AppSearchDocument\n"
+                        + "public class Gift {\n"
+                        + "  @AppSearchDocument.Uri String uri;\n"
+                        + "  @AppSearchDocument.Namespace String ns1;\n"
+                        + "  @AppSearchDocument.Namespace String ns2;\n"
+                        + "}\n");
+        CompilationSubject.assertThat(compilation).hadErrorContaining(
+                "contains multiple fields annotated @Namespace");
+    }
+
+    @Test
     public void testManyTtlMillis() {
         Compilation compilation = compile(
                 "@AppSearchDocument\n"
@@ -616,10 +629,11 @@
                 "@AppSearchDocument\n"
                         + "public class Gift {\n"
                         + "  @AppSearchDocument.Uri String uri;\n"
-                        + "  @AppSearchDocument.Score int score;\n"
+                        + "  @AppSearchDocument.Namespace String namespace;\n"
                         + "  @AppSearchDocument.CreationTimestampMillis long creationTs;\n"
                         + "  @AppSearchDocument.TtlMillis int ttlMs;\n"
                         + "  @AppSearchDocument.Property int price;\n"
+                        + "  @AppSearchDocument.Score int score;\n"
                         + "}\n");
         CompilationSubject.assertThat(compilation).succeededWithoutWarnings();
         checkEqualsGolden();
diff --git a/appsearch/annotation/src/test/resources/androidx/appsearch/compiler/goldens/testAllSpecialFields_Field.JAVA b/appsearch/annotation/src/test/resources/androidx/appsearch/compiler/goldens/testAllSpecialFields_Field.JAVA
index 5c36bd6..a3f831d 100644
--- a/appsearch/annotation/src/test/resources/androidx/appsearch/compiler/goldens/testAllSpecialFields_Field.JAVA
+++ b/appsearch/annotation/src/test/resources/androidx/appsearch/compiler/goldens/testAllSpecialFields_Field.JAVA
@@ -19,6 +19,10 @@
   public GenericDocument toGenericDocument(Gift dataClass) {
     GenericDocument.Builder<?> builder =
         new GenericDocument.Builder<>(dataClass.uri, SCHEMA_TYPE);
+    String namespaceCopy = dataClass.namespace;
+    if (namespaceCopy != null) {
+      builder.setNamespace(namespaceCopy);
+    }
     builder.setCreationTimestampMillis(dataClass.creationTs);
     builder.setTtlMillis(dataClass.ttlMs);
     builder.setScore(dataClass.score);
@@ -28,16 +32,18 @@
 
   public Gift fromGenericDocument(GenericDocument genericDoc) {
     String uriConv = genericDoc.getUri();
+    String namespaceConv = genericDoc.getNamespace();
     long creationTsConv = genericDoc.getCreationTimestampMillis();
     long ttlMsConv = genericDoc.getTtlMillis();
     int scoreConv = genericDoc.getScore();
     int priceConv = genericDoc.getPropertyLong("price");
     Gift dataClass = new Gift();
     dataClass.uri = uriConv;
-    dataClass.score = scoreConv;
+    dataClass.namespace = namespaceConv;
     dataClass.creationTs = creationTsConv;
     dataClass.ttlMs = ttlMsConv;
     dataClass.price = priceConv;
+    dataClass.score = scoreConv;
     return dataClass;
   }
 }
diff --git a/appsearch/appsearch/src/main/java/androidx/appsearch/annotation/AppSearchDocument.java b/appsearch/appsearch/src/main/java/androidx/appsearch/annotation/AppSearchDocument.java
index 96cf3f3..8d1bf2f 100644
--- a/appsearch/appsearch/src/main/java/androidx/appsearch/annotation/AppSearchDocument.java
+++ b/appsearch/appsearch/src/main/java/androidx/appsearch/annotation/AppSearchDocument.java
@@ -90,6 +90,27 @@
     @interface Uri {}
 
     /**
+     * Marks a member field of a document as the document's namespace.
+     *
+     * <p>The namespace is an arbitrary user-provided string that can be used to group documents
+     * during querying or deletion. Indexing a document with a particular {@link java.net.URI}
+     * replaces any existing documents with the same URI in that namespace.
+     *
+     * <p>This field is not required. If not present or not set, the document will be assigned to
+     * the default namespace, {@link androidx.appsearch.app.GenericDocument#DEFAULT_NAMESPACE}.
+     *
+     * <p>If present, the field must be of type {@code String}.
+     *
+     * <p>See the class description of {@link AppSearchDocument} for other requirements (i.e. if
+     * present it must be visible, or have a visible getter and setter, or be exposed through a
+     * visible constructor).
+     */
+    @Documented
+    @Retention(RetentionPolicy.CLASS)
+    @Target(ElementType.FIELD)
+    @interface Namespace {}
+
+    /**
      * Marks a member field of a document as the document's creation timestamp.
      *
      * <p>The creation timestamp is used for document expiry (see {@link TtlMillis}) and as one