diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/CustomClassMapper.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/CustomClassMapper.java index 6c685767e..96689ff6f 100644 --- a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/CustomClassMapper.java +++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/CustomClassMapper.java @@ -266,7 +266,21 @@ private static T deserializeToParameterizedType( Type genericType = type.getActualTypeArguments()[0]; if (o instanceof List) { List list = (List) o; - List result = new ArrayList<>(list.size()); + List result; + try { + result = + (rawType == List.class) + ? new ArrayList<>(list.size()) + : (List) rawType.getDeclaredConstructor().newInstance(); + } catch (InstantiationException + | IllegalAccessException + | NoSuchMethodException + | InvocationTargetException e) { + throw deserializeError( + context.errorPath, + String.format( + "Unable to deserialize to %s: %s", rawType.getSimpleName(), e.toString())); + } for (int i = 0; i < list.size(); i++) { result.add( deserializeToType( @@ -287,7 +301,21 @@ private static T deserializeToParameterizedType( "Only Maps with string keys are supported, but found Map with key type " + keyType); } Map map = expectMap(o, context); - HashMap result = new HashMap<>(); + HashMap result; + try { + result = + (rawType == Map.class) + ? new HashMap() + : (HashMap) rawType.getDeclaredConstructor().newInstance(); + } catch (InstantiationException + | IllegalAccessException + | NoSuchMethodException + | InvocationTargetException e) { + throw deserializeError( + context.errorPath, + String.format( + "Unable to deserialize to %s: %s", rawType.getSimpleName(), e.toString())); + } for (Map.Entry entry : map.entrySet()) { result.put( entry.getKey(), diff --git a/google-cloud-firestore/src/test/java/com/google/cloud/firestore/DocumentReferenceTest.java b/google-cloud-firestore/src/test/java/com/google/cloud/firestore/DocumentReferenceTest.java index 9464c4c22..0cce72d07 100644 --- a/google-cloud-firestore/src/test/java/com/google/cloud/firestore/DocumentReferenceTest.java +++ b/google-cloud-firestore/src/test/java/com/google/cloud/firestore/DocumentReferenceTest.java @@ -24,6 +24,8 @@ import static com.google.cloud.firestore.LocalFirestoreHelper.DOCUMENT_NAME; import static com.google.cloud.firestore.LocalFirestoreHelper.DOCUMENT_PATH; import static com.google.cloud.firestore.LocalFirestoreHelper.FIELD_TRANSFORM_COMMIT_RESPONSE; +import static com.google.cloud.firestore.LocalFirestoreHelper.FOO_LIST; +import static com.google.cloud.firestore.LocalFirestoreHelper.FOO_MAP; import static com.google.cloud.firestore.LocalFirestoreHelper.GEO_POINT; import static com.google.cloud.firestore.LocalFirestoreHelper.NESTED_CLASS_OBJECT; import static com.google.cloud.firestore.LocalFirestoreHelper.SERVER_TIMESTAMP_PROTO; @@ -69,10 +71,13 @@ import com.google.cloud.firestore.LocalFirestoreHelper.InvalidPOJO; import com.google.cloud.firestore.spi.v1.FirestoreRpc; import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.firestore.v1.ArrayValue; import com.google.firestore.v1.BatchGetDocumentsRequest; import com.google.firestore.v1.BatchGetDocumentsResponse; import com.google.firestore.v1.CommitRequest; import com.google.firestore.v1.CommitResponse; +import com.google.firestore.v1.MapValue; import com.google.firestore.v1.Value; import java.math.BigInteger; import java.util.ArrayList; @@ -1073,4 +1078,66 @@ public void deleteNestedFieldUsingFieldPath() throws Exception { Collections.emptyMap(), Collections.singletonList("`a.b`.`c.d`"))); assertEquals(expectedCommit, commitCapture.getValue()); } + + @Test + public void deserializeCustomList() throws ExecutionException, InterruptedException { + ImmutableMap CUSTOM_LIST_PROTO = + ImmutableMap.builder() + .put( + "fooList", + Value.newBuilder() + .setArrayValue( + ArrayValue.newBuilder() + .addValues( + Value.newBuilder() + .setMapValue( + MapValue.newBuilder().putAllFields(SINGLE_FIELD_PROTO)) + .build())) + .build()) + .build(); + doAnswer(getAllResponse(CUSTOM_LIST_PROTO)) + .when(firestoreMock) + .streamRequest( + getAllCapture.capture(), + streamObserverCapture.capture(), + Matchers.any()); + DocumentSnapshot snapshot = documentReference.get().get(); + LocalFirestoreHelper.CustomList customList = + snapshot.toObject(LocalFirestoreHelper.CustomList.class); + + assertEquals(FOO_LIST, customList.fooList); + assertEquals(SINGLE_FIELD_OBJECT, customList.fooList.get(0)); + } + + @Test + public void deserializeCustomMap() throws ExecutionException, InterruptedException { + ImmutableMap CUSTOM_MAP_PROTO = + ImmutableMap.builder() + .put( + "fooMap", + Value.newBuilder() + .setMapValue( + MapValue.newBuilder() + .putFields( + "customMap", + Value.newBuilder() + .setMapValue( + MapValue.newBuilder().putAllFields(SINGLE_FIELD_PROTO)) + .build()) + .build()) + .build()) + .build(); + doAnswer(getAllResponse(CUSTOM_MAP_PROTO)) + .when(firestoreMock) + .streamRequest( + getAllCapture.capture(), + streamObserverCapture.capture(), + Matchers.any()); + DocumentSnapshot snapshot = documentReference.get().get(); + LocalFirestoreHelper.CustomMap customMap = + snapshot.toObject(LocalFirestoreHelper.CustomMap.class); + + assertEquals(FOO_MAP, customMap.fooMap); + assertEquals(SINGLE_FIELD_OBJECT, customMap.fooMap.get("customMap")); + } } diff --git a/google-cloud-firestore/src/test/java/com/google/cloud/firestore/LocalFirestoreHelper.java b/google-cloud-firestore/src/test/java/com/google/cloud/firestore/LocalFirestoreHelper.java index db022ba95..47d63e376 100644 --- a/google-cloud-firestore/src/test/java/com/google/cloud/firestore/LocalFirestoreHelper.java +++ b/google-cloud-firestore/src/test/java/com/google/cloud/firestore/LocalFirestoreHelper.java @@ -143,6 +143,8 @@ public final class LocalFirestoreHelper { public static final Timestamp TIMESTAMP; public static final GeoPoint GEO_POINT; public static final Blob BLOB; + public static final FooList FOO_LIST = new FooList<>(); + public static final FooMap FOO_MAP = new FooMap<>(); public static final Precondition UPDATE_PRECONDITION; @@ -165,6 +167,30 @@ public boolean equals(Object o) { } } + public static class FooList extends ArrayList { + public FooList() { + super(); + } + } + + public static class CustomList { + public CustomList() {} + + public FooList fooList; + } + + public static class FooMap extends HashMap { + public FooMap() { + super(); + } + } + + public static class CustomMap { + public CustomMap() {} + + public FooMap fooMap; + } + public static class NestedClass { public SingleField first = new SingleField(); public AllSupportedTypes second = new AllSupportedTypes(); @@ -773,6 +799,8 @@ public boolean equals(Object o) { SINGLE_FIELD_MAP = map("foo", (Object) "bar"); SINGLE_FILED_MAP_WITH_DOT = map("c.d", (Object) "bar"); SINGLE_FIELD_OBJECT = new SingleField(); + FOO_LIST.add(SINGLE_FIELD_OBJECT); + FOO_MAP.put("customMap", SINGLE_FIELD_OBJECT); SINGLE_FIELD_PROTO = map("foo", Value.newBuilder().setStringValue("bar").build()); UPDATED_POJO_PROTO = map( diff --git a/google-cloud-firestore/src/test/java/com/google/cloud/firestore/it/ITSystemTest.java b/google-cloud-firestore/src/test/java/com/google/cloud/firestore/it/ITSystemTest.java index 512e88464..8036bc71b 100644 --- a/google-cloud-firestore/src/test/java/com/google/cloud/firestore/it/ITSystemTest.java +++ b/google-cloud-firestore/src/test/java/com/google/cloud/firestore/it/ITSystemTest.java @@ -16,6 +16,8 @@ package com.google.cloud.firestore.it; +import static com.google.cloud.firestore.LocalFirestoreHelper.FOO_LIST; +import static com.google.cloud.firestore.LocalFirestoreHelper.FOO_MAP; import static com.google.cloud.firestore.LocalFirestoreHelper.UPDATE_SINGLE_FIELD_OBJECT; import static com.google.cloud.firestore.LocalFirestoreHelper.map; import static com.google.common.truth.Truth.assertThat; @@ -1402,6 +1404,34 @@ public Void updateCallback(Transaction transaction) throws Exception { } } + @Test + public void deserializeCustomList() throws Exception { + LocalFirestoreHelper.CustomList customList = new LocalFirestoreHelper.CustomList(); + customList.fooList = FOO_LIST; + DocumentReference documentReference = randomColl.document("doc1"); + documentReference.set(customList).get(); + DocumentSnapshot documentSnapshots = documentReference.get().get(); + LocalFirestoreHelper.CustomList targetCustomList = + documentSnapshots.toObject(LocalFirestoreHelper.CustomList.class); + + assertEquals(FOO_LIST, targetCustomList.fooList); + assertEquals(SINGLE_FIELD_OBJECT, targetCustomList.fooList.get(0)); + } + + @Test + public void deserializeCustomMap() throws Exception { + LocalFirestoreHelper.CustomMap customMap = new LocalFirestoreHelper.CustomMap(); + customMap.fooMap = FOO_MAP; + DocumentReference documentReference = randomColl.document("doc1"); + documentReference.set(customMap).get(); + DocumentSnapshot documentSnapshots = documentReference.get().get(); + LocalFirestoreHelper.CustomMap targetCustomMap = + documentSnapshots.toObject(LocalFirestoreHelper.CustomMap.class); + + assertEquals(FOO_MAP, targetCustomMap.fooMap); + assertEquals(SINGLE_FIELD_OBJECT, targetCustomMap.fooMap.get("customMap")); + } + /** Wrapper around ApiStreamObserver that returns the results in a list. */ private static class StreamConsumer implements ApiStreamObserver { SettableApiFuture> done = SettableApiFuture.create();