import java.io.IOException; import java.io.UncheckedIOException; import java.nio.file.ClosedWatchServiceException; import java.nio.file.FileSystems; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.StandardWatchEventKinds; import java.nio.file.WatchKey; import java.nio.file.WatchService; import java.util.Map; import java.util.concurrent.BlockingQueue; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.LinkedBlockingQueue; record IncomingMessage(String mailbox, Message message) {} final class FileSubscriptions implements AutoCloseable { private final WatchService watcher; private final Map subscriptions = new ConcurrentHashMap<>(); private final Map mailboxes = new ConcurrentHashMap<>(); private final BlockingQueue messages = new LinkedBlockingQueue<>(); private final Thread observer; FileSubscriptions() throws IOException { watcher = FileSystems.getDefault().newWatchService(); observer = Thread.ofVirtual().start(this::observe); } void subscribe(String mailbox) throws IOException { if (subscriptions.containsKey(mailbox)) return; var directory = FileMessageBoard.directory(mailbox); Files.createDirectories(directory); var key = directory.register( watcher, StandardWatchEventKinds.ENTRY_CREATE ); subscriptions.put(mailbox, key); mailboxes.put(key, mailbox); } void unsubscribe(String mailbox) { var key = subscriptions.remove(mailbox); if (key != null) { mailboxes.remove(key); key.cancel(); } } IncomingMessage take() throws InterruptedException { return messages.take(); } private void observe() { try { while (true) { var key = watcher.take(); var mailbox = mailboxes.get(key); if (mailbox == null) { key.reset(); continue; } var directory = FileMessageBoard.directory(mailbox); 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)) messages.put( new IncomingMessage(mailbox, FileMessageBoard.read(file)) ); } if (!key.reset()) { subscriptions.remove(mailbox); mailboxes.remove(key); } } } catch (ClosedWatchServiceException _) { // close() ends the observer. } catch (InterruptedException _) { Thread.currentThread().interrupt(); } catch (IOException exception) { throw new UncheckedIOException(exception); } } public void close() throws IOException { watcher.close(); observer.interrupt(); } }