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

jp.nephy.kchroner.plugin.KotlinPluginLoader.kt Maven / Gradle / Ivy

package jp.nephy.kchroner.plugin

import jp.nephy.kchroner.KChroner
import jp.nephy.kchroner.api.LoopSubscription
import jp.nephy.kchroner.api.ScheduledSubscription
import jp.nephy.kchroner.api.TwitterSubscription
import org.jetbrains.kotlin.cli.common.environment.setIdeaIoUseFallback
import org.jetbrains.kotlin.cli.common.repl.KotlinJsr223JvmScriptEngineBase
import org.jetbrains.kotlin.cli.common.repl.KotlinJsr223JvmScriptEngineFactoryBase
import org.jetbrains.kotlin.cli.common.repl.ScriptArgsWithTypes
import org.jetbrains.kotlin.script.jsr223.KotlinJsr223JvmLocalScriptEngine
import org.jetbrains.kotlin.script.jsr223.KotlinStandardJsr223ScriptTemplate
import org.jetbrains.kotlin.script.util.scriptCompilationClasspathFromContextOrStlib
import org.jetbrains.kotlin.utils.addToStdlib.measureTimeMillisWithResult
import java.net.URLClassLoader
import java.nio.file.Files
import java.nio.file.Path
import java.nio.file.Paths
import javax.script.*
import kotlin.reflect.full.findAnnotation
import kotlin.reflect.full.memberFunctions
import kotlin.streams.toList

private val logger = KChroner.logger("KChroner.KotlinPluginLoader")

class KotlinPluginLoader {
    companion object {
        val pluginExtensions = arrayOf("kt")
        private val compiledCacheRootDirectory = Paths.get("plugins_cache")
        private val compiledCacheFileRegex = "^Line_\\d+\\.class$".toRegex()
        val additionalLibraryDirectory = Paths.get("lib")!!
    }

    fun load(path: Path): List {
        val compiledCacheDirectory = Paths.get(compiledCacheRootDirectory.toString(), *path.drop(1).map { it.toString() }.toTypedArray())

        if (!Files.isDirectory(compiledCacheDirectory) || enumulateCompiledCaches(compiledCacheDirectory).any { Files.getLastModifiedTime(it) < Files.getLastModifiedTime(path) }) {
            compile(path, compiledCacheDirectory) ?: return emptyList()
        }

        return enumulateCompiledCaches(compiledCacheDirectory).enumulateInstances(compiledCacheDirectory).map { it.buildPlugin() }
    }

    fun load(pluginClass: Class<*>): Plugin? {
        val instance = try {
            if (pluginClass.isAnnotationPresent(jp.nephy.kchroner.api.Plugin::class.java)) {
                pluginClass.newInstance()
            } else {
                null
            }
        } catch (e: Exception) {
            logger.error(e) { "インスタンスの作成に失敗しました. (${pluginClass.name}" }
            null
        }

        return instance?.buildPlugin()
    }

    private fun compile(path: Path, compiledCacheDirectory: Path): KotlinJsr223JvmScriptEngineBase.CompiledKotlinScript? {
        return try {
            val engine = CustomKotlinJsr223JvmLocalScriptEngineFactory().scriptEngine
            setIdeaIoUseFallback()

            val content = path.toFile().reader().use { it.readText() }
            val (compileTimeMs, compiledScript) = measureTimeMillisWithResult {
                (engine as Compilable).compile(content) as KotlinJsr223JvmScriptEngineBase.CompiledKotlinScript
            }
            logger.debug { "コンパイル成功: $path ($compileTimeMs ms)" }

            for (compiledClass in compiledScript.compiledData.classes) {
                val cachePath = Paths.get(compiledCacheDirectory.toString(), compiledClass.path)
                Files.createDirectories(cachePath.parent)
                cachePath.toFile().writeBytes(compiledClass.bytes)
            }
            logger.trace { "$path のコンパイルキャッシュを保存しました." }

            compiledScript
        } catch (e: ScriptException) {
            logger.error(e) { "スクリプトのコンパイルに失敗しました. ($path)" }
            null
        }
    }

    private fun enumulateCompiledCaches(compiledCacheDirectory: Path): List {
        return Files.walk(compiledCacheDirectory).filter { compiledCacheFileRegex.matches(it.fileName.toString()) }.toList()
    }

    private fun List.enumulateInstances(compiledCacheDirectory: Path): List {
        val classLoader = URLClassLoader(
                arrayOf(compiledCacheDirectory.toUri().toURL()) + Files.walk(KotlinPluginLoader.additionalLibraryDirectory).map { it.toUri().toURL() }.toList()
        )
        return mapNotNull {
            val loadedClass = try {
                classLoader.loadClass(it.fileName.toString().removeSuffix(".class"))
            } catch (e: Exception) {
                logger.error(e) { "クラスロードに失敗しました. ($it)" }
                null
            } ?: return@mapNotNull null

            loadedClass.classes.mapNotNull { childClass ->
                try {
                    if (childClass.isAnnotationPresent(jp.nephy.kchroner.api.Plugin::class.java)) {
                        childClass.newInstance()
                    } else {
                        null
                    }
                } catch (e: Exception) {
                    logger.error(e) { "インスタンスの作成に失敗しました. (${childClass.name}" }
                    null
                }
            }
        }.flatten()
    }

    private fun Any.buildPlugin(path: Path? = null): Plugin {
        return Plugin(path, this, this::class.java.getAnnotation(jp.nephy.kchroner.api.Plugin::class.java)).also { plugin ->
            logger.debug { "Kotlinプラグイン: ${plugin.name} のサブスクリプションをロードを開始しました." }

            this::class.memberFunctions.forEach { function ->
                val annotation = function.findAnnotation()
                        ?: function.findAnnotation()
                        ?: function.findAnnotation()
                        ?: return@forEach

                val subscription = Subscription(plugin, function, annotation)
                plugin.register(subscription)
                logger.debug { "サブスクリプション: ${subscription.name} をロードしました." }
            }
        }
    }
}

private class CustomKotlinJsr223JvmLocalScriptEngineFactory: KotlinJsr223JvmScriptEngineFactoryBase() {
    override fun getScriptEngine(): ScriptEngine {
        val classPath = scriptCompilationClasspathFromContextOrStlib("kotlin-script-util.jar", wholeClasspath = true) + Files.walk(KotlinPluginLoader.additionalLibraryDirectory).map { it.toFile() }.toList()

        return KotlinJsr223JvmLocalScriptEngine(
                this, classPath, KotlinStandardJsr223ScriptTemplate::class.qualifiedName!!,
                { ctx, types ->
                    ScriptArgsWithTypes(arrayOf(ctx.getBindings(ScriptContext.ENGINE_SCOPE)), types ?: emptyArray())
                }, arrayOf(Bindings::class)
        )
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy