Skip to content

Commit

Permalink
feat: logging and code handling
Browse files Browse the repository at this point in the history
  • Loading branch information
jenspots committed Apr 19, 2024
1 parent ab35208 commit b2f24f4
Show file tree
Hide file tree
Showing 10 changed files with 211 additions and 53 deletions.
4 changes: 4 additions & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,12 @@ repositories {

dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.0")

// RDF dependencies.
implementation("org.apache.jena:apache-jena-libs:5.0.0")
implementation("org.apache.jena:jena-arq:5.0.0")

// Initialize testing.
testImplementation("org.jetbrains.kotlin:kotlin-test")
}

Expand Down
22 changes: 15 additions & 7 deletions src/main/kotlin/Configuration.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,41 +3,46 @@ package technology.idlab
import kotlinx.coroutines.async
import kotlinx.coroutines.runBlocking
import org.apache.jena.rdf.model.ModelFactory
import org.apache.jena.riot.Lang
import org.apache.jena.riot.RDFDataMgr
import org.apache.jena.shacl.ShaclValidator
import org.apache.jena.shacl.ValidationReport
import kotlin.system.exitProcess
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<Any>)

/** Shared logger object. */
private val logger = createLogger();

/** Processors described in the config. */
private val processors: MutableMap<String, Processor> = mutableMapOf()

/** Concrete functions in the pipeline, also known as steps. */
private val steps: MutableList<Step> = 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()) {
println("ERROR: Configuration does not conform to SHACL rules.")
RDFDataMgr.write(System.out, report.model, Lang.TTL)
exitProcess(-1)
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")))
Expand All @@ -48,10 +53,13 @@ class Configuration(configPath: String) {
* Execute all processors in the configuration in parallel, and block until
* all are done.
*/
fun executeSync() =
fun executeSync() {
logger.info("Executing pipeline")
runBlocking {
steps.map {
async { it.processor.executeSync(it.arguments) }
}.map { it.await() }
}
logger.info("Pipeline executed successfully")
}
}
9 changes: 5 additions & 4 deletions src/main/kotlin/Main.kt
Original file line number Diff line number Diff line change
@@ -1,18 +1,19 @@
package technology.idlab

import java.io.File
import kotlin.system.exitProcess

fun main(args: Array<String>) {
// Parse arguments.
if (args.size < 2) {
if (args.size != 1) {
println("Usage: jvm-runner <config>")
exitProcess(-1)
}

// Parse and load the configuration.
val path = args[1]
println("Loading configuration from $path")
val config = Configuration(path)
val relativePath = args[0]
val absolutePath = File(relativePath).absoluteFile
val config = Configuration(absolutePath.toString())

// Execute all functions.
config.executeSync()
Expand Down
51 changes: 9 additions & 42 deletions src/main/kotlin/Processor.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@ 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 java.io.File
import java.lang.reflect.Method
import javax.tools.ToolProvider
import kotlin.system.exitProcess

class Processor(
Expand All @@ -19,6 +20,7 @@ class Processor(
private val argumentTypes: List<String> = listOf("java.lang.String"),
) {
// Runtime objects.
private val logger = createLogger();
private val instance: Any
private val method: Method

Expand Down Expand Up @@ -100,48 +102,13 @@ class Processor(
init {
// Parse the source code.
val file = File(path)
JavaCodeHandler().compile(file)

// Initialize the compiler.
val compiler = ToolProvider.getSystemJavaCompiler()
if (compiler == null) {
println("ERROR: No Java compiler found.")
exitProcess(-1)
}

// Configure the compiler.
val fileManager = compiler.getStandardFileManager(null, null, null)
val compilationUnits =
fileManager.getJavaFileObjectsFromFiles(
listOf(file),
)
val task =
compiler.getTask(
null,
fileManager,
null,
null,
null,
compilationUnits,
)

// Execute compilation.
val success = task.call()
if (!success) {
println("ERROR: Compilation failed.")
exitProcess(-1)
}

// Load the compiled class, call the constructor to create a new object.
val compiledClass = Class.forName(this.targetClass)
val constructor = compiledClass.getConstructor()
this.instance = constructor.newInstance()

// Retrieve the method based on the argument types.
val argumentTypes: Array<Class<*>> =
argumentTypes.map {
Class.forName(it)
}.toTypedArray()
this.method = compiledClass.getMethod(this.targetMethod, *argumentTypes)
// 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)
}

/**
Expand Down
51 changes: 51 additions & 0 deletions src/main/kotlin/compiler/CodeHandler.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
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 open 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<String>): Array<Class<*>> {
return input.map {
Class.forName(it)
}.toTypedArray()
}
}

31 changes: 31 additions & 0 deletions src/main/kotlin/compiler/FileClassLoader.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package technology.idlab.compiler

import technology.idlab.logging.createLogger
import technology.idlab.logging.fatal
import java.io.File

class FileClassLoader(private val directory: String) : ClassLoader() {
private val logger = createLogger()

override fun findClass(name: String): Class<*> {
// Define the path to the class file.
val path = "$directory/${name.replace('.', File.separatorChar)}.class"

// 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()

// Define and return the class.
return try {
defineClass(name, bytes, 0, bytes.size)
} catch (e: ClassFormatError) {
logger.fatal("Failed to load class $name")
}
}
}
27 changes: 27 additions & 0 deletions src/main/kotlin/compiler/JavaCodeHandler.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package technology.idlab.compiler

import technology.idlab.logging.createLogger
import technology.idlab.logging.fatal
import java.io.File
import javax.tools.JavaCompiler
import javax.tools.ToolProvider

class JavaCodeHandler : CodeHandler() {
override val logger = createLogger()
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 compilationUnits = fileManager.getJavaFileObjectsFromFiles(listOf(file))
val options = listOf("-d", outputDirectory)
val task = compiler.getTask(null, 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")
}
}
25 changes: 25 additions & 0 deletions src/main/kotlin/logging/BasicFormatter.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import java.util.*
import java.util.logging.Formatter

class BasicFormatter(): Formatter() {
override fun format(record: java.util.logging.LogRecord): String {
// Parse date and time.
val instant = Date(record.millis).toInstant()
val instantTimezone = instant.atZone(TimeZone.getDefault().toZoneId())
val isoString = instantTimezone.format(java.time.format.DateTimeFormatter.ISO_LOCAL_TIME)
val time = isoString.padEnd(12, '0');

// Parse thread.
val thread = "[${record.longThreadID}]".padEnd(6, ' ');
// Parse level.
val level = record.level.toString().padEnd(7, ' ');

// Parse class and method.
val className = record.sourceClassName.split(".").last();
val location = "${className}::${record.sourceMethodName}"
.padEnd(30, ' ');

// Output the result as a single string.
return "$time $thread $level $location ${record.message}\n"
}
}
33 changes: 33 additions & 0 deletions src/main/kotlin/logging/Extension.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
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)
}
11 changes: 11 additions & 0 deletions src/main/kotlin/logging/StandardOutput.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package technology.idlab.logging

import BasicFormatter
import java.util.logging.ConsoleHandler

class StandardOutput(): ConsoleHandler() {
init {
this.formatter = BasicFormatter()
this.setOutputStream(System.out)
}
}

0 comments on commit b2f24f4

Please sign in to comment.