diff --git a/pom.xml b/pom.xml index 964d22c..d8c084f 100644 --- a/pom.xml +++ b/pom.xml @@ -15,6 +15,7 @@ https://manuel.friedli.info/labyrinth.html + 2.14.2 17 17 24.0.1 @@ -59,6 +60,16 @@ pdfbox ${pdfbox.version} + + com.fasterxml.jackson.core + jackson-annotations + ${jackson.version} + + + com.fasterxml.jackson.core + jackson-databind + ${jackson.version} + org.slf4j slf4j-api @@ -82,6 +93,24 @@ + + org.jsonschema2pojo + jsonschema2pojo-maven-plugin + 1.2.1 + + + generate-sources + + generate + + + + src/main/resources/labyrinth.schema.json + + + + + maven-assembly-plugin diff --git a/src/main/java/ch/fritteli/labyrinth/generator/Main.java b/src/main/java/ch/fritteli/labyrinth/generator/Main.java index 9c16e66..423db6a 100644 --- a/src/main/java/ch/fritteli/labyrinth/generator/Main.java +++ b/src/main/java/ch/fritteli/labyrinth/generator/Main.java @@ -3,23 +3,28 @@ package ch.fritteli.labyrinth.generator; import ch.fritteli.labyrinth.generator.model.Labyrinth; import ch.fritteli.labyrinth.generator.renderer.html.HTMLRenderer; import ch.fritteli.labyrinth.generator.renderer.htmlfile.HTMLFileRenderer; +import ch.fritteli.labyrinth.generator.renderer.json.JsonRenderer; +import ch.fritteli.labyrinth.generator.renderer.jsonfile.JsonFileRenderer; import ch.fritteli.labyrinth.generator.renderer.pdffile.PDFFileRenderer; import ch.fritteli.labyrinth.generator.renderer.text.TextRenderer; import ch.fritteli.labyrinth.generator.renderer.textfile.TextFileRenderer; -import lombok.NonNull; -import lombok.extern.slf4j.Slf4j; - import java.nio.file.Path; import java.nio.file.Paths; +import lombok.NonNull; +import lombok.experimental.UtilityClass; +import lombok.extern.slf4j.Slf4j; @Slf4j +@UtilityClass public class Main { + public static void main(@NonNull final String[] args) { final int width = 20; final int height = 30; final Labyrinth labyrinth = new Labyrinth(width, height/*, 0*/); final TextRenderer textRenderer = TextRenderer.newInstance(); final HTMLRenderer htmlRenderer = HTMLRenderer.newInstance(); + final JsonRenderer jsonRenderer = JsonRenderer.newInstance(); final Path userHome = Paths.get(System.getProperty("user.home")); final String baseFilename = getBaseFilename(labyrinth); final TextFileRenderer textFileRenderer = TextFileRenderer.newInstance() @@ -27,6 +32,8 @@ public class Main { .setTargetSolutionFile(userHome.resolve(baseFilename + "-solution.txt")); final HTMLFileRenderer htmlFileRenderer = HTMLFileRenderer.newInstance() .setTargetFile(userHome.resolve(baseFilename + ".html")); + final JsonFileRenderer jsonFileRenderer = JsonFileRenderer.newInstance() + .setTargetFile(userHome.resolve(baseFilename + ".json")); final PDFFileRenderer pdfFileRenderer = PDFFileRenderer.newInstance() .setTargetFile(userHome.resolve(baseFilename + ".pdf")); @@ -37,10 +44,14 @@ public class Main { log.info("Text rendering with solution:\n{}", textRenderer.setRenderSolution(true).render(labyrinth)); // Render HTML to stdout log.info("HTML rendering:\n{}", htmlRenderer.render(labyrinth)); + // Render JSON to stdout + log.info("JSON rendering:\n{}", jsonRenderer.render(labyrinth)); // Render Labyrinth and solution to (separate) files log.info("Text rendering to file:\n{}", textFileRenderer.render(labyrinth)); // Render HTML to file log.info("HTML rendering to file:\n{}", htmlFileRenderer.render(labyrinth)); + // Render JSON to file + log.info("JSON rendering to file:\n{}", jsonFileRenderer.render(labyrinth)); // Render PDF to file log.info("PDF rendering to file:\n{}", pdfFileRenderer.render(labyrinth)); } diff --git a/src/main/java/ch/fritteli/labyrinth/generator/renderer/json/Generator.java b/src/main/java/ch/fritteli/labyrinth/generator/renderer/json/Generator.java new file mode 100644 index 0000000..09e69b1 --- /dev/null +++ b/src/main/java/ch/fritteli/labyrinth/generator/renderer/json/Generator.java @@ -0,0 +1,48 @@ +package ch.fritteli.labyrinth.generator.renderer.json; + +import ch.fritteli.labyrinth.generator.json.JsonCell; +import ch.fritteli.labyrinth.generator.json.JsonLabyrinth; +import ch.fritteli.labyrinth.generator.model.Direction; +import ch.fritteli.labyrinth.generator.model.Labyrinth; +import ch.fritteli.labyrinth.generator.model.Tile; +import java.util.ArrayList; +import java.util.List; +import lombok.AccessLevel; +import lombok.NonNull; +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor(access = AccessLevel.PACKAGE) +class Generator { + + @NonNull + private final Labyrinth labyrinth; + + @NonNull + JsonLabyrinth generate() { + final JsonLabyrinth result = new JsonLabyrinth(); + result.setId(this.labyrinth.getRandomSeed()); + result.setWidth(this.labyrinth.getWidth()); + result.setHeight(this.labyrinth.getHeight()); + final List> rows = new ArrayList<>(); + for (int y = 0; y < this.labyrinth.getHeight(); y++) { + final ArrayList row = new ArrayList<>(); + for (int x = 0; x < this.labyrinth.getWidth(); x++) { + // x and y are not effectively final and can therefore not be accessed from within the lambda. Hence, create the string beforehand. + final String exceptionString = "Failed to obtain tile at %dx%d, although labyrinth has dimensoins %dx%d" + .formatted(x, y, this.labyrinth.getWidth(), this.labyrinth.getHeight()); + final Tile tile = this.labyrinth.getTileAt(x, y) + .getOrElseThrow(() -> new IllegalStateException(exceptionString)); + final JsonCell cell = new JsonCell(); + cell.setTop(tile.hasWallAt(Direction.TOP)); + cell.setRight(tile.hasWallAt(Direction.RIGHT)); + cell.setBottom(tile.hasWallAt(Direction.BOTTOM)); + cell.setLeft(tile.hasWallAt(Direction.LEFT)); + cell.setSolution(tile.isSolution()); + row.add(cell); + } + rows.add(row); + } + result.setGrid(rows); + return result; + } +} diff --git a/src/main/java/ch/fritteli/labyrinth/generator/renderer/json/JsonRenderer.java b/src/main/java/ch/fritteli/labyrinth/generator/renderer/json/JsonRenderer.java new file mode 100644 index 0000000..e2b3fe2 --- /dev/null +++ b/src/main/java/ch/fritteli/labyrinth/generator/renderer/json/JsonRenderer.java @@ -0,0 +1,68 @@ +package ch.fritteli.labyrinth.generator.renderer.json; + +import ch.fritteli.labyrinth.generator.json.JsonCell; +import ch.fritteli.labyrinth.generator.json.JsonLabyrinth; +import ch.fritteli.labyrinth.generator.model.Labyrinth; +import ch.fritteli.labyrinth.generator.renderer.Renderer; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import java.util.ArrayList; +import java.util.List; +import lombok.NonNull; + +public class JsonRenderer implements Renderer { + + @NonNull + private final ObjectMapper objectMapper; + + private JsonRenderer() { + this.objectMapper = new ObjectMapper() + .enable(SerializationFeature.INDENT_OUTPUT); + } + + @NonNull + public static JsonRenderer newInstance() { + return new JsonRenderer(); + } + + @NonNull + private static JsonLabyrinth createSingleCellLabyrinth() { + // This is the only cell. + final JsonCell cell = new JsonCell(); + cell.setRight(true); + cell.setLeft(true); + cell.setSolution(true); + // Wrap that in a nested list. + final List> rows = new ArrayList<>(); + rows.add(new ArrayList<>()); + rows.get(0).add(cell); + // Wrap it all in an instance of JsonLabyrinth. + final JsonLabyrinth jsonLabyrinth = new JsonLabyrinth(); + jsonLabyrinth.setId(0L); + jsonLabyrinth.setGrid(rows); + return jsonLabyrinth; + } + + @NonNull + private String toString(@NonNull final JsonLabyrinth jsonLabyrinth) { + try { + return this.objectMapper.writeValueAsString(jsonLabyrinth); + } catch (final JsonProcessingException e) { + throw new RuntimeException(e); + } + } + + @NonNull + @Override + public String render(@NonNull final Labyrinth labyrinth) { + final JsonLabyrinth jsonLabyrinth; + if (labyrinth.getWidth() == 0 || labyrinth.getHeight() == 0) { + jsonLabyrinth = createSingleCellLabyrinth(); + } else { + final Generator generator = new Generator(labyrinth); + jsonLabyrinth = generator.generate(); + } + return toString(jsonLabyrinth); + } +} diff --git a/src/main/java/ch/fritteli/labyrinth/generator/renderer/jsonfile/JsonFileRenderer.java b/src/main/java/ch/fritteli/labyrinth/generator/renderer/jsonfile/JsonFileRenderer.java new file mode 100644 index 0000000..f1af684 --- /dev/null +++ b/src/main/java/ch/fritteli/labyrinth/generator/renderer/jsonfile/JsonFileRenderer.java @@ -0,0 +1,68 @@ +package ch.fritteli.labyrinth.generator.renderer.jsonfile; + +import ch.fritteli.labyrinth.generator.model.Labyrinth; +import ch.fritteli.labyrinth.generator.renderer.Renderer; +import ch.fritteli.labyrinth.generator.renderer.json.JsonRenderer; +import io.vavr.control.Option; +import io.vavr.control.Try; +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.NoSuchElementException; +import lombok.NonNull; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class JsonFileRenderer implements Renderer { + + @NonNull + private static final JsonRenderer JSON_RENDERER = JsonRenderer.newInstance(); + @NonNull + private Option targetFile; + + private JsonFileRenderer() { + this.targetFile = Try + .of(() -> Files.createTempFile("labyrinth_", ".json")) + .onFailure(ex -> log.error("Unable to set default target file.", ex)) + .toOption(); + } + + @NonNull + public static JsonFileRenderer newInstance() { + return new JsonFileRenderer(); + } + + public boolean isTargetFileDefinedAndWritable() { + return this.targetFile + .map(Path::toFile) + .exists(File::canWrite); + } + + @NonNull + public JsonFileRenderer setTargetFile(@NonNull final Path targetFile) { + this.targetFile = Option.of(targetFile); + return this; + } + + @NonNull + @Override + public Path render(@NonNull final Labyrinth labyrinth) { + if (!this.isTargetFileDefinedAndWritable()) { + try { + Files.createFile(this.targetFile.get()); + } catch (final IOException | NoSuchElementException e) { + throw new IllegalArgumentException("Cannot write to target file.", e); + } + } + final String json = JSON_RENDERER.render(labyrinth); + final Path outputFile = this.targetFile.get(); + try { + Files.writeString(outputFile, json, StandardCharsets.UTF_8); + } catch (final IOException e) { + log.error("Failed writing to file %s".formatted(outputFile.normalize()), e); + } + return outputFile; + } +} diff --git a/src/main/resources/labyrinth.schema.json b/src/main/resources/labyrinth.schema.json new file mode 100644 index 0000000..b5be24b --- /dev/null +++ b/src/main/resources/labyrinth.schema.json @@ -0,0 +1,69 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://manuel.friedli.info/labyrinth-1/labyrinth.schema.json", + "javaType": "ch.fritteli.labyrinth.generator.json.JsonLabyrinth", + "type": "object", + "additionalProperties": false, + "required": [ + "id", + "width", + "height", + "grid" + ], + "properties": { + "id": { + "type": "integer", + "existingJavaType": "java.lang.Long" + }, + "width": { + "type": "integer", + "minimum": 1 + }, + "height": { + "type": "integer", + "minimum": 1 + }, + "grid": { + "$ref": "#/$defs/grid" + } + }, + "$defs": { + "grid": { + "type": "array", + "items": { + "$ref": "#/$defs/row" + }, + "minItems": 1 + }, + "row": { + "type": "array", + "items": { + "$ref": "#/$defs/cell" + }, + "minItems": 1 + }, + "cell": { + "type": "object", + "javaType": "ch.fritteli.labyrinth.generator.json.JsonCell", + "additionalProperties": false, + "required": [], + "properties": { + "top": { + "type": "boolean" + }, + "right": { + "type": "boolean" + }, + "bottom": { + "type": "boolean" + }, + "left": { + "type": "boolean" + }, + "solution": { + "type": "boolean" + } + } + } + } +}