Skip to content

Ntfy#20

Closed
gurkvatten wants to merge 17 commits intomainfrom
ntfy
Closed

Ntfy#20
gurkvatten wants to merge 17 commits intomainfrom
ntfy

Conversation

@gurkvatten
Copy link

@gurkvatten gurkvatten commented Nov 14, 2025

Summary by CodeRabbit

  • New Features

    • JavaFX chat app with send/receive, message list and formatted timestamps.
    • New UI layout and styling for the chat experience.
    • Live integration with an external notification service.
  • Chores

    • Replaced legacy "Hello World" example with the chat application.
    • Added environment configuration support and updated build setup.
    • Repository now ignores local environment files.
  • Tests

    • Added unit/integration tests and a test spy for messaging behavior.

@coderabbitai
Copy link

coderabbitai bot commented Nov 14, 2025

Walkthrough

Replaces the HelloFX example with a chat application: new JavaFX entrypoint and controller, chat model and message types, Ntfy HTTP connection and DTOs, FXML/CSS UI, module/dependency updates, tests and a test spy; hello-related files removed and .env added to .gitignore.

Changes

Cohort / File(s) Summary
Project Configuration
\.gitignore, pom.xml, src/main/java/module-info.java
Added .env to .gitignore; updated pom.xml with JavaFX, dotenv-java, jackson-databind, test libs (WireMock, Awaitility, TestFX, slf4j-simple) and changed javafx-maven-plugin mainClass to com.example.ChatApplication; renamed module to chatapp and added requires for Jackson annotations, Dotenv, tools.jackson.databind, and java.net.http.
Chat Application Core
src/main/java/com/example/ChatApplication.java, src/main/java/com/example/ChatController.java, src/main/java/com/example/ChatModel.java, src/main/java/com/example/ChatMessage.java
Added JavaFX entrypoint ChatApplication; ChatController with UI bindings, custom cell formatting and send action; ChatModel managing an observable list, delegating send/receive to NtfyConnection and appending messages (using JavaFX threading); and ChatMessage record (content + timestamp).
Ntfy Integration
src/main/java/com/example/NtfyConnection.java, src/main/java/com/example/NtfyConnectionImpl.java, src/main/java/com/example/NtfyMessageDto.java
Added NtfyConnection interface (send, receive); implemented NtfyConnectionImpl using Dotenv for host resolution and java.net.http async POST/GET streaming with Jackson mapping and logging; added NtfyMessageDto record annotated with @JsonIgnoreProperties.
UI Resources
src/main/resources/com/example/chat-view.fxml, src/main/resources/com/example/chat-style.css
Added FXML chat layout (ListView, input TextField, send Button) and CSS styling for the chat UI.
Legacy Removal
src/main/java/com/example/HelloFX.java, src/main/java/com/example/HelloController.java, src/main/java/com/example/HelloModel.java, src/main/resources/com/example/hello-view.fxml
Removed the Hello-world application files and view.
Tests & Test Utilities
src/test/java/com/example/NtfyConnectionSpy.java, src/test/java/com/example/ChatModelTest.java
Added NtfyConnectionSpy test double (captures sent messages, can simulate incoming messages); added ChatModelTest using WireMock, Awaitility and a spy to verify send/receive behavior and HTTP POST interactions.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant App as ChatApplication
    participant Ctrl as ChatController
    participant Model as ChatModel
    participant Conn as NtfyConnectionImpl
    participant Server as Ntfy HTTP

    User->>App: launch()
    App->>Conn: new NtfyConnectionImpl()
    App->>Model: new ChatModel(Conn)
    App->>Ctrl: FXMLLoader loads view + controller(Model)
    App->>Model: startReceiving()
    Model->>Conn: receive(handler)
    Conn->>Server: GET /mytopic/json (async stream)

    User->>Ctrl: enter text & click Send
    Ctrl->>Model: sendMessage(text)
    Model->>Conn: send(text)
    Conn->>Server: POST /mytopic

    Server-->>Conn: stream message lines
    Conn->>Model: handler.accept(NtfyMessageDto)
    Model->>Model: Platform.runLater -> append message
    Model->>Ctrl: observable list updated
    Ctrl-->>User: UI updates with new message
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

  • Pay extra attention to:
    • NtfyConnectionImpl: async HTTP streaming, JSON parsing, error handling and resource lifecycle.
    • ChatModel: concurrency and correct use of Platform.runLater when mutating observable lists.
    • ChatController + FXML: fx:id/controller wiring and cell rendering.
    • module-info.java and pom.xml: module requires vs. dependencies consistency and plugin mainClass.
    • Tests: WireMock dynamic port usage and JavaFX platform initialization.

Poem

🐰 I hopped from Hello to a chatty tree,
Messages fluttered in just for me.
Streams of JSON, a button that sends,
Tests and styles stitched by rabbit friends.
Hooray — new hops, new chats, new glee!

Pre-merge checks and finishing touches

❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
Title check ❓ Inconclusive The PR title 'Ntfy' is too vague and generic. While 'Ntfy' appears in the codebase (NtfyConnection, NtfyConnectionImpl), the title does not clearly convey the substantial changes being made, such as replacing the Hello MVC demo with a chat application using Ntfy integration. Provide a more descriptive title that summarizes the main change, such as 'Add Ntfy chat application' or 'Replace Hello demo with Ntfy-based chat app'.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🧹 Nitpick comments (12)
src/main/java/com/example/NtfyConnection.java (1)

7-9: Consider removing redundant public modifiers.

Interface methods are implicitly public in Java. Modern style typically omits the redundant modifier for cleaner code.

Apply this diff:

-    public boolean send(String message);
+    boolean send(String message);

