Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Move API to autograder-api & dynamic loading of autograder-core and autograder-extra #568

Merged
merged 15 commits into from
Jul 14, 2024
Merged
2 changes: 1 addition & 1 deletion .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -58,4 +58,4 @@ jobs:
if: startsWith(github.ref, 'refs/tags/')
with:
body: 'Minor Release (mostly for the automatic download of the jar file)'
files: autograder-cmd/target/autograder-cmd.jar
files: autograder-cmd/target/autograder-cmd.jar
42 changes: 42 additions & 0 deletions autograder-api/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">

<parent>
<groupId>de.firemage.autograder</groupId>
<artifactId>autograder-parent</artifactId>
<version>0.5.41</version>
</parent>
<modelVersion>4.0.0</modelVersion>

<artifactId>autograder-api</artifactId>
<name>autograder-api</name>
<description>Frontend API for the autograder</description>
<url>https://github.com/Feuermagier/autograder/autograder-api</url>

<dependencies>
<dependency>
<groupId>net.xyzsd.fluent</groupId>
<artifactId>fluent-base</artifactId>
<version>${fluent.version}</version>
</dependency>

<dependency>
<groupId>org.reflections</groupId>
<artifactId>reflections</artifactId>
</dependency>

<!-- Config -->
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-yaml</artifactId>
<version>${jackson.version}</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>${jackson.version}</version>
</dependency>
</dependencies>
</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package de.firemage.autograder.api;

import java.nio.file.Path;

