✨ Add listWindows to Client
authorDavid Roden <github-a8in@qsheltier.de>
Mon, 5 May 2025 06:07:04 +0000 (08:07 +0200)
committerDavid Roden <github-a8in@qsheltier.de>
Mon, 5 May 2025 06:07:04 +0000 (08:07 +0200)
client/src/main/java/de/qsheltier/msta/client/Client.java
client/src/main/java/de/qsheltier/msta/client/Window.java [new file with mode: 0644]
client/src/test/java/de/qsheltier/msta/client/ClientTest.java
client/src/test/java/de/qsheltier/msta/client/FakeTcpServer.java

index 2830534..c4a63ca 100644 (file)
@@ -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<Window> listWindows() throws IOException, InterruptedException {
+               var latch = new CountDownLatch(1);
+               var windows = new ArrayList<Window>();
+               Consumer<JsonNode> 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<Runnable> connectListeners = new ArrayList<>();
+       private final List<Consumer<JsonNode>> 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 (file)
index 0000000..158cd51
--- /dev/null
@@ -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) {}
index 16d8bc2..a48642e 100644 (file)
@@ -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 {
        }
 
index d4d94b1..eab4909 100644 (file)
@@ -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.
+        * <h2>Example</h2>
+        * <p>
+        * This example echoes the line being received by this FakeTcpServer,
+        * prefixing it with {@code "received line: "}.
+        * <pre>
+        * tcpServer.react(input -> "received line: " + input);
+        * </pre>
+        *
+        * @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<String> react(Function<String, String> 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();