diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index b93b98b8cd..55bfd39c93 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -3,6 +3,6 @@ contact_links: - name: General Questions and Inquiries url: https://www.mongodb.com/community/forums/tags/c/realm-sdks/58/java about: Please ask general design/architecture questions in the community forums. - - name: MongoDB Realm (Sync) Production Issues + - name: MongoDB Atlas Device Sync Production Issues url: https://support.mongodb.com/ about: Please report urgent production issues to the support portal directly. diff --git a/CHANGELOG.md b/CHANGELOG.md index bb130c15eb..acaebceeb8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,17 +1,23 @@ -## 10.12.0 (YYYY-MM-DD) +## 10.12.0-transformer-api (YYYY-MM-DD) + +### Breaking Changes +* Only works with Android Gradle Plugin 7.4 or newer. (Issue [#7714](https://github.com/realm/realm-java/issues/7714)) ### Enhancements * [RealmApp] Introduced `SyncSession.RecoverOrDiscardUnsyncedChangesStrategy`, an alternative automatic client reset strategy that tries to automatically recover any unsynced data from the client, and discards any unsynced data if not possible. This is now the default client reset policy if not explicitly set in the `SyncConfiguration`. * [RealmApp] Introduced `SyncSession.RecoverUnsyncedChangesStrategy`, an alternative automatic client reset strategy that tries to automatically recover any unsynced data from the client and will revert to manual client reset if not possible. ### Fixed -* None +* Now queries can point to fields with query language-reserved words like 'desc', 'sort', 'distinct', etc. Issue [#7705](https://github.com/realm/realm-java/issues/7705) ### Compatibility * File format: Generates Realms with format v22. Unsynced Realms will be upgraded from Realm Java 2.0 and later. Synced Realms can only be read and upgraded if created with Realm Java v10.0.0-BETA.1. * APIs are backwards compatible with all previous release of realm-java in the 10.6.y series. * Realm Studio 11.0.0-alpha.0 or above is required to open Realms created by this version. +### Internal +* Update to Realm Core 12.6.0, commit: 5da7744b4056ad185c025bccf0924f17f73f7a91. + ## 10.11.1 (2022-07-14) diff --git a/Jenkinsfile b/Jenkinsfile index 9ee6fd487d..967962da09 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -125,8 +125,9 @@ try { [$class: 'AmazonWebServicesCredentialsBinding', credentialsId: 'realm-kotlin-baas-aws-credentials', accessKeyVariable: 'BAAS_AWS_ACCESS_KEY_ID', secretKeyVariable: 'BAAS_AWS_SECRET_ACCESS_KEY'] ]) { def tempDir = runCommand('mktemp -d -t app_config.XXXXXXXXXX') - sh "tools/sync_test_server/app_config_generator.sh ${tempDir} tools/sync_test_server/app_template partition testapp1 testapp2" - sh "tools/sync_test_server/app_config_generator.sh ${tempDir} tools/sync_test_server/app_template flex testapp3" + sh "tools/sync_test_server/app_config_generator.sh ${tempDir} tools/sync_test_server/app_template partition auto testapp1" + sh "tools/sync_test_server/app_config_generator.sh ${tempDir} tools/sync_test_server/app_template partition email testapp2" + sh "tools/sync_test_server/app_config_generator.sh ${tempDir} tools/sync_test_server/app_template flex function testapp3" sh "docker network create ${dockerNetworkId}" mongoDbRealmContainer = mdbRealmImage.run("--network ${dockerNetworkId} -v$tempDir:/apps -e AWS_ACCESS_KEY_ID='$BAAS_AWS_ACCESS_KEY_ID' -e AWS_SECRET_ACCESS_KEY='$BAAS_AWS_SECRET_ACCESS_KEY'") mongoDbRealmCommandServerContainer = commandServerEnv.run("--network container:${mongoDbRealmContainer.id} -v$tempDir:/apps") @@ -162,7 +163,7 @@ try { sh """yes '\n' | avdmanager create avd -n CIEmulator -k '${emulatorImage}' --force""" sh "adb start-server" // https://stackoverflow.com/questions/56198290/problems-with-adb-exe // Need to go to ANDROID_HOME due to https://askubuntu.com/questions/1005944/emulator-avd-does-not-launch-the-virtual-device - sh "cd \$ANDROID_HOME/tools && emulator -avd CIEmulator -no-boot-anim -no-window -wipe-data -noaudio -partition-size 4098 &" + sh "cd \$ANDROID_HOME/tools && emulator -avd CIEmulator -no-boot-anim -no-window -wipe-data -noaudio -partition-size 4098 -memory 2048 &" try { runBuild(buildFlags, instrumentationTestTarget) } finally { diff --git a/LICENSE b/LICENSE index e163ae2f84..f13a843379 100644 --- a/LICENSE +++ b/LICENSE @@ -1,10 +1,3 @@ -TABLE OF CONTENTS - -1. Apache License version 2.0 -2. Export Compliance - -1. ------------------------------------------------------------------------------- - Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ @@ -180,25 +173,5 @@ TABLE OF CONTENTS incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. -2. ------------------------------------------------------------------------------- - -EXPORT COMPLIANCE - -You understand that the Software may contain cryptographic functions that may be -subject to export restrictions, and you represent and warrant that you are not -(i) located in a jurisdiction that is subject to United States economic -sanctions (“Prohibited Jurisdiction”), including Cuba, Iran, North Korea, -Sudan, Syria or the Crimea region, (ii) a person listed on any U.S. government -blacklist (to include the List of Specially Designated Nationals and Blocked -Persons or the Consolidated Sanctions List administered by the U.S. Department -of the Treasury’s Office of Foreign Assets Control, or the Denied Persons List -or Entity List administered by the U.S. Department of Commerce) -(“Sanctioned Person”), or (iii) controlled or 50% or more owned by a Sanctioned -Person. - -You agree to comply with all export, re-export and import restrictions and -regulations of the U.S. Department of Commerce or other agency or authority of -the United States or other applicable countries. You also agree not to transfer, -or authorize the transfer of, directly or indirectly, of the Software to any -Prohibited Jurisdiction, or otherwise in violation of any such restrictions or -regulations. + END OF TERMS AND CONDITIONS + \ No newline at end of file diff --git a/README.md b/README.md index 3b97135083..13b1546a9e 100644 --- a/README.md +++ b/README.md @@ -288,10 +288,6 @@ Realm Java is published under the Apache 2.0 license. Realm Core is also published under the Apache 2.0 license and is available [here](https://github.com/realm/realm-core). -**This product is not being made available to any person located in Cuba, Iran, -North Korea, Sudan, Syria or the Crimea region, or to any other person that is -not eligible to receive the product under U.S. law.** - ## Feedback **_If you use Realm and are happy with it, all we ask is that you, please consider sending out a tweet mentioning [@realm](http://twitter.com/realm) to share your thoughts!_** diff --git a/dependencies.list b/dependencies.list index 3d08953f21..59d1732ed5 100644 --- a/dependencies.list +++ b/dependencies.list @@ -1,6 +1,6 @@ # Realm Core release used by Realm Java # https://github.com/realm/realm-core/releases -REALM_CORE=12.3.0 +REALM_CORE=12.6.0 # Version of MongoDB Realm used by integration tests # See https://github.com/realm/ci/packages/147854 for available versions @@ -15,10 +15,10 @@ REALM_BAAS_GIT_HASH=ba172f8dd37c3102499ab7d728e7c0dcb9e52ae8 REALM_BAAS_UI_GIT_HASH=1fc10a6e04f87247790662d825500c9e21dbfbc7 # Common Android settings across projects -GRADLE_BUILD_TOOLS=7.1.0 +GRADLE_BUILD_TOOLS=7.4.0-alpha08 ANDROID_BUILD_TOOLS=30.0.3 -KOTLIN=1.5.31 -KOTLIN_COROUTINES=1.5.2 +KOTLIN=1.6.21 +KOTLIN_COROUTINES=1.6.0 # Common classpath dependencies gradle=7.3.3 diff --git a/examples/architectureComponentsExample/src/main/java/io/realm/examples/arch/PersonListFragment.java b/examples/architectureComponentsExample/src/main/java/io/realm/examples/arch/PersonListFragment.java index b879caced3..a114dfd84a 100644 --- a/examples/architectureComponentsExample/src/main/java/io/realm/examples/arch/PersonListFragment.java +++ b/examples/architectureComponentsExample/src/main/java/io/realm/examples/arch/PersonListFragment.java @@ -114,7 +114,7 @@ static class ViewHolder extends RecyclerView.ViewHolder { return; } AppCompatActivity activity = ContextUtils.findActivity(view.getContext()); - PersonFragment personFragment = PersonFragment.create(person.getName()); + PersonFragment personFragment = PersonFragment.create(person.name); activity.getSupportFragmentManager() .beginTransaction() .setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE) diff --git a/examples/architectureComponentsExample/src/main/java/io/realm/examples/arch/model/Person.java b/examples/architectureComponentsExample/src/main/java/io/realm/examples/arch/model/Person.java index fac53a7398..de97eb924b 100644 --- a/examples/architectureComponentsExample/src/main/java/io/realm/examples/arch/model/Person.java +++ b/examples/architectureComponentsExample/src/main/java/io/realm/examples/arch/model/Person.java @@ -21,7 +21,7 @@ public class Person extends RealmObject { @Index - private String name; + public String name; private int age; diff --git a/examples/coroutinesExample/build.gradle b/examples/coroutinesExample/build.gradle index 2dd3516bf7..4e909277c9 100644 --- a/examples/coroutinesExample/build.gradle +++ b/examples/coroutinesExample/build.gradle @@ -5,7 +5,8 @@ apply plugin: 'kotlin-kapt' apply plugin: 'realm-android' android { - compileSdkVersion rootProject.sdkVersion + // androidx.lifecycle dependencies requires Android APIs 31 or later + compileSdkVersion 31 buildToolsVersion rootProject.buildTools defaultConfig { @@ -57,10 +58,10 @@ dependencies { implementation "androidx.fragment:fragment-ktx:1.2.5" - implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0" - implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.2.0" - implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.2.0" - implementation "androidx.lifecycle:lifecycle-common-java8:2.2.0" + implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.4.0" + implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.4.0" + implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.4.0" + implementation "androidx.lifecycle:lifecycle-common-java8:2.4.0" implementation "androidx.legacy:legacy-support-v4:1.0.0" @@ -68,7 +69,7 @@ dependencies { implementation "androidx.recyclerview:recyclerview:1.1.0" - implementation "com.dropbox.mobile.store:store4:4.0.0" + implementation "com.dropbox.mobile.store:store4:4.0.5" implementation "com.google.android.material:material:1.2.1" diff --git a/examples/encryptionExample/src/main/java/io/realm/examples/encryption/EncryptionExampleActivity.java b/examples/encryptionExample/src/main/java/io/realm/examples/encryption/EncryptionExampleActivity.java index fd5ae8d804..9665125016 100644 --- a/examples/encryptionExample/src/main/java/io/realm/examples/encryption/EncryptionExampleActivity.java +++ b/examples/encryptionExample/src/main/java/io/realm/examples/encryption/EncryptionExampleActivity.java @@ -52,6 +52,8 @@ protected void onCreate(Bundle savedInstanceState) { RealmConfiguration realmConfiguration = new RealmConfiguration.Builder() .encryptionKey(key) + .allowQueriesOnUiThread(true) + .allowWritesOnUiThread(true) .build(); // Start with a clean slate every time diff --git a/examples/gradle/wrapper/gradle-wrapper.properties b/examples/gradle/wrapper/gradle-wrapper.properties index 2e6e5897b5..26e62a13f1 100644 --- a/examples/gradle/wrapper/gradle-wrapper.properties +++ b/examples/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-rc-2-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/examples/introExample/src/main/java/io/realm/examples/intro/IntroExampleActivity.java b/examples/introExample/src/main/java/io/realm/examples/intro/IntroExampleActivity.java index 4660587de3..adb9c48b4b 100644 --- a/examples/introExample/src/main/java/io/realm/examples/intro/IntroExampleActivity.java +++ b/examples/introExample/src/main/java/io/realm/examples/intro/IntroExampleActivity.java @@ -28,6 +28,7 @@ import io.realm.OrderedRealmCollectionChangeListener; import io.realm.Realm; +import io.realm.RealmConfiguration; import io.realm.RealmResults; import io.realm.Sort; import io.realm.examples.intro.model.Cat; @@ -66,7 +67,10 @@ protected void onCreate(Bundle savedInstanceState) { Realm.deleteRealm(Realm.getDefaultConfiguration()); // Create the Realm instance - realm = Realm.getDefaultInstance(); + RealmConfiguration build = new RealmConfiguration.Builder() + .allowWritesOnUiThread(true) + .allowQueriesOnUiThread(true).build(); + realm = Realm.getInstance(build); // Asynchronous queries are evaluated on a background thread, // and passed to the registered change listener when it's done. diff --git a/examples/kotlinExample/build.gradle b/examples/kotlinExample/build.gradle index 3f969a9178..f4b29cbc0d 100644 --- a/examples/kotlinExample/build.gradle +++ b/examples/kotlinExample/build.gradle @@ -39,7 +39,6 @@ realm { kotlinExtensionsEnabled = true } - // enable @ParametersAreNonnullByDefault annotation. See https://blog.jetbrains.com/kotlin/2017/09/kotlin-1-1-50-is-out/ tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all { kotlinOptions { @@ -48,6 +47,5 @@ tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all { } dependencies { - implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:${kotlin_version}" implementation "org.jetbrains.anko:anko-commons:0.10.4" } diff --git a/examples/kotlinExample/src/main/kotlin/io/realm/examples/kotlin/KotlinExampleActivity.kt b/examples/kotlinExample/src/main/kotlin/io/realm/examples/kotlin/KotlinExampleActivity.kt index 3d7af8c2dc..2e810e70dd 100644 --- a/examples/kotlinExample/src/main/kotlin/io/realm/examples/kotlin/KotlinExampleActivity.kt +++ b/examples/kotlinExample/src/main/kotlin/io/realm/examples/kotlin/KotlinExampleActivity.kt @@ -22,6 +22,7 @@ import android.util.Log import android.widget.LinearLayout import android.widget.TextView import io.realm.Realm +import io.realm.RealmConfiguration import io.realm.Sort import io.realm.examples.kotlin.model.Cat import io.realm.examples.kotlin.model.Dog @@ -46,6 +47,13 @@ class KotlinExampleActivity : Activity() { rootLayout = findViewById(R.id.container) rootLayout.removeAllViews() + Realm.setDefaultConfiguration( + RealmConfiguration.Builder() + .allowQueriesOnUiThread(true) + .allowWritesOnUiThread(true) + .build() + ) + // Open the realm for the UI thread. realm = Realm.getDefaultInstance() diff --git a/examples/moduleExample/app/src/main/java/io/realm/examples/appmodules/ModulesExampleActivity.java b/examples/moduleExample/app/src/main/java/io/realm/examples/appmodules/ModulesExampleActivity.java index d08f35687a..16e23ed3e7 100644 --- a/examples/moduleExample/app/src/main/java/io/realm/examples/appmodules/ModulesExampleActivity.java +++ b/examples/moduleExample/app/src/main/java/io/realm/examples/appmodules/ModulesExampleActivity.java @@ -35,6 +35,7 @@ import io.realm.examples.librarymodules.model.Elephant; import io.realm.examples.librarymodules.model.Lion; import io.realm.examples.librarymodules.model.Zebra; +import io.realm.examples.librarymodules.modules.AllAnimalsModule; import io.realm.examples.librarymodules.modules.DomesticAnimalsModule; import io.realm.examples.librarymodules.modules.ZooAnimalsModule; import io.realm.exceptions.RealmException; @@ -58,20 +59,29 @@ protected void onCreate(Bundle savedInstanceState) { // The default Realm instance implicitly knows about all classes in the realmModuleAppExample Android Studio // module. This does not include the classes from the realmModuleLibraryExample AS module so a Realm using this // configuration would know about the following classes: { Cow, Pig, Snake, Spider } - RealmConfiguration defaultConfig = new RealmConfiguration.Builder().build(); + RealmConfiguration defaultConfig = new RealmConfiguration + .Builder() + .modules(Realm.getDefaultModule(), new AllAnimalsModule()) + .allowQueriesOnUiThread(true) + .allowWritesOnUiThread(true) + .build(); // It is possible to extend the default schema by adding additional Realm modules using modules(). This can // also be Realm modules from libraries. The below Realm contains the following classes: { Cow, Pig, Snake, // Spider, Cat, Dog } RealmConfiguration farmAnimalsConfig = new RealmConfiguration.Builder() .name("farm.realm") - .modules(Realm.getDefaultModule(), new DomesticAnimalsModule()) + .allowQueriesOnUiThread(true) + .allowWritesOnUiThread(true) + .modules(Realm.getDefaultModule(), new AllAnimalsModule()) .build(); // Or you can completely replace the default schema. // This Realm contains the following classes: { Elephant, Lion, Zebra, Snake, Spider } RealmConfiguration exoticAnimalsConfig = new RealmConfiguration.Builder() .name("exotic.realm") + .allowQueriesOnUiThread(true) + .allowWritesOnUiThread(true) .modules(new ZooAnimalsModule(), new CreepyAnimalsModule()) .build(); diff --git a/examples/moduleExample/library/src/main/java/io/realm/examples/librarymodules/Zoo.java b/examples/moduleExample/library/src/main/java/io/realm/examples/librarymodules/Zoo.java index b7d68f2eb6..1c0e4b5f7e 100644 --- a/examples/moduleExample/library/src/main/java/io/realm/examples/librarymodules/Zoo.java +++ b/examples/moduleExample/library/src/main/java/io/realm/examples/librarymodules/Zoo.java @@ -35,7 +35,10 @@ public class Zoo { public Zoo() { realmConfig = new RealmConfiguration.Builder() // The app is responsible for calling `Realm.init(Context)` .name("library.zoo.realm") // So always use a unique name - .modules(new AllAnimalsModule()) // Always use explicit modules in library projects + .allowQueriesOnUiThread(true) + .allowWritesOnUiThread(true) + .modules(new AllAnimalsModule()) + // Always use explicit modules in library projects .build(); // Reset Realm diff --git a/examples/mongoDbRealmExample/build.gradle b/examples/mongoDbRealmExample/build.gradle index 7ccfb65314..fa9bf356b1 100644 --- a/examples/mongoDbRealmExample/build.gradle +++ b/examples/mongoDbRealmExample/build.gradle @@ -5,7 +5,8 @@ apply plugin: 'kotlin-android-extensions' apply plugin: 'realm-android' android { - compileSdkVersion rootProject.sdkVersion + // androidx.lifecycle dependencies requires Android APIs 31 or later + compileSdkVersion 31 buildToolsVersion rootProject.buildTools defaultConfig { @@ -53,8 +54,8 @@ realm { } dependencies { - implementation 'androidx.appcompat:appcompat:1.1.0' - implementation 'com.google.android.material:material:1.1.0' + implementation 'androidx.appcompat:appcompat:1.4.2' + implementation 'com.google.android.material:material:1.6.1' implementation 'me.zhanghai.android.materialprogressbar:library:1.6.1' implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" } diff --git a/examples/multiprocessExample/src/main/java/io/realm/examples/realmmultiprocessexample/MyApplication.java b/examples/multiprocessExample/src/main/java/io/realm/examples/realmmultiprocessexample/MyApplication.java index d078915489..7d55aa6831 100644 --- a/examples/multiprocessExample/src/main/java/io/realm/examples/realmmultiprocessexample/MyApplication.java +++ b/examples/multiprocessExample/src/main/java/io/realm/examples/realmmultiprocessexample/MyApplication.java @@ -26,7 +26,11 @@ public class MyApplication extends Application { public void onCreate() { super.onCreate(); Realm.init(this); - RealmConfiguration configuration = new RealmConfiguration.Builder().deleteRealmIfMigrationNeeded().build(); + RealmConfiguration configuration = new RealmConfiguration.Builder() + .deleteRealmIfMigrationNeeded() + .allowWritesOnUiThread(true) + .allowQueriesOnUiThread(true) + .build(); Realm.setDefaultConfiguration(configuration); } } diff --git a/examples/rxJavaExample/src/main/java/io/realm/examples/rxjava/MyApplication.java b/examples/rxJavaExample/src/main/java/io/realm/examples/rxjava/MyApplication.java index 52ed89fdb1..ae98502370 100644 --- a/examples/rxJavaExample/src/main/java/io/realm/examples/rxjava/MyApplication.java +++ b/examples/rxJavaExample/src/main/java/io/realm/examples/rxjava/MyApplication.java @@ -45,7 +45,10 @@ public class MyApplication extends Application { public void onCreate() { super.onCreate(); Realm.init(this); - RealmConfiguration config = new RealmConfiguration.Builder().build(); + RealmConfiguration config = new RealmConfiguration.Builder() + .allowWritesOnUiThread(true) + .allowQueriesOnUiThread(true) + .build(); Realm.deleteRealm(config); Realm.setDefaultConfiguration(config); createTestData(); diff --git a/examples/threadExample/src/main/java/io/realm/examples/threads/MyApplication.java b/examples/threadExample/src/main/java/io/realm/examples/threads/MyApplication.java index a043477ecf..8c18e77467 100644 --- a/examples/threadExample/src/main/java/io/realm/examples/threads/MyApplication.java +++ b/examples/threadExample/src/main/java/io/realm/examples/threads/MyApplication.java @@ -29,7 +29,10 @@ public void onCreate() { // Configure Realm for the application Realm.init(this); - RealmConfiguration realmConfiguration = new RealmConfiguration.Builder().build(); + RealmConfiguration realmConfiguration = new RealmConfiguration.Builder() + .allowWritesOnUiThread(true) + .allowQueriesOnUiThread(true) + .build(); Realm.deleteRealm(realmConfiguration); // Clean slate Realm.setDefaultConfiguration(realmConfiguration); // Make this Realm the default } diff --git a/examples/unitTestExample/src/main/java/io/realm/examples/unittesting/ExampleActivity.java b/examples/unitTestExample/src/main/java/io/realm/examples/unittesting/ExampleActivity.java index 4b0f94e752..f1a5f58a2a 100644 --- a/examples/unitTestExample/src/main/java/io/realm/examples/unittesting/ExampleActivity.java +++ b/examples/unitTestExample/src/main/java/io/realm/examples/unittesting/ExampleActivity.java @@ -24,6 +24,7 @@ import android.widget.TextView; import io.realm.Realm; +import io.realm.RealmConfiguration; import io.realm.RealmResults; import io.realm.examples.unittesting.model.Person; @@ -43,7 +44,12 @@ protected void onCreate(Bundle savedInstanceState) { rootLayout.removeAllViews(); // Open the default Realm for the UI thread. - realm = Realm.getDefaultInstance(); + RealmConfiguration conf = new RealmConfiguration + .Builder() + .allowWritesOnUiThread(true) + .allowQueriesOnUiThread(true) + .build(); + realm = Realm.getInstance(conf); // Clean up from previous run cleanUp(); diff --git a/gradle-plugin/build.gradle b/gradle-plugin/build.gradle index cb66eaf55d..4f82390038 100644 --- a/gradle-plugin/build.gradle +++ b/gradle-plugin/build.gradle @@ -5,7 +5,7 @@ import org.gradle.api.internal.project.ProjectInternal buildscript { def properties = new Properties() properties.load(new FileInputStream("${rootDir}/../dependencies.list")) - + ext.kotlin_version = properties.get('KOTLIN') repositories { jcenter() maven { @@ -15,10 +15,12 @@ buildscript { dependencies { classpath "org.jfrog.buildinfo:build-info-extractor-gradle:${properties.get('BUILD_INFO_EXTRACTOR_GRADLE')}" classpath "io.github.gradle-nexus:publish-plugin:${properties.get("GRADLE_NEXUS_PLUGIN")}" + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" } } -apply plugin: 'groovy-gradle-plugin' +apply plugin: 'kotlin' +apply plugin: 'java-gradle-plugin' apply plugin: 'maven-publish' def props = new Properties() @@ -33,8 +35,8 @@ repositories { jcenter() } -sourceCompatibility = 1.8 -targetCompatibility = 1.8 +sourceCompatibility = 11 +targetCompatibility = 11 group = 'io.realm' version = file("${projectDir}/../version.txt").text.trim() @@ -78,8 +80,8 @@ dependencies { } task generateVersionClass(type: Copy) { - from 'src/main/templates/Version.java' - into 'build/generated-src/main/java/io/realm' + from 'src/main/templates/Version.kt' + into 'build/generated-src/main/kotlin/io/realm' filter(ReplaceTokens, tokens: [version: version]) outputs.upToDateWhen { false } } @@ -88,12 +90,12 @@ task generateVersionClass(type: Copy) { sourceSets { main { java { - srcDir 'build/generated-src/main/java' + srcDirs += ['build/generated-src/main/kotlin'] } } } -compileJava.dependsOn generateVersionClass +compileKotlin.dependsOn generateVersionClass apply from: "${rootDir}/../mavencentral-publications.gradle" apply from: "${rootDir}/../mavencentral-publish.gradle" diff --git a/gradle-plugin/gradle/wrapper/gradle-wrapper.properties b/gradle-plugin/gradle/wrapper/gradle-wrapper.properties index 2e6e5897b5..41dfb87909 100644 --- a/gradle-plugin/gradle/wrapper/gradle-wrapper.properties +++ b/gradle-plugin/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/gradle-plugin/src/main/groovy/io/realm/gradle/Realm.groovy b/gradle-plugin/src/main/groovy/io/realm/gradle/Realm.groovy deleted file mode 100644 index 5675799f91..0000000000 --- a/gradle-plugin/src/main/groovy/io/realm/gradle/Realm.groovy +++ /dev/null @@ -1,159 +0,0 @@ -/* - * Copyright 2016 Realm Inc. - * - * 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 io.realm.gradle - -import com.android.build.gradle.AppPlugin -import com.android.build.gradle.LibraryPlugin -import com.neenbedankt.gradle.androidapt.AndroidAptPlugin -import io.realm.transformer.RealmTransformer -import org.gradle.api.GradleException -import org.gradle.api.Plugin -import org.gradle.api.Project -import org.gradle.api.artifacts.UnknownConfigurationException - -class Realm implements Plugin { - - @Override - void apply(Project project) { - // Make sure the project is either an Android application or library - def isAndroidApp = project.plugins.withType(AppPlugin) - def isAndroidLib = project.plugins.withType(LibraryPlugin) - if (!isAndroidApp && !isAndroidLib) { - throw new GradleException("'com.android.application' or 'com.android.library' plugin required.") - } - - if (!isTransformAvailable()) { - throw new GradleException('Realm gradle plugin only supports android gradle plugin 1.5.0 or later.') - } - - def syncEnabledDefault = false - def usesAptPlugin = project.plugins.findPlugin('com.neenbedankt.android-apt') != null - def isKotlinProject = project.plugins.findPlugin('kotlin-android') != null || project.plugins.findPlugin("kotlin-multiplatform") != null - def useKotlinExtensionsDefault = isKotlinProject - def hasAnnotationProcessorConfiguration = project.getConfigurations().findByName('annotationProcessor') != null - // TODO add a parameter in 'realm' block if this should be specified by users - def preferAptOnKotlinProject = false - def dependencyConfigurationName = getDependencyConfigurationName(project) - def extension = project.extensions.create('realm', RealmPluginExtension) - extension.addPropertyListener(RealmPluginExtension.KEY_KOTLIN_EXTENSIONS_ENABLED, new RealmPluginExtension.PropertyChangedListener() { - @Override - void onChange(Boolean checked) { - setDependencies(project, dependencyConfigurationName, extension.syncEnabled, extension.kotlinExtensionsEnabled) - } - }) - extension.addPropertyListener(RealmPluginExtension.KEY_SYNC_ENABLED, new RealmPluginExtension.PropertyChangedListener() { - @Override - void onChange(Boolean checked) { - setDependencies(project, dependencyConfigurationName, extension.syncEnabled, extension.kotlinExtensionsEnabled) - } - }) - extension.kotlinExtensionsEnabled = useKotlinExtensionsDefault - - if (shouldApplyAndroidAptPlugin(usesAptPlugin, isKotlinProject, - hasAnnotationProcessorConfiguration, preferAptOnKotlinProject)) { - project.plugins.apply(AndroidAptPlugin) - usesAptPlugin = true - } - - // Register transformer during the evaluations phase, so the Android Plugin - // is able to pick it up. The project is passed in in order to gather various - // metadata in `project.afterEvaluate { }`, but the transformer is not allowed - // to store a reference to it if we want to support the Gradle Configuration Cache. - project.android.registerTransform(new RealmTransformer(project)) - - project.dependencies.add(dependencyConfigurationName, "io.realm:realm-annotations:${Version.VERSION}") - if (usesAptPlugin) { - project.dependencies.add("apt", "io.realm:realm-annotations-processor:${Version.VERSION}") - project.dependencies.add("androidTestApt", "io.realm:realm-annotations-processor:${Version.VERSION}") - } else if (isKotlinProject && !preferAptOnKotlinProject) { - project.dependencies.add("kapt", "io.realm:realm-annotations-processor:${Version.VERSION}") - project.dependencies.add("kaptAndroidTest", "io.realm:realm-annotations-processor:${Version.VERSION}") - } else { - assert hasAnnotationProcessorConfiguration - project.dependencies.add("annotationProcessor", "io.realm:realm-annotations-processor:${Version.VERSION}") - project.dependencies.add("androidTestAnnotationProcessor", "io.realm:realm-annotations-processor:${Version.VERSION}") - } - } - - private static boolean isTransformAvailable() { - try { - Class.forName('com.android.build.api.transform.Transform') - return true - } catch (Exception ignored) { - return false - } - } - - private static String getDependencyConfigurationName(Project project) { - /* - * Dependency configuration name for android gradle plugin 3.0.0-*. - * We need to use 'api' instead of 'implementation' since user's model class - * might be using Realm's classes and annotations. - */ - def newDependencyName = "api" - def oldDependencyName = "compile" - try { - project.getConfigurations().getByName(newDependencyName) - return newDependencyName - } catch (UnknownConfigurationException ignored) { - oldDependencyName - } - } - - private static boolean shouldApplyAndroidAptPlugin(boolean usesAptPlugin, boolean isKotlinProject, - boolean hasAnnotationProcessorConfiguration, - boolean preferAptOnKotlinProject) { - if (usesAptPlugin) { - // for any projects that uses android-apt plugin already. No need to apply it twice. - return false - } - if (isKotlinProject) { - // for any Kotlin projects where user did not apply 'android-apt' plugin manually. - return preferAptOnKotlinProject && !hasAnnotationProcessorConfiguration - } - // for any Java Projects where user did not apply 'android-apt' plugin manually. - return !hasAnnotationProcessorConfiguration - } - - // This will setup the required dependencies. - // Due to how Gradle works, we have no choice but to run this code every time any of the parameters - // in the Realm extension is changed. - private static void setDependencies(Project project, String dependencyConfigurationName, boolean syncEnabled, boolean kotlinExtensionsEnabled) { - // remove libraries first - def iterator = project.getConfigurations().getByName(dependencyConfigurationName).getDependencies().iterator() - while (iterator.hasNext()) { - def item = iterator.next() - if (item.group == 'io.realm') { - if (item.name.startsWith('realm-android-library')) { - iterator.remove() - } - if (item.name.startsWith('realm-android-kotlin-extensions')) { - iterator.remove() - } - } - } - - // then add again - def syncArtifactName = "realm-android-library${syncEnabled ? '-object-server' : ''}" - project.dependencies.add(dependencyConfigurationName, "io.realm:${syncArtifactName}:${Version.VERSION}") - - if (kotlinExtensionsEnabled) { - def kotlinExtArtifactName = "realm-android-kotlin-extensions${syncEnabled ? '-object-server' : ''}" - project.dependencies.add(dependencyConfigurationName, "io.realm:${kotlinExtArtifactName}:${Version.VERSION}") - } - } -} diff --git a/gradle-plugin/src/main/kotlin/io/realm/gradle/Realm.kt b/gradle-plugin/src/main/kotlin/io/realm/gradle/Realm.kt new file mode 100644 index 0000000000..118bd77fbe --- /dev/null +++ b/gradle-plugin/src/main/kotlin/io/realm/gradle/Realm.kt @@ -0,0 +1,159 @@ +package io.realm.gradle + +import com.android.build.gradle.AppPlugin +import com.android.build.gradle.LibraryPlugin +import io.realm.transformer.RealmTransformer +import org.gradle.api.GradleException +import org.gradle.api.Plugin +import org.gradle.api.Project +import org.gradle.api.artifacts.UnknownConfigurationException +import org.gradle.api.plugins.PluginCollection +import org.slf4j.Logger +import org.slf4j.LoggerFactory + +val logger: Logger = LoggerFactory.getLogger("realm-logger") + +// TODO Run a Task or Visitor to collect runtimeClassPath, then serialize it +// Run another task that depends on the output of the first task in order to deserialize the ClassPool and process each class apart +open class Realm : Plugin { + override fun apply(project: Project) { + // Make sure the project is either an Android application or library + val isAndroidApp: PluginCollection = + project.plugins.withType(AppPlugin::class.java) + val isAndroidLib: PluginCollection = + project.plugins.withType(LibraryPlugin::class.java) + + if (isAndroidApp.isEmpty() && isAndroidLib.isEmpty()) { + throw GradleException("'com.android.application' or 'com.android.library' plugin required.") + } + + checkCompatibleAGPVersion() + + val isKotlinProject: Boolean = + project.plugins.findPlugin("kotlin-android") != null || project.plugins.findPlugin("kotlin-multiplatform") != null + val hasAnnotationProcessorConfiguration = + project.getConfigurations().findByName("annotationProcessor") != null + // TODO add a parameter in 'realm' block if this should be specified by users + val dependencyConfigurationName: String = getDependencyConfigurationName(project) + val extension = project.extensions.create("realm", RealmPluginExtension::class.java) + + extension.isKotlinExtensionsEnabled = isKotlinProject + + + RealmTransformer.register(project) + + project.dependencies.add( + dependencyConfigurationName, + "io.realm:realm-annotations:${Version.VERSION}" + ) + if (isKotlinProject) { + project.dependencies.add( + "kapt", + "io.realm:realm-annotations-processor:${Version.VERSION}" + ) + project.dependencies.add( + "kaptAndroidTest", + "io.realm:realm-annotations-processor:${Version.VERSION}" + ) + } else { + assert(hasAnnotationProcessorConfiguration) + project.dependencies.add( + "annotationProcessor", + "io.realm:realm-annotations-processor:${Version.VERSION}" + ) + project.dependencies.add( + "androidTestAnnotationProcessor", + "io.realm:realm-annotations-processor:${Version.VERSION}" + ) + } + project.afterEvaluate { + setDependencies( + project, + dependencyConfigurationName, + extension.isSyncEnabled, + extension.isKotlinExtensionsEnabled + ) + } + } + + companion object { + + private fun checkCompatibleAGPVersion() { + val version = SimpleAGPVersion.ANDROID_GRADLE_PLUGIN_VERSION + return when { + version >= SimpleAGPVersion(7, 4) -> { + // minimum version compatible with https://developer.android.com/studio/releases/gradle-plugin-api-updates#support_for_transformations_based_on_whole_program_analysis + logger.debug("Realm Plugin used with AGP version: ${version.major}.${version.minor}.") + } + version >= SimpleAGPVersion(4, 2) -> { + throw GradleException("Realm Plugin used with incompatible AGP version: ${version.major}.${version.minor}. You should consider using a Realm-Java v10.11.0 or lower. Or migrate your project to use AGP 7.4 or newer.") + } + else -> { + throw GradleException("Android Gradle Plugin $version is not supported") + } + } + } + + private fun getDependencyConfigurationName(project: Project): String { + /* + * Dependency configuration name for android gradle plugin 3.0.0-*. + * We need to use 'api' instead of 'implementation' since user's model class + * might be using Realm's classes and annotations. + */ + val newDependencyName = "api" + val oldDependencyName = "compile" + return try { + project.configurations.getByName(newDependencyName) + newDependencyName + } catch (ignored: UnknownConfigurationException) { + oldDependencyName + } + } + + // This will setup the required dependencies. + // This must only be called once, as removal of dependencies through the iterator API will + // not propagate correctly into the IterationOrderRetainingSetElementSource which is backing + // the dependency set deep inside the DefaultDependencySet implementation, and failure to do + // so causes some internal caching in IterationOrderRetainingSetElementSource to skip + // re-adding it if it had already been there once. + private fun setDependencies( + project: Project, + dependencyConfigurationName: String, + syncEnabled: Boolean, + kotlinExtensionsEnabled: Boolean + ) { + // remove libraries first + val iterator = + project.configurations.getByName(dependencyConfigurationName).dependencies.iterator() + while (iterator.hasNext()) { + val item = iterator.next() + if (item.group == "io.realm") { + if (item.name.startsWith("realm-android-library")) { + iterator.remove() + } + if (item.name.startsWith("realm-android-kotlin-extensions")) { + iterator.remove() + } + } + } + + // then add again + val syncArtifactName = + "realm-android-library${if (syncEnabled) "-object-server" else ""}" + project.dependencies.add( + dependencyConfigurationName, + "io.realm:${syncArtifactName}:${Version.VERSION}" + ) + + if (kotlinExtensionsEnabled) { + val kotlinExtArtifactName = + "realm-android-kotlin-extensions${if (syncEnabled) "-object-server" else ""}" + project.dependencies.add( + dependencyConfigurationName, + "io.realm:${kotlinExtArtifactName}:${Version.VERSION}" + ) + } + } + + } +} diff --git a/gradle-plugin/src/main/kotlin/io/realm/gradle/SimpleAGPVersion.kt b/gradle-plugin/src/main/kotlin/io/realm/gradle/SimpleAGPVersion.kt new file mode 100644 index 0000000000..32c1f8ae43 --- /dev/null +++ b/gradle-plugin/src/main/kotlin/io/realm/gradle/SimpleAGPVersion.kt @@ -0,0 +1,75 @@ +package io.realm.gradle + +/* + * Copyright (C) 2022 The Dagger Authors. + * + * 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. + */ + +/** + * Simple Android Gradle Plugin version class since there is no public API one. b/175816217 + */ +data class SimpleAGPVersion( + val major: Int, + val minor: Int, +) : Comparable { + + override fun compareTo(other: SimpleAGPVersion): Int { + return compareValuesBy( + this, + other, + compareBy(SimpleAGPVersion::major).thenBy(SimpleAGPVersion::minor) + ) { it } + } + + companion object { + + // TODO: Migrate to AndroidPluginVersion once it is available (b/175816217) + val ANDROID_GRADLE_PLUGIN_VERSION by lazy { + val clazz = + findClass("com.android.Version") + ?: findClass("com.android.builder.model.Version") + if (clazz != null) { + return@lazy parse( + clazz.getField("ANDROID_GRADLE_PLUGIN_VERSION").get(null) as String + ) + } + error( + "Unable to obtain AGP version. It is likely that the AGP version being used is too old." + ) + } + + private fun parse(version: String?) = + tryParse(version) ?: error("Unable to parse AGP version: $version") + + private fun tryParse(version: String?): SimpleAGPVersion? { + if (version == null) { + return null + } + + val parts = version.split('.') + if (parts.size == 1) { + return SimpleAGPVersion(parts[0].toInt(), 0) + } + + return SimpleAGPVersion(parts[0].toInt(), parts[1].toInt()) + } + + private fun findClass(fqName: String) = + try { + Class.forName(fqName) + } catch (ex: ClassNotFoundException) { + null + } + } +} diff --git a/gradle-plugin/src/main/templates/Version.java b/gradle-plugin/src/main/templates/Version.java deleted file mode 100644 index 330285c5a5..0000000000 --- a/gradle-plugin/src/main/templates/Version.java +++ /dev/null @@ -1,5 +0,0 @@ -package io.realm.gradle; - -public class Version { - public static final String VERSION = "@version@"; -} diff --git a/gradle-plugin/src/main/templates/Version.kt b/gradle-plugin/src/main/templates/Version.kt new file mode 100644 index 0000000000..3e422086e2 --- /dev/null +++ b/gradle-plugin/src/main/templates/Version.kt @@ -0,0 +1,5 @@ +package io.realm.gradle; + +object Version { + const val VERSION = "@version@" +} diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 2e6e5897b5..26e62a13f1 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.3.3-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-rc-2-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/library-benchmarks/build.gradle b/library-benchmarks/build.gradle index 313e9729b7..95bbdf68e3 100644 --- a/library-benchmarks/build.gradle +++ b/library-benchmarks/build.gradle @@ -52,12 +52,12 @@ android { } compileOptions { - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 + sourceCompatibility JavaVersion.VERSION_11 + targetCompatibility JavaVersion.VERSION_11 } kotlinOptions { - jvmTarget = '1.8' + jvmTarget = '11' } } diff --git a/library-benchmarks/gradle/wrapper/gradle-wrapper.properties b/library-benchmarks/gradle/wrapper/gradle-wrapper.properties index 2e6e5897b5..fcfb29d920 100644 --- a/library-benchmarks/gradle/wrapper/gradle-wrapper.properties +++ b/library-benchmarks/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-rc-1-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/library-build-transformer/build.gradle b/library-build-transformer/build.gradle index a931f49f93..5ac9a2b65f 100644 --- a/library-build-transformer/build.gradle +++ b/library-build-transformer/build.gradle @@ -1,6 +1,3 @@ -group 'io.realm' -version '1.0.0' - buildscript { def properties = new Properties() properties.load(new FileInputStream("${projectDir}/../dependencies.list")) @@ -23,6 +20,7 @@ group = 'io.realm' version = file("${projectDir}/../version.txt").text.trim() apply plugin: 'kotlin' +apply plugin: 'java' apply plugin: 'maven-publish' apply from: "${rootDir}/../mavencentral-publications.gradle" @@ -30,25 +28,26 @@ repositories { google() mavenCentral() } - +def properties = new Properties() +properties.load(new FileInputStream("${projectDir}/../dependencies.list")) dependencies { implementation gradleApi() - compileOnly 'com.android.tools.build:gradle:3.1.1' implementation 'org.ow2.asm:asm:6.2' implementation 'org.ow2.asm:asm-util:6.2' implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:${kotlin_version}" + compileOnly "com.android.tools.build:gradle:${properties.get("GRADLE_BUILD_TOOLS")}" testImplementation group:'junit', name:'junit', version:'4.12' } -sourceCompatibility = '1.8' -targetCompatibility = '1.8' +sourceCompatibility = JavaVersion.VERSION_11 +targetCompatibility = JavaVersion.VERSION_11 compileKotlin { - kotlinOptions.jvmTarget = "1.8" + kotlinOptions.jvmTarget = "11" } compileTestKotlin { - kotlinOptions.jvmTarget = "1.8" + kotlinOptions.jvmTarget = "11" } def commonPom = { diff --git a/library-build-transformer/gradle/wrapper/gradle-wrapper.properties b/library-build-transformer/gradle/wrapper/gradle-wrapper.properties index 2e6e5897b5..fcfb29d920 100644 --- a/library-build-transformer/gradle/wrapper/gradle-wrapper.properties +++ b/library-build-transformer/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-rc-1-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/library-build-transformer/src/main/kotlin/io/realm/buildtransformer/RealmBuildTransformer.kt b/library-build-transformer/src/main/kotlin/io/realm/buildtransformer/RealmBuildTransformer.kt index c213b96fed..a20a856e4c 100644 --- a/library-build-transformer/src/main/kotlin/io/realm/buildtransformer/RealmBuildTransformer.kt +++ b/library-build-transformer/src/main/kotlin/io/realm/buildtransformer/RealmBuildTransformer.kt @@ -15,15 +15,31 @@ */ package io.realm.buildtransformer -import com.android.build.api.transform.* -import com.google.common.collect.ImmutableSet -import io.realm.buildtransformer.asm.ClassPoolTransformer -import io.realm.buildtransformer.ext.packageHierarchyRootDir -import io.realm.buildtransformer.ext.shouldBeDeleted +import com.android.build.api.variant.AndroidComponentsExtension +import io.realm.buildtransformer.asm.visitors.AnnotatedCodeStripVisitor import io.realm.buildtransformer.util.Stopwatch +import org.gradle.api.DefaultTask +import org.gradle.api.Project +import org.gradle.api.file.Directory +import org.gradle.api.file.RegularFile +import org.gradle.api.file.RegularFileProperty +import org.gradle.api.provider.ListProperty +import org.gradle.api.provider.Property +import org.gradle.api.tasks.Input +import org.gradle.api.tasks.InputFiles +import org.gradle.api.tasks.OutputFiles +import org.gradle.api.tasks.TaskAction +import org.objectweb.asm.ClassReader +import org.objectweb.asm.ClassWriter import org.slf4j.Logger import org.slf4j.LoggerFactory -import java.io.File +import java.io.BufferedOutputStream +import java.io.FileInputStream +import java.io.FileOutputStream +import java.io.InputStream +import java.util.jar.JarEntry +import java.util.jar.JarFile +import java.util.jar.JarOutputStream // Type aliases for improving readability typealias ByteCodeTypeDescriptor = String @@ -39,123 +55,132 @@ val logger: Logger = LoggerFactory.getLogger("realm-build-logger") * a specific Android flavour. It is also possible to provide a list of files to delete whether or * not they have the annotation. These files will only be deleted from the defined flavour. */ -class RealmBuildTransformer(private val flavorToStrip: String, - private val annotationQualifiedName: QualifiedName, - private val specificFilesToStrip: Set = setOf()) : Transform() { - - override fun getName(): String { - return "RealmBuildTransformer" - } - - override fun isIncremental(): Boolean { - return true - } - - override fun getScopes(): MutableSet { - return mutableSetOf(QualifiedContent.Scope.PROJECT) - } - - override fun getInputTypes(): Set { - return setOf(QualifiedContent.DefaultContentType.CLASSES) - } - - override fun getReferencedScopes(): MutableSet { - return ImmutableSet.of() - } - - override fun transform(context: Context?, - inputs: MutableCollection?, - referencedInputs: MutableCollection?, - outputProvider: TransformOutputProvider?, - isIncremental: Boolean) { - @Suppress("DEPRECATION") - super.transform(context, inputs, referencedInputs, outputProvider, isIncremental) - - // Poor mans version of detecting variants, since the Gradle API does not allow us to - // register a transformer for only a single build variant - // https://issuetracker.google.com/issues/37072849 - val transformClasses: Boolean = context?.variantName?.startsWith(flavorToStrip.toLowerCase()) == true +class RealmBuildTransformer( + private val annotationQualifiedName: Property, + private val input: ListProperty, + private val output: RegularFileProperty, +) { + + internal fun transform() { + // The AGP build infrastructure will inject the task input and outputs and location is not + // to be considered as part of the API, so we must be able to accept the current scenario + // where the input and output is actually the same JAR. Thus, we write everything to a + // temporary output to avoid truncating the input, and then write this temporary output + // back to the final output location in the end. + // See https://developer.android.com/reference/tools/gradle-api/7.4/com/android/build/api/variant/ScopedArtifactsOperation#toTransform(com.android.build.api.artifact.ScopedArtifact,kotlin.Function1,kotlin.Function1,kotlin.Function1) + // for further information. + val temporaryOutput = output.get().asFile.absolutePath + ".tmp" + val outputProvider = JarOutputStream( + BufferedOutputStream(FileOutputStream(temporaryOutput)) + ) val timer = Stopwatch() timer.start("Build Transform time") - if (isIncremental) { - runIncrementalTransform(inputs!!, outputProvider!!, transformClasses) - } else { - runFullTransform(inputs!!, outputProvider!!, transformClasses) - } - timer.stop() - } - private fun runFullTransform(inputs: MutableCollection, outputProvider: TransformOutputProvider, transformClasses: Boolean) { - logger.debug("Run full transform") - val outputDir = outputProvider.getContentLocation("realmlibrarytransformer", outputTypes, scopes, Format.DIRECTORY) - val inputFiles: MutableSet = mutableSetOf() - inputs.forEach { - it.directoryInputs.forEach { - // Non-incremental build: Include all files - val dirPath: String = it.file.absolutePath - it.file.walkTopDown() - .filter { it.isFile } - .filter { it.name.endsWith(".class") } - .forEach { file -> - file.packageHierarchyRootDir = dirPath - file.shouldBeDeleted = transformClasses && specificFilesToStrip.find { file.absolutePath.endsWith(it) } != null - inputFiles.add(file) - } + // The ASM infrastructure does not allow to inspect annotations of methods before visiting + // them. This prevents inspecting and stripping methods in the same pass, thus we first + // collect information about annotations and then secondly strip the annotation symbols. + // 1. Collect annotation information + val annotationDescriptor = createDescriptor(annotationQualifiedName.get()) + val metadataCollector = + io.realm.buildtransformer.asm.visitors.AnnotationVisitor(annotationDescriptor) + forEachJarEntry { jarEntry, inputStream -> + if (jarEntry.name.endsWith(".class")) { + inputStream.use { + val classReader = ClassReader(it) + classReader.accept(metadataCollector, 0) + } } } - - transformClassFiles(outputDir, inputFiles, transformClasses) - } - - private fun runIncrementalTransform(inputs: MutableCollection, outputProvider: TransformOutputProvider, transformClasses: Boolean) { - logger.debug("Run incremental transform") - val outputDir = outputProvider.getContentLocation("realmlibrarytransformer", outputTypes, scopes, Format.DIRECTORY) - val inputFiles: MutableSet = mutableSetOf() - inputs.forEach { - it.directoryInputs.forEach iterateDirs@{ - if (!it.file.exists()) { - return@iterateDirs // Directory was deleted + // 2. Strip annotated symbols + forEachJarEntry { jarEntry, inputStream -> + val bytes = inputStream.use { inputStream -> + if (jarEntry.name.endsWith(".class")) { + val writer = + ClassWriter(0) // We don't modify methods so no reason to re-calculate method frames + val classRemover = AnnotatedCodeStripVisitor( + annotationDescriptor, + metadataCollector.annotatedClasses, + metadataCollector.annotatedMethods, + metadataCollector.annotatedFields, + writer + ) + ClassReader(inputStream).accept(classRemover, 0) + if (classRemover.deleteClass) ByteArray(0) else writer.toByteArray() + } else { + inputStream.readBytes() } - val dirPath: String = it.file.absolutePath - it.changedFiles.entries - .filter { it.key.isFile } - .filter { it.key.name.endsWith(".class") } - .filterNot { it.value == Status.REMOVED } - .forEach { - val file: File = it.key - file.packageHierarchyRootDir = dirPath - file.shouldBeDeleted = transformClasses && specificFilesToStrip.find { file.absolutePath.endsWith(it) } != null - inputFiles.add(file) - } + } + if (bytes.isNotEmpty()) { + outputProvider.putNextEntry(JarEntry(jarEntry.name)) + outputProvider.write(bytes) + outputProvider.closeEntry() } } - - transformClassFiles(outputDir, inputFiles, transformClasses) + outputProvider.close() + // Write the temporary output to the final output location. See comment about + // temporaryOutput for the details + FileInputStream(temporaryOutput).channel.use { input -> + FileOutputStream(this.output.asFile.get().absoluteFile).channel.use { output -> + output.transferFrom(input, 0, input.size()) + } + } + timer.stop() } - private fun transformClassFiles(outputDir: File, inputFiles: MutableSet, transformClasses: Boolean) { - val files: Set = if (transformClasses) { - val transformer = ClassPoolTransformer(annotationQualifiedName, inputFiles) - transformer.transform() - } else { - inputFiles + private fun forEachJarEntry(block: (jarEntry: JarEntry, inputStream: InputStream) -> Unit) { + val jarFiles: List = input.get().map { JarFile(it.asFile) } + jarFiles.forEach { jarFile -> + jarFile.entries().toList().map { + block(it, jarFile.getInputStream(it)) + } } + } - copyToOutput(outputDir, files) + private fun createDescriptor(qualifiedName: String): String { + return "L${qualifiedName.replace(".", "/")};" } - private fun copyToOutput(outputDir: File, files: Set) { - files.forEach { - val outputFile = File(outputDir, it.absolutePath.substring(it.packageHierarchyRootDir.length)) - if (it.shouldBeDeleted) { - if (outputFile.exists()) { - outputFile.delete() + companion object { + fun register(project: Project, flavorToStrip: String, annotationQualifiedName: QualifiedName) { + val androidComponents = project.extensions.getByType(AndroidComponentsExtension::class.java) + androidComponents.onVariants { variant -> + if (variant.name.startsWith(flavorToStrip)) { + val taskProvider = project.tasks.register( + "${variant.name}RealmBuildTransformer", + ModifyClassesTask::class.java + ) { + it.annotationQualifiedName.set(annotationQualifiedName) + } + variant.artifacts.forScope(com.android.build.api.variant.ScopedArtifacts.Scope.PROJECT) + .use(taskProvider) + .toTransform( + com.android.build.api.artifact.ScopedArtifact.CLASSES, + ModifyClassesTask::allJars, + ModifyClassesTask::allDirectories, + ModifyClassesTask::output + ) } - } else { - it.copyTo(outputFile, overwrite = true) } } } +} +abstract class ModifyClassesTask: DefaultTask() { + @get:InputFiles + abstract val allJars: ListProperty + + @get:InputFiles + abstract val allDirectories: ListProperty + @get:OutputFiles + abstract val output: RegularFileProperty + + @get:Input + abstract val annotationQualifiedName : Property + + @TaskAction + fun taskAction() { + RealmBuildTransformer(annotationQualifiedName, allJars, output) + .transform() + } } diff --git a/library-build-transformer/src/main/kotlin/io/realm/buildtransformer/asm/visitors/AnnotatedCodeStripVisitor.kt b/library-build-transformer/src/main/kotlin/io/realm/buildtransformer/asm/visitors/AnnotatedCodeStripVisitor.kt index 0892d82cba..64dcf85c1f 100644 --- a/library-build-transformer/src/main/kotlin/io/realm/buildtransformer/asm/visitors/AnnotatedCodeStripVisitor.kt +++ b/library-build-transformer/src/main/kotlin/io/realm/buildtransformer/asm/visitors/AnnotatedCodeStripVisitor.kt @@ -30,7 +30,7 @@ class AnnotatedCodeStripVisitor(private val annotationDescriptor: String, private val markedClasses: Set, private val markedMethods: Map>, private val markedFields: Map>, - classWriter: ClassVisitor) : ClassVisitor(Opcodes.ASM6, classWriter) { + classWriter: ClassVisitor) : ClassVisitor(Opcodes.ASM7, classWriter) { var deleteClass: Boolean = false private lateinit var markedMethodsInClass: Set @@ -51,7 +51,7 @@ class AnnotatedCodeStripVisitor(private val annotationDescriptor: String, // Remove INNERCLASS definitions from the bytecode in the top level class. It isn't clear if // these are used by any relevant API's, but better remove them just in case. override fun visitInnerClass(name: String?, outerName: String?, innerName: String?, access: Int) { - if (!markedClasses.contains(name)) { + if (!markedClasses.contains(name) && !deleteClass) { super.visitInnerClass(name, outerName, innerName, access) } else { logger.debug("Removing inner class description: $name") @@ -59,7 +59,7 @@ class AnnotatedCodeStripVisitor(private val annotationDescriptor: String, } override fun visitField(access: Int, name: String?, descriptor: String?, signature: String?, value: Any?): FieldVisitor? { - return if (!markedFieldsInClass.contains(name)) { + return if (!markedFieldsInClass.contains(name) && !deleteClass) { super.visitField(access, name, descriptor, signature, value) } else { logger.debug("Removing field: $name") @@ -68,7 +68,7 @@ class AnnotatedCodeStripVisitor(private val annotationDescriptor: String, } override fun visitMethod(access: Int, name: ByteCodeMethodName?, descriptor: String?, signature: String?, exceptions: Array?): MethodVisitor? { - return if (!markedMethodsInClass.contains(name + descriptor)) { + return if (!markedMethodsInClass.contains(name + descriptor) && !deleteClass) { super.visitMethod(access, name, descriptor, signature, exceptions) } else { logger.debug("Removing method: $name $descriptor") diff --git a/library-build-transformer/src/main/kotlin/io/realm/buildtransformer/asm/visitors/AnnotationVisitor.kt b/library-build-transformer/src/main/kotlin/io/realm/buildtransformer/asm/visitors/AnnotationVisitor.kt index a34825b972..43bcce4d14 100644 --- a/library-build-transformer/src/main/kotlin/io/realm/buildtransformer/asm/visitors/AnnotationVisitor.kt +++ b/library-build-transformer/src/main/kotlin/io/realm/buildtransformer/asm/visitors/AnnotationVisitor.kt @@ -18,7 +18,6 @@ package io.realm.buildtransformer.asm.visitors; import io.realm.buildtransformer.ByteCodeMethodName import io.realm.buildtransformer.ByteCodeTypeDescriptor import io.realm.buildtransformer.FieldName -import io.realm.buildtransformer.logger import org.objectweb.asm.* import org.objectweb.asm.AnnotationVisitor @@ -27,7 +26,7 @@ import org.objectweb.asm.AnnotationVisitor * pass and is required for correctly identifying them in the 2nd pass before any byte code is * written. */ -class AnnotationVisitor(private val annotationDescriptor: String) : ClassVisitor(Opcodes.ASM6) { +class AnnotationVisitor(private val annotationDescriptor: String) : ClassVisitor(Opcodes.ASM7) { val annotatedClasses: MutableSet = mutableSetOf() val annotatedMethods: MutableMap> = mutableMapOf() diff --git a/realm-annotations/build.gradle b/realm-annotations/build.gradle index 7da81ff129..b397f25e72 100644 --- a/realm-annotations/build.gradle +++ b/realm-annotations/build.gradle @@ -17,8 +17,8 @@ buildscript { apply plugin: 'java' apply plugin: 'maven-publish' -sourceCompatibility = '1.8' -targetCompatibility = '1.8' +sourceCompatibility = JavaVersion.VERSION_1_8 +targetCompatibility = JavaVersion.VERSION_1_8 group = 'io.realm' version = file("${projectDir}/../version.txt").text.trim() @@ -44,4 +44,10 @@ publishing { java { withSourcesJar() withJavadocJar() + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 +} + +javadoc { + options.encoding = 'UTF-8' } diff --git a/realm-annotations/gradle/wrapper/gradle-wrapper.properties b/realm-annotations/gradle/wrapper/gradle-wrapper.properties index 2e6e5897b5..fcfb29d920 100644 --- a/realm-annotations/gradle/wrapper/gradle-wrapper.properties +++ b/realm-annotations/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-rc-1-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/realm-transformer/build.gradle b/realm-transformer/build.gradle index c360c53a73..7982fcbb5f 100644 --- a/realm-transformer/build.gradle +++ b/realm-transformer/build.gradle @@ -27,8 +27,8 @@ properties.load(new FileInputStream("${projectDir}/../dependencies.list")) def coreVersion = properties.getProperty('REALM_CORE') -sourceCompatibility = '1.8' -targetCompatibility = '1.8' +sourceCompatibility = JavaVersion.VERSION_11 +targetCompatibility = JavaVersion.VERSION_11 repositories { mavenLocal() @@ -96,10 +96,13 @@ publishing { java { withSourcesJar() withJavadocJar() + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 } compileKotlin { kotlinOptions { - freeCompilerArgs = ["-Xinline-classes"] + jvmTarget = JavaVersion.VERSION_11 + freeCompilerArgs = ["-Xinline-classes", "-Xjvm-default=all-compatibility"] } } diff --git a/realm-transformer/gradle/wrapper/gradle-wrapper.properties b/realm-transformer/gradle/wrapper/gradle-wrapper.properties index 2e6e5897b5..fcfb29d920 100644 --- a/realm-transformer/gradle/wrapper/gradle-wrapper.properties +++ b/realm-transformer/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-rc-1-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/realm-transformer/src/main/java/io/realm/gradle/RealmPluginExtension.java b/realm-transformer/src/main/java/io/realm/gradle/RealmPluginExtension.java index a83fed35f0..f2b14f4155 100644 --- a/realm-transformer/src/main/java/io/realm/gradle/RealmPluginExtension.java +++ b/realm-transformer/src/main/java/io/realm/gradle/RealmPluginExtension.java @@ -22,13 +22,8 @@ import java.util.Map; public class RealmPluginExtension { - - public static final String KEY_SYNC_ENABLED = "syncEnabled"; - public static final String KEY_KOTLIN_EXTENSIONS_ENABLED = "kotlinExtensionsEnabled"; - private boolean syncEnabled; private boolean kotlinExtensionsEnabled; - private Map listeners = new LinkedHashMap<>(); @Input public boolean isSyncEnabled() { @@ -37,7 +32,6 @@ public boolean isSyncEnabled() { public void setSyncEnabled(boolean syncEnabled) { this.syncEnabled = syncEnabled; - notifyChange(KEY_SYNC_ENABLED, syncEnabled); } @Input @@ -47,25 +41,5 @@ public boolean isKotlinExtensionsEnabled() { public void setKotlinExtensionsEnabled(boolean kotlinExtensionsEnabled) { this.kotlinExtensionsEnabled = kotlinExtensionsEnabled; - notifyChange(KEY_KOTLIN_EXTENSIONS_ENABLED, kotlinExtensionsEnabled); - } - - public void addPropertyListener(String property, PropertyChangedListener listener) { - listeners.put(property, listener); - } - - private void notifyChange(String key, Object value) { - PropertyChangedListener listener = listeners.get(key); - if (listener != null) { - // Up to users of the API to use the correct generic type, otherwise it will crash - // at runtime. - //noinspection unchecked - listener.onChange(value); - } - } - - // Callback triggered when the extension property is changed - public interface PropertyChangedListener { - void onChange(T value); } } diff --git a/realm-transformer/src/main/java/io/realm/transformer/Utils.java b/realm-transformer/src/main/java/io/realm/transformer/Utils.java index 7069bfe429..c515d8c8a5 100644 --- a/realm-transformer/src/main/java/io/realm/transformer/Utils.java +++ b/realm-transformer/src/main/java/io/realm/transformer/Utils.java @@ -29,7 +29,7 @@ public class Utils { * Encode the given string with Base64 * @param data the string to encode * @return the encoded string - * @throws UnsupportedEncodingException + * @throws UnsupportedEncodingException if string is not valid utf8 */ public static String base64Encode(String data) throws UnsupportedEncodingException { return DatatypeConverter.printBase64Binary(data.getBytes("UTF-8")); diff --git a/realm-transformer/src/main/kotlin/io/realm/analytics/AnalyticsData.kt b/realm-transformer/src/main/kotlin/io/realm/analytics/AnalyticsData.kt index 505f7dd3ca..d0f7e729d0 100644 --- a/realm-transformer/src/main/kotlin/io/realm/analytics/AnalyticsData.kt +++ b/realm-transformer/src/main/kotlin/io/realm/analytics/AnalyticsData.kt @@ -20,7 +20,8 @@ import io.realm.transformer.Version import java.net.SocketException import java.security.NoSuchAlgorithmException -inline class PublicAppId(val id: String) { +@JvmInline +value class PublicAppId(val id: String) { fun anonymize(): String { val idBytes: ByteArray = id.toByteArray() return Utils.hexStringify(Utils.sha256Hash(idBytes)) diff --git a/realm-transformer/src/main/kotlin/io/realm/analytics/ComputerIdentifierGenerator.kt b/realm-transformer/src/main/kotlin/io/realm/analytics/ComputerIdentifierGenerator.kt index 8d007c825f..ba5df1e07d 100644 --- a/realm-transformer/src/main/kotlin/io/realm/analytics/ComputerIdentifierGenerator.kt +++ b/realm-transformer/src/main/kotlin/io/realm/analytics/ComputerIdentifierGenerator.kt @@ -33,7 +33,7 @@ import java.util.* class ComputerIdentifierGenerator { companion object { private const val UNKNOWN = "unknown" - private val OS: String = System.getProperty("os.name").toLowerCase() + private val OS: String = System.getProperty("os.name").lowercase(Locale.getDefault()) private val isWindows: Boolean = OS.contains("win") private val isMac: Boolean = OS.contains("mac") private val isLinux: Boolean = OS.contains("inux") diff --git a/realm-transformer/src/main/kotlin/io/realm/transformer/ManagedClassPool.kt b/realm-transformer/src/main/kotlin/io/realm/transformer/ManagedClassPool.kt index ce337d9a53..aa4906cd4e 100644 --- a/realm-transformer/src/main/kotlin/io/realm/transformer/ManagedClassPool.kt +++ b/realm-transformer/src/main/kotlin/io/realm/transformer/ManagedClassPool.kt @@ -16,18 +16,21 @@ package io.realm.transformer -import com.android.build.api.transform.TransformInput import javassist.ClassPath import javassist.ClassPool +import org.gradle.api.file.ConfigurableFileCollection +import org.gradle.api.file.Directory +import org.gradle.api.provider.ListProperty import java.io.Closeable +import java.io.File /** * This class is a wrapper around JavaAssists {@code ClassPool} class that allows for correct cleanup * of the resources used. */ -class ManagedClassPool(inputs: Collection, referencedInputs: Collection) : ClassPool(), Closeable { +class ManagedClassPool(inputs: ListProperty, referencedInputs: ConfigurableFileCollection) : ClassPool(), Closeable { - val pathElements: ArrayList = arrayListOf() + private val pathElements: ArrayList = arrayListOf() /** * Constructor for creating and populating the JavAssist class pool. @@ -42,24 +45,14 @@ class ManagedClassPool(inputs: Collection, referencedInputs: Col // will use a cached object and all the classes will be frozen. appendSystemPath() - inputs.forEach{ - it.directoryInputs.forEach { - pathElements.add(appendClassPath(it.file.absolutePath)) - } - - it.jarInputs.forEach { - pathElements.add(appendClassPath(it.file.absolutePath)) + inputs.get().forEach{ directory: Directory -> + directory.asFile.walk().filter(File::isDirectory).forEach { + pathElements.add(appendClassPath(it.absolutePath)) } } referencedInputs.forEach { - it.directoryInputs.forEach { - pathElements.add(appendClassPath(it.file.absolutePath)) - } - - it.jarInputs.forEach { - pathElements.add(appendClassPath(it.file.absolutePath)) - } + pathElements.add(appendClassPath(it.absolutePath)) } } diff --git a/realm-transformer/src/main/kotlin/io/realm/transformer/RealmTransformer.kt b/realm-transformer/src/main/kotlin/io/realm/transformer/RealmTransformer.kt index c85acccbf1..5db110cd6c 100644 --- a/realm-transformer/src/main/kotlin/io/realm/transformer/RealmTransformer.kt +++ b/realm-transformer/src/main/kotlin/io/realm/transformer/RealmTransformer.kt @@ -16,17 +16,29 @@ package io.realm.transformer -import com.android.build.api.transform.* +import com.android.build.api.variant.AndroidComponentsExtension +import com.android.build.gradle.internal.publishing.AndroidArtifacts import io.realm.analytics.RealmAnalytics import io.realm.transformer.build.BuildTemplate import io.realm.transformer.build.FullBuild -import io.realm.transformer.build.IncrementalBuild import io.realm.transformer.ext.getBootClasspath -import javassist.CtClass +import org.gradle.api.DefaultTask import org.gradle.api.Project +import org.gradle.api.file.ConfigurableFileCollection +import org.gradle.api.file.Directory +import org.gradle.api.file.RegularFile +import org.gradle.api.file.RegularFileProperty +import org.gradle.api.provider.ListProperty +import org.gradle.api.tasks.Classpath +import org.gradle.api.tasks.InputFiles +import org.gradle.api.tasks.OutputFiles +import org.gradle.api.tasks.TaskAction import org.slf4j.Logger import org.slf4j.LoggerFactory +import java.io.BufferedOutputStream import java.io.File +import java.io.FileOutputStream +import java.util.jar.JarOutputStream // Package level logger val logger: Logger = LoggerFactory.getLogger("realm-logger") @@ -43,8 +55,11 @@ data class ProjectMetaData( /** * This class implements the Transform API provided by the Android Gradle plugin. */ -class RealmTransformer(project: Project) : Transform() { - private val logger: Logger = LoggerFactory.getLogger("realm-logger") +class RealmTransformer(project: Project, + private val inputs: ListProperty, + private val allJars: ListProperty, + private val referencedInputs: ConfigurableFileCollection, + private val output: RegularFileProperty) { private lateinit var metadata: ProjectMetaData private var analytics: RealmAnalytics? = null @@ -52,47 +67,51 @@ class RealmTransformer(project: Project) : Transform() { // Fetch project metadata when registering the transformer, but as some of the properties // we need to read might not be initialized yet (e.g. the Android extension), we need // to wait until after the build files have been evaluated. - project.afterEvaluate { - metadata = ProjectMetaData( - // Plugin requirements - project.gradle.startParameter.isOffline, - project.getBootClasspath() - ) - - try { - this.analytics = RealmAnalytics() - this.analytics!!.calculateAnalyticsData(project) - } catch (e: Exception) { - // Analytics should never crash the build. - logger.debug("Could not calculate Realm analytics data:\n$e" ) - } - } - } - override fun getName(): String { - return "RealmTransformer" - } - - override fun getInputTypes(): Set { - return setOf(QualifiedContent.DefaultContentType.CLASSES) - } + metadata = ProjectMetaData( + // Plugin requirements + project.gradle.startParameter.isOffline, + project.getBootClasspath() + ) - override fun isIncremental(): Boolean { - return true - } + try { + this.analytics = RealmAnalytics() + this.analytics!!.calculateAnalyticsData(project) - override fun getScopes(): MutableSet { - return mutableSetOf(QualifiedContent.Scope.PROJECT) + } catch (e: Exception) { + // Analytics should never crash the build. + logger.debug("Could not calculate Realm analytics data:\n$e") + } } - override fun getReferencedScopes(): MutableSet { - // Scope.PROJECT_LOCAL_DEPS and Scope.SUB_PROJECTS_LOCAL_DEPS is only for compatibility with AGP 1.x, 2.x - return mutableSetOf( - QualifiedContent.Scope.EXTERNAL_LIBRARIES, - QualifiedContent.Scope.PROJECT_LOCAL_DEPS, - QualifiedContent.Scope.SUB_PROJECTS, - QualifiedContent.Scope.SUB_PROJECTS_LOCAL_DEPS, - QualifiedContent.Scope.TESTED_CODE - ) + companion object { + + fun register(project: Project) { + val androidComponents = project.extensions.getByType(AndroidComponentsExtension::class.java) + androidComponents.onVariants { variant -> + variant.components.forEach { component -> + val taskProvider = + project.tasks.register( + "${component.name}RealmAccessorsTransformer", + ModifyClassesTask::class.java + ) { + it.fullRuntimeClasspath.setFrom(component.runtimeConfiguration.incoming.artifactView { c -> + c.attributes.attribute( + AndroidArtifacts.ARTIFACT_TYPE, + AndroidArtifacts.ArtifactType.CLASSES_JAR.type + ) + }.files) + } + component.artifacts.forScope(com.android.build.api.variant.ScopedArtifacts.Scope.PROJECT) + .use(taskProvider) + .toTransform( + com.android.build.api.artifact.ScopedArtifact.CLASSES, + ModifyClassesTask::allJars, + ModifyClassesTask::allDirectories, + ModifyClassesTask::output + ) + } + } + } } /** @@ -103,34 +122,29 @@ class RealmTransformer(project: Project) : Transform() { * incremental build. In a full build, we can use text matching to go from a proxy class * to the model class. Something we cannot do when building incrementally. In that case * we have to deduce all information from the class at hand. - * - * @param context - * @param inputs - * @param referencedInputs - * @param outputProvider - * @param isIncremental - * @throws IOException - * @throws TransformException - * @throws InterruptedException */ - override fun transform(context: Context?, inputs: MutableCollection?, - referencedInputs: Collection?, - outputProvider: TransformOutputProvider?, isIncremental: Boolean) { - + internal fun transform() { val timer = Stopwatch() timer.start("Realm Transform time") - val build: BuildTemplate = if (isIncremental) IncrementalBuild(metadata, outputProvider!!, this) - else FullBuild(metadata, outputProvider!!, this) + val jarOutput = JarOutputStream( + BufferedOutputStream( + FileOutputStream( + output.get().asFile + ) + ) + ) - build.prepareOutputClasses(inputs!!) + val build: BuildTemplate = FullBuild(metadata, allJars, jarOutput, this) + + build.prepareOutputClasses(inputs) timer.splitTime("Prepare output classes") if (build.hasNoOutput()) { // Abort transform as quickly as possible if no files where found for processing. - exitTransform(emptySet(), emptySet(), timer) + exitTransform(timer) return } - build.prepareReferencedClasses(referencedInputs!!) + build.prepareReferencedClasses(referencedInputs) timer.splitTime("Prepare referenced classes") build.markMediatorsAsTransformed() timer.splitTime("Mark mediators as transformed") @@ -138,13 +152,37 @@ class RealmTransformer(project: Project) : Transform() { timer.splitTime("Transform model classes") build.transformDirectAccessToModelFields() timer.splitTime("Transform references to model fields") + build.copyProcessedClasses() + timer.splitTime("Copy processed classes") build.copyResourceFiles() - timer.splitTime("Copy resource files") - exitTransform(inputs, build.getOutputModelClasses(), timer) + timer.splitTime("Copy jar files") + jarOutput.close() + exitTransform(timer) } - private fun exitTransform(inputs: Collection, outputModelClasses: Set, timer: Stopwatch) { + private fun exitTransform(timer: Stopwatch) { timer.stop() analytics?.execute() } } + +abstract class ModifyClassesTask: DefaultTask() { + @get:Classpath + abstract val fullRuntimeClasspath : ConfigurableFileCollection + + @get:InputFiles + abstract val allJars: ListProperty + + @get:InputFiles + abstract val allDirectories: ListProperty + + @get:OutputFiles + abstract val output: RegularFileProperty + + @TaskAction + fun taskAction() { + RealmTransformer(project, allDirectories, allJars, fullRuntimeClasspath, output) + .transform() + } +} + diff --git a/realm-transformer/src/main/kotlin/io/realm/transformer/build/BuildTemplate.kt b/realm-transformer/src/main/kotlin/io/realm/transformer/build/BuildTemplate.kt index ef66df5139..8cf1e596c4 100644 --- a/realm-transformer/src/main/kotlin/io/realm/transformer/build/BuildTemplate.kt +++ b/realm-transformer/src/main/kotlin/io/realm/transformer/build/BuildTemplate.kt @@ -15,17 +15,17 @@ */ package io.realm.transformer.build -import com.android.build.api.transform.DirectoryInput -import com.android.build.api.transform.Format -import com.android.build.api.transform.JarInput -import com.android.build.api.transform.Transform -import com.android.build.api.transform.TransformInput -import com.android.build.api.transform.TransformOutputProvider -import com.google.common.io.Files import io.realm.transformer.* import javassist.ClassPool import javassist.CtClass +import org.gradle.api.file.ConfigurableFileCollection +import org.gradle.api.file.Directory +import org.gradle.api.file.RegularFile +import org.gradle.api.provider.ListProperty import java.io.File +import java.util.jar.JarEntry +import java.util.jar.JarFile +import java.util.jar.JarOutputStream import java.util.regex.Pattern public const val DOT_CLASS = ".class" @@ -35,18 +35,19 @@ public const val DOT_JAR = ".jar" * Abstract class defining the structure of doing different types of builds. * */ -abstract class BuildTemplate(val metadata: ProjectMetaData, val outputProvider: TransformOutputProvider, val transform: Transform) { +abstract class BuildTemplate(private val metadata: ProjectMetaData, private val allJars: ListProperty, protected val outputProvider: JarOutputStream, val transform: RealmTransformer) { - protected lateinit var inputs: MutableCollection + protected lateinit var inputs: ListProperty protected lateinit var classPool: ManagedClassPool protected val outputClassNames: MutableSet = hashSetOf() protected val outputReferencedClassNames: MutableSet = hashSetOf() protected val outputModelClasses: ArrayList = arrayListOf() + protected val processedClasses = mutableMapOf() /** * Find all the class names available for transforms as well as all referenced classes. */ - abstract fun prepareOutputClasses(inputs: MutableCollection) + abstract fun prepareOutputClasses(inputs: ListProperty) /** * Helper method for going through all `TransformInput` and sort classes into buckets of @@ -58,9 +59,11 @@ abstract class BuildTemplate(val metadata: ProjectMetaData, val outputProvider: * @param jaFiles the set of files found in jar files. These will never be transformed. This should * already be done when creating the jar file. */ - protected abstract fun categorizeClassNames(inputs: Collection, - directoryFiles: MutableSet, - referencedFiles: MutableSet) + protected abstract fun categorizeClassNames(inputs: ListProperty, + directoryFiles: MutableSet) + + protected abstract fun categorizeClassNames(referencedInputs: ConfigurableFileCollection, + jarFiles: MutableSet) /** * Returns `true` if this build contains no relevant classes to transform. @@ -70,8 +73,8 @@ abstract class BuildTemplate(val metadata: ProjectMetaData, val outputProvider: } - fun prepareReferencedClasses(referencedInputs: Collection) { - categorizeClassNames(referencedInputs, outputReferencedClassNames, outputReferencedClassNames) // referenced files + fun prepareReferencedClasses(referencedInputs: ConfigurableFileCollection) { + categorizeClassNames(referencedInputs, outputReferencedClassNames) // referenced files // Create and populate the Javassist class pool this.classPool = ManagedClassPool(inputs, referencedInputs) @@ -97,6 +100,7 @@ abstract class BuildTemplate(val metadata: ProjectMetaData, val outputProvider: logger.debug("Proxy Mediator Classes: ${proxyMediatorClasses.joinToString(",") { it.name }}") proxyMediatorClasses.forEach { BytecodeModifier.overrideTransformedMarker(it) + processedClasses[it.name] = it } } @@ -107,51 +111,46 @@ abstract class BuildTemplate(val metadata: ProjectMetaData, val outputProvider: BytecodeModifier.addRealmAccessors(it) BytecodeModifier.addRealmProxyInterface(it, classPool) BytecodeModifier.callInjectObjectContextFromConstructors(it) + + processedClasses[it.name] = it } } abstract fun transformDirectAccessToModelFields() - fun copyResourceFiles() { - copyResourceFiles(inputs) - classPool.close(); + fun copyProcessedClasses() { + for ((fqname: String, clazz: CtClass) in processedClasses) { + outputProvider.putNextEntry(JarEntry("${fqname.replace('.', '/')}.class")) + outputProvider.write(clazz.toBytecode()) + outputProvider.closeEntry() + } } - private fun copyResourceFiles(inputs: MutableCollection) { - inputs.forEach { input: TransformInput -> - input.directoryInputs.forEach { directory: DirectoryInput -> - val dirPath: String = directory.file.absolutePath - directory.file.walkTopDown().forEach { file: File -> - if (file.isFile) { - if (!file.absolutePath.endsWith(DOT_CLASS)) { - logger.debug(" Copying resource file: $file") - val dest = File(getOutputFile(outputProvider, Format.DIRECTORY), file.absolutePath.substring(dirPath.length)) - dest.parentFile.mkdirs() - Files.copy(file, dest) - } - } + fun copyResourceFiles() { + inputs.get().forEach { directory: Directory -> + val dirName = directory.asFile.absolutePath + File.separator + directory.asFile.walk().filter(File::isFile).forEach { file -> + if (!file.absolutePath.endsWith(DOT_CLASS)) { + val removePrefix = file.absolutePath.removePrefix(dirName) + outputProvider.putNextEntry(JarEntry(removePrefix)) + outputProvider.write(file.readBytes()) + outputProvider.closeEntry() } } - - input.jarInputs.forEach { jar: JarInput -> - logger.debug("Found JAR file: ${jar.file.absolutePath}") - val dirPath: String = jar.file.absolutePath - jar.file.walkTopDown().forEach { file: File -> - if (file.isFile) { - if (file.absolutePath.endsWith(DOT_JAR)) { - logger.debug(" Copying jar file: $file") - val dest = File(getOutputFile(outputProvider, Format.JAR), file.absolutePath.substring(dirPath.length)) - dest.parentFile.mkdirs() - Files.copy(file, dest) - } - } + } + allJars.get().forEach { file -> + val jarFile = JarFile(file.asFile) + for (jarEntry: JarEntry in jarFile.entries()) { + outputProvider.putNextEntry(JarEntry(jarEntry.name)) + jarFile.getInputStream(jarEntry).use { + outputProvider.write(it.readBytes()) } + outputProvider.closeEntry() } + jarFile.close() } - } - protected fun getOutputFile(outputProvider: TransformOutputProvider, format: Format): File { - return outputProvider.getContentLocation("realm", transform.inputTypes, transform.scopes, format) + classPool.close() } /** diff --git a/realm-transformer/src/main/kotlin/io/realm/transformer/build/FullBuild.kt b/realm-transformer/src/main/kotlin/io/realm/transformer/build/FullBuild.kt index d317a69e2b..8bda056559 100644 --- a/realm-transformer/src/main/kotlin/io/realm/transformer/build/FullBuild.kt +++ b/realm-transformer/src/main/kotlin/io/realm/transformer/build/FullBuild.kt @@ -16,9 +16,6 @@ package io.realm.transformer.build -import com.android.build.api.transform.Format -import com.android.build.api.transform.TransformInput -import com.android.build.api.transform.TransformOutputProvider import io.realm.transformer.BytecodeModifier import io.realm.transformer.ProjectMetaData import io.realm.transformer.RealmTransformer @@ -26,41 +23,46 @@ import io.realm.transformer.ext.safeSubtypeOf import io.realm.transformer.logger import javassist.CtClass import javassist.CtField +import org.gradle.api.file.ConfigurableFileCollection +import org.gradle.api.file.Directory +import org.gradle.api.file.RegularFile +import org.gradle.api.provider.ListProperty import java.io.File import java.util.jar.JarFile +import java.util.jar.JarOutputStream -class FullBuild(metadata: ProjectMetaData, outputProvider: TransformOutputProvider, transformer: RealmTransformer) - : BuildTemplate(metadata, outputProvider, transformer) { +class FullBuild(metadata: ProjectMetaData, allJars: ListProperty, outputProvider: JarOutputStream, transformer: RealmTransformer) + : BuildTemplate(metadata, allJars, outputProvider, transformer) { private val allModelClasses: ArrayList = arrayListOf() - override fun prepareOutputClasses(inputs: MutableCollection) { + override fun prepareOutputClasses(inputs: ListProperty) { this.inputs = inputs; - categorizeClassNames(inputs, outputClassNames, outputReferencedClassNames) + categorizeClassNames(inputs, outputClassNames) logger.debug("Full build. Files being processed: ${outputClassNames.size}.") } - override fun categorizeClassNames(inputs: Collection, - directoryFiles: MutableSet, - jarFiles: MutableSet) { - inputs.forEach { - it.directoryInputs.forEach { - val dirPath: String = it.file.absolutePath - // Non-incremental build: Include all files - it.file.walkTopDown().forEach { - if (it.isFile) { - if (it.absolutePath.endsWith(DOT_CLASS)) { - val className: String = it.absolutePath - .substring(dirPath.length + 1, it.absolutePath.length - DOT_CLASS.length) - .replace(File.separatorChar, '.') - directoryFiles.add(className) - } - } + override fun categorizeClassNames(inputs: ListProperty, + directoryFiles: MutableSet) { + + inputs.get().forEach { directory -> + val dirPath: String = directory.asFile.absolutePath + directory.asFile.walk().filter(File::isFile).forEach { file -> + if (file.absolutePath.endsWith(DOT_CLASS)) { + val className: String = file.absolutePath + .substring(dirPath.length + 1, file.absolutePath.length - DOT_CLASS.length) + .replace(File.separatorChar, '.') + directoryFiles.add(className) } } + } + } - it.jarInputs.forEach { - val jarFile = JarFile(it.file) + override fun categorizeClassNames(referencedInputs: ConfigurableFileCollection, + jarFiles: MutableSet) { + + referencedInputs.forEach { + val jarFile = JarFile(it) jarFile.entries() .toList() .filter { @@ -79,7 +81,6 @@ class FullBuild(metadata: ProjectMetaData, outputProvider: TransformOutputProvid } jarFile.close() // Crash transformer if this fails } - } } override fun findModelClasses(classNames: Set): Collection { @@ -133,8 +134,11 @@ class FullBuild(metadata: ProjectMetaData, outputProvider: TransformOutputProvid logger.debug("Modifying accessors in class: $it") try { val ctClass: CtClass = classPool.getCtClass(it) + if (ctClass.isFrozen) { + ctClass.defrost() + } BytecodeModifier.useRealmAccessors(classPool, ctClass, allManagedFields) - ctClass.writeFile(getOutputFile(outputProvider, Format.DIRECTORY).canonicalPath) + processedClasses[it] = ctClass } catch (e: Exception) { throw RuntimeException("Failed to transform $it.", e) } diff --git a/realm-transformer/src/main/kotlin/io/realm/transformer/build/IncrementalBuild.kt b/realm-transformer/src/main/kotlin/io/realm/transformer/build/IncrementalBuild.kt deleted file mode 100644 index 34e8bafc28..0000000000 --- a/realm-transformer/src/main/kotlin/io/realm/transformer/build/IncrementalBuild.kt +++ /dev/null @@ -1,151 +0,0 @@ -/* - * Copyright 2018 Realm Inc. - * - * 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 io.realm.transformer.build - -import com.android.build.api.transform.Format -import com.android.build.api.transform.Status -import com.android.build.api.transform.TransformInput -import com.android.build.api.transform.TransformOutputProvider -import io.realm.annotations.RealmClass -import io.realm.transformer.BytecodeModifier -import io.realm.transformer.ProjectMetaData -import io.realm.transformer.RealmTransformer -import io.realm.transformer.ext.safeSubtypeOf -import io.realm.transformer.logger -import javassist.CtClass -import javassist.NotFoundException -import java.io.File -import java.util.jar.JarFile - -class IncrementalBuild(metadata: ProjectMetaData, outputProvider: TransformOutputProvider, transform: RealmTransformer) - : BuildTemplate(metadata, outputProvider, transform) { - - override fun prepareOutputClasses(inputs: MutableCollection) { - this.inputs = inputs; - categorizeClassNames(inputs, outputClassNames, outputReferencedClassNames) // Output files - logger.debug("Incremental build. Files being processed: ${outputClassNames.size}.") - logger.debug("Incremental files: ${outputClassNames.joinToString(",")}") - } - - override fun filterForModelClasses(outputClassNames: Set, outputReferencedClassNames: Set) { - outputModelClasses.addAll(findModelClasses(outputClassNames)) - } - - override fun transformDirectAccessToModelFields() { - // Use accessors instead of direct field access - outputClassNames.forEach { - logger.debug("Modify accessors in class: $it") - val ctClass: CtClass = classPool.getCtClass(it) - BytecodeModifier.useRealmAccessors(classPool, ctClass, null) - ctClass.writeFile(getOutputFile(outputProvider, Format.DIRECTORY).canonicalPath) - } - } - - /** - * Categorize the transform input into its two main categorizes: `directoryFiles` which are - * source files in the current project and `jarFiles` which are source files found in jars. - * - * @param inputs set of input files - * @param directoryFiles the set of files in directories getting compiled. These are candidates for the transformer. - * @param jarFiles the set of files that are possible referenced but never transformed (required by JavaAssist). - * @param isIncremental `true` if build is incremental. - */ - override fun categorizeClassNames(inputs: Collection, - directoryFiles: MutableSet, - jarFiles: MutableSet) { - inputs.forEach { - it.directoryInputs.forEach { - val dirPath: String = it.file.absolutePath - - it.changedFiles.entries.forEach { - if (it.value == Status.NOTCHANGED || it.value == Status.REMOVED) { - return@forEach - } - val filePath: String = it.key.absolutePath - if (filePath.endsWith(DOT_CLASS)) { - val className = filePath - .substring(dirPath.length + 1, filePath.length - DOT_CLASS.length) - .replace(File.separatorChar, '.') - directoryFiles.add(className) - } - } - } - - it.jarInputs.forEach { - if (it.status == Status.REMOVED) { - return@forEach - } - - val jarFile = JarFile(it.file) - jarFile.entries() - .toList() - .filter { - !it.isDirectory && it.name.endsWith(DOT_CLASS) - } - .forEach { - val path: String = it.name - // The jar might not using File.separatorChar as the path separator. So we just replace both `\` and - // `/`. It depends on how the jar file was created. - // See http://stackoverflow.com/questions/13846000/file-separators-of-path-name-of-zipentry - val className: String = path - .substring(0, path.length - DOT_CLASS.length) - .replace('/', '.') - .replace('\\', '.') - jarFiles.add(className) - } - jarFile.close() // Crash transformer if this fails - } - } - } - - override fun findModelClasses(classNames: Set): Collection { - val realmObjectProxyInterface: CtClass = classPool.get("io.realm.internal.RealmObjectProxy") - // For incremental builds we need to determine if a class is a model class file - // based on information in the file itself. This require checks that are only - // possible once we loaded the CtClass from the ClassPool and is slower - // than the approach used when doing full builds. - return classNames - // Map strings to CtClass'es. - .map { classPool.getCtClass(it) } - // Model classes either have the @RealmClass annotation directly (if implementing RealmModel) - // or their superclass has it (if extends RealmObject). The annotation processor - // will have ensured the annotation is only present in these cases. - .filter { - var result: Boolean - if (it.hasAnnotation(RealmClass::class.java)) { - result = true - } else { - try { - result = it.superclass?.hasAnnotation(RealmClass::class.java) == true - } catch (e: NotFoundException) { - // Can happen if the super class is part of the `android.jar` which might - // not have been loaded. In any case, any base class part of Android cannot - // be a Realm model class. - result = false - } - } - return@filter result - } - // Proxy classes are generated by the Realm Annotation Processor and might accidentally - // pass the above check (e.g. if the model class has the @RealmClass annotation), so - // ignore them. - .filter { !it.safeSubtypeOf(realmObjectProxyInterface) } - // Unfortunately the RealmObject base class passes all above checks, so explicitly - // ignore it. - .filter { !it.name.equals("io.realm.RealmObject") } - } -} \ No newline at end of file diff --git a/realm-transformer/src/main/kotlin/io/realm/transformer/ext/ProjectExt.kt b/realm-transformer/src/main/kotlin/io/realm/transformer/ext/ProjectExt.kt index ee533aa6db..97c2b5a9ad 100644 --- a/realm-transformer/src/main/kotlin/io/realm/transformer/ext/ProjectExt.kt +++ b/realm-transformer/src/main/kotlin/io/realm/transformer/ext/ProjectExt.kt @@ -77,7 +77,7 @@ fun Project.getAgpVersion(): String { * Returns the `bootClasspath` for this project */ fun Project.getBootClasspath(): List { - return getAndroidExtension(this).bootClasspath ?: listOf() + return getAndroidExtension(this).bootClasspath } private fun getAndroidExtension(project: Project): BaseExtension { diff --git a/realm/build.gradle b/realm/build.gradle index bfa2df0e05..64e7fd591a 100644 --- a/realm/build.gradle +++ b/realm/build.gradle @@ -37,7 +37,7 @@ allprojects { project.ext.set(key, val) } project.ext.minSdkVersion = 16 - project.ext.compileSdkVersion = 29 + project.ext.compileSdkVersion = 30 project.ext.buildToolsVersion = projectDependencies.get("ANDROID_BUILD_TOOLS") group = 'io.realm' version = file("${rootDir}/../version.txt").text.trim() diff --git a/realm/gradle/wrapper/gradle-wrapper.properties b/realm/gradle/wrapper/gradle-wrapper.properties index 2e6e5897b5..26e62a13f1 100644 --- a/realm/gradle/wrapper/gradle-wrapper.properties +++ b/realm/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-rc-2-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/realm/kotlin-extensions/build.gradle b/realm/kotlin-extensions/build.gradle index 4c2a713d17..9c96c8db5f 100644 --- a/realm/kotlin-extensions/build.gradle +++ b/realm/kotlin-extensions/build.gradle @@ -73,8 +73,12 @@ android { // Required from Kotlin 1.1.2 compileOptions { - targetCompatibility 1.8 - sourceCompatibility 1.8 + targetCompatibility JavaVersion.VERSION_1_8 + sourceCompatibility JavaVersion.VERSION_1_8 + } + + kotlinOptions { + jvmTarget = JavaVersion.VERSION_1_8 } } @@ -171,4 +175,5 @@ artifacts { publishToMavenLocal.dependsOn assemble -android.registerTransform(new io.realm.transformer.RealmTransformer(project)) +io.realm.transformer.RealmTransformer.@Companion.register(project) + diff --git a/realm/realm-annotations-processor/build.gradle b/realm/realm-annotations-processor/build.gradle index 7d6973599c..7335fc39c6 100644 --- a/realm/realm-annotations-processor/build.gradle +++ b/realm/realm-annotations-processor/build.gradle @@ -1,8 +1,8 @@ apply plugin: 'kotlin' apply plugin: 'maven-publish' -sourceCompatibility = '1.8' -targetCompatibility = '1.8' +sourceCompatibility = JavaVersion.VERSION_1_8 +targetCompatibility = JavaVersion.VERSION_1_8 def properties = new Properties() properties.load(new FileInputStream("${projectDir}/../../dependencies.list")) @@ -65,13 +65,20 @@ java { compileKotlin { kotlinOptions { - jvmTarget = "1.8" + jvmTarget = JavaVersion.VERSION_1_8 freeCompilerArgs += ["-XXLanguage:+InlineClasses"] } } compileTestKotlin { kotlinOptions { - jvmTarget = "1.8" + jvmTarget = JavaVersion.VERSION_1_8 } } + +tasks.withType(JavaCompile) { + options.encoding = "UTF-8" +} +tasks.withType(Test) { + systemProperty "file.encoding", "UTF-8" +} \ No newline at end of file diff --git a/realm/realm-annotations-processor/src/test/java/io/realm/processor/NameConverterTests.java b/realm/realm-annotations-processor/src/test/java/io/realm/processor/NameConverterTests.java index 8465d0d28c..51b0c99ee9 100644 --- a/realm/realm-annotations-processor/src/test/java/io/realm/processor/NameConverterTests.java +++ b/realm/realm-annotations-processor/src/test/java/io/realm/processor/NameConverterTests.java @@ -1,5 +1,7 @@ package io.realm.processor; +import static org.junit.Assert.assertEquals; + import org.junit.Test; import java.util.LinkedHashMap; @@ -10,8 +12,6 @@ import io.realm.processor.nameconverter.NameConverter; import io.realm.processor.nameconverter.PascalCaseConverter; -import static org.junit.Assert.assertEquals; - public class NameConverterTests { @Test diff --git a/realm/realm-library/build.gradle b/realm/realm-library/build.gradle index bb3b5ef4cc..f5a1be13db 100644 --- a/realm/realm-library/build.gradle +++ b/realm/realm-library/build.gradle @@ -111,6 +111,10 @@ android { targetCompatibility JavaVersion.VERSION_1_8 } + kotlinOptions { + jvmTarget = JavaVersion.VERSION_1_8 + } + packagingOptions { exclude 'META-INF/NOTICE.txt' exclude 'META-INF/LICENSE.txt' @@ -149,18 +153,14 @@ android { } } -android.registerTransform(new io.realm.transformer.RealmTransformer(project)) -android.registerTransform(new io.realm.buildtransformer.RealmBuildTransformer( +io.realm.transformer.RealmTransformer.@Companion.register(project) +// TODO RealmBuildTransformer only supports stripping symbols from Jars so must be applied +// after the accessor transformer (that combines all inputs into one common Jar) +io.realm.buildtransformer.RealmBuildTransformer.@Companion.register( + project, "base", "io.realm.internal.annotations.ObjectServer", - [ - "io_realm_sync_permissions_ClassPermissionsRealmProxyInterface.class", - "io_realm_sync_permissions_PermissionRealmProxyInterface.class", - "io_realm_sync_permissions_PermissionUserRealmProxyInterface.class", - "io_realm_sync_permissions_RealmPermissionsRealmProxyInterface.class", - "io_realm_sync_permissions_RoleRealmProxyInterface.class" - ].toSet() -)) +) project.afterEvaluate { tasks.withType(JavaCompile) { @@ -239,17 +239,14 @@ task sourcesJar(type: Jar) { classifier = 'sources' } -def betaTag = 'Beta:a:
This software is considered in beta phase. ' + - 'It indicates that any public interface can change without prior announcements. ' + - 'Moreover, classes, constructors, and methods annotated as beta are not ' + - 'considered at production quality, and should be used with care.
' - task javadoc(type: Javadoc) { + failOnError false dependsOn("compileObjectServerReleaseJavaWithJavac") source android.sourceSets.objectServer.java.srcDirs source android.sourceSets.main.java.srcDirs source "../../realm-annotations/src/main/java" classpath += project.files(android.getBootClasspath().join(File.pathSeparator)) + options { title = "Realm ${project.version}" memberLevel = JavadocMemberLevel.PUBLIC @@ -263,8 +260,6 @@ task javadoc(type: Javadoc) { links "http://reactivex.io/RxJava/javadoc/" links "https://developer.android.com/reference" links "https://www.javadoc.io/doc/org.mongodb/bson/${properties.getProperty('BSON_DEPENDENCY')}/" - - tags = [betaTag] } exclude '**/internal/**' exclude '**/BuildConfig.java' @@ -408,12 +403,9 @@ project.afterEvaluate { // all Java files must be compiled before native build // See https://github.com/android/ndk-samples/issues/284 - android.libraryVariants.all { anotherVariant -> - if (variant.flavorName == anotherVariant.flavorName) { - variant.externalNativeBuildProviders[0].configure { - dependsOn "compile${anotherVariant.name.capitalize()}JavaWithJavac" - } - } + variant.externalNativeBuildProviders[0].configure { + it.taskDependencies.getDependencies(it).find { it.name.startsWith("buildCMake") } + .dependsOn "compile${variant.name.capitalize()}JavaWithJavac" } // as of android gradle plugin 3.0.0-alpha5, generateJsonModel* triggers native build. Java files must be compiled before them. android.buildTypes.all { buildType -> diff --git a/realm/realm-library/src/androidTest/AndroidManifest.xml b/realm/realm-library/src/androidTest/AndroidManifest.xml index eda3aabdc9..07f785081c 100644 --- a/realm/realm-library/src/androidTest/AndroidManifest.xml +++ b/realm/realm-library/src/androidTest/AndroidManifest.xml @@ -1,5 +1,5 @@ - @@ -20,7 +20,7 @@ diff --git a/realm/realm-library/src/androidTest/java/io/realm/RealmQueryTests.java b/realm/realm-library/src/androidTest/java/io/realm/RealmQueryTests.java index 350cd7257c..55ed6bafa2 100644 --- a/realm/realm-library/src/androidTest/java/io/realm/RealmQueryTests.java +++ b/realm/realm-library/src/androidTest/java/io/realm/RealmQueryTests.java @@ -51,6 +51,7 @@ import io.realm.entities.DictionaryAllTypes; import io.realm.entities.Dog; import io.realm.entities.IndexedFields; +import io.realm.entities.KeywordFieldNames; import io.realm.entities.NoPrimaryKeyNullTypes; import io.realm.entities.NonLatinFieldNames; import io.realm.entities.NullTypes; @@ -4082,6 +4083,18 @@ public void rawPredicate_invalidFormatOptions() { } } + @Test + public void rawPredicate_reservedKeywords() { + realm.beginTransaction(); + realm.insert(new KeywordFieldNames()); + realm.commitTransaction(); + + realm.where(KeywordFieldNames.class).rawPredicate("desc = $0", "value").findAll(); + realm.where(KeywordFieldNames.class).rawPredicate("limit = $0", "value").findAll(); + realm.where(KeywordFieldNames.class).rawPredicate("sort = $0", "value").findAll(); + realm.where(KeywordFieldNames.class).rawPredicate("distinct = $0", "value").findAll(); + } + @Test public void limit() { populateTestRealm(realm, TEST_DATA_SIZE); diff --git a/realm/realm-library/src/androidTestObjectServer/kotlin/io/realm/EmailPasswordAuthTests.kt b/realm/realm-library/src/androidTestObjectServer/kotlin/io/realm/EmailPasswordAuthTests.kt index 887e722b63..441bd3c8e6 100644 --- a/realm/realm-library/src/androidTestObjectServer/kotlin/io/realm/EmailPasswordAuthTests.kt +++ b/realm/realm-library/src/androidTestObjectServer/kotlin/io/realm/EmailPasswordAuthTests.kt @@ -32,16 +32,19 @@ import org.junit.Ignore import org.junit.Test import org.junit.runner.RunWith import kotlin.test.assertFailsWith +import io.realm.TEST_APP_1 +import io.realm.TEST_APP_2 +import io.realm.TEST_APP_3 +import kotlin.random.Random -@RunWith(AndroidJUnit4::class) -class EmailPasswordAuthTests { +abstract class EmailPasswordAuthTests { - private val looperThread = BlockingLooperThread() - private lateinit var app: TestApp - private lateinit var admin: ServerAdmin + protected val looperThread = BlockingLooperThread() + protected lateinit var app: TestApp + protected lateinit var admin: ServerAdmin // Callback use to verify that an Illegal Argument was thrown from async methods - private val checkNullArgCallback = App.Callback { result -> + protected val checkNullArgCallback = App.Callback { result -> if (result.isSuccess) { fail() } else { @@ -51,7 +54,7 @@ class EmailPasswordAuthTests { } // Methods exposed by the EmailPasswordAuthProvider - enum class Method { + protected enum class Method { REGISTER_USER, CONFIRM_USER, RESEND_CONFIRMATION_EMAIL, @@ -60,11 +63,15 @@ class EmailPasswordAuthTests { RETRY_CUSTOM_CONFIRMATION, RESET_PASSWORD } +} + +@RunWith(AndroidJUnit4::class) +class EmailPasswordAuthWithAutoConfirm: EmailPasswordAuthTests() { @Before fun setUp() { Realm.init(InstrumentationRegistry.getInstrumentation().targetContext) - app = TestApp() + app = TestApp(appName = TEST_APP_1) RealmLog.setLevel(LogLevel.DEBUG) admin = ServerAdmin(app) admin.deleteAllUsers() @@ -72,9 +79,7 @@ class EmailPasswordAuthTests { @After fun tearDown() { - if (this::app.isInitialized) { - app.close() - } + app.close() RealmLog.setLevel(LogLevel.WARN) } @@ -193,200 +198,6 @@ class EmailPasswordAuthTests { provider.confirmUserAsync("token", TestHelper.getNull(), checkNullArgCallback) } } - - @Test - fun resendConfirmationEmail() { - // We only test that the server successfully accepts the request. We have no way of knowing - // if the Email was actually sent. - // FIXME: Figure out a way to check if this actually happened. Perhaps a custom SMTP server? - val email = "test@10gen.com" - admin.setAutomaticConfirmation(false) - try { - val provider = app.emailPassword - provider.registerUser(email, "123456") - provider.resendConfirmationEmail(email) - } finally { - admin.setAutomaticConfirmation(true) - } - } - - @Test - fun resendConfirmationEmailAsync() { - // We only test that the server successfully accepts the request. We have no way of knowing - // if the Email was actually sent. - val email = "test@10gen.com" - admin.setAutomaticConfirmation(false) - try { - looperThread.runBlocking { - val provider = app.emailPassword - provider.registerUser(email, "123456") - provider.resendConfirmationEmailAsync(email) { result -> - when (result.isSuccess) { - true -> looperThread.testComplete() - false -> fail(result.error.toString()) - } - } - } - } finally { - admin.setAutomaticConfirmation(true) - } - } - - @Test - fun resendConfirmationEmail_invalidServerArgsThrows() { - val email = "test@10gen.com" - admin.setAutomaticConfirmation(false) - val provider = app.emailPassword - provider.registerUser(email, "123456") - try { - provider.resendConfirmationEmail("foo") - fail() - } catch (error: AppException) { - assertEquals(ErrorCode.USER_NOT_FOUND, error.errorCode) - } finally { - admin.setAutomaticConfirmation(true) - } - } - - @Test - fun resendConfirmationEmailAsync_invalidServerArgsThrows() { - val email = "test@10gen.com" - admin.setAutomaticConfirmation(false) - val provider = app.emailPassword - provider.registerUser(email, "123456") - try { - looperThread.runBlocking { - provider.resendConfirmationEmailAsync("foo") { result -> - if (result.isSuccess) { - fail() - } else { - assertEquals(ErrorCode.USER_NOT_FOUND, result.error.errorCode) - looperThread.testComplete() - } - } - } - } finally { - admin.setAutomaticConfirmation(true) - } - } - - @Test - fun resendConfirmationEmail_invalidArgumentsThrows() { - val provider: EmailPasswordAuth = app.emailPassword - assertFailsWith { provider.resendConfirmationEmail(TestHelper.getNull()) } - looperThread.runBlocking { - provider.resendConfirmationEmailAsync(TestHelper.getNull(), checkNullArgCallback) - } - } - - @Test - fun retryCustomConfirmation() { - val email = "test_realm_tests_do_autoverify@10gen.com" - admin.setAutomaticConfirmation(false) - try { - val provider = app.emailPassword - provider.registerUser(email, "123456") - admin.setCustomConfirmation(true) - - provider.retryCustomConfirmation(email) - } finally { - admin.setCustomConfirmation(false) - } - } - - @Test - fun retryCustomConfirmation_failConfirmation() { - // Only emails containing realm_tests_do_autoverify will be confirmed - val email = "test@10gen.com" - admin.setAutomaticConfirmation(false) - try { - val provider = app.emailPassword - provider.registerUser(email, "123456") - admin.setCustomConfirmation(true) - - val exception = assertFailsWith { - provider.retryCustomConfirmation(email) - } - - assertEquals("failed to confirm user test@10gen.com", exception.errorMessage) - - } finally { - admin.setCustomConfirmation(false) - } - } - - @Test - fun retryCustomConfirmationAsync() { - val email = "test_realm_tests_do_autoverify@10gen.com" - admin.setAutomaticConfirmation(false) - try { - looperThread.runBlocking { - val provider = app.emailPassword - provider.registerUser(email, "123456") - admin.setCustomConfirmation(true) - - provider.retryCustomConfirmationAsync(email) { result -> - when (result.isSuccess) { - true -> looperThread.testComplete() - false -> fail(result.error.toString()) - } - } - } - } finally { - admin.setCustomConfirmation(false) - } - } - - @Test - fun retryCustomConfirmation_invalidServerArgsThrows() { - val email = "test@10gen.com" - admin.setAutomaticConfirmation(false) - val provider = app.emailPassword - provider.registerUser(email, "123456") - admin.setCustomConfirmation(true) - - try { - provider.retryCustomConfirmation("foo") - fail() - } catch (error: AppException) { - assertEquals(ErrorCode.USER_NOT_FOUND, error.errorCode) - } finally { - admin.setCustomConfirmation(false) - } - } - - @Test - fun retryCustomConfirmationAsync_invalidServerArgsThrows() { - val email = "test@10gen.com" - admin.setAutomaticConfirmation(false) - val provider = app.emailPassword - provider.registerUser(email, "123456") - admin.setCustomConfirmation(true) - try { - looperThread.runBlocking { - provider.retryCustomConfirmationAsync("foo") { result -> - if (result.isSuccess) { - fail() - } else { - assertEquals(ErrorCode.USER_NOT_FOUND, result.error.errorCode) - looperThread.testComplete() - } - } - } - } finally { - admin.setCustomConfirmation(false) - } - } - - @Test - fun retryCustomConfirmation_invalidArgumentsThrows() { - val provider: EmailPasswordAuth = app.emailPassword - assertFailsWith { provider.retryCustomConfirmation(TestHelper.getNull()) } - looperThread.runBlocking { - provider.retryCustomConfirmationAsync(TestHelper.getNull(), checkNullArgCallback) - } - } - @Test fun sendResetPasswordEmail() { val provider = app.emailPassword @@ -470,8 +281,8 @@ class EmailPasswordAuthTests { try { looperThread.runBlocking { provider.callResetPasswordFunctionAsync(email, - "new-password", - arrayOf("say-the-magic-word", 42)) { result -> + "new-password", + arrayOf("say-the-magic-word", 42)) { result -> if (result.isSuccess) { val user = app.login(Credentials.emailPassword(email, "new-password")) user.logOut() @@ -510,9 +321,9 @@ class EmailPasswordAuthTests { try { looperThread.runBlocking { provider.callResetPasswordFunctionAsync( - email, - "new-password", - arrayOf("wrong-magic-word")) { result -> + email, + "new-password", + arrayOf("wrong-magic-word")) { result -> if (result.isSuccess) { fail() } else { @@ -639,3 +450,181 @@ class EmailPasswordAuthTests { } } +@RunWith(AndroidJUnit4::class) +class EmailPasswordAuthWithEmailConfirmTests: EmailPasswordAuthTests() { + @Before + fun setUp() { + Realm.init(InstrumentationRegistry.getInstrumentation().targetContext) + app = TestApp(appName = TEST_APP_2) + RealmLog.setLevel(LogLevel.DEBUG) + admin = ServerAdmin(app) + admin.deleteAllUsers() + } + + @After + fun tearDown() { + app.close() + RealmLog.setLevel(LogLevel.WARN) + } + + @Test + fun resendConfirmationEmail() { + // We only test that the server successfully accepts the request. We have no way of knowing + // if the Email was actually sent. + // TODO: Figure out a way to check if this actually happened. Perhaps a custom SMTP server? + val email = "test@10gen.com" + val provider = app.emailPassword + provider.registerUser(email, "123456") + provider.resendConfirmationEmail(email) + } + + @Test + fun resendConfirmationEmailAsync() { + // We only test that the server successfully accepts the request. We have no way of knowing + // if the Email was actually sent. + val email = "test@10gen.com" + looperThread.runBlocking { + val provider = app.emailPassword + provider.registerUser(email, "123456") + provider.resendConfirmationEmailAsync(email) { result -> + when (result.isSuccess) { + true -> looperThread.testComplete() + false -> fail(result.error.toString()) + } + } + } + } + + @Test + fun resendConfirmationEmail_invalidServerArgsThrows() { + val email = "test@10gen.com" + val provider = app.emailPassword + provider.registerUser(email, "123456") + try { + provider.resendConfirmationEmail("foo") + fail() + } catch (error: AppException) { + assertEquals(ErrorCode.USER_NOT_FOUND, error.errorCode) + } + } + + @Test + fun resendConfirmationEmailAsync_invalidServerArgsThrows() { + val email = "test@10gen.com" + val provider = app.emailPassword + provider.registerUser(email, "123456") + looperThread.runBlocking { + provider.resendConfirmationEmailAsync("foo") { result -> + if (result.isSuccess) { + fail() + } else { + assertEquals(ErrorCode.USER_NOT_FOUND, result.error.errorCode) + looperThread.testComplete() + } + } + } + } + + @Test + fun resendConfirmationEmail_invalidArgumentsThrows() { + val provider: EmailPasswordAuth = app.emailPassword + assertFailsWith { provider.resendConfirmationEmail(TestHelper.getNull()) } + looperThread.runBlocking { + provider.resendConfirmationEmailAsync(TestHelper.getNull(), checkNullArgCallback) + } + } +} + +@RunWith(AndroidJUnit4::class) +class EmailPasswordAuthWithCustomFunctionConfirmTests: EmailPasswordAuthTests() { + @Before + fun setUp() { + Realm.init(InstrumentationRegistry.getInstrumentation().targetContext) + app = TestApp(appName = TEST_APP_3) + RealmLog.setLevel(LogLevel.DEBUG) + admin = ServerAdmin(app) + admin.deleteAllUsers() + } + + @After + fun tearDown() { + app.close() + RealmLog.setLevel(LogLevel.WARN) + } + + @Test + fun retryCustomConfirmation() { + val email = "test_realm_pending_${Random.nextLong()}@10gen.com" + val provider = app.emailPassword + provider.registerUser(email, "123456") + provider.retryCustomConfirmation(email) + } + + @Test + fun retryCustomConfirmation_failConfirmation() { + // Only emails containing realm_tests_do_autoverify or @10gen.com will be confirmed + val email = "test_only_realm_pending_${Random.nextLong()}@mongodb.com" + val provider = app.emailPassword + provider.registerUser(email, "123456") + val exception = assertFailsWith { + provider.retryCustomConfirmation(email) + } + assertEquals("failed to confirm user $email", exception.errorMessage) + } + + @Test + fun retryCustomConfirmationAsync() { + // First call to register will move any email with `realm_pending` into `Pending`. Next + // call will register it. + val email = "test_realm_pending_${Random.nextLong()}@10gen.com" + looperThread.runBlocking { + val provider = app.emailPassword + provider.registerUser(email, "123456") + provider.retryCustomConfirmationAsync(email) { result -> + when (result.isSuccess) { + true -> looperThread.testComplete() + false -> fail(result.error.toString()) + } + } + } + } + + @Test + fun retryCustomConfirmation_invalidServerArgsThrows() { + val email = "test_${Random.nextLong()}@10gen.com" + val provider = app.emailPassword + provider.registerUser(email, "123456") + try { + provider.retryCustomConfirmation("foo") + fail() + } catch (error: AppException) { + assertEquals(ErrorCode.USER_NOT_FOUND, error.errorCode) + } + } + + @Test + fun retryCustomConfirmationAsync_invalidServerArgsThrows() { + val email = "test_${Random.nextLong()}@10gen.com" + val provider = app.emailPassword + provider.registerUser(email, "123456") + looperThread.runBlocking { + provider.retryCustomConfirmationAsync("foo") { result -> + if (result.isSuccess) { + fail() + } else { + assertEquals(ErrorCode.USER_NOT_FOUND, result.error.errorCode) + looperThread.testComplete() + } + } + } + } + + @Test + fun retryCustomConfirmation_invalidArgumentsThrows() { + val provider: EmailPasswordAuth = app.emailPassword + assertFailsWith { provider.retryCustomConfirmation(TestHelper.getNull()) } + looperThread.runBlocking { + provider.retryCustomConfirmationAsync(TestHelper.getNull(), checkNullArgCallback) + } + } +} diff --git a/realm/realm-library/src/androidTestObjectServer/kotlin/io/realm/admin/ServerAdmin.kt b/realm/realm-library/src/androidTestObjectServer/kotlin/io/realm/admin/ServerAdmin.kt index cf98a2c2d1..638597ff7b 100644 --- a/realm/realm-library/src/androidTestObjectServer/kotlin/io/realm/admin/ServerAdmin.kt +++ b/realm/realm-library/src/androidTestObjectServer/kotlin/io/realm/admin/ServerAdmin.kt @@ -97,81 +97,6 @@ class ServerAdmin(private val app: App) { throw IllegalArgumentException("Could not find app: $") } - /** - * Toggle whether or not automatic confirmation of new users are enabled. - */ - fun setAutomaticConfirmation(enabled: Boolean) { - val providerId: String = getLocalUserPassProviderId() - val url = "$baseUrl/groups/$groupId/apps/$appId/auth_providers/$providerId" - var request = Request.Builder() - .url(url) - .get() - val authProviderConfig = JSONObject(executeRequest(request, true)) - authProviderConfig.getJSONObject("config").apply { - put("autoConfirm", enabled) - } - // Change autoConfirm and update the provider - request = Request.Builder() - .url(url) - .patch(RequestBody.create(json, authProviderConfig.toString())) - executeRequest(request, true) - - request = Request.Builder() - .url(url) - .get() - val config = JSONObject(executeRequest(request, true)) - RealmLog.error("SetAutomaticConfirmation($enabled): ${config.toString(4)}") - waitForDeployment() - } - - private fun waitForDeployment() { - // TODO Attempt to work-around, what looks like a race condition on the server deploying - // changes to the server. Even though the /deployments endpoint report success, it seems - // like the change hasn't propagated fully. This usually surfaces as registerUser errors - // where it tries to use the customFunc instead of automatically registering. - val url = "$baseUrl/groups/$groupId/apps/$appId/deployments" - var request = Request.Builder() - .url(url) - .get() - val deployments = JSONArray(executeRequest(request, true)) - val dep = deployments[0] as JSONObject - if (dep.getString("status") != "successful") { - RealmLog.error("Failed to deploy: ${dep.toString(4)}") - } - - // Work-around for /deployments reporting success, but /register still failing. - SystemClock.sleep(5000) - } - - /** - * Toggle whether or not custom confirmation functions are enabled. - */ - fun setCustomConfirmation(enabled: Boolean) { - val providerId: String = getLocalUserPassProviderId() - val url = "$baseUrl/groups/$groupId/apps/$appId/auth_providers/$providerId" - var request = Request.Builder() - .url(url) - .get() - val authProviderConfig = JSONObject(executeRequest(request, true)) - - authProviderConfig.getJSONObject("config").apply { - put("autoConfirm", !enabled) - put("runConfirmationFunction", enabled) - } - // Change autoConfirm and update the provider - request = Request.Builder() - .url(url) - .patch(RequestBody.create(json, authProviderConfig.toString())) - executeRequest(request, true) - - request = Request.Builder() - .url(url) - .get() - val config = JSONObject(executeRequest(request, true)) - RealmLog.error("setCustomConfirmation($enabled): ${config.toString(4)}") - waitForDeployment() - } - fun enableFlexibleSync() { var request = Request.Builder() .url("$baseUrl/groups/$groupId/apps/$appId/services") @@ -212,8 +137,6 @@ class ServerAdmin(private val app: App) { } } - val JSON = MediaType.parse("application/json; charset=utf-8") - fun disableUser(user: User) { var request = Request.Builder() .url("$baseUrl/groups/$groupId/apps/$appId/users/${user.id}/disable") diff --git a/realm/realm-library/src/androidTestObjectServer/kotlin/io/realm/mongodb/sync/SyncedRealmTests.kt b/realm/realm-library/src/androidTestObjectServer/kotlin/io/realm/mongodb/sync/SyncedRealmTests.kt index c085e34731..65c3469fd5 100644 --- a/realm/realm-library/src/androidTestObjectServer/kotlin/io/realm/mongodb/sync/SyncedRealmTests.kt +++ b/realm/realm-library/src/androidTestObjectServer/kotlin/io/realm/mongodb/sync/SyncedRealmTests.kt @@ -462,14 +462,14 @@ class SyncedRealmTests { Assert.assertEquals(2, realm.where().count()) val nodeResults = realm.where().findAll() - Assert.assertTrue(nodeResults.any { it.treeNodeId == "node1" }) - Assert.assertTrue(nodeResults.any { it.treeNodeId == "node2" }) + assertTrue(nodeResults.any { it.treeNodeId == "node1" }) + assertTrue(nodeResults.any { it.treeNodeId == "node2" }) Assert.assertEquals(3, realm.where().count()) val leafResults = realm.where().findAll() - Assert.assertTrue(leafResults.any { it.treeLeafId == "leaf1" }) - Assert.assertTrue(leafResults.any { it.treeLeafId == "leaf2" }) - Assert.assertTrue(leafResults.any { it.treeLeafId == "leaf3" }) + assertTrue(leafResults.any { it.treeLeafId == "leaf1" }) + assertTrue(leafResults.any { it.treeLeafId == "leaf2" }) + assertTrue(leafResults.any { it.treeLeafId == "leaf3" }) } } @@ -496,10 +496,10 @@ class SyncedRealmTests { // Make sure we can synchronize changes realm1 = Realm.getInstance(config1) realm2 = Realm.getInstance(config2) - realm1.syncSession.downloadAllServerChanges() - realm2.syncSession.downloadAllServerChanges() - Assert.assertTrue(realm1.isEmpty) - Assert.assertTrue(realm2.isEmpty) + realm1.syncSession.downloadAllServerChanges(1, TimeUnit.MINUTES) + realm2.syncSession.downloadAllServerChanges(1, TimeUnit.MINUTES) + assertTrue(realm1.isEmpty) + assertTrue(realm2.isEmpty) } finally { realm1?.close() realm2?.close() diff --git a/realm/realm-library/src/main/cpp/io_realm_internal_OsList.cpp b/realm/realm-library/src/main/cpp/io_realm_internal_OsList.cpp index 568c9aae6c..065ca6d863 100644 --- a/realm/realm-library/src/main/cpp/io_realm_internal_OsList.cpp +++ b/realm/realm-library/src/main/cpp/io_realm_internal_OsList.cpp @@ -605,7 +605,7 @@ JNIEXPORT jobject JNICALL Java_io_realm_internal_OsList_nativeGetValue(JNIEnv* e try { auto& wrapper = *reinterpret_cast(list_ptr); JavaAccessorContext context(env); - return any_cast(wrapper.collection().get(context, pos)); + return util::any_cast(wrapper.collection().get(context, pos)); } CATCH_STD() diff --git a/realm/realm-library/src/main/cpp/io_realm_internal_OsResults.cpp b/realm/realm-library/src/main/cpp/io_realm_internal_OsResults.cpp index 0ae7d8463e..a3b03bfefa 100644 --- a/realm/realm-library/src/main/cpp/io_realm_internal_OsResults.cpp +++ b/realm/realm-library/src/main/cpp/io_realm_internal_OsResults.cpp @@ -536,7 +536,7 @@ Java_io_realm_internal_OsResults_nativeGetValue(JNIEnv* env, jclass, jlong nativ try { auto& wrapper = *reinterpret_cast(native_ptr); JavaAccessorContext context(env); - return any_cast(wrapper.collection().get(context, pos)); + return util::any_cast(wrapper.collection().get(context, pos)); } CATCH_STD() diff --git a/realm/realm-library/src/main/cpp/io_realm_mongodb_sync_Sync.cpp b/realm/realm-library/src/main/cpp/io_realm_mongodb_sync_Sync.cpp index d8b574741c..0b7ea861f0 100644 --- a/realm/realm-library/src/main/cpp/io_realm_mongodb_sync_Sync.cpp +++ b/realm/realm-library/src/main/cpp/io_realm_mongodb_sync_Sync.cpp @@ -66,7 +66,15 @@ JNIEXPORT void JNICALL Java_io_realm_mongodb_sync_Sync_nativeSimulateSyncError(J type == "realm::sync::ProtocolError" ? realm::sync::protocol_error_category() : realm::sync::client_error_category() }; - SyncSession::OnlyForTesting::handle_error(*session, {code, std::string(message), to_bool(is_fatal)}); + + SyncError sync_error( + code, + std::string(message), + to_bool(is_fatal) + ); + sync_error.server_requests_action = sync::ProtocolErrorInfo::Action::ClientReset; + + SyncSession::OnlyForTesting::handle_error(*session, sync_error); } CATCH_STD() } diff --git a/realm/realm-library/src/main/cpp/observable_collection_wrapper.hpp b/realm/realm-library/src/main/cpp/observable_collection_wrapper.hpp index b73d7ccaa2..0d3a1620b7 100644 --- a/realm/realm-library/src/main/cpp/observable_collection_wrapper.hpp +++ b/realm/realm-library/src/main/cpp/observable_collection_wrapper.hpp @@ -86,21 +86,11 @@ void ObservableCollectionWrapper::start_listening(JNIEnv* env, jobject j_coll m_collection_weak_ref = jni_util::JavaGlobalWeakRef(env, j_collection_object); } - auto cb = [=](CollectionChangeSet const& changes, std::exception_ptr err) { + auto cb = [=](CollectionChangeSet const& changes) { // OS will call all notifiers' callback in one run, so check the Java exception first!! if (env->ExceptionCheck()) return; - if (err) { - try { - std::rethrow_exception(err); - } - catch (const std::exception& e) { - realm::jni_util::Log::e("Caught exception in collection change callback %1", e.what()); - return; - } - } - m_collection_weak_ref.call_with_local_ref(env, [&](JNIEnv* local_env, jobject collection_obj) { local_env->CallVoidMethod( collection_obj, notify_change_listeners, diff --git a/realm/realm-library/src/main/cpp/observable_dictionary_wrapper.hpp b/realm/realm-library/src/main/cpp/observable_dictionary_wrapper.hpp index 16704fa2f3..23430fd125 100644 --- a/realm/realm-library/src/main/cpp/observable_dictionary_wrapper.hpp +++ b/realm/realm-library/src/main/cpp/observable_dictionary_wrapper.hpp @@ -70,21 +70,11 @@ void ObservableDictionaryWrapper::start_listening(JNIEnv* env, jobject j_observa m_collection_weak_ref = jni_util::JavaGlobalWeakRef(env, j_observable_map); } - auto cb = [=](DictionaryChangeSet changes, std::exception_ptr err) { + auto cb = [=](DictionaryChangeSet changes) { // OS will call all notifiers' callback in one run, so check the Java exception first!! if (env->ExceptionCheck()) return; - if (err) { - try { - std::rethrow_exception(err); - } - catch (const std::exception& e) { - realm::jni_util::Log::e("Caught exception in dictionary change callback %1", e.what()); - return; - } - } - m_collection_weak_ref.call_with_local_ref(env, [&](JNIEnv* local_env, jobject collection_obj) { bool changes_empty = changes.deletions.empty() && changes.insertions.empty() && diff --git a/realm/realm-library/src/main/cpp/realm-core b/realm/realm-library/src/main/cpp/realm-core index 55a48c287b..5da7744b40 160000 --- a/realm/realm-library/src/main/cpp/realm-core +++ b/realm/realm-library/src/main/cpp/realm-core @@ -1 +1 @@ -Subproject commit 55a48c287b5e3a8ca129c257ec7e3b92bcb2a05f +Subproject commit 5da7744b4056ad185c025bccf0924f17f73f7a91 diff --git a/realm/realm-library/src/main/java/io/realm/internal/UnmanagedSubscription.java b/realm/realm-library/src/main/java/io/realm/internal/UnmanagedSubscription.java index 3f4cee09c9..c2b8848aa0 100644 --- a/realm/realm-library/src/main/java/io/realm/internal/UnmanagedSubscription.java +++ b/realm/realm-library/src/main/java/io/realm/internal/UnmanagedSubscription.java @@ -20,11 +20,13 @@ import javax.annotation.Nullable; import io.realm.RealmQuery; +import io.realm.internal.annotations.ObjectServer; import io.realm.mongodb.sync.Subscription; /** * Class that handles unmanaged subscriptions. Required as we need to track a realm query ptr. */ +@ObjectServer public class UnmanagedSubscription implements Subscription { private final Date createdAt; diff --git a/realm/realm-library/src/main/java/io/realm/internal/objectstore/OsMutableSubscriptionSet.java b/realm/realm-library/src/main/java/io/realm/internal/objectstore/OsMutableSubscriptionSet.java index 2332c6c36c..e798c1a9bf 100644 --- a/realm/realm-library/src/main/java/io/realm/internal/objectstore/OsMutableSubscriptionSet.java +++ b/realm/realm-library/src/main/java/io/realm/internal/objectstore/OsMutableSubscriptionSet.java @@ -18,12 +18,12 @@ import io.realm.RealmModel; import io.realm.internal.RealmProxyMediator; import io.realm.internal.UnmanagedSubscription; +import io.realm.internal.annotations.ObjectServer; import io.realm.internal.async.RealmThreadPoolExecutor; import io.realm.mongodb.sync.MutableSubscriptionSet; import io.realm.mongodb.sync.Subscription; -// TODO Adding @ObjectServer here seems to break the Realm Build Transformer. Investigate why. -//@ObjectServer +@ObjectServer public class OsMutableSubscriptionSet extends OsSubscriptionSet implements MutableSubscriptionSet { public OsMutableSubscriptionSet(long nativePtr, diff --git a/realm/realm-library/src/main/java/io/realm/internal/objectstore/OsSubscription.java b/realm/realm-library/src/main/java/io/realm/internal/objectstore/OsSubscription.java index 67bce6ff58..e11b939d4f 100644 --- a/realm/realm-library/src/main/java/io/realm/internal/objectstore/OsSubscription.java +++ b/realm/realm-library/src/main/java/io/realm/internal/objectstore/OsSubscription.java @@ -21,8 +21,7 @@ import io.realm.internal.annotations.ObjectServer; import io.realm.mongodb.sync.Subscription; -// TODO Adding @ObjectServer here seems to break the Realm Build Transformer. Investigate why. -//@ObjectServer +@ObjectServer public class OsSubscription implements NativeObject, Subscription { private static final long nativeFinalizerPtr = nativeGetFinalizerMethodPtr(); diff --git a/realm/realm-library/src/main/java/io/realm/internal/objectstore/OsSubscriptionSet.java b/realm/realm-library/src/main/java/io/realm/internal/objectstore/OsSubscriptionSet.java index ad4561b45f..5d19858818 100644 --- a/realm/realm-library/src/main/java/io/realm/internal/objectstore/OsSubscriptionSet.java +++ b/realm/realm-library/src/main/java/io/realm/internal/objectstore/OsSubscriptionSet.java @@ -31,13 +31,13 @@ import io.realm.RealmQuery; import io.realm.internal.NativeObject; import io.realm.internal.RealmProxyMediator; +import io.realm.internal.annotations.ObjectServer; import io.realm.internal.async.RealmAsyncTaskImpl; import io.realm.internal.async.RealmThreadPoolExecutor; import io.realm.mongodb.sync.Subscription; import io.realm.mongodb.sync.SubscriptionSet; -// TODO Adding @ObjectServer here seems to break the Realm Build Transformer. Investigate why. -//@ObjectServer +@ObjectServer public class OsSubscriptionSet implements NativeObject, SubscriptionSet { public static final byte STATE_VALUE_UNCOMMITTED = 0; diff --git a/realm/realm-library/src/main/java/io/realm/mongodb/sync/SubscriptionSet.java b/realm/realm-library/src/main/java/io/realm/mongodb/sync/SubscriptionSet.java index ba7266190a..6930206661 100644 --- a/realm/realm-library/src/main/java/io/realm/mongodb/sync/SubscriptionSet.java +++ b/realm/realm-library/src/main/java/io/realm/mongodb/sync/SubscriptionSet.java @@ -36,6 +36,7 @@ */ @Beta @Keep +@ObjectServer public interface SubscriptionSet extends Iterable { /** diff --git a/realm/realm-library/src/objectServer/java/io/realm/mongodb/ErrorCode.java b/realm/realm-library/src/objectServer/java/io/realm/mongodb/ErrorCode.java index f848addea0..02bf8275ae 100644 --- a/realm/realm-library/src/objectServer/java/io/realm/mongodb/ErrorCode.java +++ b/realm/realm-library/src/objectServer/java/io/realm/mongodb/ErrorCode.java @@ -110,6 +110,7 @@ public enum ErrorCode { SERVER_PERMISSIONS_CHANGED(Type.PROTOCOL, 228), // Server permissions for this file ident have changed since the last time it was used (IDENT) INITIAL_SYNC_NOT_COMPLETE(Type.PROTOCOL, 229), // Client tried to open a session before initial sync is complete (BIND) WRITE_NOT_ALLOWED(Type.PROTOCOL, 230), // Client attempted a write that is disallowed by permissions, or modifies an object outside the current query - requires client reset (UPLOAD) + COMPENSATING_WRITE(Type.PROTOCOL, 231), // Client attempted a write that is disallowed by permissions, or modifies an object outside the current query, and the server undid the change // Sync Network Client errors. // See https://github.com/realm/realm-core/blob/master/src/realm/sync/client_base.hpp#L73 diff --git a/realm/realm-library/src/syncIntegrationTest/kotlin/io/realm/SyncedRealmIntegrationTests.kt b/realm/realm-library/src/syncIntegrationTest/kotlin/io/realm/SyncedRealmIntegrationTests.kt index 83c959d6e8..fbb11dec96 100644 --- a/realm/realm-library/src/syncIntegrationTest/kotlin/io/realm/SyncedRealmIntegrationTests.kt +++ b/realm/realm-library/src/syncIntegrationTest/kotlin/io/realm/SyncedRealmIntegrationTests.kt @@ -18,6 +18,7 @@ package io.realm import android.os.SystemClock import androidx.test.annotation.UiThreadTest import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.FlakyTest import androidx.test.platform.app.InstrumentationRegistry import io.realm.entities.DefaultSyncSchema import io.realm.entities.StringOnly @@ -184,7 +185,7 @@ class SyncedRealmIntegrationTests { .build() Realm.getInstance(configOld).use { realm -> // Create many changesets to make sure that download is "slow" - for (i in 0..999) { + for (i in 0..99) { realm.executeTransaction { realm -> realm.createObject(SyncStringOnly::class.java, ObjectId()).chars = "Foo$i" } @@ -206,7 +207,7 @@ class SyncedRealmIntegrationTests { Realm.getInstanceAsync(config, object: Realm.Callback() { override fun onSuccess(realm: Realm) { looperThread.closeAfterTest(realm) - assertEquals(1000, realm.where(SyncStringOnly::class.java).count()) + assertEquals(100, realm.where(SyncStringOnly::class.java).count()) looperThread.testComplete() } @@ -218,6 +219,7 @@ class SyncedRealmIntegrationTests { // Try an scenario where a Sync and Async race to wait for the initial remote data. @Test + @FlakyTest(detail = "Depend on the server being able to integrate changes into MongoDB fast enough before the new client downloads data.") fun waitForInitialRemoteData_getInstance_race_AsyncAndSync() = looperThread.runBlocking { // 1. Copy a valid Realm to the server (and pray it does it within 10 seconds) val configOld: SyncConfiguration = configurationFactory.createSyncConfigurationBuilder(user, user.id) @@ -226,7 +228,7 @@ class SyncedRealmIntegrationTests { .build() Realm.getInstance(configOld).use { realm -> // Create many changesets to make sure that download is "slow" - for (i in 0..999) { + for (i in 0..99) { realm.executeTransaction { realm -> realm.createObject(SyncStringOnly::class.java, ObjectId()).chars = "Foo$i" } @@ -250,7 +252,7 @@ class SyncedRealmIntegrationTests { Realm.getInstanceAsync(config, object: Realm.Callback() { override fun onSuccess(realm: Realm) { looperThread.closeAfterTest(realm) - assertEquals(1000, realm.where(SyncStringOnly::class.java).count()) + assertEquals(100, realm.where(SyncStringOnly::class.java).count()) looperThread.testComplete() } @@ -259,7 +261,7 @@ class SyncedRealmIntegrationTests { } }) Realm.getInstance(config).use { realm -> - assertEquals(1000, realm.where(SyncStringOnly::class.java).count()) + assertEquals(100, realm.where(SyncStringOnly::class.java).count()) } } diff --git a/realm/realm-library/src/testUtils/java/io/realm/TestHelper.java b/realm/realm-library/src/testUtils/java/io/realm/TestHelper.java index 88cf50ee4e..f4ea664931 100644 --- a/realm/realm-library/src/testUtils/java/io/realm/TestHelper.java +++ b/realm/realm-library/src/testUtils/java/io/realm/TestHelper.java @@ -140,6 +140,7 @@ public static RealmFieldType getColumnType(Object o) { * with primary key defined well. Primary key has to be set with `setXxxUnique` as the first thing to do after row * added. */ + @Deprecated public static long addRowWithValues(Table table, long[] columnKeys, Object[] values) { long rowKey = OsObject.createRow(table); @@ -357,7 +358,7 @@ public static int getRandomId() { public static String getRandomEmail() { StringBuilder sb = new StringBuilder(UUID.randomUUID().toString().toLowerCase()); sb.append('@'); - sb.append("androidtest.realm.io"); + sb.append("10gen.com"); return sb.toString(); } diff --git a/realm/realm-library/src/testUtils/java/io/realm/entities/KeywordFieldNames.java b/realm/realm-library/src/testUtils/java/io/realm/entities/KeywordFieldNames.java new file mode 100644 index 0000000000..b32a7fdedc --- /dev/null +++ b/realm/realm-library/src/testUtils/java/io/realm/entities/KeywordFieldNames.java @@ -0,0 +1,10 @@ +package io.realm.entities; + +import io.realm.RealmObject; + +public class KeywordFieldNames extends RealmObject { + public String desc; + public String sort; + public String distinct; + public String limit; +} \ No newline at end of file diff --git a/realm/realm-library/src/testUtils/java/io/realm/services/RemoteProcessService.java b/realm/realm-library/src/testUtils/java/io/realm/services/RemoteProcessService.java index a2441c4ef5..bbf99aaf12 100644 --- a/realm/realm-library/src/testUtils/java/io/realm/services/RemoteProcessService.java +++ b/realm/realm-library/src/testUtils/java/io/realm/services/RemoteProcessService.java @@ -38,6 +38,7 @@ * Helper service for multi-processes support testing. * @deprecated use {@link RemoteTestService} instead. */ +@Deprecated public class RemoteProcessService extends Service { public abstract static class Step { diff --git a/tools/sync_test_server/app_config_generator.sh b/tools/sync_test_server/app_config_generator.sh index 886b34aa3e..0e52051435 100755 --- a/tools/sync_test_server/app_config_generator.sh +++ b/tools/sync_test_server/app_config_generator.sh @@ -2,6 +2,7 @@ TARGET_APP_PATH=$1;shift TEMPLATE_APP_PATH=$1;shift SYNC_MODE=$1;shift # Must be either "partition" or "flex" +AUTH_MODE=$1;shift # Must be either "auto", "function" or "email" mkdir -p $TARGET_APP_PATH for APP_NAME in "$@" do @@ -9,10 +10,62 @@ do sed -i'.bak' 's/APP_NAME_PLACEHOLDER/'$APP_NAME'/g' $TARGET_APP_PATH/$APP_NAME/config.json done +# Setup auth config +for APP_NAME in "$@" +do + JSON="placeholder" + if [ "$AUTH_MODE" = "auto" ]; then + JSON=' + "config": { + "autoConfirm": true, + "runConfirmationFunction": false, + "confirmationFunctionName": "confirmFunc", + "emailConfirmationUrl": "http://realm.io/confirm-user", + "resetFunctionName": "resetFunc", + "resetPasswordSubject": "Reset Password", + "resetPasswordUrl": "http://realm.io/reset-password", + "runResetFunction": false + }, + ' + fi + if [ "$AUTH_MODE" = "function" ]; then + JSON=' + "config": { + "autoConfirm": false, + "runConfirmationFunction": true, + "confirmationFunctionName": "confirmFunc", + "emailConfirmationUrl": "http://realm.io/confirm-user", + "resetFunctionName": "resetFunc", + "resetPasswordSubject": "Reset Password", + "resetPasswordUrl": "http://realm.io/reset-password", + "runResetFunction": false + }, + ' + fi + if [ "$AUTH_MODE" = "email" ]; then + JSON=' + "config": { + "autoConfirm": false, + "runConfirmationFunction": false, + "confirmationFunctionName": "confirmFunc", + "emailConfirmationUrl": "http://realm.io/confirm-user", + "resetFunctionName": "resetFunc", + "resetPasswordSubject": "Reset Password", + "resetPasswordUrl": "http://realm.io/reset-password", + "runResetFunction": false + }, + ' + fi + + ESCAPED_JSON=`echo ${JSON} | tr '\n' "\\n"` + cp -r $TEMPLATE_APP_PATH $TARGET_APP_PATH/$APP_NAME + sed -i'.bak' "s#%EMAIL_AUTH_CONFIG%#$ESCAPED_JSON#g" $TARGET_APP_PATH/$APP_NAME/auth_providers/local-userpass.json +done + # Setup sync configuration for APP_NAME in "$@" do - JSON="boo" + JSON="placeholder" if [ "$SYNC_MODE" = "partition" ]; then JSON=' "sync": { @@ -22,8 +75,26 @@ do "key": "realm_id", "type": "string", "permissions": { - "read": true, - "write": true + "read": { + "%%true": { + "%function": { + "arguments": [ + "%%partition" + ], + "name": "canReadPartition" + } + } + }, + "write": { + "%%true": { + "%function": { + "arguments": [ + "%%partition" + ], + "name": "canWritePartition" + } + } + } } } } @@ -39,7 +110,18 @@ do "name", "color", "section" - ] + ], + "permissions": { + "rules": {}, + "defaultRoles": [ + { + "name": "read-write", + "applyWhen": {}, + "read": true, + "write": true + } + ] + } } ' fi diff --git a/tools/sync_test_server/app_template/auth_providers/local-userpass.json b/tools/sync_test_server/app_template/auth_providers/local-userpass.json index 76be7a9a19..69e8697893 100644 --- a/tools/sync_test_server/app_template/auth_providers/local-userpass.json +++ b/tools/sync_test_server/app_template/auth_providers/local-userpass.json @@ -2,15 +2,6 @@ "id": "60489e7df5d9bdc94de663db", "name": "local-userpass", "type": "local-userpass", - "config": { - "autoConfirm": true, - "confirmationFunctionName": "confirmFunc", - "emailConfirmationUrl": "http://realm.io/confirm-user", - "resetFunctionName": "resetFunc", - "resetPasswordSubject": "Reset Password", - "resetPasswordUrl": "http://realm.io/reset-password", - "runConfirmationFunction": false, - "runResetFunction": false - }, + %EMAIL_AUTH_CONFIG% "disabled": false } diff --git a/tools/sync_test_server/app_template/functions/canReadPartition/config.json b/tools/sync_test_server/app_template/functions/canReadPartition/config.json new file mode 100644 index 0000000000..452fce7a18 --- /dev/null +++ b/tools/sync_test_server/app_template/functions/canReadPartition/config.json @@ -0,0 +1,6 @@ +{ + "can_evaluate": {}, + "id": "60489e7df5d9bdc94de663da", + "name": "canReadPartition", + "private": false +} diff --git a/tools/sync_test_server/app_template/functions/canReadPartition/source.js b/tools/sync_test_server/app_template/functions/canReadPartition/source.js new file mode 100644 index 0000000000..3cbf1868da --- /dev/null +++ b/tools/sync_test_server/app_template/functions/canReadPartition/source.js @@ -0,0 +1,12 @@ +/** + * Users with an email that contains `_noread_` do not have read access, + * all others do. + */ +exports = async (partition) => { + const email = context.user.data.email; + if (email != undefined) { + return(!email.includes("_noread_")); + } else { + return true; + } +} diff --git a/tools/sync_test_server/app_template/functions/canWritePartition/config.json b/tools/sync_test_server/app_template/functions/canWritePartition/config.json new file mode 100644 index 0000000000..f96e8f824a --- /dev/null +++ b/tools/sync_test_server/app_template/functions/canWritePartition/config.json @@ -0,0 +1,6 @@ +{ + "can_evaluate": {}, + "id": "60489e7df5d9bdc94de783cf", + "name": "canWritePartition", + "private": false +} diff --git a/tools/sync_test_server/app_template/functions/canWritePartition/source.js b/tools/sync_test_server/app_template/functions/canWritePartition/source.js new file mode 100644 index 0000000000..4b0effd12d --- /dev/null +++ b/tools/sync_test_server/app_template/functions/canWritePartition/source.js @@ -0,0 +1,12 @@ +/** + * Users with an email that contains `_nowrite_` do not have write access, + * all others do. + */ +exports = async (partition) => { + const email = context.user.data.email; + if (email != undefined) { + return(!email.includes("_nowrite_")); + } else { + return true; + } +} diff --git a/tools/sync_test_server/app_template/functions/confirmFunc/source.js b/tools/sync_test_server/app_template/functions/confirmFunc/source.js index 6a3d024046..40d11dea9b 100644 --- a/tools/sync_test_server/app_template/functions/confirmFunc/source.js +++ b/tools/sync_test_server/app_template/functions/confirmFunc/source.js @@ -1,4 +1,3 @@ - /* This function will be run AFTER a user registers their username and password and is called with an object parameter @@ -38,12 +37,37 @@ The uncommented function below is just a placeholder and will result in failure. */ - - exports = ({ token, tokenId, username }) => { +exports = async ({ token, tokenId, username }) => { // process the confirm token, tokenId and username - if (username.includes("realm_tests_do_autoverify")) { + + if (username.includes("realm_verify")) { + // Automatically confirm users with `realm_verify` in their email. return { status: 'success' } + } else if (username.includes("realm_pending")) { + // This supports two versions of custom registering: + // + // 1. Emails with `realm_pending` in their email will be placed in Pending + // the first time they register and then fully confirmed when they + // retry the confirmation logic. + // 2. Emails with `only_realm_pending` in their email will be placed in + // Pending the first time they register and fail all subsequent attempts + // at retrying the confirmation logic. + const mdb = context.services.get("BackingDB"); + const collection = mdb.db("custom-auth").collection("users"); + const existing = await collection.findOne({ username: username }); + if (existing) { + if (username.includes("only_realm_pending")) { + return { status: 'fail' } + } else { + return { status: 'success' }; + } + } + await collection.insertOne({ username: username }); + return { status: 'pending' } + } else if (username.endsWith("@10gen.com") || username.includes("realm_tests_do_autoverify")) { + return { status: 'success' } + } else { + // All other emails should fail to confirm outright. + return { status: 'fail' }; } - // do not confirm the user - return { status: 'fail' }; }; diff --git a/tools/sync_test_server/app_template/functions/testAuthFunc/source.js b/tools/sync_test_server/app_template/functions/testAuthFunc/source.js index 4c1e87f25a..72700e13ac 100644 --- a/tools/sync_test_server/app_template/functions/testAuthFunc/source.js +++ b/tools/sync_test_server/app_template/functions/testAuthFunc/source.js @@ -1,7 +1,7 @@ exports = ({mail, id}) => { - // Auth function will fail for emails with a domain different to @androidtest.realm.io + // Auth function will fail for emails with a domain different to @10gen.com // or with id lower than 666 - if (!new RegExp("@androidtest.realm.io$").test(mail) || id < 666) { + if (!new RegExp("@10gen.com$").test(mail) || id < 666) { return 0; } else { // Use the users email as UID diff --git a/tools/sync_test_server/start_local_server.sh b/tools/sync_test_server/start_local_server.sh index 5a8127bc6c..f50099d090 100755 --- a/tools/sync_test_server/start_local_server.sh +++ b/tools/sync_test_server/start_local_server.sh @@ -126,8 +126,9 @@ function boot_command_server () { function generate_app_configs () { APP_CONFIG_DIR=`mktemp -d -t app_config` - $SCRIPTPATH/app_config_generator.sh $APP_CONFIG_DIR $SCRIPTPATH/app_template partition testapp1 testapp2 - $SCRIPTPATH/app_config_generator.sh $APP_CONFIG_DIR $SCRIPTPATH/app_template flex testapp3 + $SCRIPTPATH/app_config_generator.sh $APP_CONFIG_DIR $SCRIPTPATH/app_template partition auto testapp1 + $SCRIPTPATH/app_config_generator.sh $APP_CONFIG_DIR $SCRIPTPATH/app_template partition email testapp2 + $SCRIPTPATH/app_config_generator.sh $APP_CONFIG_DIR $SCRIPTPATH/app_template flex function testapp3 } function import_apps () { diff --git a/tools/sync_test_server/start_server.sh b/tools/sync_test_server/start_server.sh index 14ce0e57ea..3347685516 100755 --- a/tools/sync_test_server/start_server.sh +++ b/tools/sync_test_server/start_server.sh @@ -38,8 +38,9 @@ SCRIPTPATH="$( cd "$(dirname "$0")" >/dev/null 2>&1 ; pwd -P )" # Create app configurations APP_CONFIG_DIR=`mktemp -d -t app_config` -$SCRIPTPATH/app_config_generator.sh $APP_CONFIG_DIR $SCRIPTPATH/app_template partition testapp1 testapp2 -$SCRIPTPATH/app_config_generator.sh $APP_CONFIG_DIR $SCRIPTPATH/app_template flex testapp3 +$SCRIPTPATH/app_config_generator.sh $APP_CONFIG_DIR $SCRIPTPATH/app_template partition auto testapp1 +$SCRIPTPATH/app_config_generator.sh $APP_CONFIG_DIR $SCRIPTPATH/app_template partition email testapp2 +$SCRIPTPATH/app_config_generator.sh $APP_CONFIG_DIR $SCRIPTPATH/app_template flex function testapp3 # Run Stitch and Stitch CLI Docker images docker network create mongodb-realm-network diff --git a/version.txt b/version.txt index 593914ca00..5d91297a1a 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -10.12.0-SNAPSHOT +10.12.0-transformer-api-SNAPSHOT