io.mindmaps.graql.GraqlShell.java Source code

Java tutorial

Introduction

Here is the source code for io.mindmaps.graql.GraqlShell.java

Source

/*
 * MindmapsDB - A Distributed Semantic Database
 * Copyright (C) 2016  Mindmaps Research Ltd
 *
 * MindmapsDB is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * MindmapsDB is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with MindmapsDB. If not, see <http://www.gnu.org/licenses/gpl.txt>.
 */

package io.mindmaps.graql;

import io.mindmaps.util.Version;
import io.mindmaps.graql.internal.shell.ErrorMessage;
import io.mindmaps.graql.internal.shell.GraQLCompleter;
import io.mindmaps.graql.internal.shell.GraqlSignalHandler;
import io.mindmaps.graql.internal.shell.ShellCommandCompleter;
import jline.console.ConsoleReader;
import jline.console.completer.AggregateCompleter;
import jline.console.history.FileHistory;
import mjson.Json;
import org.apache.commons.cli.CommandLine;
import org.apache.commons.cli.CommandLineParser;
import org.apache.commons.cli.DefaultParser;
import org.apache.commons.cli.HelpFormatter;
import org.apache.commons.cli.Options;
import org.apache.commons.cli.ParseException;
import org.eclipse.jetty.websocket.api.Session;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketConnect;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketMessage;
import org.eclipse.jetty.websocket.api.annotations.WebSocket;
import sun.misc.Signal;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintWriter;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Scanner;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.stream.Collectors;

import static io.mindmaps.util.REST.RemoteShell.ACTION;
import static io.mindmaps.util.REST.RemoteShell.ACTION_AUTOCOMPLETE;
import static io.mindmaps.util.REST.RemoteShell.ACTION_COMMIT;
import static io.mindmaps.util.REST.RemoteShell.ACTION_NAMESPACE;
import static io.mindmaps.util.REST.RemoteShell.ACTION_QUERY;
import static io.mindmaps.util.REST.RemoteShell.ACTION_QUERY_END;
import static io.mindmaps.util.REST.RemoteShell.AUTOCOMPLETE_CURSOR;
import static io.mindmaps.util.REST.RemoteShell.ERROR;
import static io.mindmaps.util.REST.RemoteShell.NAMESPACE;
import static io.mindmaps.util.REST.RemoteShell.QUERY;
import static io.mindmaps.util.REST.RemoteShell.QUERY_LINES;
import static io.mindmaps.util.REST.WebPath.REMOTE_SHELL_URI;

/**
 * A Graql REPL shell that can be run from the command line
 */
@WebSocket
public class GraqlShell implements AutoCloseable {
    private static final String LICENSE_PROMPT = "\n" + "MindmapsDB  Copyright (C) 2016  Mindmaps Research Ltd \n"
            + "This is free software, and you are welcome to redistribute it \n"
            + "under certain conditions; type 'license' for details.\n";

    private static final String LICENSE_LOCATION = "LICENSE.txt";

    private static final String DEFAULT_NAMESPACE = "mindmaps";
    private static final String DEFAULT_URI = "localhost:4567";

    private static final String PROMPT = ">>> ";

    private static final String EDIT_COMMAND = "edit";
    private static final String COMMIT_COMMAND = "commit";
    private static final String LOAD_COMMAND = "load";
    private static final String CLEAR_COMMAND = "clear";
    private static final String EXIT_COMMAND = "exit";
    private static final String LICENSE_COMMAND = "license";

    /**
     * Array of available commands in shell
     */
    public static final String[] COMMANDS = { EDIT_COMMAND, COMMIT_COMMAND, LOAD_COMMAND, CLEAR_COMMAND,
            EXIT_COMMAND };

    private static final String TEMP_FILENAME = "/graql-tmp.gql";
    private static final String HISTORY_FILENAME = "/graql-history";

    private static final String DEFAULT_EDITOR = "vim";

    private final File tempFile = new File(System.getProperty("java.io.tmpdir") + TEMP_FILENAME);
    private final String namespace;
    private ConsoleReader console;

    // A future containing the session, once the client has connected
    private CompletableFuture<Session> session = new CompletableFuture<>();

    // A future containing an autocomplete result, once it has been received
    private CompletableFuture<Json> autocompleteResponse = new CompletableFuture<>();

    private boolean waitingQuery = false;

