Skip to content
Closed

Ntfy #20

Show file tree
Hide file tree
Changes from 12 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
target/
/.idea/
.env
40 changes: 37 additions & 3 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,40 @@
<artifactId>javafx-fxml</artifactId>
<version>${javafx.version}</version>
</dependency>
<dependency>
<groupId>io.github.cdimascio</groupId>
<artifactId>dotenv-java</artifactId>
<version>3.2.0</version>
</dependency>
<dependency>
<groupId>tools.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>3.0.1</version>
</dependency>
<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>
Comment on lines +59 to +76
Copy link
Copy Markdown

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.

<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
<version>2.0.12</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.awaitility</groupId>
<artifactId>awaitility</artifactId>
<version>4.3.0</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
Expand All @@ -53,9 +87,9 @@
<artifactId>javafx-maven-plugin</artifactId>
<version>0.0.8</version>
<configuration>
<mainClass>com.example.HelloFX</mainClass>
<mainClass>com.example.ChatApplication</mainClass>
<options>
<option>--enable-native-access=javafx.graphics</option>
<option>--enable-native-access=javafx.graphics</option>
</options>
<launcher>javafx</launcher>
<stripDebug>true</stripDebug>
Expand All @@ -65,4 +99,4 @@
</plugin>
</plugins>
</build>
</project>
</project>
36 changes: 36 additions & 0 deletions src/main/java/com/example/ChatApplication.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package com.example;

import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.stage.Stage;

import java.io.IOException;

public class ChatApplication extends Application {

@Override
public void start(Stage stage) throws IOException {

NtfyConnection ntfyService = new NtfyConnectionImpl();
ChatModel model = new ChatModel(ntfyService);

model.startReceiving();

FXMLLoader fxmlLoader = new FXMLLoader(
ChatApplication.class.getResource("chat-view.fxml"));

ChatController controller = new ChatController(model);
fxmlLoader.setController(controller);

Scene scene = new Scene(fxmlLoader.load(), 600, 400);
stage.setTitle("JavaFX Ntfy Chat App");
stage.setScene(scene);
stage.show();
}

public static void main(String[] args) {
launch();
}
}
67 changes: 67 additions & 0 deletions src/main/java/com/example/ChatController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package com.example;

import io.github.cdimascio.dotenv.Dotenv;
import javafx.fxml.FXML;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.ListView;
import javafx.scene.control.TextField;

import java.time.Instant;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.Objects;


public class ChatController {

@FXML private ListView<ChatMessage> messageListView;
@FXML private TextField inputTextField;
@FXML private Button sendButton;

private final ChatModel model;

private final DateTimeFormatter timeFormatter =
DateTimeFormatter.ofPattern("HH:mm:ss").withZone(ZoneId.systemDefault());


public ChatController(ChatModel model) {
this.model = model;
}

@FXML
public void initialize() {
messageListView.setItems(model.getMessages());

messageListView.setCellFactory(lv -> new javafx.scene.control.ListCell<ChatMessage>() {
@Override
protected void updateItem(ChatMessage msg, boolean empty) {
super.updateItem(msg, empty);
if (empty || msg == null) {
setText(null);
} else {
String formattedTime = timeFormatter.format(Instant.ofEpochSecond(msg.timestamp()));
setText("[" + formattedTime + "] " + msg.content());
}
}
});


inputTextField.setOnAction(event -> sendMessageAction());
sendButton.setOnAction(event -> sendMessageAction());
}


private void sendMessageAction() {
String message = inputTextField.getText().trim();
if (!message.isEmpty()) {

model.sendMessage(message);


inputTextField.clear();
}
}


}
4 changes: 4 additions & 0 deletions src/main/java/com/example/ChatMessage.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package com.example;

public record ChatMessage(String content, long timestamp) {
}
33 changes: 33 additions & 0 deletions src/main/java/com/example/ChatModel.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package com.example;


import javafx.application.Platform;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;

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

private final NtfyConnection ntfyConnection;

