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="sourceFolder" forTests="false" /> | ||||
|     <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:fontbox:2.0.20" 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" 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> | ||||
| </module> | ||||
							
								
								
									
										19
									
								
								pom.xml
									
										
									
									
									
								
							
							
						
						
									
										19
									
								
								pom.xml
									
										
									
									
									
								
							|  | @ -28,6 +28,25 @@ | |||
| 			<groupId>org.projectlombok</groupId> | ||||
| 			<artifactId>lombok</artifactId> | ||||
| 		</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> | ||||
| 
 | ||||
| 	<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.text.TextRenderer; | ||||
| import com.sun.net.httpserver.Headers; | ||||
| import com.sun.net.httpserver.HttpExchange; | ||||
| import com.sun.net.httpserver.HttpServer; | ||||
| import io.vavr.collection.HashMap; | ||||
| import io.vavr.collection.HashSet; | ||||
|  | @ -16,79 +17,32 @@ import io.vavr.control.Option; | |||
| import io.vavr.control.Try; | ||||
| import lombok.Getter; | ||||
| import lombok.NonNull; | ||||
| import lombok.extern.slf4j.Slf4j; | ||||
| import org.jetbrains.annotations.Nullable; | ||||
| 
 | ||||
| import java.io.IOException; | ||||
| import java.io.OutputStream; | ||||
| import java.net.InetSocketAddress; | ||||
| import java.nio.charset.StandardCharsets; | ||||
| import java.util.concurrent.atomic.AtomicBoolean; | ||||
| import java.util.function.Function; | ||||
| 
 | ||||
| @Slf4j | ||||
| public class LabyrinthServer { | ||||
| 
 | ||||
|     private final HttpServer httpServer; | ||||
| 
 | ||||
|     public LabyrinthServer(final int port) throws IOException { | ||||
|         this.httpServer = HttpServer.create(new InetSocketAddress(port), 0); | ||||
|         this.httpServer.createContext("/create", exchange -> { | ||||
|             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 LabyrinthServer(@NonNull final ServerConfig config) throws IOException { | ||||
|         this.httpServer = HttpServer.create(new InetSocketAddress(config.getAddress(), config.getPort()), 5); | ||||
|         this.httpServer.createContext("/create", this::handleCreate); | ||||
|     } | ||||
| 
 | ||||
|     public static Option<LabyrinthServer> createListener() { | ||||
|         final String listenerPort = System.getProperty("fritteli.labyrinth.listenerport"); | ||||
|         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.")) | ||||
|     public static Option<LabyrinthServer> createAndStartServer() { | ||||
|         final Option<LabyrinthServer> serverOption = Try.of(ServerConfig::init) | ||||
|                 .mapTry(LabyrinthServer::new) | ||||
|                 .onFailure(cause -> System.err.println("Failed to create Listener: " + cause)) | ||||
|                 .onFailure(cause -> log.error("Failed to create LabyrinthServer.", cause)) | ||||
|                 .toOption(); | ||||
|         listenerOption.forEach(LabyrinthServer::start); | ||||
|         return listenerOption; | ||||
|         serverOption.forEach(LabyrinthServer::start); | ||||
|         return serverOption; | ||||
|     } | ||||
| 
 | ||||
|     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() { | ||||
|         Runtime.getRuntime().addShutdownHook(new Thread(this::stop, "listener-stopper")); | ||||
|         this.httpServer.start(); | ||||
|         System.out.println("Listening on " + this.httpServer.getAddress()); | ||||
|         log.info("Listening on {}", this.httpServer.getAddress()); | ||||
|     } | ||||
| 
 | ||||
|     public void stop() { | ||||
|         System.out.println("Stopping listener ..."); | ||||
|         log.info("Stopping server ..."); | ||||
|         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 { | ||||
|  |  | |||
|  | @ -1,8 +1,11 @@ | |||
| package ch.fritteli.labyrinth.server; | ||||
| 
 | ||||
| import lombok.extern.slf4j.Slf4j; | ||||
| 
 | ||||
| @Slf4j | ||||
| public class Main { | ||||
|     public static void main(String[] args) { | ||||
|         LabyrinthServer.createListener() | ||||
|                 .onEmpty(() -> System.err.println("Failed to create server. Stopping.")); | ||||
|         LabyrinthServer.createAndStartServer() | ||||
|                 .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…
	
	Add table
		Add a link
		
	
		Reference in a new issue