blob: 48d3ac09d99794077edd9415e46d122a65eb2d4b [file] [log] [blame]
/*
* Copyright 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package android.app.appsearch;
import android.annotation.SuppressLint;
import android.os.Bundle;
import android.util.Log;
import android.annotation.IntRange;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.appsearch.exceptions.AppSearchException;
import com.android.internal.util.Preconditions;
import java.lang.reflect.Array;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Set;
/**
* Represents a document unit.
*
* <p>Documents are constructed via {@link GenericDocument.Builder}.
* @hide
*/
public class GenericDocument {
private static final String TAG = "GenericDocument";
/** The default empty namespace.*/
public static final String DEFAULT_NAMESPACE = "";
/**
* The maximum number of elements in a repeatable field. Will reject the request if exceed
* this limit.
*/
private static final int MAX_REPEATED_PROPERTY_LENGTH = 100;
/**
* The maximum {@link String#length} of a {@link String} field. Will reject the request if
* {@link String}s longer than this.
*/
private static final int MAX_STRING_LENGTH = 20_000;
/** The maximum number of indexed properties a document can have. */
private static final int MAX_INDEXED_PROPERTIES = 16;
/** The default score of document. */
private static final int DEFAULT_SCORE = 0;
/** The default time-to-live in millisecond of a document, which is infinity. */
private static final long DEFAULT_TTL_MILLIS = 0L;
private static final String PROPERTIES_FIELD = "properties";
private static final String BYTE_ARRAY_FIELD = "byteArray";
private static final String SCHEMA_TYPE_FIELD = "schemaType";
private static final String URI_FIELD = "uri";
private static final String SCORE_FIELD = "score";
private static final String TTL_MILLIS_FIELD = "ttlMillis";
private static final String CREATION_TIMESTAMP_MILLIS_FIELD = "creationTimestampMillis";
private static final String NAMESPACE_FIELD = "namespace";
/**
* The maximum number of indexed properties a document can have.
*
* <p>Indexed properties are properties where the
* {@link android.app.appsearch.annotation.AppSearchDocument.Property#indexingType} constant is
* anything other than {@link
* android.app.appsearch.AppSearchSchema.PropertyConfig.IndexingType#INDEXING_TYPE_NONE}.
*/
public static int getMaxIndexedProperties() {
return MAX_INDEXED_PROPERTIES;
}
/** Contains {@link GenericDocument} basic information (uri, schemaType etc).*/
@NonNull
final Bundle mBundle;
/** Contains all properties in {@link GenericDocument} to support getting properties via keys.*/
@NonNull
private final Bundle mProperties;
@NonNull
private final String mUri;
@NonNull
private final String mSchemaType;
private final long mCreationTimestampMillis;
@Nullable
private Integer mHashCode;
/**
* Rebuilds a {@link GenericDocument} by the a bundle.
* @param bundle Contains {@link GenericDocument} basic information (uri, schemaType etc) and
* a properties bundle contains all properties in {@link GenericDocument} to
* support getting properties via keys.
* @hide
*/
public GenericDocument(@NonNull Bundle bundle) {
Preconditions.checkNotNull(bundle);
mBundle = bundle;
mProperties = Preconditions.checkNotNull(bundle.getParcelable(PROPERTIES_FIELD));
mUri = Preconditions.checkNotNull(mBundle.getString(URI_FIELD));
mSchemaType = Preconditions.checkNotNull(mBundle.getString(SCHEMA_TYPE_FIELD));
mCreationTimestampMillis = mBundle.getLong(CREATION_TIMESTAMP_MILLIS_FIELD,
System.currentTimeMillis());
}
/**
* Creates a new {@link GenericDocument} from an existing instance.
*
* <p>This method should be only used by constructor of a subclass.
*/
protected GenericDocument(@NonNull GenericDocument document) {
this(document.mBundle);
}
/**
* Returns the {@link Bundle} populated by this builder.
* @hide
*/
@NonNull
public Bundle getBundle() {
return mBundle;
}
/** Returns the URI of the {@link GenericDocument}. */
@NonNull
public String getUri() {
return mUri;
}
/** Returns the namespace of the {@link GenericDocument}. */
@NonNull
public String getNamespace() {
return mBundle.getString(NAMESPACE_FIELD, DEFAULT_NAMESPACE);
}
/** Returns the schema type of the {@link GenericDocument}. */
@NonNull
public String getSchemaType() {
return mSchemaType;
}
/** Returns the creation timestamp of the {@link GenericDocument}, in milliseconds. */
public long getCreationTimestampMillis() {
return mCreationTimestampMillis;
}
/**
* Returns the TTL (Time To Live) of the {@link GenericDocument}, in milliseconds.
*
* <p>The default value is 0, which means the document is permanent and won't be auto-deleted
* until the app is uninstalled.
*/
public long getTtlMillis() {
return mBundle.getLong(TTL_MILLIS_FIELD, DEFAULT_TTL_MILLIS);
}
/**
* Returns the score of the {@link GenericDocument}.
*
* <p>The score is a query-independent measure of the document's quality, relative to other
* {@link GenericDocument}s of the same type.
*
* <p>The default value is 0.
*/
public int getScore() {
return mBundle.getInt(SCORE_FIELD, DEFAULT_SCORE);
}
/** Returns the names of all properties defined in this document. */
@NonNull
public Set<String> getPropertyNames() {
return Collections.unmodifiableSet(mProperties.keySet());
}
/**
* Retrieves a {@link String} value by key.
*
* @param key The key to look for.
* @return The first {@link String} associated with the given key or {@code null} if there
* is no such key or the value is of a different type.
*/
@Nullable
public String getPropertyString(@NonNull String key) {
Preconditions.checkNotNull(key);
String[] propertyArray = getPropertyStringArray(key);
if (propertyArray == null || propertyArray.length == 0) {
return null;
}
warnIfSinglePropertyTooLong("String", key, propertyArray.length);
return propertyArray[0];
}
/**
* Retrieves a {@code long} value by key.
*
* @param key The key to look for.
* @return The first {@code long} associated with the given key or default value {@code 0} if
* there is no such key or the value is of a different type.
*/
public long getPropertyLong(@NonNull String key) {
Preconditions.checkNotNull(key);
long[] propertyArray = getPropertyLongArray(key);
if (propertyArray == null || propertyArray.length == 0) {
return 0;
}
warnIfSinglePropertyTooLong("Long", key, propertyArray.length);
return propertyArray[0];
}
/**
* Retrieves a {@code double} value by key.
*
* @param key The key to look for.
* @return The first {@code double} associated with the given key or default value {@code 0.0}
* if there is no such key or the value is of a different type.
*/
public double getPropertyDouble(@NonNull String key) {
Preconditions.checkNotNull(key);
double[] propertyArray = getPropertyDoubleArray(key);
if (propertyArray == null || propertyArray.length == 0) {
return 0.0;
}
warnIfSinglePropertyTooLong("Double", key, propertyArray.length);
return propertyArray[0];
}
/**
* Retrieves a {@code boolean} value by key.
*
* @param key The key to look for.
* @return The first {@code boolean} associated with the given key or default value
* {@code false} if there is no such key or the value is of a different type.
*/
public boolean getPropertyBoolean(@NonNull String key) {
Preconditions.checkNotNull(key);
boolean[] propertyArray = getPropertyBooleanArray(key);
if (propertyArray == null || propertyArray.length == 0) {
return false;
}
warnIfSinglePropertyTooLong("Boolean", key, propertyArray.length);
return propertyArray[0];
}
/**
* Retrieves a {@code byte[]} value by key.
*
* @param key The key to look for.
* @return The first {@code byte[]} associated with the given key or {@code null} if there
* is no such key or the value is of a different type.
*/
@Nullable
public byte[] getPropertyBytes(@NonNull String key) {
Preconditions.checkNotNull(key);
byte[][] propertyArray = getPropertyBytesArray(key);
if (propertyArray == null || propertyArray.length == 0) {
return null;
}
warnIfSinglePropertyTooLong("ByteArray", key, propertyArray.length);
return propertyArray[0];
}
/**
* Retrieves a {@link GenericDocument} value by key.
*
* @param key The key to look for.
* @return The first {@link GenericDocument} associated with the given key or {@code null} if
* there is no such key or the value is of a different type.
*/
@Nullable
public GenericDocument getPropertyDocument(@NonNull String key) {
Preconditions.checkNotNull(key);
GenericDocument[] propertyArray = getPropertyDocumentArray(key);
if (propertyArray == null || propertyArray.length == 0) {
return null;
}
warnIfSinglePropertyTooLong("Document", key, propertyArray.length);
return propertyArray[0];
}
/** Prints a warning to logcat if the given propertyLength is greater than 1. */
private static void warnIfSinglePropertyTooLong(
@NonNull String propertyType, @NonNull String key, int propertyLength) {
if (propertyLength > 1) {
Log.w(TAG, "The value for \"" + key + "\" contains " + propertyLength
+ " elements. Only the first one will be returned from "
+ "getProperty" + propertyType + "(). Try getProperty" + propertyType
+ "Array().");
}
}
/**
* Retrieves a repeated {@code String} property by key.
*
* @param key The key to look for.
* @return The {@code String[]} associated with the given key, or {@code null} if no value
* is set or the value is of a different type.
*/
@Nullable
public String[] getPropertyStringArray(@NonNull String key) {
Preconditions.checkNotNull(key);
return getAndCastPropertyArray(key, String[].class);
}
/**
* Retrieves a repeated {@link String} property by key.
*
* @param key The key to look for.
* @return The {@code long[]} associated with the given key, or {@code null} if no value is
* set or the value is of a different type.
*/
@Nullable
public long[] getPropertyLongArray(@NonNull String key) {
Preconditions.checkNotNull(key);
return getAndCastPropertyArray(key, long[].class);
}
/**
* Retrieves a repeated {@code double} property by key.
*
* @param key The key to look for.
* @return The {@code double[]} associated with the given key, or {@code null} if no value
* is set or the value is of a different type.
*/
@Nullable
public double[] getPropertyDoubleArray(@NonNull String key) {
Preconditions.checkNotNull(key);
return getAndCastPropertyArray(key, double[].class);
}
/**
* Retrieves a repeated {@code boolean} property by key.
*
* @param key The key to look for.
* @return The {@code boolean[]} associated with the given key, or {@code null} if no value
* is set or the value is of a different type.
*/
@Nullable
public boolean[] getPropertyBooleanArray(@NonNull String key) {
Preconditions.checkNotNull(key);
return getAndCastPropertyArray(key, boolean[].class);
}
/**
* Retrieves a {@code byte[][]} property by key.
*
* @param key The key to look for.
* @return The {@code byte[][]} associated with the given key, or {@code null} if no value
* is set or the value is of a different type.
*/
@SuppressLint("ArrayReturn")
@Nullable
@SuppressWarnings("unchecked")
public byte[][] getPropertyBytesArray(@NonNull String key) {
Preconditions.checkNotNull(key);
ArrayList<Bundle> bundles = getAndCastPropertyArray(key, ArrayList.class);
if (bundles == null || bundles.size() == 0) {
return null;
}
byte[][] bytes = new byte[bundles.size()][];
for (int i = 0; i < bundles.size(); i++) {
Bundle bundle = bundles.get(i);
if (bundle == null) {
Log.e(TAG, "The inner bundle is null at " + i + ", for key: " + key);
continue;
}
byte[] innerBytes = bundle.getByteArray(BYTE_ARRAY_FIELD);
if (innerBytes == null) {
Log.e(TAG, "The bundle at " + i + " contains a null byte[].");
continue;
}
bytes[i] = innerBytes;
}
return bytes;
}
/**
* Retrieves a repeated {@link GenericDocument} property by key.
*
* @param key The key to look for.
* @return The {@link GenericDocument}[] associated with the given key, or {@code null} if no
* value is set or the value is of a different type.
*/
@SuppressLint("ArrayReturn")
@Nullable
public GenericDocument[] getPropertyDocumentArray(@NonNull String key) {
Preconditions.checkNotNull(key);
Bundle[] bundles = getAndCastPropertyArray(key, Bundle[].class);
if (bundles == null || bundles.length == 0) {
return null;
}
GenericDocument[] documents = new GenericDocument[bundles.length];
for (int i = 0; i < bundles.length; i++) {
if (bundles[i] == null) {
Log.e(TAG, "The inner bundle is null at " + i + ", for key: " + key);
continue;
}
documents[i] = new GenericDocument(bundles[i]);
}
return documents;
}
/**
* Gets a repeated property of the given key, and casts it to the given class type, which
* must be an array class type.
*/
@Nullable
private <T> T getAndCastPropertyArray(@NonNull String key, @NonNull Class<T> tClass) {
Object value = mProperties.get(key);
if (value == null) {
return null;
}
try {
return tClass.cast(value);
} catch (ClassCastException e) {
Log.w(TAG, "Error casting to requested type for key \"" + key + "\"", e);
return null;
}
}
@Override
public boolean equals(@Nullable Object other) {
if (this == other) {
return true;
}
if (!(other instanceof GenericDocument)) {
return false;
}
GenericDocument otherDocument = (GenericDocument) other;
return bundleEquals(this.mBundle, otherDocument.mBundle);
}
/**
* Deeply checks two bundles are equally or not.
* <p> Two bundles will be considered equally if they contain same content.
*/
@SuppressWarnings("unchecked")
private static boolean bundleEquals(Bundle one, Bundle two) {
if (one.size() != two.size()) {
return false;
}
Set<String> keySetOne = one.keySet();
Object valueOne;
Object valueTwo;
// Bundle inherit its equals() from Object.java, which only compare their memory address.
// We should iterate all keys and check their presents and values in both bundle.
for (String key : keySetOne) {
valueOne = one.get(key);
valueTwo = two.get(key);
if (valueOne instanceof Bundle
&& valueTwo instanceof Bundle
&& !bundleEquals((Bundle) valueOne, (Bundle) valueTwo)) {
return false;
} else if (valueOne == null && (valueTwo != null || !two.containsKey(key))) {
// If we call bundle.get(key) when the 'key' doesn't actually exist in the
// bundle, we'll get back a null. So make sure that both values are null and
// both keys exist in the bundle.
return false;
} else if (valueOne instanceof boolean[]) {
if (!(valueTwo instanceof boolean[])
|| !Arrays.equals((boolean[]) valueOne, (boolean[]) valueTwo)) {
return false;
}
} else if (valueOne instanceof long[]) {
if (!(valueTwo instanceof long[])
|| !Arrays.equals((long[]) valueOne, (long[]) valueTwo)) {
return false;
}
} else if (valueOne instanceof double[]) {
if (!(valueTwo instanceof double[])
|| !Arrays.equals((double[]) valueOne, (double[]) valueTwo)) {
return false;
}
} else if (valueOne instanceof Bundle[]) {
if (!(valueTwo instanceof Bundle[])) {
return false;
}
Bundle[] bundlesOne = (Bundle[]) valueOne;
Bundle[] bundlesTwo = (Bundle[]) valueTwo;
if (bundlesOne.length != bundlesTwo.length) {
return false;
}
for (int i = 0; i < bundlesOne.length; i++) {
if (!bundleEquals(bundlesOne[i], bundlesTwo[i])) {
return false;
}
}
} else if (valueOne instanceof ArrayList) {
if (!(valueTwo instanceof ArrayList)) {
return false;
}
ArrayList<Bundle> bundlesOne = (ArrayList<Bundle>) valueOne;
ArrayList<Bundle> bundlesTwo = (ArrayList<Bundle>) valueTwo;
if (bundlesOne.size() != bundlesTwo.size()) {
return false;
}
for (int i = 0; i < bundlesOne.size(); i++) {
if (!bundleEquals(bundlesOne.get(i), bundlesTwo.get(i))) {
return false;
}
}
} else if (valueOne instanceof Object[]) {
if (!(valueTwo instanceof Object[])
|| !Arrays.equals((Object[]) valueOne, (Object[]) valueTwo)) {
return false;
}
}
}
return true;
}
@Override
public int hashCode() {
if (mHashCode == null) {
mHashCode = bundleHashCode(mBundle);
}
return mHashCode;
}
/**
* Calculates the hash code for a bundle.
* <p> The hash code is only effected by the contents in the bundle. Bundles will get
* consistent hash code if they have same contents.
*/
@SuppressWarnings("unchecked")
private static int bundleHashCode(Bundle bundle) {
int[] hashCodes = new int[bundle.size()];
int i = 0;
// Bundle inherit its hashCode() from Object.java, which only relative to their memory
// address. Bundle doesn't have an order, so we should iterate all keys and combine
// their value's hashcode into an array. And use the hashcode of the array to be
// the hashcode of the bundle.
for (String key : bundle.keySet()) {
Object value = bundle.get(key);
if (value instanceof boolean[]) {
hashCodes[i++] = Arrays.hashCode((boolean[]) value);
} else if (value instanceof long[]) {
hashCodes[i++] = Arrays.hashCode((long[]) value);
} else if (value instanceof double[]) {
hashCodes[i++] = Arrays.hashCode((double[]) value);
} else if (value instanceof String[]) {
hashCodes[i++] = Arrays.hashCode((Object[]) value);
} else if (value instanceof Bundle) {
hashCodes[i++] = bundleHashCode((Bundle) value);
} else if (value instanceof Bundle[]) {
Bundle[] bundles = (Bundle[]) value;
int[] innerHashCodes = new int[bundles.length];
for (int j = 0; j < innerHashCodes.length; j++) {
innerHashCodes[j] = bundleHashCode(bundles[j]);
}
hashCodes[i++] = Arrays.hashCode(innerHashCodes);
} else if (value instanceof ArrayList) {
ArrayList<Bundle> bundles = (ArrayList<Bundle>) value;
int[] innerHashCodes = new int[bundles.size()];
for (int j = 0; j < innerHashCodes.length; j++) {
innerHashCodes[j] = bundleHashCode(bundles.get(j));
}
hashCodes[i++] = Arrays.hashCode(innerHashCodes);
} else {
hashCodes[i++] = value.hashCode();
}
}
return Arrays.hashCode(hashCodes);
}
@Override
@NonNull
public String toString() {
return bundleToString(mBundle).toString();
}
@SuppressWarnings("unchecked")
private static StringBuilder bundleToString(Bundle bundle) {
StringBuilder stringBuilder = new StringBuilder();
try {
final Set<String> keySet = bundle.keySet();
String[] keys = keySet.toArray(new String[0]);
// Sort keys to make output deterministic. We need a custom comparator to handle
// nulls (arbitrarily putting them first, similar to Comparator.nullsFirst, which is
// only available since N).
Arrays.sort(
keys,
(@Nullable String s1, @Nullable String s2) -> {
if (s1 == null) {
return s2 == null ? 0 : -1;
} else if (s2 == null) {
return 1;
} else {
return s1.compareTo(s2);
}
});
for (String key : keys) {
stringBuilder.append("{ key: '").append(key).append("' value: ");
Object valueObject = bundle.get(key);
if (valueObject == null) {
stringBuilder.append("<null>");
} else if (valueObject instanceof Bundle) {
stringBuilder.append(bundleToString((Bundle) valueObject));
} else if (valueObject.getClass().isArray()) {
stringBuilder.append("[ ");
for (int i = 0; i < Array.getLength(valueObject); i++) {
Object element = Array.get(valueObject, i);
stringBuilder.append("'");
if (element instanceof Bundle) {
stringBuilder.append(bundleToString((Bundle) element));
} else {
stringBuilder.append(Array.get(valueObject, i));
}
stringBuilder.append("' ");
}
stringBuilder.append("]");
} else if (valueObject instanceof ArrayList) {
for (Bundle innerBundle : (ArrayList<Bundle>) valueObject) {
stringBuilder.append(bundleToString(innerBundle));
}
} else {
stringBuilder.append(valueObject.toString());
}
stringBuilder.append(" } ");
}
} catch (RuntimeException e) {
// Catch any exceptions here since corrupt Bundles can throw different types of
// exceptions (e.g. b/38445840 & b/68937025).
stringBuilder.append("<error>");
}
return stringBuilder;
}
/**
* The builder class for {@link GenericDocument}.
*
* @param <BuilderType> Type of subclass who extends this.
*/
// This builder is specifically designed to be extended by classes deriving from
// GenericDocument.
@SuppressLint("StaticFinalBuilder")
public static class Builder<BuilderType extends Builder> {
private final Bundle mProperties = new Bundle();
private final Bundle mBundle = new Bundle();
private final BuilderType mBuilderTypeInstance;
private boolean mBuilt = false;
/**
* Create a new {@link GenericDocument.Builder}.
*
* @param uri The uri of {@link GenericDocument}.
* @param schemaType The schema type of the {@link GenericDocument}. The passed-in
* {@code schemaType} must be defined using {@link AppSearchSession#setSchema} prior
* to inserting a document of this {@code schemaType} into the AppSearch index using
* {@link AppSearchSession#putDocuments}. Otherwise, the document will be
* rejected by {@link AppSearchSession#putDocuments}.
*/
@SuppressWarnings("unchecked")
public Builder(@NonNull String uri, @NonNull String schemaType) {
Preconditions.checkNotNull(uri);
Preconditions.checkNotNull(schemaType);
mBuilderTypeInstance = (BuilderType) this;
mBundle.putString(GenericDocument.URI_FIELD, uri);
mBundle.putString(GenericDocument.SCHEMA_TYPE_FIELD, schemaType);
mBundle.putString(GenericDocument.NAMESPACE_FIELD, DEFAULT_NAMESPACE);
// Set current timestamp for creation timestamp by default.
mBundle.putLong(GenericDocument.CREATION_TIMESTAMP_MILLIS_FIELD,
System.currentTimeMillis());
mBundle.putLong(GenericDocument.TTL_MILLIS_FIELD, DEFAULT_TTL_MILLIS);
mBundle.putInt(GenericDocument.SCORE_FIELD, DEFAULT_SCORE);
mBundle.putBundle(PROPERTIES_FIELD, mProperties);
}
/**
* Sets the app-defined namespace this Document resides in. No special values are
* reserved or understood by the infrastructure.
*
* <p>URIs are unique within a namespace.
*
* <p>The number of namespaces per app should be kept small for efficiency reasons.
*/
@NonNull
public BuilderType setNamespace(@NonNull String namespace) {
mBundle.putString(GenericDocument.NAMESPACE_FIELD, namespace);
return mBuilderTypeInstance;
}
/**
* Sets the score of the {@link GenericDocument}.
*
* <p>The score is a query-independent measure of the document's quality, relative to
* other {@link GenericDocument}s of the same type.
*
* @throws IllegalArgumentException If the provided value is negative.
*/
@NonNull
public BuilderType setScore(@IntRange(from = 0, to = Integer.MAX_VALUE) int score) {
Preconditions.checkState(!mBuilt, "Builder has already been used");
if (score < 0) {
throw new IllegalArgumentException("Document score cannot be negative.");
}
mBundle.putInt(GenericDocument.SCORE_FIELD, score);
return mBuilderTypeInstance;
}
/**
* Sets the creation timestamp of the {@link GenericDocument}, in milliseconds. Should be
* set using a value obtained from the {@link System#currentTimeMillis()} time base.
*/
@NonNull
public BuilderType setCreationTimestampMillis(long creationTimestampMillis) {
Preconditions.checkState(!mBuilt, "Builder has already been used");
mBundle.putLong(GenericDocument.CREATION_TIMESTAMP_MILLIS_FIELD,
creationTimestampMillis);
return mBuilderTypeInstance;
}
/**
* Sets the TTL (Time To Live) of the {@link GenericDocument}, in milliseconds.
*
* <p>After this many milliseconds since the {@link #setCreationTimestampMillis creation
* timestamp}, the document is deleted.
*
* @param ttlMillis A non-negative duration in milliseconds.
* @throws IllegalArgumentException If the provided value is negative.
*/
@NonNull
public BuilderType setTtlMillis(long ttlMillis) {
Preconditions.checkState(!mBuilt, "Builder has already been used");
if (ttlMillis < 0) {
throw new IllegalArgumentException("Document ttlMillis cannot be negative.");
}
mBundle.putLong(GenericDocument.TTL_MILLIS_FIELD, ttlMillis);
return mBuilderTypeInstance;
}
/**
* Sets one or multiple {@code String} values for a property, replacing its previous
* values.
*
* @param key The key associated with the {@code values}.
* @param values The {@code String} values of the property.
*/
@NonNull
public BuilderType setPropertyString(@NonNull String key, @NonNull String... values) {
Preconditions.checkState(!mBuilt, "Builder has already been used");
Preconditions.checkNotNull(key);
Preconditions.checkNotNull(values);
putInPropertyBundle(key, values);
return mBuilderTypeInstance;
}
/**
* Sets one or multiple {@code boolean} values for a property, replacing its previous
* values.
*
* @param key The key associated with the {@code values}.
* @param values The {@code boolean} values of the property.
*/
@NonNull
public BuilderType setPropertyBoolean(@NonNull String key, @NonNull boolean... values) {
Preconditions.checkState(!mBuilt, "Builder has already been used");
Preconditions.checkNotNull(key);
Preconditions.checkNotNull(values);
putInPropertyBundle(key, values);
return mBuilderTypeInstance;
}
/**
* Sets one or multiple {@code long} values for a property, replacing its previous
* values.
*
* @param key The key associated with the {@code values}.
* @param values The {@code long} values of the property.
*/
@NonNull
public BuilderType setPropertyLong(@NonNull String key, @NonNull long... values) {
Preconditions.checkState(!mBuilt, "Builder has already been used");
Preconditions.checkNotNull(key);
Preconditions.checkNotNull(values);
putInPropertyBundle(key, values);
return mBuilderTypeInstance;
}
/**
* Sets one or multiple {@code double} values for a property, replacing its previous
* values.
*
* @param key The key associated with the {@code values}.
* @param values The {@code double} values of the property.
*/
@NonNull
public BuilderType setPropertyDouble(@NonNull String key, @NonNull double... values) {
Preconditions.checkState(!mBuilt, "Builder has already been used");
Preconditions.checkNotNull(key);
Preconditions.checkNotNull(values);
putInPropertyBundle(key, values);
return mBuilderTypeInstance;
}
/**
* Sets one or multiple {@code byte[]} for a property, replacing its previous values.
*
* @param key The key associated with the {@code values}.
* @param values The {@code byte[]} of the property.
*/
@NonNull
public BuilderType setPropertyBytes(@NonNull String key, @NonNull byte[]... values) {
Preconditions.checkState(!mBuilt, "Builder has already been used");
Preconditions.checkNotNull(key);
Preconditions.checkNotNull(values);
putInPropertyBundle(key, values);
return mBuilderTypeInstance;
}
/**
* Sets one or multiple {@link GenericDocument} values for a property, replacing its
* previous values.
*
* @param key The key associated with the {@code values}.
* @param values The {@link GenericDocument} values of the property.
*/
@NonNull
public BuilderType setPropertyDocument(
@NonNull String key, @NonNull GenericDocument... values) {
Preconditions.checkState(!mBuilt, "Builder has already been used");
Preconditions.checkNotNull(key);
Preconditions.checkNotNull(values);
putInPropertyBundle(key, values);
return mBuilderTypeInstance;
}
private void putInPropertyBundle(@NonNull String key, @NonNull String[] values)
throws IllegalArgumentException {
validateRepeatedPropertyLength(key, values.length);
for (int i = 0; i < values.length; i++) {
if (values[i] == null) {
throw new IllegalArgumentException("The String at " + i + " is null.");
} else if (values[i].length() > MAX_STRING_LENGTH) {
throw new IllegalArgumentException("The String at " + i + " length is: "
+ values[i].length() + ", which exceeds length limit: "
+ MAX_STRING_LENGTH + ".");
}
}
mProperties.putStringArray(key, values);
}
private void putInPropertyBundle(@NonNull String key, @NonNull boolean[] values) {
validateRepeatedPropertyLength(key, values.length);
mProperties.putBooleanArray(key, values);
}
private void putInPropertyBundle(@NonNull String key, @NonNull double[] values) {
validateRepeatedPropertyLength(key, values.length);
mProperties.putDoubleArray(key, values);
}
private void putInPropertyBundle(@NonNull String key, @NonNull long[] values) {
validateRepeatedPropertyLength(key, values.length);
mProperties.putLongArray(key, values);
}
/**
* Converts and saves a byte[][] into {@link #mProperties}.
*
* <p>Bundle doesn't support for two dimension array byte[][], we are converting byte[][]
* into ArrayList<Bundle>, and each elements will contain a one dimension byte[].
*/
private void putInPropertyBundle(@NonNull String key, @NonNull byte[][] values) {
validateRepeatedPropertyLength(key, values.length);
ArrayList<Bundle> bundles = new ArrayList<>(values.length);
for (int i = 0; i < values.length; i++) {
if (values[i] == null) {
throw new IllegalArgumentException("The byte[] at " + i + " is null.");
}
Bundle bundle = new Bundle();
bundle.putByteArray(BYTE_ARRAY_FIELD, values[i]);
bundles.add(bundle);
}
mProperties.putParcelableArrayList(key, bundles);
}
private void putInPropertyBundle(@NonNull String key, @NonNull GenericDocument[] values) {
validateRepeatedPropertyLength(key, values.length);
Bundle[] documentBundles = new Bundle[values.length];
for (int i = 0; i < values.length; i++) {
if (values[i] == null) {
throw new IllegalArgumentException("The document at " + i + " is null.");
}
documentBundles[i] = values[i].mBundle;
}
mProperties.putParcelableArray(key, documentBundles);
}
private static void validateRepeatedPropertyLength(@NonNull String key, int length) {
if (length == 0) {
throw new IllegalArgumentException("The input array is empty.");
} else if (length > MAX_REPEATED_PROPERTY_LENGTH) {
throw new IllegalArgumentException(
"Repeated property \"" + key + "\" has length " + length
+ ", which exceeds the limit of "
+ MAX_REPEATED_PROPERTY_LENGTH);
}
}
/** Builds the {@link GenericDocument} object. */
@NonNull
public GenericDocument build() {
Preconditions.checkState(!mBuilt, "Builder has already been used");
mBuilt = true;
return new GenericDocument(mBundle);
}
}
}