summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorBen Culkin <scorpress@gmail.com>2023-06-23 19:48:38 -0400
committerBen Culkin <scorpress@gmail.com>2023-06-23 19:48:38 -0400
commit2df02c35b70f7e8077832470de9594b657f1be67 (patch)
tree4353ce1f78571e038bbe8fed62d321c77a7b868c
parent4a96d9cad446ea405b51dfeebb01a1b6d7f6fb2b (diff)
Add terminal
This is some functionality based on the way that MVS/other IBM OSes handle their UI
-rw-r--r--base/src/examples/java/bjc/utils/examples/cli/StreamTerminalExample.java11
-rw-r--r--base/src/main/java/bjc/utils/cli/StreamTerminal.java152
-rw-r--r--base/src/main/java/bjc/utils/cli/Terminal.java51
-rw-r--r--base/src/main/java/bjc/utils/cli/TerminalCodes.java36
-rw-r--r--base/src/main/java/bjc/utils/funcutils/StringUtils.java28
-rw-r--r--base/src/test/java/bjc/utils/test/cli/StreamTerminalTest.java86
6 files changed, 364 insertions, 0 deletions
diff --git a/base/src/examples/java/bjc/utils/examples/cli/StreamTerminalExample.java b/base/src/examples/java/bjc/utils/examples/cli/StreamTerminalExample.java
new file mode 100644
index 0000000..208ecf7
--- /dev/null
+++ b/base/src/examples/java/bjc/utils/examples/cli/StreamTerminalExample.java
@@ -0,0 +1,11 @@
+package bjc.utils.examples.cli;
+
+import java.util.Scanner;
+
+public class StreamTerminalExample {
+ public static void main(String[] args) {
+ Scanner sysIn = new Scanner(System.in);
+
+ // TODO Figure out a good way to demo this
+ }
+}
diff --git a/base/src/main/java/bjc/utils/cli/StreamTerminal.java b/base/src/main/java/bjc/utils/cli/StreamTerminal.java
new file mode 100644
index 0000000..a45a22e
--- /dev/null
+++ b/base/src/main/java/bjc/utils/cli/StreamTerminal.java
@@ -0,0 +1,152 @@
+package bjc.utils.cli;
+
+import static bjc.utils.cli.TerminalCodes.*;
+
+import java.io.*;
+import java.util.*;
+import java.util.concurrent.*;
+import java.util.concurrent.locks.*;
+
+public class StreamTerminal implements Terminal, Runnable {
+ private SortedSet<Long> pendingRequests;
+ private ConcurrentMap<Long, String> pendingReplies;
+
+ private Lock replyLock;
+ private Condition replyCondition;
+
+ private Queue<String> pendingOutput;
+
+ private boolean running;
+
+ private Scanner inputScanner;
+ private Writer output;
+
+ private long currentRequest = -1;
+
+ public StreamTerminal(Reader input, Writer output) {
+ this.inputScanner = new Scanner(input);
+ this.output = output;
+
+ this.pendingRequests = new TreeSet<>();
+ this.pendingReplies = new ConcurrentHashMap<>();
+ this.pendingOutput = new ArrayDeque<>();
+
+ this.replyLock = new ReentrantLock();
+ this.replyCondition = replyLock.newCondition();
+ }
+
+ @Override
+ public void run() {
+ running = true;
+ try {
+ output.write(INFO_STARTCOMPROC.toString() + "\n");
+ } catch (IOException e) {
+ // TODO Consider if there is some better way to handle these
+ throw new RuntimeException(e);
+ }
+
+ overall: while (running && inputScanner.hasNextLine()) {
+ try {
+ while (!pendingOutput.isEmpty())
+ output.write(pendingOutput.remove());
+
+ String ln = inputScanner.nextLine();
+ String com = "";
+ int spcIdx = ln.indexOf(' ');
+ if (spcIdx == -1) {
+ com = ln;
+ } else {
+ com = ln.substring(0, spcIdx);
+ ln = ln.substring(spcIdx + 1);
+ }
+ comswt: switch (com) {
+ case "r": {
+ // General command format is 'r <request no.>,<reply>
+ String subRep = ln.substring(2);
+ // Process a reply
+ int comIndex = subRep.indexOf(',');
+ long repNo = 0;
+ if (comIndex == -1) {
+ // Reply to the oldest message by default
+ repNo = pendingRequests.first();
+ } else {
+ String repStr = subRep.substring(0, comIndex);
+ try {
+ repNo = Long.parseLong(repStr);
+ } catch (NumberFormatException nfex) {
+ output.write(ERROR_INVREPNO.toString() + "\n");
+ continue overall;
+ }
+ // Skip over the comma
+ subRep = subRep.substring(comIndex + 1);
+ }
+
+ if (!pendingRequests.contains(repNo)) {
+ output.write(ERROR_UNKREPNO.toString() + "\n");
+ continue overall;
+ }
+
+ pendingRequests.remove(repNo);
+ pendingReplies.put(repNo, subRep);
+
+ replyLock.lock();
+ replyCondition.signalAll();
+ replyLock.unlock();
+ break comswt;
+ }
+ case "q":
+ running = false;
+ break comswt;
+ default:
+ output.write(ERROR_UNRECCOM.toString() + "\n");
+ }
+ } catch (IOException ioex) {
+ throw new RuntimeException(ioex);
+ }
+ }
+
+ running = false;
+ try {
+ output.write(INFO_ENDCOMPROC.toString() + "\n");
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ @Override
+ public long submitRequest(String req) {
+ long reqNo = currentRequest + 1;
+ currentRequest += 1;
+
+ pendingOutput.add(reqNo + " " + req + "\n");
+ pendingRequests.add(reqNo);
+ return reqNo;
+ }
+
+ @Override
+ public String awaitReply(long id) throws InterruptedException {
+ if (pendingReplies.containsKey(id))
+ return pendingReplies.get(id);
+ while (true) {
+ replyLock.lock();
+ replyCondition.await();
+ replyLock.unlock();
+ // Explanation: Since the reply map is add-only, the lock isn't actually
+ // protecting anything. We just want to wait until a response is received.
+ if (pendingReplies.containsKey(id))
+ return pendingReplies.get(id);
+ }
+ }
+
+ @Override
+ public Optional<String> checkReply(long id) {
+ return Optional.ofNullable(pendingReplies.get(id));
+ }
+
+ @Override
+ public String submitRequestSync(String req) throws InterruptedException {
+ return awaitReply(submitRequest(req));
+ }
+
+ // TODO add variants of the two blocking methods above with timeout support
+} \ No newline at end of file
diff --git a/base/src/main/java/bjc/utils/cli/Terminal.java b/base/src/main/java/bjc/utils/cli/Terminal.java
new file mode 100644
index 0000000..a10d82e
--- /dev/null
+++ b/base/src/main/java/bjc/utils/cli/Terminal.java
@@ -0,0 +1,51 @@
+package bjc.utils.cli;
+
+import java.util.*;
+
+/**
+ * A terminal with support for asking multiple questions, and retrieving the
+ * results piecemeal.
+ *
+ * This class is heavily inspired by the way that the old IBM MVS terminal
+ * worked, where the terminal would send you a series of requests and you would
+ * reply to them in whatever order you wanted.
+ *
+ * @author bjcul
+ */
+public interface Terminal {
+ /**
+ * Submit a request to the terminal
+ *
+ * @param req The body of the request
+ * @return The ID of the request
+ */
+ long submitRequest(String req);
+
+ /**
+ * Await a reply for a given request. Will block the current thread until a
+ * response is available.
+ *
+ * @param id The ID of the request
+ * @return The response to that request
+ * @throws InterruptedException If we are interrupted waiting for the reply
+ */
+ String awaitReply(long id) throws InterruptedException;
+
+ /**
+ * Check if a reply for a request is available, without blocking.
+ *
+ * @param id The ID of the request
+ * @return The reply to the request if one is available, and empty otherwise
+ */
+ Optional<String> checkReply(long id);
+
+ /**
+ * Submit a request and await the reply to it. Will block the current thread
+ * until a response is received.
+ *
+ * @param req The request to submit
+ * @return The reply to the request
+ * @throws InterruptedException If we are interrupted waiting for the reply
+ */
+ String submitRequestSync(String req) throws InterruptedException;;
+} \ No newline at end of file
diff --git a/base/src/main/java/bjc/utils/cli/TerminalCodes.java b/base/src/main/java/bjc/utils/cli/TerminalCodes.java
new file mode 100644
index 0000000..c1d7dfc
--- /dev/null
+++ b/base/src/main/java/bjc/utils/cli/TerminalCodes.java
@@ -0,0 +1,36 @@
+package bjc.utils.cli;
+
+/**
+ * Status codes output by {@link Terminal}.
+ *
+ * Format is a two-letter system code, a two letter subsystem code, a severity
+ * letter and a five digit error code.
+ *
+ * @author bjcul
+ *
+ */
+public enum TerminalCodes {
+ // TODO convert these into a class, and add an ability to read from the file
+ // The general idea of the format would be a tab-separated value file, with the
+ // first value being a command, and then the rest being the body of that command.
+ // Would also have the line-continuation feature
+ INFO_STARTCOMPROC("IOLPI00001", "STARTING PROCESSING"),
+ INFO_ENDCOMPROC("IOLPI00002", "ENDING PROCESSING"),
+
+ ERROR_UNRECCOM("IOINE00001", "UNRECOGNIZED COMMAND"),
+ ERROR_INVREPNO("IOINE00002", "INVALID REPLY NUMBER FORMAT"),
+ ERROR_UNKREPNO("IOINE00002", "UNKNOWN REPLY NUMBER"),
+ ;
+ public final String code;
+ public final String message;
+
+ private TerminalCodes(String code, String message) {
+ this.code = code;
+ this.message = message;
+ }
+
+ @Override
+ public String toString() {
+ return code + " " + message;
+ }
+}
diff --git a/base/src/main/java/bjc/utils/funcutils/StringUtils.java b/base/src/main/java/bjc/utils/funcutils/StringUtils.java
index 0b57e18..2d86083 100644
--- a/base/src/main/java/bjc/utils/funcutils/StringUtils.java
+++ b/base/src/main/java/bjc/utils/funcutils/StringUtils.java
@@ -312,4 +312,32 @@ public class StringUtils {
String... splits) {
return LevelSplitter.def.levelSplit(phrase, keepDelims, splits);
}
+
+ /**
+ * Convert a string into a pseudorandom anagram.
+ *
+ * Works by swapping each character in the string with a random one.
+ *
+ * @param s The string to convert.
+ *
+ * @return A pseudo-random anagram
+ */
+ public static String strfry(String s) {
+ char[] chars = s.toCharArray();
+
+ int strlen = chars.length;
+ Random rng = new Random();
+
+ for (int i = 0; i < strlen; i++) {
+ int randIdx = rng.nextInt(strlen);
+
+ char source = chars[i];
+ char dest = chars[randIdx];
+
+ chars[i] = dest;
+ chars[randIdx] = source;
+ }
+
+ return String.valueOf(chars);
+ }
} \ No newline at end of file
diff --git a/base/src/test/java/bjc/utils/test/cli/StreamTerminalTest.java b/base/src/test/java/bjc/utils/test/cli/StreamTerminalTest.java
new file mode 100644
index 0000000..de300d2
--- /dev/null
+++ b/base/src/test/java/bjc/utils/test/cli/StreamTerminalTest.java
@@ -0,0 +1,86 @@
+package bjc.utils.test.cli;
+
+import static org.junit.Assert.*;
+
+import java.io.*;
+import java.util.Scanner;
+
+import org.junit.Test;
+
+import bjc.utils.cli.StreamTerminal;
+
+/**
+ * Test {@link StreamTerminal}
+ *
+ * @author bjcul
+ *
+ */
+public class StreamTerminalTest {
+
+ /**
+ * Single-threaded test of stream-terminal
+ */
+ @SuppressWarnings("resource")
+ @Test
+ public void stTest() {
+ try {
+ PipedInputStream inPipeIn = new PipedInputStream();
+ PipedOutputStream inPipeOut = new PipedOutputStream(inPipeIn);
+
+ InputStreamReader inPipeReader = new InputStreamReader(inPipeIn);
+ OutputStreamWriter inPipeWriter = new OutputStreamWriter(inPipeOut);
+
+ PipedInputStream outPipeIn = new PipedInputStream();
+ PipedOutputStream outPipeOut = new PipedOutputStream(outPipeIn);
+
+ InputStreamReader outPipeReader = new InputStreamReader(outPipeIn);
+ OutputStreamWriter outPipeWriter = new OutputStreamWriter(outPipeOut);
+
+ StreamTerminal terminal = new StreamTerminal(inPipeReader, outPipeWriter);
+
+ long reqID1 = terminal.submitRequest("Request 1");
+ long reqID2 = terminal.submitRequest("Request 2");
+
+ assertEquals(0, reqID1);
+ assertEquals(1, reqID2);
+
+ inPipeWriter.write("r 0,A\n");
+ inPipeWriter.write("r 1,B\n");
+ inPipeWriter.write("q\n");
+ inPipeWriter.flush();
+ inPipeWriter.close();
+
+ terminal.run();
+
+ outPipeWriter.flush();
+ outPipeWriter.close();
+
+ try (Scanner outPipeScanner = new Scanner(outPipeReader)) {
+ assertTrue(outPipeScanner.hasNextLine());
+ String out1 = outPipeScanner.nextLine();
+ assertEquals("IOLPI00001 STARTING PROCESSING", out1);
+
+ assertTrue(outPipeScanner.hasNextLine());
+ String out2 = outPipeScanner.nextLine();
+ assertEquals("0 Request 1", out2);
+
+ assertTrue(outPipeScanner.hasNextLine());
+ String out3 = outPipeScanner.nextLine();
+ assertEquals("1 Request 2", out3);
+
+ assertTrue(outPipeScanner.hasNextLine());
+ String out4 = outPipeScanner.nextLine();
+ assertEquals("IOLPI00002 ENDING PROCESSING", out4);
+ }
+
+ assertEquals("A", terminal.awaitReply(reqID1));
+ assertEquals("B", terminal.awaitReply(reqID2));
+ } catch (IOException ioex) {
+ throw new RuntimeException(ioex);
+ } catch (InterruptedException iex) {
+ throw new RuntimeException(iex);
+ }
+ }
+
+ // TODO write a test that ensures that the multi-threading/reply-wait functionality works
+}