-    public void receive(Consumer<NtfyMessageDto> messageHandler);
+    void receive(Consumer<NtfyMessageDto> messageHandler);
src/main/resources/com/example/chat-view.fxml (2)

2-3: Remove unused imports.

StackPane and Label are imported but not used in the layout.

Apply this diff:

-<?import javafx.scene.layout.StackPane?>
-<?import javafx.scene.control.Label?>
 <?import javafx.scene.layout.VBox?>

21-22: Consider internationalization for UI strings.

The UI text is hardcoded in Swedish ("Skriv ditt meddelande...", "Skicka"). For better maintainability and potential localization, consider using resource bundles or properties files.

src/main/java/com/example/ChatApplication.java (1)

19-31: Consider adding lifecycle cleanup via stop()

If NtfyConnectionImpl starts background threads or holds resources, consider overriding Application.stop() to shut down the connection/model to avoid leaks on application exit.

src/main/java/com/example/ChatController.java (1)

55-63: Optional: surface failures from sendMessage

sendMessageAction trims input and skips empty messages, which is good. If ChatModel.sendMessage/NtfyConnection.send can fail, consider adding some basic feedback (e.g., status label or dialog) so users know when a message was not delivered.

src/test/java/com/example/JavaModelTest.java (1)

14-83: Optional: align file name with test class name

The class is ChatModelTest but the file is JavaModelTest.java. Renaming the file to ChatModelTest.java would better match common Java conventions and make the test easier to find.

src/test/java/com/example/NtfyConnectionSpy.java (1)

8-12: Optional: reduce exposure of mutable spy state

For slightly cleaner test APIs, you could make sentMessage and receivedHandler package‑private or private with accessors, keeping direct mutation internal to the spy while still allowing assertions in tests.

src/main/java/com/example/ChatModel.java (1)

19-34: Guard against multiple startReceiving registrations

Right now, calling startReceiving() more than once would register multiple receive handlers on the underlying connection (depending on its implementation). It’s not a problem with current usage, but guarding against this would make the model more robust.

You could, for example:

 public class ChatModel {
     private final ObservableList<ChatMessage> messages = FXCollections.observableArrayList();

     private final NtfyConnection ntfyConnection;
+    private boolean receivingStarted = false;

@@
     public void startReceiving() {
-
-        ntfyConnection.receive(ntfyDto -> {
+        if (receivingStarted) {
+            return;
+        }
+        receivingStarted = true;
+
+        ntfyConnection.receive(ntfyDto -> {
@@
-            Platform.runLater(() -> {
-                messages.add(chatMsg);
-            });
+            Platform.runLater(() -> messages.add(chatMsg));
         });
     }
 }
src/main/java/com/example/NtfyConnectionImpl.java (4)

20-23: Consider providing a more descriptive error message.

The Objects.requireNonNull will throw a NullPointerException if HOST_NAME is missing from the environment, but the error message won't be clear to developers.

Apply this diff to improve the error message:

     public NtfyConnectionImpl() {
         Dotenv dotenv = Dotenv.load();
-        hostName = Objects.requireNonNull(dotenv.get("HOST_NAME"));
+        hostName = Objects.requireNonNull(dotenv.get("HOST_NAME"), 
+                "HOST_NAME environment variable is required");
     }

25-27: Add null validation for defensive coding.

The constructor accepts a hostName parameter without validation, which could lead to NullPointerException later when constructing URIs.

Apply this diff to add validation:

     public NtfyConnectionImpl(String hostName) {
-        this.hostName = hostName;
+        this.hostName = Objects.requireNonNull(hostName, "hostName cannot be null");
     }

33-33: Remove commented-out code.

The commented header should either be enabled if needed or removed. Keeping commented code reduces readability, and version control can recover it if needed later.


31-35: Consider adding request timeouts.

Neither the send nor receive methods configure a timeout on their HTTP requests. This could lead to indefinitely hanging connections if the server becomes unresponsive.

Consider adding a timeout to the requests:

// For send method:
HttpRequest httpRequest = HttpRequest.newBuilder()
        .POST(HttpRequest.BodyPublishers.ofString(message))
        .uri(URI.create(hostName + "/mytopic"))
        .timeout(Duration.ofSeconds(30))
        .build();

// For receive method:
HttpRequest httpRequest = HttpRequest.newBuilder()
        .GET()
        .uri(URI.create(hostName + "/mytopic/json"))
        .timeout(Duration.ofMinutes(5))  // Longer timeout for streaming
        .build();

You'll need to import java.time.Duration.

Also applies to: 52-55

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 21c51e8 and 40c96db.

📒 Files selected for processing (18)
  • .gitignore (1 hunks)
  • pom.xml (3 hunks)
  • src/main/java/com/example/ChatApplication.java (1 hunks)
  • src/main/java/com/example/ChatController.java (1 hunks)
  • src/main/java/com/example/ChatMessage.java (1 hunks)
  • src/main/java/com/example/ChatModel.java (1 hunks)
  • src/main/java/com/example/HelloController.java (0 hunks)
  • src/main/java/com/example/HelloFX.java (0 hunks)
  • src/main/java/com/example/HelloModel.java (0 hunks)
  • src/main/java/com/example/NtfyConnection.java (1 hunks)
  • src/main/java/com/example/NtfyConnectionImpl.java (1 hunks)
  • src/main/java/com/example/NtfyMessageDto.java (1 hunks)
  • src/main/java/module-info.java (1 hunks)
  • src/main/resources/com/example/chat-style.css (1 hunks)
  • src/main/resources/com/example/chat-view.fxml (1 hunks)
  • src/main/resources/com/example/hello-view.fxml (0 hunks)
  • src/test/java/com/example/JavaModelTest.java (1 hunks)
  • src/test/java/com/example/NtfyConnectionSpy.java (1 hunks)
💤 Files with no reviewable changes (4)
  • src/main/java/com/example/HelloModel.java
  • src/main/resources/com/example/hello-view.fxml
  • src/main/java/com/example/HelloController.java
  • src/main/java/com/example/HelloFX.java
🧰 Additional context used
🪛 Biome (2.1.2)
src/main/resources/com/example/chat-style.css

[error] 3-3: Unknown property is not allowed.

See CSS Specifications and browser specific properties for more details.
To resolve this issue, replace the unknown property with a valid CSS property.

(lint/correctness/noUnknownProperty)


[error] 4-4: Unknown property is not allowed.

See CSS Specifications and browser specific properties for more details.
To resolve this issue, replace the unknown property with a valid CSS property.

(lint/correctness/noUnknownProperty)


[error] 5-5: Unknown property is not allowed.

See CSS Specifications and browser specific properties for more details.
To resolve this issue, replace the unknown property with a valid CSS property.

(lint/correctness/noUnknownProperty)


[error] 6-6: Unknown property is not allowed.

See CSS Specifications and browser specific properties for more details.
To resolve this issue, replace the unknown property with a valid CSS property.

(lint/correctness/noUnknownProperty)


[error] 7-7: Unknown property is not allowed.

See CSS Specifications and browser specific properties for more details.
To resolve this issue, replace the unknown property with a valid CSS property.

(lint/correctness/noUnknownProperty)


[error] 8-8: Unknown property is not allowed.

See CSS Specifications and browser specific properties for more details.
To resolve this issue, replace the unknown property with a valid CSS property.

(lint/correctness/noUnknownProperty)


[error] 9-9: Unknown property is not allowed.

See CSS Specifications and browser specific properties for more details.
To resolve this issue, replace the unknown property with a valid CSS property.

(lint/correctness/noUnknownProperty)


[error] 13-13: Unknown property is not allowed.

See CSS Specifications and browser specific properties for more details.
To resolve this issue, replace the unknown property with a valid CSS property.

(lint/correctness/noUnknownProperty)


[error] 14-14: Unknown property is not allowed.

See CSS Specifications and browser specific properties for more details.
To resolve this issue, replace the unknown property with a valid CSS property.

(lint/correctness/noUnknownProperty)


[error] 18-18: Unknown property is not allowed.

See CSS Specifications and browser specific properties for more details.
To resolve this issue, replace the unknown property with a valid CSS property.

(lint/correctness/noUnknownProperty)


[error] 19-19: Unknown property is not allowed.

See CSS Specifications and browser specific properties for more details.
To resolve this issue, replace the unknown property with a valid CSS property.

(lint/correctness/noUnknownProperty)


[error] 20-20: Unknown property is not allowed.

See CSS Specifications and browser specific properties for more details.
To resolve this issue, replace the unknown property with a valid CSS property.

(lint/correctness/noUnknownProperty)


[error] 21-21: Unknown property is not allowed.

See CSS Specifications and browser specific properties for more details.
To resolve this issue, replace the unknown property with a valid CSS property.

(lint/correctness/noUnknownProperty)


[error] 22-22: Unknown property is not allowed.

See CSS Specifications and browser specific properties for more details.
To resolve this issue, replace the unknown property with a valid CSS property.

(lint/correctness/noUnknownProperty)


[error] 23-23: Unknown property is not allowed.

See CSS Specifications and browser specific properties for more details.
To resolve this issue, replace the unknown property with a valid CSS property.

(lint/correctness/noUnknownProperty)


[error] 27-27: Unknown property is not allowed.

See CSS Specifications and browser specific properties for more details.
To resolve this issue, replace the unknown property with a valid CSS property.

(lint/correctness/noUnknownProperty)


[error] 28-28: Unknown property is not allowed.

See CSS Specifications and browser specific properties for more details.
To resolve this issue, replace the unknown property with a valid CSS property.

(lint/correctness/noUnknownProperty)


[error] 29-29: Unknown property is not allowed.

See CSS Specifications and browser specific properties for more details.
To resolve this issue, replace the unknown property with a valid CSS property.

(lint/correctness/noUnknownProperty)


[error] 30-30: Unknown property is not allowed.

See CSS Specifications and browser specific properties for more details.
To resolve this issue, replace the unknown property with a valid CSS property.

(lint/correctness/noUnknownProperty)


[error] 31-31: Unknown property is not allowed.

See CSS Specifications and browser specific properties for more details.
To resolve this issue, replace the unknown property with a valid CSS property.

(lint/correctness/noUnknownProperty)


[error] 32-32: Unknown property is not allowed.

See CSS Specifications and browser specific properties for more details.
To resolve this issue, replace the unknown property with a valid CSS property.

(lint/correctness/noUnknownProperty)


[error] 33-33: Unknown property is not allowed.

See CSS Specifications and browser specific properties for more details.
To resolve this issue, replace the unknown property with a valid CSS property.

(lint/correctness/noUnknownProperty)


[error] 37-37: Unknown property is not allowed.

See CSS Specifications and browser specific properties for more details.
To resolve this issue, replace the unknown property with a valid CSS property.

(lint/correctness/noUnknownProperty)


[error] 38-38: Unknown property is not allowed.

See CSS Specifications and browser specific properties for more details.
To resolve this issue, replace the unknown property with a valid CSS property.

(lint/correctness/noUnknownProperty)


[error] 39-39: Unknown property is not allowed.

See CSS Specifications and browser specific properties for more details.
To resolve this issue, replace the unknown property with a valid CSS property.

(lint/correctness/noUnknownProperty)


[error] 40-40: Unknown property is not allowed.

See CSS Specifications and browser specific properties for more details.
To resolve this issue, replace the unknown property with a valid CSS property.

(lint/correctness/noUnknownProperty)


[error] 41-41: Unknown property is not allowed.

See CSS Specifications and browser specific properties for more details.
To resolve this issue, replace the unknown property with a valid CSS property.

(lint/correctness/noUnknownProperty)


[error] 45-45: Unknown property is not allowed.

See CSS Specifications and browser specific properties for more details.
To resolve this issue, replace the unknown property with a valid CSS property.

(lint/correctness/noUnknownProperty)


[error] 45-45: Unexpected unknown function: derive

Use a known function instead.
See MDN web docs for more details.

(lint/correctness/noUnknownFunction)

🔇 Additional comments (11)
.gitignore (1)

3-3: LGTM! Good security practice.

Adding .env to the ignore list prevents accidental commits of environment variables and credentials, which aligns with the dotenv-java dependency introduced in this PR.

src/main/java/com/example/ChatMessage.java (1)

3-4: LGTM! Clean record definition.

The record provides a simple, immutable representation of a chat message with content and timestamp.

src/main/resources/com/example/chat-style.css (1)

1-46: LGTM! Static analysis warnings are false positives.

The Biome linter is flagging JavaFX-specific CSS properties and functions as unknown because it's designed for standard web CSS. All the -fx- prefixed properties (e.g., -fx-background-color, -fx-text-fill) and the derive() function are valid JavaFX CSS extensions. The stylesheet is correctly structured for JavaFX applications.

src/main/java/com/example/NtfyMessageDto.java (1)

5-7: LGTM! Good use of Jackson annotation.

The @JsonIgnoreProperties(ignoreUnknown = true) annotation provides resilience against API changes by allowing the DTO to ignore unexpected fields during deserialization.

pom.xml (2)

78-78: LGTM! Main class updated correctly.

The main class has been appropriately changed from com.example.HelloFX to com.example.ChatApplication to match the new chat application structure.


53-57: ****

The review comment is based on incorrect information. The groupId tools.jackson.core is correct for Jackson 3.x, and version 3.0.1 exists as a valid Jackson release. The pom.xml dependency is already properly configured. While you could update to version 3.0.2 (the latest stable), downgrading to 2.18.2 as suggested would unnecessarily change from Jackson 3.x to 2.x.

Likely an incorrect or invalid review comment.

src/main/java/com/example/ChatApplication.java (1)

16-30: Clean wiring of model, controller, and FXML

The application bootstraps NtfyConnectionImpl, ChatModel, and ChatController cleanly, starts receiving before showing the stage, and loads chat-view.fxml from the correct package context. Assuming chat-view.fxml does not declare its own fx:controller, this setup is solid.

src/main/java/com/example/ChatController.java (1)

32-52: Initialization and message rendering look solid

Binding the model’s observable list to messageListView and using a custom ListCell with Instant.ofEpochSecond(msg.timestamp()) plus a fixed HH:mm:ss formatter yields clear, readable messages. Wiring both the text field and button to the same send handler keeps the UX consistent.

src/test/java/com/example/JavaModelTest.java (1)

16-64: Good coverage of send/receive behavior in ChatModel

The three tests nicely cover:

  • Outgoing messages via NtfyConnectionSpy.
  • Incoming messages mapped into ChatMessage and added to the observable list, with JavaFX updates flushed before assertions.
  • Integration with the real NtfyConnectionImpl against a fake HTTP server.

This gives solid confidence in the model–connection contract.

src/test/java/com/example/NtfyConnectionSpy.java (1)

5-33: Spy implementation is clear and effective

NtfyConnectionSpy captures sent messages and allows tests to simulate incoming messages via a synthetic NtfyMessageDto, which matches how ChatModel consumes the DTO. This is a straightforward and effective test double for the current tests.

src/main/java/com/example/ChatModel.java (1)

8-34: Model behavior and FX-thread updates are correct

ChatModel cleanly wraps the ObservableList<ChatMessage>, delegates sending to NtfyConnection, and uses Platform.runLater to add incoming messages on the JavaFX Application Thread. The DTO → ChatMessage mapping (message/time) matches how the controller and tests use these values.

Comment on lines +58 to +69
<dependency>
<groupId>org.wiremock</groupId>
<artifactId>wiremock</artifactId>
<version>4.0.0-beta.15</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testfx</groupId>
<artifactId>testfx-core</artifactId>
<version>4.0.16-alpha</version>
<scope>test</scope>
</dependency>
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

Pre-release versions in test dependencies.

Both WireMock (4.0.0-beta.15) and TestFX (4.0.16-alpha) are using pre-release versions. While acceptable for test dependencies, ensure these versions are stable enough for your testing needs.


🌐 Web query:

Are WireMock 4.0.0-beta.15 and TestFX 4.0.16-alpha stable for testing?

💡 Result:

Short answer: No — both are pre-release and should be used with caution, not considered “stable” for critical/testing without verification.

  • WireMock 4.0.0-beta.15 is explicitly a 4.x beta (WireMock v4 is still labelled beta and may contain breaking/unstable changes; Java 17 is the baseline). If you need stability, the WireMock 3.x line is the production release. [1][2]

  • TestFX 4.0.16-alpha is an alpha (pre‑release) artifact from 2019 and has known compatibility issues with newer Java/JavaFX releases; newer TestFX builds (e.g., 4.0.18) exist. TestFX alphas frequently require careful Java/JavaFX version matching and may need workarounds. [3][4]

If you want actionable guidance: use WireMock 3.x for stable CI tests or accept beta risk and pin Java 17 for v4 beta; for UI tests prefer a maintained TestFX release (or verify your exact JDK/OpenJFX/TestFX combination in CI) before relying on 4.0.16-alpha. References: WireMock v4 beta docs and downloads, TestFX Maven listing and GitHub issue describing JavaFX compatibility problems. [1][2][3][4]


Replace pre-release test dependencies with stable versions.

Verification confirms both are problematic for testing:

  • WireMock 4.0.0-beta.15: Beta version with no stability guarantee; use WireMock 3.x (stable production release) instead.
  • TestFX 4.0.16-alpha: Alpha from 2019 with known Java/JavaFX compatibility issues; upgrade to TestFX 4.0.18 or newer.

Consider pinning stable versions in pom.xml lines 58–69 to avoid CI failures or unexpected behavior during test execution.

🤖 Prompt for AI Agents
In pom.xml around lines 58–69, the test dependencies use pre-release versions;
change org.wiremock:wiremock from 4.0.0-beta.15 to a stable 3.x release (e.g.,
3.0.0 or the latest 3.x available) and change org.testfx:testfx-core from
4.0.16-alpha to 4.0.18 or newer, keep scope as test; update the version tags
accordingly, save pom.xml, then run a clean build (mvn -U test) to verify tests
and CI pass.

Comment on lines +50 to +64
@Override
public void receive(Consumer<NtfyMessageDto> messageHandler) {
HttpRequest httpRequest = HttpRequest.newBuilder()
.GET()
.uri(URI.create(hostName + "/mytopic/json"))
.build();

http.sendAsync(httpRequest, HttpResponse.BodyHandlers.ofLines())
.thenAccept(response -> response.body()
.map(s ->
mapper.readValue(s, NtfyMessageDto.class))
.filter(message -> message.event().equals("message"))
.peek(System.out::println)
.forEach(messageHandler));
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Fix compilation error and add error handling.

This code has a critical compilation issue and lacks error handling:

  1. Critical: Line 60 - mapper.readValue(s, NtfyMessageDto.class) throws JsonProcessingException (a checked exception), which cannot be used directly in a Stream's map operation. This should not compile.

  2. Major: No error handling for the async HTTP request failure.

  3. Major: No error handling for JSON parsing failures in the stream.

Apply this diff to fix the compilation error and add proper error handling:

     @Override
     public void receive(Consumer<NtfyMessageDto> messageHandler) {
         HttpRequest httpRequest = HttpRequest.newBuilder()
                 .GET()
                 .uri(URI.create(hostName + "/mytopic/json"))
                 .build();
 
         http.sendAsync(httpRequest, HttpResponse.BodyHandlers.ofLines())
                 .thenAccept(response -> response.body()
-                        .map(s ->
-                                mapper.readValue(s, NtfyMessageDto.class))
+                        .map(s -> {
+                            try {
+                                return mapper.readValue(s, NtfyMessageDto.class);
+                            } catch (IOException e) {
+                                System.err.println("Error parsing message: " + e.getMessage());
+                                return null;
+                            }
+                        })
+                        .filter(Objects::nonNull)
                         .filter(message -> message.event().equals("message"))
                         .peek(System.out::println)
-                        .forEach(messageHandler));
+                        .forEach(messageHandler))
+                .exceptionally(throwable -> {
+                    System.err.println("Error receiving messages: " + throwable.getMessage());
+                    return null;
+                });
     }

requires javafx.fxml;
requires com.fasterxml.jackson.annotation;
requires io.github.cdimascio.dotenv.java;
requires tools.jackson.databind;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Incorrect Jackson module name.

The module name tools.jackson.databind doesn't match standard Jackson naming conventions. Jackson modules typically use com.fasterxml.jackson.* naming. This aligns with the incorrect groupId tools.jackson.core used in pom.xml (line 54).

Apply this diff to use the correct module name:

-    requires tools.jackson.databind;
+    requires com.fasterxml.jackson.databind;

Note: This change must be coordinated with fixing the dependency in pom.xml. Run the following script to verify Jackson usage in the codebase:


🏁 Script executed:

#!/bin/bash
# Verify Jackson module usage and check for standard Jackson dependencies

echo "=== Checking for Jackson imports in source files ==="
rg -n "import com\.fasterxml\.jackson" --type java

echo -e "\n=== Checking pom.xml for Jackson dependencies ==="
rg -n "jackson" pom.xml

Length of output: 323


Incorrect Jackson module name in module-info.java.

The module name tools.jackson.databind is incorrect and will cause module resolution failures. Source code correctly imports from com.fasterxml.jackson.*, which requires the standard Jackson module name com.fasterxml.jackson.databind in the module declaration. This also needs to be coordinated with fixing the dependency in pom.xml (line 54) from the incorrect groupId tools.jackson.core to com.fasterxml.jackson.core.

Apply this change:

-    requires tools.jackson.databind;
+    requires com.fasterxml.jackson.databind;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
requires tools.jackson.databind;
requires com.fasterxml.jackson.databind;
🤖 Prompt for AI Agents
In src/main/java/module-info.java at line 6, replace the incorrect module
declaration `requires tools.jackson.databind;` with the correct standard Jackson
module `requires com.fasterxml.jackson.databind;` to match your imports; also
update pom.xml at line 54 to change the groupId from `tools.jackson.core` to
`com.fasterxml.jackson.core` so the module name and Maven dependency align and
module resolution succeeds.

<?import javafx.scene.control.ListView?>
<?import javafx.geometry.Insets?>
<?import java.net.URL?>
<VBox xmlns="http://javafx.com/javafx" xmlns:fx="http://javafx.com/fxml" >
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Missing controller attribute in FXML root element.

The VBox root element doesn't specify which controller class to use. Based on the AI summary, this FXML should be bound to ChatController.

Apply this diff to add the controller:

-<VBox xmlns="http://javafx.com/javafx" xmlns:fx="http://javafx.com/fxml" >
+<VBox xmlns="http://javafx.com/javafx" xmlns:fx="http://javafx.com/fxml" fx:controller="com.example.ChatController">
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<VBox xmlns="http://javafx.com/javafx" xmlns:fx="http://javafx.com/fxml" >
<VBox xmlns="http://javafx.com/javafx" xmlns:fx="http://javafx.com/fxml" fx:controller="com.example.ChatController">
🤖 Prompt for AI Agents
In src/main/resources/com/example/chat-view.fxml around line 12, the root <VBox>
element is missing the fx:controller attribute that should bind it to the
ChatController; add an fx:controller="com.example.ChatController" attribute to
the root VBox (using the controller's fully-qualified class name), ensure the
ChatController class exists in that package and is accessible to the FXML
loader, and update any package name if your controller resides elsewhere.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (3)
src/test/java/com/example/ChatModelTest.java (3)

16-23: Add a comment to the empty catch block.

The empty catch block silently swallows IllegalStateException without any explanation. While this is likely intentional (to handle cases where the JavaFX platform is already initialized), it makes the code harder to understand and debug.

Apply this diff to add a clarifying comment:

         } catch (IllegalStateException e) {
-
+            // Platform already started, safe to ignore
         }

52-58: Consider adding clarity and safety improvements.

Two minor observations:

  1. Line 52: The division by 1000 (converting milliseconds to seconds) is not explained. A brief comment would clarify the intent.
  2. Line 58: waitForFxEvents() has no explicit timeout. If JavaFX events don't complete, this could hang indefinitely.

Consider adding a comment for clarity:

-        long testTimestamp = System.currentTimeMillis() / 1000;
+        long testTimestamp = System.currentTimeMillis() / 1000; // Convert to seconds

For the timeout concern, verify that the default timeout behavior of WaitForAsyncUtils is sufficient for your use case, or consider using an overload with an explicit timeout if available.


26-83: Consider adding error case tests.

The current test suite covers happy path scenarios well, but lacks coverage for error conditions such as:

  • Sending null or empty messages
  • Network failures or connection timeouts
  • Invalid timestamps or malformed messages
  • Exception handling in async operations

Adding these tests would improve confidence in error handling and edge case behavior.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 40c96db and 3aeaad1.

📒 Files selected for processing (1)
  • src/test/java/com/example/ChatModelTest.java (1 hunks)

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (1)
src/main/java/module-info.java (1)

1-7: Fix incorrect Jackson module name (tools.jackson.databind).

Line 6 still uses requires tools.jackson.databind;, which is not a valid Jackson JPMS module and will cause module resolution failures. For jackson-databind you should require com.fasterxml.jackson.databind instead; this also needs to line up with the Maven dependency’s groupId.

Apply this change:

-    requires tools.jackson.databind;
+    requires com.fasterxml.jackson.databind;

Also double‑check that your pom.xml uses the standard Jackson coordinates (e.g. com.fasterxml.jackson.core:jackson-databind) so the module name and dependency are consistent.

🧹 Nitpick comments (2)
src/test/java/com/example/ChatModelTest.java (2)

15-22: Clarify why IllegalStateException is swallowed in JavaFX startup.

The static Platform.startup block is a common pattern, but the empty catch makes it unclear that the only expected failure is “Platform already started”. Consider adding a brief comment or minimal logging to document that this exception is intentionally ignored so future refactors don’t mask a different IllegalStateException.


65-79: Avoid Thread.sleep in HTTP/WireMock test if possible.

sendMessageToFakeServer_viaNtfyConnectionImpl works, but relying on Thread.sleep(500) makes the test slower and potentially flaky if timing changes (especially if sendMessage ever performs slower async I/O).

If NtfyConnectionImpl.sendMessage is synchronous, you can drop the sleep entirely. If it’s async, prefer a more deterministic wait (e.g., waiting on a CompletableFuture returned by the client or using a small polling loop that times out) instead of a fixed delay.

The WireMock stubbing and verification of the POST to /mytopic with the expected body look correct.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3aeaad1 and d3464f7.

📒 Files selected for processing (3)
  • src/main/java/com/example/NtfyConnectionImpl.java (1 hunks)
  • src/main/java/module-info.java (1 hunks)
  • src/test/java/com/example/ChatModelTest.java (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/main/java/com/example/NtfyConnectionImpl.java
🔇 Additional comments (3)
src/test/java/com/example/ChatModelTest.java (3)

1-14: Imports and WireMock setup look consistent with test usage.

Package, imports, and @WireMockTest annotation align with the tests below and keep concerns (spy vs WireMock) nicely separated.


25-38: Spy-based sendMessage test looks good.

The sendMessage_callsConnectionWithCorrectMessage test cleanly verifies that ChatModel.sendMessage forwards the exact text to NtfyConnectionSpy and keeps the setup/assertion focused on behavior rather than implementation details.


41-63: Receive-side test correctly waits for FX events and asserts state.

receiveMessage_addsToObservableList starts receiving, simulates an incoming message, waits for FX events, and then asserts both list size and the message’s content/timestamp. This gives good coverage of the asynchronous receive path.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (1)
src/main/java/com/example/NtfyConnectionImpl.java (1)

69-81: Add error handling for the async request.

The receive method lacks an exceptionally handler on the sendAsync call. If the HTTP request fails (network error, connection timeout, etc.), the failure will be silent and the messageHandler won't be notified.

Apply this diff to add error handling:

         http.sendAsync(httpRequest, HttpResponse.BodyHandlers.ofLines())
                 .thenAccept(response -> response.body()
                         .map(s -> {
                             try {
                                 return mapper.readValue(s, NtfyMessageDto.class);
                             } catch (Exception e) {
                                 System.err.println("Failed to parse message: " + e.getMessage());
                                 return null;
                             }
                         })
                         .filter(messageDto -> messageDto != null && messageDto.event().equals("message"))
                         .peek(System.out::println)
-                        .forEach(messageHandler));
+                        .forEach(messageHandler))
+                .exceptionally(e -> {
+                    System.err.println("Error receiving messages: " + e.getMessage());
+                    return null;
+                });
     }
🧹 Nitpick comments (3)
src/main/java/com/example/NtfyConnectionImpl.java (3)

19-35: Consider validating the hostName.

The constructor loads the hostName from environment with a sensible fallback, but doesn't validate that the resulting value is a well-formed URL. If the .env file contains a malformed URL, the error will only surface when send() or receive() is called.

You could add basic validation after line 30:

if (this.hostName == null || this.hostName.isEmpty()) {
    throw new IllegalArgumentException("HOST_NAME cannot be null or empty");
}

62-82: Consider providing a mechanism to cancel/cleanup the receive stream.

The receive method starts a long-running async HTTP stream but provides no way to cancel it or clean up resources. If a client wants to stop receiving messages, they have no clean way to do so, potentially leading to resource leaks.

Consider one of these approaches:

  1. Return a CompletableFuture that can be cancelled:
public CompletableFuture<Void> receive(Consumer<NtfyMessageDto> messageHandler) {
    // ... existing code ...
    return http.sendAsync(httpRequest, HttpResponse.BodyHandlers.ofLines())
            .thenAccept(/* ... */);
}
  1. Add a separate stopReceiving() method that tracks and cancels the ongoing request.

  2. Use a more sophisticated pattern with AutoCloseable to manage the connection lifecycle.


45-45: Consider making the topic name configurable.

Both send and receive methods hardcode the topic name as "/mytopic". If you need to support multiple topics or want to make the application more flexible, consider passing the topic name as a parameter or setting it via a constructor/field.

Also applies to: 66-66

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d3464f7 and 5a6b509.

📒 Files selected for processing (6)
  • pom.xml (3 hunks)
  • src/main/java/com/example/ChatModel.java (1 hunks)
  • src/main/java/com/example/NtfyConnection.java (1 hunks)
  • src/main/java/com/example/NtfyConnectionImpl.java (1 hunks)
  • src/test/java/com/example/ChatModelTest.java (1 hunks)
  • src/test/java/com/example/NtfyConnectionSpy.java (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (4)
  • pom.xml
  • src/main/java/com/example/ChatModel.java
  • src/test/java/com/example/ChatModelTest.java
  • src/test/java/com/example/NtfyConnectionSpy.java
🔇 Additional comments (2)
src/main/java/com/example/NtfyConnection.java (1)

6-9: LGTM! Clean interface design.

The interface appropriately uses CompletableFuture for asynchronous sending and a callback pattern for receiving messages.

src/main/java/com/example/NtfyConnectionImpl.java (1)

41-60: LGTM! Async implementation addresses previous feedback.

The send method correctly uses sendAsync with proper error handling via thenAccept and exceptionally, which prevents blocking the JavaFX UI thread.

return null;
}
})
.filter(messageDto -> messageDto != null && messageDto.event().equals("message"))
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Avoid potential NPE in event comparison.

