diff options
| author | Ben Culkin <scorpress@gmail.com> | 2023-06-23 19:48:38 -0400 |
|---|---|---|
| committer | Ben Culkin <scorpress@gmail.com> | 2023-06-23 19:48:38 -0400 |
| commit | 2df02c35b70f7e8077832470de9594b657f1be67 (patch) | |
| tree | 4353ce1f78571e038bbe8fed62d321c77a7b868c | |
| parent | 4a96d9cad446ea405b51dfeebb01a1b6d7f6fb2b (diff) | |
Add terminal
This is some functionality based on the way that MVS/other IBM OSes
handle their UI
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 +} |
