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

oracle.kv.util.shell.ShellInputReader Maven / Gradle / Ivy

Go to download

NoSQL Database Server - supplies build and runtime support for the server (store) side of the Oracle NoSQL Database.

The newest version!
/*-
 * Copyright (C) 2011, 2018 Oracle and/or its affiliates. All rights reserved.
 *
 * This file was distributed by Oracle as part of a version of Oracle NoSQL
 * Database made available at:
 *
 * http://www.oracle.com/technetwork/database/database-technologies/nosqldb/downloads/index.html
 *
 * Please see the LICENSE file included in the top-level directory of the
 * appropriate version of Oracle NoSQL Database for a copy of the license and
 * additional information.
 */

package oracle.kv.util.shell;

import java.io.BufferedReader;
import java.io.Console;
import java.io.File;
import java.io.IOError;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.PrintStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.StringTokenizer;

import oracle.kv.util.shell.Shell.CommandHistory;

public class ShellInputReader {

    /* JLine 3 APIs */
    /* org.jline.reader.LineReader methods */
    private Method readLineMeth;
    private Method readLineWithMaskMeth;
    private Method setVariableMeth;
    private Method setOptMeth;
    private String histFileVar;
    private String histFileSizeVar;
    /* org.jline.terminal.Terminal methods */
    private Method getHeightMeth;
    private Method closeMeth;
    private Method resumeMeth;
    private Method pauseMeth;
    private Method isPausedMeth;
    /* org.jline.reader.History methods */
    private Method sizeMeth;
    private Method getMeth;
    private Method saveMeth;
    /* A boolean flag that indicates if the terminal is in paused state */
    private boolean isTermPaused;
    /**
     * LineReader.readLine() may throw UserInterruptException and
     * EndOfFileException.
     */
    private Class userInterruptException;
    private Class endOfFileException;

    /* Default value for terminal height */
    private static final int TERMINAL_HEIGHT_DEFAULT = 25;

    /* Property name to disable JLine. */
    private static final String PROP_JLINE_DISABLE =
        "oracle.kv.shell.jline.disable";
    /* Property name of JLine history file. */
    private static final String PROP_HISTORY_FILE =
        "oracle.kv.shell.history.file";
    /* Property name of JLine history size. */
    private static final String PROP_HISTORY_SIZE =
        "oracle.kv.shell.history.size";

    private Object jReaderObj = null;
    private Object jFileHistoryObj = null;
    private Object jTermObj = null;
    private BufferedReader inputReader = null;
    private PrintStream output = null;
    private String prompt = "";

    public ShellInputReader(InputStream input,
                            PrintStream output) {
        this(input, output, null, true, null);
    }

    public ShellInputReader(Shell shell) {
        this(shell.input, shell.output, getHistoryFile(shell),
             shell.isJlineEventDesignatorDisabled(), shell.getMaskFlags());
        loadCommandHistory(shell);
    }

    public ShellInputReader(InputStream input,
                            PrintStream output,
                            File historyFile,
                            boolean disableExpandEvents,
                            String[] maskFlags) {
        initInputReader(input, output, historyFile,
                        disableExpandEvents, maskFlags);
        this.output = output;
    }

