Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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,176 @@

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 java.util.Objects;
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
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
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
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();
}

@Override
public boolean equals(Object o) {

This comment was marked as spam.

This comment was marked as spam.

if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
ArrayUnionFieldValue that = (ArrayUnionFieldValue) o;
return Objects.equals(elements, that.elements);
}

@Override
public int hashCode() {
return Objects.hash(elements);
}
}

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
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();
}

@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
ArrayRemoveFieldValue that = (ArrayRemoveFieldValue) o;
return Objects.equals(elements, that.elements);
}

@Override
public int hashCode() {
return Objects.hash(elements);
}
}

private FieldValue() {}

Expand All @@ -31,16 +194,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
Loading