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 @@ - - - - + + + + + + + + + + + +