Add SIN.04028 tutorial material, tutor prompt and conversation log
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+13
@@ -0,0 +1,13 @@
|
||||
void main() throws Exception {
|
||||
FileMessageBoard.publish(
|
||||
"rot13/inbox",
|
||||
new Message("TOPIC", "consoleIn/outbox")
|
||||
);
|
||||
|
||||
FileMessageBoard.publish(
|
||||
"consoleOut/inbox",
|
||||
new Message("TOPIC", "rot13/outbox")
|
||||
);
|
||||
|
||||
IO.println("Wired: consoleIn -> rot13 -> consoleOut");
|
||||
}
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
void main() throws Exception {
|
||||
try (var subscriptions = new FileSubscriptions()) {
|
||||
var controlMailbox = "consoleOut/inbox";
|
||||
String textMailbox = null;
|
||||
subscriptions.subscribe(controlMailbox);
|
||||
|
||||
while (true) {
|
||||
var incoming = subscriptions.take();
|
||||
var message = incoming.message();
|
||||
|
||||
if (incoming.mailbox().equals(controlMailbox)
|
||||
&& message.type().equals("TOPIC")) {
|
||||
if (textMailbox != null)
|
||||
subscriptions.unsubscribe(textMailbox);
|
||||
|
||||
textMailbox = message.value();
|
||||
subscriptions.subscribe(textMailbox);
|
||||
IO.println("ConsoleOut now listens to " + textMailbox);
|
||||
} else if (message.type().equals("TEXT")) {
|
||||
IO.println(message.value());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
void main(String[] arguments) throws Exception {
|
||||
if (arguments.length != 3
|
||||
|| !arguments[0].matches("[A-Za-z0-9_-]+")) {
|
||||
IO.println(
|
||||
"Usage: java DynamicMediatorOrchestrator.java "
|
||||
+ "<mediator-name> <from-mailbox> <to-mailbox>"
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
var mediator = arguments[0];
|
||||
var from = arguments[1];
|
||||
var to = arguments[2];
|
||||
|
||||
FileMessageBoard.publish(
|
||||
"mediators/" + mediator + "/inbox",
|
||||
new Message("ROUTE", from + "\n" + to)
|
||||
);
|
||||
|
||||
IO.println(
|
||||
"Requested: " + mediator + " " + from + " -> " + to
|
||||
);
|
||||
}
|
||||
+88
@@ -0,0 +1,88 @@
|
||||
record DynamicRoute(String from, String to) {}
|
||||
|
||||
DynamicRoute readRoute(Message message, String controlMailbox) {
|
||||
var mailboxes = message.value().lines().toList();
|
||||
|
||||
if (mailboxes.size() != 2
|
||||
|| mailboxes.get(0).isBlank()
|
||||
|| mailboxes.get(1).isBlank()) {
|
||||
IO.println("ROUTE needs exactly two lines: <from> and <to>");
|
||||
return null;
|
||||
}
|
||||
|
||||
var route = new DynamicRoute(mailboxes.get(0), mailboxes.get(1));
|
||||
|
||||
try {
|
||||
FileMessageBoard.directory(route.from());
|
||||
FileMessageBoard.directory(route.to());
|
||||
} catch (IllegalArgumentException exception) {
|
||||
IO.println("Rejected ROUTE: " + exception.getMessage());
|
||||
return null;
|
||||
}
|
||||
|
||||
if (route.from().equals(route.to())) {
|
||||
IO.println("Rejected ROUTE: from and to must differ");
|
||||
return null;
|
||||
}
|
||||
|
||||
if (route.from().equals(controlMailbox)
|
||||
|| route.to().equals(controlMailbox)) {
|
||||
IO.println("Rejected ROUTE: the control mailbox is not a data mailbox");
|
||||
return null;
|
||||
}
|
||||
|
||||
return route;
|
||||
}
|
||||
|
||||
void main(String[] arguments) throws Exception {
|
||||
if (arguments.length != 1
|
||||
|| !arguments[0].matches("[A-Za-z0-9_-]+")) {
|
||||
IO.println("Usage: java DynamicMediatorService.java <mediator-name>");
|
||||
return;
|
||||
}
|
||||
|
||||
var name = arguments[0];
|
||||
var controlMailbox = "mediators/" + name + "/inbox";
|
||||
|
||||
try (var subscriptions = new FileSubscriptions()) {
|
||||
DynamicRoute route = null;
|
||||
subscriptions.subscribe(controlMailbox);
|
||||
|
||||
IO.println(name + " waits for ROUTE on " + controlMailbox);
|
||||
|
||||
while (true) {
|
||||
var incoming = subscriptions.take();
|
||||
var message = incoming.message();
|
||||
|
||||
if (incoming.mailbox().equals(controlMailbox)) {
|
||||
if (!message.type().equals("ROUTE")) {
|
||||
IO.println("Ignored " + message.type() + "; expected ROUTE");
|
||||
continue;
|
||||
}
|
||||
|
||||
var requestedRoute = readRoute(message, controlMailbox);
|
||||
|
||||
if (requestedRoute == null)
|
||||
continue;
|
||||
|
||||
if (route == null
|
||||
|| !route.from().equals(requestedRoute.from())) {
|
||||
subscriptions.subscribe(requestedRoute.from());
|
||||
|
||||
if (route != null)
|
||||
subscriptions.unsubscribe(route.from());
|
||||
}
|
||||
|
||||
route = requestedRoute;
|
||||
|
||||
IO.println(
|
||||
name + " now mediates: "
|
||||
+ route.from() + " -> " + route.to()
|
||||
);
|
||||
} else if (route != null
|
||||
&& incoming.mailbox().equals(route.from())) {
|
||||
FileMessageBoard.publish(route.to(), message);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
String rot13(String text) {
|
||||
var result = new StringBuilder();
|
||||
|
||||
for (var character : text.toCharArray()) {
|
||||
if (character >= 'a' && character <= 'z')
|
||||
character = (char) ('a' + (character - 'a' + 13) % 26);
|
||||
else if (character >= 'A' && character <= 'Z')
|
||||
character = (char) ('A' + (character - 'A' + 13) % 26);
|
||||
|
||||
result.append(character);
|
||||
}
|
||||
|
||||
return result.toString();
|
||||
}
|
||||
|
||||
void main() throws Exception {
|
||||
try (var subscriptions = new FileSubscriptions()) {
|
||||
var controlMailbox = "rot13/inbox";
|
||||
String textMailbox = null;
|
||||
subscriptions.subscribe(controlMailbox);
|
||||
|
||||
while (true) {
|
||||
var incoming = subscriptions.take();
|
||||
var message = incoming.message();
|
||||
|
||||
if (incoming.mailbox().equals(controlMailbox)
|
||||
&& message.type().equals("TOPIC")) {
|
||||
if (textMailbox != null)
|
||||
subscriptions.unsubscribe(textMailbox);
|
||||
|
||||
textMailbox = message.value();
|
||||
subscriptions.subscribe(textMailbox);
|
||||
IO.println("ROT13 now listens to " + textMailbox);
|
||||
} else if (message.type().equals("TEXT")) {
|
||||
FileMessageBoard.publish(
|
||||
"rot13/outbox",
|
||||
new Message("TEXT", rot13(message.value()))
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+68
@@ -0,0 +1,68 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
+106
@@ -0,0 +1,106 @@
|
||||
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<String, WatchKey> subscriptions = new ConcurrentHashMap<>();
|
||||
private final Map<WatchKey, String> mailboxes = new ConcurrentHashMap<>();
|
||||
private final BlockingQueue<IncomingMessage> 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();
|
||||
}
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
import java.time.Instant;
|
||||
|
||||
record Message(String type, String value, Instant timestamp) {
|
||||
Message(String type, String value) {
|
||||
this(type, value, Instant.now());
|
||||
}
|
||||
}
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
void main() throws Exception {
|
||||
while (true) {
|
||||
var text = IO.readln();
|
||||
|
||||
if (text == null)
|
||||
return;
|
||||
|
||||
FileMessageBoard.publish(
|
||||
"consoleIn/outbox",
|
||||
new Message("TEXT", text)
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user