/* * 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.FlaggedApi; import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.SuppressLint; import android.app.appsearch.annotation.CanIgnoreReturnValue; import android.app.appsearch.exceptions.AppSearchException; import android.app.appsearch.exceptions.IllegalSchemaException; import android.app.appsearch.flags.Flags; import android.app.appsearch.safeparcel.AbstractSafeParcelable; import android.app.appsearch.safeparcel.PropertyConfigParcel; import android.app.appsearch.safeparcel.PropertyConfigParcel.DocumentIndexingConfigParcel; import android.app.appsearch.safeparcel.PropertyConfigParcel.JoinableConfigParcel; import android.app.appsearch.safeparcel.PropertyConfigParcel.StringIndexingConfigParcel; import android.app.appsearch.safeparcel.SafeParcelable; import android.app.appsearch.util.IndentingStringBuilder; import android.os.Bundle; import android.os.Parcel; import android.os.Parcelable; 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.Collection; import java.util.Collections; import java.util.LinkedHashSet; 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 */ @SafeParcelable.Class(creator = "AppSearchSchemaCreator") @SuppressWarnings("HiddenSuperclass") public final class AppSearchSchema extends AbstractSafeParcelable { @FlaggedApi(Flags.FLAG_ENABLE_SAFE_PARCELABLE_2) @NonNull public static final Parcelable.Creator CREATOR = new AppSearchSchemaCreator(); @Field(id = 1, getter = "getSchemaType") private final String mSchemaType; @Field(id = 2) final List mPropertyConfigParcels; @Field(id = 3, getter = "getParentTypes") private final List mParentTypes; @Field(id = 4, getter = "getDescription") private final String mDescription; @Constructor AppSearchSchema( @Param(id = 1) @NonNull String schemaType, @Param(id = 2) @NonNull List propertyConfigParcels, @Param(id = 3) @NonNull List parentTypes, @Param(id = 4) @NonNull String description) { mSchemaType = Objects.requireNonNull(schemaType); mPropertyConfigParcels = Objects.requireNonNull(propertyConfigParcels); mParentTypes = Objects.requireNonNull(parentTypes); mDescription = Objects.requireNonNull(description); } @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("description: \"").append(getDescription()).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, such as Email. */ @NonNull public String getSchemaType() { return mSchemaType; } /** * Returns a natural language description of this schema type. * *

Ex. The description for an Email type could be "A type of electronic message". * *

This information is purely to help apps consuming this type to understand its semantic * meaning. This field has no effect in AppSearch - it is just stored with the AppSearchSchema. * If {@link Builder#setDescription} is uncalled, then this method will return an empty string. */ @FlaggedApi(Flags.FLAG_ENABLE_APP_FUNCTIONS) @NonNull public String getDescription() { return mDescription; } /** * Returns the list of {@link PropertyConfig}s that are part of this schema. * *

This method creates a new list when called. */ @NonNull @SuppressWarnings({"MixedMutabilityReturnType"}) public List getProperties() { if (mPropertyConfigParcels.isEmpty()) { return Collections.emptyList(); } List ret = new ArrayList<>(mPropertyConfigParcels.size()); for (int i = 0; i < mPropertyConfigParcels.size(); i++) { ret.add(PropertyConfig.fromParcel(mPropertyConfigParcels.get(i))); } return ret; } /** Returns the list of parent types of this schema for polymorphism. */ @FlaggedApi(Flags.FLAG_ENABLE_GET_PARENT_TYPES_AND_INDEXABLE_NESTED_PROPERTIES) @NonNull public List getParentTypes() { return Collections.unmodifiableList(mParentTypes); } @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; } if (!getDescription().equals(otherSchema.getDescription())) { return false; } if (!getParentTypes().equals(otherSchema.getParentTypes())) { return false; } return getProperties().equals(otherSchema.getProperties()); } @Override public int hashCode() { return Objects.hash(getSchemaType(), getProperties(), getParentTypes(), getDescription()); } @FlaggedApi(Flags.FLAG_ENABLE_SAFE_PARCELABLE_2) @Override public void writeToParcel(@NonNull Parcel dest, int flags) { AppSearchSchemaCreator.writeToParcel(this, dest, flags); } /** Builder for {@link AppSearchSchema objects}. */ public static final class Builder { private final String mSchemaType; private String mDescription = ""; private ArrayList mPropertyConfigParcels = new ArrayList<>(); private LinkedHashSet mParentTypes = new LinkedHashSet<>(); private final Set mPropertyNames = new ArraySet<>(); private boolean mBuilt = false; /** Creates a new {@link AppSearchSchema.Builder}. */ public Builder(@NonNull String schemaType) { mSchemaType = Objects.requireNonNull(schemaType); } /** * Sets a natural language description of this schema type. * *

For more details about the description field, see {@link * AppSearchSchema#getDescription}. */ @FlaggedApi(Flags.FLAG_ENABLE_APP_FUNCTIONS) @CanIgnoreReturnValue @NonNull public AppSearchSchema.Builder setDescription(@NonNull String description) { Objects.requireNonNull(description); resetIfBuilt(); mDescription = description; return this; } /** Adds a property to the given type. */ @CanIgnoreReturnValue @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); } mPropertyConfigParcels.add(propertyConfig.mPropertyConfigParcel); return this; } /** * Adds a parent type to the given type for polymorphism, so that the given type will be * considered as a subtype of {@code parentSchemaType}. * *

Subtype relations are automatically considered transitive, so callers are only * required to provide direct parents. Specifically, if T1 <: T2 and T2 <: T3 are * known, then T1 <: T3 will be inferred automatically, where <: is the subtype * symbol. * *

Polymorphism is currently supported in the following ways: * *

* *

Subtypes must meet the following requirements. A violation of the requirements will * cause {@link AppSearchSession#setSchema} to throw an {@link AppSearchException} with the * result code of {@link AppSearchResult#RESULT_INVALID_ARGUMENT}. Consider a type Artist * and a type Person, and Artist claims to be a subtype of Person, then: * *

* *

A type can be defined to have multiple parents, but it must be compatible with each of * its parents based on the above rules. For example, if LocalBusiness is defined as a * subtype of both Place and Organization, then the compatibility of LocalBusiness with * Place and the compatibility of LocalBusiness with Organization will both be checked. */ @CanIgnoreReturnValue @NonNull public AppSearchSchema.Builder addParentType(@NonNull String parentSchemaType) { Objects.requireNonNull(parentSchemaType); resetIfBuilt(); mParentTypes.add(parentSchemaType); return this; } /** Constructs a new {@link AppSearchSchema} from the contents of this builder. */ @NonNull public AppSearchSchema build() { mBuilt = true; return new AppSearchSchema( mSchemaType, mPropertyConfigParcels, new ArrayList<>(mParentTypes), mDescription); } private void resetIfBuilt() { if (mBuilt) { mPropertyConfigParcels = new ArrayList<>(mPropertyConfigParcels); mParentTypes = new LinkedHashSet<>(mParentTypes); 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 { /** * Physical data-types of the contents of the property. * *

NOTE: The integer values of these constants must match the proto enum constants in * com.google.android.icing.proto.PropertyConfigProto.DataType.Code. * * @hide */ @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). * *

NOTE: The integer values of these constants must match the proto enum constants in * com.google.android.icing.proto.PropertyConfigProto.Cardinality.Code. * * @hide */ @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 PropertyConfigParcel mPropertyConfigParcel; PropertyConfig(@NonNull PropertyConfigParcel propertyConfigParcel) { mPropertyConfigParcel = Objects.requireNonNull(propertyConfigParcel); } @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"); builder.append("description: \"").append(getDescription()).append("\",\n"); if (this instanceof AppSearchSchema.StringPropertyConfig) { ((StringPropertyConfig) this).appendStringPropertyConfigFields(builder); } else if (this instanceof AppSearchSchema.DocumentPropertyConfig) { ((DocumentPropertyConfig) this).appendDocumentPropertyConfigFields(builder); } else if (this instanceof AppSearchSchema.LongPropertyConfig) { ((LongPropertyConfig) this).appendLongPropertyConfigFields(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 mPropertyConfigParcel.getName(); } /** * Returns a natural language description of this property. * *

Ex. The description for the "homeAddress" property of a "Person" type could be "the * address at which this person lives". * *

This information is purely to help apps consuming this type the semantic meaning of * its properties. This field has no effect in AppSearch - it is just stored with the * AppSearchSchema. If the description is not set, then this method will return an empty * string. */ @FlaggedApi(Flags.FLAG_ENABLE_APP_FUNCTIONS) @NonNull public String getDescription() { return mPropertyConfigParcel.getDescription(); } /** * Returns the type of data the property contains (such as string, int, bytes, etc). * * @hide */ @DataType public int getDataType() { return mPropertyConfigParcel.getDataType(); } /** * Returns the cardinality of the property (whether it is optional, required or repeated). */ @Cardinality public int getCardinality() { return mPropertyConfigParcel.getCardinality(); } @Override public boolean equals(@Nullable Object other) { if (this == other) { return true; } if (!(other instanceof PropertyConfig)) { return false; } PropertyConfig otherProperty = (PropertyConfig) other; return Objects.equals(mPropertyConfigParcel, otherProperty.mPropertyConfigParcel); } @Override public int hashCode() { return mPropertyConfigParcel.hashCode(); } /** * 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 fromParcel( @NonNull PropertyConfigParcel propertyConfigParcel) { Objects.requireNonNull(propertyConfigParcel); switch (propertyConfigParcel.getDataType()) { case PropertyConfig.DATA_TYPE_STRING: return new StringPropertyConfig(propertyConfigParcel); case PropertyConfig.DATA_TYPE_LONG: return new LongPropertyConfig(propertyConfigParcel); case PropertyConfig.DATA_TYPE_DOUBLE: return new DoublePropertyConfig(propertyConfigParcel); case PropertyConfig.DATA_TYPE_BOOLEAN: return new BooleanPropertyConfig(propertyConfigParcel); case PropertyConfig.DATA_TYPE_BYTES: return new BytesPropertyConfig(propertyConfigParcel); case PropertyConfig.DATA_TYPE_DOCUMENT: return new DocumentPropertyConfig(propertyConfigParcel); default: throw new IllegalArgumentException( "Unsupported property bundle of type " + propertyConfigParcel.getDataType() + "; contents: " + propertyConfigParcel); } } } /** Configuration for a property of type String in a Document. */ public static final class StringPropertyConfig extends PropertyConfig { /** * 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. * *

For example, 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. * *

For example, 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. * *

NOTE: The integer values of these constants must match the proto enum constants in * com.google.android.icing.proto.IndexingConfig.TokenizerType.Code. * * @hide */ @IntDef( value = { TOKENIZER_TYPE_NONE, TOKENIZER_TYPE_PLAIN, TOKENIZER_TYPE_VERBATIM, TOKENIZER_TYPE_RFC822 }) @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. * *

For example, 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; /** * This value indicates that no normalization or segmentation should be applied to string * values that are tokenized using this type. Therefore, the output token is equivalent to * the raw string value. * *

For example, a property with "Hello, world!" will produce the token "Hello, world!", * preserving punctuation and capitalization, and not creating separate tokens between the * space. * *

It is only valid for tokenizer_type to be 'VERBATIM' if {@link #getIndexingType} is * {@link #INDEXING_TYPE_EXACT_TERMS} or {@link #INDEXING_TYPE_PREFIXES}. */ public static final int TOKENIZER_TYPE_VERBATIM = 2; /** * Tokenization for emails. This value indicates that tokens should be extracted from this * property based on email structure. * *

For example, a property with "alex.sav@google.com" will produce tokens for "alex", * "sav", "alex.sav", "google", "com", and "alexsav@google.com" * *

It is only valid for tokenizer_type to be 'RFC822' if {@link #getIndexingType} is * {@link #INDEXING_TYPE_EXACT_TERMS} or {@link #INDEXING_TYPE_PREFIXES}. */ public static final int TOKENIZER_TYPE_RFC822 = 3; /** * The joinable value type of the property. By setting the appropriate joinable value type * for a property, the client can use the property for joining documents from other schema * types using Search API (see {@link JoinSpec}). * * @hide */ // NOTE: The integer values of these constants must match the proto enum constants in // com.google.android.icing.proto.JoinableConfig.ValueType.Code. @IntDef( value = { JOINABLE_VALUE_TYPE_NONE, JOINABLE_VALUE_TYPE_QUALIFIED_ID, }) @Retention(RetentionPolicy.SOURCE) public @interface JoinableValueType {} /** Content in this property is not joinable. */ public static final int JOINABLE_VALUE_TYPE_NONE = 0; /** * Content in this string property will be used as a qualified id to join documents. * *

*/ public static final int JOINABLE_VALUE_TYPE_QUALIFIED_ID = 1; StringPropertyConfig(@NonNull PropertyConfigParcel propertyConfigParcel) { super(propertyConfigParcel); } /** Returns how the property is indexed. */ @IndexingType public int getIndexingType() { StringIndexingConfigParcel indexingConfigParcel = mPropertyConfigParcel.getStringIndexingConfigParcel(); if (indexingConfigParcel == null) { return INDEXING_TYPE_NONE; } return indexingConfigParcel.getIndexingType(); } /** Returns how this property is tokenized (split into words). */ @TokenizerType public int getTokenizerType() { StringIndexingConfigParcel indexingConfigParcel = mPropertyConfigParcel.getStringIndexingConfigParcel(); if (indexingConfigParcel == null) { return TOKENIZER_TYPE_NONE; } return indexingConfigParcel.getTokenizerType(); } /** * Returns how this property is going to be used to join documents from other schema types. */ @JoinableValueType public int getJoinableValueType() { JoinableConfigParcel joinableConfigParcel = mPropertyConfigParcel.getJoinableConfigParcel(); if (joinableConfigParcel == null) { return JOINABLE_VALUE_TYPE_NONE; } return joinableConfigParcel.getJoinableValueType(); } /** Builder for {@link StringPropertyConfig}. */ public static final class Builder { private final String mPropertyName; private String mDescription = ""; @Cardinality private int mCardinality = CARDINALITY_OPTIONAL; @IndexingType private int mIndexingType = INDEXING_TYPE_NONE; @TokenizerType private int mTokenizerType = TOKENIZER_TYPE_NONE; @JoinableValueType private int mJoinableValueType = JOINABLE_VALUE_TYPE_NONE; private boolean mDeletionPropagation = false; /** Creates a new {@link StringPropertyConfig.Builder}. */ public Builder(@NonNull String propertyName) { mPropertyName = Objects.requireNonNull(propertyName); } /** * Sets a natural language description of this property. * *

For more details about the description field, see {@link * AppSearchSchema.PropertyConfig#getDescription}. */ @FlaggedApi(Flags.FLAG_ENABLE_APP_FUNCTIONS) @SuppressWarnings("MissingGetterMatchingBuilder") // getter defined in superclass @NonNull public StringPropertyConfig.Builder setDescription(@NonNull String description) { mDescription = Objects.requireNonNull(description); return this; } /** * Sets 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}. */ @CanIgnoreReturnValue @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. */ @CanIgnoreReturnValue @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 (that is, if * {@link #setIndexingType} has been called with a value other than {@link * StringPropertyConfig#INDEXING_TYPE_NONE}). */ @CanIgnoreReturnValue @NonNull public StringPropertyConfig.Builder setTokenizerType(@TokenizerType int tokenizerType) { Preconditions.checkArgumentInRange( tokenizerType, TOKENIZER_TYPE_NONE, TOKENIZER_TYPE_RFC822, "tokenizerType"); mTokenizerType = tokenizerType; return this; } /** * Configures how this property should be used as a joining matcher. * *

If this method is not called, the default joinable value type is {@link * StringPropertyConfig#JOINABLE_VALUE_TYPE_NONE}, so that it is not joinable. * *

At most, 64 properties can be set as joinable per schema. */ @CanIgnoreReturnValue @NonNull public StringPropertyConfig.Builder setJoinableValueType( @JoinableValueType int joinableValueType) { Preconditions.checkArgumentInRange( joinableValueType, JOINABLE_VALUE_TYPE_NONE, JOINABLE_VALUE_TYPE_QUALIFIED_ID, "joinableValueType"); mJoinableValueType = joinableValueType; 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."); } if (mJoinableValueType == JOINABLE_VALUE_TYPE_QUALIFIED_ID) { Preconditions.checkState( mCardinality != CARDINALITY_REPEATED, "Cannot set JOINABLE_VALUE_TYPE_QUALIFIED_ID with" + " CARDINALITY_REPEATED."); } else { Preconditions.checkState( !mDeletionPropagation, "Cannot set deletion " + "propagation without setting a joinable value type"); } PropertyConfigParcel.StringIndexingConfigParcel stringConfigParcel = new StringIndexingConfigParcel(mIndexingType, mTokenizerType); JoinableConfigParcel joinableConfigParcel = new JoinableConfigParcel(mJoinableValueType, mDeletionPropagation); return new StringPropertyConfig( PropertyConfigParcel.createForString( mPropertyName, mDescription, mCardinality, stringConfigParcel, joinableConfigParcel)); } } /** * 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; case AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_VERBATIM: builder.append("tokenizerType: TOKENIZER_TYPE_VERBATIM,\n"); break; case AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_RFC822: builder.append("tokenizerType: TOKENIZER_TYPE_RFC822,\n"); break; default: builder.append("tokenizerType: TOKENIZER_TYPE_UNKNOWN,\n"); } switch (getJoinableValueType()) { case AppSearchSchema.StringPropertyConfig.JOINABLE_VALUE_TYPE_NONE: builder.append("joinableValueType: JOINABLE_VALUE_TYPE_NONE,\n"); break; case AppSearchSchema.StringPropertyConfig.JOINABLE_VALUE_TYPE_QUALIFIED_ID: builder.append("joinableValueType: JOINABLE_VALUE_TYPE_QUALIFIED_ID,\n"); break; default: builder.append("joinableValueType: JOINABLE_VALUE_TYPE_UNKNOWN,\n"); } } } /** Configuration for a property containing a 64-bit integer. */ public static final class LongPropertyConfig extends PropertyConfig { /** * Encapsulates the configurations on how AppSearch should query/index these 64-bit * integers. * * @hide */ @IntDef(value = {INDEXING_TYPE_NONE, INDEXING_TYPE_RANGE}) @Retention(RetentionPolicy.SOURCE) public @interface IndexingType {} /** Content in this property will not be indexed. */ public static final int INDEXING_TYPE_NONE = 0; /** * Content in this property will be indexed and can be fetched via numeric search range * query. * *

For example, a property with 1024 should match numeric search range query [0, 2000]. */ public static final int INDEXING_TYPE_RANGE = 1; LongPropertyConfig(@NonNull PropertyConfigParcel propertyConfigParcel) { super(propertyConfigParcel); } /** Returns how the property is indexed. */ @IndexingType public int getIndexingType() { PropertyConfigParcel.IntegerIndexingConfigParcel indexingConfigParcel = mPropertyConfigParcel.getIntegerIndexingConfigParcel(); if (indexingConfigParcel == null) { return INDEXING_TYPE_NONE; } return indexingConfigParcel.getIndexingType(); } /** Builder for {@link LongPropertyConfig}. */ public static final class Builder { private final String mPropertyName; private String mDescription = ""; @Cardinality private int mCardinality = CARDINALITY_OPTIONAL; @IndexingType private int mIndexingType = INDEXING_TYPE_NONE; /** Creates a new {@link LongPropertyConfig.Builder}. */ public Builder(@NonNull String propertyName) { mPropertyName = Objects.requireNonNull(propertyName); } /** * Sets a natural language description of this property. * *

For more details about the description field, see {@link * AppSearchSchema.PropertyConfig#getDescription}. */ @FlaggedApi(Flags.FLAG_ENABLE_APP_FUNCTIONS) @SuppressWarnings("MissingGetterMatchingBuilder") // getter defined in superclass @NonNull public LongPropertyConfig.Builder setDescription(@NonNull String description) { mDescription = Objects.requireNonNull(description); return this; } /** * Sets 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}. */ @CanIgnoreReturnValue @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; } /** * 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 * LongPropertyConfig#INDEXING_TYPE_NONE}, so that it will not be indexed and cannot be * matched by queries. */ @CanIgnoreReturnValue @NonNull public LongPropertyConfig.Builder setIndexingType(@IndexingType int indexingType) { Preconditions.checkArgumentInRange( indexingType, INDEXING_TYPE_NONE, INDEXING_TYPE_RANGE, "indexingType"); mIndexingType = indexingType; return this; } /** Constructs a new {@link LongPropertyConfig} from the contents of this builder. */ @NonNull public LongPropertyConfig build() { return new LongPropertyConfig( PropertyConfigParcel.createForLong( mPropertyName, mDescription, mCardinality, mIndexingType)); } } /** * Appends a debug string for the {@link LongPropertyConfig} instance to the given string * builder. * *

This appends fields specific to a {@link LongPropertyConfig} instance. * * @param builder the builder to append to. */ void appendLongPropertyConfigFields(@NonNull IndentingStringBuilder builder) { switch (getIndexingType()) { case AppSearchSchema.LongPropertyConfig.INDEXING_TYPE_NONE: builder.append("indexingType: INDEXING_TYPE_NONE,\n"); break; case AppSearchSchema.LongPropertyConfig.INDEXING_TYPE_RANGE: builder.append("indexingType: INDEXING_TYPE_RANGE,\n"); break; default: builder.append("indexingType: INDEXING_TYPE_UNKNOWN,\n"); } } } /** Configuration for a property containing a double-precision decimal number. */ public static final class DoublePropertyConfig extends PropertyConfig { DoublePropertyConfig(@NonNull PropertyConfigParcel propertyConfigParcel) { super(propertyConfigParcel); } /** Builder for {@link DoublePropertyConfig}. */ public static final class Builder { private final String mPropertyName; private String mDescription = ""; @Cardinality private int mCardinality = CARDINALITY_OPTIONAL; /** Creates a new {@link DoublePropertyConfig.Builder}. */ public Builder(@NonNull String propertyName) { mPropertyName = Objects.requireNonNull(propertyName); } /** * Sets a natural language description of this property. * *

For more details about the description field, see {@link * AppSearchSchema.PropertyConfig#getDescription}. */ @FlaggedApi(Flags.FLAG_ENABLE_APP_FUNCTIONS) @SuppressWarnings("MissingGetterMatchingBuilder") // getter defined in superclass @NonNull public DoublePropertyConfig.Builder setDescription(@NonNull String description) { mDescription = Objects.requireNonNull(description); return this; } /** * Sets 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}. */ @CanIgnoreReturnValue @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() { return new DoublePropertyConfig( PropertyConfigParcel.createForDouble( mPropertyName, mDescription, mCardinality)); } } } /** Configuration for a property containing a boolean. */ public static final class BooleanPropertyConfig extends PropertyConfig { BooleanPropertyConfig(@NonNull PropertyConfigParcel propertyConfigParcel) { super(propertyConfigParcel); } /** Builder for {@link BooleanPropertyConfig}. */ public static final class Builder { private final String mPropertyName; private String mDescription = ""; @Cardinality private int mCardinality = CARDINALITY_OPTIONAL; /** Creates a new {@link BooleanPropertyConfig.Builder}. */ public Builder(@NonNull String propertyName) { mPropertyName = Objects.requireNonNull(propertyName); } /** * Sets a natural language description of this property. * *

For more details about the description field, see {@link * AppSearchSchema.PropertyConfig#getDescription}. */ @FlaggedApi(Flags.FLAG_ENABLE_APP_FUNCTIONS) @SuppressWarnings("MissingGetterMatchingBuilder") // getter defined in superclass @NonNull public BooleanPropertyConfig.Builder setDescription(@NonNull String description) { mDescription = Objects.requireNonNull(description); return this; } /** * Sets 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}. */ @CanIgnoreReturnValue @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() { return new BooleanPropertyConfig( PropertyConfigParcel.createForBoolean( mPropertyName, mDescription, mCardinality)); } } } /** Configuration for a property containing a byte array. */ public static final class BytesPropertyConfig extends PropertyConfig { BytesPropertyConfig(@NonNull PropertyConfigParcel propertyConfigParcel) { super(propertyConfigParcel); } /** Builder for {@link BytesPropertyConfig}. */ public static final class Builder { private final String mPropertyName; private String mDescription = ""; @Cardinality private int mCardinality = CARDINALITY_OPTIONAL; /** Creates a new {@link BytesPropertyConfig.Builder}. */ public Builder(@NonNull String propertyName) { mPropertyName = Objects.requireNonNull(propertyName); } /** * Sets a natural language description of this property. * *

For more details about the description field, see {@link * AppSearchSchema.PropertyConfig#getDescription}. */ @FlaggedApi(Flags.FLAG_ENABLE_APP_FUNCTIONS) @SuppressWarnings("MissingGetterMatchingBuilder") // getter defined in superclass @NonNull public BytesPropertyConfig.Builder setDescription(@NonNull String description) { mDescription = Objects.requireNonNull(description); return this; } /** * Sets 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}. */ @CanIgnoreReturnValue @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() { return new BytesPropertyConfig( PropertyConfigParcel.createForBytes( mPropertyName, mDescription, mCardinality)); } } } /** Configuration for a property containing another Document. */ public static final class DocumentPropertyConfig extends PropertyConfig { DocumentPropertyConfig(@NonNull PropertyConfigParcel propertyConfigParcel) { super(propertyConfigParcel); } /** Returns the logical schema-type of the contents of this document property. */ @NonNull public String getSchemaType() { return Objects.requireNonNull(mPropertyConfigParcel.getSchemaType()); } /** * Returns whether properties 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. * * @see DocumentPropertyConfig.Builder#addIndexableNestedProperties(Collection) for indexing * a subset of properties from the nested document. */ public boolean shouldIndexNestedProperties() { DocumentIndexingConfigParcel indexingConfigParcel = mPropertyConfigParcel.getDocumentIndexingConfigParcel(); if (indexingConfigParcel == null) { return false; } return indexingConfigParcel.shouldIndexNestedProperties(); } /** Returns the list of indexable nested properties for the nested document. */ @FlaggedApi(Flags.FLAG_ENABLE_GET_PARENT_TYPES_AND_INDEXABLE_NESTED_PROPERTIES) @NonNull public List getIndexableNestedProperties() { DocumentIndexingConfigParcel indexingConfigParcel = mPropertyConfigParcel.getDocumentIndexingConfigParcel(); if (indexingConfigParcel == null) { return Collections.emptyList(); } List indexableNestedPropertiesList = indexingConfigParcel.getIndexableNestedPropertiesList(); if (indexableNestedPropertiesList == null) { return Collections.emptyList(); } return Collections.unmodifiableList(indexableNestedPropertiesList); } /** Builder for {@link DocumentPropertyConfig}. */ public static final class Builder { private final String mPropertyName; private final String mSchemaType; private String mDescription = ""; @Cardinality private int mCardinality = CARDINALITY_OPTIONAL; private boolean mShouldIndexNestedProperties = false; private final Set mIndexableNestedPropertiesList = new ArraySet<>(); /** * 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); } /** * Sets a natural language description of this property. * *

For more details about the description field, see {@link * AppSearchSchema.PropertyConfig#getDescription}. */ @FlaggedApi(Flags.FLAG_ENABLE_APP_FUNCTIONS) @SuppressWarnings("MissingGetterMatchingBuilder") // getter defined in superclass @NonNull public DocumentPropertyConfig.Builder setDescription(@NonNull String description) { mDescription = Objects.requireNonNull(description); return this; } /** * Sets 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}. */ @CanIgnoreReturnValue @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 properties 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. * *

To index a subset of properties from the nested document, set this to false and * use {@link #addIndexableNestedProperties(Collection)}. */ @CanIgnoreReturnValue @NonNull public DocumentPropertyConfig.Builder setShouldIndexNestedProperties( boolean indexNestedProperties) { mShouldIndexNestedProperties = indexNestedProperties; return this; } /** * Adds one or more properties for indexing from the nested document property. * * @see #addIndexableNestedProperties(Collection) */ @FlaggedApi(Flags.FLAG_ENABLE_GET_PARENT_TYPES_AND_INDEXABLE_NESTED_PROPERTIES) @CanIgnoreReturnValue @NonNull public DocumentPropertyConfig.Builder addIndexableNestedProperties( @NonNull String... indexableNestedProperties) { Objects.requireNonNull(indexableNestedProperties); return addIndexableNestedProperties(Arrays.asList(indexableNestedProperties)); } /** * Adds one or more property paths for indexing from the nested document property. * * @see #addIndexableNestedProperties(Collection) */ @FlaggedApi(Flags.FLAG_ENABLE_GET_PARENT_TYPES_AND_INDEXABLE_NESTED_PROPERTIES) @CanIgnoreReturnValue @SuppressLint("MissingGetterMatchingBuilder") @NonNull public DocumentPropertyConfig.Builder addIndexableNestedPropertyPaths( @NonNull PropertyPath... indexableNestedPropertyPaths) { Objects.requireNonNull(indexableNestedPropertyPaths); return addIndexableNestedPropertyPaths(Arrays.asList(indexableNestedPropertyPaths)); } /** * Adds one or more properties for indexing from the nested document. The added property * will be indexed according to that property's indexing configurations in the * document's schema definition. All properties in this list will consume a sectionId * regardless of its actual indexing config -- this includes properties added that do * not actually exist, as well as properties that are not set as indexable in the nested * schema type. * *

Input strings should follow the format of the property path for the nested * property, with '.' as the path separator. This nested document's property name should * not be included in the property path. * *

Ex. Consider an 'Organization' schema type which defines a nested document * property 'address' (Address schema type), where Address has a nested document * property 'country' (Country schema type with string 'name' property), and a string * 'street' property. The 'street' and 'country's name' properties from the 'address' * document property can be indexed for the 'Organization' schema type by calling: * *

{@code
             * OrganizationSchema.addProperty(
             *                 new DocumentPropertyConfig.Builder("address", "Address")
             *                         .addIndexableNestedProperties("street", "country.name")
             *                         .build()).
             * }
* *

{@link DocumentPropertyConfig.Builder#setShouldIndexNestedProperties} is required * to be false if any indexable nested property is added this way for the document * property. Attempting to build a DocumentPropertyConfig when this is not true throws * {@link IllegalArgumentException}. */ @CanIgnoreReturnValue @NonNull public DocumentPropertyConfig.Builder addIndexableNestedProperties( @NonNull Collection indexableNestedProperties) { Objects.requireNonNull(indexableNestedProperties); mIndexableNestedPropertiesList.addAll(indexableNestedProperties); return this; } /** * Adds one or more property paths for indexing from the nested document property. * * @see #addIndexableNestedProperties(Collection) */ @FlaggedApi(Flags.FLAG_ENABLE_GET_PARENT_TYPES_AND_INDEXABLE_NESTED_PROPERTIES) @CanIgnoreReturnValue @SuppressLint("MissingGetterMatchingBuilder") @NonNull public DocumentPropertyConfig.Builder addIndexableNestedPropertyPaths( @NonNull Collection indexableNestedPropertyPaths) { Objects.requireNonNull(indexableNestedPropertyPaths); List propertyPathList = new ArrayList<>(indexableNestedPropertyPaths); for (int i = 0; i < indexableNestedPropertyPaths.size(); i++) { mIndexableNestedPropertiesList.add(propertyPathList.get(i).toString()); } return this; } /** * Constructs a new {@link PropertyConfig} from the contents of this builder. * * @throws IllegalArgumentException if the provided PropertyConfig sets {@link * #shouldIndexNestedProperties()} to true and has one or more properties defined * using {@link #addIndexableNestedProperties(Collection)}. */ @NonNull public DocumentPropertyConfig build() { if (mShouldIndexNestedProperties && !mIndexableNestedPropertiesList.isEmpty()) { throw new IllegalArgumentException( "DocumentIndexingConfig#shouldIndexNestedProperties is required " + "to be false when one or more indexableNestedProperties are " + "provided."); } return new DocumentPropertyConfig( PropertyConfigParcel.createForDocument( mPropertyName, mDescription, mCardinality, mSchemaType, new DocumentIndexingConfigParcel( mShouldIndexNestedProperties, new ArrayList<>(mIndexableNestedPropertiesList)))); } } /** * 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("indexableNestedProperties: ") .append(getIndexableNestedProperties()) .append(",\n"); builder.append("schemaType: \"").append(getSchemaType()).append("\",\n"); } } }