Skip to content
Closed
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
22 changes: 22 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,28 @@
<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.awaitility</groupId>
<artifactId>awaitility</artifactId>
<version>4.3.0</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
Expand Down
45 changes: 43 additions & 2 deletions src/main/java/com/example/HelloController.java
Original file line number Diff line number Diff line change
@@ -1,14 +1,26 @@
package com.example;

import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.scene.control.Label;
import javafx.scene.control.*;
import javafx.util.Callback;


/**
* Controller layer: mediates between the view (FXML) and the model.
*/
public class HelloController {

private final HelloModel model = new HelloModel();
private final HelloModel model = new HelloModel(new NtfyConnectionImpl());

@FXML
private ListView<NtfyMessageDto> messageView;

@FXML
private TextArea chatArea;

@FXML
private Button chatButton;

@FXML
private Label messageLabel;
Expand All @@ -18,5 +30,34 @@ private void initialize() {
if (messageLabel != null) {
messageLabel.setText(model.getGreeting());
}
messageView.setItems(model.getMessages());
messageView.setCellFactory(showOnlyMessages());
}




private static Callback<ListView<NtfyMessageDto>, ListCell<NtfyMessageDto>> showOnlyMessages() {
return List -> new ListCell<>() {
@Override
protected void updateItem(NtfyMessageDto item, boolean empty) {
super.updateItem(item, empty);
if (empty || item == null) {
setText(null);
} else {
setText(item.message());
}
}
};
}

public void sendMessage(ActionEvent actionEvent) {
String input = chatArea.getText().trim();
if (!input.isEmpty()) {
model.setMessageToSend(input);
model.sendMessage();
chatArea.clear();
}
}
}

1 change: 1 addition & 0 deletions src/main/java/com/example/HelloFX.java
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ public class HelloFX extends Application {

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

FXMLLoader fxmlLoader = new FXMLLoader(HelloFX.class.getResource("hello-view.fxml"));
Parent root = fxmlLoader.load();
Scene scene = new Scene(root, 640, 480);
Expand Down
55 changes: 55 additions & 0 deletions src/main/java/com/example/HelloModel.java
Original file line number Diff line number Diff line change
@@ -1,9 +1,28 @@
package com.example;

import javafx.application.Platform;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;

import java.util.concurrent.CompletableFuture;

/**
* Model layer: encapsulates application data and business logic.
*/
public class HelloModel {

private final NtfyConnection connection;

private final ObservableList<NtfyMessageDto> messages = FXCollections.observableArrayList();
private final StringProperty messageToSend = new SimpleStringProperty();

public HelloModel(NtfyConnection connection) {
this.connection = connection;
receiveMessage();
}
Comment on lines +21 to +24
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 starting async operations in the constructor.

Line 23 calls receiveMessage() from the constructor, which starts asynchronous operations before the object is fully constructed. This is problematic because:

  1. The this reference can escape before construction completes
  2. If receiveMessage() is overridden in a subclass, it could access uninitialized state
  3. It makes testing and object lifecycle management more difficult

Consider one of these alternatives:

Option 1: Require explicit initialization after construction:

     public HelloModel(NtfyConnection connection) {
         this.connection = connection;
-        receiveMessage();
+    }
+    
+    public void initialize() {
+        receiveMessage();
     }

Then update callers to call model.initialize() after construction.

Option 2: Use a factory method:

    private HelloModel(NtfyConnection connection) {
        this.connection = connection;
    }
    
    public static HelloModel create(NtfyConnection connection) {
        HelloModel model = new HelloModel(connection);
        model.receiveMessage();
        return model;
    }
🤖 Prompt for AI Agents
In src/main/java/com/example/HelloModel.java around lines 21 to 24, the
constructor calls receiveMessage(), which starts asynchronous work during
construction and risks "this" escaping and subclass/uninitialized-state issues;
remove the receiveMessage() invocation from the constructor, provide either (a)
a public initialize() method that callers must invoke after new
HelloModel(connection) to start async processing, or (b) make the constructor
private and add a static factory create(connection) that constructs the instance
and then calls receiveMessage() before returning; update all callers to use the
new initialize() or create(...) flow and add/adjust tests to ensure
initialization is explicit and lifecycle handled safely.


/**
* Returns a greeting based on the current Java and JavaFX versions.
*/
Expand All @@ -12,4 +31,40 @@ public String getGreeting() {
String javafxVersion = System.getProperty("javafx.version");
return "Hello, JavaFX " + javafxVersion + ", running on Java " + javaVersion + ".";
}
public ObservableList<NtfyMessageDto> getMessages() {
return messages;
}

public String getMessageToSend() {
return messageToSend.get();
}

public StringProperty messageToSendProperty() {
return messageToSend;
}

public void setMessageToSend(String message) {
messageToSend.set(message);
}

public CompletableFuture<Void> sendMessage() {
return connection.send(messageToSend.get());
}

public void receiveMessage() {
connection.receive(m->runOnFx(()-> messages.add(m)));
}

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
task.run();
}
}

}


