diff --git a/src/main/java/edu/caltech/nanodb/client/ExclusiveClient.java b/src/main/java/edu/caltech/nanodb/client/ExclusiveClient.java index 5ec1918c231b82e8d4867b822a0258ed63b0ee40..afabd40c580fc1d889af6143c318fed7deb23f9d 100755 --- a/src/main/java/edu/caltech/nanodb/client/ExclusiveClient.java +++ b/src/main/java/edu/caltech/nanodb/client/ExclusiveClient.java @@ -53,19 +53,7 @@ public class ExclusiveClient extends InteractiveClient { @Override public CommandResult handleCommand(String command) { - CommandResult result = server.doCommand(command, false); - - if (result.failed()) { - Exception e = result.getFailure(); - if (e instanceof ParseCancellationException) { - System.out.println("ERROR: Could not parse command"); - } - else { - System.out.println("ERROR: " + e.getMessage()); - } - } - - return result; + return server.doCommand(command, false); } diff --git a/src/main/java/edu/caltech/nanodb/client/InteractiveClient.java b/src/main/java/edu/caltech/nanodb/client/InteractiveClient.java index 032820edf0af663814234a3d8d1e393518670490..45c01492d4d1a79b927ab92b7d669d4792f93c64 100644 --- a/src/main/java/edu/caltech/nanodb/client/InteractiveClient.java +++ b/src/main/java/edu/caltech/nanodb/client/InteractiveClient.java @@ -2,12 +2,16 @@ package edu.caltech.nanodb.client; import java.io.BufferedReader; +import java.io.FileNotFoundException; +import java.io.FileReader; +import java.io.IOException; import java.io.InputStreamReader; import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.LogManager; import edu.caltech.nanodb.server.CommandResult; +import org.antlr.v4.runtime.misc.ParseCancellationException; /** @@ -29,11 +33,25 @@ public abstract class InteractiveClient { /** The buffer that accumulates each command's text. */ private StringBuilder enteredText; + /** A flag that records if we are exiting the interactive client. */ + private boolean exiting; + + /** + * Start up the interactive client. The specific way the client + * interacts with the server dictates how this startup mechanism + * will work. + * + * @throws Exception if any error occurs during startup + */ public abstract void startup() throws Exception; - public void mainloop() { + /** + * This is the interactive mainloop that handles input from the standard + * input stream of the program. + */ + protected void mainloop() { // We don't use the console directly, since we can't read/write it // if someone redirects a file onto the client's input-stream. boolean hasConsole = (System.console() != null); @@ -43,13 +61,13 @@ public abstract class InteractiveClient { "Welcome to NanoDB. Exit with EXIT or QUIT command.\n"); } - boolean exiting = false; - BufferedReader bufReader = new BufferedReader(new InputStreamReader(System.in)); + exiting = false; + BufferedReader bufReader = + new BufferedReader(new InputStreamReader(System.in)); while (!exiting) { enteredText = new StringBuilder(); boolean firstLine = true; -getcmd: while (true) { try { if (hasConsole) { @@ -73,21 +91,9 @@ getcmd: enteredText.append(line).append('\n'); - // Process any commands in the entered text. - while (true) { - String command = getCommandString(); - if (command == null) - break; // No more complete commands. - - // if (logger.isDebugEnabled()) - // logger.debug("Command string:\n" + command); - - CommandResult result = handleCommand(command); - if (result.isExit()) { - exiting = true; - break getcmd; - } - } + processEnteredText(); + if (exiting) + break; if (enteredText.length() == 0) firstLine = true; @@ -102,6 +108,111 @@ getcmd: } + /** + * This helper function processes the contents of the {@link #enteredText} + * field, consuming comments, handling client "shell commands" and + * regular commands that are handled by the + * {@link edu.caltech.nanodb.server.NanoDBServer}. Whatever command or + * comment is processed will also be removed from the {@code enteredText} + * buffer by this function. Note also that multiple commands will be + * processed, if present. + */ + private void processEnteredText() { + // Process any commands in the entered text. + while (true) { + // Consume leading whitespace + while (enteredText.length() > 0 && + Character.isWhitespace(enteredText.charAt(0))) { + enteredText.deleteCharAt(0); + } + + // Consume comments + if (enteredText.length() >= 2) { + if ("--".equals(enteredText.substring(0, 2))) { + // Consume single-line comment + int endIdx = 2; + + // Look for the end of the line. + while (endIdx < enteredText.length() && + enteredText.charAt(endIdx) != '\n') { + endIdx++; + } + + if (endIdx == enteredText.length()) { + // Didn't find newline character. Can't consume this + // comment yet. + return; + } + + endIdx++; // Skip the newline character as well. + enteredText.delete(0, endIdx); + + // Go back and try to find more commands. + continue; + } + else if ("/*".equals(enteredText.substring(0, 2))) { + // Consume block comment + + int endIdx = 2; + + // Look for the end of the block comment. + while (endIdx + 1 < enteredText.length() && + (enteredText.charAt(endIdx) != '*' || + enteredText.charAt(endIdx + 1) != '/')) { + endIdx++; + } + + if (endIdx + 1 == enteredText.length()) { + // Didn't find end of block comment. Can't consume + // this comment yet. + return; + } + + endIdx += 2; // Skip the end of the block-comment. + enteredText.delete(0, endIdx); + + // Go back and try to find more commands. + continue; + } + } + + // Look for shell commands + if (enteredText.length() > 0 && enteredText.charAt(0) == '\\') { + // This is a shell command, which continues to the + // end of the current line. + int endIdx = 0; + while (endIdx < enteredText.length() && + enteredText.charAt(endIdx) != '\n') { + endIdx++; + } + endIdx++; // Include the newline character too + + String shellCommand = enteredText.substring(0, endIdx); + enteredText.delete(0, endIdx); + + handleShellCommand(shellCommand); + continue; + } + + String command = getCommandString(); + if (command == null) + break; // Couldn't find a complete command. + + // if (logger.isDebugEnabled()) + // logger.debug("Command string:\n" + command); + + CommandResult result = handleCommand(command); + if (result.isExit()) { + exiting = true; + break; + } + else { + outputCommandResult(result); + } + } + } + + /** * This helper method goes through the {@link #enteredText} buffer, trying * to identify the extent of the next command string. This is done using @@ -165,6 +276,99 @@ getcmd: public abstract CommandResult handleCommand(String command); + /** + * Handle "shell commands," which are commands that the client itself + * handles on behalf of the user. Shell commands start with a backslash + * "\" character. + * + * @param shellCommand the command to handle. + */ + private void handleShellCommand(String shellCommand) { + // Split the shell command into parts + String[] parts = shellCommand.split("\\s+", 2); + parts[0] = parts[0].toLowerCase(); + + if ("\\source".equals(parts[0])) { + // Source the requested SQL file. + + StringBuilder oldText = enteredText; + enteredText = new StringBuilder(); + String filename = parts[1].strip(); + + // Open the file with a try-with-resources so it will always be + // closed when we are done with the file. + try (BufferedReader reader = + new BufferedReader(new FileReader(filename))) { + + while (true) { + String line = reader.readLine(); + if (line == null) + break; + + // Inject the line of the file into the buffer. + enteredText.append(line).append('\n'); + + // Attempt to process any operations in the entered text. + // If there are no complete commands, this will be a + // no-op. If there are multiple complete commands, they + // will all be processed. + processEnteredText(); + } + } + catch (FileNotFoundException e) { + System.out.println("ERROR: Could not open file \"" + + filename + "\""); + } + catch (IOException e) { + System.out.println("ERROR: Could not read file \"" + + filename + "\": " + e.getMessage()); + } + + enteredText = oldText; + } + else if ("\\help".equals(parts[0])) { + // Show help information. + System.out.println("You can enter any NanoDB SQL command, or " + + "the following built-in commands."); + System.out.println("EXIT; or QUIT; will exit the NanoDB client."); + System.out.println(); + System.out.println("\\help"); + System.out.println("\tDisplays this help information."); + System.out.println(); + System.out.println("\\source filename.sql"); + System.out.println("\tLoads and executes the contents of \"filename.sql\"."); + System.out.println(); + } + else { + System.out.println("ERROR: Unrecognized shell command \"" + + parts[0] + "\""); + } + + } + + + /** + * Outputs relevant information from the command-result object. + * + * @param result the command-result object to output + */ + private void outputCommandResult(CommandResult result) { + // TODO: Right now we only print out error information. In the + // future we will want to output other details. This + // functionality also needs to be integrated with the tuple- + // output code from the server. Need to think about this more. + if (result.failed()) { + Exception e = result.getFailure(); + if (e instanceof ParseCancellationException) { + System.out.println("ERROR: Could not parse command"); + } + else { + System.out.println("ERROR: " + e.getMessage()); + } + } + } + + /** * Shut down the interactive client. The specific way the client * interacts with the server dictates how this shutdown mechanism