    @SuppressWarnings({"unchecked", "rawtypes"})
    private void initInputReader(InputStream input,
                                 PrintStream output1,
                                 File historyFile,
                                 boolean disableExpandEvents,
                                 String[] maskFlags) {

        if (!isJlineCompatiblePlatform()) {
            inputReader = new BufferedReader(new InputStreamReader(input));
            return;
        }

        try {
            final Class readerBuilder =
                Class.forName("org.jline.reader.LineReaderBuilder");
            final Class reader =
                Class.forName("org.jline.reader.LineReader");
            final Class termBuilder =
                Class.forName("org.jline.terminal.TerminalBuilder");
            final Class term =
                Class.forName("org.jline.terminal.Terminal");
            final Class dumbTerm =
                Class.forName("org.jline.terminal.impl.DumbTerminal");
            final Class history =
                Class.forName("org.jline.reader.History");
            final Class defaultHistory =
                Class.forName("org.jline.reader.impl.history.DefaultHistory");
            final Class readerOpt =
                Class.forName("org.jline.reader.LineReader$Option");
            userInterruptException =
                Class.forName("org.jline.reader.UserInterruptException");
            endOfFileException =
                Class.forName("org.jline.reader.EndOfFileException");

            /* TerminalBuilder methods */
            final Method tbBuilder = termBuilder.getMethod("builder");
            final Method tbBuild = termBuilder.getMethod("build");
            final Method tbSystem =
                termBuilder.getMethod("system", boolean.class);
            final Method tbPaused =
                termBuilder.getMethod("paused", boolean.class);

            /* Terminal methods */
            getHeightMeth = term.getMethod("getHeight");
            closeMeth = term.getMethod("close");
            resumeMeth = term.getMethod("resume");
            pauseMeth = term.getMethod("pause");
            isPausedMeth = term.getMethod("paused");

            /* LineReaderBuilder methods */
            final Method lrbBuilder = readerBuilder.getMethod("builder");
            final Method lrbBuild = readerBuilder.getMethod("build");
            final Method lrbTerm = readerBuilder.getMethod("terminal", term);
            final Method lrbHistory =
                readerBuilder.getMethod("history", history);

            /* LineReader methods */
            readLineMeth = reader.getMethod("readLine", String.class);
            readLineWithMaskMeth =
                reader.getMethod("readLine", String.class, Character.class);
            setVariableMeth =
                reader.getMethod("setVariable", String.class, Object.class);
            setOptMeth = reader.getMethod("setOpt", readerOpt);

            /* History methods */
            sizeMeth = defaultHistory.getMethod("size");
            getMeth = defaultHistory.getMethod("get", int.class);
            saveMeth = defaultHistory.getMethod("save");

            /* Create a Terminal instance */
            Object termBuilderObj = tbBuilder.invoke(null);
            final boolean createSysTerm =
                (System.in == input) && (System.out == output1);
            if (createSysTerm) {
                /**
                 * Try to create a system terminal. If the input device is not
                 * a tty, or jna/jansi library does not exist in the classpath,
                 * jline would fall back to create a DumbTerminal.
                 */
                termBuilderObj = tbSystem.invoke(termBuilderObj, true);
                /**
                 * Initialize the terminal with paused state so that the input
                 * streams are not consumed until LineReader.readLine() is
                 * called.
                 */
                termBuilderObj = tbPaused.invoke(termBuilderObj, true);
                jTermObj = tbBuild.invoke(termBuilderObj);
            } else {
                /**
                 * As of jline-3.7.1, the terminal implementations that use the
                 * pump mechanism (PosixPtyTerminal and ExternalTerminal)
                 * would repeatedly throw IOExceptions that originate from
                 * the underlying input stream. This leads to an issue that
                 * once an IOException occurs, subsequent reads would always
                 * fail no matter the IOException is recoverable or not (see
                 * https://github.com/jline/jline3/issues/270). We have to
                 * use the DumbTerminal which does not depend on the pump
                 * mechanism to work around the issue. As the TerminalBuilder
                 * API cannot create a DumbTerminal instance with supplied
                 * input/output streams, we have to call the constructor
                 * directly.
                 */
                final Constructor dumbTermCtor = dumbTerm.getConstructor(
                    new Class[] {InputStream.class, OutputStream.class});
                jTermObj = dumbTermCtor.newInstance(input, output1);
                pauseMeth.invoke(jTermObj);
            }
            isTermPaused = (Boolean) isPausedMeth.invoke(jTermObj);

            Object lrbObj = lrbBuilder.invoke(null);
            lrbObj = lrbTerm.invoke(lrbObj, jTermObj);
            if (historyFile != null) {
                final Constructor histCtor =
                    defaultHistory.getConstructor();
                jFileHistoryObj = histCtor.newInstance();
                final Object historyObj = (maskFlags == null) ?
                    jFileHistoryObj :
                    FileHistoryProxy.create(jFileHistoryObj, maskFlags);
                lrbObj = lrbHistory.invoke(lrbObj, historyObj);
            }

            /* Create a LineReader instance */
            jReaderObj = lrbBuild.invoke(lrbObj);
            if (historyFile != null) {
                final Field histFile = reader.getField("HISTORY_FILE");
                final Field histFileSize =
                    reader.getField("HISTORY_FILE_SIZE");
                histFileVar = (String) histFile.get(jReaderObj);
                histFileSizeVar = (String) histFileSize.get(jReaderObj);
                setReaderVariable(histFileVar, historyFile.getAbsolutePath());
                /* set history file size */
                setHistoryFileSize();
            }

            /* Disable the event designators, it is enabled by default */
            if (disableExpandEvents) {
                final Enum[] constants =
                    (Enum[]) readerOpt.getEnumConstants();
                for (Enum e : constants) {
                    if ("DISABLE_EVENT_EXPANSION".equals(e.name())) {
                        final Enum option =
                            Enum.valueOf((Class) readerOpt, e.name());
                        setReaderOption(option);
                        break;
                    }
                }
            }
        } catch (Exception ignored)  /* CHECKSTYLE:OFF */ {
        } /* CHECKSTYLE:ON */

        if (jReaderObj == null) {
            /* Use normal inputStreamReader if failed to load jline library */
            inputReader = new BufferedReader(new InputStreamReader(input));
        }
    }

