Add SIN.04028 tutorial material, tutor prompt and conversation log
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -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