Merge pull request 'feature/render-binary' (#1) from feature/render-binary into master
All checks were successful
continuous-integration/drone/push Build is passing
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: java/labyrinth-server#1
This commit is contained in:
commit
1a8e3d247a
9 changed files with 362 additions and 62 deletions
|
@ -20,7 +20,9 @@ steps:
|
|||
from_secret: repo-token
|
||||
commands:
|
||||
- mvn -s maven-settings.xml deploy -DskipTests=true
|
||||
when:
|
||||
trigger:
|
||||
branch:
|
||||
include:
|
||||
- master
|
||||
event:
|
||||
exclude:
|
||||
- pull_request
|
||||
|
|
|
@ -12,16 +12,17 @@
|
|||
</content>
|
||||
<orderEntry type="inheritedJdk" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
<orderEntry type="library" name="Maven: ch.fritteli.labyrinth:labyrinth-generator:0.0.1" level="project" />
|
||||
<orderEntry type="library" name="Maven: org.apache.pdfbox:pdfbox:2.0.20" level="project" />
|
||||
<orderEntry type="library" name="Maven: org.apache.pdfbox:fontbox:2.0.20" level="project" />
|
||||
<orderEntry type="module" module-name="labyrinth-generator" />
|
||||
<orderEntry type="library" name="Maven: org.apache.pdfbox:pdfbox:2.0.25" level="project" />
|
||||
<orderEntry type="library" name="Maven: org.apache.pdfbox:fontbox:2.0.25" level="project" />
|
||||
<orderEntry type="library" name="Maven: commons-logging:commons-logging:1.2" level="project" />
|
||||
<orderEntry type="library" name="Maven: io.vavr:vavr:0.10.2" level="project" />
|
||||
<orderEntry type="library" name="Maven: io.vavr:vavr-match:0.10.2" level="project" />
|
||||
<orderEntry type="library" scope="PROVIDED" name="Maven: org.projectlombok:lombok:1.18.12" level="project" />
|
||||
<orderEntry type="library" name="Maven: org.jetbrains:annotations:19.0.0" level="project" />
|
||||
<orderEntry type="library" name="Maven: org.slf4j:slf4j-api:1.7.30" level="project" />
|
||||
<orderEntry type="library" name="Maven: org.slf4j:slf4j-simple:1.7.30" level="project" />
|
||||
<orderEntry type="library" name="Maven: org.slf4j:slf4j-api:1.7.35" level="project" />
|
||||
<orderEntry type="library" name="Maven: ch.qos.logback:logback-classic:1.2.10" level="project" />
|
||||
<orderEntry type="library" name="Maven: ch.qos.logback:logback-core:1.2.10" level="project" />
|
||||
<orderEntry type="library" scope="TEST" name="Maven: org.junit.jupiter:junit-jupiter-api:5.6.1" level="project" />
|
||||
<orderEntry type="library" scope="TEST" name="Maven: org.apiguardian:apiguardian-api:1.1.0" level="project" />
|
||||
<orderEntry type="library" scope="TEST" name="Maven: org.opentest4j:opentest4j:1.2.0" level="project" />
|
||||
|
|
31
pom.xml
31
pom.xml
|
@ -14,11 +14,16 @@
|
|||
<artifactId>labyrinth-server</artifactId>
|
||||
<version>0.0.1-SNAPSHOT</version>
|
||||
|
||||
<properties>
|
||||
<logback.version>1.2.10</logback.version>
|
||||
<slf4j.version>1.7.35</slf4j.version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>ch.fritteli.labyrinth</groupId>
|
||||
<artifactId>labyrinth-generator</artifactId>
|
||||
<version>0.0.1</version>
|
||||
<version>0.0.2</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.vavr</groupId>
|
||||
|
@ -35,12 +40,12 @@
|
|||
<dependency>
|
||||
<groupId>org.slf4j</groupId>
|
||||
<artifactId>slf4j-api</artifactId>
|
||||
<version>1.7.30</version>
|
||||
<version>${slf4j.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.slf4j</groupId>
|
||||
<artifactId>slf4j-simple</artifactId>
|
||||
<version>1.7.30</version>
|
||||
<groupId>ch.qos.logback</groupId>
|
||||
<artifactId>logback-classic</artifactId>
|
||||
<version>${logback.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.junit.jupiter</groupId>
|
||||
|
@ -95,12 +100,24 @@
|
|||
</distributionManagement>
|
||||
<repositories>
|
||||
<repository>
|
||||
<id>repo.gittr.ch</id>
|
||||
<url>https://repo.gittr.ch/</url>
|
||||
<id>repo.gittr.ch.releases</id>
|
||||
<url>https://repo.gittr.ch/releases/</url>
|
||||
<releases>
|
||||
<enabled>true</enabled>
|
||||
<updatePolicy>never</updatePolicy>
|
||||
</releases>
|
||||
<snapshots>
|
||||
<enabled>false</enabled>
|
||||
<updatePolicy>never</updatePolicy>
|
||||
</snapshots>
|
||||
</repository>
|
||||
<repository>
|
||||
<id>repo.gittr.ch.snapshots</id>
|
||||
<url>https://repo.gittr.ch/snapshots/</url>
|
||||
<releases>
|
||||
<enabled>false</enabled>
|
||||
<updatePolicy>never</updatePolicy>
|
||||
</releases>
|
||||
<snapshots>
|
||||
<enabled>true</enabled>
|
||||
<updatePolicy>always</updatePolicy>
|
||||
|
|
|
@ -4,10 +4,16 @@ import ch.fritteli.labyrinth.generator.model.Labyrinth;
|
|||
import ch.fritteli.labyrinth.generator.renderer.html.HTMLRenderer;
|
||||
import ch.fritteli.labyrinth.generator.renderer.pdf.PDFRenderer;
|
||||
import ch.fritteli.labyrinth.generator.renderer.text.TextRenderer;
|
||||
import ch.fritteli.labyrinth.generator.serialization.SerializerDeserializer;
|
||||
import com.sun.net.httpserver.Headers;
|
||||
import com.sun.net.httpserver.HttpExchange;
|
||||
import com.sun.net.httpserver.HttpServer;
|
||||
import io.vavr.collection.*;
|
||||
import io.vavr.collection.HashMap;
|
||||
import io.vavr.collection.HashSet;
|
||||
import io.vavr.collection.List;
|
||||
import io.vavr.collection.Map;
|
||||
import io.vavr.collection.Set;
|
||||
import io.vavr.collection.Stream;
|
||||
import io.vavr.control.Option;
|
||||
import io.vavr.control.Try;
|
||||
import lombok.Getter;
|
||||
|
@ -18,17 +24,33 @@ import org.jetbrains.annotations.Nullable;
|
|||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.URISyntaxException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.SynchronousQueue;
|
||||
import java.util.concurrent.ThreadPoolExecutor;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.function.Function;
|
||||
|
||||
@Slf4j
|
||||
public class LabyrinthServer {
|
||||
@NonNull
|
||||
private final HttpServer httpServer;
|
||||
@NonNull
|
||||
private final ExecutorService executorService = new ThreadPoolExecutor(
|
||||
0,
|
||||
1_000,
|
||||
5,
|
||||
TimeUnit.SECONDS,
|
||||
new SynchronousQueue<>()
|
||||
);
|
||||
|
||||
public LabyrinthServer(@NonNull final ServerConfig config) throws IOException {
|
||||
public LabyrinthServer(@NonNull final ServerConfig config) throws IOException, URISyntaxException {
|
||||
this.httpServer = HttpServer.create(new InetSocketAddress(config.getAddress(), config.getPort()), 5);
|
||||
this.httpServer.createContext("/", new StaticResourcesFileHandler(this.executorService));
|
||||
this.httpServer.createContext("/create", this::handleCreate);
|
||||
this.httpServer.createContext("/render", this::handleRender);
|
||||
}
|
||||
|
||||
public static Option<LabyrinthServer> createAndStartServer() {
|
||||
|
@ -83,16 +105,25 @@ public class LabyrinthServer {
|
|||
public void stop() {
|
||||
log.info("Stopping server ...");
|
||||
this.httpServer.stop(5);
|
||||
this.executorService.shutdown();
|
||||
try {
|
||||
if (!this.executorService.awaitTermination(5, TimeUnit.SECONDS)) {
|
||||
log.warn("Timeout occurred while awaiting termination of executor service");
|
||||
}
|
||||
} catch (final InterruptedException e) {
|
||||
log.error("Failed to await termination of executor service", e);
|
||||
}
|
||||
log.info("Server stopped.");
|
||||
}
|
||||
|
||||
private void handleCreate(HttpExchange exchange) throws IOException {
|
||||
this.executorService.submit(() -> {
|
||||
log.debug("Handling request to {}", exchange.getRequestURI());
|
||||
try {
|
||||
final String requestMethod = exchange.getRequestMethod();
|
||||
if (!requestMethod.equals("GET")) {
|
||||
exchange.getResponseBody().close();
|
||||
exchange.sendResponseHeaders(405, -1);
|
||||
exchange.close();
|
||||
return;
|
||||
}
|
||||
final Map<RequestParameter, String> requestParams = this.parseQueryString(exchange.getRequestURI().getQuery());
|
||||
|
@ -107,7 +138,6 @@ public class LabyrinthServer {
|
|||
if (needsRedirect.get()) {
|
||||
responseHeaders.add("Location", "/create?width=" + width + "&height=" + height + "&output=" + output.toString() + "&id=" + id);
|
||||
exchange.sendResponseHeaders(302, -1);
|
||||
exchange.close();
|
||||
return;
|
||||
}
|
||||
final Labyrinth labyrinth = new Labyrinth(width, height, id);
|
||||
|
@ -120,7 +150,53 @@ public class LabyrinthServer {
|
|||
final OutputStream responseBody = exchange.getResponseBody();
|
||||
responseBody.write(("Error: " + e).getBytes(StandardCharsets.UTF_8));
|
||||
responseBody.flush();
|
||||
return;
|
||||
}
|
||||
responseHeaders.add("Content-type", output.getContentType());
|
||||
if (output.equals(OutputType.BINARY)) {
|
||||
responseHeaders.add("Content-disposition", "attachment; filename=\"labyrinth-" + width + "x" + height + "-" + id + ".laby\"");
|
||||
}
|
||||
exchange.sendResponseHeaders(200, 0);
|
||||
final OutputStream responseBody = exchange.getResponseBody();
|
||||
responseBody.write(render);
|
||||
responseBody.flush();
|
||||
} catch (Exception e) {
|
||||
log.error("FSCK!", e);
|
||||
} finally {
|
||||
exchange.close();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void handleRender(final HttpExchange exchange) throws IOException {
|
||||
this.executorService.submit(() -> {
|
||||
try {
|
||||
log.debug("Handling request to {}", exchange.getRequestURI());
|
||||
final String requestMethod = exchange.getRequestMethod();
|
||||
if (!requestMethod.equals("POST")) {
|
||||
exchange.getResponseBody().close();
|
||||
exchange.sendResponseHeaders(405, -1);
|
||||
return;
|
||||
}
|
||||
final byte[] bytes = exchange.getRequestBody().readAllBytes();
|
||||
|
||||
final Labyrinth labyrinth = SerializerDeserializer.deserialize(bytes);
|
||||
|
||||
final OutputType output = exchange.getRequestHeaders()
|
||||
.get("Accept")
|
||||
.contains("text/html") ?
|
||||
OutputType.HTML :
|
||||
OutputType.TEXT_PLAIN;
|
||||
final byte[] render;
|
||||
final Headers responseHeaders = exchange.getResponseHeaders();
|
||||
try {
|
||||
render = output.render(labyrinth);
|
||||
} catch (Exception e) {
|
||||
responseHeaders.add("Content-type", "text/plain; charset=UTF-8");
|
||||
exchange.sendResponseHeaders(500, 0);
|
||||
final OutputStream responseBody = exchange.getResponseBody();
|
||||
responseBody.write(("Error: " + e).getBytes(StandardCharsets.UTF_8));
|
||||
responseBody.flush();
|
||||
return;
|
||||
}
|
||||
responseHeaders.add("Content-type", output.getContentType());
|
||||
|
@ -128,8 +204,13 @@ public class LabyrinthServer {
|
|||
final OutputStream responseBody = exchange.getResponseBody();
|
||||
responseBody.write(render);
|
||||
responseBody.flush();
|
||||
} catch (Exception e) {
|
||||
log.error("FSCK!", e);
|
||||
} finally {
|
||||
exchange.close();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private enum RequestParameter {
|
||||
WIDTH("w", "width"),
|
||||
|
@ -154,7 +235,8 @@ public class LabyrinthServer {
|
|||
private enum OutputType {
|
||||
TEXT_PLAIN("text/plain; charset=UTF-8", labyrinth -> TextRenderer.newInstance().render(labyrinth).getBytes(StandardCharsets.UTF_8), "t", "text"),
|
||||
HTML("text/html", labyrinth -> HTMLRenderer.newInstance().render(labyrinth).getBytes(StandardCharsets.UTF_8), "h", "html"),
|
||||
PDF("application/pdf", labyrinth -> PDFRenderer.newInstance().render(labyrinth), "p", "pdf");
|
||||
PDF("application/pdf", labyrinth -> PDFRenderer.newInstance().render(labyrinth), "p", "pdf"),
|
||||
BINARY("application/octet-stream", SerializerDeserializer::serialize, "b", "binary");
|
||||
@Getter
|
||||
@NonNull
|
||||
private final String contentType;
|
||||
|
|
|
@ -0,0 +1,98 @@
|
|||
package ch.fritteli.labyrinth.server;
|
||||
|
||||
import com.sun.net.httpserver.HttpExchange;
|
||||
import com.sun.net.httpserver.HttpHandler;
|
||||
import lombok.NonNull;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.pdfbox.io.IOUtils;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.URI;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
|
||||
@Slf4j
|
||||
public class StaticResourcesFileHandler implements HttpHandler {
|
||||
|
||||
public static final String WEBASSETS_DIRECTORY = "webassets";
|
||||
private final ExecutorService executorService;
|
||||
|
||||
public StaticResourcesFileHandler(final ExecutorService executorService) {
|
||||
this.executorService = executorService;
|
||||
}
|
||||
|
||||
private static void redirect(@NonNull final HttpExchange exchange, @NonNull final String target) throws IOException {
|
||||
log.debug("Sending redirect to {}", target);
|
||||
exchange.getResponseHeaders().add("Location", target);
|
||||
exchange.sendResponseHeaders(302, -1);
|
||||
}
|
||||
|
||||
private static void notFound(@NonNull final HttpExchange exchange, @NonNull final String path) throws IOException {
|
||||
log.debug("Resource '{}' not found, replying with HTTP 404", path);
|
||||
exchange.getResponseHeaders().add("Content-type", "text/plain; charset=utf-8");
|
||||
exchange.sendResponseHeaders(404, 0);
|
||||
exchange.getResponseBody().write("404 - Not found".getBytes(StandardCharsets.UTF_8));
|
||||
exchange.getResponseBody().flush();
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private static byte[] getBytes(@NonNull final String path) throws IOException {
|
||||
final InputStream stream = StaticResourcesFileHandler.class.getClassLoader().getResourceAsStream(WEBASSETS_DIRECTORY + path);
|
||||
if (stream == null) {
|
||||
log.debug("Resource '{}' not found in classpath.", path);
|
||||
return new byte[0];
|
||||
}
|
||||
return IOUtils.toByteArray(stream);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private static String getMimeType(@NonNull final String path) {
|
||||
if (path.endsWith(".html")) {
|
||||
return "text/html";
|
||||
}
|
||||
if (path.endsWith(".css")) {
|
||||
return "text/css";
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handle(@NonNull final HttpExchange exchange) throws IOException {
|
||||
this.executorService.submit(() -> {
|
||||
try {
|
||||
final URI requestURI = exchange.getRequestURI();
|
||||
final String path = requestURI.getPath();
|
||||
log.debug("Handling request to {}", path);
|
||||
if ("/".equals(path)) {
|
||||
redirect(exchange, "/index.html");
|
||||
return;
|
||||
}
|
||||
if (!path.startsWith("/")) {
|
||||
notFound(exchange, path);
|
||||
return;
|
||||
}
|
||||
final String mimeType = getMimeType(path);
|
||||
if (mimeType == null) {
|
||||
notFound(exchange, path);
|
||||
return;
|
||||
}
|
||||
final byte[] responseBytes = getBytes(path);
|
||||
if (responseBytes.length == 0) {
|
||||
notFound(exchange, path);
|
||||
return;
|
||||
}
|
||||
log.debug("Serving {}{} with mimetype {}: {} bytes", WEBASSETS_DIRECTORY, path, mimeType, responseBytes.length);
|
||||
exchange.getResponseHeaders().add("Content-type", mimeType);
|
||||
exchange.sendResponseHeaders(200, 0);
|
||||
exchange.getResponseBody().write(responseBytes);
|
||||
exchange.getResponseBody().flush();
|
||||
} catch (Exception e) {
|
||||
log.error("FSCK!", e);
|
||||
} finally {
|
||||
exchange.close();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
17
src/main/resources/logback.xml
Normal file
17
src/main/resources/logback.xml
Normal file
|
@ -0,0 +1,17 @@
|
|||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<configuration>
|
||||
<shutdownHook class="ch.qos.logback.core.hook.DelayingShutdownHook"/>
|
||||
|
||||
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
|
||||
<!-- encoders are by default assigned the type
|
||||
ch.qos.logback.classic.encoder.PatternLayoutEncoder -->
|
||||
<encoder>
|
||||
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
|
||||
</encoder>
|
||||
</appender>
|
||||
|
||||
<root level="info">
|
||||
<appender-ref ref="STDOUT"/>
|
||||
</root>
|
||||
<logger name="ch.fritteli.labyrinth.server.StaticResourcesFileHandler" level="debug"/>
|
||||
</configuration>
|
32
src/main/resources/webassets/index.html
Normal file
32
src/main/resources/webassets/index.html
Normal file
|
@ -0,0 +1,32 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Labyrinth Generator</title>
|
||||
<link rel="stylesheet" href="style.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="content">
|
||||
<h1>Labyrinth Generator</h1>
|
||||
<p>Enter some values, click the "Create!" button and see what happens!</p>
|
||||
<form action="/create" method="get">
|
||||
<div class="inputs-wrapper">
|
||||
<label for="width">Width:</label><input id="width" name="width" type="number" min="1" required>
|
||||
<label for="height">Height:</label><input id="height" name="height" type="number" min="1" required>
|
||||
<label for="output">Output format:</label>
|
||||
<select id="output" name="output" required>
|
||||
<option label="HTML Document" value="html"></option>
|
||||
<option label="Plain text" value="text"></option>
|
||||
<option label="PDF Document" value="pdf"></option>
|
||||
<option label="Binary" value="binary"></option>
|
||||
</select>
|
||||
<label for="id">Seed (optional):</label><input id="id" name="id" type="number">
|
||||
</div>
|
||||
<div class="controls-wrapper">
|
||||
<button type="submit" class="primary">Create!</button>
|
||||
<button type="reset">Reset form</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
51
src/main/resources/webassets/style.css
Normal file
51
src/main/resources/webassets/style.css
Normal file
|
@ -0,0 +1,51 @@
|
|||
:root {
|
||||
--color-background: #181a1b;
|
||||
--color-foreground: #e8e6e3;
|
||||
--color-border: #5d6164;
|
||||
--color-background-highlight: #292b2c;
|
||||
--color-foreground-highlight: #f8f7f4;
|
||||
--color-border-highlight: #6e7275;
|
||||
}
|
||||
|
||||
body {
|
||||
display: flex;
|
||||
font-family: sans-serif;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
body, button, input, select {
|
||||
background-color: var(--color-background);
|
||||
color: var(--color-foreground);
|
||||
}
|
||||
|
||||
button, input, select {
|
||||
border: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
button:active, input:active, select:active,
|
||||
button:focus, input:focus, select:focus,
|
||||
button:hover, input:hover, select:hover {
|
||||
background-color: var(--color-background-highlight);
|
||||
border-color: var(--color-border-highlight);
|
||||
color: var(--color-foreground-highlight);
|
||||
}
|
||||
|
||||
button.primary {
|
||||
background-color: #ffcc00;
|
||||
border-color: #eebb00;
|
||||
color: var(--color-background);
|
||||
}
|
||||
|
||||
.content {
|
||||
width: 75%;
|
||||
}
|
||||
|
||||
.content h1 {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.inputs-wrapper {
|
||||
display: grid;
|
||||
grid-row-gap: 1em;
|
||||
grid-template-columns: 0.5fr 1fr;
|
||||
}
|
|
@ -4,7 +4,6 @@ import org.junit.jupiter.api.BeforeEach;
|
|||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.net.UnknownHostException;
|
||||
import java.util.Properties;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
|
@ -12,7 +11,8 @@ import static org.junit.jupiter.api.Assertions.assertThrows;
|
|||
class ServerConfigTest {
|
||||
@BeforeEach
|
||||
void clearSysProperties() {
|
||||
System.setProperties(new Properties());
|
||||
System.clearProperty(ServerConfig.SYSPROP_HOST);
|
||||
System.clearProperty(ServerConfig.SYSPROP_PORT);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
Loading…
Reference in a new issue