This commit is contained in:
parent
41995979e1
commit
5ba689841f
7 changed files with 215 additions and 64 deletions
|
@ -13,12 +13,18 @@
|
||||||
<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="library" name="Maven: ch.fritteli.labyrinth:labyrinth-generator:0.0.1" level="project" />
|
||||||
<orderEntry type="library" name="Maven: org.jetbrains:annotations:19.0.0" level="project" />
|
|
||||||
<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.20" 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.20" 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.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" 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" />
|
||||||
|
<orderEntry type="library" scope="TEST" name="Maven: org.junit.platform:junit-platform-commons:1.6.1" level="project" />
|
||||||
</component>
|
</component>
|
||||||
</module>
|
</module>
|
19
pom.xml
19
pom.xml
|
@ -28,6 +28,25 @@
|
||||||
<groupId>org.projectlombok</groupId>
|
<groupId>org.projectlombok</groupId>
|
||||||
<artifactId>lombok</artifactId>
|
<artifactId>lombok</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.jetbrains</groupId>
|
||||||
|
<artifactId>annotations</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.slf4j</groupId>
|
||||||
|
<artifactId>slf4j-api</artifactId>
|
||||||
|
<version>1.7.30</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.slf4j</groupId>
|
||||||
|
<artifactId>slf4j-simple</artifactId>
|
||||||
|
<version>1.7.30</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.junit.jupiter</groupId>
|
||||||
|
<artifactId>junit-jupiter-api</artifactId>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|
||||||
<build>
|
<build>
|
||||||
|
|
|
@ -0,0 +1,13 @@
|
||||||
|
package ch.fritteli.labyrinth.server;
|
||||||
|
|
||||||
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
|
public class ConfigurationException extends RuntimeException {
|
||||||
|
public ConfigurationException(@Nullable final String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ConfigurationException(@Nullable final String message, @Nullable final Throwable cause) {
|
||||||
|
super(message, cause);
|
||||||
|
}
|
||||||
|
}
|
|
@ -5,6 +5,7 @@ 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 com.sun.net.httpserver.Headers;
|
import com.sun.net.httpserver.Headers;
|
||||||
|
import com.sun.net.httpserver.HttpExchange;
|
||||||
import com.sun.net.httpserver.HttpServer;
|
import com.sun.net.httpserver.HttpServer;
|
||||||
import io.vavr.collection.HashMap;
|
import io.vavr.collection.HashMap;
|
||||||
import io.vavr.collection.HashSet;
|
import io.vavr.collection.HashSet;
|
||||||
|
@ -16,79 +17,32 @@ import io.vavr.control.Option;
|
||||||
import io.vavr.control.Try;
|
import io.vavr.control.Try;
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import lombok.NonNull;
|
import lombok.NonNull;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.jetbrains.annotations.Nullable;
|
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.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
import java.util.function.Function;
|
import java.util.function.Function;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
public class LabyrinthServer {
|
public class LabyrinthServer {
|
||||||
|
|
||||||
private final HttpServer httpServer;
|
private final HttpServer httpServer;
|
||||||
|
|
||||||
public LabyrinthServer(final int port) throws IOException {
|
public LabyrinthServer(@NonNull final ServerConfig config) throws IOException {
|
||||||
this.httpServer = HttpServer.create(new InetSocketAddress(port), 0);
|
this.httpServer = HttpServer.create(new InetSocketAddress(config.getAddress(), config.getPort()), 5);
|
||||||
this.httpServer.createContext("/create", exchange -> {
|
this.httpServer.createContext("/create", this::handleCreate);
|
||||||
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());
|
|
||||||
final int width = this.getOrDefault(requestParams.get(RequestParameter.WIDTH), Integer::valueOf, 1);
|
|
||||||
final int height = this.getOrDefault(requestParams.get(RequestParameter.HEIGHT), Integer::valueOf, 1);
|
|
||||||
final Option<Long> seedOption = requestParams.get(RequestParameter.ID).toTry().map(Long::valueOf).toOption();
|
|
||||||
final long seed;
|
|
||||||
final Option<OutputType> outputOption = requestParams.get(RequestParameter.OUTPUT).flatMap(OutputType::ofString);
|
|
||||||
final OutputType output;
|
|
||||||
final Headers responseHeaders = exchange.getResponseHeaders();
|
|
||||||
boolean needsRedirect = false;
|
|
||||||
if (seedOption.isEmpty()) {
|
|
||||||
needsRedirect = true;
|
|
||||||
seed = System.nanoTime();
|
|
||||||
} else {
|
|
||||||
seed = seedOption.get();
|
|
||||||
}
|
|
||||||
if (outputOption.isEmpty()) {
|
|
||||||
needsRedirect = true;
|
|
||||||
output = OutputType.HTML;
|
|
||||||
} else {
|
|
||||||
output = outputOption.get();
|
|
||||||
}
|
|
||||||
if (needsRedirect) {
|
|
||||||
responseHeaders.add("Location", "/create?width=" + width + "&height=" + height + "&output=" + output.toString() + "&id=" + seed);
|
|
||||||
exchange.sendResponseHeaders(302, -1);
|
|
||||||
exchange.close();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
responseHeaders.add("Content-type", output.getContentType());
|
|
||||||
exchange.sendResponseHeaders(200, 0);
|
|
||||||
final Labyrinth labyrinth = new Labyrinth(width, height, seed);
|
|
||||||
final byte[] render = output.render(labyrinth);
|
|
||||||
final OutputStream responseBody = exchange.getResponseBody();
|
|
||||||
responseBody.write(render);
|
|
||||||
responseBody.flush();
|
|
||||||
exchange.close();
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Option<LabyrinthServer> createListener() {
|
public static Option<LabyrinthServer> createAndStartServer() {
|
||||||
final String listenerPort = System.getProperty("fritteli.labyrinth.listenerport");
|
final Option<LabyrinthServer> serverOption = Try.of(ServerConfig::init)
|
||||||
final Option<LabyrinthServer> listenerOption = Option.of(listenerPort)
|
|
||||||
.toTry()
|
|
||||||
.map(Integer::valueOf)
|
|
||||||
.onFailure(cause -> System.err.println("Invalid port specified via system property 'fritteli.labyrinth.listenerport': "
|
|
||||||
+ listenerPort
|
|
||||||
+ ". Not starting webserver."))
|
|
||||||
.mapTry(LabyrinthServer::new)
|
.mapTry(LabyrinthServer::new)
|
||||||
.onFailure(cause -> System.err.println("Failed to create Listener: " + cause))
|
.onFailure(cause -> log.error("Failed to create LabyrinthServer.", cause))
|
||||||
.toOption();
|
.toOption();
|
||||||
listenerOption.forEach(LabyrinthServer::start);
|
serverOption.forEach(LabyrinthServer::start);
|
||||||
return listenerOption;
|
return serverOption;
|
||||||
}
|
}
|
||||||
|
|
||||||
private <T> T getOrDefault(@NonNull final Option<String> input, @NonNull final Function<String, T> mapper, @Nullable final T defaultValue) {
|
private <T> T getOrDefault(@NonNull final Option<String> input, @NonNull final Function<String, T> mapper, @Nullable final T defaultValue) {
|
||||||
|
@ -128,13 +82,47 @@ public class LabyrinthServer {
|
||||||
public void start() {
|
public void start() {
|
||||||
Runtime.getRuntime().addShutdownHook(new Thread(this::stop, "listener-stopper"));
|
Runtime.getRuntime().addShutdownHook(new Thread(this::stop, "listener-stopper"));
|
||||||
this.httpServer.start();
|
this.httpServer.start();
|
||||||
System.out.println("Listening on " + this.httpServer.getAddress());
|
log.info("Listening on {}", this.httpServer.getAddress());
|
||||||
}
|
}
|
||||||
|
|
||||||
public void stop() {
|
public void stop() {
|
||||||
System.out.println("Stopping listener ...");
|
log.info("Stopping server ...");
|
||||||
this.httpServer.stop(5);
|
this.httpServer.stop(5);
|
||||||
System.out.println("Listener stopped.");
|
log.info("Server stopped.");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handleCreate(HttpExchange exchange) throws IOException {
|
||||||
|
log.debug("Handling request to {}", exchange.getRequestURI());
|
||||||
|
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());
|
||||||
|
final int width = this.getOrDefault(requestParams.get(RequestParameter.WIDTH), Integer::valueOf, 1);
|
||||||
|
final int height = this.getOrDefault(requestParams.get(RequestParameter.HEIGHT), Integer::valueOf, 1);
|
||||||
|
final Option<Long> idOption = requestParams.get(RequestParameter.ID).toTry().map(Long::valueOf).toOption();
|
||||||
|
final Option<OutputType> outputOption = requestParams.get(RequestParameter.OUTPUT).flatMap(OutputType::ofString);
|
||||||
|
final Headers responseHeaders = exchange.getResponseHeaders();
|
||||||
|
final AtomicBoolean needsRedirect = new AtomicBoolean(false);
|
||||||
|
final long id = idOption.onEmpty(() -> needsRedirect.set(true)).getOrElse(System::nanoTime);
|
||||||
|
final OutputType output = outputOption.onEmpty(() -> needsRedirect.set(true)).getOrElse(OutputType.HTML);
|
||||||
|
if (needsRedirect.get()) {
|
||||||
|
responseHeaders.add("Location", "/create?width=" + width + "&height=" + height + "&output=" + output.toString() + "&id=" + id);
|
||||||
|
exchange.sendResponseHeaders(302, -1);
|
||||||
|
exchange.close();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
responseHeaders.add("Content-type", output.getContentType());
|
||||||
|
exchange.sendResponseHeaders(200, 0);
|
||||||
|
final Labyrinth labyrinth = new Labyrinth(width, height, id);
|
||||||
|
final byte[] render = output.render(labyrinth);
|
||||||
|
final OutputStream responseBody = exchange.getResponseBody();
|
||||||
|
responseBody.write(render);
|
||||||
|
responseBody.flush();
|
||||||
|
exchange.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
private enum RequestParameter {
|
private enum RequestParameter {
|
||||||
|
|
|
@ -1,8 +1,11 @@
|
||||||
package ch.fritteli.labyrinth.server;
|
package ch.fritteli.labyrinth.server;
|
||||||
|
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
public class Main {
|
public class Main {
|
||||||
public static void main(String[] args) {
|
public static void main(String[] args) {
|
||||||
LabyrinthServer.createListener()
|
LabyrinthServer.createAndStartServer()
|
||||||
.onEmpty(() -> System.err.println("Failed to create server. Stopping."));
|
.onEmpty(() -> log.error("Failed to create server. Stopping."));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
61
src/main/java/ch/fritteli/labyrinth/server/ServerConfig.java
Normal file
61
src/main/java/ch/fritteli/labyrinth/server/ServerConfig.java
Normal file
|
@ -0,0 +1,61 @@
|
||||||
|
package ch.fritteli.labyrinth.server;
|
||||||
|
|
||||||
|
import io.vavr.control.Try;
|
||||||
|
import lombok.AccessLevel;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.NonNull;
|
||||||
|
import lombok.Value;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
|
import java.net.InetAddress;
|
||||||
|
|
||||||
|
|
||||||
|
@AllArgsConstructor(access = AccessLevel.PRIVATE)
|
||||||
|
@Slf4j
|
||||||
|
@Value
|
||||||
|
public class ServerConfig {
|
||||||
|
public static final String SYSPROP_HOST = "fritteli.labyrinth.server.host";
|
||||||
|
public static final String SYSPROP_PORT = "fritteli.labyrinth.server.port";
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
InetAddress address;
|
||||||
|
int port;
|
||||||
|
|
||||||
|
public ServerConfig(@Nullable final String address, final int port) throws ConfigurationException {
|
||||||
|
this.address = validateAddress(address);
|
||||||
|
this.port = validatePort(port);
|
||||||
|
log.debug("host={}, port={}", this.address, this.port);
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
public static ServerConfig init() throws ConfigurationException {
|
||||||
|
final String host = System.getProperty(SYSPROP_HOST);
|
||||||
|
final String portString = System.getProperty(SYSPROP_PORT);
|
||||||
|
final int port = validatePort(portString);
|
||||||
|
return new ServerConfig(host, port);
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
private static InetAddress validateAddress(@Nullable final String address) {
|
||||||
|
return Try.of(() -> InetAddress.getByName(address))
|
||||||
|
.getOrElseThrow(cause -> new ConfigurationException("Invalid hostname/address: " + address, cause));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int validatePort(@Nullable final String portString) {
|
||||||
|
if (portString == null) {
|
||||||
|
log.info("No port configured; using default.");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return Try.of(() -> Integer.valueOf(portString))
|
||||||
|
.map(ServerConfig::validatePort)
|
||||||
|
.getOrElseThrow(cause -> new ConfigurationException("Failed to parse port specified in system property '" + SYSPROP_PORT + "': " + portString, cause));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int validatePort(final int port) {
|
||||||
|
if (port < 0 || port > 0xFFFF) {
|
||||||
|
throw new ConfigurationException("Port out of range (0..65535): " + port);
|
||||||
|
}
|
||||||
|
return port;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,61 @@
|
||||||
|
package ch.fritteli.labyrinth.server;
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
class ServerConfigTest {
|
||||||
|
@BeforeEach
|
||||||
|
void clearSysProperties() {
|
||||||
|
System.setProperties(new Properties());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testInit_noProperties() throws ConfigurationException {
|
||||||
|
|
||||||
|
// act
|
||||||
|
final ServerConfig sut = ServerConfig.init();
|
||||||
|
|
||||||
|
// assert
|
||||||
|
assertEquals("127.0.0.1", sut.getAddress().getHostAddress());
|
||||||
|
assertEquals("localhost", sut.getAddress().getHostName());
|
||||||
|
assertEquals(0, sut.getPort());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testInit_unparseablePort() {
|
||||||
|
// arrange
|
||||||
|
System.setProperty(ServerConfig.SYSPROP_PORT, "Hello World!");
|
||||||
|
|
||||||
|
// act / assert
|
||||||
|
assertThrows(ConfigurationException.class, ServerConfig::init);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testInit_invalidPort() {
|
||||||
|
// arrange
|
||||||
|
System.setProperty(ServerConfig.SYSPROP_PORT, "99999");
|
||||||
|
|
||||||
|
// act / assert
|
||||||
|
assertThrows(ConfigurationException.class, ServerConfig::init);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testInit() throws ConfigurationException, UnknownHostException {
|
||||||
|
// arrange
|
||||||
|
System.setProperty(ServerConfig.SYSPROP_HOST, "127.0.0.1");
|
||||||
|
System.setProperty(ServerConfig.SYSPROP_PORT, "12345");
|
||||||
|
|
||||||
|
// act
|
||||||
|
final ServerConfig sut = ServerConfig.init();
|
||||||
|
|
||||||
|
// assert
|
||||||
|
assertEquals("127.0.0.1", sut.getAddress().getHostAddress());
|
||||||
|
assertEquals(12345, sut.getPort());
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue