diff --git a/.gitignore b/.gitignore
index 6ac465db..5a54815d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,2 +1,3 @@
target/
/.idea/
+.env
\ No newline at end of file
diff --git a/pom.xml b/pom.xml
index c40f667e..d094f8fc 100644
--- a/pom.xml
+++ b/pom.xml
@@ -45,6 +45,26 @@
javafx-fxml
${javafx.version}
+
+ io.github.cdimascio
+ dotenv-java
+ 3.2.0
+
+
+ org.openjfx
+ javafx-swing
+ ${javafx.version}
+
+
+ tools.jackson.core
+ jackson-databind
+ 3.0.1
+
+
+ org.wiremock
+ wiremock
+ 4.0.0-beta.15
+
@@ -55,7 +75,7 @@
com.example.HelloFX
-
+
javafx
true
@@ -65,4 +85,4 @@
-
+
\ No newline at end of file
diff --git a/src/main/java/com/example/AttachmentDto.java b/src/main/java/com/example/AttachmentDto.java
new file mode 100644
index 00000000..69c73342
--- /dev/null
+++ b/src/main/java/com/example/AttachmentDto.java
@@ -0,0 +1,7 @@
+package com.example;
+
+public record AttachmentDto(
+ String name,
+ String url,
+ String type,
+ long size) {}
diff --git a/src/main/java/com/example/HelloController.java b/src/main/java/com/example/HelloController.java
index fdd160a0..55707898 100644
--- a/src/main/java/com/example/HelloController.java
+++ b/src/main/java/com/example/HelloController.java
@@ -1,22 +1,124 @@
package com.example;
+import javafx.application.Platform;
+import javafx.event.ActionEvent;
import javafx.fxml.FXML;
+import javafx.scene.control.*;
+import javafx.stage.FileChooser;
+
+import javafx.scene.layout.VBox;
+import javafx.geometry.Insets;
import javafx.scene.control.Label;
+import java.awt.*;
+import java.net.URI;
+import java.time.Instant;
+import java.time.ZoneId;
+
+
+import java.io.File;
+import java.nio.file.Path;
+
+import static com.example.HelloModel.runOnFx;
+
/**
* 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 void initialize() {
+ System.out.println("Controller init: kopplar ListView");
+
if (messageLabel != null) {
messageLabel.setText(model.getGreeting());
}
+
+ messageView.setItems(model.getMessages());
+
+ messageView.setCellFactory(listView -> new ListCell<>() {
+ @Override
+ protected void updateItem(NtfyMessageDto item, boolean empty) {
+ super.updateItem(item, empty);
+ if (empty || item == null) {
+ setText(null);
+ setGraphic(null);
+ } else {
+ VBox container = new VBox();
+ container.setSpacing(4);
+
+ Label topicLabel = new Label(item.topic());
+ topicLabel.setStyle("-fx-font-weight: bold; -fx-text-fill: #2a9df4;");
+
+ Label messageLabel = new Label(item.message());
+ messageLabel.setWrapText(true);
+ messageLabel.setStyle("-fx-text-fill: #333333;");
+
+ Label timeLabel = new Label("⏰ " + Instant.ofEpochMilli(item.time()).atZone(ZoneId.systemDefault()).toLocalDateTime());
+ timeLabel.setStyle("-fx-font-size: 10px; -fx-text-fill: #888888;");
+
+ container.getChildren().addAll(topicLabel, messageLabel, timeLabel);
+
+ // Lägg till nedladdningslänk om fil finns
+ if (item.attachmentUrl() != null && !item.attachmentUrl().isEmpty()) {
+ Hyperlink downloadLink = new Hyperlink("📎 Ladda ner fil");
+ downloadLink.setOnAction(e -> {
+ try {
+ Desktop.getDesktop().browse(new URI(item.attachmentUrl()));
+ } catch (Exception ex) {
+ ex.printStackTrace();
+ }
+ });
+ container.getChildren().add(downloadLink);
+ }
+
+ container.setPadding(new Insets(8));
+ container.setStyle("-fx-background-color: #f4f4f4; -fx-background-radius: 6;");
+
+ setGraphic(container);
+ }
+ }
+ });
+ }
+
+ @FXML
+ private javafx.scene.control.TextField messageInput;
+
+
+ public void sendMessage(ActionEvent actionEvent) {
+ String content = messageInput.getText();
+ model.setMessageToSend(content);
+ model.sendMessage();
+ }
+
+ @FXML
+ private void sendFile(ActionEvent event) {
+ FileChooser fileChooser = new FileChooser();
+ fileChooser.setTitle("Välj en fil att skicka");
+ File selectedFile = fileChooser.showOpenDialog(messageInput.getScene().getWindow());
+
+ if (selectedFile != null) {
+ Path filePath = selectedFile.toPath();
+ model.sendFile(filePath).thenAccept(success -> {
+ runOnFx(() -> {
+ if (success) {
+ System.out.println("Fil skickad: " + filePath.getFileName());
+ } else {
+ System.out.println("Misslyckades att skicka filen.");
+ }
+ });
+ });
+ } else {
+ System.out.println("Ingen fil vald.");
+ }
}
+
}
+
diff --git a/src/main/java/com/example/HelloFX.java b/src/main/java/com/example/HelloFX.java
index 96bdc5ca..6716ac16 100644
--- a/src/main/java/com/example/HelloFX.java
+++ b/src/main/java/com/example/HelloFX.java
@@ -13,6 +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);
+ scene.getStylesheets().add(getClass().getResource("/com/example/style.css").toExternalForm());
stage.setTitle("Hello MVC");
stage.setScene(scene);
stage.show();
diff --git a/src/main/java/com/example/HelloModel.java b/src/main/java/com/example/HelloModel.java
index 385cfd10..e0026abe 100644
--- a/src/main/java/com/example/HelloModel.java
+++ b/src/main/java/com/example/HelloModel.java
@@ -1,15 +1,79 @@
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.nio.file.Path;
+import java.util.concurrent.CompletableFuture;
+
/**
* 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;
+ receiveMessage();
+ }
+
+ 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 "Hello, 404 java not found!";
+ }
+
+ public CompletableFuture sendMessage() {
+ System.out.println("Meddelande att skicka: " + messageToSend.get());
+ return connection.send(messageToSend.get());
}
-}
+
+ public CompletableFuture sendFile(Path filePath) {
+ return connection.sendFile(filePath);
+ }
+
+ public void receiveMessage() {
+ connection.receive(m -> runOnFx(() -> messages.add(m)));
+ }
+
+ public void testAddMessage() {
+ NtfyMessageDto test = new NtfyMessageDto("id123", System.currentTimeMillis(), "message", "mytopic", "Testmeddelande", null);
+ runOnFx(() -> messages.add(test));
+ }
+
+ static void runOnFx(Runnable task) {
+ try {
+ if (Platform.isFxApplicationThread()) task.run();
+ else Platform.runLater(task);
+ } catch (IllegalStateException notInitialized) {
+ task.run();
+ }
+ }
+
+}
\ 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..4fa1bbf5
--- /dev/null
+++ b/src/main/java/com/example/ManyParameters.java
@@ -0,0 +1,18 @@
+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..8e5197eb
--- /dev/null
+++ b/src/main/java/com/example/NtfyConnection.java
@@ -0,0 +1,15 @@
+package com.example;
+
+import java.nio.file.Path;
+import java.util.concurrent.CompletableFuture;
+import java.util.function.Consumer;
+
+public interface NtfyConnection {
+
+ public CompletableFuture send(String message);
+
+ public void receive(Consumer messageHandler);
+
+ public CompletableFuture sendFile(Path path);
+
+}
\ 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..ab87dc81
--- /dev/null
+++ b/src/main/java/com/example/NtfyConnectionImpl.java
@@ -0,0 +1,97 @@
+package com.example;
+
+import io.github.cdimascio.dotenv.Dotenv;
+import tools.jackson.databind.ObjectMapper;
+
+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.Path;
+import java.util.Objects;
+import java.util.concurrent.CompletableFuture;
+import java.util.function.Consumer;
+
+public class NtfyConnectionImpl implements NtfyConnection {
+
+ private final HttpClient http = HttpClient.newBuilder()
+ .followRedirects(HttpClient.Redirect.ALWAYS)
+ .build();
+ 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 send(String message) {
+ HttpRequest httpRequest = HttpRequest.newBuilder()
+ .POST(HttpRequest.BodyPublishers.ofString(message))
+ .header("Cache", "no")
+ .uri(URI.create(hostName + "/mytopic"))
+ .build();
+
+ return http.sendAsync(httpRequest, HttpResponse.BodyHandlers.discarding())
+ .thenApply( response -> {
+ System.out.println("Statuskod: " + response.statusCode());
+ System.out.println("Your message was sent");
+ return true;
+ })
+ .exceptionally(e -> {
+ System.out.println("Error sending the message: " + e.getMessage());
+ return false;
+ });
+ }
+
+ public CompletableFuture sendFile(Path path) {
+ try {
+ String filename = path.getFileName().toString();
+
+ HttpRequest httpRequest = HttpRequest.newBuilder()
+ .PUT(HttpRequest.BodyPublishers.ofFile(path))
+ .header("Filename", filename)
+ .header("Content-Type", "application/octet-stream")
+ .uri(URI.create(hostName + "/mytopic"))
+ .build();
+
+ return http.sendAsync(httpRequest, HttpResponse.BodyHandlers.discarding())
+ .thenApply(response -> response.statusCode() == 200);
+
+ } catch (IOException e) {
+ System.out.println("Kunde inte läsa filen: " + e.getMessage());
+ return CompletableFuture.completedFuture(false);
+ }
+ }
+
+ @Override
+ public void receive(Consumer messageHandler) {
+ HttpRequest httpRequest = HttpRequest.newBuilder()
+ .GET()
+ .uri(URI.create(hostName + "/mytopic/json"))
+ .build();
+
+ http.sendAsync(httpRequest, HttpResponse.BodyHandlers.ofLines())
+ .thenAccept(response -> response.body()
+ .flatMap(line -> {
+ if (line.isBlank()) return java.util.stream.Stream.empty();
+ try {
+ NtfyMessageDto msg = mapper.readValue(line, NtfyMessageDto.class);
+ return java.util.stream.Stream.of(msg);
+ } catch (Exception e) {
+ System.err.println("Kunde inte tolka: " + line);
+ return java.util.stream.Stream.empty();
+ }
+ })
+ .filter(msg -> "message".equals(msg.event()))
+ .peek(msg -> System.out.println("Mottaget: " + msg.message()))
+ .forEach(messageHandler));
+ }
+}
\ 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..075668cf
--- /dev/null
+++ b/src/main/java/com/example/NtfyMessageDto.java
@@ -0,0 +1,17 @@
+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,
+ AttachmentDto attachment
+) {
+ public String attachmentUrl() {
+ return attachment != null ? attachment.url() : null;
+ }
+}
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..dffa05d2 100644
--- a/src/main/java/module-info.java
+++ b/src/main/java/module-info.java
@@ -1,6 +1,11 @@
module hellofx {
requires javafx.controls;
+ requires javafx.base;
requires javafx.fxml;
+ requires io.github.cdimascio.dotenv.java;
+ requires java.net.http;
+ requires tools.jackson.databind;
+ requires java.desktop;
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..f499b0cc 100644
--- a/src/main/resources/com/example/hello-view.fxml
+++ b/src/main/resources/com/example/hello-view.fxml
@@ -2,8 +2,26 @@
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/main/resources/com/example/style.css b/src/main/resources/com/example/style.css
new file mode 100644
index 00000000..d2d0e40c
--- /dev/null
+++ b/src/main/resources/com/example/style.css
@@ -0,0 +1,35 @@
+.root {
+ -fx-background-color: #1e1e2f;
+ -fx-font-family: "Segoe UI", sans-serif;
+ -fx-text-fill: #ffffff;
+}
+
+.greeting {
+ -fx-font-size: 18px;
+ -fx-text-fill: #ffffff;
+}
+
+.input {
+ -fx-background-color: #2e2e3e;
+ -fx-text-fill: #ffffff;
+ -fx-border-radius: 5;
+ -fx-background-radius: 5;
+ -fx-padding: 5 10;
+ -fx-pref-width: 250;
+}
+
+.send-button {
+ -fx-background-color: #4e8cff;
+ -fx-text-fill: white;
+ -fx-font-weight: bold;
+ -fx-background-radius: 5;
+ -fx-padding: 5 15;
+}
+
+.message-list {
+ -fx-background-color: white;
+ -fx-border-color: #cccccc;
+ -fx-border-radius: 5;
+ -fx-padding: 10;
+}
+
diff --git a/src/test/java/com/example/HelloModelTest.java b/src/test/java/com/example/HelloModelTest.java
new file mode 100644
index 00000000..f7de5a9d
--- /dev/null
+++ b/src/test/java/com/example/HelloModelTest.java
@@ -0,0 +1,92 @@
+package com.example;
+
+import com.github.tomakehurst.wiremock.junit5.WireMockRuntimeInfo;
+import com.github.tomakehurst.wiremock.junit5.WireMockTest;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+
+import java.util.concurrent.atomic.AtomicReference;
+
+import static com.github.tomakehurst.wiremock.client.WireMock.*;
+import static org.assertj.core.api.Assertions.assertThat;
+
+@WireMockTest
+class HelloModelTest {
+
+ @Test
+ @DisplayName("Given a model with messageToSend when calling sendMessage then send method on connection should be called")
+ void sendMessageCallsConnectionWithMessageToSend() {
+ //Arrange Given
+ var spy = new NtfyConnectionSpy();
+ var model = new HelloModel(spy);
+ model.setMessageToSend("Hello World");
+ //Act When
+ model.sendMessage().join();
+ //Assert Then
+ assertThat(spy.message).isEqualTo("Hello World");
+ }
+
+ @Test
+ void sendMessageToFakeServer(WireMockRuntimeInfo wmRuntimeInfo) {
+ var con = new NtfyConnectionImpl("http://localhost:" + wmRuntimeInfo.getHttpPort());
+ var model = new HelloModel(con);
+ model.setMessageToSend("Hello World");
+ stubFor(post("/mytopic").willReturn(ok()));
+
+ model.sendMessage().join();
+
+ //Verify call made to server
+ verify(postRequestedFor(urlEqualTo("/mytopic"))
+ .withRequestBody(matching("Hello World")));
+ }
+
+ @Test
+ @DisplayName("Given a registered message handler, when a message is simulated, then it should be received")
+ void receiveShouldTriggerHandler() {
+ var spy = new NtfyConnectionSpy();
+ var model = new HelloModel(spy);
+
+ AtomicReference received = new AtomicReference<>();
+ spy.receive(received::set);
+
+ NtfyMessageDto mockResponse = new NtfyMessageDto(
+ "123",
+ System.currentTimeMillis(),
+ "message",
+ "mytopic",
+ "Hej från testet!",
+ null
+ );
+
+ // Act
+ spy.simulateIncomingMessage(mockResponse);
+
+ // Assert
+ assertThat(received.get()).isEqualTo(mockResponse);
+ }
+
+ @Test
+ @DisplayName("Given HelloModel with spy connection, when a message is simulated, then it should be added to messages list")
+ void receiveShouldAddMessageToModel() {
+ new javafx.embed.swing.JFXPanel();
+ var spy = new NtfyConnectionSpy();
+ var model = new HelloModel(spy);
+
+ NtfyMessageDto mockMessage = new NtfyMessageDto(
+ "123",
+ System.currentTimeMillis(),
+ "message",
+ "mytopic",
+ "Hej från testet!",
+ null
+ );
+
+ // Act
+ spy.simulateIncomingMessage(mockMessage);
+
+ // Assert
+ assertThat(model.getMessages()).containsExactly(mockMessage);
+ }
+
+
+}
\ 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..33d2ac38
--- /dev/null
+++ b/src/test/java/com/example/NtfyConnectionSpy.java
@@ -0,0 +1,34 @@
+package com.example;
+
+import java.nio.file.Path;
+import java.util.concurrent.CompletableFuture;
+import java.util.function.Consumer;
+
+public class NtfyConnectionSpy implements NtfyConnection{
+
+ String message;
+ Consumer messageHandler;
+
+
+ @Override
+ public CompletableFuture send(String message) {
+ this.message = message;
+ return CompletableFuture.completedFuture(true);
+ }
+
+ @Override
+ public void receive(Consumer messageHandler) {
+ this.messageHandler = messageHandler;
+ }
+
+ @Override
+ public CompletableFuture sendFile(Path path) {
+ return null;
+ }
+
+ public void simulateIncomingMessage(NtfyMessageDto message) {
+ if (messageHandler != null) {
+ messageHandler.accept(message);
+ }
+ }
+}
\ No newline at end of file