import java.io.IOException; import java.nio.file.FileSystems; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.StandardCopyOption; import java.nio.file.StandardWatchEventKinds; import java.time.Instant; import java.util.Arrays; import java.util.UUID; import java.util.function.Consumer; 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 { publishText(mailbox, serialize(message)); } static void publishText(String mailbox, String content) 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, content); Files.move(temporary, published, StandardCopyOption.ATOMIC_MOVE); } static void watch(String mailbox, Consumer receiver) throws IOException, InterruptedException { var directory = directory(mailbox); Files.createDirectories(directory); try (var watcher = FileSystems.getDefault().newWatchService()) { directory.register(watcher, StandardWatchEventKinds.ENTRY_CREATE); while (true) { var key = watcher.take(); for (var event : key.pollEvents()) { if (event.kind() == StandardWatchEventKinds.OVERFLOW) continue; var filename = (Path) event.context(); if (!filename.toString().endsWith(".msg")) continue; var file = directory.resolve(filename); if (Files.isRegularFile(file)) receiver.accept(read(file)); } if (!key.reset()) return; } } } 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; } }