23
23
import com .google .firebase .firestore .model .value .FieldValue ;
24
24
import com .google .firestore .v1 .MapValue ;
25
25
import com .google .firestore .v1 .Value ;
26
+ import java .util .HashMap ;
26
27
import java .util .HashSet ;
27
- import java .util .Iterator ;
28
28
import java .util .Map ;
29
29
import java .util .Set ;
30
- import java .util .SortedMap ;
31
- import java .util .TreeMap ;
32
30
33
31
public class ObjectValue extends PrimitiveValue {
34
32
private static final ObjectValue EMPTY_VALUE =
@@ -37,13 +35,18 @@ public class ObjectValue extends PrimitiveValue {
37
35
.setMapValue (com .google .firestore .v1 .MapValue .getDefaultInstance ())
38
36
.build ());
39
37
38
+ public ObjectValue (Value value ) {
39
+ super (value );
40
+ hardAssert (isType (value , TYPE_ORDER_OBJECT ), "ObjectValues must be backed by a MapValue" );
41
+ }
42
+
40
43
public static ObjectValue emptyObject () {
41
44
return EMPTY_VALUE ;
42
45
}
43
46
44
- public ObjectValue ( Value value ) {
45
- super ( value );
46
- hardAssert ( isType ( value , TYPE_ORDER_OBJECT ), "ObjectValues must be backed by a MapValue" );
47
+ /** Returns a new Builder instance that is based on an empty object. */
48
+ public static ObjectValue . Builder newBuilder () {
49
+ return EMPTY_VALUE . toBuilder ( );
47
50
}
48
51
49
52
/**
@@ -53,17 +56,19 @@ public ObjectValue(Value value) {
53
56
* @return The value at the path or if there it doesn't exist.
54
57
*/
55
58
public @ Nullable FieldValue get (FieldPath fieldPath ) {
56
- Value value = internalValue ;
57
-
58
- for (int i = 0 ; i < fieldPath .length () - 1 ; ++i ) {
59
- value = value .getMapValue ().getFieldsOrDefault (fieldPath .getSegment (i ), null );
60
- if (!isType (value , TYPE_ORDER_OBJECT )) {
61
- return null ;
59
+ if (fieldPath .isEmpty ()) {
60
+ return this ;
61
+ } else {
62
+ Value value = internalValue ;
63
+ for (int i = 0 ; i < fieldPath .length () - 1 ; ++i ) {
64
+ value = value .getMapValue ().getFieldsOrDefault (fieldPath .getSegment (i ), null );
65
+ if (!isType (value , TYPE_ORDER_OBJECT )) {
66
+ return null ;
67
+ }
62
68
}
69
+ value = value .getMapValue ().getFieldsOrDefault (fieldPath .getLastSegment (), null );
70
+ return value != null ? FieldValue .of (value ) : null ;
63
71
}
64
-
65
- value = value .getMapValue ().getFieldsOrDefault (fieldPath .getLastSegment (), null );
66
- return value != null ? FieldValue .of (value ) : null ;
67
72
}
68
73
69
74
/** Recursively extracts the FieldPaths that are set in this ObjectValue. */
@@ -106,15 +111,15 @@ public static class Builder {
106
111
private ObjectValue baseObject ;
107
112
108
113
/**
109
- * A list of FieldPath/Value pairs to apply to the base object. `null` values indicate field
110
- * deletes. MapValues are expanded before they are stored in the overlay map, so that an entry
111
- * exists for each leaf node .
114
+ * A nested map that contains the accumulated changes in this builder. Values can either be
115
+ * `Value` protos, `Map<String, Object>` values (to represent additional nesting) or `null` (to
116
+ * represent field deletes) .
112
117
*/
113
- private SortedMap < FieldPath , Value > overlayMap ;
118
+ private Map < String , Object > overlayMap ;
114
119
115
120
Builder (ObjectValue baseObject ) {
116
121
this .baseObject = baseObject ;
117
- this .overlayMap = new TreeMap <>();
122
+ this .overlayMap = new HashMap <>();
118
123
}
119
124
120
125
/**
@@ -126,7 +131,6 @@ public static class Builder {
126
131
*/
127
132
public Builder set (FieldPath path , Value value ) {
128
133
hardAssert (!path .isEmpty (), "Cannot set field for empty path on ObjectValue" );
129
- removeConflictingOverlays (path );
130
134
setOverlay (path , value );
131
135
return this ;
132
136
}
@@ -140,127 +144,94 @@ public Builder set(FieldPath path, Value value) {
140
144
*/
141
145
public Builder delete (FieldPath path ) {
142
146
hardAssert (!path .isEmpty (), "Cannot delete field for empty path on ObjectValue" );
143
- removeConflictingOverlays (path );
144
147
setOverlay (path , null );
145
148
return this ;
146
149
}
147
150
148
- /** Remove any existing overlays that would be replaced by setting `path` to a new value. */
149
- private void removeConflictingOverlays (FieldPath path ) {
150
- Iterator <FieldPath > iterator =
151
- overlayMap .subMap (path , createSuccessor (path )).keySet ().iterator ();
152
- while (iterator .hasNext ()) {
153
- iterator .next ();
154
- iterator .remove ();
155
- }
156
- }
157
-
158
- /**
159
- * Adds `value` to the overlay map at `path`. MapValues are recursively expanded into one
160
- * overlay per leaf node.
161
- */
151
+ /** Adds `value` to the overlay map at `path`. Creates nested map entries if needed. */
162
152
private void setOverlay (FieldPath path , @ Nullable Value value ) {
163
- if (!isType (value , TYPE_ORDER_OBJECT ) || value .getMapValue ().getFieldsCount () == 0 ) {
164
- overlayMap .put (path , value );
165
- } else {
166
- for (Map .Entry <String , Value > entry : value .getMapValue ().getFieldsMap ().entrySet ()) {
167
- setOverlay (path .append (entry .getKey ()), entry .getValue ());
153
+ Map <String , Object > currentLevel = overlayMap ;
154
+
155
+ for (int i = 0 ; i < path .length () - 1 ; ++i ) {
156
+ String currentSegment = path .getSegment (i );
157
+ Object currentValue = currentLevel .get (currentSegment );
158
+
159
+ if (currentValue instanceof Map ) {
160
+ // Re-use a previously created map
161
+ currentLevel = (Map <String , Object >) currentValue ;
162
+ } else if (currentValue instanceof Value
163
+ && ((Value ) currentValue ).getValueTypeCase () == Value .ValueTypeCase .MAP_VALUE ) {
164
+ // Convert the existing Protobuf MapValue into a Java map
165
+ Map <String , Object > nextLevel =
166
+ new HashMap <>(((Value ) currentValue ).getMapValue ().getFieldsMap ());
167
+ currentLevel .put (currentSegment , nextLevel );
168
+ currentLevel = nextLevel ;
169
+ } else {
170
+ // Create an empty hash map to represent the current nesting level
171
+ Map <String , Object > nextLevel = new HashMap <>();
172
+ currentLevel .put (currentSegment , nextLevel );
173
+ currentLevel = nextLevel ;
168
174
}
169
175
}
176
+
177
+ currentLevel .put (path .getLastSegment (), value );
170
178
}
171
179
172
180
/** Returns an ObjectValue with all mutations applied. */
173
181
public ObjectValue build () {
174
- if (overlayMap .isEmpty ()) {
175
- return baseObject ;
182
+ MapValue mergedResult = applyOverlay (FieldPath .EMPTY_PATH , overlayMap );
183
+ if (mergedResult != null ) {
184
+ return new ObjectValue (Value .newBuilder ().setMapValue (mergedResult ).build ());
176
185
} else {
177
- MapValue .Builder result = baseObject .internalValue .getMapValue ().toBuilder ();
178
- applyOverlay (FieldPath .EMPTY_PATH , result );
179
- return new ObjectValue (Value .newBuilder ().setMapValue (result ).build ());
186
+ return this .baseObject ;
180
187
}
181
188
}
182
189
183
190
/**
184
- * Applies any overlays from `overlayMap` that exist at `currentPath` to the `resultAtPath` map.
185
- * Overlays are expanded recursively based on their location in the backing ObjectValue's
186
- * subtree and are processed by nesting level.
187
- *
188
- * <p>Example: Overlays { 'a.b.c' : 'foo', 'a.b.d' : 'bar', 'a.e' : 'foobar' }
189
- *
190
- * <p>To apply these overlays, the methods first creates a MapValue.Builder for `a`. It then
191
- * calls applyOverlay() with a current path of `a` and the newly created MapValue.Builder. In
192
- * its second call, `applyOverlay` assigns `a.b` to a new MapBuilder and `a.e` to 'foobar'. The
193
- * third call assigns `a.b.c` and `a.b.d` to the MapValue.Builder created in the second step.
194
- *
195
- * <p>The overall aim of this method is to minimize conversions between MapValues and their
196
- * builders.
191
+ * Applies any overlays from `currentOverlays` that exist at `currentPath` and returns the
192
+ * merged data at `currentPath` (or null if there were no changes).
197
193
*
198
194
* @param currentPath The path at the current nesting level. Can be set toFieldValue.EMPTY_PATH
199
195
* to represent the root.
200
- * @param resultAtPath A mutable copy of the existing data at the current nesting level.
201
- * Overlays are applied to this argument.
202
- * @return Whether any modifications were applied (in any part of the subtree under
203
- * currentPath).
196
+ * @param currentOverlays The overlays at the current nesting level in the same format as
197
+ * `overlayMap`.
198
+ * @return The merged data at `currentPath` or null if no modifications were applied.
204
199
*/
205
- private boolean applyOverlay (FieldPath currentPath , MapValue .Builder resultAtPath ) {
206
- // Extract the data that exists at or below the current path. Te extracted subtree is
207
- // subdivided during each iteration. The iteration stops when the slice becomes empty.
208
- SortedMap <FieldPath , Value > currentSlice =
209
- currentPath .isEmpty ()
210
- ? overlayMap
211
- : overlayMap .subMap (currentPath , createSuccessor (currentPath ));
212
-
200
+ private @ Nullable MapValue applyOverlay (
201
+ FieldPath currentPath , Map <String , Object > currentOverlays ) {
213
202
boolean modified = false ;
214
203
215
- while (!currentSlice .isEmpty ()) {
216
- FieldPath fieldPath = currentSlice .firstKey ();
217
-
218
- if (fieldPath .length () == currentPath .length () + 1 ) {
219
- // The key in the slice is a leaf node. We can apply the value directly.
220
- String fieldName = fieldPath .getLastSegment ();
221
- Value overlayValue = overlayMap .get (fieldPath );
222
- if (overlayValue != null ) {
223
- resultAtPath .putFields (fieldName , overlayValue );
224
- modified = true ;
225
- } else if (resultAtPath .containsFields (fieldName )) {
226
- resultAtPath .removeFields (fieldName );
204
+ @ Nullable FieldValue existingValue = baseObject .get (currentPath );
205
+ MapValue .Builder resultAtPath =
206
+ existingValue instanceof ObjectValue
207
+ // If there is already data at the current path, base our modifications on top
208
+ // of the existing data.
209
+ ? ((ObjectValue ) existingValue ).internalValue .getMapValue ().toBuilder ()
210
+ : MapValue .newBuilder ();
211
+
212
+ for (Map .Entry <String , Object > entry : currentOverlays .entrySet ()) {
213
+ String pathSegment = entry .getKey ();
214
+ Object value = entry .getValue ();
215
+
216
+ if (value instanceof Map ) {
217
+ @ Nullable
218
+ MapValue nested =
219
+ applyOverlay (currentPath .append (pathSegment ), (Map <String , Object >) value );
220
+ if (nested != null ) {
221
+ resultAtPath .putFields (pathSegment , Value .newBuilder ().setMapValue (nested ).build ());
227
222
modified = true ;
228
223
}
229
- } else {
230
- // Since we need a MapValue.Builder at each nesting level (e.g. to create the field for
231
- // `a.b.c` we need to create a MapValue.Builder for `a` as well as `a.b`), we invoke
232
- // applyOverlay() recursively with the next nesting level.
233
- FieldPath nextSliceStart = fieldPath .keepFirst (currentPath .length () + 1 );
234
- @ Nullable FieldValue existingValue = baseObject .get (nextSliceStart );
235
- MapValue .Builder nextSliceBuilder =
236
- existingValue instanceof ObjectValue
237
- // If there is already data at the current path, base our modifications on top
238
- // of the existing data.
239
- ? ((ObjectValue ) existingValue ).internalValue .getMapValue ().toBuilder ()
240
- : MapValue .newBuilder ();
241
- modified = applyOverlay (nextSliceStart , nextSliceBuilder ) || modified ;
242
- if (modified ) {
243
- // Only apply the result if a field has been modified. This avoids adding an empty
244
- // map entry for deletes of non-existing fields.
245
- resultAtPath .putFields (
246
- nextSliceStart .getLastSegment (),
247
- Value .newBuilder ().setMapValue (nextSliceBuilder ).build ());
248
- }
224
+ } else if (value instanceof Value ) {
225
+ resultAtPath .putFields (pathSegment , (Value ) value );
226
+ modified = true ;
227
+ } else if (resultAtPath .containsFields (pathSegment )) {
228
+ hardAssert (value == null , "Expected entry to be a Map, a Value or null" );
229
+ resultAtPath .removeFields (pathSegment );
230
+ modified = true ;
249
231
}
250
-
251
- // Shrink the subtree to contain only values after the current field path. Note that we are
252
- // still bound by the subtree created at the initial method invocation. The current loop
253
- // exits when the subtree becomes empty.
254
- currentSlice = currentSlice .tailMap (createSuccessor (fieldPath ));
255
232
}
256
233
257
- return modified ;
258
- }
259
-
260
- /** Create the first field path that is not part of the subtree created by `currentPath`. */
261
- private FieldPath createSuccessor (FieldPath currentPath ) {
262
- hardAssert (!currentPath .isEmpty (), "Can't create a successor for an empty path" );
263
- return currentPath .popLast ().append (currentPath .getLastSegment () + '0' );
234
+ return modified ? resultAtPath .build () : null ;
264
235
}
265
236
}
266
237
}
0 commit comments