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