public interface AbstractCodePosition {
Path path();
int startLine();

int endLine();

int startColumn();

int endColumn();
String readSourceFile();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package de.firemage.autograder.api;

import java.io.IOException;
import java.nio.file.Path;
import java.util.List;
import java.util.Locale;
import java.util.function.Consumer;

public interface AbstractLinter {
List<? extends AbstractProblem> checkFile(Path file, JavaVersion version, CheckConfiguration checkConfiguration, Consumer<Translatable> statusConsumer) throws LinterException, IOException;
Luro02 marked this conversation as resolved.
Show resolved Hide resolved
String translateMessage(Translatable translatable);

static Builder builder(Locale locale) {
return new Builder(locale);
}

class Builder {
private final Locale locale;
private AbstractTempLocation tempLocation;
private int threads;
private ClassLoader classLoader;
private int maxProblemsPerCheck = -1;

private Builder(Locale locale) {
this.locale = locale;
}

public Builder tempLocation(AbstractTempLocation tempLocation) {
this.tempLocation = tempLocation;
return this;
}

public AbstractTempLocation getTempLocation() {
return tempLocation;
}

public Builder threads(int threads) {
this.threads = threads;
return this;
}

public int getThreads() {
return threads;
}

public Builder maxProblemsPerCheck(int maxProblemsPerCheck) {
this.maxProblemsPerCheck = maxProblemsPerCheck;
return this;
}

public int getMaxProblemsPerCheck() {
return maxProblemsPerCheck;
}

public Builder classLoader(ClassLoader classLoader) {
this.classLoader = classLoader;
return this;
}

public ClassLoader getClassLoader() {
return classLoader;
}

public Locale getLocale() {
return locale;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package de.firemage.autograder.api;

public interface AbstractProblem {
String getCheckName();
Translatable getLinterName();
Translatable getExplanation();
String getDisplayLocation();
AbstractCodePosition getPosition();
String getType();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package de.firemage.autograder.api;

public interface AbstractProblemType {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package de.firemage.autograder.api;

import java.io.Closeable;
import java.io.IOException;
import java.io.Serializable;
import java.nio.file.Path;

public interface AbstractTempLocation extends Closeable, Serializable {
AbstractTempLocation createTempDirectory(String prefix) throws IOException;
Path createTempFile(String name) throws IOException;
Path toPath();
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package de.firemage.autograder.core;
package de.firemage.autograder.api;

import java.io.IOException;
import java.nio.file.Files;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
package de.firemage.autograder.core;
package de.firemage.autograder.api;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
import de.firemage.autograder.api.loader.ImplementationBinder;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;

public record CheckConfiguration(List<ProblemType> problemsToReport, List<String> excludedClasses) {
public record CheckConfiguration(List<? extends AbstractProblemType> problemsToReport, List<String> excludedClasses) {
public static CheckConfiguration empty() {
return new CheckConfiguration(List.of(), List.of());
}
Expand All @@ -21,12 +23,19 @@ public static CheckConfiguration fromConfigString(String configString) throws IO
if (!configString.contains("problemsToReport") && configString.startsWith("[")) {
configString = "problemsToReport: " + configString;
}
var config = new ObjectMapper(new YAMLFactory()).readValue(configString, CheckConfiguration.class);

// Tell Jackson how to instantiate AbstractProblemType
var coreModule = new SimpleModule("autograder-core");
coreModule.addAbstractTypeMapping(AbstractProblemType.class, new ImplementationBinder<>(AbstractProblemType.class).findImplementation());

var mapper = new ObjectMapper(new YAMLFactory());
mapper.registerModule(coreModule);
var config = mapper.readValue(configString, CheckConfiguration.class);
config.validate();
return config;
}

public static CheckConfiguration fromProblemTypes(List<ProblemType> problemsToReport) {
public static CheckConfiguration fromProblemTypes(List<? extends AbstractProblemType> problemsToReport) {
return new CheckConfiguration(problemsToReport, List.of());
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package de.firemage.autograder.core;
package de.firemage.autograder.api;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package de.firemage.autograder.core.compiler;
package de.firemage.autograder.api;

import java.util.Arrays;
import java.util.Comparator;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package de.firemage.autograder.core;
package de.firemage.autograder.api;

public class LinterConfigurationException extends LinterException {
public LinterConfigurationException(String message) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package de.firemage.autograder.core;
package de.firemage.autograder.api;

public class LinterException extends Exception {
public LinterException() {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package de.firemage.autograder.api;

import java.nio.file.Path;

public interface PathLike {
Path toPath();
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package de.firemage.autograder.core;
package de.firemage.autograder.api;

import fluent.bundle.FluentBundle;

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
package de.firemage.autograder.api.loader;

import de.firemage.autograder.api.AbstractLinter;
import de.firemage.autograder.api.AbstractProblemType;
import de.firemage.autograder.api.AbstractTempLocation;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.FileOutputStream;
import java.io.IOException;
import java.net.URL;
import java.net.URLClassLoader;
import java.net.URLConnection;
import java.nio.channels.Channels;
import java.nio.channels.ReadableByteChannel;
import java.nio.file.Path;
import java.nio.file.Files;

public class AutograderLoader {
private static final String AUTOGRADER_DOWNLOAD_URL = "https://github.com/Feuermagier/autograder/releases/latest/download/autograder-cmd.jar";
private static final String AUTOGRADER_RELEASE_PATH = "https://github.com/Feuermagier/autograder/releases/latest";

private static final Logger LOG = LoggerFactory.getLogger(AutograderLoader.class);

private static URLClassLoader autograderClassLoader = null;
private static String currentTag = null;
private static Path downloadedAutograderPath = null;

public static void loadFromFile(Path autograderPath) throws IOException {
loadAutograderIntoProcess(autograderPath);
}

public static void loadFromGithubWithExtraChecks() throws IOException {
String tag = getAutograderVersionTag();
if (downloadedAutograderPath == null || !downloadedAutograderPath.getFileName().startsWith(tag)) {
downloadAutograderRelease(tag);
}
currentTag = tag;
loadAutograderIntoProcess(downloadedAutograderPath);
}

public static boolean isCurrentVersionLoaded() throws IOException {
if (downloadedAutograderPath == null) {
return true;
}

return getAutograderVersionTag().equals(currentTag);
}

public static boolean isAutograderLoaded() {
return autograderClassLoader != null;
}

public static AbstractLinter instantiateLinter(AbstractLinter.Builder builder) {
return new ImplementationBinder<>(AbstractLinter.class)
.param(AbstractLinter.Builder.class, builder)
.classLoader(autograderClassLoader)
.instantiate();
}

public static AbstractTempLocation instantiateTempLocation(Path path) {
return new ImplementationBinder<>(AbstractTempLocation.class)
.param(Path.class, path)
.classLoader(autograderClassLoader)
.instantiate();
}

public static AbstractTempLocation instantiateTempLocation() {
return new ImplementationBinder<>(AbstractTempLocation.class)
.classLoader(autograderClassLoader)
.instantiate();
}

public static AbstractProblemType convertProblemType(String problemType) {
return new ImplementationBinder<>(AbstractProblemType.class)
.param(String.class, problemType)
.classLoader(autograderClassLoader)
.callStatic("fromString", AbstractProblemType.class);
}

private static String getAutograderVersionTag() throws IOException {
URLConnection connection = new URL(AUTOGRADER_RELEASE_PATH).openConnection();
connection.connect();
// Open stream to force redirect to the latest release
try (var inputStream = connection.getInputStream()) {
String[] components = connection.getURL().getFile().split("/");
return components[components.length - 1];
}
}

private static void downloadAutograderRelease(String tag) throws IOException {
Path targetPath = Files.createTempFile(tag + "_autograder_jar", ".jar");
LOG.info("Downloading autograder JAR with version/tag {} to {}", tag, targetPath.toAbsolutePath());
Files.deleteIfExists(targetPath);
Files.createFile(targetPath);
URL url = new URL(AUTOGRADER_DOWNLOAD_URL);
ReadableByteChannel channel = Channels.newChannel(url.openStream());
try (var outStream = new FileOutputStream(targetPath.toFile())) {
outStream.getChannel().transferFrom(channel, 0, Long.MAX_VALUE);
}
downloadedAutograderPath = targetPath;
}

private static void loadAutograderIntoProcess(Path jar) throws IOException {
if (autograderClassLoader != null) {
throw new IllegalStateException("Autograder already loaded. Restart the process to load a new version.");
}

if (!Files.exists(jar)) {
throw new IOException("Autograder JAR not found at " + jar.toAbsolutePath());
}

LOG.info("Loading autograder JAR from {}", jar.toAbsolutePath());
autograderClassLoader = new URLClassLoader(new URL[]{jar.toUri().toURL()}, AutograderLoader.class.getClassLoader());
}
}
Loading
Loading