Implement a JSON and a JSON-file renderer. #5

Merged
manuel merged 1 commit from feature/json-renderer into master 2023-04-14 01:24:28 +02:00
6 changed files with 296 additions and 3 deletions

29
pom.xml
View file

@ -15,6 +15,7 @@
<url>https://manuel.friedli.info/labyrinth.html</url> <url>https://manuel.friedli.info/labyrinth.html</url>
<properties> <properties>
<jackson.version>2.14.2</jackson.version>
<java.source.version>17</java.source.version> <java.source.version>17</java.source.version>
<java.target.version>17</java.target.version> <java.target.version>17</java.target.version>
<jetbrains-annotations.version>24.0.1</jetbrains-annotations.version> <jetbrains-annotations.version>24.0.1</jetbrains-annotations.version>
@ -59,6 +60,16 @@
<artifactId>pdfbox</artifactId> <artifactId>pdfbox</artifactId>
<version>${pdfbox.version}</version> <version>${pdfbox.version}</version>
</dependency> </dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-annotations</artifactId>
<version>${jackson.version}</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>${jackson.version}</version>
</dependency>
<dependency> <dependency>
<groupId>org.slf4j</groupId> <groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId> <artifactId>slf4j-api</artifactId>
@ -82,6 +93,24 @@
</dependencies> </dependencies>
<build> <build>
<plugins> <plugins>
<plugin>
<groupId>org.jsonschema2pojo</groupId>
<artifactId>jsonschema2pojo-maven-plugin</artifactId>
<version>1.2.1</version>
<executions>
<execution>
<phase>generate-sources</phase>
<goals>
<goal>generate</goal>
</goals>
<configuration>
<sourcePaths>
<sourcePath>src/main/resources/labyrinth.schema.json</sourcePath>
</sourcePaths>
</configuration>
</execution>
</executions>
</plugin>
<plugin> <plugin>
<artifactId>maven-assembly-plugin</artifactId> <artifactId>maven-assembly-plugin</artifactId>
<configuration> <configuration>

View file

@ -3,23 +3,28 @@ package ch.fritteli.labyrinth.generator;
import ch.fritteli.labyrinth.generator.model.Labyrinth; import ch.fritteli.labyrinth.generator.model.Labyrinth;
import ch.fritteli.labyrinth.generator.renderer.html.HTMLRenderer; import ch.fritteli.labyrinth.generator.renderer.html.HTMLRenderer;
import ch.fritteli.labyrinth.generator.renderer.htmlfile.HTMLFileRenderer; 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.pdffile.PDFFileRenderer;
import ch.fritteli.labyrinth.generator.renderer.text.TextRenderer; import ch.fritteli.labyrinth.generator.renderer.text.TextRenderer;
import ch.fritteli.labyrinth.generator.renderer.textfile.TextFileRenderer; 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.Path;
import java.nio.file.Paths; import java.nio.file.Paths;
import lombok.NonNull;
import lombok.experimental.UtilityClass;
import lombok.extern.slf4j.Slf4j;
@Slf4j @Slf4j
@UtilityClass
public class Main { public class Main {
public static void main(@NonNull final String[] args) { public static void main(@NonNull final String[] args) {
final int width = 20; final int width = 20;
final int height = 30; final int height = 30;
final Labyrinth labyrinth = new Labyrinth(width, height/*, 0*/); final Labyrinth labyrinth = new Labyrinth(width, height/*, 0*/);
final TextRenderer textRenderer = TextRenderer.newInstance(); final TextRenderer textRenderer = TextRenderer.newInstance();
final HTMLRenderer htmlRenderer = HTMLRenderer.newInstance(); final HTMLRenderer htmlRenderer = HTMLRenderer.newInstance();
final JsonRenderer jsonRenderer = JsonRenderer.newInstance();
final Path userHome = Paths.get(System.getProperty("user.home")); final Path userHome = Paths.get(System.getProperty("user.home"));
final String baseFilename = getBaseFilename(labyrinth); final String baseFilename = getBaseFilename(labyrinth);
final TextFileRenderer textFileRenderer = TextFileRenderer.newInstance() final TextFileRenderer textFileRenderer = TextFileRenderer.newInstance()
@ -27,6 +32,8 @@ public class Main {
.setTargetSolutionFile(userHome.resolve(baseFilename + "-solution.txt")); .setTargetSolutionFile(userHome.resolve(baseFilename + "-solution.txt"));
final HTMLFileRenderer htmlFileRenderer = HTMLFileRenderer.newInstance() final HTMLFileRenderer htmlFileRenderer = HTMLFileRenderer.newInstance()
.setTargetFile(userHome.resolve(baseFilename + ".html")); .setTargetFile(userHome.resolve(baseFilename + ".html"));
final JsonFileRenderer jsonFileRenderer = JsonFileRenderer.newInstance()
.setTargetFile(userHome.resolve(baseFilename + ".json"));
final PDFFileRenderer pdfFileRenderer = PDFFileRenderer.newInstance() final PDFFileRenderer pdfFileRenderer = PDFFileRenderer.newInstance()
.setTargetFile(userHome.resolve(baseFilename + ".pdf")); .setTargetFile(userHome.resolve(baseFilename + ".pdf"));
@ -37,10 +44,14 @@ public class Main {
log.info("Text rendering with solution:\n{}", textRenderer.setRenderSolution(true).render(labyrinth)); log.info("Text rendering with solution:\n{}", textRenderer.setRenderSolution(true).render(labyrinth));
// Render HTML to stdout // Render HTML to stdout
log.info("HTML rendering:\n{}", htmlRenderer.render(labyrinth)); 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 // Render Labyrinth and solution to (separate) files
log.info("Text rendering to file:\n{}", textFileRenderer.render(labyrinth)); log.info("Text rendering to file:\n{}", textFileRenderer.render(labyrinth));
// Render HTML to file // Render HTML to file
log.info("HTML rendering to file:\n{}", htmlFileRenderer.render(labyrinth)); 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 // Render PDF to file
log.info("PDF rendering to file:\n{}", pdfFileRenderer.render(labyrinth)); log.info("PDF rendering to file:\n{}", pdfFileRenderer.render(labyrinth));
} }

View file

@ -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<List<JsonCell>> rows = new ArrayList<>();
for (int y = 0; y < this.labyrinth.getHeight(); y++) {
final ArrayList<JsonCell> 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;
}
}

View file

@ -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<String> {
@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<List<JsonCell>> 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);
}
}

View file

@ -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<Path> {
@NonNull
private static final JsonRenderer JSON_RENDERER = JsonRenderer.newInstance();
@NonNull
private Option<Path> 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;
}
}

View file

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