All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.realm.transformer.build.BuildTemplate.kt Maven / Gradle / Ivy

Go to download

Android Gradle Transformer for Realm. Realm is a mobile database: Build better apps, faster.

There is a newer version: 10.19.0
Show newest version
/*
 * 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.SdkConstants
import com.android.build.api.transform.Format
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.BytecodeModifier
import io.realm.transformer.ManagedClassPool
import io.realm.transformer.logger
import io.realm.transformer.Utils
import io.realm.transformer.ext.getBootClasspath
import javassist.ClassPool
import javassist.CtClass
import org.gradle.api.Project
import java.io.File
import java.util.regex.Pattern

/**
 * Abstract class defining the structure of doing different types of builds.
 *
 */
abstract class BuildTemplate(val project: Project, val outputProvider: TransformOutputProvider, val transform: Transform) {

    protected lateinit var inputs: MutableCollection
    protected lateinit var classPool: ManagedClassPool
    protected val outputClassNames: MutableSet = hashSetOf()
    protected val outputReferencedClassNames: MutableSet = hashSetOf()
    protected val outputModelClasses: ArrayList = arrayListOf()

    /**
     * Find all the class names available for transforms as well as all referenced classes.
     */
    abstract fun prepareOutputClasses(inputs: MutableCollection)

    /**
     * Helper method for going through all `TransformInput` and sort classes into buckets of
     * source files in the current project or source files found in jar files.
     *
     * @param inputs set of input files
     * @param directoryFiles the set of files in directories getting compiled. These are potential
     * candidates for the transformer.
     * @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)

    /**
     * Returns `true` if this build contains no relevant classes to transform.
     */
    fun hasNoOutput(): Boolean {
        return outputClassNames.isEmpty()
    }


    fun prepareReferencedClasses(referencedInputs: Collection) {
        categorizeClassNames(referencedInputs, outputReferencedClassNames, outputReferencedClassNames) // referenced files

        // Create and populate the Javassist class pool
        this.classPool = ManagedClassPool(inputs, referencedInputs)
        // Append android.jar to class pool. We don't need the class names of them but only the class in the pool for
        // javassist. See https://github.com/realm/realm-java/issues/2703.
        addBootClassesToClassPool(classPool)
        logger.debug("ClassPool contains Realm classes: ${classPool.getOrNull("io.realm.RealmList") != null}")

        filterForModelClasses(outputClassNames, outputReferencedClassNames)
    }

    protected abstract fun filterForModelClasses(outputClassNames: Set, outputReferencedClassNames: Set)


    fun markMediatorsAsTransformed() {
        val baseProxyMediator: CtClass = classPool.get("io.realm.internal.RealmProxyMediator")
        val mediatorPattern: Pattern = Pattern.compile("^io\\.realm\\.[^.]+Mediator$")
        val proxyMediatorClasses: Collection = outputClassNames
                .filter { mediatorPattern.matcher(it).find() }
                .map { classPool.getCtClass(it) }
                .filter { it.superclass.equals(baseProxyMediator) }

        logger.debug("Proxy Mediator Classes: ${proxyMediatorClasses.joinToString(",") { it.name }}")
        proxyMediatorClasses.forEach {
            BytecodeModifier.overrideTransformedMarker(it)
        }
    }

    fun transformModelClasses() {
        // Add accessors to the model classes in the target project
        outputModelClasses.forEach {
            logger.debug("Modify model class: ${it.name}")
            BytecodeModifier.addRealmAccessors(it)
            BytecodeModifier.addRealmProxyInterface(it, classPool)
            BytecodeModifier.callInjectObjectContextFromConstructors(it)
        }
    }

    abstract fun transformDirectAccessToModelFields()

    fun copyResourceFiles() {
        copyResourceFiles(inputs)
        classPool.close();
    }

    private fun copyResourceFiles(inputs: MutableCollection) {
        inputs.forEach {
            it.directoryInputs.forEach {
                val dirPath: String = it.file.absolutePath
                it.file.walkTopDown().forEach {
                    if (it.isFile) {
                        if (!it.absolutePath.endsWith(SdkConstants.DOT_CLASS)) {
                            logger.debug("  Copying resource file: $it")
                            val dest = File(getOutputFile(outputProvider, Format.DIRECTORY), it.absolutePath.substring(dirPath.length))
                            dest.parentFile.mkdirs()
                            Files.copy(it, dest)
                        }
                    }
                }
            }

            it.jarInputs.forEach {
                logger.debug("Found JAR file: ${it.file.absolutePath}")
                val dirPath: String = it.file.absolutePath
                it.file.walkTopDown().forEach {
                    if (it.isFile) {
                        if (it.absolutePath.endsWith(SdkConstants.DOT_JAR)) {
                            logger.debug("  Copying jar file: $it")
                            val dest = File(getOutputFile(outputProvider, Format.JAR), it.absolutePath.substring(dirPath.length))
                            dest.parentFile.mkdirs()
                            Files.copy(it, dest)
                        }
                    }
                }
            }
        }
    }

    protected fun getOutputFile(outputProvider: TransformOutputProvider, format: Format): File {
        return outputProvider.getContentLocation("realm", transform.inputTypes, transform.scopes, format)
    }

    /**
     * There is no official way to get the path to android.jar for transform.
     * See https://code.google.com/p/android/issues/detail?id=209426
     */
    private fun addBootClassesToClassPool(classPool: ClassPool) {
        try {
            project.getBootClasspath().forEach {
                val path: String = it.absolutePath
                logger.debug("Add boot class $path to class pool.")
                classPool.appendClassPath(path)
            }
        } catch (e: Exception) {
            // Just log it. It might not impact the transforming if the method which needs to be transformer doesn't
            // contain classes from android.jar.
            logger.debug("Cannot get bootClasspath caused by: ", e)
        }
    }

    fun getOutputModelClasses(): Set {
        return outputModelClasses.toSet()
    }

    protected abstract fun findModelClasses(classNames: Set): Collection

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy