Skip to content

Commit 4c62091

Browse files
committed
>:(
1 parent 845f3e2 commit 4c62091

9 files changed

Lines changed: 464 additions & 53 deletions

File tree

src/main/java/com/example/HelloController.java

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,27 +4,65 @@
44
import javafx.fxml.FXML;
55
import javafx.scene.control.Label;
66
import javafx.scene.control.ListView;
7+
import javafx.scene.control.TextField;
8+
import javafx.stage.FileChooser;
9+
import javafx.stage.Stage;
10+
11+
import java.io.File;
712

813
/**
914
* Controller layer: mediates between the view (FXML) and the model.
1015
*/
1116
public class HelloController {
1217

1318
private final HelloModel model = new HelloModel(new NtfyConnectionImpl());
19+
20+
@FXML
1421
public ListView<NtfyMessageDto> messageView;
1522

1623
@FXML
1724
private Label messageLabel;
1825

26+
@FXML
27+
private TextField messageInput;
28+
1929
@FXML
2030
private void initialize() {
2131
if (messageLabel != null) {
2232
messageLabel.setText(model.getGreeting());
2333
}
2434
messageView.setItems(model.getMessages());
35+
36+
37+
model.messageToSendProperty().bindBidirectional(messageInput.textProperty());
38+
39+
40+
messageView.setCellFactory(lv -> new javafx.scene.control.ListCell<NtfyMessageDto>() {
41+
@Override
42+
protected void updateItem(NtfyMessageDto item, boolean empty) {
43+
super.updateItem(item, empty);
44+
if (empty || item == null) {
45+
setText(null);
46+
} else {
47+
setText(item.message() != null ? item.message() : "(No message content)");
48+
}
49+
}
50+
});
2551
}
2652

2753
public void sendMessage(ActionEvent actionEvent) {
2854
model.sendMessage();
55+
56+
messageInput.clear();
57+
}
58+
59+
public void attachFile(ActionEvent actionEvent) {
60+
FileChooser fileChooser = new FileChooser();
61+
fileChooser.setTitle("Select File to Attach");
62+
File file = fileChooser.showOpenDialog(new Stage());
63+
64+
if (file != null) {
65+
model.sendFile(file);
66+
}
2967
}
3068
}

src/main/java/com/example/HelloFX.java

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,12 @@ public void start(Stage stage) throws Exception {
1313
FXMLLoader fxmlLoader = new FXMLLoader(HelloFX.class.getResource("hello-view.fxml"));
1414
Parent root = fxmlLoader.load();
1515
Scene scene = new Scene(root, 640, 480);
16-
stage.setTitle("Hello MVC");
16+
stage.setTitle("JavaFX Chat App");
1717
stage.setScene(scene);
1818
stage.show();
1919
}
2020

2121
public static void main(String[] args) {
2222
launch();
2323
}
24-
2524
}

src/main/java/com/example/HelloModel.java

Lines changed: 33 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,19 +6,20 @@
66
import javafx.collections.FXCollections;
77
import javafx.collections.ObservableList;
88

