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;
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;
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) {
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));
+ }
}
}
}
private final int port;
private final List<Runnable> connectListeners = new ArrayList<>();
+ private final List<Consumer<JsonNode>> windowListListeners = new ArrayList<>();
}
--- /dev/null
+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) {}
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;
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;
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 {
}
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 {
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();