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,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>
@@ -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() {}
}
@@ -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");
}
@@ -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();
}
}
@@ -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();
}
@@ -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();
}
@@ -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;
}
}