From 02a1a490eff1e2e85b0bfba804f16544a9edafe9 Mon Sep 17 00:00:00 2001 From: Younes Date: Mon, 3 Nov 2025 21:02:52 +0100 Subject: [PATCH 1/9] =?UTF-8?q?allt=20f=C3=B6rutom=20mock=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 3 + pom.xml | 76 ++++--- src/main/java/com/example/HelloFX.java | 16 +- .../example/controller/ChatController.java | 112 ++++++++++ .../java/com/example/model/ChatModel.java | 52 +++++ .../java/com/example/model/NtfyMessage.java | 16 ++ .../example/network/ChatNetworkClient.java | 14 ++ .../com/example/network/NtfyHttpClient.java | 72 +++++++ src/main/java/com/example/util/EnvLoader.java | 41 ++++ src/main/java/module-info.java | 15 +- src/main/resources/com/example/ChatView.fxml | 204 ++++++++++++++++++ .../java/com/example/model/ChatModelTest.java | 54 +++++ 12 files changed, 634 insertions(+), 41 deletions(-) create mode 100644 src/main/java/com/example/controller/ChatController.java create mode 100644 src/main/java/com/example/model/ChatModel.java create mode 100644 src/main/java/com/example/model/NtfyMessage.java create mode 100644 src/main/java/com/example/network/ChatNetworkClient.java create mode 100644 src/main/java/com/example/network/NtfyHttpClient.java create mode 100644 src/main/java/com/example/util/EnvLoader.java create mode 100644 src/main/resources/com/example/ChatView.fxml create mode 100644 src/test/java/com/example/model/ChatModelTest.java diff --git a/.gitignore b/.gitignore index 6ac465db..5e108027 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,5 @@ target/ /.idea/ +.env +.env.local +*.env \ No newline at end of file diff --git a/pom.xml b/pom.xml index c40f667e..7f2db3b2 100644 --- a/pom.xml +++ b/pom.xml @@ -1,53 +1,65 @@ + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> 4.0.0 com.example - javafx + javafx-chat-app 1.0-SNAPSHOT - 25 - UTF-8 - 6.0.0 - 3.27.6 - 5.20.0 - 25 + 21.0.3 + 21 + 21 + - org.junit.jupiter - junit-jupiter - ${junit.jupiter.version} - test + org.openjfx + javafx-controls + ${javafx.version} - org.assertj - assertj-core - ${assertj.core.version} - test + org.openjfx + javafx-fxml + ${javafx.version} - org.mockito - mockito-junit-jupiter - ${mockito.version} + com.fasterxml.jackson.core + jackson-databind + 2.17.0 + + + org.junit.jupiter + junit-jupiter + 5.10.2 test - org.openjfx - javafx-controls - ${javafx.version} + net.bytebuddy + byte-buddy + 1.15.10 + test - org.openjfx - javafx-fxml - ${javafx.version} + net.bytebuddy + byte-buddy-agent + 1.15.10 + test + + + org.apache.maven.plugins + maven-compiler-plugin + 3.13.0 + + 21 + + org.openjfx javafx-maven-plugin @@ -55,14 +67,16 @@ com.example.HelloFX - + + - javafx - true - true - true + + org.apache.maven.plugins + maven-surefire-plugin + 3.2.5 + - + \ No newline at end of file diff --git a/src/main/java/com/example/HelloFX.java b/src/main/java/com/example/HelloFX.java index 96bdc5ca..480979ed 100644 --- a/src/main/java/com/example/HelloFX.java +++ b/src/main/java/com/example/HelloFX.java @@ -1,25 +1,27 @@ package com.example; +import com.example.util.EnvLoader; import javafx.application.Application; import javafx.fxml.FXMLLoader; -import javafx.scene.Parent; import javafx.scene.Scene; import javafx.stage.Stage; +import java.io.IOException; + 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(); - Scene scene = new Scene(root, 640, 480); - stage.setTitle("Hello MVC"); + public void start(Stage stage) throws IOException { + FXMLLoader fxmlLoader = new FXMLLoader(HelloFX.class.getResource("ChatView.fxml")); + Scene scene = new Scene(fxmlLoader.load()); + + stage.setTitle("Java25 Chat App"); stage.setScene(scene); stage.show(); } public static void main(String[] args) { + EnvLoader.load(); launch(); } - } \ No newline at end of file diff --git a/src/main/java/com/example/controller/ChatController.java b/src/main/java/com/example/controller/ChatController.java new file mode 100644 index 00000000..91f3a3b1 --- /dev/null +++ b/src/main/java/com/example/controller/ChatController.java @@ -0,0 +1,112 @@ +package com.example.controller; + +import com.example.model.ChatModel; +import com.example.model.NtfyMessage; +import javafx.fxml.FXML; +import javafx.scene.control.ListView; +import javafx.scene.control.TextField; +import javafx.scene.control.Label; +import javafx.scene.control.ListCell; +import javafx.concurrent.Task; +import javafx.geometry.Pos; + +public class ChatController { + + @FXML + private ListView messageListView; + + @FXML + private TextField messageInput; + + @FXML + private Label statusLabel; + + private ChatModel model; + + @FXML + public void initialize() { + this.model = new ChatModel(); + + updateStatusOnline(); + + messageListView.setCellFactory(listView -> new ListCell() { + @Override + protected void updateItem(String item, boolean empty) { + super.updateItem(item, empty); + if (empty || item == null) { + setText(null); + setGraphic(null); + setStyle(""); + } else { + setText(item); + setAlignment(Pos.CENTER_LEFT); + setStyle("-fx-background-color: rgba(255, 250, 240, 0.6); " + + "-fx-text-fill: #3d3d3d; " + + "-fx-padding: 14 18 14 18; " + + "-fx-border-color: rgba(214, 69, 69, 0.15); " + + "-fx-border-width: 0 0 0 3; " + + "-fx-font-size: 13px; " + + "-fx-background-radius: 0; " + + "-fx-border-radius: 0; " + + "-fx-effect: dropshadow(gaussian, rgba(0,0,0,0.05), 3, 0, 0, 2);"); + } + } + }); + + model.getMessages().addListener((javafx.collections.ListChangeListener.Change change) -> { + while (change.next()) { + if (change.wasAdded()) { + for (NtfyMessage msg : change.getAddedSubList()) { + String formatted = "🌸 " + msg.message(); + messageListView.getItems().add(formatted); + } + messageListView.scrollTo(messageListView.getItems().size() - 1); + } + } + }); + } + + @FXML + private void handleSendButtonAction() { + String text = messageInput.getText(); + if (text != null && !text.trim().isEmpty()) { + Task task = new Task<>() { + @Override + protected Void call() throws Exception { + model.sendMessage(text.trim()); + return null; + } + }; + + task.setOnSucceeded(e -> { + messageInput.clear(); + updateStatusOnline(); + }); + + task.setOnFailed(e -> { + System.err.println("Send failed: " + task.getException().getMessage()); + updateStatusOffline(); + }); + + new Thread(task).start(); + } + } + + private void updateStatusOnline() { + if (statusLabel != null) { + statusLabel.setText("● online"); + statusLabel.setStyle("-fx-text-fill: #c93939; " + + "-fx-font-size: 10px; " + + "-fx-effect: dropshadow(gaussian, rgba(0,0,0,0.3), 2, 0, 0, 1);"); + } + } + + private void updateStatusOffline() { + if (statusLabel != null) { + statusLabel.setText("● offline"); + statusLabel.setStyle("-fx-text-fill: #6b5d54; " + + "-fx-font-size: 10px; " + + "-fx-effect: dropshadow(gaussian, rgba(0,0,0,0.3), 2, 0, 0, 1);"); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/example/model/ChatModel.java b/src/main/java/com/example/model/ChatModel.java new file mode 100644 index 00000000..34fc1617 --- /dev/null +++ b/src/main/java/com/example/model/ChatModel.java @@ -0,0 +1,52 @@ +package com.example.model; + +import com.example.network.ChatNetworkClient; +import com.example.network.NtfyHttpClient; +import javafx.application.Platform; +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; + +public class ChatModel { + + private final ObservableList messages = FXCollections.observableArrayList(); + private final ChatNetworkClient networkClient; + private ChatNetworkClient.Subscription subscription; + private final String baseUrl; + private final String topic; + + public ChatModel() { + this.networkClient = new NtfyHttpClient(); + + String url = System.getProperty("NTFY_BASE_URL"); + if (url == null || url.isBlank()) { + url = System.getenv("NTFY_BASE_URL"); + } + if (url == null || url.isBlank()) { + url = "https://ntfy.fungover.org"; + } + + this.baseUrl = url; + this.topic = "myChatTopic"; + + System.out.println("Using NTFY URL: " + this.baseUrl); + connect(); + } + + public ObservableList getMessages() { + return messages; + } + + public void connect() { + this.subscription = networkClient.subscribe( + baseUrl, + topic, + msg -> Platform.runLater(() -> messages.add(msg)), + error -> System.err.println("Error: " + error.getMessage()) + ); + } + + public void sendMessage(String text) throws Exception { + NtfyMessage message = new NtfyMessage(topic, text); + networkClient.send(baseUrl, message); + } +} \ No newline at end of file diff --git a/src/main/java/com/example/model/NtfyMessage.java b/src/main/java/com/example/model/NtfyMessage.java new file mode 100644 index 00000000..d8030002 --- /dev/null +++ b/src/main/java/com/example/model/NtfyMessage.java @@ -0,0 +1,16 @@ +package com.example.model; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +@JsonIgnoreProperties(ignoreUnknown = true) +public record NtfyMessage( + String id, + Long time, + String event, + String topic, + String message +) { + public NtfyMessage(String topic, String message) { + this(null, null, "message", topic, message); + } +} \ No newline at end of file diff --git a/src/main/java/com/example/network/ChatNetworkClient.java b/src/main/java/com/example/network/ChatNetworkClient.java new file mode 100644 index 00000000..f56abb26 --- /dev/null +++ b/src/main/java/com/example/network/ChatNetworkClient.java @@ -0,0 +1,14 @@ +package com.example.network; + +import com.example.model.NtfyMessage; +import java.io.IOException; +import java.util.function.Consumer; + +public interface ChatNetworkClient { + void send(String baseUrl, NtfyMessage message) throws IOException, InterruptedException; + Subscription subscribe(String baseUrl, String topic, Consumer onMessage, Consumer onError); + + interface Subscription { + void close(); + } +} \ No newline at end of file diff --git a/src/main/java/com/example/network/NtfyHttpClient.java b/src/main/java/com/example/network/NtfyHttpClient.java new file mode 100644 index 00000000..76930f71 --- /dev/null +++ b/src/main/java/com/example/network/NtfyHttpClient.java @@ -0,0 +1,72 @@ +package com.example.network; + +import com.example.model.NtfyMessage; +import com.fasterxml.jackson.databind.ObjectMapper; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.nio.charset.StandardCharsets; +import java.util.concurrent.CompletableFuture; +import java.util.function.Consumer; + +public class NtfyHttpClient implements ChatNetworkClient { + + private final HttpClient http; + private final ObjectMapper objectMapper; + + public NtfyHttpClient() { + this.http = HttpClient.newHttpClient(); + this.objectMapper = new ObjectMapper(); + } + + @Override + public void send(String baseUrl, NtfyMessage message) throws IOException, InterruptedException { + String json = objectMapper.writeValueAsString(message); + + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(baseUrl.endsWith("/") ? baseUrl.substring(0, baseUrl.length() - 1) : baseUrl)) + .header("Content-Type", "application/json") + .POST(HttpRequest.BodyPublishers.ofString(json, StandardCharsets.UTF_8)) + .build(); + + http.send(request, HttpResponse.BodyHandlers.ofString()); + } + + @Override + public Subscription subscribe(String baseUrl, String topic, Consumer onMessage, Consumer onError) { + String url = (baseUrl.endsWith("/") ? baseUrl.substring(0, baseUrl.length() - 1) : baseUrl) + "/" + topic + "/json"; + + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(url)) + .GET() + .build(); + + CompletableFuture future = http.sendAsync(request, HttpResponse.BodyHandlers.ofInputStream()) + .thenAccept(response -> { + try (BufferedReader reader = new BufferedReader(new InputStreamReader(response.body()))) { + String line; + while ((line = reader.readLine()) != null) { + if (!line.trim().isEmpty()) { + NtfyMessage message = objectMapper.readValue(line.trim(), NtfyMessage.class); + if ("message".equals(message.event())) { + onMessage.accept(message); + } + } + } + } catch (Exception e) { + onError.accept(e); + } + }) + .exceptionally(error -> { + onError.accept(error); + return null; + }); + + return () -> future.cancel(true); + } +} \ No newline at end of file diff --git a/src/main/java/com/example/util/EnvLoader.java b/src/main/java/com/example/util/EnvLoader.java new file mode 100644 index 00000000..ca40fb9a --- /dev/null +++ b/src/main/java/com/example/util/EnvLoader.java @@ -0,0 +1,41 @@ +package com.example.util; + +import java.io.BufferedReader; +import java.io.FileReader; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; + +public class EnvLoader { + + public static void load() { + Path envPath = Paths.get(".env"); + + if (!Files.exists(envPath)) { + System.out.println("No .env file found, using system environment variables"); + return; + } + + try (BufferedReader reader = new BufferedReader(new FileReader(envPath.toFile()))) { + String line; + while ((line = reader.readLine()) != null) { + line = line.trim(); + + if (line.isEmpty() || line.startsWith("#")) { + continue; + } + + String[] parts = line.split("=", 2); + if (parts.length == 2) { + String key = parts[0].trim(); + String value = parts[1].trim(); + System.setProperty(key, value); + System.out.println("Loaded env: " + key + "=" + value); + } + } + } catch (IOException e) { + System.err.println("Failed to load .env file: " + e.getMessage()); + } + } +} \ No newline at end of file diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java index 71574a27..3956337c 100644 --- a/src/main/java/module-info.java +++ b/src/main/java/module-info.java @@ -1,7 +1,16 @@ -module hellofx { +module com.example.javafxchatapp { + exports com.example; + exports com.example.controller; + exports com.example.model; + exports com.example.network; + exports com.example.util; + requires javafx.controls; requires javafx.fxml; + requires java.net.http; + requires com.fasterxml.jackson.databind; - opens com.example to javafx.fxml; - exports com.example; + opens com.example.controller to javafx.fxml; + opens com.example to javafx.graphics; + opens com.example.model to com.fasterxml.jackson.databind; } \ No newline at end of file diff --git a/src/main/resources/com/example/ChatView.fxml b/src/main/resources/com/example/ChatView.fxml new file mode 100644 index 00000000..e743d6d0 --- /dev/null +++ b/src/main/resources/com/example/ChatView.fxml @@ -0,0 +1,204 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +