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 /base/src/main | |
| parent | 4a96d9cad446ea405b51dfeebb01a1b6d7f6fb2b (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')
| -rw-r--r-- | base/src/main/java/bjc/utils/cli/StreamTerminal.java | 152 | ||||
| -rw-r--r-- | base/src/main/java/bjc/utils/cli/Terminal.java | 51 | ||||
| -rw-r--r-- | base/src/main/java/bjc/utils/cli/TerminalCodes.java | 36 | ||||
| -rw-r--r-- | base/src/main/java/bjc/utils/funcutils/StringUtils.java | 28 |
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 |