    /**
     * Set a variable for the LineReader object by invoking the
     * LineReader.setVariable(String, Object) API
     * @param name variable name
     * @param value variable value
     */
    private void setReaderVariable(String name, Object value) {
        if (jReaderObj != null && setVariableMeth != null) {
            try {
                invokeMethod(jReaderObj, setVariableMeth,
                             new Object[] {name, value});
            } catch (Exception ignored) /* CHECKSTYLE:OFF */ {
            } /* CHECKSTYLE:ON */
        }
    }

    /**
     * Set an option for the LineReader object by invoking the
     * LineReader.setOpt(LineReader.Option) API
     * @param option a LineReader option
     */
    private void setReaderOption(Enum option) {
        if (jReaderObj != null && setOptMeth != null) {
            try {
                invokeMethod(jReaderObj, setOptMeth, new Object[] {option});
            } catch (Exception ignored) /* CHECKSTYLE:OFF */ {
            } /* CHECKSTYLE:ON */
        }
    }

    /**
     * Resume the terminal associated with LineReader by invoking the
     * Terminal.resume() API, which would create a new thread to
     * handle the underlying input streams if the previous one is stopped.
     */
    private void resumeTerminal() {
        if (jTermObj != null && resumeMeth != null) {
            try {
                invokeMethod(jTermObj, resumeMeth, null);
                isTermPaused = false;
            } catch (Exception ignored) /* CHECKSTYLE:OFF */ {
            } /* CHECKSTYLE:ON */
        }
    }

    /* Load command history to shell.CommandHistory. */
    private void loadCommandHistory(Shell shell) {
        if (jFileHistoryObj == null) {
            return;
        }
        final CommandHistory history = shell.getHistory();
        try {
            final int size = getHistorySize();
            if (size == 0) {
                return;
            }
            for (int i = 0; i < size; i++) {
                history.add(getHistoryCommand(i), null);
            }
        } catch (IOException ignored) /* CHECKSTYLE:OFF */  {
            /* Continue if loading command history from history file failed. */
        } /* CHECKSTYLE:ON */
    }

    /* Get number of commands in the history file. */
    private int getHistorySize()
        throws IOException {

        if (jFileHistoryObj != null && sizeMeth != null) {
            return (Integer) invokeMethod(jFileHistoryObj, sizeMeth, null);
        }
        return 0;
    }

    /* Get nth command in the history file. */
    private String getHistoryCommand(int index)
        throws IOException {

        if (jFileHistoryObj != null && getMeth != null) {
            return (String) invokeMethod(jFileHistoryObj, getMeth,
                new Object[] {Integer.valueOf(index)});
        }
        return "";
    }

    /**
     * Return the file for JLine commands history.
     * jline 3 API cannot read history files created by jline2, so we have to
     * use a different naming convention (.jline3-* vs .jline-*) for the
     * history file to avoid conflicts when loading the old history files.
     *
     * If the property is not set, return the default path
     * /.jline3-.history.
     * Return null if the specified file is not readable or writable.
     */
    private static File getHistoryFile(Shell shell) {
        final String path = System.getProperty(PROP_HISTORY_FILE);
        File file = null;
        if (path != null) {
            file = new File(path);
        } else {
            file = new File(System.getProperty("user.home"),
                        ".jline3-" + shell.getClass().getName() + ".history");
        }
        if (!file.exists()) {
            try {
                file.createNewFile();
            } catch (IOException e) {
                shell.verboseOutput("Failed to create the command line " +
                    "history file: " + file.getAbsolutePath() +
                    ", command history will be stored in memory.");
                return null;
            }
        } else {
            if (!file.canRead() || !file.canWrite()) {
                shell.verboseOutput("Cannot access the command line " +
                    "history file: " + file.getAbsolutePath() +
                    ", command history will be stored in memory.");
                return null;
            }
        }
        return file;
    }

    private void setHistoryFileSize() {

        final String historySize = System.getProperty(PROP_HISTORY_SIZE);
        if (historySize == null) {
            return;
        }

        int maxSize;
        try {
            maxSize = Integer.valueOf(historySize);
        } catch (NumberFormatException nfe) {
            return;
        }
        if (histFileSizeVar != null) {
            setReaderVariable(histFileSizeVar, maxSize);
        }
    }

    public void shutdown() {
        if (jTermObj != null && closeMeth != null) {
            try {
                invokeMethod(jTermObj, closeMeth, null);
            } catch (IOException ignored) /* CHECKSTYLE:OFF */ {
            } /* CHECKSTYLE:ON */
        }

        if (jFileHistoryObj != null && saveMeth != null) {
            try {
                invokeMethod(jFileHistoryObj, saveMeth, null);
            } catch (IOException ignored) /* CHECKSTYLE:OFF */ {
            } /* CHECKSTYLE:ON */
        }
    }