If messageDto.event() returns null, calling .equals("message") will throw a NullPointerException. Use the safer null-safe comparison pattern.

Apply this diff:

-                        .filter(messageDto -> messageDto != null && messageDto.event().equals("message"))
+                        .filter(messageDto -> messageDto != null && "message".equals(messageDto.event()))
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
.filter(messageDto -> messageDto != null && messageDto.event().equals("message"))
.filter(messageDto -> messageDto != null && "message".equals(messageDto.event()))
🤖 Prompt for AI Agents
In src/main/java/com/example/NtfyConnectionImpl.java around line 79, the filter
currently does messageDto != null && messageDto.event().equals("message") which
can NPE if event() is null; change the comparison to a null-safe form such as
"message".equals(messageDto.event()) or use Objects.equals("message",
messageDto.event()), keeping the existing messageDto != null check (or remove it
if using Objects.equals) so the filter never calls equals on a potentially null
event.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (2)
src/test/java/com/example/ChatModelTest.java (2)

49-50: Replace Swedish comments with English.

Comments should be in English for consistency and accessibility to all contributors.

Apply this diff:

-    // Använd System.currentTimeMillis() om din ChatModel skickar detta;
-    // annars, använd ett statiskt värde för att vara säker.
+    // Use System.currentTimeMillis() if ChatModel sends this;
+    // otherwise, use a static value to be safe.
     long expectedTimestamp = System.currentTimeMillis();

