diff --git a/README.md b/README.md
index 2ba69568..394fe535 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,6 @@
## webrepl
-A quick and dirty REPL for JPv3's IIIF Cookbook recipes.
+A quick and dirty, single-threaded REPL for JPv3's IIIF Cookbook recipes.
### How to Build
diff --git a/pom.xml b/pom.xml
index 6e25138d..a18d47ec 100644
--- a/pom.xml
+++ b/pom.xml
@@ -46,7 +46,7 @@
0.11
0.0.0-SNAPSHOT
- 5.0.7
+ 5.0.10
webrepl
@@ -60,6 +60,8 @@
5.11.0
5.14.2
+ 4.2.2
+ 1.2.1
true
@@ -116,6 +118,18 @@
${mockito.version}
test
+
+ com.github.stefanbirkner
+ system-lambda
+ ${system.lambda.version}
+ test
+
+
+ org.awaitility
+ awaitility
+ ${awaitility.version}
+ test
+
@@ -169,7 +183,7 @@
- info.freelibrary.iiif.webrepl.Server
+ info.freelibrary.iiif.webrepl.WebRepl
@@ -241,7 +255,8 @@
maven-surefire-plugin
- ${jacoco.agent.arg} -javaagent:${org.mockito:mockito-core:jar}
+ ${jacoco.agent.arg} -javaagent:${org.mockito:mockito-core:jar}
+ --add-opens java.base/java.util=ALL-UNNAMED -Xshare:off
${test.http.port}
@@ -255,7 +270,8 @@
${test.http.port}
- --add-opens java.base/sun.nio.ch=ALL-UNNAMED --add-opens java.base/java.io=ALL-UNNAMED
+ --add-opens java.base/sun.nio.ch=ALL-UNNAMED --add-opens java.base/java.io=ALL-UNNAMED
+ -Xshare:off
@@ -396,7 +412,7 @@
freelib-parent
info.freelibrary
- 12.0.0
+ 12.0.3
diff --git a/src/main/java/info/freelibrary/iiif/webrepl/DiagConsumer.java b/src/main/java/info/freelibrary/iiif/webrepl/DiagnosticConsumer.java
similarity index 92%
rename from src/main/java/info/freelibrary/iiif/webrepl/DiagConsumer.java
rename to src/main/java/info/freelibrary/iiif/webrepl/DiagnosticConsumer.java
index 24022ac5..bcb79259 100644
--- a/src/main/java/info/freelibrary/iiif/webrepl/DiagConsumer.java
+++ b/src/main/java/info/freelibrary/iiif/webrepl/DiagnosticConsumer.java
@@ -13,7 +13,7 @@
/**
* A diagnostic consumer for handling rejected code snippets.
*/
-public class DiagConsumer implements Consumer {
+public class DiagnosticConsumer implements Consumer {
/** A delimiter for the end of problematic code. */
private static final String END = "]]";
@@ -42,7 +42,7 @@ public class DiagConsumer implements Consumer {
*
* @param aBuffer An output buffer
*/
- public DiagConsumer(final StringBuilder aBuffer) {
+ public DiagnosticConsumer(final StringBuilder aBuffer) {
myBuffer = aBuffer;
}
@@ -71,7 +71,7 @@ public void accept(final Diag aDiagnostic) {
}
// Add line numbers to what's returned
- code = StringUtils.addLineNumbers(myBuffer.toString());
+ code = StringUtils.addLineNumbers(myBuffer.toString().trim());
// Zero out the output buffer
myBuffer.setLength(0);
diff --git a/src/main/java/info/freelibrary/iiif/webrepl/EnvOptions.java b/src/main/java/info/freelibrary/iiif/webrepl/EnvOptions.java
new file mode 100644
index 00000000..caf2bc7d
--- /dev/null
+++ b/src/main/java/info/freelibrary/iiif/webrepl/EnvOptions.java
@@ -0,0 +1,42 @@
+
+package info.freelibrary.iiif.webrepl;
+
+import java.time.Duration;
+
+import org.microhttp.Options;
+
+import info.freelibrary.util.Constants;
+import info.freelibrary.util.Env;
+
+/**
+ * The server's environmental options.
+ */
+public class EnvOptions {
+
+ /** The maximum request size. */
+ private static final int DEFAULT_MAX_REQ_SIZE = 1_024 * 1_024;
+
+ /** The default port at which the server listens. */
+ private static final int DEFAULT_PORT = 8888;
+
+ /** The read buffer size. */
+ private static final int DEFAULT_READ_BUF_SIZE = 1_024 * 64;
+
+ /** The request timeout. */
+ private static final long DEFAULT_REQ_TIMEOUT = 60L;
+
+ /**
+ * Gets the environmental options.
+ *
+ * @return The configuration options
+ */
+ public Options getOpts() {
+ final int port = Env.get(Config.HTTP_PORT, DEFAULT_PORT);
+ final int reqSize = Env.get(Config.MAX_REQUEST_SIZE, DEFAULT_MAX_REQ_SIZE);
+ final int bufSize = Env.get(Config.READ_BUFFER_SIZE, DEFAULT_READ_BUF_SIZE);
+ final long timeout = (long) Env.get(Config.REQUEST_TIMEOUT, DEFAULT_REQ_TIMEOUT);
+
+ return Options.builder().withHost(Constants.INADDR_ANY).withPort(port).withMaxRequestSize(reqSize)
+ .withRequestTimeout(Duration.ofSeconds(timeout)).withReadBufferSize(bufSize).build();
+ }
+}
diff --git a/src/main/java/info/freelibrary/iiif/webrepl/Imports.java b/src/main/java/info/freelibrary/iiif/webrepl/Imports.java
new file mode 100644
index 00000000..9f5baf0c
--- /dev/null
+++ b/src/main/java/info/freelibrary/iiif/webrepl/Imports.java
@@ -0,0 +1,93 @@
+
+package info.freelibrary.iiif.webrepl;
+
+import static info.freelibrary.util.Constants.EOL;
+import static info.freelibrary.util.StringUtils.addLineNumbers;
+import static info.freelibrary.util.StringUtils.indent;
+import static java.util.stream.Collectors.joining;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.IOException;
+import java.io.StringReader;
+import java.nio.file.Files;
+import java.nio.file.Path;
+
+import info.freelibrary.util.warnings.PMD;
+import info.freelibrary.util.warnings.Sonar;
+
+/**
+ * A string of imports.
+ */
+public class Imports {
+
+ /** A label for the display of imports in the logs. */
+ private static final String LABEL = "Imports:";
+
+ /** A string of imports. */
+ private final String myImports;
+
+ /**
+ * Creates a new imports string.
+ *
+ * @throws IOException If there is trouble reading from the imports file
+ */
+ public Imports() throws IOException {
+ File importsFile = Path.of("/etc/jshell/imports.jsh").toFile();
+
+ // Check to see if we're running from the Maven build
+ if (!importsFile.exists()) {
+ importsFile = Path.of("src/main/docker/imports.jsh").toFile();
+ }
+
+ try (BufferedReader reader = Files.newBufferedReader(importsFile.toPath())) {
+ myImports = reader.lines().filter(line -> !line.isBlank()).collect(joining(EOL));
+ }
+ }
+
+ /**
+ * Gets all the imports.
+ *
+ * @return An imports string
+ */
+ @SuppressWarnings({ Sonar.SYSTEM_OUT_ERR, PMD.SYSTEM_PRINTLN })
+ public String getAll() {
+ if (myImports.isEmpty()) {
+ System.err.println();
+ } else {
+ System.err.println(EOL + LABEL);
+ System.err.println(indent(addLineNumbers(myImports), 2));
+ }
+
+ return myImports;
+ }
+
+ /**
+ * Gets all the imports that are referenced in the supplied code snippet.
+ *
+ * @param aSnippet A code snippet to check for imports
+ * @return An imports string
+ * @throws IOException if there is trouble parsing the imports
+ */
+ @SuppressWarnings({ Sonar.SYSTEM_OUT_ERR, PMD.SYSTEM_PRINTLN })
+ public String getReferenced(final String aSnippet) throws IOException {
+ final String imports;
+
+ try (BufferedReader reader = new BufferedReader(new StringReader(myImports))) {
+ imports = reader.lines().filter(line -> {
+ final String className = line.substring(line.lastIndexOf('.') + 1, line.length() - 1);
+ return aSnippet == null || aSnippet.contains(className);
+ }).collect(joining(EOL)).trim();
+ }
+
+ // The above gets imports for the user, the below shows the imported imports in the logs
+ if (imports.isEmpty()) {
+ System.err.println();
+ } else {
+ System.err.println(EOL + LABEL);
+ System.err.println(indent(addLineNumbers(imports), 2));
+ }
+
+ return imports;
+ }
+}
diff --git a/src/main/java/info/freelibrary/iiif/webrepl/ParsingError.java b/src/main/java/info/freelibrary/iiif/webrepl/ParsingError.java
new file mode 100644
index 00000000..0b20aedf
--- /dev/null
+++ b/src/main/java/info/freelibrary/iiif/webrepl/ParsingError.java
@@ -0,0 +1,34 @@
+
+package info.freelibrary.iiif.webrepl;
+
+import info.freelibrary.util.Constants;
+import info.freelibrary.util.StringUtils;
+
+/**
+ * A {@code WebRepl} parsing error.
+ */
+public class ParsingError extends Exception {
+
+ /** The message template used in constructing the exception message. */
+ static final String MESSAGE_TEMPLATE =
+ "Parsing error, but there wasn't a useful diagnostic message. Submitted source code:" + Constants.EOL +
+ Constants.EOL + "{}";
+
+ /** The {@code serialVersionUID} for the ParsingError class. */
+ private static final long serialVersionUID = 8614571124201029070L;
+
+ /**
+ * Creates a parsing error for the supplied source. This type of parsing error lacks any diagnostic information from
+ * JShell.
+ *
+ * @param aSource A source code snippet
+ */
+ public ParsingError(final String aSource) {
+ super(StringUtils.format(MESSAGE_TEMPLATE, aSource));
+ }
+
+ @Override
+ public String toString() {
+ return getMessage();
+ }
+}
diff --git a/src/main/java/info/freelibrary/iiif/webrepl/Server.java b/src/main/java/info/freelibrary/iiif/webrepl/Server.java
index 2355f716..c355cfdf 100644
--- a/src/main/java/info/freelibrary/iiif/webrepl/Server.java
+++ b/src/main/java/info/freelibrary/iiif/webrepl/Server.java
@@ -4,44 +4,32 @@
import static info.freelibrary.iiif.webrepl.Status.METHOD_NOT_ALLOWED;
import static info.freelibrary.iiif.webrepl.Status.NOT_FOUND;
import static info.freelibrary.iiif.webrepl.Status.OK;
-import static info.freelibrary.util.Constants.EMPTY;
-import static info.freelibrary.util.Constants.EOL;
-import static info.freelibrary.util.Constants.INADDR_ANY;
-import static info.freelibrary.util.Constants.SPACE;
-import static java.nio.charset.StandardCharsets.UTF_8;
+import static info.freelibrary.util.StringUtils.indent;
-import java.io.BufferedReader;
import java.io.ByteArrayOutputStream;
-import java.io.File;
import java.io.IOException;
import java.io.PrintStream;
import java.net.URISyntaxException;
import java.net.URLDecoder;
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.security.CodeSource;
-import java.time.Duration;
+import java.nio.charset.StandardCharsets;
import java.util.List;
+import java.util.Optional;
import java.util.function.Consumer;
import java.util.function.Function;
-import java.util.stream.Collectors;
+import java.util.regex.Pattern;
import org.microhttp.EventLoop;
import org.microhttp.Handler;
import org.microhttp.Header;
-import org.microhttp.Options;
import org.microhttp.Request;
import org.microhttp.Response;
-import info.freelibrary.util.Env;
+import info.freelibrary.util.Constants;
import info.freelibrary.util.StringUtils;
import info.freelibrary.util.warnings.PMD;
import info.freelibrary.util.warnings.Sonar;
-import info.freelibrary.iiif.presentation.v3.Manifest;
-
import jdk.jshell.JShell;
-import jdk.jshell.JShellException;
import jdk.jshell.Snippet;
import jdk.jshell.Snippet.Status;
import jdk.jshell.SnippetEvent;
@@ -52,18 +40,6 @@
@SuppressWarnings({ PMD.TOO_MANY_STATIC_IMPORTS, PMD.EXCESSIVE_IMPORTS })
public final class Server {
- /** The maximum request size. */
- private static final int DEFAULT_MAX_REQ_SIZE = 1_024 * 1_024;
-
- /** The default port at which the server listens. */
- private static final int DEFAULT_PORT = 8888;
-
- /** The read buffer size. */
- private static final int DEFAULT_READ_BUF_SIZE = 1_024 * 64;
-
- /** The request timeout. */
- private static final long DEFAULT_REQ_TIMEOUT = 60L;
-
/** The server's event loop. **/
private final EventLoop myEventLoop;
@@ -75,7 +51,7 @@ public final class Server {
* @throws URISyntaxException If an invalid URI is passed to the server configuration
*/
public Server() throws IOException, URISyntaxException, ClassNotFoundException {
- myEventLoop = new EventLoop(getOptions(), new Server.JPv3Handler());
+ myEventLoop = new EventLoop(new EnvOptions().getOpts(), new Server.JPv3Handler());
myEventLoop.start();
}
@@ -88,7 +64,7 @@ public Server() throws IOException, URISyntaxException, ClassNotFoundException {
* @throws URISyntaxException If an invalid URI is passed to the server configuration
*/
public Server(final Handler aHandler) throws IOException {
- myEventLoop = new EventLoop(getOptions(), aHandler);
+ myEventLoop = new EventLoop(new EnvOptions().getOpts(), aHandler);
myEventLoop.start();
}
@@ -97,7 +73,7 @@ public Server(final Handler aHandler) throws IOException {
*
* @throws InterruptedException If the application's process is interrupted
*/
- public void run() throws InterruptedException {
+ public void start() throws InterruptedException {
myEventLoop.join();
}
@@ -108,36 +84,6 @@ public void stop() {
myEventLoop.stop();
}
- /**
- * Gets the configuration of the event loop.
- *
- * @return An event loop configuration
- */
- Options getOptions() {
- final int port = Env.get(Config.HTTP_PORT, DEFAULT_PORT);
- final int reqSize = Env.get(Config.MAX_REQUEST_SIZE, DEFAULT_MAX_REQ_SIZE);
- final int bufSize = Env.get(Config.READ_BUFFER_SIZE, DEFAULT_READ_BUF_SIZE);
- final long timeout = (long) Env.get(Config.REQUEST_TIMEOUT, DEFAULT_REQ_TIMEOUT);
-
- return Options.builder().withHost(INADDR_ANY).withPort(port).withMaxRequestSize(reqSize)
- .withRequestTimeout(Duration.ofSeconds(timeout)).withReadBufferSize(bufSize).build();
- }
-
- /**
- * Runs the server.
- *
- * @param anArgsArray An array of arguments
- * @throws IOException If there is trouble starting the server
- * @throws InterruptedException If the process is interrupted before it's completed
- * @throws URISyntaxException If the Jar path cannot be converted into a URI
- * @throws ClassNotFoundException If the Jar file with the JPv3 classes cannot be found
- */
- @SuppressWarnings({ "checkstyle:UncommentedMain" })
- public static void main(final String[] anArgsArray)
- throws IOException, InterruptedException, URISyntaxException, ClassNotFoundException {
- new Server().run();
- }
-
/**
* An event handler for code evaluation requests.
*/
@@ -149,6 +95,9 @@ static class JPv3Handler implements Handler {
/** A constant for the content type header. */
private static final String CONTENT_TYPE = "Context-Type";
+ /** A regex pattern to match submission requests. */
+ private static final Pattern EDITOR_PATTERN = Pattern.compile("editor(/)?$");
+
/** An empty response body. */
private static final byte[] EMPTY_BODY = {};
@@ -158,12 +107,21 @@ static class JPv3Handler implements Handler {
/** A hard-coded snippet that will return the result of the supplied code snippet. */
private static final String MAIN_METHOD = "Jpv3Snippet.main(new String[]{});";
+ /** A constant for the status response. */
+ private static final String STATUS_LABEL = "Status: ";
+
+ /** A regex pattern to match submission requests. */
+ private static final Pattern SUBMIT_PATTERN = Pattern.compile("submit(/)?$");
+
/** The response headers that are returned for plain text responses. */
private static final List TEXT_CONTENT_TYPE = getHeaders(new Header(CONTENT_TYPE, "text/plain"));
/** A cached {@code WebResource}. */
private final byte[] myHTML;
+ /** The imports used by the Java shell environment. */
+ private final Imports myImports;
+
/** The Java shell's output stream. */
private final ByteArrayOutputStream myOutputStream;
@@ -177,100 +135,46 @@ static class JPv3Handler implements Handler {
* @throws ClassNotFoundException If the JPv3 classes cannot be found
* @throws URISyntaxException If the Jar file's path couldn't be converted into a URI
*/
- @SuppressWarnings({ Sonar.SYSTEM_OUT_ERR })
+ @SuppressWarnings({ Sonar.SYSTEM_OUT_ERR, PMD.SYSTEM_PRINTLN })
JPv3Handler() throws IOException, ClassNotFoundException, URISyntaxException {
+ this(new Imports());
+ }
+
+ /**
+ * Creates a new {@code JPv3Handler} with the supplied imports list.
+ *
+ * @param aImportsList A list of imports in string form
+ * @throws IOException If there is trouble reading the {@code WebResource}
+ * @throws ClassNotFoundException If the JPv3 classes cannot be found
+ * @throws URISyntaxException If the Jar file's path couldn't be converted into a URI
+ */
+ @SuppressWarnings({ Sonar.SYSTEM_OUT_ERR, PMD.SYSTEM_PRINTLN })
+ JPv3Handler(final Imports aImportsList) throws IOException, ClassNotFoundException, URISyntaxException {
myOutputStream = new ByteArrayOutputStream();
+ myImports = aImportsList;
+
+ // Create the shell, specifying preview features and a version that we depend on
myShell = JShell.builder().compilerOptions("--enable-preview", "--source", "21")
.out(new PrintStream(myOutputStream)).build();
// Pre-load and cache the code editor's HTML page
myHTML = new WebResource("index.html").getBytes();
- // Check that all the imports can be loaded successfully
- myShell.eval(getImports()).stream().filter(event -> !Snippet.Status.VALID.equals(event.status()))
+ // Check that all the imports can be loaded successfully (i.e., that our classpath is current)
+ myShell.eval(myImports.getAll()).stream().filter(event -> !Status.VALID.equals(event.status()))
.map((Function super SnippetEvent, Status>) SnippetEvent::status).forEach(System.err::println);
- // Load the JPv3 classes so the imports have something to load
- myShell.addToClasspath(getJarClasspath());
+ // Just add a new line to distinguish between the startup import load and what follows
+ System.err.println();
}
@Override
- @SuppressWarnings({ PMD.COGNITIVE_COMPLEXITY, PMD.SYSTEM_PRINTLN, Sonar.COGNITIVE_COMPLEXITY,
- Sonar.SYSTEM_OUT_ERR })
public void handle(final Request aRequest, final Consumer aCallback) {
- final String uri = aRequest.uri();
- final Response response;
-
- switch (aRequest.method()) {
- case "POST" -> {
- System.err.println(aRequest.method() + SPACE + uri);
-
- if (uri.endsWith("submit") || uri.endsWith("submit/")) {
- final StringBuilder submission = new StringBuilder();
-
- submission.append(decodeSubmission(aRequest.body()));
- System.err.println("body: " + submission.toString());
-
- try {
- myShell.eval(getCode(submission.toString().trim())).forEach(event -> {
- switch (event.status()) {
- case VALID -> {
- // The submitted code is valid, so get its result to write out
- final List results = myShell.eval(MAIN_METHOD);
-
- results.forEach(output -> {
- if (Snippet.Status.VALID.equals(output.status())) {
- System.out.println(output.value());
- }
- });
- }
- case REJECTED -> {
- final Snippet snippet = event.snippet();
- final StringBuilder buffer = new StringBuilder(snippet.source());
-
- // Just check one at a time, and let the editor iterate
- myShell.diagnostics(snippet).findFirst().ifPresentOrElse(
- new DiagConsumer(buffer), () -> buffer.delete(0, buffer.length())
- .append("Parsing error, but diagnostics were not found"));
-
- try {
- myOutputStream.write(buffer.toString().getBytes(UTF_8));
- } catch (final IOException details) {
- System.err.println(details);
- }
- }
- default -> {
- final JShellException exception = event.exception();
-
- if (exception != null) {
- System.err.println(exception);
- System.out.println(exception);
- }
- }
- }
- });
- } catch (final IOException details) {
- System.err.println(details);
- System.out.println(details);
- }
-
- response = getResponse(OK, TEXT_CONTENT_TYPE, myOutputStream.toString().getBytes(UTF_8));
- myOutputStream.reset(); // After writing it to the browser, zero out its contents
- } else {
- response = getResponse(NOT_FOUND, TEXT_CONTENT_TYPE, EMPTY_BODY);
- }
- }
- case "GET" -> {
- System.err.println(aRequest.method() + SPACE + uri);
-
- if (uri.endsWith("editor") || uri.endsWith("editor/")) {
- response = getResponse(OK, HTML_CONTENT_TYPE, myHTML);
- } else {
- response = getResponse(NOT_FOUND, TEXT_CONTENT_TYPE, EMPTY_BODY);
- }
- }
- default -> response = getResponse(METHOD_NOT_ALLOWED, TEXT_CONTENT_TYPE, EMPTY_BODY);
- }
+ final Response response = switch (aRequest.method()) {
+ case "POST" -> handlePost(aRequest);
+ case "GET" -> handleGet(aRequest);
+ default -> getResponse(METHOD_NOT_ALLOWED, TEXT_CONTENT_TYPE, EMPTY_BODY);
+ };
aCallback.accept(response);
}
@@ -282,14 +186,14 @@ public void handle(final Request aRequest, final Consumer aCallback) {
* @return A decoded code submission
*/
private String decodeSubmission(final byte[] aSubmission) {
- final String data = new String(aSubmission, UTF_8);
+ final String data = new String(aSubmission, StandardCharsets.UTF_8);
if (data.startsWith(CODE_DELIM)) {
- return URLDecoder.decode(data.substring(CODE_DELIM.length()), UTF_8);
+ return URLDecoder.decode(data.substring(CODE_DELIM.length()), StandardCharsets.UTF_8);
}
// If submission wasn't valid, just return an empty string which will evaluate to nothing
- return EMPTY;
+ return Constants.EMPTY;
}
/**
@@ -299,86 +203,159 @@ private String decodeSubmission(final byte[] aSubmission) {
* @return The formatted code
* @throws IOException If there is trouble reading the imports for the code block
*/
+ @SuppressWarnings({ Sonar.SYSTEM_OUT_ERR, PMD.SYSTEM_PRINTLN })
private String getCode(final String aCodeBlock) throws IOException {
final String code = """
{}
- class Jpv3Snippet {
- public static void main(String[] args) {
+ class Jpv3Snippet {
+ public static void main(String[] args) {
{}
-
- }
+ }
}
""";
- return StringUtils.format(code, getImports(aCodeBlock), aCodeBlock);
+ return StringUtils.format(code, myImports.getReferenced(aCodeBlock), indent(aCodeBlock, 4));
}
/**
- * Gets a list of Java imports.
+ * Creates a server response from the supplied parameters.
*
- * @return A list of Java imports
- * @throws IOException If there is trouble reading the imports file
+ * @param aEnum An HTTP response enum
+ * @param aHeaderList A list of response headers
+ * @param aBody A response body
+ * @return The newly constructed response
*/
- private String getImports() throws IOException {
- return getImports(null);
+ private Response getResponse(final info.freelibrary.iiif.webrepl.Status aEnum, final List aHeaderList,
+ final byte[] aBody) {
+ return new Response(aEnum.getCode(), aEnum.getMessage(), aHeaderList, aBody);
}
/**
- * Gets a list of Java imports.
+ * Handle a GET request.
*
- * @param aSnippet A snippet to evaluate
- * @return A list of Java imports
- * @throws IOException If there is trouble reading the imports file
+ * @param aRequest A GET request to handle
+ * @return A response to the GET request
*/
@SuppressWarnings({ PMD.SYSTEM_PRINTLN, Sonar.SYSTEM_OUT_ERR })
- private String getImports(final String aSnippet) throws IOException {
- File importsFile = Path.of("/etc/jshell/imports.jsh").toFile();
-
- // Check to see if we're running from the Maven build
- if (!importsFile.exists()) {
- importsFile = Path.of("src/main/docker/imports.jsh").toFile();
- }
+ private Response handleGet(final Request aRequest) {
+ final String uri = aRequest.uri();
- try (BufferedReader reader = Files.newBufferedReader(importsFile.toPath())) {
- final String imports = reader.lines().filter(line -> !line.isBlank()).filter(line -> {
- final String className = line.substring(line.lastIndexOf('.') + 1, line.length() - 1);
- return aSnippet == null || aSnippet.contains(className);
- }).collect(Collectors.joining(EOL)) + EOL;
+ System.err.println(aRequest.method() + Constants.SPACE + uri + Constants.EOL);
- System.err.println(imports);
- return imports;
+ if (!EDITOR_PATTERN.matcher(uri).find()) {
+ return getResponse(NOT_FOUND, TEXT_CONTENT_TYPE, EMPTY_BODY);
}
+
+ return getResponse(OK, HTML_CONTENT_TYPE, myHTML);
}
/**
- * Gets the classpath of the Jar file that contains the JPv3 classes.
+ * Handle a POST request.
*
- * @return The path to the Jar file in string form
- * @throws ClassNotFoundException If the JPv3 classes cannot be found
- * @throws URISyntaxException If there is trouble converting the Jar path into a URI
+ * @param aRequest A POST request to handle
+ * @return A response to the POST request
*/
- private String getJarClasspath() throws ClassNotFoundException, URISyntaxException {
- final CodeSource codeSource = Manifest.class.getProtectionDomain().getCodeSource();
+ @SuppressWarnings({ PMD.COGNITIVE_COMPLEXITY, Sonar.COGNITIVE_COMPLEXITY, PMD.SYSTEM_PRINTLN,
+ Sonar.SYSTEM_OUT_ERR })
+ private Response handlePost(final Request aRequest) {
+ final String uri = aRequest.uri();
+ final byte[] bytes;
+ final String body;
- if (codeSource != null) {
- return new File(codeSource.getLocation().toURI()).getAbsolutePath();
+ System.err.println(aRequest.method() + Constants.SPACE + uri);
+
+ // If we're not processing a code submission, we don't care
+ if (!SUBMIT_PATTERN.matcher(uri).find()) {
+ return getResponse(NOT_FOUND, TEXT_CONTENT_TYPE, EMPTY_BODY);
}
- throw new ClassNotFoundException();
+ body = decodeSubmission(aRequest.body()).trim();
+
+ System.err.println("Body: " + Constants.EOL + indent(StringUtils.addLineNumbers(body), 2));
+
+ try {
+ myShell.eval(getCode(body)).forEach(event -> {
+ final Status status = event.status();
+
+ switch (status) {
+ case VALID -> {
+ // The submitted code is valid, so get its result to write out
+ final SnippetEvent output = myShell.eval(MAIN_METHOD).get(0);
+
+ // Before returning the output though, log the VALID status
+ System.err.println(Constants.EOL + STATUS_LABEL + status);
+
+ if (Status.VALID.equals(output.status())) {
+ System.out.println(output.value());
+ }
+ }
+ case REJECTED, RECOVERABLE_DEFINED -> {
+ final Snippet snippet = event.snippet();
+ final StringBuilder buffer = new StringBuilder(snippet.source());
+
+ // Just check one at a time, and let the editor iterate
+ myShell.diagnostics(snippet).findFirst().ifPresentOrElse(new DiagnosticConsumer(buffer),
+ () -> buffer.delete(0, buffer.length())
+ .append(new ParsingError(StringUtils.addLineNumbers(body))));
+ try {
+ final String output = buffer.toString().trim();
+
+ // Write correctly formatted code to the user
+ myOutputStream.write(output.getBytes(StandardCharsets.UTF_8));
+
+ // Wrap the user's response in another format
+ System.err.println("Error: ");
+ System.err.println(indent(reformat(output), 2));
+ System.err.println(Constants.EOL + STATUS_LABEL + status + Constants.EOL);
+ } catch (final IOException details) {
+ System.err.println(details);
+ } finally {
+ try {
+ myOutputStream.flush();
+ } catch (final IOException details) {
+ System.err.println(details);
+ }
+ }
+ }
+ default -> {
+ System.err.println("Unhandled status: " + status);
+
+ Optional.ofNullable(event.exception()).ifPresentOrElse(exception -> {
+ System.err.println(exception);
+ System.out.println(exception);
+ }, () -> {
+ final ParsingError error = new ParsingError(body);
+
+ System.err.println(error);
+ System.out.println(error);
+ });
+ }
+ }
+ });
+
+ // Clear out our snippets for a fresh start with the next request
+ myShell.snippets().forEach(myShell::drop);
+ } catch (final IOException details) {
+ System.err.println(details);
+ System.out.println(details);
+ }
+
+ bytes = myOutputStream.toString().trim().getBytes(StandardCharsets.UTF_8);
+ myOutputStream.reset();
+
+ return getResponse(OK, TEXT_CONTENT_TYPE, bytes);
}
/**
- * Creates a server response from the supplied parameters.
+ * Strips line numbers from a user error message's code block so that they can be re-added across the whole
+ * message.
*
- * @param aEnum An HTTP response enum
- * @param aHeaderList A list of response headers
- * @param aBody A response body
- * @return The newly constructed response
+ * @param aMessage An error message that's being returned to the user
+ * @return An error message that has reformatted what was returned to the user
*/
- private Response getResponse(final info.freelibrary.iiif.webrepl.Status aEnum, final List aHeaderList,
- final byte[] aBody) {
- return new Response(aEnum.getCode(), aEnum.getMessage(), aHeaderList, aBody);
+ private String reformat(final String aMessage) {
+ return StringUtils.addLineNumbers(aMessage.replaceAll("(?m)^\\d+\\s+(?=[A-Za-z])", Constants.EMPTY));
}
/**
diff --git a/src/main/java/info/freelibrary/iiif/webrepl/WebRepl.java b/src/main/java/info/freelibrary/iiif/webrepl/WebRepl.java
new file mode 100644
index 00000000..73e61659
--- /dev/null
+++ b/src/main/java/info/freelibrary/iiif/webrepl/WebRepl.java
@@ -0,0 +1,46 @@
+
+package info.freelibrary.iiif.webrepl;
+
+import java.io.IOException;
+import java.net.URISyntaxException;
+
+import info.freelibrary.util.warnings.Checkstyle;
+
+/**
+ * An application that serves an interpreter for testing JPV3 code.
+ */
+public final class WebRepl {
+
+ /** The WebRepl's server. */
+ private static Server myServer;
+
+ /**
+ * A private constructor for the WebRepl application.
+ */
+ private WebRepl() {
+ // This is intentionally left empty.
+ }
+
+ /**
+ * Runs the WebRepl application.
+ *
+ * @param anArgsArray An array of initial arguments
+ * @throws IOException If the server has trouble starting
+ * @throws InterruptedException If the server is interrupted
+ * @throws URISyntaxException If the server is using an invalid URI
+ * @throws ClassNotFoundException If a class used by the server cannot be found
+ */
+ @SuppressWarnings({ Checkstyle.UNCOMMENTED_MAIN, "checkstyle:UncommentedMain" })
+ public static void main(final String[] anArgsArray)
+ throws IOException, InterruptedException, URISyntaxException, ClassNotFoundException {
+ myServer = new Server();
+ myServer.start();
+ }
+
+ /**
+ * Stops the WebRepl server.
+ */
+ static void stop() {
+ myServer.stop();
+ }
+}
diff --git a/src/test/java/info/freelibrary/iiif/webrepl/DiagConsumerTest.java b/src/test/java/info/freelibrary/iiif/webrepl/DiagConsumerTest.java
index 9704861c..581599f7 100644
--- a/src/test/java/info/freelibrary/iiif/webrepl/DiagConsumerTest.java
+++ b/src/test/java/info/freelibrary/iiif/webrepl/DiagConsumerTest.java
@@ -13,7 +13,7 @@
import jdk.jshell.Diag;
/**
- * Tests of {@link DiagConsumer}.
+ * Tests of {@link DiagnosticConsumer}.
*/
class DiagConsumerTest {
@@ -35,8 +35,8 @@ class DiagConsumerTest {
/** The test buffer. */
private StringBuilder myBuffer;
- /** The {@link DiagConsumer} being tested. */
- private DiagConsumer myDiagConsumer;
+ /** The {@link DiagnosticConsumer} being tested. */
+ private DiagnosticConsumer myDiagConsumer;
/**
* Sets up the testing environment.
@@ -44,7 +44,7 @@ class DiagConsumerTest {
@BeforeEach
void setUp() {
myBuffer = new StringBuilder();
- myDiagConsumer = new DiagConsumer(myBuffer);
+ myDiagConsumer = new DiagnosticConsumer(myBuffer);
}
/**
diff --git a/src/test/java/info/freelibrary/iiif/webrepl/EnvOptionsTest.java b/src/test/java/info/freelibrary/iiif/webrepl/EnvOptionsTest.java
new file mode 100644
index 00000000..3b8bee28
--- /dev/null
+++ b/src/test/java/info/freelibrary/iiif/webrepl/EnvOptionsTest.java
@@ -0,0 +1,50 @@
+
+package info.freelibrary.iiif.webrepl;
+
+import static com.github.stefanbirkner.systemlambda.SystemLambda.withEnvironmentVariable;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import java.io.IOException;
+import java.net.ServerSocket;
+
+import org.junit.jupiter.api.Test;
+import org.microhttp.Options;
+
+/**
+ * Tests of the {@code EnvOptions} class.
+ */
+class EnvOptionsTest {
+
+ /**
+ * Tests getting the environmental options.
+ */
+ @Test
+ final void testGetOpts() throws Exception {
+ final int port = getPort();
+
+ withEnvironmentVariable(Config.HTTP_PORT, Integer.toString(port))//
+ .and(Config.MAX_REQUEST_SIZE, Integer.toString(1_024))//
+ .and(Config.READ_BUFFER_SIZE, Integer.toString(1_024))//
+ .and(Config.REQUEST_TIMEOUT, Long.toString(30L))//
+ .execute(() -> {
+ final Options opts = new EnvOptions().getOpts();
+
+ assertEquals(port, opts.port());
+ assertEquals(1_024, opts.maxRequestSize());
+ assertEquals(1_024, opts.readBufferSize());
+ assertEquals(30L, opts.requestTimeout().getSeconds());
+ });
+ }
+
+ /**
+ * Gets an open port.
+ *
+ * @return An open port
+ * @throws IOException If there is trouble finding an open port
+ */
+ private static int getPort() throws IOException {
+ try (ServerSocket serverSocket = new ServerSocket(0)) {
+ return serverSocket.getLocalPort();
+ }
+ }
+}
diff --git a/src/test/java/info/freelibrary/iiif/webrepl/ImportsTest.java b/src/test/java/info/freelibrary/iiif/webrepl/ImportsTest.java
new file mode 100644
index 00000000..890e0b98
--- /dev/null
+++ b/src/test/java/info/freelibrary/iiif/webrepl/ImportsTest.java
@@ -0,0 +1,79 @@
+
+package info.freelibrary.iiif.webrepl;
+
+import static com.github.stefanbirkner.systemlambda.SystemLambda.tapSystemErr;
+import static info.freelibrary.util.Constants.EOL;
+import static info.freelibrary.util.StringUtils.addLineNumbers;
+import static info.freelibrary.util.StringUtils.indent;
+import static java.util.stream.Collectors.joining;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.IOException;
+import java.io.StringReader;
+
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+
+import info.freelibrary.util.StringUtils;
+
+/**
+ * Tests the {@code Imports} class.
+ */
+class ImportsTest {
+
+ /** The imports being tested. */
+ private static String myImports;
+
+ /**
+ * Tests getting all the imports.
+ */
+ @Test
+ final void testGetAll() throws Exception {
+ final String expected = "Imports:" + EOL + indent(addLineNumbers(myImports), 2);
+ final String found = tapSystemErr(() -> {
+ assertEquals(myImports, new Imports().getAll());
+ });
+
+ assertEquals(expected.trim(), found.trim());
+ }
+
+ /**
+ * Tests getting only the imports that are needed by the supplied code snippet.
+ *
+ * @throws Exception If there is trouble reading the imports
+ */
+ @Test
+ final void testGetReferenced() throws Exception {
+ final String imports = "import info.freelibrary.iiif.presentation.v3.Canvas;";
+ final String snippet = "Canvas";
+
+ final String expected = """
+ Imports:
+ 1 import info.freelibrary.iiif.presentation.v3.Canvas;
+ """.trim();
+
+ final String found = tapSystemErr(() -> {
+ assertEquals(imports, new Imports().getReferenced(snippet));
+ }).trim();
+
+ assertEquals(expected, found);
+ }
+
+ /**
+ * Sets up the testing environment.
+ *
+ * @throws IOException If there is trouble reading the imports from file
+ */
+ @BeforeAll
+ static final void setUp() throws IOException {
+ final String imports = StringUtils.read(new File("src/main/docker/imports.jsh"));
+
+ // Skip blank lines so we can make the imports.jsh file more human readable
+ try (BufferedReader reader = new BufferedReader(new StringReader(imports))) {
+ myImports = reader.lines().filter(line -> !line.isBlank()).collect(joining(EOL));
+ }
+ }
+
+}
diff --git a/src/test/java/info/freelibrary/iiif/webrepl/ParsingErrorTest.java b/src/test/java/info/freelibrary/iiif/webrepl/ParsingErrorTest.java
new file mode 100644
index 00000000..3becbc7b
--- /dev/null
+++ b/src/test/java/info/freelibrary/iiif/webrepl/ParsingErrorTest.java
@@ -0,0 +1,26 @@
+
+package info.freelibrary.iiif.webrepl;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import org.junit.jupiter.api.Test;
+
+import info.freelibrary.util.StringUtils;
+
+/**
+ * Tests of the {@code ParsingError} class.
+ */
+class ParsingErrorTest {
+
+ /** The code snippet used in the test. */
+ private static final String TEST_CODE = "Hello!";
+
+ /**
+ * Tests the {@code ParsingError} constructor.
+ */
+ @Test
+ final void testParsingErrorConstruction() {
+ assertEquals(StringUtils.format(ParsingError.MESSAGE_TEMPLATE, TEST_CODE),
+ new ParsingError(TEST_CODE).toString());
+ }
+}
diff --git a/src/test/java/info/freelibrary/iiif/webrepl/ServerTest.java b/src/test/java/info/freelibrary/iiif/webrepl/ServerTest.java
index c9a8f808..2c77dc01 100644
--- a/src/test/java/info/freelibrary/iiif/webrepl/ServerTest.java
+++ b/src/test/java/info/freelibrary/iiif/webrepl/ServerTest.java
@@ -1,88 +1,158 @@
package info.freelibrary.iiif.webrepl;
-import static info.freelibrary.util.Constants.INADDR_ANY;
+import static com.github.stefanbirkner.systemlambda.SystemLambda.tapSystemErr;
+import static com.github.stefanbirkner.systemlambda.SystemLambda.tapSystemErrAndOut;
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.fail;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
-import java.io.IOException;
-import java.net.URISyntaxException;
+import java.nio.charset.StandardCharsets;
import java.util.List;
-import java.util.concurrent.ExecutionException;
-import java.util.concurrent.ExecutorService;
-import java.util.concurrent.Executors;
-import java.util.concurrent.Future;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.TimeoutException;
import java.util.function.Consumer;
import org.junit.jupiter.api.Test;
-import org.microhttp.Options;
import org.microhttp.Request;
import org.microhttp.Response;
import org.mockito.ArgumentCaptor;
import org.mockito.Mockito;
+import info.freelibrary.util.Constants;
+import info.freelibrary.util.HTTP;
import info.freelibrary.util.warnings.JDK;
-import info.freelibrary.iiif.webrepl.Server.JPv3Handler;
/**
* Tests of the {@link Server} class.
*/
class ServerTest {
- /** The GET method constant. */
- private static final String GET = "GET";
+ /** The HTTP version constant. */
+ private static final String HTTP_VERSION = "HTTP/1.1";
- /** The POST method constant. */
- private static final String POST = "POST";
+ /** The source code submission endpoint. */
+ private static final String SUBMIT = "/submit";
/** The test code passed to the consumer. */
- private static final String TEST_CODE = "System.out.println(\"Hello, World!\");";
+ private static final byte[] TEST_CODE =
+ "code=System.out.println(\"Hello, World!\");".getBytes(StandardCharsets.UTF_8);
/**
- * Tests the server's POST response handler.
+ * Tests that {@code System.err} reports when a bad import is loaded in server initialization.
+ *
+ * @throws Exception If there is trouble reading the imports
+ */
+ @Test
+ void testBadImportForServer() throws Exception {
+ final Imports mockImports = Mockito.mock(Imports.class);
+ final String output;
+
+ when(mockImports.getAll()).thenReturn("import something.that.does.not.Exist;");
+
+ output = tapSystemErr(() -> {
+ new Server.JPv3Handler(mockImports);
+ });
+
+ assertEquals("REJECTED", output.trim());
+ }
+
+ /**
+ * Tests submitting code to an invalid endpoint.
+ *
+ * @throws Exception If an exception occurs
+ */
+ @Test
+ @SuppressWarnings(JDK.UNCHECKED)
+ void testHandlePostSubmitInvalidEndpoint() throws Exception {
+ final Consumer mockConsumer = Mockito.mock(Consumer.class);
+ final Request mockRequest = new Request(HTTP.Method.POST, "/nothing", HTTP_VERSION, List.of(), TEST_CODE);
+ final ArgumentCaptor responseCaptor;
+ final Response response;
+
+ tapSystemErr(() -> {
+ new Server.JPv3Handler().handle(mockRequest, mockConsumer);
+ });
+
+ responseCaptor = ArgumentCaptor.forClass(Response.class);
+ verify(mockConsumer).accept(responseCaptor.capture());
+ response = responseCaptor.getValue();
+
+ assertEquals(404, response.status());
+ }
+
+ /**
+ * Tests submitting code to an invalid endpoint.
+ *
+ * @throws Exception If an exception occurs
*/
@Test
@SuppressWarnings(JDK.UNCHECKED)
- void testHandlePostSubmitValidCode() throws URISyntaxException, ClassNotFoundException, IOException {
- final JPv3Handler handler = new Server.JPv3Handler();
+ void testHandlePostSubmitInvalidParam() throws Exception {
+ final byte[] testCode = "bad=System.out.println(\"Hello, World!\");".getBytes(StandardCharsets.UTF_8);
final Consumer mockConsumer = Mockito.mock(Consumer.class);
- final byte[] code = ("code=" + TEST_CODE).getBytes();
- final Request mockRequest = new Request(POST, "/submit", "HTTP/1.1", List.of(), code);
+ final Request mockRequest = new Request(HTTP.Method.POST, SUBMIT, HTTP_VERSION, List.of(), testCode);
final ArgumentCaptor responseCaptor;
final Response response;
- handler.handle(mockRequest, mockConsumer);
+ tapSystemErrAndOut(() -> {
+ new Server.JPv3Handler().handle(mockRequest, mockConsumer);
+ });
responseCaptor = ArgumentCaptor.forClass(Response.class);
verify(mockConsumer).accept(responseCaptor.capture());
response = responseCaptor.getValue();
assertEquals(201, response.status());
+ assertEquals(Constants.EMPTY, new String(response.body(), StandardCharsets.UTF_8));
+ }
+
+ /**
+ * Tests the server's POST response handler.
+ *
+ * @throws Exception If an exception occurs
+ */
+ @Test
+ @SuppressWarnings(JDK.UNCHECKED)
+ void testHandlePostSubmitValidCode() throws Exception {
+ final Consumer mockConsumer = Mockito.mock(Consumer.class);
+ final Request mockRequest = new Request(HTTP.Method.POST, SUBMIT, HTTP_VERSION, List.of(), TEST_CODE);
+ final ArgumentCaptor responseCaptor;
+ final Response response;
+
+ tapSystemErrAndOut(() -> {
+ new Server.JPv3Handler().handle(mockRequest, mockConsumer);
+ });
+
+ responseCaptor = ArgumentCaptor.forClass(Response.class);
+ verify(mockConsumer).accept(responseCaptor.capture());
+ response = responseCaptor.getValue();
+
+ assertEquals(HTTP.CREATED, response.status());
assertEquals("OK", response.reason());
assertEquals("text/plain", response.headers().get(0).value());
- assertEquals("Hello, World!\n", new String(response.body()));
+ assertEquals("Hello, World!", new String(response.body()));
}
/**
* Tests the server's GET response handler.
+ *
+ * @throws Exception If an exception occurs
*/
@Test
@SuppressWarnings(JDK.UNCHECKED)
- final void testHandlerBadMethod() throws URISyntaxException, ClassNotFoundException, IOException {
+ final void testHandlerBadMethod() throws Exception {
final Request mockRequest = Mockito.mock(Request.class);
final Consumer mockConsumer = Mockito.mock(Consumer.class);
final ArgumentCaptor responseCaptor;
final Response response;
when(mockRequest.uri()).thenReturn("http://0.0.0.0/yada");
- when(mockRequest.method()).thenReturn("DELETE");
+ when(mockRequest.method()).thenReturn(HTTP.Method.DELETE);
when(mockRequest.body()).thenReturn(new byte[] {});
- new Server.JPv3Handler().handle(mockRequest, mockConsumer);
+ tapSystemErr(() -> {
+ new Server.JPv3Handler().handle(mockRequest, mockConsumer);
+ });
// Capture the Response passed to the Consumer
responseCaptor = ArgumentCaptor.forClass(Response.class);
@@ -94,20 +164,24 @@ final void testHandlerBadMethod() throws URISyntaxException, ClassNotFoundExcept
/**
* Tests the server's response handler.
+ *
+ * @throws Exception If an exception occurs
*/
@Test
@SuppressWarnings(JDK.UNCHECKED)
- final void testHandlerGet() throws URISyntaxException, ClassNotFoundException, IOException {
+ final void testHandlerGet() throws Exception {
final Request mockRequest = Mockito.mock(Request.class);
final Consumer mockConsumer = Mockito.mock(Consumer.class);
final ArgumentCaptor responseCaptor;
final Response response;
when(mockRequest.uri()).thenReturn("http://0.0.0.0/");
- when(mockRequest.method()).thenReturn(GET);
+ when(mockRequest.method()).thenReturn(HTTP.Method.GET);
when(mockRequest.body()).thenReturn(new byte[] {});
- new Server.JPv3Handler().handle(mockRequest, mockConsumer);
+ tapSystemErr(() -> {
+ new Server.JPv3Handler().handle(mockRequest, mockConsumer);
+ });
// Capture the Response passed to the Consumer
responseCaptor = ArgumentCaptor.forClass(Response.class);
@@ -119,20 +193,24 @@ final void testHandlerGet() throws URISyntaxException, ClassNotFoundException, I
/**
* Tests the server's GET response handler.
+ *
+ * @throws Exception If an exception occurs
*/
@Test
@SuppressWarnings(JDK.UNCHECKED)
- final void testHandlerGetEditor() throws URISyntaxException, ClassNotFoundException, IOException {
+ final void testHandlerGetEditor() throws Exception {
final Request mockRequest = Mockito.mock(Request.class);
final Consumer mockConsumer = Mockito.mock(Consumer.class);
final ArgumentCaptor responseCaptor;
final Response response;
when(mockRequest.uri()).thenReturn("http://0.0.0.0/editor");
- when(mockRequest.method()).thenReturn(GET);
+ when(mockRequest.method()).thenReturn(HTTP.Method.GET);
when(mockRequest.body()).thenReturn(new byte[] {});
- new Server.JPv3Handler().handle(mockRequest, mockConsumer);
+ tapSystemErr(() -> {
+ new Server.JPv3Handler().handle(mockRequest, mockConsumer);
+ });
// Capture the Response passed to the Consumer
responseCaptor = ArgumentCaptor.forClass(Response.class);
@@ -143,63 +221,51 @@ final void testHandlerGetEditor() throws URISyntaxException, ClassNotFoundExcept
}
/**
- * Tests the startup of the server.
+ * Tests passing a handler to the Server's constructor.
*
- * @throws InterruptedException If the server cannot be started
+ * @throws Exception If there is trouble capturing {@code System.err}.
*/
@Test
- final void testServer() throws InterruptedException, ClassNotFoundException, URISyntaxException, IOException {
- final ExecutorService executor = Executors.newSingleThreadExecutor();
- final Server server = new Server();
-
- final Runnable task = () -> {
- try {
- server.run();
- } catch (final InterruptedException details) {
- Thread.currentThread().interrupt();
- fail(details.getMessage(), details);
- }
- };
-
- try {
- final Future> future = executor.submit(task);
-
- future.get(2, TimeUnit.SECONDS);
- } catch (TimeoutException | ExecutionException | InterruptedException details) {
- server.stop();
-
- if (!(details instanceof TimeoutException)) {
- fail(details.getMessage());
- }
-
- } finally {
- executor.shutdown();
-
- try {
- if (!executor.awaitTermination(5, TimeUnit.SECONDS)) {
- server.stop();
- }
- } catch (final InterruptedException details) {
- Thread.currentThread().interrupt();
- }
- }
+ final void testHandlerPassedToServer() throws Exception {
+ tapSystemErr(() -> {
+ assertDoesNotThrow(() -> {
+ new Server(new Server.JPv3Handler()).stop();
+ });
+ });
}
/**
- * Tests the server constructor that takes a event loop handler.
+ * Tests that {@code System.err} reports when a bad import is loaded in handler.
*
- * @throws ClassNotFoundException If the handler cannot be found
- * @throws URISyntaxException If the handler uses an invalid URI
- * @throws IOException If there is trouble reading or writing from the server
+ * @throws Exception If there is trouble reading the imports
*/
@Test
- final void testServerWithHandler() throws ClassNotFoundException, URISyntaxException, IOException {
- final Server server = new Server(new Server.JPv3Handler());
- final Options opts = server.getOptions();
+ @SuppressWarnings(JDK.UNCHECKED)
+ void testRejectedCode() throws Exception {
+ final byte[] testCode = "code=System.out.printn(\"Hello, World!\");".getBytes(StandardCharsets.UTF_8);
+ final Consumer mockConsumer = Mockito.mock(Consumer.class);
+ final Request mockRequest = new Request(HTTP.Method.POST, SUBMIT, HTTP_VERSION, List.of(), testCode);
+ final ArgumentCaptor responseCaptor;
+ final Response response;
+ final String expected = """
+ Could not parse:
- assertEquals(INADDR_ANY, opts.host());
- assertEquals(Integer.parseInt(System.getenv(Config.HTTP_PORT)), opts.port());
+ 1 [[System.out.printn]]("Hello, World!");
- server.stop();
+ Reason: cannot find symbol
+ symbol: method printn(java.lang.String)
+ location: variable out of type java.io.PrintStream
+ """.trim();
+
+ tapSystemErrAndOut(() -> {
+ new Server.JPv3Handler().handle(mockRequest, mockConsumer);
+ });
+
+ responseCaptor = ArgumentCaptor.forClass(Response.class);
+ verify(mockConsumer).accept(responseCaptor.capture());
+ response = responseCaptor.getValue();
+
+ assertEquals(201, response.status());
+ assertEquals(expected, new String(response.body(), StandardCharsets.UTF_8));
}
}
diff --git a/src/test/java/info/freelibrary/iiif/webrepl/WebReplTest.java b/src/test/java/info/freelibrary/iiif/webrepl/WebReplTest.java
new file mode 100644
index 00000000..ea385a1a
--- /dev/null
+++ b/src/test/java/info/freelibrary/iiif/webrepl/WebReplTest.java
@@ -0,0 +1,91 @@
+
+package info.freelibrary.iiif.webrepl;
+
+import static com.github.stefanbirkner.systemlambda.SystemLambda.tapSystemErrAndOutNormalized;
+import static com.github.stefanbirkner.systemlambda.SystemLambda.withEnvironmentVariable;
+import static org.awaitility.Awaitility.await;
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+
+import java.io.IOException;
+import java.net.HttpURLConnection;
+import java.net.ServerSocket;
+import java.net.URI;
+import java.net.URL;
+import java.util.concurrent.TimeUnit;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.function.Executable;
+
+import info.freelibrary.util.HTTP;
+import info.freelibrary.util.StringUtils;
+
+/**
+ * Tests the WebRepl application.
+ */
+class WebReplTest {
+
+ /** A constant for two seconds. */
+ private static final int TWO_SECONDS = 2000;
+
+ /**
+ * Tests the {@code WebRepl}'s main method.
+ *
+ * @throws Exception If starting or stopping the server fails
+ */
+ @Test
+ final void testMain() throws Exception {
+ final int port = getOpenPort();
+
+ // Configure server with a unique port number
+ withEnvironmentVariable(Config.HTTP_PORT, Integer.toString(port)).execute(() -> {
+ final Thread mainThread = new Thread(() -> {
+ try {
+ // Prevent StdOut and StdErr from showing during testing
+ tapSystemErrAndOutNormalized(() -> {
+ WebRepl.main(new String[] {});
+ });
+ } catch (final Exception details) {
+ throw new RuntimeException("Error starting WebRepl", details);
+ }
+ });
+
+ // Start the server
+ mainThread.start();
+
+ // Check the server's endpoints until we can confirm it's started
+ await().atMost(12, TimeUnit.SECONDS).until(() -> {
+ try {
+ final URL url = URI.create(StringUtils.format("http://0.0.0.0:{}/editor/", port)).toURL();
+ final HttpURLConnection http = (HttpURLConnection) url.openConnection();
+
+ http.setRequestMethod(HTTP.Method.GET);
+ http.setReadTimeout(TWO_SECONDS);
+ http.setConnectTimeout(TWO_SECONDS);
+
+ if (http.getResponseCode() == HTTP.CREATED) {
+ return true;
+ }
+ } catch (final Exception details) {
+ // We're intentionally ignoring this
+ }
+
+ return false;
+ });
+
+ assertDoesNotThrow((Executable) WebRepl::stop);
+ mainThread.join();
+ });
+ }
+
+ /**
+ * Gets a port that's open on the local system.
+ *
+ * @return An open port
+ * @throws IOException If there is trouble finding an open port
+ */
+ private static int getOpenPort() throws IOException {
+ try (ServerSocket serverSocket = new ServerSocket(0)) {
+ return serverSocket.getLocalPort();
+ }
+ }
+}