import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.StandardCopyOption; import java.time.Instant; import java.util.Arrays; import java.util.UUID; final class FileMessageBoard { static final Path ROOT = Path.of( System.getProperty("messageBoard.root", "messageBoard") ).toAbsolutePath().normalize(); private FileMessageBoard() {} static String serialize(Message message) { var text = message.type() + "\n" + message.value(); if (message.timestamp() != null) text += "\nTIMESTAMP: " + message.timestamp(); return text; } static Message read(Path file) throws IOException { var lines = Files.readString(file).split("\\R", -1); if (lines.length < 2) throw new IOException("A message needs a type and a value: " + file); var endOfValue = lines.length; Instant timestamp = null; var lastLine = lines[lines.length - 1]; if (lastLine.startsWith("TIMESTAMP: ")) { timestamp = Instant.parse(lastLine.substring("TIMESTAMP: ".length())); endOfValue--; } var value = String.join( "\n", Arrays.copyOfRange(lines, 1, endOfValue) ); return new Message(lines[0], value, timestamp); } static void publish(String mailbox, Message message) throws IOException { var directory = directory(mailbox); Files.createDirectories(directory); var id = UUID.randomUUID().toString(); var temporary = directory.resolve(id + ".tmp"); var published = directory.resolve(id + ".msg"); Files.writeString(temporary, serialize(message)); Files.move(temporary, published, StandardCopyOption.ATOMIC_MOVE); } static Path directory(String name) { var directory = ROOT.resolve(name).normalize(); if (!directory.startsWith(ROOT)) throw new IllegalArgumentException("Mailbox escapes the message board"); return directory; } }