• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright 2020 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package android.app.appsearch;
18 
19 import android.annotation.IntDef;
20 import android.annotation.NonNull;
21 import android.annotation.Nullable;
22 import android.app.appsearch.exceptions.IllegalSchemaException;
23 import android.app.appsearch.util.BundleUtil;
24 import android.app.appsearch.util.IndentingStringBuilder;
25 import android.os.Bundle;
26 import android.util.ArraySet;
27 
28 import com.android.internal.util.Preconditions;
29 
30 import java.lang.annotation.Retention;
31 import java.lang.annotation.RetentionPolicy;
32 import java.util.ArrayList;
33 import java.util.Arrays;
34 import java.util.Collections;
35 import java.util.List;
36 import java.util.Objects;
37 import java.util.Set;
38 
39 /**
40  * The AppSearch Schema for a particular type of document.
41  *
42  * <p>For example, an e-mail message or a music recording could be a schema type.
43  *
44  * <p>The schema consists of type information, properties, and config (like tokenization type).
45  *
46  * @see AppSearchSession#setSchema
47  */
48 public final class AppSearchSchema {
49     private static final String SCHEMA_TYPE_FIELD = "schemaType";
50     private static final String PROPERTIES_FIELD = "properties";
51 
52     private final Bundle mBundle;
53 
54     /** @hide */
AppSearchSchema(@onNull Bundle bundle)55     public AppSearchSchema(@NonNull Bundle bundle) {
56         Objects.requireNonNull(bundle);
57         mBundle = bundle;
58     }
59 
60     /**
61      * Returns the {@link Bundle} populated by this builder.
62      *
63      * @hide
64      */
65     @NonNull
getBundle()66     public Bundle getBundle() {
67         return mBundle;
68     }
69 
70     @Override
71     @NonNull
toString()72     public String toString() {
73         IndentingStringBuilder stringBuilder = new IndentingStringBuilder();
74         appendAppSearchSchemaString(stringBuilder);
75         return stringBuilder.toString();
76     }
77 
78     /**
79      * Appends a debugging string for the {@link AppSearchSchema} instance to the given string
80      * builder.
81      *
82      * @param builder the builder to append to.
83      */
appendAppSearchSchemaString(@onNull IndentingStringBuilder builder)84     private void appendAppSearchSchemaString(@NonNull IndentingStringBuilder builder) {
85         Objects.requireNonNull(builder);
86 
87         builder.append("{\n");
88         builder.increaseIndentLevel();
89         builder.append("schemaType: \"").append(getSchemaType()).append("\",\n");
90         builder.append("properties: [\n");
91 
92         AppSearchSchema.PropertyConfig[] sortedProperties =
93                 getProperties().toArray(new AppSearchSchema.PropertyConfig[0]);
94         Arrays.sort(sortedProperties, (o1, o2) -> o1.getName().compareTo(o2.getName()));
95 
96         for (int i = 0; i < sortedProperties.length; i++) {
97             AppSearchSchema.PropertyConfig propertyConfig = sortedProperties[i];
98             builder.increaseIndentLevel();
99             propertyConfig.appendPropertyConfigString(builder);
100             if (i != sortedProperties.length - 1) {
101                 builder.append(",\n");
102             }
103             builder.decreaseIndentLevel();
104         }
105 
106         builder.append("\n");
107         builder.append("]\n");
108         builder.decreaseIndentLevel();
109         builder.append("}");
110     }
111 
112     /** Returns the name of this schema type, e.g. Email. */
113     @NonNull
getSchemaType()114     public String getSchemaType() {
115         return mBundle.getString(SCHEMA_TYPE_FIELD, "");
116     }
117 
118     /**
119      * Returns the list of {@link PropertyConfig}s that are part of this schema.
120      *
121      * <p>This method creates a new list when called.
122      */
123     @NonNull
124     @SuppressWarnings("MixedMutabilityReturnType")
getProperties()125     public List<PropertyConfig> getProperties() {
126         ArrayList<Bundle> propertyBundles =
127                 mBundle.getParcelableArrayList(AppSearchSchema.PROPERTIES_FIELD);
128         if (propertyBundles.isEmpty()) {
129             return Collections.emptyList();
130         }
131         List<PropertyConfig> ret = new ArrayList<>(propertyBundles.size());
132         for (int i = 0; i < propertyBundles.size(); i++) {
133             ret.add(PropertyConfig.fromBundle(propertyBundles.get(i)));
134         }
135         return ret;
136     }
137 
138     @Override
equals(@ullable Object other)139     public boolean equals(@Nullable Object other) {
140         if (this == other) {
141             return true;
142         }
143         if (!(other instanceof AppSearchSchema)) {
144             return false;
145         }
146         AppSearchSchema otherSchema = (AppSearchSchema) other;
147         if (!getSchemaType().equals(otherSchema.getSchemaType())) {
148             return false;
149         }
150         return getProperties().equals(otherSchema.getProperties());
151     }
152 
153     @Override
hashCode()154     public int hashCode() {
155         return Objects.hash(getSchemaType(), getProperties());
156     }
157 
158     /** Builder for {@link AppSearchSchema objects}. */
159     public static final class Builder {
160         private final String mSchemaType;
161         private ArrayList<Bundle> mPropertyBundles = new ArrayList<>();
162         private final Set<String> mPropertyNames = new ArraySet<>();
163         private boolean mBuilt = false;
164 
165         /** Creates a new {@link AppSearchSchema.Builder}. */
Builder(@onNull String schemaType)166         public Builder(@NonNull String schemaType) {
167             Objects.requireNonNull(schemaType);
168             mSchemaType = schemaType;
169         }
170 
171         /** Adds a property to the given type. */
172         @NonNull
addProperty(@onNull PropertyConfig propertyConfig)173         public AppSearchSchema.Builder addProperty(@NonNull PropertyConfig propertyConfig) {
174             Objects.requireNonNull(propertyConfig);
175             resetIfBuilt();
176             String name = propertyConfig.getName();
177             if (!mPropertyNames.add(name)) {
178                 throw new IllegalSchemaException("Property defined more than once: " + name);
179             }
180             mPropertyBundles.add(propertyConfig.mBundle);
181             return this;
182         }
183 
184         /** Constructs a new {@link AppSearchSchema} from the contents of this builder. */
185         @NonNull
build()186         public AppSearchSchema build() {
187             Bundle bundle = new Bundle();
188             bundle.putString(AppSearchSchema.SCHEMA_TYPE_FIELD, mSchemaType);
189             bundle.putParcelableArrayList(AppSearchSchema.PROPERTIES_FIELD, mPropertyBundles);
190             mBuilt = true;
191             return new AppSearchSchema(bundle);
192         }
193 
resetIfBuilt()194         private void resetIfBuilt() {
195             if (mBuilt) {
196                 mPropertyBundles = new ArrayList<>(mPropertyBundles);
197                 mBuilt = false;
198             }
199         }
200     }
201 
202     /**
203      * Common configuration for a single property (field) in a Document.
204      *
205      * <p>For example, an {@code EmailMessage} would be a type and the {@code subject} would be a
206      * property.
207      */
208     public abstract static class PropertyConfig {
209         static final String NAME_FIELD = "name";
210         static final String DATA_TYPE_FIELD = "dataType";
211         static final String CARDINALITY_FIELD = "cardinality";
212 
213         /**
214          * Physical data-types of the contents of the property.
215          *
216          * @hide
217          */
218         // NOTE: The integer values of these constants must match the proto enum constants in
219         // com.google.android.icing.proto.PropertyConfigProto.DataType.Code.
220         @IntDef(
221                 value = {
222                     DATA_TYPE_STRING,
223                     DATA_TYPE_LONG,
224                     DATA_TYPE_DOUBLE,
225                     DATA_TYPE_BOOLEAN,
226                     DATA_TYPE_BYTES,
227                     DATA_TYPE_DOCUMENT,
228                 })
229         @Retention(RetentionPolicy.SOURCE)
230         public @interface DataType {}
231 
232         /** @hide */
233         public static final int DATA_TYPE_STRING = 1;
234 
235         /** @hide */
236         public static final int DATA_TYPE_LONG = 2;
237 
238         /** @hide */
239         public static final int DATA_TYPE_DOUBLE = 3;
240 
241         /** @hide */
242         public static final int DATA_TYPE_BOOLEAN = 4;
243 
244         /**
245          * Unstructured BLOB.
246          *
247          * @hide
248          */
249         public static final int DATA_TYPE_BYTES = 5;
250 
251         /**
252          * Indicates that the property is itself a {@link GenericDocument}, making it part of a
253          * hierarchical schema. Any property using this DataType MUST have a valid {@link
254          * PropertyConfig#getSchemaType}.
255          *
256          * @hide
257          */
258         public static final int DATA_TYPE_DOCUMENT = 6;
259 
260         /**
261          * The cardinality of the property (whether it is required, optional or repeated).
262          *
263          * @hide
264          */
265         // NOTE: The integer values of these constants must match the proto enum constants in
266         // com.google.android.icing.proto.PropertyConfigProto.Cardinality.Code.
267         @IntDef(
268                 value = {
269                     CARDINALITY_REPEATED,
270                     CARDINALITY_OPTIONAL,
271                     CARDINALITY_REQUIRED,
272                 })
273         @Retention(RetentionPolicy.SOURCE)
274         public @interface Cardinality {}
275 
276         /** Any number of items (including zero) [0...*]. */
277         public static final int CARDINALITY_REPEATED = 1;
278 
279         /** Zero or one value [0,1]. */
280         public static final int CARDINALITY_OPTIONAL = 2;
281 
282         /** Exactly one value [1]. */
283         public static final int CARDINALITY_REQUIRED = 3;
284 
285         final Bundle mBundle;
286 
287         @Nullable private Integer mHashCode;
288 
PropertyConfig(@onNull Bundle bundle)289         PropertyConfig(@NonNull Bundle bundle) {
290             mBundle = Objects.requireNonNull(bundle);
291         }
292 
293         @Override
294         @NonNull
toString()295         public String toString() {
296             IndentingStringBuilder stringBuilder = new IndentingStringBuilder();
297             appendPropertyConfigString(stringBuilder);
298             return stringBuilder.toString();
299         }
300 
301         /**
302          * Appends a debug string for the {@link AppSearchSchema.PropertyConfig} instance to the
303          * given string builder.
304          *
305          * @param builder the builder to append to.
306          */
appendPropertyConfigString(@onNull IndentingStringBuilder builder)307         void appendPropertyConfigString(@NonNull IndentingStringBuilder builder) {
308             Objects.requireNonNull(builder);
309 
310             builder.append("{\n");
311             builder.increaseIndentLevel();
312             builder.append("name: \"").append(getName()).append("\",\n");
313 
314             if (this instanceof AppSearchSchema.StringPropertyConfig) {
315                 ((StringPropertyConfig) this).appendStringPropertyConfigFields(builder);
316             } else if (this instanceof AppSearchSchema.DocumentPropertyConfig) {
317                 ((DocumentPropertyConfig) this).appendDocumentPropertyConfigFields(builder);
318             }
319 
320             switch (getCardinality()) {
321                 case AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED:
322                     builder.append("cardinality: CARDINALITY_REPEATED,\n");
323                     break;
324                 case AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL:
325                     builder.append("cardinality: CARDINALITY_OPTIONAL,\n");
326                     break;
327                 case AppSearchSchema.PropertyConfig.CARDINALITY_REQUIRED:
328                     builder.append("cardinality: CARDINALITY_REQUIRED,\n");
329                     break;
330                 default:
331                     builder.append("cardinality: CARDINALITY_UNKNOWN,\n");
332             }
333 
334             switch (getDataType()) {
335                 case AppSearchSchema.PropertyConfig.DATA_TYPE_STRING:
336                     builder.append("dataType: DATA_TYPE_STRING,\n");
337                     break;
338                 case AppSearchSchema.PropertyConfig.DATA_TYPE_LONG:
339                     builder.append("dataType: DATA_TYPE_LONG,\n");
340                     break;
341                 case AppSearchSchema.PropertyConfig.DATA_TYPE_DOUBLE:
342                     builder.append("dataType: DATA_TYPE_DOUBLE,\n");
343                     break;
344                 case AppSearchSchema.PropertyConfig.DATA_TYPE_BOOLEAN:
345                     builder.append("dataType: DATA_TYPE_BOOLEAN,\n");
346                     break;
347                 case AppSearchSchema.PropertyConfig.DATA_TYPE_BYTES:
348                     builder.append("dataType: DATA_TYPE_BYTES,\n");
349                     break;
350                 case AppSearchSchema.PropertyConfig.DATA_TYPE_DOCUMENT:
351                     builder.append("dataType: DATA_TYPE_DOCUMENT,\n");
352                     break;
353                 default:
354                     builder.append("dataType: DATA_TYPE_UNKNOWN,\n");
355             }
356             builder.decreaseIndentLevel();
357             builder.append("}");
358         }
359 
360         /** Returns the name of this property. */
361         @NonNull
getName()362         public String getName() {
363             return mBundle.getString(NAME_FIELD, "");
364         }
365 
366         /**
367          * Returns the type of data the property contains (e.g. string, int, bytes, etc).
368          *
369          * @hide
370          */
getDataType()371         public @DataType int getDataType() {
372             return mBundle.getInt(DATA_TYPE_FIELD, -1);
373         }
374 
375         /**
376          * Returns the cardinality of the property (whether it is optional, required or repeated).
377          */
getCardinality()378         public @Cardinality int getCardinality() {
379             return mBundle.getInt(CARDINALITY_FIELD, CARDINALITY_OPTIONAL);
380         }
381 
382         @Override
equals(@ullable Object other)383         public boolean equals(@Nullable Object other) {
384             if (this == other) {
385                 return true;
386             }
387             if (!(other instanceof PropertyConfig)) {
388                 return false;
389             }
390             PropertyConfig otherProperty = (PropertyConfig) other;
391             return BundleUtil.deepEquals(this.mBundle, otherProperty.mBundle);
392         }
393 
394         @Override
hashCode()395         public int hashCode() {
396             if (mHashCode == null) {
397                 mHashCode = BundleUtil.deepHashCode(mBundle);
398             }
399             return mHashCode;
400         }
401 
402         /**
403          * Converts a {@link Bundle} into a {@link PropertyConfig} depending on its internal data
404          * type.
405          *
406          * <p>The bundle is not cloned.
407          *
408          * @throws IllegalArgumentException if the bundle does no contain a recognized value in its
409          *     {@code DATA_TYPE_FIELD}.
410          * @hide
411          */
412         @NonNull
fromBundle(@onNull Bundle propertyBundle)413         public static PropertyConfig fromBundle(@NonNull Bundle propertyBundle) {
414             switch (propertyBundle.getInt(PropertyConfig.DATA_TYPE_FIELD)) {
415                 case PropertyConfig.DATA_TYPE_STRING:
416                     return new StringPropertyConfig(propertyBundle);
417                 case PropertyConfig.DATA_TYPE_LONG:
418                     return new LongPropertyConfig(propertyBundle);
419                 case PropertyConfig.DATA_TYPE_DOUBLE:
420                     return new DoublePropertyConfig(propertyBundle);
421                 case PropertyConfig.DATA_TYPE_BOOLEAN:
422                     return new BooleanPropertyConfig(propertyBundle);
423                 case PropertyConfig.DATA_TYPE_BYTES:
424                     return new BytesPropertyConfig(propertyBundle);
425                 case PropertyConfig.DATA_TYPE_DOCUMENT:
426                     return new DocumentPropertyConfig(propertyBundle);
427                 default:
428                     throw new IllegalArgumentException(
429                             "Unsupported property bundle of type "
430                                     + propertyBundle.getInt(PropertyConfig.DATA_TYPE_FIELD)
431                                     + "; contents: "
432                                     + propertyBundle);
433             }
434         }
435     }
436 
437     /** Configuration for a property of type String in a Document. */
438     public static final class StringPropertyConfig extends PropertyConfig {
439         private static final String INDEXING_TYPE_FIELD = "indexingType";
440         private static final String TOKENIZER_TYPE_FIELD = "tokenizerType";
441 
442         /**
443          * Encapsulates the configurations on how AppSearch should query/index these terms.
444          *
445          * @hide
446          */
447         @IntDef(
448                 value = {
449                     INDEXING_TYPE_NONE,
450                     INDEXING_TYPE_EXACT_TERMS,
451                     INDEXING_TYPE_PREFIXES,
452                 })
453         @Retention(RetentionPolicy.SOURCE)
454         public @interface IndexingType {}
455 
456         /** Content in this property will not be tokenized or indexed. */
457         public static final int INDEXING_TYPE_NONE = 0;
458 
459         /**
460          * Content in this property should only be returned for queries matching the exact tokens
461          * appearing in this property.
462          *
463          * <p>Ex. A property with "fool" should NOT match a query for "foo".
464          */
465         public static final int INDEXING_TYPE_EXACT_TERMS = 1;
466 
467         /**
468          * Content in this property should be returned for queries that are either exact matches or
469          * query matches of the tokens appearing in this property.
470          *
471          * <p>Ex. A property with "fool" <b>should</b> match a query for "foo".
472          */
473         public static final int INDEXING_TYPE_PREFIXES = 2;
474 
475         /**
476          * Configures how tokens should be extracted from this property.
477          *
478          * @hide
479          */
480         // NOTE: The integer values of these constants must match the proto enum constants in
481         // com.google.android.icing.proto.IndexingConfig.TokenizerType.Code.
482         @IntDef(
483                 value = {
484                     TOKENIZER_TYPE_NONE,
485                     TOKENIZER_TYPE_PLAIN,
486                 })
487         @Retention(RetentionPolicy.SOURCE)
488         public @interface TokenizerType {}
489 
490         /**
491          * This value indicates that no tokens should be extracted from this property.
492          *
493          * <p>It is only valid for tokenizer_type to be 'NONE' if {@link #getIndexingType} is {@link
494          * #INDEXING_TYPE_NONE}.
495          */
496         public static final int TOKENIZER_TYPE_NONE = 0;
497 
498         /**
499          * Tokenization for plain text. This value indicates that tokens should be extracted from
500          * this property based on word breaks. Segments of whitespace and punctuation are not
501          * considered tokens.
502          *
503          * <p>Ex. A property with "foo bar. baz." will produce tokens for "foo", "bar" and "baz".
504          * The segments " " and "." will not be considered tokens.
505          *
506          * <p>It is only valid for tokenizer_type to be 'PLAIN' if {@link #getIndexingType} is
507          * {@link #INDEXING_TYPE_EXACT_TERMS} or {@link #INDEXING_TYPE_PREFIXES}.
508          */
509         public static final int TOKENIZER_TYPE_PLAIN = 1;
510 
StringPropertyConfig(@onNull Bundle bundle)511         StringPropertyConfig(@NonNull Bundle bundle) {
512             super(bundle);
513         }
514 
515         /** Returns how the property is indexed. */
getIndexingType()516         public @IndexingType int getIndexingType() {
517             return mBundle.getInt(INDEXING_TYPE_FIELD);
518         }
519 
520         /** Returns how this property is tokenized (split into words). */
getTokenizerType()521         public @TokenizerType int getTokenizerType() {
522             return mBundle.getInt(TOKENIZER_TYPE_FIELD);
523         }
524 
525         /** Builder for {@link StringPropertyConfig}. */
526         public static final class Builder {
527             private final String mPropertyName;
528             private @Cardinality int mCardinality = CARDINALITY_OPTIONAL;
529             private @IndexingType int mIndexingType = INDEXING_TYPE_NONE;
530             private @TokenizerType int mTokenizerType = TOKENIZER_TYPE_NONE;
531 
532             /** Creates a new {@link StringPropertyConfig.Builder}. */
Builder(@onNull String propertyName)533             public Builder(@NonNull String propertyName) {
534                 mPropertyName = Objects.requireNonNull(propertyName);
535             }
536 
537             /**
538              * The cardinality of the property (whether it is optional, required or repeated).
539              *
540              * <p>If this method is not called, the default cardinality is {@link
541              * PropertyConfig#CARDINALITY_OPTIONAL}.
542              */
543             @SuppressWarnings("MissingGetterMatchingBuilder") // getter defined in superclass
544             @NonNull
setCardinality(@ardinality int cardinality)545             public StringPropertyConfig.Builder setCardinality(@Cardinality int cardinality) {
546                 Preconditions.checkArgumentInRange(
547                         cardinality, CARDINALITY_REPEATED, CARDINALITY_REQUIRED, "cardinality");
548                 mCardinality = cardinality;
549                 return this;
550             }
551 
552             /**
553              * Configures how a property should be indexed so that it can be retrieved by queries.
554              *
555              * <p>If this method is not called, the default indexing type is {@link
556              * StringPropertyConfig#INDEXING_TYPE_NONE}, so that it cannot be matched by queries.
557              */
558             @NonNull
setIndexingType(@ndexingType int indexingType)559             public StringPropertyConfig.Builder setIndexingType(@IndexingType int indexingType) {
560                 Preconditions.checkArgumentInRange(
561                         indexingType, INDEXING_TYPE_NONE, INDEXING_TYPE_PREFIXES, "indexingType");
562                 mIndexingType = indexingType;
563                 return this;
564             }
565 
566             /**
567              * Configures how this property should be tokenized (split into words).
568              *
569              * <p>If this method is not called, the default indexing type is {@link
570              * StringPropertyConfig#TOKENIZER_TYPE_NONE}, so that it is not tokenized.
571              *
572              * <p>This method must be called with a value other than {@link
573              * StringPropertyConfig#TOKENIZER_TYPE_NONE} if the property is indexed (i.e. if {@link
574              * #setIndexingType} has been called with a value other than {@link
575              * StringPropertyConfig#INDEXING_TYPE_NONE}).
576              */
577             @NonNull
setTokenizerType(@okenizerType int tokenizerType)578             public StringPropertyConfig.Builder setTokenizerType(@TokenizerType int tokenizerType) {
579                 Preconditions.checkArgumentInRange(
580                         tokenizerType, TOKENIZER_TYPE_NONE, TOKENIZER_TYPE_PLAIN, "tokenizerType");
581                 mTokenizerType = tokenizerType;
582                 return this;
583             }
584 
585             /** Constructs a new {@link StringPropertyConfig} from the contents of this builder. */
586             @NonNull
build()587             public StringPropertyConfig build() {
588                 if (mTokenizerType == TOKENIZER_TYPE_NONE) {
589                     Preconditions.checkState(
590                             mIndexingType == INDEXING_TYPE_NONE,
591                             "Cannot set "
592                                     + "TOKENIZER_TYPE_NONE with an indexing type other than "
593                                     + "INDEXING_TYPE_NONE.");
594                 } else {
595                     Preconditions.checkState(
596                             mIndexingType != INDEXING_TYPE_NONE,
597                             "Cannot set " + "TOKENIZER_TYPE_PLAIN  with INDEXING_TYPE_NONE.");
598                 }
599                 Bundle bundle = new Bundle();
600                 bundle.putString(NAME_FIELD, mPropertyName);
601                 bundle.putInt(DATA_TYPE_FIELD, DATA_TYPE_STRING);
602                 bundle.putInt(CARDINALITY_FIELD, mCardinality);
603                 bundle.putInt(INDEXING_TYPE_FIELD, mIndexingType);
604                 bundle.putInt(TOKENIZER_TYPE_FIELD, mTokenizerType);
605                 return new StringPropertyConfig(bundle);
606             }
607         }
608 
609         /**
610          * Appends a debug string for the {@link StringPropertyConfig} instance to the given string
611          * builder.
612          *
613          * <p>This appends fields specific to a {@link StringPropertyConfig} instance.
614          *
615          * @param builder the builder to append to.
616          */
appendStringPropertyConfigFields(@onNull IndentingStringBuilder builder)617         void appendStringPropertyConfigFields(@NonNull IndentingStringBuilder builder) {
618             switch (getIndexingType()) {
619                 case AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_NONE:
620                     builder.append("indexingType: INDEXING_TYPE_NONE,\n");
621                     break;
622                 case AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS:
623                     builder.append("indexingType: INDEXING_TYPE_EXACT_TERMS,\n");
624                     break;
625                 case AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_PREFIXES:
626                     builder.append("indexingType: INDEXING_TYPE_PREFIXES,\n");
627                     break;
628                 default:
629                     builder.append("indexingType: INDEXING_TYPE_UNKNOWN,\n");
630             }
631 
632             switch (getTokenizerType()) {
633                 case AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_NONE:
634                     builder.append("tokenizerType: TOKENIZER_TYPE_NONE,\n");
635                     break;
636                 case AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_PLAIN:
637                     builder.append("tokenizerType: TOKENIZER_TYPE_PLAIN,\n");
638                     break;
639                 default:
640                     builder.append("tokenizerType: TOKENIZER_TYPE_UNKNOWN,\n");
641             }
642         }
643     }
644 
645     /** Configuration for a property containing a 64-bit integer. */
646     public static final class LongPropertyConfig extends PropertyConfig {
LongPropertyConfig(@onNull Bundle bundle)647         LongPropertyConfig(@NonNull Bundle bundle) {
648             super(bundle);
649         }
650 
651         /** Builder for {@link LongPropertyConfig}. */
652         public static final class Builder {
653             private final String mPropertyName;
654             private @Cardinality int mCardinality = CARDINALITY_OPTIONAL;
655 
656             /** Creates a new {@link LongPropertyConfig.Builder}. */
Builder(@onNull String propertyName)657             public Builder(@NonNull String propertyName) {
658                 mPropertyName = Objects.requireNonNull(propertyName);
659             }
660 
661             /**
662              * The cardinality of the property (whether it is optional, required or repeated).
663              *
664              * <p>If this method is not called, the default cardinality is {@link
665              * PropertyConfig#CARDINALITY_OPTIONAL}.
666              */
667             @SuppressWarnings("MissingGetterMatchingBuilder") // getter defined in superclass
668             @NonNull
setCardinality(@ardinality int cardinality)669             public LongPropertyConfig.Builder setCardinality(@Cardinality int cardinality) {
670                 Preconditions.checkArgumentInRange(
671                         cardinality, CARDINALITY_REPEATED, CARDINALITY_REQUIRED, "cardinality");
672                 mCardinality = cardinality;
673                 return this;
674             }
675 
676             /** Constructs a new {@link LongPropertyConfig} from the contents of this builder. */
677             @NonNull
build()678             public LongPropertyConfig build() {
679                 Bundle bundle = new Bundle();
680                 bundle.putString(NAME_FIELD, mPropertyName);
681                 bundle.putInt(DATA_TYPE_FIELD, DATA_TYPE_LONG);
682                 bundle.putInt(CARDINALITY_FIELD, mCardinality);
683                 return new LongPropertyConfig(bundle);
684             }
685         }
686     }
687 
688     /** Configuration for a property containing a double-precision decimal number. */
689     public static final class DoublePropertyConfig extends PropertyConfig {
DoublePropertyConfig(@onNull Bundle bundle)690         DoublePropertyConfig(@NonNull Bundle bundle) {
691             super(bundle);
692         }
693 
694         /** Builder for {@link DoublePropertyConfig}. */
695         public static final class Builder {
696             private final String mPropertyName;
697             private @Cardinality int mCardinality = CARDINALITY_OPTIONAL;
698 
699             /** Creates a new {@link DoublePropertyConfig.Builder}. */
Builder(@onNull String propertyName)700             public Builder(@NonNull String propertyName) {
701                 mPropertyName = Objects.requireNonNull(propertyName);
702             }
703 
704             /**
705              * The cardinality of the property (whether it is optional, required or repeated).
706              *
707              * <p>If this method is not called, the default cardinality is {@link
708              * PropertyConfig#CARDINALITY_OPTIONAL}.
709              */
710             @SuppressWarnings("MissingGetterMatchingBuilder") // getter defined in superclass
711             @NonNull
setCardinality(@ardinality int cardinality)712             public DoublePropertyConfig.Builder setCardinality(@Cardinality int cardinality) {
713                 Preconditions.checkArgumentInRange(
714                         cardinality, CARDINALITY_REPEATED, CARDINALITY_REQUIRED, "cardinality");
715                 mCardinality = cardinality;
716                 return this;
717             }
718 
719             /** Constructs a new {@link DoublePropertyConfig} from the contents of this builder. */
720             @NonNull
build()721             public DoublePropertyConfig build() {
722                 Bundle bundle = new Bundle();
723                 bundle.putString(NAME_FIELD, mPropertyName);
724                 bundle.putInt(DATA_TYPE_FIELD, DATA_TYPE_DOUBLE);
725                 bundle.putInt(CARDINALITY_FIELD, mCardinality);
726                 return new DoublePropertyConfig(bundle);
727             }
728         }
729     }
730 
731     /** Configuration for a property containing a boolean. */
732     public static final class BooleanPropertyConfig extends PropertyConfig {
BooleanPropertyConfig(@onNull Bundle bundle)733         BooleanPropertyConfig(@NonNull Bundle bundle) {
734             super(bundle);
735         }
736 
737         /** Builder for {@link BooleanPropertyConfig}. */
738         public static final class Builder {
739             private final String mPropertyName;
740             private @Cardinality int mCardinality = CARDINALITY_OPTIONAL;
741 
742             /** Creates a new {@link BooleanPropertyConfig.Builder}. */
Builder(@onNull String propertyName)743             public Builder(@NonNull String propertyName) {
744                 mPropertyName = Objects.requireNonNull(propertyName);
745             }
746 
747             /**
748              * The cardinality of the property (whether it is optional, required or repeated).
749              *
750              * <p>If this method is not called, the default cardinality is {@link
751              * PropertyConfig#CARDINALITY_OPTIONAL}.
752              */
753             @SuppressWarnings("MissingGetterMatchingBuilder") // getter defined in superclass
754             @NonNull
setCardinality(@ardinality int cardinality)755             public BooleanPropertyConfig.Builder setCardinality(@Cardinality int cardinality) {
756                 Preconditions.checkArgumentInRange(
757                         cardinality, CARDINALITY_REPEATED, CARDINALITY_REQUIRED, "cardinality");
758                 mCardinality = cardinality;
759                 return this;
760             }
761 
762             /** Constructs a new {@link BooleanPropertyConfig} from the contents of this builder. */
763             @NonNull
build()764             public BooleanPropertyConfig build() {
765                 Bundle bundle = new Bundle();
766                 bundle.putString(NAME_FIELD, mPropertyName);
767                 bundle.putInt(DATA_TYPE_FIELD, DATA_TYPE_BOOLEAN);
768                 bundle.putInt(CARDINALITY_FIELD, mCardinality);
769                 return new BooleanPropertyConfig(bundle);
770             }
771         }
772     }
773 
774     /** Configuration for a property containing a byte array. */
775     public static final class BytesPropertyConfig extends PropertyConfig {
BytesPropertyConfig(@onNull Bundle bundle)776         BytesPropertyConfig(@NonNull Bundle bundle) {
777             super(bundle);
778         }
779 
780         /** Builder for {@link BytesPropertyConfig}. */
781         public static final class Builder {
782             private final String mPropertyName;
783             private @Cardinality int mCardinality = CARDINALITY_OPTIONAL;
784 
785             /** Creates a new {@link BytesPropertyConfig.Builder}. */
Builder(@onNull String propertyName)786             public Builder(@NonNull String propertyName) {
787                 mPropertyName = Objects.requireNonNull(propertyName);
788             }
789 
790             /**
791              * The cardinality of the property (whether it is optional, required or repeated).
792              *
793              * <p>If this method is not called, the default cardinality is {@link
794              * PropertyConfig#CARDINALITY_OPTIONAL}.
795              */
796             @SuppressWarnings("MissingGetterMatchingBuilder") // getter defined in superclass
797             @NonNull
setCardinality(@ardinality int cardinality)798             public BytesPropertyConfig.Builder setCardinality(@Cardinality int cardinality) {
799                 Preconditions.checkArgumentInRange(
800                         cardinality, CARDINALITY_REPEATED, CARDINALITY_REQUIRED, "cardinality");
801                 mCardinality = cardinality;
802                 return this;
803             }
804 
805             /** Constructs a new {@link BytesPropertyConfig} from the contents of this builder. */
806             @NonNull
build()807             public BytesPropertyConfig build() {
808                 Bundle bundle = new Bundle();
809                 bundle.putString(NAME_FIELD, mPropertyName);
810                 bundle.putInt(DATA_TYPE_FIELD, DATA_TYPE_BYTES);
811                 bundle.putInt(CARDINALITY_FIELD, mCardinality);
812                 return new BytesPropertyConfig(bundle);
813             }
814         }
815     }
816 
817     /** Configuration for a property containing another Document. */
818     public static final class DocumentPropertyConfig extends PropertyConfig {
819         private static final String SCHEMA_TYPE_FIELD = "schemaType";
820         private static final String INDEX_NESTED_PROPERTIES_FIELD = "indexNestedProperties";
821 
DocumentPropertyConfig(@onNull Bundle bundle)822         DocumentPropertyConfig(@NonNull Bundle bundle) {
823             super(bundle);
824         }
825 
826         /** Returns the logical schema-type of the contents of this document property. */
827         @NonNull
getSchemaType()828         public String getSchemaType() {
829             return Objects.requireNonNull(mBundle.getString(SCHEMA_TYPE_FIELD));
830         }
831 
832         /**
833          * Returns whether fields in the nested document should be indexed according to that
834          * document's schema.
835          *
836          * <p>If false, the nested document's properties are not indexed regardless of its own
837          * schema.
838          */
shouldIndexNestedProperties()839         public boolean shouldIndexNestedProperties() {
840             return mBundle.getBoolean(INDEX_NESTED_PROPERTIES_FIELD);
841         }
842 
843         /** Builder for {@link DocumentPropertyConfig}. */
844         public static final class Builder {
845             private final String mPropertyName;
846             private final String mSchemaType;
847             private @Cardinality int mCardinality = CARDINALITY_OPTIONAL;
848             private boolean mShouldIndexNestedProperties = false;
849 
850             /**
851              * Creates a new {@link DocumentPropertyConfig.Builder}.
852              *
853              * @param propertyName The logical name of the property in the schema, which will be
854              *     used as the key for this property in {@link
855              *     GenericDocument.Builder#setPropertyDocument}.
856              * @param schemaType The type of documents which will be stored in this property.
857              *     Documents of different types cannot be mixed into a single property.
858              */
Builder(@onNull String propertyName, @NonNull String schemaType)859             public Builder(@NonNull String propertyName, @NonNull String schemaType) {
860                 mPropertyName = Objects.requireNonNull(propertyName);
861                 mSchemaType = Objects.requireNonNull(schemaType);
862             }
863 
864             /**
865              * The cardinality of the property (whether it is optional, required or repeated).
866              *
867              * <p>If this method is not called, the default cardinality is {@link
868              * PropertyConfig#CARDINALITY_OPTIONAL}.
869              */
870             @SuppressWarnings("MissingGetterMatchingBuilder") // getter defined in superclass
871             @NonNull
setCardinality(@ardinality int cardinality)872             public DocumentPropertyConfig.Builder setCardinality(@Cardinality int cardinality) {
873                 Preconditions.checkArgumentInRange(
874                         cardinality, CARDINALITY_REPEATED, CARDINALITY_REQUIRED, "cardinality");
875                 mCardinality = cardinality;
876                 return this;
877             }
878 
879             /**
880              * Configures whether fields in the nested document should be indexed according to that
881              * document's schema.
882              *
883              * <p>If false, the nested document's properties are not indexed regardless of its own
884              * schema.
885              */
886             @NonNull
setShouldIndexNestedProperties( boolean indexNestedProperties)887             public DocumentPropertyConfig.Builder setShouldIndexNestedProperties(
888                     boolean indexNestedProperties) {
889                 mShouldIndexNestedProperties = indexNestedProperties;
890                 return this;
891             }
892 
893             /** Constructs a new {@link PropertyConfig} from the contents of this builder. */
894             @NonNull
build()895             public DocumentPropertyConfig build() {
896                 Bundle bundle = new Bundle();
897                 bundle.putString(NAME_FIELD, mPropertyName);
898                 bundle.putInt(DATA_TYPE_FIELD, DATA_TYPE_DOCUMENT);
899                 bundle.putInt(CARDINALITY_FIELD, mCardinality);
900                 bundle.putBoolean(INDEX_NESTED_PROPERTIES_FIELD, mShouldIndexNestedProperties);
901                 bundle.putString(SCHEMA_TYPE_FIELD, mSchemaType);
902                 return new DocumentPropertyConfig(bundle);
903             }
904         }
905 
906         /**
907          * Appends a debug string for the {@link DocumentPropertyConfig} instance to the given
908          * string builder.
909          *
910          * <p>This appends fields specific to a {@link DocumentPropertyConfig} instance.
911          *
912          * @param builder the builder to append to.
913          */
appendDocumentPropertyConfigFields(@onNull IndentingStringBuilder builder)914         void appendDocumentPropertyConfigFields(@NonNull IndentingStringBuilder builder) {
915             builder.append("shouldIndexNestedProperties: ")
916                     .append(shouldIndexNestedProperties())
917                     .append(",\n");
918 
919             builder.append("schemaType: \"").append(getSchemaType()).append("\",\n");
920         }
921     }
922 }
923