feature/render-binary #1
9 changed files with 362 additions and 62 deletions
|
@ -20,7 +20,9 @@ steps:
|
||||||
from_secret: repo-token
|
from_secret: repo-token
|
||||||
commands:
|
commands:
|
||||||
- mvn -s maven-settings.xml deploy -DskipTests=true
|
- mvn -s maven-settings.xml deploy -DskipTests=true
|
||||||
when:
|
trigger:
|
||||||
branch:
|
branch:
|
||||||
include:
|
- master
|
||||||
- master
|
event:
|
||||||
|
exclude:
|
||||||
|
- pull_request
|
||||||
|
|
|
@ -12,16 +12,17 @@
|
||||||
</content>
|
</content>
|
||||||
<orderEntry type="inheritedJdk" />
|
<orderEntry type="inheritedJdk" />
|
||||||
<orderEntry type="sourceFolder" forTests="false" />
|
<orderEntry type="sourceFolder" forTests="false" />
|
||||||
<orderEntry type="library" name="Maven: ch.fritteli.labyrinth:labyrinth-generator:0.0.1" level="project" />
|
<orderEntry type="module" module-name="labyrinth-generator" />
|
||||||
<orderEntry type="library" name="Maven: org.apache.pdfbox:pdfbox:2.0.20" level="project" />
|
<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.20" 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: 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:0.10.2" level="project" />
|
||||||
<orderEntry type="library" name="Maven: io.vavr:vavr-match: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" 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.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-api:1.7.35" level="project" />
|
||||||
<orderEntry type="library" name="Maven: org.slf4j:slf4j-simple:1.7.30" 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.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.apiguardian:apiguardian-api:1.1.0" level="project" />
|
||||||
<orderEntry type="library" scope="TEST" name="Maven: org.opentest4j:opentest4j:1.2.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>
|
<artifactId>labyrinth-server</artifactId>
|
||||||
<version>0.0.1-SNAPSHOT</version>
|
<version>0.0.1-SNAPSHOT</version>
|
||||||
|
|
||||||
|
<properties>
|
||||||
|
<logback.version>1.2.10</logback.version>
|
||||||
|
<slf4j.version>1.7.35</slf4j.version>
|
||||||
|
</properties>
|
||||||
|
|
||||||
<dependencies>
|
<dependencies>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>ch.fritteli.labyrinth</groupId>
|
<groupId>ch.fritteli.labyrinth</groupId>
|
||||||
<artifactId>labyrinth-generator</artifactId>
|
<artifactId>labyrinth-generator</artifactId>
|
||||||
<version>0.0.1</version>
|
<version>0.0.2</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>io.vavr</groupId>
|
<groupId>io.vavr</groupId>
|
||||||
|
@ -35,12 +40,12 @@
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.slf4j</groupId>
|
<groupId>org.slf4j</groupId>
|
||||||
<artifactId>slf4j-api</artifactId>
|
<artifactId>slf4j-api</artifactId>
|
||||||
<version>1.7.30</version>
|
<version>${slf4j.version}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.slf4j</groupId>
|
<groupId>ch.qos.logback</groupId>
|
||||||
<artifactId>slf4j-simple</artifactId>
|
<artifactId>logback-classic</artifactId>
|
||||||
<version>1.7.30</version>
|
<version>${logback.version}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.junit.jupiter</groupId>
|
<groupId>org.junit.jupiter</groupId>
|
||||||
|
@ -95,12 +100,24 @@
|
||||||
</distributionManagement>
|
</distributionManagement>
|
||||||
<repositories>
|
<repositories>
|
||||||
<repository>
|
<repository>
|
||||||
<id>repo.gittr.ch</id>
|
<id>repo.gittr.ch.releases</id>
|
||||||
<url>https://repo.gittr.ch/</url>
|
<url>https://repo.gittr.ch/releases/</url>
|
||||||
<releases>
|
<releases>
|
||||||
<enabled>true</enabled>
|
<enabled>true</enabled>
|
||||||
<updatePolicy>never</updatePolicy>
|
<updatePolicy>never</updatePolicy>
|
||||||
</releases>
|
</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>
|
<snapshots>
|
||||||
<enabled>true</enabled>
|
<enabled>true</enabled>
|
||||||
<updatePolicy>always</updatePolicy>
|
<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.html.HTMLRenderer;
|
||||||
import ch.fritteli.labyrinth.generator.renderer.pdf.PDFRenderer;
|
import ch.fritteli.labyrinth.generator.renderer.pdf.PDFRenderer;
|
||||||
import ch.fritteli.labyrinth.generator.renderer.text.TextRenderer;
|
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.Headers;
|
||||||
import com.sun.net.httpserver.HttpExchange;
|
import com.sun.net.httpserver.HttpExchange;
|
||||||
import com.sun.net.httpserver.HttpServer;
|
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.Option;
|
||||||
import io.vavr.control.Try;
|
import io.vavr.control.Try;
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
|
@ -18,17 +24,33 @@ import org.jetbrains.annotations.Nullable;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.OutputStream;
|
import java.io.OutputStream;
|
||||||
import java.net.InetSocketAddress;
|
import java.net.InetSocketAddress;
|
||||||
|
import java.net.URISyntaxException;
|
||||||
import java.nio.charset.StandardCharsets;
|
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.concurrent.atomic.AtomicBoolean;
|
||||||
import java.util.function.Function;
|
import java.util.function.Function;
|
||||||
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
public class LabyrinthServer {
|
public class LabyrinthServer {
|
||||||
|
@NonNull
|
||||||
private final HttpServer httpServer;
|
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 = 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("/create", this::handleCreate);
|
||||||
|
this.httpServer.createContext("/render", this::handleRender);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Option<LabyrinthServer> createAndStartServer() {
|
public static Option<LabyrinthServer> createAndStartServer() {
|
||||||
|
@ -83,52 +105,111 @@ public class LabyrinthServer {
|
||||||
public void stop() {
|
public void stop() {
|
||||||
log.info("Stopping server ...");
|
log.info("Stopping server ...");
|
||||||
this.httpServer.stop(5);
|
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.");
|
log.info("Server stopped.");
|
||||||
}
|
}
|
||||||
|
|
||||||
private void handleCreate(HttpExchange exchange) throws IOException {
|
private void handleCreate(HttpExchange exchange) throws IOException {
|
||||||
log.debug("Handling request to {}", exchange.getRequestURI());
|
this.executorService.submit(() -> {
|
||||||
final String requestMethod = exchange.getRequestMethod();
|
log.debug("Handling request to {}", exchange.getRequestURI());
|
||||||
if (!requestMethod.equals("GET")) {
|
try {
|
||||||
exchange.getResponseBody().close();
|
final String requestMethod = exchange.getRequestMethod();
|
||||||
exchange.sendResponseHeaders(405, -1);
|
if (!requestMethod.equals("GET")) {
|
||||||
exchange.close();
|
exchange.getResponseBody().close();
|
||||||
return;
|
exchange.sendResponseHeaders(405, -1);
|
||||||
}
|
return;
|
||||||
final Map<RequestParameter, String> requestParams = this.parseQueryString(exchange.getRequestURI().getQuery());
|
}
|
||||||
final int width = this.getOrDefault(requestParams.get(RequestParameter.WIDTH), Integer::valueOf, 5);
|
final Map<RequestParameter, String> requestParams = this.parseQueryString(exchange.getRequestURI().getQuery());
|
||||||
final int height = this.getOrDefault(requestParams.get(RequestParameter.HEIGHT), Integer::valueOf, 5);
|
final int width = this.getOrDefault(requestParams.get(RequestParameter.WIDTH), Integer::valueOf, 5);
|
||||||
final Option<Long> idOption = requestParams.get(RequestParameter.ID).toTry().map(Long::valueOf).toOption();
|
final int height = this.getOrDefault(requestParams.get(RequestParameter.HEIGHT), Integer::valueOf, 5);
|
||||||
final Option<OutputType> outputOption = requestParams.get(RequestParameter.OUTPUT).flatMap(OutputType::ofString);
|
final Option<Long> idOption = requestParams.get(RequestParameter.ID).toTry().map(Long::valueOf).toOption();
|
||||||
final Headers responseHeaders = exchange.getResponseHeaders();
|
final Option<OutputType> outputOption = requestParams.get(RequestParameter.OUTPUT).flatMap(OutputType::ofString);
|
||||||
final AtomicBoolean needsRedirect = new AtomicBoolean(false);
|
final Headers responseHeaders = exchange.getResponseHeaders();
|
||||||
final long id = idOption.onEmpty(() -> needsRedirect.set(true)).getOrElse(System::nanoTime);
|
final AtomicBoolean needsRedirect = new AtomicBoolean(false);
|
||||||
final OutputType output = outputOption.onEmpty(() -> needsRedirect.set(true)).getOrElse(OutputType.HTML);
|
final long id = idOption.onEmpty(() -> needsRedirect.set(true)).getOrElse(System::nanoTime);
|
||||||
if (needsRedirect.get()) {
|
final OutputType output = outputOption.onEmpty(() -> needsRedirect.set(true)).getOrElse(OutputType.HTML);
|
||||||
responseHeaders.add("Location", "/create?width=" + width + "&height=" + height + "&output=" + output.toString() + "&id=" + id);
|
if (needsRedirect.get()) {
|
||||||
exchange.sendResponseHeaders(302, -1);
|
responseHeaders.add("Location", "/create?width=" + width + "&height=" + height + "&output=" + output.toString() + "&id=" + id);
|
||||||
exchange.close();
|
exchange.sendResponseHeaders(302, -1);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
final Labyrinth labyrinth = new Labyrinth(width, height, id);
|
final Labyrinth labyrinth = new Labyrinth(width, height, id);
|
||||||
final byte[] render;
|
final byte[] render;
|
||||||
try {
|
try {
|
||||||
render = output.render(labyrinth);
|
render = output.render(labyrinth);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
responseHeaders.add("Content-type", "text/plain; charset=UTF-8");
|
responseHeaders.add("Content-type", "text/plain; charset=UTF-8");
|
||||||
exchange.sendResponseHeaders(500, 0);
|
exchange.sendResponseHeaders(500, 0);
|
||||||
final OutputStream responseBody = exchange.getResponseBody();
|
final OutputStream responseBody = exchange.getResponseBody();
|
||||||
responseBody.write(("Error: " + e).getBytes(StandardCharsets.UTF_8));
|
responseBody.write(("Error: " + e).getBytes(StandardCharsets.UTF_8));
|
||||||
responseBody.flush();
|
responseBody.flush();
|
||||||
exchange.close();
|
return;
|
||||||
return;
|
}
|
||||||
}
|
responseHeaders.add("Content-type", output.getContentType());
|
||||||
responseHeaders.add("Content-type", output.getContentType());
|
if (output.equals(OutputType.BINARY)) {
|
||||||
exchange.sendResponseHeaders(200, 0);
|
responseHeaders.add("Content-disposition", "attachment; filename=\"labyrinth-" + width + "x" + height + "-" + id + ".laby\"");
|
||||||
final OutputStream responseBody = exchange.getResponseBody();
|
}
|
||||||
responseBody.write(render);
|
exchange.sendResponseHeaders(200, 0);
|
||||||
responseBody.flush();
|
final OutputStream responseBody = exchange.getResponseBody();
|
||||||
exchange.close();
|
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());
|
||||||
|
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 enum RequestParameter {
|
private enum RequestParameter {
|
||||||
|
@ -154,7 +235,8 @@ public class LabyrinthServer {
|
||||||
private enum OutputType {
|
private enum OutputType {
|
||||||
TEXT_PLAIN("text/plain; charset=UTF-8", labyrinth -> TextRenderer.newInstance().render(labyrinth).getBytes(StandardCharsets.UTF_8), "t", "text"),
|
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"),
|
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
|
@Getter
|
||||||
@NonNull
|
@NonNull
|
||||||
private final String contentType;
|
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 org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
import java.net.UnknownHostException;
|
import java.net.UnknownHostException;
|
||||||
import java.util.Properties;
|
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||||
|
@ -12,7 +11,8 @@ import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||||
class ServerConfigTest {
|
class ServerConfigTest {
|
||||||
@BeforeEach
|
@BeforeEach
|
||||||
void clearSysProperties() {
|
void clearSysProperties() {
|
||||||
System.setProperties(new Properties());
|
System.clearProperty(ServerConfig.SYSPROP_HOST);
|
||||||
|
System.clearProperty(ServerConfig.SYSPROP_PORT);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
Loading…
Reference in a new issue