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.annotation.CanIgnoreReturnValue; 23 import android.app.appsearch.exceptions.IllegalSchemaException; 24 import android.app.appsearch.util.BundleUtil; 25 import android.app.appsearch.util.IndentingStringBuilder; 26 import android.os.Bundle; 27 import android.util.ArraySet; 28 29 import com.android.internal.util.Preconditions; 30 31 import java.lang.annotation.Retention; 32 import java.lang.annotation.RetentionPolicy; 33 import java.util.ArrayList; 34 import java.util.Arrays; 35 import java.util.Collections; 36 import java.util.List; 37 import java.util.Objects; 38 import java.util.Set; 39 40 /** 41 * The AppSearch Schema for a particular type of document. 42 * 43 * <p>For example, an e-mail message or a music recording could be a schema type. 44 * 45 * <p>The schema consists of type information, properties, and config (like tokenization type). 46 * 47 * @see AppSearchSession#setSchema 48 */ 49 public final class AppSearchSchema { 50 private static final String SCHEMA_TYPE_FIELD = "schemaType"; 51 private static final String PROPERTIES_FIELD = "properties"; 52 53 private final Bundle mBundle; 54 55 /** @hide */ AppSearchSchema(@onNull Bundle bundle)56 public AppSearchSchema(@NonNull Bundle bundle) { 57 Objects.requireNonNull(bundle); 58 mBundle = bundle; 59 } 60 61 /** 62 * Returns the {@link Bundle} populated by this builder. 63 * 64 * @hide 65 */ 66 @NonNull getBundle()67 public Bundle getBundle() { 68 return mBundle; 69 } 70 71 @Override 72 @NonNull toString()73 public String toString() { 74 IndentingStringBuilder stringBuilder = new IndentingStringBuilder(); 75 appendAppSearchSchemaString(stringBuilder); 76 return stringBuilder.toString(); 77 } 78 79 /** 80 * Appends a debugging string for the {@link AppSearchSchema} instance to the given string 81 * builder. 82 * 83 * @param builder the builder to append to. 84 */ appendAppSearchSchemaString(@onNull IndentingStringBuilder builder)85 private void appendAppSearchSchemaString(@NonNull IndentingStringBuilder builder) { 86 Objects.requireNonNull(builder); 87 88 builder.append("{\n"); 89 builder.increaseIndentLevel(); 90 builder.append("schemaType: \"").append(getSchemaType()).append("\",\n"); 91 builder.append("properties: [\n"); 92 93 AppSearchSchema.PropertyConfig[] sortedProperties = 94 getProperties().toArray(new AppSearchSchema.PropertyConfig[0]); 95 Arrays.sort(sortedProperties, (o1, o2) -> o1.getName().compareTo(o2.getName())); 96 97 for (int i = 0; i < sortedProperties.length; i++) { 98 AppSearchSchema.PropertyConfig propertyConfig = sortedProperties[i]; 99 builder.increaseIndentLevel(); 100 propertyConfig.appendPropertyConfigString(builder); 101 if (i != sortedProperties.length - 1) { 102 builder.append(",\n"); 103 } 104 builder.decreaseIndentLevel(); 105 } 106 107 builder.append("\n"); 108 builder.append("]\n"); 109 builder.decreaseIndentLevel(); 110 builder.append("}"); 111 } 112 113 /** Returns the name of this schema type, such as Email. */ 114 @NonNull getSchemaType()115 public String getSchemaType() { 116 return mBundle.getString(SCHEMA_TYPE_FIELD, ""); 117 } 118 119 /** 120 * Returns the list of {@link PropertyConfig}s that are part of this schema. 121 * 122 * <p>This method creates a new list when called. 123 */ 124 @NonNull 125 @SuppressWarnings({"MixedMutabilityReturnType", "deprecation"}) getProperties()126 public List<PropertyConfig> getProperties() { 127 ArrayList<Bundle> propertyBundles = 128 mBundle.getParcelableArrayList(AppSearchSchema.PROPERTIES_FIELD); 129 if (propertyBundles == null || propertyBundles.isEmpty()) { 130 return Collections.emptyList(); 131 } 132 List<PropertyConfig> ret = new ArrayList<>(propertyBundles.size()); 133 for (int i = 0; i < propertyBundles.size(); i++) { 134 ret.add(PropertyConfig.fromBundle(propertyBundles.get(i))); 135 } 136 return ret; 137 } 138 139 @Override equals(@ullable Object other)140 public boolean equals(@Nullable Object other) { 141 if (this == other) { 142 return true; 143 } 144 if (!(other instanceof AppSearchSchema)) { 145 return false; 146 } 147 AppSearchSchema otherSchema = (AppSearchSchema) other; 148 if (!getSchemaType().equals(otherSchema.getSchemaType())) { 149 return false; 150 } 151 return getProperties().equals(otherSchema.getProperties()); 152 } 153 154 @Override hashCode()155 public int hashCode() { 156 return Objects.hash(getSchemaType(), getProperties()); 157 } 158 159 /** Builder for {@link AppSearchSchema objects}. */ 160 public static final class Builder { 161 private final String mSchemaType; 162 private ArrayList<Bundle> mPropertyBundles = new ArrayList<>(); 163 private final Set<String> mPropertyNames = new ArraySet<>(); 164 private boolean mBuilt = false; 165 166 /** Creates a new {@link AppSearchSchema.Builder}. */ Builder(@onNull String schemaType)167 public Builder(@NonNull String schemaType) { 168 Objects.requireNonNull(schemaType); 169 mSchemaType = schemaType; 170 } 171 172 /** Adds a property to the given type. */ 173 @CanIgnoreReturnValue 174 @NonNull addProperty(@onNull PropertyConfig propertyConfig)175 public AppSearchSchema.Builder addProperty(@NonNull PropertyConfig propertyConfig) { 176 Objects.requireNonNull(propertyConfig); 177 resetIfBuilt(); 178 String name = propertyConfig.getName(); 179 if (!mPropertyNames.add(name)) { 180 throw new IllegalSchemaException("Property defined more than once: " + name); 181 } 182 mPropertyBundles.add(propertyConfig.mBundle); 183 return this; 184 } 185 186 /** Constructs a new {@link AppSearchSchema} from the contents of this builder. */ 187 @NonNull build()188 public AppSearchSchema build() { 189 Bundle bundle = new Bundle(); 190 bundle.putString(AppSearchSchema.SCHEMA_TYPE_FIELD, mSchemaType); 191 bundle.putParcelableArrayList(AppSearchSchema.PROPERTIES_FIELD, mPropertyBundles); 192 mBuilt = true; 193 return new AppSearchSchema(bundle); 194 } 195 resetIfBuilt()196 private void resetIfBuilt() { 197 if (mBuilt) { 198 mPropertyBundles = new ArrayList<>(mPropertyBundles); 199 mBuilt = false; 200 } 201 } 202 } 203 204 /** 205 * Common configuration for a single property (field) in a Document. 206 * 207 * <p>For example, an {@code EmailMessage} would be a type and the {@code subject} would be a 208 * property. 209 */ 210 public abstract static class PropertyConfig { 211 static final String NAME_FIELD = "name"; 212 static final String DATA_TYPE_FIELD = "dataType"; 213 static final String CARDINALITY_FIELD = "cardinality"; 214 215 /** 216 * Physical data-types of the contents of the property. 217 * 218 * <p>NOTE: The integer values of these constants must match the proto enum constants in 219 * com.google.android.icing.proto.PropertyConfigProto.DataType.Code. 220 * 221 * @hide 222 */ 223 @IntDef( 224 value = { 225 DATA_TYPE_STRING, 226 DATA_TYPE_LONG, 227 DATA_TYPE_DOUBLE, 228 DATA_TYPE_BOOLEAN, 229 DATA_TYPE_BYTES, 230 DATA_TYPE_DOCUMENT, 231 }) 232 @Retention(RetentionPolicy.SOURCE) 233 public @interface DataType {} 234 235 /** @hide */ 236 public static final int DATA_TYPE_STRING = 1; 237 238 /** @hide */ 239 public static final int DATA_TYPE_LONG = 2; 240 241 /** @hide */ 242 public static final int DATA_TYPE_DOUBLE = 3; 243 244 /** @hide */ 245 public static final int DATA_TYPE_BOOLEAN = 4; 246 247 /** 248 * Unstructured BLOB. 249 * 250 * @hide 251 */ 252 public static final int DATA_TYPE_BYTES = 5; 253 254 /** 255 * Indicates that the property is itself a {@link GenericDocument}, making it part of a 256 * hierarchical schema. Any property using this DataType MUST have a valid {@link 257 * PropertyConfig#getSchemaType}. 258 * 259 * @hide 260 */ 261 public static final int DATA_TYPE_DOCUMENT = 6; 262 263 /** 264 * The cardinality of the property (whether it is required, optional or repeated). 265 * 266 * <p>NOTE: The integer values of these constants must match the proto enum constants in 267 * com.google.android.icing.proto.PropertyConfigProto.Cardinality.Code. 268 * 269 * @hide 270 */ 271 @IntDef( 272 value = { 273 CARDINALITY_REPEATED, 274 CARDINALITY_OPTIONAL, 275 CARDINALITY_REQUIRED, 276 }) 277 @Retention(RetentionPolicy.SOURCE) 278 public @interface Cardinality {} 279 280 /** Any number of items (including zero) [0...*]. */ 281 public static final int CARDINALITY_REPEATED = 1; 282 283 /** Zero or one value [0,1]. */ 284 public static final int CARDINALITY_OPTIONAL = 2; 285 286 /** Exactly one value [1]. */ 287 public static final int CARDINALITY_REQUIRED = 3; 288 289 final Bundle mBundle; 290 291 @Nullable private Integer mHashCode; 292 PropertyConfig(@onNull Bundle bundle)293 PropertyConfig(@NonNull Bundle bundle) { 294 mBundle = Objects.requireNonNull(bundle); 295 } 296 297 @Override 298 @NonNull toString()299 public String toString() { 300 IndentingStringBuilder stringBuilder = new IndentingStringBuilder(); 301 appendPropertyConfigString(stringBuilder); 302 return stringBuilder.toString(); 303 } 304 305 /** 306 * Appends a debug string for the {@link AppSearchSchema.PropertyConfig} instance to the 307 * given string builder. 308 * 309 * @param builder the builder to append to. 310 */ appendPropertyConfigString(@onNull IndentingStringBuilder builder)311 void appendPropertyConfigString(@NonNull IndentingStringBuilder builder) { 312 Objects.requireNonNull(builder); 313 314 builder.append("{\n"); 315 builder.increaseIndentLevel(); 316 builder.append("name: \"").append(getName()).append("\",\n"); 317 318 if (this instanceof AppSearchSchema.StringPropertyConfig) { 319 ((StringPropertyConfig) this).appendStringPropertyConfigFields(builder); 320 } else if (this instanceof AppSearchSchema.DocumentPropertyConfig) { 321 ((DocumentPropertyConfig) this).appendDocumentPropertyConfigFields(builder); 322 } else if (this instanceof AppSearchSchema.LongPropertyConfig) { 323 ((LongPropertyConfig) this).appendLongPropertyConfigFields(builder); 324 } 325 326 switch (getCardinality()) { 327 case AppSearchSchema.PropertyConfig.CARDINALITY_REPEATED: 328 builder.append("cardinality: CARDINALITY_REPEATED,\n"); 329 break; 330 case AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL: 331 builder.append("cardinality: CARDINALITY_OPTIONAL,\n"); 332 break; 333 case AppSearchSchema.PropertyConfig.CARDINALITY_REQUIRED: 334 builder.append("cardinality: CARDINALITY_REQUIRED,\n"); 335 break; 336 default: 337 builder.append("cardinality: CARDINALITY_UNKNOWN,\n"); 338 } 339 340 switch (getDataType()) { 341 case AppSearchSchema.PropertyConfig.DATA_TYPE_STRING: 342 builder.append("dataType: DATA_TYPE_STRING,\n"); 343 break; 344 case AppSearchSchema.PropertyConfig.DATA_TYPE_LONG: 345 builder.append("dataType: DATA_TYPE_LONG,\n"); 346 break; 347 case AppSearchSchema.PropertyConfig.DATA_TYPE_DOUBLE: 348 builder.append("dataType: DATA_TYPE_DOUBLE,\n"); 349 break; 350 case AppSearchSchema.PropertyConfig.DATA_TYPE_BOOLEAN: 351 builder.append("dataType: DATA_TYPE_BOOLEAN,\n"); 352 break; 353 case AppSearchSchema.PropertyConfig.DATA_TYPE_BYTES: 354 builder.append("dataType: DATA_TYPE_BYTES,\n"); 355 break; 356 case AppSearchSchema.PropertyConfig.DATA_TYPE_DOCUMENT: 357 builder.append("dataType: DATA_TYPE_DOCUMENT,\n"); 358 break; 359 default: 360 builder.append("dataType: DATA_TYPE_UNKNOWN,\n"); 361 } 362 builder.decreaseIndentLevel(); 363 builder.append("}"); 364 } 365 366 /** Returns the name of this property. */ 367 @NonNull getName()368 public String getName() { 369 return mBundle.getString(NAME_FIELD, ""); 370 } 371 372 /** 373 * Returns the type of data the property contains (such as string, int, bytes, etc). 374 * 375 * @hide 376 */ 377 @DataType getDataType()378 public int getDataType() { 379 return mBundle.getInt(DATA_TYPE_FIELD, -1); 380 } 381 382 /** 383 * Returns the cardinality of the property (whether it is optional, required or repeated). 384 */ 385 @Cardinality getCardinality()386 public int getCardinality() { 387 return mBundle.getInt(CARDINALITY_FIELD, CARDINALITY_OPTIONAL); 388 } 389 390 @Override equals(@ullable Object other)391 public boolean equals(@Nullable Object other) { 392 if (this == other) { 393 return true; 394 } 395 if (!(other instanceof PropertyConfig)) { 396 return false; 397 } 398 PropertyConfig otherProperty = (PropertyConfig) other; 399 return BundleUtil.deepEquals(this.mBundle, otherProperty.mBundle); 400 } 401 402 @Override hashCode()403 public int hashCode() { 404 if (mHashCode == null) { 405 mHashCode = BundleUtil.deepHashCode(mBundle); 406 } 407 return mHashCode; 408 } 409 410 /** 411 * Converts a {@link Bundle} into a {@link PropertyConfig} depending on its internal data 412 * type. 413 * 414 * <p>The bundle is not cloned. 415 * 416 * @throws IllegalArgumentException if the bundle does no contain a recognized value in its 417 * {@code DATA_TYPE_FIELD}. 418 * @hide 419 */ 420 @NonNull fromBundle(@onNull Bundle propertyBundle)421 public static PropertyConfig fromBundle(@NonNull Bundle propertyBundle) { 422 switch (propertyBundle.getInt(PropertyConfig.DATA_TYPE_FIELD)) { 423 case PropertyConfig.DATA_TYPE_STRING: 424 return new StringPropertyConfig(propertyBundle); 425 case PropertyConfig.DATA_TYPE_LONG: 426 return new LongPropertyConfig(propertyBundle); 427 case PropertyConfig.DATA_TYPE_DOUBLE: 428 return new DoublePropertyConfig(propertyBundle); 429 case PropertyConfig.DATA_TYPE_BOOLEAN: 430 return new BooleanPropertyConfig(propertyBundle); 431 case PropertyConfig.DATA_TYPE_BYTES: 432 return new BytesPropertyConfig(propertyBundle); 433 case PropertyConfig.DATA_TYPE_DOCUMENT: 434 return new DocumentPropertyConfig(propertyBundle); 435 default: 436 throw new IllegalArgumentException( 437 "Unsupported property bundle of type " 438 + propertyBundle.getInt(PropertyConfig.DATA_TYPE_FIELD) 439 + "; contents: " 440 + propertyBundle); 441 } 442 } 443 } 444 445 /** Configuration for a property of type String in a Document. */ 446 public static final class StringPropertyConfig extends PropertyConfig { 447 private static final String INDEXING_TYPE_FIELD = "indexingType"; 448 private static final String TOKENIZER_TYPE_FIELD = "tokenizerType"; 449 private static final String JOINABLE_VALUE_TYPE_FIELD = "joinableValueType"; 450 private static final String DELETION_PROPAGATION_FIELD = "deletionPropagation"; 451 452 /** 453 * Encapsulates the configurations on how AppSearch should query/index these terms. 454 * 455 * @hide 456 */ 457 @IntDef( 458 value = { 459 INDEXING_TYPE_NONE, 460 INDEXING_TYPE_EXACT_TERMS, 461 INDEXING_TYPE_PREFIXES, 462 }) 463 @Retention(RetentionPolicy.SOURCE) 464 public @interface IndexingType {} 465 466 /** Content in this property will not be tokenized or indexed. */ 467 public static final int INDEXING_TYPE_NONE = 0; 468 469 /** 470 * Content in this property should only be returned for queries matching the exact tokens 471 * appearing in this property. 472 * 473 * <p>For example, a property with "fool" should NOT match a query for "foo". 474 */ 475 public static final int INDEXING_TYPE_EXACT_TERMS = 1; 476 477 /** 478 * Content in this property should be returned for queries that are either exact matches or 479 * query matches of the tokens appearing in this property. 480 * 481 * <p>For example, a property with "fool" <b>should</b> match a query for "foo". 482 */ 483 public static final int INDEXING_TYPE_PREFIXES = 2; 484 485 /** 486 * Configures how tokens should be extracted from this property. 487 * 488 * <p>NOTE: The integer values of these constants must match the proto enum constants in 489 * com.google.android.icing.proto.IndexingConfig.TokenizerType.Code. 490 * 491 * @hide 492 */ 493 @IntDef( 494 value = { 495 TOKENIZER_TYPE_NONE, 496 TOKENIZER_TYPE_PLAIN, 497 TOKENIZER_TYPE_VERBATIM, 498 TOKENIZER_TYPE_RFC822 499 }) 500 @Retention(RetentionPolicy.SOURCE) 501 public @interface TokenizerType {} 502 503 /** 504 * This value indicates that no tokens should be extracted from this property. 505 * 506 * <p>It is only valid for tokenizer_type to be 'NONE' if {@link #getIndexingType} is {@link 507 * #INDEXING_TYPE_NONE}. 508 */ 509 public static final int TOKENIZER_TYPE_NONE = 0; 510 511 /** 512 * Tokenization for plain text. This value indicates that tokens should be extracted from 513 * this property based on word breaks. Segments of whitespace and punctuation are not 514 * considered tokens. 515 * 516 * <p>For example, a property with "foo bar. baz." will produce tokens for "foo", "bar" and 517 * "baz". The segments " " and "." will not be considered tokens. 518 * 519 * <p>It is only valid for tokenizer_type to be 'PLAIN' if {@link #getIndexingType} is 520 * {@link #INDEXING_TYPE_EXACT_TERMS} or {@link #INDEXING_TYPE_PREFIXES}. 521 */ 522 public static final int TOKENIZER_TYPE_PLAIN = 1; 523 524 /** 525 * This value indicates that no normalization or segmentation should be applied to string 526 * values that are tokenized using this type. Therefore, the output token is equivalent to 527 * the raw string value. 528 * 529 * <p>For example, a property with "Hello, world!" will produce the token "Hello, world!", 530 * preserving punctuation and capitalization, and not creating separate tokens between the 531 * space. 532 * 533 * <p>It is only valid for tokenizer_type to be 'VERBATIM' if {@link #getIndexingType} is 534 * {@link #INDEXING_TYPE_EXACT_TERMS} or {@link #INDEXING_TYPE_PREFIXES}. 535 */ 536 public static final int TOKENIZER_TYPE_VERBATIM = 2; 537 538 /** 539 * Tokenization for emails. This value indicates that tokens should be extracted from this 540 * property based on email structure. 541 * 542 * <p>For example, a property with "alex.sav@google.com" will produce tokens for "alex", 543 * "sav", "alex.sav", "google", "com", and "alexsav@google.com" 544 * 545 * <p>It is only valid for tokenizer_type to be 'RFC822' if {@link #getIndexingType} is 546 * {@link #INDEXING_TYPE_EXACT_TERMS} or {@link #INDEXING_TYPE_PREFIXES}. 547 */ 548 public static final int TOKENIZER_TYPE_RFC822 = 3; 549 550 /** 551 * The joinable value type of the property. By setting the appropriate joinable value type 552 * for a property, the client can use the property for joining documents from other schema 553 * types using Search API (see {@link JoinSpec}). 554 * 555 * @hide 556 */ 557 // NOTE: The integer values of these constants must match the proto enum constants in 558 // com.google.android.icing.proto.JoinableConfig.ValueType.Code. 559 @IntDef( 560 value = { 561 JOINABLE_VALUE_TYPE_NONE, 562 JOINABLE_VALUE_TYPE_QUALIFIED_ID, 563 }) 564 @Retention(RetentionPolicy.SOURCE) 565 public @interface JoinableValueType {} 566 567 /** Content in this property is not joinable. */ 568 public static final int JOINABLE_VALUE_TYPE_NONE = 0; 569 570 /** 571 * Content in this string property will be used as a qualified id to join documents. 572 * 573 * <ul> 574 * <li>Qualified id: a unique identifier for a document, and this joinable value type is 575 * similar to primary and foreign key in relational database. See {@link 576 * android.app.appsearch.util.DocumentIdUtil} for more details. 577 * <li>Currently we only support single string joining, so it should only be used with 578 * {@link PropertyConfig#CARDINALITY_OPTIONAL} and {@link 579 * PropertyConfig#CARDINALITY_REQUIRED}. 580 * </ul> 581 */ 582 public static final int JOINABLE_VALUE_TYPE_QUALIFIED_ID = 1; 583 StringPropertyConfig(@onNull Bundle bundle)584 StringPropertyConfig(@NonNull Bundle bundle) { 585 super(bundle); 586 } 587 588 /** Returns how the property is indexed. */ 589 @IndexingType getIndexingType()590 public int getIndexingType() { 591 return mBundle.getInt(INDEXING_TYPE_FIELD); 592 } 593 594 /** Returns how this property is tokenized (split into words). */ 595 @TokenizerType getTokenizerType()596 public int getTokenizerType() { 597 return mBundle.getInt(TOKENIZER_TYPE_FIELD); 598 } 599 600 /** 601 * Returns how this property is going to be used to join documents from other schema types. 602 */ 603 @JoinableValueType getJoinableValueType()604 public int getJoinableValueType() { 605 return mBundle.getInt(JOINABLE_VALUE_TYPE_FIELD, JOINABLE_VALUE_TYPE_NONE); 606 } 607 608 /** 609 * Returns whether or not documents in this schema should be deleted when the document 610 * referenced by this field is deleted. 611 * 612 * @see JoinSpec 613 * @hide 614 */ getDeletionPropagation()615 public boolean getDeletionPropagation() { 616 return mBundle.getBoolean(DELETION_PROPAGATION_FIELD, false); 617 } 618 619 /** Builder for {@link StringPropertyConfig}. */ 620 public static final class Builder { 621 private final String mPropertyName; 622 @Cardinality private int mCardinality = CARDINALITY_OPTIONAL; 623 @IndexingType private int mIndexingType = INDEXING_TYPE_NONE; 624 @TokenizerType private int mTokenizerType = TOKENIZER_TYPE_NONE; 625 @JoinableValueType private int mJoinableValueType = JOINABLE_VALUE_TYPE_NONE; 626 private boolean mDeletionPropagation = false; 627 628 /** Creates a new {@link StringPropertyConfig.Builder}. */ Builder(@onNull String propertyName)629 public Builder(@NonNull String propertyName) { 630 mPropertyName = Objects.requireNonNull(propertyName); 631 } 632 633 /** 634 * Sets the cardinality of the property (whether it is optional, required or repeated). 635 * 636 * <p>If this method is not called, the default cardinality is {@link 637 * PropertyConfig#CARDINALITY_OPTIONAL}. 638 */ 639 @CanIgnoreReturnValue 640 @SuppressWarnings("MissingGetterMatchingBuilder") // getter defined in superclass 641 @NonNull setCardinality(@ardinality int cardinality)642 public StringPropertyConfig.Builder setCardinality(@Cardinality int cardinality) { 643 Preconditions.checkArgumentInRange( 644 cardinality, CARDINALITY_REPEATED, CARDINALITY_REQUIRED, "cardinality"); 645 mCardinality = cardinality; 646 return this; 647 } 648 649 /** 650 * Configures how a property should be indexed so that it can be retrieved by queries. 651 * 652 * <p>If this method is not called, the default indexing type is {@link 653 * StringPropertyConfig#INDEXING_TYPE_NONE}, so that it cannot be matched by queries. 654 */ 655 @CanIgnoreReturnValue 656 @NonNull setIndexingType(@ndexingType int indexingType)657 public StringPropertyConfig.Builder setIndexingType(@IndexingType int indexingType) { 658 Preconditions.checkArgumentInRange( 659 indexingType, INDEXING_TYPE_NONE, INDEXING_TYPE_PREFIXES, "indexingType"); 660 mIndexingType = indexingType; 661 return this; 662 } 663 664 /** 665 * Configures how this property should be tokenized (split into words). 666 * 667 * <p>If this method is not called, the default indexing type is {@link 668 * StringPropertyConfig#TOKENIZER_TYPE_NONE}, so that it is not tokenized. 669 * 670 * <p>This method must be called with a value other than {@link 671 * StringPropertyConfig#TOKENIZER_TYPE_NONE} if the property is indexed (that is, if 672 * {@link #setIndexingType} has been called with a value other than {@link 673 * StringPropertyConfig#INDEXING_TYPE_NONE}). 674 */ 675 @CanIgnoreReturnValue 676 @NonNull setTokenizerType(@okenizerType int tokenizerType)677 public StringPropertyConfig.Builder setTokenizerType(@TokenizerType int tokenizerType) { 678 Preconditions.checkArgumentInRange( 679 tokenizerType, TOKENIZER_TYPE_NONE, TOKENIZER_TYPE_RFC822, "tokenizerType"); 680 mTokenizerType = tokenizerType; 681 return this; 682 } 683 684 /** 685 * Configures how this property should be used as a joining matcher. 686 * 687 * <p>If this method is not called, the default joinable value type is {@link 688 * StringPropertyConfig#JOINABLE_VALUE_TYPE_NONE}, so that it is not joinable. 689 * 690 * <p>At most, 64 properties can be set as joinable per schema. 691 */ 692 @CanIgnoreReturnValue 693 @NonNull setJoinableValueType( @oinableValueType int joinableValueType)694 public StringPropertyConfig.Builder setJoinableValueType( 695 @JoinableValueType int joinableValueType) { 696 Preconditions.checkArgumentInRange( 697 joinableValueType, 698 JOINABLE_VALUE_TYPE_NONE, 699 JOINABLE_VALUE_TYPE_QUALIFIED_ID, 700 "joinableValueType"); 701 mJoinableValueType = joinableValueType; 702 return this; 703 } 704 705 /** 706 * Configures whether or not documents in this schema will be removed when the document 707 * referred to by this property is deleted. 708 * 709 * <p>Requires that a joinable value type is set. 710 * 711 * @hide 712 */ 713 @SuppressWarnings("MissingGetterMatchingBuilder") // getDeletionPropagation 714 @NonNull setDeletionPropagation(boolean deletionPropagation)715 public Builder setDeletionPropagation(boolean deletionPropagation) { 716 mDeletionPropagation = deletionPropagation; 717 return this; 718 } 719 720 /** Constructs a new {@link StringPropertyConfig} from the contents of this builder. */ 721 @NonNull build()722 public StringPropertyConfig build() { 723 if (mTokenizerType == TOKENIZER_TYPE_NONE) { 724 Preconditions.checkState( 725 mIndexingType == INDEXING_TYPE_NONE, 726 "Cannot set " 727 + "TOKENIZER_TYPE_NONE with an indexing type other than " 728 + "INDEXING_TYPE_NONE."); 729 } else { 730 Preconditions.checkState( 731 mIndexingType != INDEXING_TYPE_NONE, 732 "Cannot set " + "TOKENIZER_TYPE_PLAIN with INDEXING_TYPE_NONE."); 733 } 734 if (mJoinableValueType == JOINABLE_VALUE_TYPE_QUALIFIED_ID) { 735 Preconditions.checkState( 736 mCardinality != CARDINALITY_REPEATED, 737 "Cannot set JOINABLE_VALUE_TYPE_QUALIFIED_ID with" 738 + " CARDINALITY_REPEATED."); 739 } else { 740 Preconditions.checkState( 741 !mDeletionPropagation, 742 "Cannot set deletion " 743 + "propagation without setting a joinable value type"); 744 } 745 Bundle bundle = new Bundle(); 746 bundle.putString(NAME_FIELD, mPropertyName); 747 bundle.putInt(DATA_TYPE_FIELD, DATA_TYPE_STRING); 748 bundle.putInt(CARDINALITY_FIELD, mCardinality); 749 bundle.putInt(INDEXING_TYPE_FIELD, mIndexingType); 750 bundle.putInt(TOKENIZER_TYPE_FIELD, mTokenizerType); 751 bundle.putInt(JOINABLE_VALUE_TYPE_FIELD, mJoinableValueType); 752 bundle.putBoolean(DELETION_PROPAGATION_FIELD, mDeletionPropagation); 753 return new StringPropertyConfig(bundle); 754 } 755 } 756 757 /** 758 * Appends a debug string for the {@link StringPropertyConfig} instance to the given string 759 * builder. 760 * 761 * <p>This appends fields specific to a {@link StringPropertyConfig} instance. 762 * 763 * @param builder the builder to append to. 764 */ appendStringPropertyConfigFields(@onNull IndentingStringBuilder builder)765 void appendStringPropertyConfigFields(@NonNull IndentingStringBuilder builder) { 766 switch (getIndexingType()) { 767 case AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_NONE: 768 builder.append("indexingType: INDEXING_TYPE_NONE,\n"); 769 break; 770 case AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_EXACT_TERMS: 771 builder.append("indexingType: INDEXING_TYPE_EXACT_TERMS,\n"); 772 break; 773 case AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_PREFIXES: 774 builder.append("indexingType: INDEXING_TYPE_PREFIXES,\n"); 775 break; 776 default: 777 builder.append("indexingType: INDEXING_TYPE_UNKNOWN,\n"); 778 } 779 780 switch (getTokenizerType()) { 781 case AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_NONE: 782 builder.append("tokenizerType: TOKENIZER_TYPE_NONE,\n"); 783 break; 784 case AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_PLAIN: 785 builder.append("tokenizerType: TOKENIZER_TYPE_PLAIN,\n"); 786 break; 787 case AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_VERBATIM: 788 builder.append("tokenizerType: TOKENIZER_TYPE_VERBATIM,\n"); 789 break; 790 case AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_RFC822: 791 builder.append("tokenizerType: TOKENIZER_TYPE_RFC822,\n"); 792 break; 793 default: 794 builder.append("tokenizerType: TOKENIZER_TYPE_UNKNOWN,\n"); 795 } 796 797 switch (getJoinableValueType()) { 798 case AppSearchSchema.StringPropertyConfig.JOINABLE_VALUE_TYPE_NONE: 799 builder.append("joinableValueType: JOINABLE_VALUE_TYPE_NONE,\n"); 800 break; 801 case AppSearchSchema.StringPropertyConfig.JOINABLE_VALUE_TYPE_QUALIFIED_ID: 802 builder.append("joinableValueType: JOINABLE_VALUE_TYPE_QUALIFIED_ID,\n"); 803 break; 804 default: 805 builder.append("joinableValueType: JOINABLE_VALUE_TYPE_UNKNOWN,\n"); 806 } 807 } 808 } 809 810 /** Configuration for a property containing a 64-bit integer. */ 811 public static final class LongPropertyConfig extends PropertyConfig { 812 private static final String INDEXING_TYPE_FIELD = "indexingType"; 813 814 /** 815 * Encapsulates the configurations on how AppSearch should query/index these 64-bit 816 * integers. 817 * 818 * @hide 819 */ 820 @IntDef(value = {INDEXING_TYPE_NONE, INDEXING_TYPE_RANGE}) 821 @Retention(RetentionPolicy.SOURCE) 822 public @interface IndexingType {} 823 824 /** Content in this property will not be indexed. */ 825 public static final int INDEXING_TYPE_NONE = 0; 826 827 /** 828 * Content in this property will be indexed and can be fetched via numeric search range 829 * query. 830 * 831 * <p>For example, a property with 1024 should match numeric search range query [0, 2000]. 832 */ 833 public static final int INDEXING_TYPE_RANGE = 1; 834 LongPropertyConfig(@onNull Bundle bundle)835 LongPropertyConfig(@NonNull Bundle bundle) { 836 super(bundle); 837 } 838 839 /** Returns how the property is indexed. */ 840 @IndexingType getIndexingType()841 public int getIndexingType() { 842 return mBundle.getInt(INDEXING_TYPE_FIELD, INDEXING_TYPE_NONE); 843 } 844 845 /** Builder for {@link LongPropertyConfig}. */ 846 public static final class Builder { 847 private final String mPropertyName; 848 @Cardinality private int mCardinality = CARDINALITY_OPTIONAL; 849 @IndexingType private int mIndexingType = INDEXING_TYPE_NONE; 850 851 /** Creates a new {@link LongPropertyConfig.Builder}. */ Builder(@onNull String propertyName)852 public Builder(@NonNull String propertyName) { 853 mPropertyName = Objects.requireNonNull(propertyName); 854 } 855 856 /** 857 * Sets the cardinality of the property (whether it is optional, required or repeated). 858 * 859 * <p>If this method is not called, the default cardinality is {@link 860 * PropertyConfig#CARDINALITY_OPTIONAL}. 861 */ 862 @CanIgnoreReturnValue 863 @SuppressWarnings("MissingGetterMatchingBuilder") // getter defined in superclass 864 @NonNull setCardinality(@ardinality int cardinality)865 public LongPropertyConfig.Builder setCardinality(@Cardinality int cardinality) { 866 Preconditions.checkArgumentInRange( 867 cardinality, CARDINALITY_REPEATED, CARDINALITY_REQUIRED, "cardinality"); 868 mCardinality = cardinality; 869 return this; 870 } 871 872 /** 873 * Configures how a property should be indexed so that it can be retrieved by queries. 874 * 875 * <p>If this method is not called, the default indexing type is {@link 876 * LongPropertyConfig#INDEXING_TYPE_NONE}, so that it will not be indexed and cannot be 877 * matched by queries. 878 */ 879 @CanIgnoreReturnValue 880 @NonNull setIndexingType(@ndexingType int indexingType)881 public LongPropertyConfig.Builder setIndexingType(@IndexingType int indexingType) { 882 Preconditions.checkArgumentInRange( 883 indexingType, INDEXING_TYPE_NONE, INDEXING_TYPE_RANGE, "indexingType"); 884 mIndexingType = indexingType; 885 return this; 886 } 887 888 /** Constructs a new {@link LongPropertyConfig} from the contents of this builder. */ 889 @NonNull build()890 public LongPropertyConfig build() { 891 Bundle bundle = new Bundle(); 892 bundle.putString(NAME_FIELD, mPropertyName); 893 bundle.putInt(DATA_TYPE_FIELD, DATA_TYPE_LONG); 894 bundle.putInt(CARDINALITY_FIELD, mCardinality); 895 bundle.putInt(INDEXING_TYPE_FIELD, mIndexingType); 896 return new LongPropertyConfig(bundle); 897 } 898 } 899 900 /** 901 * Appends a debug string for the {@link LongPropertyConfig} instance to the given string 902 * builder. 903 * 904 * <p>This appends fields specific to a {@link LongPropertyConfig} instance. 905 * 906 * @param builder the builder to append to. 907 */ appendLongPropertyConfigFields(@onNull IndentingStringBuilder builder)908 void appendLongPropertyConfigFields(@NonNull IndentingStringBuilder builder) { 909 switch (getIndexingType()) { 910 case AppSearchSchema.LongPropertyConfig.INDEXING_TYPE_NONE: 911 builder.append("indexingType: INDEXING_TYPE_NONE,\n"); 912 break; 913 case AppSearchSchema.LongPropertyConfig.INDEXING_TYPE_RANGE: 914 builder.append("indexingType: INDEXING_TYPE_RANGE,\n"); 915 break; 916 default: 917 builder.append("indexingType: INDEXING_TYPE_UNKNOWN,\n"); 918 } 919 } 920 } 921 922 /** Configuration for a property containing a double-precision decimal number. */ 923 public static final class DoublePropertyConfig extends PropertyConfig { DoublePropertyConfig(@onNull Bundle bundle)924 DoublePropertyConfig(@NonNull Bundle bundle) { 925 super(bundle); 926 } 927 928 /** Builder for {@link DoublePropertyConfig}. */ 929 public static final class Builder { 930 private final String mPropertyName; 931 @Cardinality private int mCardinality = CARDINALITY_OPTIONAL; 932 933 /** Creates a new {@link DoublePropertyConfig.Builder}. */ Builder(@onNull String propertyName)934 public Builder(@NonNull String propertyName) { 935 mPropertyName = Objects.requireNonNull(propertyName); 936 } 937 938 /** 939 * Sets the cardinality of the property (whether it is optional, required or repeated). 940 * 941 * <p>If this method is not called, the default cardinality is {@link 942 * PropertyConfig#CARDINALITY_OPTIONAL}. 943 */ 944 @CanIgnoreReturnValue 945 @SuppressWarnings("MissingGetterMatchingBuilder") // getter defined in superclass 946 @NonNull setCardinality(@ardinality int cardinality)947 public DoublePropertyConfig.Builder setCardinality(@Cardinality int cardinality) { 948 Preconditions.checkArgumentInRange( 949 cardinality, CARDINALITY_REPEATED, CARDINALITY_REQUIRED, "cardinality"); 950 mCardinality = cardinality; 951 return this; 952 } 953 954 /** Constructs a new {@link DoublePropertyConfig} from the contents of this builder. */ 955 @NonNull build()956 public DoublePropertyConfig build() { 957 Bundle bundle = new Bundle(); 958 bundle.putString(NAME_FIELD, mPropertyName); 959 bundle.putInt(DATA_TYPE_FIELD, DATA_TYPE_DOUBLE); 960 bundle.putInt(CARDINALITY_FIELD, mCardinality); 961 return new DoublePropertyConfig(bundle); 962 } 963 } 964 } 965 966 /** Configuration for a property containing a boolean. */ 967 public static final class BooleanPropertyConfig extends PropertyConfig { BooleanPropertyConfig(@onNull Bundle bundle)968 BooleanPropertyConfig(@NonNull Bundle bundle) { 969 super(bundle); 970 } 971 972 /** Builder for {@link BooleanPropertyConfig}. */ 973 public static final class Builder { 974 private final String mPropertyName; 975 @Cardinality private int mCardinality = CARDINALITY_OPTIONAL; 976 977 /** Creates a new {@link BooleanPropertyConfig.Builder}. */ Builder(@onNull String propertyName)978 public Builder(@NonNull String propertyName) { 979 mPropertyName = Objects.requireNonNull(propertyName); 980 } 981 982 /** 983 * Sets the cardinality of the property (whether it is optional, required or repeated). 984 * 985 * <p>If this method is not called, the default cardinality is {@link 986 * PropertyConfig#CARDINALITY_OPTIONAL}. 987 */ 988 @CanIgnoreReturnValue 989 @SuppressWarnings("MissingGetterMatchingBuilder") // getter defined in superclass 990 @NonNull setCardinality(@ardinality int cardinality)991 public BooleanPropertyConfig.Builder setCardinality(@Cardinality int cardinality) { 992 Preconditions.checkArgumentInRange( 993 cardinality, CARDINALITY_REPEATED, CARDINALITY_REQUIRED, "cardinality"); 994 mCardinality = cardinality; 995 return this; 996 } 997 998 /** Constructs a new {@link BooleanPropertyConfig} from the contents of this builder. */ 999 @NonNull build()1000 public BooleanPropertyConfig build() { 1001 Bundle bundle = new Bundle(); 1002 bundle.putString(NAME_FIELD, mPropertyName); 1003 bundle.putInt(DATA_TYPE_FIELD, DATA_TYPE_BOOLEAN); 1004 bundle.putInt(CARDINALITY_FIELD, mCardinality); 1005 return new BooleanPropertyConfig(bundle); 1006 } 1007 } 1008 } 1009 1010 /** Configuration for a property containing a byte array. */ 1011 public static final class BytesPropertyConfig extends PropertyConfig { BytesPropertyConfig(@onNull Bundle bundle)1012 BytesPropertyConfig(@NonNull Bundle bundle) { 1013 super(bundle); 1014 } 1015 1016 /** Builder for {@link BytesPropertyConfig}. */ 1017 public static final class Builder { 1018 private final String mPropertyName; 1019 @Cardinality private int mCardinality = CARDINALITY_OPTIONAL; 1020 1021 /** Creates a new {@link BytesPropertyConfig.Builder}. */ Builder(@onNull String propertyName)1022 public Builder(@NonNull String propertyName) { 1023 mPropertyName = Objects.requireNonNull(propertyName); 1024 } 1025 1026 /** 1027 * Sets the cardinality of the property (whether it is optional, required or repeated). 1028 * 1029 * <p>If this method is not called, the default cardinality is {@link 1030 * PropertyConfig#CARDINALITY_OPTIONAL}. 1031 */ 1032 @CanIgnoreReturnValue 1033 @SuppressWarnings("MissingGetterMatchingBuilder") // getter defined in superclass 1034 @NonNull setCardinality(@ardinality int cardinality)1035 public BytesPropertyConfig.Builder setCardinality(@Cardinality int cardinality) { 1036 Preconditions.checkArgumentInRange( 1037 cardinality, CARDINALITY_REPEATED, CARDINALITY_REQUIRED, "cardinality"); 1038 mCardinality = cardinality; 1039 return this; 1040 } 1041 1042 /** Constructs a new {@link BytesPropertyConfig} from the contents of this builder. */ 1043 @NonNull build()1044 public BytesPropertyConfig build() { 1045 Bundle bundle = new Bundle(); 1046 bundle.putString(NAME_FIELD, mPropertyName); 1047 bundle.putInt(DATA_TYPE_FIELD, DATA_TYPE_BYTES); 1048 bundle.putInt(CARDINALITY_FIELD, mCardinality); 1049 return new BytesPropertyConfig(bundle); 1050 } 1051 } 1052 } 1053 1054 /** Configuration for a property containing another Document. */ 1055 public static final class DocumentPropertyConfig extends PropertyConfig { 1056 private static final String SCHEMA_TYPE_FIELD = "schemaType"; 1057 private static final String INDEX_NESTED_PROPERTIES_FIELD = "indexNestedProperties"; 1058 DocumentPropertyConfig(@onNull Bundle bundle)1059 DocumentPropertyConfig(@NonNull Bundle bundle) { 1060 super(bundle); 1061 } 1062 1063 /** Returns the logical schema-type of the contents of this document property. */ 1064 @NonNull getSchemaType()1065 public String getSchemaType() { 1066 return Objects.requireNonNull(mBundle.getString(SCHEMA_TYPE_FIELD)); 1067 } 1068 1069 /** 1070 * Returns whether fields in the nested document should be indexed according to that 1071 * document's schema. 1072 * 1073 * <p>If false, the nested document's properties are not indexed regardless of its own 1074 * schema. 1075 */ shouldIndexNestedProperties()1076 public boolean shouldIndexNestedProperties() { 1077 return mBundle.getBoolean(INDEX_NESTED_PROPERTIES_FIELD); 1078 } 1079 1080 /** Builder for {@link DocumentPropertyConfig}. */ 1081 public static final class Builder { 1082 private final String mPropertyName; 1083 private final String mSchemaType; 1084 @Cardinality private int mCardinality = CARDINALITY_OPTIONAL; 1085 private boolean mShouldIndexNestedProperties = false; 1086 1087 /** 1088 * Creates a new {@link DocumentPropertyConfig.Builder}. 1089 * 1090 * @param propertyName The logical name of the property in the schema, which will be 1091 * used as the key for this property in {@link 1092 * GenericDocument.Builder#setPropertyDocument}. 1093 * @param schemaType The type of documents which will be stored in this property. 1094 * Documents of different types cannot be mixed into a single property. 1095 */ Builder(@onNull String propertyName, @NonNull String schemaType)1096 public Builder(@NonNull String propertyName, @NonNull String schemaType) { 1097 mPropertyName = Objects.requireNonNull(propertyName); 1098 mSchemaType = Objects.requireNonNull(schemaType); 1099 } 1100 1101 /** 1102 * Sets the cardinality of the property (whether it is optional, required or repeated). 1103 * 1104 * <p>If this method is not called, the default cardinality is {@link 1105 * PropertyConfig#CARDINALITY_OPTIONAL}. 1106 */ 1107 @CanIgnoreReturnValue 1108 @SuppressWarnings("MissingGetterMatchingBuilder") // getter defined in superclass 1109 @NonNull setCardinality(@ardinality int cardinality)1110 public DocumentPropertyConfig.Builder setCardinality(@Cardinality int cardinality) { 1111 Preconditions.checkArgumentInRange( 1112 cardinality, CARDINALITY_REPEATED, CARDINALITY_REQUIRED, "cardinality"); 1113 mCardinality = cardinality; 1114 return this; 1115 } 1116 1117 /** 1118 * Configures whether fields in the nested document should be indexed according to that 1119 * document's schema. 1120 * 1121 * <p>If false, the nested document's properties are not indexed regardless of its own 1122 * schema. 1123 */ 1124 @CanIgnoreReturnValue 1125 @NonNull setShouldIndexNestedProperties( boolean indexNestedProperties)1126 public DocumentPropertyConfig.Builder setShouldIndexNestedProperties( 1127 boolean indexNestedProperties) { 1128 mShouldIndexNestedProperties = indexNestedProperties; 1129 return this; 1130 } 1131 1132 /** Constructs a new {@link PropertyConfig} from the contents of this builder. */ 1133 @NonNull build()1134 public DocumentPropertyConfig build() { 1135 Bundle bundle = new Bundle(); 1136 bundle.putString(NAME_FIELD, mPropertyName); 1137 bundle.putInt(DATA_TYPE_FIELD, DATA_TYPE_DOCUMENT); 1138 bundle.putInt(CARDINALITY_FIELD, mCardinality); 1139 bundle.putBoolean(INDEX_NESTED_PROPERTIES_FIELD, mShouldIndexNestedProperties); 1140 bundle.putString(SCHEMA_TYPE_FIELD, mSchemaType); 1141 return new DocumentPropertyConfig(bundle); 1142 } 1143 } 1144 1145 /** 1146 * Appends a debug string for the {@link DocumentPropertyConfig} instance to the given 1147 * string builder. 1148 * 1149 * <p>This appends fields specific to a {@link DocumentPropertyConfig} instance. 1150 * 1151 * @param builder the builder to append to. 1152 */ appendDocumentPropertyConfigFields(@onNull IndentingStringBuilder builder)1153 void appendDocumentPropertyConfigFields(@NonNull IndentingStringBuilder builder) { 1154 builder.append("shouldIndexNestedProperties: ") 1155 .append(shouldIndexNestedProperties()) 1156 .append(",\n"); 1157 1158 builder.append("schemaType: \"").append(getSchemaType()).append("\",\n"); 1159 } 1160 } 1161 } 1162