diff --git a/src/main/java/ch/fritteli/labyrinth/server/OutputType.java b/src/main/java/ch/fritteli/labyrinth/server/OutputType.java index 60ff74f..d40f668 100644 --- a/src/main/java/ch/fritteli/labyrinth/server/OutputType.java +++ b/src/main/java/ch/fritteli/labyrinth/server/OutputType.java @@ -56,6 +56,7 @@ public enum OutputType { private final Function render; @Getter private final boolean attachment; + @Getter @NonNull private final List names; diff --git a/src/main/java/ch/fritteli/labyrinth/server/handler/CreateHandler.java b/src/main/java/ch/fritteli/labyrinth/server/handler/CreateHandler.java index 46cdfd4..c5a304a 100644 --- a/src/main/java/ch/fritteli/labyrinth/server/handler/CreateHandler.java +++ b/src/main/java/ch/fritteli/labyrinth/server/handler/CreateHandler.java @@ -17,9 +17,11 @@ import io.vavr.collection.Set; import io.vavr.collection.Stream; 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 org.slf4j.MDC; import java.nio.ByteBuffer; import java.time.Instant; @@ -27,6 +29,7 @@ import java.time.temporal.ChronoUnit; import java.util.Deque; import java.util.Map; import java.util.Random; +import java.util.function.Function; @Slf4j public class CreateHandler extends AbstractHttpHandler { @@ -39,8 +42,10 @@ public class CreateHandler extends AbstractHttpHandler { this.createLabyrinthFromRequestParameters(exchange.getQueryParameters()) .onFailure(e -> { log.error("Error creating Labyrinth from request", e); - exchange.setStatusCode(StatusCodes.INTERNAL_SERVER_ERROR); - exchange.setReasonPhrase(StatusCodes.INTERNAL_SERVER_ERROR_STRING); + exchange.setStatusCode(StatusCodes.INTERNAL_SERVER_ERROR) + .setReasonPhrase(StatusCodes.INTERNAL_SERVER_ERROR_STRING) + .getResponseSender() + .send(e.getMessage()); }) .forEach(tuple -> { final OutputType outputType = tuple._1(); @@ -50,30 +55,47 @@ public class CreateHandler extends AbstractHttpHandler { bytes = outputType.render(labyrinth); } catch (@NonNull final Exception e) { log.error("Error rendering Labyrinth", e); - exchange.setStatusCode(StatusCodes.INTERNAL_SERVER_ERROR); - exchange.setReasonPhrase(StatusCodes.INTERNAL_SERVER_ERROR_STRING); + exchange.setStatusCode(StatusCodes.INTERNAL_SERVER_ERROR) + .setReasonPhrase(StatusCodes.INTERNAL_SERVER_ERROR_STRING) + .getResponseSender() + .send("Error creating the Labyrinth. Please contact the administrator. Request id=%s".formatted(MDC.get("correlationId"))); return; } + final long durationMillis = start.until(Instant.now(), ChronoUnit.MILLIS); exchange.getResponseHeaders() .put(Headers.CONTENT_TYPE, outputType.getContentType()) .put(HttpString.tryFromString("X-Labyrinth-ID"), String.valueOf(labyrinth.getRandomSeed())) .put(HttpString.tryFromString("X-Labyrinth-Width"), String.valueOf(labyrinth.getWidth())) .put(HttpString.tryFromString("X-Labyrinth-Height"), String.valueOf(labyrinth.getHeight())) - .put(HttpString.tryFromString("X-Labyrinth-Generation-Duration-millis"), String.valueOf(start.until(Instant.now(), ChronoUnit.MILLIS))); + .put(HttpString.tryFromString("X-Labyrinth-Generation-Duration-millis"), String.valueOf(durationMillis)); exchange.getResponseSender().send(ByteBuffer.wrap(bytes)); - log.debug("Create request handled."); + log.debug("Create request handled in {}ms.", durationMillis); }); } - private @NonNull Try> createLabyrinthFromRequestParameters(final Map> queryParameters) { + @NonNull + private Try> createLabyrinthFromRequestParameters(final Map> queryParameters) { return new ParametersToLabyrinthExtractor(queryParameters).createLabyrinth(); } private enum RequestParameter { - WIDTH("w", "width"), HEIGHT("h", "height"), ID("i", "id"), OUTPUT("o", "output"); + WIDTH(p -> Try.of(() -> Integer.parseInt(p)).toOption(), "w", "width"), + HEIGHT(p -> Try.of(() -> Integer.parseInt(p)).toOption(), "h", "height"), + ID(p -> Try.of(() -> Long.parseLong(p)).toOption(), "i", "id"), + OUTPUT(OutputType::ofString, "o", "output"); + @NonNull + private final Function> extractor; + @Getter + @NonNull private final Set names; RequestParameter(@NonNull final String... names) { + this.extractor = null; + this.names = HashSet.of(names); + } + + RequestParameter(@NonNull final Function> extractor, @NonNull final String... names) { + this.extractor = extractor; this.names = HashSet.of(names); } @@ -83,6 +105,10 @@ public class CreateHandler extends AbstractHttpHandler { } return Stream.of(values()).find(param -> param.names.exists(name::equalsIgnoreCase)); } + + @NonNull Option extractParameterValue(@NonNull final String parameter) { + return (Option) this.extractor.apply(parameter); + } } private static class ParametersToLabyrinthExtractor { @@ -98,22 +124,37 @@ public class CreateHandler extends AbstractHttpHandler { @NonNull Try> createLabyrinth() { - final Option output = getParameterValues(RequestParameter.OUTPUT) - .foldLeft(Option.none(), (type, param) -> type.orElse(() -> OutputType.ofString(param))); - final Option width = getParameterValues(RequestParameter.WIDTH) - .foldLeft(Option.none(), (value, param) -> value.orElse(() -> Try.of(() -> Integer.parseInt(param)).toOption())); - final Option height = getParameterValues(RequestParameter.HEIGHT) - .foldLeft(Option.none(), (value, param) -> value.orElse(() -> Try.of(() -> Integer.parseInt(param)).toOption())); - final Option id = getParameterValues(RequestParameter.ID) - .foldLeft(Option.none(), (value, param) -> value.orElse(() -> Try.of(() -> Long.parseLong(param)).toOption())); + final Option output = getParameterValue(RequestParameter.OUTPUT); + final Option width = getParameterValue(RequestParameter.WIDTH); + final Option height = getParameterValue(RequestParameter.HEIGHT); + final Option id = getParameterValue(RequestParameter.ID); - return Try.of(() -> { - final OutputType t1 = output.get(); - final Integer width1 = width.get(); - final Integer height1 = height.get(); - final Long orElse = id.getOrElse(() -> new Random().nextLong()); - return Tuple.of(t1, new Labyrinth(width1, height1, orElse)); - }); + if (output.isEmpty()) { + return Try.failure(new IllegalArgumentException("Path parameter %s is required and must be one of: %s".formatted( + RequestParameter.OUTPUT.getNames().mkString("'", " / ", "'"), + Stream.of(OutputType.values()) + .flatMap(OutputType::getNames) + .mkString(", ") + ))); + } + if (width.isEmpty()) { + return Try.failure(new IllegalArgumentException("Query parameter %s is required and must be a positive integer value".formatted( + RequestParameter.WIDTH.getNames().mkString("'", " / ", "'") + ))); + } + if (height.isEmpty()) { + return Try.failure(new IllegalArgumentException("Query parameter %s is required and must be a positive integer value".formatted( + RequestParameter.HEIGHT.getNames().mkString("'", " / ", "'") + ))); + } + + return Try.of(() -> Tuple.of(output.get(), new Labyrinth(width.get(), height.get(), id.getOrElse(() -> new Random().nextLong())))); + } + + @NonNull + private Option getParameterValue(@NonNull final RequestParameter parameter) { + return this.getParameterValues(parameter) + .foldLeft(Option.none(), (type, param) -> type.orElse(() -> parameter.extractParameterValue(param))); } @NonNull