From 6f607909c63be92d2c45f612ad9499039174c08b Mon Sep 17 00:00:00 2001 From: Jens Pots Date: Tue, 23 Apr 2024 13:22:15 +0200 Subject: [PATCH] refactor: restructured repository --- src/main/java/Template.java | 7 ++ src/main/kotlin/Configuration.kt | 65 ---------- src/main/kotlin/Main.kt | 9 +- src/main/kotlin/Processor.kt | 119 ------------------ src/main/kotlin/compiler/CodeHandler.kt | 51 -------- src/main/kotlin/compiler/Compiler.kt | 61 +++++++++ src/main/kotlin/compiler/FileClassLoader.kt | 22 ++-- src/main/kotlin/compiler/JavaCodeHandler.kt | 47 ------- src/main/kotlin/compiler/MemoryClassLoader.kt | 21 ++++ src/main/kotlin/compiler/MemoryFileManager.kt | 51 ++++++++ src/main/kotlin/logging/Extension.kt | 33 ----- src/main/kotlin/logging/Logger.kt | 66 ++++++++++ .../{BasicFormatter.kt => PrettyFormatter.kt} | 14 ++- src/main/kotlin/logging/StandardOutput.kt | 11 -- src/main/kotlin/runner/Parser.kt | 96 ++++++++++++++ src/main/kotlin/runner/Pipeline.kt | 40 ++++++ src/main/kotlin/runner/Processor.kt | 25 ++++ src/main/kotlin/runner/Stage.kt | 7 ++ src/main/kotlin/util/Reflect.kt | 80 ++++++++++++ src/main/resources/Template.java | 7 -- src/test/kotlin/TestGreeting.kt | 6 +- 21 files changed, 483 insertions(+), 355 deletions(-) create mode 100644 src/main/java/Template.java delete mode 100644 src/main/kotlin/Configuration.kt delete mode 100644 src/main/kotlin/Processor.kt delete mode 100644 src/main/kotlin/compiler/CodeHandler.kt create mode 100644 src/main/kotlin/compiler/Compiler.kt delete mode 100644 src/main/kotlin/compiler/JavaCodeHandler.kt create mode 100644 src/main/kotlin/compiler/MemoryClassLoader.kt create mode 100644 src/main/kotlin/compiler/MemoryFileManager.kt delete mode 100644 src/main/kotlin/logging/Extension.kt create mode 100644 src/main/kotlin/logging/Logger.kt rename src/main/kotlin/logging/{BasicFormatter.kt => PrettyFormatter.kt} (78%) delete mode 100644 src/main/kotlin/logging/StandardOutput.kt create mode 100644 src/main/kotlin/runner/Parser.kt create mode 100644 src/main/kotlin/runner/Pipeline.kt create mode 100644 src/main/kotlin/runner/Processor.kt create mode 100644 src/main/kotlin/runner/Stage.kt create mode 100644 src/main/kotlin/util/Reflect.kt delete mode 100644 src/main/resources/Template.java diff --git a/src/main/java/Template.java b/src/main/java/Template.java new file mode 100644 index 0000000..d234802 --- /dev/null +++ b/src/main/java/Template.java @@ -0,0 +1,7 @@ +import java.util.logging.Logger; + +import static technology.idlab.logging.LoggerKt.createLogger; + +public class Template { + protected Logger logger = createLogger(); +} diff --git a/src/main/kotlin/Configuration.kt b/src/main/kotlin/Configuration.kt deleted file mode 100644 index 9b73deb..0000000 --- a/src/main/kotlin/Configuration.kt +++ /dev/null @@ -1,65 +0,0 @@ -package technology.idlab - -import kotlinx.coroutines.async -import kotlinx.coroutines.runBlocking -import org.apache.jena.rdf.model.ModelFactory -import org.apache.jena.shacl.ShaclValidator -import org.apache.jena.shacl.ValidationReport -import technology.idlab.logging.createLogger -import technology.idlab.logging.fatal - -class Configuration(configPath: String) { - /** A step is simply a concrete execution of a function. */ - data class Step(val processor: Processor, val arguments: List) - - /** Shared logger object. */ - private val logger = createLogger() - - /** Processors described in the config. */ - private val processors: MutableMap = mutableMapOf() - - /** Concrete functions in the pipeline, also known as steps. */ - private val steps: MutableList = mutableListOf() - - init { - logger.info("Parsing configuration from $configPath to graph") - - // Initialize the RDF model. - val model = ModelFactory.createDefaultModel() - model.read(configPath, "TURTLE") - val graph = model.graph - - // Parse correctness using SHACL. - logger.info("Validating configuration using SHACL") - val report: ValidationReport = - ShaclValidator.get().validate( - graph, - graph, - ) - - if (!report.conforms()) { - logger.fatal("Configuration does not conform to SHACL rules") - } - - // Parse processors from the model and initialize steps. - logger.info("Extracting processors from configuration") - Processor.fromModel(model).forEach { - processors[it.name] = it - steps.add(Step(it, listOf("JVM Runner"))) - } - } - - /** - * Execute all processors in the configuration in parallel, and block until - * all are done. - */ - fun executeSync() { - logger.info("Executing pipeline") - runBlocking { - steps.map { - async { it.processor.executeSync(it.arguments) } - }.map { it.await() } - } - logger.info("Pipeline executed successfully") - } -} diff --git a/src/main/kotlin/Main.kt b/src/main/kotlin/Main.kt index 9781d9b..92a41b9 100644 --- a/src/main/kotlin/Main.kt +++ b/src/main/kotlin/Main.kt @@ -1,5 +1,6 @@ package technology.idlab +import technology.idlab.runner.Pipeline import java.io.File import kotlin.system.exitProcess @@ -11,10 +12,10 @@ fun main(args: Array) { } // Parse and load the configuration. - val relativePath = args[0] - val absolutePath = File(relativePath).absoluteFile - val config = Configuration(absolutePath.toString()) + val configPath = args[0] + val config = File(configPath) + val pipeline = Pipeline(config) // Execute all functions. - config.executeSync() + pipeline.executeSync() } diff --git a/src/main/kotlin/Processor.kt b/src/main/kotlin/Processor.kt deleted file mode 100644 index 2d78cfe..0000000 --- a/src/main/kotlin/Processor.kt +++ /dev/null @@ -1,119 +0,0 @@ -package technology.idlab - -import org.apache.jena.query.QueryExecutionFactory -import org.apache.jena.query.QueryFactory -import org.apache.jena.query.QuerySolution -import org.apache.jena.rdf.model.Model -import technology.idlab.compiler.JavaCodeHandler -import technology.idlab.logging.createLogger -import technology.idlab.logging.fatal -import java.io.File -import java.lang.reflect.Method -import kotlin.system.exitProcess - -class Processor( - val name: String, - private val path: String, - private val targetClass: String, - private val targetMethod: String, - private val language: String, - private val argumentNames: List, - private val argumentTypes: List = listOf("java.lang.String"), -) { - // Runtime objects. - private val instance: Any - private val method: Method - - companion object { - private val logger = createLogger() - - /** - * Read the SPARQL query from the resources folder and return it as a string. - */ - private fun readQuery(): String { - val resource = object {}.javaClass.getResource("/processors.sparql") - logger.info("Reading SPARQL query from ${resource?.path}") - - return resource?.readText() - ?: logger.fatal("Could not read ${resource?.path}") - } - - /** - * Parse a query solution into a processor object. - */ - private fun fromQuerySolution(sol: QuerySolution): Processor { - val name = sol["processor"].toString() - val file = sol["file"].toString().drop(7) - val targetClass = sol["class"].toString() - val targetMethod = sol["method"].toString() - val language = sol["language"].toString() - - // Check language constraints. - if (language != "java") { - println("ERROR: Unsupported language : \"${language}\".") - exitProcess(-1) - } - - // Parse the argument types. - val argumentNamesShuffled = sol["names"].toString().split(";") - val argumentIndices = - sol["indices"].toString().split( - ";", - ).map { it.toInt() } - val argumentNames = MutableList(argumentNamesShuffled.size) { "" } - for (i in argumentIndices.indices) { - argumentNames[argumentIndices[i]] = argumentNamesShuffled[i] - } - - return Processor( - name, - file, - targetClass, - targetMethod, - language, - argumentNames, - ) - } - - /** - * Parse an RDF model into a list of processors. - */ - fun fromModel(model: Model): List { - // Parse query from file. - val query = QueryFactory.create(readQuery()) - val result: MutableList = mutableListOf() - - // Go over the resulting solutions and initialize a processor - // object. - logger.info("Executing SPARQL query against model") - QueryExecutionFactory.create(query, model).use { - val results = it.execSelect() - while (results.hasNext()) { - val solution = results.nextSolution() - result.add(fromQuerySolution(solution)) - } - } - - return result - } - } - - init { - // Parse the source code. - val file = File(path) - JavaCodeHandler().compile(file) - - // Read the resulting compiled class. - val clazz = JavaCodeHandler().load(targetClass) - val arguments = JavaCodeHandler().mapToType(argumentTypes) - this.instance = JavaCodeHandler().createInstance(clazz) - this.method = clazz.getMethod(this.targetMethod, *arguments) - } - - /** - * Execute the processor synchronously. May not return. - */ - fun executeSync(arguments: List) { - method.invoke(instance, *arguments.toTypedArray()) - } -} diff --git a/src/main/kotlin/compiler/CodeHandler.kt b/src/main/kotlin/compiler/CodeHandler.kt deleted file mode 100644 index 1429892..0000000 --- a/src/main/kotlin/compiler/CodeHandler.kt +++ /dev/null @@ -1,51 +0,0 @@ -package technology.idlab.compiler - -import technology.idlab.logging.createLogger -import technology.idlab.logging.fatal -import java.io.File - -abstract class CodeHandler { - protected val outputDirectory = "out" - protected val logger = createLogger() - private val classLoader: ClassLoader = FileClassLoader(outputDirectory) - - /** - * Compile a single file using the language's compiler and write it to the - * output directory. - */ - abstract fun compile(file: File) - - /** - * Load a class with specific name as a variable. - */ - fun load(name: String): Class<*> { - return classLoader.loadClass(name) - } - - /** - * Initialize a class using its default constructor. - */ - fun createInstance(clazz: Class<*>): Any { - val constructor = - try { - clazz.getConstructor() - } catch (e: NoSuchMethodException) { - logger.fatal("Could not find constructor for ${clazz.name}") - } - - return try { - constructor.newInstance() - } catch (e: Exception) { - logger.fatal("Could not instantiate ${clazz.name}") - } - } - - /** - * Given a list of strings, map them to their respective classes. - */ - fun mapToType(input: List): Array> { - return input.map { - Class.forName(it) - }.toTypedArray() - } -} diff --git a/src/main/kotlin/compiler/Compiler.kt b/src/main/kotlin/compiler/Compiler.kt new file mode 100644 index 0000000..aa253db --- /dev/null +++ b/src/main/kotlin/compiler/Compiler.kt @@ -0,0 +1,61 @@ +package technology.idlab.compiler + +import technology.idlab.logging.createLogger +import technology.idlab.logging.fatal +import java.io.File +import java.io.PrintWriter +import javax.tools.DiagnosticCollector +import javax.tools.JavaFileObject +import javax.tools.ToolProvider + +class Compiler { + private val logger = createLogger() + private val compiler = + ToolProvider.getSystemJavaCompiler() ?: logger.fatal( + "No Java compiler found.", + ) + + fun compile(file: File): ByteArray { + logger.info(file.absolutePath) + + // Prepare compilation. + val files = listOf(file) + val fileManager = compiler.getStandardFileManager(null, null, null) + val results = MemoryFileManager(fileManager) + val compilationUnits = fileManager.getJavaFileObjectsFromFiles(files) + val diagnosticCollector = DiagnosticCollector() + + // Create a compilation task. + val task = + compiler.getTask( + PrintWriter(System.out), + results, + diagnosticCollector, + listOf("-d", ""), + null, + compilationUnits, + ) + + // Execute compilation. + val success = task.call() + + // Write diagnostics to logger. + diagnosticCollector.diagnostics.forEach { + logger.info(it.toString()) + } + + if (!success) { + logger.fatal("Failure when compiling $file") + } + + return results.get(file.nameWithoutExtension) + } + + companion object { + private val instance = Compiler() + + fun compile(file: File): ByteArray { + return instance.compile(file) + } + } +} diff --git a/src/main/kotlin/compiler/FileClassLoader.kt b/src/main/kotlin/compiler/FileClassLoader.kt index 4eb7807..545f31f 100644 --- a/src/main/kotlin/compiler/FileClassLoader.kt +++ b/src/main/kotlin/compiler/FileClassLoader.kt @@ -4,26 +4,18 @@ import technology.idlab.logging.createLogger import technology.idlab.logging.fatal import java.io.File -class FileClassLoader(private val directory: String) : ClassLoader() { +class FileClassLoader : ClassLoader() { private val logger = createLogger() - override fun findClass(name: String): Class<*> { - // Define the path to the class file. - var path = "$directory/${name.replace('.', File.separatorChar)}.class" - path = File(path).absolutePath - - // Open file pointer. - val file = File(path) - if (!file.exists()) { - logger.fatal("Failed to read file $name") - } - - // Read the file into a byte array. - logger.info("Reading $path") - val bytes = file.readBytes() + fun fromFile( + file: File, + name: String, + ): Class<*> { + logger.info("Loading ${file.absoluteFile}") // Define and return the class. return try { + val bytes = file.readBytes() defineClass(name, bytes, 0, bytes.size) } catch (e: ClassFormatError) { logger.fatal("Failed to load class $name") diff --git a/src/main/kotlin/compiler/JavaCodeHandler.kt b/src/main/kotlin/compiler/JavaCodeHandler.kt deleted file mode 100644 index cbd5e15..0000000 --- a/src/main/kotlin/compiler/JavaCodeHandler.kt +++ /dev/null @@ -1,47 +0,0 @@ -package technology.idlab.compiler - -import technology.idlab.logging.fatal -import java.io.File -import java.io.PrintWriter -import javax.tools.JavaCompiler -import javax.tools.ToolProvider - -class JavaCodeHandler : CodeHandler() { - private val compiler: JavaCompiler = - ToolProvider.getSystemJavaCompiler() ?: logger.fatal( - "No Java compiler found.", - ) - private val fileManager = compiler.getStandardFileManager(null, null, null) - - override fun compile(file: File) { - val templateUrl = - this::class.java.getResource( - "/Template.java", - ) ?: logger.fatal("Could not find template in resources") - val template = File(templateUrl.toURI()) - logger.info("Using template $template") - - val compilationUnits = - fileManager.getJavaFileObjectsFromFiles( - listOf(template, file), - ) - val options = listOf("-d", outputDirectory) - val task = - compiler.getTask( - PrintWriter(System.out), - fileManager, - null, - options, - null, - compilationUnits, - ) - - // Execute compilation. - logger.info("Compiling $file") - val success = task.call() - if (!success) { - logger.fatal("Compilation of $file failed") - } - logger.info("Compilation of $file succeeded") - } -} diff --git a/src/main/kotlin/compiler/MemoryClassLoader.kt b/src/main/kotlin/compiler/MemoryClassLoader.kt new file mode 100644 index 0000000..d27f2c0 --- /dev/null +++ b/src/main/kotlin/compiler/MemoryClassLoader.kt @@ -0,0 +1,21 @@ +package technology.idlab.compiler + +import technology.idlab.logging.createLogger +import technology.idlab.logging.fatal + +class MemoryClassLoader : ClassLoader() { + private val logger = createLogger() + + fun fromBytes( + bytes: ByteArray, + name: String, + ): Class<*> { + logger.info("Loading class $name") + + return try { + defineClass(name, bytes, 0, bytes.size) + } catch (e: ClassFormatError) { + createLogger().fatal("Failed to load class $name") + } + } +} diff --git a/src/main/kotlin/compiler/MemoryFileManager.kt b/src/main/kotlin/compiler/MemoryFileManager.kt new file mode 100644 index 0000000..926cef1 --- /dev/null +++ b/src/main/kotlin/compiler/MemoryFileManager.kt @@ -0,0 +1,51 @@ +package technology.idlab.compiler + +import technology.idlab.logging.createLogger +import technology.idlab.logging.fatal +import java.io.ByteArrayOutputStream +import java.io.OutputStream +import java.net.URI +import javax.tools.FileObject +import javax.tools.ForwardingJavaFileManager +import javax.tools.JavaFileManager +import javax.tools.JavaFileObject +import javax.tools.SimpleJavaFileObject + +/** + * A custom file manager which stores compiled classes in memory and does not + * require the need to write to disk. + */ +class MemoryFileManager( + fileManager: JavaFileManager, +) : ForwardingJavaFileManager(fileManager) { + private val results: MutableMap = HashMap() + private val logger = createLogger() + + override fun getJavaFileForOutput( + location: JavaFileManager.Location?, + className: String, + kind: JavaFileObject.Kind, + sibling: FileObject?, + ): JavaFileObject { + val uri = + URI.create( + "string:///" + className.replace('.', '/') + kind.extension, + ) + + return object : SimpleJavaFileObject(uri, kind) { + override fun openOutputStream(): OutputStream { + return object : ByteArrayOutputStream() { + override fun close() { + results[className] = this.toByteArray() + super.close() + } + } + } + } + } + + fun get(className: String): ByteArray { + logger.info("Retrieving $className") + return results[className] ?: logger.fatal("Class $className not found") + } +} diff --git a/src/main/kotlin/logging/Extension.kt b/src/main/kotlin/logging/Extension.kt deleted file mode 100644 index 09baeb9..0000000 --- a/src/main/kotlin/logging/Extension.kt +++ /dev/null @@ -1,33 +0,0 @@ -package technology.idlab.logging - -import java.util.logging.Level -import java.util.logging.Logger -import kotlin.system.exitProcess - -fun createLogger(): Logger { - val caller = Throwable().stackTrace[1] - val logger = Logger.getLogger(caller.className) - logger.addHandler(StandardOutput()) - logger.level = Level.ALL - return logger -} - -/** - * Log a message and exit the program with a status code of -1. This function - * is intended to use in a try-catch block. Since it returns Nothing, it can be - * the only expression in the catch block of an assignment. - * - * Example: - * ``` - * const x = try { - * 10 / 0 - * } catch (e: Exception) { - * logger.fatal("An error occurred: ${e.message}") - * } - * ``` - */ -fun Logger.fatal(message: String): Nothing { - val caller = Throwable().stackTrace[1] - logp(Level.SEVERE, caller.className, caller.methodName, message) - exitProcess(-1) -} diff --git a/src/main/kotlin/logging/Logger.kt b/src/main/kotlin/logging/Logger.kt new file mode 100644 index 0000000..6e23732 --- /dev/null +++ b/src/main/kotlin/logging/Logger.kt @@ -0,0 +1,66 @@ +package technology.idlab.logging + +import PrettyFormatter +import java.lang.Exception +import java.util.logging.ConsoleHandler +import java.util.logging.Level +import java.util.logging.Logger +import kotlin.system.exitProcess + +/** + * Create a new logger which uses the calling class name as the logger name. + * Messages are logged to the console using the PrettyFormatter. + */ +fun createLogger(): Logger { + val caller = Throwable().stackTrace[1] + val logger = Logger.getLogger(caller.className) + + // Output to the console. + logger.addHandler( + object : ConsoleHandler() { + init { + this.formatter = PrettyFormatter() + this.setOutputStream(System.out) + } + }, + ) + + logger.level = Level.ALL + return logger +} + +/** + * Log a message and exit the program with a status code of -1. This function + * is intended to use in a try-catch block. Since it returns Nothing, it can be + * the only expression in the catch block of an assignment. + * + * Example: + * ``` + * const x = try { + * 10 / 0 + * } catch (e: Exception) { + * logger.fatal("An error occurred: ${e.message}") + * } + * ``` + */ +fun Logger.fatal( + message: String, + exception: Exception? = null, +): Nothing { + // Log the message. + val caller = Throwable().stackTrace[1] + logp(Level.SEVERE, caller.className, caller.methodName, message) + + // Log the exception if it exists. + if (exception != null) { + logp( + Level.SEVERE, + caller.className, + caller.methodName, + exception.message, + ) + } + + // Exit the program. + exitProcess(-1) +} diff --git a/src/main/kotlin/logging/BasicFormatter.kt b/src/main/kotlin/logging/PrettyFormatter.kt similarity index 78% rename from src/main/kotlin/logging/BasicFormatter.kt rename to src/main/kotlin/logging/PrettyFormatter.kt index 0a53f25..6b1a0bf 100644 --- a/src/main/kotlin/logging/BasicFormatter.kt +++ b/src/main/kotlin/logging/PrettyFormatter.kt @@ -2,7 +2,19 @@ import java.util.Date import java.util.TimeZone import java.util.logging.Formatter -class BasicFormatter() : Formatter() { +/** + * Pretty humanized formatting for use with the JVM logging framework. + * + * Included fields are: + * - Time in ISO 8601 format. + * - Thread ID. + * - Log level. + * - Class and method. + * - Message + * + * Example: `2021-09-30T12:00:00.000Z [1] INFO Main::main Hello, world!` + */ +class PrettyFormatter : Formatter() { override fun format(record: java.util.logging.LogRecord): String { // Parse date and time. val instant = Date(record.millis).toInstant() diff --git a/src/main/kotlin/logging/StandardOutput.kt b/src/main/kotlin/logging/StandardOutput.kt deleted file mode 100644 index 708aad2..0000000 --- a/src/main/kotlin/logging/StandardOutput.kt +++ /dev/null @@ -1,11 +0,0 @@ -package technology.idlab.logging - -import BasicFormatter -import java.util.logging.ConsoleHandler - -class StandardOutput() : ConsoleHandler() { - init { - this.formatter = BasicFormatter() - this.setOutputStream(System.out) - } -} diff --git a/src/main/kotlin/runner/Parser.kt b/src/main/kotlin/runner/Parser.kt new file mode 100644 index 0000000..a09cd52 --- /dev/null +++ b/src/main/kotlin/runner/Parser.kt @@ -0,0 +1,96 @@ +package technology.idlab.runner + +import org.apache.jena.graph.Graph +import org.apache.jena.query.QueryExecutionFactory +import org.apache.jena.query.QueryFactory +import org.apache.jena.query.QuerySolution +import org.apache.jena.rdf.model.ModelFactory +import org.apache.jena.shacl.ShaclValidator +import org.apache.jena.shacl.ValidationReport +import technology.idlab.logging.createLogger +import technology.idlab.logging.fatal +import java.io.File + +/** + * Parse a solution to a Processor instance. + */ +fun QuerySolution.toProcessor(): Processor { + val logger = createLogger() + + // Retrieve the processor's attributes. + val name = this["processor"].toString() + val filePath = this["file"].toString().drop(7) + val targetClass = this["class"].toString() + val targetMethod = this["method"].toString() + + // Parse path as file. + val file = File(filePath) + if (!file.exists()) { + logger.fatal("File $filePath does not exist") + } + + // TODO: Parse argument types. + val argumentTypes = listOf("java.lang.String") + + // Return the processor. + return Processor( + name, + file, + targetClass, + targetMethod, + argumentTypes, + ) +} + +class Parser(file: File) { + private val logger = createLogger() + private val model = ModelFactory.createDefaultModel() + private val graph: Graph + + init { + // Parse the Turtle file as a model. + model.read(file.absolutePath, "TURTLE") + graph = model.graph + + // Validate it using SHACL. + logger.info("Validating configuration using SHACL") + val report: ValidationReport = + ShaclValidator.get().validate( + graph, + graph, + ) + + if (!report.conforms()) { + logger.fatal("Configuration does not conform to SHACL rules") + } + } + + fun getProcessors(): List { + val processors = mutableListOf() + + val resource = this.javaClass.getResource("/processors.sparql") + logger.info("Reading SPARQL query from ${resource?.path}") + val processorQuery = + resource?.readText() ?: logger.fatal( + "Could not read ${resource?.path}", + ) + val query = QueryFactory.create(processorQuery) + + logger.info("Executing SPARQL query against model") + QueryExecutionFactory.create(query, model).use { + val results = it.execSelect() + while (results.hasNext()) { + val solution = results.nextSolution() + val processor = solution.toProcessor() + processors.add(processor) + } + } + + return processors + } + + fun getStages(): List { + val processors = getProcessors() + return listOf(Stage(processors[0], listOf("JVM Runner"))) + } +} diff --git a/src/main/kotlin/runner/Pipeline.kt b/src/main/kotlin/runner/Pipeline.kt new file mode 100644 index 0000000..9a33bce --- /dev/null +++ b/src/main/kotlin/runner/Pipeline.kt @@ -0,0 +1,40 @@ +package technology.idlab.runner + +import kotlinx.coroutines.async +import kotlinx.coroutines.runBlocking +import technology.idlab.logging.createLogger +import java.io.File + +class Pipeline(config: File) { + private val logger = createLogger() + + /** Processors described in the config. */ + private val processors: MutableMap = mutableMapOf() + + /** Concrete functions in the pipeline, also known as steps. */ + private val stages: List + + init { + val parser = Parser(config) + val processors = parser.getProcessors() + + this.processors.putAll(processors.map { it.name to it }) + this.stages = parser.getStages() + } + + /** + * Execute all processors in the configuration in parallel, and block until + * all are done. + */ + fun executeSync() { + logger.info("Executing pipeline") + runBlocking { + stages.map { + async { it.execute() } + }.map { + it.await() + } + } + logger.info("Pipeline executed successfully") + } +} diff --git a/src/main/kotlin/runner/Processor.kt b/src/main/kotlin/runner/Processor.kt new file mode 100644 index 0000000..7793eb9 --- /dev/null +++ b/src/main/kotlin/runner/Processor.kt @@ -0,0 +1,25 @@ +package technology.idlab.runner + +import technology.idlab.util.Reflect +import java.io.File + +class Processor( + val name: String, + file: File, + targetClass: String, + targetMethod: String, + arguments: List, +) { + // Runtime objects. + private val reflect = Reflect() + private val clazz = reflect.getClassFromFile(file, targetClass) + private val instance = reflect.createInstance(clazz) + private val method = reflect.getMethod(clazz, targetMethod, arguments) + + /** + * Execute the processor synchronously. May not return. + */ + fun executeSync(arguments: List) { + method.invoke(instance, *arguments.toTypedArray()) + } +} diff --git a/src/main/kotlin/runner/Stage.kt b/src/main/kotlin/runner/Stage.kt new file mode 100644 index 0000000..3e9344c --- /dev/null +++ b/src/main/kotlin/runner/Stage.kt @@ -0,0 +1,7 @@ +package technology.idlab.runner + +data class Stage(val processor: Processor, val arguments: List) { + fun execute() { + processor.executeSync(arguments) + } +} diff --git a/src/main/kotlin/util/Reflect.kt b/src/main/kotlin/util/Reflect.kt new file mode 100644 index 0000000..0ec2e36 --- /dev/null +++ b/src/main/kotlin/util/Reflect.kt @@ -0,0 +1,80 @@ +package technology.idlab.util + +import technology.idlab.compiler.Compiler +import technology.idlab.compiler.FileClassLoader +import technology.idlab.compiler.MemoryClassLoader +import technology.idlab.logging.createLogger +import technology.idlab.logging.fatal +import java.io.File +import java.lang.reflect.Method + +class Reflect { + private val logger = createLogger() + + /** + * Parse a source of class file and load the result into memory. + */ + fun getClassFromFile( + file: File, + name: String, + ): Class<*> { + logger.info("Loading ${file.absoluteFile}") + + // Check if compilation needs to be run at runtime. + if (file.absolutePath.endsWith(".java")) { + val bytes = Compiler.compile(file) + return MemoryClassLoader().fromBytes(bytes, name) + } + + // Load from memory and return. + return FileClassLoader().fromFile(file, name) + } + + /** + * Create an instance of a class using the default constructor. + */ + fun createInstance(clazz: Class<*>): Any { + logger.info("Instantiating ${clazz.name}") + + val constructor = + try { + clazz.getConstructor() + } catch (e: NoSuchMethodException) { + logger.fatal("Could not find constructor for ${clazz.name}") + } + + return try { + constructor.newInstance() + } catch (e: Exception) { + logger.fatal("Could not instantiate ${clazz.name}") + } + } + + /** + * Retrieve a method by class, method name and argument types. + */ + fun getMethod( + clazz: Class<*>, + name: String, + args: List, + ): Method { + val arguments = + args.map { + try { + Class.forName(it) + } catch (e: ClassNotFoundException) { + logger.fatal("Could not find argument class $it") + } + }.toTypedArray() + + logger.info( + "Retrieving method $name with arguments ${args.joinToString()}", + ) + + return try { + clazz.getMethod(name, *arguments) + } catch (e: NoSuchMethodException) { + logger.fatal("Could not find method $name in ${clazz.name}") + } + } +} diff --git a/src/main/resources/Template.java b/src/main/resources/Template.java deleted file mode 100644 index dc5a48b..0000000 --- a/src/main/resources/Template.java +++ /dev/null @@ -1,7 +0,0 @@ -import java.util.logging.Logger; - -import static technology.idlab.logging.ExtensionKt.createLogger; - -public class Template { - public Logger logger = createLogger(); -} diff --git a/src/test/kotlin/TestGreeting.kt b/src/test/kotlin/TestGreeting.kt index fbf98df..9582685 100644 --- a/src/test/kotlin/TestGreeting.kt +++ b/src/test/kotlin/TestGreeting.kt @@ -1,7 +1,8 @@ import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.BeforeEach -import technology.idlab.Configuration +import technology.idlab.runner.Pipeline import java.io.ByteArrayOutputStream +import java.io.File import java.io.PrintStream import kotlin.test.Test import kotlin.test.assertTrue @@ -23,7 +24,8 @@ class TestGreeting { @Test fun testGreeting() { val path = TestGreeting::class.java.getResource("/Greeting.ttl")?.path - val config = Configuration(path.toString()) + val file = File(path!!) + val config = Pipeline(file) config.executeSync() assertTrue(outputStreamCaptor.toString().contains("Hello, JVM Runner!")) }