/* * Copyright 2020 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package android.app.appsearch; import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; import android.app.appsearch.exceptions.IllegalSchemaException; import android.app.appsearch.util.BundleUtil; import android.app.appsearch.util.IndentingStringBuilder; import android.os.Bundle; import android.util.ArraySet; import com.android.internal.util.Preconditions; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Objects; import java.util.Set; /** * The AppSearch Schema for a particular type of document. * *

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). * * @see AppSearchSession#setSchema */ public final class AppSearchSchema { private static final String SCHEMA_TYPE_FIELD = "schemaType"; private static final String PROPERTIES_FIELD = "properties"; private final Bundle mBundle; /** @hide */ public AppSearchSchema(@NonNull Bundle bundle) { Objects.requireNonNull(bundle); mBundle = bundle; } /** * Returns the {@link Bundle} populated by this builder. * * @hide */ @NonNull public Bundle getBundle() { return mBundle; } @Override @NonNull public String toString() { IndentingStringBuilder stringBuilder = new IndentingStringBuilder(); appendAppSearchSchemaString(stringBuilder); return stringBuilder.toString(); } /** * Appends a debugging string for the {@link AppSearchSchema} instance to the given string * builder. * * @param builder the builder to append to. */ private void appendAppSearchSchemaString(@NonNull IndentingStringBuilder builder) { Objects.requireNonNull(builder); builder.append("{\n"); builder.increaseIndentLevel(); builder.append("schemaType: \"").append(getSchemaType()).append("\",\n"); builder.append("properties: [\n"); AppSearchSchema.PropertyConfig[] sortedProperties = getProperties().toArray(new AppSearchSchema.PropertyConfig[0]); Arrays.sort(sortedProperties, (o1, o2) -> o1.getName().compareTo(o2.getName())); for (int i = 0; i < sortedProperties.length; i++) { AppSearchSchema.PropertyConfig propertyConfig = sortedProperties[i]; builder.increaseIndentLevel(); propertyConfig.appendPropertyConfigString(builder); if (i != sortedProperties.length - 1) { builder.append(",\n"); } builder.decreaseIndentLevel(); } builder.append("\n"); builder.append("]\n"); builder.decreaseIndentLevel(); builder.append("}"); } /** Returns the name of this schema type, e.g. Email. */ @NonNull public String getSchemaType() { return mBundle.getString(SCHEMA_TYPE_FIELD, ""); } /** * Returns the list of {@link PropertyConfig}s that are part of this schema. * *

This method creates a new list when called. */ @NonNull @SuppressWarnings({"MixedMutabilityReturnType", "deprecation"}) public List getProperties() { ArrayList propertyBundles = mBundle.getParcelableArrayList(AppSearchSchema.PROPERTIES_FIELD); if (propertyBundles.isEmpty()) { return Collections.emptyList(); } List ret = new ArrayList<>(propertyBundles.size()); for (int i = 0; i < propertyBundles.size(); i++) { ret.add(PropertyConfig.fromBundle(propertyBundles.get(i))); } return ret; } @Override public boolean equals(@Nullable Object other) { if (this == other) { return true; } if (!(other instanceof AppSearchSchema)) { return false; } AppSearchSchema otherSchema = (AppSearchSchema) other; if (!getSchemaType().equals(otherSchema.getSchemaType())) { return false; } return getProperties().equals(otherSchema.getProperties()); } @Override public int hashCode() { return Objects.hash(getSchemaType(), getProperties()); } /** Builder for {@link AppSearchSchema objects}. */ public static final class Builder { private final String mSchemaType; private ArrayList mPropertyBundles = new ArrayList<>(); private final Set mPropertyNames = new ArraySet<>(); private boolean mBuilt = false; /** Creates a new {@link AppSearchSchema.Builder}. */ public Builder(@NonNull String schemaType) { Objects.requireNonNull(schemaType); mSchemaType = schemaType; } /** Adds a property to the given type. */ @NonNull public AppSearchSchema.Builder addProperty(@NonNull PropertyConfig propertyConfig) { Objects.requireNonNull(propertyConfig); resetIfBuilt(); String name = propertyConfig.getName(); if (!mPropertyNames.add(name)) { throw new IllegalSchemaException("Property defined more than once: " + name); } mPropertyBundles.add(propertyConfig.mBundle); return this; } /** Constructs a new {@link AppSearchSchema} from the contents of this builder. */ @NonNull public AppSearchSchema build() { Bundle bundle = new Bundle(); bundle.putString(AppSearchSchema.SCHEMA_TYPE_FIELD, mSchemaType); bundle.putParcelableArrayList(AppSearchSchema.PROPERTIES_FIELD, mPropertyBundles); mBuilt = true; return new AppSearchSchema(bundle); } private void resetIfBuilt() { if (mBuilt) { mPropertyBundles = new ArrayList<>(mPropertyBundles); mBuilt = false; } } } /** * Common configuration for a single property (field) in a Document. * *

For example, an {@code EmailMessage} would be a type and the {@code subject} would be a * property. */ public abstract static class PropertyConfig { static final String NAME_FIELD = "name"; static final String DATA_TYPE_FIELD = "dataType"; static final String CARDINALITY_FIELD = "cardinality"; /** * Physical data-types of the contents of the property. * * @hide */ // NOTE: The integer values of these constants must match the proto enum constants in // com.google.android.icing.proto.PropertyConfigProto.DataType.Code. @IntDef( value = { DATA_TYPE_STRING, DATA_TYPE_LONG, DATA_TYPE_DOUBLE, DATA_TYPE_BOOLEAN, DATA_TYPE_BYTES, DATA_TYPE_DOCUMENT, }) @Retention(RetentionPolicy.SOURCE) public @interface DataType {} /** @hide */ public static final int DATA_TYPE_STRING = 1; /** @hide */ public static final int DATA_TYPE_LONG = 2; /** @hide */ public static final int DATA_TYPE_DOUBLE = 3; /** @hide */ public static final int DATA_TYPE_BOOLEAN = 4; /** * Unstructured BLOB. * * @hide */ public static final int DATA_TYPE_BYTES = 5; /** * Indicates that the property is itself a {@link GenericDocument}, making it part of a * hierarchical schema. Any property using this DataType MUST have a valid {@link * PropertyConfig#getSchemaType}. * * @hide */ public static final int DATA_TYPE_DOCUMENT = 6; /** * The cardinality of the property (whether it is required, optional or repeated). * * @hide */ // NOTE: The integer values of these constants must match the proto enum constants in // com.google.android.icing.proto.PropertyConfigProto.Cardinality.Code. @IntDef( value = { CARDINALITY_REPEATED, CARDINALITY_OPTIONAL, CARDINALITY_REQUIRED, }) @Retention(RetentionPolicy.SOURCE) public @interface Cardinality {} /** Any number of items (including zero) [0...*]. */ public static final int CARDINALITY_REPEATED = 1; /** Zero or one value [0,1]. */ public static final int CARDINALITY_OPTIONAL = 2; /** Exactly one value [1]. */ public static final int CARDINALITY_REQUIRED = 3; final Bundle mBundle; @Nullable private Integer mHashCode; PropertyConfig(@NonNull Bundle bundle) { mBundle = Objects.requireNonNull(bundle); } @Override @NonNull public String toString() { IndentingStringBuilder stringBuilder = new IndentingStringBuilder(); appendPropertyConfigString(stringBuilder); return stringBuilder.toString(); } /** * Appends a debug string for the {@link AppSearchSchema.PropertyConfig} instance to the * given string builder. * * @param builder the builder to append to. */ void appendPropertyConfigString(@NonNull IndentingStringBuilder builder) { Objects.requireNonNull(builder); builder.append("{\n"); builder.increaseIndentLevel(); builder.append("name: \"").append(getName()).append("\",\n"); if (this instanceof AppSearchSchema.StringPropertyConfig) { ((StringPropertyConfig) this).appendStringPropertyConfigFields(builder); } else if (this instanceof AppSearchSchema.DocumentPropertyConfig) { ((DocumentPropertyConfig) this).appendDocumentPropertyConfigFields(builder); } switch (getCardinality()) { case AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED: builder.append("cardinality: CARDINALITY_REPEATED,\n"); break; case AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL: builder.append("cardinality: CARDINALITY_OPTIONAL,\n"); break; case AppSearchSchema.PropertyConfig.CARDINALITY_REQUIRED: builder.append("cardinality: CARDINALITY_REQUIRED,\n"); break; default: builder.append("cardinality: CARDINALITY_UNKNOWN,\n"); } switch (getDataType()) { case AppSearchSchema.PropertyConfig.DATA_TYPE_STRING: builder.append("dataType: DATA_TYPE_STRING,\n"); break; case AppSearchSchema.PropertyConfig.DATA_TYPE_LONG: builder.append("dataType: DATA_TYPE_LONG,\n"); break; case AppSearchSchema.PropertyConfig.DATA_TYPE_DOUBLE: builder.append("dataType: DATA_TYPE_DOUBLE,\n"); break; case AppSearchSchema.PropertyConfig.DATA_TYPE_BOOLEAN: builder.append("dataType: DATA_TYPE_BOOLEAN,\n"); break; case AppSearchSchema.PropertyConfig.DATA_TYPE_BYTES: builder.append("dataType: DATA_TYPE_BYTES,\n"); break; case AppSearchSchema.PropertyConfig.DATA_TYPE_DOCUMENT: builder.append("dataType: DATA_TYPE_DOCUMENT,\n"); break; default: builder.append("dataType: DATA_TYPE_UNKNOWN,\n"); } builder.decreaseIndentLevel(); builder.append("}"); } /** Returns the name of this property. */ @NonNull public String getName() { return mBundle.getString(NAME_FIELD, ""); } /** * Returns the type of data the property contains (e.g. string, int, bytes, etc). * * @hide */ public @DataType int getDataType() { return mBundle.getInt(DATA_TYPE_FIELD, -1); } /** * Returns the cardinality of the property (whether it is optional, required or repeated). */ public @Cardinality int getCardinality() { return mBundle.getInt(CARDINALITY_FIELD, CARDINALITY_OPTIONAL); } @Override public boolean equals(@Nullable Object other) { if (this == other) { return true; } if (!(other instanceof PropertyConfig)) { return false; } PropertyConfig otherProperty = (PropertyConfig) other; return BundleUtil.deepEquals(this.mBundle, otherProperty.mBundle); } @Override public int hashCode() { if (mHashCode == null) { mHashCode = BundleUtil.deepHashCode(mBundle); } return mHashCode; } /** * Converts a {@link Bundle} into a {@link PropertyConfig} depending on its internal data * type. * *

The bundle is not cloned. * * @throws IllegalArgumentException if the bundle does no contain a recognized value in its * {@code DATA_TYPE_FIELD}. * @hide */ @NonNull public static PropertyConfig fromBundle(@NonNull Bundle propertyBundle) { switch (propertyBundle.getInt(PropertyConfig.DATA_TYPE_FIELD)) { case PropertyConfig.DATA_TYPE_STRING: return new StringPropertyConfig(propertyBundle); case PropertyConfig.DATA_TYPE_LONG: return new LongPropertyConfig(propertyBundle); case PropertyConfig.DATA_TYPE_DOUBLE: return new DoublePropertyConfig(propertyBundle); case PropertyConfig.DATA_TYPE_BOOLEAN: return new BooleanPropertyConfig(propertyBundle); case PropertyConfig.DATA_TYPE_BYTES: return new BytesPropertyConfig(propertyBundle); case PropertyConfig.DATA_TYPE_DOCUMENT: return new DocumentPropertyConfig(propertyBundle); default: throw new IllegalArgumentException( "Unsupported property bundle of type " + propertyBundle.getInt(PropertyConfig.DATA_TYPE_FIELD) + "; contents: " + propertyBundle); } } } /** Configuration for a property of type String in a Document. */ public static final class StringPropertyConfig extends PropertyConfig { private static final String INDEXING_TYPE_FIELD = "indexingType"; private static final String TOKENIZER_TYPE_FIELD = "tokenizerType"; /** * Encapsulates the configurations on how AppSearch should query/index these terms. * * @hide */ @IntDef( value = { INDEXING_TYPE_NONE, INDEXING_TYPE_EXACT_TERMS, INDEXING_TYPE_PREFIXES, }) @Retention(RetentionPolicy.SOURCE) public @interface IndexingType {} /** Content in this property will not be tokenized or indexed. */ public static final int INDEXING_TYPE_NONE = 0; /** * Content in this property should only be returned for queries matching the exact tokens * appearing in this property. * *

Ex. A property with "fool" should NOT match a query for "foo". */ public static final int INDEXING_TYPE_EXACT_TERMS = 1; /** * Content in this property should be returned for queries that are either exact matches or * query matches of the tokens appearing in this property. * *

Ex. A property with "fool" should match a query for "foo". */ public static final int INDEXING_TYPE_PREFIXES = 2; /** * Configures how tokens should be extracted from this property. * * @hide */ // NOTE: The integer values of these constants must match the proto enum constants in // com.google.android.icing.proto.IndexingConfig.TokenizerType.Code. @IntDef( value = { TOKENIZER_TYPE_NONE, TOKENIZER_TYPE_PLAIN, }) @Retention(RetentionPolicy.SOURCE) public @interface TokenizerType {} /** * This value indicates that no tokens should be extracted from this property. * *

It is only valid for tokenizer_type to be 'NONE' if {@link #getIndexingType} is {@link * #INDEXING_TYPE_NONE}. */ public static final int TOKENIZER_TYPE_NONE = 0; /** * Tokenization for plain text. This value indicates that tokens should be extracted from * this property based on word breaks. Segments of whitespace and punctuation are not * considered tokens. * *

Ex. A property with "foo bar. baz." will produce tokens for "foo", "bar" and "baz". * The segments " " and "." will not be considered tokens. * *

It is only valid for tokenizer_type to be 'PLAIN' if {@link #getIndexingType} is * {@link #INDEXING_TYPE_EXACT_TERMS} or {@link #INDEXING_TYPE_PREFIXES}. */ public static final int TOKENIZER_TYPE_PLAIN = 1; StringPropertyConfig(@NonNull Bundle bundle) { super(bundle); } /** Returns how the property is indexed. */ public @IndexingType int getIndexingType() { return mBundle.getInt(INDEXING_TYPE_FIELD); } /** Returns how this property is tokenized (split into words). */ public @TokenizerType int getTokenizerType() { return mBundle.getInt(TOKENIZER_TYPE_FIELD); } /** Builder for {@link StringPropertyConfig}. */ public static final class Builder { private final String mPropertyName; private @Cardinality int mCardinality = CARDINALITY_OPTIONAL; private @IndexingType int mIndexingType = INDEXING_TYPE_NONE; private @TokenizerType int mTokenizerType = TOKENIZER_TYPE_NONE; /** Creates a new {@link StringPropertyConfig.Builder}. */ public Builder(@NonNull String propertyName) { mPropertyName = Objects.requireNonNull(propertyName); } /** * The cardinality of the property (whether it is optional, required or repeated). * *

If this method is not called, the default cardinality is {@link * PropertyConfig#CARDINALITY_OPTIONAL}. */ @SuppressWarnings("MissingGetterMatchingBuilder") // getter defined in superclass @NonNull public StringPropertyConfig.Builder setCardinality(@Cardinality int cardinality) { Preconditions.checkArgumentInRange( cardinality, CARDINALITY_REPEATED, CARDINALITY_REQUIRED, "cardinality"); mCardinality = cardinality; return this; } /** * Configures how a property should be indexed so that it can be retrieved by queries. * *

If this method is not called, the default indexing type is {@link * StringPropertyConfig#INDEXING_TYPE_NONE}, so that it cannot be matched by queries. */ @NonNull public StringPropertyConfig.Builder setIndexingType(@IndexingType int indexingType) { Preconditions.checkArgumentInRange( indexingType, INDEXING_TYPE_NONE, INDEXING_TYPE_PREFIXES, "indexingType"); mIndexingType = indexingType; return this; } /** * Configures how this property should be tokenized (split into words). * *

If this method is not called, the default indexing type is {@link * StringPropertyConfig#TOKENIZER_TYPE_NONE}, so that it is not tokenized. * *

This method must be called with a value other than {@link * StringPropertyConfig#TOKENIZER_TYPE_NONE} if the property is indexed (i.e. if {@link * #setIndexingType} has been called with a value other than {@link * StringPropertyConfig#INDEXING_TYPE_NONE}). */ @NonNull public StringPropertyConfig.Builder setTokenizerType(@TokenizerType int tokenizerType) { Preconditions.checkArgumentInRange( tokenizerType, TOKENIZER_TYPE_NONE, TOKENIZER_TYPE_PLAIN, "tokenizerType"); mTokenizerType = tokenizerType; return this; } /** Constructs a new {@link StringPropertyConfig} from the contents of this builder. */ @NonNull public StringPropertyConfig build() { if (mTokenizerType == TOKENIZER_TYPE_NONE) { Preconditions.checkState( mIndexingType == INDEXING_TYPE_NONE, "Cannot set " + "TOKENIZER_TYPE_NONE with an indexing type other than " + "INDEXING_TYPE_NONE."); } else { Preconditions.checkState( mIndexingType != INDEXING_TYPE_NONE, "Cannot set " + "TOKENIZER_TYPE_PLAIN with INDEXING_TYPE_NONE."); } Bundle bundle = new Bundle(); bundle.putString(NAME_FIELD, mPropertyName); bundle.putInt(DATA_TYPE_FIELD, DATA_TYPE_STRING); bundle.putInt(CARDINALITY_FIELD, mCardinality); bundle.putInt(INDEXING_TYPE_FIELD, mIndexingType); bundle.putInt(TOKENIZER_TYPE_FIELD, mTokenizerType); return new StringPropertyConfig(bundle); } } /** * Appends a debug string for the {@link StringPropertyConfig} instance to the given string * builder. * *

This appends fields specific to a {@link StringPropertyConfig} instance. * * @param builder the builder to append to. */ void appendStringPropertyConfigFields(@NonNull IndentingStringBuilder builder) { switch (getIndexingType()) { case AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_NONE: builder.append("indexingType: INDEXING_TYPE_NONE,\n"); break; case AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS: builder.append("indexingType: INDEXING_TYPE_EXACT_TERMS,\n"); break; case AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_PREFIXES: builder.append("indexingType: INDEXING_TYPE_PREFIXES,\n"); break; default: builder.append("indexingType: INDEXING_TYPE_UNKNOWN,\n"); } switch (getTokenizerType()) { case AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_NONE: builder.append("tokenizerType: TOKENIZER_TYPE_NONE,\n"); break; case AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_PLAIN: builder.append("tokenizerType: TOKENIZER_TYPE_PLAIN,\n"); break; default: builder.append("tokenizerType: TOKENIZER_TYPE_UNKNOWN,\n"); } } } /** Configuration for a property containing a 64-bit integer. */ public static final class LongPropertyConfig extends PropertyConfig { LongPropertyConfig(@NonNull Bundle bundle) { super(bundle); } /** Builder for {@link LongPropertyConfig}. */ public static final class Builder { private final String mPropertyName; private @Cardinality int mCardinality = CARDINALITY_OPTIONAL; /** Creates a new {@link LongPropertyConfig.Builder}. */ public Builder(@NonNull String propertyName) { mPropertyName = Objects.requireNonNull(propertyName); } /** * The cardinality of the property (whether it is optional, required or repeated). * *

If this method is not called, the default cardinality is {@link * PropertyConfig#CARDINALITY_OPTIONAL}. */ @SuppressWarnings("MissingGetterMatchingBuilder") // getter defined in superclass @NonNull public LongPropertyConfig.Builder setCardinality(@Cardinality int cardinality) { Preconditions.checkArgumentInRange( cardinality, CARDINALITY_REPEATED, CARDINALITY_REQUIRED, "cardinality"); mCardinality = cardinality; return this; } /** Constructs a new {@link LongPropertyConfig} from the contents of this builder. */ @NonNull public LongPropertyConfig build() { Bundle bundle = new Bundle(); bundle.putString(NAME_FIELD, mPropertyName); bundle.putInt(DATA_TYPE_FIELD, DATA_TYPE_LONG); bundle.putInt(CARDINALITY_FIELD, mCardinality); return new LongPropertyConfig(bundle); } } } /** Configuration for a property containing a double-precision decimal number. */ public static final class DoublePropertyConfig extends PropertyConfig { DoublePropertyConfig(@NonNull Bundle bundle) { super(bundle); } /** Builder for {@link DoublePropertyConfig}. */ public static final class Builder { private final String mPropertyName; private @Cardinality int mCardinality = CARDINALITY_OPTIONAL; /** Creates a new {@link DoublePropertyConfig.Builder}. */ public Builder(@NonNull String propertyName) { mPropertyName = Objects.requireNonNull(propertyName); } /** * The cardinality of the property (whether it is optional, required or repeated). * *

If this method is not called, the default cardinality is {@link * PropertyConfig#CARDINALITY_OPTIONAL}. */ @SuppressWarnings("MissingGetterMatchingBuilder") // getter defined in superclass @NonNull public DoublePropertyConfig.Builder setCardinality(@Cardinality int cardinality) { Preconditions.checkArgumentInRange( cardinality, CARDINALITY_REPEATED, CARDINALITY_REQUIRED, "cardinality"); mCardinality = cardinality; return this; } /** Constructs a new {@link DoublePropertyConfig} from the contents of this builder. */ @NonNull public DoublePropertyConfig build() { Bundle bundle = new Bundle(); bundle.putString(NAME_FIELD, mPropertyName); bundle.putInt(DATA_TYPE_FIELD, DATA_TYPE_DOUBLE); bundle.putInt(CARDINALITY_FIELD, mCardinality); return new DoublePropertyConfig(bundle); } } } /** Configuration for a property containing a boolean. */ public static final class BooleanPropertyConfig extends PropertyConfig { BooleanPropertyConfig(@NonNull Bundle bundle) { super(bundle); } /** Builder for {@link BooleanPropertyConfig}. */ public static final class Builder { private final String mPropertyName; private @Cardinality int mCardinality = CARDINALITY_OPTIONAL; /** Creates a new {@link BooleanPropertyConfig.Builder}. */ public Builder(@NonNull String propertyName) { mPropertyName = Objects.requireNonNull(propertyName); } /** * The cardinality of the property (whether it is optional, required or repeated). * *

If this method is not called, the default cardinality is {@link * PropertyConfig#CARDINALITY_OPTIONAL}. */ @SuppressWarnings("MissingGetterMatchingBuilder") // getter defined in superclass @NonNull public BooleanPropertyConfig.Builder setCardinality(@Cardinality int cardinality) { Preconditions.checkArgumentInRange( cardinality, CARDINALITY_REPEATED, CARDINALITY_REQUIRED, "cardinality"); mCardinality = cardinality; return this; } /** Constructs a new {@link BooleanPropertyConfig} from the contents of this builder. */ @NonNull public BooleanPropertyConfig build() { Bundle bundle = new Bundle(); bundle.putString(NAME_FIELD, mPropertyName); bundle.putInt(DATA_TYPE_FIELD, DATA_TYPE_BOOLEAN); bundle.putInt(CARDINALITY_FIELD, mCardinality); return new BooleanPropertyConfig(bundle); } } } /** Configuration for a property containing a byte array. */ public static final class BytesPropertyConfig extends PropertyConfig { BytesPropertyConfig(@NonNull Bundle bundle) { super(bundle); } /** Builder for {@link BytesPropertyConfig}. */ public static final class Builder { private final String mPropertyName; private @Cardinality int mCardinality = CARDINALITY_OPTIONAL; /** Creates a new {@link BytesPropertyConfig.Builder}. */ public Builder(@NonNull String propertyName) { mPropertyName = Objects.requireNonNull(propertyName); } /** * The cardinality of the property (whether it is optional, required or repeated). * *

If this method is not called, the default cardinality is {@link * PropertyConfig#CARDINALITY_OPTIONAL}. */ @SuppressWarnings("MissingGetterMatchingBuilder") // getter defined in superclass @NonNull public BytesPropertyConfig.Builder setCardinality(@Cardinality int cardinality) { Preconditions.checkArgumentInRange( cardinality, CARDINALITY_REPEATED, CARDINALITY_REQUIRED, "cardinality"); mCardinality = cardinality; return this; } /** Constructs a new {@link BytesPropertyConfig} from the contents of this builder. */ @NonNull public BytesPropertyConfig build() { Bundle bundle = new Bundle(); bundle.putString(NAME_FIELD, mPropertyName); bundle.putInt(DATA_TYPE_FIELD, DATA_TYPE_BYTES); bundle.putInt(CARDINALITY_FIELD, mCardinality); return new BytesPropertyConfig(bundle); } } } /** Configuration for a property containing another Document. */ public static final class DocumentPropertyConfig extends PropertyConfig { private static final String SCHEMA_TYPE_FIELD = "schemaType"; private static final String INDEX_NESTED_PROPERTIES_FIELD = "indexNestedProperties"; DocumentPropertyConfig(@NonNull Bundle bundle) { super(bundle); } /** Returns the logical schema-type of the contents of this document property. */ @NonNull public String getSchemaType() { return Objects.requireNonNull(mBundle.getString(SCHEMA_TYPE_FIELD)); } /** * Returns whether fields in the nested document should be indexed according to that * document's schema. * *

If false, the nested document's properties are not indexed regardless of its own * schema. */ public boolean shouldIndexNestedProperties() { return mBundle.getBoolean(INDEX_NESTED_PROPERTIES_FIELD); } /** Builder for {@link DocumentPropertyConfig}. */ public static final class Builder { private final String mPropertyName; private final String mSchemaType; private @Cardinality int mCardinality = CARDINALITY_OPTIONAL; private boolean mShouldIndexNestedProperties = false; /** * Creates a new {@link DocumentPropertyConfig.Builder}. * * @param propertyName The logical name of the property in the schema, which will be * used as the key for this property in {@link * GenericDocument.Builder#setPropertyDocument}. * @param schemaType The type of documents which will be stored in this property. * Documents of different types cannot be mixed into a single property. */ public Builder(@NonNull String propertyName, @NonNull String schemaType) { mPropertyName = Objects.requireNonNull(propertyName); mSchemaType = Objects.requireNonNull(schemaType); } /** * The cardinality of the property (whether it is optional, required or repeated). * *

If this method is not called, the default cardinality is {@link * PropertyConfig#CARDINALITY_OPTIONAL}. */ @SuppressWarnings("MissingGetterMatchingBuilder") // getter defined in superclass @NonNull public DocumentPropertyConfig.Builder setCardinality(@Cardinality int cardinality) { Preconditions.checkArgumentInRange( cardinality, CARDINALITY_REPEATED, CARDINALITY_REQUIRED, "cardinality"); mCardinality = cardinality; return this; } /** * Configures whether fields in the nested document should be indexed according to that * document's schema. * *

If false, the nested document's properties are not indexed regardless of its own * schema. */ @NonNull public DocumentPropertyConfig.Builder setShouldIndexNestedProperties( boolean indexNestedProperties) { mShouldIndexNestedProperties = indexNestedProperties; return this; } /** Constructs a new {@link PropertyConfig} from the contents of this builder. */ @NonNull public DocumentPropertyConfig build() { Bundle bundle = new Bundle(); bundle.putString(NAME_FIELD, mPropertyName); bundle.putInt(DATA_TYPE_FIELD, DATA_TYPE_DOCUMENT); bundle.putInt(CARDINALITY_FIELD, mCardinality); bundle.putBoolean(INDEX_NESTED_PROPERTIES_FIELD, mShouldIndexNestedProperties); bundle.putString(SCHEMA_TYPE_FIELD, mSchemaType); return new DocumentPropertyConfig(bundle); } } /** * Appends a debug string for the {@link DocumentPropertyConfig} instance to the given * string builder. * *

This appends fields specific to a {@link DocumentPropertyConfig} instance. * * @param builder the builder to append to. */ void appendDocumentPropertyConfigFields(@NonNull IndentingStringBuilder builder) { builder.append("shouldIndexNestedProperties: ") .append(shouldIndexNestedProperties()) .append(",\n"); builder.append("schemaType: \"").append(getSchemaType()).append("\",\n"); } } }