diff options
Diffstat (limited to 'base/src/main/java/bjc/utils/cli')
4 files changed, 199 insertions, 58 deletions
diff --git a/base/src/main/java/bjc/utils/cli/StreamTerminal.java b/base/src/main/java/bjc/utils/cli/StreamTerminal.java index a45a22e..185891f 100644 --- a/base/src/main/java/bjc/utils/cli/StreamTerminal.java +++ b/base/src/main/java/bjc/utils/cli/StreamTerminal.java @@ -6,7 +6,16 @@ import java.io.*; import java.util.*; import java.util.concurrent.*; import java.util.concurrent.locks.*; +import java.util.function.Consumer; +import bjc.data.Either; + +/** + * Implementation of {@link Terminal} using {@link Reader} and {@link Writer} + * + * @author bjcul + * + */ public class StreamTerminal implements Terminal, Runnable { private SortedSet<Long> pendingRequests; private ConcurrentMap<Long, String> pendingReplies; @@ -21,9 +30,32 @@ public class StreamTerminal implements Terminal, Runnable { private Scanner inputScanner; private Writer output; + private String prefix; + private Consumer<String> mode; + private long currentRequest = -1; + /** + * Create a new stream terminal. + * + * @param input The input source + * @param output The output source + */ public StreamTerminal(Reader input, Writer output) { + this(input, output, null, null); + } + + /** + * Create a new stream terminal which backs an application + * + * @param input The input source + * @param output The output source + * @param prefix The string any input must start with to be directed to the + * terminal + * @param mode The place where all input not directed to the terminal is + * written. + */ + public StreamTerminal(Reader input, Writer output, String prefix, Consumer<String> mode) { this.inputScanner = new Scanner(input); this.output = output; @@ -33,6 +65,9 @@ public class StreamTerminal implements Terminal, Runnable { this.replyLock = new ReentrantLock(); this.replyCondition = replyLock.newCondition(); + + this.prefix = prefix; + this.mode = mode; } @Override @@ -40,6 +75,9 @@ public class StreamTerminal implements Terminal, Runnable { running = true; try { output.write(INFO_STARTCOMPROC.toString() + "\n"); + while (!pendingOutput.isEmpty()) + output.write(pendingOutput.remove()); + output.flush(); } catch (IOException e) { // TODO Consider if there is some better way to handle these throw new RuntimeException(e); @@ -49,57 +87,71 @@ public class StreamTerminal implements Terminal, Runnable { try { while (!pendingOutput.isEmpty()) output.write(pendingOutput.remove()); + output.flush(); 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(); + if (prefix != null && ln.startsWith(prefix)) { + ln = ln.substring(prefix.length()); + String com = ""; + int spcIdx = ln.indexOf(' '); + if (spcIdx == -1) { + com = ln; } 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); + com = ln.substring(0, spcIdx); + ln = ln.substring(spcIdx + 1); } - if (!pendingRequests.contains(repNo)) { - output.write(ERROR_UNKREPNO.toString() + "\n"); - continue overall; - } + 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"); + output.flush(); + continue overall; + } + // Skip over the comma + subRep = subRep.substring(comIndex + 1); + } - pendingRequests.remove(repNo); - pendingReplies.put(repNo, subRep); + if (!pendingRequests.contains(repNo)) { + output.write(ERROR_UNKREPNO.toString() + "\n"); + output.flush(); + continue overall; + } - replyLock.lock(); - replyCondition.signalAll(); - replyLock.unlock(); - break comswt; - } - case "q": - running = false; - break comswt; - default: - output.write(ERROR_UNRECCOM.toString() + "\n"); + 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"); + output.flush(); + } + } else { + mode.accept(ln); } + + while (!pendingOutput.isEmpty()) + output.write(pendingOutput.remove()); + output.flush(); } catch (IOException ioex) { throw new RuntimeException(ioex); } @@ -107,7 +159,10 @@ public class StreamTerminal implements Terminal, Runnable { running = false; try { + while (!pendingOutput.isEmpty()) + output.write(pendingOutput.remove()); output.write(INFO_ENDCOMPROC.toString() + "\n"); + output.flush(); } catch (IOException e) { throw new RuntimeException(e); } @@ -139,6 +194,25 @@ public class StreamTerminal implements Terminal, Runnable { } @Override + public Optional<String> awaitReply(long id, TimeUnit unit, long delay) throws InterruptedException { + if (pendingReplies.containsKey(id)) + return Optional.of(pendingReplies.get(id)); + while (true) { + replyLock.lock(); + boolean stat = replyCondition.await(delay, unit); + replyLock.unlock(); + + // If we timed out, say so + if (stat == false) + return Optional.empty(); + // 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 Optional.of(pendingReplies.get(id)); + } + } + + @Override public Optional<String> checkReply(long id) { return Optional.ofNullable(pendingReplies.get(id)); } @@ -147,6 +221,29 @@ public class StreamTerminal implements Terminal, Runnable { public String submitRequestSync(String req) throws InterruptedException { return awaitReply(submitRequest(req)); } + + @Override + public Either<String, Long> submitRequestSync(String req, TimeUnit unit, long delay) throws InterruptedException { + long id = submitRequest(req); + Optional<String> rep = awaitReply(id, unit, delay); + return rep.isEmpty() ? Either.right(id) : Either.left(rep.get()); + } - // TODO add variants of the two blocking methods above with timeout support + /** + * Add a string to be printed next time output is printed. + * + * @param out The output + */ + public void addOutput(String out) { + pendingOutput.add(out); + } + + /** + * Set the mode for handling non-terminal input + * + * @param mode The new mode for handling non-terminal input + */ + public void setMode(Consumer<String> mode) { + this.mode = mode; + } }
\ 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 index a10d82e..e8b3029 100644 --- a/base/src/main/java/bjc/utils/cli/Terminal.java +++ b/base/src/main/java/bjc/utils/cli/Terminal.java @@ -1,6 +1,9 @@ package bjc.utils.cli; import java.util.*; +import java.util.concurrent.TimeUnit; + +import bjc.data.Either; /** * A terminal with support for asking multiple questions, and retrieving the @@ -17,6 +20,7 @@ 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); @@ -26,15 +30,32 @@ public interface Terminal { * 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; + + /** + * Await a reply for a given request. Will block the current thread until a + * response is available or the specified timeout occurs. + * + * @param id The ID of the request + * @param unit The unit of time to wait + * @param delay The amount of units to wait before timing out + * + * @return The response to that request, or an empty optional if it timed out + * + * @throws InterruptedException If we are interrupted waiting for the reply + */ + Optional<String> awaitReply(long id, TimeUnit unit, long delay) 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); @@ -47,5 +68,20 @@ public interface Terminal { * @return The reply to the request * @throws InterruptedException If we are interrupted waiting for the reply */ - String submitRequestSync(String req) throws InterruptedException;; + String submitRequestSync(String req) throws InterruptedException; + + /** + * Submit a request and await the reply to it. Will block the current thread + * until a response is received or the timeout occurs. + * + * @param req The request to submit + * @param unit The unit of time to wait + * @param delay The amount of units to wait before timing out + * + * @return The reply to the request, or the ID of the request if the timeout occurred + * + * @throws InterruptedException If we are interrupted waiting for the reply + */ + Either<String, Long> submitRequestSync(String req, TimeUnit unit, long delay) 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 index c1d7dfc..7e2206e 100644 --- a/base/src/main/java/bjc/utils/cli/TerminalCodes.java +++ b/base/src/main/java/bjc/utils/cli/TerminalCodes.java @@ -14,14 +14,22 @@ public enum TerminalCodes { // 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 + /** Alert for starting processing. */ INFO_STARTCOMPROC("IOLPI00001", "STARTING PROCESSING"), + /** Alert for ending processing. */ INFO_ENDCOMPROC("IOLPI00002", "ENDING PROCESSING"), + /** Error for an unknown command */ ERROR_UNRECCOM("IOINE00001", "UNRECOGNIZED COMMAND"), + /** Error for an invalid number format when identifying a reply. */ ERROR_INVREPNO("IOINE00002", "INVALID REPLY NUMBER FORMAT"), + /** Error for specifying an unrecognized reply. */ ERROR_UNKREPNO("IOINE00002", "UNKNOWN REPLY NUMBER"), ; + + /** The code for this error. */ public final String code; + /** The summary message for this error. */ public final String message; private TerminalCodes(String code, String message) { diff --git a/base/src/main/java/bjc/utils/cli/objects/DelimSplitterCLI.java b/base/src/main/java/bjc/utils/cli/objects/DelimSplitterCLI.java index 53d6d1e..ba478f7 100644 --- a/base/src/main/java/bjc/utils/cli/objects/DelimSplitterCLI.java +++ b/base/src/main/java/bjc/utils/cli/objects/DelimSplitterCLI.java @@ -60,24 +60,22 @@ public class DelimSplitterCLI { * Run the tester interface. */ private void runLoop() { - final Scanner scn = new Scanner(System.in); + try (Scanner scn = new Scanner(System.in)) { + System.out.print("Enter a command (blank line to quit): "); + String inp = scn.nextLine().trim(); + System.out.println(); - System.out.print("Enter a command (blank line to quit): "); - String inp = scn.nextLine().trim(); - System.out.println(); + while (!inp.equals("")) { + handleCommand(inp, scn, true); - while (!inp.equals("")) { - handleCommand(inp, scn, true); + System.out.println(); - System.out.println(); + System.out.print("Enter a command (blank line to quit): "); + inp = scn.nextLine(); - System.out.print("Enter a command (blank line to quit): "); - inp = scn.nextLine(); - - System.out.println(); + System.out.println(); + } } - - scn.close(); } /* @@ -221,6 +219,8 @@ public class DelimSplitterCLI { } try (FileInputStream fis = new FileInputStream(pth)) { + @SuppressWarnings("resource") + // Handled by above final Scanner scn = new Scanner(fis); while (scn.hasNextLine()) { |
