From 0a26313afbfd6a9c9152f280e6df3d017f21f9bd Mon Sep 17 00:00:00 2001 From: Alexander Dorokhine Date: Thu, 16 Jan 2020 19:37:58 -0800 Subject: [PATCH] Remove IndexingConfig and merge its constants into PropertyConfig. With 4-tab indents, these nested structures become difficult to write and maintain. The information encoded here doesn't seem to benefit from being separated into its own config. Test: atest CtsAppSearchTestCases FrameworksCoreTests:android.app.appsearch FrameworksServicesTests:com.android.server.appsearch.impl Bug: 145635424 Change-Id: I6c771e0af1ca34a05b09d1532f0f227a069e0d2e --- .../app/appsearch/AppSearchManager.java | 18 +- .../app/appsearch/AppSearchSchema.java | 371 ++++++++---------- .../app/appsearch/AppSearchSchemaTest.java | 180 +++++---- 3 files changed, 266 insertions(+), 303 deletions(-) diff --git a/apex/appsearch/framework/java/android/app/appsearch/AppSearchManager.java b/apex/appsearch/framework/java/android/app/appsearch/AppSearchManager.java index 83195dc73db66..a79894fa67bc8 100644 --- a/apex/appsearch/framework/java/android/app/appsearch/AppSearchManager.java +++ b/apex/appsearch/framework/java/android/app/appsearch/AppSearchManager.java @@ -71,10 +71,10 @@ public class AppSearchManager { *
  • Adding a 'required' property * * - * @param schema The schema config for this app. * @param executor Executor on which to invoke the callback. * @param callback Callback to receive errors resulting from setting the schema. If the * operation succeeds, the callback will be invoked with {@code null}. + * @param schemas The schema configs for the types used by the calling app. * * @hide */ @@ -84,11 +84,19 @@ public class AppSearchManager { // TODO(b/145635424): Update the documentation above once the Schema mutation APIs of Icing // Library are finalized public void setSchema( - @NonNull AppSearchSchema schema, @NonNull @CallbackExecutor Executor executor, - @NonNull Consumer callback) { - SchemaProto schemaProto = schema.getProto(); - byte[] schemaBytes = schemaProto.toByteArray(); + @NonNull Consumer callback, + @NonNull AppSearchSchema... schemas) { + // Prepare the merged schema for transmission. + SchemaProto.Builder schemaProtoBuilder = SchemaProto.newBuilder(); + for (AppSearchSchema schema : schemas) { + schemaProtoBuilder.addTypes(schema.getProto()); + } + + // Serialize and send the schema. + // TODO: This should use com.android.internal.infra.RemoteStream or another mechanism to + // avoid binder limits. + byte[] schemaBytes = schemaProtoBuilder.build().toByteArray(); AndroidFuture future = new AndroidFuture<>(); try { mService.setSchema(schemaBytes, future); diff --git a/apex/appsearch/framework/java/android/app/appsearch/AppSearchSchema.java b/apex/appsearch/framework/java/android/app/appsearch/AppSearchSchema.java index 7e5f187b88c9a..1d54dc4971a69 100644 --- a/apex/appsearch/framework/java/android/app/appsearch/AppSearchSchema.java +++ b/apex/appsearch/framework/java/android/app/appsearch/AppSearchSchema.java @@ -18,41 +18,38 @@ package android.app.appsearch; import android.annotation.IntDef; import android.annotation.NonNull; +import android.util.ArraySet; import com.android.internal.annotations.VisibleForTesting; import com.google.android.icing.proto.PropertyConfigProto; -import com.google.android.icing.proto.SchemaProto; import com.google.android.icing.proto.SchemaTypeConfigProto; +import com.google.android.icing.proto.TermMatchType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +import java.util.Set; /** - * Representation of the AppSearch Schema. + * The AppSearch Schema for a particular type of document. * - *

    The schema is the set of document types, properties, and config (like tokenization type) - * understood by AppSearch for this app. + *

    For example, an e-mail message or a music recording could be a schema type. + * + *

    The schema consists of type information, properties, and config (like tokenization type). * * @hide */ public final class AppSearchSchema { - private final SchemaProto mProto; + private final SchemaTypeConfigProto mProto; - private AppSearchSchema(SchemaProto proto) { + private AppSearchSchema(SchemaTypeConfigProto proto) { mProto = proto; } /** Creates a new {@link AppSearchSchema.Builder}. */ @NonNull - public static AppSearchSchema.Builder newBuilder() { - return new AppSearchSchema.Builder(); - } - - /** Creates a new {@link SchemaType.Builder}. */ - @NonNull - public static SchemaType.Builder newSchemaTypeBuilder(@NonNull String typeName) { - return new SchemaType.Builder(typeName); + public static AppSearchSchema.Builder newBuilder(@NonNull String typeName) { + return new AppSearchSchema.Builder(typeName); } /** Creates a new {@link PropertyConfig.Builder}. */ @@ -61,32 +58,34 @@ public final class AppSearchSchema { return new PropertyConfig.Builder(propertyName); } - /** Creates a new {@link IndexingConfig.Builder}. */ - @NonNull - public static IndexingConfig.Builder newIndexingConfigBuilder() { - return new IndexingConfig.Builder(); - } - /** - * Returns the schema proto populated by the {@link AppSearchSchema} builders. + * Returns the {@link SchemaTypeConfigProto} populated by this builder. * @hide */ @NonNull @VisibleForTesting - public SchemaProto getProto() { + public SchemaTypeConfigProto getProto() { return mProto; } + @Override + public String toString() { + return mProto.toString(); + } + /** Builder for {@link AppSearchSchema objects}. */ public static final class Builder { - private final SchemaProto.Builder mProtoBuilder = SchemaProto.newBuilder(); + private final SchemaTypeConfigProto.Builder mProtoBuilder = + SchemaTypeConfigProto.newBuilder(); - private Builder() {} + private Builder(@NonNull String typeName) { + mProtoBuilder.setSchemaType(typeName); + } - /** Adds a supported type to this app's AppSearch schema. */ + /** Adds a property to the given type. */ @NonNull - public AppSearchSchema.Builder addType(@NonNull SchemaType schemaType) { - mProtoBuilder.addTypes(schemaType.mProto); + public AppSearchSchema.Builder addProperty(@NonNull PropertyConfig propertyConfig) { + mProtoBuilder.addProperties(propertyConfig.mProto); return this; } @@ -97,50 +96,18 @@ public final class AppSearchSchema { */ @NonNull public AppSearchSchema build() { + Set propertyNames = new ArraySet<>(); + for (PropertyConfigProto propertyConfigProto : mProtoBuilder.getPropertiesList()) { + if (!propertyNames.add(propertyConfigProto.getPropertyName())) { + throw new IllegalSchemaException( + "Property defined more than once: " + + propertyConfigProto.getPropertyName()); + } + } return new AppSearchSchema(mProtoBuilder.build()); } } - /** - * Represents a type of a document. - * - *

    For example, an e-mail message or a music recording could be a schema type. - */ - public static final class SchemaType { - private final SchemaTypeConfigProto mProto; - - private SchemaType(SchemaTypeConfigProto proto) { - mProto = proto; - } - - /** Builder for {@link SchemaType} objects. */ - public static final class Builder { - private final SchemaTypeConfigProto.Builder mProtoBuilder = - SchemaTypeConfigProto.newBuilder(); - - private Builder(@NonNull String typeName) { - mProtoBuilder.setSchemaType(typeName); - } - - /** Adds a property to the given type. */ - @NonNull - public SchemaType.Builder addProperty(@NonNull PropertyConfig propertyConfig) { - mProtoBuilder.addProperties(propertyConfig.mProto); - return this; - } - - /** - * Constructs a new {@link SchemaType} from the contents of this builder. - * - *

    After calling this method, the builder must no longer be used. - */ - @NonNull - public SchemaType build() { - return new SchemaType(mProtoBuilder.build()); - } - } - } - /** * Configuration for a single property (field) of a document type. * @@ -197,130 +164,14 @@ public final class AppSearchSchema { /** Exactly one value [1]. */ public static final int CARDINALITY_REQUIRED = 3; - private final PropertyConfigProto mProto; - - private PropertyConfig(PropertyConfigProto proto) { - mProto = proto; - } - - /** - * Builder for {@link PropertyConfig}. - * - *

    The following properties must be set, or {@link PropertyConfig} construction will - * fail: - *

      - *
    • dataType - *
    • cardinality - *
    - * - *

    In addition, if {@code schemaType} is {@link #DATA_TYPE_DOCUMENT}, {@code schemaType} - * is also required. - */ - public static final class Builder { - private final PropertyConfigProto.Builder mProtoBuilder = - PropertyConfigProto.newBuilder(); - - private Builder(String propertyName) { - mProtoBuilder.setPropertyName(propertyName); - } - - /** - * Type of data the property contains (e.g. string, int, bytes, etc). - * - *

    This property must be set. - */ - @NonNull - public PropertyConfig.Builder setDataType(@DataType int dataType) { - PropertyConfigProto.DataType.Code dataTypeProto = - PropertyConfigProto.DataType.Code.forNumber(dataType); - if (dataTypeProto == null) { - throw new IllegalArgumentException("Invalid dataType: " + dataType); - } - mProtoBuilder.setDataType(dataTypeProto); - return this; - } - - /** - * The logical schema-type of the contents of this property. - * - *

    Only required when {@link #setDataType(int)} is set to - * {@link #DATA_TYPE_DOCUMENT}. Otherwise, it is ignored. - */ - @NonNull - public PropertyConfig.Builder setSchemaType(@NonNull String schemaType) { - mProtoBuilder.setSchemaType(schemaType); - return this; - } - - /** - * The cardinality of the property (whether it is optional, required or repeated). - * - *

    This property must be set. - */ - @NonNull - public PropertyConfig.Builder setCardinality(@Cardinality int cardinality) { - PropertyConfigProto.Cardinality.Code cardinalityProto = - PropertyConfigProto.Cardinality.Code.forNumber(cardinality); - if (cardinalityProto == null) { - throw new IllegalArgumentException("Invalid cardinality: " + cardinality); - } - mProtoBuilder.setCardinality(cardinalityProto); - return this; - } - - /** - * Configures how this property should be indexed. - * - *

    If this is not supplied, the property will not be indexed at all. - */ - @NonNull - public PropertyConfig.Builder setIndexingConfig( - @NonNull IndexingConfig indexingConfig) { - mProtoBuilder.setIndexingConfig(indexingConfig.mProto); - return this; - } - - /** - * Constructs a new {@link PropertyConfig} from the contents of this builder. - * - *

    After calling this method, the builder must no longer be used. - * - * @throws IllegalSchemaException If the property is not correctly populated (e.g. - * missing {@code dataType}). - */ - @NonNull - public PropertyConfig build() { - if (mProtoBuilder.getDataType() == PropertyConfigProto.DataType.Code.UNKNOWN) { - throw new IllegalSchemaException("Missing field: dataType"); - } - if (mProtoBuilder.getSchemaType().isEmpty() - && mProtoBuilder.getDataType() - == PropertyConfigProto.DataType.Code.DOCUMENT) { - throw new IllegalSchemaException( - "Missing field: schemaType (required for configs with " - + "dataType = DOCUMENT)"); - } - if (mProtoBuilder.getCardinality() - == PropertyConfigProto.Cardinality.Code.UNKNOWN) { - throw new IllegalSchemaException("Missing field: cardinality"); - } - return new PropertyConfig(mProtoBuilder.build()); - } - } - } - - /** Configures how a property should be indexed so that it can be retrieved by queries. */ - public static final class IndexingConfig { /** Encapsulates the configurations on how AppSearch should query/index these terms. */ - // NOTE: The integer values of these constants must match the proto enum constants in - // com.google.android.icing.proto.TermMatchType.Code. - @IntDef(prefix = {"TERM_MATCH_TYPE_"}, value = { - TERM_MATCH_TYPE_UNKNOWN, - TERM_MATCH_TYPE_EXACT_ONLY, - TERM_MATCH_TYPE_PREFIX, + @IntDef(prefix = {"INDEXING_TYPE_"}, value = { + INDEXING_TYPE_NONE, + INDEXING_TYPE_EXACT_TERMS, + INDEXING_TYPE_PREFIXES, }) @Retention(RetentionPolicy.SOURCE) - public @interface TermMatchType {} + public @interface IndexingType {} /** * Content in this property will not be tokenized or indexed. @@ -328,9 +179,9 @@ public final class AppSearchSchema { *

    Useful if the data type is not made up of terms (e.g. * {@link PropertyConfig#DATA_TYPE_DOCUMENT} or {@link PropertyConfig#DATA_TYPE_BYTES} * type). All the properties inside the nested property won't be indexed regardless of the - * value of {@code termMatchType} for the nested properties. + * value of {@code indexingType} for the nested properties. */ - public static final int TERM_MATCH_TYPE_UNKNOWN = 0; + public static final int INDEXING_TYPE_NONE = 0; /** * Content in this property should only be returned for queries matching the exact tokens @@ -338,7 +189,7 @@ public final class AppSearchSchema { * *

    Ex. A property with "fool" should NOT match a query for "foo". */ - public static final int TERM_MATCH_TYPE_EXACT_ONLY = 1; + public static final int INDEXING_TYPE_EXACT_TERMS = 1; /** * Content in this property should be returned for queries that are either exact matches or @@ -346,7 +197,7 @@ public final class AppSearchSchema { * *

    Ex. A property with "fool" should match a query for "foo". */ - public static final int TERM_MATCH_TYPE_PREFIX = 2; + public static final int INDEXING_TYPE_PREFIXES = 2; /** Configures how tokens should be extracted from this property. */ // NOTE: The integer values of these constants must match the proto enum constants in @@ -367,59 +218,151 @@ public final class AppSearchSchema { /** Tokenization for plain text. */ public static final int TOKENIZER_TYPE_PLAIN = 1; - private final com.google.android.icing.proto.IndexingConfig mProto; + private final PropertyConfigProto mProto; - private IndexingConfig(com.google.android.icing.proto.IndexingConfig proto) { + private PropertyConfig(PropertyConfigProto proto) { mProto = proto; } + @Override + public String toString() { + return mProto.toString(); + } + /** - * Builder for {@link IndexingConfig} objects. + * Builder for {@link PropertyConfig}. * - *

    You may skip adding an {@link IndexingConfig} for a property, which is equivalent to - * an {@link IndexingConfig} having {@code termMatchType} equal to - * {@link #TERM_MATCH_TYPE_UNKNOWN}. In this case the property will not be indexed. + *

    The following properties must be set, or {@link PropertyConfig} construction will + * fail: + *

      + *
    • dataType + *
    • cardinality + *
    + * + *

    In addition, if {@code schemaType} is {@link #DATA_TYPE_DOCUMENT}, {@code schemaType} + * is also required. */ public static final class Builder { - private final com.google.android.icing.proto.IndexingConfig.Builder mProtoBuilder = - com.google.android.icing.proto.IndexingConfig.newBuilder(); + private final PropertyConfigProto.Builder mPropertyConfigProto = + PropertyConfigProto.newBuilder(); + private final com.google.android.icing.proto.IndexingConfig.Builder + mIndexingConfigProto = + com.google.android.icing.proto.IndexingConfig.newBuilder(); - private Builder() {} + private Builder(String propertyName) { + mPropertyConfigProto.setPropertyName(propertyName); + } - /** Configures how the content of this property should be matched in the index. */ + /** + * Type of data the property contains (e.g. string, int, bytes, etc). + * + *

    This property must be set. + */ @NonNull - public IndexingConfig.Builder setTermMatchType(@TermMatchType int termMatchType) { - com.google.android.icing.proto.TermMatchType.Code termMatchTypeProto = - com.google.android.icing.proto.TermMatchType.Code.forNumber(termMatchType); - if (termMatchTypeProto == null) { - throw new IllegalArgumentException("Invalid termMatchType: " + termMatchType); + public PropertyConfig.Builder setDataType(@DataType int dataType) { + PropertyConfigProto.DataType.Code dataTypeProto = + PropertyConfigProto.DataType.Code.forNumber(dataType); + if (dataTypeProto == null) { + throw new IllegalArgumentException("Invalid dataType: " + dataType); } - mProtoBuilder.setTermMatchType(termMatchTypeProto); + mPropertyConfigProto.setDataType(dataTypeProto); + return this; + } + + /** + * The logical schema-type of the contents of this property. + * + *

    Only required when {@link #setDataType(int)} is set to + * {@link #DATA_TYPE_DOCUMENT}. Otherwise, it is ignored. + */ + @NonNull + public PropertyConfig.Builder setSchemaType(@NonNull String schemaType) { + mPropertyConfigProto.setSchemaType(schemaType); + return this; + } + + /** + * The cardinality of the property (whether it is optional, required or repeated). + * + *

    This property must be set. + */ + @NonNull + public PropertyConfig.Builder setCardinality(@Cardinality int cardinality) { + PropertyConfigProto.Cardinality.Code cardinalityProto = + PropertyConfigProto.Cardinality.Code.forNumber(cardinality); + if (cardinalityProto == null) { + throw new IllegalArgumentException("Invalid cardinality: " + cardinality); + } + mPropertyConfigProto.setCardinality(cardinalityProto); + return this; + } + + /** + * Configures how a property should be indexed so that it can be retrieved by queries. + */ + @NonNull + public PropertyConfig.Builder setIndexingType(@IndexingType int indexingType) { + TermMatchType.Code termMatchTypeProto; + switch (indexingType) { + case INDEXING_TYPE_NONE: + termMatchTypeProto = TermMatchType.Code.UNKNOWN; + break; + case INDEXING_TYPE_EXACT_TERMS: + termMatchTypeProto = TermMatchType.Code.EXACT_ONLY; + break; + case INDEXING_TYPE_PREFIXES: + termMatchTypeProto = TermMatchType.Code.PREFIX; + break; + default: + throw new IllegalArgumentException("Invalid indexingType: " + indexingType); + } + mIndexingConfigProto.setTermMatchType(termMatchTypeProto); return this; } /** Configures how this property should be tokenized (split into words). */ @NonNull - public IndexingConfig.Builder setTokenizerType(@TokenizerType int tokenizerType) { + public PropertyConfig.Builder setTokenizerType(@TokenizerType int tokenizerType) { com.google.android.icing.proto.IndexingConfig.TokenizerType.Code tokenizerTypeProto = com.google.android.icing.proto.IndexingConfig - .TokenizerType.Code.forNumber(tokenizerType); + .TokenizerType.Code.forNumber(tokenizerType); if (tokenizerTypeProto == null) { throw new IllegalArgumentException("Invalid tokenizerType: " + tokenizerType); } - mProtoBuilder.setTokenizerType(tokenizerTypeProto); + mIndexingConfigProto.setTokenizerType(tokenizerTypeProto); return this; } /** - * Constructs a new {@link IndexingConfig} from the contents of this builder. + * Constructs a new {@link PropertyConfig} from the contents of this builder. * *

    After calling this method, the builder must no longer be used. + * + * @throws IllegalSchemaException If the property is not correctly populated (e.g. + * missing {@code dataType}). */ @NonNull - public IndexingConfig build() { - return new IndexingConfig(mProtoBuilder.build()); + public PropertyConfig build() { + mPropertyConfigProto.setIndexingConfig(mIndexingConfigProto); + // TODO(b/147692920): Send the schema to Icing Lib for official validation, instead + // of partially reimplementing some of the validation Icing does here. + if (mPropertyConfigProto.getDataType() + == PropertyConfigProto.DataType.Code.UNKNOWN) { + throw new IllegalSchemaException("Missing field: dataType"); + } + if (mPropertyConfigProto.getSchemaType().isEmpty() + && mPropertyConfigProto.getDataType() + == PropertyConfigProto.DataType.Code.DOCUMENT) { + throw new IllegalSchemaException( + "Missing field: schemaType (required for configs with " + + "dataType = DOCUMENT)"); + } + if (mPropertyConfigProto.getCardinality() + == PropertyConfigProto.Cardinality.Code.UNKNOWN) { + throw new IllegalSchemaException("Missing field: cardinality"); + } + return new PropertyConfig(mPropertyConfigProto.build()); } } } diff --git a/core/tests/coretests/src/android/app/appsearch/AppSearchSchemaTest.java b/core/tests/coretests/src/android/app/appsearch/AppSearchSchemaTest.java index 0be52c1803385..08ec2d0b10674 100644 --- a/core/tests/coretests/src/android/app/appsearch/AppSearchSchemaTest.java +++ b/core/tests/coretests/src/android/app/appsearch/AppSearchSchemaTest.java @@ -21,14 +21,12 @@ import static com.google.common.truth.Truth.assertThat; import static org.testng.Assert.assertThrows; import static org.testng.Assert.expectThrows; -import android.app.appsearch.AppSearchSchema.IndexingConfig; import android.app.appsearch.AppSearchSchema.PropertyConfig; import androidx.test.filters.SmallTest; import com.google.android.icing.proto.IndexingConfig.TokenizerType; import com.google.android.icing.proto.PropertyConfigProto; -import com.google.android.icing.proto.SchemaProto; import com.google.android.icing.proto.SchemaTypeConfigProto; import com.google.android.icing.proto.TermMatchType; @@ -37,94 +35,87 @@ import org.junit.Test; @SmallTest public class AppSearchSchemaTest { @Test - public void testSuccess() { - AppSearchSchema schema = AppSearchSchema.newBuilder() - .addType(AppSearchSchema.newSchemaTypeBuilder("Email") - .addProperty(AppSearchSchema.newPropertyBuilder("subject") - .setDataType(PropertyConfig.DATA_TYPE_STRING) - .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL) - .setIndexingConfig(AppSearchSchema.newIndexingConfigBuilder() - .setTokenizerType(IndexingConfig.TOKENIZER_TYPE_PLAIN) - .setTermMatchType(IndexingConfig.TERM_MATCH_TYPE_PREFIX) - .build() - ).build() - ).addProperty(AppSearchSchema.newPropertyBuilder("body") - .setDataType(PropertyConfig.DATA_TYPE_STRING) - .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL) - .setIndexingConfig(AppSearchSchema.newIndexingConfigBuilder() - .setTokenizerType(IndexingConfig.TOKENIZER_TYPE_PLAIN) - .setTermMatchType(IndexingConfig.TERM_MATCH_TYPE_PREFIX) - .build() - ).build() - ).build() - - ).addType(AppSearchSchema.newSchemaTypeBuilder("MusicRecording") - .addProperty(AppSearchSchema.newPropertyBuilder("artist") - .setDataType(PropertyConfig.DATA_TYPE_STRING) - .setCardinality(PropertyConfig.CARDINALITY_REPEATED) - .setIndexingConfig(AppSearchSchema.newIndexingConfigBuilder() - .setTokenizerType(IndexingConfig.TOKENIZER_TYPE_PLAIN) - .setTermMatchType(IndexingConfig.TERM_MATCH_TYPE_PREFIX) - .build() - ).build() - ).addProperty(AppSearchSchema.newPropertyBuilder("pubDate") - .setDataType(PropertyConfig.DATA_TYPE_INT64) - .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL) - .setIndexingConfig(AppSearchSchema.newIndexingConfigBuilder() - .setTokenizerType(IndexingConfig.TOKENIZER_TYPE_NONE) - .setTermMatchType(IndexingConfig.TERM_MATCH_TYPE_UNKNOWN) - .build() - ).build() - ).build() + public void testGetProto_Email() { + AppSearchSchema emailSchema = AppSearchSchema.newBuilder("Email") + .addProperty(AppSearchSchema.newPropertyBuilder("subject") + .setDataType(PropertyConfig.DATA_TYPE_STRING) + .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL) + .setIndexingType(PropertyConfig.INDEXING_TYPE_PREFIXES) + .setTokenizerType(PropertyConfig.TOKENIZER_TYPE_PLAIN) + .build() + ).addProperty(AppSearchSchema.newPropertyBuilder("body") + .setDataType(PropertyConfig.DATA_TYPE_STRING) + .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL) + .setIndexingType(PropertyConfig.INDEXING_TYPE_PREFIXES) + .setTokenizerType(PropertyConfig.TOKENIZER_TYPE_PLAIN) + .build() ).build(); - SchemaProto expectedProto = SchemaProto.newBuilder() - .addTypes(SchemaTypeConfigProto.newBuilder() - .setSchemaType("Email") - .addProperties(PropertyConfigProto.newBuilder() - .setPropertyName("subject") - .setDataType(PropertyConfigProto.DataType.Code.STRING) - .setCardinality(PropertyConfigProto.Cardinality.Code.OPTIONAL) - .setIndexingConfig( - com.google.android.icing.proto.IndexingConfig.newBuilder() - .setTokenizerType(TokenizerType.Code.PLAIN) - .setTermMatchType(TermMatchType.Code.PREFIX) - ) - ).addProperties(PropertyConfigProto.newBuilder() - .setPropertyName("body") - .setDataType(PropertyConfigProto.DataType.Code.STRING) - .setCardinality(PropertyConfigProto.Cardinality.Code.OPTIONAL) - .setIndexingConfig( - com.google.android.icing.proto.IndexingConfig.newBuilder() - .setTokenizerType(TokenizerType.Code.PLAIN) - .setTermMatchType(TermMatchType.Code.PREFIX) - ) + SchemaTypeConfigProto expectedEmailProto = SchemaTypeConfigProto.newBuilder() + .setSchemaType("Email") + .addProperties(PropertyConfigProto.newBuilder() + .setPropertyName("subject") + .setDataType(PropertyConfigProto.DataType.Code.STRING) + .setCardinality(PropertyConfigProto.Cardinality.Code.OPTIONAL) + .setIndexingConfig( + com.google.android.icing.proto.IndexingConfig.newBuilder() + .setTokenizerType(TokenizerType.Code.PLAIN) + .setTermMatchType(TermMatchType.Code.PREFIX) ) - - ).addTypes(SchemaTypeConfigProto.newBuilder() - .setSchemaType("MusicRecording") - .addProperties(PropertyConfigProto.newBuilder() - .setPropertyName("artist") - .setDataType(PropertyConfigProto.DataType.Code.STRING) - .setCardinality(PropertyConfigProto.Cardinality.Code.REPEATED) - .setIndexingConfig( - com.google.android.icing.proto.IndexingConfig.newBuilder() - .setTokenizerType(TokenizerType.Code.PLAIN) - .setTermMatchType(TermMatchType.Code.PREFIX) - ) - ).addProperties(PropertyConfigProto.newBuilder() - .setPropertyName("pubDate") - .setDataType(PropertyConfigProto.DataType.Code.INT64) - .setCardinality(PropertyConfigProto.Cardinality.Code.OPTIONAL) - .setIndexingConfig( - com.google.android.icing.proto.IndexingConfig.newBuilder() - .setTokenizerType(TokenizerType.Code.NONE) - .setTermMatchType(TermMatchType.Code.UNKNOWN) - ) + ).addProperties(PropertyConfigProto.newBuilder() + .setPropertyName("body") + .setDataType(PropertyConfigProto.DataType.Code.STRING) + .setCardinality(PropertyConfigProto.Cardinality.Code.OPTIONAL) + .setIndexingConfig( + com.google.android.icing.proto.IndexingConfig.newBuilder() + .setTokenizerType(TokenizerType.Code.PLAIN) + .setTermMatchType(TermMatchType.Code.PREFIX) ) ).build(); - assertThat(schema.getProto()).isEqualTo(expectedProto); + assertThat(emailSchema.getProto()).isEqualTo(expectedEmailProto); + } + + @Test + public void testGetProto_MusicRecording() { + AppSearchSchema musicRecordingSchema = AppSearchSchema.newBuilder("MusicRecording") + .addProperty(AppSearchSchema.newPropertyBuilder("artist") + .setDataType(PropertyConfig.DATA_TYPE_STRING) + .setCardinality(PropertyConfig.CARDINALITY_REPEATED) + .setIndexingType(PropertyConfig.INDEXING_TYPE_PREFIXES) + .setTokenizerType(PropertyConfig.TOKENIZER_TYPE_PLAIN) + .build() + ).addProperty(AppSearchSchema.newPropertyBuilder("pubDate") + .setDataType(PropertyConfig.DATA_TYPE_INT64) + .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL) + .setIndexingType(PropertyConfig.INDEXING_TYPE_NONE) + .setTokenizerType(PropertyConfig.TOKENIZER_TYPE_NONE) + .build() + ).build(); + + SchemaTypeConfigProto expectedMusicRecordingProto = SchemaTypeConfigProto.newBuilder() + .setSchemaType("MusicRecording") + .addProperties(PropertyConfigProto.newBuilder() + .setPropertyName("artist") + .setDataType(PropertyConfigProto.DataType.Code.STRING) + .setCardinality(PropertyConfigProto.Cardinality.Code.REPEATED) + .setIndexingConfig( + com.google.android.icing.proto.IndexingConfig.newBuilder() + .setTokenizerType(TokenizerType.Code.PLAIN) + .setTermMatchType(TermMatchType.Code.PREFIX) + ) + ).addProperties(PropertyConfigProto.newBuilder() + .setPropertyName("pubDate") + .setDataType(PropertyConfigProto.DataType.Code.INT64) + .setCardinality(PropertyConfigProto.Cardinality.Code.OPTIONAL) + .setIndexingConfig( + com.google.android.icing.proto.IndexingConfig.newBuilder() + .setTokenizerType(TokenizerType.Code.NONE) + .setTermMatchType(TermMatchType.Code.UNKNOWN) + ) + ).build(); + + assertThat(musicRecordingSchema.getProto()).isEqualTo(expectedMusicRecordingProto); } @Test @@ -151,4 +142,25 @@ public class AppSearchSchemaTest { builder.setCardinality(PropertyConfig.CARDINALITY_REPEATED); builder.build(); } + + @Test + public void testDuplicateProperties() { + AppSearchSchema.Builder builder = AppSearchSchema.newBuilder("Email") + .addProperty(AppSearchSchema.newPropertyBuilder("subject") + .setDataType(PropertyConfig.DATA_TYPE_STRING) + .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL) + .setIndexingType(PropertyConfig.INDEXING_TYPE_PREFIXES) + .setTokenizerType(PropertyConfig.TOKENIZER_TYPE_PLAIN) + .build() + ).addProperty(AppSearchSchema.newPropertyBuilder("subject") + .setDataType(PropertyConfig.DATA_TYPE_STRING) + .setCardinality(PropertyConfig.CARDINALITY_OPTIONAL) + .setIndexingType(PropertyConfig.INDEXING_TYPE_PREFIXES) + .setTokenizerType(PropertyConfig.TOKENIZER_TYPE_PLAIN) + .build() + ); + + Exception e = expectThrows(IllegalSchemaException.class, builder::build); + assertThat(e).hasMessageThat().contains("Property defined more than once: subject"); + } }