11 changes: 11 additions & 0 deletions src/main/java/com/example/NtfyConnection.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
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);
}
67 changes: 67 additions & 0 deletions src/main/java/com/example/NtfyConnectionImpl.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 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.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();

public NtfyConnectionImpl() {
Dotenv dotenv = Dotenv.load();
hostName = Objects.requireNonNull(dotenv.get("HOST_NAME"));
}

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

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

return http.sendAsync(httpRequest, HttpResponse.BodyHandlers.discarding())
.thenAccept(response -> System.out.println("Message sent!"))
.exceptionally(e -> {
System.out.println("Error sending message");
return null;
});
}
Comment on lines +29 to +43
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

Fix the Cache header and use proper logging.

Line 34 uses an incorrect header name "Cache". The correct header for cache control is "Cache-Control". Additionally, lines 39 and 41 use System.out.println instead of a proper logging framework, which limits production observability.

Apply this diff:

     public CompletableFuture<Void> send(String message) {
         HttpRequest httpRequest = HttpRequest.newBuilder()
                 .POST(HttpRequest.BodyPublishers.ofString(message))
-                .header("Cache", "no")
+                .header("Cache-Control", "no-cache")
                 .uri(URI.create(hostName + "/mytopic"))
                 .build();
 
         return http.sendAsync(httpRequest, HttpResponse.BodyHandlers.discarding())
-                .thenAccept(response -> System.out.println("Message sent!"))
+                .thenAccept(response -> logger.info("Message sent successfully"))
                 .exceptionally(e -> {
-                    System.out.println("Error sending message");
+                    logger.error("Error sending message", e);
                     return null;
                 });
     }

You'll need to add a logger field:

private static final Logger logger = LoggerFactory.getLogger(NtfyConnectionImpl.class);
🤖 Prompt for AI Agents
In src/main/java/com/example/NtfyConnectionImpl.java around lines 30 to 44, the
HTTP request uses the wrong header name ("Cache") and the method uses
System.out.println for logging; add a logger field (private static final Logger
logger = LoggerFactory.getLogger(NtfyConnectionImpl.class)); change the header
to "Cache-Control", replace the success print with logger.info("Message sent!"),
and replace the exception handler to log the error with logger.error("Error
sending message", e) (ensure the exception is captured in the exceptionally
block).


@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.out.println("Failed to parse message");
return null;
}
})
.filter(message -> message !=null && message.event().equals("message"))
.peek(System.out::println)
.forEach(messageHandler));
}
}

12 changes: 12 additions & 0 deletions src/main/java/com/example/NtfyMessageDto.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
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
){}
4 changes: 4 additions & 0 deletions src/main/java/module-info.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
module hellofx {
requires javafx.controls;
requires javafx.fxml;
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

Incorrect module name for Jackson databind.

The module name tools.jackson.databind is incorrect. This corresponds to the wrong groupId in pom.xml. The correct module name for Jackson databind is com.fasterxml.jackson.databind.