    private boolean isJlineCompatiblePlatform() {
        /* Check system property that whether jline is disabled. */
        if (Boolean.getBoolean(PROP_JLINE_DISABLE)) {
            return false;
        }

        final String os = System.getProperty("os.name").toLowerCase();
        if (os.indexOf("windows") != -1) {
            /**
             * Disable jline on Windows because of a Cygwin problem:
             * https://github.com/jline/jline2/issues/62
             * This will be fixed in a later patch.
             */
            return false;
        }
        return true;
    }

    public void setDefaultPrompt(String prompt) {
        this.prompt = prompt;
    }

    public String getDefaultPrompt() {
        return this.prompt;
    }

    public String readLine()
        throws IOException {

        return readLine(null);
    }

    public String readLine(String promptString)
        throws IOException {
        final String promptStr = (promptString != null) ?
            promptString : this.prompt;
        if (isTermPaused) {
            resumeTerminal();
        }
        if (jReaderObj != null && readLineMeth != null) {
            return (String) invokeMethod(jReaderObj, readLineMeth,
                                        new Object[]{promptStr});
        }
        if (promptStr != null) {
            output.print(promptStr);
        }
        return inputReader.readLine();
    }

    public char[] readPassword(String promptString) throws IOException {
        String input = null;
        final String pwdPrompt = (promptString != null) ? promptString :
                                                          this.prompt;
        if (isTermPaused) {
            resumeTerminal();
        }
        if (jReaderObj != null && readLineWithMaskMeth != null) {
            input = (String) invokeMethod(jReaderObj, readLineWithMaskMeth,
                new Object[]{pwdPrompt, Character.valueOf((char) 0)});
            return input == null ? null : input.toCharArray();
        }

        final Console console = System.console();
        if (console != null) {
            return console.readPassword(pwdPrompt);
        }

        output.print(pwdPrompt);
        input = inputReader.readLine();
        return input == null ? null : input.toCharArray();
    }

    public int getTerminalHeight() {
        if (jTermObj != null && getHeightMeth != null) {
            try {
                return (Integer) invokeMethod(jTermObj, getHeightMeth, null);
            } catch (IOException ignored)  /* CHECKSTYLE:OFF */ {
            } /* CHECKSTYLE:ON */
        }
        return getTermHeightImpl();
    }

    private Object invokeMethod(Object obj, Method method, Object[] args)
        throws IOException {

        final String name = method.getName();
        try {
            return method.invoke(obj, args);
        } catch (IllegalAccessException | IllegalArgumentException ex) {
            final String clsName = method.getDeclaringClass().getSimpleName();
            final String msg =
                String.format("Failed to invoke %s.%s.", clsName, name);
            throw new IOException(msg, ex);
        } catch (InvocationTargetException ite) {
            final Throwable cause = ite.getCause();
            if (cause == null) {
                throw new IOException(ite);
            }
            if (obj == jReaderObj && "readLine".equals(name)) {
                if (cause instanceof IOError) {
                    /**
                     * As of jline-3.7.1, if terminal input reader runs into an
                     * IOException, it stops reading and wraps the IOException
                     * in an IOError, which is conveyed back to users over the
                     * LineReader.readLine() API. We need to resume the
                     * terminal in the next read attempt.
                     */
                    isTermPaused = true;
                } else if (userInterruptException.isInstance(cause) ||
                           endOfFileException.isInstance(cause)) {
                    return null;
                }
            }
            throw new IOException(cause.getMessage(), cause);
        }
    }

    private int getTermHeightImpl() {
        final String os = System.getProperty("os.name").toLowerCase();
        if (os.indexOf("windows") != -1) {
            return TERMINAL_HEIGHT_DEFAULT;
        }
        int height = getUnixTermHeight();
        if (height == -1) {
            height = TERMINAL_HEIGHT_DEFAULT;
        }
        return height;
    }

    /*
     * stty -a
     *  speed 38400 baud; rows 48; columns 165; line = 0; ...
     */
    private int getUnixTermHeight() {
        String ttyProps = null;
        final String name = "rows";
        try {
            ttyProps = getTermSttyProps();
            if (ttyProps != null && ttyProps.length() > 0) {
                return getTermSttyPropValue(ttyProps, name);
            }
        } catch (IOException ignored)  /* CHECKSTYLE:OFF */ {
        } catch (InterruptedException ignored) {
        } /* CHECKSTYLE:ON */
        return -1;
    }

    private String getTermSttyProps()
        throws IOException, InterruptedException {

        final String[] cmd = {"/bin/sh", "-c", "stty -a 




© 2015 - 2025 Weber Informatics LLC | Privacy Policy