First draft of KotlinPluginEnvironment

This commit is contained in:
Gary Tierney
2017-05-26 00:51:51 +01:00
parent 3ae6a1b290
commit 3403c0a2d1
5 changed files with 236 additions and 1 deletions
+25
View File
@@ -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()
}
@@ -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<File>, 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<out KotlinPluginScript> {
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<out KotlinPluginScript>
} catch (e: CompilationException) {
throw KotlinPluginCompilerException("Compilation failed", e)
} finally {
Disposer.dispose(rootDisposable)
}
}
}
class KotlinPluginCompilerException(message: String, cause: Throwable? = null) : Exception(message, cause) {
}
@@ -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 <T : Message> on(type: () -> KClass<T>): KotlinMessageHandler<T> {
return KotlinMessageHandler(world, context, type.invoke())
}
}
class KotlinMessageHandler<T : Message>(val world: World, val context: PluginContext, val type: KClass<T>) : MessageHandler<T>(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<T> {
this.predicate = predicate
return this
}
fun then(function: T.(Player) -> Unit) {
this.function = function
this.context.addMessageHandler(type.java, this)
}
}
@@ -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<File> 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<File> 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<? extends KotlinPluginScript> pluginClass = pluginCompiler.compile(name);
Constructor<? extends KotlinPluginScript> 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;
}
}
@@ -146,7 +146,7 @@ public final class PluginManager {
Map<String, PluginMetaData> plugins = createMap(findPlugins());
Set<PluginMetaData> 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()) {