Skip to content
Open
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
25 changes: 25 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,22 @@
<artifactId>javafx-fxml</artifactId>
<version>${javafx.version}</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.17.2</version>
</dependency>
<dependency>
<groupId>io.github.cdimascio</groupId>
<artifactId>java-dotenv</artifactId>
<version>5.2.2</version>
</dependency>
<dependency>
<groupId>org.wiremock</groupId>
<artifactId>wiremock</artifactId>
<version>4.0.0-beta.15</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
Expand All @@ -63,6 +79,15 @@
<noManPages>true</noManPages>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>25</source>
<target>25</target>
<enablePreview>true</enablePreview>
</configuration>
</plugin>
</plugins>
</build>
</project>
60 changes: 54 additions & 6 deletions src/main/java/com/example/HelloController.java
Original file line number Diff line number Diff line change
@@ -1,22 +1,70 @@
package com.example;

import javafx.collections.ListChangeListener;
import javafx.fxml.FXML;
import javafx.scene.control.Label;
import javafx.scene.control.Button;
import javafx.scene.control.TextArea;
import javafx.scene.input.KeyCode;
import javafx.stage.FileChooser;

import java.io.File;

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

private final HelloModel model = new HelloModel();
@FXML private TextArea inputField;
@FXML private Button sendButton;
@FXML private TextArea chatArea;
@FXML private Button sendFileButton;


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

@FXML
private Label messageLabel;

