From 3403c0a2d119708d63328742e390c69c869b1070 Mon Sep 17 00:00:00 2001 From: Gary Tierney Date: Fri, 26 May 2017 00:51:51 +0100 Subject: [PATCH] First draft of KotlinPluginEnvironment --- game/build.gradle | 25 +++++ .../plugin/kotlin/KotlinPluginCompiler.kt | 64 +++++++++++ .../game/plugin/kotlin/KotlinPluginScript.kt | 45 ++++++++ .../game/plugin/KotlinPluginEnvironment.java | 101 ++++++++++++++++++ .../org/apollo/game/plugin/PluginManager.java | 2 +- 5 files changed, 236 insertions(+), 1 deletion(-) create mode 100644 game/src/kotlin/org/apollo/game/plugin/kotlin/KotlinPluginCompiler.kt create mode 100644 game/src/kotlin/org/apollo/game/plugin/kotlin/KotlinPluginScript.kt create mode 100644 game/src/main/org/apollo/game/plugin/KotlinPluginEnvironment.java diff --git a/game/build.gradle b/game/build.gradle index a96d17a9..15e9fbb8 100644 --- a/game/build.gradle +++ b/game/build.gradle @@ -1,7 +1,32 @@ description = 'Apollo Game' +buildscript { + ext.kotlinVersion = '1.1.2-4' + + repositories { + mavenCentral() + } + + dependencies { + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion" + } +} + +apply plugin: 'kotlin' + dependencies { compile project(':cache') compile project(':net') compile project(':util') + + compile group: 'org.jetbrains.kotlin', name: 'kotlin-compiler', version: $kotlinVersion + compile group: 'org.jetbrains.kotlin', name: 'kotlin-stdlib-jre8', version: $kotlinVersion +} + +sourceSets { + main.kotlin.srcDirs += 'src/kotlin' +} + +repositories { + mavenCentral() } diff --git a/game/src/kotlin/org/apollo/game/plugin/kotlin/KotlinPluginCompiler.kt b/game/src/kotlin/org/apollo/game/plugin/kotlin/KotlinPluginCompiler.kt new file mode 100644 index 00000000..e87dbe5d --- /dev/null +++ b/game/src/kotlin/org/apollo/game/plugin/kotlin/KotlinPluginCompiler.kt @@ -0,0 +1,64 @@ +package org.apollo.game.plugin.kotlin + +import com.intellij.openapi.util.Disposer +import org.apollo.Server +import org.apollo.game.model.World +import org.apollo.game.plugin.PluginContext +import org.jetbrains.kotlin.cli.common.CLIConfigurationKeys +import org.jetbrains.kotlin.cli.common.messages.MessageCollector +import org.jetbrains.kotlin.cli.jvm.compiler.EnvironmentConfigFiles +import org.jetbrains.kotlin.cli.jvm.compiler.KotlinCoreEnvironment +import org.jetbrains.kotlin.cli.jvm.compiler.KotlinToJVMBytecodeCompiler +import org.jetbrains.kotlin.cli.jvm.config.JvmClasspathRoot +import org.jetbrains.kotlin.codegen.CompilationException +import org.jetbrains.kotlin.config.CommonConfigurationKeys +import org.jetbrains.kotlin.config.CompilerConfiguration +import org.jetbrains.kotlin.config.JVMConfigurationKeys +import org.jetbrains.kotlin.config.addKotlinSourceRoot +import org.jetbrains.kotlin.script.KotlinScriptDefinitionFromAnnotatedTemplate +import java.io.File + +class KotlinPluginCompiler(val classpath: List, val messageCollector: MessageCollector) { + + private fun createCompilerConfiguration(inputPath: String): CompilerConfiguration { + val configuration = CompilerConfiguration() + val scriptDefinition = KotlinScriptDefinitionFromAnnotatedTemplate(KotlinPluginScript::class) + + configuration.add(JVMConfigurationKeys.SCRIPT_DEFINITIONS, scriptDefinition) + configuration.put(JVMConfigurationKeys.CONTENT_ROOTS, classpath.map { JvmClasspathRoot(it) }) + configuration.put(JVMConfigurationKeys.RETAIN_OUTPUT_IN_MEMORY, true) + configuration.put(CLIConfigurationKeys.MESSAGE_COLLECTOR_KEY, messageCollector) + configuration.put(CommonConfigurationKeys.MODULE_NAME, inputPath) + configuration.addKotlinSourceRoot(inputPath) + + return configuration + } + + @Throws(KotlinPluginCompilerException::class) + fun compile(inputPath: String): Class { + val rootDisposable = Disposer.newDisposable() + val configuration = createCompilerConfiguration(inputPath) + + val configFiles = EnvironmentConfigFiles.JVM_CONFIG_FILES + val environment = KotlinCoreEnvironment.createForProduction(rootDisposable, configuration, configFiles) + + try { + val clazz = KotlinToJVMBytecodeCompiler.compileScript(environment, Server::class.java.classLoader) + + if (clazz?.getConstructor(World::class.java, PluginContext::class.java) == null) { + throw KotlinPluginCompilerException("Unable to compile $inputPath, no plugin constructor found") + } + + return clazz as Class + } catch (e: CompilationException) { + throw KotlinPluginCompilerException("Compilation failed", e) + } finally { + Disposer.dispose(rootDisposable) + } + } + +} + +class KotlinPluginCompilerException(message: String, cause: Throwable? = null) : Exception(message, cause) { + +} \ No newline at end of file diff --git a/game/src/kotlin/org/apollo/game/plugin/kotlin/KotlinPluginScript.kt b/game/src/kotlin/org/apollo/game/plugin/kotlin/KotlinPluginScript.kt new file mode 100644 index 00000000..29a84213 --- /dev/null +++ b/game/src/kotlin/org/apollo/game/plugin/kotlin/KotlinPluginScript.kt @@ -0,0 +1,45 @@ +package org.apollo.game.plugin.kotlin + +import org.apollo.game.message.handler.MessageHandler +import org.apollo.game.model.World +import org.apollo.game.model.entity.Player +import org.apollo.game.plugin.PluginContext +import org.apollo.net.message.Message +import kotlin.reflect.KClass +import kotlin.script.templates.ScriptTemplateDefinition + +@ScriptTemplateDefinition( + scriptFilePattern = ".*\\.plugin\\.kts" +) +abstract class KotlinPluginScript(val world: World, val context: PluginContext) { + + protected fun on(type: () -> KClass): KotlinMessageHandler { + return KotlinMessageHandler(world, context, type.invoke()) + } + +} + + +class KotlinMessageHandler(val world: World, val context: PluginContext, val type: KClass) : MessageHandler(world) { + + override fun handle(player: Player, message: T) { + if (message.predicate()) { + message.function(player) + } + } + + var function: T.(Player) -> Unit = { _ -> } + + var predicate: T.() -> Boolean = { true } + + fun where(predicate: T.() -> Boolean): KotlinMessageHandler { + this.predicate = predicate + return this + } + + fun then(function: T.(Player) -> Unit) { + this.function = function + this.context.addMessageHandler(type.java, this) + } + +} diff --git a/game/src/main/org/apollo/game/plugin/KotlinPluginEnvironment.java b/game/src/main/org/apollo/game/plugin/KotlinPluginEnvironment.java new file mode 100644 index 00000000..0dfe06cd --- /dev/null +++ b/game/src/main/org/apollo/game/plugin/KotlinPluginEnvironment.java @@ -0,0 +1,101 @@ +package org.apollo.game.plugin; + +import org.apollo.game.model.World; +import org.apollo.game.plugin.kotlin.KotlinPluginCompiler; +import org.apollo.game.plugin.kotlin.KotlinPluginScript; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.kotlin.cli.common.messages.CompilerMessageLocation; +import org.jetbrains.kotlin.cli.common.messages.CompilerMessageSeverity; +import org.jetbrains.kotlin.cli.common.messages.MessageCollector; + +import java.io.File; +import java.io.InputStream; +import java.lang.reflect.Constructor; +import java.net.URISyntaxException; +import java.net.URL; +import java.net.URLClassLoader; +import java.util.ArrayList; +import java.util.List; +import java.util.logging.Level; +import java.util.logging.Logger; + +public class KotlinPluginEnvironment implements PluginEnvironment, MessageCollector { + + private static final Logger logger = Logger.getLogger(KotlinPluginEnvironment.class.getName()); + + private final World world; + private final KotlinPluginCompiler pluginCompiler; + + private PluginContext context; + + public KotlinPluginEnvironment(World world) { + this.world = world; + this.pluginCompiler = new KotlinPluginCompiler(resolveClasspath(), this); + } + + /** + * Resolve the classpath of the current running {@link Thread}. + * + * @return A {@link List} of {@link File}s pointing to classpath entries. + */ + private static List resolveClasspath() { + ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); + + if (!(classLoader instanceof URLClassLoader)) { + throw new RuntimeException("Unable to resolve classpath for current ClassLoader"); + } + + URLClassLoader urlClassLoader = (URLClassLoader) classLoader; + URL[] classpathUrls = urlClassLoader.getURLs(); + List classpath = new ArrayList<>(); + + for (URL classpathUrl : classpathUrls) { + try { + classpath.add(new File(classpathUrl.toURI())); + } catch (URISyntaxException e) { + throw new RuntimeException("URL returned by ClassLoader is invalid"); + } + } + + return classpath; + } + + @Override + public void parse(InputStream is, String name) { + //@todo - wait until all plugin classes are loading until running constructors? + try { + Class pluginClass = pluginCompiler.compile(name); + Constructor pluginConstructor = pluginClass.getConstructor(World.class, + PluginContext.class); + + pluginConstructor.newInstance(world, context); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + @Override + public void setContext(PluginContext context) { + this.context = context; + } + + @Override + public void clear() { + + } + + @Override + public void report(@NotNull CompilerMessageSeverity severity, @NotNull String message, + @NotNull CompilerMessageLocation location) { + if (severity.isError()) { + logger.log(Level.SEVERE, String.format("%s:%s-%s: %s", location.getPath(), location.getLine(), + location.getColumn(), message)); + } + } + + @Override + public boolean hasErrors() { + return false; + } + +} diff --git a/game/src/main/org/apollo/game/plugin/PluginManager.java b/game/src/main/org/apollo/game/plugin/PluginManager.java index acf980d0..2e498785 100644 --- a/game/src/main/org/apollo/game/plugin/PluginManager.java +++ b/game/src/main/org/apollo/game/plugin/PluginManager.java @@ -146,7 +146,7 @@ public final class PluginManager { Map plugins = createMap(findPlugins()); Set started = new HashSet<>(); - PluginEnvironment env = new RubyPluginEnvironment(world); // TODO isolate plugins if possible in the future! + PluginEnvironment env = new KotlinPluginEnvironment(world); // TODO isolate plugins if possible in the future! env.setContext(context); for (PluginMetaData plugin : plugins.values()) {