     requires java.net.http;
-    requires tools.jackson.databind;
+    requires com.fasterxml.jackson.databind;
     requires javafx.graphics;

This must be fixed alongside the pom.xml groupId correction.

📝 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 around line 6, the module declaration uses the
incorrect module name "tools.jackson.databind"; change it to
"com.fasterxml.jackson.databind" and ensure the project's pom.xml groupId is
corrected to match the official Jackson coordinates so the module name resolves
correctly; update the requires clause to the correct module name and verify
build imports and module-path references compile successfully.

requires javafx.graphics;

opens com.example to javafx.fxml;
exports com.example;
Expand Down
32 changes: 24 additions & 8 deletions src/main/resources/com/example/hello-view.fxml
Original file line number Diff line number Diff line change
@@ -1,9 +1,25 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.layout.StackPane?>
<?import javafx.scene.control.Label?>

<StackPane xmlns="http://javafx.com/javafx" xmlns:fx="http://javafx.com/fxml" fx:controller="com.example.HelloController">
<children>
<Label fx:id="messageLabel" text="Hello, JavaFX!" />
</children>
</StackPane>

<?import javafx.geometry.*?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>

<VBox prefHeight="500.0" prefWidth="300.0" stylesheets="@../../css/style.css" xmlns="http://javafx.com/javafx/17.0.12" xmlns:fx="http://javafx.com/fxml/1" fx:controller="com.example.HelloController">

<Label fx:id="messageLabel" alignment="CENTER" contentDisplay="CENTER" maxWidth="Infinity" text="Hello, JavaFX!" VBox.vgrow="NEVER" />
<BorderPane VBox.vgrow="ALWAYS">
<center>
<ListView fx:id="messageView">
<padding>
<Insets bottom="1.0" left="1.0" right="1.0" top="1.0" />
</padding>
</ListView>
</center>
</BorderPane>

<HBox VBox.vgrow="NEVER" alignment="CENTER_LEFT"> <padding> <Insets bottom="5.0" left="10.0" right="10.0" top="10.0" /></padding>
<TextArea fx:id="chatArea" opacity="0.8" prefRowCount="3" wrapText="true" HBox.hgrow="ALWAYS" />
<Button fx:id="chatButton" maxWidth="-Infinity" minWidth="-Infinity" onAction="#sendMessage" text="Send Message" />
</HBox>

</VBox>
59 changes: 59 additions & 0 deletions src/main/resources/css/style.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@

/*
styles.css
*/
.root {
-fx-background-color:
linear-gradient(to bottom,
rgba(0,0,0,0.1) 0%,
rgba(0,0,0,0) 10%),
linear-gradient(to bottom,
#8b0000 30%,
#ffd700 100%);
-fx-background-insets: 0, 4;
-fx-background-radius: 0, 3;
-fx-effect: dropshadow(gaussian, rgba(0, 0, 0, 0.4), 10, 0.5, 0, 0)

}
.list-view {
-fx-background-color: transparent; /* Make background transparent */
-fx-background-insets: 0;
-fx-padding: 0;
-fx-border-color: transparent;
-fx-background-radius: 5;
-fx-border-radius: 5;
}
.list-cell{
-fx-background-color: transparent;
-fx-border-color: transparent;
-fx-control-inner-background: transparent;
-fx-font-size: 14;
-fx-font-weight: bold;
-fx-padding: 5 10 5 10;
}
.text-area{
-fx-control-inner-background: #FFFFFF;
-fx-padding: 5px 7px;
-fx-text-fill: black;
-fx-font-size: 14;

-fx-border-radius: 10;
-fx-background-radius: 10;
-fx-background-color: transparent;
-fx-effect: dropshadow(three-pass-box, rgba(0,0,0,0.2), 5, 0, 0, 0);
}
.text-area .content, .viewport {
-fx-background-radius: 8;
-fx-border-radius: 8;
}
.button{
-fx-background-color: #610202;
-fx-text-fill: white;
-fx-font-size: 12px;
-fx-padding: 10 15 10 15;
-fx-background-radius: 5px;
-fx-border-radius: 8px;
}
.button:hover{
-fx-background-color: #400101;
}
Loading
Loading