@FXML
private void initialize() {
if (messageLabel != null) {
messageLabel.setText(model.getGreeting());

model.getMessages().addListener((ListChangeListener<NtfyMessageDto>) change ->{
while(change.next()) {
if (change.wasAdded()) {
for (var msg : change.getAddedSubList()) {
chatArea.appendText(msg.message() + "\n");
}
}
}
});

sendFileButton.setOnAction(e -> attachFile());
sendButton.setOnAction(e -> sendMessage());

inputField.setOnKeyPressed(event -> {
if (event.getCode() == KeyCode.ENTER && !event.isShiftDown()) {
sendMessage();
event.consume();
}
});
}
@FXML
private void sendMessage() {
String msg = inputField.getText().trim();
if (msg.isEmpty()) return;

chatArea.appendText("Du: " + msg + "\n");
model.setMessageToSend(msg);
model.sendMessage();
inputField.clear();
}
Comment on lines +50 to +58
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

Sent messages may appear twice in the chat.

Outgoing messages are appended locally with "Du:" prefix (line 54), but if the server echoes messages back, they will also appear via the ListChangeListener. Consider filtering out own messages in the listener or not appending locally.

🤖 Prompt for AI Agents
In src/main/java/com/example/HelloController.java around lines 50 to 58,
outgoing messages are appended locally which causes duplicates when the server
echoes them back; fix by either (A) stop appending the message locally and rely
solely on the ListChangeListener to display messages (remove
chatArea.appendText(...) and clear input after send), or (B) mark outgoing
messages with a senderId/flag on the message model before sending and update the
ListChangeListener to ignore messages whose senderId equals the current client’s
id so echoed messages are filtered out; implement one of these two approaches
and ensure inputField is still cleared and model.sendMessage() is called.

@FXML
private void attachFile() {
FileChooser chooser = new FileChooser();
chooser.setTitle("Välj fil att bifoga");

File file = chooser.showOpenDialog(sendFileButton.getScene().getWindow());
if (file != null) {
model.sendFile(file);
chatArea.appendText("Du skickade fil: " + file.getName() + "\n");
}
Comment on lines +60 to 68
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

Missing user feedback on file send failure.

model.sendFile() may fail, but the success message is always displayed. Consider checking the result or handling errors.

     File file = chooser.showOpenDialog(sendFileButton.getScene().getWindow());
     if (file != null) {
-        model.sendFile(file);
-        chatArea.appendText("Du skickade fil: " + file.getName() + "\n");
+        boolean success = model.sendFile(file);
+        if (success) {
+            chatArea.appendText("Du skickade fil: " + file.getName() + "\n");
+        } else {
+            chatArea.appendText("Kunde inte skicka fil: " + file.getName() + "\n");
+        }
     }

Note: This requires changing sendFile in HelloModel to return a boolean.

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

}
}
6 changes: 3 additions & 3 deletions src/main/java/com/example/HelloFX.java
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,16 @@ public class HelloFX extends Application {

@Override
public void start(Stage stage) throws Exception {
System.getenv("HOST_NAME");
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

Dead code: System.getenv() result is discarded.

The call System.getenv("HOST_NAME") retrieves the environment variable but doesn't assign the result to anything. Either remove this line or assign it to a variable if it's needed.

-        System.getenv("HOST_NAME");
📝 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
System.getenv("HOST_NAME");
🤖 Prompt for AI Agents
In src/main/java/com/example/HelloFX.java around line 13, the call to
System.getenv("HOST_NAME") is dead code because its return value is discarded;
either remove the line if the environment variable is not needed, or assign the
result to a variable (or use it directly) where needed (e.g., String hostName =
System.getenv("HOST_NAME"); or pass the value to whatever consumer). Ensure any
subsequent logic uses that variable or perform a null check/default if required.

FXMLLoader fxmlLoader = new FXMLLoader(HelloFX.class.getResource("hello-view.fxml"));
Parent root = fxmlLoader.load();
Scene scene = new Scene(root, 640, 480);
stage.setTitle("Hello MVC");
stage.setTitle("JavaFX Chat App");
stage.setScene(scene);
stage.show();
}

public static void main(String[] args) {
static void main() {
launch();
}

}
71 changes: 67 additions & 4 deletions src/main/java/com/example/HelloModel.java
Original file line number Diff line number Diff line change
@@ -1,15 +1,78 @@
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.io.*;
import java.net.URI;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.util.UUID;

/**
* Model layer: encapsulates application data and business logic.
*/
public class HelloModel {
/**
* Returns a greeting based on the current Java and JavaFX versions.
*/
public String getGreeting() {
String javaVersion = System.getProperty("java.version");
String javafxVersion = System.getProperty("javafx.version");
return "Hello, JavaFX " + javafxVersion + ", running on Java " + javaVersion + ".";

private final NtfyConnection connection;
private final StringProperty messageToSend = new SimpleStringProperty("");
private final ObservableList<NtfyMessageDto> messages = FXCollections.observableArrayList();
private final String clientId = UUID.randomUUID().toString();

//Konstuktor för prod
public HelloModel() {
this.connection = new NtfyConnectionImpl();
receiveMessage();
}

//Konstuktor för test
public HelloModel(NtfyConnection connection) {
this.connection = connection;
receiveMessage();
}

public ObservableList<NtfyMessageDto> getMessages() {
return messages;
}

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

public void sendMessage() {
String text = messageToSend.get().trim();
if (text.isEmpty()) return;

connection.send(text);

messageToSend.set("");
}

public void sendFile(File file) {
try {
connection.sendFile(file.toPath());
} catch (IOException e) {
e.printStackTrace();
}
}

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









12 changes: 12 additions & 0 deletions src/main/java/com/example/NtfyConnection.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.example;
import java.io.FileNotFoundException;
import java.nio.file.Path;
import java.util.function.Consumer;

public interface NtfyConnection {
boolean send (String message);

void receive (Consumer<NtfyMessageDto> messageHandler);

boolean sendFile(Path filePath) throws FileNotFoundException;
}
95 changes: 95 additions & 0 deletions src/main/java/com/example/NtfyConnectionImpl.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
package com.example;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.github.cdimascio.dotenv.Dotenv;

import java.io.File;
import java.io.FileNotFoundException;
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.nio.file.Files;
import java.nio.file.Path;
import java.util.UUID;
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();
String host = dotenv.get("HOST_NAME");

if (host == null || host.isEmpty()) {
throw new IllegalStateException("HOST_NAME has to be defined");
}
this.hostName = host;
}

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

@Override
public boolean send (String message) {
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(hostName + "/mytopic"))
.POST(HttpRequest.BodyPublishers.ofString(message))
.build();

try {
http.send(request, HttpResponse.BodyHandlers.discarding());
return true;

} catch (IOException | InterruptedException e) {
System.out.println("Error sending message!");
return false;
}
Comment on lines +45 to +52
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

Restore interrupt status when catching InterruptedException.

When catching InterruptedException, the thread's interrupt status should be restored to allow proper interrupt handling by calling code.

         } catch (IOException | InterruptedException e) {
+            if (e instanceof InterruptedException) {
+                Thread.currentThread().interrupt();
+            }
             System.out.println("Error sending message!");
             return false;
         }
🤖 Prompt for AI Agents
In src/main/java/com/example/NtfyConnectionImpl.java around lines 45 to 52, the
catch block lumps IOException and InterruptedException together and does not
restore the thread's interrupt status; split the exceptions into separate
catches or detect InterruptedException and call
Thread.currentThread().interrupt() before returning, and include the exception
in the log message so the interrupt is preserved for callers while still
handling IO errors appropriately.

}
Comment on lines +38 to +53
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

