Skip to content

Lab3#1

Merged
simonforsberg merged 17 commits intomainfrom
lab3
Nov 14, 2025
Merged

Lab3#1
simonforsberg merged 17 commits intomainfrom
lab3

Conversation

@simonforsberg
Copy link
Collaborator

@simonforsberg simonforsberg commented Nov 13, 2025

Summary by CodeRabbit

  • New Features

    • Real-time messaging: connect to a topic, send messages, and receive live incoming messages.
  • UI/UX Updates

    • Rebranded to "JavaFX Chat App 💬", wider window, redesigned chat layout with topic connect control, message input, Send button, and live message list.
  • Tests

    • Added test coverage for send/receive, connect/disconnect, and message handling.
  • Chores

    • .env added to ignored files.

@coderabbitai
Copy link

coderabbitai bot commented Nov 13, 2025

Walkthrough

Adds ntfy-style messaging and a chat UI: new NtfyConnection interface and HTTP implementation, HelloModel with observable messages and send/connect lifecycle, controller and FXML updated for chat, Maven deps (Dotenv, Jackson, WireMock), .env ignored, and new tests (spy/stub + WireMock).

Changes

Cohort / File(s) Change Summary
Build & Modules
​.gitignore, ​pom.xml, ​src/main/java/module-info.java
.env added to .gitignore; pom.xml adds io.github.cdimascio:dotenv-java:3.2.0, tools.jackson.core:jackson-databind:3.0.1, and test-scoped org.wiremock:wiremock:4.0.0-beta.15; module-info.java adds requires for dotenv, java.net.http, and Jackson.
API & DTO
​src/main/java/com/example/NtfyConnection.java, ​src/main/java/com/example/NtfyMessageDto.java
New public NtfyConnection interface with send/receive and nested Subscription; new NtfyMessageDto record annotated with @JsonIgnoreProperties(ignoreUnknown = true) and toString() returning message.
HTTP Implementation
​src/main/java/com/example/NtfyConnectionImpl.java
New NtfyConnectionImpl implementing HTTP POST for send and async long-poll GET for receive; reads HOST_NAME via Dotenv (default ctor or provided), uses Jackson to parse JSON lines, manages HttpClient, background receive task and cancellable Subscription.
Model
​src/main/java/com/example/HelloModel.java
New HelloModel(NtfyConnection) with ObservableList<NtfyMessageDto> messages, StringProperty fields for topic/messageToSend, connection state property, methods sendMessage(), connectToTopic(), disconnect(), and runOnFx helper; greeting logic updated.
Controller & UI
​src/main/java/com/example/HelloController.java, ​src/main/java/com/example/HelloFX.java, ​src/main/resources/com/example/hello-view.fxml
Controller now supports constructor injection, exposes @FXML fields (topicField, inputField, messageView, statusLabel) and handlers connectToTopic(ActionEvent) and sendMessage(ActionEvent); FXML replaced StackPane with VBox chat layout; window title set to "JavaFX Chat App 💬" and scene width changed to 740px.
Tests & Helpers
​src/test/java/com/example/HelloModelTest.java, ​src/test/java/com/example/NtfyConnectionSpy.java, ​src/test/java/com/example/NtfyConnectionStub.java
Added HelloModelTest (WireMock-based tests), NtfyConnectionSpy to capture sends and subscription state, and NtfyConnectionStub to simulate incoming messages for tests.

Sequence Diagram

sequenceDiagram
    participant User
    participant HelloController
    participant HelloModel
    participant NtfyConnectionImpl
    participant Server

    rect rgba(200,230,255,0.18)
    User->>HelloController: enter topic & click Connect
    HelloController->>HelloModel: connectToTopic()
    HelloModel->>NtfyConnectionImpl: receive(topic, handler)
    NtfyConnectionImpl->>Server: GET /{topic}/json (long-poll)
    end

    rect rgba(200,255,200,0.18)
    User->>HelloController: type message & click Send
    HelloController->>HelloModel: sendMessage()
    HelloModel->>NtfyConnectionImpl: send(topic, message)
    NtfyConnectionImpl->>Server: POST /{topic}
    end

    rect rgba(255,230,200,0.14)
    Server-->>NtfyConnectionImpl: stream JSON event lines
    NtfyConnectionImpl->>HelloModel: parsed NtfyMessageDto -> handler
    HelloModel->>HelloController: update ObservableList (FX thread)
    HelloController->>User: UI reflects new message
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

  • Review focus:
    • src/main/java/com/example/NtfyConnectionImpl.java — long-poll lifecycle, JSON parsing, cancellation and InterruptedException handling.
    • src/main/java/com/example/HelloModel.java — FX-thread marshaling (runOnFx), subscription lifecycle, and exception handling in sendMessage().
    • src/main/java/com/example/HelloController.java and FXML — @FXML bindings, constructor injection, and event handler wiring.
    • src/test/java/com/example/HelloModelTest.java — timing-sensitive WireMock assertions and test stability.

Poem

🐇 I hopped into the convo stream,
A topic set, a carrot beam.
I typed a note that crossed the net,
JSON crumbs and a tiny yet—
Hooray, new chats that make me gleam!

Pre-merge checks and finishing touches

❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 74.47% 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 'Lab3' is vague and generic, using a non-descriptive term that doesn't convey meaningful information about the changeset's actual content or primary changes. Replace 'Lab3' with a descriptive title that summarizes the main change, such as 'Implement chat messaging UI with model-controller separation and Ntfy integration' or 'Add JavaFX chat interface with message handling and topic subscriptions'.
✅ 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
  • Commit unit tests in branch lab3

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: 3

🧹 Nitpick comments (4)
src/main/resources/com/example/hello-view.fxml (1)

23-24: Consider using promptText instead of text for better UX.

The topicField has a default value text="mytopic". While this works for demo purposes, using promptText="mytopic" would be more idiomatic—it provides a hint without pre-filling the field.

If you want to keep the default value for convenience, apply this diff:

         <TextField fx:id="topicField"
-                   text="mytopic"
+                   promptText="Enter topic name"
                    HBox.hgrow="ALWAYS" />
pom.xml (1)

58-63: Consider using a stable WireMock release.

The dependency uses a beta version (4.0.0-beta.15). While acceptable for test dependencies, a stable release would be preferable if available.

Check if WireMock 3.x stable versions are sufficient for your needs:

What is the latest stable version of WireMock library?
src/main/java/com/example/NtfyConnection.java (2)

7-9: Remove redundant public modifiers.

Interface methods are implicitly public in Java. The explicit public keyword is redundant and can be removed for cleaner code.

Apply this diff:

-    public boolean send(String topic, String message);
+    boolean send(String topic, String message);
 
-    public void receive(String topic, Consumer<NtfyMessageDto> messageHandler);
+    void receive(String topic, Consumer<NtfyMessageDto> messageHandler);

7-7: Consider exceptions instead of boolean return for send().

The send() method returns boolean for success/failure. Consider throwing checked exceptions for error cases instead—this provides more context about what went wrong (network failure, auth error, etc.).

This is a design choice, but for a network operation, exceptions would provide better error handling:

void send(String topic, String message) throws IOException;

Or if you prefer unchecked exceptions:

void send(String topic, String message) throws NtfyException;
📜 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 e245be3.

