All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.jsimpledb.cli.Console Maven / Gradle / Ivy

The newest version!

/*
 * Copyright (C) 2015 Archie L. Cobbs. All rights reserved.
 */

package org.jsimpledb.cli;

import com.google.common.base.Preconditions;
import com.google.common.collect.Lists;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.io.Reader;
import java.io.StringWriter;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;

import org.dellroad.stuff.java.ProcessRunner;
import org.jsimpledb.JSimpleDB;
import org.jsimpledb.cli.cmd.EvalCommand;
import org.jsimpledb.core.Database;
import org.jsimpledb.kv.KVDatabase;
import org.jsimpledb.parse.ParseException;
import org.jsimpledb.util.ParseContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import jline.Terminal;
import jline.TerminalFactory;
import jline.console.ConsoleReader;
import jline.console.UserInterruptException;
import jline.console.completer.Completer;
import jline.console.history.FileHistory;

/**
 * CLI console.
 */
public class Console {

    protected final Logger log = LoggerFactory.getLogger(this.getClass());
    protected final ConsoleReader console;
    protected final CliSession session;

    private final CommandParser commandParser = new CommandParser();
    private final CommandListParser commandListParser = new CommandListParser(this.commandParser);

    private FileHistory history;

    /**
     * Simplified constructor for {@link org.jsimpledb.SessionMode#KEY_VALUE} mode.
     *
     * @param kvdb key/value {@link KVDatabase}
     * @param input console input
     * @param output console output
     * @throws IOException if an I/O error occurs
     */
    public Console(KVDatabase kvdb, InputStream input, OutputStream output) throws IOException {
        this(kvdb, null, null, input, output, null, null, null);
    }

    /**
     * Simplified constructor for {@link org.jsimpledb.SessionMode#CORE_API} mode.
     *
     * @param db core API {@link Database}
     * @param input console input
     * @param output console output
     * @throws IOException if an I/O error occurs
     */
    public Console(Database db, InputStream input, OutputStream output) throws IOException {
        this(null, db, null, input, output, null, null, null);
    }

    /**
     * Simplified constructor for {@link org.jsimpledb.SessionMode#JSIMPLEDB} mode.
     *
     * @param jdb {@link JSimpleDB} database
     * @param input console input
     * @param output console output
     * @throws IOException if an I/O error occurs
     */
    public Console(JSimpleDB jdb, InputStream input, OutputStream output) throws IOException {
        this(null, null, jdb, input, output, null, null, null);
    }

    /**
     * Generic constructor.
     *
     * @param kvdb {@link KVDatabase} for {@link org.jsimpledb.SessionMode#KEY_VALUE} (otherwise must be null)
     * @param db {@link Database} for {@link org.jsimpledb.SessionMode#CORE_API} (otherwise must be null)
     * @param jdb {@link JSimpleDB} for {@link org.jsimpledb.SessionMode#JSIMPLEDB} (otherwise must be null)
     * @param input console input
     * @param output console output
     * @param terminal JLine terminal interface, or null for default
     * @param encoding character encoding for {@code terminal}, or null for default
     * @param appName JLine application name, or null for none
     * @throws IOException if an I/O error occurs
     * @throws IllegalArgumentException if not exactly one of {@code kvdb}, {@code db} or {@code jdb} is not null
     */
    public Console(KVDatabase kvdb, Database db, JSimpleDB jdb, InputStream input, OutputStream output,
      Terminal terminal, String encoding, String appName) throws IOException {
        Preconditions.checkArgument((kvdb != null ? 1 : 0) + (db != null ? 1 : 0) + (jdb != null ? 1 : 0) == 1,
          "exactly one of kvdb, db or jdb must be not null");
        Preconditions.checkArgument(input != null, "null input");
        Preconditions.checkArgument(output != null, "null output");
        if (terminal == null)
            terminal = Console.getTerminal();
        this.console = new ConsoleReader(appName, input, output, terminal, encoding);
        this.console.setBellEnabled(true);
        this.console.setHistoryEnabled(true);
        this.console.setHandleUserInterrupt(true);
        this.console.setExpandEvents(false);
        final PrintWriter writer = new PrintWriter(console.getOutput(), true);
        this.session = jdb != null ? new CliSession(jdb, writer, this) :
          db != null ? new CliSession(db, writer, this) : new CliSession(kvdb, writer, this);
    }

    /**
     * Get the associated JLine {@link ConsoleReader}.
     *
     * @return associated console reader
     */
    public ConsoleReader getConsoleReader() {
        return this.console;
    }

