From af3e38d08ccf6812493a045c899d02f6ce6075e6 Mon Sep 17 00:00:00 2001 From: Linnea Vardal Date: Fri, 14 Nov 2025 10:21:43 +0100 Subject: [PATCH 01/13] Updated .gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 6ac465db..244268f9 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ target/ /.idea/ +.env From ef857502eb58e1c81dbe214b05e30f5cef2940a0 Mon Sep 17 00:00:00 2001 From: Linnea Vardal Date: Fri, 14 Nov 2025 10:22:52 +0100 Subject: [PATCH 02/13] Updated pom.xml --- pom.xml | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/pom.xml b/pom.xml index c40f667e..00a38fc9 100644 --- a/pom.xml +++ b/pom.xml @@ -45,6 +45,21 @@ javafx-fxml ${javafx.version} + + org.openjfx + javafx-base + ${javafx.version} + + + com.fasterxml.jackson.core + jackson-databind + 2.17.0 + + + io.github.cdimascio + dotenv-java + 3.1.0 + From 9902ef7b4a4340633264e72485c4f429f70b84f9 Mon Sep 17 00:00:00 2001 From: Linnea Vardal Date: Fri, 14 Nov 2025 12:43:41 +0100 Subject: [PATCH 03/13] Created interfaces and record --- .../java/com/example/ChatNetworkClient.java | 19 +++++++++++++++++++ src/main/java/com/example/NtfyMessage.java | 7 +++++++ src/main/java/com/example/Subscription.java | 11 +++++++++++ src/main/java/module-info.java | 1 + 4 files changed, 38 insertions(+) create mode 100644 src/main/java/com/example/ChatNetworkClient.java create mode 100644 src/main/java/com/example/NtfyMessage.java create mode 100644 src/main/java/com/example/Subscription.java diff --git a/src/main/java/com/example/ChatNetworkClient.java b/src/main/java/com/example/ChatNetworkClient.java new file mode 100644 index 00000000..f65f0106 --- /dev/null +++ b/src/main/java/com/example/ChatNetworkClient.java @@ -0,0 +1,19 @@ +package com.example; + +import java.io.Closeable; +import java.io.IOException; +import java.util.function.Consumer; + +public interface ChatNetworkClient { + void send(String baseUrl, NtfyMessage message) throws Exception; + + //Returnerar mitt egna Subscription-interface + Subscription subscribe(String baseUrl, String topic, Consumer messageHandler); + + //Inner interface för Subscription + interface Subscription extends Closeable { + @Override + void close() throws IOException; + boolean isOpen(); + } +} diff --git a/src/main/java/com/example/NtfyMessage.java b/src/main/java/com/example/NtfyMessage.java new file mode 100644 index 00000000..5b6318e4 --- /dev/null +++ b/src/main/java/com/example/NtfyMessage.java @@ -0,0 +1,7 @@ +package com.example; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +@JsonIgnoreProperties(ignoreUnknown = true) +public record NtfyMessage(String id, Long time, String event, String topic, String message) { +} diff --git a/src/main/java/com/example/Subscription.java b/src/main/java/com/example/Subscription.java new file mode 100644 index 00000000..73328460 --- /dev/null +++ b/src/main/java/com/example/Subscription.java @@ -0,0 +1,11 @@ +package com.example; + +import java.io.Closeable; +import java.io.IOException; + +public interface Subscription extends Closeable { + + @Override + void close() throws IOException; + boolean isOpen(); +} diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java index 71574a27..071f1a69 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 com.fasterxml.jackson.annotation; opens com.example to javafx.fxml; exports com.example; From ddd7bcc31661d114c1e01c68fa895e1ac740fee9 Mon Sep 17 00:00:00 2001 From: Linnea Vardal Date: Sat, 15 Nov 2025 12:27:26 +0100 Subject: [PATCH 04/13] Updated HelloController and HelloModel --- .../java/com/example/HelloController.java | 32 +++++++++++++++++-- src/main/java/com/example/HelloModel.java | 30 +++++++++++++++++ 2 files changed, 60 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/example/HelloController.java b/src/main/java/com/example/HelloController.java index fdd160a0..85a95017 100644 --- a/src/main/java/com/example/HelloController.java +++ b/src/main/java/com/example/HelloController.java @@ -2,6 +2,9 @@ import javafx.fxml.FXML; import javafx.scene.control.Label; +import javafx.scene.control.ListView; +import javafx.scene.control.TextField; +import javafx.scene.control.Button; /** * Controller layer: mediates between the view (FXML) and the model. @@ -10,13 +13,38 @@ public class HelloController { private final HelloModel model = new HelloModel(); - @FXML - private Label messageLabel; + @FXML private Label messageLabel; + + //Nya FXML-komponenter: + @FXML private ListView messageListView; //Visar meddelanden från HelloModel + @FXML private TextField messageTextField; //Används för att skriva nya meddelanden + @FXML private Button sendButton; //Används för att skicka meddelanden @FXML private void initialize() { if (messageLabel != null) { messageLabel.setText(model.getGreeting()); } + //Binder listView till meddelandelistan i modellen + messageListView.setItems(model.getMessages()); + } + + //Ny metod för att hantera knapptryckningar + @FXML + private void onSendButtonClick() { + String messageText = messageTextField.getText(); + if (!messageText.isEmpty()) { + //Skapa ett nytt meddelande och lägg till det i modellen + NtfyMessage message = new NtfyMessage( + "1", // ID (kan genereras bättre) + System.currentTimeMillis(), // Tid + "message", // Eventtyp + "chat", // Topic + messageText //Meddelandetext + ); + model.addMessage(message); + messageTextField.clear(); // Rensa inputfältet + } + } } diff --git a/src/main/java/com/example/HelloModel.java b/src/main/java/com/example/HelloModel.java index 385cfd10..95daa62b 100644 --- a/src/main/java/com/example/HelloModel.java +++ b/src/main/java/com/example/HelloModel.java @@ -1,5 +1,11 @@ package com.example; +import javafx.beans.property.BooleanProperty; +import javafx.beans.property.ReadOnlyBooleanProperty; +import javafx.beans.property.SimpleBooleanProperty; +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; + /** * Model layer: encapsulates application data and business logic. */ @@ -12,4 +18,28 @@ public String getGreeting() { String javafxVersion = System.getProperty("javafx.version"); return "Hello, JavaFX " + javafxVersion + ", running on Java " + javaVersion + "."; } + + //Nya fält för chat-funktionalitet + private final ObservableList messages = FXCollections.observableArrayList(); + private final BooleanProperty connected = new SimpleBooleanProperty(false); + + //Getter för meddelandelistan + public ObservableList getMessages() { + return messages; + } + + //Getter för anslutningstillstånd + public ReadOnlyBooleanProperty connectedProperty() { + return connected; + } + + //Metod för att lägga till meddelande + public void addMessage(NtfyMessage message) { + messages.add(message); + } + + //Metod för att uppdatera anslutningstillstånd + public void setConnected(boolean connected) { + this.connected.set(connected); + } } From 5306c6270a1a0fc820fada6264bb04715ae090b0 Mon Sep 17 00:00:00 2001 From: Linnea Vardal Date: Sat, 15 Nov 2025 16:32:05 +0100 Subject: [PATCH 05/13] Updated files with DTO, Asynchronic operations, thread safety, Builder pattern, Singleton and exceptions --- .../java/com/example/HelloController.java | 24 ++++- src/main/java/com/example/HelloFX.java | 17 ++++ src/main/java/com/example/HelloModel.java | 15 +++- src/main/java/com/example/NtfyHttpClient.java | 87 +++++++++++++++++++ src/main/java/com/example/NtfyMessage.java | 7 +- src/main/java/module-info.java | 3 + 6 files changed, 148 insertions(+), 5 deletions(-) create mode 100644 src/main/java/com/example/NtfyHttpClient.java diff --git a/src/main/java/com/example/HelloController.java b/src/main/java/com/example/HelloController.java index 85a95017..0eaa12fb 100644 --- a/src/main/java/com/example/HelloController.java +++ b/src/main/java/com/example/HelloController.java @@ -11,7 +11,9 @@ */ public class HelloController { - private final HelloModel model = new HelloModel(); + private final HelloModel model; + private final ChatNetworkClient httpClient; // Abstraktion (Dependency Inversion) + private final String hostName; @FXML private Label messageLabel; @@ -20,6 +22,13 @@ public class HelloController { @FXML private TextField messageTextField; //Används för att skriva nya meddelanden @FXML private Button sendButton; //Används för att skicka meddelanden + //Konstruktor för Dependency Injection + public HelloController(HelloModel model, ChatNetworkClient httpClient, String hostName) { + this.model = model; + this.httpClient = httpClient; + this.hostName = hostName; + } + @FXML private void initialize() { if (messageLabel != null) { @@ -43,7 +52,18 @@ private void onSendButtonClick() { messageText //Meddelandetext ); model.addMessage(message); - messageTextField.clear(); // Rensa inputfältet + + //Lägg till meddelandet i modellen + model.addMessage(message); + + try { + //Använd hostName-fältet (skickat via konstruktorn) + httpClient.send(hostName, message); + messageTextField.clear(); + } catch (Exception e) { + System.err.println("Fel vid sändning: " + e.getMessage()); + } + } } diff --git a/src/main/java/com/example/HelloFX.java b/src/main/java/com/example/HelloFX.java index 96bdc5ca..dec7e0e1 100644 --- a/src/main/java/com/example/HelloFX.java +++ b/src/main/java/com/example/HelloFX.java @@ -1,5 +1,6 @@ package com.example; +import io.github.cdimascio.dotenv.Dotenv; import javafx.application.Application; import javafx.fxml.FXMLLoader; import javafx.scene.Parent; @@ -10,7 +11,23 @@ public class HelloFX extends Application { @Override public void start(Stage stage) throws Exception { + Dotenv dotenv = Dotenv.load(); //Ladda .env-filen + String hostName = dotenv.get("HOST_NAME"); + + //Validering (valfritt) + if (hostName == null) { + throw new IllegalStateException( + "Kunde inte läsa HOST_NAME från .env-filen. " + + "Kontrollera att filen finns i projektets rotmapp!" + ); + } + + System.out.println("Ansluter till ntfy på: " + hostName); //Debug-logg + + HelloModel model = new HelloModel(); + ChatNetworkClient httpClient = new NtfyHttpClient(); FXMLLoader fxmlLoader = new FXMLLoader(HelloFX.class.getResource("hello-view.fxml")); + fxmlLoader.setControllerFactory(c -> new HelloController(model, httpClient, hostName)); Parent root = fxmlLoader.load(); Scene scene = new Scene(root, 640, 480); stage.setTitle("Hello MVC"); diff --git a/src/main/java/com/example/HelloModel.java b/src/main/java/com/example/HelloModel.java index 95daa62b..628b8932 100644 --- a/src/main/java/com/example/HelloModel.java +++ b/src/main/java/com/example/HelloModel.java @@ -1,5 +1,6 @@ package com.example; +import javafx.application.Platform; import javafx.beans.property.BooleanProperty; import javafx.beans.property.ReadOnlyBooleanProperty; import javafx.beans.property.SimpleBooleanProperty; @@ -35,11 +36,21 @@ public ReadOnlyBooleanProperty connectedProperty() { //Metod för att lägga till meddelande public void addMessage(NtfyMessage message) { - messages.add(message); + runOnFX(() -> messages.add(message)); } //Metod för att uppdatera anslutningstillstånd public void setConnected(boolean connected) { - this.connected.set(connected); + runOnFX(() -> this.connected.set(connected)); + } + + //Dispatcher Utility + private static void runOnFX(Runnable task) { + try { + if (Platform.isFxApplicationThread()) task.run(); + else Platform.runLater(task); + } catch (IllegalThreadStateException notInitialized) { + task.run(); //Fallback för enhetstester + } } } diff --git a/src/main/java/com/example/NtfyHttpClient.java b/src/main/java/com/example/NtfyHttpClient.java new file mode 100644 index 00000000..fa63e3f4 --- /dev/null +++ b/src/main/java/com/example/NtfyHttpClient.java @@ -0,0 +1,87 @@ +package com.example; + +import com.fasterxml.jackson.databind.ObjectMapper; +import javafx.application.Platform; + +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.concurrent.CompletableFuture; +import java.util.function.Consumer; + +public class NtfyHttpClient implements ChatNetworkClient{ + private final HttpClient http; //Singleton (återanvänds för alla anrop) + private final ObjectMapper mapper; + + public NtfyHttpClient() { + this.http = HttpClient.newHttpClient(); //Implicit Singleton + this.mapper = new ObjectMapper(); + } + + @Override + public void send(String baseUrl, NtfyMessage message) throws Exception{ + //Builder Pattern för HttpRequest + HttpRequest request = HttpRequest.newBuilder() + .POST(HttpRequest.BodyPublishers.ofString( + String.format("{\"topic\":\"%s\",\"message\":\"%s\"}", message.topic(), message.message()) + )) + .header("Content-Type", "application/json") + .uri(URI.create(baseUrl + "/" + message.topic())) + .build(); + + //Synkront anrop (blockerande) + HttpResponse response = http.send( + request, + HttpResponse.BodyHandlers.discarding() + ); + //validera statuskoden + if (response.statusCode() >= 400) { + throw new IOException("Fel vid sändning: HTTP " + response.statusCode()); + } + } + + public Subscription subscribe(String baseUrl, String topic, Consumer messageHandler) { + HttpRequest request = HttpRequest.newBuilder() + .GET() + .uri(URI.create(baseUrl + "/" + topic + "/json")) + .build(); + + //Asynkront anrop (icke-blockerande) + CompletableFuture future = http.sendAsync( + request, + HttpResponse.BodyHandlers.ofLines() + ).thenAccept(response -> { + response.body() + .map(line-> { + try { + return mapper.readValue(line, NtfyMessage.class); + } catch (IOException e) { + throw new RuntimeException(e); + } + }) + .filter(message -> message.event().equals("message")) + .forEach(message -> { + //Uppdatera UI på JavaFX-tråden + Platform.runLater(() -> messageHandler.accept(message)); + }); + }).exceptionally(error -> { + System.err.println("Fel vid prenumeration: " + error.getMessage()); + return null; + }); + + //Command Pattern: Returnera ett Subscription-objekt + return new Subscription() { + @Override + public void close() throws IOException { + future.cancel(true); + } + + @Override + public boolean isOpen() { + return !future.isDone(); + } + }; + } +} diff --git a/src/main/java/com/example/NtfyMessage.java b/src/main/java/com/example/NtfyMessage.java index 5b6318e4..242f16f3 100644 --- a/src/main/java/com/example/NtfyMessage.java +++ b/src/main/java/com/example/NtfyMessage.java @@ -3,5 +3,10 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; @JsonIgnoreProperties(ignoreUnknown = true) -public record NtfyMessage(String id, Long time, String event, String topic, String message) { +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 071f1a69..80a2a9e5 100644 --- a/src/main/java/module-info.java +++ b/src/main/java/module-info.java @@ -2,6 +2,9 @@ requires javafx.controls; requires javafx.fxml; requires com.fasterxml.jackson.annotation; + requires java.net.http; + requires io.github.cdimascio.dotenv.java; + requires com.fasterxml.jackson.databind; opens com.example to javafx.fxml; exports com.example; From 2bd68aa3389f1902b6a49809784be73ae635869b Mon Sep 17 00:00:00 2001 From: Linnea Vardal Date: Sat, 15 Nov 2025 18:20:33 +0100 Subject: [PATCH 06/13] =?UTF-8?q?Uppdaterade=20gr=C3=A4nssnittet=20i=20hel?= =?UTF-8?q?lo-view.fxml=20och=20kopplade=20ihop=20den=20med=20metoderna=20?= =?UTF-8?q?i=20HelloController.java?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/example/HelloController.java | 39 +++++++++++++++++-- .../resources/com/example/hello-view.fxml | 39 +++++++++++++++---- 2 files changed, 67 insertions(+), 11 deletions(-) diff --git a/src/main/java/com/example/HelloController.java b/src/main/java/com/example/HelloController.java index 0eaa12fb..2d769b6c 100644 --- a/src/main/java/com/example/HelloController.java +++ b/src/main/java/com/example/HelloController.java @@ -1,11 +1,14 @@ package com.example; +import javafx.beans.binding.Bindings; import javafx.fxml.FXML; import javafx.scene.control.Label; import javafx.scene.control.ListView; import javafx.scene.control.TextField; import javafx.scene.control.Button; +import java.io.IOException; + /** * Controller layer: mediates between the view (FXML) and the model. */ @@ -14,6 +17,7 @@ public class HelloController { private final HelloModel model; private final ChatNetworkClient httpClient; // Abstraktion (Dependency Inversion) private final String hostName; + private ChatNetworkClient.Subscription subscription; @FXML private Label messageLabel; @@ -21,6 +25,7 @@ public class HelloController { @FXML private ListView messageListView; //Visar meddelanden från HelloModel @FXML private TextField messageTextField; //Används för att skriva nya meddelanden @FXML private Button sendButton; //Används för att skicka meddelanden + @FXML private Label connectionStatusLabel; // Status för anslutning //Konstruktor för Dependency Injection public HelloController(HelloModel model, ChatNetworkClient httpClient, String hostName) { @@ -36,6 +41,13 @@ private void initialize() { } //Binder listView till meddelandelistan i modellen messageListView.setItems(model.getMessages()); + + //Binder anslutningsstatus till etiketten + connectionStatusLabel.textProperty().bind( + Bindings.when(model.connectedProperty()) + .then("Ansluten: ja") + .otherwise("Ansluten: nej") + ); } //Ny metod för att hantera knapptryckningar @@ -45,7 +57,7 @@ private void onSendButtonClick() { if (!messageText.isEmpty()) { //Skapa ett nytt meddelande och lägg till det i modellen NtfyMessage message = new NtfyMessage( - "1", // ID (kan genereras bättre) + "1", // ID (kan genereras bättre) //TODO System.currentTimeMillis(), // Tid "message", // Eventtyp "chat", // Topic @@ -55,7 +67,6 @@ private void onSendButtonClick() { //Lägg till meddelandet i modellen model.addMessage(message); - try { //Använd hostName-fältet (skickat via konstruktorn) httpClient.send(hostName, message); @@ -63,8 +74,30 @@ private void onSendButtonClick() { } catch (Exception e) { System.err.println("Fel vid sändning: " + e.getMessage()); } - } + } + + //Metod för att prenumerera på meddelanden + @FXML + private void onSubscribeButtonClick() { + subscription = httpClient.subscribe( + hostName, + "chat", + message -> model.addMessage(message) //Lägg till meddelande i modellen + ); + model.setConnected(true); + } + //Metod för att avbryta prenumeration + @FXML + private void onUnsubscribeButtonClick() { + if (subscription != null) { + try { + subscription.close(); + model.setConnected(false); + } catch (IOException e) { + System.err.println("Fel vid avprenumeration: " + e.getMessage()); + } + } } } diff --git a/src/main/resources/com/example/hello-view.fxml b/src/main/resources/com/example/hello-view.fxml index 20a7dc82..1bb7dd4a 100644 --- a/src/main/resources/com/example/hello-view.fxml +++ b/src/main/resources/com/example/hello-view.fxml @@ -1,9 +1,32 @@ - - - - - - - + + + + + +