summaryrefslogtreecommitdiff
path: root/base/src/main/java/bjc
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 /base/src/main/java/bjc
parent4a96d9cad446ea405b51dfeebb01a1b6d7f6fb2b (diff)
Add terminal
This is some functionality based on the way that MVS/other IBM OSes handle their UI
Diffstat (limited to 'base/src/main/java/bjc')
-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
4 files changed, 267 insertions, 0 deletions
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