Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 31 additions & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -45,24 +45,54 @@
<artifactId>javafx-fxml</artifactId>
<version>${javafx.version}</version>
</dependency>


</dependencies>
<build>
<plugins>

<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.11.0</version>
<configuration>
<source>25</source>
<target>25</target>
<release>25</release>
</configuration>
</plugin>

<plugin>
<groupId>org.openjfx</groupId>
<artifactId>javafx-maven-plugin</artifactId>
<version>0.0.8</version>
<configuration>
<mainClass>com.example.HelloFX</mainClass>
<options>
<option>--enable-native-access=javafx.graphics</option>
<option>--enable-native-access=javafx.graphics</option>
</options>
<launcher>javafx</launcher>
<stripDebug>true</stripDebug>
<noHeaderFiles>true</noHeaderFiles>
<noManPages>true</noManPages>
</configuration>
</plugin>

<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.1.2</version>
<configuration>
<useModulePath>false</useModulePath>
<includes>
<include>**/*Test.java</include>
</includes>
</configuration>
</plugin>

</plugins>



</build>
</project>
42 changes: 42 additions & 0 deletions src/main/java/com/example/ChatController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package com.example;

import javafx.fxml.FXML;
import javafx.scene.control.ListView;
import javafx.scene.control.TextField;
import javafx.application.Platform;

public class ChatController {

@FXML
private ListView<String> messagesList;

@FXML
private TextField inputField;

private final ChatModel model = new ChatModel();


@FXML
private void onSend() {
String message = inputField.getText().trim();
if (!message.isEmpty()) {
messagesList.getItems().add("Me: " + message);
model.sendMessage(message);
inputField.clear();
}
}


@FXML
private void initialize(){
model.subscribe(msg -> {
Platform.runLater(() -> messagesList.getItems().add("Friend: " + msg));
});
}






}
120 changes: 120 additions & 0 deletions src/main/java/com/example/ChatModel.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
package com.example;

import javafx.application.Platform;

import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.util.Collections;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Consumer;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class ChatModel {

private final String sendUrl;
private final String subscribeUrl;
private final String clientId;
private final Set<String> sentMessages = Collections.newSetFromMap(new ConcurrentHashMap<>());
private final HttpClient httpClient;
private final Consumer<Runnable> platformRunner;

public ChatModel() {
this(HttpClient.newHttpClient(), Platform::runLater);
}

public ChatModel(HttpClient httpClient, Consumer<Runnable> platformRunner) {
String topic = System.getenv().getOrDefault("NTFY_TOPIC", "https://ntfy.sh/newchatroom3");
this.sendUrl = topic;
this.subscribeUrl = topic + "/sse";
this.clientId = UUID.randomUUID().toString();
this.httpClient = httpClient;
this.platformRunner = platformRunner;
}

public void sendMessage(String message) {
sentMessages.add(message);

new Thread(() -> {
try {
Thread.sleep(5000);
} catch (InterruptedException ignored) {
}
sentMessages.remove(message);
}, "SentMessageCleaner").start();

HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(sendUrl))
.header("Content-Type", "application/json")
.header("X-Client-ID", clientId)
.header("Title", "Friend: ")
.POST(HttpRequest.BodyPublishers.ofString(message))
.build();

httpClient.sendAsync(request, HttpResponse.BodyHandlers.ofString())
.thenAccept(response -> System.out.println("Sent, status=" + response.statusCode()))
.exceptionally(e -> {
e.printStackTrace();
return null;
});
}


public void subscribe(Consumer<String> onMessageReceived) {
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(subscribeUrl))
.header("Accept", "text/event-stream")
.build();

httpClient.sendAsync(request, HttpResponse.BodyHandlers.ofInputStream())
.thenAccept(response -> {
try (BufferedReader reader = new BufferedReader(new InputStreamReader(response.body()))) {
String line;
// boolean firstMessageSkipped = false;
while ((line = reader.readLine()) != null) {
if (line.startsWith("data:")) {
// if (!firstMessageSkipped) {
// firstMessageSkipped = true;
// continue;
// }

String raw = line.substring(5).trim();
String msg = parseMessage(raw);

if (msg != null && !sentMessages.contains(msg)) {
platformRunner.accept(() -> onMessageReceived.accept(msg));
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
})
.exceptionally(e -> { e.printStackTrace(); return null; });
}

public String parseMessage(String data) {
try {
Matcher eventMatcher = Pattern.compile("\"event\"\\s*:\\s*\"(.*?)\"").matcher(data);
if (eventMatcher.find()) {
String event = eventMatcher.group(1);
if (!"message".equals(event)) {
return null;
}
}

Matcher msgMatcher = Pattern.compile("\"message\"\\s*:\\s*\"(.*?)\"").matcher(data);
if (msgMatcher.find()) {
return msgMatcher.group(1).replace("\\\"", "\"");
}
} catch (Exception ignored) {}
return null;
}

}
4 changes: 2 additions & 2 deletions src/main/java/com/example/HelloFX.java
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,10 @@ public class HelloFX extends Application {

@Override
public void start(Stage stage) throws Exception {
FXMLLoader fxmlLoader = new FXMLLoader(HelloFX.class.getResource("hello-view.fxml"));
FXMLLoader fxmlLoader = new FXMLLoader(HelloFX.class.getResource("chat-view.fxml"));
Parent root = fxmlLoader.load();
Scene scene = new Scene(root, 640, 480);
stage.setTitle("Hello MVC");
stage.setTitle("Chat App");
stage.setScene(scene);
stage.show();
}
Expand Down
2 changes: 2 additions & 0 deletions src/main/java/module-info.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
module hellofx {
requires javafx.controls;
requires javafx.fxml;
requires java.net.http;


opens com.example to javafx.fxml;
exports com.example;
Expand Down
24 changes: 24 additions & 0 deletions src/main/resources/com/example/chat-view.fxml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="UTF-8"?>

<?import javafx.scene.layout.BorderPane?>
<?import javafx.scene.control.ListView?>
<?import javafx.scene.control.TextField?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.layout.HBox?>

<BorderPane xmlns="http://javafx.com/javafx"
xmlns:fx="http://javafx.com/fxml"
fx:controller="com.example.ChatController">

<center>
<ListView fx:id="messagesList"/>
</center>

<bottom>
<HBox spacing="8" style="-fx-padding: 10;">
<TextField fx:id="inputField" HBox.hgrow="ALWAYS"/>
<Button text="Send" onAction="#onSend"/>
</HBox>
</bottom>

</BorderPane>
75 changes: 75 additions & 0 deletions src/test/java/com/example/ChatModelTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package com.example;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.atomic.AtomicReference;

import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*;

class ChatModelTest {

private HttpClient mockHttpClient;
private ChatModel chatModel;

@BeforeEach
void setUp() {
mockHttpClient = mock(HttpClient.class);
chatModel = new ChatModel(mockHttpClient, Runnable::run);
}

@Test
void testParseMessageReturnsMessage() {
String data = "{\"event\":\"message\",\"message\":\"Hello World\"}";
String result = chatModel.parseMessage(data);
assertEquals("Hello World", result);
}

@Test
void testParseMessageIgnoresNonMessageEvent() {
String data = "{\"event\":\"update\",\"message\":\"Hello World\"}";
String result = chatModel.parseMessage(data);
assertNull(result);
}

@Test
void testSendMessageCallsHttpClient() throws Exception {
HttpResponse<String> mockResponse = mock(HttpResponse.class);
when(mockResponse.statusCode()).thenReturn(200);

CompletableFuture<HttpResponse<String>> future = CompletableFuture.completedFuture(mockResponse);
when(mockHttpClient.sendAsync(any(HttpRequest.class), any(HttpResponse.BodyHandler.class)))
.thenReturn(future);

chatModel.sendMessage("Hi there");

Thread.sleep(100);

verify(mockHttpClient, times(1))
.sendAsync(any(HttpRequest.class), any(HttpResponse.BodyHandler.class));
}

@Test
void testSubscribeCallsConsumer() throws Exception {
String sseData = "data:{\"event\":\"message\",\"message\":\"Hello\"}\n";
HttpResponse<java.io.InputStream> mockResponse = mock(HttpResponse.class);
when(mockResponse.body()).thenReturn(new java.io.ByteArrayInputStream(sseData.getBytes()));

CompletableFuture<HttpResponse<java.io.InputStream>> future = CompletableFuture.completedFuture(mockResponse);
when(mockHttpClient.sendAsync(any(HttpRequest.class), any(HttpResponse.BodyHandler.class)))
.thenReturn(future);

AtomicReference<String> received = new AtomicReference<>();
chatModel.subscribe(received::set);

Thread.sleep(100);

assertEquals("Hello", received.get());
}
}
Loading