9+
import java.io.File;
10+
911
/**
1012
* Model layer: encapsulates application data and business logic.
1113
*/
1214
public class HelloModel {
1315

1416
private final NtfyConnection connection;
15-
1617
private final ObservableList<NtfyMessageDto> messages = FXCollections.observableArrayList();
1718
private final StringProperty messageToSend = new SimpleStringProperty();
1819

1920
public HelloModel(NtfyConnection connection) {
2021
this.connection = connection;
21-
receiveMessage();
22+
startReceivingMessages();
2223
}
2324

2425
public ObservableList<NtfyMessageDto> getMessages() {
@@ -43,15 +44,40 @@ public void setMessageToSend(String message) {
4344
public String getGreeting() {
4445
String javaVersion = System.getProperty("java.version");
4546
String javafxVersion = System.getProperty("javafx.version");
46-
return "Hello, JavaFX " + javafxVersion + ", running on Java " + javaVersion + ".";
47+
return "Welcome to JavaFX Chat App " + javafxVersion + ", running on Java " + javaVersion + ".";
48+
}
49+
50+
public boolean sendMessage() {
51+
String message = getMessageToSend();
52+
if (message == null || message.trim().isEmpty()) {
53+
return false;
54+
}
55+
return connection.send(message.trim());
4756
}
4857

49-
public void sendMessage() {
50-
connection.send(messageToSend.get());
58+
public boolean sendFile(File file) {
59+
if (file == null || !file.exists()) {
60+
return false;
61+
}
62+
return connection.sendFile(file);
63+
}
64+
65+
private void startReceivingMessages() {
66+
connection.receive(this::addMessageToUI);
67+
}
5168

69+
private void addMessageToUI(NtfyMessageDto message) {
70+
// Check if we're on JavaFX application thread, if not use Platform.runLater
71+
if (Platform.isFxApplicationThread()) {
72+
messages.add(message);
73+
} else {
74+
Platform.runLater(() -> messages.add(message));
75+
}
5276
}
5377

54-
public void receiveMessage() {
55-
connection.receive(m -> Platform.runLater(() -> messages.add(m)));
78+
// Test helper method - package private for testing
79+
void addTestMessage(NtfyMessageDto message) {
80+
// Direct add for testing (bypasses Platform.runLater)
81+
messages.add(message);
5682
}
5783
}
Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
11
package com.example;
22

3+
import java.io.File;
34
import java.util.function.Consumer;
45

56
public interface NtfyConnection {
6-
7-
public boolean send(String message);
8-
9-
public void receive(Consumer<NtfyMessageDto> messageHandler);
10-
7+
boolean send(String message);
8+
void receive(Consumer<NtfyMessageDto> messageHandler);
9+
boolean sendFile(File file);
1110
}

src/main/java/com/example/NtfyConnectionImpl.java

Lines changed: 132 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -3,63 +3,166 @@
33
import io.github.cdimascio.dotenv.Dotenv;
44
import tools.jackson.databind.ObjectMapper;
55

6+
import java.io.File;
67
import java.io.IOException;
78
import java.net.URI;
89
import java.net.http.HttpClient;
910
import java.net.http.HttpRequest;
1011
import java.net.http.HttpResponse;
12+
import java.nio.file.Files;
13+
import java.util.Base64;
1114
import java.util.Objects;
1215
import java.util.function.Consumer;
1316

1417
public class NtfyConnectionImpl implements NtfyConnection {
1518

16-
private final HttpClient http = HttpClient.newHttpClient();
19+
private final HttpClient http;
1720
private final String hostName;
18-
private final ObjectMapper mapper = new ObjectMapper();
21+
private final ObjectMapper mapper;
1922

2023
public NtfyConnectionImpl() {
21-
Dotenv dotenv = Dotenv.load();
22-
hostName = Objects.requireNonNull(dotenv.get("HOST_NAME"));
24+
this(HttpClient.newHttpClient(), new ObjectMapper(), loadHostNameFromEnv());
2325
}
2426

2527
public NtfyConnectionImpl(String hostName) {
28+
this(HttpClient.newHttpClient(), new ObjectMapper(), hostName);
29+
}
30+
31+
32+
NtfyConnectionImpl(HttpClient http, ObjectMapper mapper, String hostName) {
33+
this.http = http;
34+
this.mapper = mapper;
2635
this.hostName = hostName;
2736
}
2837

38+
private static String loadHostNameFromEnv() {
39+
Dotenv dotenv = Dotenv.load();
40+
return Objects.requireNonNull(dotenv.get("HOST_NAME"));
41+
}
42+
2943
@Override
3044
public boolean send(String message) {
31-
HttpRequest httpRequest = HttpRequest.newBuilder()
32-
.POST(HttpRequest.BodyPublishers.ofString(message))
33-
.header("Cache", "no")
34-
.uri(URI.create(hostName + "/mytopic"))
35-
.build();
45+
if (message == null || message.trim().isEmpty()) {
46+
return false;
47+
}
48+
49+
try {
50+
51+
String jsonPayload = String.format("{\"message\":\"%s\"}", escapeJson(message.trim()));
52+
53+
HttpRequest httpRequest = HttpRequest.newBuilder()
54+
.POST(HttpRequest.BodyPublishers.ofString(jsonPayload))
55+
.header("Content-Type", "application/json")
56+
.header("Cache", "no-cache")
57+
.uri(URI.create(hostName + "/mytopic"))
58+
.build();
59+
60+
61+
http.sendAsync(httpRequest, HttpResponse.BodyHandlers.discarding())
62+
.thenAccept(response -> {
63+
if (response.statusCode() == 200) {
64+
System.out.println("Message sent successfully");
65+
} else {
66+
System.out.println("Failed to send message. Status: " + response.statusCode());
67+
}
68+
})
69+
.exceptionally(ex -> {
70+
System.out.println("Error sending message: " + ex.getMessage());
71+
return null;
72+
});
73+
74+
return true;
75+
} catch (Exception e) {
76+
System.out.println("Error in send: " + e.getMessage());
77+
return false;
78+
}
79+
}
80+
81+
@Override
82+
public boolean sendFile(File file) {
83+
if (file == null || !file.exists()) {
84+
return false;
85+
}
86+
3687
try {
37-
//Todo: handle long blocking send requests to not freeze the JavaFX thread
38-
//1. Use thread send message?
39-
//2. Use async?
40-
var reponse = http.send(httpRequest, HttpResponse.BodyHandlers.discarding());
88+
byte[] fileContent = Files.readAllBytes(file.toPath());
89+
String base64Content = Base64.getEncoder().encodeToString(fileContent);
90+
91+
92+
String jsonPayload = String.format(
93+
"{\"message\":\"File: %s\", \"file\":\"%s\", \"filename\":\"%s\"}",
94+
escapeJson(file.getName()), base64Content, escapeJson(file.getName())
95+
);
96+
97+
HttpRequest httpRequest = HttpRequest.newBuilder()
98+
.POST(HttpRequest.BodyPublishers.ofString(jsonPayload))
99+
.header("Content-Type", "application/json")
100+
.header("Cache", "no-cache")
101+
.uri(URI.create(hostName + "/mytopic"))
102+
.build();
103+
104+
http.sendAsync(httpRequest, HttpResponse.BodyHandlers.discarding())
105+
.thenAccept(response -> {
106+
if (response.statusCode() == 200) {
107+
System.out.println("File sent successfully: " + file.getName());
108+
} else {
109+
System.out.println("Failed to send file. Status: " + response.statusCode());
110+
}
111+
})
112+
.exceptionally(ex -> {
113+
System.out.println("Error sending file: " + ex.getMessage());
114+
return null;
115+
});
116+
41117
return true;
42118
} catch (IOException e) {
43-
System.out.println("Error sending message");
44-
} catch (InterruptedException e) {
45-
System.out.println("Interruped sending message");
119+
System.out.println("Error reading file: " + e.getMessage());
120+
return false;
46121
}
47-
return false;
48122
}
49123

50124
@Override
51125
public void receive(Consumer<NtfyMessageDto> messageHandler) {
52-
HttpRequest httpRequest = HttpRequest.newBuilder()
53-
.GET()
54-
.uri(URI.create(hostName + "/mytopic/json"))
55-
.build();
56-
57-
http.sendAsync(httpRequest, HttpResponse.BodyHandlers.ofLines())
58-
.thenAccept(response -> response.body()
59-
.map(s ->
60-
mapper.readValue(s, NtfyMessageDto.class))
61-
.filter(message -> message.event().equals("message"))
62-
.peek(System.out::println)
63-
.forEach(messageHandler));
126+
try {
127+
HttpRequest httpRequest = HttpRequest.newBuilder()
128+
.GET()
129+
.uri(URI.create(hostName + "/mytopic/json"))
130+
.build();
131+
132+
http.sendAsync(httpRequest, HttpResponse.BodyHandlers.ofLines())
133+
.thenAccept(response -> {
134+
if (response.statusCode() == 200) {
135+
response.body()
136+
.map(s -> {
137+
try {
138+
return mapper.readValue(s, NtfyMessageDto.class);
139+
} catch (Exception e) {
140+
System.out.println("Error parsing message: " + e.getMessage());
141+
return null;
142+
}
143+
})
144+
.filter(Objects::nonNull)
145+
.filter(message -> "message".equals(message.event()))
146+
.forEach(messageHandler);
147+
} else {
148+
System.out.println("Failed to receive messages. Status: " + response.statusCode());
149+
}
150+
})
151+
.exceptionally(ex -> {
152+
System.out.println("Error receiving messages: " + ex.getMessage());
153+
return null; // Lade till return statement här
154+
});
155+
} catch (Exception e) {
156+
System.out.println("Error in receive: " + e.getMessage());
157+
}
158+
}
159+
160+
private String escapeJson(String text) {
161+
if (text == null) return "";
162+
return text.replace("\\", "\\\\")
163+
.replace("\"", "\\\"")
164+
.replace("\n", "\\n")
165+
.replace("\r", "\\r")
166+
.replace("\t", "\\t");
64167
}
65168
}

0 commit comments

Comments
 (0)