Skip to content
Closed

Ntfy #20

Show file tree
Hide file tree
Changes from all 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
53 changes: 44 additions & 9 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,27 @@
<javafx.version>25</javafx.version>
</properties>
<dependencies>
<dependency>
<groupId>org.openjfx</groupId>
<artifactId>javafx-controls</artifactId>
<version>${javafx.version}</version>
</dependency>
<dependency>
<groupId>org.openjfx</groupId>
<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.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
Expand All @@ -36,14 +57,28 @@
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.openjfx</groupId>
<artifactId>javafx-controls</artifactId>
<version>${javafx.version}</version>
<groupId>org.wiremock</groupId>
<artifactId>wiremock</artifactId>
<version>4.0.0-beta.15</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.openjfx</groupId>
<artifactId>javafx-fxml</artifactId>
<version>${javafx.version}</version>
<groupId>org.awaitility</groupId>
<artifactId>awaitility</artifactId>
<version>4.3.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testfx</groupId>
<artifactId>testfx-junit5</artifactId>
<version>4.0.16-alpha</version>
<scope>test</scope>
</dependency>
Comment on lines +59 to +76
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

Pre-release versions in test dependencies.

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


🌐 Web query:

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

💡 Result:

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

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

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

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


Replace pre-release test dependencies with stable versions.

Verification confirms both are problematic for testing:

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

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

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

<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
<version>2.0.12</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
Expand All @@ -53,9 +88,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 +100,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) {
}
31 changes: 31 additions & 0 deletions src/main/java/com/example/ChatModel.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
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());

messages.add(chatMsg);
});
}
Comment on lines +24 to +30
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

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

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

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

Apply this diff to fix the threading issue:

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

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

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

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

@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

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

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

}
Loading
Loading