    /**
     * Run a Graql REPL
     * @param args arguments to the Graql shell. Possible arguments can be listed by running {@code graql.sh --help}
     */
    public static void main(String[] args) {
        runShell(args, Version.VERSION, new GraqlClientImpl());
    }

    public static void runShell(String[] args, String version, GraqlClient client) {

        Options options = new Options();
        options.addOption("n", "name", true, "name of the graph");
        options.addOption("e", "execute", true, "query to execute");
        options.addOption("f", "file", true, "graql file path to execute");
        options.addOption("u", "uri", true, "uri to connect to engine");
        options.addOption("h", "help", false, "print usage message");
        options.addOption("v", "version", false, "print version");

        CommandLineParser parser = new DefaultParser();
        CommandLine cmd;

        try {
            cmd = parser.parse(options, args);
        } catch (ParseException e) {
            System.err.println(e.getMessage());
            return;
        }

        String query = cmd.getOptionValue("e");
        String filePath = cmd.getOptionValue("f");

        // Print usage message if requested or if invalid arguments provided
        if (cmd.hasOption("h") || !cmd.getArgList().isEmpty()) {
            HelpFormatter helpFormatter = new HelpFormatter();
            PrintWriter printWriter = new PrintWriter(System.out);
            int width = helpFormatter.getWidth();
            int leftPadding = helpFormatter.getLeftPadding();
            int descPadding = helpFormatter.getDescPadding();
            helpFormatter.printHelp(printWriter, width, "graql.sh", null, options, leftPadding, descPadding, null);
            printWriter.flush();
            return;
        }

        if (cmd.hasOption("v")) {
            System.out.println(version);
            return;
        }

        String namespace = cmd.getOptionValue("n", DEFAULT_NAMESPACE);
        String uriString = cmd.getOptionValue("u", DEFAULT_URI);

        try (GraqlShell shell = new GraqlShell(namespace)) {
            client.connect(shell, new URI("ws://" + uriString + REMOTE_SHELL_URI));

            if (filePath != null) {
                query = loadQuery(filePath);
            }

            if (query != null) {
                shell.executeQuery(query);
                shell.commit();
            } else {
                shell.executeRepl();
            }
        } catch (IOException | InterruptedException | ExecutionException | URISyntaxException e) {
            System.err.println(e.toString());
        } finally {
            client.close();
        }
    }

    private static String loadQuery(String filePath) throws IOException {
        List<String> lines = Files.readAllLines(Paths.get(filePath), StandardCharsets.UTF_8);
        return lines.stream().collect(Collectors.joining("\n"));
    }

    /**
     * Create a new Graql shell
     */
    GraqlShell(String namespace) throws IOException {
        this.namespace = namespace;
        console = new ConsoleReader(System.in, System.out);

        // Create handler to handle SIGINT (Ctrl-C) interrupts
        Signal signal = new Signal("INT");
        GraqlSignalHandler signalHandler = new GraqlSignalHandler(this);
        Signal.handle(signal, signalHandler);
    }

    @Override
    public void close() throws IOException, ExecutionException, InterruptedException {
        console.flush();
        session.get().close();
    }

    /**
     * Run a Read-Evaluate-Print loop until the input terminates
     */
    void executeRepl() throws IOException {
        console.print(LICENSE_PROMPT);

        // Disable JLine feature when seeing a '!', which is used in our queries
        console.setExpandEvents(false);

        console.setPrompt(PROMPT);

        // Create temporary file
        if (!tempFile.exists()) {
            boolean success = tempFile.createNewFile();
            if (!success)
                print(ErrorMessage.COULD_NOT_CREATE_TEMP_FILE.getMessage());
        }

        // Create history file
        File historyFile = new File(System.getProperty("java.io.tmpdir") + HISTORY_FILENAME);
        //noinspection ResultOfMethodCallIgnored
        historyFile.createNewFile();
        FileHistory history = new FileHistory(historyFile);
        console.setHistory(history);

        // Add all autocompleters
        console.addCompleter(new AggregateCompleter(new GraQLCompleter(this), new ShellCommandCompleter()));

        String queryString;

        while ((queryString = console.readLine()) != null) {
            history.flush();

            switch (queryString) {
            case EDIT_COMMAND:
                executeQuery(runEditor());
                break;
            case COMMIT_COMMAND:
                commit();
                break;
            case CLEAR_COMMAND:
                console.clearScreen();
                break;
            case LICENSE_COMMAND:
                printLicense();
                break;
            case EXIT_COMMAND:
                return;
            case "":
                // Ignore empty command
                break;
            default:
                // Load from a file if load command used
                if (queryString.startsWith(LOAD_COMMAND + " ")) {
                    String path = queryString.substring(LOAD_COMMAND.length() + 1);

                    try {
                        queryString = loadQuery(path);
                    } catch (IOException e) {
                        System.err.println(e.toString());
                        break;
                    }
                }

                executeQuery(queryString);
                break;
            }
        }
    }

