| /* |
| * 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 androidx.appsearch.app; |
| |
| import android.annotation.SuppressLint; |
| import android.os.Bundle; |
| |
| import androidx.annotation.IntDef; |
| import androidx.annotation.NonNull; |
| import androidx.annotation.Nullable; |
| import androidx.annotation.RestrictTo; |
| import androidx.appsearch.exceptions.IllegalSchemaException; |
| import androidx.appsearch.util.BundleUtil; |
| import androidx.collection.ArraySet; |
| import androidx.core.util.ObjectsCompat; |
| import androidx.core.util.Preconditions; |
| |
| import java.lang.annotation.Retention; |
| import java.lang.annotation.RetentionPolicy; |
| import java.util.ArrayList; |
| import java.util.Collections; |
| import java.util.List; |
| import java.util.Set; |
| |
| /** |
| * The AppSearch Schema for a particular type of document. |
| * |
| * <p>For example, an e-mail message or a music recording could be a schema type. |
| * |
| * <p>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 */ |
| @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) |
| public AppSearchSchema(@NonNull Bundle bundle) { |
| Preconditions.checkNotNull(bundle); |
| mBundle = bundle; |
| } |
| |
| /** |
| * Returns the {@link Bundle} populated by this builder. |
| * @hide |
| */ |
| @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) |
| @NonNull |
| public Bundle getBundle() { |
| return mBundle; |
| } |
| |
| @Override |
| public String toString() { |
| return mBundle.toString(); |
| } |
| |
| /** 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. |
| * |
| * <p>This method creates a new list when called. |
| */ |
| @NonNull |
| public List<PropertyConfig> getProperties() { |
| ArrayList<Bundle> propertyBundles = |
| mBundle.getParcelableArrayList(AppSearchSchema.PROPERTIES_FIELD); |
| if (propertyBundles.isEmpty()) { |
| return Collections.emptyList(); |
| } |
| List<PropertyConfig> ret = new ArrayList<>(propertyBundles.size()); |
| for (int i = 0; i < propertyBundles.size(); i++) { |
| ret.add(new PropertyConfig(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 ObjectsCompat.hash(getSchemaType(), getProperties()); |
| } |
| |
| /** Builder for {@link AppSearchSchema objects}. */ |
| public static final class Builder { |
| private final String mSchemaType; |
| private final ArrayList<Bundle> mPropertyBundles = new ArrayList<>(); |
| private final Set<String> mPropertyNames = new ArraySet<>(); |
| private boolean mBuilt = false; |
| |
| /** Creates a new {@link AppSearchSchema.Builder}. */ |
| public Builder(@NonNull String schemaType) { |
| Preconditions.checkNotNull(schemaType); |
| mSchemaType = schemaType; |
| } |
| |
| /** Adds a property to the given type. */ |
| // TODO(b/171360120): MissingGetterMatchingBuilder expects a method called getPropertys, but |
| // we provide the (correct) method getProperties. Once the bug referenced in this TODO is |
| // fixed, remove this SuppressLint. |
| @SuppressLint("MissingGetterMatchingBuilder") |
| @NonNull |
| public AppSearchSchema.Builder addProperty(@NonNull PropertyConfig propertyConfig) { |
| Preconditions.checkState(!mBuilt, "Builder has already been used"); |
| Preconditions.checkNotNull(propertyConfig); |
| 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. |
| * |
| * <p>After calling this method, the builder must no longer be used. |
| */ |
| @NonNull |
| public AppSearchSchema build() { |
| Preconditions.checkState(!mBuilt, "Builder has already been used"); |
| Bundle bundle = new Bundle(); |
| bundle.putString(AppSearchSchema.SCHEMA_TYPE_FIELD, mSchemaType); |
| bundle.putParcelableArrayList(AppSearchSchema.PROPERTIES_FIELD, mPropertyBundles); |
| mBuilt = true; |
| return new AppSearchSchema(bundle); |
| } |
| } |
| |
| /** |
| * Configuration for a single property (field) of a document type. |
| * |
| * <p>For example, an {@code EmailMessage} would be a type and the {@code subject} would be |
| * a property. |
| */ |
| public static final class PropertyConfig { |
| private static final String NAME_FIELD = "name"; |
| private static final String DATA_TYPE_FIELD = "dataType"; |
| private static final String SCHEMA_TYPE_FIELD = "schemaType"; |
| private static final String CARDINALITY_FIELD = "cardinality"; |
| private static final String INDEXING_TYPE_FIELD = "indexingType"; |
| private static final String TOKENIZER_TYPE_FIELD = "tokenizerType"; |
| |
| /** |
| * 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_INT64, |
| DATA_TYPE_DOUBLE, |
| DATA_TYPE_BOOLEAN, |
| DATA_TYPE_BYTES, |
| DATA_TYPE_DOCUMENT, |
| }) |
| @Retention(RetentionPolicy.SOURCE) |
| public @interface DataType {} |
| |
| public static final int DATA_TYPE_STRING = 1; |
| public static final int DATA_TYPE_INT64 = 2; |
| public static final int DATA_TYPE_DOUBLE = 3; |
| public static final int DATA_TYPE_BOOLEAN = 4; |
| |
| /** Unstructured BLOB. */ |
| 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}. |
| */ |
| 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; |
| |
| /** |
| * 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. |
| * |
| * <p>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). None of the properties inside the nested property will be indexed regardless of |
| * the value of {@code indexingType} for the nested properties. |
| */ |
| 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. |
| * |
| * <p>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. |
| * |
| * <p>Ex. A property with "fool" <b>should</b> 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 {} |
| |
| /** |
| * It is only valid for tokenizer_type to be 'NONE' if the data type is |
| * {@link PropertyConfig#DATA_TYPE_DOCUMENT}. |
| */ |
| public static final int TOKENIZER_TYPE_NONE = 0; |
| |
| /** Tokenization for plain text. */ |
| public static final int TOKENIZER_TYPE_PLAIN = 1; |
| |
| final Bundle mBundle; |
| |
| @Nullable |
| private Integer mHashCode; |
| |
| PropertyConfig(@NonNull Bundle bundle) { |
| mBundle = Preconditions.checkNotNull(bundle); |
| } |
| |
| @Override |
| public String toString() { |
| return mBundle.toString(); |
| } |
| |
| /** 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). */ |
| public @DataType int getDataType() { |
| return mBundle.getInt(DATA_TYPE_FIELD, -1); |
| } |
| |
| /** |
| * Returns the logical schema-type of the contents of this property. |
| * |
| * <p>Only set when {@link #getDataType} is set to {@link #DATA_TYPE_DOCUMENT}. |
| * Otherwise, it is {@code null}. |
| */ |
| @Nullable |
| public String getSchemaType() { |
| return mBundle.getString(SCHEMA_TYPE_FIELD); |
| } |
| |
| /** |
| * Returns the cardinality of the property (whether it is optional, required or repeated). |
| */ |
| public @Cardinality int getCardinality() { |
| return mBundle.getInt(CARDINALITY_FIELD, -1); |
| } |
| |
| /** 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); |
| } |
| |
| @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; |
| } |
| |
| /** |
| * Builder for {@link PropertyConfig}. |
| * |
| * <p>The following properties must be set, or {@link PropertyConfig} construction will |
| * fail: |
| * <ul> |
| * <li>dataType |
| * <li>cardinality |
| * </ul> |
| * |
| * <p>In addition, if {@code schemaType} is {@link #DATA_TYPE_DOCUMENT}, {@code schemaType} |
| * is also required. |
| */ |
| public static final class Builder { |
| private final Bundle mBundle = new Bundle(); |
| private boolean mBuilt = false; |
| |
| /** Creates a new {@link PropertyConfig.Builder}. */ |
| public Builder(@NonNull String propertyName) { |
| mBundle.putString(NAME_FIELD, propertyName); |
| } |
| |
| /** |
| * Type of data the property contains (e.g. string, int, bytes, etc). |
| * |
| * <p>This property must be set. |
| */ |
| @NonNull |
| public PropertyConfig.Builder setDataType(@DataType int dataType) { |
| Preconditions.checkState(!mBuilt, "Builder has already been used"); |
| Preconditions.checkArgumentInRange( |
| dataType, DATA_TYPE_STRING, DATA_TYPE_DOCUMENT, "dataType"); |
| mBundle.putInt(DATA_TYPE_FIELD, dataType); |
| return this; |
| } |
| |
| /** |
| * The logical schema-type of the contents of this property. |
| * |
| * <p>Only required when {@link #setDataType} is set to |
| * {@link #DATA_TYPE_DOCUMENT}. Otherwise, it is ignored. |
| */ |
| @NonNull |
| public PropertyConfig.Builder setSchemaType(@NonNull String schemaType) { |
| Preconditions.checkState(!mBuilt, "Builder has already been used"); |
| Preconditions.checkNotNull(schemaType); |
| mBundle.putString(SCHEMA_TYPE_FIELD, schemaType); |
| return this; |
| } |
| |
| /** |
| * The cardinality of the property (whether it is optional, required or repeated). |
| * |
| * <p>This property must be set. |
| */ |
| @NonNull |
| public PropertyConfig.Builder setCardinality(@Cardinality int cardinality) { |
| Preconditions.checkState(!mBuilt, "Builder has already been used"); |
| Preconditions.checkArgumentInRange( |
| cardinality, CARDINALITY_REPEATED, CARDINALITY_REQUIRED, "cardinality"); |
| mBundle.putInt(CARDINALITY_FIELD, cardinality); |
| 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) { |
| Preconditions.checkState(!mBuilt, "Builder has already been used"); |
| Preconditions.checkArgumentInRange( |
| indexingType, INDEXING_TYPE_NONE, INDEXING_TYPE_PREFIXES, "indexingType"); |
| mBundle.putInt(INDEXING_TYPE_FIELD, indexingType); |
| return this; |
| } |
| |
| /** Configures how this property should be tokenized (split into words). */ |
| @NonNull |
| public PropertyConfig.Builder setTokenizerType(@TokenizerType int tokenizerType) { |
| Preconditions.checkState(!mBuilt, "Builder has already been used"); |
| Preconditions.checkArgumentInRange( |
| tokenizerType, TOKENIZER_TYPE_NONE, TOKENIZER_TYPE_PLAIN, "tokenizerType"); |
| mBundle.putInt(TOKENIZER_TYPE_FIELD, tokenizerType); |
| return this; |
| } |
| |
| /** |
| * Constructs a new {@link PropertyConfig} from the contents of this builder. |
| * |
| * <p>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() { |
| Preconditions.checkState(!mBuilt, "Builder has already been used"); |
| // TODO(b/147692920): Send the schema to Icing Lib for official validation, instead |
| // of partially reimplementing some of the validation Icing does here. |
| if (!mBundle.containsKey(DATA_TYPE_FIELD)) { |
| throw new IllegalSchemaException("Missing field: dataType"); |
| } |
| if (mBundle.getString(SCHEMA_TYPE_FIELD, "").isEmpty() |
| && mBundle.getInt(DATA_TYPE_FIELD) == DATA_TYPE_DOCUMENT) { |
| throw new IllegalSchemaException( |
| "Missing field: schemaType (required for configs with " |
| + "dataType = DOCUMENT)"); |
| } |
| if (!mBundle.containsKey(CARDINALITY_FIELD)) { |
| throw new IllegalSchemaException("Missing field: cardinality"); |
| } |
| mBuilt = true; |
| return new PropertyConfig(mBundle); |
| } |
| } |
| } |
| } |