🏁 Script executed:

#!/bin/bash
# 1. First, check the full context of the NtfyConnectionImpl class
cat -n src/main/java/com/example/NtfyConnectionImpl.java 2>/dev/null | head -100

Repository: ithsjava25/ithsjava25-java-java-fx-and-network-javafx-classrom

Length of output: 268


🌐 Web query:

Java HttpClient send vs sendAsync blocking behavior

💡 Result:

Short answer

  • client.send(request, bodyHandler) is synchronous: it blocks the calling thread until the request/response cycle is finished (including whatever the BodyHandler does). [1]
  • client.sendAsync(request, bodyHandler) is asynchronous: it returns a CompletableFuture immediately and the request/response work runs on the HttpClient's executor; the Future completes when the response (as handled by the BodyHandler) is ready. Calling get()/join() on that Future will block the caller. [1][2]

Extra notes (brief)

  • The default HttpClient executor in OpenJDK is a cached thread pool (newCachedThreadPool) unless you supply your own, so async tasks run on those threads. [2]
  • “Finished” means whatever your BodyHandler requires (e.g., BodyHandlers.ofString reads the whole body before completion). [1]

Sources

  • Oracle Java HttpClient API (send / sendAsync behavior and executor note). [1]
  • OpenJDK / community notes on default HttpClient thread-pool (newCachedThreadPool). [2]

Blocking HTTP call may freeze the UI if invoked from the JavaFX Application Thread.

http.send() is a synchronous (blocking) call confirmed by Java API documentation. When invoked from the JavaFX Application Thread, it will freeze the UI until the request completes. sendAsync() is available as an alternative that returns a CompletableFuture and executes on a background thread pool, or explicitly run send() on a background thread (e.g., using Task or Platform.runLater()).

🤖 Prompt for AI Agents
In src/main/java/com/example/NtfyConnectionImpl.java around lines 38–53, the
method currently calls the blocking http.send() on the calling thread which can
freeze the JavaFX Application Thread; change the method to perform the HTTP call
asynchronously: update the signature to return a CompletableFuture<Boolean> (or
provide a callback) and replace http.send(...) with http.sendAsync(request,
HttpResponse.BodyHandlers.discarding()).thenApply(response ->
true).exceptionally(e -> { /* log e */ return false; }); ensure any UI updates
resulting from the response are marshalled back to the JavaFX thread via
Platform.runLater().


