Make TextRenderer and HTMLRenderer thread-safe (or so I think ...)

This commit is contained in:
Manuel Friedli 2020-10-02 22:58:42 +02:00
parent 8774324b65
commit cf96949f8e
5 changed files with 292 additions and 294 deletions

View file

@ -9,6 +9,8 @@ import java.nio.file.Path;
import java.nio.file.StandardOpenOption; import java.nio.file.StandardOpenOption;
public class HTMLFileRenderer implements Renderer<Path> { public class HTMLFileRenderer implements Renderer<Path> {
@NonNull
private static final HTMLRenderer HTML_RENDERER = HTMLRenderer.newInstance();
@Setter @Setter
private Path targetFile; private Path targetFile;
@ -35,7 +37,7 @@ public class HTMLFileRenderer implements Renderer<Path> {
if (!this.isTargetFileDefinedAndWritable()) { if (!this.isTargetFileDefinedAndWritable()) {
throw new IllegalArgumentException("Cannot write to target file. See previous log messages for details."); throw new IllegalArgumentException("Cannot write to target file. See previous log messages for details.");
} }
final String html = HTMLRenderer.newInstance().render(labyrinth); final String html = HTML_RENDERER.render(labyrinth);
try { try {
Files.writeString(this.targetFile, html, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.WRITE); Files.writeString(this.targetFile, html, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.WRITE);
} catch (IOException e) { } catch (IOException e) {

View file

@ -3,6 +3,7 @@ package ch.fritteli.labyrinth;
import io.vavr.collection.HashSet; import io.vavr.collection.HashSet;
import io.vavr.collection.Set; import io.vavr.collection.Set;
import lombok.NonNull; import lombok.NonNull;
import lombok.RequiredArgsConstructor;
public class HTMLRenderer implements Renderer<String> { public class HTMLRenderer implements Renderer<String> {
private static final String PREAMBLE = "<!DOCTYPE html><html lang=\"en\">" + private static final String PREAMBLE = "<!DOCTYPE html><html lang=\"en\">" +
@ -33,11 +34,6 @@ public class HTMLRenderer implements Renderer<String> {
"<body>" + "<body>" +
"<input type=\"checkbox\" onclick=\"toggleSolution()\">show solution</input>"; "<input type=\"checkbox\" onclick=\"toggleSolution()\">show solution</input>";
private static final String POSTAMBLE = "</body></html>"; private static final String POSTAMBLE = "</body></html>";
private Labyrinth labyrinth;
private int width;
private int height;
// row counter
private int y = 0;
private HTMLRenderer() { private HTMLRenderer() {
} }
@ -49,56 +45,60 @@ public class HTMLRenderer implements Renderer<String> {
@NonNull @NonNull
public String render(@NonNull final Labyrinth labyrinth) { public String render(@NonNull final Labyrinth labyrinth) {
this.labyrinth = labyrinth; if (labyrinth.getWidth() == 0 || labyrinth.getHeight() == 0) {
this.width = labyrinth.getWidth();
this.height = labyrinth.getHeight();
if (this.width == 0 || this.height == 0) {
return PREAMBLE + POSTAMBLE; return PREAMBLE + POSTAMBLE;
} }
final Generator generator = new Generator(labyrinth);
final StringBuilder sb = new StringBuilder(PREAMBLE); final StringBuilder sb = new StringBuilder(PREAMBLE);
sb.append("<table>"); sb.append("<table>");
while (this.hasNext()) { while (generator.hasNext()) {
sb.append(this.next()); sb.append(generator.next());
} }
sb.append("</table>"); sb.append("</table>");
sb.append(POSTAMBLE); sb.append(POSTAMBLE);
return sb.toString(); return sb.toString();
} }
private boolean hasNext() { @RequiredArgsConstructor
return this.y < this.height; private static class Generator {
} private final Labyrinth labyrinth;
private int y = 0;
private String next() { private boolean hasNext() {
StringBuilder sb = new StringBuilder("<tr>"); return this.y < this.labyrinth.getHeight();
for (int x = 0; x < this.width; x++) {
final Tile currentTile = this.labyrinth.getTileAt(x, this.y);
sb.append("<td class=\"");
sb.append(this.getClasses(currentTile).mkString(" "));
sb.append("\">&nbsp;</td>");
} }
sb.append("</tr>");
this.y++;
return sb.toString();
}
private Set<String> getClasses(@NonNull final Tile tile) { private String next() {
Set<String> result = HashSet.empty(); StringBuilder sb = new StringBuilder("<tr>");
if (tile.hasWallAt(Direction.TOP)) { for (int x = 0; x < this.labyrinth.getWidth(); x++) {
result = result.add("top"); final Tile currentTile = this.labyrinth.getTileAt(x, this.y);
sb.append("<td class=\"");
sb.append(this.getClasses(currentTile).mkString(" "));
sb.append("\">&nbsp;</td>");
}
sb.append("</tr>");
this.y++;
return sb.toString();
} }
if (tile.hasWallAt(Direction.RIGHT)) {
result = result.add("right"); private Set<String> getClasses(@NonNull final Tile tile) {
Set<String> result = HashSet.empty();
if (tile.hasWallAt(Direction.TOP)) {
result = result.add("top");
}
if (tile.hasWallAt(Direction.RIGHT)) {
result = result.add("right");
}
if (tile.hasWallAt(Direction.BOTTOM)) {
result = result.add("bottom");
}
if (tile.hasWallAt(Direction.LEFT)) {
result = result.add("left");
}
if (tile.isSolution()) {
result = result.add("solution");
}
return result;
} }
if (tile.hasWallAt(Direction.BOTTOM)) {
result = result.add("bottom");
}
if (tile.hasWallAt(Direction.LEFT)) {
result = result.add("left");
}
if (tile.isSolution()) {
result = result.add("solution");
}
return result;
} }
} }

View file

@ -9,7 +9,7 @@ public class Main {
final Labyrinth labyrinth = new Labyrinth(width, height); final Labyrinth labyrinth = new Labyrinth(width, height);
final TextRenderer textRenderer = TextRenderer.newInstance(); final TextRenderer textRenderer = TextRenderer.newInstance();
System.out.println(textRenderer.render(labyrinth)); System.out.println(textRenderer.render(labyrinth));
System.out.println(textRenderer.setRenderingSolution(true).render(labyrinth)); System.out.println(textRenderer.setRenderSolution(true).render(labyrinth));
final HTMLRenderer htmlRenderer = HTMLRenderer.newInstance(); final HTMLRenderer htmlRenderer = HTMLRenderer.newInstance();
System.out.println(htmlRenderer.render(labyrinth)); System.out.println(htmlRenderer.render(labyrinth));
System.out.println(HTMLFileRenderer.newInstance().render(labyrinth)); System.out.println(HTMLFileRenderer.newInstance().render(labyrinth));

View file

@ -4,20 +4,12 @@ import lombok.AccessLevel;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
import lombok.NonNull; import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import lombok.experimental.FieldDefaults; import lombok.experimental.FieldDefaults;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
public class TextRenderer implements Renderer<String> { public class TextRenderer implements Renderer<String> {
private Labyrinth labyrinth;
private int width;
private int height;
private boolean renderSolution; private boolean renderSolution;
// column counter
private int x;
// row counter
private int y;
// line counter (top-, center- or bottom line of a row)
private int line;
private TextRenderer() { private TextRenderer() {
this.renderSolution = false; this.renderSolution = false;
@ -29,295 +21,299 @@ public class TextRenderer implements Renderer<String> {
} }
@NonNull @NonNull
public TextRenderer setRenderingSolution(final boolean renderSolution) { public TextRenderer setRenderSolution(final boolean renderSolution) {
this.renderSolution = renderSolution; this.renderSolution = renderSolution;
return this; return this;
} }
@NonNull @NonNull
public String render(@NonNull final Labyrinth labyrinth) { public String render(@NonNull final Labyrinth labyrinth) {
this.labyrinth = labyrinth; if (labyrinth.getWidth() == 0 || labyrinth.getHeight() == 0) {
this.width = labyrinth.getWidth();
this.height = labyrinth.getHeight();
if (this.width == 0 || this.height == 0) {
return ""; return "";
} }
this.x = 0; final Generator generator = new Generator(labyrinth, this.renderSolution);
this.y = 0;
this.line = 0;
final StringBuilder sb = new StringBuilder(); final StringBuilder sb = new StringBuilder();
while (this.hasNext()) { while (generator.hasNext()) {
sb.append(this.next()); sb.append(generator.next());
} }
return sb.toString(); return sb.toString();
} }
private boolean hasNext() { @RequiredArgsConstructor(access = AccessLevel.PRIVATE)
return this.y < this.height; static class Generator {
} @NonNull
private final Labyrinth labyrinth;
private final boolean renderSolution;
private int x = 0;
private int y = 0;
private int line = 0;
private String next() { private boolean hasNext() {
final Tile currentTile = this.labyrinth.getTileAt(this.x, this.y); return this.y < this.labyrinth.getHeight();
final Tile leftTile = this.getTileOrNull(this.x - 1, this.y);
final Tile topTile = this.getTileOrNull(this.x, this.y - 1);
final String s;
switch (this.line) {
case 0:
s = this.renderTopLine(currentTile, leftTile, topTile);
break;
case 1:
s = this.renderCenterLine(currentTile);
break;
case 2:
s = this.renderBottomLine(currentTile, leftTile);
break;
default:
s = "";
break;
} }
this.prepareNextStep();
return s;
}
private void prepareNextStep() { private String next() {
// do some magic ... final Tile currentTile = this.labyrinth.getTileAt(this.x, this.y);
this.x++; final Tile leftTile = this.getTileOrNull(this.x - 1, this.y);
if (this.x == this.width) { final Tile topTile = this.getTileOrNull(this.x, this.y - 1);
// Reached the end of the row? final String s;
// On to the next line then! switch (this.line) {
this.x = 0; case 0:
this.line++; s = this.renderTopLine(currentTile, leftTile, topTile);
break;
case 1:
s = this.renderCenterLine(currentTile);
break;
case 2:
s = this.renderBottomLine(currentTile, leftTile);
break;
default:
s = "";
break;
}
this.prepareNextStep();
return s;
} }
if (this.line == 2 && this.y < this.height - 1) {
// Finished rendering the center line, and more rows available?
// On to the next row then!
this.line = 0;
this.y++;
}
if (this.line == 3) {
// Finished rendering the bottom line (of the last row)?
// Increment row counter one more time => this is the exit condition.
this.line = 0;
this.y++;
}
}
private Tile getTileOrNull(final int x, final int y) { private void prepareNextStep() {
if (x < 0 || y < 0 || x >= this.width || y >= this.height) { // do some magic ...
return null; this.x++;
if (this.x == this.labyrinth.getWidth()) {
// Reached the end of the row?
// On to the next line then!
this.x = 0;
this.line++;
}
if (this.line == 2 && this.y < this.labyrinth.getHeight() - 1) {
// Finished rendering the center line, and more rows available?
// On to the next row then!
this.line = 0;
this.y++;
}
if (this.line == 3) {
// Finished rendering the bottom line (of the last row)?
// Increment row counter one more time => this is the exit condition.
this.line = 0;
this.y++;
}
} }
return this.labyrinth.getTileAt(x, y);
}
private String renderTopLine(@NonNull final Tile currentTile, @Nullable final Tile leftTile, @Nullable final Tile topTile) { private Tile getTileOrNull(final int x, final int y) {
final CharDefinition charDef1 = new CharDefinition(); if (x < 0 || y < 0 || x >= this.labyrinth.getWidth() || y >= this.labyrinth.getHeight()) {
final CharDefinition charDef2 = new CharDefinition(); return null;
final CharDefinition charDef3 = new CharDefinition(); }
String result; return this.labyrinth.getTileAt(x, y);
if (currentTile.hasWallAt(Direction.LEFT)) {
charDef1.down();
} }
if (currentTile.hasWallAt(Direction.TOP)) {
charDef1.right(); private String renderTopLine(@NonNull final Tile currentTile, @Nullable final Tile leftTile, @Nullable final Tile topTile) {
charDef2.horizontal(); final CharDefinition charDef1 = new CharDefinition();
charDef3.left(); final CharDefinition charDef2 = new CharDefinition();
final CharDefinition charDef3 = new CharDefinition();
String result;
if (currentTile.hasWallAt(Direction.LEFT)) {
charDef1.down();
}
if (currentTile.hasWallAt(Direction.TOP)) {
charDef1.right();
charDef2.horizontal();
charDef3.left();
}
if (currentTile.hasWallAt(Direction.RIGHT)) {
charDef3.down();
}
if (leftTile != null && leftTile.hasWallAt(Direction.TOP)) {
charDef1.left();
}
if (topTile != null) {
if (topTile.hasWallAt(Direction.LEFT)) {
charDef1.up();
}
if (topTile.hasWallAt(Direction.RIGHT)) {
charDef3.up();
}
}
result = charDef1.toString() + charDef2;
if (this.x == this.labyrinth.getWidth() - 1) {
result += charDef3 + "\n";
}
return result;
} }
if (currentTile.hasWallAt(Direction.RIGHT)) {
charDef3.down(); private String renderCenterLine(@NonNull final Tile currentTile) {
final CharDefinition charDef1 = new CharDefinition();
final CharDefinition charDef2 = new CharDefinition();
String result;
if (currentTile.hasWallAt(Direction.LEFT)) {
charDef1.vertical();
}
if (currentTile.hasWallAt(Direction.RIGHT)) {
charDef2.vertical();
}
result = charDef1 + (currentTile.isSolution() && this.renderSolution ? "x" : " ");
if (this.x == this.labyrinth.getWidth() - 1) {
result += charDef2 + "\n";
}
return result;
} }
if (leftTile != null && leftTile.hasWallAt(Direction.TOP)) {
charDef1.left(); private String renderBottomLine(@NonNull final Tile currentTile, @Nullable final Tile leftTile) {
} String result;
if (topTile != null) { final CharDefinition charDef1 = new CharDefinition();
if (topTile.hasWallAt(Direction.LEFT)) { final CharDefinition charDef2 = new CharDefinition();
final CharDefinition charDef3 = new CharDefinition();
if (currentTile.hasWallAt(Direction.LEFT)) {
charDef1.up(); charDef1.up();
} }
if (topTile.hasWallAt(Direction.RIGHT)) { if (currentTile.hasWallAt(Direction.BOTTOM)) {
charDef1.right();
charDef2.horizontal();
charDef3.left();
}
if (currentTile.hasWallAt(Direction.RIGHT)) {
charDef3.up(); charDef3.up();
} }
} if (leftTile != null && leftTile.hasWallAt(Direction.BOTTOM)) {
result = charDef1.toString() + charDef2; charDef1.left();
if (this.x == this.width - 1) { }
result += charDef3 + "\n";
}
return result;
}
private String renderCenterLine(@NonNull final Tile currentTile) { result = charDef1.toString() + charDef2;
final CharDefinition charDef1 = new CharDefinition();
final CharDefinition charDef2 = new CharDefinition(); if (this.x == this.labyrinth.getWidth() - 1) {
String result; result += charDef3;
if (currentTile.hasWallAt(Direction.LEFT)) { }
charDef1.vertical(); return result;
}
if (currentTile.hasWallAt(Direction.RIGHT)) {
charDef2.vertical();
} }
result = charDef1 + (currentTile.isSolution() && this.renderSolution ? "x" : " "); @FieldDefaults(level = AccessLevel.PRIVATE)
@NoArgsConstructor
@AllArgsConstructor
static class CharDefinition {
//
static final String HORIZONTAL = "\u2500";
//
static final String VERTICAL = "\u2502";
//
static final String LEFT = "\u2574";
//
static final String UP = "\u2575";
//
static final String RIGHT = "\u2576";
//
static final String DOWN = "\u2577";
//
static final String DOWN_RIGHT = "\u250c";
//
static final String DOWN_LEFT = "\u2510";
//
static final String UP_RIGHT = "\u2514";
//
static final String UP_LEFT = "\u2518";
//
static final String VERTICAL_RIGHT = "\u251c";
//
static final String VERTICAL_LEFT = "\u2524";
//
static final String HORIZONTAL_DOWN = "\u252c";
//
static final String HORIZONTAL_UP = "\u2534";
//
static final String CROSS = "\u253c";
boolean up = false;
boolean down = false;
boolean left = false;
boolean right = false;
if (this.x == this.width - 1) { CharDefinition up() {
result += charDef2 + "\n"; this.up = true;
} return this;
}
return result; CharDefinition down() {
} this.down = true;
return this;
}
private String renderBottomLine(@NonNull final Tile currentTile, @Nullable final Tile leftTile) { CharDefinition vertical() {
String result; return this.up().down();
final CharDefinition charDef1 = new CharDefinition(); }
final CharDefinition charDef2 = new CharDefinition();
final CharDefinition charDef3 = new CharDefinition();
if (currentTile.hasWallAt(Direction.LEFT)) { CharDefinition left() {
charDef1.up(); this.left = true;
} return this;
if (currentTile.hasWallAt(Direction.BOTTOM)) { }
charDef1.right();
charDef2.horizontal();
charDef3.left();
}
if (currentTile.hasWallAt(Direction.RIGHT)) {
charDef3.up();
}
if (leftTile != null && leftTile.hasWallAt(Direction.BOTTOM)) {
charDef1.left();
}
result = charDef1.toString() + charDef2; CharDefinition right() {
this.right = true;
return this;
}
if (this.x == this.width - 1) { CharDefinition horizontal() {
result += charDef3; return this.left().right();
} }
return result;
}
@FieldDefaults(level = AccessLevel.PRIVATE) public String toString() {
@NoArgsConstructor if (this.up) {
@AllArgsConstructor if (this.down) {
static class CharDefinition { if (this.left) {
// if (this.right) {
static final String HORIZONTAL = "\u2500"; return CROSS;
// } else {
static final String VERTICAL = "\u2502"; return VERTICAL_LEFT;
// }
static final String LEFT = "\u2574";
//
static final String UP = "\u2575";
//
static final String RIGHT = "\u2576";
//
static final String DOWN = "\u2577";
//
static final String DOWN_RIGHT = "\u250c";
//
static final String DOWN_LEFT = "\u2510";
//
static final String UP_RIGHT = "\u2514";
//
static final String UP_LEFT = "\u2518";
//
static final String VERTICAL_RIGHT = "\u251c";
//
static final String VERTICAL_LEFT = "\u2524";
//
static final String HORIZONTAL_DOWN = "\u252c";
//
static final String HORIZONTAL_UP = "\u2534";
//
static final String CROSS = "\u253c";
boolean up = false;
boolean down = false;
boolean left = false;
boolean right = false;
CharDefinition up() {
this.up = true;
return this;
}
CharDefinition down() {
this.down = true;
return this;
}
CharDefinition vertical() {
return this.up().down();
}
CharDefinition left() {
this.left = true;
return this;
}
CharDefinition right() {
this.right = true;
return this;
}
CharDefinition horizontal() {
return this.left().right();
}
public String toString() {
if (this.up) {
if (this.down) {
if (this.left) {
if (this.right) {
return CROSS;
} else { } else {
return VERTICAL_LEFT; if (this.right) {
return VERTICAL_RIGHT;
} else {
return VERTICAL;
}
} }
} else { } else {
if (this.right) { if (this.left) {
return VERTICAL_RIGHT; if (this.right) {
return HORIZONTAL_UP;
} else {
return UP_LEFT;
}
} else { } else {
return VERTICAL; if (this.right) {
return UP_RIGHT;
} else {
return UP;
}
} }
} }
} else { } else {
if (this.left) { if (this.down) {
if (this.right) { if (this.left) {
return HORIZONTAL_UP; if (this.right) {
return HORIZONTAL_DOWN;
} else {
return DOWN_LEFT;
}
} else { } else {
return UP_LEFT; if (this.right) {
return DOWN_RIGHT;
} else {
return DOWN;
}
} }
} else { } else {
if (this.right) { if (this.left) {
return UP_RIGHT; if (this.right) {
return HORIZONTAL;
} else {
return LEFT;
}
} else { } else {
return UP; if (this.right) {
} return RIGHT;
} } else {
} return " ";
} else { }
if (this.down) {
if (this.left) {
if (this.right) {
return HORIZONTAL_DOWN;
} else {
return DOWN_LEFT;
}
} else {
if (this.right) {
return DOWN_RIGHT;
} else {
return DOWN;
}
}
} else {
if (this.left) {
if (this.right) {
return HORIZONTAL;
} else {
return LEFT;
}
} else {
if (this.right) {
return RIGHT;
} else {
return " ";
} }
} }
} }

View file

@ -1,6 +1,6 @@
package ch.fritteli.labyrinth; package ch.fritteli.labyrinth;
import ch.fritteli.labyrinth.TextRenderer.CharDefinition; import ch.fritteli.labyrinth.TextRenderer.Generator.CharDefinition;
import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;