From 4679f9fb5f3297061d127d25ec00146d82fe1153 Mon Sep 17 00:00:00 2001 From: FMOUSSA Date: Sun, 16 Nov 2025 15:59:57 +0100 Subject: [PATCH 01/33] Test raw client calls to ntfy.sh --- .gitignore | 1 + pom.xml | 8 ++++++ src/main/java/com/example/HelloFX.java | 37 +++++++++++++++++++++++++- src/main/java/module-info.java | 1 + 4 files changed, 46 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 6ac465db..5a54815d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ target/ /.idea/ +.env \ No newline at end of file diff --git a/pom.xml b/pom.xml index c40f667e..5cb3cc99 100644 --- a/pom.xml +++ b/pom.xml @@ -63,6 +63,14 @@ true + + org.apache.maven.plugins + maven-compiler-plugin + 3.11.0 + + 25 + + diff --git a/src/main/java/com/example/HelloFX.java b/src/main/java/com/example/HelloFX.java index 96bdc5ca..e78976de 100644 --- a/src/main/java/com/example/HelloFX.java +++ b/src/main/java/com/example/HelloFX.java @@ -6,6 +6,13 @@ import javafx.scene.Scene; import javafx.stage.Stage; +import java.io.IOException; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.util.UUID; + public class HelloFX extends Application { @Override @@ -18,8 +25,36 @@ public void start(Stage stage) throws Exception { stage.show(); } - public static void main(String[] args) { + public static void main(String[] args) throws IOException, InterruptedException { + + String baseUrl = "https://ntfy.sh"; + String topic = "a5ce42c9-6d30-4f7b-942b-7e11d3075925"; + + HttpClient client = HttpClient.newHttpClient(); + + HttpRequest sendRequest = HttpRequest.newBuilder() + .uri(URI.create(baseUrl + "/" + topic)) + .POST(HttpRequest.BodyPublishers.ofString("I've been expecting you.")) + .build(); + + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(baseUrl + "/" + topic + "/json")) + .GET() + .build(); + + client.send(sendRequest, HttpResponse.BodyHandlers.discarding()); + + client.sendAsync(request, HttpResponse.BodyHandlers.ofLines()) + .thenAccept(response -> { + response.body().forEach(line -> { + System.out.println("Received raw line: " + line); + }); + }); + + System.out.println("Listening for messages on topic: " + topic); + Thread.sleep(Long.MAX_VALUE); launch(); + } } \ No newline at end of file diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java index 71574a27..2df6074f 100644 --- a/src/main/java/module-info.java +++ b/src/main/java/module-info.java @@ -1,6 +1,7 @@ module hellofx { requires javafx.controls; requires javafx.fxml; + requires java.net.http; opens com.example to javafx.fxml; exports com.example; From efce903bc11e99515bb48ec3039ae036262e5f75 Mon Sep 17 00:00:00 2001 From: WHITEROSE Date: Sun, 16 Nov 2025 16:05:20 +0100 Subject: [PATCH 02/33] Add .env loader util --- src/main/java/com/example/HelloFX.java | 9 +++++++-- .../java/com/example/utils/EnvLoader.java | 19 +++++++++++++++++++ 2 files changed, 26 insertions(+), 2 deletions(-) create mode 100644 src/main/java/com/example/utils/EnvLoader.java diff --git a/src/main/java/com/example/HelloFX.java b/src/main/java/com/example/HelloFX.java index e78976de..c2801254 100644 --- a/src/main/java/com/example/HelloFX.java +++ b/src/main/java/com/example/HelloFX.java @@ -11,8 +11,11 @@ import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; +import java.util.Properties; import java.util.UUID; +import static com.example.utils.EnvLoader.loadEnv; + public class HelloFX extends Application { @Override @@ -27,8 +30,10 @@ public void start(Stage stage) throws Exception { public static void main(String[] args) throws IOException, InterruptedException { - String baseUrl = "https://ntfy.sh"; - String topic = "a5ce42c9-6d30-4f7b-942b-7e11d3075925"; + Properties env = loadEnv(); + + String baseUrl = env.getProperty("NTFY_BASE_URL"); + String topic = env.getProperty("NTFY_TOPIC"); HttpClient client = HttpClient.newHttpClient(); diff --git a/src/main/java/com/example/utils/EnvLoader.java b/src/main/java/com/example/utils/EnvLoader.java new file mode 100644 index 00000000..3daf8220 --- /dev/null +++ b/src/main/java/com/example/utils/EnvLoader.java @@ -0,0 +1,19 @@ +package com.example.utils; + +import java.io.FileInputStream; +import java.io.IOException; +import java.util.Properties; + +public class EnvLoader { + public static Properties loadEnv() { + Properties props = new Properties(); + + try (FileInputStream fis = new FileInputStream(".env")) { + props.load(fis); + } catch (IOException e) { + throw new RuntimeException("Could not load .env file", e); + } + + return props; + } +} \ No newline at end of file From 76fea6624ca919ae0dc0e2c186c284b58519f311 Mon Sep 17 00:00:00 2001 From: WHITEROSE Date: Sun, 16 Nov 2025 16:20:36 +0100 Subject: [PATCH 03/33] feat(domain): implement NtfyMessage and enable JSON deserialization via Jackson --- pom.xml | 5 +++++ src/main/java/com/example/domain/NtfyMessage.java | 13 +++++++++++++ src/main/java/module-info.java | 1 + 3 files changed, 19 insertions(+) create mode 100644 src/main/java/com/example/domain/NtfyMessage.java diff --git a/pom.xml b/pom.xml index 5cb3cc99..82177a9b 100644 --- a/pom.xml +++ b/pom.xml @@ -17,6 +17,11 @@ 25 + + com.fasterxml.jackson.core + jackson-databind + 2.17.2 + org.junit.jupiter junit-jupiter diff --git a/src/main/java/com/example/domain/NtfyMessage.java b/src/main/java/com/example/domain/NtfyMessage.java new file mode 100644 index 00000000..99840d55 --- /dev/null +++ b/src/main/java/com/example/domain/NtfyMessage.java @@ -0,0 +1,13 @@ +package com.example.domain; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +@JsonIgnoreProperties +public record NtfyMessage( + String id, + Long time, + String event, + String topic, + String message +) { +} diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java index 2df6074f..567a7b77 100644 --- a/src/main/java/module-info.java +++ b/src/main/java/module-info.java @@ -2,6 +2,7 @@ requires javafx.controls; requires javafx.fxml; requires java.net.http; + requires com.fasterxml.jackson.annotation; opens com.example to javafx.fxml; exports com.example; From e9a9a5fb447dc1580bd4544817a6310322aa7f6c Mon Sep 17 00:00:00 2001 From: WHITEROSE Date: Sun, 16 Nov 2025 16:38:01 +0100 Subject: [PATCH 04/33] feat(domain): map ntfy events into internal NtfyMessage, log events with sl4j --- pom.xml | 10 ++++++++++ src/main/java/com/example/HelloFX.java | 17 ++++++++++++++++- .../java/com/example/domain/NtfyMessage.java | 2 +- src/main/java/module-info.java | 4 ++++ 4 files changed, 31 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index 82177a9b..a458cce6 100644 --- a/pom.xml +++ b/pom.xml @@ -17,6 +17,16 @@ 25 + + org.slf4j + slf4j-api + 2.0.9 + + + org.slf4j + slf4j-simple + 2.0.9 + com.fasterxml.jackson.core jackson-databind diff --git a/src/main/java/com/example/HelloFX.java b/src/main/java/com/example/HelloFX.java index c2801254..eacf9576 100644 --- a/src/main/java/com/example/HelloFX.java +++ b/src/main/java/com/example/HelloFX.java @@ -1,10 +1,14 @@ package com.example; +import com.example.domain.NtfyMessage; +import com.fasterxml.jackson.databind.ObjectMapper; import javafx.application.Application; import javafx.fxml.FXMLLoader; import javafx.scene.Parent; import javafx.scene.Scene; import javafx.stage.Stage; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import java.io.IOException; import java.net.URI; @@ -14,9 +18,11 @@ import java.util.Properties; import java.util.UUID; + import static com.example.utils.EnvLoader.loadEnv; public class HelloFX extends Application { + private static final Logger logger = LoggerFactory.getLogger("MAIN"); @Override public void start(Stage stage) throws Exception { @@ -34,6 +40,7 @@ public static void main(String[] args) throws IOException, InterruptedException String baseUrl = env.getProperty("NTFY_BASE_URL"); String topic = env.getProperty("NTFY_TOPIC"); + ObjectMapper mapper = new ObjectMapper(); HttpClient client = HttpClient.newHttpClient(); @@ -52,7 +59,15 @@ public static void main(String[] args) throws IOException, InterruptedException client.sendAsync(request, HttpResponse.BodyHandlers.ofLines()) .thenAccept(response -> { response.body().forEach(line -> { - System.out.println("Received raw line: " + line); + logger.info("Event received: {}", line); + + try { + NtfyMessage msg = mapper.readValue(line, NtfyMessage.class); + logger.info("NtfyMessage:\n id = {}\n event = {}\n topic = {}\n message= {}", + msg.id(), msg.event(), msg.topic(), msg.message()); + } catch (Exception e) { + logger.error("Failed to parse line: {}", line, e); + } }); }); diff --git a/src/main/java/com/example/domain/NtfyMessage.java b/src/main/java/com/example/domain/NtfyMessage.java index 99840d55..ab23a47f 100644 --- a/src/main/java/com/example/domain/NtfyMessage.java +++ b/src/main/java/com/example/domain/NtfyMessage.java @@ -2,7 +2,7 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -@JsonIgnoreProperties +@JsonIgnoreProperties(ignoreUnknown = true) public record NtfyMessage( String id, Long time, diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java index 567a7b77..25cccf11 100644 --- a/src/main/java/module-info.java +++ b/src/main/java/module-info.java @@ -3,7 +3,11 @@ requires javafx.fxml; requires java.net.http; requires com.fasterxml.jackson.annotation; + requires com.fasterxml.jackson.databind; + requires java.logging; + requires org.slf4j; opens com.example to javafx.fxml; + opens com.example.domain to com.fasterxml.jackson.databind; exports com.example; } \ No newline at end of file From 1a1094b659eee187096cbcab694e78e22beb5a34 Mon Sep 17 00:00:00 2001 From: WHITEROSE Date: Sun, 16 Nov 2025 17:56:36 +0100 Subject: [PATCH 05/33] feat(domain): add NtfyMessages to observable list in ChatModel --- .../java/com/example/HelloController.java | 25 ++++++++++----- src/main/java/com/example/HelloFX.java | 32 ++++++++++--------- .../java/com/example/domain/ChatModel.java | 17 ++++++++++ src/main/java/module-info.java | 1 + 4 files changed, 52 insertions(+), 23 deletions(-) create mode 100644 src/main/java/com/example/domain/ChatModel.java diff --git a/src/main/java/com/example/HelloController.java b/src/main/java/com/example/HelloController.java index fdd160a0..e3555fe5 100644 --- a/src/main/java/com/example/HelloController.java +++ b/src/main/java/com/example/HelloController.java @@ -1,22 +1,31 @@ package com.example; +import com.example.domain.ChatModel; +import com.example.domain.NtfyMessage; +import javafx.collections.ListChangeListener; import javafx.fxml.FXML; import javafx.scene.control.Label; +import java.util.stream.Collectors; + /** * Controller layer: mediates between the view (FXML) and the model. */ public class HelloController { + private ChatModel model; - private final HelloModel model = new HelloModel(); + public void setModel(ChatModel model) { + this.model = model; + model.getMessages().addListener((ListChangeListener) c -> { + String events = model.getMessages() + .stream() + .map(NtfyMessage::event) + .collect(Collectors.joining(";")); + messageLabel.setText(events); + }); + } @FXML private Label messageLabel; - - @FXML - private void initialize() { - if (messageLabel != null) { - messageLabel.setText(model.getGreeting()); - } - } } + diff --git a/src/main/java/com/example/HelloFX.java b/src/main/java/com/example/HelloFX.java index eacf9576..c2c2088f 100644 --- a/src/main/java/com/example/HelloFX.java +++ b/src/main/java/com/example/HelloFX.java @@ -1,8 +1,11 @@ package com.example; +import com.example.domain.ChatModel; import com.example.domain.NtfyMessage; +import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import javafx.application.Application; +import javafx.application.Platform; import javafx.fxml.FXMLLoader; import javafx.scene.Parent; import javafx.scene.Scene; @@ -16,18 +19,20 @@ import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.util.Properties; -import java.util.UUID; import static com.example.utils.EnvLoader.loadEnv; public class HelloFX extends Application { private static final Logger logger = LoggerFactory.getLogger("MAIN"); + static final ChatModel model = new ChatModel(); @Override public void start(Stage stage) throws Exception { FXMLLoader fxmlLoader = new FXMLLoader(HelloFX.class.getResource("hello-view.fxml")); Parent root = fxmlLoader.load(); + HelloController controller = fxmlLoader.getController(); + controller.setModel(model); Scene scene = new Scene(root, 640, 480); stage.setTitle("Hello MVC"); stage.setScene(scene); @@ -58,21 +63,18 @@ public static void main(String[] args) throws IOException, InterruptedException client.sendAsync(request, HttpResponse.BodyHandlers.ofLines()) .thenAccept(response -> { - response.body().forEach(line -> { - logger.info("Event received: {}", line); - - try { - NtfyMessage msg = mapper.readValue(line, NtfyMessage.class); - logger.info("NtfyMessage:\n id = {}\n event = {}\n topic = {}\n message= {}", - msg.id(), msg.event(), msg.topic(), msg.message()); - } catch (Exception e) { - logger.error("Failed to parse line: {}", line, e); - } - }); + response.body() + .map(event -> { + try { + return mapper.readValue(event, NtfyMessage.class); + } catch (JsonProcessingException e) { + throw new RuntimeException(e); + } + }) + .filter(msg -> msg.event().equals("message")) + .forEach(msg -> Platform.runLater(() -> + model.addMessage(msg))); }); - - System.out.println("Listening for messages on topic: " + topic); - Thread.sleep(Long.MAX_VALUE); launch(); } diff --git a/src/main/java/com/example/domain/ChatModel.java b/src/main/java/com/example/domain/ChatModel.java new file mode 100644 index 00000000..a5f89b8c --- /dev/null +++ b/src/main/java/com/example/domain/ChatModel.java @@ -0,0 +1,17 @@ +package com.example.domain; + +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; + + +public class ChatModel { + private final ObservableList messages = FXCollections.observableArrayList(); + + public void addMessage(NtfyMessage msg) { + messages.add(msg); + } + public ObservableList getMessages() { + return messages; + } + +} diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java index 25cccf11..ab14768f 100644 --- a/src/main/java/module-info.java +++ b/src/main/java/module-info.java @@ -10,4 +10,5 @@ opens com.example to javafx.fxml; opens com.example.domain to com.fasterxml.jackson.databind; exports com.example; + exports com.example.domain; } \ No newline at end of file From 6aaef5698c0f6acc7dec58a9aa39f4c0737ac3ee Mon Sep 17 00:00:00 2001 From: WHITEROSE Date: Sun, 16 Nov 2025 18:47:56 +0100 Subject: [PATCH 06/33] feat(client): abstract the HttpClient operations --- src/main/java/com/example/HelloFX.java | 43 ++------- .../com/example/client/ChatNetworkClient.java | 13 +++ .../example/client/HttpClientProvider.java | 15 ++++ .../com/example/client/NtfyHttpClient.java | 88 +++++++++++++++++++ 4 files changed, 123 insertions(+), 36 deletions(-) create mode 100644 src/main/java/com/example/client/ChatNetworkClient.java create mode 100644 src/main/java/com/example/client/HttpClientProvider.java create mode 100644 src/main/java/com/example/client/NtfyHttpClient.java diff --git a/src/main/java/com/example/HelloFX.java b/src/main/java/com/example/HelloFX.java index c2c2088f..ccac3c47 100644 --- a/src/main/java/com/example/HelloFX.java +++ b/src/main/java/com/example/HelloFX.java @@ -1,11 +1,9 @@ package com.example; +import com.example.client.ChatNetworkClient; +import com.example.client.NtfyHttpClient; import com.example.domain.ChatModel; -import com.example.domain.NtfyMessage; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; import javafx.application.Application; -import javafx.application.Platform; import javafx.fxml.FXMLLoader; import javafx.scene.Parent; import javafx.scene.Scene; @@ -14,17 +12,13 @@ import org.slf4j.LoggerFactory; import java.io.IOException; -import java.net.URI; -import java.net.http.HttpClient; -import java.net.http.HttpRequest; -import java.net.http.HttpResponse; import java.util.Properties; import static com.example.utils.EnvLoader.loadEnv; public class HelloFX extends Application { - private static final Logger logger = LoggerFactory.getLogger("MAIN"); + private static final Logger log = LoggerFactory.getLogger("MAIN"); static final ChatModel model = new ChatModel(); @Override @@ -45,36 +39,13 @@ public static void main(String[] args) throws IOException, InterruptedException String baseUrl = env.getProperty("NTFY_BASE_URL"); String topic = env.getProperty("NTFY_TOPIC"); - ObjectMapper mapper = new ObjectMapper(); - HttpClient client = HttpClient.newHttpClient(); + ChatNetworkClient client = new NtfyHttpClient(model); - HttpRequest sendRequest = HttpRequest.newBuilder() - .uri(URI.create(baseUrl + "/" + topic)) - .POST(HttpRequest.BodyPublishers.ofString("I've been expecting you.")) - .build(); + ChatNetworkClient.Subscription sub = client.subscribe(baseUrl, topic); + log.info("Subscription: {}", sub); + // client.send(baseUrl, topic, "I've been expecting you."); - HttpRequest request = HttpRequest.newBuilder() - .uri(URI.create(baseUrl + "/" + topic + "/json")) - .GET() - .build(); - - client.send(sendRequest, HttpResponse.BodyHandlers.discarding()); - - client.sendAsync(request, HttpResponse.BodyHandlers.ofLines()) - .thenAccept(response -> { - response.body() - .map(event -> { - try { - return mapper.readValue(event, NtfyMessage.class); - } catch (JsonProcessingException e) { - throw new RuntimeException(e); - } - }) - .filter(msg -> msg.event().equals("message")) - .forEach(msg -> Platform.runLater(() -> - model.addMessage(msg))); - }); launch(); } diff --git a/src/main/java/com/example/client/ChatNetworkClient.java b/src/main/java/com/example/client/ChatNetworkClient.java new file mode 100644 index 00000000..1b6e1975 --- /dev/null +++ b/src/main/java/com/example/client/ChatNetworkClient.java @@ -0,0 +1,13 @@ +package com.example.client; + + +public interface ChatNetworkClient { + Subscription subscribe(String baseUrl, String topic); + void send(String baseUrl, String topic, String message); + + interface Subscription extends AutoCloseable { + @Override + void close() throws Exception; + boolean isOpen(); + } +} diff --git a/src/main/java/com/example/client/HttpClientProvider.java b/src/main/java/com/example/client/HttpClientProvider.java new file mode 100644 index 00000000..a0e86184 --- /dev/null +++ b/src/main/java/com/example/client/HttpClientProvider.java @@ -0,0 +1,15 @@ +package com.example.client; + +import java.net.http.HttpClient; + +public final class HttpClientProvider { + + private static final HttpClient INSTANCE = HttpClient.newHttpClient(); + + private HttpClientProvider() { + } + + public static HttpClient get() { + return INSTANCE; + } +} diff --git a/src/main/java/com/example/client/NtfyHttpClient.java b/src/main/java/com/example/client/NtfyHttpClient.java new file mode 100644 index 00000000..44aee781 --- /dev/null +++ b/src/main/java/com/example/client/NtfyHttpClient.java @@ -0,0 +1,88 @@ +package com.example.client; + +import com.example.domain.ChatModel; +import com.example.domain.NtfyMessage; +import com.fasterxml.jackson.databind.ObjectMapper; +import javafx.application.Platform; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.net.URI; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.atomic.AtomicBoolean; + +public class NtfyHttpClient implements ChatNetworkClient { + + private static final ObjectMapper mapper = new ObjectMapper(); + private static final Logger log = LoggerFactory.getLogger(NtfyHttpClient.class); + private final ChatModel model; + + public NtfyHttpClient(ChatModel model) { + this.model = model; + } + + @Override + public Subscription subscribe(String baseUrl, String topic) { + + HttpRequest req = HttpRequest.newBuilder() + .uri(URI.create(baseUrl + "/" + topic + "/json")) + .GET() + .build(); + + CompletableFuture>> future = + HttpClientProvider.get().sendAsync(req, HttpResponse.BodyHandlers.ofLines()); + + AtomicBoolean open = new AtomicBoolean(true); + + future.thenAccept(response -> { + response.body().forEach(line -> { + if (!open.get()) return; + + try { + NtfyMessage msg = mapper.readValue(line, NtfyMessage.class); + if ("message".equals(msg.event())) { + Platform.runLater(() -> + model.addMessage(msg) + ); + } + } catch (Exception _) { + } + }); + }); + log.info("Subscribing to topic {}", topic); + + return new Subscription() { + @Override + public void close() throws Exception { + open.set(false); + future.cancel(true); + } + + @Override + public boolean isOpen() { + return open.get(); + } + }; + } + + @Override + public void send(String baseUrl, String topic, String message) { + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(baseUrl + "/" + topic)) + .POST(HttpRequest.BodyPublishers.ofString(message)) + .build(); + + HttpClientProvider.get() + .sendAsync(request, HttpResponse.BodyHandlers.ofString()) + .whenComplete((response, error) -> { + if (error != null) { + log.error("Failed to send message", error); + return; + } + log.info("ok"); + }); + } + +} From 33f17eb6f5eb600efdf18a648ae4a0f9c4bf9540 Mon Sep 17 00:00:00 2001 From: WHITEROSE Date: Sun, 16 Nov 2025 19:00:33 +0100 Subject: [PATCH 07/33] feature(client): update exception --- src/main/java/com/example/client/ChatNetworkClient.java | 4 +++- src/main/java/com/example/client/NtfyHttpClient.java | 3 ++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/example/client/ChatNetworkClient.java b/src/main/java/com/example/client/ChatNetworkClient.java index 1b6e1975..71122746 100644 --- a/src/main/java/com/example/client/ChatNetworkClient.java +++ b/src/main/java/com/example/client/ChatNetworkClient.java @@ -1,13 +1,15 @@ package com.example.client; +import java.io.IOException; + public interface ChatNetworkClient { Subscription subscribe(String baseUrl, String topic); void send(String baseUrl, String topic, String message); interface Subscription extends AutoCloseable { @Override - void close() throws Exception; + void close() throws IOException; boolean isOpen(); } } diff --git a/src/main/java/com/example/client/NtfyHttpClient.java b/src/main/java/com/example/client/NtfyHttpClient.java index 44aee781..c9c3f2a4 100644 --- a/src/main/java/com/example/client/NtfyHttpClient.java +++ b/src/main/java/com/example/client/NtfyHttpClient.java @@ -7,6 +7,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.io.IOException; import java.net.URI; import java.net.http.HttpRequest; import java.net.http.HttpResponse; @@ -55,7 +56,7 @@ public Subscription subscribe(String baseUrl, String topic) { return new Subscription() { @Override - public void close() throws Exception { + public void close() throws IOException { open.set(false); future.cancel(true); } From d91e8397c035ea8bbbef157313232fed85b57c21 Mon Sep 17 00:00:00 2001 From: WHITEROSE Date: Sun, 16 Nov 2025 19:26:24 +0100 Subject: [PATCH 08/33] feat(client): update send method to post synchronous request --- .../com/example/client/NtfyHttpClient.java | 27 ++++++++++++------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/src/main/java/com/example/client/NtfyHttpClient.java b/src/main/java/com/example/client/NtfyHttpClient.java index c9c3f2a4..d322ffc5 100644 --- a/src/main/java/com/example/client/NtfyHttpClient.java +++ b/src/main/java/com/example/client/NtfyHttpClient.java @@ -11,6 +11,7 @@ import java.net.URI; import java.net.http.HttpRequest; import java.net.http.HttpResponse; +import java.net.http.HttpTimeoutException; import java.util.concurrent.CompletableFuture; import java.util.concurrent.atomic.AtomicBoolean; @@ -75,15 +76,21 @@ public void send(String baseUrl, String topic, String message) { .POST(HttpRequest.BodyPublishers.ofString(message)) .build(); - HttpClientProvider.get() - .sendAsync(request, HttpResponse.BodyHandlers.ofString()) - .whenComplete((response, error) -> { - if (error != null) { - log.error("Failed to send message", error); - return; - } - log.info("ok"); - }); - } + try { + HttpClientProvider.get() + .send(request, HttpResponse.BodyHandlers.ofString()); + + log.info("ok"); + } catch (HttpTimeoutException e) { + log.error("Timeout while sending message", e); + + } catch (IOException e) { + log.error("IO error while sending message", e); + + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + log.error("Send operation interrupted", e); + } + } } From a730854f5a23c8c4e927fa2b84c42f70ce6fd459 Mon Sep 17 00:00:00 2001 From: WHITEROSE Date: Sun, 16 Nov 2025 19:30:41 +0100 Subject: [PATCH 09/33] fix(domain): update UI through runOnFx to ensure thread safety --- src/main/java/com/example/domain/ChatModel.java | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/example/domain/ChatModel.java b/src/main/java/com/example/domain/ChatModel.java index a5f89b8c..b49a87fd 100644 --- a/src/main/java/com/example/domain/ChatModel.java +++ b/src/main/java/com/example/domain/ChatModel.java @@ -1,5 +1,6 @@ package com.example.domain; +import javafx.application.Platform; import javafx.collections.FXCollections; import javafx.collections.ObservableList; @@ -7,11 +8,22 @@ public class ChatModel { private final ObservableList messages = FXCollections.observableArrayList(); + public void addMessage(NtfyMessage msg) { - messages.add(msg); + runOnFx(() -> messages.add(msg)); } + public ObservableList getMessages() { return messages; } + private static void runOnFx(Runnable task) { + try { + if (Platform.isFxApplicationThread()) task.run(); + else Platform.runLater(task); + } catch (IllegalStateException notInitialized) { + task.run(); + } + } + } From 69e40f66d7e2bb0f07ff0b3a1ca293125d039ff3 Mon Sep 17 00:00:00 2001 From: WHITEROSE Date: Sun, 16 Nov 2025 19:35:41 +0100 Subject: [PATCH 10/33] fix(client): serialize msg to DTO before send --- src/main/java/com/example/client/ChatNetworkClient.java | 4 +++- src/main/java/com/example/client/NtfyHttpClient.java | 4 ++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/example/client/ChatNetworkClient.java b/src/main/java/com/example/client/ChatNetworkClient.java index 71122746..4682d009 100644 --- a/src/main/java/com/example/client/ChatNetworkClient.java +++ b/src/main/java/com/example/client/ChatNetworkClient.java @@ -1,11 +1,13 @@ package com.example.client; +import com.example.domain.NtfyMessage; + import java.io.IOException; public interface ChatNetworkClient { Subscription subscribe(String baseUrl, String topic); - void send(String baseUrl, String topic, String message); + void send(String baseUrl, String topic, NtfyMessage message); interface Subscription extends AutoCloseable { @Override diff --git a/src/main/java/com/example/client/NtfyHttpClient.java b/src/main/java/com/example/client/NtfyHttpClient.java index d322ffc5..f0e10236 100644 --- a/src/main/java/com/example/client/NtfyHttpClient.java +++ b/src/main/java/com/example/client/NtfyHttpClient.java @@ -70,10 +70,10 @@ public boolean isOpen() { } @Override - public void send(String baseUrl, String topic, String message) { + public void send(String baseUrl, String topic, NtfyMessage msg) { HttpRequest request = HttpRequest.newBuilder() .uri(URI.create(baseUrl + "/" + topic)) - .POST(HttpRequest.BodyPublishers.ofString(message)) + .POST(HttpRequest.BodyPublishers.ofString(msg.message())) .build(); try { From 9ab7852702f06c8ce73485f07d69d121a52af84b Mon Sep 17 00:00:00 2001 From: WHITEROSE Date: Sun, 16 Nov 2025 19:51:05 +0100 Subject: [PATCH 11/33] fix(domain): update controller to deserialize and display the correct msg field --- src/main/java/com/example/HelloController.java | 4 ++-- src/main/java/com/example/HelloFX.java | 5 ++++- src/main/java/com/example/client/ChatNetworkClient.java | 2 +- src/main/java/com/example/client/NtfyHttpClient.java | 8 ++++---- 4 files changed, 11 insertions(+), 8 deletions(-) diff --git a/src/main/java/com/example/HelloController.java b/src/main/java/com/example/HelloController.java index e3555fe5..552e5ab1 100644 --- a/src/main/java/com/example/HelloController.java +++ b/src/main/java/com/example/HelloController.java @@ -19,8 +19,8 @@ public void setModel(ChatModel model) { model.getMessages().addListener((ListChangeListener) c -> { String events = model.getMessages() .stream() - .map(NtfyMessage::event) - .collect(Collectors.joining(";")); + .map(NtfyMessage::message) + .collect(Collectors.joining(", ")); messageLabel.setText(events); }); } diff --git a/src/main/java/com/example/HelloFX.java b/src/main/java/com/example/HelloFX.java index ccac3c47..890c0451 100644 --- a/src/main/java/com/example/HelloFX.java +++ b/src/main/java/com/example/HelloFX.java @@ -3,6 +3,7 @@ import com.example.client.ChatNetworkClient; import com.example.client.NtfyHttpClient; import com.example.domain.ChatModel; +import com.example.domain.NtfyMessage; import javafx.application.Application; import javafx.fxml.FXMLLoader; import javafx.scene.Parent; @@ -12,7 +13,9 @@ import org.slf4j.LoggerFactory; import java.io.IOException; +import java.time.LocalDateTime; import java.util.Properties; +import java.util.UUID; import static com.example.utils.EnvLoader.loadEnv; @@ -44,7 +47,7 @@ public static void main(String[] args) throws IOException, InterruptedException ChatNetworkClient.Subscription sub = client.subscribe(baseUrl, topic); log.info("Subscription: {}", sub); - // client.send(baseUrl, topic, "I've been expecting you."); + client.send(baseUrl, new NtfyMessage(UUID.randomUUID().toString(), System.currentTimeMillis(), "message", topic, "HELLO")); launch(); diff --git a/src/main/java/com/example/client/ChatNetworkClient.java b/src/main/java/com/example/client/ChatNetworkClient.java index 4682d009..c05d0104 100644 --- a/src/main/java/com/example/client/ChatNetworkClient.java +++ b/src/main/java/com/example/client/ChatNetworkClient.java @@ -7,7 +7,7 @@ public interface ChatNetworkClient { Subscription subscribe(String baseUrl, String topic); - void send(String baseUrl, String topic, NtfyMessage message); + void send(String baseUrl, NtfyMessage message); interface Subscription extends AutoCloseable { @Override diff --git a/src/main/java/com/example/client/NtfyHttpClient.java b/src/main/java/com/example/client/NtfyHttpClient.java index f0e10236..cb7cd58c 100644 --- a/src/main/java/com/example/client/NtfyHttpClient.java +++ b/src/main/java/com/example/client/NtfyHttpClient.java @@ -44,7 +44,7 @@ public Subscription subscribe(String baseUrl, String topic) { try { NtfyMessage msg = mapper.readValue(line, NtfyMessage.class); - if ("message".equals(msg.event())) { + if (msg.event().equals("message")) { Platform.runLater(() -> model.addMessage(msg) ); @@ -53,7 +53,7 @@ public Subscription subscribe(String baseUrl, String topic) { } }); }); - log.info("Subscribing to topic {}", topic); + log.info("Successfully subscribed to topic: {}", topic); return new Subscription() { @Override @@ -70,9 +70,9 @@ public boolean isOpen() { } @Override - public void send(String baseUrl, String topic, NtfyMessage msg) { + public void send(String baseUrl, NtfyMessage msg) { HttpRequest request = HttpRequest.newBuilder() - .uri(URI.create(baseUrl + "/" + topic)) + .uri(URI.create(baseUrl + "/" + msg.topic())) .POST(HttpRequest.BodyPublishers.ofString(msg.message())) .build(); From 8e718d1dcc5241948568087f759367b757693ee2 Mon Sep 17 00:00:00 2001 From: WHITEROSE Date: Sun, 16 Nov 2025 21:46:07 +0100 Subject: [PATCH 12/33] fix(client): throw exceptions at client level --- .../com/example/client/ChatNetworkClient.java | 4 ++-- .../com/example/client/NtfyHttpClient.java | 21 ++++--------------- 2 files changed, 6 insertions(+), 19 deletions(-) diff --git a/src/main/java/com/example/client/ChatNetworkClient.java b/src/main/java/com/example/client/ChatNetworkClient.java index c05d0104..22704466 100644 --- a/src/main/java/com/example/client/ChatNetworkClient.java +++ b/src/main/java/com/example/client/ChatNetworkClient.java @@ -7,11 +7,11 @@ public interface ChatNetworkClient { Subscription subscribe(String baseUrl, String topic); - void send(String baseUrl, NtfyMessage message); + void send(String baseUrl, NtfyMessage message) throws IOException, InterruptedException; interface Subscription extends AutoCloseable { @Override - void close() throws IOException; + void close(); boolean isOpen(); } } diff --git a/src/main/java/com/example/client/NtfyHttpClient.java b/src/main/java/com/example/client/NtfyHttpClient.java index cb7cd58c..9a5f3376 100644 --- a/src/main/java/com/example/client/NtfyHttpClient.java +++ b/src/main/java/com/example/client/NtfyHttpClient.java @@ -11,7 +11,6 @@ import java.net.URI; import java.net.http.HttpRequest; import java.net.http.HttpResponse; -import java.net.http.HttpTimeoutException; import java.util.concurrent.CompletableFuture; import java.util.concurrent.atomic.AtomicBoolean; @@ -57,7 +56,7 @@ public Subscription subscribe(String baseUrl, String topic) { return new Subscription() { @Override - public void close() throws IOException { + public void close() { open.set(false); future.cancel(true); } @@ -70,27 +69,15 @@ public boolean isOpen() { } @Override - public void send(String baseUrl, NtfyMessage msg) { + public void send(String baseUrl, NtfyMessage msg) throws IOException, InterruptedException { HttpRequest request = HttpRequest.newBuilder() .uri(URI.create(baseUrl + "/" + msg.topic())) .POST(HttpRequest.BodyPublishers.ofString(msg.message())) .build(); - try { - HttpClientProvider.get() - .send(request, HttpResponse.BodyHandlers.ofString()); + HttpClientProvider.get().send(request, HttpResponse.BodyHandlers.ofString()); - log.info("ok"); + log.info("Successfully sent message: {}", msg.message()); - } catch (HttpTimeoutException e) { - log.error("Timeout while sending message", e); - - } catch (IOException e) { - log.error("IO error while sending message", e); - - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - log.error("Send operation interrupted", e); - } } } From cc3f95de9f7588c494a611ff75f81f4f70c730d9 Mon Sep 17 00:00:00 2001 From: WHITEROSE Date: Sun, 16 Nov 2025 22:05:11 +0100 Subject: [PATCH 13/33] fix(client): log all received and appended events --- src/main/java/com/example/HelloFX.java | 2 +- src/main/java/com/example/client/NtfyHttpClient.java | 8 ++++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/example/HelloFX.java b/src/main/java/com/example/HelloFX.java index 890c0451..16a5cfb2 100644 --- a/src/main/java/com/example/HelloFX.java +++ b/src/main/java/com/example/HelloFX.java @@ -46,7 +46,7 @@ public static void main(String[] args) throws IOException, InterruptedException ChatNetworkClient client = new NtfyHttpClient(model); ChatNetworkClient.Subscription sub = client.subscribe(baseUrl, topic); - log.info("Subscription: {}", sub); + log.info("Subscription Active: {}", sub.isOpen()); client.send(baseUrl, new NtfyMessage(UUID.randomUUID().toString(), System.currentTimeMillis(), "message", topic, "HELLO")); launch(); diff --git a/src/main/java/com/example/client/NtfyHttpClient.java b/src/main/java/com/example/client/NtfyHttpClient.java index 9a5f3376..2c917256 100644 --- a/src/main/java/com/example/client/NtfyHttpClient.java +++ b/src/main/java/com/example/client/NtfyHttpClient.java @@ -2,6 +2,7 @@ import com.example.domain.ChatModel; import com.example.domain.NtfyMessage; +import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import javafx.application.Platform; import org.slf4j.Logger; @@ -17,7 +18,7 @@ public class NtfyHttpClient implements ChatNetworkClient { private static final ObjectMapper mapper = new ObjectMapper(); - private static final Logger log = LoggerFactory.getLogger(NtfyHttpClient.class); + private static final Logger log = LoggerFactory.getLogger("NtfyClient"); private final ChatModel model; public NtfyHttpClient(ChatModel model) { @@ -39,6 +40,7 @@ public Subscription subscribe(String baseUrl, String topic) { future.thenAccept(response -> { response.body().forEach(line -> { + log.info("Event received: {}", line); if (!open.get()) return; try { @@ -47,8 +49,10 @@ public Subscription subscribe(String baseUrl, String topic) { Platform.runLater(() -> model.addMessage(msg) ); + log.info("Message added: {}", msg); } - } catch (Exception _) { + } catch (JsonProcessingException e) { + throw new RuntimeException(e); } }); }); From ed2f9767641d451f06742469229320c005643658 Mon Sep 17 00:00:00 2001 From: WHITEROSE Date: Sun, 16 Nov 2025 22:14:17 +0100 Subject: [PATCH 14/33] fix(client): remove reduntant Platform.runLater from sub handler --- src/main/java/com/example/client/NtfyHttpClient.java | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/main/java/com/example/client/NtfyHttpClient.java b/src/main/java/com/example/client/NtfyHttpClient.java index 2c917256..791f5af3 100644 --- a/src/main/java/com/example/client/NtfyHttpClient.java +++ b/src/main/java/com/example/client/NtfyHttpClient.java @@ -46,9 +46,7 @@ public Subscription subscribe(String baseUrl, String topic) { try { NtfyMessage msg = mapper.readValue(line, NtfyMessage.class); if (msg.event().equals("message")) { - Platform.runLater(() -> - model.addMessage(msg) - ); + model.addMessage(msg); log.info("Message added: {}", msg); } } catch (JsonProcessingException e) { From c6476ce186a99e778fe047bd6294125fafa3ef87 Mon Sep 17 00:00:00 2001 From: WHITEROSE Date: Sun, 16 Nov 2025 22:24:43 +0100 Subject: [PATCH 15/33] feat(ui): move client operations to controller --- .../java/com/example/HelloController.java | 54 ++++++++++++++++--- src/main/java/com/example/HelloFX.java | 31 +++++------ src/main/java/module-info.java | 1 + .../resources/com/example/hello-view.fxml | 29 +++++++--- 4 files changed, 82 insertions(+), 33 deletions(-) diff --git a/src/main/java/com/example/HelloController.java b/src/main/java/com/example/HelloController.java index 552e5ab1..d127b9e3 100644 --- a/src/main/java/com/example/HelloController.java +++ b/src/main/java/com/example/HelloController.java @@ -1,11 +1,16 @@ package com.example; +import com.example.client.ChatNetworkClient; import com.example.domain.ChatModel; import com.example.domain.NtfyMessage; import javafx.collections.ListChangeListener; import javafx.fxml.FXML; import javafx.scene.control.Label; +import javafx.scene.control.ListView; +import javafx.scene.control.TextField; +import java.io.IOException; +import java.util.UUID; import java.util.stream.Collectors; /** @@ -13,19 +18,52 @@ */ public class HelloController { private ChatModel model; + private ChatNetworkClient client; + private String baseUrl; + private String topic; + + @FXML + private Label messageLabel; + + @FXML + private ListView messagesList; + + @FXML + private TextField messageInput; + + + public void setClient(ChatNetworkClient client, String baseUrl, String topic) { + this.client = client; + this.baseUrl = baseUrl; + this.topic = topic; + } public void setModel(ChatModel model) { this.model = model; - model.getMessages().addListener((ListChangeListener) c -> { - String events = model.getMessages() - .stream() - .map(NtfyMessage::message) - .collect(Collectors.joining(", ")); - messageLabel.setText(events); - }); + messagesList.setItems(model.getMessages()); } @FXML - private Label messageLabel; + private void onSend() { + String txt = messageInput.getText(); + if (txt == null || txt.isBlank()) return; + + NtfyMessage msg = new NtfyMessage( + UUID.randomUUID().toString(), + System.currentTimeMillis(), + "message", + topic, + txt + ); + + try { + client.send(baseUrl, msg); + } catch (InterruptedException | IOException e) { + throw new RuntimeException(e); + } + + messageInput.clear(); + } + } diff --git a/src/main/java/com/example/HelloFX.java b/src/main/java/com/example/HelloFX.java index 16a5cfb2..2a5ee8d4 100644 --- a/src/main/java/com/example/HelloFX.java +++ b/src/main/java/com/example/HelloFX.java @@ -26,30 +26,27 @@ public class HelloFX extends Application { @Override public void start(Stage stage) throws Exception { - FXMLLoader fxmlLoader = new FXMLLoader(HelloFX.class.getResource("hello-view.fxml")); - Parent root = fxmlLoader.load(); - HelloController controller = fxmlLoader.getController(); - controller.setModel(model); - Scene scene = new Scene(root, 640, 480); - stage.setTitle("Hello MVC"); - stage.setScene(scene); - stage.show(); - } - - public static void main(String[] args) throws IOException, InterruptedException { - Properties env = loadEnv(); - String baseUrl = env.getProperty("NTFY_BASE_URL"); String topic = env.getProperty("NTFY_TOPIC"); + FXMLLoader loader = new FXMLLoader(HelloFX.class.getResource("hello-view.fxml")); + Parent root = loader.load(); + + HelloController controller = loader.getController(); + controller.setModel(model); + ChatNetworkClient client = new NtfyHttpClient(model); + controller.setClient(client, baseUrl, topic); - ChatNetworkClient.Subscription sub = client.subscribe(baseUrl, topic); - log.info("Subscription Active: {}", sub.isOpen()); - client.send(baseUrl, new NtfyMessage(UUID.randomUUID().toString(), System.currentTimeMillis(), "message", topic, "HELLO")); + client.subscribe(baseUrl, topic); + + stage.setScene(new Scene(root)); + stage.show(); + } - launch(); + public static void main(String[] args) { + launch(args); } diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java index ab14768f..3c136a33 100644 --- a/src/main/java/module-info.java +++ b/src/main/java/module-info.java @@ -11,4 +11,5 @@ opens com.example.domain to com.fasterxml.jackson.databind; exports com.example; exports com.example.domain; + exports com.example.client; } \ No newline at end of file diff --git a/src/main/resources/com/example/hello-view.fxml b/src/main/resources/com/example/hello-view.fxml index 20a7dc82..d0469f5a 100644 --- a/src/main/resources/com/example/hello-view.fxml +++ b/src/main/resources/com/example/hello-view.fxml @@ -1,9 +1,22 @@ - - - - - - - + + + + + + + + + + + + +