    private void printLicense() {
        StringBuilder result = new StringBuilder("");

        //Get file from resources folder
        ClassLoader classloader = Thread.currentThread().getContextClassLoader();
        InputStream is = classloader.getResourceAsStream(LICENSE_LOCATION);

        Scanner scanner = new Scanner(is);
        while (scanner.hasNextLine()) {
            String line = scanner.nextLine();
            result.append(line).append("\n");
        }
        result.append("\n");
        scanner.close();

        this.print(result.toString());
    }

    @OnWebSocketConnect
    public void onConnect(Session session) throws IOException, ExecutionException, InterruptedException {
        // Send the requested keyspace to the server once connected
        sendJson(Json.object(ACTION, ACTION_NAMESPACE, NAMESPACE, namespace), session);
        this.session.complete(session);
    }

    @OnWebSocketMessage
    public void onMessage(String msg) {
        Json json = Json.read(msg);

        if (json.has(ERROR)) {
            System.err.println(json.at(ERROR).asString());
        }

        switch (json.at(ACTION).asString()) {
        case ACTION_QUERY:
            List<Json> lines = json.at(QUERY_LINES).asJsonList();
            lines.forEach(line -> println(line.asString()));
            break;
        case ACTION_QUERY_END:
            // Alert the shell that the query has finished, so it can prompt for another query
            synchronized (this) {
                notifyAll();
            }
            break;
        case ACTION_AUTOCOMPLETE:
            autocompleteResponse.complete(json);
            break;
        }
    }

    private void executeQuery(String queryString) {
        try {
            sendJson(Json.object(ACTION, ACTION_QUERY, QUERY, queryString));

            // Wait for the end of the query results before continuing
            waitingQuery = true;
            synchronized (this) {
                wait();
            }
            waitingQuery = false;

        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }

    private void commit() {
        sendJson(Json.object(ACTION, ACTION_COMMIT));
    }

    /**
     * load the user's preferred editor to edit a query
     * @return the string written to the editor
     */
    private String runEditor() throws IOException {
        // Get preferred editor
        Map<String, String> env = System.getenv();
        String editor = Optional.ofNullable(env.get("EDITOR")).orElse(DEFAULT_EDITOR);

        // Run the editor, pipe input into and out of tty so we can provide the input/output to the editor via Graql
        ProcessBuilder builder = new ProcessBuilder("/bin/bash", "-c",
                editor + " </dev/tty >/dev/tty " + tempFile.getAbsolutePath());

        // Wait for user to finish editing
        try {
            builder.start().waitFor();
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }

        return String.join("\n", Files.readAllLines(tempFile.toPath()));
    }

    /**
     * Interrupt the shell. If the user is waiting for query results, tell the server to stop sending them.
     * Otherwise, exit normally.
     */
    public void interrupt() {
        if (waitingQuery) {
            sendJson(Json.object(ACTION, ACTION_QUERY_END));
            waitingQuery = false;
        } else {
            System.exit(0);
        }
    }

    public synchronized Json getAutocompleteCandidates(String queryString, int cursorPosition)
            throws InterruptedException, ExecutionException, IOException {
        sendJson(Json.object(ACTION, ACTION_AUTOCOMPLETE, QUERY, queryString, AUTOCOMPLETE_CURSOR, cursorPosition));

        Json json = autocompleteResponse.get();

        autocompleteResponse = new CompletableFuture<>();

        return json;
    }

    private void sendJson(Json json) {
        try {
            sendJson(json, session.get());
        } catch (InterruptedException | ExecutionException e) {
            throw new RuntimeException(e);
        }
    }

    private void sendJson(Json json, Session session) {
        try {
            session.getRemote().sendString(json.toString());
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    private void print(String string) {
        try {
            console.print(string);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    private void println(String string) {
        print(string + "\n");
    }
}