From 8141e874695b52850157556ec1516b90e4d79241 Mon Sep 17 00:00:00 2001 From: Patrick Reinhart Date: Wed, 5 Apr 2023 20:29:42 +0200 Subject: [PATCH] Adds basic MQTT support with remote events - adds short cut to signal restart on rc 42 - adds heartbeat over mqtt Signed-off-by: Patrick Reinhart --- generic2d/build.gradle | 3 +- .../java/org/tweetwallfx/generic/Main.java | 30 ++- mqtt/build.gradle | 30 +++ .../java/org/tweetwallfx/mqtt/MqttEvent.java | 37 ++++ .../org/tweetwallfx/mqtt/MqttProcess.java | 182 ++++++++++++++++++ .../main/java/org/tweetwallfx/mqtt/State.java | 50 +++++ .../java/org/tweetwallfx/mqtt/SystemInfo.java | 62 ++++++ .../tweetwallfx/mqtt/config/MqttSettings.java | 111 +++++++++++ ....tweetwallfx.config.ConfigurationConverter | 1 + settings.gradle | 3 +- 10 files changed, 505 insertions(+), 4 deletions(-) create mode 100644 mqtt/build.gradle create mode 100644 mqtt/src/main/java/org/tweetwallfx/mqtt/MqttEvent.java create mode 100644 mqtt/src/main/java/org/tweetwallfx/mqtt/MqttProcess.java create mode 100644 mqtt/src/main/java/org/tweetwallfx/mqtt/State.java create mode 100644 mqtt/src/main/java/org/tweetwallfx/mqtt/SystemInfo.java create mode 100644 mqtt/src/main/java/org/tweetwallfx/mqtt/config/MqttSettings.java create mode 100644 mqtt/src/main/resources/META-INF/services/org.tweetwallfx.config.ConfigurationConverter diff --git a/generic2d/build.gradle b/generic2d/build.gradle index d779374c8..3ad71bf1e 100644 --- a/generic2d/build.gradle +++ b/generic2d/build.gradle @@ -1,7 +1,7 @@ /* * The MIT License (MIT) * - * Copyright (c) 2015-2022 TweetWallFX + * Copyright (c) 2015-2023 TweetWallFX * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -25,6 +25,7 @@ dependencies { implementation 'org.apache.logging.log4j:log4j-core:2.20.0' implementation project(':tweetwallfx-2d') + implementation project(':tweetwallfx-mqtt') implementation project(':tweetwallfx-tweet-api') runtimeOnly 'org.apache.logging.log4j:log4j-slf4j2-impl:2.20.0' } diff --git a/generic2d/src/main/java/org/tweetwallfx/generic/Main.java b/generic2d/src/main/java/org/tweetwallfx/generic/Main.java index 4c66ed0c5..7bd79fb2a 100644 --- a/generic2d/src/main/java/org/tweetwallfx/generic/Main.java +++ b/generic2d/src/main/java/org/tweetwallfx/generic/Main.java @@ -24,6 +24,7 @@ package org.tweetwallfx.generic; import java.util.Optional; +import java.util.concurrent.atomic.AtomicInteger; import javafx.application.Application; import javafx.application.Platform; import javafx.scene.Scene; @@ -39,16 +40,32 @@ import org.slf4j.LoggerFactory; import org.tweetwallfx.config.Configuration; import org.tweetwallfx.config.TweetwallSettings; +import org.tweetwallfx.mqtt.MqttProcess; import org.tweetwallfx.tweet.api.Tweeter; import org.tweetwallfx.twod.TagTweets; +import static org.tweetwallfx.mqtt.MqttEvent.RESTART; +import static org.tweetwallfx.mqtt.MqttEvent.STOP; + public class Main extends Application { private static final Logger LOG = LoggerFactory.getLogger(Main.class); + private static final AtomicInteger RC = new AtomicInteger(); + + final MqttProcess mqttProcess = new MqttProcess(); @Override public void start(Stage primaryStage) { + new Thread(mqttProcess).start(); + mqttProcess.addMqttEventHandler(e -> { + if (STOP.equals(e.getEventType())) { + exitApplication(0); // normal exit + } else if (RESTART.equals(e.getEventType())) { + exitApplication(42); // restart + } + }); + BorderPane borderPane = new BorderPane(); - Scene scene = new Scene(borderPane, 1920, 1280); + Scene scene = new Scene(borderPane, 1920, 1080); borderPane.getStyleClass().add("splash"); final TweetwallSettings tweetwallSettings @@ -79,7 +96,8 @@ public void start(Stage primaryStage) { switch (character) { case "D" -> toggleStatusLine(borderPane, spa, statusLineHost); case "F" -> primaryStage.setFullScreen(!primaryStage.isFullScreen()); - case "X", "Q" -> Platform.exit(); + case "R" -> exitApplication(42); // restart + case "X", "Q" -> exitApplication(0); // normal exit default -> LOG.warn("Unknown character: '{}'", character); }; } @@ -92,6 +110,12 @@ public void start(Stage primaryStage) { primaryStage.setFullScreen(!Boolean.getBoolean("org.tweetwallfx.disable-full-screen")); } + private void exitApplication(int exitCode) { + LOG.info("Exit application with rc={}", exitCode); + RC.set(exitCode); + Platform.exit(); + } + private static void toggleStatusLine(BorderPane borderPane, StringPropertyAppender spa, HBox statusLineHost) { final LoggerConfig rootLogger = LoggerContext.getContext(false).getConfiguration().getRootLogger(); if (null == statusLineHost.getParent()) { @@ -107,6 +131,7 @@ private static void toggleStatusLine(BorderPane borderPane, StringPropertyAppend public void stop() { LOG.info("closing..."); Tweeter.getInstance().shutdown(); + mqttProcess.stop(); } /** @@ -116,5 +141,6 @@ public void stop() { */ public static void main(String[] args) { launch(args); + System.exit(RC.get()); } } diff --git a/mqtt/build.gradle b/mqtt/build.gradle new file mode 100644 index 000000000..022ed821e --- /dev/null +++ b/mqtt/build.gradle @@ -0,0 +1,30 @@ +/* + * The MIT License + * + * Copyright 2023-2023 TweetWallFX + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +dependencies { + implementation project(':tweetwallfx-configuration') + implementation 'org.eclipse.paho:org.eclipse.paho.client.mqttv3:1.2.5' + implementation 'org.eclipse:yasson:3.0.2' + implementation 'org.slf4j:slf4j-api:2.0.6' +} diff --git a/mqtt/src/main/java/org/tweetwallfx/mqtt/MqttEvent.java b/mqtt/src/main/java/org/tweetwallfx/mqtt/MqttEvent.java new file mode 100644 index 000000000..2b4a2873b --- /dev/null +++ b/mqtt/src/main/java/org/tweetwallfx/mqtt/MqttEvent.java @@ -0,0 +1,37 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2023 TweetWallFX + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package org.tweetwallfx.mqtt; + +import javafx.event.Event; +import javafx.event.EventType; + +public class MqttEvent extends Event { + public static final EventType ANY = new EventType<>(Event.ANY, "ANY"); + public static final EventType RESTART = new EventType<>(Event.ANY, "RESTART"); + public static final EventType STOP = new EventType<>(Event.ANY, "STOP"); + + public MqttEvent(Object source, EventType eventType) { + super(source, null, eventType); + } +} diff --git a/mqtt/src/main/java/org/tweetwallfx/mqtt/MqttProcess.java b/mqtt/src/main/java/org/tweetwallfx/mqtt/MqttProcess.java new file mode 100644 index 000000000..e9af631e6 --- /dev/null +++ b/mqtt/src/main/java/org/tweetwallfx/mqtt/MqttProcess.java @@ -0,0 +1,182 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2023 TweetWallFX + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package org.tweetwallfx.mqtt; + +import jakarta.json.bind.Jsonb; +import jakarta.json.bind.JsonbBuilder; +import javafx.beans.property.BooleanProperty; +import javafx.beans.property.ReadOnlyBooleanProperty; +import javafx.beans.property.SimpleBooleanProperty; +import javafx.event.EventHandler; +import org.eclipse.paho.client.mqttv3.MqttClient; +import org.eclipse.paho.client.mqttv3.MqttClientPersistence; +import org.eclipse.paho.client.mqttv3.MqttConnectOptions; +import org.eclipse.paho.client.mqttv3.MqttException; +import org.eclipse.paho.client.mqttv3.MqttMessage; +import org.eclipse.paho.client.mqttv3.persist.MemoryPersistence; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.tweetwallfx.config.Configuration; +import org.tweetwallfx.mqtt.config.MqttSettings; + +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; + +import static java.util.concurrent.TimeUnit.MILLISECONDS; + +public class MqttProcess implements Runnable { + private static final String TWEETWALL_STATE = "tweetwall/state/"; + private static final Logger LOG = LoggerFactory.getLogger(MqttProcess.class); + + private final BooleanProperty stopProperty = new SimpleBooleanProperty(); + private final BooleanProperty runningProperty = new SimpleBooleanProperty(); + private final List> handlers = new ArrayList<>(); + private final AtomicReference clientRef = new AtomicReference<>(); + + private void fire(MqttEvent mqttEvent) { + handlers.forEach(h -> h.handle(mqttEvent)); + } + + public void addMqttEventHandler(EventHandler handler) { + handlers.add(Objects.requireNonNull(handler)); + } + + public ReadOnlyBooleanProperty runningProperty() { + return ReadOnlyBooleanProperty.readOnlyBooleanProperty(runningProperty); + } + + public void stop() { + stopProperty.set(true); + while (runningProperty.get()) { + waitFor(MILLISECONDS, 500); + } + } + + @Override + public void run() { + try { + Thread.currentThread().setName("MQTT-Command-Dispatcher"); + final MqttSettings mqttSettings = Configuration.getInstance() + .getConfigTyped(MqttSettings.CONFIG_KEY, MqttSettings.class); + if (!mqttSettings.enabled()) { + LOG.info("MQTT disabled"); + return; + } + long lastHeartbeat = 0; + while (!stopProperty.get()) { + final String broker = mqttSettings.brokerUrl(); + final String clientId = mqttSettings.clientId(); + try (MqttClientPersistence persistence = new MemoryPersistence(); + MqttClient mqttClient = new MqttClient(broker, clientId, persistence)) { + clientRef.set(mqttClient); + MqttConnectOptions connOpts = new MqttConnectOptions(); + connOpts.setCleanSession(true); + connOpts.setConnectionTimeout(0); + connOpts.setKeepAliveInterval(30); + connOpts.setAutomaticReconnect(true); + final MqttSettings.Auth auth = mqttSettings.auth(); + Optional.ofNullable(auth.userName()).ifPresent(connOpts::setUserName); + Optional.ofNullable(auth.secret()).ifPresent(pw -> connOpts.setPassword(pw.toCharArray())); + LOG.info("Connect to {}", broker); + mqttClient.connect(connOpts); + stopProperty.addListener((observableValue, oldValue, newValue) -> { + if (newValue) { + sendMessage(TWEETWALL_STATE, State.stopping()); + try { + LOG.info("Disconnecting"); + mqttClient.disconnect(); + } catch (MqttException e) { + LOG.error("Failed to send stop notification", e); + } + } + }); + runningProperty.set(true); + sendMessage(TWEETWALL_STATE, State.starting(SystemInfo.info())); + LOG.info("Connection established"); + mqttClient.subscribe("tweetwall/action/#", (t, m) -> handleActionMessage(clientId, t, m)); + while (!stopProperty.get()) { + // heart beat task + long currentTime = System.currentTimeMillis(); + long duration = MILLISECONDS.toSeconds(currentTime - lastHeartbeat); + if (duration >= mqttSettings.heartbeatSeconds()) { + lastHeartbeat = currentTime; + LOG.debug("Sending heart beat message"); + sendMessage(TWEETWALL_STATE, State.alive()); + } else { + waitFor(MILLISECONDS, 500); + } + } + } catch (MqttException e) { + LOG.error("Failure while handling MQTT", e); + } finally { + clientRef.set(null); + } + } + } finally { + runningProperty.set(false); + } + } + + private static void waitFor(TimeUnit unit, long amount) { + try { + unit.sleep(amount); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + LOG.error("Interrupted while waiting", e); + } + } + + private void handleActionMessage(String clientId, String topic, MqttMessage message) { + String payload = new String(message.getPayload(), StandardCharsets.UTF_8); + if (topic.equals("tweetwall/action/" + clientId)) { + switch (payload) { + case "stop" -> fire(new MqttEvent(this, MqttEvent.STOP)); + case "restart" -> fire(new MqttEvent(this, MqttEvent.RESTART)); + case "info" -> sendMessage(TWEETWALL_STATE, State.info(SystemInfo.info())); + default -> LOG.warn("Unknown action payload: {}", payload); + } + } else { + LOG.warn("Unknown payload '{}' for topic {}", payload, topic); + } + } + + private void sendMessage(String topic, Object messageObject) { + final MqttClient mqttClient = clientRef.get(); + if (mqttClient == null) { + LOG.error("Failed to send '{}' for topic {} as no client available", messageObject, topic); + } else { + try (Jsonb jsonb = JsonbBuilder.create()) { + final byte[] payload = jsonb.toJson(messageObject).getBytes(StandardCharsets.UTF_8); + mqttClient.publish(topic + mqttClient.getClientId(), payload, 2, false); + } catch (Exception e) { + LOG.error("Failed to send '{}' for topic {}", messageObject, topic, e); + } + } + } +} diff --git a/mqtt/src/main/java/org/tweetwallfx/mqtt/State.java b/mqtt/src/main/java/org/tweetwallfx/mqtt/State.java new file mode 100644 index 000000000..b6ef4b365 --- /dev/null +++ b/mqtt/src/main/java/org/tweetwallfx/mqtt/State.java @@ -0,0 +1,50 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2023 TweetWallFX + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package org.tweetwallfx.mqtt; + +import java.time.ZonedDateTime; + +import static java.time.Clock.systemUTC; + +public record State(String name, ZonedDateTime zonedDateTime, Object payload) { + static State stopping() { + return new State("stopping", getNow(), null); + } + + static State starting(Object payload) { + return new State("starting", getNow(), payload); + } + + static State alive() { + return new State("alive", getNow(), null); + } + + static State info(Object payload) { + return new State("info", getNow(), payload); + } + + private static ZonedDateTime getNow() { + return ZonedDateTime.now(systemUTC()); + } +} diff --git a/mqtt/src/main/java/org/tweetwallfx/mqtt/SystemInfo.java b/mqtt/src/main/java/org/tweetwallfx/mqtt/SystemInfo.java new file mode 100644 index 000000000..365ce36f4 --- /dev/null +++ b/mqtt/src/main/java/org/tweetwallfx/mqtt/SystemInfo.java @@ -0,0 +1,62 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2023 TweetWallFX + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package org.tweetwallfx.mqtt; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.net.NetworkInterface; +import java.net.SocketException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class SystemInfo { + private static final Logger LOG = LoggerFactory.getLogger(SystemInfo.class); + + public static Map> info() { + try { + var infos = new HashMap>(); + var interfaces = NetworkInterface.getNetworkInterfaces(); + while (interfaces.hasMoreElements()) { + var networkInterface = interfaces.nextElement(); + if (!networkInterface.isLoopback()) { + var nicName = networkInterface.getName(); + var addresses = networkInterface.getInetAddresses(); + while (addresses.hasMoreElements()) { + var address = addresses.nextElement(); + if (!address.isAnyLocalAddress() && !address.isLoopbackAddress()) { + infos.computeIfAbsent(nicName, k -> new ArrayList<>()).add(address.getHostAddress()); + } + } + } + } + return infos; + } catch (SocketException e) { + LOG.error("Failed to get system info", e); + } + return null; + } +} diff --git a/mqtt/src/main/java/org/tweetwallfx/mqtt/config/MqttSettings.java b/mqtt/src/main/java/org/tweetwallfx/mqtt/config/MqttSettings.java new file mode 100644 index 000000000..6ec8cab7d --- /dev/null +++ b/mqtt/src/main/java/org/tweetwallfx/mqtt/config/MqttSettings.java @@ -0,0 +1,111 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2023 TweetWallFX + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +package org.tweetwallfx.mqtt.config; + +import org.tweetwallfx.config.ConfigurationConverter; + +import java.net.URL; +import java.util.UUID; + +import static org.tweetwallfx.util.Nullable.valueOrDefault; + +/** + * POJO for reading Settings concerning the MQTT client. + * + *

+ * Param {@code debugEnabled} a flag indicating that the MQTT client is to + * work in debug mode + * + *

+ * Param {@code brokerUrl} the broker connection URL + * + *

+ * Param {@code clientId} the MQTT client id for the connection + * the extended mode + * + *

+ * Param {@code heartbeatSeconds} the MQTT heart beat delay time in seconds + * + *

+ * Param {@code auth} the Auth setting the MQTT client is to use in order + * to connect with the broker + */ +public record MqttSettings( + Boolean enabled, + Boolean debugEnabled, + String brokerUrl, + String clientId, + Integer heartbeatSeconds, + Auth auth) { + + public MqttSettings( + final Boolean enabled, + final Boolean debugEnabled, + final String brokerUrl, + final String clientId, + Integer heartbeatSeconds, + final Auth auth) { + this.enabled = valueOrDefault(enabled, true); + this.debugEnabled = valueOrDefault(debugEnabled, false); + this.brokerUrl = valueOrDefault(brokerUrl, "tcp://127.0.0.1:1883"); + this.clientId = valueOrDefault(clientId, UUID.randomUUID().toString()); + this.heartbeatSeconds = checkValue(valueOrDefault(heartbeatSeconds, Integer.valueOf(5))); + this.auth = auth; + } + + private Integer checkValue(Integer checkedValue) { + if (checkedValue.intValue() < 1 ) { + throw new IllegalArgumentException("heartbeatSeconds must be a positive value"); + } + return checkedValue; + } + + /** + * Configuration key under which the data for this Settings object is stored + * in the configuration data map. + */ + public static final String CONFIG_KEY = "mqtt"; + + /** + * Service implementation converting the configuration data of the root key + * {@link MqttSettings#CONFIG_KEY} into {@link MqttSettings}. + */ + public static class Converter implements ConfigurationConverter { + + @Override + public String getResponsibleKey() { + return MqttSettings.CONFIG_KEY; + } + + @Override + public Class getDataClass() { + return MqttSettings.class; + } + } + + public static record Auth( + String userName, + String secret) { + } +} diff --git a/mqtt/src/main/resources/META-INF/services/org.tweetwallfx.config.ConfigurationConverter b/mqtt/src/main/resources/META-INF/services/org.tweetwallfx.config.ConfigurationConverter new file mode 100644 index 000000000..f46d02a31 --- /dev/null +++ b/mqtt/src/main/resources/META-INF/services/org.tweetwallfx.config.ConfigurationConverter @@ -0,0 +1 @@ +org.tweetwallfx.mqtt.config.MqttSettings$Converter diff --git a/settings.gradle b/settings.gradle index 9eed17737..93ac11a04 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,7 +1,7 @@ /* * The MIT License (MIT) * - * Copyright (c) 2015-2022 TweetWallFX + * Copyright (c) 2015-2023 TweetWallFX * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -45,6 +45,7 @@ includeWithName ':transitions', 'tweetwallfx-transitions' includeWithName ':tweet-api', 'tweetwallfx-tweet-api' includeWithName ':tweet-impl-twitter4j', 'tweetwallfx-tweet-impl-twitter4j' includeWithName ':tweet-impl-mastodon4j', 'tweetwallfx-tweet-impl-mastodon4j' +includeWithName ':mqtt', 'tweetwallfx-mqtt' includeWithName ':util', 'tweetwallfx-utility' includeWithName ':controls', 'tweetwallfx-controls'