    /**
     * Get the associated {@link CliSession}.
     *
     * @return associated CLI session
     */
    public CliSession getSession() {
        return this.session;
    }

    /**
     * Set/update the command history file. May be reconfigured while executing.
     *
     * @param historyFile file for storing command history
     * @throws IOException if {@code historyFile} cannot be read
     * @throws IllegalArgumentException if {@code historyFile} is null
     */
    public void setHistoryFile(File historyFile) throws IOException {

        // Sanity check
        Preconditions.checkArgument(historyFile != null, "null historyFile");

        // Open new history
        final FileHistory newHistory = new FileHistory(historyFile);

        // Close current history
        if (this.history != null)
            this.history.flush();

        // Replace in console
        this.history = newHistory;
        this.console.setHistory(this.history);
    }

    /**
     * Run this instance in non-interactive (or "batch") mode on the given input.
     *
     * @param input command input; is not closed by this method
     * @param inputDescription description of input (e.g., file name) used for error reporting, or null for none
     * @return true if successful, false if an error occurred
     * @throws IOException if an I/O error occurs
     */
    public boolean runNonInteractive(Reader input, String inputDescription) throws IOException {

        // Read entire input in as a string XXX currently we don't have the capability to parse streams
        final StringWriter data = new StringWriter();
        final char[] buf = new char[1024];
        int r;
        while ((r = input.read(buf)) != -1)
            data.write(buf, 0, r);
        String text = data.toString();

        // Remove comments
        text = text.replaceAll("(?m)^[\\s&&[^\\n]]*#.*$", "");

        // Parse and execute commands one at a time
        final ParseContext ctx = new ParseContext(text);
        final CliSession.Action[] action = new CliSession.Action[1];
        boolean error = false;
        while (true) {

            // Skip whitespace
            ctx.skipWhitespace();
            if (ctx.isEOF())
                break;

            // Set new error prefix while handling the next command
            final String previousErrorMessagePrefix = this.session.getErrorMessagePrefix();
            final int lineNumber = text.substring(0, ctx.getIndex()).replaceAll("[^\\n]", "").length() + 1;
            this.session.setErrorMessagePrefix(previousErrorMessagePrefix
              + (inputDescription != null ? inputDescription + ": " : "") + "line " + lineNumber + ": ");
            try {

                // Parse next command
                if (!this.session.performCliSessionAction(
                  session -> action[0] = Console.this.commandParser.parse(session, ctx, false))) {
                    error = true;
                    break;
                }

                // Execute command
                if (!this.session.performCliSessionAction(action[0])
                  || (action[0] instanceof EvalCommand.EvalAction
                   && ((EvalCommand.EvalAction)action[0]).getEvalException() != null)) {
                    error = true;
                    break;
                }
            } finally {
                this.session.setErrorMessagePrefix(previousErrorMessagePrefix);
            }

            // Skip whitespace
            ctx.skipWhitespace();
            if (ctx.isEOF())
                break;

            // Expecte semi-colon separator
            if (!ctx.tryLiteral(";")) {
                this.session.reportException(new ParseException(ctx, "expected `;'"));
                error = true;
                break;
            }
        }

        // Flush output
        this.console.flush();

        // Done
        return !error;
    }