public ChatModel(NtfyConnection ntfyConnection) {
this.ntfyConnection = ntfyConnection;
}
public ObservableList<ChatMessage> getMessages() {
return messages;
}

public void sendMessage(String text) {
ntfyConnection.send(text);
}

public void startReceiving() {
ntfyConnection.receive(ntfyDto -> {
ChatMessage chatMsg = new ChatMessage(ntfyDto.message(), ntfyDto.time());

Platform.runLater(() -> {
messages.add(chatMsg);
});
});
}
Comment on lines +24 to +30
Copy link
Copy Markdown

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.

}
22 changes: 0 additions & 22 deletions src/main/java/com/example/HelloController.java

This file was deleted.

25 changes: 0 additions & 25 deletions src/main/java/com/example/HelloFX.java

This file was deleted.

15 changes: 0 additions & 15 deletions src/main/java/com/example/HelloModel.java

This file was deleted.

9 changes: 9 additions & 0 deletions src/main/java/com/example/NtfyConnection.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.example;

import java.util.concurrent.CompletableFuture;
import java.util.function.Consumer;

public interface NtfyConnection {
CompletableFuture<Void> send(String message);
void receive(Consumer<NtfyMessageDto> messageHandler);
}
83 changes: 83 additions & 0 deletions src/main/java/com/example/NtfyConnectionImpl.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
package com.example;

import io.github.cdimascio.dotenv.Dotenv;
import tools.jackson.databind.ObjectMapper;

import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.util.concurrent.CompletableFuture;
import java.util.function.Consumer;

public class NtfyConnectionImpl implements NtfyConnection {

private final HttpClient http = HttpClient.newHttpClient();
private final String hostName;
private final ObjectMapper mapper = new ObjectMapper();

public NtfyConnectionImpl() {
String loadedHostName = null;
try {
Dotenv dotenv = Dotenv.load();
loadedHostName = dotenv.get("HOST_NAME");
} catch (Exception e) {
System.err.println("WARNING: Could not load .env file for HOST_NAME. Using fallback.");
}

this.hostName = (loadedHostName != null)
? loadedHostName
: "http://localhost:8080";

if (this.hostName.equals("http://localhost:8080")) {
System.out.println("DEBUG: NtfyConnectionImpl running in test/fallback mode.");
}
}

public NtfyConnectionImpl(String hostName) {
this.hostName = hostName;
}

@Override
public CompletableFuture<Void> send(String message) {
HttpRequest httpRequest = HttpRequest.newBuilder()
.POST(HttpRequest.BodyPublishers.ofString(message))
.uri(URI.create(hostName + "/mytopic"))
.build();

return http.sendAsync(httpRequest, HttpResponse.BodyHandlers.discarding())
.thenAccept(response -> {
if (response.statusCode() >= 200 && response.statusCode() < 300) {
System.out.println("Message sent successfully.");
} else {
System.err.println("Error while sending: " + response.statusCode());
}
})
.exceptionally(e -> {
System.err.println("Network issue: " + e.getMessage());
return null;
});
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

@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 -> {
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"))
Copy link
Copy Markdown

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.

.peek(System.out::println)
.forEach(messageHandler));
}
Comment on lines +62 to +82
Copy link
Copy Markdown

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;
+                });
     }

}
7 changes: 7 additions & 0 deletions src/main/java/com/example/NtfyMessageDto.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.example;

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;

@JsonIgnoreProperties(ignoreUnknown = true)
public record NtfyMessageDto(String id, long time, String event, String topic, String message) {
}
6 changes: 5 additions & 1 deletion src/main/java/module-info.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
module hellofx {
module chatapp {
requires javafx.controls;
requires javafx.fxml;
requires com.fasterxml.jackson.annotation;
requires io.github.cdimascio.dotenv.java;
requires tools.jackson.databind;
Copy link
Copy Markdown

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.

requires java.net.http;

opens com.example to javafx.fxml;
exports com.example;
Expand Down
Loading
Loading