From 334bf93c1523083d14597ce1490c163ca37a10e6 Mon Sep 17 00:00:00 2001 From: David Roden Date: Mon, 5 May 2025 08:07:04 +0200 Subject: [PATCH] =?utf8?q?=E2=9C=A8=20Add=20listWindows=20to=20Client?= MIME-Version: 1.0 Content-Type: text/plain; charset=utf8 Content-Transfer-Encoding: 8bit --- .../main/java/de/qsheltier/msta/client/Client.java | 29 ++++++++++++++++++++++ .../main/java/de/qsheltier/msta/client/Window.java | 11 ++++++++ .../java/de/qsheltier/msta/client/ClientTest.java | 25 ++++++++++++++++++- .../de/qsheltier/msta/client/FakeTcpServer.java | 28 +++++++++++++++++++-- 4 files changed, 90 insertions(+), 3 deletions(-) create mode 100644 client/src/main/java/de/qsheltier/msta/client/Window.java diff --git a/client/src/main/java/de/qsheltier/msta/client/Client.java b/client/src/main/java/de/qsheltier/msta/client/Client.java index 2830534..c4a63ca 100644 --- a/client/src/main/java/de/qsheltier/msta/client/Client.java +++ b/client/src/main/java/de/qsheltier/msta/client/Client.java @@ -1,5 +1,6 @@ package de.qsheltier.msta.client; +import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import java.io.BufferedReader; import java.io.BufferedWriter; @@ -12,6 +13,8 @@ import java.net.InetSocketAddress; import java.net.Socket; import java.util.ArrayList; import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.function.Consumer; import static java.nio.charset.StandardCharsets.UTF_8; @@ -48,6 +51,28 @@ public class Client { outputWriter.flush(); } + /** + * Retrieves a list of {@link Window}s from the server. + * + * @return The list of currently on-screen windows + * @throws IOException if an I/O error occurs + * @throws InterruptedException if the thread was interrupted + */ + public List listWindows() throws IOException, InterruptedException { + var latch = new CountDownLatch(1); + var windows = new ArrayList(); + Consumer onWindowList = (JsonNode windowJson) -> { + windowJson.get("windows").elements() + .forEachRemaining(windowNode -> windows.add(new Window(windowNode.get("id").asInt(), windowNode.get("type").asText(), windowNode.get("title").asText()))); + latch.countDown(); + }; + windowListListeners.add(onWindowList); + sendCommand("list windows"); + latch.await(); + windowListListeners.remove(onWindowList); + return windows; + } + private void handleConnection(InputStream inputStream) throws IOException { try (BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, UTF_8))) { while (true) { @@ -59,6 +84,9 @@ public class Client { if (reply.has("event") && reply.get("event").asText().equalsIgnoreCase("connected")) { connectListeners.forEach(Runnable::run); } + if (reply.has("list") && reply.get("list").asText().equals("windows")) { + windowListListeners.forEach(listener -> listener.accept(reply)); + } } } } @@ -69,5 +97,6 @@ public class Client { private final int port; private final List connectListeners = new ArrayList<>(); + private final List> windowListListeners = new ArrayList<>(); } diff --git a/client/src/main/java/de/qsheltier/msta/client/Window.java b/client/src/main/java/de/qsheltier/msta/client/Window.java new file mode 100644 index 0000000..158cd51 --- /dev/null +++ b/client/src/main/java/de/qsheltier/msta/client/Window.java @@ -0,0 +1,11 @@ +package de.qsheltier.msta.client; + +/** + * Represents a top-level window in an application. + * + * @param id The internal ID of the window + * @param type The type of the window, e.g. {@code java.awt.Frame} + * @param title The title of the window, if it has a title; + * {@code null} otherwise + */ +public record Window(int id, String type, String title) {} diff --git a/client/src/test/java/de/qsheltier/msta/client/ClientTest.java b/client/src/test/java/de/qsheltier/msta/client/ClientTest.java index 16d8bc2..a48642e 100644 --- a/client/src/test/java/de/qsheltier/msta/client/ClientTest.java +++ b/client/src/test/java/de/qsheltier/msta/client/ClientTest.java @@ -1,7 +1,6 @@ package de.qsheltier.msta.client; import java.io.IOException; -import java.util.concurrent.ExecutionException; import java.util.concurrent.Executors; import java.util.concurrent.atomic.AtomicBoolean; import org.junit.jupiter.api.Test; @@ -9,6 +8,7 @@ import org.junit.jupiter.api.Timeout; import static java.util.concurrent.TimeUnit.SECONDS; import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsInAnyOrder; import static org.hamcrest.Matchers.equalTo; import static org.junit.jupiter.api.Timeout.ThreadMode.SEPARATE_THREAD; @@ -43,6 +43,29 @@ public class ClientTest { assertThat(line, equalTo("shutdown")); } + @Test + public void clientSendsCorrectCommandForListingWindows() throws Exception { + client.connect(); + server.connect().get(); + server.writeLine("{\"event\": \"connected\"}"); + var sentCommand = server.react(input -> "{\"list\":\"windows\",\"windows\": [{\"id\":1,\"type\": \"java.awt.Frame\",\"title\": \"Test Frame\"},{\"id\":2,\"type\": \"javax.swing.JFrame\",\"title\": \"Another Frame\"}]}"); + client.listWindows(); + assertThat(sentCommand.get(), equalTo("list windows")); + } + + @Test + public void clientCanListWindowsFromServer() throws Exception { + client.connect(); + server.connect().get(); + server.writeLine("{\"event\": \"connected\"}"); + server.react(input -> "{\"list\":\"windows\",\"windows\": [{\"id\":1,\"type\": \"java.awt.Frame\",\"title\": \"Test Frame\"},{\"id\":2,\"type\": \"javax.swing.JFrame\",\"title\": \"Another Frame\"}]}"); + var windows = client.listWindows(); + assertThat(windows, containsInAnyOrder( + new Window(1, "java.awt.Frame", "Test Frame"), + new Window(2, "javax.swing.JFrame", "Another Frame") + )); + } + public ClientTest() throws IOException { } diff --git a/client/src/test/java/de/qsheltier/msta/client/FakeTcpServer.java b/client/src/test/java/de/qsheltier/msta/client/FakeTcpServer.java index d4d94b1..eab4909 100644 --- a/client/src/test/java/de/qsheltier/msta/client/FakeTcpServer.java +++ b/client/src/test/java/de/qsheltier/msta/client/FakeTcpServer.java @@ -3,11 +3,10 @@ package de.qsheltier.msta.client; import java.io.Closeable; import java.io.IOException; import java.net.ServerSocket; -import java.util.List; import java.util.concurrent.ExecutorService; import java.util.concurrent.Future; import java.util.concurrent.atomic.AtomicReference; -import org.hamcrest.Matcher; +import java.util.function.Function; public class FakeTcpServer implements Closeable { @@ -44,6 +43,31 @@ public class FakeTcpServer implements Closeable { return clientSocket.get().readLine(); } + /** + * Handles a single line of input. One line is {@link #readLine() read}, + * and the result of calling the given function is + * {@link #writeLine(String...) written} as a result. + *

Example

+ *

+ * This example echoes the line being received by this FakeTcpServer, + * prefixing it with {@code "received line: "}. + *

+	 * tcpServer.react(input -> "received line: " + input);
+	 * 
+ * + * @param outputCreator The function for generating the output from the input + * @return A {@link Future} that will complete once the line has been + * read and processed; the input line will be returned + */ + public Future react(Function outputCreator) { + return executorService.submit(() -> { + var input = readLine(); + var output = outputCreator.apply(input); + clientSocket.get().writeLine(output); + return input; + }); + } + @Override public void close() throws IOException { clientSocket.get().close(); -- 2.7.4