Skip to content

Commit 0c0064d

Browse files
WIP: Protobuf backed FieldValues
1 parent baa0a89 commit 0c0064d

File tree

11 files changed

+1075
-102
lines changed

11 files changed

+1075
-102
lines changed

firebase-firestore/src/main/java/com/google/firebase/firestore/Blob.java

Lines changed: 1 addition & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -82,18 +82,6 @@ public int hashCode() {
8282

8383
@Override
8484
public int compareTo(@NonNull Blob other) {
85-
int size = Math.min(bytes.size(), other.bytes.size());
86-
for (int i = 0; i < size; i++) {
87-
// Make sure the bytes are unsigned
88-
int thisByte = bytes.byteAt(i) & 0xff;
89-
int otherByte = other.bytes.byteAt(i) & 0xff;
90-
if (thisByte < otherByte) {
91-
return -1;
92-
} else if (thisByte > otherByte) {
93-
return 1;
94-
}
95-
// Byte values are equal, continue with comparison
96-
}
97-
return Util.compareIntegers(bytes.size(), other.bytes.size());
85+
return Util.compareByteString(bytes, other.bytes);
9886
}
9987
}
Lines changed: 233 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,233 @@
1+
// Copyright 2020 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package com.google.firebase.firestore.model.protovalue;
16+
17+
import static com.google.firebase.firestore.util.Assert.hardAssert;
18+
import static com.google.firebase.firestore.util.ValueUtil.isType;
19+
20+
import androidx.annotation.Nullable;
21+
import com.google.firebase.firestore.model.FieldPath;
22+
import com.google.firebase.firestore.model.mutation.FieldMask;
23+
import com.google.firebase.firestore.model.value.FieldValue;
24+
import com.google.firestore.v1.MapValue;
25+
import com.google.firestore.v1.Value;
26+
import java.util.HashSet;
27+
import java.util.Iterator;
28+
import java.util.Map;
29+
import java.util.Set;
30+
import java.util.SortedMap;
31+
import java.util.TreeMap;
32+
33+
// TODO(mrschmidt): Rename to DocumentValue
34+
public class ObjectValue extends PrimitiveValue {
35+
private static final ObjectValue EMPTY_MAP_VALUE =
36+
new ObjectValue(
37+
com.google.firestore.v1.Value.newBuilder()
38+
.setMapValue(com.google.firestore.v1.MapValue.getDefaultInstance())
39+
.build());
40+
41+
public static ObjectValue emptyObject() {
42+
return EMPTY_MAP_VALUE;
43+
}
44+
45+
public ObjectValue(Value value) {
46+
super(value);
47+
hardAssert(isType(value, TYPE_ORDER_OBJECT), "ObjectValues must be backed by a MapValue");
48+
}
49+
50+
/**
51+
* Returns the value at the given path or null.
52+
*
53+
* @param fieldPath the path to search
54+
* @return The value at the path or if there it doesn't exist.
55+
*/
56+
public @Nullable FieldValue get(FieldPath fieldPath) {
57+
Value value = internalValue;
58+
59+
for (int i = 0; i < fieldPath.length() - 1; ++i) {
60+
value = value.getMapValue().getFieldsMap().get(fieldPath.getSegment(i));
61+
if (!isType(value, TYPE_ORDER_OBJECT)) {
62+
return null;
63+
}
64+
}
65+
66+
return value.getMapValue().containsFields(fieldPath.getLastSegment())
67+
? FieldValue.of(value.getMapValue().getFieldsOrThrow(fieldPath.getLastSegment()))
68+
: null;
69+
}
70+
71+
/** Recursively extracts the FieldPaths that are set in this ObjectValue. */
72+
public FieldMask getFieldMask() {
73+
return extractFieldMask(internalValue.getMapValue());
74+
}
75+
76+
private FieldMask extractFieldMask(MapValue value) {
77+
Set<FieldPath> fields = new HashSet<>();
78+
for (Map.Entry<String, Value> entry : value.getFieldsMap().entrySet()) {
79+
FieldPath currentPath = FieldPath.fromSingleSegment(entry.getKey());
80+
Value child = entry.getValue();
81+
if (isType(child, TYPE_ORDER_OBJECT)) {
82+
FieldMask nestedMask = extractFieldMask(child.getMapValue());
83+
Set<FieldPath> nestedFields = nestedMask.getMask();
84+
if (nestedFields.isEmpty()) {
85+
// Preserve the empty map by adding it to the FieldMask.
86+
fields.add(currentPath);
87+
} else {
88+
// For nested and non-empty ObjectValues, add the FieldPath of the leaf nodes.
89+
for (FieldPath nestedPath : nestedFields) {
90+
fields.add(currentPath.append(nestedPath));
91+
}
92+
}
93+
} else {
94+
fields.add(currentPath);
95+
}
96+
}
97+
return FieldMask.fromSet(fields);
98+
}
99+
100+
/** Creates a ObjectValue.Builder instance that is based on the current value. */
101+
public ObjectValue.Builder toBuilder() {
102+
return new Builder(this);
103+
}
104+
105+
/**
106+
* An ObjectValue.Builder provides APIs to set and delete fields from an ObjectValue. All
107+
* operations mutate the existing instance.
108+
*/
109+
public static class Builder {
110+
111+
/** The existing data to mutate. */
112+
private ObjectValue baseObject;
113+
114+
/**
115+
* A list of FieldPath:Value pairs to apply to the base object. `null` values indicate field
116+
* deletes. All MapValues are expanded and contain an entry for each leaf node.
117+
*/
118+
private SortedMap<FieldPath, Value> overlayMap;
119+
120+
Builder(ObjectValue baseObject) {
121+
this.baseObject = baseObject;
122+
this.overlayMap = new TreeMap<>();
123+
}
124+
125+
/**
126+
* Sets the field to the provided value.
127+
*
128+
* @param path The field path to set.
129+
* @param value The value to set.
130+
* @return The current Builder instance.
131+
*/
132+
public Builder set(FieldPath path, Value value) {
133+
hardAssert(!path.isEmpty(), "Cannot set field for empty path on ObjectValue");
134+
removeConflictingOverlays(path);
135+
setOverlay(path, value);
136+
return this;
137+
}
138+
139+
/**
140+
* Removes the field at the current path. If there is no field at the specified path nothing is
141+
* changed.
142+
*
143+
* @param path The field path to remove
144+
* @return The current Builder instance.
145+
*/
146+
public Builder delete(FieldPath path) {
147+
hardAssert(!path.isEmpty(), "Cannot delete field for empty path on ObjectValue");
148+
removeConflictingOverlays(path);
149+
setOverlay(path, null);
150+
return this;
151+
}
152+
153+
/** Remove any existing overlays that will be replaced by setting `path` to a new value. */
154+
private void removeConflictingOverlays(FieldPath path) {
155+
Iterator<FieldPath> iterator =
156+
overlayMap.subMap(path, createSuccessor(path)).keySet().iterator();
157+
while (iterator.hasNext()) {
158+
iterator.next();
159+
iterator.remove();
160+
}
161+
}
162+
163+
/**
164+
* Adds `value` to the overlay map at `path`. MapValues are recursively expanded into one
165+
* overlay per leaf node.
166+
*/
167+
private void setOverlay(FieldPath path, @Nullable Value value) {
168+
if (!isType(value, TYPE_ORDER_OBJECT)) {
169+
overlayMap.put(path, value);
170+
} else {
171+
for (Map.Entry<String, Value> entry : value.getMapValue().getFieldsMap().entrySet()) {
172+
setOverlay(path.append(entry.getKey()), entry.getValue());
173+
}
174+
}
175+
}
176+
177+
public ObjectValue build() {
178+
if (overlayMap.isEmpty()) {
179+
return baseObject;
180+
} else {
181+
MapValue.Builder result = baseObject.internalValue.getMapValue().toBuilder();
182+
setNested(result, FieldPath.EMPTY_PATH);
183+
return new ObjectValue(Value.newBuilder().setMapValue(result).build());
184+
}
185+
}
186+
187+
private boolean setNested(MapValue.Builder nestedResult, FieldPath currentPath) {
188+
SortedMap<FieldPath, Value> currentSlice =
189+
currentPath.isEmpty()
190+
? overlayMap
191+
: overlayMap.subMap(currentPath, createSuccessor(currentPath));
192+
193+
boolean modified = false;
194+
195+
while (!currentSlice.isEmpty()) {
196+
FieldPath fieldPath = currentSlice.firstKey();
197+
Value value = currentSlice.get(fieldPath);
198+
199+
if (fieldPath.length() == currentPath.length() + 1) {
200+
String fieldName = fieldPath.getLastSegment();
201+
if (value != null) {
202+
nestedResult.putFields(fieldName, value);
203+
modified = true;
204+
} else if (nestedResult.containsFields(fieldName)) {
205+
nestedResult.removeFields(fieldName);
206+
modified = true;
207+
}
208+
} else {
209+
FieldPath nextSliceStart = fieldPath.keepFirst(currentPath.length() + 1);
210+
@Nullable FieldValue existingValue = baseObject.get(nextSliceStart);
211+
MapValue.Builder nextSliceBuilder =
212+
existingValue instanceof ObjectValue
213+
? ((ObjectValue) existingValue).internalValue.getMapValue().toBuilder()
214+
: MapValue.newBuilder();
215+
modified = setNested(nextSliceBuilder, nextSliceStart) || modified;
216+
if (modified) {
217+
nestedResult.putFields(
218+
nextSliceStart.getLastSegment(),
219+
Value.newBuilder().setMapValue(nextSliceBuilder).build());
220+
}
221+
}
222+
223+
currentSlice = currentSlice.tailMap(createSuccessor(fieldPath));
224+
}
225+
226+
return modified;
227+
}
228+
229+
private FieldPath createSuccessor(FieldPath currentPath) {
230+
return currentPath.popLast().append(currentPath.getLastSegment() + '0');
231+
}
232+
}
233+
}
Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
// Copyright 2020 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package com.google.firebase.firestore.model.protovalue;
16+
17+
import static com.google.firebase.firestore.remote.RemoteSerializer.extractLocalPathFromResourceName;
18+
import static com.google.firebase.firestore.util.Assert.fail;
19+
20+
import androidx.annotation.NonNull;
21+
import androidx.annotation.Nullable;
22+
import com.google.firebase.Timestamp;
23+
import com.google.firebase.firestore.Blob;
24+
import com.google.firebase.firestore.GeoPoint;
25+
import com.google.firebase.firestore.model.DocumentKey;
26+
import com.google.firebase.firestore.model.ResourcePath;
27+
import com.google.firebase.firestore.model.value.FieldValue;
28+
import com.google.firebase.firestore.model.value.ServerTimestampValue;
29+
import com.google.firebase.firestore.remote.RemoteSerializer;
30+
import com.google.firebase.firestore.util.ValueUtil;
31+
import com.google.firestore.v1.ArrayValue;
32+
import com.google.firestore.v1.Value;
33+
import java.util.ArrayList;
34+
import java.util.HashMap;
35+
import java.util.List;
36+
import java.util.Map;
37+
38+
public class PrimitiveValue extends FieldValue {
39+
protected final Value internalValue;
40+
41+
public PrimitiveValue(Value value) {
42+
this.internalValue = value;
43+
}
44+
45+
@Override
46+
public int typeOrder() {
47+
return ValueUtil.typeOrder(internalValue);
48+
}
49+
50+
@Nullable
51+
@Override
52+
public Object value() {
53+
return convertValue(internalValue);
54+
}
55+
56+
@Nullable
57+
private Object convertValue(Value value) {
58+
switch (value.getValueTypeCase()) {
59+
case NULL_VALUE:
60+
return null;
61+
case BOOLEAN_VALUE:
62+
return value.getBooleanValue();
63+
case INTEGER_VALUE:
64+
return value.getIntegerValue();
65+
case DOUBLE_VALUE:
66+
return value.getDoubleValue();
67+
case TIMESTAMP_VALUE:
68+
return new Timestamp(
69+
value.getTimestampValue().getSeconds(), value.getTimestampValue().getNanos());
70+
case STRING_VALUE:
71+
return value.getStringValue();
72+
case BYTES_VALUE:
73+
return Blob.fromByteString(value.getBytesValue());
74+
case REFERENCE_VALUE:
75+
return convertReference(value.getReferenceValue());
76+
case GEO_POINT_VALUE:
77+
return new GeoPoint(
78+
value.getGeoPointValue().getLatitude(), value.getGeoPointValue().getLongitude());
79+
case ARRAY_VALUE:
80+
return convertArray(value.getArrayValue());
81+
case MAP_VALUE:
82+
return convertMap(value.getMapValue());
83+
default:
84+
throw fail("Unknown value type: " + value.getValueTypeCase());
85+
}
86+
}
87+
88+
private Object convertReference(String value) {
89+
ResourcePath resourceName = RemoteSerializer.decodeResourceName(value);
90+
return DocumentKey.fromPath(extractLocalPathFromResourceName(resourceName));
91+
}
92+
93+
private Map<String, Object> convertMap(com.google.firestore.v1.MapValue mapValue) {
94+
Map<String, Object> result = new HashMap<>();
95+
for (Map.Entry<String, Value> entry : mapValue.getFieldsMap().entrySet()) {
96+
result.put(entry.getKey(), convertValue(entry.getValue()));
97+
}
98+
return result;
99+
}
100+
101+
private List<Object> convertArray(ArrayValue arrayValue) {
102+
ArrayList<Object> result = new ArrayList<>(arrayValue.getValuesCount());
103+
for (Value v : arrayValue.getValuesList()) {
104+
result.add(convertValue(v));
105+
}
106+
return result;
107+
}
108+
109+
@Override
110+
public boolean equals(Object o) {
111+
if (this == o) {
112+
return true;
113+
}
114+
115+
if (o instanceof PrimitiveValue) {
116+
PrimitiveValue value = (PrimitiveValue) o;
117+
return ValueUtil.equals(this.internalValue, value.internalValue);
118+
}
119+
120+
return false;
121+
}
122+
123+
@Override
124+
public int hashCode() {
125+
return internalValue.hashCode();
126+
}
127+
128+
@Override
129+
public int compareTo(@NonNull FieldValue other) {
130+
if (other instanceof PrimitiveValue) {
131+
return ValueUtil.compare(this.internalValue, ((PrimitiveValue) other).internalValue);
132+
} else if (ValueUtil.isType(this.internalValue, TYPE_ORDER_TIMESTAMP)
133+
&& other instanceof ServerTimestampValue) {
134+
return -1;
135+
} else {
136+
return defaultCompareTo(other);
137+
}
138+
}
139+
140+
public Value toProto() {
141+
return internalValue;
142+
}
143+
}

0 commit comments

Comments
 (0)