Add SIN.04028 tutorial material, tutor prompt and conversation log

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-15 15:11:02 +02:00
co-authored by Claude Opus 5
parent bac80295b7
commit f615bef426
30 changed files with 8123 additions and 0 deletions
@@ -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");
}
@@ -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());
}
}
}
}
@@ -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
);
}
@@ -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);
}
}
}
}
@@ -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()))
);
}
}
}
}
@@ -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;
}
}
@@ -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();
}
}
@@ -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());
}
}
@@ -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)
);
}
}