    /**
     * Run this instance. This method blocks until the connected user exits the console.
     *
     * @throws IOException if an I/O error occurs
     */
    public void run() throws IOException {

        // Input buffer
        final StringBuilder lineBuffer = new StringBuilder();

        // Set up tab completion
        this.console.addCompleter(new ConsoleCompleter(lineBuffer));

        // Get prompt
        final String prompt;
        switch (this.session.getMode()) {
        case KEY_VALUE:
            prompt = "KeyValue> ";
            break;
        case CORE_API:
            prompt = "CoreAPI> ";
            break;
        case JSIMPLEDB:
            prompt = "JSimpleDB> ";
            break;
        default:
            throw new RuntimeException("internal error");
        }

        // Main command loop
        try {

            this.console.println("Welcome to JSimpleDB. You are in " + this.session.getMode() + " mode. Type `help' for help.");
            this.console.println();
            while (!session.isDone()) {

                // Read command line
                String line;
                try {
                    line = this.console.readLine(lineBuffer.length() == 0 ?
                      prompt : String.format("%" + (prompt.length() - 3) + "s-> ", ""));
                } catch (UserInterruptException e) {
                    this.console.print("^C");
                    line = null;
                }
                if (line == null) {
                    this.console.println();
                    break;
                }

                // Detect backslash continuations
                boolean continuation = false;
                if (line.length() > 0 && line.charAt(line.length() - 1) == '\\') {
                    line = line.substring(0, line.length() - 1) + "\n";
                    continuation = true;
                }

                // Append line to buffer
                lineBuffer.append(line);

                // Handle backslash continuations
                if (continuation)
                    continue;
                final ParseContext ctx = new ParseContext(lineBuffer.toString());

                // Skip initial whitespace
                ctx.skipWhitespace();

                // Ignore blank input
                if (ctx.getInput().length() == 0)
                    continue;

                // Parse command(s)
                final ArrayList actions = new ArrayList<>();
                final boolean[] needMoreInput = new boolean[1];
                final boolean ok = this.session.performCliSessionAction(session -> {
                    try {
                        actions.addAll(Console.this.commandListParser.parse(session, ctx, false));
                    } catch (ParseException e) {
                        if (ctx.getInput().length() == 0)
                            needMoreInput[0] = true;
                        else
                            throw e;
                    }
                });
                if (needMoreInput[0]) {
                    lineBuffer.append('\n');
                    continue;
                }
                lineBuffer.setLength(0);
                if (!ok)
                    continue;

                // Execute commands
                for (CliSession.Action action : actions) {
                    if (!this.session.performCliSessionAction(action))
                        break;
                }

                // Proceed
                this.console.flush();
            }
        } finally {
            if (this.history != null)
                this.history.flush();
            this.console.flush();
            this.console.shutdown();
        }
    }

    /**
     * Parse the given command(s).
     *
     * @param text command input
     * @return command actions, or null if there was an error during parsing
     * @throws ParseException if {@code text} cannot be parsed
     * @throws IllegalArgumentException if {@code text} is null
     */
    public List parseCommand(String text) {
        Preconditions.checkArgument(text != null, "null text");
        final ParseContext ctx = new ParseContext(text);
        final ArrayList actions = new ArrayList<>();
        return this.session.performCliSessionAction(session -> actions.addAll(commandListParser.parse(session, ctx, false))) ?
          actions : null;
    }

    /**
     * Get the {@link Terminal} instance appropriate for this operating system.
     *
     * @return JLine {@link Terminal} to use
     * @throws IOException if an I/O error occurs
     */
    public static Terminal getTerminal() throws IOException {

        // Are we running on Windows under Cygwin? If so use UNIX flavor instead of Windows
        final boolean windows = System.getProperty("os.name", "generic").toLowerCase(Locale.ENGLISH).contains("win");
        while (windows) {
            final ProcessRunner runner;
            try {
                runner = new ProcessRunner(Runtime.getRuntime().exec(new String[] { "uname", "-s" }));
            } catch (IOException e) {
                break;
            }
            runner.setDiscardStandardError(true);
            try {
                runner.run();
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                break;
            }
            if (!new String(runner.getStandardOutput(), StandardCharsets.UTF_8).trim().matches("(?is)^cygwin.*"))
                break;
            try {
                return TerminalFactory.getFlavor(TerminalFactory.Flavor.UNIX);
            } catch (Exception e) {
                break;
            }
        }

        // Do the normal thing
        return TerminalFactory.get();
    }

// ConsoleCompleter

    private class ConsoleCompleter implements Completer {

        private final StringBuilder lineBuffer;

        ConsoleCompleter(StringBuilder lineBuffer) {
            this.lineBuffer = lineBuffer;
        }

        @Override
        public int complete(final String buffer, final int cursor, final List candidates) {
            final ParseContext ctx = new ParseContext(this.lineBuffer + buffer.substring(0, cursor));
            try {
                Console.this.commandListParser.parse(Console.this.session, ctx, true);
            } catch (ParseException e) {
                String prefix = "";
                int index = ctx.getIndex();
                while (index > 0 && Character.isJavaIdentifierPart(ctx.getOriginalInput().charAt(index - 1)))
                    prefix = ctx.getOriginalInput().charAt(--index) + prefix;
                final String prefix0 = prefix;
                candidates.addAll(Lists.transform(e.getCompletions(), string -> prefix0 + string));
                return index;
            } catch (Exception e) {
                try {
                    Console.this.console.println();
                    Console.this.console.println("Error: got exception calculating command line completions");
                    e.printStackTrace(Console.this.session.getWriter());
                } catch (IOException e2) {
                    // ignore
                }
            }
            return cursor;
        }
    }
}





© 2015 - 2025 Weber Informatics LLC | Privacy Policy