Skip to content

Commit 180f5a9

Browse files
Praful MakaniBenWhitehead
authored andcommitted
feat(firestore): allow passing POJOs as field values throughout API reference (#6843)
Ported from firebase/firebase-android-sdk#76
1 parent ec1b680 commit 180f5a9

File tree

6 files changed

+85
-126
lines changed

6 files changed

+85
-126
lines changed

google-cloud-firestore/src/main/java/com/google/cloud/firestore/CollectionReference.java

Lines changed: 2 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,6 @@
2828
import com.google.firestore.v1.DocumentMask;
2929
import com.google.firestore.v1.ListDocumentsRequest;
3030
import java.util.Iterator;
31-
import java.util.Map;
3231
import javax.annotation.Nonnull;
3332
import javax.annotation.Nullable;
3433

@@ -171,13 +170,13 @@ public void remove() {
171170
* Adds a new document to this collection with the specified data, assigning it a document ID
172171
* automatically.
173172
*
174-
* @param fields A Map containing the data for the new document.
173+
* @param fields The Map or POJO containing the data for the new document.
175174
* @return An ApiFuture that will be resolved with the DocumentReference of the newly created
176175
* document.
177176
* @see #document()
178177
*/
179178
@Nonnull
180-
public ApiFuture<DocumentReference> add(@Nonnull final Map<String, Object> fields) {
179+
public ApiFuture<DocumentReference> add(@Nonnull final Object fields) {
181180
final DocumentReference documentReference = document();
182181
ApiFuture<WriteResult> createFuture = documentReference.create(fields);
183182

@@ -192,23 +191,6 @@ public DocumentReference apply(WriteResult writeResult) {
192191
MoreExecutors.directExecutor());
193192
}
194193

195-
/**
196-
* Adds a new document to this collection with the specified POJO as contents, assigning it a
197-
* document ID automatically.
198-
*
199-
* @param pojo The POJO that will be used to populate the contents of the document
200-
* @return An ApiFuture that will be resolved with the DocumentReference of the newly created
201-
* document.
202-
* @see #document()
203-
*/
204-
public ApiFuture<DocumentReference> add(Object pojo) {
205-
Object converted = CustomClassMapper.convertToPlainJavaTypes(pojo);
206-
if (!(converted instanceof Map)) {
207-
FirestoreException.invalidState("Can't set a document's data to an array or primitive");
208-
}
209-
return add((Map<String, Object>) converted);
210-
}
211-
212194
/** Returns a resource path pointing to this collection. */
213195
ResourcePath getResourcePath() {
214196
return options.getParentPath().append(options.getCollectionId());

google-cloud-firestore/src/main/java/com/google/cloud/firestore/DocumentReference.java

Lines changed: 7 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -135,24 +135,11 @@ public T apply(List<T> results) {
135135
MoreExecutors.directExecutor());
136136
}
137137

138-
/**
139-
* Creates a new Document at the DocumentReference's Location. It fails the write if the document
140-
* exists.
141-
*
142-
* @param fields A map of the fields and values for the document.
143-
* @return An ApiFuture that will be resolved when the write finishes.
144-
*/
145-
@Nonnull
146-
public ApiFuture<WriteResult> create(@Nonnull Map<String, Object> fields) {
147-
WriteBatch writeBatch = firestore.batch();
148-
return extractFirst(writeBatch.create(this, fields).commit());
149-
}
150-
151138
/**
152139
* Creates a new Document at the DocumentReference location. It fails the write if the document
153140
* exists.
154141
*
155-
* @param pojo A map of the fields and values for the document.
142+
* @param pojo The Map or POJO that will be used to populate the document contents.
156143
* @return An ApiFuture that will be resolved when the write finishes.
157144
*/
158145
@Nonnull
@@ -165,11 +152,12 @@ public ApiFuture<WriteResult> create(@Nonnull Object pojo) {
165152
* Overwrites the document referred to by this DocumentReference. If no document exists yet, it
166153
* will be created. If a document already exists, it will be overwritten.
167154
*
168-
* @param fields A map of the fields and values for the document.
155+
* @param fields The fields to write to the document (e.g. a Map or a POJO containing the desired
156+
* document contents).
169157
* @return An ApiFuture that will be resolved when the write finishes.
170158
*/
171159
@Nonnull
172-
public ApiFuture<WriteResult> set(@Nonnull Map<String, Object> fields) {
160+
public ApiFuture<WriteResult> set(@Nonnull Object fields) {
173161
WriteBatch writeBatch = firestore.batch();
174162
return extractFirst(writeBatch.set(this, fields).commit());
175163
}
@@ -179,45 +167,17 @@ public ApiFuture<WriteResult> set(@Nonnull Map<String, Object> fields) {
179167
* exist, it will be created. If you pass {@link SetOptions}, the provided data can be merged into
180168
* an existing document.
181169
*
182-
* @param fields A map of the fields and values for the document.
170+
* @param fields The fields to write on the document (e.g. a Map or a POJO containing the desired
171+
* document contents).
183172
* @param options An object to configure the set behavior.
184173
* @return An ApiFuture that will be resolved when the write finishes.
185174
*/
186175
@Nonnull
187-
public ApiFuture<WriteResult> set(
188-
@Nonnull Map<String, Object> fields, @Nonnull SetOptions options) {
176+
public ApiFuture<WriteResult> set(@Nonnull Object fields, @Nonnull SetOptions options) {
189177
WriteBatch writeBatch = firestore.batch();
190178
return extractFirst(writeBatch.set(this, fields, options).commit());
191179
}
192180

193-
/**
194-
* Overwrites the document referred to by this DocumentReference. If no document exists yet, it
195-
* will be created. If a document already exists, it will be overwritten.
196-
*
197-
* @param pojo The POJO that will be used to populate the document contents.
198-
* @return An ApiFuture that will be resolved when the write finishes.
199-
*/
200-
@Nonnull
201-
public ApiFuture<WriteResult> set(@Nonnull Object pojo) {
202-
WriteBatch writeBatch = firestore.batch();
203-
return extractFirst(writeBatch.set(this, pojo).commit());
204-
}
205-
206-
/**
207-
* Writes to the document referred to by this DocumentReference. If the document does not yet
208-
* exist, it will be created. If you pass {@link SetOptions}, the provided data can be merged into
209-
* an existing document.
210-
*
211-
* @param pojo The POJO that will be used to populate the document contents.
212-
* @param options An object to configure the set behavior.
213-
* @return An ApiFuture that will be resolved when the write finishes.
214-
*/
215-
@Nonnull
216-
public ApiFuture<WriteResult> set(@Nonnull Object pojo, @Nonnull SetOptions options) {
217-
WriteBatch writeBatch = firestore.batch();
218-
return extractFirst(writeBatch.set(this, pojo, options).commit());
219-
}
220-
221181
/**
222182
* Updates fields in the document referred to by this DocumentReference. If the document doesn't
223183
* exist yet, the update will fail.

google-cloud-firestore/src/main/java/com/google/cloud/firestore/DocumentSnapshot.java

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -262,6 +262,19 @@ public Object get(@Nonnull String field) {
262262
return get(FieldPath.fromDotSeparatedString(field));
263263
}
264264

265+
/**
266+
* Returns the value at the field, converted to a POJO, or null if the field or document doesn't
267+
* exist.
268+
*
269+
* @param field The path to the field
270+
* @param valueType The Java class to convert the field value to.
271+
* @return The value at the given field or null.
272+
*/
273+
@Nullable
274+
public <T> T get(@Nonnull String field, @Nonnull Class<T> valueType) {
275+
return get(FieldPath.fromDotSeparatedString(field), valueType);
276+
}
277+
265278
/**
266279
* Returns the value at the field or null if the field doesn't exist.
267280
*
@@ -280,6 +293,20 @@ public Object get(@Nonnull FieldPath fieldPath) {
280293
return convertToDateIfNecessary(decodedValue);
281294
}
282295

296+
/**
297+
* Returns the value at the field, converted to a POJO, or null if the field or document doesn't
298+
* exist.
299+
*
300+
* @param fieldPath The path to the field
301+
* @param valueType The Java class to convert the field value to.
302+
* @return The value at the given field or null.
303+
*/
304+
@Nullable
305+
public <T> T get(@Nonnull FieldPath fieldPath, Class<T> valueType) {
306+
Object data = get(fieldPath);
307+
return data == null ? null : CustomClassMapper.convertToCustomClass(data, valueType, docRef);
308+
}
309+
283310
private Object convertToDateIfNecessary(Object decodedValue) {
284311
if (decodedValue instanceof Timestamp) {
285312
if (!this.firestore.areTimestampsInSnapshotsEnabled()) {

google-cloud-firestore/src/main/java/com/google/cloud/firestore/UpdateBuilder.java

Lines changed: 14 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -105,13 +105,16 @@ private static Map<String, Object> expandObject(Map<FieldPath, Object> data) {
105105
* exists.
106106
*
107107
* @param documentReference The DocumentReference to create.
108-
* @param fields A map of the fields and values for the document.
108+
* @param fields The Map or POJO that will be used to populate the document contents.
109109
* @return The instance for chaining.
110110
*/
111111
@Nonnull
112-
public T create(
113-
@Nonnull DocumentReference documentReference, @Nonnull Map<String, Object> fields) {
114-
return performCreate(documentReference, fields);
112+
public T create(@Nonnull DocumentReference documentReference, @Nonnull Object fields) {
113+
Object data = CustomClassMapper.convertToPlainJavaTypes(fields);
114+
if (!(data instanceof Map)) {
115+
FirestoreException.invalidState("Can't set a document's data to an array or primitive");
116+
}
117+
return performCreate(documentReference, (Map<String, Object>) data);
115118
}
116119

117120
private T performCreate(
@@ -146,33 +149,16 @@ private Mutation addMutation() {
146149
return mutation;
147150
}
148151

149-
/**
150-
* Creates a new Document at the DocumentReference location. It fails the write if the document
151-
* exists.
152-
*
153-
* @param documentReference The DocumentReference to create.
154-
* @param pojo A map of the fields and values for the document.
155-
* @return The instance for chaining.
156-
*/
157-
@Nonnull
158-
public T create(@Nonnull DocumentReference documentReference, @Nonnull Object pojo) {
159-
Object data = CustomClassMapper.convertToPlainJavaTypes(pojo);
160-
if (!(data instanceof Map)) {
161-
FirestoreException.invalidState("Can't set a document's data to an array or primitive");
162-
}
163-
return performCreate(documentReference, (Map<String, Object>) data);
164-
}
165-
166152
/**
167153
* Overwrites the document referred to by this DocumentReference. If the document doesn't exist
168154
* yet, it will be created. If a document already exists, it will be overwritten.
169155
*
170156
* @param documentReference The DocumentReference to overwrite.
171-
* @param fields A map of the field paths and values for the document.
157+
* @param fields The Map or POJO that will be used to populate the document contents.
172158
* @return The instance for chaining.
173159
*/
174160
@Nonnull
175-
public T set(@Nonnull DocumentReference documentReference, @Nonnull Map<String, Object> fields) {
161+
public T set(@Nonnull DocumentReference documentReference, @Nonnull Object fields) {
176162
return set(documentReference, fields, SetOptions.OVERWRITE);
177163
}
178164

@@ -182,47 +168,16 @@ public T set(@Nonnull DocumentReference documentReference, @Nonnull Map<String,
182168
* an existing document.
183169
*
184170
* @param documentReference The DocumentReference to overwrite.
185-
* @param fields A map of the field paths and values for the document.
186-
* @param options An object to configure the set behavior.
187-
* @return The instance for chaining.
188-
*/
189-
@Nonnull
190-
public T set(
191-
@Nonnull DocumentReference documentReference,
192-
@Nonnull Map<String, Object> fields,
193-
@Nonnull SetOptions options) {
194-
return performSet(documentReference, fields, options);
195-
}
196-
197-
/**
198-
* Overwrites the document referred to by this DocumentReference. If the document doesn't exist
199-
* yet, it will be created. If a document already exists, it will be overwritten.
200-
*
201-
* @param documentReference The DocumentReference to overwrite.
202-
* @param pojo The POJO that will be used to populate the document contents.
203-
* @return The instance for chaining.
204-
*/
205-
@Nonnull
206-
public T set(@Nonnull DocumentReference documentReference, @Nonnull Object pojo) {
207-
return set(documentReference, pojo, SetOptions.OVERWRITE);
208-
}
209-
210-
/**
211-
* Overwrites the document referred to by this DocumentReference. If the document doesn't exist
212-
* yet, it will be created. If you pass {@link SetOptions}, the provided data can be merged into
213-
* an existing document.
214-
*
215-
* @param documentReference The DocumentReference to overwrite.
216-
* @param pojo The POJO that will be used to populate the document contents.
171+
* @param fields The Map or POJO that will be used to populate the document contents.
217172
* @param options An object to configure the set behavior.
218173
* @return The instance for chaining.
219174
*/
220175
@Nonnull
221176
public T set(
222177
@Nonnull DocumentReference documentReference,
223-
@Nonnull Object pojo,
178+
@Nonnull Object fields,
224179
@Nonnull SetOptions options) {
225-
Object data = CustomClassMapper.convertToPlainJavaTypes(pojo);
180+
Object data = CustomClassMapper.convertToPlainJavaTypes(fields);
226181
if (!(data instanceof Map)) {
227182
throw new IllegalArgumentException("Can't set a document's data to an array or primitive");
228183
}
@@ -471,8 +426,9 @@ private T performUpdate(
471426
@Nonnull FieldPath fieldPath,
472427
@Nullable Object value,
473428
Object[] moreFieldsAndValues) {
429+
Object data = CustomClassMapper.convertToPlainJavaTypes(value);
474430
Map<FieldPath, Object> fields = new HashMap<>();
475-
fields.put(fieldPath, value);
431+
fields.put(fieldPath, data);
476432

477433
Preconditions.checkArgument(
478434
moreFieldsAndValues.length % 2 == 0, "moreFieldsAndValues must be key-value pairs.");

google-cloud-firestore/src/test/java/com/google/cloud/firestore/LocalFirestoreHelper.java

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,8 +89,10 @@ public final class LocalFirestoreHelper {
8989
public static final Map<String, Value> SINGLE_FIELD_PROTO;
9090
public static final DocumentSnapshot SINGLE_FIELD_SNAPSHOT;
9191
public static final Value SINGLE_FIELD_VALUE;
92+
public static final SingleField UPDATE_SINGLE_FIELD_OBJECT;
9293
public static final Map<String, Object> UPDATED_FIELD_MAP;
9394
public static final Map<String, Value> UPDATED_FIELD_PROTO;
95+
public static final Map<String, Value> UPDATED_SINGLE_FIELD_PROTO;
9496

9597
public static final Map<String, Float> SINGLE_FLOAT_MAP;
9698
public static final Map<String, Value> SINGLE_FLOAT_PROTO;
@@ -731,10 +733,21 @@ public boolean equals(Object o) {
731733
Value.Builder singleFieldValueBuilder = Value.newBuilder();
732734
singleFieldValueBuilder.getMapValueBuilder().putAllFields(SINGLE_FIELD_PROTO);
733735
SINGLE_FIELD_VALUE = singleFieldValueBuilder.build();
736+
UPDATE_SINGLE_FIELD_OBJECT = new SingleField();
737+
UPDATE_SINGLE_FIELD_OBJECT.foo = "foobar";
734738

735739
UPDATED_FIELD_MAP = map("foo", (Object) "foobar");
736740
UPDATED_FIELD_PROTO = map("foo", Value.newBuilder().setStringValue("foobar").build());
737-
741+
UPDATED_SINGLE_FIELD_PROTO =
742+
ImmutableMap.<String, Value>builder()
743+
.put(
744+
"foo",
745+
Value.newBuilder()
746+
.setMapValue(
747+
MapValue.newBuilder()
748+
.putFields("foo", Value.newBuilder().setStringValue("foobar").build()))
749+
.build())
750+
.build();
738751
SERVER_TIMESTAMP_MAP = new HashMap<>();
739752
SERVER_TIMESTAMP_MAP.put("foo", FieldValue.serverTimestamp());
740753
SERVER_TIMESTAMP_MAP.put("inner", new HashMap<String, Object>());

google-cloud-firestore/src/test/java/com/google/cloud/firestore/WriteBatchTest.java

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616

1717
package com.google.cloud.firestore;
1818

19+
import static com.google.cloud.firestore.LocalFirestoreHelper.UPDATED_SINGLE_FIELD_PROTO;
20+
import static com.google.cloud.firestore.LocalFirestoreHelper.UPDATE_SINGLE_FIELD_OBJECT;
1921
import static com.google.cloud.firestore.LocalFirestoreHelper.commit;
2022
import static com.google.cloud.firestore.LocalFirestoreHelper.commitResponse;
2123
import static com.google.cloud.firestore.LocalFirestoreHelper.create;
@@ -109,6 +111,25 @@ public void updateDocument() throws Exception {
109111
assertEquals(commit(writes.toArray(new Write[] {})), commitRequest);
110112
}
111113

114+
@Test
115+
public void updateDocumentWithPOJO() throws Exception {
116+
doReturn(commitResponse(1, 0))
117+
.when(firestoreMock)
118+
.sendRequest(
119+
commitCapture.capture(), Matchers.<UnaryCallable<CommitRequest, CommitResponse>>any());
120+
121+
batch.update(documentReference, "foo", UPDATE_SINGLE_FIELD_OBJECT);
122+
assertEquals(1, batch.getMutationsSize());
123+
124+
List<WriteResult> writeResults = batch.commit().get();
125+
assertEquals(1, writeResults.size());
126+
127+
CommitRequest actual = commitCapture.getValue();
128+
CommitRequest expected =
129+
commit(update(UPDATED_SINGLE_FIELD_PROTO, Collections.singletonList("foo")));
130+
assertEquals(expected, actual);
131+
}
132+
112133
@Test
113134
public void setDocument() throws Exception {
114135
doReturn(commitResponse(4, 0))

0 commit comments

Comments
 (0)