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..bc863a7a 100644 --- a/pom.xml +++ b/pom.xml @@ -45,6 +45,28 @@ 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 + + + org.slf4j + slf4j-simple + 2.0.12 + test + @@ -55,7 +77,7 @@ com.example.HelloFX - + javafx true @@ -65,4 +87,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..1ce67548 100644 --- a/src/main/java/com/example/HelloController.java +++ b/src/main/java/com/example/HelloController.java @@ -1,22 +1,71 @@ package com.example; +import javafx.application.Platform; +import javafx.event.ActionEvent; import javafx.fxml.FXML; -import javafx.scene.control.Label; +import javafx.scene.control.*; +import java.time.Instant; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; -/** - * 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 Label messageLabel; + @FXML private ListView messageView; + @FXML private TextArea messageInput; + + private final DateTimeFormatter timeFormatter = + DateTimeFormatter.ofPattern("HH:mm:ss").withZone(ZoneId.systemDefault()); @FXML private void initialize() { - if (messageLabel != null) { - messageLabel.setText(model.getGreeting()); - } + //visa välkomstmeddelande + messageLabel.setText(model.getGreeting()); + + messageView.setItems(model.getMessages()); + + messageInput.textProperty().bindBidirectional(model.messageToSendProperty()); + + //formatera meddelanden i ListView + messageView.setCellFactory(lv -> new ListCell<>() { + @Override + protected void updateItem(NtfyMessageDto msg, boolean empty) { + super.updateItem(msg, empty); + if (empty || msg == null) { + setText(null); + } else { + setText("[" + timeFormatter.format(Instant.ofEpochMilli(msg.time())) + "] " + msg.message()); + } + } + }); + + // Scrolla automatiskt till senaste meddelandet + model.getMessages().addListener((javafx.collections.ListChangeListener) change -> { + while (change.next()) { + if (change.wasAdded()) { + Platform.runLater(() -> { + int size = messageView.getItems().size(); + if (size > 0) { + messageView.scrollTo(size - 1); + } + }); + } + } + }); + } + + @FXML + private void sendMessage(ActionEvent event) { + //skicka asynkront – HelloModel hanterar rensning och callback + model.sendMessageAsync(success -> { + if (!success) { + Platform.runLater(() -> { + Alert alert = new Alert(Alert.AlertType.ERROR, "Kunde inte skicka meddelandet."); + alert.show(); + }); + } + }); } -} +} \ 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..5aaa17af 100644 --- a/src/main/java/com/example/HelloFX.java +++ b/src/main/java/com/example/HelloFX.java @@ -12,7 +12,7 @@ public class HelloFX extends Application { 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 scene = new Scene(root, 1280, 480); 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..76bb9c78 100644 --- a/src/main/java/com/example/HelloModel.java +++ b/src/main/java/com/example/HelloModel.java @@ -1,15 +1,74 @@ package com.example; -/** - * Model layer: encapsulates application data and business logic. - */ +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.function.Consumer; + 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 ObservableList messages = FXCollections.observableArrayList(); + private final StringProperty messageToSend = new SimpleStringProperty(""); + + public HelloModel(NtfyConnection connection) { + this.connection = connection; + startReceiving(); + } + + private void startReceiving() { + connection.receive(incoming -> { + if (incoming == null || incoming.message() == null || incoming.message().isBlank()) { + return; + } + runOnFx(() -> messages.add(incoming)); + }); + } + + public void sendMessageAsync(Consumer callback) { + String msg = messageToSend.get(); + if (msg == null || msg.isBlank()) { + callback.accept(false); + return; + } + + try { + connection.send(msg, success -> { + if (success) { + runOnFx(() -> { + if (msg.equals(messageToSend.get())) { + messageToSend.set(""); + } + }); + callback.accept(true); + } else { + callback.accept(false); + } + }); + } catch (Exception e) { + // FÅNGA ALLA EXCEPTIONS HÄR! + System.err.println("Exception during send: " + e.getMessage()); + callback.accept(false); + } + } + + private static void runOnFx(Runnable task) { + try { + if (Platform.isFxApplicationThread()) { + task.run(); + } else { + Platform.runLater(task); + } + } catch (Exception e) { + task.run(); // fallback i tester + } + } + + public ObservableList getMessages() { return messages; } + public String getMessageToSend() { return messageToSend.get(); } + public StringProperty messageToSendProperty() { return messageToSend; } + public void setMessageToSend(String v) { messageToSend.set(v); } + public String getGreeting() { return "Welcome to ChatApp"; } +} \ 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..d956cc24 --- /dev/null +++ b/src/main/java/com/example/NtfyConnection.java @@ -0,0 +1,12 @@ +package com.example; + +import java.util.function.Consumer; + +interface NtfyConnection { + void send(String message, Consumer callback); + void receive(Consumer handler); + } + + //public boolean send(String message); + + //public void receive(Consumer messageHandler); diff --git a/src/main/java/com/example/NtfyConnectionImpl.java b/src/main/java/com/example/NtfyConnectionImpl.java new file mode 100644 index 00000000..cb50582e --- /dev/null +++ b/src/main/java/com/example/NtfyConnectionImpl.java @@ -0,0 +1,87 @@ +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.time.Duration; +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 = new ObjectMapper(); + + public NtfyConnectionImpl() { + Dotenv dotenv = Dotenv.load(); + hostName = Objects.requireNonNull(dotenv.get("HOST_NAME")); + this.http = HttpClient.newBuilder() + .connectTimeout(Duration.ofSeconds(10)) + .build(); + } + + public NtfyConnectionImpl(String hostName) { + this.hostName = hostName; + this.http = HttpClient.newBuilder() + .connectTimeout(Duration.ofSeconds(5)) + .build(); + } + + @Override + public void send(String message, Consumer callback) { + HttpRequest request = HttpRequest.newBuilder() + .POST(HttpRequest.BodyPublishers.ofString(message)) + .uri(URI.create(hostName + "/mytopic")) + .header("Cache", "no") + .timeout(Duration.ofSeconds(10)) //request timeout + .build(); + + http.sendAsync(request, HttpResponse.BodyHandlers.discarding()) + .thenAccept(response -> { + boolean success = response.statusCode() >= 200 && response.statusCode() < 300; + callback.accept(success); + }) + .exceptionally(throwable -> { + System.err.println("Error sending message: " + throwable.getMessage()); + callback.accept(false); + return null; + }); + } + + @Override + public void receive(Consumer messageHandler) { + HttpRequest request = HttpRequest.newBuilder() + .GET() + .uri(URI.create(hostName + "/mytopic/json")) + .timeout(Duration.ofSeconds(30)) //timeout för receive + .build(); + + http.sendAsync(request, HttpResponse.BodyHandlers.ofLines()) + .thenAccept(response -> { + try { + response.body() + .map(line -> { + try { + return mapper.readValue(line, NtfyMessageDto.class); + } catch (Exception e) { + System.err.println("Failed to parse message: " + line); + return null; + } + }) + .filter(Objects::nonNull) + .forEach(messageHandler); + } catch (Exception e) { + System.err.println("Stream processing error: " + e.getMessage()); + } + }) + .exceptionally(ex -> { + System.err.println("Error receiving messages: " + ex.getMessage()); + return null; + }); + } +} \ 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..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..2b046ce2 100644 --- a/src/main/java/module-info.java +++ b/src/main/java/module-info.java @@ -1,6 +1,9 @@ module hellofx { requires javafx.controls; requires javafx.fxml; + requires tools.jackson.databind; + requires io.github.cdimascio.dotenv.java; + requires java.net.http; 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..63851ee9 100644 --- a/src/main/resources/com/example/hello-view.fxml +++ b/src/main/resources/com/example/hello-view.fxml @@ -1,9 +1,30 @@ - - - - - - - + + + + + + + + + + + + + +