From 28d2e8059d62eaf043a01918d09eb88021279f80 Mon Sep 17 00:00:00 2001 From: eranl <1707552+eranl@users.noreply.github.com> Date: Fri, 3 Mar 2023 02:15:52 +0200 Subject: [PATCH 01/51] feat: Add support for java record serialization --- google-cloud-firestore-jdk17/pom.xml | 121 ++ .../jdk17/annotation/DocumentId.java | 46 + .../jdk17/annotation/PropertyName.java | 30 + .../jdk17/annotation/ServerTimestamp.java | 31 + .../RecordDocumentReferenceTest.java | 358 ++++++ .../cloud/firestore/RecordMapperTest.java | 1131 +++++++++++++++++ .../firestore/jdk17/LocalFirestoreHelper.java | 413 ++++++ .../google/cloud/firestore/BeanMapper.java | 131 ++ .../cloud/firestore/CustomClassMapper.java | 171 +-- .../cloud/firestore/DeserializeContext.java | 73 ++ .../google/cloud/firestore/RecordMapper.java | 292 +++++ pom.xml | 1 + 12 files changed, 2663 insertions(+), 135 deletions(-) create mode 100644 google-cloud-firestore-jdk17/pom.xml create mode 100644 google-cloud-firestore-jdk17/src/main/java/com/google/cloud/firestore/jdk17/annotation/DocumentId.java create mode 100644 google-cloud-firestore-jdk17/src/main/java/com/google/cloud/firestore/jdk17/annotation/PropertyName.java create mode 100644 google-cloud-firestore-jdk17/src/main/java/com/google/cloud/firestore/jdk17/annotation/ServerTimestamp.java create mode 100644 google-cloud-firestore-jdk17/src/test/java/com/google/cloud/firestore/RecordDocumentReferenceTest.java create mode 100644 google-cloud-firestore-jdk17/src/test/java/com/google/cloud/firestore/RecordMapperTest.java create mode 100644 google-cloud-firestore-jdk17/src/test/java/com/google/cloud/firestore/jdk17/LocalFirestoreHelper.java create mode 100644 google-cloud-firestore/src/main/java/com/google/cloud/firestore/BeanMapper.java create mode 100644 google-cloud-firestore/src/main/java/com/google/cloud/firestore/DeserializeContext.java create mode 100644 google-cloud-firestore/src/main/java/com/google/cloud/firestore/RecordMapper.java diff --git a/google-cloud-firestore-jdk17/pom.xml b/google-cloud-firestore-jdk17/pom.xml new file mode 100644 index 000000000..e1b7cf5a2 --- /dev/null +++ b/google-cloud-firestore-jdk17/pom.xml @@ -0,0 +1,121 @@ + + + 4.0.0 + google-cloud-firestore-jdk17 + 3.8.3-SNAPSHOT + jar + Google Cloud Firestore JDK17 extension + https://github.com/googleapis/java-firestore + + Java idiomatic client for Google Cloud Firestore. + + + com.google.cloud + google-cloud-firestore-parent + 3.8.3-SNAPSHOT + + + google-cloud-firestore-jdk17 + 17 + 17 + + + + + + javax.annotation + javax.annotation-api + test + + + com.google.api + api-common + test + + + com.google.cloud + google-cloud-core + test + + + com.google.cloud + google-cloud-firestore + test + + + com.google.api.grpc + proto-google-cloud-firestore-v1 + test + + + com.google.guava + guava + test + + + com.google.code.gson + gson + test + + + junit + junit + test + + + org.mockito + mockito-all + 1.10.19 + test + + + + + com.google.api + gax + testlib + test + + + + + + + org.apache.maven.plugins + maven-javadoc-plugin + 3.5.0 + + protected + true + none + true + + + + + + + + + + maven-compiler-plugin + 3.10.1 + + 17 + 17 + UTF-8 + -Xlint:unchecked + -Xlint:deprecation + true + + + + + + + org.codehaus.mojo + flatten-maven-plugin + + + + diff --git a/google-cloud-firestore-jdk17/src/main/java/com/google/cloud/firestore/jdk17/annotation/DocumentId.java b/google-cloud-firestore-jdk17/src/main/java/com/google/cloud/firestore/jdk17/annotation/DocumentId.java new file mode 100644 index 000000000..16729b7af --- /dev/null +++ b/google-cloud-firestore-jdk17/src/main/java/com/google/cloud/firestore/jdk17/annotation/DocumentId.java @@ -0,0 +1,46 @@ +/* + * Copyright 2019 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.cloud.firestore.jdk17.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Annotation used to mark a record component to be automatically populated with the document's ID + * when the record is created from a Cloud Firestore document (for example, via + * DocumentSnapshot#toObject). + * + * + * + *

When using a record to write to a document (via DocumentReference#set or WriteBatch#set), the + * property annotated by @DocumentId is ignored, which allows writing the record back to any + * document, even if it's not the origin of the record. + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.RECORD_COMPONENT) +public @interface DocumentId {} diff --git a/google-cloud-firestore-jdk17/src/main/java/com/google/cloud/firestore/jdk17/annotation/PropertyName.java b/google-cloud-firestore-jdk17/src/main/java/com/google/cloud/firestore/jdk17/annotation/PropertyName.java new file mode 100644 index 000000000..b453701c3 --- /dev/null +++ b/google-cloud-firestore-jdk17/src/main/java/com/google/cloud/firestore/jdk17/annotation/PropertyName.java @@ -0,0 +1,30 @@ +/* + * Copyright 2017 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.cloud.firestore.jdk17.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** Marks a component to be renamed when serialized. */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.RECORD_COMPONENT) +public @interface PropertyName { + + String value(); +} diff --git a/google-cloud-firestore-jdk17/src/main/java/com/google/cloud/firestore/jdk17/annotation/ServerTimestamp.java b/google-cloud-firestore-jdk17/src/main/java/com/google/cloud/firestore/jdk17/annotation/ServerTimestamp.java new file mode 100644 index 000000000..f94f84389 --- /dev/null +++ b/google-cloud-firestore-jdk17/src/main/java/com/google/cloud/firestore/jdk17/annotation/ServerTimestamp.java @@ -0,0 +1,31 @@ +/* + * Copyright 2017 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.cloud.firestore.jdk17.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Annotation used to mark a timestamp component as being populated via Server Timestamps. If a + * record being written contains null for a @ServerTimestamp annotated component, it will be + * replaced with a server-generated timestamp. + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.RECORD_COMPONENT) +public @interface ServerTimestamp {} diff --git a/google-cloud-firestore-jdk17/src/test/java/com/google/cloud/firestore/RecordDocumentReferenceTest.java b/google-cloud-firestore-jdk17/src/test/java/com/google/cloud/firestore/RecordDocumentReferenceTest.java new file mode 100644 index 000000000..52f6e590f --- /dev/null +++ b/google-cloud-firestore-jdk17/src/test/java/com/google/cloud/firestore/RecordDocumentReferenceTest.java @@ -0,0 +1,358 @@ +/* + * Copyright 2017 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.cloud.firestore; + +import static com.google.cloud.firestore.jdk17.LocalFirestoreHelper.*; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.doReturn; + +import com.google.api.gax.rpc.ResponseObserver; +import com.google.api.gax.rpc.ServerStreamingCallable; +import com.google.api.gax.rpc.UnaryCallable; +import com.google.cloud.firestore.jdk17.LocalFirestoreHelper; +import com.google.cloud.firestore.spi.v1.FirestoreRpc; +import com.google.firestore.v1.BatchGetDocumentsRequest; +import com.google.firestore.v1.CommitRequest; +import com.google.firestore.v1.CommitResponse; +import com.google.firestore.v1.Value; +import java.math.BigInteger; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.Matchers; +import org.mockito.Mockito; +import org.mockito.Spy; +import org.mockito.runners.MockitoJUnitRunner; + +@RunWith(MockitoJUnitRunner.class) +public class RecordDocumentReferenceTest { + + @Spy + private final FirestoreImpl firestoreMock = + new FirestoreImpl( + FirestoreOptions.newBuilder().setProjectId("test-project").build(), + Mockito.mock(FirestoreRpc.class)); + + @Captor private ArgumentCaptor commitCapture; + + @Captor private ArgumentCaptor getAllCapture; + + @Captor private ArgumentCaptor> streamObserverCapture; + + private DocumentReference documentReference; + + @Before + public void before() { + documentReference = firestoreMock.document("coll/doc"); + } + + @Test + public void serializeBasicTypes() throws Exception { + doReturn(SINGLE_WRITE_COMMIT_RESPONSE) + .when(firestoreMock) + .sendRequest( + commitCapture.capture(), Matchers.>any()); + + documentReference.set(ALL_SUPPORTED_TYPES_OBJECT).get(); + + var expectedCommit = commit(set(ALL_SUPPORTED_TYPES_PROTO)); + assertCommitEquals(expectedCommit, commitCapture.getAllValues().get(0)); + } + + @Test + public void doesNotSerializeAdvancedNumberTypes() { + Map expectedErrorMessages = new HashMap<>(); + + var record = new InvalidRecord(new BigInteger("0"), null, null); + expectedErrorMessages.put( + record, + "Could not serialize object. Numbers of type BigInteger are not supported, please use an int, long, float, double or BigDecimal (found in field 'bigIntegerValue')"); + + record = new InvalidRecord(null, (byte) 0, null); + expectedErrorMessages.put( + record, + "Could not serialize object. Numbers of type Byte are not supported, please use an int, long, float, double or BigDecimal (found in field 'byteValue')"); + + record = new InvalidRecord(null, null, (short) 0); + expectedErrorMessages.put( + record, + "Could not serialize object. Numbers of type Short are not supported, please use an int, long, float, double or BigDecimal (found in field 'shortValue')"); + + for (var testCase : expectedErrorMessages.entrySet()) { + try { + documentReference.set(testCase.getKey()); + fail(); + } catch (IllegalArgumentException e) { + assertEquals(testCase.getValue(), e.getMessage()); + } + } + } + + @Test + public void doesNotDeserializeAdvancedNumberTypes() throws Exception { + var fieldNamesToTypeNames = + map("bigIntegerValue", "BigInteger", "shortValue", "Short", "byteValue", "Byte"); + + for (var testCase : fieldNamesToTypeNames.entrySet()) { + var fieldName = testCase.getKey(); + var typeName = testCase.getValue(); + var response = map(fieldName, Value.newBuilder().setIntegerValue(0).build()); + + doAnswer(getAllResponse(response)) + .when(firestoreMock) + .streamRequest( + getAllCapture.capture(), + streamObserverCapture.capture(), + Matchers.any()); + + var snapshot = documentReference.get().get(); + try { + snapshot.toObject(InvalidRecord.class); + fail(); + } catch (RuntimeException e) { + assertEquals( + String.format( + "Could not deserialize object. Deserializing values to %s is not supported (found in field '%s')", + typeName, fieldName), + e.getMessage()); + } + } + } + + @Test + public void createDocument() throws Exception { + doReturn(SINGLE_WRITE_COMMIT_RESPONSE) + .when(firestoreMock) + .sendRequest( + commitCapture.capture(), Matchers.>any()); + + documentReference.create(SINGLE_COMPONENT_OBJECT).get(); + + CommitRequest expectedCommit = commit(create(SINGLE_COMPONENT_PROTO)); + + List commitRequests = commitCapture.getAllValues(); + assertCommitEquals(expectedCommit, commitRequests.get(0)); + } + + @Test + public void createWithServerTimestamp() throws Exception { + doReturn(SINGLE_WRITE_COMMIT_RESPONSE) + .when(firestoreMock) + .sendRequest( + commitCapture.capture(), Matchers.>any()); + + documentReference.create(LocalFirestoreHelper.SERVER_TIMESTAMP_OBJECT).get(); + + var create = + commit( + create(Collections.emptyMap()), + transform("foo", serverTimestamp(), "inner.bar", serverTimestamp())); + + var commitRequests = commitCapture.getAllValues(); + assertCommitEquals(create, commitRequests.get(0)); + } + + @Test + public void setWithServerTimestamp() throws Exception { + doReturn(FIELD_TRANSFORM_COMMIT_RESPONSE) + .when(firestoreMock) + .sendRequest( + commitCapture.capture(), Matchers.>any()); + + documentReference.set(LocalFirestoreHelper.SERVER_TIMESTAMP_OBJECT).get(); + + var set = + commit( + set(SERVER_TIMESTAMP_PROTO), + transform("foo", serverTimestamp(), "inner.bar", serverTimestamp())); + + var commitRequests = commitCapture.getAllValues(); + assertCommitEquals(set, commitRequests.get(0)); + } + + @Test + public void mergeWithServerTimestamps() throws Exception { + doReturn(SINGLE_WRITE_COMMIT_RESPONSE) + .when(firestoreMock) + .sendRequest( + commitCapture.capture(), Matchers.>any()); + + documentReference + .set(LocalFirestoreHelper.SERVER_TIMESTAMP_OBJECT, SetOptions.mergeFields("inner.bar")) + .get(); + + var set = + commit( + set(SERVER_TIMESTAMP_PROTO, new ArrayList<>()), + transform("inner.bar", serverTimestamp())); + + var commitRequests = commitCapture.getAllValues(); + assertCommitEquals(set, commitRequests.get(0)); + } + + @Test + public void setDocumentWithMerge() throws Exception { + doReturn(SINGLE_WRITE_COMMIT_RESPONSE) + .when(firestoreMock) + .sendRequest( + commitCapture.capture(), Matchers.>any()); + + documentReference.set(SINGLE_COMPONENT_OBJECT, SetOptions.merge()).get(); + documentReference.set(ALL_SUPPORTED_TYPES_OBJECT, SetOptions.mergeFields("foo")).get(); + documentReference + .set(ALL_SUPPORTED_TYPES_OBJECT, SetOptions.mergeFields(Arrays.asList("foo"))) + .get(); + documentReference + .set( + ALL_SUPPORTED_TYPES_OBJECT, + SetOptions.mergeFieldPaths(Arrays.asList(FieldPath.of("foo")))) + .get(); + + var expectedCommit = commit(set(SINGLE_COMPONENT_PROTO, Arrays.asList("foo"))); + + for (var i = 0; i < 4; ++i) { + assertCommitEquals(expectedCommit, commitCapture.getAllValues().get(i)); + } + } + + @Test + public void setDocumentWithNestedMerge() throws Exception { + doReturn(SINGLE_WRITE_COMMIT_RESPONSE) + .when(firestoreMock) + .sendRequest( + commitCapture.capture(), Matchers.>any()); + + documentReference.set(NESTED_RECORD_OBJECT, SetOptions.mergeFields("first.foo")).get(); + documentReference + .set(NESTED_RECORD_OBJECT, SetOptions.mergeFields(Arrays.asList("first.foo"))) + .get(); + documentReference + .set( + NESTED_RECORD_OBJECT, + SetOptions.mergeFieldPaths(Arrays.asList(FieldPath.of("first", "foo")))) + .get(); + + Map nestedUpdate = new HashMap<>(); + var nestedProto = Value.newBuilder(); + nestedProto.getMapValueBuilder().putAllFields(SINGLE_COMPONENT_PROTO); + nestedUpdate.put("first", nestedProto.build()); + + var expectedCommit = commit(set(nestedUpdate, Arrays.asList("first.foo"))); + + for (var i = 0; i < 3; ++i) { + assertCommitEquals(expectedCommit, commitCapture.getAllValues().get(i)); + } + } + + @Test + public void setMultipleFieldsWithMerge() throws Exception { + doReturn(SINGLE_WRITE_COMMIT_RESPONSE) + .when(firestoreMock) + .sendRequest( + commitCapture.capture(), Matchers.>any()); + + documentReference + .set( + NESTED_RECORD_OBJECT, + SetOptions.mergeFields("first.foo", "second.foo", "second.trueValue")) + .get(); + + Map nestedUpdate = new HashMap<>(); + var nestedProto = Value.newBuilder(); + nestedProto.getMapValueBuilder().putAllFields(SINGLE_COMPONENT_PROTO); + nestedUpdate.put("first", nestedProto.build()); + nestedProto + .getMapValueBuilder() + .putFields("trueValue", Value.newBuilder().setBooleanValue(true).build()); + nestedUpdate.put("second", nestedProto.build()); + + var expectedCommit = + commit(set(nestedUpdate, Arrays.asList("first.foo", "second.foo", "second.trueValue"))); + + assertCommitEquals(expectedCommit, commitCapture.getValue()); + } + + @Test + public void setNestedMapWithMerge() throws Exception { + doReturn(SINGLE_WRITE_COMMIT_RESPONSE) + .when(firestoreMock) + .sendRequest( + commitCapture.capture(), Matchers.>any()); + + documentReference.set(NESTED_RECORD_OBJECT, SetOptions.mergeFields("first", "second")).get(); + + Map nestedUpdate = new HashMap<>(); + var nestedProto = Value.newBuilder(); + nestedProto.getMapValueBuilder().putAllFields(SINGLE_COMPONENT_PROTO); + nestedUpdate.put("first", nestedProto.build()); + nestedProto.getMapValueBuilder().putAllFields(ALL_SUPPORTED_TYPES_PROTO); + nestedUpdate.put("second", nestedProto.build()); + + var expectedCommit = commit(set(nestedUpdate, Arrays.asList("first", "second"))); + assertCommitEquals(expectedCommit, commitCapture.getValue()); + } + + @Test + public void extractFieldMaskFromMerge() throws Exception { + doReturn(SINGLE_WRITE_COMMIT_RESPONSE) + .when(firestoreMock) + .sendRequest( + commitCapture.capture(), Matchers.>any()); + + documentReference.set(NESTED_RECORD_OBJECT, SetOptions.merge()).get(); + + Map nestedUpdate = new HashMap<>(); + var nestedProto = Value.newBuilder(); + nestedProto.getMapValueBuilder().putAllFields(SINGLE_COMPONENT_PROTO); + nestedUpdate.put("first", nestedProto.build()); + nestedProto.getMapValueBuilder().putAllFields(ALL_SUPPORTED_TYPES_PROTO); + nestedUpdate.put("second", nestedProto.build()); + + var updateMask = + Arrays.asList( + "first.foo", + "second.arrayValue", + "second.bytesValue", + "second.dateValue", + "second.doubleValue", + "second.falseValue", + "second.foo", + "second.geoPointValue", + "second.infValue", + "second.longValue", + "second.nanValue", + "second.negInfValue", + "second.nullValue", + "second.objectValue.foo", + "second.timestampValue", + "second.trueValue", + "second.model.foo"); + + var expectedCommit = commit(set(nestedUpdate, updateMask)); + assertCommitEquals(expectedCommit, commitCapture.getValue()); + } +} diff --git a/google-cloud-firestore-jdk17/src/test/java/com/google/cloud/firestore/RecordMapperTest.java b/google-cloud-firestore-jdk17/src/test/java/com/google/cloud/firestore/RecordMapperTest.java new file mode 100644 index 000000000..af5e742d3 --- /dev/null +++ b/google-cloud-firestore-jdk17/src/test/java/com/google/cloud/firestore/RecordMapperTest.java @@ -0,0 +1,1131 @@ +/* + * Copyright 2019 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.cloud.firestore; + +import java.io.Serializable; +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mockito; +import org.mockito.Spy; +import org.mockito.runners.MockitoJUnitRunner; + +import com.google.cloud.firestore.annotation.ThrowOnExtraProperties; +import com.google.cloud.firestore.jdk17.annotation.DocumentId; +import com.google.cloud.firestore.jdk17.annotation.PropertyName; +import com.google.cloud.firestore.spi.v1.FirestoreRpc; +import com.google.common.collect.ImmutableList; +import com.google.firestore.v1.DatabaseRootName; + +import static com.google.cloud.firestore.jdk17.LocalFirestoreHelper.fromSingleQuotedString; +import static com.google.cloud.firestore.jdk17.LocalFirestoreHelper.mapAnyType; +import static org.junit.Assert.*; + +@SuppressWarnings({"unused", "WeakerAccess", "SpellCheckingInspection"}) +@RunWith(MockitoJUnitRunner.class) +public class RecordMapperTest { + + @Spy + private final FirestoreImpl firestoreMock = + new FirestoreImpl( + FirestoreOptions.newBuilder().setProjectId("test-project").build(), + Mockito.mock(FirestoreRpc.class)); + + private static final double EPSILON = 0.0003; + + public record StringBean ( + String value + ){} + + public record DoubleBean ( + double value + ){} + + public record BigDecimalBean ( + BigDecimal value + ){} + + public record FloatBean ( + float value + ){} + + public record LongBean ( + long value + ){} + + public record IntBean ( + int value + ){} + + public record BooleanBean ( + boolean value + ){} + + public record ShortBean ( + short value + ){} + + public record ByteBean ( + byte value + ){} + + public record CharBean ( + char value + ){} + + public record IntArrayBean ( + int[] values + ){} + + public record StringArrayBean ( + String[] values + ){} + + public record XMLAndURLBean ( + String XMLAndURL + ){} + + public record CaseSensitiveFieldBean1 ( + String VALUE + ){} + + public record CaseSensitiveFieldBean2 ( + String value + ){} + + public record CaseSensitiveFieldBean3 ( + String Value + ){} + + public record CaseSensitiveFieldBean4 ( + String valUE + ){} + + public record NestedBean ( + StringBean bean + ){} + + public record ObjectBean ( + Object value + ){} + + public record GenericBean ( + B value + ){} + + public record DoubleGenericBean ( + A valueA, + B valueB + ){} + + public record ListBean ( + List values + ){} + + public record SetBean ( + Set values + ){} + + public record CollectionBean ( + Collection values + ){} + + public record MapBean ( + Map values + ){} + + /** + * This form is not terribly useful in Java, but Kotlin Maps are immutable and are rewritten into + * this form (b/67470108 has more details). + */ + public record UpperBoundedMapBean ( + Map values + ){} + + public record MultiBoundedMapBean ( + Map values + ){} + + public record MultiBoundedMapHolderBean ( + MultiBoundedMapBean map + ){} + + public record UnboundedMapBean ( + Map values + ){} + + public record UnboundedTypeVariableMapBean ( + Map values + ){} + + public record UnboundedTypeVariableMapHolderBean ( + UnboundedTypeVariableMapBean map + ){} + + public record NestedListBean ( + List values + ){} + + public record NestedMapBean ( + Map values + ){} + + public record IllegalKeyMapBean ( + Map values + ){} + + @ThrowOnExtraProperties + public record ThrowOnUnknownPropertiesBean ( + String value + ){} + + @ThrowOnExtraProperties + public record NoFieldBean( + ){} + + public record PropertyNameBean ( + @PropertyName("my_key") + String key, + + @PropertyName("my_value") + String value + ){} + + @SuppressWarnings({"NonAsciiCharacters"}) + public record UnicodeBean ( + String 漢字 + ){} + + private static T deserialize(String jsonString, Class clazz) { + return deserialize(jsonString, clazz, /*docRef=*/ null); + } + + private static T deserialize(Map json, Class clazz) { + return deserialize(json, clazz, /*docRef=*/ null); + } + + private static T deserialize(String jsonString, Class clazz, DocumentReference docRef) { + var json = fromSingleQuotedString(jsonString); + return CustomClassMapper.convertToCustomClass(json, clazz, docRef); + } + + private static T deserialize( + Map json, Class clazz, DocumentReference docRef) { + return CustomClassMapper.convertToCustomClass(json, clazz, docRef); + } + + private static Object serialize(Object object) { + return CustomClassMapper.convertToPlainJavaTypes(object); + } + + private static void assertJson(String expected, Object actual) { + assertEquals(fromSingleQuotedString(expected), actual); + } + + private static void assertExceptionContains(String partialMessage, Runnable run) { + try { + run.run(); + fail("Expected exception not thrown"); + } catch (RuntimeException e) { + assertTrue(e.getMessage().contains(partialMessage)); + } + } + + private static T convertToCustomClass( + Object object, Class clazz, DocumentReference docRef) { + return CustomClassMapper.convertToCustomClass(object, clazz, docRef); + } + + private static T convertToCustomClass(Object object, Class clazz) { + return CustomClassMapper.convertToCustomClass(object, clazz, null); + } + + @Test + public void primitiveDeserializeString() { + var bean = deserialize("{'value': 'foo'}", StringBean.class); + assertEquals("foo", bean.value()); + + // Double + try { + deserialize("{'value': 1.1}", StringBean.class); + fail("Should throw"); + } catch (RuntimeException e) { // ignore + } + + // Int + try { + deserialize("{'value': 1}", StringBean.class); + fail("Should throw"); + } catch (RuntimeException e) { // ignore + } + + // Long + try { + deserialize("{'value': 1234567890123}", StringBean.class); + fail("Should throw"); + } catch (RuntimeException e) { // ignore + } + + // Boolean + try { + deserialize("{'value': true}", StringBean.class); + fail("Should throw"); + } catch (RuntimeException e) { // ignore + } + } + + @Test + public void primitiveDeserializeBoolean() { + var beanBoolean = deserialize("{'value': true}", BooleanBean.class); + assertEquals(true, beanBoolean.value()); + + // Double + try { + deserialize("{'value': 1.1}", BooleanBean.class); + fail("Should throw"); + } catch (RuntimeException e) { // ignore + } + + // Long + try { + deserialize("{'value': 1234567890123}", BooleanBean.class); + fail("Should throw"); + } catch (RuntimeException e) { // ignore + } + + // Int + try { + deserialize("{'value': 1}", BooleanBean.class); + fail("Should throw"); + } catch (RuntimeException e) { // ignore + } + + // String + try { + deserialize("{'value': 'foo'}", BooleanBean.class); + fail("Should throw"); + } catch (RuntimeException e) { // ignore + } + } + + @Test + public void primitiveDeserializeDouble() { + var beanDouble = deserialize("{'value': 1.1}", DoubleBean.class); + assertEquals(1.1, beanDouble.value(), EPSILON); + + // Int + var beanInt = deserialize("{'value': 1}", DoubleBean.class); + assertEquals(1, beanInt.value(), EPSILON); + // Long + var beanLong = deserialize("{'value': 1234567890123}", DoubleBean.class); + assertEquals(1234567890123L, beanLong.value(), EPSILON); + + // Boolean + try { + deserialize("{'value': true}", DoubleBean.class); + fail("Should throw"); + } catch (RuntimeException e) { // ignore + } + + // String + try { + deserialize("{'value': 'foo'}", DoubleBean.class); + fail("Should throw"); + } catch (RuntimeException e) { // ignore + } + } + + @Test + public void primitiveDeserializeBigDecimal() { + var beanBigdecimal = deserialize("{'value': 123}", BigDecimalBean.class); + assertEquals(BigDecimal.valueOf(123.0), beanBigdecimal.value()); + + beanBigdecimal = deserialize("{'value': '123'}", BigDecimalBean.class); + assertEquals(BigDecimal.valueOf(123), beanBigdecimal.value()); + + // Int + var beanInt = + deserialize(Collections.singletonMap("value", 1), BigDecimalBean.class); + assertEquals(BigDecimal.valueOf(1), beanInt.value()); + + // Long + var beanLong = + deserialize(Collections.singletonMap("value", 1234567890123L), BigDecimalBean.class); + assertEquals(BigDecimal.valueOf(1234567890123L), beanLong.value()); + + // Double + var beanDouble = + deserialize(Collections.singletonMap("value", 1.1), BigDecimalBean.class); + assertEquals(BigDecimal.valueOf(1.1), beanDouble.value()); + + // BigDecimal + var beanBigDecimal = + deserialize( + Collections.singletonMap("value", BigDecimal.valueOf(1.2)), BigDecimalBean.class); + assertEquals(BigDecimal.valueOf(1.2), beanBigDecimal.value()); + + // Boolean + try { + deserialize("{'value': true}", BigDecimalBean.class); + fail("Should throw"); + } catch (RuntimeException e) { // ignore + } + + // String + try { + deserialize("{'value': 'foo'}", BigDecimalBean.class); + fail("Should throw"); + } catch (RuntimeException e) { // ignore + } + } + + @Test + public void primitiveDeserializeFloat() { + var beanFloat = deserialize("{'value': 1.1}", FloatBean.class); + assertEquals(1.1, beanFloat.value(), EPSILON); + + // Int + var beanInt = deserialize(Collections.singletonMap("value", 1), FloatBean.class); + assertEquals(1, beanInt.value(), EPSILON); + // Long + var beanLong = + deserialize(Collections.singletonMap("value", 1234567890123L), FloatBean.class); + assertEquals((float) 1234567890123L, beanLong.value(), EPSILON); + + // Boolean + try { + deserialize("{'value': true}", FloatBean.class); + fail("Should throw"); + } catch (RuntimeException e) { // ignore + } + + // String + try { + deserialize("{'value': 'foo'}", FloatBean.class); + fail("Should throw"); + } catch (RuntimeException e) { // ignore + } + } + + @Test + public void primitiveDeserializeInt() { + var beanInt = deserialize("{'value': 1}", IntBean.class); + assertEquals(1, beanInt.value()); + + // Double + var beanDouble = deserialize("{'value': 1.1}", IntBean.class); + assertEquals(1, beanDouble.value()); + + // Large doubles + try { + deserialize("{'value': 1e10}", IntBean.class); + fail("Should throw"); + } catch (RuntimeException e) { // ignore + } + + // Long + try { + deserialize("{'value': 1234567890123}", IntBean.class); + fail("Should throw"); + } catch (RuntimeException e) { // ignore + } + + // Boolean + try { + deserialize("{'value': true}", IntBean.class); + fail("Should throw"); + } catch (RuntimeException e) { // ignore + } + + // String + try { + deserialize("{'value': 'foo'}", IntBean.class); + fail("Should throw"); + } catch (RuntimeException e) { // ignore + } + } + + @Test + public void primitiveDeserializeLong() { + var beanLong = deserialize("{'value': 1234567890123}", LongBean.class); + assertEquals(1234567890123L, beanLong.value()); + + // Int + var beanInt = deserialize("{'value': 1}", LongBean.class); + assertEquals(1, beanInt.value()); + + // Double + var beanDouble = deserialize("{'value': 1.1}", LongBean.class); + assertEquals(1, beanDouble.value()); + + // Large doubles + try { + deserialize("{'value': 1e300}", LongBean.class); + fail("Should throw"); + } catch (RuntimeException e) { // ignore + } + + // Boolean + try { + deserialize("{'value': true}", LongBean.class); + fail("Should throw"); + } catch (RuntimeException e) { // ignore + } + + // String + try { + deserialize("{'value': 'foo'}", LongBean.class); + fail("Should throw"); + } catch (RuntimeException e) { // ignore + } + } + + @Test + public void primitiveDeserializeWrongTypeMap() { + var expectedExceptionMessage = + ".* Failed to convert value of type .*Map to String \\(found in field 'value'\\).*"; + Throwable exception = + assertThrows( + RuntimeException.class, + () -> deserialize("{'value': {'foo': 'bar'}}", StringBean.class)); + assertTrue(exception.getMessage().matches(expectedExceptionMessage)); + } + + @Test + public void primitiveDeserializeWrongTypeList() { + assertExceptionContains( + "Failed to convert value of type java.util.ArrayList to String" + + " (found in field 'value')", + () -> deserialize("{'value': ['foo']}", StringBean.class)); + } + + @Test + public void noFieldDeserialize() { + assertExceptionContains( + "No properties to serialize found on class " + + "com.google.cloud.firestore.RecordMapperTest$NoFieldBean", + () -> deserialize("{'value': 'foo'}", NoFieldBean.class)); + } + + @Test + public void throwOnUnknownProperties() { + assertExceptionContains( + "No accessor for unknown found on class " + + "com.google.cloud.firestore.RecordMapperTest$ThrowOnUnknownPropertiesBean", + () -> + deserialize("{'value': 'foo', 'unknown': 'bar'}", ThrowOnUnknownPropertiesBean.class)); + } + + @Test + public void XMLAndURLBean() { + var bean = + deserialize("{'XMLAndURL': 'foo'}", XMLAndURLBean.class); + assertEquals("foo", bean.XMLAndURL()); + } + + public record AllCapsDefaultHandlingBean ( + String UUID + ){} + + @Test + public void allCapsSerializesToUppercaseByDefault() { + var bean = new AllCapsDefaultHandlingBean("value"); + assertJson("{'UUID': 'value'}", serialize(bean)); + var deserialized = + deserialize("{'UUID': 'value'}", AllCapsDefaultHandlingBean.class); + assertEquals("value", deserialized.UUID()); + } + + public record AllCapsWithPropertyName ( + @PropertyName("uuid") + String UUID + ){} + + @Test + public void allCapsWithPropertyNameSerializesToLowercase() { + var bean = new AllCapsWithPropertyName("value"); + assertJson("{'uuid': 'value'}", serialize(bean)); + var deserialized = + deserialize("{'uuid': 'value'}", AllCapsWithPropertyName.class); + assertEquals("value", deserialized.UUID()); + } + + @Test + public void nestedParsingWorks() { + var bean = deserialize("{'bean': {'value': 'foo'}}", NestedBean.class); + assertEquals("foo", bean.bean().value()); + } + + @Test + public void beansCanContainLists() { + var bean = deserialize("{'values': ['foo', 'bar']}", ListBean.class); + assertEquals(Arrays.asList("foo", "bar"), bean.values()); + } + + @Test + public void beansCanContainMaps() { + var bean = deserialize("{'values': {'foo': 'bar'}}", MapBean.class); + var expected = fromSingleQuotedString("{'foo': 'bar'}"); + assertEquals(expected, bean.values()); + } + + @Test + public void beansCanContainUpperBoundedMaps() { + var date = new Date(1491847082123L); + var source = mapAnyType("values", mapAnyType("foo", date)); + var bean = convertToCustomClass(source, UpperBoundedMapBean.class); + var expected = mapAnyType("foo", date); + assertEquals(expected, bean.values()); + } + + @Test + public void beansCanContainMultiBoundedMaps() { + var date = new Date(1491847082123L); + var source = mapAnyType("map", mapAnyType("values", mapAnyType("foo", date))); + var bean = convertToCustomClass(source, MultiBoundedMapHolderBean.class); + + var expected = mapAnyType("foo", date); + assertEquals(expected, bean.map().values()); + } + + @Test + public void beansCanContainUnboundedMaps() { + var bean = deserialize("{'values': {'foo': 'bar'}}", UnboundedMapBean.class); + var expected = mapAnyType("foo", "bar"); + assertEquals(expected, bean.values()); + } + + @Test + public void beansCanContainUnboundedTypeVariableMaps() { + var source = mapAnyType("map", mapAnyType("values", mapAnyType("foo", "bar"))); + var bean = + convertToCustomClass(source, UnboundedTypeVariableMapHolderBean.class); + + var expected = mapAnyType("foo", "bar"); + assertEquals(expected, bean.map().values()); + } + + @Test + public void beansCanContainNestedUnboundedMaps() { + var bean = + deserialize("{'values': {'foo': {'bar': 'baz'}}}", UnboundedMapBean.class); + var expected = mapAnyType("foo", mapAnyType("bar", "baz")); + assertEquals(expected, bean.values()); + } + + @Test + public void beansCanContainBeanLists() { + var bean = deserialize("{'values': [{'value': 'foo'}]}", NestedListBean.class); + assertEquals(1, bean.values().size()); + assertEquals("foo", bean.values().get(0).value()); + } + + @Test + public void beansCanContainBeanMaps() { + var bean = deserialize("{'values': {'key': {'value': 'foo'}}}", NestedMapBean.class); + assertEquals(1, bean.values().size()); + assertEquals("foo", bean.values().get("key").value()); + } + + @Test + public void beanMapsMustHaveStringKeys() { + assertExceptionContains( + "Only Maps with string keys are supported, but found Map with key type class " + + "java.lang.Integer (found in field 'values')", + () -> deserialize("{'values': {'1': 'bar'}}", IllegalKeyMapBean.class)); + } + + @Test + public void serializeStringBean() { + var bean = new StringBean("foo"); + assertJson("{'value': 'foo'}", serialize(bean)); + } + + @Test + public void serializeDoubleBean() { + var bean = new DoubleBean(1.1); + assertJson("{'value': 1.1}", serialize(bean)); + } + + @Test + public void serializeIntBean() { + var bean = new IntBean(1); + assertJson("{'value': 1}", serialize(Collections.singletonMap("value", 1.0))); + } + + @Test + public void serializeLongBean() { + var bean = new LongBean(1234567890123L); + assertJson( + "{'value': 1.234567890123E12}", + serialize(Collections.singletonMap("value", 1.234567890123E12))); + } + + @Test + public void serializeBigDecimalBean() { + var bean = new BigDecimalBean(BigDecimal.valueOf(1.1)); + assertEquals(mapAnyType("value", "1.1"), serialize(bean)); + } + + @Test + public void bigDecimalRoundTrip() { + var doubleMaxPlusOne = BigDecimal.valueOf(Double.MAX_VALUE).add(BigDecimal.ONE); + var a = new BigDecimalBean(doubleMaxPlusOne); + var serialized = (Map) serialize(a); + var b = convertToCustomClass(serialized, BigDecimalBean.class); + assertEquals(a, b); + } + + @Test + public void serializeBooleanBean() { + var bean = new BooleanBean(true); + assertJson("{'value': true}", serialize(bean)); + } + + @Test + public void serializeFloatBean() { + var bean = new FloatBean(0.5f); + + // We don't use assertJson as it converts all floating point numbers to Double. + assertEquals(mapAnyType("value", 0.5f), serialize(bean)); + } + + @Test + public void serializePrivateFieldBean() { + final var bean = new NoFieldBean(); + assertExceptionContains( + "No properties to serialize found on class " + + "com.google.cloud.firestore.RecordMapperTest$NoFieldBean", + () -> serialize(bean)); + } + + @Test + public void nestedSerializingWorks() { + var bean = new NestedBean(new StringBean("foo")); + assertJson("{'bean': {'value': 'foo'}}", serialize(bean)); + } + + @Test + public void serializingListsWorks() { + var bean = new ListBean(Arrays.asList("foo", "bar")); + assertJson("{'values': ['foo', 'bar']}", serialize(bean)); + } + + @Test + public void serializingMapsWorks() { + var bean = new MapBean(new HashMap<>()); + bean.values().put("foo", "bar"); + assertJson("{'values': {'foo': 'bar'}}", serialize(bean)); + } + + @Test + public void serializingUpperBoundedMapsWorks() { + var date = new Date(1491847082123L); + var bean = new UpperBoundedMapBean(Map.of("foo", date)); + var expected = + mapAnyType("values", mapAnyType("foo", new Date(date.getTime()))); + assertEquals(expected, serialize(bean)); + } + + @Test + public void serializingMultiBoundedObjectsWorks() { + var date = new Date(1491847082123L); + + var values = new HashMap(); + values.put("foo", date); + + var holder = new MultiBoundedMapHolderBean(new MultiBoundedMapBean<>(values)); + + var expected = + mapAnyType("map", mapAnyType("values", mapAnyType("foo", new Date(date.getTime())))); + assertEquals(expected, serialize(holder)); + } + + @Test + public void serializeListOfBeansWorks() { + var stringBean = new StringBean("foo"); + + var bean = new NestedListBean(new ArrayList<>()); + bean.values().add(stringBean); + + assertJson("{'values': [{'value': 'foo'}]}", serialize(bean)); + } + + @Test + public void serializeMapOfBeansWorks() { + var stringBean = new StringBean("foo"); + + var bean = new NestedMapBean(new HashMap<>()); + bean.values().put("key", stringBean); + + assertJson("{'values': {'key': {'value': 'foo'}}}", serialize(bean)); + } + + @Test + public void beanMapsMustHaveStringKeysForSerializing() { + var stringBean = new StringBean("foo"); + + final var bean = new IllegalKeyMapBean(new HashMap<>()); + bean.values().put(1, stringBean); + + assertExceptionContains( + "Maps with non-string keys are not supported (found in field 'values')", + () -> serialize(bean)); + } + + @Test + public void serializeUPPERCASE() { + var bean = new XMLAndURLBean("foo"); + assertJson("{'XMLAndURL': 'foo'}", serialize(bean)); + } + + @Test + public void roundTripCaseSensitiveFieldBean1() { + var bean = new CaseSensitiveFieldBean1("foo"); + assertJson("{'VALUE': 'foo'}", serialize(bean)); + var deserialized = + deserialize("{'VALUE': 'foo'}", CaseSensitiveFieldBean1.class); + assertEquals("foo", deserialized.VALUE()); + } + + @Test + public void roundTripCaseSensitiveFieldBean2() { + var bean = new CaseSensitiveFieldBean2("foo"); + assertJson("{'value': 'foo'}", serialize(bean)); + var deserialized = + deserialize("{'value': 'foo'}", CaseSensitiveFieldBean2.class); + assertEquals("foo", deserialized.value()); + } + + @Test + public void roundTripCaseSensitiveFieldBean3() { + var bean = new CaseSensitiveFieldBean3("foo"); + assertJson("{'Value': 'foo'}", serialize(bean)); + var deserialized = + deserialize("{'Value': 'foo'}", CaseSensitiveFieldBean3.class); + assertEquals("foo", deserialized.Value()); + } + + @Test + public void roundTripCaseSensitiveFieldBean4() { + var bean = new CaseSensitiveFieldBean4("foo"); + assertJson("{'valUE': 'foo'}", serialize(bean)); + var deserialized = + deserialize("{'valUE': 'foo'}", CaseSensitiveFieldBean4.class); + assertEquals("foo", deserialized.valUE()); + } + + @Test + public void roundTripUnicodeBean() { + var bean = new UnicodeBean("foo"); + assertJson("{'漢字': 'foo'}", serialize(bean)); + var deserialized = deserialize("{'漢字': 'foo'}", UnicodeBean.class); + assertEquals("foo", deserialized.漢字()); + } + + @Test + public void shortsCantBeSerialized() { + final var bean = new ShortBean((short) 1); + assertExceptionContains( + "Numbers of type Short are not supported, please use an int, long, float, double or BigDecimal (found in field 'value')", + () -> serialize(bean)); + } + + @Test + public void bytesCantBeSerialized() { + final var bean = new ByteBean((byte) 1); + assertExceptionContains( + "Numbers of type Byte are not supported, please use an int, long, float, double or BigDecimal (found in field 'value')", + () -> serialize(bean)); + } + + @Test + public void charsCantBeSerialized() { + final var bean = new CharBean((char) 1); + assertExceptionContains( + "Characters are not supported, please use Strings (found in field 'value')", + () -> serialize(bean)); + } + + @Test + public void intArraysCantBeSerialized() { + final var bean = new IntArrayBean(new int[] {1}); + assertExceptionContains( + "Serializing Arrays is not supported, please use Lists instead " + + "(found in field 'values')", + () -> serialize(bean)); + } + + @Test + public void objectArraysCantBeSerialized() { + final var bean = new StringArrayBean(new String[] {"foo"}); + assertExceptionContains( + "Serializing Arrays is not supported, please use Lists instead " + + "(found in field 'values')", + () -> serialize(bean)); + } + + @Test + public void shortsCantBeDeserialized() { + assertExceptionContains( + "Deserializing values to short is not supported (found in field 'value')", + () -> deserialize("{'value': 1}", ShortBean.class)); + } + + @Test + public void bytesCantBeDeserialized() { + assertExceptionContains( + "Deserializing values to byte is not supported (found in field 'value')", + () -> deserialize("{'value': 1}", ByteBean.class)); + } + + @Test + public void charsCantBeDeserialized() { + assertExceptionContains( + "Deserializing values to char is not supported (found in field 'value')", + () -> deserialize("{'value': '1'}", CharBean.class)); + } + + @Test + public void intArraysCantBeDeserialized() { + assertExceptionContains( + "Converting to Arrays is not supported, please use Lists instead (found in field 'values')", + () -> deserialize("{'values': [1]}", IntArrayBean.class)); + } + + @Test + public void objectArraysCantBeDeserialized() { + assertExceptionContains( + "Could not deserialize object. Converting to Arrays is not supported, please use Lists " + + "instead (found in field 'values')", + () -> deserialize("{'values': ['foo']}", StringArrayBean.class)); + } + + @Test + public void objectAcceptsAnyObject() { + var stringValue = deserialize("{'value': 'foo'}", ObjectBean.class); + assertEquals("foo", stringValue.value()); + var listValue = deserialize("{'value': ['foo']}", ObjectBean.class); + assertEquals(Collections.singletonList("foo"), listValue.value()); + var mapValue = deserialize("{'value': {'foo':'bar'}}", ObjectBean.class); + assertEquals(fromSingleQuotedString("{'foo':'bar'}"), mapValue.value()); + var complex = "{'value': {'foo':['bar', ['baz'], {'bam': 'qux'}]}, 'other':{'a': ['b']}}"; + var complexValue = deserialize(complex, ObjectBean.class); + assertEquals(fromSingleQuotedString(complex).get("value"), complexValue.value()); + } + + @Test + public void passingInGenericBeanTopLevelThrows() { + assertExceptionContains( + "Class com.google.cloud.firestore.RecordMapperTest$GenericBean has generic type " + + "parameters, please use GenericTypeIndicator instead", + () -> deserialize("{'value': 'foo'}", GenericBean.class)); + } + + @Test + public void collectionsCanBeSerializedWhenList() { + var bean = new CollectionBean(Collections.singletonList("foo")); + assertJson("{'values': ['foo']}", serialize(bean)); + } + + @Test + public void collectionsCantBeSerializedWhenSet() { + final var bean = new CollectionBean(Collections.singleton("foo")); + assertExceptionContains( + "Serializing Collections is not supported, please use Lists instead " + + "(found in field 'values')", + () -> serialize(bean)); + } + + @Test + public void collectionsCantBeDeserialized() { + assertExceptionContains( + "Collections are not supported, please use Lists instead (found in field 'values')", + () -> deserialize("{'values': ['foo']}", CollectionBean.class)); + } + + @Test + public void serializingGenericBeansSupported() { + var stringBean = new GenericBean("foo"); + assertJson("{'value': 'foo'}", serialize(stringBean)); + + var mapBean = new GenericBean>(Collections.singletonMap("foo", "bar")); + assertJson("{'value': {'foo': 'bar'}}", serialize(mapBean)); + + var listBean = new GenericBean>(Collections.singletonList("foo")); + assertJson("{'value': ['foo']}", serialize(listBean)); + + var recursiveBean = new GenericBean>(new GenericBean<>("foo")); + assertJson("{'value': {'value': 'foo'}}", serialize(recursiveBean)); + + var doubleBean = new DoubleGenericBean("foo", 1.0); + assertJson("{'valueB': 1, 'valueA': 'foo'}", serialize(doubleBean)); + } + + @Test + public void propertyNamesAreSerialized() { + var bean = new PropertyNameBean("foo", "bar"); + + assertJson("{'my_key': 'foo', 'my_value': 'bar'}", serialize(bean)); + } + + @Test + public void propertyNamesAreParsed() { + var bean = + deserialize("{'my_key': 'foo', 'my_value': 'bar'}", PropertyNameBean.class); + assertEquals("foo", bean.key()); + assertEquals("bar", bean.value()); + } + + // Bean definitions with @DocumentId applied to wrong type. + public record FieldWithDocumentIdOnWrongTypeBean ( + @DocumentId Integer intField + ){} + + public record PropertyWithDocumentIdOnWrongTypeBean ( + @PropertyName("intField") + @DocumentId + int intField + ){} + + @Test + public void documentIdAnnotateWrongTypeThrows() { + final var expectedErrorMessage = "instead of String or DocumentReference"; + assertExceptionContains( + expectedErrorMessage, () -> serialize(new FieldWithDocumentIdOnWrongTypeBean(100))); + assertExceptionContains( + expectedErrorMessage, + () -> deserialize("{'intField': 1}", FieldWithDocumentIdOnWrongTypeBean.class)); + + assertExceptionContains( + expectedErrorMessage, () -> serialize(new PropertyWithDocumentIdOnWrongTypeBean(100))); + assertExceptionContains( + expectedErrorMessage, + () -> deserialize("{'intField': 1}", PropertyWithDocumentIdOnWrongTypeBean.class)); + } + + public record DocumentIdOnStringField ( + @DocumentId String docId + ){} + + public record DocumentIdOnStringFieldAsProperty ( + @PropertyName("docIdProperty") + @DocumentId + String docId, + + @PropertyName("anotherProperty") + int someOtherProperty + ){} + + public record DocumentIdOnNestedObjects ( + @PropertyName("nestedDocIdHolder") + DocumentIdOnStringField nestedDocIdHolder + ){} + + @Test + public void documentIdsDeserialize() { + var ref = + new DocumentReference( + firestoreMock, + ResourcePath.create( + DatabaseRootName.of("test-project", "(default)"), + ImmutableList.of("coll", "doc123"))); + + assertEquals("doc123", deserialize("{}", DocumentIdOnStringField.class, ref).docId()); + + assertEquals( + "doc123", + deserialize(Collections.singletonMap("property", 100), DocumentIdOnStringField.class, ref) + .docId()); + + var target = + deserialize("{'anotherProperty': 100}", DocumentIdOnStringFieldAsProperty.class, ref); + assertEquals("doc123", target.docId()); + assertEquals(100, target.someOtherProperty()); + + assertEquals( + "doc123", + deserialize("{'nestedDocIdHolder': {}}", DocumentIdOnNestedObjects.class, ref) + .nestedDocIdHolder() + .docId()); + } + + @Test + public void documentIdsRoundTrip() { + // Implicitly verifies @DocumentId is ignored during serialization. + + final var ref = + new DocumentReference( + firestoreMock, + ResourcePath.create( + DatabaseRootName.of("test-project", "(default)"), + ImmutableList.of("coll", "doc123"))); + + assertEquals( + Collections.emptyMap(), serialize(deserialize("{}", DocumentIdOnStringField.class, ref))); + + assertEquals( + Collections.singletonMap("anotherProperty", 100), + serialize( + deserialize("{'anotherProperty': 100}", DocumentIdOnStringFieldAsProperty.class, ref))); + + assertEquals( + Collections.singletonMap("nestedDocIdHolder", Collections.emptyMap()), + serialize(deserialize("{'nestedDocIdHolder': {}}", DocumentIdOnNestedObjects.class, ref))); + } + + @Test + public void documentIdsDeserializeConflictThrows() { + final var expectedErrorMessage = "cannot apply @DocumentId on this property"; + final var ref = + new DocumentReference( + firestoreMock, + ResourcePath.create( + DatabaseRootName.of("test-project", "(default)"), + ImmutableList.of("coll", "doc123"))); + + assertExceptionContains( + expectedErrorMessage, + () -> deserialize("{'docId': 'toBeOverwritten'}", DocumentIdOnStringField.class, ref)); + + assertExceptionContains( + expectedErrorMessage, + () -> + deserialize( + "{'docIdProperty': 'toBeOverwritten', 'anotherProperty': 100}", + DocumentIdOnStringFieldAsProperty.class, + ref)); + + assertExceptionContains( + expectedErrorMessage, + () -> + deserialize( + "{'nestedDocIdHolder': {'docId': 'toBeOverwritten'}}", + DocumentIdOnNestedObjects.class, + ref)); + } +} diff --git a/google-cloud-firestore-jdk17/src/test/java/com/google/cloud/firestore/jdk17/LocalFirestoreHelper.java b/google-cloud-firestore-jdk17/src/test/java/com/google/cloud/firestore/jdk17/LocalFirestoreHelper.java new file mode 100644 index 000000000..adeeb0cf9 --- /dev/null +++ b/google-cloud-firestore-jdk17/src/test/java/com/google/cloud/firestore/jdk17/LocalFirestoreHelper.java @@ -0,0 +1,413 @@ +/* + * Copyright 2017 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.cloud.firestore.jdk17; + +import java.math.BigInteger; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Comparator; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +import javax.annotation.Nullable; + +import org.mockito.stubbing.Answer; + +import com.google.api.core.ApiFuture; +import com.google.api.core.ApiFutures; +import com.google.api.gax.rpc.ResponseObserver; +import com.google.api.gax.rpc.StreamController; +import com.google.cloud.Timestamp; +import com.google.cloud.firestore.Blob; +import com.google.cloud.firestore.GeoPoint; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.firestore.v1.ArrayValue; +import com.google.firestore.v1.BatchGetDocumentsResponse; +import com.google.firestore.v1.CommitRequest; +import com.google.firestore.v1.CommitResponse; +import com.google.firestore.v1.DocumentMask; +import com.google.firestore.v1.DocumentTransform.FieldTransform; +import com.google.firestore.v1.MapValue; +import com.google.firestore.v1.Value; +import com.google.firestore.v1.Write; +import com.google.gson.Gson; +import com.google.gson.reflect.TypeToken; +import com.google.protobuf.ByteString; +import com.google.protobuf.NullValue; +import com.google.type.LatLng; + +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.mock; + + +public final class LocalFirestoreHelper { + + public static final String DATABASE_NAME; + public static final String DOCUMENT_PATH; + public static final String DOCUMENT_NAME; + public static final String DOCUMENT_ROOT; + + public static final SingleComponent SINGLE_COMPONENT_OBJECT; + public static final Map SINGLE_COMPONENT_PROTO; + + public static final NestedRecord NESTED_RECORD_OBJECT; + + public static final ServerTimestamp SERVER_TIMESTAMP_OBJECT; + public static final Map SERVER_TIMESTAMP_PROTO; + + public static final AllSupportedTypes ALL_SUPPORTED_TYPES_OBJECT; + public static final Map ALL_SUPPORTED_TYPES_PROTO; + + public static final ApiFuture SINGLE_WRITE_COMMIT_RESPONSE; + + public static final ApiFuture FIELD_TRANSFORM_COMMIT_RESPONSE; + + public static final Date DATE; + public static final Timestamp TIMESTAMP; + public static final GeoPoint GEO_POINT; + public static final Blob BLOB; + + + public record SingleComponent( + + String foo + ){} + + public record NestedRecord( + SingleComponent first, + AllSupportedTypes second + ){} + + public record ServerTimestamp ( + + @com.google.cloud.firestore.jdk17.annotation.ServerTimestamp Date foo, + Inner inner + + ){ + record Inner ( + + @com.google.cloud.firestore.jdk17.annotation.ServerTimestamp Date bar + ){} + } + + public record InvalidRecord ( + BigInteger bigIntegerValue, + Byte byteValue, + Short shortValue + ){} + + public static Map map(K key, V value, Object... moreKeysAndValues) { + Map map = new HashMap<>(); + map.put(key, value); + + for (var i = 0; i < moreKeysAndValues.length; i += 2) { + map.put((K) moreKeysAndValues[i], (V) moreKeysAndValues[i + 1]); + } + + return map; + } + + public static Answer getAllResponse( + final Map... fields) { + var responses = new BatchGetDocumentsResponse[fields.length]; + + for (var i = 0; i < fields.length; ++i) { + var name = DOCUMENT_NAME; + if (fields.length > 1) { + name += i + 1; + } + var response = BatchGetDocumentsResponse.newBuilder(); + response + .getFoundBuilder() + .setCreateTime(com.google.protobuf.Timestamp.newBuilder().setSeconds(1).setNanos(2)); + response + .getFoundBuilder() + .setUpdateTime(com.google.protobuf.Timestamp.newBuilder().setSeconds(3).setNanos(4)); + response.setReadTime(com.google.protobuf.Timestamp.newBuilder().setSeconds(5).setNanos(6)); + response.getFoundBuilder().setName(name).putAllFields(fields[i]); + responses[i] = response.build(); + } + + return streamingResponse(responses, null); + } + + /** Returns a stream of responses followed by an optional exception. */ + public static Answer streamingResponse( + final T[] response, @Nullable final Throwable throwable) { + return invocation -> { + var args = invocation.getArguments(); + var observer = (ResponseObserver) args[1]; + observer.onStart(mock(StreamController.class)); + for (var resp : response) { + observer.onResponse(resp); + } + if (throwable != null) { + observer.onError(throwable); + } + observer.onComplete(); + return null; + }; + } + + public static ApiFuture commitResponse(int adds, int deletes) { + var commitResponse = CommitResponse.newBuilder(); + commitResponse.getCommitTimeBuilder().setSeconds(0).setNanos(0); + for (var i = 0; i < adds; ++i) { + commitResponse.addWriteResultsBuilder().getUpdateTimeBuilder().setSeconds(i).setNanos(i); + } + for (var i = 0; i < deletes; ++i) { + commitResponse.addWriteResultsBuilder(); + } + return ApiFutures.immediateFuture(commitResponse.build()); + } + + public static FieldTransform serverTimestamp() { + return FieldTransform.newBuilder() + .setSetToServerValue(FieldTransform.ServerValue.REQUEST_TIME) + .build(); + } + + public static List transform( + String fieldPath, FieldTransform fieldTransform, Object... fieldPathOrTransform) { + + List transforms = new ArrayList<>(); + var transformBuilder = FieldTransform.newBuilder(); + transformBuilder.setFieldPath(fieldPath).mergeFrom(fieldTransform); + + transforms.add(transformBuilder.build()); + + for (var i = 0; i < fieldPathOrTransform.length; i += 2) { + var path = (String) fieldPathOrTransform[i]; + var transform = (FieldTransform) fieldPathOrTransform[i + 1]; + transforms.add(FieldTransform.newBuilder().setFieldPath(path).mergeFrom(transform).build()); + } + return transforms; + } + + public static Write create(Map fields, String docPath) { + var write = Write.newBuilder(); + var document = write.getUpdateBuilder(); + document.setName(DOCUMENT_ROOT + docPath); + document.putAllFields(fields); + write.getCurrentDocumentBuilder().setExists(false); + return write.build(); + } + + public static Write create(Map fields) { + return create(fields, DOCUMENT_PATH); + } + + public static Write set(Map fields) { + return set(fields, null, DOCUMENT_PATH); + } + + public static Write set(Map fields, @Nullable List fieldMap) { + return set(fields, fieldMap, DOCUMENT_PATH); + } + + public static Write set( + Map fields, @Nullable List fieldMap, String docPath) { + var write = Write.newBuilder(); + var document = write.getUpdateBuilder(); + document.setName(DOCUMENT_ROOT + docPath); + document.putAllFields(fields); + + if (fieldMap != null) { + write.getUpdateMaskBuilder().addAllFieldPaths(fieldMap); + } + + return write.build(); + } + + public static CommitRequest commit(@Nullable String transactionId, Write... writes) { + var commitRequest = CommitRequest.newBuilder(); + commitRequest.setDatabase(DATABASE_NAME); + commitRequest.addAllWrites(Arrays.asList(writes)); + + if (transactionId != null) { + commitRequest.setTransaction(ByteString.copyFromUtf8(transactionId)); + } + + return commitRequest.build(); + } + + public static CommitRequest commit(Write... writes) { + return commit(null, writes); + } + + public static CommitRequest commit(Write write, List transforms) { + return commit((String) null, write.toBuilder().addAllUpdateTransforms(transforms).build()); + } + + public static void assertCommitEquals(CommitRequest expected, CommitRequest actual) { + assertEquals(sortCommit(expected), sortCommit(actual)); + } + + private static CommitRequest sortCommit(CommitRequest commit) { + var builder = commit.toBuilder(); + + for (var writes : builder.getWritesBuilderList()) { + if (writes.hasUpdateMask()) { + var updateMask = new ArrayList<>(writes.getUpdateMask().getFieldPathsList()); + Collections.sort(updateMask); + writes.setUpdateMask(DocumentMask.newBuilder().addAllFieldPaths(updateMask)); + } + + if (!writes.getUpdateTransformsList().isEmpty()) { + var transformList = new ArrayList<>(writes.getUpdateTransformsList()); + transformList.sort(Comparator.comparing(FieldTransform::getFieldPath)); + writes.clearUpdateTransforms().addAllUpdateTransforms(transformList); + } + } + + return builder.build(); + } + + public record AllSupportedTypes ( + + String foo, + Double doubleValue, + long longValue, + double nanValue, + double infValue, + double negInfValue, + boolean trueValue, + boolean falseValue, + SingleComponent objectValue, + Date dateValue, + Timestamp timestampValue, + List arrayValue, + String nullValue, + Blob bytesValue, + GeoPoint geoPointValue, + Map model + ){} + + static { + try { + DATE = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.S z").parse("1985-03-18 08:20:00.123 CET"); + } catch (ParseException e) { + throw new RuntimeException("Failed to parse date", e); + } + + TIMESTAMP = + Timestamp.ofTimeSecondsAndNanos( + TimeUnit.MILLISECONDS.toSeconds(DATE.getTime()), + 123000); // Firestore truncates to microsecond precision. + GEO_POINT = new GeoPoint(50.1430847, -122.9477780); + BLOB = Blob.fromBytes(new byte[] {1, 2, 3}); + + DATABASE_NAME = "projects/test-project/databases/(default)"; + DOCUMENT_PATH = "coll/doc"; + DOCUMENT_NAME = DATABASE_NAME + "/documents/" + DOCUMENT_PATH; + DOCUMENT_ROOT = DATABASE_NAME + "/documents/"; + + SINGLE_COMPONENT_OBJECT = new SingleComponent("bar"); + SINGLE_COMPONENT_PROTO = map("foo", Value.newBuilder().setStringValue("bar").build()); + + SERVER_TIMESTAMP_PROTO = Collections.emptyMap(); + SERVER_TIMESTAMP_OBJECT = new ServerTimestamp(null, new ServerTimestamp.Inner(null)); + + ALL_SUPPORTED_TYPES_OBJECT = new AllSupportedTypes("bar", 0.0, 0L, Double.NaN, Double.POSITIVE_INFINITY, + Double.NEGATIVE_INFINITY, true, false, + new SingleComponent("bar"), DATE, + TIMESTAMP, ImmutableList.of("foo"), null, BLOB, GEO_POINT, + ImmutableMap.of("foo", SINGLE_COMPONENT_OBJECT.foo())); + ALL_SUPPORTED_TYPES_PROTO = + ImmutableMap.builder() + .put("foo", Value.newBuilder().setStringValue("bar").build()) + .put("doubleValue", Value.newBuilder().setDoubleValue(0.0).build()) + .put("longValue", Value.newBuilder().setIntegerValue(0L).build()) + .put("nanValue", Value.newBuilder().setDoubleValue(Double.NaN).build()) + .put("infValue", Value.newBuilder().setDoubleValue(Double.POSITIVE_INFINITY).build()) + .put("negInfValue", Value.newBuilder().setDoubleValue(Double.NEGATIVE_INFINITY).build()) + .put("trueValue", Value.newBuilder().setBooleanValue(true).build()) + .put("falseValue", Value.newBuilder().setBooleanValue(false).build()) + .put( + "objectValue", + Value.newBuilder() + .setMapValue(MapValue.newBuilder().putAllFields(SINGLE_COMPONENT_PROTO)) + .build()) + .put( + "dateValue", + Value.newBuilder() + .setTimestampValue( + com.google.protobuf.Timestamp.newBuilder() + .setSeconds(479978400) + .setNanos(123000000)) // Dates only support millisecond precision. + .build()) + .put( + "timestampValue", + Value.newBuilder() + .setTimestampValue( + com.google.protobuf.Timestamp.newBuilder() + .setSeconds(479978400) + .setNanos(123000)) // Timestamps supports microsecond precision. + .build()) + .put( + "arrayValue", + Value.newBuilder() + .setArrayValue( + ArrayValue.newBuilder().addValues(Value.newBuilder().setStringValue("foo"))) + .build()) + .put("nullValue", Value.newBuilder().setNullValue(NullValue.NULL_VALUE).build()) + .put("bytesValue", Value.newBuilder().setBytesValue(BLOB.toByteString()).build()) + .put( + "geoPointValue", + Value.newBuilder() + .setGeoPointValue( + LatLng.newBuilder().setLatitude(50.1430847).setLongitude(-122.9477780)) + .build()) + .put( + "model", + Value.newBuilder() + .setMapValue(MapValue.newBuilder().putAllFields(SINGLE_COMPONENT_PROTO)) + .build()) + .build(); + SINGLE_WRITE_COMMIT_RESPONSE = commitResponse(/* adds= */ 1, /* deletes= */ 0); + + FIELD_TRANSFORM_COMMIT_RESPONSE = commitResponse(/* adds= */ 2, /* deletes= */ 0); + + NESTED_RECORD_OBJECT = new NestedRecord(SINGLE_COMPONENT_OBJECT, ALL_SUPPORTED_TYPES_OBJECT); + } + + @SuppressWarnings("unchecked") + public static Map mapAnyType(Object... entries) { + Map res = new HashMap<>(); + for (var i = 0; i < entries.length; i += 2) { + res.put((String) entries[i], (T) entries[i + 1]); + } + return res; + } + + private static Map fromJsonString(String json) { + var type = new TypeToken>() {}.getType(); + var gson = new Gson(); + return gson.fromJson(json, type); + } + + public static Map fromSingleQuotedString(String json) { + return fromJsonString(json.replace("'", "\"")); + } +} diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/BeanMapper.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/BeanMapper.java new file mode 100644 index 000000000..7f49d1b6a --- /dev/null +++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/BeanMapper.java @@ -0,0 +1,131 @@ +/* + * Copyright 2017 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.cloud.firestore; + +import com.google.cloud.firestore.annotation.IgnoreExtraProperties; +import com.google.cloud.firestore.annotation.ThrowOnExtraProperties; +import java.lang.reflect.Type; +import java.lang.reflect.TypeVariable; +import java.util.Collection; +import java.util.Collections; +import java.util.Map; + +/** Base bean mapper class, providing common functionality for class and record serialization. */ +abstract class BeanMapper { + private final Class clazz; + // Whether to throw exception if there are properties we don't know how to set to + // custom object fields/setters or record components during deserialization. + private final boolean throwOnUnknownProperties; + // Whether to log a message if there are properties we don't know how to set to + // custom object fields/setters or record components during deserialization. + private final boolean warnOnUnknownProperties; + + BeanMapper(Class clazz) { + this.clazz = clazz; + throwOnUnknownProperties = clazz.isAnnotationPresent(ThrowOnExtraProperties.class); + warnOnUnknownProperties = !clazz.isAnnotationPresent(IgnoreExtraProperties.class); + } + + Class getClazz() { + return clazz; + } + + boolean isThrowOnUnknownProperties() { + return throwOnUnknownProperties; + } + + boolean isWarnOnUnknownProperties() { + return warnOnUnknownProperties; + } + + /** + * Serialize an object to a map. + * + * @param object the object to serialize + * @param path the path to a specific field/component in an object, for use in error messages + * @return the map + */ + abstract Map serialize(T object, DeserializeContext.ErrorPath path); + + /** + * Deserialize a map to an object. + * + * @param values the map to deserialize + * @param types generic type mappings + * @param context context information about the deserialization operation + * @return the deserialized object + */ + abstract T deserialize( + Map values, + Map>, Type> types, + DeserializeContext context); + + void ensureValidDocumentIdType(String fieldDescription, String operation, Type type) { + if (type != String.class && type != DocumentReference.class) { + throw new IllegalArgumentException( + fieldDescription + + " is annotated with @DocumentId but " + + operation + + " " + + type + + " instead of String or DocumentReference."); + } + } + + void verifyValidType(T object) { + if (!clazz.isAssignableFrom(object.getClass())) { + throw new IllegalArgumentException( + "Can't serialize object of class " + + object.getClass() + + " with BeanMapper for class " + + clazz); + } + } + + Type resolveType(Type type, Map>, Type> types) { + if (type instanceof TypeVariable) { + Type resolvedType = types.get(type); + if (resolvedType == null) { + throw new IllegalStateException("Could not resolve type " + type); + } + + return resolvedType; + } + + return type; + } + + void checkForDocIdConflict( + String docIdPropertyName, + Collection deserializedProperties, + DeserializeContext context) { + if (deserializedProperties.contains(docIdPropertyName)) { + String message = + "'" + + docIdPropertyName + + "' was found from document " + + context.documentRef.getPath() + + ", cannot apply @DocumentId on this property for class " + + clazz.getName(); + throw new RuntimeException(message); + } + } + + T deserialize(Map values, DeserializeContext context) { + return deserialize(values, Collections.emptyMap(), context); + } +} 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 a17c3a08e..a864b9ad1 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 @@ -19,10 +19,8 @@ import com.google.cloud.Timestamp; import com.google.cloud.firestore.annotation.DocumentId; import com.google.cloud.firestore.annotation.Exclude; -import com.google.cloud.firestore.annotation.IgnoreExtraProperties; import com.google.cloud.firestore.annotation.PropertyName; import com.google.cloud.firestore.annotation.ServerTimestamp; -import com.google.cloud.firestore.annotation.ThrowOnExtraProperties; import com.google.firestore.v1.Value; import java.lang.reflect.AccessibleObject; import java.lang.reflect.Constructor; @@ -38,7 +36,6 @@ import java.math.BigDecimal; import java.util.ArrayList; import java.util.Collection; -import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.HashSet; @@ -97,15 +94,16 @@ public static Map convertToPlainJavaTypes(Map update) * @return The POJO object. */ static T convertToCustomClass(Object object, Class clazz, DocumentReference docRef) { - return deserializeToClass(object, clazz, new DeserializeContext(ErrorPath.EMPTY, docRef)); + return deserializeToClass( + object, clazz, new DeserializeContext(DeserializeContext.ErrorPath.EMPTY, docRef)); } static Object serialize(T o) { - return serialize(o, ErrorPath.EMPTY); + return serialize(o, DeserializeContext.ErrorPath.EMPTY); } @SuppressWarnings("unchecked") - private static Object serialize(T o, ErrorPath path) { + static Object serialize(T o, DeserializeContext.ErrorPath path) { if (path.getLength() > MAX_DEPTH) { throw serializeError( path, @@ -163,7 +161,7 @@ private static Object serialize(T o, ErrorPath path) { String enumName = ((Enum) o).name(); try { Field enumField = o.getClass().getField(enumName); - return BeanMapper.propertyName(enumField); + return PojoBeanMapper.propertyName(enumField); } catch (NoSuchFieldException ex) { return enumName; } @@ -183,7 +181,7 @@ private static Object serialize(T o, ErrorPath path) { } @SuppressWarnings({"unchecked", "TypeParameterUnusedInFormals"}) - private static T deserializeToType(Object o, Type type, DeserializeContext context) { + static T deserializeToType(Object o, Type type, DeserializeContext context) { if (o == null) { return null; } else if (type instanceof ParameterizedType) { @@ -332,7 +330,7 @@ private static T deserializeToParameterizedType( Map map = expectMap(o, context); BeanMapper mapper = (BeanMapper) loadOrCreateBeanMapperForClass(rawType); HashMap>, Type> typeMapping = new HashMap<>(); - TypeVariable>[] typeVariables = mapper.clazz.getTypeParameters(); + TypeVariable>[] typeVariables = mapper.getClazz().getTypeParameters(); Type[] types = type.getActualTypeArguments(); if (types.length != typeVariables.length) { throw new IllegalStateException("Mismatched lengths for type variables and actual types"); @@ -378,7 +376,7 @@ private static T deserializeToEnum( Field[] enumFields = clazz.getFields(); for (Field field : enumFields) { if (field.isEnumConstant()) { - String propertyName = BeanMapper.propertyName(field); + String propertyName = PojoBeanMapper.propertyName(field); if (value.equals(propertyName)) { value = field.getName(); break; @@ -407,7 +405,11 @@ private static BeanMapper loadOrCreateBeanMapperForClass(Class clazz) @SuppressWarnings("unchecked") BeanMapper mapper = (BeanMapper) mappers.get(clazz); if (mapper == null) { - mapper = new BeanMapper<>(clazz); + if (isRecordType(clazz)) { + mapper = new RecordMapper<>(clazz); + } else { + mapper = new PojoBeanMapper<>(clazz); + } // Inserting without checking is fine because mappers are "pure" and it's okay // if we create and use multiple by different threads temporarily mappers.put(clazz, mapper); @@ -598,7 +600,8 @@ private static T convertBean(Object o, Class clazz, DeserializeContext co } } - private static IllegalArgumentException serializeError(ErrorPath path, String reason) { + private static IllegalArgumentException serializeError( + DeserializeContext.ErrorPath path, String reason) { reason = "Could not serialize object. " + reason; if (path.getLength() > 0) { reason = reason + " (found in field '" + path.toString() + "')"; @@ -606,7 +609,8 @@ private static IllegalArgumentException serializeError(ErrorPath path, String re return new IllegalArgumentException(reason); } - private static RuntimeException deserializeError(ErrorPath path, String reason) { + private static RuntimeException deserializeError( + DeserializeContext.ErrorPath path, String reason) { reason = "Could not deserialize object. " + reason; if (path.getLength() > 0) { reason = reason + " (found in field '" + path.toString() + "')"; @@ -614,16 +618,14 @@ private static RuntimeException deserializeError(ErrorPath path, String reason) return new RuntimeException(reason); } + private static boolean isRecordType(Class cls) { + Class parent = cls.getSuperclass(); + return parent != null && "java.lang.Record".equals(parent.getName()); + } + // Helper class to convert from maps to custom objects (Beans), and vice versa. - private static class BeanMapper { - private final Class clazz; + private static class PojoBeanMapper extends BeanMapper { private final Constructor constructor; - // Whether to throw exception if there are properties we don't know how to set to - // custom object fields/setters during deserialization. - private final boolean throwOnUnknownProperties; - // Whether to log a message if there are properties we don't know how to set to - // custom object fields/setters during deserialization. - private final boolean warnOnUnknownProperties; // Case insensitive mapping of properties to their case sensitive versions private final Map properties; @@ -644,10 +646,8 @@ private static class BeanMapper { // serialization. private final HashSet documentIdPropertyNames; - BeanMapper(Class clazz) { - this.clazz = clazz; - throwOnUnknownProperties = clazz.isAnnotationPresent(ThrowOnExtraProperties.class); - warnOnUnknownProperties = !clazz.isAnnotationPresent(IgnoreExtraProperties.class); + PojoBeanMapper(Class clazz) { + super(clazz); properties = new HashMap<>(); setters = new HashMap<>(); @@ -788,10 +788,7 @@ private void addProperty(String property) { } } - T deserialize(Map values, DeserializeContext context) { - return deserialize(values, Collections.emptyMap(), context); - } - + @Override T deserialize( Map values, Map>, Type> types, @@ -800,7 +797,7 @@ T deserialize( throw deserializeError( context.errorPath, "Class " - + clazz.getName() + + getClazz().getName() + " does not define a no-argument constructor. If you are using ProGuard, make " + "sure these constructors are not stripped"); } @@ -814,7 +811,7 @@ T deserialize( HashSet deserialzedProperties = new HashSet<>(); for (Map.Entry entry : values.entrySet()) { String propertyName = entry.getKey(); - ErrorPath childPath = context.errorPath.child(propertyName); + DeserializeContext.ErrorPath childPath = context.errorPath.child(propertyName); if (setters.containsKey(propertyName)) { Method setter = setters.get(propertyName); Type[] params = setter.getGenericParameterTypes(); @@ -845,13 +842,13 @@ T deserialize( deserialzedProperties.add(propertyName); } else { String message = - "No setter/field for " + propertyName + " found on class " + clazz.getName(); + "No setter/field for " + propertyName + " found on class " + getClazz().getName(); if (properties.containsKey(propertyName.toLowerCase(Locale.US))) { message += " (fields/setters are case sensitive!)"; } - if (throwOnUnknownProperties) { + if (isThrowOnUnknownProperties()) { throw new RuntimeException(message); - } else if (warnOnUnknownProperties) { + } else if (isWarnOnUnknownProperties()) { LOGGER.warning(message); } } @@ -870,17 +867,8 @@ private void populateDocumentIdProperties( T instance, HashSet deserialzedProperties) { for (String docIdPropertyName : documentIdPropertyNames) { - if (deserialzedProperties.contains(docIdPropertyName)) { - String message = - "'" - + docIdPropertyName - + "' was found from document " - + context.documentRef.getPath() - + ", cannot apply @DocumentId on this property for class " - + clazz.getName(); - throw new RuntimeException(message); - } - ErrorPath childPath = context.errorPath.child(docIdPropertyName); + checkForDocIdConflict(docIdPropertyName, deserialzedProperties, context); + DeserializeContext.ErrorPath childPath = context.errorPath.child(docIdPropertyName); if (setters.containsKey(docIdPropertyName)) { Method setter = setters.get(docIdPropertyName); Type[] params = setter.getGenericParameterTypes(); @@ -912,27 +900,9 @@ private void populateDocumentIdProperties( } } - private Type resolveType(Type type, Map>, Type> types) { - if (type instanceof TypeVariable) { - Type resolvedType = types.get(type); - if (resolvedType == null) { - throw new IllegalStateException("Could not resolve type " + type); - } else { - return resolvedType; - } - } else { - return type; - } - } - - Map serialize(T object, ErrorPath path) { - if (!clazz.isAssignableFrom(object.getClass())) { - throw new IllegalArgumentException( - "Can't serialize object of class " - + object.getClass() - + " with BeanMapper for class " - + clazz); - } + @Override + Map serialize(T object, DeserializeContext.ErrorPath path) { + verifyValidType(object); Map result = new HashMap<>(); for (String property : properties.values()) { // Skip @DocumentId annotated properties; @@ -1032,18 +1002,6 @@ private void applySetterAnnotations(Method method) { } } - private void ensureValidDocumentIdType(String fieldDescription, String operation, Type type) { - if (type != String.class && type != DocumentReference.class) { - throw new IllegalArgumentException( - fieldDescription - + " is annotated with @DocumentId but " - + operation - + " " - + type - + " instead of String or DocumentReference."); - } - } - private static boolean shouldIncludeGetter(Method method) { if (!method.getName().startsWith("get") && !method.getName().startsWith("is")) { return false; @@ -1185,61 +1143,4 @@ private static String serializedName(String methodName) { return new String(chars); } } - - /** - * Immutable class representing the path to a specific field in an object. Used to provide better - * error messages. - */ - static class ErrorPath { - private final int length; - private final ErrorPath parent; - private final String name; - - static final ErrorPath EMPTY = new ErrorPath(null, null, 0); - - ErrorPath(ErrorPath parent, String name, int length) { - this.parent = parent; - this.name = name; - this.length = length; - } - - int getLength() { - return length; - } - - ErrorPath child(String name) { - return new ErrorPath(this, name, length + 1); - } - - @Override - public String toString() { - if (length == 0) { - return ""; - } else if (length == 1) { - return name; - } else { - // This is not very efficient, but it's only hit if there's an error. - return parent.toString() + "." + name; - } - } - } - - /** Holds information a deserialization operation needs to complete the job. */ - private static class DeserializeContext { - - /** Current path to the field being deserialized, used for better error messages. */ - final ErrorPath errorPath; - - /** Value used to set to {@link DocumentId} annotated fields during deserialization, if any. */ - final DocumentReference documentRef; - - DeserializeContext(ErrorPath path, DocumentReference docRef) { - errorPath = path; - documentRef = docRef; - } - - DeserializeContext newInstanceWithErrorPath(ErrorPath newPath) { - return new DeserializeContext(newPath, documentRef); - } - } } diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/DeserializeContext.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/DeserializeContext.java new file mode 100644 index 000000000..818c553e5 --- /dev/null +++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/DeserializeContext.java @@ -0,0 +1,73 @@ +/* + * Copyright 2017 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.cloud.firestore; + +/** Holds information a deserialization operation needs to complete the job. */ +class DeserializeContext { + /** + * Immutable class representing the path to a specific field in an object. Used to provide better + * error messages. + */ + static class ErrorPath { + static final ErrorPath EMPTY = new ErrorPath(null, null, 0); + + private final int length; + private final ErrorPath parent; + private final String name; + + ErrorPath child(String name) { + return new ErrorPath(this, name, length + 1); + } + + @Override + public String toString() { + if (length == 0) { + return ""; + } else if (length == 1) { + return name; + } else { + // This is not very efficient, but it's only hit if there's an error. + return parent.toString() + "." + name; + } + } + + ErrorPath(ErrorPath parent, String name, int length) { + this.parent = parent; + this.name = name; + this.length = length; + } + + int getLength() { + return length; + } + } + + /** Current path to the field being deserialized, used for better error messages. */ + final ErrorPath errorPath; + + /** Value used to set to {@link DocumentId} annotated fields during deserialization, if any. */ + final DocumentReference documentRef; + + DeserializeContext newInstanceWithErrorPath(ErrorPath newPath) { + return new DeserializeContext(newPath, documentRef); + } + + DeserializeContext(ErrorPath path, DocumentReference docRef) { + errorPath = path; + documentRef = docRef; + } +} diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/RecordMapper.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/RecordMapper.java new file mode 100644 index 000000000..114e220bc --- /dev/null +++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/RecordMapper.java @@ -0,0 +1,292 @@ +/* + * Copyright 2017 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.cloud.firestore; + +import com.google.cloud.Timestamp; +import java.lang.annotation.Annotation; +import java.lang.reflect.AnnotatedElement; +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Type; +import java.lang.reflect.TypeVariable; +import java.util.Arrays; +import java.util.Date; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.logging.Logger; + +/** + * Serializes java records. Uses automatic record constructors and accessors only. Therefore, + * exclusion of fields is not supported. Supports DocumentId, PropertyName, and ServerTimestamp + * annotations on record components. However, those annotations have to + * include @Target(ElementType.RECORD_COMPONENT), so the ones defined in the + * google-cloud-firestore-jdk17 module should be used, rather than the ones defined in this module. + * Since records are not supported in JDK8, reflection is used for inspecting record metadata. This + * class will fail to load on java versions that don't support records. + * + * @author Eran Leshem + */ +class RecordMapper extends BeanMapper { + private static final Logger LOGGER = Logger.getLogger(RecordMapper.class.getName()); + private static final RecordInspector RECORD_INSPECTOR = new RecordInspector(); + + // Below are maps to find an accessor and constructor parameter index from a given property name. + // A property name is the name annotated by @PropertyName, if exists; or the component name. + // See method propertyName for details. + private final Map accessors = new HashMap<>(); + private final Constructor constructor; + private final Map constructorParamIndexes = new HashMap<>(); + // A set of property names that were annotated with @ServerTimestamp. + private final Set serverTimestamps = new HashSet<>(); + // A set of property names that were annotated with @DocumentId. These properties will be + // populated with document ID values during deserialization, and be skipped during + // serialization. + private final Set documentIdPropertyNames = new HashSet<>(); + + RecordMapper(Class clazz) { + super(clazz); + + constructor = RECORD_INSPECTOR.getCanonicalConstructor(clazz); + + AnnotatedElement[] recordComponents = RECORD_INSPECTOR.getRecordComponents(clazz); + if (recordComponents.length == 0) { + throw new RuntimeException("No properties to serialize found on class " + clazz.getName()); + } + + for (int i = 0; i < recordComponents.length; i++) { + AnnotatedElement recordComponent = recordComponents[i]; + String propertyName = propertyName(recordComponent); + constructorParamIndexes.put(propertyName, i); + accessors.put(propertyName, RECORD_INSPECTOR.getAccessor(recordComponent)); + applyComponentAnnotations(recordComponent); + } + } + + @Override + Map serialize(T object, DeserializeContext.ErrorPath path) { + verifyValidType(object); + Map result = new HashMap<>(); + for (Map.Entry entry : accessors.entrySet()) { + String property = entry.getKey(); + // Skip @DocumentId annotated properties; + if (documentIdPropertyNames.contains(property)) { + continue; + } + + Object propertyValue; + Method accessor = entry.getValue(); + try { + propertyValue = accessor.invoke(object); + } catch (IllegalAccessException | InvocationTargetException e) { + throw new RuntimeException(e); + } + + Object serializedValue; + if (serverTimestamps.contains(property) && propertyValue == null) { + // Replace null ServerTimestamp-annotated fields with the sentinel. + serializedValue = FieldValue.serverTimestamp(); + } else { + serializedValue = CustomClassMapper.serialize(propertyValue, path.child(property)); + } + result.put(property, serializedValue); + } + return result; + } + + @Override + T deserialize( + Map values, + Map>, Type> types, + DeserializeContext context) { + Object[] constructorParams = new Object[constructor.getParameterCount()]; + Set deserializedProperties = new HashSet<>(values.size()); + for (Map.Entry entry : values.entrySet()) { + String propertyName = entry.getKey(); + if (accessors.containsKey(propertyName)) { + Method accessor = accessors.get(propertyName); + Type resolvedType = resolveType(accessor.getGenericReturnType(), types); + DeserializeContext.ErrorPath childPath = context.errorPath.child(propertyName); + Object value = + CustomClassMapper.deserializeToType( + entry.getValue(), resolvedType, context.newInstanceWithErrorPath(childPath)); + constructorParams[constructorParamIndexes.get(propertyName).intValue()] = value; + deserializedProperties.add(propertyName); + } else { + String message = + "No accessor for " + propertyName + " found on class " + getClazz().getName(); + if (isThrowOnUnknownProperties()) { + throw new RuntimeException(message); + } + + if (isWarnOnUnknownProperties()) { + LOGGER.warning(message); + } + } + } + + populateDocumentIdProperties(types, context, constructorParams, deserializedProperties); + + try { + return constructor.newInstance(constructorParams); + } catch (InstantiationException | IllegalAccessException | InvocationTargetException e) { + throw new RuntimeException(e); + } + } + + private void applyComponentAnnotations(AnnotatedElement component) { + if (isAnnotationPresent(component, "ServerTimestamp")) { + Class componentType = RECORD_INSPECTOR.getType(component); + if (componentType != Date.class && componentType != Timestamp.class) { + throw new IllegalArgumentException( + "Component " + + RECORD_INSPECTOR.getName(component) + + " is annotated with @ServerTimestamp but is " + + componentType + + " instead of Date or Timestamp."); + } + serverTimestamps.add(propertyName(component)); + } + + if (isAnnotationPresent(component, "DocumentId")) { + Class type = RECORD_INSPECTOR.getType(component); + ensureValidDocumentIdType("Component", "is", type); + documentIdPropertyNames.add(propertyName(component)); + } + } + + private static String propertyName(AnnotatedElement component) { + Optional propertyName = getAnnotation(component, "PropertyName"); + if (propertyName.isPresent()) { + Annotation annotation = propertyName.get(); + try { + return (String) annotation.getClass().getMethod("value").invoke(annotation); + } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) { + throw new IllegalArgumentException("Failed to get PropertyName annotation value", e); + } + } + + return RECORD_INSPECTOR.getName(component); + } + + private static boolean isAnnotationPresent(AnnotatedElement element, String annotationName) { + return getAnnotation(element, annotationName).isPresent(); + } + + private static Optional getAnnotation( + AnnotatedElement element, String annotationName) { + return Arrays.stream(element.getAnnotations()) + .filter(annotation -> annotation.annotationType().getSimpleName().equals(annotationName)) + .findAny(); + } + + // Populate @DocumentId annotated components. If there is a conflict (@DocumentId annotation is + // applied to a property that is already deserialized from the firestore document) + // a runtime exception will be thrown. + private void populateDocumentIdProperties( + Map>, Type> types, + DeserializeContext context, + Object[] params, + Set deserialzedProperties) { + for (String docIdPropertyName : documentIdPropertyNames) { + checkForDocIdConflict(docIdPropertyName, deserialzedProperties, context); + + if (accessors.containsKey(docIdPropertyName)) { + Object id; + Type resolvedType = + resolveType(accessors.get(docIdPropertyName).getGenericReturnType(), types); + if (resolvedType == String.class) { + id = context.documentRef.getId(); + } else { + id = context.documentRef; + } + params[constructorParamIndexes.get(docIdPropertyName).intValue()] = id; + } + } + } + + private static final class RecordInspector { + private final Method _getRecordComponents; + private final Method _getName; + private final Method _getType; + private final Method _getAccessor; + + @SuppressWarnings("JavaReflectionMemberAccess") + private RecordInspector() { + try { + _getRecordComponents = Class.class.getMethod("getRecordComponents"); + Class recordComponentClass = Class.forName("java.lang.reflect.RecordComponent"); + _getName = recordComponentClass.getMethod("getName"); + _getType = recordComponentClass.getMethod("getType"); + _getAccessor = recordComponentClass.getMethod("getAccessor"); + } catch (ClassNotFoundException | NoSuchMethodException e) { + throw new IllegalStateException( + "Failed to access class or methods needed to support record serialization", e); + } + } + + private Constructor getCanonicalConstructor(Class cls) { + try { + Class[] paramTypes = + Arrays.stream(getRecordComponents(cls)).map(this::getType).toArray(Class[]::new); + Constructor constructor = cls.getDeclaredConstructor(paramTypes); + constructor.setAccessible(true); + return constructor; + } catch (NoSuchMethodException e) { + throw new IllegalStateException(e); + } + } + + private AnnotatedElement[] getRecordComponents(Class recordType) { + try { + return (AnnotatedElement[]) _getRecordComponents.invoke(recordType); + } catch (InvocationTargetException | IllegalAccessException e) { + throw new IllegalArgumentException( + "Failed to load components of record " + recordType.getName(), e); + } + } + + private Class getType(AnnotatedElement recordComponent) { + try { + return (Class) _getType.invoke(recordComponent); + } catch (InvocationTargetException | IllegalAccessException e) { + throw new IllegalArgumentException("Failed to get record component type", e); + } + } + + private String getName(AnnotatedElement recordComponent) { + try { + return (String) _getName.invoke(recordComponent); + } catch (InvocationTargetException | IllegalAccessException e) { + throw new IllegalArgumentException("Failed to get record component name", e); + } + } + + private Method getAccessor(AnnotatedElement recordComponent) { + try { + Method accessor = (Method) _getAccessor.invoke(recordComponent); + accessor.setAccessible(true); + return accessor; + } catch (InvocationTargetException | IllegalAccessException e) { + throw new IllegalArgumentException("Failed to get record component accessor", e); + } + } + } +} diff --git a/pom.xml b/pom.xml index cb8ec1254..32508c547 100644 --- a/pom.xml +++ b/pom.xml @@ -216,6 +216,7 @@ proto-google-cloud-firestore-bundle-v1 google-cloud-firestore-admin google-cloud-firestore-bom + google-cloud-firestore-jdk17 From ae4648662f8c108b338dbf36b6534907cc119ea7 Mon Sep 17 00:00:00 2001 From: eranl <1707552+eranl@users.noreply.github.com> Date: Sat, 23 Sep 2023 21:10:30 +0300 Subject: [PATCH 02/51] Add support for java record serialization - use field annotations --- google-cloud-firestore-jdk17/pom.xml | 4 +- .../jdk17/annotation/DocumentId.java | 46 ------------ .../jdk17/annotation/PropertyName.java | 30 -------- .../jdk17/annotation/ServerTimestamp.java | 31 -------- .../cloud/firestore/RecordMapperTest.java | 6 +- .../firestore/jdk17/LocalFirestoreHelper.java | 4 +- .../google/cloud/firestore/BeanMapper.java | 52 +++++++++++++ .../cloud/firestore/CustomClassMapper.java | 53 +------------- .../google/cloud/firestore/RecordMapper.java | 73 +++---------------- 9 files changed, 73 insertions(+), 226 deletions(-) delete mode 100644 google-cloud-firestore-jdk17/src/main/java/com/google/cloud/firestore/jdk17/annotation/DocumentId.java delete mode 100644 google-cloud-firestore-jdk17/src/main/java/com/google/cloud/firestore/jdk17/annotation/PropertyName.java delete mode 100644 google-cloud-firestore-jdk17/src/main/java/com/google/cloud/firestore/jdk17/annotation/ServerTimestamp.java diff --git a/google-cloud-firestore-jdk17/pom.xml b/google-cloud-firestore-jdk17/pom.xml index e1b7cf5a2..060de6b89 100644 --- a/google-cloud-firestore-jdk17/pom.xml +++ b/google-cloud-firestore-jdk17/pom.xml @@ -64,8 +64,8 @@ org.mockito - mockito-all - 1.10.19 + mockito-core + 2.25.0 test diff --git a/google-cloud-firestore-jdk17/src/main/java/com/google/cloud/firestore/jdk17/annotation/DocumentId.java b/google-cloud-firestore-jdk17/src/main/java/com/google/cloud/firestore/jdk17/annotation/DocumentId.java deleted file mode 100644 index 16729b7af..000000000 --- a/google-cloud-firestore-jdk17/src/main/java/com/google/cloud/firestore/jdk17/annotation/DocumentId.java +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.google.cloud.firestore.jdk17.annotation; - -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -/** - * Annotation used to mark a record component to be automatically populated with the document's ID - * when the record is created from a Cloud Firestore document (for example, via - * DocumentSnapshot#toObject). - * - *

- * - *

When using a record to write to a document (via DocumentReference#set or WriteBatch#set), the - * property annotated by @DocumentId is ignored, which allows writing the record back to any - * document, even if it's not the origin of the record. - */ -@Retention(RetentionPolicy.RUNTIME) -@Target(ElementType.RECORD_COMPONENT) -public @interface DocumentId {} diff --git a/google-cloud-firestore-jdk17/src/main/java/com/google/cloud/firestore/jdk17/annotation/PropertyName.java b/google-cloud-firestore-jdk17/src/main/java/com/google/cloud/firestore/jdk17/annotation/PropertyName.java deleted file mode 100644 index b453701c3..000000000 --- a/google-cloud-firestore-jdk17/src/main/java/com/google/cloud/firestore/jdk17/annotation/PropertyName.java +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Copyright 2017 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.google.cloud.firestore.jdk17.annotation; - -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -/** Marks a component to be renamed when serialized. */ -@Retention(RetentionPolicy.RUNTIME) -@Target(ElementType.RECORD_COMPONENT) -public @interface PropertyName { - - String value(); -} diff --git a/google-cloud-firestore-jdk17/src/main/java/com/google/cloud/firestore/jdk17/annotation/ServerTimestamp.java b/google-cloud-firestore-jdk17/src/main/java/com/google/cloud/firestore/jdk17/annotation/ServerTimestamp.java deleted file mode 100644 index f94f84389..000000000 --- a/google-cloud-firestore-jdk17/src/main/java/com/google/cloud/firestore/jdk17/annotation/ServerTimestamp.java +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Copyright 2017 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.google.cloud.firestore.jdk17.annotation; - -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -/** - * Annotation used to mark a timestamp component as being populated via Server Timestamps. If a - * record being written contains null for a @ServerTimestamp annotated component, it will be - * replaced with a server-generated timestamp. - */ -@Retention(RetentionPolicy.RUNTIME) -@Target(ElementType.RECORD_COMPONENT) -public @interface ServerTimestamp {} diff --git a/google-cloud-firestore-jdk17/src/test/java/com/google/cloud/firestore/RecordMapperTest.java b/google-cloud-firestore-jdk17/src/test/java/com/google/cloud/firestore/RecordMapperTest.java index af5e742d3..19c261a6b 100644 --- a/google-cloud-firestore-jdk17/src/test/java/com/google/cloud/firestore/RecordMapperTest.java +++ b/google-cloud-firestore-jdk17/src/test/java/com/google/cloud/firestore/RecordMapperTest.java @@ -28,18 +28,18 @@ import java.util.Map; import java.util.Set; +import com.google.cloud.firestore.annotation.DocumentId; +import com.google.cloud.firestore.annotation.PropertyName; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mockito; import org.mockito.Spy; -import org.mockito.runners.MockitoJUnitRunner; import com.google.cloud.firestore.annotation.ThrowOnExtraProperties; -import com.google.cloud.firestore.jdk17.annotation.DocumentId; -import com.google.cloud.firestore.jdk17.annotation.PropertyName; import com.google.cloud.firestore.spi.v1.FirestoreRpc; import com.google.common.collect.ImmutableList; import com.google.firestore.v1.DatabaseRootName; +import org.mockito.junit.MockitoJUnitRunner; import static com.google.cloud.firestore.jdk17.LocalFirestoreHelper.fromSingleQuotedString; import static com.google.cloud.firestore.jdk17.LocalFirestoreHelper.mapAnyType; diff --git a/google-cloud-firestore-jdk17/src/test/java/com/google/cloud/firestore/jdk17/LocalFirestoreHelper.java b/google-cloud-firestore-jdk17/src/test/java/com/google/cloud/firestore/jdk17/LocalFirestoreHelper.java index adeeb0cf9..bf34b1ecb 100644 --- a/google-cloud-firestore-jdk17/src/test/java/com/google/cloud/firestore/jdk17/LocalFirestoreHelper.java +++ b/google-cloud-firestore-jdk17/src/test/java/com/google/cloud/firestore/jdk17/LocalFirestoreHelper.java @@ -101,13 +101,13 @@ public record NestedRecord( public record ServerTimestamp ( - @com.google.cloud.firestore.jdk17.annotation.ServerTimestamp Date foo, + @com.google.cloud.firestore.annotation.ServerTimestamp Date foo, Inner inner ){ record Inner ( - @com.google.cloud.firestore.jdk17.annotation.ServerTimestamp Date bar + @com.google.cloud.firestore.annotation.ServerTimestamp Date bar ){} } diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/BeanMapper.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/BeanMapper.java index 7f49d1b6a..dc804a640 100644 --- a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/BeanMapper.java +++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/BeanMapper.java @@ -16,12 +16,21 @@ package com.google.cloud.firestore; +import com.google.cloud.Timestamp; +import com.google.cloud.firestore.annotation.DocumentId; import com.google.cloud.firestore.annotation.IgnoreExtraProperties; +import com.google.cloud.firestore.annotation.PropertyName; +import com.google.cloud.firestore.annotation.ServerTimestamp; import com.google.cloud.firestore.annotation.ThrowOnExtraProperties; + +import java.lang.reflect.AccessibleObject; +import java.lang.reflect.Field; import java.lang.reflect.Type; import java.lang.reflect.TypeVariable; import java.util.Collection; import java.util.Collections; +import java.util.Date; +import java.util.HashSet; import java.util.Map; /** Base bean mapper class, providing common functionality for class and record serialization. */ @@ -33,11 +42,19 @@ abstract class BeanMapper { // Whether to log a message if there are properties we don't know how to set to // custom object fields/setters or record components during deserialization. private final boolean warnOnUnknownProperties; + // A set of property names that were annotated with @ServerTimestamp. + final HashSet serverTimestamps; + // A set of property names that were annotated with @DocumentId. These properties will be + // populated with document ID values during deserialization, and be skipped during + // serialization. + final HashSet documentIdPropertyNames; BeanMapper(Class clazz) { this.clazz = clazz; throwOnUnknownProperties = clazz.isAnnotationPresent(ThrowOnExtraProperties.class); warnOnUnknownProperties = !clazz.isAnnotationPresent(IgnoreExtraProperties.class); + serverTimestamps = new HashSet<>(); + documentIdPropertyNames = new HashSet<>(); } Class getClazz() { @@ -128,4 +145,39 @@ void checkForDocIdConflict( T deserialize(Map values, DeserializeContext context) { return deserialize(values, Collections.emptyMap(), context); } + + void applyFieldAnnotations(Field field) { + if (field.isAnnotationPresent(ServerTimestamp.class)) { + Class fieldType = field.getType(); + if (fieldType != Date.class && fieldType != Timestamp.class) { + throw new IllegalArgumentException( + "Field " + + field.getName() + + " is annotated with @ServerTimestamp but is " + + fieldType + + " instead of Date or Timestamp."); + } + serverTimestamps.add(propertyName(field)); + } + + if (field.isAnnotationPresent(DocumentId.class)) { + Class fieldType = field.getType(); + ensureValidDocumentIdType("Field", "is", fieldType); + documentIdPropertyNames.add(propertyName(field)); + } + } + + static String propertyName(Field field) { + String annotatedName = annotatedName(field); + return annotatedName != null ? annotatedName : field.getName(); + } + + static String annotatedName(AccessibleObject obj) { + if (obj.isAnnotationPresent(PropertyName.class)) { + PropertyName annotation = obj.getAnnotation(PropertyName.class); + return annotation.value(); + } + + return null; + } } 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 a864b9ad1..17fea2d12 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 @@ -19,10 +19,9 @@ import com.google.cloud.Timestamp; import com.google.cloud.firestore.annotation.DocumentId; import com.google.cloud.firestore.annotation.Exclude; -import com.google.cloud.firestore.annotation.PropertyName; import com.google.cloud.firestore.annotation.ServerTimestamp; import com.google.firestore.v1.Value; -import java.lang.reflect.AccessibleObject; + import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.GenericArrayType; @@ -161,7 +160,7 @@ static Object serialize(T o, DeserializeContext.ErrorPath path) { String enumName = ((Enum) o).name(); try { Field enumField = o.getClass().getField(enumName); - return PojoBeanMapper.propertyName(enumField); + return BeanMapper.propertyName(enumField); } catch (NoSuchFieldException ex) { return enumName; } @@ -376,7 +375,7 @@ private static T deserializeToEnum( Field[] enumFields = clazz.getFields(); for (Field field : enumFields) { if (field.isEnumConstant()) { - String propertyName = PojoBeanMapper.propertyName(field); + String propertyName = BeanMapper.propertyName(field); if (value.equals(propertyName)) { value = field.getName(); break; @@ -638,14 +637,6 @@ private static class PojoBeanMapper extends BeanMapper { private final Map setters; private final Map fields; - // A set of property names that were annotated with @ServerTimestamp. - private final HashSet serverTimestamps; - - // A set of property names that were annotated with @DocumentId. These properties will be - // populated with document ID values during deserialization, and be skipped during - // serialization. - private final HashSet documentIdPropertyNames; - PojoBeanMapper(Class clazz) { super(clazz); properties = new HashMap<>(); @@ -654,9 +645,6 @@ private static class PojoBeanMapper extends BeanMapper { getters = new HashMap<>(); fields = new HashMap<>(); - serverTimestamps = new HashSet<>(); - documentIdPropertyNames = new HashSet<>(); - Constructor constructor; try { constructor = clazz.getDeclaredConstructor(); @@ -943,27 +931,6 @@ Map serialize(T object, DeserializeContext.ErrorPath path) { return result; } - private void applyFieldAnnotations(Field field) { - if (field.isAnnotationPresent(ServerTimestamp.class)) { - Class fieldType = field.getType(); - if (fieldType != Date.class && fieldType != Timestamp.class) { - throw new IllegalArgumentException( - "Field " - + field.getName() - + " is annotated with @ServerTimestamp but is " - + fieldType - + " instead of Date or Timestamp."); - } - serverTimestamps.add(propertyName(field)); - } - - if (field.isAnnotationPresent(DocumentId.class)) { - Class fieldType = field.getType(); - ensureValidDocumentIdType("Field", "is", fieldType); - documentIdPropertyNames.add(propertyName(field)); - } - } - private void applyGetterAnnotations(Method method) { if (method.isAnnotationPresent(ServerTimestamp.class)) { Class returnType = method.getReturnType(); @@ -1101,25 +1068,11 @@ private static boolean isSetterOverride(Method base, Method override) { && baseParameterTypes[0].equals(overrideParameterTypes[0]); } - private static String propertyName(Field field) { - String annotatedName = annotatedName(field); - return annotatedName != null ? annotatedName : field.getName(); - } - private static String propertyName(Method method) { String annotatedName = annotatedName(method); return annotatedName != null ? annotatedName : serializedName(method.getName()); } - private static String annotatedName(AccessibleObject obj) { - if (obj.isAnnotationPresent(PropertyName.class)) { - PropertyName annotation = obj.getAnnotation(PropertyName.class); - return annotation.value(); - } - - return null; - } - private static String serializedName(String methodName) { String[] prefixes = new String[] {"get", "set", "is"}; String methodPrefix = null; diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/RecordMapper.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/RecordMapper.java index 114e220bc..138d67330 100644 --- a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/RecordMapper.java +++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/RecordMapper.java @@ -16,20 +16,17 @@ package com.google.cloud.firestore; -import com.google.cloud.Timestamp; -import java.lang.annotation.Annotation; import java.lang.reflect.AnnotatedElement; import java.lang.reflect.Constructor; +import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.lang.reflect.Type; import java.lang.reflect.TypeVariable; import java.util.Arrays; -import java.util.Date; import java.util.HashMap; import java.util.HashSet; import java.util.Map; -import java.util.Optional; import java.util.Set; import java.util.logging.Logger; @@ -54,12 +51,6 @@ class RecordMapper extends BeanMapper { private final Map accessors = new HashMap<>(); private final Constructor constructor; private final Map constructorParamIndexes = new HashMap<>(); - // A set of property names that were annotated with @ServerTimestamp. - private final Set serverTimestamps = new HashSet<>(); - // A set of property names that were annotated with @DocumentId. These properties will be - // populated with document ID values during deserialization, and be skipped during - // serialization. - private final Set documentIdPropertyNames = new HashSet<>(); RecordMapper(Class clazz) { super(clazz); @@ -71,12 +62,16 @@ class RecordMapper extends BeanMapper { throw new RuntimeException("No properties to serialize found on class " + clazz.getName()); } - for (int i = 0; i < recordComponents.length; i++) { - AnnotatedElement recordComponent = recordComponents[i]; - String propertyName = propertyName(recordComponent); - constructorParamIndexes.put(propertyName, i); - accessors.put(propertyName, RECORD_INSPECTOR.getAccessor(recordComponent)); - applyComponentAnnotations(recordComponent); + try { + for (int i = 0; i < recordComponents.length; i++) { + Field field = clazz.getDeclaredField(RECORD_INSPECTOR.getName(recordComponents[i])); + String propertyName = propertyName(field); + constructorParamIndexes.put(propertyName, i); + accessors.put(propertyName, RECORD_INSPECTOR.getAccessor(recordComponents[i])); + applyFieldAnnotations(field); + } + } catch (NoSuchFieldException e) { + throw new RuntimeException(e); } } @@ -151,52 +146,6 @@ T deserialize( } } - private void applyComponentAnnotations(AnnotatedElement component) { - if (isAnnotationPresent(component, "ServerTimestamp")) { - Class componentType = RECORD_INSPECTOR.getType(component); - if (componentType != Date.class && componentType != Timestamp.class) { - throw new IllegalArgumentException( - "Component " - + RECORD_INSPECTOR.getName(component) - + " is annotated with @ServerTimestamp but is " - + componentType - + " instead of Date or Timestamp."); - } - serverTimestamps.add(propertyName(component)); - } - - if (isAnnotationPresent(component, "DocumentId")) { - Class type = RECORD_INSPECTOR.getType(component); - ensureValidDocumentIdType("Component", "is", type); - documentIdPropertyNames.add(propertyName(component)); - } - } - - private static String propertyName(AnnotatedElement component) { - Optional propertyName = getAnnotation(component, "PropertyName"); - if (propertyName.isPresent()) { - Annotation annotation = propertyName.get(); - try { - return (String) annotation.getClass().getMethod("value").invoke(annotation); - } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) { - throw new IllegalArgumentException("Failed to get PropertyName annotation value", e); - } - } - - return RECORD_INSPECTOR.getName(component); - } - - private static boolean isAnnotationPresent(AnnotatedElement element, String annotationName) { - return getAnnotation(element, annotationName).isPresent(); - } - - private static Optional getAnnotation( - AnnotatedElement element, String annotationName) { - return Arrays.stream(element.getAnnotations()) - .filter(annotation -> annotation.annotationType().getSimpleName().equals(annotationName)) - .findAny(); - } - // Populate @DocumentId annotated components. If there is a conflict (@DocumentId annotation is // applied to a property that is already deserialized from the firestore document) // a runtime exception will be thrown. From b50fc2549c1e3d7310b719eec7069d95e122ef36 Mon Sep 17 00:00:00 2001 From: eranl <1707552+eranl@users.noreply.github.com> Date: Sat, 23 Sep 2023 22:45:00 +0300 Subject: [PATCH 03/51] Add support for java record serialization - update doc --- .../main/java/com/google/cloud/firestore/RecordMapper.java | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/RecordMapper.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/RecordMapper.java index 138d67330..d315bbaf5 100644 --- a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/RecordMapper.java +++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/RecordMapper.java @@ -33,11 +33,8 @@ /** * Serializes java records. Uses automatic record constructors and accessors only. Therefore, * exclusion of fields is not supported. Supports DocumentId, PropertyName, and ServerTimestamp - * annotations on record components. However, those annotations have to - * include @Target(ElementType.RECORD_COMPONENT), so the ones defined in the - * google-cloud-firestore-jdk17 module should be used, rather than the ones defined in this module. - * Since records are not supported in JDK8, reflection is used for inspecting record metadata. This - * class will fail to load on java versions that don't support records. + * annotations on record components. + * Since records are not supported in JDK versions < 16, reflection is used for inspecting record metadata. * * @author Eran Leshem */ From d050e2425d46b817dc5908a0dd6146c338dc03ae Mon Sep 17 00:00:00 2001 From: eranl <1707552+eranl@users.noreply.github.com> Date: Sat, 28 Oct 2023 04:08:42 +0300 Subject: [PATCH 04/51] Add support for java record serialization - fix formatting --- .../src/main/java/com/google/cloud/firestore/BeanMapper.java | 1 - .../java/com/google/cloud/firestore/CustomClassMapper.java | 1 - .../main/java/com/google/cloud/firestore/RecordMapper.java | 4 ++-- 3 files changed, 2 insertions(+), 4 deletions(-) diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/BeanMapper.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/BeanMapper.java index dc804a640..ef25554bc 100644 --- a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/BeanMapper.java +++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/BeanMapper.java @@ -22,7 +22,6 @@ import com.google.cloud.firestore.annotation.PropertyName; import com.google.cloud.firestore.annotation.ServerTimestamp; import com.google.cloud.firestore.annotation.ThrowOnExtraProperties; - import java.lang.reflect.AccessibleObject; import java.lang.reflect.Field; import java.lang.reflect.Type; 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 17fea2d12..0011555c6 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 @@ -21,7 +21,6 @@ import com.google.cloud.firestore.annotation.Exclude; import com.google.cloud.firestore.annotation.ServerTimestamp; import com.google.firestore.v1.Value; - import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.GenericArrayType; diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/RecordMapper.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/RecordMapper.java index d315bbaf5..0dd4ae909 100644 --- a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/RecordMapper.java +++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/RecordMapper.java @@ -33,8 +33,8 @@ /** * Serializes java records. Uses automatic record constructors and accessors only. Therefore, * exclusion of fields is not supported. Supports DocumentId, PropertyName, and ServerTimestamp - * annotations on record components. - * Since records are not supported in JDK versions < 16, reflection is used for inspecting record metadata. + * annotations on record components. Since records are not supported in JDK versions < 16, + * reflection is used for inspecting record metadata. * * @author Eran Leshem */ From 53aabf50517d8b4006917c69b86ba8ecac67b68c Mon Sep 17 00:00:00 2001 From: eranl <1707552+eranl@users.noreply.github.com> Date: Fri, 3 Nov 2023 20:23:58 +0200 Subject: [PATCH 05/51] Add support for java record serialization - move test code to google-cloud-firestore module by adding a test-jdk17 directory and a java17 profile, and eliminate google-cloud-firestore-jdk17 module --- google-cloud-firestore-jdk17/pom.xml | 121 ------------------ google-cloud-firestore/pom.xml | 44 +++++++ .../RecordDocumentReferenceTest.java | 51 ++++---- .../cloud/firestore/RecordMapperTest.java | 34 ++--- .../record}/LocalFirestoreHelper.java | 34 +++-- pom.xml | 1 - 6 files changed, 104 insertions(+), 181 deletions(-) delete mode 100644 google-cloud-firestore-jdk17/pom.xml rename {google-cloud-firestore-jdk17/src/test => google-cloud-firestore/src/test-jdk17}/java/com/google/cloud/firestore/RecordDocumentReferenceTest.java (88%) rename {google-cloud-firestore-jdk17/src/test => google-cloud-firestore/src/test-jdk17}/java/com/google/cloud/firestore/RecordMapperTest.java (98%) rename {google-cloud-firestore-jdk17/src/test/java/com/google/cloud/firestore/jdk17 => google-cloud-firestore/src/test-jdk17/java/com/google/cloud/firestore/record}/LocalFirestoreHelper.java (99%) diff --git a/google-cloud-firestore-jdk17/pom.xml b/google-cloud-firestore-jdk17/pom.xml deleted file mode 100644 index 060de6b89..000000000 --- a/google-cloud-firestore-jdk17/pom.xml +++ /dev/null @@ -1,121 +0,0 @@ - - - 4.0.0 - google-cloud-firestore-jdk17 - 3.8.3-SNAPSHOT - jar - Google Cloud Firestore JDK17 extension - https://github.com/googleapis/java-firestore - - Java idiomatic client for Google Cloud Firestore. - - - com.google.cloud - google-cloud-firestore-parent - 3.8.3-SNAPSHOT - - - google-cloud-firestore-jdk17 - 17 - 17 - - - - - - javax.annotation - javax.annotation-api - test - - - com.google.api - api-common - test - - - com.google.cloud - google-cloud-core - test - - - com.google.cloud - google-cloud-firestore - test - - - com.google.api.grpc - proto-google-cloud-firestore-v1 - test - - - com.google.guava - guava - test - - - com.google.code.gson - gson - test - - - junit - junit - test - - - org.mockito - mockito-core - 2.25.0 - test - - - - - com.google.api - gax - testlib - test - - - - - - - org.apache.maven.plugins - maven-javadoc-plugin - 3.5.0 - - protected - true - none - true - - - - - - - - - - maven-compiler-plugin - 3.10.1 - - 17 - 17 - UTF-8 - -Xlint:unchecked - -Xlint:deprecation - true - - - - - - - org.codehaus.mojo - flatten-maven-plugin - - - - diff --git a/google-cloud-firestore/pom.xml b/google-cloud-firestore/pom.xml index 395a4025d..492786304 100644 --- a/google-cloud-firestore/pom.xml +++ b/google-cloud-firestore/pom.xml @@ -230,5 +230,49 @@ + + + java17 + + 17 + + + + + org.codehaus.mojo + build-helper-maven-plugin + + + add-test-source + generate-test-sources + + add-test-source + + + + src/test-jdk17/java + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + true + + + 17 + 17 + + -parameters + --add-opens=java.base/java.lang=ALL-UNNAMED + --add-opens=java.base/java.util=ALL-UNNAMED + + + + + + diff --git a/google-cloud-firestore-jdk17/src/test/java/com/google/cloud/firestore/RecordDocumentReferenceTest.java b/google-cloud-firestore/src/test-jdk17/java/com/google/cloud/firestore/RecordDocumentReferenceTest.java similarity index 88% rename from google-cloud-firestore-jdk17/src/test/java/com/google/cloud/firestore/RecordDocumentReferenceTest.java rename to google-cloud-firestore/src/test-jdk17/java/com/google/cloud/firestore/RecordDocumentReferenceTest.java index 52f6e590f..405ca2dc4 100644 --- a/google-cloud-firestore-jdk17/src/test/java/com/google/cloud/firestore/RecordDocumentReferenceTest.java +++ b/google-cloud-firestore/src/test-jdk17/java/com/google/cloud/firestore/RecordDocumentReferenceTest.java @@ -16,7 +16,7 @@ package com.google.cloud.firestore; -import static com.google.cloud.firestore.jdk17.LocalFirestoreHelper.*; +import static com.google.cloud.firestore.record.LocalFirestoreHelper.*; import static org.junit.Assert.assertEquals; import static org.junit.Assert.fail; import static org.mockito.Mockito.doAnswer; @@ -25,28 +25,29 @@ import com.google.api.gax.rpc.ResponseObserver; import com.google.api.gax.rpc.ServerStreamingCallable; import com.google.api.gax.rpc.UnaryCallable; -import com.google.cloud.firestore.jdk17.LocalFirestoreHelper; import com.google.cloud.firestore.spi.v1.FirestoreRpc; import com.google.firestore.v1.BatchGetDocumentsRequest; import com.google.firestore.v1.CommitRequest; import com.google.firestore.v1.CommitResponse; import com.google.firestore.v1.Value; -import java.math.BigInteger; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; +import org.mockito.ArgumentMatchers; import org.mockito.Captor; -import org.mockito.Matchers; import org.mockito.Mockito; import org.mockito.Spy; -import org.mockito.runners.MockitoJUnitRunner; +import org.mockito.junit.MockitoJUnitRunner; + +import java.math.BigInteger; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + @RunWith(MockitoJUnitRunner.class) public class RecordDocumentReferenceTest { @@ -75,7 +76,7 @@ public void serializeBasicTypes() throws Exception { doReturn(SINGLE_WRITE_COMMIT_RESPONSE) .when(firestoreMock) .sendRequest( - commitCapture.capture(), Matchers.>any()); + commitCapture.capture(), ArgumentMatchers.>any()); documentReference.set(ALL_SUPPORTED_TYPES_OBJECT).get(); @@ -127,7 +128,7 @@ public void doesNotDeserializeAdvancedNumberTypes() throws Exception { .streamRequest( getAllCapture.capture(), streamObserverCapture.capture(), - Matchers.any()); + ArgumentMatchers.any()); var snapshot = documentReference.get().get(); try { @@ -148,7 +149,7 @@ public void createDocument() throws Exception { doReturn(SINGLE_WRITE_COMMIT_RESPONSE) .when(firestoreMock) .sendRequest( - commitCapture.capture(), Matchers.>any()); + commitCapture.capture(), ArgumentMatchers.>any()); documentReference.create(SINGLE_COMPONENT_OBJECT).get(); @@ -163,9 +164,9 @@ public void createWithServerTimestamp() throws Exception { doReturn(SINGLE_WRITE_COMMIT_RESPONSE) .when(firestoreMock) .sendRequest( - commitCapture.capture(), Matchers.>any()); + commitCapture.capture(), ArgumentMatchers.>any()); - documentReference.create(LocalFirestoreHelper.SERVER_TIMESTAMP_OBJECT).get(); + documentReference.create(SERVER_TIMESTAMP_OBJECT).get(); var create = commit( @@ -181,9 +182,9 @@ public void setWithServerTimestamp() throws Exception { doReturn(FIELD_TRANSFORM_COMMIT_RESPONSE) .when(firestoreMock) .sendRequest( - commitCapture.capture(), Matchers.>any()); + commitCapture.capture(), ArgumentMatchers.>any()); - documentReference.set(LocalFirestoreHelper.SERVER_TIMESTAMP_OBJECT).get(); + documentReference.set(SERVER_TIMESTAMP_OBJECT).get(); var set = commit( @@ -199,10 +200,10 @@ public void mergeWithServerTimestamps() throws Exception { doReturn(SINGLE_WRITE_COMMIT_RESPONSE) .when(firestoreMock) .sendRequest( - commitCapture.capture(), Matchers.>any()); + commitCapture.capture(), ArgumentMatchers.>any()); documentReference - .set(LocalFirestoreHelper.SERVER_TIMESTAMP_OBJECT, SetOptions.mergeFields("inner.bar")) + .set(SERVER_TIMESTAMP_OBJECT, SetOptions.mergeFields("inner.bar")) .get(); var set = @@ -219,7 +220,7 @@ public void setDocumentWithMerge() throws Exception { doReturn(SINGLE_WRITE_COMMIT_RESPONSE) .when(firestoreMock) .sendRequest( - commitCapture.capture(), Matchers.>any()); + commitCapture.capture(), ArgumentMatchers.>any()); documentReference.set(SINGLE_COMPONENT_OBJECT, SetOptions.merge()).get(); documentReference.set(ALL_SUPPORTED_TYPES_OBJECT, SetOptions.mergeFields("foo")).get(); @@ -244,7 +245,7 @@ public void setDocumentWithNestedMerge() throws Exception { doReturn(SINGLE_WRITE_COMMIT_RESPONSE) .when(firestoreMock) .sendRequest( - commitCapture.capture(), Matchers.>any()); + commitCapture.capture(), ArgumentMatchers.>any()); documentReference.set(NESTED_RECORD_OBJECT, SetOptions.mergeFields("first.foo")).get(); documentReference @@ -273,7 +274,7 @@ public void setMultipleFieldsWithMerge() throws Exception { doReturn(SINGLE_WRITE_COMMIT_RESPONSE) .when(firestoreMock) .sendRequest( - commitCapture.capture(), Matchers.>any()); + commitCapture.capture(), ArgumentMatchers.>any()); documentReference .set( @@ -301,7 +302,7 @@ public void setNestedMapWithMerge() throws Exception { doReturn(SINGLE_WRITE_COMMIT_RESPONSE) .when(firestoreMock) .sendRequest( - commitCapture.capture(), Matchers.>any()); + commitCapture.capture(), ArgumentMatchers.>any()); documentReference.set(NESTED_RECORD_OBJECT, SetOptions.mergeFields("first", "second")).get(); @@ -321,7 +322,7 @@ public void extractFieldMaskFromMerge() throws Exception { doReturn(SINGLE_WRITE_COMMIT_RESPONSE) .when(firestoreMock) .sendRequest( - commitCapture.capture(), Matchers.>any()); + commitCapture.capture(), ArgumentMatchers.>any()); documentReference.set(NESTED_RECORD_OBJECT, SetOptions.merge()).get(); diff --git a/google-cloud-firestore-jdk17/src/test/java/com/google/cloud/firestore/RecordMapperTest.java b/google-cloud-firestore/src/test-jdk17/java/com/google/cloud/firestore/RecordMapperTest.java similarity index 98% rename from google-cloud-firestore-jdk17/src/test/java/com/google/cloud/firestore/RecordMapperTest.java rename to google-cloud-firestore/src/test-jdk17/java/com/google/cloud/firestore/RecordMapperTest.java index 19c261a6b..dc256e1ba 100644 --- a/google-cloud-firestore-jdk17/src/test/java/com/google/cloud/firestore/RecordMapperTest.java +++ b/google-cloud-firestore/src/test-jdk17/java/com/google/cloud/firestore/RecordMapperTest.java @@ -16,6 +16,18 @@ package com.google.cloud.firestore; +import com.google.cloud.firestore.annotation.DocumentId; +import com.google.cloud.firestore.annotation.PropertyName; +import com.google.cloud.firestore.annotation.ThrowOnExtraProperties; +import com.google.cloud.firestore.spi.v1.FirestoreRpc; +import com.google.common.collect.ImmutableList; +import com.google.firestore.v1.DatabaseRootName; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mockito; +import org.mockito.Spy; +import org.mockito.junit.MockitoJUnitRunner; + import java.io.Serializable; import java.math.BigDecimal; import java.util.ArrayList; @@ -28,22 +40,12 @@ import java.util.Map; import java.util.Set; -import com.google.cloud.firestore.annotation.DocumentId; -import com.google.cloud.firestore.annotation.PropertyName; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.Mockito; -import org.mockito.Spy; - -import com.google.cloud.firestore.annotation.ThrowOnExtraProperties; -import com.google.cloud.firestore.spi.v1.FirestoreRpc; -import com.google.common.collect.ImmutableList; -import com.google.firestore.v1.DatabaseRootName; -import org.mockito.junit.MockitoJUnitRunner; - -import static com.google.cloud.firestore.jdk17.LocalFirestoreHelper.fromSingleQuotedString; -import static com.google.cloud.firestore.jdk17.LocalFirestoreHelper.mapAnyType; -import static org.junit.Assert.*; +import static com.google.cloud.firestore.record.LocalFirestoreHelper.fromSingleQuotedString; +import static com.google.cloud.firestore.record.LocalFirestoreHelper.mapAnyType; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; @SuppressWarnings({"unused", "WeakerAccess", "SpellCheckingInspection"}) @RunWith(MockitoJUnitRunner.class) diff --git a/google-cloud-firestore-jdk17/src/test/java/com/google/cloud/firestore/jdk17/LocalFirestoreHelper.java b/google-cloud-firestore/src/test-jdk17/java/com/google/cloud/firestore/record/LocalFirestoreHelper.java similarity index 99% rename from google-cloud-firestore-jdk17/src/test/java/com/google/cloud/firestore/jdk17/LocalFirestoreHelper.java rename to google-cloud-firestore/src/test-jdk17/java/com/google/cloud/firestore/record/LocalFirestoreHelper.java index bf34b1ecb..ccb30f64e 100644 --- a/google-cloud-firestore-jdk17/src/test/java/com/google/cloud/firestore/jdk17/LocalFirestoreHelper.java +++ b/google-cloud-firestore/src/test-jdk17/java/com/google/cloud/firestore/record/LocalFirestoreHelper.java @@ -14,24 +14,7 @@ * limitations under the License. */ -package com.google.cloud.firestore.jdk17; - -import java.math.BigInteger; -import java.text.ParseException; -import java.text.SimpleDateFormat; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.Comparator; -import java.util.Date; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.concurrent.TimeUnit; - -import javax.annotation.Nullable; - -import org.mockito.stubbing.Answer; +package com.google.cloud.firestore.record; import com.google.api.core.ApiFuture; import com.google.api.core.ApiFutures; @@ -56,6 +39,21 @@ import com.google.protobuf.ByteString; import com.google.protobuf.NullValue; import com.google.type.LatLng; +import org.mockito.stubbing.Answer; + +import javax.annotation.Nullable; +import java.math.BigInteger; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Comparator; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; import static org.junit.Assert.assertEquals; import static org.mockito.Mockito.mock; diff --git a/pom.xml b/pom.xml index 52092d70d..e5347ba5b 100644 --- a/pom.xml +++ b/pom.xml @@ -216,7 +216,6 @@ proto-google-cloud-firestore-bundle-v1 google-cloud-firestore-admin google-cloud-firestore-bom - google-cloud-firestore-jdk17 From 3fc7a23fb5b54e3bb7d4aa0e494f5376a4b68440 Mon Sep 17 00:00:00 2001 From: milaGGL <107142260+milaGGL@users.noreply.github.com> Date: Wed, 28 Aug 2024 13:53:04 -0400 Subject: [PATCH 06/51] check if CI running the new tests --- .../com/google/cloud/firestore/RecordDocumentReferenceTest.java | 1 + 1 file changed, 1 insertion(+) diff --git a/google-cloud-firestore/src/test-jdk17/java/com/google/cloud/firestore/RecordDocumentReferenceTest.java b/google-cloud-firestore/src/test-jdk17/java/com/google/cloud/firestore/RecordDocumentReferenceTest.java index 405ca2dc4..a70d58560 100644 --- a/google-cloud-firestore/src/test-jdk17/java/com/google/cloud/firestore/RecordDocumentReferenceTest.java +++ b/google-cloud-firestore/src/test-jdk17/java/com/google/cloud/firestore/RecordDocumentReferenceTest.java @@ -86,6 +86,7 @@ public void serializeBasicTypes() throws Exception { @Test public void doesNotSerializeAdvancedNumberTypes() { + System.out.println("========================== Record tests run on CI"); Map expectedErrorMessages = new HashMap<>(); var record = new InvalidRecord(new BigInteger("0"), null, null); From b6197d5c070ae160ccebfd69103fb8a8eb523b33 Mon Sep 17 00:00:00 2001 From: milaGGL <107142260+milaGGL@users.noreply.github.com> Date: Wed, 28 Aug 2024 14:03:18 -0400 Subject: [PATCH 07/51] check if new pom setup leads to units(8) failure --- google-cloud-firestore/pom.xml | 44 ---------------------------------- 1 file changed, 44 deletions(-) diff --git a/google-cloud-firestore/pom.xml b/google-cloud-firestore/pom.xml index 869bd7afe..2516b9165 100644 --- a/google-cloud-firestore/pom.xml +++ b/google-cloud-firestore/pom.xml @@ -321,49 +321,5 @@ - - - java17 - - 17 - - - - - org.codehaus.mojo - build-helper-maven-plugin - - - add-test-source - generate-test-sources - - add-test-source - - - - src/test-jdk17/java - - - - - - - org.apache.maven.plugins - maven-compiler-plugin - true - - - 17 - 17 - - -parameters - --add-opens=java.base/java.lang=ALL-UNNAMED - --add-opens=java.base/java.util=ALL-UNNAMED - - - - - - From b155bee1c2783faa9dac2e93843145e6bb5123b1 Mon Sep 17 00:00:00 2001 From: milaGGL <107142260+milaGGL@users.noreply.github.com> Date: Wed, 28 Aug 2024 14:10:13 -0400 Subject: [PATCH 08/51] try ignoring the ToStringTest --- .kokoro/build.sh | 1 + google-cloud-firestore/pom.xml | 44 ++++++++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+) diff --git a/.kokoro/build.sh b/.kokoro/build.sh index 205347243..0b620a739 100755 --- a/.kokoro/build.sh +++ b/.kokoro/build.sh @@ -49,6 +49,7 @@ case ${JOB_TYPE} in test) echo "SUREFIRE_JVM_OPT: ${SUREFIRE_JVM_OPT}" mvn test -B -ntp -Dclirr.skip=true -Denforcer.skip=true ${SUREFIRE_JVM_OPT} + javap -verbose -cp google-cloud-firestore/target/test-classes com/google/cloud/firestore/ToStringTest RETURN_CODE=$? ;; lint) diff --git a/google-cloud-firestore/pom.xml b/google-cloud-firestore/pom.xml index 2516b9165..869bd7afe 100644 --- a/google-cloud-firestore/pom.xml +++ b/google-cloud-firestore/pom.xml @@ -321,5 +321,49 @@ + + + java17 + + 17 + + + + + org.codehaus.mojo + build-helper-maven-plugin + + + add-test-source + generate-test-sources + + add-test-source + + + + src/test-jdk17/java + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + true + + + 17 + 17 + + -parameters + --add-opens=java.base/java.lang=ALL-UNNAMED + --add-opens=java.base/java.util=ALL-UNNAMED + + + + + + From 0bea0231d8359507fee7941245d6216a61981a1e Mon Sep 17 00:00:00 2001 From: milaGGL <107142260+milaGGL@users.noreply.github.com> Date: Wed, 28 Aug 2024 15:17:55 -0400 Subject: [PATCH 09/51] remove var --- .../RecordDocumentReferenceTest.java | 54 ++--- .../cloud/firestore/RecordMapperTest.java | 220 +++++++++--------- .../record/LocalFirestoreHelper.java | 54 ++--- 3 files changed, 164 insertions(+), 164 deletions(-) diff --git a/google-cloud-firestore/src/test-jdk17/java/com/google/cloud/firestore/RecordDocumentReferenceTest.java b/google-cloud-firestore/src/test-jdk17/java/com/google/cloud/firestore/RecordDocumentReferenceTest.java index a70d58560..c18f50357 100644 --- a/google-cloud-firestore/src/test-jdk17/java/com/google/cloud/firestore/RecordDocumentReferenceTest.java +++ b/google-cloud-firestore/src/test-jdk17/java/com/google/cloud/firestore/RecordDocumentReferenceTest.java @@ -80,7 +80,7 @@ public void serializeBasicTypes() throws Exception { documentReference.set(ALL_SUPPORTED_TYPES_OBJECT).get(); - var expectedCommit = commit(set(ALL_SUPPORTED_TYPES_PROTO)); + CommitRequest expectedCommit = commit(set(ALL_SUPPORTED_TYPES_PROTO)); assertCommitEquals(expectedCommit, commitCapture.getAllValues().get(0)); } @@ -89,7 +89,7 @@ public void doesNotSerializeAdvancedNumberTypes() { System.out.println("========================== Record tests run on CI"); Map expectedErrorMessages = new HashMap<>(); - var record = new InvalidRecord(new BigInteger("0"), null, null); + InvalidRecord record = new InvalidRecord(new BigInteger("0"), null, null); expectedErrorMessages.put( record, "Could not serialize object. Numbers of type BigInteger are not supported, please use an int, long, float, double or BigDecimal (found in field 'bigIntegerValue')"); @@ -104,7 +104,7 @@ record = new InvalidRecord(null, null, (short) 0); record, "Could not serialize object. Numbers of type Short are not supported, please use an int, long, float, double or BigDecimal (found in field 'shortValue')"); - for (var testCase : expectedErrorMessages.entrySet()) { + for (Map.Entry testCase : expectedErrorMessages.entrySet()) { try { documentReference.set(testCase.getKey()); fail(); @@ -116,13 +116,13 @@ record = new InvalidRecord(null, null, (short) 0); @Test public void doesNotDeserializeAdvancedNumberTypes() throws Exception { - var fieldNamesToTypeNames = + Map fieldNamesToTypeNames = map("bigIntegerValue", "BigInteger", "shortValue", "Short", "byteValue", "Byte"); - for (var testCase : fieldNamesToTypeNames.entrySet()) { - var fieldName = testCase.getKey(); - var typeName = testCase.getValue(); - var response = map(fieldName, Value.newBuilder().setIntegerValue(0).build()); + for (Entry testCase : fieldNamesToTypeNames.entrySet()) { + String fieldName = testCase.getKey(); + String typeName = testCase.getValue(); + Map response = map(fieldName, Value.newBuilder().setIntegerValue(0).build()); doAnswer(getAllResponse(response)) .when(firestoreMock) @@ -131,7 +131,7 @@ public void doesNotDeserializeAdvancedNumberTypes() throws Exception { streamObserverCapture.capture(), ArgumentMatchers.any()); - var snapshot = documentReference.get().get(); + DocumentSnapshot snapshot = documentReference.get().get(); try { snapshot.toObject(InvalidRecord.class); fail(); @@ -169,12 +169,12 @@ public void createWithServerTimestamp() throws Exception { documentReference.create(SERVER_TIMESTAMP_OBJECT).get(); - var create = + CommitRequest create = commit( create(Collections.emptyMap()), transform("foo", serverTimestamp(), "inner.bar", serverTimestamp())); - var commitRequests = commitCapture.getAllValues(); + List commitRequests = commitCapture.getAllValues(); assertCommitEquals(create, commitRequests.get(0)); } @@ -187,12 +187,12 @@ public void setWithServerTimestamp() throws Exception { documentReference.set(SERVER_TIMESTAMP_OBJECT).get(); - var set = + CommitRequest set = commit( set(SERVER_TIMESTAMP_PROTO), transform("foo", serverTimestamp(), "inner.bar", serverTimestamp())); - var commitRequests = commitCapture.getAllValues(); + List commitRequests = commitCapture.getAllValues(); assertCommitEquals(set, commitRequests.get(0)); } @@ -207,12 +207,12 @@ public void mergeWithServerTimestamps() throws Exception { .set(SERVER_TIMESTAMP_OBJECT, SetOptions.mergeFields("inner.bar")) .get(); - var set = + CommitRequest set = commit( set(SERVER_TIMESTAMP_PROTO, new ArrayList<>()), transform("inner.bar", serverTimestamp())); - var commitRequests = commitCapture.getAllValues(); + List commitRequests = commitCapture.getAllValues(); assertCommitEquals(set, commitRequests.get(0)); } @@ -234,9 +234,9 @@ public void setDocumentWithMerge() throws Exception { SetOptions.mergeFieldPaths(Arrays.asList(FieldPath.of("foo")))) .get(); - var expectedCommit = commit(set(SINGLE_COMPONENT_PROTO, Arrays.asList("foo"))); + CommitRequest expectedCommit = commit(set(SINGLE_COMPONENT_PROTO, Arrays.asList("foo"))); - for (var i = 0; i < 4; ++i) { + for (int i = 0; i < 4; ++i) { assertCommitEquals(expectedCommit, commitCapture.getAllValues().get(i)); } } @@ -259,13 +259,13 @@ public void setDocumentWithNestedMerge() throws Exception { .get(); Map nestedUpdate = new HashMap<>(); - var nestedProto = Value.newBuilder(); + Value.Builder nestedProto = Value.newBuilder(); nestedProto.getMapValueBuilder().putAllFields(SINGLE_COMPONENT_PROTO); nestedUpdate.put("first", nestedProto.build()); - var expectedCommit = commit(set(nestedUpdate, Arrays.asList("first.foo"))); + CommitRequest expectedCommit = commit(set(nestedUpdate, Arrays.asList("first.foo"))); - for (var i = 0; i < 3; ++i) { + for (int i = 0; i < 3; ++i) { assertCommitEquals(expectedCommit, commitCapture.getAllValues().get(i)); } } @@ -284,7 +284,7 @@ public void setMultipleFieldsWithMerge() throws Exception { .get(); Map nestedUpdate = new HashMap<>(); - var nestedProto = Value.newBuilder(); + Value.Builder nestedProto = Value.newBuilder(); nestedProto.getMapValueBuilder().putAllFields(SINGLE_COMPONENT_PROTO); nestedUpdate.put("first", nestedProto.build()); nestedProto @@ -292,7 +292,7 @@ public void setMultipleFieldsWithMerge() throws Exception { .putFields("trueValue", Value.newBuilder().setBooleanValue(true).build()); nestedUpdate.put("second", nestedProto.build()); - var expectedCommit = + CommitRequest expectedCommit = commit(set(nestedUpdate, Arrays.asList("first.foo", "second.foo", "second.trueValue"))); assertCommitEquals(expectedCommit, commitCapture.getValue()); @@ -308,13 +308,13 @@ public void setNestedMapWithMerge() throws Exception { documentReference.set(NESTED_RECORD_OBJECT, SetOptions.mergeFields("first", "second")).get(); Map nestedUpdate = new HashMap<>(); - var nestedProto = Value.newBuilder(); + Value.Builder nestedProto = Value.newBuilder(); nestedProto.getMapValueBuilder().putAllFields(SINGLE_COMPONENT_PROTO); nestedUpdate.put("first", nestedProto.build()); nestedProto.getMapValueBuilder().putAllFields(ALL_SUPPORTED_TYPES_PROTO); nestedUpdate.put("second", nestedProto.build()); - var expectedCommit = commit(set(nestedUpdate, Arrays.asList("first", "second"))); + CommitRequest expectedCommit = commit(set(nestedUpdate, Arrays.asList("first", "second"))); assertCommitEquals(expectedCommit, commitCapture.getValue()); } @@ -328,13 +328,13 @@ public void extractFieldMaskFromMerge() throws Exception { documentReference.set(NESTED_RECORD_OBJECT, SetOptions.merge()).get(); Map nestedUpdate = new HashMap<>(); - var nestedProto = Value.newBuilder(); + Value.Builder nestedProto = Value.newBuilder(); nestedProto.getMapValueBuilder().putAllFields(SINGLE_COMPONENT_PROTO); nestedUpdate.put("first", nestedProto.build()); nestedProto.getMapValueBuilder().putAllFields(ALL_SUPPORTED_TYPES_PROTO); nestedUpdate.put("second", nestedProto.build()); - var updateMask = + List updateMask = Arrays.asList( "first.foo", "second.arrayValue", @@ -354,7 +354,7 @@ public void extractFieldMaskFromMerge() throws Exception { "second.trueValue", "second.model.foo"); - var expectedCommit = commit(set(nestedUpdate, updateMask)); + CommitRequest expectedCommit = commit(set(nestedUpdate, updateMask)); assertCommitEquals(expectedCommit, commitCapture.getValue()); } } diff --git a/google-cloud-firestore/src/test-jdk17/java/com/google/cloud/firestore/RecordMapperTest.java b/google-cloud-firestore/src/test-jdk17/java/com/google/cloud/firestore/RecordMapperTest.java index dc256e1ba..3b6103206 100644 --- a/google-cloud-firestore/src/test-jdk17/java/com/google/cloud/firestore/RecordMapperTest.java +++ b/google-cloud-firestore/src/test-jdk17/java/com/google/cloud/firestore/RecordMapperTest.java @@ -231,7 +231,7 @@ private static T deserialize(Map json, Class clazz) { } private static T deserialize(String jsonString, Class clazz, DocumentReference docRef) { - var json = fromSingleQuotedString(jsonString); + Map json = fromSingleQuotedString(jsonString); return CustomClassMapper.convertToCustomClass(json, clazz, docRef); } @@ -268,7 +268,7 @@ private static T convertToCustomClass(Object object, Class clazz) { @Test public void primitiveDeserializeString() { - var bean = deserialize("{'value': 'foo'}", StringBean.class); + StringBean bean = deserialize("{'value': 'foo'}", StringBean.class); assertEquals("foo", bean.value()); // Double @@ -302,7 +302,7 @@ public void primitiveDeserializeString() { @Test public void primitiveDeserializeBoolean() { - var beanBoolean = deserialize("{'value': true}", BooleanBean.class); + BooleanBean beanBoolean = deserialize("{'value': true}", BooleanBean.class); assertEquals(true, beanBoolean.value()); // Double @@ -336,14 +336,14 @@ public void primitiveDeserializeBoolean() { @Test public void primitiveDeserializeDouble() { - var beanDouble = deserialize("{'value': 1.1}", DoubleBean.class); + DoubleBean beanDouble = deserialize("{'value': 1.1}", DoubleBean.class); assertEquals(1.1, beanDouble.value(), EPSILON); // Int - var beanInt = deserialize("{'value': 1}", DoubleBean.class); + DoubleBean beanInt = deserialize("{'value': 1}", DoubleBean.class); assertEquals(1, beanInt.value(), EPSILON); // Long - var beanLong = deserialize("{'value': 1234567890123}", DoubleBean.class); + DoubleBean beanLong = deserialize("{'value': 1234567890123}", DoubleBean.class); assertEquals(1234567890123L, beanLong.value(), EPSILON); // Boolean @@ -363,29 +363,29 @@ public void primitiveDeserializeDouble() { @Test public void primitiveDeserializeBigDecimal() { - var beanBigdecimal = deserialize("{'value': 123}", BigDecimalBean.class); + BigDecimalBean beanBigdecimal = deserialize("{'value': 123}", BigDecimalBean.class); assertEquals(BigDecimal.valueOf(123.0), beanBigdecimal.value()); beanBigdecimal = deserialize("{'value': '123'}", BigDecimalBean.class); assertEquals(BigDecimal.valueOf(123), beanBigdecimal.value()); // Int - var beanInt = + BigDecimalBean beanInt = deserialize(Collections.singletonMap("value", 1), BigDecimalBean.class); assertEquals(BigDecimal.valueOf(1), beanInt.value()); // Long - var beanLong = + BigDecimalBean beanLong = deserialize(Collections.singletonMap("value", 1234567890123L), BigDecimalBean.class); assertEquals(BigDecimal.valueOf(1234567890123L), beanLong.value()); // Double - var beanDouble = + BigDecimalBean beanDouble = deserialize(Collections.singletonMap("value", 1.1), BigDecimalBean.class); assertEquals(BigDecimal.valueOf(1.1), beanDouble.value()); // BigDecimal - var beanBigDecimal = + BigDecimalBean beanBigDecimal = deserialize( Collections.singletonMap("value", BigDecimal.valueOf(1.2)), BigDecimalBean.class); assertEquals(BigDecimal.valueOf(1.2), beanBigDecimal.value()); @@ -407,14 +407,14 @@ public void primitiveDeserializeBigDecimal() { @Test public void primitiveDeserializeFloat() { - var beanFloat = deserialize("{'value': 1.1}", FloatBean.class); + FloatBean beanFloat = deserialize("{'value': 1.1}", FloatBean.class); assertEquals(1.1, beanFloat.value(), EPSILON); // Int - var beanInt = deserialize(Collections.singletonMap("value", 1), FloatBean.class); + FloatBean beanInt = deserialize(Collections.singletonMap("value", 1), FloatBean.class); assertEquals(1, beanInt.value(), EPSILON); // Long - var beanLong = + FloatBean beanLong = deserialize(Collections.singletonMap("value", 1234567890123L), FloatBean.class); assertEquals((float) 1234567890123L, beanLong.value(), EPSILON); @@ -435,11 +435,11 @@ public void primitiveDeserializeFloat() { @Test public void primitiveDeserializeInt() { - var beanInt = deserialize("{'value': 1}", IntBean.class); + IntBean beanInt = deserialize("{'value': 1}", IntBean.class); assertEquals(1, beanInt.value()); // Double - var beanDouble = deserialize("{'value': 1.1}", IntBean.class); + IntBean beanDouble = deserialize("{'value': 1.1}", IntBean.class); assertEquals(1, beanDouble.value()); // Large doubles @@ -473,15 +473,15 @@ public void primitiveDeserializeInt() { @Test public void primitiveDeserializeLong() { - var beanLong = deserialize("{'value': 1234567890123}", LongBean.class); + LongBean beanLong = deserialize("{'value': 1234567890123}", LongBean.class); assertEquals(1234567890123L, beanLong.value()); // Int - var beanInt = deserialize("{'value': 1}", LongBean.class); + LongBean beanInt = deserialize("{'value': 1}", LongBean.class); assertEquals(1, beanInt.value()); // Double - var beanDouble = deserialize("{'value': 1.1}", LongBean.class); + LongBean beanDouble = deserialize("{'value': 1.1}", LongBean.class); assertEquals(1, beanDouble.value()); // Large doubles @@ -508,7 +508,7 @@ public void primitiveDeserializeLong() { @Test public void primitiveDeserializeWrongTypeMap() { - var expectedExceptionMessage = + String expectedExceptionMessage = ".* Failed to convert value of type .*Map to String \\(found in field 'value'\\).*"; Throwable exception = assertThrows( @@ -544,7 +544,7 @@ public void throwOnUnknownProperties() { @Test public void XMLAndURLBean() { - var bean = + XMLAndURLBean bean = deserialize("{'XMLAndURL': 'foo'}", XMLAndURLBean.class); assertEquals("foo", bean.XMLAndURL()); } @@ -555,9 +555,9 @@ public record AllCapsDefaultHandlingBean ( @Test public void allCapsSerializesToUppercaseByDefault() { - var bean = new AllCapsDefaultHandlingBean("value"); + AllCapsDefaultHandlingBean bean = new AllCapsDefaultHandlingBean("value"); assertJson("{'UUID': 'value'}", serialize(bean)); - var deserialized = + AllCapsDefaultHandlingBean deserialized = deserialize("{'UUID': 'value'}", AllCapsDefaultHandlingBean.class); assertEquals("value", deserialized.UUID()); } @@ -569,86 +569,86 @@ public record AllCapsWithPropertyName ( @Test public void allCapsWithPropertyNameSerializesToLowercase() { - var bean = new AllCapsWithPropertyName("value"); + AllCapsWithPropertyName bean = new AllCapsWithPropertyName("value"); assertJson("{'uuid': 'value'}", serialize(bean)); - var deserialized = + AllCapsWithPropertyName deserialized = deserialize("{'uuid': 'value'}", AllCapsWithPropertyName.class); assertEquals("value", deserialized.UUID()); } @Test public void nestedParsingWorks() { - var bean = deserialize("{'bean': {'value': 'foo'}}", NestedBean.class); + NestedBean bean = deserialize("{'bean': {'value': 'foo'}}", NestedBean.class); assertEquals("foo", bean.bean().value()); } @Test public void beansCanContainLists() { - var bean = deserialize("{'values': ['foo', 'bar']}", ListBean.class); + ListBean bean = deserialize("{'values': ['foo', 'bar']}", ListBean.class); assertEquals(Arrays.asList("foo", "bar"), bean.values()); } @Test public void beansCanContainMaps() { - var bean = deserialize("{'values': {'foo': 'bar'}}", MapBean.class); - var expected = fromSingleQuotedString("{'foo': 'bar'}"); + MapBean bean = deserialize("{'values': {'foo': 'bar'}}", MapBean.class); + Map expected = fromSingleQuotedString("{'foo': 'bar'}"); assertEquals(expected, bean.values()); } @Test public void beansCanContainUpperBoundedMaps() { - var date = new Date(1491847082123L); - var source = mapAnyType("values", mapAnyType("foo", date)); - var bean = convertToCustomClass(source, UpperBoundedMapBean.class); - var expected = mapAnyType("foo", date); + Date date = new Date(1491847082123L); + Map source = mapAnyType("values", mapAnyType("foo", date)); + UpperBoundedMapBean bean = convertToCustomClass(source, UpperBoundedMapBean.class); + Map expected = mapAnyType("foo", date); assertEquals(expected, bean.values()); } @Test public void beansCanContainMultiBoundedMaps() { - var date = new Date(1491847082123L); - var source = mapAnyType("map", mapAnyType("values", mapAnyType("foo", date))); - var bean = convertToCustomClass(source, MultiBoundedMapHolderBean.class); + Date date = new Date(1491847082123L); + Map source = mapAnyType("map", mapAnyType("values", mapAnyType("foo", date))); + MultiBoundedMapHolderBean bean = convertToCustomClass(source, MultiBoundedMapHolderBean.class); - var expected = mapAnyType("foo", date); + Map expected = mapAnyType("foo", date); assertEquals(expected, bean.map().values()); } @Test public void beansCanContainUnboundedMaps() { - var bean = deserialize("{'values': {'foo': 'bar'}}", UnboundedMapBean.class); - var expected = mapAnyType("foo", "bar"); + UnboundedMapBean bean = deserialize("{'values': {'foo': 'bar'}}", UnboundedMapBean.class); + Map expected = mapAnyType("foo", "bar"); assertEquals(expected, bean.values()); } @Test public void beansCanContainUnboundedTypeVariableMaps() { - var source = mapAnyType("map", mapAnyType("values", mapAnyType("foo", "bar"))); - var bean = + Map source = mapAnyType("map", mapAnyType("values", mapAnyType("foo", "bar"))); + UnboundedTypeVariableMapHolderBean bean = convertToCustomClass(source, UnboundedTypeVariableMapHolderBean.class); - var expected = mapAnyType("foo", "bar"); + Map expected = mapAnyType("foo", "bar"); assertEquals(expected, bean.map().values()); } @Test public void beansCanContainNestedUnboundedMaps() { - var bean = + UnboundedMapBean bean = deserialize("{'values': {'foo': {'bar': 'baz'}}}", UnboundedMapBean.class); - var expected = mapAnyType("foo", mapAnyType("bar", "baz")); + Map expected = mapAnyType("foo", mapAnyType("bar", "baz")); assertEquals(expected, bean.values()); } @Test public void beansCanContainBeanLists() { - var bean = deserialize("{'values': [{'value': 'foo'}]}", NestedListBean.class); + NestedListBean bean = deserialize("{'values': [{'value': 'foo'}]}", NestedListBean.class); assertEquals(1, bean.values().size()); assertEquals("foo", bean.values().get(0).value()); } @Test public void beansCanContainBeanMaps() { - var bean = deserialize("{'values': {'key': {'value': 'foo'}}}", NestedMapBean.class); + NestedMapBean bean = deserialize("{'values': {'key': {'value': 'foo'}}}", NestedMapBean.class); assertEquals(1, bean.values().size()); assertEquals("foo", bean.values().get("key").value()); } @@ -663,25 +663,25 @@ public void beanMapsMustHaveStringKeys() { @Test public void serializeStringBean() { - var bean = new StringBean("foo"); + StringBean bean = new StringBean("foo"); assertJson("{'value': 'foo'}", serialize(bean)); } @Test public void serializeDoubleBean() { - var bean = new DoubleBean(1.1); + DoubleBean bean = new DoubleBean(1.1); assertJson("{'value': 1.1}", serialize(bean)); } @Test public void serializeIntBean() { - var bean = new IntBean(1); + IntBean bean = new IntBean(1); assertJson("{'value': 1}", serialize(Collections.singletonMap("value", 1.0))); } @Test public void serializeLongBean() { - var bean = new LongBean(1234567890123L); + LongBean bean = new LongBean(1234567890123L); assertJson( "{'value': 1.234567890123E12}", serialize(Collections.singletonMap("value", 1.234567890123E12))); @@ -689,28 +689,28 @@ public void serializeLongBean() { @Test public void serializeBigDecimalBean() { - var bean = new BigDecimalBean(BigDecimal.valueOf(1.1)); + BigDecimalBean bean = new BigDecimalBean(BigDecimal.valueOf(1.1)); assertEquals(mapAnyType("value", "1.1"), serialize(bean)); } @Test public void bigDecimalRoundTrip() { - var doubleMaxPlusOne = BigDecimal.valueOf(Double.MAX_VALUE).add(BigDecimal.ONE); - var a = new BigDecimalBean(doubleMaxPlusOne); - var serialized = (Map) serialize(a); - var b = convertToCustomClass(serialized, BigDecimalBean.class); + BigDecimal doubleMaxPlusOne = BigDecimal.valueOf(Double.MAX_VALUE).add(BigDecimal.ONE); + BigDecimalBean a = new BigDecimalBean(doubleMaxPlusOne); + Map serialized = (Map) serialize(a); + BigDecimalBean b = convertToCustomClass(serialized, BigDecimalBean.class); assertEquals(a, b); } @Test public void serializeBooleanBean() { - var bean = new BooleanBean(true); + BooleanBean bean = new BooleanBean(true); assertJson("{'value': true}", serialize(bean)); } @Test public void serializeFloatBean() { - var bean = new FloatBean(0.5f); + FloatBean bean = new FloatBean(0.5f); // We don't use assertJson as it converts all floating point numbers to Double. assertEquals(mapAnyType("value", 0.5f), serialize(bean)); @@ -718,7 +718,7 @@ public void serializeFloatBean() { @Test public void serializePrivateFieldBean() { - final var bean = new NoFieldBean(); + final NoFieldBean bean = new NoFieldBean(); assertExceptionContains( "No properties to serialize found on class " + "com.google.cloud.firestore.RecordMapperTest$NoFieldBean", @@ -727,51 +727,51 @@ public void serializePrivateFieldBean() { @Test public void nestedSerializingWorks() { - var bean = new NestedBean(new StringBean("foo")); + NestedBean bean = new NestedBean(new StringBean("foo")); assertJson("{'bean': {'value': 'foo'}}", serialize(bean)); } @Test public void serializingListsWorks() { - var bean = new ListBean(Arrays.asList("foo", "bar")); + ListBean bean = new ListBean(Arrays.asList("foo", "bar")); assertJson("{'values': ['foo', 'bar']}", serialize(bean)); } @Test public void serializingMapsWorks() { - var bean = new MapBean(new HashMap<>()); + MapBean bean = new MapBean(new HashMap<>()); bean.values().put("foo", "bar"); assertJson("{'values': {'foo': 'bar'}}", serialize(bean)); } @Test public void serializingUpperBoundedMapsWorks() { - var date = new Date(1491847082123L); - var bean = new UpperBoundedMapBean(Map.of("foo", date)); - var expected = + Date date = new Date(1491847082123L); + UpperBoundedMapBean bean = new UpperBoundedMapBean(Map.of("foo", date)); + Map expected = mapAnyType("values", mapAnyType("foo", new Date(date.getTime()))); assertEquals(expected, serialize(bean)); } @Test public void serializingMultiBoundedObjectsWorks() { - var date = new Date(1491847082123L); + Date date = new Date(1491847082123L); - var values = new HashMap(); + HashMap values = new HashMap(); values.put("foo", date); - var holder = new MultiBoundedMapHolderBean(new MultiBoundedMapBean<>(values)); + MultiBoundedMapHolderBean holder = new MultiBoundedMapHolderBean(new MultiBoundedMapBean<>(values)); - var expected = + Map expected = mapAnyType("map", mapAnyType("values", mapAnyType("foo", new Date(date.getTime())))); assertEquals(expected, serialize(holder)); } @Test public void serializeListOfBeansWorks() { - var stringBean = new StringBean("foo"); + StringBean stringBean = new StringBean("foo"); - var bean = new NestedListBean(new ArrayList<>()); + NestedListBean bean = new NestedListBean(new ArrayList<>()); bean.values().add(stringBean); assertJson("{'values': [{'value': 'foo'}]}", serialize(bean)); @@ -779,9 +779,9 @@ public void serializeListOfBeansWorks() { @Test public void serializeMapOfBeansWorks() { - var stringBean = new StringBean("foo"); + StringBean stringBean = new StringBean("foo"); - var bean = new NestedMapBean(new HashMap<>()); + NestedMapBean bean = new NestedMapBean(new HashMap<>()); bean.values().put("key", stringBean); assertJson("{'values': {'key': {'value': 'foo'}}}", serialize(bean)); @@ -789,9 +789,9 @@ public void serializeMapOfBeansWorks() { @Test public void beanMapsMustHaveStringKeysForSerializing() { - var stringBean = new StringBean("foo"); + StringBean stringBean = new StringBean("foo"); - final var bean = new IllegalKeyMapBean(new HashMap<>()); + final IllegalKeyMapBean bean = new IllegalKeyMapBean(new HashMap<>()); bean.values().put(1, stringBean); assertExceptionContains( @@ -801,57 +801,57 @@ public void beanMapsMustHaveStringKeysForSerializing() { @Test public void serializeUPPERCASE() { - var bean = new XMLAndURLBean("foo"); + XMLAndURLBean bean = new XMLAndURLBean("foo"); assertJson("{'XMLAndURL': 'foo'}", serialize(bean)); } @Test public void roundTripCaseSensitiveFieldBean1() { - var bean = new CaseSensitiveFieldBean1("foo"); + CaseSensitiveFieldBean1 bean = new CaseSensitiveFieldBean1("foo"); assertJson("{'VALUE': 'foo'}", serialize(bean)); - var deserialized = + CaseSensitiveFieldBean1 deserialized = deserialize("{'VALUE': 'foo'}", CaseSensitiveFieldBean1.class); assertEquals("foo", deserialized.VALUE()); } @Test public void roundTripCaseSensitiveFieldBean2() { - var bean = new CaseSensitiveFieldBean2("foo"); + CaseSensitiveFieldBean2 bean = new CaseSensitiveFieldBean2("foo"); assertJson("{'value': 'foo'}", serialize(bean)); - var deserialized = + CaseSensitiveFieldBean2 deserialized = deserialize("{'value': 'foo'}", CaseSensitiveFieldBean2.class); assertEquals("foo", deserialized.value()); } @Test public void roundTripCaseSensitiveFieldBean3() { - var bean = new CaseSensitiveFieldBean3("foo"); + CaseSensitiveFieldBean3 bean = new CaseSensitiveFieldBean3("foo"); assertJson("{'Value': 'foo'}", serialize(bean)); - var deserialized = + CaseSensitiveFieldBean3 deserialized = deserialize("{'Value': 'foo'}", CaseSensitiveFieldBean3.class); assertEquals("foo", deserialized.Value()); } @Test public void roundTripCaseSensitiveFieldBean4() { - var bean = new CaseSensitiveFieldBean4("foo"); + CaseSensitiveFieldBean4 bean = new CaseSensitiveFieldBean4("foo"); assertJson("{'valUE': 'foo'}", serialize(bean)); - var deserialized = + CaseSensitiveFieldBean4 deserialized = deserialize("{'valUE': 'foo'}", CaseSensitiveFieldBean4.class); assertEquals("foo", deserialized.valUE()); } @Test public void roundTripUnicodeBean() { - var bean = new UnicodeBean("foo"); + UnicodeBean bean = new UnicodeBean("foo"); assertJson("{'漢字': 'foo'}", serialize(bean)); - var deserialized = deserialize("{'漢字': 'foo'}", UnicodeBean.class); + UnicodeBean deserialized = deserialize("{'漢字': 'foo'}", UnicodeBean.class); assertEquals("foo", deserialized.漢字()); } @Test public void shortsCantBeSerialized() { - final var bean = new ShortBean((short) 1); + final ShortBean bean = new ShortBean((short) 1); assertExceptionContains( "Numbers of type Short are not supported, please use an int, long, float, double or BigDecimal (found in field 'value')", () -> serialize(bean)); @@ -859,7 +859,7 @@ public void shortsCantBeSerialized() { @Test public void bytesCantBeSerialized() { - final var bean = new ByteBean((byte) 1); + final ByteBean bean = new ByteBean((byte) 1); assertExceptionContains( "Numbers of type Byte are not supported, please use an int, long, float, double or BigDecimal (found in field 'value')", () -> serialize(bean)); @@ -867,7 +867,7 @@ public void bytesCantBeSerialized() { @Test public void charsCantBeSerialized() { - final var bean = new CharBean((char) 1); + final CharBean bean = new CharBean((char) 1); assertExceptionContains( "Characters are not supported, please use Strings (found in field 'value')", () -> serialize(bean)); @@ -875,7 +875,7 @@ public void charsCantBeSerialized() { @Test public void intArraysCantBeSerialized() { - final var bean = new IntArrayBean(new int[] {1}); + final IntArrayBean bean = new IntArrayBean(new int[] {1}); assertExceptionContains( "Serializing Arrays is not supported, please use Lists instead " + "(found in field 'values')", @@ -884,7 +884,7 @@ public void intArraysCantBeSerialized() { @Test public void objectArraysCantBeSerialized() { - final var bean = new StringArrayBean(new String[] {"foo"}); + final StringArrayBean bean = new StringArrayBean(new String[] {"foo"}); assertExceptionContains( "Serializing Arrays is not supported, please use Lists instead " + "(found in field 'values')", @@ -929,14 +929,14 @@ public void objectArraysCantBeDeserialized() { @Test public void objectAcceptsAnyObject() { - var stringValue = deserialize("{'value': 'foo'}", ObjectBean.class); + ObjectBean stringValue = deserialize("{'value': 'foo'}", ObjectBean.class); assertEquals("foo", stringValue.value()); - var listValue = deserialize("{'value': ['foo']}", ObjectBean.class); + ObjectBean listValue = deserialize("{'value': ['foo']}", ObjectBean.class); assertEquals(Collections.singletonList("foo"), listValue.value()); - var mapValue = deserialize("{'value': {'foo':'bar'}}", ObjectBean.class); + ObjectBean mapValue = deserialize("{'value': {'foo':'bar'}}", ObjectBean.class); assertEquals(fromSingleQuotedString("{'foo':'bar'}"), mapValue.value()); - var complex = "{'value': {'foo':['bar', ['baz'], {'bam': 'qux'}]}, 'other':{'a': ['b']}}"; - var complexValue = deserialize(complex, ObjectBean.class); + String complex = "{'value': {'foo':['bar', ['baz'], {'bam': 'qux'}]}, 'other':{'a': ['b']}}"; + ObjectBean complexValue = deserialize(complex, ObjectBean.class); assertEquals(fromSingleQuotedString(complex).get("value"), complexValue.value()); } @@ -950,13 +950,13 @@ public void passingInGenericBeanTopLevelThrows() { @Test public void collectionsCanBeSerializedWhenList() { - var bean = new CollectionBean(Collections.singletonList("foo")); + CollectionBean bean = new CollectionBean(Collections.singletonList("foo")); assertJson("{'values': ['foo']}", serialize(bean)); } @Test public void collectionsCantBeSerializedWhenSet() { - final var bean = new CollectionBean(Collections.singleton("foo")); + final CollectionBean bean = new CollectionBean(Collections.singleton("foo")); assertExceptionContains( "Serializing Collections is not supported, please use Lists instead " + "(found in field 'values')", @@ -972,32 +972,32 @@ public void collectionsCantBeDeserialized() { @Test public void serializingGenericBeansSupported() { - var stringBean = new GenericBean("foo"); + GenericBean stringBean = new GenericBean("foo"); assertJson("{'value': 'foo'}", serialize(stringBean)); - var mapBean = new GenericBean>(Collections.singletonMap("foo", "bar")); + GenericBean> mapBean = new GenericBean>(Collections.singletonMap("foo", "bar")); assertJson("{'value': {'foo': 'bar'}}", serialize(mapBean)); - var listBean = new GenericBean>(Collections.singletonList("foo")); + GenericBean> listBean = new GenericBean>(Collections.singletonList("foo")); assertJson("{'value': ['foo']}", serialize(listBean)); - var recursiveBean = new GenericBean>(new GenericBean<>("foo")); + GenericBean> recursiveBean = new GenericBean>(new GenericBean<>("foo")); assertJson("{'value': {'value': 'foo'}}", serialize(recursiveBean)); - var doubleBean = new DoubleGenericBean("foo", 1.0); + DoubleGenericBean doubleBean = new DoubleGenericBean("foo", 1.0); assertJson("{'valueB': 1, 'valueA': 'foo'}", serialize(doubleBean)); } @Test public void propertyNamesAreSerialized() { - var bean = new PropertyNameBean("foo", "bar"); + PropertyNameBean bean = new PropertyNameBean("foo", "bar"); assertJson("{'my_key': 'foo', 'my_value': 'bar'}", serialize(bean)); } @Test public void propertyNamesAreParsed() { - var bean = + PropertyNameBean bean = deserialize("{'my_key': 'foo', 'my_value': 'bar'}", PropertyNameBean.class); assertEquals("foo", bean.key()); assertEquals("bar", bean.value()); @@ -1016,7 +1016,7 @@ public record PropertyWithDocumentIdOnWrongTypeBean ( @Test public void documentIdAnnotateWrongTypeThrows() { - final var expectedErrorMessage = "instead of String or DocumentReference"; + final String expectedErrorMessage = "instead of String or DocumentReference"; assertExceptionContains( expectedErrorMessage, () -> serialize(new FieldWithDocumentIdOnWrongTypeBean(100))); assertExceptionContains( @@ -1050,7 +1050,7 @@ public record DocumentIdOnNestedObjects ( @Test public void documentIdsDeserialize() { - var ref = + DocumentReference ref = new DocumentReference( firestoreMock, ResourcePath.create( @@ -1064,7 +1064,7 @@ public void documentIdsDeserialize() { deserialize(Collections.singletonMap("property", 100), DocumentIdOnStringField.class, ref) .docId()); - var target = + DocumentIdOnStringFieldAsProperty target = deserialize("{'anotherProperty': 100}", DocumentIdOnStringFieldAsProperty.class, ref); assertEquals("doc123", target.docId()); assertEquals(100, target.someOtherProperty()); @@ -1080,7 +1080,7 @@ public void documentIdsDeserialize() { public void documentIdsRoundTrip() { // Implicitly verifies @DocumentId is ignored during serialization. - final var ref = + final DocumentReference ref = new DocumentReference( firestoreMock, ResourcePath.create( @@ -1102,8 +1102,8 @@ public void documentIdsRoundTrip() { @Test public void documentIdsDeserializeConflictThrows() { - final var expectedErrorMessage = "cannot apply @DocumentId on this property"; - final var ref = + final String expectedErrorMessage = "cannot apply @DocumentId on this property"; + final DocumentReference ref = new DocumentReference( firestoreMock, ResourcePath.create( diff --git a/google-cloud-firestore/src/test-jdk17/java/com/google/cloud/firestore/record/LocalFirestoreHelper.java b/google-cloud-firestore/src/test-jdk17/java/com/google/cloud/firestore/record/LocalFirestoreHelper.java index ccb30f64e..7a8d0f4c1 100644 --- a/google-cloud-firestore/src/test-jdk17/java/com/google/cloud/firestore/record/LocalFirestoreHelper.java +++ b/google-cloud-firestore/src/test-jdk17/java/com/google/cloud/firestore/record/LocalFirestoreHelper.java @@ -119,7 +119,7 @@ public static Map map(K key, V value, Object... moreKeysAndValues) Map map = new HashMap<>(); map.put(key, value); - for (var i = 0; i < moreKeysAndValues.length; i += 2) { + for (int i = 0; i < moreKeysAndValues.length; i += 2) { map.put((K) moreKeysAndValues[i], (V) moreKeysAndValues[i + 1]); } @@ -128,14 +128,14 @@ public static Map map(K key, V value, Object... moreKeysAndValues) public static Answer getAllResponse( final Map... fields) { - var responses = new BatchGetDocumentsResponse[fields.length]; + BatchGetDocumentsResponse[] responses = new BatchGetDocumentsResponse[fields.length]; - for (var i = 0; i < fields.length; ++i) { - var name = DOCUMENT_NAME; + for (int i = 0; i < fields.length; ++i) { + String name = DOCUMENT_NAME; if (fields.length > 1) { name += i + 1; } - var response = BatchGetDocumentsResponse.newBuilder(); + BatchGetDocumentsResponse.Builder response = BatchGetDocumentsResponse.newBuilder(); response .getFoundBuilder() .setCreateTime(com.google.protobuf.Timestamp.newBuilder().setSeconds(1).setNanos(2)); @@ -154,10 +154,10 @@ public static Answer getAllResponse( public static Answer streamingResponse( final T[] response, @Nullable final Throwable throwable) { return invocation -> { - var args = invocation.getArguments(); - var observer = (ResponseObserver) args[1]; + Object[] args = invocation.getArguments(); + ResponseObserver observer = (ResponseObserver) args[1]; observer.onStart(mock(StreamController.class)); - for (var resp : response) { + for (T resp : response) { observer.onResponse(resp); } if (throwable != null) { @@ -169,12 +169,12 @@ public static Answer streamingResponse( } public static ApiFuture commitResponse(int adds, int deletes) { - var commitResponse = CommitResponse.newBuilder(); + CommitResponse.Builder commitResponse = CommitResponse.newBuilder(); commitResponse.getCommitTimeBuilder().setSeconds(0).setNanos(0); - for (var i = 0; i < adds; ++i) { + for (int i = 0; i < adds; ++i) { commitResponse.addWriteResultsBuilder().getUpdateTimeBuilder().setSeconds(i).setNanos(i); } - for (var i = 0; i < deletes; ++i) { + for (int i = 0; i < deletes; ++i) { commitResponse.addWriteResultsBuilder(); } return ApiFutures.immediateFuture(commitResponse.build()); @@ -190,22 +190,22 @@ public static List transform( String fieldPath, FieldTransform fieldTransform, Object... fieldPathOrTransform) { List transforms = new ArrayList<>(); - var transformBuilder = FieldTransform.newBuilder(); + FieldTransform.Builder transformBuilder = FieldTransform.newBuilder(); transformBuilder.setFieldPath(fieldPath).mergeFrom(fieldTransform); transforms.add(transformBuilder.build()); - for (var i = 0; i < fieldPathOrTransform.length; i += 2) { - var path = (String) fieldPathOrTransform[i]; - var transform = (FieldTransform) fieldPathOrTransform[i + 1]; + for (int i = 0; i < fieldPathOrTransform.length; i += 2) { + String path = (String) fieldPathOrTransform[i]; + FieldTransform transform = (FieldTransform) fieldPathOrTransform[i + 1]; transforms.add(FieldTransform.newBuilder().setFieldPath(path).mergeFrom(transform).build()); } return transforms; } public static Write create(Map fields, String docPath) { - var write = Write.newBuilder(); - var document = write.getUpdateBuilder(); + Write.Builder write = Write.newBuilder(); + Document.Builder document = write.getUpdateBuilder(); document.setName(DOCUMENT_ROOT + docPath); document.putAllFields(fields); write.getCurrentDocumentBuilder().setExists(false); @@ -226,8 +226,8 @@ public static Write set(Map fields, @Nullable List fieldM public static Write set( Map fields, @Nullable List fieldMap, String docPath) { - var write = Write.newBuilder(); - var document = write.getUpdateBuilder(); + Write.Builder write = Write.newBuilder(); + Document.Builder document = write.getUpdateBuilder(); document.setName(DOCUMENT_ROOT + docPath); document.putAllFields(fields); @@ -239,7 +239,7 @@ public static Write set( } public static CommitRequest commit(@Nullable String transactionId, Write... writes) { - var commitRequest = CommitRequest.newBuilder(); + CommitRequest.Builder commitRequest = CommitRequest.newBuilder(); commitRequest.setDatabase(DATABASE_NAME); commitRequest.addAllWrites(Arrays.asList(writes)); @@ -263,17 +263,17 @@ public static void assertCommitEquals(CommitRequest expected, CommitRequest actu } private static CommitRequest sortCommit(CommitRequest commit) { - var builder = commit.toBuilder(); + CommitRequest.Builder builder = commit.toBuilder(); - for (var writes : builder.getWritesBuilderList()) { + for (Write.Builder writes : builder.getWritesBuilderList()) { if (writes.hasUpdateMask()) { - var updateMask = new ArrayList<>(writes.getUpdateMask().getFieldPathsList()); + ArrayList updateMask = new ArrayList<>(writes.getUpdateMask().getFieldPathsList()); Collections.sort(updateMask); writes.setUpdateMask(DocumentMask.newBuilder().addAllFieldPaths(updateMask)); } if (!writes.getUpdateTransformsList().isEmpty()) { - var transformList = new ArrayList<>(writes.getUpdateTransformsList()); + ArrayList transformList = new ArrayList<>(writes.getUpdateTransformsList()); transformList.sort(Comparator.comparing(FieldTransform::getFieldPath)); writes.clearUpdateTransforms().addAllUpdateTransforms(transformList); } @@ -393,15 +393,15 @@ public record AllSupportedTypes ( @SuppressWarnings("unchecked") public static Map mapAnyType(Object... entries) { Map res = new HashMap<>(); - for (var i = 0; i < entries.length; i += 2) { + for (int i = 0; i < entries.length; i += 2) { res.put((String) entries[i], (T) entries[i + 1]); } return res; } private static Map fromJsonString(String json) { - var type = new TypeToken>() {}.getType(); - var gson = new Gson(); + Type type = new TypeToken>() {}.getType(); + Gson gson = new Gson(); return gson.fromJson(json, type); } From 91e128230f0210a2fca15ac6586a4c1cc80b1bd3 Mon Sep 17 00:00:00 2001 From: milaGGL <107142260+milaGGL@users.noreply.github.com> Date: Wed, 28 Aug 2024 15:46:12 -0400 Subject: [PATCH 10/51] import missing types --- .../com/google/cloud/firestore/RecordDocumentReferenceTest.java | 2 +- .../com/google/cloud/firestore/record/LocalFirestoreHelper.java | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/google-cloud-firestore/src/test-jdk17/java/com/google/cloud/firestore/RecordDocumentReferenceTest.java b/google-cloud-firestore/src/test-jdk17/java/com/google/cloud/firestore/RecordDocumentReferenceTest.java index c18f50357..1f48d312c 100644 --- a/google-cloud-firestore/src/test-jdk17/java/com/google/cloud/firestore/RecordDocumentReferenceTest.java +++ b/google-cloud-firestore/src/test-jdk17/java/com/google/cloud/firestore/RecordDocumentReferenceTest.java @@ -119,7 +119,7 @@ public void doesNotDeserializeAdvancedNumberTypes() throws Exception { Map fieldNamesToTypeNames = map("bigIntegerValue", "BigInteger", "shortValue", "Short", "byteValue", "Byte"); - for (Entry testCase : fieldNamesToTypeNames.entrySet()) { + for (Map.Entry testCase : fieldNamesToTypeNames.entrySet()) { String fieldName = testCase.getKey(); String typeName = testCase.getValue(); Map response = map(fieldName, Value.newBuilder().setIntegerValue(0).build()); diff --git a/google-cloud-firestore/src/test-jdk17/java/com/google/cloud/firestore/record/LocalFirestoreHelper.java b/google-cloud-firestore/src/test-jdk17/java/com/google/cloud/firestore/record/LocalFirestoreHelper.java index 7a8d0f4c1..9dc624102 100644 --- a/google-cloud-firestore/src/test-jdk17/java/com/google/cloud/firestore/record/LocalFirestoreHelper.java +++ b/google-cloud-firestore/src/test-jdk17/java/com/google/cloud/firestore/record/LocalFirestoreHelper.java @@ -29,6 +29,7 @@ import com.google.firestore.v1.BatchGetDocumentsResponse; import com.google.firestore.v1.CommitRequest; import com.google.firestore.v1.CommitResponse; +import com.google.firestore.v1.Document; import com.google.firestore.v1.DocumentMask; import com.google.firestore.v1.DocumentTransform.FieldTransform; import com.google.firestore.v1.MapValue; @@ -39,6 +40,7 @@ import com.google.protobuf.ByteString; import com.google.protobuf.NullValue; import com.google.type.LatLng; +import java.lang.reflect.Type; import org.mockito.stubbing.Answer; import javax.annotation.Nullable; From c6fd9136badec5f83a821e711c17785077ed8fbe Mon Sep 17 00:00:00 2001 From: milaGGL <107142260+milaGGL@users.noreply.github.com> Date: Thu, 29 Aug 2024 10:29:52 -0400 Subject: [PATCH 11/51] set javadoc source to java 17 --- google-cloud-firestore/pom.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/google-cloud-firestore/pom.xml b/google-cloud-firestore/pom.xml index 869bd7afe..8a2bedd8f 100644 --- a/google-cloud-firestore/pom.xml +++ b/google-cloud-firestore/pom.xml @@ -276,6 +276,7 @@ ../../../../../google-api-grpc/grpc-google-cloud-firestore-v1/target/site/apidocs/ + 17 From a85a466864782f5f22a0ce92a96cac7d2e42bb06 Mon Sep 17 00:00:00 2001 From: milaGGL <107142260+milaGGL@users.noreply.github.com> Date: Thu, 29 Aug 2024 10:42:03 -0400 Subject: [PATCH 12/51] update maven-javadoc-plugin at the root pom.xml --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index fd97a6a13..ed92ea06c 100644 --- a/pom.xml +++ b/pom.xml @@ -233,7 +233,7 @@ - 8 + 17 https://googleapis.dev/java/api-common/latest/ https://googleapis.dev/java/gax/latest/ From 9e46f9c6a0d53b6a298dbfec4629a3f731abf400 Mon Sep 17 00:00:00 2001 From: milaGGL <107142260+milaGGL@users.noreply.github.com> Date: Thu, 29 Aug 2024 10:49:26 -0400 Subject: [PATCH 13/51] Update pom.xml --- google-cloud-firestore/pom.xml | 1 - 1 file changed, 1 deletion(-) diff --git a/google-cloud-firestore/pom.xml b/google-cloud-firestore/pom.xml index 8a2bedd8f..869bd7afe 100644 --- a/google-cloud-firestore/pom.xml +++ b/google-cloud-firestore/pom.xml @@ -276,7 +276,6 @@ ../../../../../google-api-grpc/grpc-google-cloud-firestore-v1/target/site/apidocs/ - 17 From 5a7043967a673fa2ea76407cc0f456ad1696fb61 Mon Sep 17 00:00:00 2001 From: milaGGL <107142260+milaGGL@users.noreply.github.com> Date: Thu, 29 Aug 2024 14:57:40 -0400 Subject: [PATCH 14/51] remove < release > in pom.xml --- google-cloud-firestore/pom.xml | 7 +++---- pom.xml | 2 +- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/google-cloud-firestore/pom.xml b/google-cloud-firestore/pom.xml index 869bd7afe..9f7fccd63 100644 --- a/google-cloud-firestore/pom.xml +++ b/google-cloud-firestore/pom.xml @@ -322,10 +322,10 @@ - - java17 + + java17-test - 17 + [17,) @@ -354,7 +354,6 @@ 17 - 17 -parameters --add-opens=java.base/java.lang=ALL-UNNAMED diff --git a/pom.xml b/pom.xml index ed92ea06c..fd97a6a13 100644 --- a/pom.xml +++ b/pom.xml @@ -233,7 +233,7 @@ - 17 + 8 https://googleapis.dev/java/api-common/latest/ https://googleapis.dev/java/gax/latest/ From 56e3031ce84c5fe2c690d396ca069092ccb7c080 Mon Sep 17 00:00:00 2001 From: milaGGL <107142260+milaGGL@users.noreply.github.com> Date: Thu, 29 Aug 2024 15:00:55 -0400 Subject: [PATCH 15/51] Update pom.xml --- google-cloud-firestore/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/google-cloud-firestore/pom.xml b/google-cloud-firestore/pom.xml index 9f7fccd63..556c403dd 100644 --- a/google-cloud-firestore/pom.xml +++ b/google-cloud-firestore/pom.xml @@ -325,7 +325,7 @@ java17-test - [17,) + 17 From b5a465b36cd3e86e07c202fc96d46b72cada2756 Mon Sep 17 00:00:00 2001 From: milaGGL <107142260+milaGGL@users.noreply.github.com> Date: Thu, 29 Aug 2024 15:04:04 -0400 Subject: [PATCH 16/51] remove the build.sh changes --- .kokoro/build.sh | 2 +- google-cloud-firestore/pom.xml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.kokoro/build.sh b/.kokoro/build.sh index 0b620a739..5444a2da9 100755 --- a/.kokoro/build.sh +++ b/.kokoro/build.sh @@ -49,7 +49,7 @@ case ${JOB_TYPE} in test) echo "SUREFIRE_JVM_OPT: ${SUREFIRE_JVM_OPT}" mvn test -B -ntp -Dclirr.skip=true -Denforcer.skip=true ${SUREFIRE_JVM_OPT} - javap -verbose -cp google-cloud-firestore/target/test-classes com/google/cloud/firestore/ToStringTest + # javap -verbose -cp google-cloud-firestore/target/test-classes com/google/cloud/firestore/ToStringTest RETURN_CODE=$? ;; lint) diff --git a/google-cloud-firestore/pom.xml b/google-cloud-firestore/pom.xml index 556c403dd..e111893cf 100644 --- a/google-cloud-firestore/pom.xml +++ b/google-cloud-firestore/pom.xml @@ -322,7 +322,7 @@ - + java17-test 17 From 646ff81513cb5df0d73bc4194f57ace49f0d7490 Mon Sep 17 00:00:00 2001 From: milaGGL <107142260+milaGGL@users.noreply.github.com> Date: Thu, 29 Aug 2024 15:08:09 -0400 Subject: [PATCH 17/51] Update pom.xml --- google-cloud-firestore/pom.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/google-cloud-firestore/pom.xml b/google-cloud-firestore/pom.xml index e111893cf..c610253fc 100644 --- a/google-cloud-firestore/pom.xml +++ b/google-cloud-firestore/pom.xml @@ -354,6 +354,7 @@ 17 + 17 -parameters --add-opens=java.base/java.lang=ALL-UNNAMED From 9e99d2c59bb2e08d7d7d9c65079502b654113f67 Mon Sep 17 00:00:00 2001 From: milaGGL <107142260+milaGGL@users.noreply.github.com> Date: Thu, 29 Aug 2024 15:11:59 -0400 Subject: [PATCH 18/51] Revert "Update pom.xml" This reverts commit 646ff81513cb5df0d73bc4194f57ace49f0d7490. --- google-cloud-firestore/pom.xml | 1 - 1 file changed, 1 deletion(-) diff --git a/google-cloud-firestore/pom.xml b/google-cloud-firestore/pom.xml index c610253fc..e111893cf 100644 --- a/google-cloud-firestore/pom.xml +++ b/google-cloud-firestore/pom.xml @@ -354,7 +354,6 @@ 17 - 17 -parameters --add-opens=java.base/java.lang=ALL-UNNAMED From 3ea41cdf702e32b1e2f67069c73e683bb3db66bd Mon Sep 17 00:00:00 2001 From: milaGGL <107142260+milaGGL@users.noreply.github.com> Date: Thu, 29 Aug 2024 15:21:59 -0400 Subject: [PATCH 19/51] remove the --- google-cloud-firestore/pom.xml | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/google-cloud-firestore/pom.xml b/google-cloud-firestore/pom.xml index e111893cf..96795ef9c 100644 --- a/google-cloud-firestore/pom.xml +++ b/google-cloud-firestore/pom.xml @@ -347,20 +347,6 @@ - - org.apache.maven.plugins - maven-compiler-plugin - true - - - 17 - - -parameters - --add-opens=java.base/java.lang=ALL-UNNAMED - --add-opens=java.base/java.util=ALL-UNNAMED - - - From 2c3f1444eb4fec8a14c1ca9c2dbe57244b70f6b9 Mon Sep 17 00:00:00 2001 From: milaGGL <107142260+milaGGL@users.noreply.github.com> Date: Thu, 29 Aug 2024 15:26:55 -0400 Subject: [PATCH 20/51] Revert "remove the " This reverts commit 3ea41cdf702e32b1e2f67069c73e683bb3db66bd. --- google-cloud-firestore/pom.xml | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/google-cloud-firestore/pom.xml b/google-cloud-firestore/pom.xml index 96795ef9c..e111893cf 100644 --- a/google-cloud-firestore/pom.xml +++ b/google-cloud-firestore/pom.xml @@ -347,6 +347,20 @@ + + org.apache.maven.plugins + maven-compiler-plugin + true + + + 17 + + -parameters + --add-opens=java.base/java.lang=ALL-UNNAMED + --add-opens=java.base/java.util=ALL-UNNAMED + + + From ec8d30c30463c299399096e654e877314f698742 Mon Sep 17 00:00:00 2001 From: milaGGL <107142260+milaGGL@users.noreply.github.com> Date: Thu, 29 Aug 2024 15:38:44 -0400 Subject: [PATCH 21/51] Update build.sh --- .kokoro/build.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.kokoro/build.sh b/.kokoro/build.sh index 5444a2da9..940eaa31b 100755 --- a/.kokoro/build.sh +++ b/.kokoro/build.sh @@ -49,8 +49,8 @@ case ${JOB_TYPE} in test) echo "SUREFIRE_JVM_OPT: ${SUREFIRE_JVM_OPT}" mvn test -B -ntp -Dclirr.skip=true -Denforcer.skip=true ${SUREFIRE_JVM_OPT} - # javap -verbose -cp google-cloud-firestore/target/test-classes com/google/cloud/firestore/ToStringTest RETURN_CODE=$? + javap -verbose -cp google-cloud-firestore/target/test-classes com/google/cloud/firestore/ToStringTest ;; lint) mvn com.coveo:fmt-maven-plugin:check -B -ntp && mvn -pl google-cloud-firestore spotless:check -B -ntp From 316df89f3d93822d795ace269c26b93f02149ff9 Mon Sep 17 00:00:00 2001 From: milaGGL <107142260+milaGGL@users.noreply.github.com> Date: Thu, 29 Aug 2024 15:42:53 -0400 Subject: [PATCH 22/51] Update pom.xml --- google-cloud-firestore/pom.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/google-cloud-firestore/pom.xml b/google-cloud-firestore/pom.xml index e111893cf..c610253fc 100644 --- a/google-cloud-firestore/pom.xml +++ b/google-cloud-firestore/pom.xml @@ -354,6 +354,7 @@ 17 + 17 -parameters --add-opens=java.base/java.lang=ALL-UNNAMED From e55907f5f45a2a0d83a33c0e6ee26bc3e2be70b6 Mon Sep 17 00:00:00 2001 From: milaGGL <107142260+milaGGL@users.noreply.github.com> Date: Fri, 30 Aug 2024 11:09:16 -0400 Subject: [PATCH 23/51] try conditionally run new tests via ci.yaml --- .github/workflows/ci.yaml | 9 ++++- .kokoro/build.sh | 9 ++++- google-cloud-firestore/pom.xml | 66 ++++++++++++---------------------- 3 files changed, 38 insertions(+), 46 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index b91fa381f..25e730bd1 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -33,7 +33,14 @@ jobs: distribution: temurin java-version: ${{matrix.java}} - run: java -version - - run: .kokoro/build.sh + # - run: .kokoro/build.sh + # env: + # JOB_TYPE: test + - if: matrix.java == '17' # Conditionally add test source directory + run: | + mkdir -p test-jdk17/java + cp -r src/test-jdk17/* test-jdk17/java + - run: .kokoro/build.sh # Assuming the script handles multiple test source dirs env: JOB_TYPE: test units-java8: diff --git a/.kokoro/build.sh b/.kokoro/build.sh index 940eaa31b..2103977ed 100755 --- a/.kokoro/build.sh +++ b/.kokoro/build.sh @@ -45,10 +45,17 @@ fi RETURN_CODE=0 set +e +# Detect Java Version (NEW) +JAVA_MAJOR_VERSION=$(java -version 2>&1 | grep "version" | awk '{print $3}' | tr -d '"') + case ${JOB_TYPE} in test) echo "SUREFIRE_JVM_OPT: ${SUREFIRE_JVM_OPT}" - mvn test -B -ntp -Dclirr.skip=true -Denforcer.skip=true ${SUREFIRE_JVM_OPT} + if [ ${JAVA_MAJOR_VERSION} -ge 17 ]; then # Conditional test execution (NEW) + mvn test -B -ntp -Dclirr.skip=true -Denforcer.skip=true ${SUREFIRE_JVM_OPT} -Dtest=**/*Test + else + mvn test -B -ntp -Dclirr.skip=true -Denforcer.skip=true ${SUREFIRE_JVM_OPT} + fi RETURN_CODE=$? javap -verbose -cp google-cloud-firestore/target/test-classes com/google/cloud/firestore/ToStringTest ;; diff --git a/google-cloud-firestore/pom.xml b/google-cloud-firestore/pom.xml index c610253fc..b17526d83 100644 --- a/google-cloud-firestore/pom.xml +++ b/google-cloud-firestore/pom.xml @@ -321,49 +321,27 @@ - - - java17-test - - 17 - - - - - org.codehaus.mojo - build-helper-maven-plugin - - - add-test-source - generate-test-sources - - add-test-source - - - - src/test-jdk17/java - - - - - - - org.apache.maven.plugins - maven-compiler-plugin - true - - - 17 - 17 - - -parameters - --add-opens=java.base/java.lang=ALL-UNNAMED - --add-opens=java.base/java.util=ALL-UNNAMED - - - - - - +   + test-jdk17 + + [17,) + + + + + org.apache.maven.plugins + maven-surefire-plugin   + 3.1.2 + + src/test-jdk17/java   + + + **/*Test.java + + + + + + From 7b2ce93e7c5c86e3524e63c5e753767b502baa25 Mon Sep 17 00:00:00 2001 From: milaGGL <107142260+milaGGL@users.noreply.github.com> Date: Fri, 30 Aug 2024 11:11:41 -0400 Subject: [PATCH 24/51] Update ci.yaml --- .github/workflows/ci.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 25e730bd1..55ccee89e 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -39,7 +39,7 @@ jobs: - if: matrix.java == '17' # Conditionally add test source directory run: | mkdir -p test-jdk17/java - cp -r src/test-jdk17/* test-jdk17/java + cp -r google-cloud-firestore/src/test-jdk17/* test-jdk17/java - run: .kokoro/build.sh # Assuming the script handles multiple test source dirs env: JOB_TYPE: test From 353cbadf6ba5dc9599f2a09c04699a85849d202d Mon Sep 17 00:00:00 2001 From: milaGGL <107142260+milaGGL@users.noreply.github.com> Date: Fri, 30 Aug 2024 11:19:02 -0400 Subject: [PATCH 25/51] revert changes --- .github/workflows/ci.yaml | 9 +---- .kokoro/build.sh | 9 +---- google-cloud-firestore/pom.xml | 66 ++++++++++++++++++++++------------ 3 files changed, 46 insertions(+), 38 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 55ccee89e..b91fa381f 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -33,14 +33,7 @@ jobs: distribution: temurin java-version: ${{matrix.java}} - run: java -version - # - run: .kokoro/build.sh - # env: - # JOB_TYPE: test - - if: matrix.java == '17' # Conditionally add test source directory - run: | - mkdir -p test-jdk17/java - cp -r google-cloud-firestore/src/test-jdk17/* test-jdk17/java - - run: .kokoro/build.sh # Assuming the script handles multiple test source dirs + - run: .kokoro/build.sh env: JOB_TYPE: test units-java8: diff --git a/.kokoro/build.sh b/.kokoro/build.sh index 2103977ed..940eaa31b 100755 --- a/.kokoro/build.sh +++ b/.kokoro/build.sh @@ -45,17 +45,10 @@ fi RETURN_CODE=0 set +e -# Detect Java Version (NEW) -JAVA_MAJOR_VERSION=$(java -version 2>&1 | grep "version" | awk '{print $3}' | tr -d '"') - case ${JOB_TYPE} in test) echo "SUREFIRE_JVM_OPT: ${SUREFIRE_JVM_OPT}" - if [ ${JAVA_MAJOR_VERSION} -ge 17 ]; then # Conditional test execution (NEW) - mvn test -B -ntp -Dclirr.skip=true -Denforcer.skip=true ${SUREFIRE_JVM_OPT} -Dtest=**/*Test - else - mvn test -B -ntp -Dclirr.skip=true -Denforcer.skip=true ${SUREFIRE_JVM_OPT} - fi + mvn test -B -ntp -Dclirr.skip=true -Denforcer.skip=true ${SUREFIRE_JVM_OPT} RETURN_CODE=$? javap -verbose -cp google-cloud-firestore/target/test-classes com/google/cloud/firestore/ToStringTest ;; diff --git a/google-cloud-firestore/pom.xml b/google-cloud-firestore/pom.xml index b17526d83..c610253fc 100644 --- a/google-cloud-firestore/pom.xml +++ b/google-cloud-firestore/pom.xml @@ -321,27 +321,49 @@ -   - test-jdk17 - - [17,) - - - - - org.apache.maven.plugins - maven-surefire-plugin   - 3.1.2 - - src/test-jdk17/java   - - - **/*Test.java - - - - - - + + + java17-test + + 17 + + + + + org.codehaus.mojo + build-helper-maven-plugin + + + add-test-source + generate-test-sources + + add-test-source + + + + src/test-jdk17/java + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + true + + + 17 + 17 + + -parameters + --add-opens=java.base/java.lang=ALL-UNNAMED + --add-opens=java.base/java.util=ALL-UNNAMED + + + + + + From 09a79eecfe9c8ab2a3ff34ee57a092a05ecd2d97 Mon Sep 17 00:00:00 2001 From: milaGGL <107142260+milaGGL@users.noreply.github.com> Date: Fri, 30 Aug 2024 12:55:21 -0400 Subject: [PATCH 26/51] spesify javadoc for test-jdk17 --- google-cloud-firestore/pom.xml | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/google-cloud-firestore/pom.xml b/google-cloud-firestore/pom.xml index c610253fc..599d44a00 100644 --- a/google-cloud-firestore/pom.xml +++ b/google-cloud-firestore/pom.xml @@ -362,6 +362,22 @@ + + org.apache.maven.plugins + maven-javadoc-plugin   + + 3.10.0 +   + + src/test-jdk17/**/*.java + + 17 + 17 + + --enable-preview + + + From bc1c06e5b4c39aea742aad5680a35ed9f5689c50 Mon Sep 17 00:00:00 2001 From: milaGGL <107142260+milaGGL@users.noreply.github.com> Date: Fri, 30 Aug 2024 13:00:32 -0400 Subject: [PATCH 27/51] Update pom.xml --- google-cloud-firestore/pom.xml | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/google-cloud-firestore/pom.xml b/google-cloud-firestore/pom.xml index 599d44a00..9fd005c76 100644 --- a/google-cloud-firestore/pom.xml +++ b/google-cloud-firestore/pom.xml @@ -364,10 +364,8 @@ org.apache.maven.plugins - maven-javadoc-plugin   - - 3.10.0 -   + maven-javadoc-plugin + src/test-jdk17/**/*.java From c5bc13e827910c0120c80678c106ba3a4f4387ca Mon Sep 17 00:00:00 2001 From: Tomo Suzuki Date: Fri, 30 Aug 2024 13:30:44 -0400 Subject: [PATCH 28/51] test: checking java 8 special setup is still needed or not --- .github/workflows/ci.yaml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index b91fa381f..fd4a694b3 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -51,10 +51,6 @@ jobs: # https://maven.apache.org/surefire/maven-surefire-plugin/test-mojo.html#jvm run: echo "SUREFIRE_JVM_OPT=-Djvm=${JAVA_HOME}/bin/java" >> $GITHUB_ENV shell: bash - - uses: actions/setup-java@v4 - with: - java-version: 17 - distribution: temurin - run: .kokoro/build.sh env: JOB_TYPE: test From e556dcf651e9ff4937dd0e6c715c05b9ad17de2c Mon Sep 17 00:00:00 2001 From: Tomo Suzuki Date: Fri, 30 Aug 2024 13:36:43 -0400 Subject: [PATCH 29/51] Update ci.yaml --- .github/workflows/ci.yaml | 20 +------------------- 1 file changed, 1 insertion(+), 19 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index fd4a694b3..ca6f52a97 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -25,7 +25,7 @@ jobs: strategy: fail-fast: false matrix: - java: [11, 17, 21] + java: [8, 11, 17, 21] steps: - uses: actions/checkout@v4 - uses: actions/setup-java@v4 @@ -36,24 +36,6 @@ jobs: - run: .kokoro/build.sh env: JOB_TYPE: test - units-java8: - # Building using Java 17 and run the tests with Java 8 runtime - name: "units (8)" - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-java@v4 - with: - java-version: 8 - distribution: temurin - - name: "Set jvm system property environment variable for surefire plugin (unit tests)" - # Maven surefire plugin (unit tests) allows us to specify JVM to run the tests. - # https://maven.apache.org/surefire/maven-surefire-plugin/test-mojo.html#jvm - run: echo "SUREFIRE_JVM_OPT=-Djvm=${JAVA_HOME}/bin/java" >> $GITHUB_ENV - shell: bash - - run: .kokoro/build.sh - env: - JOB_TYPE: test windows: runs-on: windows-latest steps: From 8e26e85e2e06ea6ec5dd698148ce179217bd164f Mon Sep 17 00:00:00 2001 From: milaGGL <107142260+milaGGL@users.noreply.github.com> Date: Fri, 30 Aug 2024 14:55:01 -0400 Subject: [PATCH 30/51] remove changes in build.sh --- .kokoro/build.sh | 1 - 1 file changed, 1 deletion(-) diff --git a/.kokoro/build.sh b/.kokoro/build.sh index 940eaa31b..205347243 100755 --- a/.kokoro/build.sh +++ b/.kokoro/build.sh @@ -50,7 +50,6 @@ test) echo "SUREFIRE_JVM_OPT: ${SUREFIRE_JVM_OPT}" mvn test -B -ntp -Dclirr.skip=true -Denforcer.skip=true ${SUREFIRE_JVM_OPT} RETURN_CODE=$? - javap -verbose -cp google-cloud-firestore/target/test-classes com/google/cloud/firestore/ToStringTest ;; lint) mvn com.coveo:fmt-maven-plugin:check -B -ntp && mvn -pl google-cloud-firestore spotless:check -B -ntp From bd7d02def16c7577a25d3c0c490248ad2742177d Mon Sep 17 00:00:00 2001 From: milaGGL <107142260+milaGGL@users.noreply.github.com> Date: Fri, 30 Aug 2024 14:57:48 -0400 Subject: [PATCH 31/51] Update pom.xml --- google-cloud-firestore/pom.xml | 17 +---------------- 1 file changed, 1 insertion(+), 16 deletions(-) diff --git a/google-cloud-firestore/pom.xml b/google-cloud-firestore/pom.xml index 9fd005c76..5444d4d3e 100644 --- a/google-cloud-firestore/pom.xml +++ b/google-cloud-firestore/pom.xml @@ -322,7 +322,7 @@ - + java17-test 17 @@ -347,21 +347,6 @@ - - org.apache.maven.plugins - maven-compiler-plugin - true - - - 17 - 17 - - -parameters - --add-opens=java.base/java.lang=ALL-UNNAMED - --add-opens=java.base/java.util=ALL-UNNAMED - - - org.apache.maven.plugins maven-javadoc-plugin From f3cfc66bdf39e04f370d4c736728b7045298663f Mon Sep 17 00:00:00 2001 From: milaGGL <107142260+milaGGL@users.noreply.github.com> Date: Fri, 30 Aug 2024 15:03:51 -0400 Subject: [PATCH 32/51] Update pom.xml --- google-cloud-firestore/pom.xml | 27 ++++++++++++++------------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/google-cloud-firestore/pom.xml b/google-cloud-firestore/pom.xml index 5444d4d3e..7ecbbb9d8 100644 --- a/google-cloud-firestore/pom.xml +++ b/google-cloud-firestore/pom.xml @@ -348,19 +348,20 @@ - org.apache.maven.plugins - maven-javadoc-plugin - - - src/test-jdk17/**/*.java - - 17 - 17 - - --enable-preview - - - + org.apache.maven.plugins + maven-compiler-plugin + true + + + 17 + 17 + + -parameters + --add-opens=java.base/java.lang=ALL-UNNAMED + --add-opens=java.base/java.util=ALL-UNNAMED + + + From 080e338af9f60dc52bdd1ed3c56f0e2dca5e98b3 Mon Sep 17 00:00:00 2001 From: milaGGL <107142260+milaGGL@users.noreply.github.com> Date: Fri, 30 Aug 2024 15:05:42 -0400 Subject: [PATCH 33/51] Update pom.xml --- google-cloud-firestore/pom.xml | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/google-cloud-firestore/pom.xml b/google-cloud-firestore/pom.xml index 7ecbbb9d8..910ee7d54 100644 --- a/google-cloud-firestore/pom.xml +++ b/google-cloud-firestore/pom.xml @@ -362,6 +362,17 @@ + + org.apache.maven.plugins + maven-javadoc-plugin + + + src/test-jdk17/**/*.java + + 17 + 17 + + From 54425c040a74a3be3f2cfd385f2a3a3f3399ff80 Mon Sep 17 00:00:00 2001 From: milaGGL <107142260+milaGGL@users.noreply.github.com> Date: Fri, 30 Aug 2024 15:12:13 -0400 Subject: [PATCH 34/51] Update pom.xml --- google-cloud-firestore/pom.xml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/google-cloud-firestore/pom.xml b/google-cloud-firestore/pom.xml index 910ee7d54..93343269a 100644 --- a/google-cloud-firestore/pom.xml +++ b/google-cloud-firestore/pom.xml @@ -325,7 +325,7 @@ java17-test - 17 + [17,) @@ -371,6 +371,9 @@ 17 17 + + --enable-preview + From f99e2226084e727b245714ed099d21d9f374df71 Mon Sep 17 00:00:00 2001 From: milaGGL <107142260+milaGGL@users.noreply.github.com> Date: Fri, 30 Aug 2024 15:31:58 -0400 Subject: [PATCH 35/51] update ci.yaml to run javadoc on jdk8 --- .github/workflows/ci.yaml | 2 +- google-cloud-firestore/pom.xml | 5 +---- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index ca6f52a97..77e15f6a1 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -70,7 +70,7 @@ jobs: - uses: actions/setup-java@v4 with: distribution: temurin - java-version: 17 + java-version: 8 - run: java -version - run: .kokoro/build.sh env: diff --git a/google-cloud-firestore/pom.xml b/google-cloud-firestore/pom.xml index 93343269a..9b1c2b9ef 100644 --- a/google-cloud-firestore/pom.xml +++ b/google-cloud-firestore/pom.xml @@ -322,7 +322,7 @@ - + java17-test [17,) @@ -371,9 +371,6 @@ 17 17 - - --enable-preview - From 46e0ede77b7d4ba59081cfac57ef085152e2bfcc Mon Sep 17 00:00:00 2001 From: milaGGL <107142260+milaGGL@users.noreply.github.com> Date: Fri, 30 Aug 2024 15:37:14 -0400 Subject: [PATCH 36/51] Revert "update ci.yaml to run javadoc on jdk8" This reverts commit f99e2226084e727b245714ed099d21d9f374df71. --- .github/workflows/ci.yaml | 2 +- google-cloud-firestore/pom.xml | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 77e15f6a1..ca6f52a97 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -70,7 +70,7 @@ jobs: - uses: actions/setup-java@v4 with: distribution: temurin - java-version: 8 + java-version: 17 - run: java -version - run: .kokoro/build.sh env: diff --git a/google-cloud-firestore/pom.xml b/google-cloud-firestore/pom.xml index 9b1c2b9ef..93343269a 100644 --- a/google-cloud-firestore/pom.xml +++ b/google-cloud-firestore/pom.xml @@ -322,7 +322,7 @@ - + java17-test [17,) @@ -371,6 +371,9 @@ 17 17 + + --enable-preview + From 9ec90dcfa51fdd1e53eea775da00465d527a1c5a Mon Sep 17 00:00:00 2001 From: milaGGL <107142260+milaGGL@users.noreply.github.com> Date: Fri, 30 Aug 2024 15:38:14 -0400 Subject: [PATCH 37/51] Update pom.xml --- google-cloud-firestore/pom.xml | 20 +++----------------- 1 file changed, 3 insertions(+), 17 deletions(-) diff --git a/google-cloud-firestore/pom.xml b/google-cloud-firestore/pom.xml index 93343269a..d1b49d57c 100644 --- a/google-cloud-firestore/pom.xml +++ b/google-cloud-firestore/pom.xml @@ -325,7 +325,7 @@ java17-test - [17,) + [21,) @@ -353,8 +353,8 @@ true - 17 - 17 + 21 + 21 -parameters --add-opens=java.base/java.lang=ALL-UNNAMED @@ -362,20 +362,6 @@ - - org.apache.maven.plugins - maven-javadoc-plugin - - - src/test-jdk17/**/*.java - - 17 - 17 - - --enable-preview - - - From 2bc0f5e86492ecad58a0ae4b2831d8d63f69ff1a Mon Sep 17 00:00:00 2001 From: milaGGL <107142260+milaGGL@users.noreply.github.com> Date: Fri, 30 Aug 2024 15:50:14 -0400 Subject: [PATCH 38/51] finalize pom.xml --- google-cloud-firestore/pom.xml | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/google-cloud-firestore/pom.xml b/google-cloud-firestore/pom.xml index d1b49d57c..e22246753 100644 --- a/google-cloud-firestore/pom.xml +++ b/google-cloud-firestore/pom.xml @@ -322,10 +322,10 @@ - + java17-test - [21,) + 17 @@ -353,8 +353,8 @@ true - 21 - 21 + 17 + 17 -parameters --add-opens=java.base/java.lang=ALL-UNNAMED @@ -362,6 +362,18 @@ + + + org.apache.maven.plugins + maven-javadoc-plugin + + + src/test-jdk17/**/*.java + + 17 + 17 + + From 11a8930453b2396b907b4375b7e37678c043ca73 Mon Sep 17 00:00:00 2001 From: Eran Leshem <1707552+eranl@users.noreply.github.com> Date: Thu, 5 Sep 2024 17:09:30 +0300 Subject: [PATCH 39/51] fix Instant-record merge (#1805) --- .../src/main/java/com/google/cloud/firestore/BeanMapper.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/BeanMapper.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/BeanMapper.java index ef25554bc..4c8005cef 100644 --- a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/BeanMapper.java +++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/BeanMapper.java @@ -26,6 +26,7 @@ import java.lang.reflect.Field; import java.lang.reflect.Type; import java.lang.reflect.TypeVariable; +import java.time.Instant; import java.util.Collection; import java.util.Collections; import java.util.Date; @@ -148,13 +149,13 @@ T deserialize(Map values, DeserializeContext context) { void applyFieldAnnotations(Field field) { if (field.isAnnotationPresent(ServerTimestamp.class)) { Class fieldType = field.getType(); - if (fieldType != Date.class && fieldType != Timestamp.class) { + if (fieldType != Date.class && fieldType != Timestamp.class && fieldType != Instant.class) { throw new IllegalArgumentException( "Field " + field.getName() + " is annotated with @ServerTimestamp but is " + fieldType - + " instead of Date or Timestamp."); + + " instead of Date, Timestamp, or Instant."); } serverTimestamps.add(propertyName(field)); } From 37b9641538bf0371c5c529c20f0b63c94711d408 Mon Sep 17 00:00:00 2001 From: milaGGL <107142260+milaGGL@users.noreply.github.com> Date: Wed, 11 Sep 2024 11:08:48 -0400 Subject: [PATCH 40/51] remove log, update copyright year --- .../src/main/java/com/google/cloud/firestore/BeanMapper.java | 2 +- .../java/com/google/cloud/firestore/DeserializeContext.java | 2 +- .../src/main/java/com/google/cloud/firestore/RecordMapper.java | 2 +- .../google/cloud/firestore/RecordDocumentReferenceTest.java | 3 +-- .../java/com/google/cloud/firestore/RecordMapperTest.java | 2 +- .../google/cloud/firestore/record/LocalFirestoreHelper.java | 2 +- 6 files changed, 6 insertions(+), 7 deletions(-) diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/BeanMapper.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/BeanMapper.java index 4c8005cef..37114b23a 100644 --- a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/BeanMapper.java +++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/BeanMapper.java @@ -1,5 +1,5 @@ /* - * Copyright 2017 Google LLC + * Copyright 2024 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/DeserializeContext.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/DeserializeContext.java index 818c553e5..6609fb50e 100644 --- a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/DeserializeContext.java +++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/DeserializeContext.java @@ -1,5 +1,5 @@ /* - * Copyright 2017 Google LLC + * Copyright 2024 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/RecordMapper.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/RecordMapper.java index 0dd4ae909..86e88990d 100644 --- a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/RecordMapper.java +++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/RecordMapper.java @@ -1,5 +1,5 @@ /* - * Copyright 2017 Google LLC + * Copyright 2024 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/google-cloud-firestore/src/test-jdk17/java/com/google/cloud/firestore/RecordDocumentReferenceTest.java b/google-cloud-firestore/src/test-jdk17/java/com/google/cloud/firestore/RecordDocumentReferenceTest.java index 1f48d312c..9511e6ea2 100644 --- a/google-cloud-firestore/src/test-jdk17/java/com/google/cloud/firestore/RecordDocumentReferenceTest.java +++ b/google-cloud-firestore/src/test-jdk17/java/com/google/cloud/firestore/RecordDocumentReferenceTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2017 Google LLC + * Copyright 2024 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -86,7 +86,6 @@ public void serializeBasicTypes() throws Exception { @Test public void doesNotSerializeAdvancedNumberTypes() { - System.out.println("========================== Record tests run on CI"); Map expectedErrorMessages = new HashMap<>(); InvalidRecord record = new InvalidRecord(new BigInteger("0"), null, null); diff --git a/google-cloud-firestore/src/test-jdk17/java/com/google/cloud/firestore/RecordMapperTest.java b/google-cloud-firestore/src/test-jdk17/java/com/google/cloud/firestore/RecordMapperTest.java index 3b6103206..5a609b9e2 100644 --- a/google-cloud-firestore/src/test-jdk17/java/com/google/cloud/firestore/RecordMapperTest.java +++ b/google-cloud-firestore/src/test-jdk17/java/com/google/cloud/firestore/RecordMapperTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2019 Google LLC + * Copyright 2024 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/google-cloud-firestore/src/test-jdk17/java/com/google/cloud/firestore/record/LocalFirestoreHelper.java b/google-cloud-firestore/src/test-jdk17/java/com/google/cloud/firestore/record/LocalFirestoreHelper.java index 9dc624102..e9368bf47 100644 --- a/google-cloud-firestore/src/test-jdk17/java/com/google/cloud/firestore/record/LocalFirestoreHelper.java +++ b/google-cloud-firestore/src/test-jdk17/java/com/google/cloud/firestore/record/LocalFirestoreHelper.java @@ -1,5 +1,5 @@ /* - * Copyright 2017 Google LLC + * Copyright 2024 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. From 62aa37c46c308378c399daad8fb0e87e8488d4f8 Mon Sep 17 00:00:00 2001 From: milaGGL <107142260+milaGGL@users.noreply.github.com> Date: Wed, 11 Sep 2024 11:46:47 -0400 Subject: [PATCH 41/51] Add nested record & pojo types to test --- .../RecordDocumentReferenceTest.java | 62 ++++++++++++------- .../record/LocalFirestoreHelper.java | 43 +++++++++++-- 2 files changed, 79 insertions(+), 26 deletions(-) diff --git a/google-cloud-firestore/src/test-jdk17/java/com/google/cloud/firestore/RecordDocumentReferenceTest.java b/google-cloud-firestore/src/test-jdk17/java/com/google/cloud/firestore/RecordDocumentReferenceTest.java index 9511e6ea2..753a6cf79 100644 --- a/google-cloud-firestore/src/test-jdk17/java/com/google/cloud/firestore/RecordDocumentReferenceTest.java +++ b/google-cloud-firestore/src/test-jdk17/java/com/google/cloud/firestore/RecordDocumentReferenceTest.java @@ -333,27 +333,47 @@ public void extractFieldMaskFromMerge() throws Exception { nestedProto.getMapValueBuilder().putAllFields(ALL_SUPPORTED_TYPES_PROTO); nestedUpdate.put("second", nestedProto.build()); - List updateMask = - Arrays.asList( - "first.foo", - "second.arrayValue", - "second.bytesValue", - "second.dateValue", - "second.doubleValue", - "second.falseValue", - "second.foo", - "second.geoPointValue", - "second.infValue", - "second.longValue", - "second.nanValue", - "second.negInfValue", - "second.nullValue", - "second.objectValue.foo", - "second.timestampValue", - "second.trueValue", - "second.model.foo"); - - CommitRequest expectedCommit = commit(set(nestedUpdate, updateMask)); + List updateMask = Arrays.asList( + "first.foo", + "second.arrayValue", + "second.bytesValue", + "second.dateValue", + "second.doubleValue", + "second.falseValue", + "second.foo", + "second.geoPointValue", + "second.infValue", + "second.longValue", + "second.nanValue", + "second.negInfValue", + "second.nullValue", + "second.objectValue.foo", + "second.timestampValue", + "second.trueValue", + "second.model.foo"); + + CommitRequest expectedCommit = commit(set(nestedUpdate, updateMask)); assertCommitEquals(expectedCommit, commitCapture.getValue()); } + + @Test + public void setNestedRecordWithPojoMapWithMerge() throws Exception { + doReturn(SINGLE_WRITE_COMMIT_RESPONSE) + .when(firestoreMock) + .sendRequest( + commitCapture.capture(), ArgumentMatchers.>any()); + + documentReference.set(NESTED_RECORD_WITH_POJO_OBJECT, SetOptions.mergeFields("first", "second")).get(); + + Map nestedUpdate = new HashMap<>(); + Value.Builder nestedProto = Value.newBuilder(); + nestedProto.getMapValueBuilder().putAllFields(SINGLE_COMPONENT_PROTO); + nestedUpdate.put("first", nestedProto.build()); + nestedProto.getMapValueBuilder().putAllFields(ALL_SUPPORTED_TYPES_PROTO); + nestedUpdate.put("second", nestedProto.build()); + + CommitRequest expectedCommit = commit(set(nestedUpdate, Arrays.asList("first", "second"))); + assertCommitEquals(expectedCommit, commitCapture.getValue()); + } + } diff --git a/google-cloud-firestore/src/test-jdk17/java/com/google/cloud/firestore/record/LocalFirestoreHelper.java b/google-cloud-firestore/src/test-jdk17/java/com/google/cloud/firestore/record/LocalFirestoreHelper.java index e9368bf47..cacc0dca0 100644 --- a/google-cloud-firestore/src/test-jdk17/java/com/google/cloud/firestore/record/LocalFirestoreHelper.java +++ b/google-cloud-firestore/src/test-jdk17/java/com/google/cloud/firestore/record/LocalFirestoreHelper.java @@ -72,6 +72,8 @@ public final class LocalFirestoreHelper { public static final Map SINGLE_COMPONENT_PROTO; public static final NestedRecord NESTED_RECORD_OBJECT; + public static final NestedRecordWithPOJO NESTED_RECORD_WITH_POJO_OBJECT; + public static final NestedPOJOWithRecord NESTED_POJO_WITH_RECORD_OBJECT; public static final ServerTimestamp SERVER_TIMESTAMP_OBJECT; public static final Map SERVER_TIMESTAMP_PROTO; @@ -90,14 +92,41 @@ public final class LocalFirestoreHelper { public record SingleComponent( + String foo) { + } + + public static class SingleField { + public String foo = "bar"; + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } - String foo - ){} + SingleField that = (SingleField) o; + + return foo.equals(that.foo); + } + } public record NestedRecord( - SingleComponent first, - AllSupportedTypes second - ){} + SingleComponent first, + AllSupportedTypes second) { + } + + public record NestedRecordWithPOJO( + SingleField first, + AllSupportedTypes second) { + } + + public static class NestedPOJOWithRecord { + public SingleField first = new SingleField(); + public AllSupportedTypes second = ALL_SUPPORTED_TYPES_OBJECT; + } public record ServerTimestamp ( @@ -390,6 +419,10 @@ public record AllSupportedTypes ( FIELD_TRANSFORM_COMMIT_RESPONSE = commitResponse(/* adds= */ 2, /* deletes= */ 0); NESTED_RECORD_OBJECT = new NestedRecord(SINGLE_COMPONENT_OBJECT, ALL_SUPPORTED_TYPES_OBJECT); + + NESTED_RECORD_WITH_POJO_OBJECT = new NestedRecordWithPOJO(new SingleField(), ALL_SUPPORTED_TYPES_OBJECT); + + NESTED_POJO_WITH_RECORD_OBJECT = new NestedPOJOWithRecord(); } @SuppressWarnings("unchecked") From 124fc47fc579d0e61c28c3461ba472a78573ace9 Mon Sep 17 00:00:00 2001 From: milaGGL <107142260+milaGGL@users.noreply.github.com> Date: Wed, 11 Sep 2024 11:52:07 -0400 Subject: [PATCH 42/51] Update RecordDocumentReferenceTest.java --- .../RecordDocumentReferenceTest.java | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/google-cloud-firestore/src/test-jdk17/java/com/google/cloud/firestore/RecordDocumentReferenceTest.java b/google-cloud-firestore/src/test-jdk17/java/com/google/cloud/firestore/RecordDocumentReferenceTest.java index 753a6cf79..6a89ce619 100644 --- a/google-cloud-firestore/src/test-jdk17/java/com/google/cloud/firestore/RecordDocumentReferenceTest.java +++ b/google-cloud-firestore/src/test-jdk17/java/com/google/cloud/firestore/RecordDocumentReferenceTest.java @@ -25,6 +25,7 @@ import com.google.api.gax.rpc.ResponseObserver; import com.google.api.gax.rpc.ServerStreamingCallable; import com.google.api.gax.rpc.UnaryCallable; +import com.google.cloud.firestore.record.LocalFirestoreHelper.InvalidRecord; import com.google.cloud.firestore.spi.v1.FirestoreRpc; import com.google.firestore.v1.BatchGetDocumentsRequest; import com.google.firestore.v1.CommitRequest; @@ -376,4 +377,25 @@ public void setNestedRecordWithPojoMapWithMerge() throws Exception { assertCommitEquals(expectedCommit, commitCapture.getValue()); } + @Test + public void setNestedPojoWithRecordMapWithMerge() throws Exception { + doReturn(SINGLE_WRITE_COMMIT_RESPONSE) + .when(firestoreMock) + .sendRequest( + commitCapture.capture(), + ArgumentMatchers.>any()); + + documentReference.set(NESTED_POJO_WITH_RECORD_OBJECT, SetOptions.mergeFields("first", "second")).get(); + System.out.println("============="); + + Map nestedUpdate = new HashMap<>(); + Value.Builder nestedProto = Value.newBuilder(); + nestedProto.getMapValueBuilder().putAllFields(SINGLE_COMPONENT_PROTO); + nestedUpdate.put("first", nestedProto.build()); + nestedProto.getMapValueBuilder().putAllFields(ALL_SUPPORTED_TYPES_PROTO); + nestedUpdate.put("second", nestedProto.build()); + + CommitRequest expectedCommit = commit(set(nestedUpdate, Arrays.asList("first", "second"))); + assertCommitEquals(expectedCommit, commitCapture.getValue()); + } } From 2f082c44f3c0b3cbc495a17216e91062f7c196bb Mon Sep 17 00:00:00 2001 From: milaGGL <107142260+milaGGL@users.noreply.github.com> Date: Wed, 11 Sep 2024 11:57:07 -0400 Subject: [PATCH 43/51] format --- google-cloud-firestore/pom.xml | 2 +- .../com/google/cloud/firestore/RecordDocumentReferenceTest.java | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/google-cloud-firestore/pom.xml b/google-cloud-firestore/pom.xml index e22246753..924d744ab 100644 --- a/google-cloud-firestore/pom.xml +++ b/google-cloud-firestore/pom.xml @@ -363,7 +363,7 @@ - + org.apache.maven.plugins maven-javadoc-plugin diff --git a/google-cloud-firestore/src/test-jdk17/java/com/google/cloud/firestore/RecordDocumentReferenceTest.java b/google-cloud-firestore/src/test-jdk17/java/com/google/cloud/firestore/RecordDocumentReferenceTest.java index 6a89ce619..ce84663b7 100644 --- a/google-cloud-firestore/src/test-jdk17/java/com/google/cloud/firestore/RecordDocumentReferenceTest.java +++ b/google-cloud-firestore/src/test-jdk17/java/com/google/cloud/firestore/RecordDocumentReferenceTest.java @@ -386,7 +386,6 @@ public void setNestedPojoWithRecordMapWithMerge() throws Exception { ArgumentMatchers.>any()); documentReference.set(NESTED_POJO_WITH_RECORD_OBJECT, SetOptions.mergeFields("first", "second")).get(); - System.out.println("============="); Map nestedUpdate = new HashMap<>(); Value.Builder nestedProto = Value.newBuilder(); From b51506afe68054dafc950da4d5ba47990da59284 Mon Sep 17 00:00:00 2001 From: milaGGL <107142260+milaGGL@users.noreply.github.com> Date: Thu, 12 Sep 2024 11:30:03 -0400 Subject: [PATCH 44/51] rename & relocate record test helper file --- .../RecordDocumentReferenceTest.java | 11 +- .../cloud/firestore/RecordMapperTest.java | 4 +- ...storeHelper.java => RecordTestHelper.java} | 234 +----------------- 3 files changed, 17 insertions(+), 232 deletions(-) rename google-cloud-firestore/src/test-jdk17/java/com/google/cloud/firestore/{record/LocalFirestoreHelper.java => RecordTestHelper.java} (50%) diff --git a/google-cloud-firestore/src/test-jdk17/java/com/google/cloud/firestore/RecordDocumentReferenceTest.java b/google-cloud-firestore/src/test-jdk17/java/com/google/cloud/firestore/RecordDocumentReferenceTest.java index ce84663b7..fb4cc57e5 100644 --- a/google-cloud-firestore/src/test-jdk17/java/com/google/cloud/firestore/RecordDocumentReferenceTest.java +++ b/google-cloud-firestore/src/test-jdk17/java/com/google/cloud/firestore/RecordDocumentReferenceTest.java @@ -16,7 +16,15 @@ package com.google.cloud.firestore; -import static com.google.cloud.firestore.record.LocalFirestoreHelper.*; +import static com.google.cloud.firestore.LocalFirestoreHelper.assertCommitEquals; +import static com.google.cloud.firestore.LocalFirestoreHelper.commit; +import static com.google.cloud.firestore.LocalFirestoreHelper.create; +import static com.google.cloud.firestore.LocalFirestoreHelper.getAllResponse; +import static com.google.cloud.firestore.LocalFirestoreHelper.map; +import static com.google.cloud.firestore.LocalFirestoreHelper.serverTimestamp; +import static com.google.cloud.firestore.LocalFirestoreHelper.set; +import static com.google.cloud.firestore.LocalFirestoreHelper.transform; +import static com.google.cloud.firestore.RecordTestHelper.*; import static org.junit.Assert.assertEquals; import static org.junit.Assert.fail; import static org.mockito.Mockito.doAnswer; @@ -25,7 +33,6 @@ import com.google.api.gax.rpc.ResponseObserver; import com.google.api.gax.rpc.ServerStreamingCallable; import com.google.api.gax.rpc.UnaryCallable; -import com.google.cloud.firestore.record.LocalFirestoreHelper.InvalidRecord; import com.google.cloud.firestore.spi.v1.FirestoreRpc; import com.google.firestore.v1.BatchGetDocumentsRequest; import com.google.firestore.v1.CommitRequest; diff --git a/google-cloud-firestore/src/test-jdk17/java/com/google/cloud/firestore/RecordMapperTest.java b/google-cloud-firestore/src/test-jdk17/java/com/google/cloud/firestore/RecordMapperTest.java index 5a609b9e2..9d876e133 100644 --- a/google-cloud-firestore/src/test-jdk17/java/com/google/cloud/firestore/RecordMapperTest.java +++ b/google-cloud-firestore/src/test-jdk17/java/com/google/cloud/firestore/RecordMapperTest.java @@ -40,8 +40,8 @@ import java.util.Map; import java.util.Set; -import static com.google.cloud.firestore.record.LocalFirestoreHelper.fromSingleQuotedString; -import static com.google.cloud.firestore.record.LocalFirestoreHelper.mapAnyType; +import static com.google.cloud.firestore.LocalFirestoreHelper.fromSingleQuotedString; +import static com.google.cloud.firestore.LocalFirestoreHelper.mapAnyType; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; diff --git a/google-cloud-firestore/src/test-jdk17/java/com/google/cloud/firestore/record/LocalFirestoreHelper.java b/google-cloud-firestore/src/test-jdk17/java/com/google/cloud/firestore/RecordTestHelper.java similarity index 50% rename from google-cloud-firestore/src/test-jdk17/java/com/google/cloud/firestore/record/LocalFirestoreHelper.java rename to google-cloud-firestore/src/test-jdk17/java/com/google/cloud/firestore/RecordTestHelper.java index cacc0dca0..6bfe845be 100644 --- a/google-cloud-firestore/src/test-jdk17/java/com/google/cloud/firestore/record/LocalFirestoreHelper.java +++ b/google-cloud-firestore/src/test-jdk17/java/com/google/cloud/firestore/RecordTestHelper.java @@ -14,54 +14,34 @@ * limitations under the License. */ -package com.google.cloud.firestore.record; +package com.google.cloud.firestore; import com.google.api.core.ApiFuture; -import com.google.api.core.ApiFutures; -import com.google.api.gax.rpc.ResponseObserver; -import com.google.api.gax.rpc.StreamController; import com.google.cloud.Timestamp; -import com.google.cloud.firestore.Blob; -import com.google.cloud.firestore.GeoPoint; +import com.google.cloud.firestore.LocalFirestoreHelper.SingleField; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.firestore.v1.ArrayValue; -import com.google.firestore.v1.BatchGetDocumentsResponse; -import com.google.firestore.v1.CommitRequest; import com.google.firestore.v1.CommitResponse; -import com.google.firestore.v1.Document; -import com.google.firestore.v1.DocumentMask; -import com.google.firestore.v1.DocumentTransform.FieldTransform; import com.google.firestore.v1.MapValue; import com.google.firestore.v1.Value; -import com.google.firestore.v1.Write; -import com.google.gson.Gson; -import com.google.gson.reflect.TypeToken; -import com.google.protobuf.ByteString; import com.google.protobuf.NullValue; import com.google.type.LatLng; -import java.lang.reflect.Type; -import org.mockito.stubbing.Answer; -import javax.annotation.Nullable; +import static com.google.cloud.firestore.LocalFirestoreHelper.commitResponse; +import static com.google.cloud.firestore.LocalFirestoreHelper.map; + import java.math.BigInteger; import java.text.ParseException; import java.text.SimpleDateFormat; -import java.util.ArrayList; -import java.util.Arrays; import java.util.Collections; -import java.util.Comparator; import java.util.Date; -import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.TimeUnit; -import static org.junit.Assert.assertEquals; -import static org.mockito.Mockito.mock; - -public final class LocalFirestoreHelper { +public final class RecordTestHelper { public static final String DATABASE_NAME; public static final String DOCUMENT_PATH; @@ -95,24 +75,6 @@ public record SingleComponent( String foo) { } - public static class SingleField { - public String foo = "bar"; - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - - SingleField that = (SingleField) o; - - return foo.equals(that.foo); - } - } - public record NestedRecord( SingleComponent first, AllSupportedTypes second) { @@ -146,172 +108,6 @@ public record InvalidRecord ( Short shortValue ){} - public static Map map(K key, V value, Object... moreKeysAndValues) { - Map map = new HashMap<>(); - map.put(key, value); - - for (int i = 0; i < moreKeysAndValues.length; i += 2) { - map.put((K) moreKeysAndValues[i], (V) moreKeysAndValues[i + 1]); - } - - return map; - } - - public static Answer getAllResponse( - final Map... fields) { - BatchGetDocumentsResponse[] responses = new BatchGetDocumentsResponse[fields.length]; - - for (int i = 0; i < fields.length; ++i) { - String name = DOCUMENT_NAME; - if (fields.length > 1) { - name += i + 1; - } - BatchGetDocumentsResponse.Builder response = BatchGetDocumentsResponse.newBuilder(); - response - .getFoundBuilder() - .setCreateTime(com.google.protobuf.Timestamp.newBuilder().setSeconds(1).setNanos(2)); - response - .getFoundBuilder() - .setUpdateTime(com.google.protobuf.Timestamp.newBuilder().setSeconds(3).setNanos(4)); - response.setReadTime(com.google.protobuf.Timestamp.newBuilder().setSeconds(5).setNanos(6)); - response.getFoundBuilder().setName(name).putAllFields(fields[i]); - responses[i] = response.build(); - } - - return streamingResponse(responses, null); - } - - /** Returns a stream of responses followed by an optional exception. */ - public static Answer streamingResponse( - final T[] response, @Nullable final Throwable throwable) { - return invocation -> { - Object[] args = invocation.getArguments(); - ResponseObserver observer = (ResponseObserver) args[1]; - observer.onStart(mock(StreamController.class)); - for (T resp : response) { - observer.onResponse(resp); - } - if (throwable != null) { - observer.onError(throwable); - } - observer.onComplete(); - return null; - }; - } - - public static ApiFuture commitResponse(int adds, int deletes) { - CommitResponse.Builder commitResponse = CommitResponse.newBuilder(); - commitResponse.getCommitTimeBuilder().setSeconds(0).setNanos(0); - for (int i = 0; i < adds; ++i) { - commitResponse.addWriteResultsBuilder().getUpdateTimeBuilder().setSeconds(i).setNanos(i); - } - for (int i = 0; i < deletes; ++i) { - commitResponse.addWriteResultsBuilder(); - } - return ApiFutures.immediateFuture(commitResponse.build()); - } - - public static FieldTransform serverTimestamp() { - return FieldTransform.newBuilder() - .setSetToServerValue(FieldTransform.ServerValue.REQUEST_TIME) - .build(); - } - - public static List transform( - String fieldPath, FieldTransform fieldTransform, Object... fieldPathOrTransform) { - - List transforms = new ArrayList<>(); - FieldTransform.Builder transformBuilder = FieldTransform.newBuilder(); - transformBuilder.setFieldPath(fieldPath).mergeFrom(fieldTransform); - - transforms.add(transformBuilder.build()); - - for (int i = 0; i < fieldPathOrTransform.length; i += 2) { - String path = (String) fieldPathOrTransform[i]; - FieldTransform transform = (FieldTransform) fieldPathOrTransform[i + 1]; - transforms.add(FieldTransform.newBuilder().setFieldPath(path).mergeFrom(transform).build()); - } - return transforms; - } - - public static Write create(Map fields, String docPath) { - Write.Builder write = Write.newBuilder(); - Document.Builder document = write.getUpdateBuilder(); - document.setName(DOCUMENT_ROOT + docPath); - document.putAllFields(fields); - write.getCurrentDocumentBuilder().setExists(false); - return write.build(); - } - - public static Write create(Map fields) { - return create(fields, DOCUMENT_PATH); - } - - public static Write set(Map fields) { - return set(fields, null, DOCUMENT_PATH); - } - - public static Write set(Map fields, @Nullable List fieldMap) { - return set(fields, fieldMap, DOCUMENT_PATH); - } - - public static Write set( - Map fields, @Nullable List fieldMap, String docPath) { - Write.Builder write = Write.newBuilder(); - Document.Builder document = write.getUpdateBuilder(); - document.setName(DOCUMENT_ROOT + docPath); - document.putAllFields(fields); - - if (fieldMap != null) { - write.getUpdateMaskBuilder().addAllFieldPaths(fieldMap); - } - - return write.build(); - } - - public static CommitRequest commit(@Nullable String transactionId, Write... writes) { - CommitRequest.Builder commitRequest = CommitRequest.newBuilder(); - commitRequest.setDatabase(DATABASE_NAME); - commitRequest.addAllWrites(Arrays.asList(writes)); - - if (transactionId != null) { - commitRequest.setTransaction(ByteString.copyFromUtf8(transactionId)); - } - - return commitRequest.build(); - } - - public static CommitRequest commit(Write... writes) { - return commit(null, writes); - } - - public static CommitRequest commit(Write write, List transforms) { - return commit((String) null, write.toBuilder().addAllUpdateTransforms(transforms).build()); - } - - public static void assertCommitEquals(CommitRequest expected, CommitRequest actual) { - assertEquals(sortCommit(expected), sortCommit(actual)); - } - - private static CommitRequest sortCommit(CommitRequest commit) { - CommitRequest.Builder builder = commit.toBuilder(); - - for (Write.Builder writes : builder.getWritesBuilderList()) { - if (writes.hasUpdateMask()) { - ArrayList updateMask = new ArrayList<>(writes.getUpdateMask().getFieldPathsList()); - Collections.sort(updateMask); - writes.setUpdateMask(DocumentMask.newBuilder().addAllFieldPaths(updateMask)); - } - - if (!writes.getUpdateTransformsList().isEmpty()) { - ArrayList transformList = new ArrayList<>(writes.getUpdateTransformsList()); - transformList.sort(Comparator.comparing(FieldTransform::getFieldPath)); - writes.clearUpdateTransforms().addAllUpdateTransforms(transformList); - } - } - - return builder.build(); - } public record AllSupportedTypes ( @@ -425,22 +221,4 @@ public record AllSupportedTypes ( NESTED_POJO_WITH_RECORD_OBJECT = new NestedPOJOWithRecord(); } - @SuppressWarnings("unchecked") - public static Map mapAnyType(Object... entries) { - Map res = new HashMap<>(); - for (int i = 0; i < entries.length; i += 2) { - res.put((String) entries[i], (T) entries[i + 1]); - } - return res; - } - - private static Map fromJsonString(String json) { - Type type = new TypeToken>() {}.getType(); - Gson gson = new Gson(); - return gson.fromJson(json, type); - } - - public static Map fromSingleQuotedString(String json) { - return fromJsonString(json.replace("'", "\"")); - } } From d08cd6b55795576312b88272e3bcd818046b6653 Mon Sep 17 00:00:00 2001 From: milaGGL <107142260+milaGGL@users.noreply.github.com> Date: Thu, 12 Sep 2024 16:55:53 -0400 Subject: [PATCH 45/51] move PojoBeanMapper out, move errors to --- .../google/cloud/firestore/BeanMapper.java | 10 + .../cloud/firestore/CustomClassMapper.java | 616 ++---------------- .../cloud/firestore/DeserializeContext.java | 18 + .../cloud/firestore/PojoBeanMapper.java | 515 +++++++++++++++ .../google/cloud/firestore/RecordMapper.java | 23 +- 5 files changed, 597 insertions(+), 585 deletions(-) create mode 100644 google-cloud-firestore/src/main/java/com/google/cloud/firestore/PojoBeanMapper.java diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/BeanMapper.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/BeanMapper.java index 37114b23a..5e50d51ce 100644 --- a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/BeanMapper.java +++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/BeanMapper.java @@ -180,4 +180,14 @@ static String annotatedName(AccessibleObject obj) { return null; } + + Object getSerializedValue( + String property, Object propertyValue, DeserializeContext.ErrorPath path) { + if (serverTimestamps.contains(property) && propertyValue == null) { + // Replace null ServerTimestamp-annotated fields with the sentinel. + return FieldValue.serverTimestamp(); + } else { + return CustomClassMapper.serialize(propertyValue, path.child(property)); + } + } } 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 d85539d4b..95dc9872b 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 @@ -18,15 +18,10 @@ import com.google.cloud.Timestamp; import com.google.cloud.firestore.annotation.DocumentId; -import com.google.cloud.firestore.annotation.Exclude; -import com.google.cloud.firestore.annotation.ServerTimestamp; import com.google.firestore.v1.Value; -import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.GenericArrayType; import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; -import java.lang.reflect.Modifier; import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; import java.lang.reflect.TypeVariable; @@ -37,17 +32,13 @@ import java.util.Collection; import java.util.Date; import java.util.HashMap; -import java.util.HashSet; import java.util.List; -import java.util.Locale; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; -import java.util.logging.Logger; /** Helper class to convert to/from custom POJO classes and plain Java types. */ class CustomClassMapper { - private static final Logger LOGGER = Logger.getLogger(CustomClassMapper.class.getName()); /** Maximum depth before we give up and assume it's a recursive object graph. */ private static final int MAX_DEPTH = 500; @@ -104,8 +95,7 @@ static Object serialize(T o) { @SuppressWarnings("unchecked") static Object serialize(T o, DeserializeContext.ErrorPath path) { if (path.getLength() > MAX_DEPTH) { - throw serializeError( - path, + throw path.serializeError( "Exceeded maximum depth of " + MAX_DEPTH + ", which likely indicates there's an object cycle"); @@ -118,8 +108,7 @@ static Object serialize(T o, DeserializeContext.ErrorPath path) { } else if (o instanceof BigDecimal) { return String.valueOf(o); } else { - throw serializeError( - path, + throw path.serializeError( String.format( "Numbers of type %s are not supported, please use an int, long, float, double or BigDecimal", o.getClass().getSimpleName())); @@ -129,7 +118,7 @@ static Object serialize(T o, DeserializeContext.ErrorPath path) { } else if (o instanceof Boolean) { return o; } else if (o instanceof Character) { - throw serializeError(path, "Characters are not supported, please use Strings"); + throw path.serializeError("Characters are not supported, please use Strings"); } else if (o instanceof Map) { Map result = new HashMap<>(); for (Map.Entry entry : ((Map) o).entrySet()) { @@ -138,7 +127,7 @@ static Object serialize(T o, DeserializeContext.ErrorPath path) { String keyString = (String) key; result.put(keyString, serialize(entry.getValue(), path.child(keyString))); } else { - throw serializeError(path, "Maps with non-string keys are not supported"); + throw path.serializeError("Maps with non-string keys are not supported"); } } return result; @@ -151,11 +140,11 @@ static Object serialize(T o, DeserializeContext.ErrorPath path) { } return result; } else { - throw serializeError( - path, "Serializing Collections is not supported, please use Lists instead"); + throw path.serializeError( + "Serializing Collections is not supported, please use Lists instead"); } } else if (o.getClass().isArray()) { - throw serializeError(path, "Serializing Arrays is not supported, please use Lists instead"); + throw path.serializeError("Serializing Arrays is not supported, please use Lists instead"); } else if (o instanceof Enum) { String enumName = ((Enum) o).name(); try { @@ -194,8 +183,8 @@ static T deserializeToType(Object o, Type type, DeserializeContext context) } else if (type instanceof WildcardType) { Type[] lowerBounds = ((WildcardType) type).getLowerBounds(); if (lowerBounds.length > 0) { - throw deserializeError( - context.errorPath, "Generic lower-bounded wildcard types are not supported"); + throw context.errorPath.deserializeError( + "Generic lower-bounded wildcard types are not supported"); } // Upper bounded wildcards are of the form . Multiple upper bounds are allowed @@ -212,10 +201,10 @@ static T deserializeToType(Object o, Type type, DeserializeContext context) return deserializeToType(o, upperBounds[0], context); } else if (type instanceof GenericArrayType) { - throw deserializeError( - context.errorPath, "Generic Arrays are not supported, please use Lists instead"); + throw context.errorPath.deserializeError( + "Generic Arrays are not supported, please use Lists instead"); } else { - throw deserializeError(context.errorPath, "Unknown type encountered: " + type); + throw context.errorPath.deserializeError("Unknown type encountered: " + type); } } @@ -245,11 +234,10 @@ private static T deserializeToClass(Object o, Class clazz, DeserializeCon } else if (DocumentReference.class.isAssignableFrom(clazz)) { return (T) convertDocumentReference(o, context); } else if (clazz.isArray()) { - throw deserializeError( - context.errorPath, "Converting to Arrays is not supported, please use Lists instead"); + throw context.errorPath.deserializeError( + "Converting to Arrays is not supported, please use Lists instead"); } else if (clazz.getTypeParameters().length > 0) { - throw deserializeError( - context.errorPath, + throw context.errorPath.deserializeError( "Class " + clazz.getName() + " has generic type parameters, please use GenericTypeIndicator instead"); @@ -281,8 +269,7 @@ private static T deserializeToParameterizedType( | IllegalAccessException | NoSuchMethodException | InvocationTargetException e) { - throw deserializeError( - context.errorPath, + throw context.errorPath.deserializeError( String.format( "Unable to deserialize to %s: %s", rawType.getSimpleName(), e.toString())); } @@ -295,14 +282,13 @@ private static T deserializeToParameterizedType( } return (T) result; } else { - throw deserializeError(context.errorPath, "Expected a List, but got a " + o.getClass()); + throw context.errorPath.deserializeError("Expected a List, but got a " + o.getClass()); } } else if (Map.class.isAssignableFrom(rawType)) { Type keyType = type.getActualTypeArguments()[0]; Type valueType = type.getActualTypeArguments()[1]; if (!keyType.equals(String.class)) { - throw deserializeError( - context.errorPath, + throw context.errorPath.deserializeError( "Only Maps with string keys are supported, but found Map with key type " + keyType); } Map map = expectMap(o, context); @@ -316,8 +302,7 @@ private static T deserializeToParameterizedType( | IllegalAccessException | NoSuchMethodException | InvocationTargetException e) { - throw deserializeError( - context.errorPath, + throw context.errorPath.deserializeError( String.format( "Unable to deserialize to %s: %s", rawType.getSimpleName(), e.toString())); } @@ -331,8 +316,8 @@ private static T deserializeToParameterizedType( } return (T) result; } else if (Collection.class.isAssignableFrom(rawType)) { - throw deserializeError( - context.errorPath, "Collections are not supported, please use Lists instead"); + throw context.errorPath.deserializeError( + "Collections are not supported, please use Lists instead"); } else { Map map = expectMap(o, context); BeanMapper mapper = (BeanMapper) loadOrCreateBeanMapperForClass(rawType); @@ -365,8 +350,7 @@ private static T deserializeToPrimitive( } else if (Float.class.isAssignableFrom(clazz) || float.class.isAssignableFrom(clazz)) { return (T) (Float) convertDouble(o, context).floatValue(); } else { - throw deserializeError( - context.errorPath, + throw context.errorPath.deserializeError( String.format("Deserializing values to %s is not supported", clazz.getSimpleName())); } } @@ -394,13 +378,11 @@ private static T deserializeToEnum( try { return (T) Enum.valueOf((Class) clazz, value); } catch (IllegalArgumentException e) { - throw deserializeError( - context.errorPath, + throw context.errorPath.deserializeError( "Could not find enum value of " + clazz.getName() + " for value \"" + value + "\""); } } else { - throw deserializeError( - context.errorPath, + throw context.errorPath.deserializeError( "Expected a String while deserializing to enum " + clazz + " but got a " @@ -430,8 +412,8 @@ private static Map expectMap(Object object, DeserializeContext c // TODO: runtime validation of keys? return (Map) object; } else { - throw deserializeError( - context.errorPath, "Expected a Map while deserializing, but got a " + object.getClass()); + throw context.errorPath.deserializeError( + "Expected a Map while deserializing, but got a " + object.getClass()); } } @@ -443,15 +425,13 @@ private static Integer convertInteger(Object o, DeserializeContext context) { if (value >= Integer.MIN_VALUE && value <= Integer.MAX_VALUE) { return ((Number) o).intValue(); } else { - throw deserializeError( - context.errorPath, + throw context.errorPath.deserializeError( "Numeric value out of 32-bit integer range: " + value + ". Did you mean to use a long or double instead of an int?"); } } else { - throw deserializeError( - context.errorPath, + throw context.errorPath.deserializeError( "Failed to convert a value of type " + o.getClass().getName() + " to int"); } } @@ -466,15 +446,13 @@ private static Long convertLong(Object o, DeserializeContext context) { if (value >= Long.MIN_VALUE && value <= Long.MAX_VALUE) { return value.longValue(); } else { - throw deserializeError( - context.errorPath, + throw context.errorPath.deserializeError( "Numeric value out of 64-bit long range: " + value + ". Did you mean to use a double instead of a long?"); } } else { - throw deserializeError( - context.errorPath, + throw context.errorPath.deserializeError( "Failed to convert a value of type " + o.getClass().getName() + " to long"); } } @@ -488,8 +466,7 @@ private static Double convertDouble(Object o, DeserializeContext context) { if (doubleValue.longValue() == value) { return doubleValue; } else { - throw deserializeError( - context.errorPath, + throw context.errorPath.deserializeError( "Loss of precision while converting number to " + "double: " + o @@ -498,8 +475,7 @@ private static Double convertDouble(Object o, DeserializeContext context) { } else if (o instanceof Double) { return (Double) o; } else { - throw deserializeError( - context.errorPath, + throw context.errorPath.deserializeError( "Failed to convert a value of type " + o.getClass().getName() + " to double"); } } @@ -516,8 +492,7 @@ private static BigDecimal convertBigDecimal(Object o, DeserializeContext context } else if (o instanceof String) { return new BigDecimal((String) o); } else { - throw deserializeError( - context.errorPath, + throw context.errorPath.deserializeError( "Failed to convert a value of type " + o.getClass().getName() + " to BigDecimal"); } } @@ -526,8 +501,7 @@ private static Boolean convertBoolean(Object o, DeserializeContext context) { if (o instanceof Boolean) { return (Boolean) o; } else { - throw deserializeError( - context.errorPath, + throw context.errorPath.deserializeError( "Failed to convert value of type " + o.getClass().getName() + " to boolean"); } } @@ -536,8 +510,7 @@ private static String convertString(Object o, DeserializeContext context) { if (o instanceof String) { return (String) o; } else { - throw deserializeError( - context.errorPath, + throw context.errorPath.deserializeError( "Failed to convert value of type " + o.getClass().getName() + " to String"); } } @@ -548,8 +521,7 @@ private static Date convertDate(Object o, DeserializeContext context) { } else if (o instanceof Timestamp) { return ((Timestamp) o).toDate(); } else { - throw deserializeError( - context.errorPath, + throw context.errorPath.deserializeError( "Failed to convert value of type " + o.getClass().getName() + " to Date"); } } @@ -560,8 +532,7 @@ private static Timestamp convertTimestamp(Object o, DeserializeContext context) } else if (o instanceof Date) { return Timestamp.of((Date) o); } else { - throw deserializeError( - context.errorPath, + throw context.errorPath.deserializeError( "Failed to convert value of type " + o.getClass().getName() + " to Timestamp"); } } @@ -573,8 +544,7 @@ private static Instant convertInstant(Object o, DeserializeContext context) { } else if (o instanceof Date) { return Instant.ofEpochMilli(((Date) o).getTime()); } else { - throw deserializeError( - context.errorPath, + throw context.errorPath.deserializeError( "Failed to convert value of type " + o.getClass().getName() + " to Instant"); } } @@ -583,8 +553,7 @@ private static Blob convertBlob(Object o, DeserializeContext context) { if (o instanceof Blob) { return (Blob) o; } else { - throw deserializeError( - context.errorPath, + throw context.errorPath.deserializeError( "Failed to convert value of type " + o.getClass().getName() + " to Blob"); } } @@ -593,8 +562,7 @@ private static GeoPoint convertGeoPoint(Object o, DeserializeContext context) { if (o instanceof GeoPoint) { return (GeoPoint) o; } else { - throw deserializeError( - context.errorPath, + throw context.errorPath.deserializeError( "Failed to convert value of type " + o.getClass().getName() + " to GeoPoint"); } } @@ -603,8 +571,7 @@ private static VectorValue convertVectorValue(Object o, DeserializeContext conte if (o instanceof VectorValue) { return (VectorValue) o; } else { - throw deserializeError( - context.errorPath, + throw context.errorPath.deserializeError( "Failed to convert value of type " + o.getClass().getName() + " to VectorValue"); } } @@ -613,8 +580,7 @@ private static DocumentReference convertDocumentReference(Object o, DeserializeC if (o instanceof DocumentReference) { return (DocumentReference) o; } else { - throw deserializeError( - context.errorPath, + throw context.errorPath.deserializeError( "Failed to convert value of type " + o.getClass().getName() + " to DocumentReference"); } } @@ -624,509 +590,13 @@ private static T convertBean(Object o, Class clazz, DeserializeContext co if (o instanceof Map) { return mapper.deserialize(expectMap(o, context), context); } else { - throw deserializeError( - context.errorPath, + throw context.errorPath.deserializeError( "Can't convert object of type " + o.getClass().getName() + " to type " + clazz.getName()); } } - private static IllegalArgumentException serializeError( - DeserializeContext.ErrorPath path, String reason) { - reason = "Could not serialize object. " + reason; - if (path.getLength() > 0) { - reason = reason + " (found in field '" + path.toString() + "')"; - } - return new IllegalArgumentException(reason); - } - - private static RuntimeException deserializeError( - DeserializeContext.ErrorPath path, String reason) { - reason = "Could not deserialize object. " + reason; - if (path.getLength() > 0) { - reason = reason + " (found in field '" + path.toString() + "')"; - } - return new RuntimeException(reason); - } - private static boolean isRecordType(Class cls) { Class parent = cls.getSuperclass(); return parent != null && "java.lang.Record".equals(parent.getName()); } - - // Helper class to convert from maps to custom objects (Beans), and vice versa. - private static class PojoBeanMapper extends BeanMapper { - private final Constructor constructor; - - // Case insensitive mapping of properties to their case sensitive versions - private final Map properties; - - // Below are maps to find getter/setter/field from a given property name. - // A property name is the name annotated by @PropertyName, if exists; or their property name - // following the Java Bean convention: field name is kept as-is while getters/setters will have - // their prefixes removed. See method propertyName for details. - private final Map getters; - private final Map setters; - private final Map fields; - - PojoBeanMapper(Class clazz) { - super(clazz); - properties = new HashMap<>(); - - setters = new HashMap<>(); - getters = new HashMap<>(); - fields = new HashMap<>(); - - Constructor constructor; - try { - constructor = clazz.getDeclaredConstructor(); - constructor.setAccessible(true); - } catch (NoSuchMethodException e) { - // We will only fail at deserialization time if no constructor is present - constructor = null; - } - this.constructor = constructor; - // Add any public getters to properties (including isXyz()) - for (Method method : clazz.getMethods()) { - if (shouldIncludeGetter(method)) { - String propertyName = propertyName(method); - addProperty(propertyName); - method.setAccessible(true); - if (getters.containsKey(propertyName)) { - throw new RuntimeException( - "Found conflicting getters for name " - + method.getName() - + " on class " - + clazz.getName()); - } - getters.put(propertyName, method); - applyGetterAnnotations(method); - } - } - - // Add any public fields to properties - for (Field field : clazz.getFields()) { - if (shouldIncludeField(field)) { - String propertyName = propertyName(field); - addProperty(propertyName); - applyFieldAnnotations(field); - } - } - - // We can use private setters and fields for known (public) properties/getters. Since - // getMethods/getFields only returns public methods/fields we need to traverse the - // class hierarchy to find the appropriate setter or field. - Class currentClass = clazz; - do { - // Add any setters - for (Method method : currentClass.getDeclaredMethods()) { - if (shouldIncludeSetter(method)) { - String propertyName = propertyName(method); - String existingPropertyName = properties.get(propertyName.toLowerCase(Locale.US)); - if (existingPropertyName != null) { - if (!existingPropertyName.equals(propertyName)) { - throw new RuntimeException( - "Found setter on " - + currentClass.getName() - + " with invalid case-sensitive name: " - + method.getName()); - } else { - Method existingSetter = setters.get(propertyName); - if (existingSetter == null) { - method.setAccessible(true); - setters.put(propertyName, method); - applySetterAnnotations(method); - } else if (!isSetterOverride(method, existingSetter)) { - // We require that setters with conflicting property names are - // overrides from a base class - if (currentClass == clazz) { - // TODO: Should we support overloads? - throw new RuntimeException( - "Class " - + clazz.getName() - + " has multiple setter overloads with name " - + method.getName()); - } else { - throw new RuntimeException( - "Found conflicting setters " - + "with name: " - + method.getName() - + " (conflicts with " - + existingSetter.getName() - + " defined on " - + existingSetter.getDeclaringClass().getName() - + ")"); - } - } - } - } - } - } - - for (Field field : currentClass.getDeclaredFields()) { - String propertyName = propertyName(field); - - // Case sensitivity is checked at deserialization time - // Fields are only added if they don't exist on a subclass - if (properties.containsKey(propertyName.toLowerCase(Locale.US)) - && !fields.containsKey(propertyName)) { - field.setAccessible(true); - fields.put(propertyName, field); - applyFieldAnnotations(field); - } - } - - // Traverse class hierarchy until we reach java.lang.Object which contains a bunch - // of fields/getters we don't want to serialize - currentClass = currentClass.getSuperclass(); - } while (currentClass != null && !currentClass.equals(Object.class)); - - if (properties.isEmpty()) { - throw new RuntimeException("No properties to serialize found on class " + clazz.getName()); - } - - // Make sure we can write to @DocumentId annotated properties before proceeding. - for (String docIdProperty : documentIdPropertyNames) { - if (!setters.containsKey(docIdProperty) && !fields.containsKey(docIdProperty)) { - throw new RuntimeException( - "@DocumentId is annotated on property " - + docIdProperty - + " of class " - + clazz.getName() - + " but no field or public setter was found"); - } - } - } - - private void addProperty(String property) { - String oldValue = properties.put(property.toLowerCase(Locale.US), property); - if (oldValue != null && !property.equals(oldValue)) { - throw new RuntimeException( - "Found two getters or fields with conflicting case " - + "sensitivity for property: " - + property.toLowerCase(Locale.US)); - } - } - - @Override - T deserialize( - Map values, - Map>, Type> types, - DeserializeContext context) { - if (constructor == null) { - throw deserializeError( - context.errorPath, - "Class " - + getClazz().getName() - + " does not define a no-argument constructor. If you are using ProGuard, make " - + "sure these constructors are not stripped"); - } - - T instance; - try { - instance = constructor.newInstance(); - } catch (InstantiationException | IllegalAccessException | InvocationTargetException e) { - throw new RuntimeException(e); - } - HashSet deserialzedProperties = new HashSet<>(); - for (Map.Entry entry : values.entrySet()) { - String propertyName = entry.getKey(); - DeserializeContext.ErrorPath childPath = context.errorPath.child(propertyName); - if (setters.containsKey(propertyName)) { - Method setter = setters.get(propertyName); - Type[] params = setter.getGenericParameterTypes(); - if (params.length != 1) { - throw deserializeError(childPath, "Setter does not have exactly one parameter"); - } - Type resolvedType = resolveType(params[0], types); - Object value = - CustomClassMapper.deserializeToType( - entry.getValue(), resolvedType, context.newInstanceWithErrorPath(childPath)); - try { - setter.invoke(instance, value); - } catch (IllegalAccessException | InvocationTargetException e) { - throw new RuntimeException(e); - } - deserialzedProperties.add(propertyName); - } else if (fields.containsKey(propertyName)) { - Field field = fields.get(propertyName); - Type resolvedType = resolveType(field.getGenericType(), types); - Object value = - CustomClassMapper.deserializeToType( - entry.getValue(), resolvedType, context.newInstanceWithErrorPath(childPath)); - try { - field.set(instance, value); - } catch (IllegalAccessException e) { - throw new RuntimeException(e); - } - deserialzedProperties.add(propertyName); - } else { - String message = - "No setter/field for " + propertyName + " found on class " + getClazz().getName(); - if (properties.containsKey(propertyName.toLowerCase(Locale.US))) { - message += " (fields/setters are case sensitive!)"; - } - if (isThrowOnUnknownProperties()) { - throw new RuntimeException(message); - } else if (isWarnOnUnknownProperties()) { - LOGGER.warning(message); - } - } - } - populateDocumentIdProperties(types, context, instance, deserialzedProperties); - - return instance; - } - - // Populate @DocumentId annotated fields. If there is a conflict (@DocumentId annotation is - // applied to a property that is already deserialized from the firestore document) - // a runtime exception will be thrown. - private void populateDocumentIdProperties( - Map>, Type> types, - DeserializeContext context, - T instance, - HashSet deserialzedProperties) { - for (String docIdPropertyName : documentIdPropertyNames) { - checkForDocIdConflict(docIdPropertyName, deserialzedProperties, context); - DeserializeContext.ErrorPath childPath = context.errorPath.child(docIdPropertyName); - if (setters.containsKey(docIdPropertyName)) { - Method setter = setters.get(docIdPropertyName); - Type[] params = setter.getGenericParameterTypes(); - if (params.length != 1) { - throw deserializeError(childPath, "Setter does not have exactly one parameter"); - } - Type resolvedType = resolveType(params[0], types); - try { - if (resolvedType == String.class) { - setter.invoke(instance, context.documentRef.getId()); - } else { - setter.invoke(instance, context.documentRef); - } - } catch (IllegalAccessException | InvocationTargetException e) { - throw new RuntimeException(e); - } - } else { - Field docIdField = fields.get(docIdPropertyName); - try { - if (docIdField.getType() == String.class) { - docIdField.set(instance, context.documentRef.getId()); - } else { - docIdField.set(instance, context.documentRef); - } - } catch (IllegalAccessException e) { - throw new RuntimeException(e); - } - } - } - } - - @Override - Map serialize(T object, DeserializeContext.ErrorPath path) { - verifyValidType(object); - Map result = new HashMap<>(); - for (String property : properties.values()) { - // Skip @DocumentId annotated properties; - if (documentIdPropertyNames.contains(property)) { - continue; - } - - Object propertyValue; - if (getters.containsKey(property)) { - Method getter = getters.get(property); - try { - propertyValue = getter.invoke(object); - } catch (IllegalAccessException | InvocationTargetException e) { - throw new RuntimeException(e); - } - } else { - // Must be a field - Field field = fields.get(property); - if (field == null) { - throw new IllegalStateException("Bean property without field or getter: " + property); - } - try { - propertyValue = field.get(object); - } catch (IllegalAccessException e) { - throw new RuntimeException(e); - } - } - - Object serializedValue; - if (serverTimestamps.contains(property) && propertyValue == null) { - // Replace null ServerTimestamp-annotated fields with the sentinel. - serializedValue = FieldValue.serverTimestamp(); - } else { - serializedValue = CustomClassMapper.serialize(propertyValue, path.child(property)); - } - result.put(property, serializedValue); - } - return result; - } - - private void applyGetterAnnotations(Method method) { - if (method.isAnnotationPresent(ServerTimestamp.class)) { - Class returnType = method.getReturnType(); - if (returnType != Date.class - && returnType != Timestamp.class - && returnType != Instant.class) { - throw new IllegalArgumentException( - "Method " - + method.getName() - + " is annotated with @ServerTimestamp but returns " - + returnType - + " instead of Date, Timestamp, or Instant."); - } - serverTimestamps.add(propertyName(method)); - } - - // Even though the value will be skipped, we still check for type matching for consistency. - if (method.isAnnotationPresent(DocumentId.class)) { - Class returnType = method.getReturnType(); - ensureValidDocumentIdType("Method", "returns", returnType); - documentIdPropertyNames.add(propertyName(method)); - } - } - - private void applySetterAnnotations(Method method) { - if (method.isAnnotationPresent(ServerTimestamp.class)) { - throw new IllegalArgumentException( - "Method " - + method.getName() - + " is annotated with @ServerTimestamp but should not be. @ServerTimestamp can" - + " only be applied to fields and getters, not setters."); - } - - if (method.isAnnotationPresent(DocumentId.class)) { - Class paramType = method.getParameterTypes()[0]; - ensureValidDocumentIdType("Method", "accepts", paramType); - documentIdPropertyNames.add(propertyName(method)); - } - } - - private static boolean shouldIncludeGetter(Method method) { - if (!method.getName().startsWith("get") && !method.getName().startsWith("is")) { - return false; - } - // Exclude methods from Object.class - if (method.getDeclaringClass().equals(Object.class)) { - return false; - } - // Non-public methods - if (!Modifier.isPublic(method.getModifiers())) { - return false; - } - // Static methods - if (Modifier.isStatic(method.getModifiers())) { - return false; - } - // No return type - if (method.getReturnType().equals(Void.TYPE)) { - return false; - } - // Non-zero parameters - if (method.getParameterTypes().length != 0) { - return false; - } - // Excluded methods - if (method.isAnnotationPresent(Exclude.class)) { - return false; - } - return true; - } - - private static boolean shouldIncludeSetter(Method method) { - if (!method.getName().startsWith("set")) { - return false; - } - // Exclude methods from Object.class - if (method.getDeclaringClass().equals(Object.class)) { - return false; - } - // Static methods - if (Modifier.isStatic(method.getModifiers())) { - return false; - } - // Has a return type - if (!method.getReturnType().equals(Void.TYPE)) { - return false; - } - // Methods without exactly one parameters - if (method.getParameterTypes().length != 1) { - return false; - } - // Excluded methods - if (method.isAnnotationPresent(Exclude.class)) { - return false; - } - return true; - } - - private static boolean shouldIncludeField(Field field) { - // Exclude methods from Object.class - if (field.getDeclaringClass().equals(Object.class)) { - return false; - } - // Non-public fields - if (!Modifier.isPublic(field.getModifiers())) { - return false; - } - // Static fields - if (Modifier.isStatic(field.getModifiers())) { - return false; - } - // Transient fields - if (Modifier.isTransient(field.getModifiers())) { - return false; - } - // Excluded fields - if (field.isAnnotationPresent(Exclude.class)) { - return false; - } - return true; - } - - private static boolean isSetterOverride(Method base, Method override) { - // We expect an overridden setter here - hardAssert( - base.getDeclaringClass().isAssignableFrom(override.getDeclaringClass()), - "Expected override from a base class"); - hardAssert(base.getReturnType().equals(Void.TYPE), "Expected void return type"); - hardAssert(override.getReturnType().equals(Void.TYPE), "Expected void return type"); - - Type[] baseParameterTypes = base.getParameterTypes(); - Type[] overrideParameterTypes = override.getParameterTypes(); - hardAssert(baseParameterTypes.length == 1, "Expected exactly one parameter"); - hardAssert(overrideParameterTypes.length == 1, "Expected exactly one parameter"); - - return base.getName().equals(override.getName()) - && baseParameterTypes[0].equals(overrideParameterTypes[0]); - } - - private static String propertyName(Method method) { - String annotatedName = annotatedName(method); - return annotatedName != null ? annotatedName : serializedName(method.getName()); - } - - private static String serializedName(String methodName) { - String[] prefixes = new String[] {"get", "set", "is"}; - String methodPrefix = null; - for (String prefix : prefixes) { - if (methodName.startsWith(prefix)) { - methodPrefix = prefix; - } - } - if (methodPrefix == null) { - throw new IllegalArgumentException("Unknown Bean prefix for method: " + methodName); - } - String strippedName = methodName.substring(methodPrefix.length()); - - // Make sure the first word or upper-case prefix is converted to lower-case - char[] chars = strippedName.toCharArray(); - int pos = 0; - while (pos < chars.length && Character.isUpperCase(chars[pos])) { - chars[pos] = Character.toLowerCase(chars[pos]); - pos++; - } - return new String(chars); - } - } } diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/DeserializeContext.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/DeserializeContext.java index 6609fb50e..62f2d64be 100644 --- a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/DeserializeContext.java +++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/DeserializeContext.java @@ -16,6 +16,8 @@ package com.google.cloud.firestore; +import com.google.cloud.firestore.annotation.DocumentId; + /** Holds information a deserialization operation needs to complete the job. */ class DeserializeContext { /** @@ -54,6 +56,22 @@ public String toString() { int getLength() { return length; } + + IllegalArgumentException serializeError(String reason) { + reason = "Could not serialize object. " + reason; + if (getLength() > 0) { + reason = reason + " (found in field '" + toString() + "')"; + } + return new IllegalArgumentException(reason); + } + + RuntimeException deserializeError(String reason) { + reason = "Could not deserialize object. " + reason; + if (getLength() > 0) { + reason = reason + " (found in field '" + toString() + "')"; + } + return new RuntimeException(reason); + } } /** Current path to the field being deserialized, used for better error messages. */ diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/PojoBeanMapper.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/PojoBeanMapper.java new file mode 100644 index 000000000..48ee0b8a5 --- /dev/null +++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/PojoBeanMapper.java @@ -0,0 +1,515 @@ +/* + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.cloud.firestore; + +import com.google.cloud.Timestamp; +import com.google.cloud.firestore.annotation.DocumentId; +import com.google.cloud.firestore.annotation.Exclude; +import com.google.cloud.firestore.annotation.ServerTimestamp; +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.lang.reflect.Type; +import java.lang.reflect.TypeVariable; +import java.time.Instant; +import java.util.Date; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Locale; +import java.util.Map; +import java.util.logging.Logger; + +// Helper class to convert from maps to custom objects (Beans), and vice versa. +class PojoBeanMapper extends BeanMapper { + private static final Logger LOGGER = Logger.getLogger(PojoBeanMapper.class.getName()); + + private final Constructor constructor; + + // Case insensitive mapping of properties to their case sensitive versions + private final Map properties; + + // Below are maps to find getter/setter/field from a given property name. + // A property name is the name annotated by @PropertyName, if exists; or their property name + // following the Java Bean convention: field name is kept as-is while getters/setters will have + // their prefixes removed. See method propertyName for details. + private final Map getters; + private final Map setters; + private final Map fields; + + PojoBeanMapper(Class clazz) { + super(clazz); + properties = new HashMap<>(); + + setters = new HashMap<>(); + getters = new HashMap<>(); + fields = new HashMap<>(); + + Constructor constructor; + try { + constructor = clazz.getDeclaredConstructor(); + constructor.setAccessible(true); + } catch (NoSuchMethodException e) { + // We will only fail at deserialization time if no constructor is present + constructor = null; + } + this.constructor = constructor; + // Add any public getters to properties (including isXyz()) + for (Method method : clazz.getMethods()) { + if (shouldIncludeGetter(method)) { + String propertyName = propertyName(method); + addProperty(propertyName); + method.setAccessible(true); + if (getters.containsKey(propertyName)) { + throw new RuntimeException( + "Found conflicting getters for name " + + method.getName() + + " on class " + + clazz.getName()); + } + getters.put(propertyName, method); + applyGetterAnnotations(method); + } + } + + // Add any public fields to properties + for (Field field : clazz.getFields()) { + if (shouldIncludeField(field)) { + String propertyName = propertyName(field); + addProperty(propertyName); + applyFieldAnnotations(field); + } + } + + // We can use private setters and fields for known (public) properties/getters. Since + // getMethods/getFields only returns public methods/fields we need to traverse the + // class hierarchy to find the appropriate setter or field. + Class currentClass = clazz; + do { + // Add any setters + for (Method method : currentClass.getDeclaredMethods()) { + if (shouldIncludeSetter(method)) { + String propertyName = propertyName(method); + String existingPropertyName = properties.get(propertyName.toLowerCase(Locale.US)); + if (existingPropertyName != null) { + if (!existingPropertyName.equals(propertyName)) { + throw new RuntimeException( + "Found setter on " + + currentClass.getName() + + " with invalid case-sensitive name: " + + method.getName()); + } else { + Method existingSetter = setters.get(propertyName); + if (existingSetter == null) { + method.setAccessible(true); + setters.put(propertyName, method); + applySetterAnnotations(method); + } else if (!isSetterOverride(method, existingSetter)) { + // We require that setters with conflicting property names are + // overrides from a base class + if (currentClass == clazz) { + // TODO: Should we support overloads? + throw new RuntimeException( + "Class " + + clazz.getName() + + " has multiple setter overloads with name " + + method.getName()); + } else { + throw new RuntimeException( + "Found conflicting setters " + + "with name: " + + method.getName() + + " (conflicts with " + + existingSetter.getName() + + " defined on " + + existingSetter.getDeclaringClass().getName() + + ")"); + } + } + } + } + } + } + + for (Field field : currentClass.getDeclaredFields()) { + String propertyName = propertyName(field); + + // Case sensitivity is checked at deserialization time + // Fields are only added if they don't exist on a subclass + if (properties.containsKey(propertyName.toLowerCase(Locale.US)) + && !fields.containsKey(propertyName)) { + field.setAccessible(true); + fields.put(propertyName, field); + applyFieldAnnotations(field); + } + } + + // Traverse class hierarchy until we reach java.lang.Object which contains a bunch + // of fields/getters we don't want to serialize + currentClass = currentClass.getSuperclass(); + } while (currentClass != null && !currentClass.equals(Object.class)); + + if (properties.isEmpty()) { + throw new RuntimeException("No properties to serialize found on class " + clazz.getName()); + } + + // Make sure we can write to @DocumentId annotated properties before proceeding. + for (String docIdProperty : documentIdPropertyNames) { + if (!setters.containsKey(docIdProperty) && !fields.containsKey(docIdProperty)) { + throw new RuntimeException( + "@DocumentId is annotated on property " + + docIdProperty + + " of class " + + clazz.getName() + + " but no field or public setter was found"); + } + } + } + + private void addProperty(String property) { + String oldValue = properties.put(property.toLowerCase(Locale.US), property); + if (oldValue != null && !property.equals(oldValue)) { + throw new RuntimeException( + "Found two getters or fields with conflicting case " + + "sensitivity for property: " + + property.toLowerCase(Locale.US)); + } + } + + @Override + Map serialize(T object, DeserializeContext.ErrorPath path) { + verifyValidType(object); + Map result = new HashMap<>(); + for (String property : properties.values()) { + // Skip @DocumentId annotated properties; + if (documentIdPropertyNames.contains(property)) { + continue; + } + + Object propertyValue; + if (getters.containsKey(property)) { + Method getter = getters.get(property); + try { + propertyValue = getter.invoke(object); + } catch (IllegalAccessException | InvocationTargetException e) { + throw new RuntimeException(e); + } + } else { + // Must be a field + Field field = fields.get(property); + if (field == null) { + throw new IllegalStateException("Bean property without field or getter: " + property); + } + try { + propertyValue = field.get(object); + } catch (IllegalAccessException e) { + throw new RuntimeException(e); + } + } + + Object serializedValue = getSerializedValue(property, propertyValue, path); + + result.put(property, serializedValue); + } + return result; + } + + @Override + T deserialize( + Map values, + Map>, Type> types, + DeserializeContext context) { + if (constructor == null) { + throw context.errorPath.deserializeError( + "Class " + + getClazz().getName() + + " does not define a no-argument constructor. If you are using ProGuard, make " + + "sure these constructors are not stripped"); + } + + T instance; + try { + instance = constructor.newInstance(); + } catch (InstantiationException | IllegalAccessException | InvocationTargetException e) { + throw new RuntimeException(e); + } + HashSet deserializedProperties = new HashSet<>(); + for (Map.Entry entry : values.entrySet()) { + String propertyName = entry.getKey(); + DeserializeContext.ErrorPath childPath = context.errorPath.child(propertyName); + if (setters.containsKey(propertyName)) { + Method setter = setters.get(propertyName); + Type[] params = setter.getGenericParameterTypes(); + if (params.length != 1) { + throw childPath.deserializeError("Setter does not have exactly one parameter"); + } + Type resolvedType = resolveType(params[0], types); + Object value = + CustomClassMapper.deserializeToType( + entry.getValue(), resolvedType, context.newInstanceWithErrorPath(childPath)); + try { + setter.invoke(instance, value); + } catch (IllegalAccessException | InvocationTargetException e) { + throw new RuntimeException(e); + } + deserializedProperties.add(propertyName); + } else if (fields.containsKey(propertyName)) { + Field field = fields.get(propertyName); + Type resolvedType = resolveType(field.getGenericType(), types); + Object value = + CustomClassMapper.deserializeToType( + entry.getValue(), resolvedType, context.newInstanceWithErrorPath(childPath)); + try { + field.set(instance, value); + } catch (IllegalAccessException e) { + throw new RuntimeException(e); + } + deserializedProperties.add(propertyName); + } else { + String message = + "No setter/field for " + propertyName + " found on class " + getClazz().getName(); + if (properties.containsKey(propertyName.toLowerCase(Locale.US))) { + message += " (fields/setters are case sensitive!)"; + } + if (isThrowOnUnknownProperties()) { + throw new RuntimeException(message); + } else if (isWarnOnUnknownProperties()) { + LOGGER.warning(message); + } + } + } + populateDocumentIdProperties(types, context, instance, deserializedProperties); + + return instance; + } + + // Populate @DocumentId annotated fields. If there is a conflict (@DocumentId annotation is + // applied to a property that is already deserialized from the firestore document) + // a runtime exception will be thrown. + private void populateDocumentIdProperties( + Map>, Type> types, + DeserializeContext context, + T instance, + HashSet deserializedProperties) { + for (String docIdPropertyName : documentIdPropertyNames) { + checkForDocIdConflict(docIdPropertyName, deserializedProperties, context); + DeserializeContext.ErrorPath childPath = context.errorPath.child(docIdPropertyName); + if (setters.containsKey(docIdPropertyName)) { + Method setter = setters.get(docIdPropertyName); + Type[] params = setter.getGenericParameterTypes(); + if (params.length != 1) { + throw childPath.deserializeError("Setter does not have exactly one parameter"); + } + Type resolvedType = resolveType(params[0], types); + try { + if (resolvedType == String.class) { + setter.invoke(instance, context.documentRef.getId()); + } else { + setter.invoke(instance, context.documentRef); + } + } catch (IllegalAccessException | InvocationTargetException e) { + throw new RuntimeException(e); + } + } else { + Field docIdField = fields.get(docIdPropertyName); + try { + if (docIdField.getType() == String.class) { + docIdField.set(instance, context.documentRef.getId()); + } else { + docIdField.set(instance, context.documentRef); + } + } catch (IllegalAccessException e) { + throw new RuntimeException(e); + } + } + } + } + + private void applyGetterAnnotations(Method method) { + if (method.isAnnotationPresent(ServerTimestamp.class)) { + Class returnType = method.getReturnType(); + if (returnType != Date.class + && returnType != Timestamp.class + && returnType != Instant.class) { + throw new IllegalArgumentException( + "Method " + + method.getName() + + " is annotated with @ServerTimestamp but returns " + + returnType + + " instead of Date, Timestamp, or Instant."); + } + serverTimestamps.add(propertyName(method)); + } + + // Even though the value will be skipped, we still check for type matching for consistency. + if (method.isAnnotationPresent(DocumentId.class)) { + Class returnType = method.getReturnType(); + ensureValidDocumentIdType("Method", "returns", returnType); + documentIdPropertyNames.add(propertyName(method)); + } + } + + private void applySetterAnnotations(Method method) { + if (method.isAnnotationPresent(ServerTimestamp.class)) { + throw new IllegalArgumentException( + "Method " + + method.getName() + + " is annotated with @ServerTimestamp but should not be. @ServerTimestamp can" + + " only be applied to fields and getters, not setters."); + } + + if (method.isAnnotationPresent(DocumentId.class)) { + Class paramType = method.getParameterTypes()[0]; + ensureValidDocumentIdType("Method", "accepts", paramType); + documentIdPropertyNames.add(propertyName(method)); + } + } + + private static boolean shouldIncludeGetter(Method method) { + if (!method.getName().startsWith("get") && !method.getName().startsWith("is")) { + return false; + } + // Exclude methods from Object.class + if (method.getDeclaringClass().equals(Object.class)) { + return false; + } + // Non-public methods + if (!Modifier.isPublic(method.getModifiers())) { + return false; + } + // Static methods + if (Modifier.isStatic(method.getModifiers())) { + return false; + } + // No return type + if (method.getReturnType().equals(Void.TYPE)) { + return false; + } + // Non-zero parameters + if (method.getParameterTypes().length != 0) { + return false; + } + // Excluded methods + if (method.isAnnotationPresent(Exclude.class)) { + return false; + } + return true; + } + + private static boolean shouldIncludeSetter(Method method) { + if (!method.getName().startsWith("set")) { + return false; + } + // Exclude methods from Object.class + if (method.getDeclaringClass().equals(Object.class)) { + return false; + } + // Static methods + if (Modifier.isStatic(method.getModifiers())) { + return false; + } + // Has a return type + if (!method.getReturnType().equals(Void.TYPE)) { + return false; + } + // Methods without exactly one parameters + if (method.getParameterTypes().length != 1) { + return false; + } + // Excluded methods + if (method.isAnnotationPresent(Exclude.class)) { + return false; + } + return true; + } + + private static boolean shouldIncludeField(Field field) { + // Exclude methods from Object.class + if (field.getDeclaringClass().equals(Object.class)) { + return false; + } + // Non-public fields + if (!Modifier.isPublic(field.getModifiers())) { + return false; + } + // Static fields + if (Modifier.isStatic(field.getModifiers())) { + return false; + } + // Transient fields + if (Modifier.isTransient(field.getModifiers())) { + return false; + } + // Excluded fields + if (field.isAnnotationPresent(Exclude.class)) { + return false; + } + return true; + } + + private static void hardAssert(boolean assertion, String message) { + if (!assertion) { + throw new RuntimeException("Hard assert failed: " + message); + } + } + + private static boolean isSetterOverride(Method base, Method override) { + // We expect an overridden setter here + hardAssert( + base.getDeclaringClass().isAssignableFrom(override.getDeclaringClass()), + "Expected override from a base class"); + hardAssert(base.getReturnType().equals(Void.TYPE), "Expected void return type"); + hardAssert(override.getReturnType().equals(Void.TYPE), "Expected void return type"); + + Type[] baseParameterTypes = base.getParameterTypes(); + Type[] overrideParameterTypes = override.getParameterTypes(); + hardAssert(baseParameterTypes.length == 1, "Expected exactly one parameter"); + hardAssert(overrideParameterTypes.length == 1, "Expected exactly one parameter"); + + return base.getName().equals(override.getName()) + && baseParameterTypes[0].equals(overrideParameterTypes[0]); + } + + private static String propertyName(Method method) { + String annotatedName = annotatedName(method); + return annotatedName != null ? annotatedName : serializedName(method.getName()); + } + + private static String serializedName(String methodName) { + String[] prefixes = new String[] {"get", "set", "is"}; + String methodPrefix = null; + for (String prefix : prefixes) { + if (methodName.startsWith(prefix)) { + methodPrefix = prefix; + } + } + if (methodPrefix == null) { + throw new IllegalArgumentException("Unknown Bean prefix for method: " + methodName); + } + String strippedName = methodName.substring(methodPrefix.length()); + + // Make sure the first word or upper-case prefix is converted to lower-case + char[] chars = strippedName.toCharArray(); + int pos = 0; + while (pos < chars.length && Character.isUpperCase(chars[pos])) { + chars[pos] = Character.toLowerCase(chars[pos]); + pos++; + } + return new String(chars); + } +} diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/RecordMapper.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/RecordMapper.java index 86e88990d..9e1a84c35 100644 --- a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/RecordMapper.java +++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/RecordMapper.java @@ -35,8 +35,6 @@ * exclusion of fields is not supported. Supports DocumentId, PropertyName, and ServerTimestamp * annotations on record components. Since records are not supported in JDK versions < 16, * reflection is used for inspecting record metadata. - * - * @author Eran Leshem */ class RecordMapper extends BeanMapper { private static final Logger LOGGER = Logger.getLogger(RecordMapper.class.getName()); @@ -91,13 +89,8 @@ Map serialize(T object, DeserializeContext.ErrorPath path) { throw new RuntimeException(e); } - Object serializedValue; - if (serverTimestamps.contains(property) && propertyValue == null) { - // Replace null ServerTimestamp-annotated fields with the sentinel. - serializedValue = FieldValue.serverTimestamp(); - } else { - serializedValue = CustomClassMapper.serialize(propertyValue, path.child(property)); - } + Object serializedValue = getSerializedValue(property, propertyValue, path); + result.put(property, serializedValue); } return result; @@ -108,6 +101,13 @@ T deserialize( Map values, Map>, Type> types, DeserializeContext context) { + if (constructor == null) { + throw context.errorPath.deserializeError( + "Class " + + getClazz().getName() + + " does not define a no-argument constructor. If you are using ProGuard, make " + + "sure these constructors are not stripped"); + } Object[] constructorParams = new Object[constructor.getParameterCount()]; Set deserializedProperties = new HashSet<>(values.size()); for (Map.Entry entry : values.entrySet()) { @@ -127,7 +127,6 @@ T deserialize( if (isThrowOnUnknownProperties()) { throw new RuntimeException(message); } - if (isWarnOnUnknownProperties()) { LOGGER.warning(message); } @@ -150,9 +149,9 @@ private void populateDocumentIdProperties( Map>, Type> types, DeserializeContext context, Object[] params, - Set deserialzedProperties) { + Set deserializedProperties) { for (String docIdPropertyName : documentIdPropertyNames) { - checkForDocIdConflict(docIdPropertyName, deserialzedProperties, context); + checkForDocIdConflict(docIdPropertyName, deserializedProperties, context); if (accessors.containsKey(docIdPropertyName)) { Object id; From 0af8a9e4c41632447c139b3a96c1d1ac5a77efce Mon Sep 17 00:00:00 2001 From: milaGGL <107142260+milaGGL@users.noreply.github.com> Date: Thu, 12 Sep 2024 17:57:33 -0400 Subject: [PATCH 46/51] relocate serialization/deserialization files --- .../cloud/firestore/CollectionReference.java | 1 + .../google/cloud/firestore/DocumentSnapshot.java | 1 + .../com/google/cloud/firestore/FieldValue.java | 1 + .../com/google/cloud/firestore/Internal.java | 1 + .../java/com/google/cloud/firestore/Query.java | 1 + .../google/cloud/firestore/QuerySnapshot.java | 1 + .../google/cloud/firestore/UpdateBuilder.java | 1 + .../firestore/{ => encoding}/BeanMapper.java | 4 +++- .../{ => encoding}/CustomClassMapper.java | 16 +++++++++++----- .../{ => encoding}/DeserializeContext.java | 3 ++- .../firestore/{ => encoding}/PojoBeanMapper.java | 2 +- .../firestore/{ => encoding}/RecordMapper.java | 2 +- .../google/cloud/firestore/RecordMapperTest.java | 1 + .../com/google/cloud/firestore/MapperTest.java | 1 + .../com/google/cloud/firestore/ToStringTest.java | 1 + 15 files changed, 28 insertions(+), 9 deletions(-) rename google-cloud-firestore/src/main/java/com/google/cloud/firestore/{ => encoding}/BeanMapper.java (97%) rename google-cloud-firestore/src/main/java/com/google/cloud/firestore/{ => encoding}/CustomClassMapper.java (97%) rename google-cloud-firestore/src/main/java/com/google/cloud/firestore/{ => encoding}/DeserializeContext.java (96%) rename google-cloud-firestore/src/main/java/com/google/cloud/firestore/{ => encoding}/PojoBeanMapper.java (99%) rename google-cloud-firestore/src/main/java/com/google/cloud/firestore/{ => encoding}/RecordMapper.java (99%) diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/CollectionReference.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/CollectionReference.java index c736d7028..e1c2841d6 100644 --- a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/CollectionReference.java +++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/CollectionReference.java @@ -22,6 +22,7 @@ import com.google.api.gax.rpc.ApiException; import com.google.api.gax.rpc.ApiExceptions; import com.google.api.gax.rpc.UnaryCallable; +import com.google.cloud.firestore.encoding.CustomClassMapper; import com.google.cloud.firestore.spi.v1.FirestoreRpc; import com.google.cloud.firestore.telemetry.TraceUtil; import com.google.cloud.firestore.telemetry.TraceUtil.Scope; diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/DocumentSnapshot.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/DocumentSnapshot.java index 3f27040ad..e1aab1cac 100644 --- a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/DocumentSnapshot.java +++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/DocumentSnapshot.java @@ -19,6 +19,7 @@ import com.google.api.core.InternalExtensionOnly; import com.google.cloud.Timestamp; import com.google.cloud.firestore.UserDataConverter.EncodingOptions; +import com.google.cloud.firestore.encoding.CustomClassMapper; import com.google.common.base.Preconditions; import com.google.firestore.v1.Document; import com.google.firestore.v1.Value; diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/FieldValue.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/FieldValue.java index 5f9e406da..2cfc41acf 100644 --- a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/FieldValue.java +++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/FieldValue.java @@ -16,6 +16,7 @@ package com.google.cloud.firestore; +import com.google.cloud.firestore.encoding.CustomClassMapper; import com.google.common.base.Preconditions; import com.google.firestore.v1.ArrayValue; import com.google.firestore.v1.DocumentTransform.FieldTransform; diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/Internal.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/Internal.java index 46f16f5f3..b25701075 100644 --- a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/Internal.java +++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/Internal.java @@ -20,6 +20,7 @@ import com.google.api.core.InternalApi; import com.google.cloud.Timestamp; +import com.google.cloud.firestore.encoding.CustomClassMapper; import com.google.cloud.firestore.spi.v1.FirestoreRpc; import com.google.common.base.Preconditions; import com.google.firestore.v1.Document; diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/Query.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/Query.java index 4721ba93d..a794b6a63 100644 --- a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/Query.java +++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/Query.java @@ -39,6 +39,7 @@ import com.google.auto.value.AutoValue; import com.google.cloud.Timestamp; import com.google.cloud.firestore.Query.QueryOptions.Builder; +import com.google.cloud.firestore.encoding.CustomClassMapper; import com.google.cloud.firestore.telemetry.TraceUtil; import com.google.cloud.firestore.telemetry.TraceUtil.Scope; import com.google.cloud.firestore.v1.FirestoreSettings; diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/QuerySnapshot.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/QuerySnapshot.java index 4f51cd583..494a298e4 100644 --- a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/QuerySnapshot.java +++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/QuerySnapshot.java @@ -18,6 +18,7 @@ import com.google.cloud.Timestamp; import com.google.cloud.firestore.DocumentChange.Type; +import com.google.cloud.firestore.encoding.CustomClassMapper; import com.google.common.collect.ImmutableList; import java.util.ArrayList; import java.util.Collections; diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/UpdateBuilder.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/UpdateBuilder.java index e93fe8310..31434667b 100644 --- a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/UpdateBuilder.java +++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/UpdateBuilder.java @@ -25,6 +25,7 @@ import com.google.api.core.ApiFutures; import com.google.api.core.InternalExtensionOnly; import com.google.cloud.firestore.UserDataConverter.EncodingOptions; +import com.google.cloud.firestore.encoding.CustomClassMapper; import com.google.cloud.firestore.telemetry.TraceUtil; import com.google.cloud.firestore.telemetry.TraceUtil.Scope; import com.google.common.base.Preconditions; diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/BeanMapper.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/encoding/BeanMapper.java similarity index 97% rename from google-cloud-firestore/src/main/java/com/google/cloud/firestore/BeanMapper.java rename to google-cloud-firestore/src/main/java/com/google/cloud/firestore/encoding/BeanMapper.java index 5e50d51ce..3d57aac3a 100644 --- a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/BeanMapper.java +++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/encoding/BeanMapper.java @@ -14,9 +14,11 @@ * limitations under the License. */ -package com.google.cloud.firestore; +package com.google.cloud.firestore.encoding; import com.google.cloud.Timestamp; +import com.google.cloud.firestore.DocumentReference; +import com.google.cloud.firestore.FieldValue; import com.google.cloud.firestore.annotation.DocumentId; import com.google.cloud.firestore.annotation.IgnoreExtraProperties; import com.google.cloud.firestore.annotation.PropertyName; 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/encoding/CustomClassMapper.java similarity index 97% rename from google-cloud-firestore/src/main/java/com/google/cloud/firestore/CustomClassMapper.java rename to google-cloud-firestore/src/main/java/com/google/cloud/firestore/encoding/CustomClassMapper.java index 95dc9872b..6a732d905 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/encoding/CustomClassMapper.java @@ -14,9 +14,14 @@ * limitations under the License. */ -package com.google.cloud.firestore; +package com.google.cloud.firestore.encoding; import com.google.cloud.Timestamp; +import com.google.cloud.firestore.Blob; +import com.google.cloud.firestore.DocumentReference; +import com.google.cloud.firestore.FieldValue; +import com.google.cloud.firestore.GeoPoint; +import com.google.cloud.firestore.VectorValue; import com.google.cloud.firestore.annotation.DocumentId; import com.google.firestore.v1.Value; import java.lang.reflect.Field; @@ -38,7 +43,7 @@ import java.util.concurrent.ConcurrentMap; /** Helper class to convert to/from custom POJO classes and plain Java types. */ -class CustomClassMapper { +public class CustomClassMapper { /** Maximum depth before we give up and assume it's a recursive object graph. */ private static final int MAX_DEPTH = 500; @@ -62,7 +67,7 @@ private static void hardAssert(boolean assertion, String message) { * @param object The representation of the JSON data * @return JSON representation containing only standard library Java types */ - static Object convertToPlainJavaTypes(Object object) { + public static Object convertToPlainJavaTypes(Object object) { return serialize(object); } @@ -83,12 +88,13 @@ public static Map convertToPlainJavaTypes(Map update) * @param docRef The value to set to {@link DocumentId} annotated fields in the custom class. * @return The POJO object. */ - static T convertToCustomClass(Object object, Class clazz, DocumentReference docRef) { + public static T convertToCustomClass( + Object object, Class clazz, DocumentReference docRef) { return deserializeToClass( object, clazz, new DeserializeContext(DeserializeContext.ErrorPath.EMPTY, docRef)); } - static Object serialize(T o) { + public static Object serialize(T o) { return serialize(o, DeserializeContext.ErrorPath.EMPTY); } diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/DeserializeContext.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/encoding/DeserializeContext.java similarity index 96% rename from google-cloud-firestore/src/main/java/com/google/cloud/firestore/DeserializeContext.java rename to google-cloud-firestore/src/main/java/com/google/cloud/firestore/encoding/DeserializeContext.java index 62f2d64be..df85f5817 100644 --- a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/DeserializeContext.java +++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/encoding/DeserializeContext.java @@ -14,8 +14,9 @@ * limitations under the License. */ -package com.google.cloud.firestore; +package com.google.cloud.firestore.encoding; +import com.google.cloud.firestore.DocumentReference; import com.google.cloud.firestore.annotation.DocumentId; /** Holds information a deserialization operation needs to complete the job. */ diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/PojoBeanMapper.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/encoding/PojoBeanMapper.java similarity index 99% rename from google-cloud-firestore/src/main/java/com/google/cloud/firestore/PojoBeanMapper.java rename to google-cloud-firestore/src/main/java/com/google/cloud/firestore/encoding/PojoBeanMapper.java index 48ee0b8a5..e3f97e06f 100644 --- a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/PojoBeanMapper.java +++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/encoding/PojoBeanMapper.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.google.cloud.firestore; +package com.google.cloud.firestore.encoding; import com.google.cloud.Timestamp; import com.google.cloud.firestore.annotation.DocumentId; diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/RecordMapper.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/encoding/RecordMapper.java similarity index 99% rename from google-cloud-firestore/src/main/java/com/google/cloud/firestore/RecordMapper.java rename to google-cloud-firestore/src/main/java/com/google/cloud/firestore/encoding/RecordMapper.java index 9e1a84c35..b48501ce5 100644 --- a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/RecordMapper.java +++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/encoding/RecordMapper.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.google.cloud.firestore; +package com.google.cloud.firestore.encoding; import java.lang.reflect.AnnotatedElement; import java.lang.reflect.Constructor; diff --git a/google-cloud-firestore/src/test-jdk17/java/com/google/cloud/firestore/RecordMapperTest.java b/google-cloud-firestore/src/test-jdk17/java/com/google/cloud/firestore/RecordMapperTest.java index 9d876e133..daa5df99d 100644 --- a/google-cloud-firestore/src/test-jdk17/java/com/google/cloud/firestore/RecordMapperTest.java +++ b/google-cloud-firestore/src/test-jdk17/java/com/google/cloud/firestore/RecordMapperTest.java @@ -19,6 +19,7 @@ import com.google.cloud.firestore.annotation.DocumentId; import com.google.cloud.firestore.annotation.PropertyName; import com.google.cloud.firestore.annotation.ThrowOnExtraProperties; +import com.google.cloud.firestore.encoding.CustomClassMapper; import com.google.cloud.firestore.spi.v1.FirestoreRpc; import com.google.common.collect.ImmutableList; import com.google.firestore.v1.DatabaseRootName; diff --git a/google-cloud-firestore/src/test/java/com/google/cloud/firestore/MapperTest.java b/google-cloud-firestore/src/test/java/com/google/cloud/firestore/MapperTest.java index b8508b787..cf5a55c4e 100644 --- a/google-cloud-firestore/src/test/java/com/google/cloud/firestore/MapperTest.java +++ b/google-cloud-firestore/src/test/java/com/google/cloud/firestore/MapperTest.java @@ -29,6 +29,7 @@ import com.google.cloud.firestore.annotation.Exclude; import com.google.cloud.firestore.annotation.PropertyName; import com.google.cloud.firestore.annotation.ThrowOnExtraProperties; +import com.google.cloud.firestore.encoding.CustomClassMapper; import com.google.cloud.firestore.spi.v1.FirestoreRpc; import com.google.common.collect.ImmutableList; import com.google.firestore.v1.DatabaseRootName; diff --git a/google-cloud-firestore/src/test/java/com/google/cloud/firestore/ToStringTest.java b/google-cloud-firestore/src/test/java/com/google/cloud/firestore/ToStringTest.java index 6779edd18..8b5a7e64e 100644 --- a/google-cloud-firestore/src/test/java/com/google/cloud/firestore/ToStringTest.java +++ b/google-cloud-firestore/src/test/java/com/google/cloud/firestore/ToStringTest.java @@ -19,6 +19,7 @@ import static com.google.common.truth.Truth.assertThat; import com.google.cloud.Timestamp; +import com.google.cloud.firestore.encoding.CustomClassMapper; import com.google.cloud.firestore.spi.v1.FirestoreRpc; import com.google.firestore.v1.Value; import java.util.Collections; From 5c94936457e16c94e28610b59ad2afd953b8ece0 Mon Sep 17 00:00:00 2001 From: milaGGL <107142260+milaGGL@users.noreply.github.com> Date: Thu, 12 Sep 2024 19:04:17 -0400 Subject: [PATCH 47/51] mark @InternalApi, move shared code to Util file, --- .../cloud/firestore/encoding/BeanMapper.java | 35 +- .../firestore/encoding/CustomClassMapper.java | 344 ++------------ .../firestore/encoding/EncodingUtil.java | 434 ++++++++++++++++++ .../firestore/encoding/PojoBeanMapper.java | 161 +------ .../firestore/encoding/RecordMapper.java | 2 +- 5 files changed, 502 insertions(+), 474 deletions(-) create mode 100644 google-cloud-firestore/src/main/java/com/google/cloud/firestore/encoding/EncodingUtil.java diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/encoding/BeanMapper.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/encoding/BeanMapper.java index 3d57aac3a..3eb51c8ee 100644 --- a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/encoding/BeanMapper.java +++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/encoding/BeanMapper.java @@ -17,14 +17,11 @@ package com.google.cloud.firestore.encoding; import com.google.cloud.Timestamp; -import com.google.cloud.firestore.DocumentReference; import com.google.cloud.firestore.FieldValue; import com.google.cloud.firestore.annotation.DocumentId; import com.google.cloud.firestore.annotation.IgnoreExtraProperties; -import com.google.cloud.firestore.annotation.PropertyName; import com.google.cloud.firestore.annotation.ServerTimestamp; import com.google.cloud.firestore.annotation.ThrowOnExtraProperties; -import java.lang.reflect.AccessibleObject; import java.lang.reflect.Field; import java.lang.reflect.Type; import java.lang.reflect.TypeVariable; @@ -93,18 +90,6 @@ abstract T deserialize( Map>, Type> types, DeserializeContext context); - void ensureValidDocumentIdType(String fieldDescription, String operation, Type type) { - if (type != String.class && type != DocumentReference.class) { - throw new IllegalArgumentException( - fieldDescription - + " is annotated with @DocumentId but " - + operation - + " " - + type - + " instead of String or DocumentReference."); - } - } - void verifyValidType(T object) { if (!clazz.isAssignableFrom(object.getClass())) { throw new IllegalArgumentException( @@ -159,28 +144,14 @@ void applyFieldAnnotations(Field field) { + fieldType + " instead of Date, Timestamp, or Instant."); } - serverTimestamps.add(propertyName(field)); + serverTimestamps.add(EncodingUtil.propertyName(field)); } if (field.isAnnotationPresent(DocumentId.class)) { Class fieldType = field.getType(); - ensureValidDocumentIdType("Field", "is", fieldType); - documentIdPropertyNames.add(propertyName(field)); - } - } - - static String propertyName(Field field) { - String annotatedName = annotatedName(field); - return annotatedName != null ? annotatedName : field.getName(); - } - - static String annotatedName(AccessibleObject obj) { - if (obj.isAnnotationPresent(PropertyName.class)) { - PropertyName annotation = obj.getAnnotation(PropertyName.class); - return annotation.value(); + EncodingUtil.ensureValidDocumentIdType("Field", "is", fieldType); + documentIdPropertyNames.add(EncodingUtil.propertyName(field)); } - - return null; } Object getSerializedValue( diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/encoding/CustomClassMapper.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/encoding/CustomClassMapper.java index 6a732d905..4414251d8 100644 --- a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/encoding/CustomClassMapper.java +++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/encoding/CustomClassMapper.java @@ -16,6 +16,7 @@ package com.google.cloud.firestore.encoding; +import com.google.api.core.InternalApi; import com.google.cloud.Timestamp; import com.google.cloud.firestore.Blob; import com.google.cloud.firestore.DocumentReference; @@ -43,6 +44,7 @@ import java.util.concurrent.ConcurrentMap; /** Helper class to convert to/from custom POJO classes and plain Java types. */ +@InternalApi public class CustomClassMapper { /** Maximum depth before we give up and assume it's a recursive object graph. */ @@ -50,16 +52,6 @@ public class CustomClassMapper { private static final ConcurrentMap, BeanMapper> mappers = new ConcurrentHashMap<>(); - private static void hardAssert(boolean assertion) { - hardAssert(assertion, "Internal inconsistency"); - } - - private static void hardAssert(boolean assertion, String message) { - if (!assertion) { - throw new RuntimeException("Hard assert failed: " + message); - } - } - /** * Converts a Java representation of JSON data to standard library Java data types: Map, Array, * String, Double, Integer and Boolean. POJOs are converted to Java Maps. @@ -73,7 +65,7 @@ public static Object convertToPlainJavaTypes(Object object) { public static Map convertToPlainJavaTypes(Map update) { Object converted = serialize(update); - hardAssert(converted instanceof Map); + EncodingUtil.hardAssert(converted instanceof Map); @SuppressWarnings("unchecked") Map convertedMap = (Map) converted; return convertedMap; @@ -155,7 +147,7 @@ static Object serialize(T o, DeserializeContext.ErrorPath path) { String enumName = ((Enum) o).name(); try { Field enumField = o.getClass().getField(enumName); - return BeanMapper.propertyName(enumField); + return EncodingUtil.propertyName(enumField); } catch (NoSuchFieldException ex) { return enumName; } @@ -198,12 +190,13 @@ static T deserializeToType(Object o, Type type, DeserializeContext context) // that this array always has at least one element, since the unbounded wildcard always // has at least an upper bound of Object. Type[] upperBounds = ((WildcardType) type).getUpperBounds(); - hardAssert(upperBounds.length > 0, "Unexpected type bounds on wildcard " + type); + EncodingUtil.hardAssert(upperBounds.length > 0, "Unexpected type bounds on wildcard " + type); return deserializeToType(o, upperBounds[0], context); } else if (type instanceof TypeVariable) { // As above, TypeVariables always have at least one upper bound of Object. Type[] upperBounds = ((TypeVariable) type).getBounds(); - hardAssert(upperBounds.length > 0, "Unexpected type bounds on type variable " + type); + EncodingUtil.hardAssert( + upperBounds.length > 0, "Unexpected type bounds on type variable " + type); return deserializeToType(o, upperBounds[0], context); } else if (type instanceof GenericArrayType) { @@ -214,48 +207,6 @@ static T deserializeToType(Object o, Type type, DeserializeContext context) } } - @SuppressWarnings("unchecked") - private static T deserializeToClass(Object o, Class clazz, DeserializeContext context) { - if (o == null) { - return null; - } else if (clazz.isPrimitive() - || Number.class.isAssignableFrom(clazz) - || Boolean.class.isAssignableFrom(clazz) - || Character.class.isAssignableFrom(clazz)) { - return deserializeToPrimitive(o, clazz, context); - } else if (String.class.isAssignableFrom(clazz)) { - return (T) convertString(o, context); - } else if (Date.class.isAssignableFrom(clazz)) { - return (T) convertDate(o, context); - } else if (Timestamp.class.isAssignableFrom(clazz)) { - return (T) convertTimestamp(o, context); - } else if (Instant.class.isAssignableFrom(clazz)) { - return (T) convertInstant(o, context); - } else if (Blob.class.isAssignableFrom(clazz)) { - return (T) convertBlob(o, context); - } else if (GeoPoint.class.isAssignableFrom(clazz)) { - return (T) convertGeoPoint(o, context); - } else if (VectorValue.class.isAssignableFrom(clazz)) { - return (T) convertVectorValue(o, context); - } else if (DocumentReference.class.isAssignableFrom(clazz)) { - return (T) convertDocumentReference(o, context); - } else if (clazz.isArray()) { - throw context.errorPath.deserializeError( - "Converting to Arrays is not supported, please use Lists instead"); - } else if (clazz.getTypeParameters().length > 0) { - throw context.errorPath.deserializeError( - "Class " - + clazz.getName() - + " has generic type parameters, please use GenericTypeIndicator instead"); - } else if (clazz.equals(Object.class)) { - return (T) o; - } else if (clazz.isEnum()) { - return deserializeToEnum(o, clazz, context); - } else { - return convertBean(o, clazz, context); - } - } - @SuppressWarnings({"unchecked", "TypeParameterUnusedInFormals"}) private static T deserializeToParameterizedType( Object o, ParameterizedType type, DeserializeContext context) { @@ -297,7 +248,7 @@ private static T deserializeToParameterizedType( throw context.errorPath.deserializeError( "Only Maps with string keys are supported, but found Map with key type " + keyType); } - Map map = expectMap(o, context); + Map map = EncodingUtil.expectMap(o, context); HashMap result; try { result = @@ -325,7 +276,7 @@ private static T deserializeToParameterizedType( throw context.errorPath.deserializeError( "Collections are not supported, please use Lists instead"); } else { - Map map = expectMap(o, context); + Map map = EncodingUtil.expectMap(o, context); BeanMapper mapper = (BeanMapper) loadOrCreateBeanMapperForClass(rawType); HashMap>, Type> typeMapping = new HashMap<>(); TypeVariable>[] typeVariables = mapper.getClazz().getTypeParameters(); @@ -341,58 +292,54 @@ private static T deserializeToParameterizedType( } @SuppressWarnings("unchecked") - private static T deserializeToPrimitive( - Object o, Class clazz, DeserializeContext context) { - if (Integer.class.isAssignableFrom(clazz) || int.class.isAssignableFrom(clazz)) { - return (T) convertInteger(o, context); - } else if (Boolean.class.isAssignableFrom(clazz) || boolean.class.isAssignableFrom(clazz)) { - return (T) convertBoolean(o, context); - } else if (Double.class.isAssignableFrom(clazz) || double.class.isAssignableFrom(clazz)) { - return (T) convertDouble(o, context); - } else if (Long.class.isAssignableFrom(clazz) || long.class.isAssignableFrom(clazz)) { - return (T) convertLong(o, context); - } else if (BigDecimal.class.isAssignableFrom(clazz)) { - return (T) convertBigDecimal(o, context); - } else if (Float.class.isAssignableFrom(clazz) || float.class.isAssignableFrom(clazz)) { - return (T) (Float) convertDouble(o, context).floatValue(); - } else { + static T deserializeToClass(Object o, Class clazz, DeserializeContext context) { + if (o == null) { + return null; + } else if (clazz.isPrimitive() + || Number.class.isAssignableFrom(clazz) + || Boolean.class.isAssignableFrom(clazz) + || Character.class.isAssignableFrom(clazz)) { + return EncodingUtil.deserializeToPrimitive(o, clazz, context); + } else if (String.class.isAssignableFrom(clazz)) { + return (T) EncodingUtil.convertString(o, context); + } else if (Date.class.isAssignableFrom(clazz)) { + return (T) EncodingUtil.convertDate(o, context); + } else if (Timestamp.class.isAssignableFrom(clazz)) { + return (T) EncodingUtil.convertTimestamp(o, context); + } else if (Instant.class.isAssignableFrom(clazz)) { + return (T) EncodingUtil.convertInstant(o, context); + } else if (Blob.class.isAssignableFrom(clazz)) { + return (T) EncodingUtil.convertBlob(o, context); + } else if (GeoPoint.class.isAssignableFrom(clazz)) { + return (T) EncodingUtil.convertGeoPoint(o, context); + } else if (VectorValue.class.isAssignableFrom(clazz)) { + return (T) EncodingUtil.convertVectorValue(o, context); + } else if (DocumentReference.class.isAssignableFrom(clazz)) { + return (T) EncodingUtil.convertDocumentReference(o, context); + } else if (clazz.isArray()) { throw context.errorPath.deserializeError( - String.format("Deserializing values to %s is not supported", clazz.getSimpleName())); + "Converting to Arrays is not supported, please use Lists instead"); + } else if (clazz.getTypeParameters().length > 0) { + throw context.errorPath.deserializeError( + "Class " + + clazz.getName() + + " has generic type parameters, please use GenericTypeIndicator instead"); + } else if (clazz.equals(Object.class)) { + return (T) o; + } else if (clazz.isEnum()) { + return EncodingUtil.deserializeToEnum(o, clazz, context); + } else { + return convertBean(o, clazz, context); } } - @SuppressWarnings("unchecked") - private static T deserializeToEnum( - Object object, Class clazz, DeserializeContext context) { - if (object instanceof String) { - String value = (String) object; - // We cast to Class without generics here since we can't prove the bound - // T extends Enum statically - - // try to use PropertyName if exist - Field[] enumFields = clazz.getFields(); - for (Field field : enumFields) { - if (field.isEnumConstant()) { - String propertyName = BeanMapper.propertyName(field); - if (value.equals(propertyName)) { - value = field.getName(); - break; - } - } - } - - try { - return (T) Enum.valueOf((Class) clazz, value); - } catch (IllegalArgumentException e) { - throw context.errorPath.deserializeError( - "Could not find enum value of " + clazz.getName() + " for value \"" + value + "\""); - } + private static T convertBean(Object o, Class clazz, DeserializeContext context) { + BeanMapper mapper = loadOrCreateBeanMapperForClass(clazz); + if (o instanceof Map) { + return mapper.deserialize(EncodingUtil.expectMap(o, context), context); } else { throw context.errorPath.deserializeError( - "Expected a String while deserializing to enum " - + clazz - + " but got a " - + object.getClass()); + "Can't convert object of type " + o.getClass().getName() + " to type " + clazz.getName()); } } @@ -412,195 +359,6 @@ private static BeanMapper loadOrCreateBeanMapperForClass(Class clazz) return mapper; } - @SuppressWarnings("unchecked") - private static Map expectMap(Object object, DeserializeContext context) { - if (object instanceof Map) { - // TODO: runtime validation of keys? - return (Map) object; - } else { - throw context.errorPath.deserializeError( - "Expected a Map while deserializing, but got a " + object.getClass()); - } - } - - private static Integer convertInteger(Object o, DeserializeContext context) { - if (o instanceof Integer) { - return (Integer) o; - } else if (o instanceof Long || o instanceof Double) { - double value = ((Number) o).doubleValue(); - if (value >= Integer.MIN_VALUE && value <= Integer.MAX_VALUE) { - return ((Number) o).intValue(); - } else { - throw context.errorPath.deserializeError( - "Numeric value out of 32-bit integer range: " - + value - + ". Did you mean to use a long or double instead of an int?"); - } - } else { - throw context.errorPath.deserializeError( - "Failed to convert a value of type " + o.getClass().getName() + " to int"); - } - } - - private static Long convertLong(Object o, DeserializeContext context) { - if (o instanceof Integer) { - return ((Integer) o).longValue(); - } else if (o instanceof Long) { - return (Long) o; - } else if (o instanceof Double) { - Double value = (Double) o; - if (value >= Long.MIN_VALUE && value <= Long.MAX_VALUE) { - return value.longValue(); - } else { - throw context.errorPath.deserializeError( - "Numeric value out of 64-bit long range: " - + value - + ". Did you mean to use a double instead of a long?"); - } - } else { - throw context.errorPath.deserializeError( - "Failed to convert a value of type " + o.getClass().getName() + " to long"); - } - } - - private static Double convertDouble(Object o, DeserializeContext context) { - if (o instanceof Integer) { - return ((Integer) o).doubleValue(); - } else if (o instanceof Long) { - Long value = (Long) o; - Double doubleValue = ((Long) o).doubleValue(); - if (doubleValue.longValue() == value) { - return doubleValue; - } else { - throw context.errorPath.deserializeError( - "Loss of precision while converting number to " - + "double: " - + o - + ". Did you mean to use a 64-bit long instead?"); - } - } else if (o instanceof Double) { - return (Double) o; - } else { - throw context.errorPath.deserializeError( - "Failed to convert a value of type " + o.getClass().getName() + " to double"); - } - } - - private static BigDecimal convertBigDecimal(Object o, DeserializeContext context) { - if (o instanceof Integer) { - return BigDecimal.valueOf(((Integer) o).intValue()); - } else if (o instanceof Long) { - return BigDecimal.valueOf(((Long) o).longValue()); - } else if (o instanceof Double) { - return BigDecimal.valueOf(((Double) o).doubleValue()).abs(); - } else if (o instanceof BigDecimal) { - return (BigDecimal) o; - } else if (o instanceof String) { - return new BigDecimal((String) o); - } else { - throw context.errorPath.deserializeError( - "Failed to convert a value of type " + o.getClass().getName() + " to BigDecimal"); - } - } - - private static Boolean convertBoolean(Object o, DeserializeContext context) { - if (o instanceof Boolean) { - return (Boolean) o; - } else { - throw context.errorPath.deserializeError( - "Failed to convert value of type " + o.getClass().getName() + " to boolean"); - } - } - - private static String convertString(Object o, DeserializeContext context) { - if (o instanceof String) { - return (String) o; - } else { - throw context.errorPath.deserializeError( - "Failed to convert value of type " + o.getClass().getName() + " to String"); - } - } - - private static Date convertDate(Object o, DeserializeContext context) { - if (o instanceof Date) { - return (Date) o; - } else if (o instanceof Timestamp) { - return ((Timestamp) o).toDate(); - } else { - throw context.errorPath.deserializeError( - "Failed to convert value of type " + o.getClass().getName() + " to Date"); - } - } - - private static Timestamp convertTimestamp(Object o, DeserializeContext context) { - if (o instanceof Timestamp) { - return (Timestamp) o; - } else if (o instanceof Date) { - return Timestamp.of((Date) o); - } else { - throw context.errorPath.deserializeError( - "Failed to convert value of type " + o.getClass().getName() + " to Timestamp"); - } - } - - private static Instant convertInstant(Object o, DeserializeContext context) { - if (o instanceof Timestamp) { - Timestamp timestamp = (Timestamp) o; - return Instant.ofEpochSecond(timestamp.getSeconds(), timestamp.getNanos()); - } else if (o instanceof Date) { - return Instant.ofEpochMilli(((Date) o).getTime()); - } else { - throw context.errorPath.deserializeError( - "Failed to convert value of type " + o.getClass().getName() + " to Instant"); - } - } - - private static Blob convertBlob(Object o, DeserializeContext context) { - if (o instanceof Blob) { - return (Blob) o; - } else { - throw context.errorPath.deserializeError( - "Failed to convert value of type " + o.getClass().getName() + " to Blob"); - } - } - - private static GeoPoint convertGeoPoint(Object o, DeserializeContext context) { - if (o instanceof GeoPoint) { - return (GeoPoint) o; - } else { - throw context.errorPath.deserializeError( - "Failed to convert value of type " + o.getClass().getName() + " to GeoPoint"); - } - } - - private static VectorValue convertVectorValue(Object o, DeserializeContext context) { - if (o instanceof VectorValue) { - return (VectorValue) o; - } else { - throw context.errorPath.deserializeError( - "Failed to convert value of type " + o.getClass().getName() + " to VectorValue"); - } - } - - private static DocumentReference convertDocumentReference(Object o, DeserializeContext context) { - if (o instanceof DocumentReference) { - return (DocumentReference) o; - } else { - throw context.errorPath.deserializeError( - "Failed to convert value of type " + o.getClass().getName() + " to DocumentReference"); - } - } - - private static T convertBean(Object o, Class clazz, DeserializeContext context) { - BeanMapper mapper = loadOrCreateBeanMapperForClass(clazz); - if (o instanceof Map) { - return mapper.deserialize(expectMap(o, context), context); - } else { - throw context.errorPath.deserializeError( - "Can't convert object of type " + o.getClass().getName() + " to type " + clazz.getName()); - } - } - private static boolean isRecordType(Class cls) { Class parent = cls.getSuperclass(); return parent != null && "java.lang.Record".equals(parent.getName()); diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/encoding/EncodingUtil.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/encoding/EncodingUtil.java new file mode 100644 index 000000000..3d16b5444 --- /dev/null +++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/encoding/EncodingUtil.java @@ -0,0 +1,434 @@ +/* + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.cloud.firestore.encoding; + +import com.google.cloud.Timestamp; +import com.google.cloud.firestore.Blob; +import com.google.cloud.firestore.DocumentReference; +import com.google.cloud.firestore.GeoPoint; +import com.google.cloud.firestore.VectorValue; +import com.google.cloud.firestore.annotation.Exclude; +import com.google.cloud.firestore.annotation.PropertyName; +import java.lang.reflect.AccessibleObject; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.lang.reflect.Type; +import java.math.BigDecimal; +import java.time.Instant; +import java.util.Date; +import java.util.Map; + +class EncodingUtil { + + static void hardAssert(boolean assertion) { + hardAssert(assertion, "Internal inconsistency"); + } + + static void hardAssert(boolean assertion, String message) { + if (!assertion) { + throw new RuntimeException("Hard assert failed: " + message); + } + } + + @SuppressWarnings("unchecked") + static T deserializeToPrimitive(Object o, Class clazz, DeserializeContext context) { + if (Integer.class.isAssignableFrom(clazz) || int.class.isAssignableFrom(clazz)) { + return (T) convertInteger(o, context); + } else if (Boolean.class.isAssignableFrom(clazz) || boolean.class.isAssignableFrom(clazz)) { + return (T) convertBoolean(o, context); + } else if (Double.class.isAssignableFrom(clazz) || double.class.isAssignableFrom(clazz)) { + return (T) convertDouble(o, context); + } else if (Long.class.isAssignableFrom(clazz) || long.class.isAssignableFrom(clazz)) { + return (T) convertLong(o, context); + } else if (BigDecimal.class.isAssignableFrom(clazz)) { + return (T) convertBigDecimal(o, context); + } else if (Float.class.isAssignableFrom(clazz) || float.class.isAssignableFrom(clazz)) { + return (T) (Float) convertDouble(o, context).floatValue(); + } else { + throw context.errorPath.deserializeError( + String.format("Deserializing values to %s is not supported", clazz.getSimpleName())); + } + } + + @SuppressWarnings("unchecked") + static T deserializeToEnum(Object object, Class clazz, DeserializeContext context) { + if (object instanceof String) { + String value = (String) object; + // We cast to Class without generics here since we can't prove the bound + // T extends Enum statically + + // try to use PropertyName if exist + Field[] enumFields = clazz.getFields(); + for (Field field : enumFields) { + if (field.isEnumConstant()) { + String propertyName = propertyName(field); + if (value.equals(propertyName)) { + value = field.getName(); + break; + } + } + } + + try { + return (T) Enum.valueOf((Class) clazz, value); + } catch (IllegalArgumentException e) { + throw context.errorPath.deserializeError( + "Could not find enum value of " + clazz.getName() + " for value \"" + value + "\""); + } + } else { + throw context.errorPath.deserializeError( + "Expected a String while deserializing to enum " + + clazz + + " but got a " + + object.getClass()); + } + } + + @SuppressWarnings("unchecked") + static Map expectMap(Object object, DeserializeContext context) { + if (object instanceof Map) { + // TODO: runtime validation of keys? + return (Map) object; + } else { + throw context.errorPath.deserializeError( + "Expected a Map while deserializing, but got a " + object.getClass()); + } + } + + private static Integer convertInteger(Object o, DeserializeContext context) { + if (o instanceof Integer) { + return (Integer) o; + } else if (o instanceof Long || o instanceof Double) { + double value = ((Number) o).doubleValue(); + if (value >= Integer.MIN_VALUE && value <= Integer.MAX_VALUE) { + return ((Number) o).intValue(); + } else { + throw context.errorPath.deserializeError( + "Numeric value out of 32-bit integer range: " + + value + + ". Did you mean to use a long or double instead of an int?"); + } + } else { + throw context.errorPath.deserializeError( + "Failed to convert a value of type " + o.getClass().getName() + " to int"); + } + } + + private static Long convertLong(Object o, DeserializeContext context) { + if (o instanceof Integer) { + return ((Integer) o).longValue(); + } else if (o instanceof Long) { + return (Long) o; + } else if (o instanceof Double) { + Double value = (Double) o; + if (value >= Long.MIN_VALUE && value <= Long.MAX_VALUE) { + return value.longValue(); + } else { + throw context.errorPath.deserializeError( + "Numeric value out of 64-bit long range: " + + value + + ". Did you mean to use a double instead of a long?"); + } + } else { + throw context.errorPath.deserializeError( + "Failed to convert a value of type " + o.getClass().getName() + " to long"); + } + } + + private static Double convertDouble(Object o, DeserializeContext context) { + if (o instanceof Integer) { + return ((Integer) o).doubleValue(); + } else if (o instanceof Long) { + Long value = (Long) o; + Double doubleValue = ((Long) o).doubleValue(); + if (doubleValue.longValue() == value) { + return doubleValue; + } else { + throw context.errorPath.deserializeError( + "Loss of precision while converting number to " + + "double: " + + o + + ". Did you mean to use a 64-bit long instead?"); + } + } else if (o instanceof Double) { + return (Double) o; + } else { + throw context.errorPath.deserializeError( + "Failed to convert a value of type " + o.getClass().getName() + " to double"); + } + } + + private static BigDecimal convertBigDecimal(Object o, DeserializeContext context) { + if (o instanceof Integer) { + return BigDecimal.valueOf(((Integer) o).intValue()); + } else if (o instanceof Long) { + return BigDecimal.valueOf(((Long) o).longValue()); + } else if (o instanceof Double) { + return BigDecimal.valueOf(((Double) o).doubleValue()).abs(); + } else if (o instanceof BigDecimal) { + return (BigDecimal) o; + } else if (o instanceof String) { + return new BigDecimal((String) o); + } else { + throw context.errorPath.deserializeError( + "Failed to convert a value of type " + o.getClass().getName() + " to BigDecimal"); + } + } + + private static Boolean convertBoolean(Object o, DeserializeContext context) { + if (o instanceof Boolean) { + return (Boolean) o; + } else { + throw context.errorPath.deserializeError( + "Failed to convert value of type " + o.getClass().getName() + " to boolean"); + } + } + + static String convertString(Object o, DeserializeContext context) { + if (o instanceof String) { + return (String) o; + } else { + throw context.errorPath.deserializeError( + "Failed to convert value of type " + o.getClass().getName() + " to String"); + } + } + + static Date convertDate(Object o, DeserializeContext context) { + if (o instanceof Date) { + return (Date) o; + } else if (o instanceof Timestamp) { + return ((Timestamp) o).toDate(); + } else { + throw context.errorPath.deserializeError( + "Failed to convert value of type " + o.getClass().getName() + " to Date"); + } + } + + static Timestamp convertTimestamp(Object o, DeserializeContext context) { + if (o instanceof Timestamp) { + return (Timestamp) o; + } else if (o instanceof Date) { + return Timestamp.of((Date) o); + } else { + throw context.errorPath.deserializeError( + "Failed to convert value of type " + o.getClass().getName() + " to Timestamp"); + } + } + + static Instant convertInstant(Object o, DeserializeContext context) { + if (o instanceof Timestamp) { + Timestamp timestamp = (Timestamp) o; + return Instant.ofEpochSecond(timestamp.getSeconds(), timestamp.getNanos()); + } else if (o instanceof Date) { + return Instant.ofEpochMilli(((Date) o).getTime()); + } else { + throw context.errorPath.deserializeError( + "Failed to convert value of type " + o.getClass().getName() + " to Instant"); + } + } + + static Blob convertBlob(Object o, DeserializeContext context) { + if (o instanceof Blob) { + return (Blob) o; + } else { + throw context.errorPath.deserializeError( + "Failed to convert value of type " + o.getClass().getName() + " to Blob"); + } + } + + static GeoPoint convertGeoPoint(Object o, DeserializeContext context) { + if (o instanceof GeoPoint) { + return (GeoPoint) o; + } else { + throw context.errorPath.deserializeError( + "Failed to convert value of type " + o.getClass().getName() + " to GeoPoint"); + } + } + + static VectorValue convertVectorValue(Object o, DeserializeContext context) { + if (o instanceof VectorValue) { + return (VectorValue) o; + } else { + throw context.errorPath.deserializeError( + "Failed to convert value of type " + o.getClass().getName() + " to VectorValue"); + } + } + + static DocumentReference convertDocumentReference(Object o, DeserializeContext context) { + if (o instanceof DocumentReference) { + return (DocumentReference) o; + } else { + throw context.errorPath.deserializeError( + "Failed to convert value of type " + o.getClass().getName() + " to DocumentReference"); + } + } + + static String propertyName(Field field) { + String annotatedName = annotatedName(field); + return annotatedName != null ? annotatedName : field.getName(); + } + + static String annotatedName(AccessibleObject obj) { + if (obj.isAnnotationPresent(PropertyName.class)) { + PropertyName annotation = obj.getAnnotation(PropertyName.class); + return annotation.value(); + } + + return null; + } + + static String propertyName(Method method) { + String annotatedName = EncodingUtil.annotatedName(method); + return annotatedName != null ? annotatedName : serializedName(method.getName()); + } + + private static String serializedName(String methodName) { + String[] prefixes = new String[] {"get", "set", "is"}; + String methodPrefix = null; + for (String prefix : prefixes) { + if (methodName.startsWith(prefix)) { + methodPrefix = prefix; + } + } + if (methodPrefix == null) { + throw new IllegalArgumentException("Unknown Bean prefix for method: " + methodName); + } + String strippedName = methodName.substring(methodPrefix.length()); + + // Make sure the first word or upper-case prefix is converted to lower-case + char[] chars = strippedName.toCharArray(); + int pos = 0; + while (pos < chars.length && Character.isUpperCase(chars[pos])) { + chars[pos] = Character.toLowerCase(chars[pos]); + pos++; + } + return new String(chars); + } + + static void ensureValidDocumentIdType(String fieldDescription, String operation, Type type) { + if (type != String.class && type != DocumentReference.class) { + throw new IllegalArgumentException( + fieldDescription + + " is annotated with @DocumentId but " + + operation + + " " + + type + + " instead of String or DocumentReference."); + } + } + + static boolean shouldIncludeGetter(Method method) { + if (!method.getName().startsWith("get") && !method.getName().startsWith("is")) { + return false; + } + // Exclude methods from Object.class + if (method.getDeclaringClass().equals(Object.class)) { + return false; + } + // Non-public methods + if (!Modifier.isPublic(method.getModifiers())) { + return false; + } + // Static methods + if (Modifier.isStatic(method.getModifiers())) { + return false; + } + // No return type + if (method.getReturnType().equals(Void.TYPE)) { + return false; + } + // Non-zero parameters + if (method.getParameterTypes().length != 0) { + return false; + } + // Excluded methods + if (method.isAnnotationPresent(Exclude.class)) { + return false; + } + return true; + } + + static boolean shouldIncludeSetter(Method method) { + if (!method.getName().startsWith("set")) { + return false; + } + // Exclude methods from Object.class + if (method.getDeclaringClass().equals(Object.class)) { + return false; + } + // Static methods + if (Modifier.isStatic(method.getModifiers())) { + return false; + } + // Has a return type + if (!method.getReturnType().equals(Void.TYPE)) { + return false; + } + // Methods without exactly one parameters + if (method.getParameterTypes().length != 1) { + return false; + } + // Excluded methods + if (method.isAnnotationPresent(Exclude.class)) { + return false; + } + return true; + } + + static boolean shouldIncludeField(Field field) { + // Exclude methods from Object.class + if (field.getDeclaringClass().equals(Object.class)) { + return false; + } + // Non-public fields + if (!Modifier.isPublic(field.getModifiers())) { + return false; + } + // Static fields + if (Modifier.isStatic(field.getModifiers())) { + return false; + } + // Transient fields + if (Modifier.isTransient(field.getModifiers())) { + return false; + } + // Excluded fields + if (field.isAnnotationPresent(Exclude.class)) { + return false; + } + return true; + } + + static boolean isSetterOverride(Method base, Method override) { + // We expect an overridden setter here + EncodingUtil.hardAssert( + base.getDeclaringClass().isAssignableFrom(override.getDeclaringClass()), + "Expected override from a base class"); + EncodingUtil.hardAssert(base.getReturnType().equals(Void.TYPE), "Expected void return type"); + EncodingUtil.hardAssert( + override.getReturnType().equals(Void.TYPE), "Expected void return type"); + + Type[] baseParameterTypes = base.getParameterTypes(); + Type[] overrideParameterTypes = override.getParameterTypes(); + EncodingUtil.hardAssert(baseParameterTypes.length == 1, "Expected exactly one parameter"); + EncodingUtil.hardAssert(overrideParameterTypes.length == 1, "Expected exactly one parameter"); + + return base.getName().equals(override.getName()) + && baseParameterTypes[0].equals(overrideParameterTypes[0]); + } +} diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/encoding/PojoBeanMapper.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/encoding/PojoBeanMapper.java index e3f97e06f..555146d39 100644 --- a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/encoding/PojoBeanMapper.java +++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/encoding/PojoBeanMapper.java @@ -18,13 +18,11 @@ import com.google.cloud.Timestamp; import com.google.cloud.firestore.annotation.DocumentId; -import com.google.cloud.firestore.annotation.Exclude; import com.google.cloud.firestore.annotation.ServerTimestamp; import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; -import java.lang.reflect.Modifier; import java.lang.reflect.Type; import java.lang.reflect.TypeVariable; import java.time.Instant; @@ -71,8 +69,8 @@ class PojoBeanMapper extends BeanMapper { this.constructor = constructor; // Add any public getters to properties (including isXyz()) for (Method method : clazz.getMethods()) { - if (shouldIncludeGetter(method)) { - String propertyName = propertyName(method); + if (EncodingUtil.shouldIncludeGetter(method)) { + String propertyName = EncodingUtil.propertyName(method); addProperty(propertyName); method.setAccessible(true); if (getters.containsKey(propertyName)) { @@ -89,8 +87,8 @@ class PojoBeanMapper extends BeanMapper { // Add any public fields to properties for (Field field : clazz.getFields()) { - if (shouldIncludeField(field)) { - String propertyName = propertyName(field); + if (EncodingUtil.shouldIncludeField(field)) { + String propertyName = EncodingUtil.propertyName(field); addProperty(propertyName); applyFieldAnnotations(field); } @@ -103,8 +101,8 @@ class PojoBeanMapper extends BeanMapper { do { // Add any setters for (Method method : currentClass.getDeclaredMethods()) { - if (shouldIncludeSetter(method)) { - String propertyName = propertyName(method); + if (EncodingUtil.shouldIncludeSetter(method)) { + String propertyName = EncodingUtil.propertyName(method); String existingPropertyName = properties.get(propertyName.toLowerCase(Locale.US)); if (existingPropertyName != null) { if (!existingPropertyName.equals(propertyName)) { @@ -119,7 +117,7 @@ class PojoBeanMapper extends BeanMapper { method.setAccessible(true); setters.put(propertyName, method); applySetterAnnotations(method); - } else if (!isSetterOverride(method, existingSetter)) { + } else if (!EncodingUtil.isSetterOverride(method, existingSetter)) { // We require that setters with conflicting property names are // overrides from a base class if (currentClass == clazz) { @@ -147,7 +145,7 @@ class PojoBeanMapper extends BeanMapper { } for (Field field : currentClass.getDeclaredFields()) { - String propertyName = propertyName(field); + String propertyName = EncodingUtil.propertyName(field); // Case sensitivity is checked at deserialization time // Fields are only added if they don't exist on a subclass @@ -353,14 +351,14 @@ private void applyGetterAnnotations(Method method) { + returnType + " instead of Date, Timestamp, or Instant."); } - serverTimestamps.add(propertyName(method)); + serverTimestamps.add(EncodingUtil.propertyName(method)); } // Even though the value will be skipped, we still check for type matching for consistency. if (method.isAnnotationPresent(DocumentId.class)) { Class returnType = method.getReturnType(); - ensureValidDocumentIdType("Method", "returns", returnType); - documentIdPropertyNames.add(propertyName(method)); + EncodingUtil.ensureValidDocumentIdType("Method", "returns", returnType); + documentIdPropertyNames.add(EncodingUtil.propertyName(method)); } } @@ -375,141 +373,8 @@ private void applySetterAnnotations(Method method) { if (method.isAnnotationPresent(DocumentId.class)) { Class paramType = method.getParameterTypes()[0]; - ensureValidDocumentIdType("Method", "accepts", paramType); - documentIdPropertyNames.add(propertyName(method)); + EncodingUtil.ensureValidDocumentIdType("Method", "accepts", paramType); + documentIdPropertyNames.add(EncodingUtil.propertyName(method)); } } - - private static boolean shouldIncludeGetter(Method method) { - if (!method.getName().startsWith("get") && !method.getName().startsWith("is")) { - return false; - } - // Exclude methods from Object.class - if (method.getDeclaringClass().equals(Object.class)) { - return false; - } - // Non-public methods - if (!Modifier.isPublic(method.getModifiers())) { - return false; - } - // Static methods - if (Modifier.isStatic(method.getModifiers())) { - return false; - } - // No return type - if (method.getReturnType().equals(Void.TYPE)) { - return false; - } - // Non-zero parameters - if (method.getParameterTypes().length != 0) { - return false; - } - // Excluded methods - if (method.isAnnotationPresent(Exclude.class)) { - return false; - } - return true; - } - - private static boolean shouldIncludeSetter(Method method) { - if (!method.getName().startsWith("set")) { - return false; - } - // Exclude methods from Object.class - if (method.getDeclaringClass().equals(Object.class)) { - return false; - } - // Static methods - if (Modifier.isStatic(method.getModifiers())) { - return false; - } - // Has a return type - if (!method.getReturnType().equals(Void.TYPE)) { - return false; - } - // Methods without exactly one parameters - if (method.getParameterTypes().length != 1) { - return false; - } - // Excluded methods - if (method.isAnnotationPresent(Exclude.class)) { - return false; - } - return true; - } - - private static boolean shouldIncludeField(Field field) { - // Exclude methods from Object.class - if (field.getDeclaringClass().equals(Object.class)) { - return false; - } - // Non-public fields - if (!Modifier.isPublic(field.getModifiers())) { - return false; - } - // Static fields - if (Modifier.isStatic(field.getModifiers())) { - return false; - } - // Transient fields - if (Modifier.isTransient(field.getModifiers())) { - return false; - } - // Excluded fields - if (field.isAnnotationPresent(Exclude.class)) { - return false; - } - return true; - } - - private static void hardAssert(boolean assertion, String message) { - if (!assertion) { - throw new RuntimeException("Hard assert failed: " + message); - } - } - - private static boolean isSetterOverride(Method base, Method override) { - // We expect an overridden setter here - hardAssert( - base.getDeclaringClass().isAssignableFrom(override.getDeclaringClass()), - "Expected override from a base class"); - hardAssert(base.getReturnType().equals(Void.TYPE), "Expected void return type"); - hardAssert(override.getReturnType().equals(Void.TYPE), "Expected void return type"); - - Type[] baseParameterTypes = base.getParameterTypes(); - Type[] overrideParameterTypes = override.getParameterTypes(); - hardAssert(baseParameterTypes.length == 1, "Expected exactly one parameter"); - hardAssert(overrideParameterTypes.length == 1, "Expected exactly one parameter"); - - return base.getName().equals(override.getName()) - && baseParameterTypes[0].equals(overrideParameterTypes[0]); - } - - private static String propertyName(Method method) { - String annotatedName = annotatedName(method); - return annotatedName != null ? annotatedName : serializedName(method.getName()); - } - - private static String serializedName(String methodName) { - String[] prefixes = new String[] {"get", "set", "is"}; - String methodPrefix = null; - for (String prefix : prefixes) { - if (methodName.startsWith(prefix)) { - methodPrefix = prefix; - } - } - if (methodPrefix == null) { - throw new IllegalArgumentException("Unknown Bean prefix for method: " + methodName); - } - String strippedName = methodName.substring(methodPrefix.length()); - - // Make sure the first word or upper-case prefix is converted to lower-case - char[] chars = strippedName.toCharArray(); - int pos = 0; - while (pos < chars.length && Character.isUpperCase(chars[pos])) { - chars[pos] = Character.toLowerCase(chars[pos]); - pos++; - } - return new String(chars); - } } diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/encoding/RecordMapper.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/encoding/RecordMapper.java index b48501ce5..7fc446737 100644 --- a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/encoding/RecordMapper.java +++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/encoding/RecordMapper.java @@ -60,7 +60,7 @@ class RecordMapper extends BeanMapper { try { for (int i = 0; i < recordComponents.length; i++) { Field field = clazz.getDeclaredField(RECORD_INSPECTOR.getName(recordComponents[i])); - String propertyName = propertyName(field); + String propertyName = EncodingUtil.propertyName(field); constructorParamIndexes.put(propertyName, i); accessors.put(propertyName, RECORD_INSPECTOR.getAccessor(recordComponents[i])); applyFieldAnnotations(field); From 48885ce65c2362ddff67f0121e9d78f15ed0aa13 Mon Sep 17 00:00:00 2001 From: milaGGL <107142260+milaGGL@users.noreply.github.com> Date: Fri, 13 Sep 2024 11:44:51 -0400 Subject: [PATCH 48/51] mark methods protected, reduce code duplication --- .../cloud/firestore/encoding/BeanMapper.java | 76 ++++++++++++------- .../firestore/encoding/CustomClassMapper.java | 2 +- .../firestore/encoding/EncodingUtil.java | 14 +++- .../firestore/encoding/PojoBeanMapper.java | 65 +++------------- 4 files changed, 73 insertions(+), 84 deletions(-) diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/encoding/BeanMapper.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/encoding/BeanMapper.java index 3eb51c8ee..131c19c42 100644 --- a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/encoding/BeanMapper.java +++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/encoding/BeanMapper.java @@ -16,19 +16,17 @@ package com.google.cloud.firestore.encoding; -import com.google.cloud.Timestamp; import com.google.cloud.firestore.FieldValue; import com.google.cloud.firestore.annotation.DocumentId; import com.google.cloud.firestore.annotation.IgnoreExtraProperties; import com.google.cloud.firestore.annotation.ServerTimestamp; import com.google.cloud.firestore.annotation.ThrowOnExtraProperties; import java.lang.reflect.Field; +import java.lang.reflect.Method; import java.lang.reflect.Type; import java.lang.reflect.TypeVariable; -import java.time.Instant; import java.util.Collection; import java.util.Collections; -import java.util.Date; import java.util.HashSet; import java.util.Map; @@ -90,7 +88,11 @@ abstract T deserialize( Map>, Type> types, DeserializeContext context); - void verifyValidType(T object) { + T deserialize(Map values, DeserializeContext context) { + return deserialize(values, Collections.emptyMap(), context); + } + + protected void verifyValidType(T object) { if (!clazz.isAssignableFrom(object.getClass())) { throw new IllegalArgumentException( "Can't serialize object of class " @@ -100,7 +102,7 @@ void verifyValidType(T object) { } } - Type resolveType(Type type, Map>, Type> types) { + protected Type resolveType(Type type, Map>, Type> types) { if (type instanceof TypeVariable) { Type resolvedType = types.get(type); if (resolvedType == null) { @@ -113,7 +115,7 @@ Type resolveType(Type type, Map>, Type> types) { return type; } - void checkForDocIdConflict( + protected void checkForDocIdConflict( String docIdPropertyName, Collection deserializedProperties, DeserializeContext context) { @@ -129,38 +131,58 @@ void checkForDocIdConflict( } } - T deserialize(Map values, DeserializeContext context) { - return deserialize(values, Collections.emptyMap(), context); + protected Object getSerializedValue( + String property, Object propertyValue, DeserializeContext.ErrorPath path) { + if (serverTimestamps.contains(property) && propertyValue == null) { + // Replace null ServerTimestamp-annotated fields with the sentinel. + return FieldValue.serverTimestamp(); + } else { + return CustomClassMapper.serialize(propertyValue, path.child(property)); + } } - void applyFieldAnnotations(Field field) { + protected void applyFieldAnnotations(Field field) { + Class fieldType = field.getType(); + if (field.isAnnotationPresent(ServerTimestamp.class)) { - Class fieldType = field.getType(); - if (fieldType != Date.class && fieldType != Timestamp.class && fieldType != Instant.class) { - throw new IllegalArgumentException( - "Field " - + field.getName() - + " is annotated with @ServerTimestamp but is " - + fieldType - + " instead of Date, Timestamp, or Instant."); - } + EncodingUtil.validateServerTimestampType("Field", "is", fieldType); serverTimestamps.add(EncodingUtil.propertyName(field)); } if (field.isAnnotationPresent(DocumentId.class)) { - Class fieldType = field.getType(); - EncodingUtil.ensureValidDocumentIdType("Field", "is", fieldType); + EncodingUtil.validateDocumentIdType("Field", "is", fieldType); documentIdPropertyNames.add(EncodingUtil.propertyName(field)); } } - Object getSerializedValue( - String property, Object propertyValue, DeserializeContext.ErrorPath path) { - if (serverTimestamps.contains(property) && propertyValue == null) { - // Replace null ServerTimestamp-annotated fields with the sentinel. - return FieldValue.serverTimestamp(); - } else { - return CustomClassMapper.serialize(propertyValue, path.child(property)); + protected void applyGetterAnnotations(Method method) { + Class returnType = method.getReturnType(); + + if (method.isAnnotationPresent(ServerTimestamp.class)) { + EncodingUtil.validateServerTimestampType("Method", "returns", returnType); + serverTimestamps.add(EncodingUtil.propertyName(method)); + } + + // Even though the value will be skipped, we still check for type matching for consistency. + if (method.isAnnotationPresent(DocumentId.class)) { + EncodingUtil.validateDocumentIdType("Method", "returns", returnType); + documentIdPropertyNames.add(EncodingUtil.propertyName(method)); + } + } + + protected void applySetterAnnotations(Method method) { + if (method.isAnnotationPresent(ServerTimestamp.class)) { + throw new IllegalArgumentException( + "Method " + + method.getName() + + " is annotated with @ServerTimestamp but should not be. @ServerTimestamp can" + + " only be applied to fields and getters, not setters."); + } + + if (method.isAnnotationPresent(DocumentId.class)) { + Class paramType = method.getParameterTypes()[0]; + EncodingUtil.validateDocumentIdType("Method", "accepts", paramType); + documentIdPropertyNames.add(EncodingUtil.propertyName(method)); } } } diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/encoding/CustomClassMapper.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/encoding/CustomClassMapper.java index 4414251d8..72dc9f243 100644 --- a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/encoding/CustomClassMapper.java +++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/encoding/CustomClassMapper.java @@ -292,7 +292,7 @@ private static T deserializeToParameterizedType( } @SuppressWarnings("unchecked") - static T deserializeToClass(Object o, Class clazz, DeserializeContext context) { + private static T deserializeToClass(Object o, Class clazz, DeserializeContext context) { if (o == null) { return null; } else if (clazz.isPrimitive() diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/encoding/EncodingUtil.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/encoding/EncodingUtil.java index 3d16b5444..9575479b0 100644 --- a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/encoding/EncodingUtil.java +++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/encoding/EncodingUtil.java @@ -320,7 +320,7 @@ private static String serializedName(String methodName) { return new String(chars); } - static void ensureValidDocumentIdType(String fieldDescription, String operation, Type type) { + static void validateDocumentIdType(String fieldDescription, String operation, Type type) { if (type != String.class && type != DocumentReference.class) { throw new IllegalArgumentException( fieldDescription @@ -332,6 +332,18 @@ static void ensureValidDocumentIdType(String fieldDescription, String operation, } } + static void validateServerTimestampType(String fieldDescription, String operation, Type type) { + if (type != Date.class && type != Timestamp.class && type != Instant.class) { + throw new IllegalArgumentException( + fieldDescription + + " is annotated with @ServerTimestamp but " + + operation + + " " + + type + + " instead of Date, Timestamp, or Instant."); + } + } + static boolean shouldIncludeGetter(Method method) { if (!method.getName().startsWith("get") && !method.getName().startsWith("is")) { return false; diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/encoding/PojoBeanMapper.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/encoding/PojoBeanMapper.java index 555146d39..92eb40997 100644 --- a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/encoding/PojoBeanMapper.java +++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/encoding/PojoBeanMapper.java @@ -16,17 +16,12 @@ package com.google.cloud.firestore.encoding; -import com.google.cloud.Timestamp; -import com.google.cloud.firestore.annotation.DocumentId; -import com.google.cloud.firestore.annotation.ServerTimestamp; import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.lang.reflect.Type; import java.lang.reflect.TypeVariable; -import java.time.Instant; -import java.util.Date; import java.util.HashMap; import java.util.HashSet; import java.util.Locale; @@ -179,16 +174,6 @@ class PojoBeanMapper extends BeanMapper { } } - private void addProperty(String property) { - String oldValue = properties.put(property.toLowerCase(Locale.US), property); - if (oldValue != null && !property.equals(oldValue)) { - throw new RuntimeException( - "Found two getters or fields with conflicting case " - + "sensitivity for property: " - + property.toLowerCase(Locale.US)); - } - } - @Override Map serialize(T object, DeserializeContext.ErrorPath path) { verifyValidType(object); @@ -296,6 +281,16 @@ T deserialize( return instance; } + private void addProperty(String property) { + String oldValue = properties.put(property.toLowerCase(Locale.US), property); + if (oldValue != null && !property.equals(oldValue)) { + throw new RuntimeException( + "Found two getters or fields with conflicting case " + + "sensitivity for property: " + + property.toLowerCase(Locale.US)); + } + } + // Populate @DocumentId annotated fields. If there is a conflict (@DocumentId annotation is // applied to a property that is already deserialized from the firestore document) // a runtime exception will be thrown. @@ -337,44 +332,4 @@ private void populateDocumentIdProperties( } } } - - private void applyGetterAnnotations(Method method) { - if (method.isAnnotationPresent(ServerTimestamp.class)) { - Class returnType = method.getReturnType(); - if (returnType != Date.class - && returnType != Timestamp.class - && returnType != Instant.class) { - throw new IllegalArgumentException( - "Method " - + method.getName() - + " is annotated with @ServerTimestamp but returns " - + returnType - + " instead of Date, Timestamp, or Instant."); - } - serverTimestamps.add(EncodingUtil.propertyName(method)); - } - - // Even though the value will be skipped, we still check for type matching for consistency. - if (method.isAnnotationPresent(DocumentId.class)) { - Class returnType = method.getReturnType(); - EncodingUtil.ensureValidDocumentIdType("Method", "returns", returnType); - documentIdPropertyNames.add(EncodingUtil.propertyName(method)); - } - } - - private void applySetterAnnotations(Method method) { - if (method.isAnnotationPresent(ServerTimestamp.class)) { - throw new IllegalArgumentException( - "Method " - + method.getName() - + " is annotated with @ServerTimestamp but should not be. @ServerTimestamp can" - + " only be applied to fields and getters, not setters."); - } - - if (method.isAnnotationPresent(DocumentId.class)) { - Class paramType = method.getParameterTypes()[0]; - EncodingUtil.ensureValidDocumentIdType("Method", "accepts", paramType); - documentIdPropertyNames.add(EncodingUtil.propertyName(method)); - } - } } From ef324e2008cf09fc42a80a6fd6edf13ea004adec Mon Sep 17 00:00:00 2001 From: milaGGL <107142260+milaGGL@users.noreply.github.com> Date: Fri, 13 Sep 2024 12:16:11 -0400 Subject: [PATCH 49/51] add class comment for EncodingUtil --- .../cloud/firestore/encoding/BeanMapper.java | 5 ----- .../cloud/firestore/encoding/EncodingUtil.java | 14 +++++++------- 2 files changed, 7 insertions(+), 12 deletions(-) diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/encoding/BeanMapper.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/encoding/BeanMapper.java index 131c19c42..276410735 100644 --- a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/encoding/BeanMapper.java +++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/encoding/BeanMapper.java @@ -143,12 +143,10 @@ protected Object getSerializedValue( protected void applyFieldAnnotations(Field field) { Class fieldType = field.getType(); - if (field.isAnnotationPresent(ServerTimestamp.class)) { EncodingUtil.validateServerTimestampType("Field", "is", fieldType); serverTimestamps.add(EncodingUtil.propertyName(field)); } - if (field.isAnnotationPresent(DocumentId.class)) { EncodingUtil.validateDocumentIdType("Field", "is", fieldType); documentIdPropertyNames.add(EncodingUtil.propertyName(field)); @@ -157,12 +155,10 @@ protected void applyFieldAnnotations(Field field) { protected void applyGetterAnnotations(Method method) { Class returnType = method.getReturnType(); - if (method.isAnnotationPresent(ServerTimestamp.class)) { EncodingUtil.validateServerTimestampType("Method", "returns", returnType); serverTimestamps.add(EncodingUtil.propertyName(method)); } - // Even though the value will be skipped, we still check for type matching for consistency. if (method.isAnnotationPresent(DocumentId.class)) { EncodingUtil.validateDocumentIdType("Method", "returns", returnType); @@ -178,7 +174,6 @@ protected void applySetterAnnotations(Method method) { + " is annotated with @ServerTimestamp but should not be. @ServerTimestamp can" + " only be applied to fields and getters, not setters."); } - if (method.isAnnotationPresent(DocumentId.class)) { Class paramType = method.getParameterTypes()[0]; EncodingUtil.validateDocumentIdType("Method", "accepts", paramType); diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/encoding/EncodingUtil.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/encoding/EncodingUtil.java index 9575479b0..5a1d26aa7 100644 --- a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/encoding/EncodingUtil.java +++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/encoding/EncodingUtil.java @@ -33,8 +33,8 @@ import java.util.Date; import java.util.Map; +/** A utility class for serializing and deserializing operations. */ class EncodingUtil { - static void hardAssert(boolean assertion) { hardAssert(assertion, "Internal inconsistency"); } @@ -283,6 +283,11 @@ static String propertyName(Field field) { return annotatedName != null ? annotatedName : field.getName(); } + static String propertyName(Method method) { + String annotatedName = EncodingUtil.annotatedName(method); + return annotatedName != null ? annotatedName : serializedName(method.getName()); + } + static String annotatedName(AccessibleObject obj) { if (obj.isAnnotationPresent(PropertyName.class)) { PropertyName annotation = obj.getAnnotation(PropertyName.class); @@ -292,12 +297,7 @@ static String annotatedName(AccessibleObject obj) { return null; } - static String propertyName(Method method) { - String annotatedName = EncodingUtil.annotatedName(method); - return annotatedName != null ? annotatedName : serializedName(method.getName()); - } - - private static String serializedName(String methodName) { + static String serializedName(String methodName) { String[] prefixes = new String[] {"get", "set", "is"}; String methodPrefix = null; for (String prefix : prefixes) { From 28465afaa6effbc86af892db5222cffce53d7b36 Mon Sep 17 00:00:00 2001 From: milaGGL <107142260+milaGGL@users.noreply.github.com> Date: Fri, 13 Sep 2024 14:25:49 -0400 Subject: [PATCH 50/51] remove EncodingUtil to make methods as private as possible --- .../cloud/firestore/encoding/BeanMapper.java | 71 ++- .../firestore/encoding/CustomClassMapper.java | 298 +++++++++++- .../firestore/encoding/EncodingUtil.java | 446 ------------------ .../firestore/encoding/PojoBeanMapper.java | 175 ++++++- .../firestore/encoding/RecordMapper.java | 2 +- 5 files changed, 494 insertions(+), 498 deletions(-) delete mode 100644 google-cloud-firestore/src/main/java/com/google/cloud/firestore/encoding/EncodingUtil.java diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/encoding/BeanMapper.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/encoding/BeanMapper.java index 276410735..fd5067b08 100644 --- a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/encoding/BeanMapper.java +++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/encoding/BeanMapper.java @@ -16,17 +16,22 @@ package com.google.cloud.firestore.encoding; +import com.google.cloud.Timestamp; +import com.google.cloud.firestore.DocumentReference; import com.google.cloud.firestore.FieldValue; import com.google.cloud.firestore.annotation.DocumentId; import com.google.cloud.firestore.annotation.IgnoreExtraProperties; +import com.google.cloud.firestore.annotation.PropertyName; import com.google.cloud.firestore.annotation.ServerTimestamp; import com.google.cloud.firestore.annotation.ThrowOnExtraProperties; +import java.lang.reflect.AccessibleObject; import java.lang.reflect.Field; -import java.lang.reflect.Method; import java.lang.reflect.Type; import java.lang.reflect.TypeVariable; +import java.time.Instant; import java.util.Collection; import java.util.Collections; +import java.util.Date; import java.util.HashSet; import java.util.Map; @@ -144,40 +149,56 @@ protected Object getSerializedValue( protected void applyFieldAnnotations(Field field) { Class fieldType = field.getType(); if (field.isAnnotationPresent(ServerTimestamp.class)) { - EncodingUtil.validateServerTimestampType("Field", "is", fieldType); - serverTimestamps.add(EncodingUtil.propertyName(field)); + validateServerTimestampType("Field", "is", fieldType); + serverTimestamps.add(propertyName(field)); } if (field.isAnnotationPresent(DocumentId.class)) { - EncodingUtil.validateDocumentIdType("Field", "is", fieldType); - documentIdPropertyNames.add(EncodingUtil.propertyName(field)); + validateDocumentIdType("Field", "is", fieldType); + documentIdPropertyNames.add(propertyName(field)); } } - protected void applyGetterAnnotations(Method method) { - Class returnType = method.getReturnType(); - if (method.isAnnotationPresent(ServerTimestamp.class)) { - EncodingUtil.validateServerTimestampType("Method", "returns", returnType); - serverTimestamps.add(EncodingUtil.propertyName(method)); - } - // Even though the value will be skipped, we still check for type matching for consistency. - if (method.isAnnotationPresent(DocumentId.class)) { - EncodingUtil.validateDocumentIdType("Method", "returns", returnType); - documentIdPropertyNames.add(EncodingUtil.propertyName(method)); + protected void validateDocumentIdType(String fieldDescription, String operation, Type type) { + if (type != String.class && type != DocumentReference.class) { + throw new IllegalArgumentException( + fieldDescription + + " is annotated with @DocumentId but " + + operation + + " " + + type + + " instead of String or DocumentReference."); } } - protected void applySetterAnnotations(Method method) { - if (method.isAnnotationPresent(ServerTimestamp.class)) { + protected void validateServerTimestampType(String fieldDescription, String operation, Type type) { + if (type != Date.class && type != Timestamp.class && type != Instant.class) { throw new IllegalArgumentException( - "Method " - + method.getName() - + " is annotated with @ServerTimestamp but should not be. @ServerTimestamp can" - + " only be applied to fields and getters, not setters."); + fieldDescription + + " is annotated with @ServerTimestamp but " + + operation + + " " + + type + + " instead of Date, Timestamp, or Instant."); + } + } + + protected String propertyName(Field field) { + String annotatedName = annotatedName(field); + return annotatedName != null ? annotatedName : field.getName(); + } + + protected String annotatedName(AccessibleObject obj) { + if (obj.isAnnotationPresent(PropertyName.class)) { + PropertyName annotation = obj.getAnnotation(PropertyName.class); + return annotation.value(); } - if (method.isAnnotationPresent(DocumentId.class)) { - Class paramType = method.getParameterTypes()[0]; - EncodingUtil.validateDocumentIdType("Method", "accepts", paramType); - documentIdPropertyNames.add(EncodingUtil.propertyName(method)); + + return null; + } + + protected void hardAssert(boolean assertion, String message) { + if (!assertion) { + throw new RuntimeException("Hard assert failed: " + message); } } } diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/encoding/CustomClassMapper.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/encoding/CustomClassMapper.java index 72dc9f243..321ead3e6 100644 --- a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/encoding/CustomClassMapper.java +++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/encoding/CustomClassMapper.java @@ -24,7 +24,9 @@ import com.google.cloud.firestore.GeoPoint; import com.google.cloud.firestore.VectorValue; import com.google.cloud.firestore.annotation.DocumentId; +import com.google.cloud.firestore.annotation.PropertyName; import com.google.firestore.v1.Value; +import java.lang.reflect.AccessibleObject; import java.lang.reflect.Field; import java.lang.reflect.GenericArrayType; import java.lang.reflect.InvocationTargetException; @@ -65,7 +67,7 @@ public static Object convertToPlainJavaTypes(Object object) { public static Map convertToPlainJavaTypes(Map update) { Object converted = serialize(update); - EncodingUtil.hardAssert(converted instanceof Map); + hardAssert(converted instanceof Map); @SuppressWarnings("unchecked") Map convertedMap = (Map) converted; return convertedMap; @@ -147,7 +149,7 @@ static Object serialize(T o, DeserializeContext.ErrorPath path) { String enumName = ((Enum) o).name(); try { Field enumField = o.getClass().getField(enumName); - return EncodingUtil.propertyName(enumField); + return propertyName(enumField); } catch (NoSuchFieldException ex) { return enumName; } @@ -190,13 +192,12 @@ static T deserializeToType(Object o, Type type, DeserializeContext context) // that this array always has at least one element, since the unbounded wildcard always // has at least an upper bound of Object. Type[] upperBounds = ((WildcardType) type).getUpperBounds(); - EncodingUtil.hardAssert(upperBounds.length > 0, "Unexpected type bounds on wildcard " + type); + hardAssert(upperBounds.length > 0, "Unexpected type bounds on wildcard " + type); return deserializeToType(o, upperBounds[0], context); } else if (type instanceof TypeVariable) { // As above, TypeVariables always have at least one upper bound of Object. Type[] upperBounds = ((TypeVariable) type).getBounds(); - EncodingUtil.hardAssert( - upperBounds.length > 0, "Unexpected type bounds on type variable " + type); + hardAssert(upperBounds.length > 0, "Unexpected type bounds on type variable " + type); return deserializeToType(o, upperBounds[0], context); } else if (type instanceof GenericArrayType) { @@ -248,7 +249,7 @@ private static T deserializeToParameterizedType( throw context.errorPath.deserializeError( "Only Maps with string keys are supported, but found Map with key type " + keyType); } - Map map = EncodingUtil.expectMap(o, context); + Map map = expectMap(o, context.errorPath); HashMap result; try { result = @@ -276,7 +277,7 @@ private static T deserializeToParameterizedType( throw context.errorPath.deserializeError( "Collections are not supported, please use Lists instead"); } else { - Map map = EncodingUtil.expectMap(o, context); + Map map = expectMap(o, context.errorPath); BeanMapper mapper = (BeanMapper) loadOrCreateBeanMapperForClass(rawType); HashMap>, Type> typeMapping = new HashMap<>(); TypeVariable>[] typeVariables = mapper.getClazz().getTypeParameters(); @@ -299,23 +300,23 @@ private static T deserializeToClass(Object o, Class clazz, DeserializeCon || Number.class.isAssignableFrom(clazz) || Boolean.class.isAssignableFrom(clazz) || Character.class.isAssignableFrom(clazz)) { - return EncodingUtil.deserializeToPrimitive(o, clazz, context); + return deserializeToPrimitive(o, clazz, context.errorPath); } else if (String.class.isAssignableFrom(clazz)) { - return (T) EncodingUtil.convertString(o, context); + return (T) convertString(o, context.errorPath); } else if (Date.class.isAssignableFrom(clazz)) { - return (T) EncodingUtil.convertDate(o, context); + return (T) convertDate(o, context.errorPath); } else if (Timestamp.class.isAssignableFrom(clazz)) { - return (T) EncodingUtil.convertTimestamp(o, context); + return (T) convertTimestamp(o, context.errorPath); } else if (Instant.class.isAssignableFrom(clazz)) { - return (T) EncodingUtil.convertInstant(o, context); + return (T) convertInstant(o, context.errorPath); } else if (Blob.class.isAssignableFrom(clazz)) { - return (T) EncodingUtil.convertBlob(o, context); + return (T) convertBlob(o, context.errorPath); } else if (GeoPoint.class.isAssignableFrom(clazz)) { - return (T) EncodingUtil.convertGeoPoint(o, context); + return (T) convertGeoPoint(o, context.errorPath); } else if (VectorValue.class.isAssignableFrom(clazz)) { - return (T) EncodingUtil.convertVectorValue(o, context); + return (T) convertVectorValue(o, context.errorPath); } else if (DocumentReference.class.isAssignableFrom(clazz)) { - return (T) EncodingUtil.convertDocumentReference(o, context); + return (T) convertDocumentReference(o, context.errorPath); } else if (clazz.isArray()) { throw context.errorPath.deserializeError( "Converting to Arrays is not supported, please use Lists instead"); @@ -327,7 +328,7 @@ private static T deserializeToClass(Object o, Class clazz, DeserializeCon } else if (clazz.equals(Object.class)) { return (T) o; } else if (clazz.isEnum()) { - return EncodingUtil.deserializeToEnum(o, clazz, context); + return deserializeToEnum(o, clazz, context.errorPath); } else { return convertBean(o, clazz, context); } @@ -336,7 +337,7 @@ private static T deserializeToClass(Object o, Class clazz, DeserializeCon private static T convertBean(Object o, Class clazz, DeserializeContext context) { BeanMapper mapper = loadOrCreateBeanMapperForClass(clazz); if (o instanceof Map) { - return mapper.deserialize(EncodingUtil.expectMap(o, context), context); + return mapper.deserialize(expectMap(o, context.errorPath), context); } else { throw context.errorPath.deserializeError( "Can't convert object of type " + o.getClass().getName() + " to type " + clazz.getName()); @@ -359,8 +360,269 @@ private static BeanMapper loadOrCreateBeanMapperForClass(Class clazz) return mapper; } + @SuppressWarnings("unchecked") + private static Map expectMap( + Object object, DeserializeContext.ErrorPath errorPath) { + if (object instanceof Map) { + // TODO: runtime validation of keys? + return (Map) object; + } else { + throw errorPath.deserializeError( + "Expected a Map while deserializing, but got a " + object.getClass()); + } + } + + @SuppressWarnings("unchecked") + private static T deserializeToPrimitive( + Object o, Class clazz, DeserializeContext.ErrorPath errorPath) { + if (Integer.class.isAssignableFrom(clazz) || int.class.isAssignableFrom(clazz)) { + return (T) convertInteger(o, errorPath); + } else if (Boolean.class.isAssignableFrom(clazz) || boolean.class.isAssignableFrom(clazz)) { + return (T) convertBoolean(o, errorPath); + } else if (Double.class.isAssignableFrom(clazz) || double.class.isAssignableFrom(clazz)) { + return (T) convertDouble(o, errorPath); + } else if (Long.class.isAssignableFrom(clazz) || long.class.isAssignableFrom(clazz)) { + return (T) convertLong(o, errorPath); + } else if (BigDecimal.class.isAssignableFrom(clazz)) { + return (T) convertBigDecimal(o, errorPath); + } else if (Float.class.isAssignableFrom(clazz) || float.class.isAssignableFrom(clazz)) { + return (T) (Float) convertDouble(o, errorPath).floatValue(); + } else { + throw errorPath.deserializeError( + String.format("Deserializing values to %s is not supported", clazz.getSimpleName())); + } + } + + @SuppressWarnings("unchecked") + private static T deserializeToEnum( + Object object, Class clazz, DeserializeContext.ErrorPath errorPath) { + if (object instanceof String) { + String value = (String) object; + // We cast to Class without generics here since we can't prove the bound + // T extends Enum statically + + // try to use PropertyName if exist + Field[] enumFields = clazz.getFields(); + for (Field field : enumFields) { + if (field.isEnumConstant()) { + String propertyName = propertyName(field); + if (value.equals(propertyName)) { + value = field.getName(); + break; + } + } + } + + try { + return (T) Enum.valueOf((Class) clazz, value); + } catch (IllegalArgumentException e) { + throw errorPath.deserializeError( + "Could not find enum value of " + clazz.getName() + " for value \"" + value + "\""); + } + } else { + throw errorPath.deserializeError( + "Expected a String while deserializing to enum " + + clazz + + " but got a " + + object.getClass()); + } + } + + private static Integer convertInteger(Object o, DeserializeContext.ErrorPath errorPath) { + if (o instanceof Integer) { + return (Integer) o; + } else if (o instanceof Long || o instanceof Double) { + double value = ((Number) o).doubleValue(); + if (value >= Integer.MIN_VALUE && value <= Integer.MAX_VALUE) { + return ((Number) o).intValue(); + } else { + throw errorPath.deserializeError( + "Numeric value out of 32-bit integer range: " + + value + + ". Did you mean to use a long or double instead of an int?"); + } + } else { + throw errorPath.deserializeError( + "Failed to convert a value of type " + o.getClass().getName() + " to int"); + } + } + + private static Long convertLong(Object o, DeserializeContext.ErrorPath errorPath) { + if (o instanceof Integer) { + return ((Integer) o).longValue(); + } else if (o instanceof Long) { + return (Long) o; + } else if (o instanceof Double) { + Double value = (Double) o; + if (value >= Long.MIN_VALUE && value <= Long.MAX_VALUE) { + return value.longValue(); + } else { + throw errorPath.deserializeError( + "Numeric value out of 64-bit long range: " + + value + + ". Did you mean to use a double instead of a long?"); + } + } else { + throw errorPath.deserializeError( + "Failed to convert a value of type " + o.getClass().getName() + " to long"); + } + } + + private static Double convertDouble(Object o, DeserializeContext.ErrorPath errorPath) { + if (o instanceof Integer) { + return ((Integer) o).doubleValue(); + } else if (o instanceof Long) { + Long value = (Long) o; + Double doubleValue = ((Long) o).doubleValue(); + if (doubleValue.longValue() == value) { + return doubleValue; + } else { + throw errorPath.deserializeError( + "Loss of precision while converting number to " + + "double: " + + o + + ". Did you mean to use a 64-bit long instead?"); + } + } else if (o instanceof Double) { + return (Double) o; + } else { + throw errorPath.deserializeError( + "Failed to convert a value of type " + o.getClass().getName() + " to double"); + } + } + + private static BigDecimal convertBigDecimal(Object o, DeserializeContext.ErrorPath errorPath) { + if (o instanceof Integer) { + return BigDecimal.valueOf(((Integer) o).intValue()); + } else if (o instanceof Long) { + return BigDecimal.valueOf(((Long) o).longValue()); + } else if (o instanceof Double) { + return BigDecimal.valueOf(((Double) o).doubleValue()).abs(); + } else if (o instanceof BigDecimal) { + return (BigDecimal) o; + } else if (o instanceof String) { + return new BigDecimal((String) o); + } else { + throw errorPath.deserializeError( + "Failed to convert a value of type " + o.getClass().getName() + " to BigDecimal"); + } + } + + private static Boolean convertBoolean(Object o, DeserializeContext.ErrorPath errorPath) { + if (o instanceof Boolean) { + return (Boolean) o; + } else { + throw errorPath.deserializeError( + "Failed to convert value of type " + o.getClass().getName() + " to boolean"); + } + } + + private static String convertString(Object o, DeserializeContext.ErrorPath errorPath) { + if (o instanceof String) { + return (String) o; + } else { + throw errorPath.deserializeError( + "Failed to convert value of type " + o.getClass().getName() + " to String"); + } + } + + private static Date convertDate(Object o, DeserializeContext.ErrorPath errorPath) { + if (o instanceof Date) { + return (Date) o; + } else if (o instanceof Timestamp) { + return ((Timestamp) o).toDate(); + } else { + throw errorPath.deserializeError( + "Failed to convert value of type " + o.getClass().getName() + " to Date"); + } + } + + private static Timestamp convertTimestamp(Object o, DeserializeContext.ErrorPath errorPath) { + if (o instanceof Timestamp) { + return (Timestamp) o; + } else if (o instanceof Date) { + return Timestamp.of((Date) o); + } else { + throw errorPath.deserializeError( + "Failed to convert value of type " + o.getClass().getName() + " to Timestamp"); + } + } + + private static Instant convertInstant(Object o, DeserializeContext.ErrorPath errorPath) { + if (o instanceof Timestamp) { + Timestamp timestamp = (Timestamp) o; + return Instant.ofEpochSecond(timestamp.getSeconds(), timestamp.getNanos()); + } else if (o instanceof Date) { + return Instant.ofEpochMilli(((Date) o).getTime()); + } else { + throw errorPath.deserializeError( + "Failed to convert value of type " + o.getClass().getName() + " to Instant"); + } + } + + private static Blob convertBlob(Object o, DeserializeContext.ErrorPath errorPath) { + if (o instanceof Blob) { + return (Blob) o; + } else { + throw errorPath.deserializeError( + "Failed to convert value of type " + o.getClass().getName() + " to Blob"); + } + } + + private static GeoPoint convertGeoPoint(Object o, DeserializeContext.ErrorPath errorPath) { + if (o instanceof GeoPoint) { + return (GeoPoint) o; + } else { + throw errorPath.deserializeError( + "Failed to convert value of type " + o.getClass().getName() + " to GeoPoint"); + } + } + + private static VectorValue convertVectorValue(Object o, DeserializeContext.ErrorPath errorPath) { + if (o instanceof VectorValue) { + return (VectorValue) o; + } else { + throw errorPath.deserializeError( + "Failed to convert value of type " + o.getClass().getName() + " to VectorValue"); + } + } + + private static DocumentReference convertDocumentReference( + Object o, DeserializeContext.ErrorPath errorPath) { + if (o instanceof DocumentReference) { + return (DocumentReference) o; + } else { + throw errorPath.deserializeError( + "Failed to convert value of type " + o.getClass().getName() + " to DocumentReference"); + } + } + private static boolean isRecordType(Class cls) { Class parent = cls.getSuperclass(); return parent != null && "java.lang.Record".equals(parent.getName()); } + + private static String propertyName(Field field) { + String annotatedName = annotatedName(field); + return annotatedName != null ? annotatedName : field.getName(); + } + + private static String annotatedName(AccessibleObject obj) { + if (obj.isAnnotationPresent(PropertyName.class)) { + PropertyName annotation = obj.getAnnotation(PropertyName.class); + return annotation.value(); + } + + return null; + } + + private static void hardAssert(boolean assertion) { + hardAssert(assertion, "Internal inconsistency"); + } + + private static void hardAssert(boolean assertion, String message) { + if (!assertion) { + throw new RuntimeException("Hard assert failed: " + message); + } + } } diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/encoding/EncodingUtil.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/encoding/EncodingUtil.java deleted file mode 100644 index 5a1d26aa7..000000000 --- a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/encoding/EncodingUtil.java +++ /dev/null @@ -1,446 +0,0 @@ -/* - * Copyright 2024 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.google.cloud.firestore.encoding; - -import com.google.cloud.Timestamp; -import com.google.cloud.firestore.Blob; -import com.google.cloud.firestore.DocumentReference; -import com.google.cloud.firestore.GeoPoint; -import com.google.cloud.firestore.VectorValue; -import com.google.cloud.firestore.annotation.Exclude; -import com.google.cloud.firestore.annotation.PropertyName; -import java.lang.reflect.AccessibleObject; -import java.lang.reflect.Field; -import java.lang.reflect.Method; -import java.lang.reflect.Modifier; -import java.lang.reflect.Type; -import java.math.BigDecimal; -import java.time.Instant; -import java.util.Date; -import java.util.Map; - -/** A utility class for serializing and deserializing operations. */ -class EncodingUtil { - static void hardAssert(boolean assertion) { - hardAssert(assertion, "Internal inconsistency"); - } - - static void hardAssert(boolean assertion, String message) { - if (!assertion) { - throw new RuntimeException("Hard assert failed: " + message); - } - } - - @SuppressWarnings("unchecked") - static T deserializeToPrimitive(Object o, Class clazz, DeserializeContext context) { - if (Integer.class.isAssignableFrom(clazz) || int.class.isAssignableFrom(clazz)) { - return (T) convertInteger(o, context); - } else if (Boolean.class.isAssignableFrom(clazz) || boolean.class.isAssignableFrom(clazz)) { - return (T) convertBoolean(o, context); - } else if (Double.class.isAssignableFrom(clazz) || double.class.isAssignableFrom(clazz)) { - return (T) convertDouble(o, context); - } else if (Long.class.isAssignableFrom(clazz) || long.class.isAssignableFrom(clazz)) { - return (T) convertLong(o, context); - } else if (BigDecimal.class.isAssignableFrom(clazz)) { - return (T) convertBigDecimal(o, context); - } else if (Float.class.isAssignableFrom(clazz) || float.class.isAssignableFrom(clazz)) { - return (T) (Float) convertDouble(o, context).floatValue(); - } else { - throw context.errorPath.deserializeError( - String.format("Deserializing values to %s is not supported", clazz.getSimpleName())); - } - } - - @SuppressWarnings("unchecked") - static T deserializeToEnum(Object object, Class clazz, DeserializeContext context) { - if (object instanceof String) { - String value = (String) object; - // We cast to Class without generics here since we can't prove the bound - // T extends Enum statically - - // try to use PropertyName if exist - Field[] enumFields = clazz.getFields(); - for (Field field : enumFields) { - if (field.isEnumConstant()) { - String propertyName = propertyName(field); - if (value.equals(propertyName)) { - value = field.getName(); - break; - } - } - } - - try { - return (T) Enum.valueOf((Class) clazz, value); - } catch (IllegalArgumentException e) { - throw context.errorPath.deserializeError( - "Could not find enum value of " + clazz.getName() + " for value \"" + value + "\""); - } - } else { - throw context.errorPath.deserializeError( - "Expected a String while deserializing to enum " - + clazz - + " but got a " - + object.getClass()); - } - } - - @SuppressWarnings("unchecked") - static Map expectMap(Object object, DeserializeContext context) { - if (object instanceof Map) { - // TODO: runtime validation of keys? - return (Map) object; - } else { - throw context.errorPath.deserializeError( - "Expected a Map while deserializing, but got a " + object.getClass()); - } - } - - private static Integer convertInteger(Object o, DeserializeContext context) { - if (o instanceof Integer) { - return (Integer) o; - } else if (o instanceof Long || o instanceof Double) { - double value = ((Number) o).doubleValue(); - if (value >= Integer.MIN_VALUE && value <= Integer.MAX_VALUE) { - return ((Number) o).intValue(); - } else { - throw context.errorPath.deserializeError( - "Numeric value out of 32-bit integer range: " - + value - + ". Did you mean to use a long or double instead of an int?"); - } - } else { - throw context.errorPath.deserializeError( - "Failed to convert a value of type " + o.getClass().getName() + " to int"); - } - } - - private static Long convertLong(Object o, DeserializeContext context) { - if (o instanceof Integer) { - return ((Integer) o).longValue(); - } else if (o instanceof Long) { - return (Long) o; - } else if (o instanceof Double) { - Double value = (Double) o; - if (value >= Long.MIN_VALUE && value <= Long.MAX_VALUE) { - return value.longValue(); - } else { - throw context.errorPath.deserializeError( - "Numeric value out of 64-bit long range: " - + value - + ". Did you mean to use a double instead of a long?"); - } - } else { - throw context.errorPath.deserializeError( - "Failed to convert a value of type " + o.getClass().getName() + " to long"); - } - } - - private static Double convertDouble(Object o, DeserializeContext context) { - if (o instanceof Integer) { - return ((Integer) o).doubleValue(); - } else if (o instanceof Long) { - Long value = (Long) o; - Double doubleValue = ((Long) o).doubleValue(); - if (doubleValue.longValue() == value) { - return doubleValue; - } else { - throw context.errorPath.deserializeError( - "Loss of precision while converting number to " - + "double: " - + o - + ". Did you mean to use a 64-bit long instead?"); - } - } else if (o instanceof Double) { - return (Double) o; - } else { - throw context.errorPath.deserializeError( - "Failed to convert a value of type " + o.getClass().getName() + " to double"); - } - } - - private static BigDecimal convertBigDecimal(Object o, DeserializeContext context) { - if (o instanceof Integer) { - return BigDecimal.valueOf(((Integer) o).intValue()); - } else if (o instanceof Long) { - return BigDecimal.valueOf(((Long) o).longValue()); - } else if (o instanceof Double) { - return BigDecimal.valueOf(((Double) o).doubleValue()).abs(); - } else if (o instanceof BigDecimal) { - return (BigDecimal) o; - } else if (o instanceof String) { - return new BigDecimal((String) o); - } else { - throw context.errorPath.deserializeError( - "Failed to convert a value of type " + o.getClass().getName() + " to BigDecimal"); - } - } - - private static Boolean convertBoolean(Object o, DeserializeContext context) { - if (o instanceof Boolean) { - return (Boolean) o; - } else { - throw context.errorPath.deserializeError( - "Failed to convert value of type " + o.getClass().getName() + " to boolean"); - } - } - - static String convertString(Object o, DeserializeContext context) { - if (o instanceof String) { - return (String) o; - } else { - throw context.errorPath.deserializeError( - "Failed to convert value of type " + o.getClass().getName() + " to String"); - } - } - - static Date convertDate(Object o, DeserializeContext context) { - if (o instanceof Date) { - return (Date) o; - } else if (o instanceof Timestamp) { - return ((Timestamp) o).toDate(); - } else { - throw context.errorPath.deserializeError( - "Failed to convert value of type " + o.getClass().getName() + " to Date"); - } - } - - static Timestamp convertTimestamp(Object o, DeserializeContext context) { - if (o instanceof Timestamp) { - return (Timestamp) o; - } else if (o instanceof Date) { - return Timestamp.of((Date) o); - } else { - throw context.errorPath.deserializeError( - "Failed to convert value of type " + o.getClass().getName() + " to Timestamp"); - } - } - - static Instant convertInstant(Object o, DeserializeContext context) { - if (o instanceof Timestamp) { - Timestamp timestamp = (Timestamp) o; - return Instant.ofEpochSecond(timestamp.getSeconds(), timestamp.getNanos()); - } else if (o instanceof Date) { - return Instant.ofEpochMilli(((Date) o).getTime()); - } else { - throw context.errorPath.deserializeError( - "Failed to convert value of type " + o.getClass().getName() + " to Instant"); - } - } - - static Blob convertBlob(Object o, DeserializeContext context) { - if (o instanceof Blob) { - return (Blob) o; - } else { - throw context.errorPath.deserializeError( - "Failed to convert value of type " + o.getClass().getName() + " to Blob"); - } - } - - static GeoPoint convertGeoPoint(Object o, DeserializeContext context) { - if (o instanceof GeoPoint) { - return (GeoPoint) o; - } else { - throw context.errorPath.deserializeError( - "Failed to convert value of type " + o.getClass().getName() + " to GeoPoint"); - } - } - - static VectorValue convertVectorValue(Object o, DeserializeContext context) { - if (o instanceof VectorValue) { - return (VectorValue) o; - } else { - throw context.errorPath.deserializeError( - "Failed to convert value of type " + o.getClass().getName() + " to VectorValue"); - } - } - - static DocumentReference convertDocumentReference(Object o, DeserializeContext context) { - if (o instanceof DocumentReference) { - return (DocumentReference) o; - } else { - throw context.errorPath.deserializeError( - "Failed to convert value of type " + o.getClass().getName() + " to DocumentReference"); - } - } - - static String propertyName(Field field) { - String annotatedName = annotatedName(field); - return annotatedName != null ? annotatedName : field.getName(); - } - - static String propertyName(Method method) { - String annotatedName = EncodingUtil.annotatedName(method); - return annotatedName != null ? annotatedName : serializedName(method.getName()); - } - - static String annotatedName(AccessibleObject obj) { - if (obj.isAnnotationPresent(PropertyName.class)) { - PropertyName annotation = obj.getAnnotation(PropertyName.class); - return annotation.value(); - } - - return null; - } - - static String serializedName(String methodName) { - String[] prefixes = new String[] {"get", "set", "is"}; - String methodPrefix = null; - for (String prefix : prefixes) { - if (methodName.startsWith(prefix)) { - methodPrefix = prefix; - } - } - if (methodPrefix == null) { - throw new IllegalArgumentException("Unknown Bean prefix for method: " + methodName); - } - String strippedName = methodName.substring(methodPrefix.length()); - - // Make sure the first word or upper-case prefix is converted to lower-case - char[] chars = strippedName.toCharArray(); - int pos = 0; - while (pos < chars.length && Character.isUpperCase(chars[pos])) { - chars[pos] = Character.toLowerCase(chars[pos]); - pos++; - } - return new String(chars); - } - - static void validateDocumentIdType(String fieldDescription, String operation, Type type) { - if (type != String.class && type != DocumentReference.class) { - throw new IllegalArgumentException( - fieldDescription - + " is annotated with @DocumentId but " - + operation - + " " - + type - + " instead of String or DocumentReference."); - } - } - - static void validateServerTimestampType(String fieldDescription, String operation, Type type) { - if (type != Date.class && type != Timestamp.class && type != Instant.class) { - throw new IllegalArgumentException( - fieldDescription - + " is annotated with @ServerTimestamp but " - + operation - + " " - + type - + " instead of Date, Timestamp, or Instant."); - } - } - - static boolean shouldIncludeGetter(Method method) { - if (!method.getName().startsWith("get") && !method.getName().startsWith("is")) { - return false; - } - // Exclude methods from Object.class - if (method.getDeclaringClass().equals(Object.class)) { - return false; - } - // Non-public methods - if (!Modifier.isPublic(method.getModifiers())) { - return false; - } - // Static methods - if (Modifier.isStatic(method.getModifiers())) { - return false; - } - // No return type - if (method.getReturnType().equals(Void.TYPE)) { - return false; - } - // Non-zero parameters - if (method.getParameterTypes().length != 0) { - return false; - } - // Excluded methods - if (method.isAnnotationPresent(Exclude.class)) { - return false; - } - return true; - } - - static boolean shouldIncludeSetter(Method method) { - if (!method.getName().startsWith("set")) { - return false; - } - // Exclude methods from Object.class - if (method.getDeclaringClass().equals(Object.class)) { - return false; - } - // Static methods - if (Modifier.isStatic(method.getModifiers())) { - return false; - } - // Has a return type - if (!method.getReturnType().equals(Void.TYPE)) { - return false; - } - // Methods without exactly one parameters - if (method.getParameterTypes().length != 1) { - return false; - } - // Excluded methods - if (method.isAnnotationPresent(Exclude.class)) { - return false; - } - return true; - } - - static boolean shouldIncludeField(Field field) { - // Exclude methods from Object.class - if (field.getDeclaringClass().equals(Object.class)) { - return false; - } - // Non-public fields - if (!Modifier.isPublic(field.getModifiers())) { - return false; - } - // Static fields - if (Modifier.isStatic(field.getModifiers())) { - return false; - } - // Transient fields - if (Modifier.isTransient(field.getModifiers())) { - return false; - } - // Excluded fields - if (field.isAnnotationPresent(Exclude.class)) { - return false; - } - return true; - } - - static boolean isSetterOverride(Method base, Method override) { - // We expect an overridden setter here - EncodingUtil.hardAssert( - base.getDeclaringClass().isAssignableFrom(override.getDeclaringClass()), - "Expected override from a base class"); - EncodingUtil.hardAssert(base.getReturnType().equals(Void.TYPE), "Expected void return type"); - EncodingUtil.hardAssert( - override.getReturnType().equals(Void.TYPE), "Expected void return type"); - - Type[] baseParameterTypes = base.getParameterTypes(); - Type[] overrideParameterTypes = override.getParameterTypes(); - EncodingUtil.hardAssert(baseParameterTypes.length == 1, "Expected exactly one parameter"); - EncodingUtil.hardAssert(overrideParameterTypes.length == 1, "Expected exactly one parameter"); - - return base.getName().equals(override.getName()) - && baseParameterTypes[0].equals(overrideParameterTypes[0]); - } -} diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/encoding/PojoBeanMapper.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/encoding/PojoBeanMapper.java index 92eb40997..adf7ef229 100644 --- a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/encoding/PojoBeanMapper.java +++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/encoding/PojoBeanMapper.java @@ -16,10 +16,14 @@ package com.google.cloud.firestore.encoding; +import com.google.cloud.firestore.annotation.DocumentId; +import com.google.cloud.firestore.annotation.Exclude; +import com.google.cloud.firestore.annotation.ServerTimestamp; import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; +import java.lang.reflect.Modifier; import java.lang.reflect.Type; import java.lang.reflect.TypeVariable; import java.util.HashMap; @@ -64,8 +68,8 @@ class PojoBeanMapper extends BeanMapper { this.constructor = constructor; // Add any public getters to properties (including isXyz()) for (Method method : clazz.getMethods()) { - if (EncodingUtil.shouldIncludeGetter(method)) { - String propertyName = EncodingUtil.propertyName(method); + if (shouldIncludeGetter(method)) { + String propertyName = propertyName(method); addProperty(propertyName); method.setAccessible(true); if (getters.containsKey(propertyName)) { @@ -82,8 +86,8 @@ class PojoBeanMapper extends BeanMapper { // Add any public fields to properties for (Field field : clazz.getFields()) { - if (EncodingUtil.shouldIncludeField(field)) { - String propertyName = EncodingUtil.propertyName(field); + if (shouldIncludeField(field)) { + String propertyName = propertyName(field); addProperty(propertyName); applyFieldAnnotations(field); } @@ -96,8 +100,8 @@ class PojoBeanMapper extends BeanMapper { do { // Add any setters for (Method method : currentClass.getDeclaredMethods()) { - if (EncodingUtil.shouldIncludeSetter(method)) { - String propertyName = EncodingUtil.propertyName(method); + if (shouldIncludeSetter(method)) { + String propertyName = propertyName(method); String existingPropertyName = properties.get(propertyName.toLowerCase(Locale.US)); if (existingPropertyName != null) { if (!existingPropertyName.equals(propertyName)) { @@ -112,7 +116,7 @@ class PojoBeanMapper extends BeanMapper { method.setAccessible(true); setters.put(propertyName, method); applySetterAnnotations(method); - } else if (!EncodingUtil.isSetterOverride(method, existingSetter)) { + } else if (!isSetterOverride(method, existingSetter)) { // We require that setters with conflicting property names are // overrides from a base class if (currentClass == clazz) { @@ -140,7 +144,7 @@ class PojoBeanMapper extends BeanMapper { } for (Field field : currentClass.getDeclaredFields()) { - String propertyName = EncodingUtil.propertyName(field); + String propertyName = propertyName(field); // Case sensitivity is checked at deserialization time // Fields are only added if they don't exist on a subclass @@ -332,4 +336,159 @@ private void populateDocumentIdProperties( } } } + + private void applyGetterAnnotations(Method method) { + Class returnType = method.getReturnType(); + if (method.isAnnotationPresent(ServerTimestamp.class)) { + validateServerTimestampType("Method", "returns", returnType); + serverTimestamps.add(propertyName(method)); + } + // Even though the value will be skipped, we still check for type matching for consistency. + if (method.isAnnotationPresent(DocumentId.class)) { + validateDocumentIdType("Method", "returns", returnType); + documentIdPropertyNames.add(propertyName(method)); + } + } + + private void applySetterAnnotations(Method method) { + if (method.isAnnotationPresent(ServerTimestamp.class)) { + throw new IllegalArgumentException( + "Method " + + method.getName() + + " is annotated with @ServerTimestamp but should not be. @ServerTimestamp can" + + " only be applied to fields and getters, not setters."); + } + if (method.isAnnotationPresent(DocumentId.class)) { + Class paramType = method.getParameterTypes()[0]; + validateDocumentIdType("Method", "accepts", paramType); + documentIdPropertyNames.add(propertyName(method)); + } + } + + private boolean shouldIncludeGetter(Method method) { + if (!method.getName().startsWith("get") && !method.getName().startsWith("is")) { + return false; + } + // Exclude methods from Object.class + if (method.getDeclaringClass().equals(Object.class)) { + return false; + } + // Non-public methods + if (!Modifier.isPublic(method.getModifiers())) { + return false; + } + // Static methods + if (Modifier.isStatic(method.getModifiers())) { + return false; + } + // No return type + if (method.getReturnType().equals(Void.TYPE)) { + return false; + } + // Non-zero parameters + if (method.getParameterTypes().length != 0) { + return false; + } + // Excluded methods + if (method.isAnnotationPresent(Exclude.class)) { + return false; + } + return true; + } + + private boolean shouldIncludeSetter(Method method) { + if (!method.getName().startsWith("set")) { + return false; + } + // Exclude methods from Object.class + if (method.getDeclaringClass().equals(Object.class)) { + return false; + } + // Static methods + if (Modifier.isStatic(method.getModifiers())) { + return false; + } + // Has a return type + if (!method.getReturnType().equals(Void.TYPE)) { + return false; + } + // Methods without exactly one parameters + if (method.getParameterTypes().length != 1) { + return false; + } + // Excluded methods + if (method.isAnnotationPresent(Exclude.class)) { + return false; + } + return true; + } + + private boolean shouldIncludeField(Field field) { + // Exclude methods from Object.class + if (field.getDeclaringClass().equals(Object.class)) { + return false; + } + // Non-public fields + if (!Modifier.isPublic(field.getModifiers())) { + return false; + } + // Static fields + if (Modifier.isStatic(field.getModifiers())) { + return false; + } + // Transient fields + if (Modifier.isTransient(field.getModifiers())) { + return false; + } + // Excluded fields + if (field.isAnnotationPresent(Exclude.class)) { + return false; + } + return true; + } + + private boolean isSetterOverride(Method base, Method override) { + // We expect an overridden setter here + hardAssert( + base.getDeclaringClass().isAssignableFrom(override.getDeclaringClass()), + "Expected override from a base class"); + hardAssert(base.getReturnType().equals(Void.TYPE), "Expected void return type"); + hardAssert(override.getReturnType().equals(Void.TYPE), "Expected void return type"); + + Type[] baseParameterTypes = base.getParameterTypes(); + Type[] overrideParameterTypes = override.getParameterTypes(); + hardAssert(baseParameterTypes.length == 1, "Expected exactly one parameter"); + hardAssert(overrideParameterTypes.length == 1, "Expected exactly one parameter"); + + return base.getName().equals(override.getName()) + && baseParameterTypes[0].equals(overrideParameterTypes[0]); + } + + private String propertyName(Method method) { + String annotatedName = annotatedName(method); + return annotatedName != null ? annotatedName : serializedName(method.getName()); + } + + private String serializedName(String methodName) { + String[] prefixes = new String[] {"get", "set", "is"}; + String methodPrefix = null; + for (String prefix : prefixes) { + if (methodName.startsWith(prefix)) { + methodPrefix = prefix; + } + } + if (methodPrefix == null) { + throw new IllegalArgumentException("Unknown Bean prefix for method: " + methodName); + } + String strippedName = methodName.substring(methodPrefix.length()); + + // Make sure the first word or upper-case prefix is converted to lower-case + char[] chars = strippedName.toCharArray(); + int pos = 0; + while (pos < chars.length && Character.isUpperCase(chars[pos])) { + chars[pos] = Character.toLowerCase(chars[pos]); + pos++; + } + return new String(chars); + } } diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/encoding/RecordMapper.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/encoding/RecordMapper.java index 7fc446737..b48501ce5 100644 --- a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/encoding/RecordMapper.java +++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/encoding/RecordMapper.java @@ -60,7 +60,7 @@ class RecordMapper extends BeanMapper { try { for (int i = 0; i < recordComponents.length; i++) { Field field = clazz.getDeclaredField(RECORD_INSPECTOR.getName(recordComponents[i])); - String propertyName = EncodingUtil.propertyName(field); + String propertyName = propertyName(field); constructorParamIndexes.put(propertyName, i); accessors.put(propertyName, RECORD_INSPECTOR.getAccessor(recordComponents[i])); applyFieldAnnotations(field); From 382d0aab9d2f2b1f9fe1594c31754538c62ed308 Mon Sep 17 00:00:00 2001 From: milaGGL <107142260+milaGGL@users.noreply.github.com> Date: Mon, 16 Sep 2024 12:03:54 -0400 Subject: [PATCH 51/51] remove redundant constructor null check --- .../com/google/cloud/firestore/encoding/RecordMapper.java | 7 ------- 1 file changed, 7 deletions(-) diff --git a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/encoding/RecordMapper.java b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/encoding/RecordMapper.java index b48501ce5..f15f65823 100644 --- a/google-cloud-firestore/src/main/java/com/google/cloud/firestore/encoding/RecordMapper.java +++ b/google-cloud-firestore/src/main/java/com/google/cloud/firestore/encoding/RecordMapper.java @@ -101,13 +101,6 @@ T deserialize( Map values, Map>, Type> types, DeserializeContext context) { - if (constructor == null) { - throw context.errorPath.deserializeError( - "Class " - + getClazz().getName() - + " does not define a no-argument constructor. If you are using ProGuard, make " - + "sure these constructors are not stripped"); - } Object[] constructorParams = new Object[constructor.getParameterCount()]; Set deserializedProperties = new HashSet<>(values.size()); for (Map.Entry entry : values.entrySet()) {