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"
+ }
+ }
+ }
+ }
+}