@Override
public boolean sendFile(Path filePath) throws FileNotFoundException {
if (!Files.exists(filePath)) {
throw new FileNotFoundException("File does not exist: " + filePath);
}

HttpRequest httpRequest = HttpRequest.newBuilder()
.uri(URI.create(hostName + "/mytopic"))
.header("Content-type", "application/octet-stream")
.POST(HttpRequest.BodyPublishers.ofFile(filePath))
.build();

try {
http.send(httpRequest, HttpResponse.BodyHandlers.discarding());
return true;
} catch (IOException | InterruptedException e) {
System.out.println("Error sending file: " + e.getMessage());
return false;
}
Comment on lines +67 to +73
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

Restore interrupt status in sendFile as well.

         } catch (IOException | InterruptedException e) {
+            if (e instanceof InterruptedException) {
+                Thread.currentThread().interrupt();
+            }
             System.out.println("Error sending file: " + e.getMessage());
             return false;
         }
📝 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
try {
http.send(httpRequest, HttpResponse.BodyHandlers.discarding());
return true;
} catch (IOException | InterruptedException e) {
System.out.println("Error sending file: " + e.getMessage());
return false;
}
try {
http.send(httpRequest, HttpResponse.BodyHandlers.discarding());
return true;
} catch (IOException | InterruptedException e) {
if (e instanceof InterruptedException) {
Thread.currentThread().interrupt();
}
System.out.println("Error sending file: " + e.getMessage());
return false;
}
🤖 Prompt for AI Agents
In src/main/java/com/example/NtfyConnectionImpl.java around lines 67 to 73, the
catch block currently swallows InterruptedException by catching it in a
multi-catch and not restoring the thread's interrupt status; split the catches
so InterruptedException is handled separately, call
Thread.currentThread().interrupt() inside the InterruptedException catch, then
log the error and return false, and keep a separate catch for IOException that
logs and returns false.

}

@Override
public void receive (Consumer<NtfyMessageDto> handler) {
HttpRequest httpRequest = HttpRequest.newBuilder()
.uri(URI.create(hostName + "/mytopic/json"))
.GET()
.build();

http.sendAsync(httpRequest, HttpResponse.BodyHandlers.ofLines())
.thenAccept(response -> response.body()
.map(line -> {
try {
return mapper.readValue(line, NtfyMessageDto.class);
} catch (JsonProcessingException e) {
return null;
}
})
.filter(msg -> msg != null && msg.event().equals("message"))
.forEach(handler));
}
}
10 changes: 10 additions & 0 deletions src/main/java/com/example/NtfyMessageDto.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
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) {
public record Attachment(String name, String type, long size, String url) {
}
}

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

public class Singelton {
private final static Singelton instance = new Singelton();

private Singelton() {}

public static Singelton getInstance() {
return instance;
}
}
5 changes: 4 additions & 1 deletion src/main/java/module-info.java
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
module hellofx {
requires javafx.controls;
requires javafx.fxml;
requires java.net.http;
requires com.fasterxml.jackson.databind;
requires java.dotenv;

opens com.example to javafx.fxml;
opens com.example to javafx.fxml, com.fasterxml.jackson.databind;
exports com.example;
}
27 changes: 19 additions & 8 deletions src/main/resources/com/example/hello-view.fxml
Original file line number Diff line number Diff line change
@@ -1,9 +1,20 @@
<?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 spacing="10" style="-fx-background-color: GRAY;" xmlns="http://javafx.com/javafx/17.0.12" xmlns:fx="http://javafx.com/fxml/1" fx:controller="com.example.HelloController">

<padding>
<Insets bottom="10" left="10" right="10" top="10" />
</padding>

<TextArea fx:id="chatArea" editable="false" wrapText="true" prefHeight="400" />

<HBox spacing="5">
<TextArea fx:id="inputField" prefRowCount="2" promptText="Skriv meddelande..." style="-fx-background-color: WHITE;" wrapText="true" HBox.hgrow="ALWAYS" />
<Button fx:id="sendButton" text="Skicka" />
<Button fx:id="sendFileButton" text="Bifoga fil"/>
</HBox>
</VBox>
Loading
Loading