📒 Files selected for processing (13)
  • .gitignore (1 hunks)
  • pom.xml (1 hunks)
  • src/main/java/com/example/HelloController.java (1 hunks)
  • src/main/java/com/example/HelloFX.java (1 hunks)
  • src/main/java/com/example/HelloModel.java (2 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/hello-view.fxml (1 hunks)
  • src/test/java/com/example/HelloModelTest.java (1 hunks)
  • src/test/java/com/example/NtfyConnectionSpy.java (1 hunks)
  • src/test/java/com/example/NtfyConnectionStub.java (1 hunks)
🔇 Additional comments (14)
.gitignore (1)

3-3: LGTM!

Adding .env to .gitignore is appropriate for preventing environment configuration files containing sensitive data from being committed to version control.

src/main/resources/com/example/hello-view.fxml (4)

1-6: LGTM!

XML declaration and JavaFX imports are correctly configured for the chat UI layout.


7-14: LGTM!

The VBox root container with center alignment, 15px spacing, and uniform 20px padding provides a clean layout foundation for the chat interface.


26-27: Buttons lack fx:id attributes for programmatic control.

The Connect and Send buttons don't have fx:id attributes. If you need to programmatically disable/enable these buttons (e.g., disable Send until connected), you'll need to add fx:id attributes.

Verify whether the controller needs to reference these buttons at runtime. If so, add fx:id attributes like:

-        <Button text="Connect"
+        <Button fx:id="connectButton"
+                text="Connect"
                 onAction="#connectToTopic" />
-        <Button text="Send"
+        <Button fx:id="sendButton"
+                text="Send"
                 onAction="#sendMessage" />

Also applies to: 34-35


38-39: LGTM!

The ListView with VBox.vgrow="ALWAYS" will correctly expand to fill available vertical space. The default cell factory will use NtfyMessageDto.toString(), which returns the message field—appropriate for display.

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

1-13: LGTM! Confirms Jackson package inconsistency.

The record structure is clean and appropriate for a DTO. The @JsonIgnoreProperties(ignoreUnknown = true) annotation provides flexibility for API evolution. The toString() override returning only the message field is perfect for ListView display.

Note: The import on line 3 uses com.fasterxml.jackson.annotation, confirming that the Jackson dependency in pom.xml should use group ID com.fasterxml.jackson.core, not tools.jackson.core.

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

15-16: LGTM!

The increased window width (740px) accommodates the expanded chat UI, and the updated title with emoji clearly reflects the application's purpose.

src/test/java/com/example/NtfyConnectionStub.java (2)

10-17: Topic parameter is ignored—verify this matches test intent.

Both send() and receive() ignore the topic parameter. The stub stores only a single message handler regardless of topic. This is fine for simple tests but won't validate topic-specific routing.

If tests need to verify topic-based message routing, consider enhancing the stub to track handlers per topic:

private final Map<String, Consumer<NtfyMessageDto>> handlers = new HashMap<>();

@Override
public void receive(String topic, Consumer<NtfyMessageDto> messageHandler) {
    handlers.put(topic, messageHandler);
}

public void simulateIncomingMessage(String topic, NtfyMessageDto message) {
    Consumer<NtfyMessageDto> handler = handlers.get(topic);
    if (handler != null) {
        handler.accept(message);
    }
}

19-23: LGTM!

The null check prevents NPE and the simulation method provides clean test control over incoming messages.

pom.xml (1)

48-52: dotenv-java 3.2.0 is current and secure.

Version 3.2.0 is the latest available as of November 13, 2025, and no known direct security vulnerabilities have been reported for this version. The dependency is stable and no action is required.

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

35-48: End-to-end send coverage looks solid.

The WireMock-backed assertion that the POST body matches the model payload gives strong confidence the HTTP wiring works as expected. Nicely done.

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

33-47: Good job wiring the bindings.

The bidirectional bindings and guarded send handler keep the UI and model in sync without leaking blank messages.

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

10-19: Spy implementation is fit for purpose.

Capturing the last topic/message is all the tests need, and the no-op receive keeps the double lightweight.

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

70-72: UI-thread handoff looks correct.

Wrapping the list mutation in Platform.runLater keeps JavaFX thread rules intact while processing async messages.

Comment on lines +53 to +57
<dependency>
<groupId>tools.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>3.0.1</version>
</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 | 🔴 Critical

Incorrect Jackson group ID.

The group ID tools.jackson.core appears incorrect. The standard Jackson library uses com.fasterxml.jackson.core as the group ID.

Apply this diff to correct the group ID:

         <dependency>
-            <groupId>tools.jackson.core</groupId>
+            <groupId>com.fasterxml.jackson.core</groupId>
             <artifactId>jackson-databind</artifactId>
-            <version>3.0.1</version>
+            <version>2.18.2</version>
         </dependency>

Note: Jackson 3.x is still in development. Version 2.18.2 is the latest stable release from the 2.x series.

📝 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
<dependency>
<groupId>tools.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>3.0.1</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.18.2</version>
</dependency>
🤖 Prompt for AI Agents
In pom.xml around lines 53 to 57, the Jackson dependency uses the wrong groupId;
change the groupId from tools.jackson.core to com.fasterxml.jackson.core and
update the version to a stable 2.x release (e.g., 2.18.2). Ensure the dependency
block reads with the corrected groupId and the recommended version to avoid
using the incorrect artifact coordinates or an unstable 3.x snapshot.

Comment on lines +3 to +20
import io.github.cdimascio.dotenv.Dotenv;
import tools.jackson.databind.ObjectMapper;

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.Objects;
import java.util.concurrent.CompletableFuture;
import java.util.function.Consumer;

public class NtfyConnectionImpl implements NtfyConnection {

private final HttpClient http = HttpClient.newHttpClient();
private final String hostName;
private final ObjectMapper mapper = new ObjectMapper();
private CompletableFuture<Void> receiveTask;
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 the Jackson import to restore compilation.

ObjectMapper lives in com.fasterxml.jackson.databind, not tools.jackson.databind. As written, the class will not compile. Please update the import.

Apply this diff:

-import tools.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.ObjectMapper;
📝 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
import io.github.cdimascio.dotenv.Dotenv;
import tools.jackson.databind.ObjectMapper;
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.Objects;
import java.util.concurrent.CompletableFuture;
import java.util.function.Consumer;
public class NtfyConnectionImpl implements NtfyConnection {
private final HttpClient http = HttpClient.newHttpClient();
private final String hostName;
private final ObjectMapper mapper = new ObjectMapper();
private CompletableFuture<Void> receiveTask;
import io.github.cdimascio.dotenv.Dotenv;
import com.fasterxml.jackson.databind.ObjectMapper;
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.Objects;
import java.util.concurrent.CompletableFuture;
import java.util.function.Consumer;
public class NtfyConnectionImpl implements NtfyConnection {
private final HttpClient http = HttpClient.newHttpClient();
private final String hostName;
private final ObjectMapper mapper = new ObjectMapper();
private CompletableFuture<Void> receiveTask;
🤖 Prompt for AI Agents
In src/main/java/com/example/NtfyConnectionImpl.java around lines 3 to 20 the
Jackson ObjectMapper import is wrong (tools.jackson.databind.ObjectMapper)
causing compilation failure; remove that incorrect import and replace it with
the correct import com.fasterxml.jackson.databind.ObjectMapper, then save and
rebuild to restore compilation.

Comment on lines +4 to +6
requires io.github.cdimascio.dotenv.java;
requires java.net.http;
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

Module name for Jackson is likely incorrect.

Line 6 requires tools.jackson.databind, but this module name is inconsistent with the standard Jackson library module name com.fasterxml.jackson.databind. This issue is related to the incorrect group ID in pom.xml.

After correcting the Jackson dependency in pom.xml, apply this diff:

     requires io.github.cdimascio.dotenv.java;
     requires java.net.http;
-    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 io.github.cdimascio.dotenv.java;
requires java.net.http;
requires tools.jackson.databind;
requires io.github.cdimascio.dotenv.java;
requires java.net.http;
requires com.fasterxml.jackson.databind;
🤖 Prompt for AI Agents
In src/main/java/module-info.java around lines 4 to 6, the Jackson module name
is incorrect (requires tools.jackson.databind); update the pom.xml to use the
official Jackson dependency (e.g., groupId com.fasterxml.jackson.core and
artifactId jackson-databind) and then change the requires line to the correct
module name com.fasterxml.jackson.databind so module-info references the proper
Jackson module.

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 (1)
src/main/java/com/example/HelloModel.java (1)

75-83: Consider alternatives to the test workaround.

The catch block at Lines 79-82 works around uninitialized JavaFX in tests by running tasks inline. While pragmatic for a lab, this changes behavior (synchronous vs asynchronous) and masks architectural concerns.

Better alternatives:

  • Initialize JavaFX in test setup using @ExtendWith(ApplicationExtension.class) (TestFX)
  • Extract a ThreadExecutor interface that can be mocked in tests
  • Use @EnabledIfSystemProperty to skip JavaFX-dependent tests when toolkit isn't available

For a lab assignment, the current approach is acceptable, but consider these patterns for production code.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e245be3 and fc4049a.

📒 Files selected for processing (4)
  • src/main/java/com/example/HelloModel.java (2 hunks)
  • src/main/java/com/example/NtfyConnection.java (1 hunks)
  • src/main/resources/com/example/hello-view.fxml (1 hunks)
  • src/test/java/com/example/HelloModelTest.java (1 hunks)
🔇 Additional comments (6)
src/test/java/com/example/HelloModelTest.java (3)

14-25: LGTM! Clean unit test with spy pattern.

The test clearly verifies the interaction between HelloModel and NtfyConnection using a spy, following the AAA pattern effectively.


27-40: LGTM! Effective integration test with WireMock.

The test properly verifies the HTTP interaction, using WireMock's dynamic port binding and request verification.


64-65: Review comment is incorrect; no race condition exists.

The simulateIncomingMessage() method in NtfyConnectionStub processes messages synchronously by directly calling messageHandler.accept(message). Since message processing completes before the method returns, the assertion at line 65 correctly receives the processed message without requiring additional synchronization.

The Thread.sleep(50) in the earlier test (line 50) is unnecessary given the synchronous nature of message processing, not a requirement that the second test is missing. The tests are correct as written.

Likely an incorrect or invalid review comment.

src/main/resources/com/example/hello-view.fxml (1)

7-40: LGTM! Well-structured chat UI layout.

The FXML properly defines the chat interface with:

  • Appropriate container hierarchy and spacing
  • Correct fx:id declarations for controller binding
  • Proper event handler bindings
  • Layout growth properties for responsive resizing

The inline style at Line 17 is acceptable for this scope, though extracting to an external CSS file would be preferred for larger applications.

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

70-73: Logic is correct, but see concerns about multiple invocations.

The implementation properly uses runOnFx to marshal messages to the JavaFX thread. However, note the critical issues already flagged:

  • This method is called from the constructor (Line 21)
  • It's called from connectToTopic without stopping previous subscriptions (Line 67)

These architectural issues don't stem from this method's implementation itself, but from how it's invoked.


24-50: LGTM! Standard JavaFX property pattern.

The property accessors and getters follow JavaFX conventions correctly, providing both property access and direct getters/setters.

Comment on lines +61 to +63
public void sendMessage() {
connection.send(topic.get(), messageToSend.get());
}
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

Handle the send result and clear the input field.

Two issues at Line 62:

  1. Ignored return value: connection.send() returns a boolean, but the result is ignored. If sending fails, the user receives no feedback.
  2. Input not cleared: After sending, messageToSend is not cleared, requiring manual cleanup by the user.

Apply this diff:

 public void sendMessage() {
-    connection.send(topic.get(), messageToSend.get());
+    boolean success = connection.send(topic.get(), messageToSend.get());
+    if (success) {
+        messageToSend.set("");
+    } else {
+        // TODO: Show error to user (e.g., add error message to messages list)
+    }
 }
🤖 Prompt for AI Agents
In src/main/java/com/example/HelloModel.java around lines 61 to 63, the call to
connection.send(...) currently ignores the boolean return and does not reset the
input; change so you capture the boolean result, check it, and handle failure
(e.g., log or notify the user and do not clear the input on failure), and on
success clear messageToSend (set it to an empty string or null as appropriate
for your binding) so the input field is reset; ensure any UI notification or
logging uses the existing app mechanism (e.g., showToast/notify or logger)
rather than introducing new APIs.

Comment on lines +5 to +10
public interface NtfyConnection {

boolean send(String topic, String message);

void receive(String topic, Consumer<NtfyMessageDto> 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 | 🟠 Major

Add documentation and consider lifecycle management methods.

The interface is missing critical documentation and has potential resource management issues:

  1. No unsubscribe mechanism: Once receive() is called, there's no way to stop the message handler, potentially leaking resources.
  2. Threading model undefined: Which thread invokes the Consumer callback?
  3. Error handling unclear: What if the Consumer throws an exception?
  4. Unclear semantics: Can receive() be called multiple times? Does it replace or add handlers?

Consider:

  • Adding Javadoc to document the contract
  • Adding a method to stop receiving: void unsubscribe(String topic) or void close()
  • Returning a Subscription or Closeable from receive() that can be disposed
  • Documenting the threading model

Example improvement:

/**
 * Connection interface for ntfy-style pub/sub messaging.
 */
public interface NtfyConnection extends AutoCloseable {
    
    /**
     * Sends a message to a topic.
     * @return true if sent successfully
     */
    boolean send(String topic, String message);
    
    /**
     * Subscribes to messages on a topic. The handler is invoked
     * on a background thread when messages arrive.
     * 
     * @return a subscription that can be closed to stop receiving
     */
    Subscription receive(String topic, Consumer<NtfyMessageDto> messageHandler);
    
    @Override
    void close();
}
🤖 Prompt for AI Agents
In src/main/java/com/example/NtfyConnection.java around lines 5-10, the
interface lacks documentation and lifecycle controls: add Javadoc to each method
and to the interface describing semantics (threading model, reentrancy, and
error-handling expectations), change receive(...) to return a disposable
Subscription or Closeable (or make the interface extend AutoCloseable) so
callers can stop receiving, document whether receive replaces or adds handlers
and which thread invokes the Consumer, and specify what happens if the Consumer
throws (e.g., exceptions are caught and logged, handler removed, etc.); update
method signatures and comments accordingly to ensure explicit unsubscribe/close
behavior and clear contract.

Comment on lines +44 to +55
void receiveMessageAddsMessagesToList() throws InterruptedException {
// Arrange - Given
var stub = new NtfyConnectionStub();
var model = new HelloModel(stub);
// Act - When
stub.simulateIncomingMessage(new NtfyMessageDto("1", System.currentTimeMillis(), "message", "mytopic", "Hello world"));
Thread.sleep(50);
// Assert - Then
assertThat(model.getMessages())
.extracting(NtfyMessageDto::message)
.containsExactly("Hello world");
}
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

Replace Thread.sleep with proper synchronization.

Using Thread.sleep(50) at Line 50 makes the test slower and potentially flaky. The hard-coded delay may be insufficient on slower systems or under load, causing intermittent failures.

Consider these alternatives:

  • Use Awaitility to poll with timeout: await().atMost(Duration.ofSeconds(1)).until(() -> model.getMessages().size() == 1)
  • Add a CountDownLatch to NtfyConnectionStub that the test can await
  • Use CompletableFuture to signal message processing completion

Would you like me to provide a concrete implementation using one of these approaches?

🤖 Prompt for AI Agents
In src/test/java/com/example/HelloModelTest.java around lines 44 to 55, replace
the brittle Thread.sleep(50) with a proper synchronization/await strategy: add
Awaitility as a test dependency (or ensure it's already on the classpath),
remove the Thread.sleep call, and replace it with an Awaitility wait such as
await().atMost(Duration.ofSeconds(1)).until(() -> model.getMessages().size() ==
1) (then perform the same extracting/containsExactly assertion). Also add the
necessary static imports (await(), Duration) or use fully-qualified calls; this
eliminates the hard-coded delay and prevents flakiness on slow systems.

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: 5

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

6-11: Lifecycle and documentation issues remain unresolved.

While send() now properly throws IOException (an improvement), the interface still lacks:

  • Documentation describing the contract, threading model, and error handling
  • Unsubscribe/close mechanism for receive() subscriptions
  • Clear semantics for multiple receive() calls

This creates resource leaks in HelloModel.connectToTopic() (see comments on that file) and makes the API difficult to use correctly.

The previous review suggestions remain valid:

  • Add Javadoc to document the contract
  • Add void close() or make the interface extend AutoCloseable
  • Return a Closeable/Subscription from receive() to allow unsubscribing
  • Document the threading model
src/main/java/com/example/NtfyConnectionImpl.java (1)

4-4: Critical: Fix the Jackson import.

The import tools.jackson.databind.ObjectMapper is incorrect and will prevent compilation. It should be com.fasterxml.jackson.databind.ObjectMapper.

-import tools.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.ObjectMapper;
src/main/java/com/example/HelloModel.java (3)

21-24: Constructor calls instance method, causing early escape of this.

Calling receiveMessage() in the constructor at Line 23 allows the this reference to escape before the object is fully initialized, potentially exposing a partially constructed object to other threads via the callback.

The previous review's suggestions remain valid: either require explicit initialization after construction, use a static factory method, or defer callback registration.


67-70: Resource leak: Multiple subscriptions without unsubscribe.

Each call to connectToTopic() invokes receiveMessage(), which calls connection.receive(). Since the NtfyConnection interface provides no unsubscribe mechanism, this creates a resource leak with accumulated handlers and potential connection/thread leaks.

This is an architectural issue that requires fixing the NtfyConnection interface (see comments on that file). Short-term, you could:

  • Track whether receiveMessage() has been called and prevent multiple calls
  • Document that topic changes are not supported

Long-term, update the interface to support unsubscribe or return a closeable subscription.


63-65: Clear the message after successful send.

After successfully sending, messageToSend is not cleared. While the controller clears inputField, the model property remains populated, creating an inconsistency between the view and the model state.

     public void sendMessage() throws IOException {
         connection.send(topic.get(), messageToSend.get());
+        messageToSend.set("");
     }
🧹 Nitpick comments (5)
src/test/java/com/example/NtfyConnectionStub.java (1)

5-23: Consider tracking or validating handler replacement.

The stub allows receive() to be called multiple times, silently replacing the previous handler. While this may be intentional for test flexibility, it could mask bugs in tests where receive() is accidentally called multiple times.

Consider one of these approaches:

  • Throw an exception if receive() is called when a handler is already set
  • Store a list of handlers if multiple subscriptions should be supported
  • Document that calling receive() multiple times is expected behavior
src/main/java/com/example/HelloController.java (1)

41-41: Consider deferring initial connection.

Calling model.connectToTopic() during initialization triggers network activity before the UI is fully displayed. If the connection fails or is slow, it may impact the user experience.

Consider moving the initial connection to a post-initialization phase or making it user-triggered.

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

62-62: Replace System.err with proper logging.

Error messages are printed to System.err, which is not appropriate for production code. Use a proper logging framework like SLF4J or java.util.logging.

-                                System.err.println("Failed to parse message: " + e.getMessage());
+                                logger.error("Failed to parse message", e);

Add a logger field at the top of the class:

private static final Logger logger = LoggerFactory.getLogger(NtfyConnectionImpl.class);

51-54: Add timeout to prevent indefinite blocking.

The HTTP request has no timeout configured. For a long-polling connection, this could hang indefinitely if the server becomes unresponsive.

         HttpRequest httpRequest = HttpRequest.newBuilder()
                 .GET()
+                .timeout(Duration.ofMinutes(5))
                 .uri(URI.create(hostName + "/" + topic + "/json"))
                 .build();

Add the import:

import java.time.Duration;

47-50: Consider graceful shutdown of previous subscription.

The method cancels the previous receiveTask with cancel(true), which interrupts the thread. While this works, it may cause InterruptedException to be thrown while processing messages.

Consider a more graceful approach if this becomes an issue:

  • Track a cancellation flag
  • Check the flag in the stream processing
  • Allow in-flight messages to complete
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between fc4049a and 200137e.

📒 Files selected for processing (7)
  • src/main/java/com/example/HelloController.java (1 hunks)
  • src/main/java/com/example/HelloModel.java (2 hunks)
  • src/main/java/com/example/NtfyConnection.java (1 hunks)
  • src/main/java/com/example/NtfyConnectionImpl.java (1 hunks)
  • src/test/java/com/example/HelloModelTest.java (1 hunks)
  • src/test/java/com/example/NtfyConnectionSpy.java (1 hunks)
  • src/test/java/com/example/NtfyConnectionStub.java (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/test/java/com/example/HelloModelTest.java
  • src/test/java/com/example/NtfyConnectionSpy.java
🔇 Additional comments (4)
src/main/java/com/example/NtfyConnectionImpl.java (2)

22-25: Good use of null validation.

The constructor properly validates that HOST_NAME is present in the environment, failing fast if misconfigured.


35-35: No changes needed.

The header Cache: no is valid and documented for the ntfy.sh API—it disables server-side caching while still delivering messages to connected subscribers. The implementation is correct as-is.

Likely an incorrect or invalid review comment.

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

77-85: Excellent thread-safety helper.

The runOnFx() method properly handles JavaFX threading requirements and gracefully degrades for unit tests where the JavaFX toolkit isn't initialized.


72-75: Good use of thread-safe wrapper for callbacks.

Wrapping the message handler with runOnFx() ensures that UI updates happen on the JavaFX application thread, preventing threading violations.

Comment on lines +56 to +69
receiveTask = 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(Objects::nonNull)
.filter(message -> "message".equals(message.event()))
.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 | 🟠 Major

Add error handling for HTTP failures.

The sendAsync() call has no error handling. If the HTTP request fails (network error, 4xx/5xx status), the thenAccept will not be invoked, and the failure will be silent.

Add error handling:

         receiveTask = http.sendAsync(httpRequest, HttpResponse.BodyHandlers.ofLines())
+                .exceptionally(throwable -> {
+                    System.err.println("Failed to receive messages: " + throwable.getMessage());
+                    return null;
+                })
                 .thenAccept(response -> {
+                    if (response == null) return;
+                    response.body()

Note: This should use proper logging instead of System.err (see previous comment).

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In src/main/java/com/example/NtfyConnectionImpl.java around lines 56 to 69, the
async HTTP call lacks error handling and uses System.err for parse failures;
update the sendAsync chain to handle HTTP failures and exceptions by attaching
an error handler (e.g., whenComplete or exceptionally) to log request-level
failures, check the HttpResponse status and log non-2xx status codes before
processing the body, and replace System.err printing in the JSON parse catch
with the class logger (log an error with the exception); ensure all logging uses
the existing logger instance and that the handler still processes only
successful 2xx responses.

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 (3)
src/main/java/com/example/NtfyConnectionImpl.java (3)

4-4: Fix the Jackson import to restore compilation.

The incorrect import tools.jackson.databind.ObjectMapper will cause compilation failure. This issue was flagged in a previous review but remains unresolved.

Apply this diff:

-import tools.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.ObjectMapper;

72-72: Remove debug code.

The peek(System.out::println) debug statement was flagged in a previous review but remains in the code. This should be removed before production.

                         .filter(Objects::nonNull)
                         .filter(message -> "message".equals(message.event()))
-                        .peek(System.out::println)
                         .forEach(messageHandler));

57-61: Add null check to prevent NPE.

The exceptionally handler returns null on error, but line 61 immediately calls response.body() without checking if response is null, causing a NullPointerException.

Apply this diff:

         receiveTask = http.sendAsync(httpRequest, HttpResponse.BodyHandlers.ofLines())
                 .exceptionally(throwable -> {
                     System.err.println("Failed to receive messages: " + throwable.getMessage());
                     return null;
                 })
-                .thenAccept(response -> response.body()
+                .thenAccept(response -> {
+                    if (response == null) return;
+                    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(Objects::nonNull)
                         .filter(message -> "message".equals(message.event()))
                         .peek(System.out::println)
-                        .forEach(messageHandler));
+                        .forEach(messageHandler);
+                });
🧹 Nitpick comments (2)
src/main/java/com/example/NtfyConnectionImpl.java (1)

58-58: Replace System.err with proper logging.

Error messages are printed to System.err instead of using a proper logging framework. This makes it difficult to configure log levels, formats, and destinations in production.

Consider adding a logger:

+import java.util.logging.Logger;
+
 public class NtfyConnectionImpl implements NtfyConnection {
+    private static final Logger logger = Logger.getLogger(NtfyConnectionImpl.class.getName());

Then replace System.err calls:

-                    System.err.println("Failed to receive messages: " + throwable.getMessage());
+                    logger.severe("Failed to receive messages: " + throwable.getMessage());
-                                System.err.println("Failed to parse message: " + e.getMessage());
+                                logger.warning("Failed to parse message: " + e.getMessage());

Also applies to: 66-66

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

52-62: Consider user-facing error notification.

The IOException is now properly caught (good!), but errors are only logged to System.err. Consider showing an Alert dialog to notify the user when message sending fails.

 public void sendMessage(ActionEvent actionEvent) {
     if (!inputField.getText().trim().isEmpty()) {
         try {
             model.sendMessage();
             inputField.clear();
         } catch (IOException e) {
-            // TODO: Show error message to user
-            System.err.println("Failed to send message: " + e.getMessage());
+            Alert alert = new Alert(Alert.AlertType.ERROR);
+            alert.setTitle("Send Error");
+            alert.setHeaderText("Failed to send message");
+            alert.setContentText(e.getMessage());
+            alert.showAndWait();
         }
     }
 }

Add import:

import javafx.scene.control.Alert;
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 200137e and df38385.

📒 Files selected for processing (2)
  • src/main/java/com/example/HelloController.java (1 hunks)
  • src/main/java/com/example/NtfyConnectionImpl.java (1 hunks)
🔇 Additional comments (4)
src/main/java/com/example/NtfyConnectionImpl.java (2)

22-29: LGTM!

Both constructors are well-designed. The default constructor provides production configuration via Dotenv with fail-fast validation, while the parameterized constructor enables dependency injection for testing.


35-35: The Cache header syntax is correct for ntfy.sh.

The Cache: no header is the correct syntax for ntfy.sh (an alias for X-Cache: no), as documented in the official ntfy.sh API documentation. No changes are needed.

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

16-23: Good improvement with constructor injection!

The addition of constructor-based dependency injection addresses the previous testability concern. The default constructor provides production wiring while the parameterized constructor enables testing with mock implementations.


43-49: LGTM!

The JavaFX property bindings are correctly established, and automatic topic connection on initialization provides a good user experience for the chat interface.

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: 3

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

4-4: The Jackson import is still incorrect and will cause compilation failure.

This issue was previously flagged but remains unaddressed. The import should be com.fasterxml.jackson.databind.ObjectMapper, not tools.jackson.databind.ObjectMapper.

Apply this diff:

-import tools.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.ObjectMapper;
src/main/java/com/example/HelloModel.java (2)

21-24: Constructor still calls instance method, allowing this to escape.

This issue was previously flagged but remains unaddressed. Calling receiveMessage() in the constructor allows the this reference to escape before the object is fully initialized, which can cause issues with:

  • Partial object state visibility in callbacks
  • Thread safety concerns
  • Testing difficulties

Consider one of the recommended solutions from the previous review:

  1. Require callers to explicitly call connectToTopic() after construction
  2. Use a static factory method
  3. Make receiveMessage() safe for construction-time calls

68-76: Resource leak: Multiple subscriptions accumulate without cleanup.

This critical issue was previously flagged but remains unaddressed. Each call to connectToTopic()receiveMessage()connection.receive() registers a new message handler without canceling the previous one. This causes:

  • Accumulated handlers from old topics
  • Messages from previous topics continuing to arrive
  • Potential thread/connection leaks in NtfyConnectionImpl

The NtfyConnection interface needs to support unsubscribing. Until then, consider:

  • Preventing multiple calls to connectToTopic() (guard with a boolean flag)
  • Documenting that topic switching is not supported
  • Or redesign the interface to return a Closeable/AutoCloseable subscription handle
🧹 Nitpick comments (3)
src/main/java/com/example/NtfyConnectionImpl.java (1)

62-76: Ensure the response body Stream is properly closed.

The response.body() call at Line 64 returns a Stream<String> that should be closed to release resources. While the stream may be auto-closed when the response is garbage collected, explicit closure is safer.

Consider wrapping the stream processing in a try-with-resources or explicitly closing it:

                 .thenAccept(response -> {
                     if (response == null) return;
-                    response.body()
+                    try (var body = response.body()) {
+                        body
                             .map(s -> {
                                 try {
                                     return mapper.readValue(s, NtfyMessageDto.class);
                                 } catch (Exception e) {
                                     logger.log(Level.WARNING, "Failed to parse message", e);
                                     return null;
                                 }
                             })
                             .filter(Objects::nonNull)
                             .filter(message -> "message".equals(message.event()))
                             .forEach(messageHandler);
+                    }
                 });

Note: Verify that terminal operations like forEach complete before the try-with-resources closes the stream.

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

50-50: Consider deferring automatic topic connection.

Calling connectToTopic() in initialize() triggers network operations during UI setup, which may block the JavaFX thread or cause initialization failures if the network is unavailable.

Consider one of these approaches:

  1. Move the initial connection to a separate "Connect" button action
  2. Execute it asynchronously after the UI is fully rendered:
Platform.runLater(model::connectToTopic);
  1. Document that connectToTopic() must be non-blocking (if it already is)
src/main/java/com/example/HelloModel.java (1)

78-86: Overly broad exception catch may hide bugs.

The catch (IllegalStateException notInitialized) at Line 82 catches all IllegalStateExceptions, not just toolkit initialization failures. Other bugs that throw IllegalStateException will be silently ignored.

Consider checking the toolkit state explicitly:

 private static void runOnFx(Runnable task) {
     try {
-        if (Platform.isFxApplicationThread()) task.run();
-        else Platform.runLater(task);
-    } catch (IllegalStateException notInitialized) {
-        // JavaFX toolkit not initialized (e.g., unit tests): run inline
+        if (Platform.isFxApplicationThread()) {
+            task.run();
+        } else {
+            Platform.runLater(task);
+        }
+    } catch (IllegalStateException e) {
+        if (e.getMessage() != null && e.getMessage().contains("Toolkit not initialized")) {
+            // JavaFX toolkit not initialized (e.g., unit tests): run inline
+            task.run();
+        } else {
+            throw e; // Re-throw other IllegalStateExceptions
+        }
-        task.run();
     }
 }

Alternatively, use a flag to detect test mode rather than catching exceptions.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between df38385 and aa03857.

📒 Files selected for processing (3)
  • src/main/java/com/example/HelloController.java (1 hunks)
  • src/main/java/com/example/HelloModel.java (2 hunks)
  • src/main/java/com/example/NtfyConnectionImpl.java (1 hunks)
🔇 Additional comments (2)
src/main/java/com/example/HelloController.java (1)

18-24: Well done implementing constructor-based dependency injection.

The dual-constructor approach enables both default production wiring and test injection. This addresses the previous testability concern effectively.

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

63-66: Good exception handling in sendMessage.

The method correctly clears the input only after a successful send. If connection.send() throws IOException, the exception propagates and Line 65 does not execute, preserving the user's message for retry.

Comment on lines +58 to +60
} catch (IOException e) {
System.err.println("Failed to send message: " + e.getMessage());
}
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

Show user-visible error feedback instead of System.err.

In a GUI application, users cannot see System.err output. When message sending fails, the user receives no feedback.

Replace console logging with a JavaFX Alert:

             } catch (IOException e) {
-                System.err.println("Failed to send message: " + e.getMessage());
+                var alert = new Alert(Alert.AlertType.ERROR);
+                alert.setTitle("Send Failed");
+                alert.setHeaderText("Failed to send message");
+                alert.setContentText(e.getMessage());
+                alert.showAndWait();
             }

Don't forget to add the import:

import javafx.scene.control.Alert;
🤖 Prompt for AI Agents
In src/main/java/com/example/HelloController.java around lines 58 to 60, replace
the System.err.println call inside the catch(IOException) with user-visible
JavaFX feedback: create and show a javafx.scene.control.Alert of type ERROR with
a clear title, header and the exception message; ensure the Alert is constructed
and shown on the JavaFX Application Thread (wrap in Platform.runLater if the
catch may run off the UI thread) and add the import for
javafx.scene.control.Alert (and javafx.application.Platform if using
Platform.runLater).


receiveTask = http.sendAsync(httpRequest, HttpResponse.BodyHandlers.ofLines())
.exceptionally(throwable -> {
System.err.println("Failed to receive messages: " + throwable.getMessage());
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 System.err with a proper logging framework.

Both error handlers use System.err.println, which is not suitable for production. Consider using a logging framework (e.g., java.util.logging.Logger or SLF4J) for proper log management and configuration.

Add a logger field:

+import java.util.logging.Logger;
+
 public class NtfyConnectionImpl implements NtfyConnection {
+    private static final Logger logger = Logger.getLogger(NtfyConnectionImpl.class.getName());

Then replace the error logging:

-                    System.err.println("Failed to receive messages: " + throwable.getMessage());
+                    logger.log(Level.SEVERE, "Failed to receive messages", throwable);
-                                    System.err.println("Failed to parse message: " + e.getMessage());
+                                    logger.log(Level.WARNING, "Failed to parse message", e);

Also applies to: 69-69

🤖 Prompt for AI Agents
In src/main/java/com/example/NtfyConnectionImpl.java around lines 58 and 69,
replace the uses of System.err.println with a proper logger: add a private
static final SLF4J logger field for this class (and necessary import), then
change the two System.err.println calls to LOGGER.error calls that include the
message and the throwable (e.g., LOGGER.error("Failed to receive messages",
throwable)). Ensure imports for org.slf4j.Logger and org.slf4j.LoggerFactory are
added and remove the System.err usage.

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 (4)
src/main/java/com/example/NtfyConnectionImpl.java (3)

47-75: Async receive pipeline is solid; replace System.err with proper logging and consider HTTP status checks

The overall receive flow (async long-poll, AtomicBoolean for active flag, cancellation via future.cancel(true), JSON parsing with filtering on event="message") looks good and is easy to follow. The main issues are:

  • Errors are logged via System.err.println, which isn’t ideal for a real app.
  • HTTP-level failures and non-2xx statuses aren’t distinguished from empty/no-body cases.

You could introduce a logger and use it in both error paths:

+import java.util.logging.Level;
+import java.util.logging.Logger;
+
 public class NtfyConnectionImpl implements NtfyConnection {
+    private static final Logger logger = Logger.getLogger(NtfyConnectionImpl.class.getName());

@@
-        CompletableFuture<Void> future = http.sendAsync(httpRequest, HttpResponse.BodyHandlers.ofLines())
-                .exceptionally(throwable -> {
-                    System.err.println("Failed to receive messages: " + throwable.getMessage());
-                    return null;
-                })
-                .thenAccept(response -> {
-                    if (response == null) return;
+        CompletableFuture<Void> future = http.sendAsync(httpRequest, HttpResponse.BodyHandlers.ofLines())
+                .whenComplete((response, throwable) -> {
+                    if (throwable != null) {
+                        logger.log(Level.SEVERE, "Failed to receive messages", throwable);
+                        return;
+                    }
+                    if (response == null || response.statusCode() / 100 != 2) {
+                        logger.log(Level.WARNING, "Unexpected HTTP status while receiving messages: {0}",
+                                response == null ? null : response.statusCode());
+                        return;
+                    }
                     response.body()
@@
-                                } catch (Exception e) {
-                                    System.err.println("Failed to parse message: " + e.getMessage());
+                                } catch (Exception e) {
+                                    logger.log(Level.WARNING, "Failed to parse message", e);
                                     return null;
                                 }

You can confirm usages of System.err.println with:

#!/bin/bash
rg -n "System\\.err\\.println" -S src/main/java

3-5: Fix Jackson ObjectMapper import to restore compilation

ObjectMapper is in com.fasterxml.jackson.databind, not tools.jackson.databind. As written, this class will not compile and will also mismatch the standard Maven coordinates for Jackson.

-import tools.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.ObjectMapper;

To double-check there are no remaining incorrect imports, you can run:

#!/bin/bash
rg -n "tools\\.jackson\\.databind\\.ObjectMapper" -S

31-44: Use standard Cache-Control header and consider basic response validation

The request currently sends a non-standard header:

.header("Cache", "no")

Servers may ignore this. Prefer a standard Cache-Control directive, e.g. "no-cache".

-                .header("Cache", "no")
+                .header("Cache-Control", "no-cache")

If you want slightly stronger guarantees from send, you might also consider checking the HTTP status and throwing an IOException on non-2xx responses instead of always discarding them, but that’s optional for this lab.

#!/bin/bash
# Inspect all Cache headers in this project
rg -n "Cache\"\\s*,\\s*\"no\"" -S
src/main/java/com/example/HelloController.java (1)

58-67: Surface send errors to the user instead of logging to System.err

Right now, a failure in model.sendMessage() only prints to System.err, which end users won’t see in a GUI app. It would be better to show an Alert (or some UI indicator) on the JavaFX thread when sending fails.

For example:

@@
-        if (!inputField.getText().trim().isEmpty()) {
-            try {
-                model.sendMessage();
-                inputField.clear();
-            } catch (IOException e) {
-                System.err.println("Failed to send message: " + e.getMessage());
-            }
-        }
+        if (!inputField.getText().trim().isEmpty()) {
+            try {
+                model.sendMessage();
+                inputField.clear();
+            } catch (IOException e) {
+                var alert = new javafx.scene.control.Alert(javafx.scene.control.Alert.AlertType.ERROR);
+                alert.setTitle("Send failed");
+                alert.setHeaderText("Failed to send message");
+                alert.setContentText(e.getMessage());
+                alert.showAndWait();
+            }
+        }

You can verify there are no remaining System.err.println calls in controller code with:

#!/bin/bash
rg -n "System\\.err\\.println" -S src/main/java/com/example/HelloController.java
🧹 Nitpick comments (5)
src/main/resources/com/example/hello-view.fxml (1)

7-43: FXML wiring matches controller fields and handlers

The fx:ids and onAction handlers line up cleanly with HelloController (topicField, inputField, messageView, statusLabel, connectToTopic, sendMessage), so the chat UI should bind correctly at runtime. If you plan to UI-test this later, consider giving fx:ids to the Connect/Send buttons as well, but it’s not required.

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

5-36: Stub correctly simulates a single active subscription

This stub is fine for tests that assume only one active subscription at a time: receive stores one handler, close flips subscriptionActive, and simulateIncomingMessage respects that flag. If you ever need to test multiple concurrent subscriptions to different topics, you’d need to extend this to track per-subscription state, but that feels unnecessary for current usage.

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

22-29: Improve HOST_NAME configuration failure feedback

Using Objects.requireNonNull(dotenv.get("HOST_NAME")) will throw a bare NullPointerException if the variable is missing, which can be opaque when debugging config issues. Consider throwing an IllegalStateException or similar with a clear message indicating that HOST_NAME is required.

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

7-18: Public interface is clean; add brief Javadoc to clarify semantics

The shape of this interface looks good (void send with IOException, receive returning a disposable Subscription). What’s missing is a brief contract:

  • On which thread will messageHandler.accept(...) be invoked?
  • Are multiple receive calls allowed and independent?
  • What happens if the handler throws?
  • Is Subscription.close() idempotent?

Even short Javadoc on send, receive, and Subscription would make this much clearer for future implementations and callers.

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

89-101: Consider using a logging framework instead of System.err.

The disconnect logic is sound and properly cleans up resources. However, using System.err.println at line 96 is generally not recommended for production code. Consider using a logging framework (e.g., SLF4J, java.util.logging) for better log management, configurability, and production monitoring.

Example with java.util.logging:

+import java.util.logging.Logger;
+
 public class HelloModel {
+    private static final Logger logger = Logger.getLogger(HelloModel.class.getName());
+
     // ... existing code ...
     
     public void disconnect() {
         if (currentSubscription != null) {
             try {
                 if (currentSubscription.isOpen()) {
                     currentSubscription.close();
                 }
             } catch (IOException e) {
-                System.err.println("Error closing subscription: " + e.getMessage());
+                logger.warning("Error closing subscription: " + e.getMessage());
             }
             currentSubscription = null;
             connected.set(false);
         }
     }
 }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between aa03857 and 1754001.

📒 Files selected for processing (8)
  • src/main/java/com/example/HelloController.java (1 hunks)
  • src/main/java/com/example/HelloModel.java (2 hunks)
  • src/main/java/com/example/NtfyConnection.java (1 hunks)
  • src/main/java/com/example/NtfyConnectionImpl.java (1 hunks)
  • src/main/resources/com/example/hello-view.fxml (1 hunks)
  • src/test/java/com/example/HelloModelTest.java (1 hunks)
  • src/test/java/com/example/NtfyConnectionSpy.java (1 hunks)
  • src/test/java/com/example/NtfyConnectionStub.java (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/test/java/com/example/HelloModelTest.java
  • src/test/java/com/example/NtfyConnectionSpy.java
🔇 Additional comments (6)
src/main/java/com/example/NtfyConnectionImpl.java (1)

77-88: Subscription lifecycle aligns with interface contract

The returned Subscription correctly propagates lifecycle: close() marks the stream as inactive and cancels the underlying future, and isOpen() reflects both the active flag and future completion. This matches the NtfyConnection.Subscription contract and should be easy for callers (e.g. HelloModel) to reason about.

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

16-56: Good use of constructor injection and JavaFX bindings

Moving to constructor-injected HelloModel (with a default wiring ctor) makes this controller much easier to test, and the FXML field bindings:

  • inputFieldmodel.messageToSendProperty()
  • topicFieldmodel.topicProperty()
  • messageView.setItems(model.getMessages())
  • statusLabel driven by connectedProperty

are all coherent with the new chat UI.


69-71: Connect button cleanly delegates to model

The connectToTopic action simply delegates to model.connectToTopic(), which keeps the controller thin and focused on UI concerns. This pairs nicely with the auto-connect call in initialize() (line 55), with no conflicting usages detected across tests and the model implementation (line 79). The pattern supports safe multiple invocations.

src/main/java/com/example/HelloModel.java (3)

25-27: Constructor improvement looks good!

The constructor no longer calls receiveMessage() or any instance methods, resolving the previous "escaping this" concern. The object is now fully initialized before any operations begin.


103-110: Excellent defensive helper for JavaFX threading.

The runOnFx method is well-designed with proper fallback handling:

  • Runs immediately if already on the FX thread (avoids unnecessary scheduling overhead)
  • Uses Platform.runLater for off-thread calls
  • Gracefully handles the case when JavaFX toolkit is not initialized (e.g., during unit testing)

This pattern makes the model more testable while maintaining thread safety for JavaFX bindings.


74-77: No issues found. Original review comment was based on incorrect assumptions.

The connection.send() method returns void and throws IOException (not boolean as implied by the original review). The current implementation is correct:

  • The method signature throws IOException provides proper exception-based error handling
  • The input field is cleared after sending (line 76)
  • No return value is being ignored since send() returns void

The code at lines 74-77 is implemented appropriately.

Likely an incorrect or invalid review comment.

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 (3)
src/main/java/com/example/NtfyConnectionImpl.java (2)

53-88: Harden async receive error handling and replace System.err with logging

The async receive pipeline works functionally but could be more robust:

  • .exceptionally and the JSON parse path both log to System.err. This is brittle in real apps and inconsistent with typical logging practices.
  • HTTP status codes are not checked; non-JSON or error responses will just emit parse errors.

Consider:

  • Introducing a logger (e.g., java.util.logging.Logger or your chosen facade) and logging failures via the logger instead of System.err.
  • In the thenAccept block, check response.statusCode() and treat non-2xx as a connection error (log and stop processing) before streaming the body.
  • Optionally include the raw line that failed to parse in the log at debug/trace level to aid diagnostics.

These changes keep the public behavior the same while making failures diagnosable and production-ready.


3-5: Fix Jackson ObjectMapper import to restore compilation

The import at Line 4 is incorrect for Jackson:

  • tools.jackson.databind.ObjectMapper does not exist in the standard Jackson artifacts.
  • The correct package is com.fasterxml.jackson.databind.ObjectMapper.

This currently prevents the project from compiling.

-import tools.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.ObjectMapper;
src/main/java/com/example/HelloModel.java (1)

79-89: Avoid clearing messages before a successful subscription is established

connectToTopic() still clears messages before connection.receive(..) is known to succeed:

disconnect();
messages.clear();

try {
    currentSubscription = connection.receive(...);
    connected.set(true);
} catch (Exception e) {
    System.err.println(...);
}

If receive(..) throws (e.g., bad URI, underlying connection error), you end up with:

  • No active subscription,
  • connected still false, and
  • The existing message history irreversibly cleared.

To keep the model state consistent, only clear messages after a subscription has been successfully created:

 public void connectToTopic() {
     disconnect();
-    messages.clear();
-
-    try {
-        currentSubscription = connection.receive(topic.get(),
-                m -> runOnFx(() -> messages.add(m)));
-        connected.set(true);
-    } catch (Exception e) {
-        System.err.println("Failed to connect to topic: " + e.getMessage());
-    }
+    try {
+        currentSubscription = connection.receive(topic.get(),
+                m -> runOnFx(() -> messages.add(m)));
+        messages.clear();
+        connected.set(true);
+    } catch (Exception e) {
+        System.err.println("Failed to connect to topic: " + e.getMessage());
+    }
 }

This way, if subscription creation fails, the previous messages remain visible.

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

7-52: Interface and lifecycle look good; consider tightening the contract docs

The interface now cleanly exposes send + a closeable Subscription and fixes the earlier unsubscribe concern. The remaining gaps are mostly semantic:

  • Explicitly state nullability expectations for topic and messageHandler (e.g., IllegalArgumentException on null/blank).
  • Clarify the threading model for receive (which thread invokes messageHandler, and whether callbacks can be concurrent).
  • Define what happens if messageHandler throws (ignored, terminates the subscription, etc.).
  • Optionally document whether multiple concurrent receive calls on the same instance/topic are supported.

This can all be handled in Javadoc without changing the signatures.

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

31-44: Consider checking HTTP status codes in send

send(..) currently:

  • Builds and sends the POST.
  • Ignores the returned HttpResponse (including status code).
  • Only surfaces failures via InterruptedExceptionIOException.

If the server returns a 4xx/5xx, the call is treated as success, which may not match the IOException contract in NtfyConnection.send and hides delivery problems.

Consider:

  • Capturing the HttpResponse<?> and treating non-2xx as failure (e.g., throw IOException with status code), or
  • At minimum, logging non-2xx responses for observability.

This keeps the API behavior closer to “send fails loudly on obvious delivery errors”.

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

74-77: sendMessage flow is reasonable; consider basic input validation (optional)

The send/clear sequence is good: on IOException the message is not cleared, which is user-friendly. If you want to harden it further, you could:

  • Skip sending when messageToSend.get() is null/blank, and
  • Optionally trim or normalize the message before sending.

This is non-critical but can protect against accidental empty sends.


87-100: Prefer a logger over System.err in the model

Both the connection failure and disconnect error paths log via System.err.println(...). For a UI app, this is hard to observe and hard to control.

Consider introducing a logger (or delegating error reporting to a higher layer) and replacing these System.err calls with structured logging:

  • Failed to connect to topic
  • Error closing subscription

That will make failures easier to diagnose and more consistent with any logging in NtfyConnectionImpl.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1754001 and 460c3da.

📒 Files selected for processing (3)
  • src/main/java/com/example/HelloModel.java (2 hunks)
  • src/main/java/com/example/NtfyConnection.java (1 hunks)
  • src/main/java/com/example/NtfyConnectionImpl.java (1 hunks)
🔇 Additional comments (1)
src/main/java/com/example/HelloModel.java (1)

106-112: FX-thread marshalling helper looks good

runOnFx(..) safely handles both production and test scenarios (FX initialized vs. not) and ensures list mutation happens on the FX thread when available. This is a solid pattern for testable JavaFX models.

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 (3)
src/main/java/com/example/NtfyConnectionImpl.java (2)

90-90: Consider using a proper logging framework.

Both error handlers use System.err.println, which lacks log management capabilities (levels, formatting, routing). For production code, consider using java.util.logging.Logger or SLF4J.

Example with java.util.logging:

+import java.util.logging.Logger;
+import java.util.logging.Level;
+
 public class NtfyConnectionImpl implements NtfyConnection {
+    private static final Logger logger = Logger.getLogger(NtfyConnectionImpl.class.getName());

Then replace the error logging:

-                    System.err.println("Failed to receive messages: " + throwable.getMessage());
+                    logger.log(Level.SEVERE, "Failed to receive messages", throwable);
-                                    System.err.println("Failed to parse message: " + e.getMessage());
+                    logger.log(Level.WARNING, "Failed to parse message", e);

Also applies to: 101-101


4-4: Critical: Wrong Jackson package breaks compilation.

The import uses tools.jackson.databind instead of com.fasterxml.jackson.databind. This will prevent compilation. Based on the library context showing Jackson 3.0.1, the correct package is com.fasterxml.jackson.databind.

Apply this diff:

-import tools.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.ObjectMapper;
src/main/java/com/example/HelloController.java (1)

81-90: Consider showing user-visible error feedback.

The exception is logged to System.err, which users cannot see in a GUI application. For better UX, display an Alert dialog to inform the user when message sending fails.

Example:

+import javafx.scene.control.Alert;
+
             } catch (IOException e) {
-                System.err.println("Failed to send message: " + e.getMessage());
+                var alert = new Alert(Alert.AlertType.ERROR);
+                alert.setTitle("Send Failed");
+                alert.setHeaderText("Failed to send message");
+                alert.setContentText(e.getMessage());
+                alert.showAndWait();
             }
🧹 Nitpick comments (1)
src/main/java/com/example/HelloModel.java (1)

118-118: Consider using a proper logging framework.

Both error handlers use System.err.println. For production code, consider using java.util.logging.Logger or SLF4J for better log management.

Example:

+import java.util.logging.Logger;
+import java.util.logging.Level;
+
 public class HelloModel {
+    private static final Logger logger = Logger.getLogger(HelloModel.class.getName());

Then replace the logging:

-            System.err.println("Failed to connect to topic: " + e.getMessage());
+            logger.log(Level.SEVERE, "Failed to connect to topic", e);
-                System.err.println("Error closing subscription: " + e.getMessage());
+                logger.log(Level.WARNING, "Error closing subscription", e);

Also applies to: 132-132

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 460c3da and 287c263.

📒 Files selected for processing (8)
  • src/main/java/com/example/HelloController.java (1 hunks)
  • src/main/java/com/example/HelloFX.java (1 hunks)
  • src/main/java/com/example/HelloModel.java (2 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/test/java/com/example/NtfyConnectionSpy.java (1 hunks)
  • src/test/java/com/example/NtfyConnectionStub.java (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/main/java/com/example/HelloFX.java
  • src/main/java/com/example/NtfyConnection.java
🔇 Additional comments (3)
src/main/java/com/example/NtfyMessageDto.java (1)

1-16: LGTM! Clean DTO design.

The record structure is well-suited for JSON deserialization, and the @JsonIgnoreProperties(ignoreUnknown = true) annotation provides good forward compatibility. The toString() override simplifies UI display in the ListView.

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

1-61: LGTM! Well-implemented test spy.

The spy correctly records sent messages and provides a functional subscription for testing. The package-private fields allow easy verification in tests.

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

1-71: LGTM! Effective test stub design.

The stub provides controlled message simulation for testing inbound message handling. The guard conditions in simulateIncomingMessage() (lines 67-68) ensure safe operation.

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

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

110-130: Race condition fix confirmed; consider logging framework.

The race condition from the previous review has been properly addressed—messages are now cleared (line 116) before the subscription is created (line 120). The backup and restore logic (lines 114, 126) provides good UX by preserving data on connection failure.

Minor suggestion: Replace System.err.println (line 128) with a proper logging framework (e.g., java.util.logging or SLF4J) for production-grade error handling.

Apply this diff to use java.util.logging:

+import java.util.logging.Logger;
+
 public class HelloModel {
+    private static final Logger logger = Logger.getLogger(HelloModel.class.getName());
     // ... existing fields ...
 
     public void connectToTopic() {
         // ... existing code ...
         } catch (Exception e) {
             messages.setAll(oldMessages);
             connected.set(false);
-            System.err.println("Failed to connect to topic: " + e.getMessage());
+            logger.warning("Failed to connect to topic: " + e.getMessage());
         }
     }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 287c263 and 78d13df.

📒 Files selected for processing (1)
  • src/main/java/com/example/HelloModel.java (2 hunks)
🔇 Additional comments (2)
src/main/java/com/example/HelloModel.java (2)

33-40: Constructor implementation looks good.

The constructor no longer calls instance methods, addressing the previous concern about this escaping before full initialization. The minimal approach is clean and safe.


154-161: Excellent testability pattern.

The runOnFx utility handles the JavaFX threading model correctly in production (lines 156-157) while providing a fallback for unit tests where the Platform isn't initialized (lines 158-160). This makes the class fully testable without requiring the JavaFX toolkit.

Comment on lines +94 to +102
/**
* Sends the value of {@link #messageToSend} to the current topic.
*
* @throws IOException if sending through the connection fails
*/
public void sendMessage() throws IOException {
connection.send(topic.get(), messageToSend.get());
messageToSend.set("");
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

Verify that the controller handles IOException from sendMessage.

The exception-based error handling is good: on success, the input is cleared (line 101); on failure, the exception prevents clearing so the user can retry. However, ensure the controller catches and handles the IOException appropriately (e.g., displaying an error message to the user).

Run this script to check how the controller handles exceptions from sendMessage():


🏁 Script executed:

#!/bin/bash
# Description: Verify HelloController handles IOException from sendMessage

# Search for sendMessage() calls and surrounding error handling
rg -n -A 10 -B 3 '\.sendMessage\s*\(' --type=java

Length of output: 3620


Error handling exists but lacks user-facing feedback.

The controller properly catches IOException from sendMessage() (HelloController line 86), and the input is correctly preserved on failure. However, error reporting is limited to stderr (System.err.println at line 87). Consider displaying the error to the user via an alert dialog or status message so they're aware the send failed, rather than only logging to stderr.

🤖 Prompt for AI Agents
In src/main/java/com/example/HelloModel.java around lines 94 to 102, the
sendMessage method is fine but the app currently only logs send failures to
stderr; update the controller catch block (where IOException is caught at
HelloController line ~86) to present a user-facing error (e.g., a dialog or
status bar update) showing a concise message plus exception.getMessage(), while
still preserving the input and keeping the existing stderr log for diagnostics;
if using Swing use JOptionPane.showMessageDialog, if using JavaFX use
Platform.runLater with an Alert, and ensure the UI-call is made on the UI
thread.

@simonforsberg simonforsberg merged commit 00be01a into main Nov 14, 2025
2 checks passed
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.

1 participant