Add SIN.04028 tutorial material, tutor prompt and conversation log
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+25
@@ -0,0 +1,25 @@
|
||||
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() {
|
||||
while (true) {
|
||||
var text = IO.readln();
|
||||
|
||||
if (text == null)
|
||||
return;
|
||||
|
||||
IO.println(rot13(text));
|
||||
}
|
||||
}
|
||||
+88
@@ -0,0 +1,88 @@
|
||||
record Message(String type, String value) {}
|
||||
|
||||
Map<String, BlockingQueue<Message>> mailboxes =
|
||||
new ConcurrentHashMap<>();
|
||||
|
||||
BlockingQueue<Message> mailbox(String name) {
|
||||
return mailboxes.computeIfAbsent(
|
||||
name,
|
||||
_ -> new LinkedBlockingQueue<>()
|
||||
);
|
||||
}
|
||||
|
||||
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 mediate(String from, String to) {
|
||||
try {
|
||||
while (true)
|
||||
mailbox(to).put(mailbox(from).take());
|
||||
} catch (InterruptedException _) {
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
}
|
||||
|
||||
void runConsoleIn() {
|
||||
try {
|
||||
while (true) {
|
||||
var text = IO.readln();
|
||||
|
||||
if (text == null)
|
||||
return;
|
||||
|
||||
mailbox("consoleIn.outbox")
|
||||
.put(new Message("TEXT", text));
|
||||
}
|
||||
} catch (InterruptedException _) {
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
}
|
||||
|
||||
void runRot13() {
|
||||
try {
|
||||
while (true) {
|
||||
var input = mailbox("rot13.inbox").take();
|
||||
var output = new Message("TEXT", rot13(input.value()));
|
||||
mailbox("rot13.outbox").put(output);
|
||||
}
|
||||
} catch (InterruptedException _) {
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
}
|
||||
|
||||
void runConsoleOut() {
|
||||
try {
|
||||
while (true)
|
||||
IO.println(mailbox("consoleOut.inbox").take().value());
|
||||
} catch (InterruptedException _) {
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
}
|
||||
|
||||
void main() throws InterruptedException {
|
||||
Thread.ofVirtual().start(this::runConsoleIn);
|
||||
Thread.ofVirtual().start(this::runRot13);
|
||||
Thread.ofVirtual().start(this::runConsoleOut);
|
||||
|
||||
Thread.ofVirtual().start(
|
||||
() -> mediate("consoleIn.outbox", "rot13.inbox")
|
||||
);
|
||||
|
||||
Thread.ofVirtual().start(
|
||||
() -> mediate("rot13.outbox", "consoleOut.inbox")
|
||||
);
|
||||
|
||||
Thread.currentThread().join();
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
void main() throws Exception {
|
||||
FileMessageBoard.watch("consoleOut/inbox", message -> {
|
||||
if (message.type().equals("TEXT"))
|
||||
IO.println(message.value());
|
||||
});
|
||||
}
|
||||
+69
@@ -0,0 +1,69 @@
|
||||
import java.io.IOException;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.util.concurrent.LinkedBlockingQueue;
|
||||
|
||||
record Route(String from, String to) {}
|
||||
|
||||
Route waitForRoute(String controlMailbox) throws InterruptedException {
|
||||
var messages = new LinkedBlockingQueue<Message>();
|
||||
|
||||
var controlListener = Thread.ofVirtual().start(() -> {
|
||||
try {
|
||||
FileMessageBoard.watch(controlMailbox, messages::add);
|
||||
} catch (InterruptedException _) {
|
||||
Thread.currentThread().interrupt();
|
||||
} catch (IOException exception) {
|
||||
throw new UncheckedIOException(exception);
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
while (true) {
|
||||
var message = messages.take();
|
||||
|
||||
if (!message.type().equals("ROUTE")) {
|
||||
IO.println("Ignored " + message.type() + "; expected ROUTE");
|
||||
continue;
|
||||
}
|
||||
|
||||
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>");
|
||||
continue;
|
||||
}
|
||||
|
||||
return new Route(mailboxes.get(0), mailboxes.get(1));
|
||||
}
|
||||
} finally {
|
||||
controlListener.interrupt();
|
||||
controlListener.join();
|
||||
}
|
||||
}
|
||||
|
||||
void main(String[] arguments) throws Exception {
|
||||
if (arguments.length != 1
|
||||
|| !arguments[0].matches("[A-Za-z0-9_-]+")) {
|
||||
IO.println("Usage: java ConfigurableMediator.java <mediator-name>");
|
||||
return;
|
||||
}
|
||||
|
||||
var name = arguments[0];
|
||||
var controlMailbox = "mediators/" + name + "/inbox";
|
||||
|
||||
IO.println(name + " waits for ROUTE on " + controlMailbox);
|
||||
|
||||
var route = waitForRoute(controlMailbox);
|
||||
|
||||
IO.println(name + " mediates: " + route.from() + " -> " + route.to());
|
||||
|
||||
FileMessageBoard.watch(route.from(), message -> {
|
||||
try {
|
||||
FileMessageBoard.publish(route.to(), message);
|
||||
} catch (IOException exception) {
|
||||
throw new UncheckedIOException(exception);
|
||||
}
|
||||
});
|
||||
}
|
||||
+107
@@ -0,0 +1,107 @@
|
||||
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<Message> 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;
|
||||
}
|
||||
}
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
void main(String[] arguments) throws Exception {
|
||||
if (arguments.length != 2) {
|
||||
IO.println("Usage: java Mediator.java <from-mailbox> <to-mailbox>");
|
||||
return;
|
||||
}
|
||||
|
||||
var from = arguments[0];
|
||||
var to = arguments[1];
|
||||
|
||||
IO.println("Mediating: " + from + " -> " + to);
|
||||
|
||||
FileMessageBoard.watch(from, message -> {
|
||||
try {
|
||||
FileMessageBoard.publish(to, message);
|
||||
} catch (IOException exception) {
|
||||
throw new UncheckedIOException(exception);
|
||||
}
|
||||
});
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
void configure(String mediator, String from, String to) throws Exception {
|
||||
FileMessageBoard.publish(
|
||||
"mediators/" + mediator + "/inbox",
|
||||
new Message("ROUTE", from + "\n" + to)
|
||||
);
|
||||
}
|
||||
|
||||
void main() throws Exception {
|
||||
configure(
|
||||
"mediator1",
|
||||
"consoleIn/outbox",
|
||||
"rot13/inbox"
|
||||
);
|
||||
|
||||
configure(
|
||||
"mediator2",
|
||||
"rot13/outbox",
|
||||
"consoleOut/inbox"
|
||||
);
|
||||
|
||||
IO.println("Configured mediator1 and mediator2");
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
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 {
|
||||
FileMessageBoard.watch("rot13/inbox", message -> {
|
||||
if (!message.type().equals("TEXT"))
|
||||
return;
|
||||
|
||||
try {
|
||||
FileMessageBoard.publish(
|
||||
"rot13/outbox",
|
||||
new Message("TEXT", rot13(message.value()))
|
||||
);
|
||||
} catch (IOException exception) {
|
||||
throw new UncheckedIOException(exception);
|
||||
}
|
||||
});
|
||||
}
|
||||
+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)
|
||||
);
|
||||
}
|
||||
}
|
||||
+136
@@ -0,0 +1,136 @@
|
||||
record Message(String type, String value) {}
|
||||
|
||||
Map<String, BlockingQueue<Message>> mailboxes =
|
||||
new ConcurrentHashMap<>();
|
||||
|
||||
BlockingQueue<Message> mailbox(String name) {
|
||||
return mailboxes.computeIfAbsent(
|
||||
name,
|
||||
_ -> new LinkedBlockingQueue<>()
|
||||
);
|
||||
}
|
||||
|
||||
void listen(String mailboxName, Consumer<Message> receiver) {
|
||||
try {
|
||||
while (true)
|
||||
receiver.accept(mailbox(mailboxName).take());
|
||||
} catch (InterruptedException _) {
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
}
|
||||
|
||||
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 runConsoleIn() {
|
||||
try {
|
||||
while (true) {
|
||||
var text = IO.readln();
|
||||
|
||||
if (text == null)
|
||||
return;
|
||||
|
||||
mailbox("consoleIn.outbox")
|
||||
.put(new Message("TEXT", text));
|
||||
}
|
||||
} catch (InterruptedException _) {
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
}
|
||||
|
||||
Map<String, Thread> rot13Subscriptions = new HashMap<>();
|
||||
Map<String, Thread> consoleOutSubscriptions = new HashMap<>();
|
||||
|
||||
void subscribe(
|
||||
Map<String, Thread> subscriptions,
|
||||
String mailboxName,
|
||||
Consumer<Message> receiver) {
|
||||
subscriptions.computeIfAbsent(
|
||||
mailboxName,
|
||||
name -> Thread.ofVirtual().start(() -> listen(name, receiver))
|
||||
);
|
||||
}
|
||||
|
||||
void processWithRot13(Message message) {
|
||||
if (!message.type().equals("TEXT"))
|
||||
return;
|
||||
|
||||
try {
|
||||
mailbox("rot13.outbox").put(
|
||||
new Message("TEXT", rot13(message.value()))
|
||||
);
|
||||
} catch (InterruptedException _) {
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
}
|
||||
|
||||
void runRot13() {
|
||||
try {
|
||||
while (true) {
|
||||
var message = mailbox("rot13.inbox").take();
|
||||
|
||||
if (message.type().equals("TOPIC"))
|
||||
subscribe(
|
||||
rot13Subscriptions,
|
||||
message.value(),
|
||||
this::processWithRot13
|
||||
);
|
||||
else
|
||||
processWithRot13(message);
|
||||
}
|
||||
} catch (InterruptedException _) {
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
}
|
||||
|
||||
void print(Message message) {
|
||||
if (message.type().equals("TEXT"))
|
||||
IO.println(message.value());
|
||||
}
|
||||
|
||||
void runConsoleOut() {
|
||||
try {
|
||||
while (true) {
|
||||
var message = mailbox("consoleOut.inbox").take();
|
||||
|
||||
if (message.type().equals("TOPIC"))
|
||||
subscribe(
|
||||
consoleOutSubscriptions,
|
||||
message.value(),
|
||||
this::print
|
||||
);
|
||||
else
|
||||
print(message);
|
||||
}
|
||||
} catch (InterruptedException _) {
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
}
|
||||
|
||||
void main() throws Exception {
|
||||
Thread.ofVirtual().start(this::runConsoleIn);
|
||||
Thread.ofVirtual().start(this::runRot13);
|
||||
Thread.ofVirtual().start(this::runConsoleOut);
|
||||
|
||||
mailbox("rot13.inbox").put(
|
||||
new Message("TOPIC", "consoleIn.outbox")
|
||||
);
|
||||
|
||||
mailbox("consoleOut.inbox").put(
|
||||
new Message("TOPIC", "rot13.outbox")
|
||||
);
|
||||
|
||||
Thread.currentThread().join();
|
||||
}
|
||||
+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)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<groupId>at.ac.fhstp.sin</groupId>
|
||||
<artifactId>calling-to-eventing</artifactId>
|
||||
<version>1.0-SNAPSHOT</version>
|
||||
|
||||
<properties>
|
||||
<maven.compiler.release>26</maven.compiler.release>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.eclipse.paho</groupId>
|
||||
<artifactId>org.eclipse.paho.mqttv5.client</artifactId>
|
||||
<version>1.2.5</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.fasterxml.jackson.core</groupId>
|
||||
<artifactId>jackson-databind</artifactId>
|
||||
<version>2.22.2</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.fasterxml.jackson.dataformat</groupId>
|
||||
<artifactId>jackson-dataformat-yaml</artifactId>
|
||||
<version>2.22.2</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<version>3.14.1</version>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-dependency-plugin</artifactId>
|
||||
<version>3.8.1</version>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
import com.fasterxml.jackson.databind.DeserializationFeature;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.SerializationFeature;
|
||||
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
|
||||
|
||||
final class JsonTools {
|
||||
static final ObjectMapper deserializationObjectMapper =
|
||||
new ObjectMapper(new YAMLFactory())
|
||||
.configure(
|
||||
DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES,
|
||||
false
|
||||
);
|
||||
|
||||
static final ObjectMapper serializationObjectMapper =
|
||||
new ObjectMapper()
|
||||
.configure(SerializationFeature.INDENT_OUTPUT, true);
|
||||
|
||||
private JsonTools() {}
|
||||
}
|
||||
+67
@@ -0,0 +1,67 @@
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.UUID;
|
||||
|
||||
import org.eclipse.paho.mqttv5.client.IMqttAsyncClient;
|
||||
import org.eclipse.paho.mqttv5.client.MqttAsyncClient;
|
||||
import org.eclipse.paho.mqttv5.client.persist.MemoryPersistence;
|
||||
import org.eclipse.paho.mqttv5.common.MqttMessage;
|
||||
|
||||
void publish(
|
||||
IMqttAsyncClient client,
|
||||
String topic,
|
||||
String content,
|
||||
int qos,
|
||||
boolean retained)
|
||||
throws Exception {
|
||||
var message = new MqttMessage(
|
||||
content.getBytes(StandardCharsets.UTF_8)
|
||||
);
|
||||
message.setQos(qos);
|
||||
message.setRetained(retained);
|
||||
client.publish(topic, message).waitForCompletion();
|
||||
}
|
||||
|
||||
void main(String[] arguments) throws Exception {
|
||||
var consoleInUnit = arguments.length > 0 ? arguments[0] : "alice";
|
||||
var rot13Unit = arguments.length > 1 ? arguments[1] : "alice";
|
||||
var consoleOutUnit = arguments.length > 2 ? arguments[2] : "alice";
|
||||
var broker = System.getProperty("mqtt.broker", "tcp://localhost:1883");
|
||||
var client = new MqttAsyncClient(
|
||||
broker,
|
||||
"configure-" + UUID.randomUUID(),
|
||||
new MemoryPersistence()
|
||||
);
|
||||
|
||||
client.connect().waitForCompletion();
|
||||
|
||||
var rot13Intent = """
|
||||
subscribe:
|
||||
topic: Tutorial/ConsoleIn/U/%s/E/text
|
||||
""".formatted(consoleInUnit);
|
||||
|
||||
var consoleOutIntent = """
|
||||
subscribe:
|
||||
topic: Tutorial/ROT13/U/%s/E/crypted
|
||||
""".formatted(rot13Unit);
|
||||
|
||||
publish(
|
||||
client,
|
||||
"Tutorial/ROT13/U/" + rot13Unit + "/I",
|
||||
rot13Intent,
|
||||
1,
|
||||
false
|
||||
);
|
||||
|
||||
publish(
|
||||
client,
|
||||
"Tutorial/ConsoleOut/U/" + consoleOutUnit + "/I",
|
||||
consoleOutIntent,
|
||||
1,
|
||||
false
|
||||
);
|
||||
|
||||
client.disconnect().waitForCompletion();
|
||||
client.close();
|
||||
|
||||
IO.println("Wired: ConsoleIn -> ROT13 -> ConsoleOut");
|
||||
}
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.UUID;
|
||||
|
||||
import org.eclipse.paho.mqttv5.client.IMqttAsyncClient;
|
||||
import org.eclipse.paho.mqttv5.client.MqttAsyncClient;
|
||||
import org.eclipse.paho.mqttv5.client.persist.MemoryPersistence;
|
||||
import org.eclipse.paho.mqttv5.common.MqttMessage;
|
||||
|
||||
record ConsoleInMessage(String value) {}
|
||||
|
||||
void publish(
|
||||
IMqttAsyncClient client,
|
||||
String topic,
|
||||
String content,
|
||||
int qos,
|
||||
boolean retained)
|
||||
throws Exception {
|
||||
var message = new MqttMessage(
|
||||
content.getBytes(StandardCharsets.UTF_8)
|
||||
);
|
||||
message.setQos(qos);
|
||||
message.setRetained(retained);
|
||||
client.publish(topic, message).waitForCompletion();
|
||||
}
|
||||
|
||||
void main(String[] arguments) throws Exception {
|
||||
var unit = arguments.length == 0 ? "alice" : arguments[0];
|
||||
var broker = System.getProperty("mqtt.broker", "tcp://localhost:1883");
|
||||
var topic = "Tutorial/ConsoleIn/U/" + unit + "/E/text";
|
||||
var client = new MqttAsyncClient(
|
||||
broker,
|
||||
"console-in-" + unit + "-" + UUID.randomUUID(),
|
||||
new MemoryPersistence()
|
||||
);
|
||||
|
||||
client.connect().waitForCompletion();
|
||||
|
||||
try {
|
||||
while (true) {
|
||||
var text = IO.readln();
|
||||
|
||||
if (text == null)
|
||||
return;
|
||||
|
||||
var json = JsonTools.serializationObjectMapper
|
||||
.writeValueAsString(new ConsoleInMessage(text));
|
||||
publish(client, topic, json, 1, false);
|
||||
}
|
||||
} finally {
|
||||
client.disconnect().waitForCompletion();
|
||||
client.close();
|
||||
}
|
||||
}
|
||||
+138
@@ -0,0 +1,138 @@
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.UUID;
|
||||
|
||||
import org.eclipse.paho.mqttv5.client.IMqttAsyncClient;
|
||||
import org.eclipse.paho.mqttv5.client.IMqttToken;
|
||||
import org.eclipse.paho.mqttv5.client.MqttAsyncClient;
|
||||
import org.eclipse.paho.mqttv5.client.MqttCallback;
|
||||
import org.eclipse.paho.mqttv5.client.MqttConnectionOptions;
|
||||
import org.eclipse.paho.mqttv5.client.MqttDisconnectResponse;
|
||||
import org.eclipse.paho.mqttv5.client.persist.MemoryPersistence;
|
||||
import org.eclipse.paho.mqttv5.common.MqttException;
|
||||
import org.eclipse.paho.mqttv5.common.MqttMessage;
|
||||
import org.eclipse.paho.mqttv5.common.packet.MqttProperties;
|
||||
|
||||
record ConsoleOutMessage(String value) {}
|
||||
|
||||
record ConsoleOutSubscription(String topic) {}
|
||||
|
||||
record ConsoleOutIntent(ConsoleOutSubscription subscribe) {}
|
||||
|
||||
MqttAsyncClient client;
|
||||
String intentTopic;
|
||||
String subscriptionStatusTopic;
|
||||
String dynamicTextTopic;
|
||||
|
||||
void publish(
|
||||
IMqttAsyncClient client,
|
||||
String topic,
|
||||
String content,
|
||||
int qos,
|
||||
boolean retained)
|
||||
throws Exception {
|
||||
var message = new MqttMessage(
|
||||
content.getBytes(StandardCharsets.UTF_8)
|
||||
);
|
||||
message.setQos(qos);
|
||||
message.setRetained(retained);
|
||||
client.publish(topic, message).waitForCompletion();
|
||||
}
|
||||
|
||||
MqttConnectionOptions connectionOptions(String lastWillTopic) {
|
||||
var options = new MqttConnectionOptions();
|
||||
options.setAutomaticReconnect(true);
|
||||
|
||||
var will = new MqttMessage(
|
||||
"{\"value\":false}".getBytes(StandardCharsets.UTF_8),
|
||||
1,
|
||||
true,
|
||||
new MqttProperties()
|
||||
);
|
||||
options.setWill(lastWillTopic, will);
|
||||
return options;
|
||||
}
|
||||
|
||||
void handleIntent(MqttMessage message) throws Exception {
|
||||
var intent = JsonTools.deserializationObjectMapper.readValue(
|
||||
message.getPayload(),
|
||||
ConsoleOutIntent.class
|
||||
);
|
||||
|
||||
if (intent.subscribe() == null)
|
||||
return;
|
||||
|
||||
var requestedTopic = intent.subscribe().topic();
|
||||
|
||||
if (!MqttTools.isValidFilter(requestedTopic)) {
|
||||
IO.println("Rejected invalid topic filter: " + requestedTopic);
|
||||
return;
|
||||
}
|
||||
|
||||
if (dynamicTextTopic != null)
|
||||
client.unsubscribe(dynamicTextTopic).waitForCompletion();
|
||||
|
||||
dynamicTextTopic = requestedTopic;
|
||||
client.subscribe(dynamicTextTopic, 1).waitForCompletion();
|
||||
|
||||
var status = JsonTools.serializationObjectMapper
|
||||
.writeValueAsString(new ConsoleOutMessage(dynamicTextTopic));
|
||||
publish(client, subscriptionStatusTopic, status, 1, true);
|
||||
|
||||
IO.println("ConsoleOut now listens to " + dynamicTextTopic);
|
||||
}
|
||||
|
||||
void handlePublication(String topic, MqttMessage message) throws Exception {
|
||||
if (topic.equals(intentTopic)) {
|
||||
handleIntent(message);
|
||||
} else if (dynamicTextTopic != null
|
||||
&& MqttTools.isTopicMatchingFilter(topic, dynamicTextTopic)) {
|
||||
var text = JsonTools.deserializationObjectMapper.readValue(
|
||||
message.getPayload(),
|
||||
ConsoleOutMessage.class
|
||||
);
|
||||
IO.println(text.value());
|
||||
}
|
||||
}
|
||||
|
||||
void main(String[] arguments) throws Exception {
|
||||
var unit = arguments.length == 0 ? "alice" : arguments[0];
|
||||
var broker = System.getProperty("mqtt.broker", "tcp://localhost:1883");
|
||||
var root = "Tutorial/ConsoleOut/U/" + unit;
|
||||
intentTopic = root + "/I";
|
||||
subscriptionStatusTopic = root + "/S/subscriptions/text";
|
||||
var onlineTopic = root + "/S/online";
|
||||
|
||||
client = new MqttAsyncClient(
|
||||
broker,
|
||||
"console-out-" + unit + "-" + UUID.randomUUID(),
|
||||
new MemoryPersistence()
|
||||
);
|
||||
|
||||
client.setCallback(new MqttCallback() {
|
||||
public void messageArrived(String topic, MqttMessage message)
|
||||
throws Exception {
|
||||
handlePublication(topic, message);
|
||||
}
|
||||
|
||||
public void disconnected(MqttDisconnectResponse response) {}
|
||||
|
||||
public void mqttErrorOccurred(MqttException exception) {
|
||||
exception.printStackTrace();
|
||||
}
|
||||
|
||||
public void deliveryComplete(IMqttToken token) {}
|
||||
|
||||
public void connectComplete(boolean reconnect, String serverURI) {}
|
||||
|
||||
public void authPacketArrived(
|
||||
int reasonCode,
|
||||
MqttProperties properties) {}
|
||||
});
|
||||
|
||||
client.connect(connectionOptions(onlineTopic)).waitForCompletion();
|
||||
client.subscribe(intentTopic, 1).waitForCompletion();
|
||||
publish(client, onlineTopic, "{\"value\":true}", 1, true);
|
||||
|
||||
IO.println("ConsoleOut waits for Intent on " + intentTopic);
|
||||
Thread.currentThread().join();
|
||||
}
|
||||
+172
@@ -0,0 +1,172 @@
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.UUID;
|
||||
|
||||
import org.eclipse.paho.mqttv5.client.IMqttAsyncClient;
|
||||
import org.eclipse.paho.mqttv5.client.IMqttToken;
|
||||
import org.eclipse.paho.mqttv5.client.MqttAsyncClient;
|
||||
import org.eclipse.paho.mqttv5.client.MqttCallback;
|
||||
import org.eclipse.paho.mqttv5.client.MqttConnectionOptions;
|
||||
import org.eclipse.paho.mqttv5.client.MqttDisconnectResponse;
|
||||
import org.eclipse.paho.mqttv5.client.persist.MemoryPersistence;
|
||||
import org.eclipse.paho.mqttv5.common.MqttException;
|
||||
import org.eclipse.paho.mqttv5.common.MqttMessage;
|
||||
import org.eclipse.paho.mqttv5.common.packet.MqttProperties;
|
||||
|
||||
record Rot13Message(String value) {}
|
||||
|
||||
record Rot13Subscription(String topic) {}
|
||||
|
||||
record Rot13Intent(
|
||||
Rot13Subscription subscribe,
|
||||
Rot13Message text
|
||||
) {}
|
||||
|
||||
MqttAsyncClient client;
|
||||
String intentTopic;
|
||||
String eventTopic;
|
||||
String subscriptionStatusTopic;
|
||||
String dynamicTextTopic;
|
||||
|
||||
void publish(
|
||||
IMqttAsyncClient client,
|
||||
String topic,
|
||||
String content,
|
||||
int qos,
|
||||
boolean retained)
|
||||
throws Exception {
|
||||
var message = new MqttMessage(
|
||||
content.getBytes(StandardCharsets.UTF_8)
|
||||
);
|
||||
message.setQos(qos);
|
||||
message.setRetained(retained);
|
||||
client.publish(topic, message).waitForCompletion();
|
||||
}
|
||||
|
||||
MqttConnectionOptions connectionOptions(String lastWillTopic) {
|
||||
var options = new MqttConnectionOptions();
|
||||
options.setAutomaticReconnect(true);
|
||||
|
||||
var will = new MqttMessage(
|
||||
"{\"value\":false}".getBytes(StandardCharsets.UTF_8),
|
||||
1,
|
||||
true,
|
||||
new MqttProperties()
|
||||
);
|
||||
options.setWill(lastWillTopic, will);
|
||||
return options;
|
||||
}
|
||||
|
||||
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 cryptAndPublish(String text) throws Exception {
|
||||
var json = JsonTools.serializationObjectMapper
|
||||
.writeValueAsString(new Rot13Message(rot13(text)));
|
||||
publish(client, eventTopic, json, 1, false);
|
||||
}
|
||||
|
||||
void handleIntent(MqttMessage message) throws Exception {
|
||||
var intent = JsonTools.deserializationObjectMapper.readValue(
|
||||
message.getPayload(),
|
||||
Rot13Intent.class
|
||||
);
|
||||
|
||||
if (intent.subscribe() != null) {
|
||||
var requestedTopic = intent.subscribe().topic();
|
||||
|
||||
if (!MqttTools.isValidFilter(requestedTopic)) {
|
||||
IO.println("Rejected invalid topic filter: " + requestedTopic);
|
||||
return;
|
||||
}
|
||||
|
||||
if (dynamicTextTopic != null)
|
||||
client.unsubscribe(dynamicTextTopic).waitForCompletion();
|
||||
|
||||
dynamicTextTopic = requestedTopic;
|
||||
client.subscribe(dynamicTextTopic, 1).waitForCompletion();
|
||||
|
||||
var status = JsonTools.serializationObjectMapper
|
||||
.writeValueAsString(new Rot13Message(dynamicTextTopic));
|
||||
publish(
|
||||
client,
|
||||
subscriptionStatusTopic,
|
||||
status,
|
||||
1,
|
||||
true
|
||||
);
|
||||
|
||||
IO.println("ROT13 now listens to " + dynamicTextTopic);
|
||||
}
|
||||
|
||||
if (intent.text() != null)
|
||||
cryptAndPublish(intent.text().value());
|
||||
}
|
||||
|
||||
void handlePublication(String topic, MqttMessage message) throws Exception {
|
||||
if (topic.equals(intentTopic)) {
|
||||
handleIntent(message);
|
||||
} else if (dynamicTextTopic != null
|
||||
&& MqttTools.isTopicMatchingFilter(topic, dynamicTextTopic)) {
|
||||
var input = JsonTools.deserializationObjectMapper.readValue(
|
||||
message.getPayload(),
|
||||
Rot13Message.class
|
||||
);
|
||||
cryptAndPublish(input.value());
|
||||
}
|
||||
}
|
||||
|
||||
void main(String[] arguments) throws Exception {
|
||||
var unit = arguments.length == 0 ? "alice" : arguments[0];
|
||||
var broker = System.getProperty("mqtt.broker", "tcp://localhost:1883");
|
||||
var root = "Tutorial/ROT13/U/" + unit;
|
||||
intentTopic = root + "/I";
|
||||
eventTopic = root + "/E/crypted";
|
||||
subscriptionStatusTopic = root + "/S/subscriptions/text";
|
||||
var onlineTopic = root + "/S/online";
|
||||
|
||||
client = new MqttAsyncClient(
|
||||
broker,
|
||||
"rot13-" + unit + "-" + UUID.randomUUID(),
|
||||
new MemoryPersistence()
|
||||
);
|
||||
|
||||
client.setCallback(new MqttCallback() {
|
||||
public void messageArrived(String topic, MqttMessage message)
|
||||
throws Exception {
|
||||
handlePublication(topic, message);
|
||||
}
|
||||
|
||||
public void disconnected(MqttDisconnectResponse response) {}
|
||||
|
||||
public void mqttErrorOccurred(MqttException exception) {
|
||||
exception.printStackTrace();
|
||||
}
|
||||
|
||||
public void deliveryComplete(IMqttToken token) {}
|
||||
|
||||
public void connectComplete(boolean reconnect, String serverURI) {}
|
||||
|
||||
public void authPacketArrived(
|
||||
int reasonCode,
|
||||
MqttProperties properties) {}
|
||||
});
|
||||
|
||||
client.connect(connectionOptions(onlineTopic)).waitForCompletion();
|
||||
client.subscribe(intentTopic, 1).waitForCompletion();
|
||||
publish(client, onlineTopic, "{\"value\":true}", 1, true);
|
||||
|
||||
IO.println("ROT13 waits for Intent on " + intentTopic);
|
||||
Thread.currentThread().join();
|
||||
}
|
||||
+66
@@ -0,0 +1,66 @@
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
final class MqttTools {
|
||||
private MqttTools() {}
|
||||
|
||||
static boolean isValidTopic(String topic) {
|
||||
return hasValidLength(topic)
|
||||
&& !topic.contains("+")
|
||||
&& !topic.contains("#");
|
||||
}
|
||||
|
||||
static boolean isValidFilter(String filter) {
|
||||
if (!hasValidLength(filter))
|
||||
return false;
|
||||
|
||||
var levels = filter.split("/", -1);
|
||||
|
||||
for (var index = 0; index < levels.length; index++) {
|
||||
var level = levels[index];
|
||||
|
||||
if (level.contains("#")
|
||||
&& !(level.equals("#") && index == levels.length - 1))
|
||||
return false;
|
||||
|
||||
if (level.contains("+") && !level.equals("+"))
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static boolean isTopicMatchingFilter(String topic, String filter) {
|
||||
if (!isValidTopic(topic) || !isValidFilter(filter))
|
||||
return false;
|
||||
|
||||
if (topic.startsWith("$") && !filter.startsWith("$"))
|
||||
return false;
|
||||
|
||||
var topicLevels = topic.split("/", -1);
|
||||
var filterLevels = filter.split("/", -1);
|
||||
var topicIndex = 0;
|
||||
|
||||
for (var filterLevel : filterLevels) {
|
||||
if (filterLevel.equals("#"))
|
||||
return true;
|
||||
|
||||
if (topicIndex == topicLevels.length)
|
||||
return false;
|
||||
|
||||
if (!filterLevel.equals("+")
|
||||
&& !filterLevel.equals(topicLevels[topicIndex]))
|
||||
return false;
|
||||
|
||||
topicIndex++;
|
||||
}
|
||||
|
||||
return topicIndex == topicLevels.length;
|
||||
}
|
||||
|
||||
private static boolean hasValidLength(String value) {
|
||||
if (value == null || value.isEmpty() || value.contains("\u0000"))
|
||||
return false;
|
||||
|
||||
return value.getBytes(StandardCharsets.UTF_8).length <= 65_535;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user