Skip to content

Commit

Permalink
Add Array Features to Firestore Java
Browse files Browse the repository at this point in the history
  • Loading branch information
schmidt-sebastian committed Aug 15, 2018
1 parent 1ecbe07 commit 7500dbf
Show file tree
Hide file tree
Showing 12 changed files with 517 additions and 59 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,8 @@ private static <T> Object serialize(T o, ErrorPath path) {
|| o instanceof Timestamp
|| o instanceof GeoPoint
|| o instanceof Blob
|| o instanceof DocumentReference) {
|| o instanceof DocumentReference
|| o instanceof FieldValue) {
return o;
} else {
Class<T> clazz = (Class<T>) o.getClass();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,10 +48,10 @@ private static List<FieldPath> extractFromMap(Map<String, Object> values, FieldP
for (Map.Entry<String, Object> entry : values.entrySet()) {
Object value = entry.getValue();
FieldPath childPath = path.append(FieldPath.of(entry.getKey()));
if (entry.getValue() == FieldValue.SERVER_TIMESTAMP_SENTINEL) {
// Ignore
} else if (entry.getValue() == FieldValue.DELETE_SENTINEL) {
fieldPaths.add(childPath);
if (entry.getValue() instanceof FieldValue) {
if (((FieldValue) entry.getValue()).includeInDocumentMask()) {
fieldPaths.add(childPath);
}
} else if (value instanceof Map) {
fieldPaths.addAll(extractFromMap((Map<String, Object>) value, childPath));
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,11 +46,11 @@ static DocumentTransform fromFieldPathMap(
for (Map.Entry<FieldPath, Object> entry : values.entrySet()) {
FieldPath path = entry.getKey();
Object value = entry.getValue();
if (value == FieldValue.SERVER_TIMESTAMP_SENTINEL) {
FieldTransform.Builder fieldTransform = FieldTransform.newBuilder();
fieldTransform.setFieldPath(path.getEncodedPath());
fieldTransform.setSetToServerValue(FieldTransform.ServerValue.REQUEST_TIME);
transforms.put(path, fieldTransform.build());
if (value instanceof FieldValue) {
FieldValue fieldValue = (FieldValue) value;
if (fieldValue.includeInDocumentTransform()) {
transforms.put(path, fieldValue.toProto(path));
}
} else if (value instanceof Map) {
transforms.putAll(
extractFromMap((Map<String, Object>) value, path, /* allowTransforms= */ true));
Expand All @@ -71,15 +71,15 @@ private static SortedMap<FieldPath, FieldTransform> extractFromMap(
for (Map.Entry<String, Object> entry : values.entrySet()) {
Object value = entry.getValue();
path = path.append(FieldPath.of(entry.getKey()));
if (value == FieldValue.SERVER_TIMESTAMP_SENTINEL) {
if (value instanceof FieldValue) {
FieldValue fieldValue = (FieldValue) value;
if (allowTransforms) {
FieldTransform.Builder fieldTransform = FieldTransform.newBuilder();
fieldTransform.setFieldPath(path.getEncodedPath());
fieldTransform.setSetToServerValue(FieldTransform.ServerValue.REQUEST_TIME);
transforms.put(path, fieldTransform.build());
if (fieldValue.includeInDocumentTransform()) {
transforms.put(path, fieldValue.toProto(path));
}
} else {
throw FirestoreException.invalidState(
"Server timestamps are not supported as Array values.");
fieldValue.getMethodName() + " is not supported inside of an array.");
}
} else if (value instanceof Map) {
transforms.putAll(extractFromMap((Map<String, Object>) value, path, allowTransforms));
Expand All @@ -96,9 +96,9 @@ private static void validateArray(List<Object> values, FieldPath path) {
for (int i = 0; i < values.size(); ++i) {
Object value = values.get(i);
path = path.append(FieldPath.of(Integer.toString(i)));
if (value == FieldValue.SERVER_TIMESTAMP_SENTINEL) {
if (value instanceof FieldValue) {
throw FirestoreException.invalidState(
"Server timestamps are not supported as Array values.");
((FieldValue) value).getMethodName() + " is not supported inside of an array.");
} else if (value instanceof Map) {
extractFromMap((Map<String, Object>) value, path, false);
} else if (value instanceof List) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,141 @@

package com.google.cloud.firestore;

import com.google.common.base.Preconditions;
import com.google.firestore.v1beta1.ArrayValue;
import com.google.firestore.v1beta1.DocumentTransform.FieldTransform;
import java.util.Arrays;
import java.util.List;
import javax.annotation.Nonnull;

/** Sentinel values that can be used when writing document fields with set() or update(). */
public abstract class FieldValue {

static final Object SERVER_TIMESTAMP_SENTINEL = new Object();
static final Object DELETE_SENTINEL = new Object();
private static final FieldValue SERVER_TIMESTAMP_SENTINEL =
new FieldValue() {
@Override
boolean includeInDocumentMask() {
return false;
}

@Override
boolean includeInDocumentTransform() {
return true;
}

@Override
public String getMethodName() {
return "FieldValue.serverTimestamp()";
}

@Override
FieldTransform toProto(FieldPath path) {
FieldTransform.Builder fieldTransform = FieldTransform.newBuilder();
fieldTransform.setFieldPath(path.getEncodedPath());
fieldTransform.setSetToServerValue(FieldTransform.ServerValue.REQUEST_TIME);
return fieldTransform.build();
}
};

static final FieldValue DELETE_SENTINEL =
new FieldValue() {
@Override
boolean includeInDocumentMask() {
return true;
}

@Override
boolean includeInDocumentTransform() {
return false;
}

@Override
public String getMethodName() {
return "FieldValue.delete()";
}

@Override
FieldTransform toProto(FieldPath path) {
throw new IllegalStateException(
"FieldValue.delete() should not be included in a FieldTransform");
}
};

static class ArrayUnionFieldValue extends FieldValue {
final List<Object> elements;

ArrayUnionFieldValue(List<Object> elements) {
this.elements = elements;
}

@Override
boolean includeInDocumentMask() {
return false;
}

@Override
boolean includeInDocumentTransform() {
return true;
}

@Override
public String getMethodName() {
return "FieldValue.arrayUnion()";
}

@Override
FieldTransform toProto(FieldPath path) {
ArrayValue.Builder encodedElements = ArrayValue.newBuilder();

for (Object element : elements) {
encodedElements.addValues(
UserDataConverter.encodeValue(path, element, UserDataConverter.ARGUMENT));
}

FieldTransform.Builder fieldTransform = FieldTransform.newBuilder();
fieldTransform.setFieldPath(path.getEncodedPath());
fieldTransform.setAppendMissingElements(encodedElements);
return fieldTransform.build();
}
}

static class ArrayRemoveFieldValue extends FieldValue {
final List<Object> elements;

ArrayRemoveFieldValue(List<Object> elements) {
this.elements = elements;
}

@Override
boolean includeInDocumentMask() {
return false;
}

@Override
boolean includeInDocumentTransform() {
return true;
}

@Override
public String getMethodName() {
return "FieldValue.arrayRemove()";
}

@Override
FieldTransform toProto(FieldPath path) {
ArrayValue.Builder encodedElements = ArrayValue.newBuilder();

for (Object element : elements) {
encodedElements.addValues(
UserDataConverter.encodeValue(path, element, UserDataConverter.ARGUMENT));
}

FieldTransform.Builder fieldTransform = FieldTransform.newBuilder();
fieldTransform.setFieldPath(path.getEncodedPath());
fieldTransform.setRemoveAllFromArray(encodedElements);
return fieldTransform.build();
}
}

private FieldValue() {}

Expand All @@ -31,16 +159,59 @@ private FieldValue() {}
* written data.
*/
@Nonnull
public static Object serverTimestamp() {
public static FieldValue serverTimestamp() {
return SERVER_TIMESTAMP_SENTINEL;
}

/** Returns a sentinel used with update() to mark a field for deletion. */
@Nonnull
public static Object delete() {
public static FieldValue delete() {
return DELETE_SENTINEL;
}

/**
* Returns a special value that can be used with set() or update() that tells the server to union
* the given elements with any array value that already exists on the server. Each specified
* element that doesn't already exist in the array will be added to the end. If the field being
* modified is not already an array it will be overwritten with an array containing exactly the
* specified elements.
*
* @param elements The elements to union into the array.
* @return The FieldValue sentinel for use in a call to set() or update().
*/
@Nonnull
public static FieldValue arrayUnion(@Nonnull Object... elements) {
Preconditions.checkArgument(elements.length > 0, "arrayUnion() expects at least 1 element");
return new ArrayUnionFieldValue(Arrays.asList(elements));
}

/**
* Returns a special value that can be used with set() or update() that tells the server to remove
* the given elements from any array value that already exists on the server. All instances of
* each element specified will be removed from the array. If the field being modified is not
* already an array it will be overwritten with an empty array.
*
* @param elements The elements to remove from the array.
* @return The FieldValue sentinel for use in a call to set() or update().
*/
@Nonnull
public static FieldValue arrayRemove(@Nonnull Object... elements) {
Preconditions.checkArgument(elements.length > 0, "arrayRemove() expects at least 1 element");
return new ArrayRemoveFieldValue(Arrays.asList(elements));
}

/** Whether this FieldTransform should be included in the document mask. */
abstract boolean includeInDocumentMask();

/** Whether this FieldTransform should be included in the list of document transforms. */
abstract boolean includeInDocumentTransform();

/** The name of the method that returned this FieldValue instance. */
abstract String getMethodName();

/** Generates the field transform proto. */
abstract FieldTransform toProto(FieldPath path);

/**
* Returns true if this FieldValue is equal to the provided object.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

package com.google.cloud.firestore;

import static com.google.firestore.v1beta1.StructuredQuery.FieldFilter.Operator.ARRAY_CONTAINS;
import static com.google.firestore.v1beta1.StructuredQuery.FieldFilter.Operator.EQUAL;
import static com.google.firestore.v1beta1.StructuredQuery.FieldFilter.Operator.GREATER_THAN;
import static com.google.firestore.v1beta1.StructuredQuery.FieldFilter.Operator.GREATER_THAN_OR_EQUAL;
Expand Down Expand Up @@ -89,7 +90,7 @@ private abstract static class FieldFilter {
Value encodeValue() {
Object sanitizedObject = CustomClassMapper.serialize(value);
Value encodedValue =
UserDataConverter.encodeValue(fieldPath, sanitizedObject, UserDataConverter.NO_DELETES);
UserDataConverter.encodeValue(fieldPath, sanitizedObject, UserDataConverter.ARGUMENT);

if (encodedValue == null) {
throw FirestoreException.invalidState("Cannot use Firestore Sentinels in FieldFilter");
Expand Down Expand Up @@ -351,7 +352,7 @@ private Cursor createCursor(List<FieldOrder> order, Object[] fieldValues, boolea
}

Value encodedValue =
UserDataConverter.encodeValue(fieldPath, sanitizedValue, UserDataConverter.NO_DELETES);
UserDataConverter.encodeValue(fieldPath, sanitizedValue, UserDataConverter.ARGUMENT);

if (encodedValue == null) {
throw FirestoreException.invalidState(
Expand Down Expand Up @@ -567,6 +568,44 @@ public Query whereGreaterThanOrEqualTo(@Nonnull FieldPath fieldPath, @Nonnull Ob
return new Query(firestore, path, newOptions);
}

/**
* Creates and returns a new Query with the additional filter that documents must contain the
* specified field, the value must be an array, and that the array must contain the provided
* value.
*
* <p>A Query can have only one whereArrayContains() filter.
*
* @param field The name of the field containing an array to search
* @param value The value that must be contained in the array
* @return The created Query.
*/
@Nonnull
public Query whereArrayContains(@Nonnull String field, @Nonnull Object value) {
return whereArrayContains(FieldPath.fromDotSeparatedString(field), value);
}

/**
* Creates and returns a new Query with the additional filter that documents must contain the
* specified field, the value must be an array, and that the array must contain the provided
* value.
*
* <p>A Query can have only one whereArrayContains() filter.
*
* @param fieldPath The path of the field containing an array to search
* @param value The value that must be contained in the array
* @return The created Query.
*/
@Nonnull
public Query whereArrayContains(@Nonnull FieldPath fieldPath, @Nonnull Object value) {
Preconditions.checkState(
options.startCursor == null && options.endCursor == null,
"Cannot call whereArrayContains() after defining a boundary with startAt(), "
+ "startAfter(), endBefore() or endAt().");
QueryOptions newOptions = new QueryOptions(options);
newOptions.fieldFilters.add(new ComparisonFilter(fieldPath, ARRAY_CONTAINS, value));
return new Query(firestore, path, newOptions);
}

/**
* Creates and returns a new Query that's additionally sorted by the specified field.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,11 @@ EncodingOptions getEncodingOptions() {
public boolean allowDelete(FieldPath fieldPath) {
return fieldMask.contains(fieldPath);
}

@Override
public boolean allowTransform() {
return true;
}
};
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -518,6 +518,11 @@ private T performUpdate(
public boolean allowDelete(FieldPath fieldPath) {
return fields.containsKey(fieldPath);
}

@Override
public boolean allowTransform() {
return true;
}
});
List<FieldPath> fieldPaths = new ArrayList<>(fields.keySet());
DocumentTransform documentTransform =
Expand Down
Loading

0 comments on commit 7500dbf

Please sign in to comment.