diff --git a/.env b/.env
new file mode 100644
index 00000000..a8db63b7
--- /dev/null
+++ b/.env
@@ -0,0 +1 @@
+HOST_NAME=https://ntfy.fungover.org
\ No newline at end of file
diff --git a/pom.xml b/pom.xml
index c40f667e..f410eea0 100644
--- a/pom.xml
+++ b/pom.xml
@@ -45,6 +45,22 @@
javafx-fxml
${javafx.version}
+
+ io.github.cdimascio
+ dotenv-java
+ 3.2.0
+
+
+ tools.jackson.core
+ jackson-databind
+ 3.0.1
+
+
+ org.wiremock
+ wiremock
+ 4.0.0-beta.15
+ test
+
@@ -55,7 +71,7 @@
com.example.HelloFX
-
+
javafx
true
@@ -65,4 +81,4 @@
-
+
\ No newline at end of file
diff --git a/src/main/java/com/example/HelloController.java b/src/main/java/com/example/HelloController.java
index fdd160a0..69b3e7a5 100644
--- a/src/main/java/com/example/HelloController.java
+++ b/src/main/java/com/example/HelloController.java
@@ -1,22 +1,68 @@
package com.example;
+import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.scene.control.Label;
+import javafx.scene.control.ListView;
+import javafx.scene.control.TextField;
+import javafx.stage.FileChooser;
+import javafx.stage.Stage;
+
+import java.io.File;
/**
* 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
+ public ListView messageView;
@FXML
private Label messageLabel;
+ @FXML
+ private TextField messageInput;
+
@FXML
private void initialize() {
if (messageLabel != null) {
messageLabel.setText(model.getGreeting());
}
+ messageView.setItems(model.getMessages());
+
+
+ model.messageToSendProperty().bindBidirectional(messageInput.textProperty());
+
+
+ messageView.setCellFactory(lv -> new javafx.scene.control.ListCell() {
+ @Override
+ protected void updateItem(NtfyMessageDto item, boolean empty) {
+ super.updateItem(item, empty);
+ if (empty || item == null) {
+ setText(null);
+ } else {
+ setText(item.message() != null ? item.message() : "(No message content)");
+ }
+ }
+ });
+ }
+
+ public void sendMessage(ActionEvent actionEvent) {
+ model.sendMessage();
+
+ messageInput.clear();
+ }
+
+ public void attachFile(ActionEvent actionEvent) {
+ FileChooser fileChooser = new FileChooser();
+ fileChooser.setTitle("Select File to Attach");
+ File file = fileChooser.showOpenDialog(new Stage());
+
+ if (file != null) {
+ model.sendFile(file);
+ }
}
-}
+}
\ No newline at end of file
diff --git a/src/main/java/com/example/HelloFX.java b/src/main/java/com/example/HelloFX.java
index 96bdc5ca..d72cc412 100644
--- a/src/main/java/com/example/HelloFX.java
+++ b/src/main/java/com/example/HelloFX.java
@@ -13,7 +13,7 @@ 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);
- stage.setTitle("Hello MVC");
+ stage.setTitle("JavaFX Chat App");
stage.setScene(scene);
stage.show();
}
@@ -21,5 +21,4 @@ public void start(Stage stage) throws Exception {
public static void main(String[] args) {
launch();
}
-
}
\ No newline at end of file
diff --git a/src/main/java/com/example/HelloModel.java b/src/main/java/com/example/HelloModel.java
index 385cfd10..2a59bf37 100644
--- a/src/main/java/com/example/HelloModel.java
+++ b/src/main/java/com/example/HelloModel.java
@@ -1,15 +1,83 @@
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.File;
+
/**
* Model layer: encapsulates application data and business logic.
*/
public class HelloModel {
+
+ private final NtfyConnection connection;
+ private final ObservableList messages = FXCollections.observableArrayList();
+ private final StringProperty messageToSend = new SimpleStringProperty();
+
+ public HelloModel(NtfyConnection connection) {
+ this.connection = connection;
+ startReceivingMessages();
+ }
+
+ public ObservableList getMessages() {
+ return messages;
+ }
+
+ public String getMessageToSend() {
+ return messageToSend.get();
+ }
+
+ public StringProperty messageToSendProperty() {
+ return messageToSend;
+ }
+
+ public void setMessageToSend(String message) {
+ messageToSend.set(message);
+ }
+
/**
* 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 + ".";
+ return "Welcome to JavaFX Chat App " + javafxVersion + ", running on Java " + javaVersion + ".";
+ }
+
+ public boolean sendMessage() {
+ String message = getMessageToSend();
+ if (message == null || message.trim().isEmpty()) {
+ return false;
+ }
+ return connection.send(message.trim());
+ }
+
+ public boolean sendFile(File file) {
+ if (file == null || !file.exists()) {
+ return false;
+ }
+ return connection.sendFile(file);
+ }
+
+ private void startReceivingMessages() {
+ connection.receive(this::addMessageToUI);
+ }
+
+ private void addMessageToUI(NtfyMessageDto message) {
+ // Check if we're on JavaFX application thread, if not use Platform.runLater
+ if (Platform.isFxApplicationThread()) {
+ messages.add(message);
+ } else {
+ Platform.runLater(() -> messages.add(message));
+ }
+ }
+
+ // Test helper method - package private for testing
+ void addTestMessage(NtfyMessageDto message) {
+ // Direct add for testing (bypasses Platform.runLater)
+ messages.add(message);
}
-}
+}
\ No newline at end of file
diff --git a/src/main/java/com/example/ManyParameters.java b/src/main/java/com/example/ManyParameters.java
new file mode 100644
index 00000000..9efe0e71
--- /dev/null
+++ b/src/main/java/com/example/ManyParameters.java
@@ -0,0 +1,19 @@
+package com.example;
+
+public class ManyParameters {
+
+ public ManyParameters(String computerName, int timeout,
+ String method, int size, byte[] data) {
+
+ }
+
+
+ static void main() {
+ ManyParametersBuilder builder = new ManyParametersBuilder();
+ builder
+ .setComputerName("localhost") //Fluent API
+ .setTimeout(10)
+ .setSize(0)
+ .createManyParameters();
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/example/ManyParametersBuilder.java b/src/main/java/com/example/ManyParametersBuilder.java
new file mode 100644
index 00000000..fd49920d
--- /dev/null
+++ b/src/main/java/com/example/ManyParametersBuilder.java
@@ -0,0 +1,38 @@
+package com.example;
+
+public class ManyParametersBuilder {
+ private String computerName;
+ private int timeout = 0;
+ private String method;
+ private int size = 0;
+ private byte[] data = null;
+
+ public ManyParametersBuilder setComputerName(String computerName) {
+ this.computerName = computerName;
+ return this;
+ }
+
+ public ManyParametersBuilder setTimeout(int timeout) {
+ this.timeout = timeout;
+ return this;
+ }
+
+ public ManyParametersBuilder setMethod(String method) {
+ this.method = method;
+ return this;
+ }
+
+ public ManyParametersBuilder setSize(int size) {
+ this.size = size;
+ return this;
+ }
+
+ public ManyParametersBuilder setData(byte[] data) {
+ this.data = data;
+ return this;
+ }
+
+ public ManyParameters createManyParameters() {
+ return new ManyParameters(computerName, timeout, method, size, data);
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/example/NtfyConnection.java b/src/main/java/com/example/NtfyConnection.java
new file mode 100644
index 00000000..aab1101d
--- /dev/null
+++ b/src/main/java/com/example/NtfyConnection.java
@@ -0,0 +1,10 @@
+package com.example;
+
+import java.io.File;
+import java.util.function.Consumer;
+
+public interface NtfyConnection {
+ boolean send(String message);
+ void receive(Consumer messageHandler);
+ boolean sendFile(File file);
+}
\ No newline at end of file
diff --git a/src/main/java/com/example/NtfyConnectionImpl.java b/src/main/java/com/example/NtfyConnectionImpl.java
new file mode 100644
index 00000000..109f1320
--- /dev/null
+++ b/src/main/java/com/example/NtfyConnectionImpl.java
@@ -0,0 +1,168 @@
+package com.example;
+
+import io.github.cdimascio.dotenv.Dotenv;
+import tools.jackson.databind.ObjectMapper;
+
+import java.io.File;
+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.util.Base64;
+import java.util.Objects;
+import java.util.function.Consumer;
+
+public class NtfyConnectionImpl implements NtfyConnection {
+
+ private final HttpClient http;
+ private final String hostName;
+ private final ObjectMapper mapper;
+
+ public NtfyConnectionImpl() {
+ this(HttpClient.newHttpClient(), new ObjectMapper(), loadHostNameFromEnv());
+ }
+
+ public NtfyConnectionImpl(String hostName) {
+ this(HttpClient.newHttpClient(), new ObjectMapper(), hostName);
+ }
+
+
+ NtfyConnectionImpl(HttpClient http, ObjectMapper mapper, String hostName) {
+ this.http = http;
+ this.mapper = mapper;
+ this.hostName = hostName;
+ }
+
+ private static String loadHostNameFromEnv() {
+ Dotenv dotenv = Dotenv.load();
+ return Objects.requireNonNull(dotenv.get("HOST_NAME"));
+ }
+
+ @Override
+ public boolean send(String message) {
+ if (message == null || message.trim().isEmpty()) {
+ return false;
+ }
+
+ try {
+
+ String jsonPayload = String.format("{\"message\":\"%s\"}", escapeJson(message.trim()));
+
+ HttpRequest httpRequest = HttpRequest.newBuilder()
+ .POST(HttpRequest.BodyPublishers.ofString(jsonPayload))
+ .header("Content-Type", "application/json")
+ .header("Cache", "no-cache")
+ .uri(URI.create(hostName + "/mytopic"))
+ .build();
+
+
+ http.sendAsync(httpRequest, HttpResponse.BodyHandlers.discarding())
+ .thenAccept(response -> {
+ if (response.statusCode() == 200) {
+ System.out.println("Message sent successfully");
+ } else {
+ System.out.println("Failed to send message. Status: " + response.statusCode());
+ }
+ })
+ .exceptionally(ex -> {
+ System.out.println("Error sending message: " + ex.getMessage());
+ return null;
+ });
+
+ return true;
+ } catch (Exception e) {
+ System.out.println("Error in send: " + e.getMessage());
+ return false;
+ }
+ }
+
+ @Override
+ public boolean sendFile(File file) {
+ if (file == null || !file.exists()) {
+ return false;
+ }
+
+ try {
+ byte[] fileContent = Files.readAllBytes(file.toPath());
+ String base64Content = Base64.getEncoder().encodeToString(fileContent);
+
+
+ String jsonPayload = String.format(
+ "{\"message\":\"File: %s\", \"file\":\"%s\", \"filename\":\"%s\"}",
+ escapeJson(file.getName()), base64Content, escapeJson(file.getName())
+ );
+
+ HttpRequest httpRequest = HttpRequest.newBuilder()
+ .POST(HttpRequest.BodyPublishers.ofString(jsonPayload))
+ .header("Content-Type", "application/json")
+ .header("Cache", "no-cache")
+ .uri(URI.create(hostName + "/mytopic"))
+ .build();
+
+ http.sendAsync(httpRequest, HttpResponse.BodyHandlers.discarding())
+ .thenAccept(response -> {
+ if (response.statusCode() == 200) {
+ System.out.println("File sent successfully: " + file.getName());
+ } else {
+ System.out.println("Failed to send file. Status: " + response.statusCode());
+ }
+ })
+ .exceptionally(ex -> {
+ System.out.println("Error sending file: " + ex.getMessage());
+ return null;
+ });
+
+ return true;
+ } catch (IOException e) {
+ System.out.println("Error reading file: " + e.getMessage());
+ return false;
+ }
+ }
+
+ @Override
+ public void receive(Consumer messageHandler) {
+ try {
+ HttpRequest httpRequest = HttpRequest.newBuilder()
+ .GET()
+ .uri(URI.create(hostName + "/mytopic/json"))
+ .build();
+
+ http.sendAsync(httpRequest, HttpResponse.BodyHandlers.ofLines())
+ .thenAccept(response -> {
+ if (response.statusCode() == 200) {
+ response.body()
+ .map(s -> {
+ try {
+ return mapper.readValue(s, NtfyMessageDto.class);
+ } catch (Exception e) {
+ System.out.println("Error parsing message: " + e.getMessage());
+ return null;
+ }
+ })
+ .filter(Objects::nonNull)
+ .filter(message -> "message".equals(message.event()))
+ .forEach(messageHandler);
+ } else {
+ System.out.println("Failed to receive messages. Status: " + response.statusCode());
+ }
+ })
+ .exceptionally(ex -> {
+ System.out.println("Error receiving messages: " + ex.getMessage());
+ return null; // Lade till return statement här
+ });
+ } catch (Exception e) {
+ System.out.println("Error in receive: " + e.getMessage());
+ }
+ }
+
+ private String escapeJson(String text) {
+ if (text == null) return "";
+ return text.replace("\\", "\\\\")
+ .replace("\"", "\\\"")
+ .replace("\n", "\\n")
+ .replace("\r", "\\r")
+ .replace("\t", "\\t");
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/example/NtfyMessageDto.java b/src/main/java/com/example/NtfyMessageDto.java
new file mode 100644
index 00000000..9db0c07d
--- /dev/null
+++ b/src/main/java/com/example/NtfyMessageDto.java
@@ -0,0 +1,47 @@
+package com.example;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+import java.util.Objects;
+
+@JsonIgnoreProperties(ignoreUnknown = true)
+public record NtfyMessageDto(
+ String id,
+ long time,
+ String event,
+ String topic,
+ String message,
+ @JsonProperty("file") String file,
+ @JsonProperty("filename") String filename
+) {
+
+ @Override
+ public boolean equals(Object o) {
+ if (o == null || getClass() != o.getClass()) return false;
+ NtfyMessageDto that = (NtfyMessageDto) o;
+ return time == that.time &&
+ Objects.equals(id, that.id) &&
+ Objects.equals(event, that.event) &&
+ Objects.equals(topic, that.topic) &&
+ Objects.equals(message, that.message);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(id, time, event, topic, message);
+ }
+
+ @Override
+ public String toString() {
+ return "NtfyMessageDto{" +
+ "id='" + id + '\'' +
+ ", time=" + time +
+ ", event='" + event + '\'' +
+ ", topic='" + topic + '\'' +
+ ", message='" + message + '\'' +
+ ", file='" + file + '\'' +
+ ", filename='" + filename + '\'' +
+ '}';
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/example/Singelton.java b/src/main/java/com/example/Singelton.java
new file mode 100644
index 00000000..b3685a01
--- /dev/null
+++ b/src/main/java/com/example/Singelton.java
@@ -0,0 +1,14 @@
+package com.example;
+
+public class Singelton {
+
+ private final static Singelton instance = new Singelton();
+
+ private Singelton(){
+
+ }
+
+ public static Singelton getInstance(){
+ return instance;
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java
index 71574a27..89e041cb 100644
--- a/src/main/java/module-info.java
+++ b/src/main/java/module-info.java
@@ -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;
+ requires javafx.graphics;
opens com.example to javafx.fxml;
exports com.example;
diff --git a/src/main/resources/com/example/hello-view.fxml b/src/main/resources/com/example/hello-view.fxml
index 20a7dc82..d1733b78 100644
--- a/src/main/resources/com/example/hello-view.fxml
+++ b/src/main/resources/com/example/hello-view.fxml
@@ -1,9 +1,25 @@
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
+
+
+
+
+
\ No newline at end of file
diff --git a/src/test/java/com/example/HelloModelTest.java b/src/test/java/com/example/HelloModelTest.java
new file mode 100644
index 00000000..d2837739
--- /dev/null
+++ b/src/test/java/com/example/HelloModelTest.java
@@ -0,0 +1,156 @@
+package com.example;
+
+import com.github.tomakehurst.wiremock.junit5.WireMockRuntimeInfo;
+import com.github.tomakehurst.wiremock.junit5.WireMockTest;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
+
+import static com.github.tomakehurst.wiremock.client.WireMock.*;
+import static org.assertj.core.api.Assertions.assertThat;
+
+@WireMockTest
+class HelloModelTest {
+
+ private NtfyConnectionSpy spy;
+ private HelloModel model;
+
+ @BeforeEach
+ void setUp() {
+ spy = new NtfyConnectionSpy();
+ model = new HelloModel(spy);
+ }
+
+ @Test
+ @DisplayName("Given a model with messageToSend when calling sendMessage then send method on connection should be called")
+ void sendMessageCallsConnectionWithMessageToSend() {
+
+ model.setMessageToSend("Hello World");
+
+
+ boolean result = model.sendMessage();
+
+
+ assertThat(result).isTrue();
+ assertThat(spy.message).isEqualTo("Hello World");
+ assertThat(spy.sendCallCount).isEqualTo(1);
+ }
+
+ @Test
+ @DisplayName("Empty message should not be sent")
+ void emptyMessageShouldNotBeSent() {
+ // Arrange
+ model.setMessageToSend("");
+
+
+ boolean result = model.sendMessage();
+
+
+ assertThat(result).isFalse();
+ assertThat(spy.message).isNull();
+ assertThat(spy.sendCallCount).isEqualTo(0);
+ }
+
+ @Test
+ @DisplayName("Null message should not be sent")
+ void nullMessageShouldNotBeSent() {
+
+ model.setMessageToSend(null);
+
+
+ boolean result = model.sendMessage();
+
+
+ assertThat(result).isFalse();
+ assertThat(spy.message).isNull();
+ assertThat(spy.sendCallCount).isEqualTo(0);
+ }
+
+ @Test
+ @DisplayName("Whitespace only message should not be sent")
+ void whitespaceMessageShouldNotBeSent() {
+ // Arrange
+ model.setMessageToSend(" ");
+
+
+ boolean result = model.sendMessage();
+
+
+ assertThat(result).isFalse();
+ assertThat(spy.message).isNull();
+ assertThat(spy.sendCallCount).isEqualTo(0);
+ }
+
+ @Test
+ @DisplayName("Send file calls connection with file")
+ void sendFileCallsConnectionWithFile() throws IOException {
+
+ File tempFile = File.createTempFile("test", ".txt");
+ Files.write(tempFile.toPath(), "Test content".getBytes());
+
+
+ boolean result = model.sendFile(tempFile);
+
+
+ assertThat(result).isTrue();
+ assertThat(spy.file).isNotNull();
+ assertThat(spy.file).isEqualTo(tempFile); // Testa att samma fil skickas
+ assertThat(spy.sendFileCallCount).isEqualTo(1);
+
+
+ tempFile.delete();
+ }
+
+ @Test
+ @DisplayName("Send null file should not call connection")
+ void sendNullFileShouldNotCallConnection() {
+
+ boolean result = model.sendFile(null);
+
+
+ assertThat(result).isFalse();
+ assertThat(spy.file).isNull();
+ assertThat(spy.sendFileCallCount).isEqualTo(0);
+ }
+
+ @Test
+ @DisplayName("Send non-existent file should not call connection")
+ void sendNonExistentFileShouldNotCallConnection() {
+
+ File nonExistentFile = new File("non_existent_file.txt");
+
+
+ boolean result = model.sendFile(nonExistentFile);
+
+
+ assertThat(result).isFalse();
+ assertThat(spy.file).isNull();
+ assertThat(spy.sendFileCallCount).isEqualTo(0);
+ }
+
+ @Test
+ @DisplayName("Receive message adds message to list")
+ void receiveMessageAddsMessageToList() {
+
+ NtfyMessageDto testMessage = new NtfyMessageDto(
+ "test-id",
+ System.currentTimeMillis(),
+ "message",
+ "mytopic",
+ "Test message from spy",
+ null,
+ null
+ );
+
+
+ model.addTestMessage(testMessage);
+
+
+ assertThat(model.getMessages()).hasSize(1);
+ assertThat(model.getMessages().get(0).message()).isEqualTo("Test message from spy");
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/com/example/NtfyConnectionSpy.java b/src/test/java/com/example/NtfyConnectionSpy.java
new file mode 100644
index 00000000..08acc7fe
--- /dev/null
+++ b/src/test/java/com/example/NtfyConnectionSpy.java
@@ -0,0 +1,64 @@
+package com.example;
+
+import java.io.File;
+import java.util.function.Consumer;
+
+public class NtfyConnectionSpy implements NtfyConnection {
+ public String message;
+ public File file;
+ public int sendCallCount = 0;
+ public int sendFileCallCount = 0;
+ private Consumer messageHandler;
+ private boolean simulateReceive = false;
+
+ @Override
+ public boolean send(String message) {
+ this.message = message;
+ this.sendCallCount++;
+ return true;
+ }
+
+ @Override
+ public void receive(Consumer messageHandler) {
+ this.messageHandler = messageHandler;
+ if (simulateReceive) {
+ triggerTestMessage();
+ }
+ }
+
+ @Override
+ public boolean sendFile(File file) {
+ this.file = file;
+ this.sendFileCallCount++;
+ return true;
+ }
+
+ // Helper method to trigger a test message
+ public void triggerTestMessage() {
+ if (messageHandler != null) {
+ NtfyMessageDto testMessage = new NtfyMessageDto(
+ "test-id",
+ System.currentTimeMillis(),
+ "message",
+ "mytopic",
+ "Test message from spy",
+ null,
+ null
+ );
+ messageHandler.accept(testMessage);
+ }
+ }
+
+ // Helper methods for testing
+ public void reset() {
+ this.message = null;
+ this.file = null;
+ this.sendCallCount = 0;
+ this.sendFileCallCount = 0;
+ this.messageHandler = null;
+ }
+
+ public void setSimulateReceive(boolean simulateReceive) {
+ this.simulateReceive = simulateReceive;
+ }
+}
\ No newline at end of file