diff --git a/CHANGELOG.md b/CHANGELOG.md index dd33ec97a5..f4aa85b6ef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,10 @@ * [ObjectServer] Added `SyncSession.isConnected()`. * [ObjectServer] Added support for observing connection changes for a session using `SyncSession.addConnectionChangeListener()` and `SyncSession.removeConnectionChangeListener()`. +### Bug Fixes + +* Methods and classes requiring synchronized Realms have been removed from the standard AAR package. They are now only visible when enabling synchronized Realms in Gradle. The methods and classes will still be visible in the source files and docs, but annotated with `@ObjectServer` (#5799). + ### Internal * Updated to Object Store commit: 97fd03819f398b3c81c8b007feaca8636629050b diff --git a/build.gradle b/build.gradle index 12370c0abf..87999ef4df 100644 --- a/build.gradle +++ b/build.gradle @@ -47,11 +47,19 @@ task installTransformer(type:GradleBuild) { tasks = ['publishToMavenLocal'] } +task installBuildTransformer(type:GradleBuild) { + group = 'Install' + description = 'Install the jar realm-library-build-transformer into mavenLocal()' + buildFile = file('library-build-transformer/build.gradle') + tasks = ['publishToMavenLocal'] +} + task assembleRealm(type:GradleBuild) { group = 'Build' description = 'Assemble the Realm project' dependsOn installAnnotations dependsOn installTransformer + dependsOn installBuildTransformer buildFile = file('realm/build.gradle') tasks = ['assemble', 'javadocJar', 'sourcesJar'] if (project.hasProperty('buildTargetABIs')) { @@ -137,6 +145,7 @@ task installRealm(type:GradleBuild) { group = 'Install' description = 'Install the artifacts of Realm libraries into mavenLocal()' dependsOn installTransformer + dependsOn installBuildTransformer buildFile = file('realm/build.gradle') tasks = ['publishToMavenLocal'] if (project.hasProperty('buildTargetABIs')) { diff --git a/library-build-transformer/.gitignore b/library-build-transformer/.gitignore new file mode 100644 index 0000000000..89f9ac04aa --- /dev/null +++ b/library-build-transformer/.gitignore @@ -0,0 +1 @@ +out/ diff --git a/library-build-transformer/README.md b/library-build-transformer/README.md new file mode 100644 index 0000000000..9a9c5ed32d --- /dev/null +++ b/library-build-transformer/README.md @@ -0,0 +1,41 @@ +# Library Transformer + +This project contains a transformer that removes all classes, methods and fields annotated with a +given annotation. + +This can be used to emulate Kotlin extension methods in cases where separating the code into flavour +folders is not feasible, like e.g. when the `Realm` class is shared between the `base` and +`objectServer` flavour. + +## Usage + +Register the transformer as normal and provide it with the flavor to strip and annotation to detect + +``` +import io.realm.buildtransformer.RealmBuildTransformer +android.registerTransform(new RealmBuildTransformer("base", "io.realm.internal.annotations.ObjectServer", [ + "explicit_files_to_remove" +])) +``` + +It is also possible to provide a specific list of files that will be removed whether or not they +have the annotation. This is used to remove some files created by the annotation processor that do +not carry over annotations. + +## Warning + +There are no checks in place with regard to it being safe or not to remove classes and methods, so +only apply the transformer when it is safe to do so (i.e. the classes/methods/fields are not in use). +Any errors will only be caught at runtime when the actual code is accessed. + +## Known limitations + +* If all constructors are stripped by this transformer, a new default constructor will not be + created. This will result in invalid byte code being generated. + +* If the top-level class is removed, all inner classes, enums and interfaces must also be annotated, + otherwise they are not removed, resulting in valid bytecode being generated. + +* Annotations on super classes will also remove subclasses, but only the first level of inheritance. + +* Single enum values cannot be stripped, only the entire enum class. \ No newline at end of file diff --git a/library-build-transformer/build.gradle b/library-build-transformer/build.gradle new file mode 100644 index 0000000000..a08d4481c9 --- /dev/null +++ b/library-build-transformer/build.gradle @@ -0,0 +1,106 @@ +group 'io.realm' +version '1.0.0' + +buildscript { + ext.kotlin_version = '1.2.51' + + repositories { + mavenCentral() + jcenter() + } + dependencies { + classpath 'org.jfrog.buildinfo:build-info-extractor-gradle:4.5.2' + classpath 'com.jfrog.bintray.gradle:gradle-bintray-plugin:1.7.3' + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + } +} + +allprojects { + def props = new Properties() + props.load(new FileInputStream("${rootDir}/../realm.properties")) + props.each { key, val -> + project.ext.set(key, val) + } +} + +group = 'io.realm' +version = file("${projectDir}/../version.txt").text.trim() + +apply plugin: 'kotlin' +apply plugin: 'maven' +apply plugin: 'maven-publish' +apply plugin: 'com.jfrog.artifactory' +apply plugin: 'com.jfrog.bintray' + +repositories { + google() + mavenCentral() +} + +dependencies { + compile gradleApi() + compileOnly 'com.android.tools.build:gradle:3.1.1' + compile 'org.ow2.asm:asm:6.2' + compile 'org.ow2.asm:asm-util:6.2' + compile "org.jetbrains.kotlin:kotlin-stdlib-jdk8:${kotlin_version}" + + testCompile group:'junit', name:'junit', version:'4.12' + testCompile "org.jetbrains.kotlin:kotlin-test-junit:$kotlin_version" + testCompile "org.jetbrains.kotlin:kotlin-stdlib-jdk8:${kotlin_version}" + +} +compileKotlin { + kotlinOptions.jvmTarget = "1.8" +} +compileTestKotlin { + kotlinOptions.jvmTarget = "1.8" +} + +def commonPom = { + licenses { + license { + name 'The Apache Software License, Version 2.0' + url 'http://www.apache.org/licenses/LICENSE-2.0.txt' + distribution 'repo' + } + } + issueManagement { + system 'github' + url 'https://github.com/realm/realm-java/issues' + } + scm { + url 'scm:https://github.com/realm/realm-java' + connection 'scm:git@github.com:realm/realm-java.git' + developerConnection 'scm:git@github.com:realm/realm-java.git' + } +} + +publishing { + publications { + realmPublication(MavenPublication) { + groupId 'io.realm' + artifactId = 'realm-library-build-transformer' + from components.java + pom.withXml { + Node root = asNode() + root.appendNode('name', 'realm-library-build-transformer') + root.appendNode('description', 'Transform library for Realm Java that will strip unwanted files at build time.') + root.appendNode('url', 'http://realm.io') + root.children().last() + commonPom + } + } + } + repositories { + maven { + credentials(AwsCredentials) { + accessKey project.hasProperty('s3AccessKey') ? s3AccessKey : 'noAccessKey' + secretKey project.hasProperty('s3SecretKey') ? s3SecretKey : 'noSecretKey' + } + if(project.version.endsWith('-SNAPSHOT')) { + url "s3://realm-ci-artifacts/maven/snapshots/" + } else { + url "s3://realm-ci-artifacts/maven/releases/" + } + } + } +} diff --git a/library-build-transformer/gradle.properties b/library-build-transformer/gradle.properties new file mode 100644 index 0000000000..160890028a --- /dev/null +++ b/library-build-transformer/gradle.properties @@ -0,0 +1 @@ +org.gradle.caching=true diff --git a/library-build-transformer/gradle/wrapper/gradle-wrapper.jar b/library-build-transformer/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000..01b8bf6b1f Binary files /dev/null and b/library-build-transformer/gradle/wrapper/gradle-wrapper.jar differ diff --git a/library-build-transformer/gradle/wrapper/gradle-wrapper.properties b/library-build-transformer/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000000..57c7d2d22b --- /dev/null +++ b/library-build-transformer/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-4.4.1-all.zip diff --git a/library-build-transformer/gradlew b/library-build-transformer/gradlew new file mode 100755 index 0000000000..cccdd3d517 --- /dev/null +++ b/library-build-transformer/gradlew @@ -0,0 +1,172 @@ +#!/usr/bin/env sh + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=$(save "$@") + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong +if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then + cd "$(dirname "$0")" +fi + +exec "$JAVACMD" "$@" diff --git a/library-build-transformer/gradlew.bat b/library-build-transformer/gradlew.bat new file mode 100755 index 0000000000..e95643d6a2 --- /dev/null +++ b/library-build-transformer/gradlew.bat @@ -0,0 +1,84 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/library-build-transformer/settings.gradle b/library-build-transformer/settings.gradle new file mode 100644 index 0000000000..8b32573375 --- /dev/null +++ b/library-build-transformer/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'library-build-transformer' 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 new file mode 100644 index 0000000000..38511246c4 --- /dev/null +++ b/library-build-transformer/src/main/kotlin/io/realm/buildtransformer/RealmBuildTransformer.kt @@ -0,0 +1,160 @@ +/* + * 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.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 io.realm.buildtransformer.util.Stopwatch +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import java.io.File + +// Type aliases for improving readability +typealias ByteCodeTypeDescriptor = String +typealias QualifiedName = String +typealias ByteCodeMethodName = String + +// Package level logger +val logger: Logger = LoggerFactory.getLogger("realm-build-logger") + +/** + * Transformer that will strip all classes, methods and fields annotated with a given annotation from + * 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 + + 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) + } + } + } + + 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 + } + 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) + } + } + } + + transformClassFiles(outputDir, inputFiles, transformClasses) + } + + private fun transformClassFiles(outputDir: File, inputFiles: MutableSet, transformClasses: Boolean) { + val files: Set = if (transformClasses) { + val transformer = ClassPoolTransformer(annotationQualifiedName, inputFiles) + transformer.transform() + } else { + inputFiles + } + + copyToOutput(outputDir, files) + } + + 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() + } + } else { + it.copyTo(outputFile, overwrite = true) + } + } + } + +} diff --git a/library-build-transformer/src/main/kotlin/io/realm/buildtransformer/asm/ClassPoolTransformer.kt b/library-build-transformer/src/main/kotlin/io/realm/buildtransformer/asm/ClassPoolTransformer.kt new file mode 100644 index 0000000000..49ba7b17c6 --- /dev/null +++ b/library-build-transformer/src/main/kotlin/io/realm/buildtransformer/asm/ClassPoolTransformer.kt @@ -0,0 +1,98 @@ +/* + * 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.buildtransformer.asm + +import io.realm.buildtransformer.ByteCodeMethodName +import io.realm.buildtransformer.ByteCodeTypeDescriptor +import io.realm.buildtransformer.QualifiedName +import io.realm.buildtransformer.asm.visitors.AnnotatedCodeStripVisitor +import io.realm.buildtransformer.asm.visitors.AnnotationVisitor +import io.realm.buildtransformer.ext.shouldBeDeleted +import org.objectweb.asm.ClassReader +import org.objectweb.asm.ClassWriter +import java.io.File + +/** + * Transformer that will transform a pool of classes by removing all classes, methods and fields annotated with + * a given annotation. + * + * It does so in 2 passes using the ASM Visitor API. The first pass will gather metadata about the class hierarchy, + * the 2nd pass will do the actual transform. The ASM Tree API was considered as well, but it does not provide + * access to referenced classes easily, so two passes would be required here as well and the Visitor API is faster and + * requires less memory. + */ +class ClassPoolTransformer(annotationQualifiedName: QualifiedName, private val inputClasses: Set) { + + private val annotationDescriptor: String = createDescriptor(annotationQualifiedName) + + /** + * Transform files + * + * @return All input files, both those that have been modified and those that have not. + */ + fun transform(): Set { + val (markedClasses, markedMethods) = pass1() + return pass2(markedClasses, markedMethods) + } + + /** + * Pass 1: Collect all classes, interfaces and enums that contain the given annotation. This include both top-level + * and inner types. + */ + private fun pass1(): Pair, Map>> { + val metadataCollector = AnnotationVisitor(annotationDescriptor) + inputClasses.forEach { + it.inputStream().use { + val classReader = ClassReader(it) + classReader.accept(metadataCollector, 0) + } + } + return Pair(metadataCollector.annotatedClasses, metadataCollector.annotatedMethods) + } + + /** + * Pass 2: Remove methods and fields marked with the annotation. Classes that are removed + * are instead marked for deletion as deleting the File is the responsibility of the + * transform API. + */ + private fun pass2(markedClasses: Set, markedMethods: Map>): Set { + inputClasses.forEach { classFile -> + var result = ByteArray(0) + if (!classFile.shouldBeDeleted) { // Respect previously set delete flag, so avoid doing any work + classFile.inputStream().use { inputStream -> + val writer = ClassWriter(0) // We don't modify methods so no reason to re-calculate method frames + val classRemover = AnnotatedCodeStripVisitor(annotationDescriptor, markedClasses, markedMethods, writer) + val reader = ClassReader(inputStream) + reader.accept(classRemover, 0) + result = if (classRemover.deleteClass) ByteArray(0) else writer.toByteArray() + } + if (result.isNotEmpty()) { + classFile.outputStream().use { outputStream -> outputStream.write(result) } + } else { + classFile.shouldBeDeleted = true + } + } + } + return inputClasses + } + + /** + * Creates the descriptor used by ASM to identify types. + */ + private fun createDescriptor(qualifiedName: String): String { + return "L${qualifiedName.replace(".", "/")};" + } +} 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 new file mode 100644 index 0000000000..c8ac8c359b --- /dev/null +++ b/library-build-transformer/src/main/kotlin/io/realm/buildtransformer/asm/visitors/AnnotatedCodeStripVisitor.kt @@ -0,0 +1,84 @@ +/* + * 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.buildtransformer.asm.visitors + +import io.realm.buildtransformer.ByteCodeMethodName +import io.realm.buildtransformer.ByteCodeTypeDescriptor +import io.realm.buildtransformer.logger +import org.objectweb.asm.* +import org.objectweb.asm.AnnotationVisitor + +/** + * Visitor that will remove all classes, methods and fields annotated with given annotation. + * Doing this requires a pre-processing step performed by the [AnnotationVisitor]. + */ +class AnnotatedCodeStripVisitor(private val annotationDescriptor: String, + private val markedClasses: Set, + private val markedMethods: Map>, + classWriter: ClassVisitor) : ClassVisitor(Opcodes.ASM6, classWriter) { + + var deleteClass: Boolean = false + private lateinit var markedMethodsInClass: Set + + override fun visit(version: Int, access: Int, name: String?, signature: String?, superName: String?, interfaces: Array?) { + // Only process this class if it or its super class doesn't have the given annotation + markedMethodsInClass = markedMethods[name!!]!! + deleteClass = (markedClasses.contains(name) || markedClasses.contains(superName)) + if (!deleteClass) { + super.visit(version, access, name, signature, superName, interfaces) + } else { + logger.debug("Removing top level class: $name") + } + } + + // 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)) { + super.visitInnerClass(name, outerName, innerName, access) + } else { + logger.debug("Removing inner class description: $name") + } + } + + override fun visitField(access: Int, name: String?, descriptor: String?, signature: String?, value: Any?): FieldVisitor { + return object: FieldVisitor(api) { + var ignoreField = false + override fun visitAnnotation(descriptor: String?, visible: Boolean): AnnotationVisitor? { + ignoreField = (annotationDescriptor == descriptor) + return null + } + override fun visitEnd() { + if (!ignoreField) { + // Call super ClassVisitor directly + this@AnnotatedCodeStripVisitor.cv.visitField(access, name, descriptor, signature, value) + } else { + logger.debug("Removing field: $name") + } + } + } + } + + override fun visitMethod(access: Int, name: ByteCodeMethodName?, descriptor: String?, signature: String?, exceptions: Array?): MethodVisitor? { + return if (!markedMethodsInClass.contains(name)) { + super.visitMethod(access, name, descriptor, signature, exceptions) + } else { + logger.debug("Removing method: $name") + null + } + } + +} \ No newline at end of file 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 new file mode 100644 index 0000000000..75a761c976 --- /dev/null +++ b/library-build-transformer/src/main/kotlin/io/realm/buildtransformer/asm/visitors/AnnotationVisitor.kt @@ -0,0 +1,68 @@ +/* + * 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.buildtransformer.asm.visitors; + +import io.realm.buildtransformer.ByteCodeMethodName +import io.realm.buildtransformer.ByteCodeTypeDescriptor +import io.realm.buildtransformer.logger +import org.objectweb.asm.AnnotationVisitor +import org.objectweb.asm.ClassVisitor +import org.objectweb.asm.MethodVisitor +import org.objectweb.asm.Opcodes + +/** + * ClassVisitor that gather all classes and methods with the given annotation. This is the first + * 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) { + + val annotatedClasses: MutableSet = mutableSetOf() + val annotatedMethods: MutableMap> = mutableMapOf() + private var internalQualifiedName: String = "" + private val annotatedMethodsInClass = mutableSetOf() + + override fun visit(version: Int, access: Int, name: String?, signature: String?, superName: String?, interfaces: Array?) { + internalQualifiedName = name!! + annotatedMethods[internalQualifiedName] = annotatedMethodsInClass + super.visit(version, access, name, signature, superName, interfaces) + } + + override fun visitAnnotation(descriptor: String?, visible: Boolean): AnnotationVisitor? { + if (descriptor == annotationDescriptor) { + annotatedClasses.add(internalQualifiedName) + } + return super.visitAnnotation(descriptor, visible) + } + + override fun visitMethod(access: Int, name: String?, descriptor: String?, signature: String?, exceptions: Array?): MethodVisitor { + val parentVisitor = super.visitMethod(access, name, descriptor, signature, exceptions) + return object: MethodVisitor(api, parentVisitor) { + override fun visitAnnotation(descriptor: String?, visible: Boolean): AnnotationVisitor? { + if (descriptor == annotationDescriptor) { + annotatedMethodsInClass.add(name!!) + } + return super.visitAnnotation(descriptor, visible) + } + } + } + + override fun visitEnd() { + annotatedMethods[internalQualifiedName] = annotatedMethodsInClass + super.visitEnd() + } + +} diff --git a/library-build-transformer/src/main/kotlin/io/realm/buildtransformer/ext/FileExt.kt b/library-build-transformer/src/main/kotlin/io/realm/buildtransformer/ext/FileExt.kt new file mode 100644 index 0000000000..f989bbf183 --- /dev/null +++ b/library-build-transformer/src/main/kotlin/io/realm/buildtransformer/ext/FileExt.kt @@ -0,0 +1,45 @@ +/* + * 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.buildtransformer.ext + +import java.io.File +import java.util.* +import kotlin.reflect.KProperty + +// Add support for extension properties +// Credit: https://stackoverflow.com/questions/36502413/extension-fields-in-kotlin +class FieldProperty(val initializer: (R) -> T = { throw IllegalStateException("Field Property not initialized.") }) { + private val map = WeakHashMap() + + operator fun getValue(thisRef: R, property: KProperty<*>): T = + map[thisRef] ?: setValue(thisRef, property, initializer(thisRef)) + + operator fun setValue(thisRef: R, property: KProperty<*>, value: T): T { + map[thisRef] = value + return value + } +} + +/** + * If the file points to a compiled `.class` file, this property stores the path leading to the root folder of the + * package hierarchy. + */ +var File.packageHierarchyRootDir: String by FieldProperty() + +/** + * `true` if the file is marked for deletion after being processed. + */ +var File.shouldBeDeleted: Boolean by FieldProperty() diff --git a/library-build-transformer/src/main/kotlin/io/realm/buildtransformer/util/Stopwatch.kt b/library-build-transformer/src/main/kotlin/io/realm/buildtransformer/util/Stopwatch.kt new file mode 100644 index 0000000000..52f4a2447c --- /dev/null +++ b/library-build-transformer/src/main/kotlin/io/realm/buildtransformer/util/Stopwatch.kt @@ -0,0 +1,63 @@ +/* + * 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.buildtransformer.util + +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import java.util.concurrent.TimeUnit + +class Stopwatch { + + val logger: Logger = LoggerFactory.getLogger("realm-stopwatch") + + var start: Long = -1L + var lastSplit: Long = -1L + lateinit var label: String + + /** + * Start the stopwatch. + */ + fun start(label: String) { + if (start != -1L) { + throw IllegalStateException("Stopwatch was already started"); + } + this.label = label + start = System.nanoTime(); + lastSplit = start; + } + + /** + * Reports the split time. + * + * @param label Label to use when printing split time + * @param reportDiffFromLastSplit if `true` report the time from last split instead of the start + */ + fun splitTime(label: String, reportDiffFromLastSplit: Boolean = true) { + val split = System.nanoTime() + val diff = if (reportDiffFromLastSplit) { split - lastSplit } else { split - start } + lastSplit = split; + logger.debug("$label: ${TimeUnit.NANOSECONDS.toMillis(diff)} ms.") + } + + /** + * Stops the timer and report the result. + */ + fun stop() { + val stop = System.nanoTime() + val diff = stop - start + logger.debug("$label: ${TimeUnit.NANOSECONDS.toMillis(diff)} ms.") + } +} \ No newline at end of file diff --git a/library-build-transformer/src/test/java/io/realm/buildtransformer/testclasses/NestedTestClass.java b/library-build-transformer/src/test/java/io/realm/buildtransformer/testclasses/NestedTestClass.java new file mode 100644 index 0000000000..0ef811df1e --- /dev/null +++ b/library-build-transformer/src/test/java/io/realm/buildtransformer/testclasses/NestedTestClass.java @@ -0,0 +1,43 @@ +/* + * 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.buildtransformer.testclasses; + +import io.realm.internal.annotations.ObjectServer; + +public class NestedTestClass { + public String name; + + @ObjectServer + public static class StaticInnerClass { + public String foo; + } + + + @ObjectServer + public class InnerClass { + public String foo; + } + + @ObjectServer + public enum Enum { + FOO + } + + @ObjectServer + public interface Interface { + void foo(); + } +} diff --git a/library-build-transformer/src/test/java/io/realm/buildtransformer/testclasses/SimpleTestClass.java b/library-build-transformer/src/test/java/io/realm/buildtransformer/testclasses/SimpleTestClass.java new file mode 100644 index 0000000000..54bcdb4c10 --- /dev/null +++ b/library-build-transformer/src/test/java/io/realm/buildtransformer/testclasses/SimpleTestClass.java @@ -0,0 +1,27 @@ +/* + * 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.buildtransformer.testclasses; + +import io.realm.internal.annotations.ObjectServer; + +@ObjectServer +public class SimpleTestClass { + public String name; + + public static class Foo { + public String bar; + } +} diff --git a/library-build-transformer/src/test/java/io/realm/buildtransformer/testclasses/SimpleTestFields.java b/library-build-transformer/src/test/java/io/realm/buildtransformer/testclasses/SimpleTestFields.java new file mode 100644 index 0000000000..3020f7fbd0 --- /dev/null +++ b/library-build-transformer/src/test/java/io/realm/buildtransformer/testclasses/SimpleTestFields.java @@ -0,0 +1,25 @@ +/* + * 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.buildtransformer.testclasses; + +import io.realm.internal.annotations.ObjectServer; + +public class SimpleTestFields { + + @ObjectServer + public String field1; + public String field2; +} diff --git a/library-build-transformer/src/test/java/io/realm/buildtransformer/testclasses/SimpleTestMethods.java b/library-build-transformer/src/test/java/io/realm/buildtransformer/testclasses/SimpleTestMethods.java new file mode 100644 index 0000000000..45c3377faf --- /dev/null +++ b/library-build-transformer/src/test/java/io/realm/buildtransformer/testclasses/SimpleTestMethods.java @@ -0,0 +1,36 @@ +/* + * 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.buildtransformer.testclasses; + +import io.realm.internal.annotations.ObjectServer; + +public class SimpleTestMethods { + + @ObjectServer + public String foo() { + return "foo"; + } + + @ObjectServer + public String foo1(String input) { + return "foo1"; + } + + public String bar() { + return "bar"; + } + +} diff --git a/library-build-transformer/src/test/java/io/realm/buildtransformer/testclasses/SubClass.java b/library-build-transformer/src/test/java/io/realm/buildtransformer/testclasses/SubClass.java new file mode 100644 index 0000000000..db3eca0d98 --- /dev/null +++ b/library-build-transformer/src/test/java/io/realm/buildtransformer/testclasses/SubClass.java @@ -0,0 +1,20 @@ +/* + * 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.buildtransformer.testclasses; + +public class SubClass extends SuperClass { + public String foo; +} diff --git a/library-build-transformer/src/test/java/io/realm/buildtransformer/testclasses/SuperClass.java b/library-build-transformer/src/test/java/io/realm/buildtransformer/testclasses/SuperClass.java new file mode 100644 index 0000000000..7c00aa001a --- /dev/null +++ b/library-build-transformer/src/test/java/io/realm/buildtransformer/testclasses/SuperClass.java @@ -0,0 +1,22 @@ +/* + * 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.buildtransformer.testclasses; + +import io.realm.internal.annotations.ObjectServer; + +@ObjectServer +public class SuperClass { +} diff --git a/library-build-transformer/src/test/java/io/realm/internal/annotations/ObjectServer.java b/library-build-transformer/src/test/java/io/realm/internal/annotations/ObjectServer.java new file mode 100644 index 0000000000..58e8ec1069 --- /dev/null +++ b/library-build-transformer/src/test/java/io/realm/internal/annotations/ObjectServer.java @@ -0,0 +1,29 @@ +/* + * 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.internal.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Copy of an interface from the Realm Library (to break cyclic dependencies) + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.TYPE, ElementType.METHOD, ElementType.FIELD}) +public @interface ObjectServer { +} diff --git a/library-build-transformer/src/test/kotlin/io/realm/buildtransformer/DynamicClassLoader.kt b/library-build-transformer/src/test/kotlin/io/realm/buildtransformer/DynamicClassLoader.kt new file mode 100644 index 0000000000..b7ff85bc5f --- /dev/null +++ b/library-build-transformer/src/test/kotlin/io/realm/buildtransformer/DynamicClassLoader.kt @@ -0,0 +1,34 @@ +/* + * 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.buildtransformer + +import java.io.File + +/** + * Custom ClassLoader that can be used to dynamically load a class that have been dynamically modified, i.e. we + * load it using the ByteArray that represents it. + */ +class DynamicClassLoader(parentLoader: ClassLoader) : ClassLoader(parentLoader) { + + fun loadClass(qualifiedClassName: String, pool: Set): Class<*> { + val classFile: File = pool.find { + it.absolutePath.endsWith("${qualifiedClassName.replace(".", "/")}.class") + } ?: throw IllegalStateException("Class pool does not contain: $qualifiedClassName") + + val classBytes: ByteArray = classFile.readBytes() + return defineClass(qualifiedClassName, classBytes, 0, classBytes.size) + } +} \ No newline at end of file diff --git a/library-build-transformer/src/test/kotlin/io/realm/buildtransformer/VisitorTests.kt b/library-build-transformer/src/test/kotlin/io/realm/buildtransformer/VisitorTests.kt new file mode 100644 index 0000000000..bd74b33d03 --- /dev/null +++ b/library-build-transformer/src/test/kotlin/io/realm/buildtransformer/VisitorTests.kt @@ -0,0 +1,142 @@ +/* + * 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.buildtransformer + +import io.realm.buildtransformer.asm.ClassPoolTransformer +import io.realm.buildtransformer.testclasses.* +import org.junit.Before +import org.junit.Test +import java.io.File +import kotlin.reflect.KClass +import kotlin.test.assertEquals +import kotlin.test.assertTrue +import kotlin.test.fail + +class VisitorTests { + + private lateinit var classLoader: DynamicClassLoader + private val qualifiedAnnotationName = "io.realm.internal.annotations.ObjectServer" + + @Before + fun setUp () { + classLoader = DynamicClassLoader(this::class.java.classLoader) + } + + @Test + fun removeFields() { + val c: Class = modifyClass(SimpleTestFields::class) + assetDefaultConstructorExists(c) + assertFieldExists("field2", c) + assertFieldRemoved("field1", c) + } + + @Test + fun removeMethods() { + val c: Class = modifyClass(SimpleTestMethods::class) + assetDefaultConstructorExists(c) + assertMethodExists("bar", c) + assertMethodRemoved("foo", c) + } + + @Test + fun removeTopLevelClass() { + try { + modifyClass(SimpleTestClass::class) + fail() + } catch(e: IllegalStateException) { + assertTrue(e.message?.contains("Class pool does not contain") == true) + } + } + + @Test + fun removeClassIfSuperClassIsAnnotated() { + try { + modifyClass(SubClass::class, setOf(SubClass::class, SuperClass::class)) + fail() + } catch(e: IllegalStateException) { + assertTrue(e.message?.contains("Class pool does not contain") == true) + } + } + + @Test + fun removeInnerClasses() { + // The reflection API does not make it possible to find inner classes, so we need to inspect the bytecode + // instead. We do this by checking the output of the transformer which will only output files modified and + // not classes deleted. + val inputClasses: MutableSet = mutableSetOf() + setOf>( + NestedTestClass::class, + NestedTestClass.InnerClass::class, + NestedTestClass.StaticInnerClass::class, + NestedTestClass.Enum::class, + NestedTestClass.Interface::class + ).forEach { inputClasses.add(getClassFile(it)) } + val transformer = ClassPoolTransformer(qualifiedAnnotationName, inputClasses) + val outputFiles: Set = transformer.transform() + assertEquals(1, outputFiles.size) // Only top level file is saved. + assertTrue(outputFiles.first().name.endsWith("NestedTestClass.class")) + } + + private fun assetDefaultConstructorExists(clazz: Class<*>) { + clazz.getConstructor() + } + + private fun assertFieldRemoved(fieldName: String, clazz: Class<*>) { + try { + clazz.getField(fieldName) + fail("Field $fieldName has not been removed"); + } catch (e: NoSuchFieldException) { + } + } + + private fun assertFieldExists(fieldName: String, clazz: Class<*>) { + clazz.getField(fieldName) + } + + private fun assertMethodRemoved(methodName: String, clazz: Class<*>) { + try { + clazz.getMethod(methodName) + fail("Method $methodName has not been removed"); + } catch (e: NoSuchMethodException) { + } + } + + private fun assertMethodExists(methodName: String, clazz: Class<*>) { + clazz.getMethod(methodName) // Will throw exception if it doesn't + } + + private fun modifyClass(clazz: KClass): Class { + return this.modifyClass(clazz, setOf(clazz)) + } + + private fun modifyClass(clazz: KClass, pool: Set>): Class { + val inputClasses: MutableSet = mutableSetOf() + pool.forEach { inputClasses.add(getClassFile(it)) } + val transformer = ClassPoolTransformer(qualifiedAnnotationName, inputClasses) + val outputFiles: Set = transformer.transform() + @Suppress("UNCHECKED_CAST") + return classLoader.loadClass(clazz.java.name, outputFiles) as Class + } + + private fun getClassFile(clazz: KClass<*>): File { + return getClassFile(clazz.java) + } + + private fun getClassFile(clazz: Class<*>): File { + val filePath = "${clazz.name.replace(".", "/")}.class" + return File(classLoader.getResource(filePath).file) + } +} diff --git a/realm/build.gradle b/realm/build.gradle index a1599ab796..af6f32dd91 100644 --- a/realm/build.gradle +++ b/realm/build.gradle @@ -23,6 +23,7 @@ buildscript { classpath 'org.jfrog.buildinfo:build-info-extractor-gradle:4.5.4' classpath 'com.jfrog.bintray.gradle:gradle-bintray-plugin:1.7.3' classpath "io.realm:realm-transformer:${file('../version.txt').text.trim()}" + classpath "io.realm:realm-library-build-transformer:${file('../version.txt').text.trim()}" classpath 'net.ltgt.gradle:gradle-errorprone-plugin:0.0.13' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath "org.jetbrains.dokka:dokka-gradle-plugin:${dokka_version}" diff --git a/realm/kotlin-extensions/build.gradle b/realm/kotlin-extensions/build.gradle index 532b468d5f..e3da497fe3 100644 --- a/realm/kotlin-extensions/build.gradle +++ b/realm/kotlin-extensions/build.gradle @@ -14,8 +14,7 @@ apply plugin: 'org.jetbrains.dokka' //apply plugin: 'com.github.kt3k.coveralls' //apply plugin: 'net.ltgt.errorprone' -import io.realm.transformer.RealmTransformer -android.registerTransform(new RealmTransformer(project)) +android.registerTransform(new io.realm.transformer.RealmTransformer(project)) android { compileSdkVersion rootProject.compileSdkVersion @@ -63,8 +62,8 @@ dependencies { implementation project(':realm-library') implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" androidTestImplementation 'junit:junit:4.12' - androidTestImplementation 'com.android.support.test:runner:1.0.1' - androidTestImplementation 'com.android.support.test:rules:1.0.1' + androidTestImplementation 'com.android.support.test:runner:1.0.2' + androidTestImplementation 'com.android.support.test:rules:1.0.2' kaptAndroidTest project(':realm-annotations-processor') androidTestImplementation "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version" } diff --git a/realm/realm-library/build.gradle b/realm/realm-library/build.gradle index 7cb75bd181..7573d814bf 100644 --- a/realm/realm-library/build.gradle +++ b/realm/realm-library/build.gradle @@ -186,9 +186,18 @@ tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all { coveralls.jacocoReportPath = "${buildDir}/reports/coverage/debug/report.xml" -import io.realm.transformer.RealmTransformer - -android.registerTransform(new RealmTransformer(project)) +android.registerTransform(new io.realm.transformer.RealmTransformer(project)) +android.registerTransform(new io.realm.buildtransformer.RealmBuildTransformer( + "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() +)) repositories { maven { url "https://jitpack.io" }