61-61: Replace Swedish comment with English.

Apply this diff:

-    // Awaitility: Väntar aktivt tills listan fylls.
+    // Awaitility: Actively waits until the list is populated.
     Awaitility.await()
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5a6b509 and d081f82.

📒 Files selected for processing (3)
  • pom.xml (4 hunks)
  • src/main/java/com/example/ChatModel.java (1 hunks)
  • src/test/java/com/example/ChatModelTest.java (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • pom.xml
🔇 Additional comments (4)
src/test/java/com/example/ChatModelTest.java (2)

16-25: LGTM!

Clean unit test that verifies the delegation pattern using a test spy.


68-68: No compatibility issue—project targets Java 25.

The project is configured with <maven.compiler.release>25</maven.compiler.release>, which exceeds the Java 21+ requirement for getLast(). The code will compile and run without errors.

src/main/java/com/example/ChatModel.java (2)

13-18: LGTM!

Constructor and getter are straightforward. The exposed ObservableList is appropriate for JavaFX data binding.


20-22: LGTM!

Clean delegation to the connection layer.

Comment on lines +24 to +30
public void startReceiving() {
ntfyConnection.receive(ntfyDto -> {
ChatMessage chatMsg = new ChatMessage(ntfyDto.message(), ntfyDto.time());

messages.add(chatMsg);
});
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Critical: JavaFX threading violation - ObservableList modified from background thread.

Line 28 modifies the JavaFX ObservableList directly from the ntfyConnection.receive() callback, which almost certainly runs on a background thread (network I/O). This will cause IllegalStateException at runtime or unpredictable behavior. All JavaFX observable collection modifications must occur on the JavaFX Application Thread.

You've imported Platform but haven't used it—wrap the modification in Platform.runLater().

Apply this diff to fix the threading issue:

     public void startReceiving() {
         ntfyConnection.receive(ntfyDto -> {
             ChatMessage chatMsg = new ChatMessage(ntfyDto.message(), ntfyDto.time());
-
-            messages.add(chatMsg);
+            Platform.runLater(() -> messages.add(chatMsg));
         });
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public void startReceiving() {
ntfyConnection.receive(ntfyDto -> {
ChatMessage chatMsg = new ChatMessage(ntfyDto.message(), ntfyDto.time());
messages.add(chatMsg);
});
}
public void startReceiving() {
ntfyConnection.receive(ntfyDto -> {
ChatMessage chatMsg = new ChatMessage(ntfyDto.message(), ntfyDto.time());
Platform.runLater(() -> messages.add(chatMsg));
});
}
🤖 Prompt for AI Agents
In src/main/java/com/example/ChatModel.java around lines 24 to 30, the code adds
to the JavaFX ObservableList from the ntfyConnection.receive callback (a
background thread); wrap the modification in Platform.runLater so the
messages.add(...) runs on the JavaFX Application Thread (e.g., create the
ChatMessage from ntfyDto on the callback, then call Platform.runLater(() ->
messages.add(chatMsg))); ensure javafx.application.Platform is imported (it
already is) and do not perform any other UI/ObservableList mutations off the FX
thread.

Comment on lines +27 to +41
@Test
void sendMessageToFakeServer_viaNtfyConnectionImpl(WireMockRuntimeInfo wmRunTimeInfo) throws InterruptedException {
var con = new NtfyConnectionImpl("http://localhost:" + wmRunTimeInfo.getHttpPort());
var model = new ChatModel(con);
String messageToSend = "Test Message 123";

stubFor(post("/mytopic").willReturn(ok()));

model.sendMessage(messageToSend);

Thread.sleep(500);

WireMock.verify(postRequestedFor(urlEqualTo("/mytopic"))
.withRequestBody(matching(messageToSend)));
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Replace Thread.sleep with Awaitility for reliable async testing.

Using Thread.sleep(500) makes the test fragile and can lead to flaky failures if the operation takes longer than expected, or waste time if it completes faster.

Apply this diff to use Awaitility (already imported and used in the third test):

     model.sendMessage(messageToSend);

-    Thread.sleep(500);
+    Awaitility.await()
+            .atMost(Duration.ofSeconds(2))
+            .pollInterval(Duration.ofMillis(50))
+            .untilAsserted(() -> {
+                WireMock.verify(postRequestedFor(urlEqualTo("/mytopic"))
+                        .withRequestBody(matching(messageToSend)));
+            });
-
-    WireMock.verify(postRequestedFor(urlEqualTo("/mytopic"))
-            .withRequestBody(matching(messageToSend)));
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@Test
void sendMessageToFakeServer_viaNtfyConnectionImpl(WireMockRuntimeInfo wmRunTimeInfo) throws InterruptedException {
var con = new NtfyConnectionImpl("http://localhost:" + wmRunTimeInfo.getHttpPort());
var model = new ChatModel(con);
String messageToSend = "Test Message 123";
stubFor(post("/mytopic").willReturn(ok()));
model.sendMessage(messageToSend);
Thread.sleep(500);
WireMock.verify(postRequestedFor(urlEqualTo("/mytopic"))
.withRequestBody(matching(messageToSend)));
}
@Test
void sendMessageToFakeServer_viaNtfyConnectionImpl(WireMockRuntimeInfo wmRunTimeInfo) throws InterruptedException {
var con = new NtfyConnectionImpl("http://localhost:" + wmRunTimeInfo.getHttpPort());
var model = new ChatModel(con);
String messageToSend = "Test Message 123";
stubFor(post("/mytopic").willReturn(ok()));
model.sendMessage(messageToSend);
Awaitility.await()
.atMost(Duration.ofSeconds(2))
.pollInterval(Duration.ofMillis(50))
.untilAsserted(() -> {
WireMock.verify(postRequestedFor(urlEqualTo("/mytopic"))
.withRequestBody(matching(messageToSend)));
});
}
🤖 Prompt for AI Agents
In src/test/java/com/example/ChatModelTest.java around lines 27 to 41, replace
the brittle Thread.sleep(500) with an Awaitility wait that repeatedly asserts
the WireMock verification until success or timeout; remove the Thread.sleep call
and wrap the WireMock.verify(...) call in an Awaitility.untilAsserted (or
await().atMost(Duration.ofSeconds(2)).untilAsserted(...)) so the test waits up
to a short timeout for the POST to occur instead of sleeping a fixed time.

@kappsegla kappsegla closed this Nov 19, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants