From 72fc2b89ee98fb6e2317b3136fa2dcc9500ee492 Mon Sep 17 00:00:00 2001 From: eranl <1707552+eranl@users.noreply.github.com> Date: Sat, 16 Sep 2023 23:50:21 +0300 Subject: [PATCH 01/11] java records --- build.gradle | 4 +- buildSrc/build.gradle.kts | 8 +- .../firebase/gradle/plugins/Metalava.kt | 2 + .../bandwagoner/bandwagoner.gradle | 1 + .../firebase-dynamic-links.gradle | 3 + firebase-firestore-sdk34/CHANGELOG.md | 0 firebase-firestore-sdk34/README.md | 118 ++ .../firebase-firestore-sdk34.gradle | 184 +++ firebase-firestore-sdk34/gradle.properties | 2 + firebase-firestore-sdk34/lint.xml | 27 + firebase-firestore-sdk34/proguard.txt | 18 + .../firebase/firestore/sdk34/DocumentId.java | 46 + .../firestore/sdk34/PropertyName.java | 30 + .../firestore/sdk34/ServerTimestamp.java | 31 + .../google/firebase/firestore/TestUtil.java | 126 ++ .../src/test/AndroidManifest.xml | 14 + .../firestore/sdk34/LocalFirestoreHelper.java | 406 ++++++ .../sdk34/RecordDocumentReferenceTest.java | 332 +++++ .../sdk34/util/RecordMapperTest.java | 1104 +++++++++++++++++ firebase-firestore/gradle.properties | 2 +- firebase-firestore/ktx/ktx.gradle | 4 + .../firebase/firestore/util/BeanMapper.java | 132 ++ .../firestore/util/CustomClassMapper.java | 199 +-- .../firestore/util/DeserializeContext.java | 76 ++ .../firebase/firestore/util/RecordMapper.java | 307 +++++ firebase-perf/dev-app/dev-app.gradle | 1 + firebase-perf/e2e-app/e2e-app.gradle | 1 + gradle.properties | 8 +- gradle/libs.versions.toml | 12 +- gradle/wrapper/gradle-wrapper.properties | 2 +- subprojects.cfg | 39 +- 31 files changed, 3061 insertions(+), 178 deletions(-) create mode 100755 firebase-firestore-sdk34/CHANGELOG.md create mode 100755 firebase-firestore-sdk34/README.md create mode 100755 firebase-firestore-sdk34/firebase-firestore-sdk34.gradle create mode 100755 firebase-firestore-sdk34/gradle.properties create mode 100755 firebase-firestore-sdk34/lint.xml create mode 100755 firebase-firestore-sdk34/proguard.txt create mode 100755 firebase-firestore-sdk34/src/main/java/com/google/firebase/firestore/sdk34/DocumentId.java create mode 100755 firebase-firestore-sdk34/src/main/java/com/google/firebase/firestore/sdk34/PropertyName.java create mode 100755 firebase-firestore-sdk34/src/main/java/com/google/firebase/firestore/sdk34/ServerTimestamp.java create mode 100755 firebase-firestore-sdk34/src/roboUtil/java/com/google/firebase/firestore/TestUtil.java create mode 100755 firebase-firestore-sdk34/src/test/AndroidManifest.xml create mode 100755 firebase-firestore-sdk34/src/test/java/com/google/firebase/firestore/sdk34/LocalFirestoreHelper.java create mode 100755 firebase-firestore-sdk34/src/test/java/com/google/firebase/firestore/sdk34/RecordDocumentReferenceTest.java create mode 100755 firebase-firestore-sdk34/src/test/java/com/google/firebase/firestore/sdk34/util/RecordMapperTest.java create mode 100755 firebase-firestore/src/main/java/com/google/firebase/firestore/util/BeanMapper.java create mode 100755 firebase-firestore/src/main/java/com/google/firebase/firestore/util/DeserializeContext.java create mode 100755 firebase-firestore/src/main/java/com/google/firebase/firestore/util/RecordMapper.java diff --git a/build.gradle b/build.gradle index 5b4a27741e9..c0a4afb5fe4 100644 --- a/build.gradle +++ b/build.gradle @@ -33,7 +33,7 @@ buildscript { } dependencies { - classpath 'com.google.protobuf:protobuf-gradle-plugin:0.9.2' + classpath libs.protobuf.gradle.plugin classpath 'net.ltgt.gradle:gradle-errorprone-plugin:1.3.0' classpath 'gradle.plugin.com.github.sherter.google-java-format:google-java-format-gradle-plugin:0.9' classpath 'com.google.gms:google-services:4.3.15' @@ -71,9 +71,11 @@ firebaseContinuousIntegration { ] } +/* if(JavaVersion.current() != JavaVersion.VERSION_11){ throw new GradleException("This build must be run with java 11. You're using ${JavaVersion.current()}.") } +*/ configure(subprojects) { repositories { diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts index 0695e90f00d..86925948c81 100644 --- a/buildSrc/build.gradle.kts +++ b/buildSrc/build.gradle.kts @@ -15,7 +15,7 @@ plugins { id("com.ncorti.ktfmt.gradle") version "0.11.0" id("com.github.sherter.google-java-format") version "0.9" - kotlin("plugin.serialization") version "1.7.10" + kotlin("plugin.serialization") version "1.8.10" `kotlin-dsl` } @@ -48,7 +48,7 @@ dependencies { implementation("com.google.firebase:perf-plugin:$perfPluginVersion") implementation("com.google.auto.value:auto-value-annotations:1.8.1") annotationProcessor("com.google.auto.value:auto-value:1.6.5") - implementation(kotlin("gradle-plugin", "1.7.10")) + implementation(kotlin("gradle-plugin", "1.8.10")) implementation("org.json:json:20210307") implementation("org.eclipse.aether:aether-api:1.0.0.v20140518") @@ -65,8 +65,8 @@ dependencies { implementation(libs.kotlinx.serialization.json) implementation("com.google.code.gson:gson:2.8.9") - implementation("com.android.tools.build:gradle:7.4.2") - implementation("com.android.tools.build:builder-test-api:7.4.2") + implementation(libs.gradle) + implementation(libs.builder.test.api) implementation("gradle.plugin.com.github.sherter.google-java-format:google-java-format-gradle-plugin:0.9") testImplementation(libs.bundles.kotest) diff --git a/buildSrc/src/main/java/com/google/firebase/gradle/plugins/Metalava.kt b/buildSrc/src/main/java/com/google/firebase/gradle/plugins/Metalava.kt index 077bf3de53f..5c9b28addb9 100644 --- a/buildSrc/src/main/java/com/google/firebase/gradle/plugins/Metalava.kt +++ b/buildSrc/src/main/java/com/google/firebase/gradle/plugins/Metalava.kt @@ -59,6 +59,7 @@ fun Project.runMetalavaWithArgs( "HiddenAbstractMethod" ) + arguments +/* project.javaexec { main = "com.android.tools.metalava.Driver" classpath = project.metalavaConfig @@ -66,6 +67,7 @@ fun Project.runMetalavaWithArgs( isIgnoreExitValue = ignoreFailure if (stdOut != null) errorOutput = stdOut } +*/ } abstract class GenerateStubsTask : DefaultTask() { diff --git a/firebase-config/bandwagoner/bandwagoner.gradle b/firebase-config/bandwagoner/bandwagoner.gradle index b45dd3bbf0b..af0ce005871 100644 --- a/firebase-config/bandwagoner/bandwagoner.gradle +++ b/firebase-config/bandwagoner/bandwagoner.gradle @@ -22,6 +22,7 @@ apply plugin: 'org.jetbrains.kotlin.android' // apply plugin: 'com.google.gms.google-services' android { + namespace "com.googletest.firebase.remoteconfig.bandwagoner" compileSdkVersion project.targetSdkVersion lintOptions { abortOnError false diff --git a/firebase-dynamic-links/firebase-dynamic-links.gradle b/firebase-dynamic-links/firebase-dynamic-links.gradle index 7c39b661989..0d8043ab991 100644 --- a/firebase-dynamic-links/firebase-dynamic-links.gradle +++ b/firebase-dynamic-links/firebase-dynamic-links.gradle @@ -47,6 +47,9 @@ android { } testOptions.unitTests.includeAndroidResources = true + buildFeatures { + aidl true + } } dependencies { diff --git a/firebase-firestore-sdk34/CHANGELOG.md b/firebase-firestore-sdk34/CHANGELOG.md new file mode 100755 index 00000000000..e69de29bb2d diff --git a/firebase-firestore-sdk34/README.md b/firebase-firestore-sdk34/README.md new file mode 100755 index 00000000000..b05694bd4a7 --- /dev/null +++ b/firebase-firestore-sdk34/README.md @@ -0,0 +1,118 @@ +# firebase-firestore + +This is the Cloud Firestore component of the Firebase Android SDK. + +Cloud Firestore is a flexible, scalable database for mobile, web, and server +development from Firebase and Google Cloud Platform. Like Firebase Realtime +Database, it keeps your data in sync across client apps through realtime +listeners and offers offline support for mobile and web so you can build +responsive apps that work regardless of network latency or Internet +connectivity. Cloud Firestore also offers seamless integration with other +Firebase and Google Cloud Platform products, including Cloud Functions. + +## Building + +All Gradle commands should be run from the source root (which is one level up +from this folder). See the README.md in the source root for instructions on +publishing/testing Cloud Firestore. + +To build Cloud Firestore, from the source root run: +```bash +./gradlew :firebase-firestore:assembleRelease +``` + +## Unit Testing + +To run unit tests for Cloud Firestore, from the source root run: +```bash +./gradlew :firebase-firestore:check +``` + +## Integration Testing + +Running integration tests requires a Firebase project because they would try +to connect to the Firestore backends. + +See [here](../README.md#project-setup) for how to setup a project. + +Once you setup the project, download `google-services.json` and place it in +the source root. + +Make sure you have created a Firestore instance for your project, before +you proceed. + +By default, integration tests run against the Firestore emulator. + +### Setting up the Firestore Emulator + +The integration tests require that the Firestore emulator is running on port +8080, which is default when running it via CLI. + + * [Install the Firebase CLI](https://firebase.google.com/docs/cli/). + ``` + npm install -g firebase-tools + ``` + * [Install the Firestore + emulator](https://firebase.google.com/docs/firestore/security/test-rules-emulator#install_the_emulator). + ``` + firebase setup:emulators:firestore + ``` + * Run the emulator + ``` + firebase emulators:start --only firestore + ``` + * Select the `Firestore Integration Tests (Firestore Emulator)` run + configuration to run all integration tests. + +To run the integration tests against prod, select `FirestoreProdIntegrationTest` +run configuration. + +### Run on Local Android Emulator + +Then simply run: +```bash +./gradlew :firebase-firestore:connectedCheck +``` + +### Run on Firebase Test Lab + +You can also test on Firebase Test Lab, which allow you to run the integration +tests on devices hosted in Google data center. + +See [here](../README.md#running-integration-tests-on-firebase-test-lab) for +instructions of how to setup Firebase Test Lab for your project. + +Run: +```bash +./gradlew :firebase-firestore:deviceCheck +``` + +## Code Formatting + +Run below to format Java code: +```bash +./gradlew :firebase-firestore:googleJavaFormat +``` + +See [here](../README.md#code-formatting) if you want to be able to format code +from within Android Studio. + +## Build Local Jar of Firestore SDK + +```bash +./gradlew -PprojectsToPublish="firebase-firestore" publishReleasingLibrariesToMavenLocal +``` + +Developers may then take a dependency on these locally published versions by adding +the `mavenLocal()` repository to your [repositories +block](https://docs.gradle.org/current/userguide/declaring_repositories.html) in +your app module's build.gradle. + +## Misc +After importing the project into Android Studio and building successfully +for the first time, Android Studio will delete the run configuration xml files +in `./idea/runConfigurations`. Undo these changes with the command: + +``` +$ git checkout .idea/runConfigurations +``` diff --git a/firebase-firestore-sdk34/firebase-firestore-sdk34.gradle b/firebase-firestore-sdk34/firebase-firestore-sdk34.gradle new file mode 100755 index 00000000000..0f3f086fc1b --- /dev/null +++ b/firebase-firestore-sdk34/firebase-firestore-sdk34.gradle @@ -0,0 +1,184 @@ +// Copyright 2018 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. + +plugins { + id 'firebase-library' + id 'com.google.protobuf' +} + +firebaseLibrary { + libraryGroup "firestore" + publishSources = true + testLab { + enabled = true + timeout = '30m' + } +} + +protobuf { + // Configure the protoc executable + protoc { + // Download from repositories + artifact = "com.google.protobuf:protoc:$protocVersion" + } + plugins { + grpc { + artifact = "io.grpc:protoc-gen-grpc-java:$grpcVersion" + } + } + generateProtoTasks { + all().each { task -> + task.builtins { + java { option 'lite' } + } + task.plugins { + grpc { + option 'lite' + } + } + } + } +} + +android { + adbOptions { + timeOutInMs 60 * 1000 + } + + namespace "com.google.firebase.firestore.sdk34" + compileSdkVersion 34 + defaultConfig { + targetSdkVersion 34 + minSdkVersion 19 + versionName version + multiDexEnabled true + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + consumerProguardFiles 'proguard.txt' + + // Acceptable values are: 'emulator', 'qa', 'nightly', and 'prod'. + def targetBackend = findProperty("targetBackend") ?: "emulator" + buildConfigField("String", "TARGET_BACKEND", "\"$targetBackend\"") + + def targetDatabaseId = findProperty('targetDatabaseId') ?: "(default)" + buildConfigField("String", "TARGET_DATABASE_ID", "\"$targetDatabaseId\"") + + def localProps = new Properties() + + try { + file("local.properties").withInputStream { localProps.load(it) } + } catch (FileNotFoundException e) { + } + } + + sourceSets { + main { + proto { + srcDir 'src/proto' + } + } + test { + java { + srcDir 'src/testUtil/java' + srcDir 'src/roboUtil/java' + } + } + androidTest { + java { + srcDir 'src/testUtil/java' + } + } + } + compileOptions { + sourceCompatibility JavaVersion.VERSION_17 + targetCompatibility JavaVersion.VERSION_17 + } + testOptions.unitTests.includeAndroidResources = true + +} + +tasks.withType(Test) { + maxParallelForks = Runtime.runtime.availableProcessors().intdiv(2) ?: 1 +} + +googleJavaFormat { + exclude 'src/testUtil/java/com/google/firebase/firestore/testutil/Assert.java' + exclude 'src/testUtil/java/com/google/firebase/firestore/testutil/ThrowingRunnable.java' +} + +dependencies { +/* + implementation 'com.google.firebase:firebase-annotations:16.2.0' + implementation 'com.google.firebase:firebase-common:20.3.1' + implementation project(':protolite-well-known-types') + implementation 'com.google.firebase:firebase-database-collection:18.0.1' + implementation 'com.google.firebase:firebase-components:17.1.0' + implementation 'com.google.firebase:firebase-appcheck-interop:17.0.0' + + //To provide @Generated annotations + compileOnly 'javax.annotation:jsr250-api:1.0' + + javadocClasspath 'com.google.auto.value:auto-value-annotations:1.6.6' + + implementation 'androidx.annotation:annotation:1.1.0' + implementation "io.grpc:grpc-stub:$grpcVersion" + implementation "io.grpc:grpc-protobuf-lite:$grpcVersion" + implementation "io.grpc:grpc-okhttp:$grpcVersion" + implementation "io.grpc:grpc-android:$grpcVersion" + implementation 'com.google.android.gms:play-services-basement:18.1.0' + implementation 'com.google.android.gms:play-services-tasks:18.0.1' + implementation 'com.google.android.gms:play-services-base:18.0.1' + + implementation('com.google.firebase:firebase-auth-interop:19.0.2') { + exclude group: "com.google.firebase", module: "firebase-common" + } + + compileOnly 'com.google.auto.value:auto-value-annotations:1.6.6' + androidTestAnnotationProcessor 'com.google.auto.value:auto-value:1.6.5' + annotationProcessor 'com.google.auto.value:auto-value:1.6.5' +*/ + + //implementation project(':firebase-firestore') + testImplementation project(':firebase-firestore-sdk34') + testImplementation 'junit:junit:4.13.2' + testImplementation "androidx.test:core:$androidxTestCoreVersion" + testImplementation "org.hamcrest:hamcrest-junit:2.0.0.0" + testImplementation 'org.mockito:mockito-core:2.25.0' + testImplementation "org.robolectric:robolectric:4.10.3" + testImplementation "com.google.truth:truth:$googleTruthVersion" + testImplementation 'com.fasterxml.jackson.core:jackson-databind:2.9.8' + testImplementation 'com.google.guava:guava-testlib:12.0-rc2' + testImplementation project(path: ':firebase-firestore') + testImplementation libs.google.gson + + androidTestImplementation 'junit:junit:4.13.2' + androidTestImplementation("com.google.truth:truth:$googleTruthVersion") { + exclude group: "org.codehaus.mojo", module: "animal-sniffer-annotations" + } + androidTestImplementation 'org.mockito:mockito-core:2.25.0' + androidTestImplementation 'org.mockito:mockito-android:2.25.0' + androidTestImplementation 'com.fasterxml.jackson.core:jackson-databind:2.9.8' + androidTestImplementation "androidx.annotation:annotation:1.1.0" + androidTestImplementation 'androidx.test:runner:1.5.2' + androidTestImplementation 'androidx.test:rules:1.5.0' + androidTestImplementation "androidx.test.ext:junit:$androidxTestJUnitVersion" +} + +gradle.projectsEvaluated { + tasks.withType(JavaCompile) { + // TODO(wuandy): Also add "-Xlint:unchecked". But currently that + // enables 100+ warnings due to our generated source code. + // TODO(wuandy): Re-enable error on warnings once errorprone issues are fixed. + options.compilerArgs << "-Xlint:deprecation" // << "-Werror" + } +} diff --git a/firebase-firestore-sdk34/gradle.properties b/firebase-firestore-sdk34/gradle.properties new file mode 100755 index 00000000000..398412fefb4 --- /dev/null +++ b/firebase-firestore-sdk34/gradle.properties @@ -0,0 +1,2 @@ +version=24.8.0-SNAPSHOT +latestReleasedVersion=24.7.1 diff --git a/firebase-firestore-sdk34/lint.xml b/firebase-firestore-sdk34/lint.xml new file mode 100755 index 00000000000..5cdbff248f9 --- /dev/null +++ b/firebase-firestore-sdk34/lint.xml @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/firebase-firestore-sdk34/proguard.txt b/firebase-firestore-sdk34/proguard.txt new file mode 100755 index 00000000000..b76f1281567 --- /dev/null +++ b/firebase-firestore-sdk34/proguard.txt @@ -0,0 +1,18 @@ +# Needed for DNS resolution. Present in OpenJDK, but not Android +-dontwarn javax.naming.** + +# Don't warn about checkerframework +# +# Guava uses the checkerframework and the annotations +# can safely be ignored at runtime. +-dontwarn org.checkerframework.** + +# Guava warnings: +-dontwarn java.lang.ClassValue +-dontwarn com.google.j2objc.annotations.Weak +-dontwarn org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement +-dontwarn javax.lang.model.element.Modifier + +# Okhttp warnings. +-dontwarn okio.** +-dontwarn com.google.j2objc.annotations.** diff --git a/firebase-firestore-sdk34/src/main/java/com/google/firebase/firestore/sdk34/DocumentId.java b/firebase-firestore-sdk34/src/main/java/com/google/firebase/firestore/sdk34/DocumentId.java new file mode 100755 index 00000000000..5985d6beec6 --- /dev/null +++ b/firebase-firestore-sdk34/src/main/java/com/google/firebase/firestore/sdk34/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.firebase.firestore.sdk34; + +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 firebase 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/firebase-firestore-sdk34/src/main/java/com/google/firebase/firestore/sdk34/PropertyName.java b/firebase-firestore-sdk34/src/main/java/com/google/firebase/firestore/sdk34/PropertyName.java new file mode 100755 index 00000000000..553c692dbdf --- /dev/null +++ b/firebase-firestore-sdk34/src/main/java/com/google/firebase/firestore/sdk34/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.firebase.firestore.sdk34; + +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/firebase-firestore-sdk34/src/main/java/com/google/firebase/firestore/sdk34/ServerTimestamp.java b/firebase-firestore-sdk34/src/main/java/com/google/firebase/firestore/sdk34/ServerTimestamp.java new file mode 100755 index 00000000000..d91103e0921 --- /dev/null +++ b/firebase-firestore-sdk34/src/main/java/com/google/firebase/firestore/sdk34/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.firebase.firestore.sdk34; + +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/firebase-firestore-sdk34/src/roboUtil/java/com/google/firebase/firestore/TestUtil.java b/firebase-firestore-sdk34/src/roboUtil/java/com/google/firebase/firestore/TestUtil.java new file mode 100755 index 00000000000..da70dab4a8c --- /dev/null +++ b/firebase-firestore-sdk34/src/roboUtil/java/com/google/firebase/firestore/TestUtil.java @@ -0,0 +1,126 @@ +// Copyright 2018 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.firebase.firestore; + +import com.google.firebase.firestore.model.DocumentKey; +import com.google.firebase.firestore.model.ResourcePath; + +import static org.mockito.Mockito.mock; + +public class TestUtil { + + private static final FirebaseFirestore FIRESTORE = mock(FirebaseFirestore.class); + + public static FirebaseFirestore firestore() { + return FIRESTORE; + } + + public static CollectionReference collectionReference(String path) { + return new CollectionReference(ResourcePath.fromString(path), FIRESTORE); + } + + public static DocumentReference documentReference(String path) { + return new DocumentReference(key(path), FIRESTORE); + } + + public static DocumentKey key(String key) { + return DocumentKey.fromPathString(key); + } + + /* + public static DocumentSnapshot documentSnapshot( + String path, Map data, boolean isFromCache) { + if (data == null) { + return DocumentSnapshot.fromNoDocument(FIRESTORE, key(path), isFromCache); + } else { + return DocumentSnapshot.fromDocument( + FIRESTORE, doc(path, 1L, data), isFromCache, */ +/*hasPendingWrites=*/ /* + false); + } + } + */ + + /* + public static Query query(String path) { + return new Query(com.google.firebase.firestore.testutil.TestUtil.query(path), FIRESTORE); + } + + /** + * A convenience method for creating a particular query snapshot for tests. + * + * @param path To be used in constructing the query. + * @param oldDocs Provides the prior set of documents in the QuerySnapshot. Each entry maps to a + * document, with the key being the document id, and the value being the document contents. + * @param docsToAdd Specifies data to be added into the query snapshot as of now. Each entry maps + * to a document, with the key being the document id, and the value being the document + * contents. + * @param isFromCache Whether the query snapshot is cache result. + * @return A query snapshot that consists of both sets of documents. + * / + public static QuerySnapshot querySnapshot( + String path, + Map oldDocs, + Map docsToAdd, + boolean hasPendingWrites, + boolean isFromCache, + boolean hasCachedResults) { + DocumentSet oldDocuments = docSet(Document.KEY_COMPARATOR); + ImmutableSortedSet mutatedKeys = DocumentKey.emptyKeySet(); + for (Map.Entry pair : oldDocs.entrySet()) { + String docKey = path + "/" + pair.getKey(); + MutableDocument doc = doc(docKey, 1L, pair.getValue()); + if (hasPendingWrites) { + doc.setHasCommittedMutations(); + mutatedKeys = mutatedKeys.insert(key(docKey)); + } + oldDocuments = oldDocuments.add(doc); + } + DocumentSet newDocuments = docSet(Document.KEY_COMPARATOR); + List documentChanges = new ArrayList<>(); + for (Map.Entry pair : docsToAdd.entrySet()) { + String docKey = path + "/" + pair.getKey(); + MutableDocument docToAdd = doc(docKey, 1L, pair.getValue()); + if (hasPendingWrites) { + docToAdd.setHasCommittedMutations(); + mutatedKeys = mutatedKeys.insert(key(docKey)); + } + newDocuments = newDocuments.add(docToAdd); + documentChanges.add(DocumentViewChange.create(Type.ADDED, docToAdd)); + } + ViewSnapshot viewSnapshot = + new ViewSnapshot( + com.google.firebase.firestore.testutil.TestUtil.query(path), + newDocuments, + oldDocuments, + documentChanges, + isFromCache, + mutatedKeys, + /* didSyncStateChange= * / true, + /* excludesMetadataChanges= * / false, + hasCachedResults); + return new QuerySnapshot(query(path), viewSnapshot, FIRESTORE); + } + + public static T waitFor(Task task) { + if (!task.isComplete()) { + Robolectric.flushBackgroundThreadScheduler(); + } + Assert.assertTrue( + "Expected task to be completed after background thread flush", task.isComplete()); + return task.getResult(); + } + */ +} diff --git a/firebase-firestore-sdk34/src/test/AndroidManifest.xml b/firebase-firestore-sdk34/src/test/AndroidManifest.xml new file mode 100755 index 00000000000..26e8ce7a35c --- /dev/null +++ b/firebase-firestore-sdk34/src/test/AndroidManifest.xml @@ -0,0 +1,14 @@ + + + + + + + + + + diff --git a/firebase-firestore-sdk34/src/test/java/com/google/firebase/firestore/sdk34/LocalFirestoreHelper.java b/firebase-firestore-sdk34/src/test/java/com/google/firebase/firestore/sdk34/LocalFirestoreHelper.java new file mode 100755 index 00000000000..b43a29512d8 --- /dev/null +++ b/firebase-firestore-sdk34/src/test/java/com/google/firebase/firestore/sdk34/LocalFirestoreHelper.java @@ -0,0 +1,406 @@ +/* + * 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.firebase.firestore.sdk34; + +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.common.reflect.TypeToken; +import com.google.firebase.Timestamp; +import com.google.firebase.firestore.Blob; +import com.google.firebase.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 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 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.firebase.firestore.sdk34.ServerTimestamp Date foo, + Inner inner + + ){ + record Inner ( + + @com.google.firebase.firestore.sdk34.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/firebase-firestore-sdk34/src/test/java/com/google/firebase/firestore/sdk34/RecordDocumentReferenceTest.java b/firebase-firestore-sdk34/src/test/java/com/google/firebase/firestore/sdk34/RecordDocumentReferenceTest.java new file mode 100755 index 00000000000..6edccd97597 --- /dev/null +++ b/firebase-firestore-sdk34/src/test/java/com/google/firebase/firestore/sdk34/RecordDocumentReferenceTest.java @@ -0,0 +1,332 @@ +/* +// Copyright 2018 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.firebase.firestore; + +import static com.google.firebase.firestore.sdk34.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.firebase.firestore.sdk34.LocalFirestoreHelper; +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.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + + +@RunWith(RobolectricTestRunner.class) +@Config(manifest = Config.NONE) +public class RecordDocumentReferenceTest { + @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/firebase-firestore-sdk34/src/test/java/com/google/firebase/firestore/sdk34/util/RecordMapperTest.java b/firebase-firestore-sdk34/src/test/java/com/google/firebase/firestore/sdk34/util/RecordMapperTest.java new file mode 100755 index 00000000000..e7bf3973956 --- /dev/null +++ b/firebase-firestore-sdk34/src/test/java/com/google/firebase/firestore/sdk34/util/RecordMapperTest.java @@ -0,0 +1,1104 @@ +// Copyright 2018 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.firebase.firestore.util; + +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.robolectric.annotation.Config; + +import com.google.firebase.firestore.DocumentReference; +import com.google.firebase.firestore.TestUtil; +import com.google.firebase.firestore.ThrowOnExtraProperties; +import com.google.firebase.firestore.sdk34.DocumentId; +import com.google.firebase.firestore.sdk34.PropertyName; + +import static com.google.firebase.firestore.sdk34.LocalFirestoreHelper.fromSingleQuotedString; +import static com.google.firebase.firestore.sdk34.LocalFirestoreHelper.mapAnyType; +import static org.junit.Assert.*; + +@org.junit.runner.RunWith(org.robolectric.RobolectricTestRunner.class) +@Config(manifest = Config.NONE, sdk = 34) +@SuppressWarnings({"unused", "WeakerAccess", "SpellCheckingInspection"}) +public class RecordMapperTest { + 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.firebase.firestore.RecordMapperTest$NoFieldBean", + () -> deserialize("{'value': 'foo'}", NoFieldBean.class)); + } + + @Test + public void throwOnUnknownProperties() { + assertExceptionContains( + "No accessor for unknown found on class " + + "com.google.firebase.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.firebase.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.firebase.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() { + DocumentReference ref = TestUtil.documentReference("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. + + DocumentReference ref = TestUtil.documentReference("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 String expectedErrorMessage = "cannot apply @DocumentId on this property"; + DocumentReference ref = TestUtil.documentReference("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/firebase-firestore/gradle.properties b/firebase-firestore/gradle.properties index 98e4de81ec2..398412fefb4 100644 --- a/firebase-firestore/gradle.properties +++ b/firebase-firestore/gradle.properties @@ -1,2 +1,2 @@ -version=24.8.0 +version=24.8.0-SNAPSHOT latestReleasedVersion=24.7.1 diff --git a/firebase-firestore/ktx/ktx.gradle b/firebase-firestore/ktx/ktx.gradle index 333d421eeb1..bcd18337dad 100644 --- a/firebase-firestore/ktx/ktx.gradle +++ b/firebase-firestore/ktx/ktx.gradle @@ -45,6 +45,10 @@ android { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } + + kotlin { + jvmToolchain(8) + } } tasks.withType(Test) { diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/util/BeanMapper.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/util/BeanMapper.java new file mode 100755 index 00000000000..6d77eb8a4f5 --- /dev/null +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/util/BeanMapper.java @@ -0,0 +1,132 @@ +/* + * 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.firebase.firestore.util; + +import com.google.firebase.firestore.DocumentReference; +import com.google.firebase.firestore.IgnoreExtraProperties; +import com.google.firebase.firestore.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/firebase-firestore/src/main/java/com/google/firebase/firestore/util/CustomClassMapper.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/util/CustomClassMapper.java index 4fdbee103f0..b31756cf420 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/util/CustomClassMapper.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/util/CustomClassMapper.java @@ -14,21 +14,6 @@ package com.google.firebase.firestore.util; -import static com.google.firebase.firestore.util.ApiUtil.invoke; -import static com.google.firebase.firestore.util.ApiUtil.newInstance; - -import android.net.Uri; -import com.google.firebase.Timestamp; -import com.google.firebase.firestore.Blob; -import com.google.firebase.firestore.DocumentId; -import com.google.firebase.firestore.DocumentReference; -import com.google.firebase.firestore.Exclude; -import com.google.firebase.firestore.FieldValue; -import com.google.firebase.firestore.GeoPoint; -import com.google.firebase.firestore.IgnoreExtraProperties; -import com.google.firebase.firestore.PropertyName; -import com.google.firebase.firestore.ServerTimestamp; -import com.google.firebase.firestore.ThrowOnExtraProperties; import java.lang.reflect.AccessibleObject; import java.lang.reflect.Constructor; import java.lang.reflect.Field; @@ -43,7 +28,6 @@ import java.net.URL; import java.util.ArrayList; import java.util.Collection; -import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.HashSet; @@ -53,6 +37,21 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; +import android.net.Uri; +import com.google.common.collect.Sets; +import com.google.firebase.Timestamp; +import com.google.firebase.firestore.Blob; +import com.google.firebase.firestore.DocumentId; +import com.google.firebase.firestore.DocumentReference; +import com.google.firebase.firestore.Exclude; +import com.google.firebase.firestore.FieldValue; +import com.google.firebase.firestore.GeoPoint; +import com.google.firebase.firestore.PropertyName; +import com.google.firebase.firestore.ServerTimestamp; + +import static com.google.firebase.firestore.util.ApiUtil.invoke; +import static com.google.firebase.firestore.util.ApiUtil.newInstance; + /** Helper class to convert to/from custom POJO classes and plain Java types. */ public class CustomClassMapper { /** Maximum depth before we give up and assume it's a recursive object graph. */ @@ -100,15 +99,16 @@ public static Map convertToPlainJavaTypes(Map update) */ public 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)); } private 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, @@ -164,7 +164,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; } @@ -185,7 +185,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) { @@ -303,7 +303,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"); @@ -347,7 +347,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; @@ -376,7 +376,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); @@ -549,7 +553,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() + "')"; @@ -557,7 +562,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() + "')"; @@ -565,16 +571,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 && Sets.newHashSet("java.lang.Record", "com.android.tools.r8.RecordTag").contains(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; @@ -595,10 +599,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<>(); @@ -739,10 +741,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, @@ -751,7 +750,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"); } @@ -760,7 +759,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(); @@ -787,13 +786,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.warn(CustomClassMapper.class.getSimpleName(), "%s", message); } } @@ -812,17 +811,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(); @@ -850,28 +840,10 @@ 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) { + @Override + Map serialize(T object, DeserializeContext.ErrorPath path) { + verifyValidType(object); // TODO(wuandy): Add logic to skip @DocumentId annotated fields in serialization. - if (!clazz.isAssignableFrom(object.getClass())) { - throw new IllegalArgumentException( - "Can't serialize object of class " - + object.getClass() - + " with BeanMapper for class " - + clazz); - } Map result = new HashMap<>(); for (String property : properties.values()) { // Skip @DocumentId annotated properties; @@ -967,18 +939,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; @@ -1120,61 +1080,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. */ - 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/firebase-firestore/src/main/java/com/google/firebase/firestore/util/DeserializeContext.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/util/DeserializeContext.java new file mode 100755 index 00000000000..594b0f45adb --- /dev/null +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/util/DeserializeContext.java @@ -0,0 +1,76 @@ +/* + * 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.firebase.firestore.util; + +import com.google.firebase.firestore.DocumentReference; + + +/** 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/firebase-firestore/src/main/java/com/google/firebase/firestore/util/RecordMapper.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/util/RecordMapper.java new file mode 100755 index 00000000000..a0a397113fa --- /dev/null +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/util/RecordMapper.java @@ -0,0 +1,307 @@ +/* + * 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.firebase.firestore.util; + +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.ArrayList; +import java.util.Date; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.logging.Logger; + +import com.google.firebase.Timestamp; +import com.google.firebase.firestore.FieldValue; + +/** + * 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 + * firebase-firestore-sdk34 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.getParameterTypes().length]; + 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) { + Annotation annotation = getAnnotation(component, "PropertyName"); + if (annotation != null) { + 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) != null; + } + + private static Annotation getAnnotation( + AnnotatedElement element, String annotationName) { + Annotation[] annotations = element.getAnnotations(); + for (Annotation annotation : annotations) { + if (annotation.annotationType().getSimpleName().equals(annotationName)) { + return annotation; + } + } + + return null; + } + + // 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 { + static final Class[] CLASSES_ARRAY_TYPE = new Class[0]; + 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 = getParamTypes(cls); + Constructor constructor = cls.getDeclaredConstructor(paramTypes); + constructor.setAccessible(true); + return constructor; + } catch (NoSuchMethodException e) { + throw new IllegalStateException(e); + } + } + + private Class[] getParamTypes(Class cls) { + AnnotatedElement[] recordComponents = getRecordComponents(cls); + List> types = new ArrayList<>(recordComponents.length); + for (AnnotatedElement element: recordComponents) { + types.add(getType(element)); + } + return types.toArray(CLASSES_ARRAY_TYPE); + } + + 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/firebase-perf/dev-app/dev-app.gradle b/firebase-perf/dev-app/dev-app.gradle index 1b54ae2bfb9..818a08620cb 100644 --- a/firebase-perf/dev-app/dev-app.gradle +++ b/firebase-perf/dev-app/dev-app.gradle @@ -22,6 +22,7 @@ firebaseTestLab { } android { + namespace "com.googletest.firebase.perf.testapp.prod" compileSdkVersion 31 compileOptions { diff --git a/firebase-perf/e2e-app/e2e-app.gradle b/firebase-perf/e2e-app/e2e-app.gradle index 2bf2b8cb2ea..eeb4fff4a97 100644 --- a/firebase-perf/e2e-app/e2e-app.gradle +++ b/firebase-perf/e2e-app/e2e-app.gradle @@ -27,6 +27,7 @@ firebaseTestLab { } android { + namespace "com.google.firebase.testing.fireperf" compileSdkVersion 31 // Specifies the build type that the Android plugin should use to run the instrumentation tests. diff --git a/gradle.properties b/gradle.properties index 43b684aa426..c9128c68103 100644 --- a/gradle.properties +++ b/gradle.properties @@ -11,7 +11,8 @@ # 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. -org.gradle.jvmargs=-Xmx8g -XX:MaxPermSize=8g +org.gradle.jvmargs=-Xmx8g + #-XX:MaxPermSize=8g org.gradle.parallel=true org.gradle.caching=true @@ -20,4 +21,7 @@ firebase.checks.lintProjects=:tools:lint systemProp.illegal-access=warn -android.useAndroidX=true \ No newline at end of file +android.useAndroidX=true +android.defaults.buildfeatures.buildconfig=true +android.nonTransitiveRClass=false +android.nonFinalResIds=false diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 0c3531e7246..9f4657b92ed 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -3,11 +3,15 @@ # it needs to match the protobuf version which grpc has transitive dependency on. android-lint = "30.3.1" autovalue = "1.10.1" +builder-test-api = "8.1.1" coroutines = "1.6.4" dagger = "2.43.2" +google-gson = "2.8.9" +gradle = "8.1.1" grpc = "1.52.1" javalite = "3.21.11" -kotlin = "1.7.10" +kotlin = "1.8.10" +protobuf-gradle-plugin = "0.9.4" protoc = "3.21.11" robolectric = "4.9" truth = "1.1.2" @@ -28,10 +32,13 @@ androidx-core = { module = "androidx.core:core", version = "1.2.0" } androidx-futures = { module = "androidx.concurrent:concurrent-futures", version = "1.1.0" } autovalue = { module = "com.google.auto.value:auto-value", version.ref = "autovalue" } autovalue-annotations = { module = "com.google.auto.value:auto-value-annotations", version.ref = "autovalue" } +builder-test-api = { module = "com.android.tools.build:builder-test-api", version.ref = "builder-test-api" } dagger-dagger = { module = "com.google.dagger:dagger", version.ref = "dagger" } dagger-compiler = { module = "com.google.dagger:dagger-compiler", version.ref = "dagger" } errorprone-annotations = { module = "com.google.errorprone:error_prone_annotations", version = "2.9.0" } findbugs-jsr305 = { module = "com.google.code.findbugs:jsr305", version = "3.0.2" } +google-gson = { module = "com.google.code.gson:gson", version.ref = "google-gson" } +gradle = { module = "com.android.tools.build:gradle", version.ref = "gradle" } javax-inject = { module = "javax.inject:javax.inject", version = "1" } kotlin-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib", version.ref = "kotlin" } kotlin-coroutines-tasks = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-play-services", version.ref = "coroutines" } @@ -52,6 +59,7 @@ junit = { module = "junit:junit", version = "4.13.2" } kotlin-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "coroutines" } mockito-core = { module = "org.mockito:mockito-core", version = "2.28.2" } mockito-dexmaker = { module = "com.linkedin.dexmaker:dexmaker-mockito", version = "2.28.3" } +protobuf-gradle-plugin = { module = "com.google.protobuf:protobuf-gradle-plugin", version.ref = "protobuf-gradle-plugin" } robolectric = { module = "org.robolectric:robolectric", version.ref = "robolectric" } truth = { module = "com.google.truth:truth", version.ref = "truth" } protobuf-java-util = { module = "com.google.protobuf:protobuf-java-util", version.ref = "protobufjavautil" } @@ -62,4 +70,4 @@ quickcheck = { module = "net.java:quickcheck", version.ref = "quickcheck" } [bundles] kotest = ["kotest-runner", "kotest-assertions", "kotest-property"] -playservices = ["playservices-base", "playservices-basement", "playservices-tasks"] \ No newline at end of file +playservices = ["playservices-base", "playservices-basement", "playservices-tasks"] diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index f42e62f3724..3a02907943e 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.6-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.0-all.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/subprojects.cfg b/subprojects.cfg index a86bc0b5e44..298dc69f096 100644 --- a/subprojects.cfg +++ b/subprojects.cfg @@ -1,9 +1,9 @@ appcheck -appcheck:firebase-appcheck-debug-testing +#appcheck:firebase-appcheck-debug-testing appcheck:firebase-appcheck-debug appcheck:firebase-appcheck-interop appcheck:firebase-appcheck-playintegrity -appcheck:firebase-appcheck-safetynet +#appcheck:firebase-appcheck-safetynet appcheck:firebase-appcheck appcheck:firebase-appcheck:ktx @@ -30,22 +30,23 @@ firebase-datatransport firebase-dynamic-links firebase-dynamic-links:ktx firebase-firestore -firebase-firestore:ktx +#firebase-firestore:ktx +firebase-firestore-sdk34 firebase-functions firebase-functions:ktx -firebase-messaging -firebase-messaging:ktx -firebase-messaging-directboot -firebase-inappmessaging -firebase-inappmessaging:ktx -firebase-inappmessaging-display -firebase-inappmessaging-display:ktx +#firebase-messaging +#firebase-messaging:ktx +#firebase-messaging-directboot +#firebase-inappmessaging +#firebase-inappmessaging:ktx +#firebase-inappmessaging-display +#firebase-inappmessaging-display:ktx firebase-installations-interop firebase-installations firebase-installations:ktx -firebase-ml-modeldownloader -firebase-ml-modeldownloader:ktx -firebase-ml-modeldownloader:ml-data-collection-tests +#firebase-ml-modeldownloader +#firebase-ml-modeldownloader:ktx +#firebase-ml-modeldownloader:ml-data-collection-tests firebase-perf firebase-perf:ktx firebase-perf:dev-app @@ -53,8 +54,8 @@ firebase-perf:e2e-app # firebase-segmentation firebase-sessions firebase-sessions:test-app -firebase-storage -firebase-storage:ktx +#firebase-storage +#firebase-storage:ktx protolite-well-known-types encoders @@ -64,8 +65,8 @@ encoders:firebase-encoders-processor encoders:firebase-encoders-proto encoders:firebase-encoders-reflective encoders:firebase-decoders-json -encoders:protoc-gen-firebase-encoders -encoders:protoc-gen-firebase-encoders:tests +#encoders:protoc-gen-firebase-encoders +#encoders:protoc-gen-firebase-encoders:tests integ-testing @@ -74,8 +75,8 @@ tools:lint transport transport:transport-api transport:transport-backend-cct -transport:transport-runtime -transport:transport-runtime-testing +#transport:transport-runtime +#transport:transport-runtime-testing # Test Apps # disabled since they require google-services.json to build which is not always available in CI. From e95fe28e5951a1ac804830eae9e5aa4dcb9f3e75 Mon Sep 17 00:00:00 2001 From: eranl <1707552+eranl@users.noreply.github.com> Date: Fri, 22 Sep 2023 04:18:46 +0300 Subject: [PATCH 02/11] java records, using field annotations --- .../firebase/firestore/sdk34/DocumentId.java | 46 ----- .../firestore/sdk34/PropertyName.java | 30 ---- .../firestore/sdk34/ServerTimestamp.java | 31 ---- .../firestore/sdk34/LocalFirestoreHelper.java | 4 +- .../sdk34/util/RecordMapperTest.java | 29 +-- .../firebase/firestore/util/BeanMapper.java | 52 +++++- .../firestore/util/CustomClassMapper.java | 52 +----- .../firebase/firestore/util/RecordMapper.java | 169 ++++-------------- 8 files changed, 101 insertions(+), 312 deletions(-) delete mode 100755 firebase-firestore-sdk34/src/main/java/com/google/firebase/firestore/sdk34/DocumentId.java delete mode 100755 firebase-firestore-sdk34/src/main/java/com/google/firebase/firestore/sdk34/PropertyName.java delete mode 100755 firebase-firestore-sdk34/src/main/java/com/google/firebase/firestore/sdk34/ServerTimestamp.java diff --git a/firebase-firestore-sdk34/src/main/java/com/google/firebase/firestore/sdk34/DocumentId.java b/firebase-firestore-sdk34/src/main/java/com/google/firebase/firestore/sdk34/DocumentId.java deleted file mode 100755 index 5985d6beec6..00000000000 --- a/firebase-firestore-sdk34/src/main/java/com/google/firebase/firestore/sdk34/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.firebase.firestore.sdk34; - -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 firebase 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/firebase-firestore-sdk34/src/main/java/com/google/firebase/firestore/sdk34/PropertyName.java b/firebase-firestore-sdk34/src/main/java/com/google/firebase/firestore/sdk34/PropertyName.java deleted file mode 100755 index 553c692dbdf..00000000000 --- a/firebase-firestore-sdk34/src/main/java/com/google/firebase/firestore/sdk34/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.firebase.firestore.sdk34; - -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/firebase-firestore-sdk34/src/main/java/com/google/firebase/firestore/sdk34/ServerTimestamp.java b/firebase-firestore-sdk34/src/main/java/com/google/firebase/firestore/sdk34/ServerTimestamp.java deleted file mode 100755 index d91103e0921..00000000000 --- a/firebase-firestore-sdk34/src/main/java/com/google/firebase/firestore/sdk34/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.firebase.firestore.sdk34; - -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/firebase-firestore-sdk34/src/test/java/com/google/firebase/firestore/sdk34/LocalFirestoreHelper.java b/firebase-firestore-sdk34/src/test/java/com/google/firebase/firestore/sdk34/LocalFirestoreHelper.java index b43a29512d8..61db82ffae4 100755 --- a/firebase-firestore-sdk34/src/test/java/com/google/firebase/firestore/sdk34/LocalFirestoreHelper.java +++ b/firebase-firestore-sdk34/src/test/java/com/google/firebase/firestore/sdk34/LocalFirestoreHelper.java @@ -92,13 +92,13 @@ public record NestedRecord( public record ServerTimestamp ( - @com.google.firebase.firestore.sdk34.ServerTimestamp Date foo, + @com.google.firebase.firestore.ServerTimestamp Date foo, Inner inner ){ record Inner ( - @com.google.firebase.firestore.sdk34.ServerTimestamp Date bar + @com.google.firebase.firestore.ServerTimestamp Date bar ){} } diff --git a/firebase-firestore-sdk34/src/test/java/com/google/firebase/firestore/sdk34/util/RecordMapperTest.java b/firebase-firestore-sdk34/src/test/java/com/google/firebase/firestore/sdk34/util/RecordMapperTest.java index e7bf3973956..702577e81c4 100755 --- a/firebase-firestore-sdk34/src/test/java/com/google/firebase/firestore/sdk34/util/RecordMapperTest.java +++ b/firebase-firestore-sdk34/src/test/java/com/google/firebase/firestore/sdk34/util/RecordMapperTest.java @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package com.google.firebase.firestore.util; +package com.google.firebase.firestore.sdk34.util; import java.io.Serializable; import java.math.BigDecimal; @@ -29,18 +29,19 @@ import org.junit.Test; import org.robolectric.annotation.Config; +import com.google.firebase.firestore.DocumentId; import com.google.firebase.firestore.DocumentReference; +import com.google.firebase.firestore.PropertyName; import com.google.firebase.firestore.TestUtil; import com.google.firebase.firestore.ThrowOnExtraProperties; -import com.google.firebase.firestore.sdk34.DocumentId; -import com.google.firebase.firestore.sdk34.PropertyName; +import com.google.firebase.firestore.util.CustomClassMapper; import static com.google.firebase.firestore.sdk34.LocalFirestoreHelper.fromSingleQuotedString; import static com.google.firebase.firestore.sdk34.LocalFirestoreHelper.mapAnyType; import static org.junit.Assert.*; @org.junit.runner.RunWith(org.robolectric.RobolectricTestRunner.class) -@Config(manifest = Config.NONE, sdk = 34) +@Config(manifest = Config.NONE, sdk = 33) @SuppressWarnings({"unused", "WeakerAccess", "SpellCheckingInspection"}) public class RecordMapperTest { private static final double EPSILON = 0.0003; @@ -347,6 +348,7 @@ public void primitiveDeserializeDouble() { } } + /* @Test public void primitiveDeserializeBigDecimal() { var beanBigdecimal = deserialize("{'value': 123}", BigDecimalBean.class); @@ -390,6 +392,7 @@ public void primitiveDeserializeBigDecimal() { } catch (RuntimeException e) { // ignore } } + */ @Test public void primitiveDeserializeFloat() { @@ -515,7 +518,7 @@ public void primitiveDeserializeWrongTypeList() { public void noFieldDeserialize() { assertExceptionContains( "No properties to serialize found on class " - + "com.google.firebase.firestore.RecordMapperTest$NoFieldBean", + + "com.google.firebase.firestore.sdk34.util.RecordMapperTest$NoFieldBean", () -> deserialize("{'value': 'foo'}", NoFieldBean.class)); } @@ -523,7 +526,7 @@ public void noFieldDeserialize() { public void throwOnUnknownProperties() { assertExceptionContains( "No accessor for unknown found on class " - + "com.google.firebase.firestore.RecordMapperTest$ThrowOnUnknownPropertiesBean", + + "com.google.firebase.firestore.sdk34.util.RecordMapperTest$ThrowOnUnknownPropertiesBean", () -> deserialize("{'value': 'foo', 'unknown': 'bar'}", ThrowOnUnknownPropertiesBean.class)); } @@ -673,6 +676,7 @@ public void serializeLongBean() { serialize(Collections.singletonMap("value", 1.234567890123E12))); } + /* @Test public void serializeBigDecimalBean() { var bean = new BigDecimalBean(BigDecimal.valueOf(1.1)); @@ -687,6 +691,7 @@ public void bigDecimalRoundTrip() { var b = convertToCustomClass(serialized, BigDecimalBean.class); assertEquals(a, b); } + */ @Test public void serializeBooleanBean() { @@ -707,7 +712,7 @@ public void serializePrivateFieldBean() { final var bean = new NoFieldBean(); assertExceptionContains( "No properties to serialize found on class " - + "com.google.firebase.firestore.RecordMapperTest$NoFieldBean", + + "com.google.firebase.firestore.sdk34.util.RecordMapperTest$NoFieldBean", () -> serialize(bean)); } @@ -839,7 +844,7 @@ public void roundTripUnicodeBean() { 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')", + "Numbers of type Short are not supported, please use an int, long, float or double (found in field 'value')", () -> serialize(bean)); } @@ -847,7 +852,7 @@ public void shortsCantBeSerialized() { 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')", + "Numbers of type Byte are not supported, please use an int, long, float or double (found in field 'value')", () -> serialize(bean)); } @@ -928,10 +933,8 @@ public void objectAcceptsAnyObject() { @Test public void passingInGenericBeanTopLevelThrows() { - assertExceptionContains( - "Class com.google.firebase.firestore.RecordMapperTest$GenericBean has generic type " - + "parameters, please use GenericTypeIndicator instead", - () -> deserialize("{'value': 'foo'}", GenericBean.class)); + assertExceptionContains("Class com.google.firebase.firestore.sdk34.util.RecordMapperTest$GenericBean has generic type parameters", + () -> deserialize("{'value': 'foo'}", GenericBean.class)); } @Test diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/util/BeanMapper.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/util/BeanMapper.java index 6d77eb8a4f5..84952a7ed75 100755 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/util/BeanMapper.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/util/BeanMapper.java @@ -16,14 +16,25 @@ package com.google.firebase.firestore.util; +import com.google.firebase.Timestamp; +import com.google.firebase.firestore.DocumentId; import com.google.firebase.firestore.DocumentReference; import com.google.firebase.firestore.IgnoreExtraProperties; +import com.google.firebase.firestore.PropertyName; +import com.google.firebase.firestore.ServerTimestamp; import com.google.firebase.firestore.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; +import java.util.Set; + /** Base bean mapper class, providing common functionality for class and record serialization. */ abstract class BeanMapper { @@ -34,11 +45,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 Set 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 Set 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() { @@ -75,7 +94,38 @@ abstract T deserialize( Map>, Type> types, DeserializeContext context); - void ensureValidDocumentIdType(String fieldDescription, String operation, Type type) { + 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; + } + + static void ensureValidDocumentIdType(String fieldDescription, String operation, Type type) { if (type != String.class && type != DocumentReference.class) { throw new IllegalArgumentException( fieldDescription diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/util/CustomClassMapper.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/util/CustomClassMapper.java index b31756cf420..88ffb87c879 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/util/CustomClassMapper.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/util/CustomClassMapper.java @@ -14,7 +14,6 @@ package com.google.firebase.firestore.util; -import java.lang.reflect.AccessibleObject; import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.GenericArrayType; @@ -46,7 +45,6 @@ import com.google.firebase.firestore.Exclude; import com.google.firebase.firestore.FieldValue; import com.google.firebase.firestore.GeoPoint; -import com.google.firebase.firestore.PropertyName; import com.google.firebase.firestore.ServerTimestamp; import static com.google.firebase.firestore.util.ApiUtil.invoke; @@ -164,7 +162,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; } @@ -347,7 +345,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; @@ -591,14 +589,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<>(); @@ -607,9 +597,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(); @@ -880,27 +867,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(); @@ -1038,25 +1004,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/firebase-firestore/src/main/java/com/google/firebase/firestore/util/RecordMapper.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/util/RecordMapper.java index a0a397113fa..2540c9e89dc 100755 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/util/RecordMapper.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/util/RecordMapper.java @@ -16,15 +16,13 @@ package com.google.firebase.firestore.util; -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.ArrayList; -import java.util.Date; import java.util.HashMap; import java.util.HashSet; import java.util.List; @@ -32,23 +30,19 @@ import java.util.Set; import java.util.logging.Logger; -import com.google.firebase.Timestamp; import com.google.firebase.firestore.FieldValue; + /** * 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 - * firebase-firestore-sdk34 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. * * @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(); + private static final Class[] CLASSES_ARRAY_TYPE = new Class[0]; // 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. @@ -56,29 +50,23 @@ 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); - constructor = RECORD_INSPECTOR.getCanonicalConstructor(clazz); + constructor = getCanonicalConstructor(clazz); - AnnotatedElement[] recordComponents = RECORD_INSPECTOR.getRecordComponents(clazz); + Field[] recordComponents = clazz.getDeclaredFields(); 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]; + Field recordComponent = recordComponents[i]; String propertyName = propertyName(recordComponent); constructorParamIndexes.put(propertyName, i); - accessors.put(propertyName, RECORD_INSPECTOR.getAccessor(recordComponent)); - applyComponentAnnotations(recordComponent); + accessors.put(propertyName, getAccessor(clazz, recordComponent)); + applyFieldAnnotations(recordComponent); } } @@ -153,54 +141,34 @@ 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 Constructor getCanonicalConstructor(Class cls) { + try { + Class[] paramTypes = getParamTypes(cls); + Constructor constructor = cls.getDeclaredConstructor(paramTypes); + constructor.setAccessible(true); + return constructor; + } catch (NoSuchMethodException e) { + throw new IllegalStateException(e); } } - private static String propertyName(AnnotatedElement component) { - Annotation annotation = getAnnotation(component, "PropertyName"); - if (annotation != null) { - try { - return (String) annotation.getClass().getMethod("value").invoke(annotation); - } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) { - throw new IllegalArgumentException("Failed to get PropertyName annotation value", e); - } + private static Class[] getParamTypes(Class cls) { + Field[] recordComponents = cls.getDeclaredFields(); + List> types = new ArrayList<>(recordComponents.length); + for (Field element : recordComponents) { + types.add(element.getType()); } - - return RECORD_INSPECTOR.getName(component); + return types.toArray(CLASSES_ARRAY_TYPE); } - private static boolean isAnnotationPresent(AnnotatedElement element, String annotationName) { - return getAnnotation(element, annotationName) != null; - } - - private static Annotation getAnnotation( - AnnotatedElement element, String annotationName) { - Annotation[] annotations = element.getAnnotations(); - for (Annotation annotation : annotations) { - if (annotation.annotationType().getSimpleName().equals(annotationName)) { - return annotation; - } + private static Method getAccessor(Class clazz, Field recordComponent) { + try { + Method accessor = clazz.getDeclaredMethod(recordComponent.getName(), CLASSES_ARRAY_TYPE); + accessor.setAccessible(true); + return accessor; + } catch (NoSuchMethodException e) { + throw new IllegalArgumentException("Failed to get record component accessor", e); } - - return null; } // Populate @DocumentId annotated components. If there is a conflict (@DocumentId annotation is @@ -227,81 +195,4 @@ private void populateDocumentIdProperties( } } } - - private static final class RecordInspector { - static final Class[] CLASSES_ARRAY_TYPE = new Class[0]; - 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 = getParamTypes(cls); - Constructor constructor = cls.getDeclaredConstructor(paramTypes); - constructor.setAccessible(true); - return constructor; - } catch (NoSuchMethodException e) { - throw new IllegalStateException(e); - } - } - - private Class[] getParamTypes(Class cls) { - AnnotatedElement[] recordComponents = getRecordComponents(cls); - List> types = new ArrayList<>(recordComponents.length); - for (AnnotatedElement element: recordComponents) { - types.add(getType(element)); - } - return types.toArray(CLASSES_ARRAY_TYPE); - } - - 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); - } - } - } } From 9321083f8297b7bf5a9a89730ab24bf7f2f314d2 Mon Sep 17 00:00:00 2001 From: eranl <1707552+eranl@users.noreply.github.com> Date: Sat, 28 Oct 2023 03:03:08 +0300 Subject: [PATCH 03/11] feat: Added API version checks, Cleanup. --- .../firebase/firestore/util/BeanMapper.java | 3 +- .../firestore/util/CustomClassMapper.java | 3 +- .../firebase/firestore/util/RecordMapper.java | 51 ++++++++----------- 3 files changed, 26 insertions(+), 31 deletions(-) diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/util/BeanMapper.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/util/BeanMapper.java index 84952a7ed75..bacea8111ff 100755 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/util/BeanMapper.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/util/BeanMapper.java @@ -25,6 +25,7 @@ import com.google.firebase.firestore.ThrowOnExtraProperties; import java.lang.reflect.AccessibleObject; +import java.lang.reflect.AnnotatedElement; import java.lang.reflect.Field; import java.lang.reflect.Type; import java.lang.reflect.TypeVariable; @@ -116,7 +117,7 @@ static String propertyName(Field field) { return annotatedName != null? annotatedName : field.getName(); } - static String annotatedName(AccessibleObject obj) { + static String annotatedName(AnnotatedElement obj) { if (obj.isAnnotationPresent(PropertyName.class)) { PropertyName annotation = obj.getAnnotation(PropertyName.class); return annotation.value(); diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/util/CustomClassMapper.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/util/CustomClassMapper.java index 88ffb87c879..de0408b099e 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/util/CustomClassMapper.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/util/CustomClassMapper.java @@ -37,6 +37,7 @@ import java.util.concurrent.ConcurrentMap; import android.net.Uri; +import android.os.Build; import com.google.common.collect.Sets; import com.google.firebase.Timestamp; import com.google.firebase.firestore.Blob; @@ -374,7 +375,7 @@ private static BeanMapper loadOrCreateBeanMapperForClass(Class clazz) @SuppressWarnings("unchecked") BeanMapper mapper = (BeanMapper) mappers.get(clazz); if (mapper == null) { - if (isRecordType(clazz)) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && isRecordType(clazz)) { mapper = new RecordMapper<>(clazz); } else { mapper = new PojoBeanMapper<>(clazz); diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/util/RecordMapper.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/util/RecordMapper.java index 2540c9e89dc..1b50dc1215a 100755 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/util/RecordMapper.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/util/RecordMapper.java @@ -20,16 +20,18 @@ import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; +import java.lang.reflect.Parameter; import java.lang.reflect.Type; import java.lang.reflect.TypeVariable; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; -import java.util.List; import java.util.Map; import java.util.Set; import java.util.logging.Logger; +import android.os.Build; +import androidx.annotation.RequiresApi; import com.google.firebase.firestore.FieldValue; @@ -51,22 +53,33 @@ class RecordMapper extends BeanMapper { private final Constructor constructor; private final Map constructorParamIndexes = new HashMap<>(); + @RequiresApi(api = Build.VERSION_CODES.O) RecordMapper(Class clazz) { super(clazz); - constructor = getCanonicalConstructor(clazz); + Constructor[] constructors = clazz.getConstructors(); + if (constructors.length != 1) { + throw new RuntimeException("Record class has custom constructor(s): " + clazz.getName()); + } + + //noinspection unchecked + constructor = (Constructor) constructors[0]; - Field[] recordComponents = clazz.getDeclaredFields(); + Parameter[] recordComponents = constructor.getParameters(); if (recordComponents.length == 0) { throw new RuntimeException("No properties to serialize found on class " + clazz.getName()); } - for (int i = 0; i < recordComponents.length; i++) { - Field recordComponent = recordComponents[i]; - String propertyName = propertyName(recordComponent); - constructorParamIndexes.put(propertyName, i); - accessors.put(propertyName, getAccessor(clazz, recordComponent)); - applyFieldAnnotations(recordComponent); + try { + for (int i = 0; i < recordComponents.length; i++) { + Field field = clazz.getDeclaredField(recordComponents[i].getName()); + String propertyName = propertyName(field); + constructorParamIndexes.put(propertyName, i); + accessors.put(propertyName, getAccessor(clazz, field)); + applyFieldAnnotations(field); + } + } catch (NoSuchFieldException e) { + throw new RuntimeException(e); } } @@ -141,26 +154,6 @@ T deserialize( } } - private static Constructor getCanonicalConstructor(Class cls) { - try { - Class[] paramTypes = getParamTypes(cls); - Constructor constructor = cls.getDeclaredConstructor(paramTypes); - constructor.setAccessible(true); - return constructor; - } catch (NoSuchMethodException e) { - throw new IllegalStateException(e); - } - } - - private static Class[] getParamTypes(Class cls) { - Field[] recordComponents = cls.getDeclaredFields(); - List> types = new ArrayList<>(recordComponents.length); - for (Field element : recordComponents) { - types.add(element.getType()); - } - return types.toArray(CLASSES_ARRAY_TYPE); - } - private static Method getAccessor(Class clazz, Field recordComponent) { try { Method accessor = clazz.getDeclaredMethod(recordComponent.getName(), CLASSES_ARRAY_TYPE); From 05128c39d100a27ce4301a96e0ab05c5922bdb19 Mon Sep 17 00:00:00 2001 From: eranl <1707552+eranl@users.noreply.github.com> Date: Fri, 3 Oct 2025 00:32:50 +0300 Subject: [PATCH 04/11] Add support for java records --- firebase-firestore-sdk34/CHANGELOG.md | 0 firebase-firestore-sdk34/README.md | 118 ---- .../firebase-firestore-sdk34.gradle | 184 ------ firebase-firestore-sdk34/gradle.properties | 2 - firebase-firestore-sdk34/lint.xml | 27 - firebase-firestore-sdk34/proguard.txt | 18 - .../google/firebase/firestore/TestUtil.java | 126 ---- .../src/test/AndroidManifest.xml | 14 - .../firestore/sdk34/LocalFirestoreHelper.java | 406 ------------- .../sdk34/RecordDocumentReferenceTest.java | 332 ----------- firebase-firestore/firebase-firestore.gradle | 21 +- .../util/AndroidRecordMapperTest.java | 26 + .../firebase/firestore/util/BeanMapper.java | 19 +- .../firestore/util/CustomClassMapper.java | 24 +- .../firestore/util/DeserializeContext.java | 1 - .../firebase/firestore/util/RecordMapper.java | 64 ++- .../firestore/util/RecordMapperTest.java | 103 ++++ .../firestore/util/BaseRecordMapperTest.java | 542 ++++++------------ 18 files changed, 369 insertions(+), 1658 deletions(-) delete mode 100755 firebase-firestore-sdk34/CHANGELOG.md delete mode 100755 firebase-firestore-sdk34/README.md delete mode 100755 firebase-firestore-sdk34/firebase-firestore-sdk34.gradle delete mode 100755 firebase-firestore-sdk34/gradle.properties delete mode 100755 firebase-firestore-sdk34/lint.xml delete mode 100755 firebase-firestore-sdk34/proguard.txt delete mode 100755 firebase-firestore-sdk34/src/roboUtil/java/com/google/firebase/firestore/TestUtil.java delete mode 100755 firebase-firestore-sdk34/src/test/AndroidManifest.xml delete mode 100755 firebase-firestore-sdk34/src/test/java/com/google/firebase/firestore/sdk34/LocalFirestoreHelper.java delete mode 100755 firebase-firestore-sdk34/src/test/java/com/google/firebase/firestore/sdk34/RecordDocumentReferenceTest.java create mode 100644 firebase-firestore/src/androidTest/java/com/google/firebase/firestore/util/AndroidRecordMapperTest.java create mode 100644 firebase-firestore/src/test/java/com/google/firebase/firestore/util/RecordMapperTest.java rename firebase-firestore-sdk34/src/test/java/com/google/firebase/firestore/sdk34/util/RecordMapperTest.java => firebase-firestore/src/testUtil/java/com/google/firebase/firestore/util/BaseRecordMapperTest.java (63%) mode change 100755 => 100644 diff --git a/firebase-firestore-sdk34/CHANGELOG.md b/firebase-firestore-sdk34/CHANGELOG.md deleted file mode 100755 index e69de29bb2d..00000000000 diff --git a/firebase-firestore-sdk34/README.md b/firebase-firestore-sdk34/README.md deleted file mode 100755 index b05694bd4a7..00000000000 --- a/firebase-firestore-sdk34/README.md +++ /dev/null @@ -1,118 +0,0 @@ -# firebase-firestore - -This is the Cloud Firestore component of the Firebase Android SDK. - -Cloud Firestore is a flexible, scalable database for mobile, web, and server -development from Firebase and Google Cloud Platform. Like Firebase Realtime -Database, it keeps your data in sync across client apps through realtime -listeners and offers offline support for mobile and web so you can build -responsive apps that work regardless of network latency or Internet -connectivity. Cloud Firestore also offers seamless integration with other -Firebase and Google Cloud Platform products, including Cloud Functions. - -## Building - -All Gradle commands should be run from the source root (which is one level up -from this folder). See the README.md in the source root for instructions on -publishing/testing Cloud Firestore. - -To build Cloud Firestore, from the source root run: -```bash -./gradlew :firebase-firestore:assembleRelease -``` - -## Unit Testing - -To run unit tests for Cloud Firestore, from the source root run: -```bash -./gradlew :firebase-firestore:check -``` - -## Integration Testing - -Running integration tests requires a Firebase project because they would try -to connect to the Firestore backends. - -See [here](../README.md#project-setup) for how to setup a project. - -Once you setup the project, download `google-services.json` and place it in -the source root. - -Make sure you have created a Firestore instance for your project, before -you proceed. - -By default, integration tests run against the Firestore emulator. - -### Setting up the Firestore Emulator - -The integration tests require that the Firestore emulator is running on port -8080, which is default when running it via CLI. - - * [Install the Firebase CLI](https://firebase.google.com/docs/cli/). - ``` - npm install -g firebase-tools - ``` - * [Install the Firestore - emulator](https://firebase.google.com/docs/firestore/security/test-rules-emulator#install_the_emulator). - ``` - firebase setup:emulators:firestore - ``` - * Run the emulator - ``` - firebase emulators:start --only firestore - ``` - * Select the `Firestore Integration Tests (Firestore Emulator)` run - configuration to run all integration tests. - -To run the integration tests against prod, select `FirestoreProdIntegrationTest` -run configuration. - -### Run on Local Android Emulator - -Then simply run: -```bash -./gradlew :firebase-firestore:connectedCheck -``` - -### Run on Firebase Test Lab - -You can also test on Firebase Test Lab, which allow you to run the integration -tests on devices hosted in Google data center. - -See [here](../README.md#running-integration-tests-on-firebase-test-lab) for -instructions of how to setup Firebase Test Lab for your project. - -Run: -```bash -./gradlew :firebase-firestore:deviceCheck -``` - -## Code Formatting - -Run below to format Java code: -```bash -./gradlew :firebase-firestore:googleJavaFormat -``` - -See [here](../README.md#code-formatting) if you want to be able to format code -from within Android Studio. - -## Build Local Jar of Firestore SDK - -```bash -./gradlew -PprojectsToPublish="firebase-firestore" publishReleasingLibrariesToMavenLocal -``` - -Developers may then take a dependency on these locally published versions by adding -the `mavenLocal()` repository to your [repositories -block](https://docs.gradle.org/current/userguide/declaring_repositories.html) in -your app module's build.gradle. - -## Misc -After importing the project into Android Studio and building successfully -for the first time, Android Studio will delete the run configuration xml files -in `./idea/runConfigurations`. Undo these changes with the command: - -``` -$ git checkout .idea/runConfigurations -``` diff --git a/firebase-firestore-sdk34/firebase-firestore-sdk34.gradle b/firebase-firestore-sdk34/firebase-firestore-sdk34.gradle deleted file mode 100755 index 0f3f086fc1b..00000000000 --- a/firebase-firestore-sdk34/firebase-firestore-sdk34.gradle +++ /dev/null @@ -1,184 +0,0 @@ -// Copyright 2018 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. - -plugins { - id 'firebase-library' - id 'com.google.protobuf' -} - -firebaseLibrary { - libraryGroup "firestore" - publishSources = true - testLab { - enabled = true - timeout = '30m' - } -} - -protobuf { - // Configure the protoc executable - protoc { - // Download from repositories - artifact = "com.google.protobuf:protoc:$protocVersion" - } - plugins { - grpc { - artifact = "io.grpc:protoc-gen-grpc-java:$grpcVersion" - } - } - generateProtoTasks { - all().each { task -> - task.builtins { - java { option 'lite' } - } - task.plugins { - grpc { - option 'lite' - } - } - } - } -} - -android { - adbOptions { - timeOutInMs 60 * 1000 - } - - namespace "com.google.firebase.firestore.sdk34" - compileSdkVersion 34 - defaultConfig { - targetSdkVersion 34 - minSdkVersion 19 - versionName version - multiDexEnabled true - testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" - consumerProguardFiles 'proguard.txt' - - // Acceptable values are: 'emulator', 'qa', 'nightly', and 'prod'. - def targetBackend = findProperty("targetBackend") ?: "emulator" - buildConfigField("String", "TARGET_BACKEND", "\"$targetBackend\"") - - def targetDatabaseId = findProperty('targetDatabaseId') ?: "(default)" - buildConfigField("String", "TARGET_DATABASE_ID", "\"$targetDatabaseId\"") - - def localProps = new Properties() - - try { - file("local.properties").withInputStream { localProps.load(it) } - } catch (FileNotFoundException e) { - } - } - - sourceSets { - main { - proto { - srcDir 'src/proto' - } - } - test { - java { - srcDir 'src/testUtil/java' - srcDir 'src/roboUtil/java' - } - } - androidTest { - java { - srcDir 'src/testUtil/java' - } - } - } - compileOptions { - sourceCompatibility JavaVersion.VERSION_17 - targetCompatibility JavaVersion.VERSION_17 - } - testOptions.unitTests.includeAndroidResources = true - -} - -tasks.withType(Test) { - maxParallelForks = Runtime.runtime.availableProcessors().intdiv(2) ?: 1 -} - -googleJavaFormat { - exclude 'src/testUtil/java/com/google/firebase/firestore/testutil/Assert.java' - exclude 'src/testUtil/java/com/google/firebase/firestore/testutil/ThrowingRunnable.java' -} - -dependencies { -/* - implementation 'com.google.firebase:firebase-annotations:16.2.0' - implementation 'com.google.firebase:firebase-common:20.3.1' - implementation project(':protolite-well-known-types') - implementation 'com.google.firebase:firebase-database-collection:18.0.1' - implementation 'com.google.firebase:firebase-components:17.1.0' - implementation 'com.google.firebase:firebase-appcheck-interop:17.0.0' - - //To provide @Generated annotations - compileOnly 'javax.annotation:jsr250-api:1.0' - - javadocClasspath 'com.google.auto.value:auto-value-annotations:1.6.6' - - implementation 'androidx.annotation:annotation:1.1.0' - implementation "io.grpc:grpc-stub:$grpcVersion" - implementation "io.grpc:grpc-protobuf-lite:$grpcVersion" - implementation "io.grpc:grpc-okhttp:$grpcVersion" - implementation "io.grpc:grpc-android:$grpcVersion" - implementation 'com.google.android.gms:play-services-basement:18.1.0' - implementation 'com.google.android.gms:play-services-tasks:18.0.1' - implementation 'com.google.android.gms:play-services-base:18.0.1' - - implementation('com.google.firebase:firebase-auth-interop:19.0.2') { - exclude group: "com.google.firebase", module: "firebase-common" - } - - compileOnly 'com.google.auto.value:auto-value-annotations:1.6.6' - androidTestAnnotationProcessor 'com.google.auto.value:auto-value:1.6.5' - annotationProcessor 'com.google.auto.value:auto-value:1.6.5' -*/ - - //implementation project(':firebase-firestore') - testImplementation project(':firebase-firestore-sdk34') - testImplementation 'junit:junit:4.13.2' - testImplementation "androidx.test:core:$androidxTestCoreVersion" - testImplementation "org.hamcrest:hamcrest-junit:2.0.0.0" - testImplementation 'org.mockito:mockito-core:2.25.0' - testImplementation "org.robolectric:robolectric:4.10.3" - testImplementation "com.google.truth:truth:$googleTruthVersion" - testImplementation 'com.fasterxml.jackson.core:jackson-databind:2.9.8' - testImplementation 'com.google.guava:guava-testlib:12.0-rc2' - testImplementation project(path: ':firebase-firestore') - testImplementation libs.google.gson - - androidTestImplementation 'junit:junit:4.13.2' - androidTestImplementation("com.google.truth:truth:$googleTruthVersion") { - exclude group: "org.codehaus.mojo", module: "animal-sniffer-annotations" - } - androidTestImplementation 'org.mockito:mockito-core:2.25.0' - androidTestImplementation 'org.mockito:mockito-android:2.25.0' - androidTestImplementation 'com.fasterxml.jackson.core:jackson-databind:2.9.8' - androidTestImplementation "androidx.annotation:annotation:1.1.0" - androidTestImplementation 'androidx.test:runner:1.5.2' - androidTestImplementation 'androidx.test:rules:1.5.0' - androidTestImplementation "androidx.test.ext:junit:$androidxTestJUnitVersion" -} - -gradle.projectsEvaluated { - tasks.withType(JavaCompile) { - // TODO(wuandy): Also add "-Xlint:unchecked". But currently that - // enables 100+ warnings due to our generated source code. - // TODO(wuandy): Re-enable error on warnings once errorprone issues are fixed. - options.compilerArgs << "-Xlint:deprecation" // << "-Werror" - } -} diff --git a/firebase-firestore-sdk34/gradle.properties b/firebase-firestore-sdk34/gradle.properties deleted file mode 100755 index 398412fefb4..00000000000 --- a/firebase-firestore-sdk34/gradle.properties +++ /dev/null @@ -1,2 +0,0 @@ -version=24.8.0-SNAPSHOT -latestReleasedVersion=24.7.1 diff --git a/firebase-firestore-sdk34/lint.xml b/firebase-firestore-sdk34/lint.xml deleted file mode 100755 index 5cdbff248f9..00000000000 --- a/firebase-firestore-sdk34/lint.xml +++ /dev/null @@ -1,27 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/firebase-firestore-sdk34/proguard.txt b/firebase-firestore-sdk34/proguard.txt deleted file mode 100755 index b76f1281567..00000000000 --- a/firebase-firestore-sdk34/proguard.txt +++ /dev/null @@ -1,18 +0,0 @@ -# Needed for DNS resolution. Present in OpenJDK, but not Android --dontwarn javax.naming.** - -# Don't warn about checkerframework -# -# Guava uses the checkerframework and the annotations -# can safely be ignored at runtime. --dontwarn org.checkerframework.** - -# Guava warnings: --dontwarn java.lang.ClassValue --dontwarn com.google.j2objc.annotations.Weak --dontwarn org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement --dontwarn javax.lang.model.element.Modifier - -# Okhttp warnings. --dontwarn okio.** --dontwarn com.google.j2objc.annotations.** diff --git a/firebase-firestore-sdk34/src/roboUtil/java/com/google/firebase/firestore/TestUtil.java b/firebase-firestore-sdk34/src/roboUtil/java/com/google/firebase/firestore/TestUtil.java deleted file mode 100755 index da70dab4a8c..00000000000 --- a/firebase-firestore-sdk34/src/roboUtil/java/com/google/firebase/firestore/TestUtil.java +++ /dev/null @@ -1,126 +0,0 @@ -// Copyright 2018 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.firebase.firestore; - -import com.google.firebase.firestore.model.DocumentKey; -import com.google.firebase.firestore.model.ResourcePath; - -import static org.mockito.Mockito.mock; - -public class TestUtil { - - private static final FirebaseFirestore FIRESTORE = mock(FirebaseFirestore.class); - - public static FirebaseFirestore firestore() { - return FIRESTORE; - } - - public static CollectionReference collectionReference(String path) { - return new CollectionReference(ResourcePath.fromString(path), FIRESTORE); - } - - public static DocumentReference documentReference(String path) { - return new DocumentReference(key(path), FIRESTORE); - } - - public static DocumentKey key(String key) { - return DocumentKey.fromPathString(key); - } - - /* - public static DocumentSnapshot documentSnapshot( - String path, Map data, boolean isFromCache) { - if (data == null) { - return DocumentSnapshot.fromNoDocument(FIRESTORE, key(path), isFromCache); - } else { - return DocumentSnapshot.fromDocument( - FIRESTORE, doc(path, 1L, data), isFromCache, */ -/*hasPendingWrites=*/ /* - false); - } - } - */ - - /* - public static Query query(String path) { - return new Query(com.google.firebase.firestore.testutil.TestUtil.query(path), FIRESTORE); - } - - /** - * A convenience method for creating a particular query snapshot for tests. - * - * @param path To be used in constructing the query. - * @param oldDocs Provides the prior set of documents in the QuerySnapshot. Each entry maps to a - * document, with the key being the document id, and the value being the document contents. - * @param docsToAdd Specifies data to be added into the query snapshot as of now. Each entry maps - * to a document, with the key being the document id, and the value being the document - * contents. - * @param isFromCache Whether the query snapshot is cache result. - * @return A query snapshot that consists of both sets of documents. - * / - public static QuerySnapshot querySnapshot( - String path, - Map oldDocs, - Map docsToAdd, - boolean hasPendingWrites, - boolean isFromCache, - boolean hasCachedResults) { - DocumentSet oldDocuments = docSet(Document.KEY_COMPARATOR); - ImmutableSortedSet mutatedKeys = DocumentKey.emptyKeySet(); - for (Map.Entry pair : oldDocs.entrySet()) { - String docKey = path + "/" + pair.getKey(); - MutableDocument doc = doc(docKey, 1L, pair.getValue()); - if (hasPendingWrites) { - doc.setHasCommittedMutations(); - mutatedKeys = mutatedKeys.insert(key(docKey)); - } - oldDocuments = oldDocuments.add(doc); - } - DocumentSet newDocuments = docSet(Document.KEY_COMPARATOR); - List documentChanges = new ArrayList<>(); - for (Map.Entry pair : docsToAdd.entrySet()) { - String docKey = path + "/" + pair.getKey(); - MutableDocument docToAdd = doc(docKey, 1L, pair.getValue()); - if (hasPendingWrites) { - docToAdd.setHasCommittedMutations(); - mutatedKeys = mutatedKeys.insert(key(docKey)); - } - newDocuments = newDocuments.add(docToAdd); - documentChanges.add(DocumentViewChange.create(Type.ADDED, docToAdd)); - } - ViewSnapshot viewSnapshot = - new ViewSnapshot( - com.google.firebase.firestore.testutil.TestUtil.query(path), - newDocuments, - oldDocuments, - documentChanges, - isFromCache, - mutatedKeys, - /* didSyncStateChange= * / true, - /* excludesMetadataChanges= * / false, - hasCachedResults); - return new QuerySnapshot(query(path), viewSnapshot, FIRESTORE); - } - - public static T waitFor(Task task) { - if (!task.isComplete()) { - Robolectric.flushBackgroundThreadScheduler(); - } - Assert.assertTrue( - "Expected task to be completed after background thread flush", task.isComplete()); - return task.getResult(); - } - */ -} diff --git a/firebase-firestore-sdk34/src/test/AndroidManifest.xml b/firebase-firestore-sdk34/src/test/AndroidManifest.xml deleted file mode 100755 index 26e8ce7a35c..00000000000 --- a/firebase-firestore-sdk34/src/test/AndroidManifest.xml +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - - - - diff --git a/firebase-firestore-sdk34/src/test/java/com/google/firebase/firestore/sdk34/LocalFirestoreHelper.java b/firebase-firestore-sdk34/src/test/java/com/google/firebase/firestore/sdk34/LocalFirestoreHelper.java deleted file mode 100755 index 61db82ffae4..00000000000 --- a/firebase-firestore-sdk34/src/test/java/com/google/firebase/firestore/sdk34/LocalFirestoreHelper.java +++ /dev/null @@ -1,406 +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.firebase.firestore.sdk34; - -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.common.reflect.TypeToken; -import com.google.firebase.Timestamp; -import com.google.firebase.firestore.Blob; -import com.google.firebase.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 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 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.firebase.firestore.ServerTimestamp Date foo, - Inner inner - - ){ - record Inner ( - - @com.google.firebase.firestore.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/firebase-firestore-sdk34/src/test/java/com/google/firebase/firestore/sdk34/RecordDocumentReferenceTest.java b/firebase-firestore-sdk34/src/test/java/com/google/firebase/firestore/sdk34/RecordDocumentReferenceTest.java deleted file mode 100755 index 6edccd97597..00000000000 --- a/firebase-firestore-sdk34/src/test/java/com/google/firebase/firestore/sdk34/RecordDocumentReferenceTest.java +++ /dev/null @@ -1,332 +0,0 @@ -/* -// Copyright 2018 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.firebase.firestore; - -import static com.google.firebase.firestore.sdk34.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.firebase.firestore.sdk34.LocalFirestoreHelper; -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.robolectric.RobolectricTestRunner; -import org.robolectric.annotation.Config; - - -@RunWith(RobolectricTestRunner.class) -@Config(manifest = Config.NONE) -public class RecordDocumentReferenceTest { - @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/firebase-firestore/firebase-firestore.gradle b/firebase-firestore/firebase-firestore.gradle index 12da631f2ec..e1a01282c4a 100644 --- a/firebase-firestore/firebase-firestore.gradle +++ b/firebase-firestore/firebase-firestore.gradle @@ -13,6 +13,7 @@ // limitations under the License. import org.jetbrains.kotlin.gradle.dsl.JvmTarget +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile plugins { id 'firebase-library' @@ -107,19 +108,33 @@ android { } } compileOptions { - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 + // Java 17 needed for record test cases + sourceCompatibility JavaVersion.VERSION_17 + targetCompatibility JavaVersion.VERSION_17 } testOptions.unitTests.includeAndroidResources = true } -kotlin { compilerOptions { jvmTarget = JvmTarget.JVM_1_8 } } +kotlin { compilerOptions { jvmTarget = JvmTarget.JVM_17 } } + +tasks.withType(KotlinCompile).configureEach { + compilerOptions { + jvmTarget.set(JvmTarget.JVM_17) + } +} tasks.withType(Test) { maxParallelForks = Runtime.runtime.availableProcessors().intdiv(2) ?: 1 } +// Needed for testing RecordMapper's constructor selection +tasks.withType(JavaCompile).configureEach { + if (it.name.contains("UnitTest") || it.name.contains("AndroidTest")) { + options.compilerArgs.add("-parameters") + } +} + dependencies { javadocClasspath libs.autovalue.annotations diff --git a/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/util/AndroidRecordMapperTest.java b/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/util/AndroidRecordMapperTest.java new file mode 100644 index 00000000000..fcb7b1d1cbd --- /dev/null +++ b/firebase-firestore/src/androidTest/java/com/google/firebase/firestore/util/AndroidRecordMapperTest.java @@ -0,0 +1,26 @@ +// Copyright 2018 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.firebase.firestore.util; + +import androidx.test.ext.junit.runners.AndroidJUnit4; +import org.junit.runner.RunWith; + +/** + * Tests for {@link RecordMapper} using desugared java records. + * + * @author Eran Leshem + */ +@RunWith(AndroidJUnit4.class) +public class AndroidRecordMapperTest extends BaseRecordMapperTest {} diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/util/BeanMapper.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/util/BeanMapper.java index bacea8111ff..9c74289e4dc 100755 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/util/BeanMapper.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/util/BeanMapper.java @@ -16,6 +16,7 @@ package com.google.firebase.firestore.util; +import android.os.Build; import com.google.firebase.Timestamp; import com.google.firebase.firestore.DocumentId; import com.google.firebase.firestore.DocumentReference; @@ -23,12 +24,11 @@ import com.google.firebase.firestore.PropertyName; import com.google.firebase.firestore.ServerTimestamp; import com.google.firebase.firestore.ThrowOnExtraProperties; - -import java.lang.reflect.AccessibleObject; import java.lang.reflect.AnnotatedElement; 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; @@ -36,7 +36,6 @@ import java.util.Map; import java.util.Set; - /** Base bean mapper class, providing common functionality for class and record serialization. */ abstract class BeanMapper { private final Class clazz; @@ -98,9 +97,15 @@ abstract T deserialize( 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."); + if (fieldType != Date.class + && fieldType != Timestamp.class + && !(Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && fieldType == Instant.class)) { + throw new IllegalArgumentException( + "Field " + + field.getName() + + " is annotated with @ServerTimestamp but is " + + fieldType + + " instead of Date, Timestamp, or Instant."); } serverTimestamps.add(propertyName(field)); } @@ -114,7 +119,7 @@ void applyFieldAnnotations(Field field) { static String propertyName(Field field) { String annotatedName = annotatedName(field); - return annotatedName != null? annotatedName : field.getName(); + return annotatedName != null ? annotatedName : field.getName(); } static String annotatedName(AnnotatedElement obj) { diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/util/CustomClassMapper.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/util/CustomClassMapper.java index 3f317a0f17d..2452fdce3e9 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/util/CustomClassMapper.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/util/CustomClassMapper.java @@ -20,6 +20,7 @@ import android.net.Uri; import android.os.Build; import androidx.annotation.RequiresApi; +import com.google.common.collect.Sets; import com.google.firebase.Timestamp; import com.google.firebase.firestore.Blob; import com.google.firebase.firestore.DocumentId; @@ -27,12 +28,8 @@ import com.google.firebase.firestore.Exclude; import com.google.firebase.firestore.FieldValue; import com.google.firebase.firestore.GeoPoint; -import com.google.firebase.firestore.IgnoreExtraProperties; -import com.google.firebase.firestore.PropertyName; import com.google.firebase.firestore.ServerTimestamp; -import com.google.firebase.firestore.ThrowOnExtraProperties; import com.google.firebase.firestore.VectorValue; -import java.lang.reflect.AccessibleObject; import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.GenericArrayType; @@ -56,27 +53,14 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; -import android.net.Uri; -import android.os.Build; -import com.google.common.collect.Sets; -import com.google.firebase.Timestamp; -import com.google.firebase.firestore.Blob; -import com.google.firebase.firestore.DocumentId; -import com.google.firebase.firestore.DocumentReference; -import com.google.firebase.firestore.Exclude; -import com.google.firebase.firestore.FieldValue; -import com.google.firebase.firestore.GeoPoint; -import com.google.firebase.firestore.ServerTimestamp; - -import static com.google.firebase.firestore.util.ApiUtil.invoke; -import static com.google.firebase.firestore.util.ApiUtil.newInstance; - /** Helper class to convert to/from custom POJO classes and plain Java types. */ public class CustomClassMapper { /** Maximum depth before we give up and assume it's a recursive object graph. */ private static final int MAX_DEPTH = 500; private static final ConcurrentMap, BeanMapper> mappers = new ConcurrentHashMap<>(); + private static final HashSet RECORD_BASE_CLASS_NAMES = + Sets.newHashSet("java.lang.Record", "com.android.tools.r8.RecordTag"); private static void hardAssert(boolean assertion) { hardAssert(assertion, "Internal inconsistency"); @@ -625,7 +609,7 @@ private static RuntimeException deserializeError( private static boolean isRecordType(Class cls) { Class parent = cls.getSuperclass(); - return parent != null && Sets.newHashSet("java.lang.Record", "com.android.tools.r8.RecordTag").contains(parent.getName()); + return parent != null && RECORD_BASE_CLASS_NAMES.contains(parent.getName()); } // Helper class to convert from maps to custom objects (Beans), and vice versa. diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/util/DeserializeContext.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/util/DeserializeContext.java index 594b0f45adb..310db22e805 100755 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/util/DeserializeContext.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/util/DeserializeContext.java @@ -18,7 +18,6 @@ import com.google.firebase.firestore.DocumentReference; - /** Holds information a deserialization operation needs to complete the job. */ class DeserializeContext { /** diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/util/RecordMapper.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/util/RecordMapper.java index 1b50dc1215a..9f49b9a2248 100755 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/util/RecordMapper.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/util/RecordMapper.java @@ -16,32 +16,40 @@ package com.google.firebase.firestore.util; +import android.os.Build; +import androidx.annotation.RequiresApi; +import com.google.firebase.firestore.FieldValue; 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.Parameter; import java.lang.reflect.Type; import java.lang.reflect.TypeVariable; -import java.util.ArrayList; +import java.util.Arrays; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Set; import java.util.logging.Logger; - -import android.os.Build; -import androidx.annotation.RequiresApi; -import com.google.firebase.firestore.FieldValue; - +import java.util.stream.Collectors; /** - * 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. + * Serializes java records. Uses canonical record constructors and accessors only. Therefore, + * exclusion of fields is not supported. Supports {@code DocumentId}, {@code PropertyName}, + * and {@code ServerTimestamp} annotations on record components. + * Since java records may be desugared, and record component-related reflection methods may be missing, + * the canonical record constructor is identified through matching of parameter names and types with fields. + * Therefore, a mapped record must not have a custom constructor + * with the same set of parameter names and types as the canonical one + * (by default, only the canonical constructor's parameter names are preserved at runtime, + * and the others' get generic runtime names, + * but that can be changed with the {@code -parameters} compiler option). * * @author Eran Leshem */ +@RequiresApi(api = Build.VERSION_CODES.O) class RecordMapper extends BeanMapper { private static final Logger LOGGER = Logger.getLogger(RecordMapper.class.getName()); private static final Class[] CLASSES_ARRAY_TYPE = new Class[0]; @@ -53,18 +61,10 @@ class RecordMapper extends BeanMapper { private final Constructor constructor; private final Map constructorParamIndexes = new HashMap<>(); - @RequiresApi(api = Build.VERSION_CODES.O) RecordMapper(Class clazz) { super(clazz); - Constructor[] constructors = clazz.getConstructors(); - if (constructors.length != 1) { - throw new RuntimeException("Record class has custom constructor(s): " + clazz.getName()); - } - - //noinspection unchecked - constructor = (Constructor) constructors[0]; - + constructor = getConstructor(clazz); Parameter[] recordComponents = constructor.getParameters(); if (recordComponents.length == 0) { throw new RuntimeException("No properties to serialize found on class " + clazz.getName()); @@ -83,6 +83,34 @@ class RecordMapper extends BeanMapper { } } + private static Constructor getConstructor(Class clazz) { + Map components = + Arrays.stream(clazz.getDeclaredFields()) + .filter(field -> !Modifier.isStatic(field.getModifiers())) + .collect(Collectors.toMap(Field::getName, Field::getGenericType)); + Constructor match = null; + //noinspection unchecked + for (Constructor ctor : (Constructor[]) clazz.getConstructors()) { + Parameter[] parameters = ctor.getParameters(); + Map parameterTypes = + Arrays.stream(parameters) + .collect(Collectors.toMap(Parameter::getName, Parameter::getParameterizedType)); + if (!parameterTypes.equals(components)) { + continue; + } + + if (match != null) { + throw new RuntimeException( + String.format( + "Multiple constructors match set of components for record %s", clazz.getName())); + } + + match = ctor; + } + + return match; + } + @Override Map serialize(T object, DeserializeContext.ErrorPath path) { verifyValidType(object); diff --git a/firebase-firestore/src/test/java/com/google/firebase/firestore/util/RecordMapperTest.java b/firebase-firestore/src/test/java/com/google/firebase/firestore/util/RecordMapperTest.java new file mode 100644 index 00000000000..7acdf7deed9 --- /dev/null +++ b/firebase-firestore/src/test/java/com/google/firebase/firestore/util/RecordMapperTest.java @@ -0,0 +1,103 @@ +// Copyright 2018 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.firebase.firestore.util; + +import static android.os.Build.VERSION_CODES.O; +import static org.junit.Assert.assertEquals; + +import com.google.firebase.firestore.DocumentReference; +import com.google.firebase.firestore.TestUtil; +import java.util.Collections; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.annotation.Config; + +/** + * Tests for {@link RecordMapper} using non-desugared java records. + * + * @author Eran Leshem + */ +@RunWith(org.robolectric.RobolectricTestRunner.class) +@Config(manifest = Config.NONE, minSdk = O) +@SuppressWarnings({"unused", "WeakerAccess"}) +public class RecordMapperTest extends BaseRecordMapperTest { + @Test + public void documentIdsDeserialize() { + DocumentReference ref = TestUtil.documentReference("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. + + DocumentReference ref = TestUtil.documentReference("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 String expectedErrorMessage = "cannot apply @DocumentId on this property"; + DocumentReference ref = TestUtil.documentReference("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/firebase-firestore-sdk34/src/test/java/com/google/firebase/firestore/sdk34/util/RecordMapperTest.java b/firebase-firestore/src/testUtil/java/com/google/firebase/firestore/util/BaseRecordMapperTest.java old mode 100755 new mode 100644 similarity index 63% rename from firebase-firestore-sdk34/src/test/java/com/google/firebase/firestore/sdk34/util/RecordMapperTest.java rename to firebase-firestore/src/testUtil/java/com/google/firebase/firestore/util/BaseRecordMapperTest.java index 702577e81c4..a1b2bf5bd04 --- a/firebase-firestore-sdk34/src/test/java/com/google/firebase/firestore/sdk34/util/RecordMapperTest.java +++ b/firebase-firestore/src/testUtil/java/com/google/firebase/firestore/util/BaseRecordMapperTest.java @@ -1,21 +1,17 @@ -// Copyright 2018 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.firebase.firestore.sdk34.util; +package com.google.firebase.firestore.util; +import static com.google.firebase.firestore.testutil.TestUtil.fromSingleQuotedString; +import static com.google.firebase.firestore.testutil.TestUtil.map; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import com.google.firebase.firestore.DocumentId; +import com.google.firebase.firestore.DocumentReference; +import com.google.firebase.firestore.PropertyName; +import com.google.firebase.firestore.ThrowOnExtraProperties; import java.io.Serializable; -import java.math.BigDecimal; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; @@ -25,233 +21,128 @@ import java.util.List; import java.util.Map; import java.util.Set; - +import org.junit.Assert; import org.junit.Test; -import org.robolectric.annotation.Config; - -import com.google.firebase.firestore.DocumentId; -import com.google.firebase.firestore.DocumentReference; -import com.google.firebase.firestore.PropertyName; -import com.google.firebase.firestore.TestUtil; -import com.google.firebase.firestore.ThrowOnExtraProperties; -import com.google.firebase.firestore.util.CustomClassMapper; - -import static com.google.firebase.firestore.sdk34.LocalFirestoreHelper.fromSingleQuotedString; -import static com.google.firebase.firestore.sdk34.LocalFirestoreHelper.mapAnyType; -import static org.junit.Assert.*; -@org.junit.runner.RunWith(org.robolectric.RobolectricTestRunner.class) -@Config(manifest = Config.NONE, sdk = 33) -@SuppressWarnings({"unused", "WeakerAccess", "SpellCheckingInspection"}) -public class RecordMapperTest { - private static final double EPSILON = 0.0003; - - public record StringBean ( - String value - ){} +/** + * @author Eran Leshem + * + * @noinspection JUnitMalformedDeclaration*/ +class BaseRecordMapperTest { + public record StringBean(String value) {} - public record DoubleBean ( - double value - ){} + public record DoubleBean(double value) {} - public record BigDecimalBean ( - BigDecimal value - ){} + public record FloatBean(float value) {} - public record FloatBean ( - float value - ){} + public record LongBean(long value) {} - public record LongBean ( - long value - ){} + public record IntBean(int value) {} - public record IntBean ( - int value - ){} + public record BooleanBean(boolean value) {} - public record BooleanBean ( - boolean value - ){} + public record ShortBean(short value) {} - public record ShortBean ( - short value - ){} + public record ByteBean(byte value) {} - public record ByteBean ( - byte value - ){} + public record CharBean(char value) {} - public record CharBean ( - char value - ){} + public record IntArrayBean(int[] values) {} - public record IntArrayBean ( - int[] values - ){} + public record StringArrayBean(String[] values) {} - public record StringArrayBean ( - String[] values - ){} + public record XMLAndURLBean(String XMLAndURL) {} - public record XMLAndURLBean ( - String XMLAndURL - ){} + public record CaseSensitiveFieldBean1(String VALUE) {} - public record CaseSensitiveFieldBean1 ( - String VALUE - ){} + public record CaseSensitiveFieldBean2(String value) {} - public record CaseSensitiveFieldBean2 ( - String value - ){} + public record CaseSensitiveFieldBean3(String Value) {} - public record CaseSensitiveFieldBean3 ( - String Value - ){} + public record CaseSensitiveFieldBean4(String valUE) {} - public record CaseSensitiveFieldBean4 ( - String valUE - ){} + public record NestedBean(StringBean bean) {} - public record NestedBean ( - StringBean bean - ){} + public record ObjectBean(Object value) {} - public record ObjectBean ( - Object value - ){} + public record GenericBean(B value) {} - public record GenericBean ( - B value - ){} + public record DoubleGenericBean(A valueA, B valueB) {} - public record DoubleGenericBean ( - A valueA, - B valueB - ){} + public record ListBean(List values) {} - public record ListBean ( - List values - ){} + public record SetBean(Set values) {} - public record SetBean ( - Set values - ){} + public record CollectionBean(Collection values) {} - public record CollectionBean ( - Collection values - ){} - - public record MapBean ( - Map 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 UpperBoundedMapBean(Map values) {} - public record MultiBoundedMapBean ( - Map values - ){} + public record MultiBoundedMapBean(Map values) {} - public record MultiBoundedMapHolderBean ( - MultiBoundedMapBean map - ){} + public record MultiBoundedMapHolderBean(MultiBoundedMapBean map) {} - public record UnboundedMapBean ( - Map values - ){} + public record UnboundedMapBean(Map values) {} - public record UnboundedTypeVariableMapBean ( - Map values - ){} + public record UnboundedTypeVariableMapBean(Map values) {} - public record UnboundedTypeVariableMapHolderBean ( - UnboundedTypeVariableMapBean map - ){} + public record UnboundedTypeVariableMapHolderBean(UnboundedTypeVariableMapBean map) {} - public record NestedListBean ( - List values - ){} + public record NestedListBean(List values) {} - public record NestedMapBean ( - Map values - ){} + public record NestedMapBean(Map values) {} - public record IllegalKeyMapBean ( - Map values - ){} + public record IllegalKeyMapBean(Map values) {} @ThrowOnExtraProperties - public record ThrowOnUnknownPropertiesBean ( - String value - ){} + public record ThrowOnUnknownPropertiesBean(String value) {} @ThrowOnExtraProperties - public record NoFieldBean( - ){} - - public record PropertyNameBean ( - @PropertyName("my_key") - String key, + public record NoFieldBean() {} - @PropertyName("my_value") - String value - ){} + public record PropertyNameBean( + @PropertyName("my_key") String key, @PropertyName("my_value") String value) {} @SuppressWarnings({"NonAsciiCharacters"}) - public record UnicodeBean ( - String 漢字 - ){} + public record UnicodeBean(String 漢字) {} - private static T deserialize(String jsonString, Class clazz) { - return deserialize(jsonString, clazz, /*docRef=*/ null); - } + public record AllCapsDefaultHandlingBean(String UUID) {} - private static T deserialize(Map json, Class clazz) { - return deserialize(json, clazz, /*docRef=*/ null); - } + public record AllCapsWithPropertyName(@PropertyName("uuid") String UUID) {} - private static T deserialize(String jsonString, Class clazz, DocumentReference docRef) { - var json = fromSingleQuotedString(jsonString); - return CustomClassMapper.convertToCustomClass(json, clazz, docRef); - } + // Bean definitions with @DocumentId applied to wrong type. + public record FieldWithDocumentIdOnWrongTypeBean(@DocumentId Integer intField) {} - private static T deserialize( - Map json, Class clazz, DocumentReference docRef) { - return CustomClassMapper.convertToCustomClass(json, clazz, docRef); - } + public record PropertyWithDocumentIdOnWrongTypeBean( + @PropertyName("intField") @DocumentId int intField) {} - private static Object serialize(Object object) { - return CustomClassMapper.convertToPlainJavaTypes(object); - } + public record DocumentIdOnStringField(@DocumentId String docId) {} - private static void assertJson(String expected, Object actual) { - assertEquals(fromSingleQuotedString(expected), actual); - } + public record DocumentIdOnStringFieldAsProperty( + @PropertyName("docIdProperty") @DocumentId String docId, + @PropertyName("anotherProperty") int someOtherProperty) {} - private static void assertExceptionContains(String partialMessage, Runnable run) { - try { - run.run(); - fail("Expected exception not thrown"); - } catch (RuntimeException e) { - assertTrue(e.getMessage().contains(partialMessage)); + public record DocumentIdOnNestedObjects( + @PropertyName("nestedDocIdHolder") DocumentIdOnStringField nestedDocIdHolder) {} + + public record CustomConstructorBean(String value) { + public CustomConstructorBean() { + this("value"); } } - private static T convertToCustomClass( - Object object, Class clazz, DocumentReference docRef) { - return CustomClassMapper.convertToCustomClass(object, clazz, docRef); + public record ConflictingConstructorBean(String value, int i) { + public ConflictingConstructorBean(int i, String value) { + this(value, i); + } } - private static T convertToCustomClass(Object object, Class clazz) { - return CustomClassMapper.convertToCustomClass(object, clazz, null); - } + private static final double EPSILON = 0.0003; @Test public void primitiveDeserializeString() { @@ -348,52 +239,6 @@ public void primitiveDeserializeDouble() { } } - /* - @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); @@ -403,8 +248,7 @@ public void primitiveDeserializeFloat() { var beanInt = deserialize(Collections.singletonMap("value", 1), FloatBean.class); assertEquals(1, beanInt.value(), EPSILON); // Long - var beanLong = - deserialize(Collections.singletonMap("value", 1234567890123L), FloatBean.class); + var beanLong = deserialize(Collections.singletonMap("value", 1234567890123L), FloatBean.class); assertEquals((float) 1234567890123L, beanLong.value(), EPSILON); // Boolean @@ -518,7 +362,7 @@ public void primitiveDeserializeWrongTypeList() { public void noFieldDeserialize() { assertExceptionContains( "No properties to serialize found on class " - + "com.google.firebase.firestore.sdk34.util.RecordMapperTest$NoFieldBean", + + "com.google.firebase.firestore.util.BaseRecordMapperTest$NoFieldBean", () -> deserialize("{'value': 'foo'}", NoFieldBean.class)); } @@ -526,42 +370,30 @@ public void noFieldDeserialize() { public void throwOnUnknownProperties() { assertExceptionContains( "No accessor for unknown found on class " - + "com.google.firebase.firestore.sdk34.util.RecordMapperTest$ThrowOnUnknownPropertiesBean", + + "com.google.firebase.firestore.util.BaseRecordMapperTest$ThrowOnUnknownPropertiesBean", () -> deserialize("{'value': 'foo', 'unknown': 'bar'}", ThrowOnUnknownPropertiesBean.class)); } @Test public void XMLAndURLBean() { - var bean = - deserialize("{'XMLAndURL': 'foo'}", XMLAndURLBean.class); + 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); + 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); + var deserialized = deserialize("{'uuid': 'value'}", AllCapsWithPropertyName.class); assertEquals("value", deserialized.UUID()); } @@ -587,44 +419,42 @@ public void beansCanContainMaps() { @Test public void beansCanContainUpperBoundedMaps() { var date = new Date(1491847082123L); - var source = mapAnyType("values", mapAnyType("foo", date)); + var source = map("values", map("foo", date)); var bean = convertToCustomClass(source, UpperBoundedMapBean.class); - var expected = mapAnyType("foo", date); + var expected = map("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 source = map("map", map("values", map("foo", date))); var bean = convertToCustomClass(source, MultiBoundedMapHolderBean.class); - var expected = mapAnyType("foo", date); + var expected = map("foo", date); assertEquals(expected, bean.map().values()); } @Test public void beansCanContainUnboundedMaps() { var bean = deserialize("{'values': {'foo': 'bar'}}", UnboundedMapBean.class); - var expected = mapAnyType("foo", "bar"); + var expected = map("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 source = map("map", map("values", map("foo", "bar"))); + var bean = convertToCustomClass(source, UnboundedTypeVariableMapHolderBean.class); - var expected = mapAnyType("foo", "bar"); + var expected = map("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")); + var bean = deserialize("{'values': {'foo': {'bar': 'baz'}}}", UnboundedMapBean.class); + var expected = map("foo", map("bar", "baz")); assertEquals(expected, bean.values()); } @@ -665,7 +495,7 @@ public void serializeDoubleBean() { @Test public void serializeIntBean() { var bean = new IntBean(1); - assertJson("{'value': 1}", serialize(Collections.singletonMap("value", 1.0))); + assertJson("{'value': 1}", serialize(Collections.singletonMap("value", 1))); } @Test @@ -676,23 +506,6 @@ public void serializeLongBean() { 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); @@ -704,7 +517,7 @@ 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)); + Assert.assertEquals(map("value", 0.5f), serialize(bean)); } @Test @@ -712,7 +525,7 @@ public void serializePrivateFieldBean() { final var bean = new NoFieldBean(); assertExceptionContains( "No properties to serialize found on class " - + "com.google.firebase.firestore.sdk34.util.RecordMapperTest$NoFieldBean", + + "com.google.firebase.firestore.util.BaseRecordMapperTest$NoFieldBean", () -> serialize(bean)); } @@ -739,8 +552,7 @@ public void serializingMapsWorks() { 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()))); + var expected = map("values", map("foo", new Date(date.getTime()))); assertEquals(expected, serialize(bean)); } @@ -753,8 +565,7 @@ public void serializingMultiBoundedObjectsWorks() { var holder = new MultiBoundedMapHolderBean(new MultiBoundedMapBean<>(values)); - var expected = - mapAnyType("map", mapAnyType("values", mapAnyType("foo", new Date(date.getTime())))); + var expected = map("map", map("values", map("foo", new Date(date.getTime())))); assertEquals(expected, serialize(holder)); } @@ -800,8 +611,7 @@ public void serializeUPPERCASE() { public void roundTripCaseSensitiveFieldBean1() { var bean = new CaseSensitiveFieldBean1("foo"); assertJson("{'VALUE': 'foo'}", serialize(bean)); - var deserialized = - deserialize("{'VALUE': 'foo'}", CaseSensitiveFieldBean1.class); + var deserialized = deserialize("{'VALUE': 'foo'}", CaseSensitiveFieldBean1.class); assertEquals("foo", deserialized.VALUE()); } @@ -809,8 +619,7 @@ public void roundTripCaseSensitiveFieldBean1() { public void roundTripCaseSensitiveFieldBean2() { var bean = new CaseSensitiveFieldBean2("foo"); assertJson("{'value': 'foo'}", serialize(bean)); - var deserialized = - deserialize("{'value': 'foo'}", CaseSensitiveFieldBean2.class); + var deserialized = deserialize("{'value': 'foo'}", CaseSensitiveFieldBean2.class); assertEquals("foo", deserialized.value()); } @@ -818,8 +627,7 @@ public void roundTripCaseSensitiveFieldBean2() { public void roundTripCaseSensitiveFieldBean3() { var bean = new CaseSensitiveFieldBean3("foo"); assertJson("{'Value': 'foo'}", serialize(bean)); - var deserialized = - deserialize("{'Value': 'foo'}", CaseSensitiveFieldBean3.class); + var deserialized = deserialize("{'Value': 'foo'}", CaseSensitiveFieldBean3.class); assertEquals("foo", deserialized.Value()); } @@ -827,8 +635,7 @@ public void roundTripCaseSensitiveFieldBean3() { public void roundTripCaseSensitiveFieldBean4() { var bean = new CaseSensitiveFieldBean4("foo"); assertJson("{'valUE': 'foo'}", serialize(bean)); - var deserialized = - deserialize("{'valUE': 'foo'}", CaseSensitiveFieldBean4.class); + var deserialized = deserialize("{'valUE': 'foo'}", CaseSensitiveFieldBean4.class); assertEquals("foo", deserialized.valUE()); } @@ -925,16 +732,17 @@ public void objectAcceptsAnyObject() { 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()); + Assert.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()); + Assert.assertEquals(fromSingleQuotedString(complex).get("value"), complexValue.value()); } @Test public void passingInGenericBeanTopLevelThrows() { - assertExceptionContains("Class com.google.firebase.firestore.sdk34.util.RecordMapperTest$GenericBean has generic type parameters", - () -> deserialize("{'value': 'foo'}", GenericBean.class)); + assertExceptionContains( + "Class com.google.firebase.firestore.util.BaseRecordMapperTest$GenericBean has generic type parameters", + () -> deserialize("{'value': 'foo'}", GenericBean.class)); } @Test @@ -974,7 +782,7 @@ public void serializingGenericBeansSupported() { assertJson("{'value': {'value': 'foo'}}", serialize(recursiveBean)); var doubleBean = new DoubleGenericBean("foo", 1.0); - assertJson("{'valueB': 1, 'valueA': 'foo'}", serialize(doubleBean)); + assertJson("{'valueB': 1.0, 'valueA': 'foo'}", serialize(doubleBean)); } @Test @@ -986,23 +794,11 @@ public void propertyNamesAreSerialized() { @Test public void propertyNamesAreParsed() { - var bean = - deserialize("{'my_key': 'foo', 'my_value': 'bar'}", PropertyNameBean.class); + 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"; @@ -1019,89 +815,71 @@ public void documentIdAnnotateWrongTypeThrows() { () -> 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() { - DocumentReference ref = TestUtil.documentReference("coll/doc123"); - - assertEquals("doc123", deserialize("{}", DocumentIdOnStringField.class, ref).docId()); - + public void customConstructorRoundTrip() { + final var bean = new CustomConstructorBean("foo"); 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()); + bean, + CustomClassMapper.convertToCustomClass(serialize(bean), CustomConstructorBean.class, null)); + } - assertEquals( - "doc123", - deserialize("{'nestedDocIdHolder': {}}", DocumentIdOnNestedObjects.class, ref) - .nestedDocIdHolder() - .docId()); + @Test + public void conflictingConstructorCantBeSerialized() { + final var bean = new ConflictingConstructorBean("foo", 5); + assertExceptionContains( + "Multiple constructors match set of components for record " + + "com.google.firebase.firestore.util.BaseRecordMapperTest$ConflictingConstructorBean", + () -> serialize(bean)); } @Test - public void documentIdsRoundTrip() { - // Implicitly verifies @DocumentId is ignored during serialization. + public void conflictingConstructorCantBeDeserialized() { + assertExceptionContains( + "Multiple constructors match set of components for record " + + "com.google.firebase.firestore.util.BaseRecordMapperTest$ConflictingConstructorBean", + () -> deserialize(map("foo", 5), ConflictingConstructorBean.class)); + } - DocumentReference ref = TestUtil.documentReference("coll/doc123"); + static T deserialize(String jsonString, Class clazz) { + return deserialize(jsonString, clazz, /* docRef= */ null); + } - assertEquals( - Collections.emptyMap(), serialize(deserialize("{}", DocumentIdOnStringField.class, ref))); + static T deserialize(Map json, Class clazz) { + return deserialize(json, clazz, /* docRef= */ null); + } - assertEquals( - Collections.singletonMap("anotherProperty", 100), - serialize( - deserialize("{'anotherProperty': 100}", DocumentIdOnStringFieldAsProperty.class, ref))); + static T deserialize(String jsonString, Class clazz, DocumentReference docRef) { + var json = fromSingleQuotedString(jsonString); + return CustomClassMapper.convertToCustomClass(json, clazz, docRef); + } - assertEquals( - Collections.singletonMap("nestedDocIdHolder", Collections.emptyMap()), - serialize(deserialize("{'nestedDocIdHolder': {}}", DocumentIdOnNestedObjects.class, ref))); + static T deserialize(Map json, Class clazz, DocumentReference docRef) { + return CustomClassMapper.convertToCustomClass(json, clazz, docRef); } - @Test - public void documentIdsDeserializeConflictThrows() { - final String expectedErrorMessage = "cannot apply @DocumentId on this property"; - DocumentReference ref = TestUtil.documentReference("coll/doc123"); + static Object serialize(Object object) { + return CustomClassMapper.convertToPlainJavaTypes(object); + } - assertExceptionContains( - expectedErrorMessage, - () -> deserialize("{'docId': 'toBeOverwritten'}", DocumentIdOnStringField.class, ref)); + private static void assertJson(String expected, Object actual) { + Assert.assertEquals(fromSingleQuotedString(expected), actual); + } - assertExceptionContains( - expectedErrorMessage, - () -> - deserialize( - "{'docIdProperty': 'toBeOverwritten', 'anotherProperty': 100}", - DocumentIdOnStringFieldAsProperty.class, - ref)); + static void assertExceptionContains(String partialMessage, Runnable run) { + try { + run.run(); + fail("Expected exception not thrown"); + } catch (RuntimeException e) { + assertTrue(e.getMessage().contains(partialMessage)); + } + } - assertExceptionContains( - expectedErrorMessage, - () -> - deserialize( - "{'nestedDocIdHolder': {'docId': 'toBeOverwritten'}}", - DocumentIdOnNestedObjects.class, - ref)); + private static T convertToCustomClass( + Object object, Class clazz, DocumentReference docRef) { + return CustomClassMapper.convertToCustomClass(object, clazz, docRef); + } + + static T convertToCustomClass(Object object, Class clazz) { + return CustomClassMapper.convertToCustomClass(object, clazz, null); } } From 6e0e6d47132b007b4a4d94bb343b88714f2d5b61 Mon Sep 17 00:00:00 2001 From: Eran Leshem <1707552+eranl@users.noreply.github.com> Date: Fri, 3 Oct 2025 00:48:46 +0300 Subject: [PATCH 05/11] Delete build.gradle --- build.gradle | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 build.gradle diff --git a/build.gradle b/build.gradle deleted file mode 100644 index e69de29bb2d..00000000000 From f1ae87038498044062c09304fb367fd8a4989ecd Mon Sep 17 00:00:00 2001 From: Eran Leshem <1707552+eranl@users.noreply.github.com> Date: Fri, 3 Oct 2025 00:49:45 +0300 Subject: [PATCH 06/11] Delete buildSrc/build.gradle.kts --- buildSrc/build.gradle.kts | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 buildSrc/build.gradle.kts diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts deleted file mode 100644 index e69de29bb2d..00000000000 From 5f7689b72ff886c50d4eb3e9957c29fcb991c60b Mon Sep 17 00:00:00 2001 From: Eran Leshem <1707552+eranl@users.noreply.github.com> Date: Fri, 3 Oct 2025 00:50:16 +0300 Subject: [PATCH 07/11] Delete firebase-dynamic-links/firebase-dynamic-links.gradle --- firebase-dynamic-links/firebase-dynamic-links.gradle | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 firebase-dynamic-links/firebase-dynamic-links.gradle diff --git a/firebase-dynamic-links/firebase-dynamic-links.gradle b/firebase-dynamic-links/firebase-dynamic-links.gradle deleted file mode 100644 index e69de29bb2d..00000000000 From e81b9dcc7ce25a44f99ed302bd4152c34efa2b79 Mon Sep 17 00:00:00 2001 From: eranl <1707552+eranl@users.noreply.github.com> Date: Fri, 3 Oct 2025 00:58:42 +0300 Subject: [PATCH 08/11] Cleanup --- .../google/firebase/firestore/util/CustomClassMapper.java | 6 +++--- gradle.properties | 3 --- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/firebase-firestore/src/main/java/com/google/firebase/firestore/util/CustomClassMapper.java b/firebase-firestore/src/main/java/com/google/firebase/firestore/util/CustomClassMapper.java index 2452fdce3e9..9723685fef9 100644 --- a/firebase-firestore/src/main/java/com/google/firebase/firestore/util/CustomClassMapper.java +++ b/firebase-firestore/src/main/java/com/google/firebase/firestore/util/CustomClassMapper.java @@ -20,7 +20,6 @@ import android.net.Uri; import android.os.Build; import androidx.annotation.RequiresApi; -import com.google.common.collect.Sets; import com.google.firebase.Timestamp; import com.google.firebase.firestore.Blob; import com.google.firebase.firestore.DocumentId; @@ -50,6 +49,7 @@ import java.util.List; import java.util.Locale; import java.util.Map; +import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; @@ -59,8 +59,8 @@ public class CustomClassMapper { private static final int MAX_DEPTH = 500; private static final ConcurrentMap, BeanMapper> mappers = new ConcurrentHashMap<>(); - private static final HashSet RECORD_BASE_CLASS_NAMES = - Sets.newHashSet("java.lang.Record", "com.android.tools.r8.RecordTag"); + private static final Set RECORD_BASE_CLASS_NAMES = + Set.of("java.lang.Record", "com.android.tools.r8.RecordTag"); private static void hardAssert(boolean assertion) { hardAssert(assertion, "Internal inconsistency"); diff --git a/gradle.properties b/gradle.properties index cd0069af236..23405247bdc 100644 --- a/gradle.properties +++ b/gradle.properties @@ -21,6 +21,3 @@ firebase.checks.lintProjects=:tools:lint systemProp.illegal-access=warn android.useAndroidX=true -android.defaults.buildfeatures.buildconfig=true -android.nonTransitiveRClass=false -android.nonFinalResIds=false From 085ac85eed8b2ae3d14d1b938113a34a9eaa46db Mon Sep 17 00:00:00 2001 From: Eran Leshem <1707552+eranl@users.noreply.github.com> Date: Fri, 3 Oct 2025 01:00:28 +0300 Subject: [PATCH 09/11] Update gradle.properties From d2c00fe5e43201584f157499cf2dca2012b1c165 Mon Sep 17 00:00:00 2001 From: Eran Leshem <1707552+eranl@users.noreply.github.com> Date: Fri, 3 Oct 2025 01:01:30 +0300 Subject: [PATCH 10/11] Update gradle.properties From be21fe5231eda8e58ba6d62fff19890d06246946 Mon Sep 17 00:00:00 2001 From: Eran Leshem <1707552+eranl@users.noreply.github.com> Date: Fri, 3 Oct 2025 01:02:45 +0300 Subject: [PATCH 11/11] Delete firebase-firestore/ktx/ktx.gradle --- firebase-firestore/ktx/ktx.gradle | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 firebase-firestore/ktx/ktx.gradle diff --git a/firebase-firestore/ktx/ktx.gradle b/firebase-firestore/ktx/ktx.gradle deleted file mode 100644 index e69de29bb2d..00000000000