diff --git a/mvnw b/mvnw
old mode 100644
new mode 100755
diff --git a/pom.xml b/pom.xml
index c40f667e..83565979 100644
--- a/pom.xml
+++ b/pom.xml
@@ -1,7 +1,8 @@
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
+ http://maven.apache.org/xsd/maven-4.0.0.xsd">
4.0.0
com.example
@@ -15,7 +16,9 @@
3.27.6
5.20.0
25
+ 2.19.2
+
org.junit.jupiter
@@ -35,6 +38,12 @@
${mockito.version}
test
+
+ org.wiremock
+ wiremock
+ 4.0.0-beta.15
+ test
+
org.openjfx
javafx-controls
@@ -45,7 +54,28 @@
javafx-fxml
${javafx.version}
+
+ io.github.cdimascio
+ dotenv-java
+ 3.2.0
+
+
+ com.fasterxml.jackson.core
+ jackson-annotations
+ ${jackson.version}
+
+
+ com.fasterxml.jackson.core
+ jackson-core
+ ${jackson.version}
+
+
+ com.fasterxml.jackson.core
+ jackson-databind
+ ${jackson.version}
+
+
@@ -55,7 +85,7 @@
com.example.HelloFX
-
+
javafx
true
@@ -66,3 +96,4 @@
+
diff --git a/src/main/java/com/example/HelloController.java b/src/main/java/com/example/HelloController.java
index fdd160a0..34c691d4 100644
--- a/src/main/java/com/example/HelloController.java
+++ b/src/main/java/com/example/HelloController.java
@@ -1,22 +1,55 @@
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 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
private Label messageLabel;
+ @FXML
+ private ListView messageView;
+
+ @FXML
+ private TextField inputMessage;
+
@FXML
private void initialize() {
- if (messageLabel != null) {
- messageLabel.setText(model.getGreeting());
+ messageLabel.setText(model.getGreeting());
+
+
+ messageView.setItems(model.getMessages());
+ }
+
+ @FXML
+ private void sendMessage(ActionEvent event) {
+ String text = inputMessage.getText();
+
+
+ model.setMessageToSend(text);
+ model.sendMessage();
+
+
+ inputMessage.clear();
+ }
+
+ @FXML
+ private void attachFile(ActionEvent event) {
+ FileChooser chooser = new FileChooser();
+ File file = chooser.showOpenDialog(messageView.getScene().getWindow());
+ if (file != null) {
+ model.sendFile(file);
}
}
}
+
+
diff --git a/src/main/java/com/example/HelloModel.java b/src/main/java/com/example/HelloModel.java
index 385cfd10..a0914345 100644
--- a/src/main/java/com/example/HelloModel.java
+++ b/src/main/java/com/example/HelloModel.java
@@ -1,15 +1,63 @@
package com.example;
-/**
- * Model layer: encapsulates application data and business logic.
- */
+import javafx.application.Platform;
+import javafx.collections.FXCollections;
+import javafx.collections.ObservableList;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
+
public class HelloModel {
- /**
- * Returns a greeting based on the current Java and JavaFX versions.
- */
+
+ private final NtfyConnection connection;
+ private final ObservableList messages = FXCollections.observableArrayList();
+
+ private String messageToSend = "";
+
+ public HelloModel(NtfyConnection connection) {
+ this.connection = connection;
+
+ messages.add(new NtfyMessageDto("init", 0, "message", "mytopic", "Initial message"));
+
+ receiveMessages();
+ }
+
+ public ObservableList getMessages() {
+ return messages;
+ }
+
+ public String getMessageToSend() {
+ return messageToSend;
+ }
+
+ public void setMessageToSend(String messageToSend) {
+ this.messageToSend = messageToSend;
+ }
+
+ public void sendMessage() {
+ if (messageToSend != null && !messageToSend.isBlank()) {
+ connection.send(messageToSend);
+ }
+ messageToSend = "";
+ }
+
+ public void sendFile(File file) {
+ try {
+ byte[] data = Files.readAllBytes(file.toPath());
+ connection.sendFile(file.getName(), data);
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+
+ public void receiveMessages() {
+ connection.receive(m -> Platform.runLater(() -> messages.add(m)));
+ }
+
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, JavaFX!";
}
}
+
+
diff --git a/src/main/java/com/example/ManyParameters.java b/src/main/java/com/example/ManyParameters.java
new file mode 100644
index 00000000..ee2e2a18
--- /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")
+ .setTimeout(10)
+ .setSize(0)
+ .createManyParameters();
+ }
+}
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..deedc6dc
--- /dev/null
+++ b/src/main/java/com/example/NtfyConnection.java
@@ -0,0 +1,33 @@
+package com.example;
+
+import java.util.function.Consumer;
+
+public interface NtfyConnection {
+
+ /**
+ * Skickar ett textmeddelande till servern.
+ *
+ * @param message meddelandet som ska skickas
+ * @return true om det lyckades, false annars
+ */
+ boolean send(String message);
+
+ /**
+ * Registrerar en mottagare som hanterar inkommande meddelanden.
+ *
+ * @param messageHandler funktion som hanterar inkommande NtfyMessageDto
+ */
+ void receive(Consumer messageHandler);
+
+ /**
+ * Skickar en fil till servern.
+ *
+ * @param filename namnet på filen
+ * @param data innehållet i filen som byte-array
+ */
+ void sendFile(String filename, byte[] data);
+}
+
+
+
+
diff --git a/src/main/java/com/example/NtfyConnectionImpl.java b/src/main/java/com/example/NtfyConnectionImpl.java
new file mode 100644
index 00000000..98fdc9b7
--- /dev/null
+++ b/src/main/java/com/example/NtfyConnectionImpl.java
@@ -0,0 +1,101 @@
+package com.example;
+
+import io.github.cdimascio.dotenv.Dotenv;
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+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.util.Objects;
+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 boolean send(String message) {
+ HttpRequest httpRequest = HttpRequest.newBuilder()
+ .POST(HttpRequest.BodyPublishers.ofString(message))
+ .header("Cache", "no")
+ .uri(URI.create(hostName + "/mytopic"))
+ .build();
+
+ try {
+ http.send(httpRequest, HttpResponse.BodyHandlers.discarding());
+ return true;
+ } catch (IOException | InterruptedException e) {
+ e.printStackTrace();
+ return false;
+ }
+ }
+
+ @Override
+ public void receive(Consumer messageHandler) {
+ HttpRequest httpRequest = HttpRequest.newBuilder()
+ .GET()
+ .uri(URI.create(hostName + "/mytopic/json"))
+ .build();
+
+ try {
+ http.send(httpRequest, HttpResponse.BodyHandlers.ofLines())
+ .body()
+ .map(s -> {
+ try {
+ return mapper.readValue(s, NtfyMessageDto.class);
+ } catch (IOException e) {
+ return null;
+ }
+ })
+ .filter(Objects::nonNull)
+ .filter(message -> "message".equals(message.event()))
+ .forEach(messageHandler);
+
+ } catch (IOException | InterruptedException e) {
+ e.printStackTrace();
+ }
+ }
+
+ @Override
+ public void sendFile(String filename, byte[] data) {
+ HttpRequest request = HttpRequest.newBuilder()
+ .POST(HttpRequest.BodyPublishers.ofByteArray(data))
+ .header("Content-Type", "application/octet-stream")
+ .header("Title", filename)
+ .uri(URI.create(hostName + "/mytopic"))
+ .build();
+
+ try {
+ http.send(request, HttpResponse.BodyHandlers.discarding());
+ } catch (IOException | InterruptedException e) {
+ e.printStackTrace();
+ }
+ }
+
+ public void sendAsync(String message) {
+ HttpRequest httpRequest = HttpRequest.newBuilder()
+ .POST(HttpRequest.BodyPublishers.ofString(message))
+ .header("Cache", "no")
+ .uri(URI.create(hostName + "/mytopic"))
+ .build();
+
+ http.sendAsync(httpRequest, HttpResponse.BodyHandlers.discarding())
+ .thenAccept(response -> System.out.println("Message sent async"))
+ .exceptionally(e -> { e.printStackTrace(); return null; });
+ }
+}
+
+
diff --git a/src/main/java/com/example/NtfyMessageDto.java b/src/main/java/com/example/NtfyMessageDto.java
new file mode 100644
index 00000000..697cccac
--- /dev/null
+++ b/src/main/java/com/example/NtfyMessageDto.java
@@ -0,0 +1,7 @@
+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) {
+}
\ 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..bef07b70 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 javafx.graphics;
+ requires com.fasterxml.jackson.databind;
opens com.example to javafx.fxml;
exports com.example;
diff --git a/src/main/resources/background.png b/src/main/resources/background.png
new file mode 100644
index 00000000..65e8e29a
Binary files /dev/null and b/src/main/resources/background.png differ
diff --git a/src/main/resources/com/example/hello-view.fxml b/src/main/resources/com/example/hello-view.fxml
index 20a7dc82..bfed5722 100644
--- a/src/main/resources/com/example/hello-view.fxml
+++ b/src/main/resources/com/example/hello-view.fxml
@@ -1,9 +1,28 @@
-
+
+
+
+
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/main/resources/css/style.css b/src/main/resources/css/style.css
new file mode 100644
index 00000000..6e3e3f34
--- /dev/null
+++ b/src/main/resources/css/style.css
@@ -0,0 +1,11 @@
+.root {
+ -fx-background-image: url("../background.png");
+ -fx-background-size: cover;
+}
+.outlined-text {
+ -fx-front-size:40px;
+ -fx-stroke: white;
+ -fx-fill: black;
+ -fx-stroke-width: 1px
+
+}
\ 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..10bc6870
--- /dev/null
+++ b/src/test/java/com/example/HelloModelTest.java
@@ -0,0 +1,84 @@
+package com.example;
+
+import com.github.tomakehurst.wiremock.client.WireMock;
+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 static com.github.tomakehurst.wiremock.client.WireMock.*;
+import static org.assertj.core.api.Assertions.*;
+
+@WireMockTest
+class HelloModelTest {
+
+ @Test
+ @DisplayName("Model should pass the text to the connection when sending a message")
+ void modelDelegatesSendToConnection() {
+ var fakeConnection = new NtfyConnectionSpy();
+ var model = new HelloModel(fakeConnection);
+
+ model.setMessageToSend("Ping!");
+ model.sendMessage();
+
+ assertThat(fakeConnection.message)
+ .as("Message should be forwarded to NtfyConnection")
+ .isEqualTo("Ping!");
+ }
+
+ @Test
+ @DisplayName("Integration: sending a message should hit /mytopic on a mock server")
+ void sendMessageReachesMockServer(WireMockRuntimeInfo wm) {
+ var baseUrl = "http://localhost:" + wm.getHttpPort();
+ var con = new NtfyConnectionImpl(baseUrl);
+ var model = new HelloModel(con);
+
+ stubFor(post("/mytopic").willReturn(aResponse().withStatus(200)));
+
+ model.setMessageToSend("Hello from test");
+ model.sendMessage();
+
+ verify(postRequestedFor(urlPathEqualTo("/mytopic"))
+ .withRequestBody(matching(".*Hello from test.*")));
+ }
+
+ @Test
+ @DisplayName("Model should contain an initial message after construction")
+ void initialMessagesListContainsWelcomeMessage() {
+ var spy = new NtfyConnectionSpy();
+ var model = new HelloModel(spy);
+
+ assertThat(model.getMessages())
+ .as("Model should initialize with one default message")
+ .hasSize(1);
+ }
+
+ @Test
+ @DisplayName("Message field should reset to empty after successful send")
+ void messageFieldClearsAfterSend() {
+ var dummyConnection = new NtfyConnectionSpy();
+ var model = new HelloModel(dummyConnection);
+
+ model.setMessageToSend("Reset me!");
+ model.sendMessage();
+
+ assertThat(model.getMessageToSend())
+ .as("Message input should be cleared after send")
+ .isEmpty();
+ }
+
+ @Test
+ @DisplayName("Sending blank or empty text should not call the connection")
+ void emptyMessageShouldNotBeSent() {
+ var spy = new NtfyConnectionSpy();
+ var model = new HelloModel(spy);
+
+ model.setMessageToSend(" ");
+ model.sendMessage();
+
+ assertThat(spy.message)
+ .as("Connection should not be called with blank input")
+ .isNullOrEmpty();
+ }
+}
+
diff --git a/src/test/java/com/example/NtfyConnectionSpy.java b/src/test/java/com/example/NtfyConnectionSpy.java
new file mode 100644
index 00000000..43bf317d
--- /dev/null
+++ b/src/test/java/com/example/NtfyConnectionSpy.java
@@ -0,0 +1,50 @@
+package com.example;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.function.Consumer;
+
+public class NtfyConnectionSpy implements NtfyConnection {
+
+
+ public String message;
+ public String fileName;
+ public byte[] fileData;
+
+
+ private final List> receivers = new ArrayList<>();
+
+ @Override
+ public boolean send(String message) {
+ this.message = message;
+ return true;
+ }
+
+ @Override
+ public void sendFile(String fileName, byte[] data) {
+ this.fileName = fileName;
+ this.fileData = data;
+ }
+
+ @Override
+ public void receive(Consumer handler) {
+ receivers.add(handler);
+ }
+
+ /**
+ * Hjälpmetod för test: trigga ett fejk-meddelande
+ */
+ public void simulateIncomingMessage(NtfyMessageDto dto) {
+ receivers.forEach(handler -> handler.accept(dto));
+ }
+
+ /**
+ * Reset mellan tester (om du vill)
+ */
+ public void reset() {
+ message = null;
+ fileName = null;
+ fileData = null;
+ receivers.clear();
+ }
+}