ConsoleIO.java

/*******************************************************************************
 * Copyright (c) 2015 Eclipse RDF4J contributors, Aduna, and others.
 *
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Distribution License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/org/documents/edl-v10.php.
 *
 * SPDX-License-Identifier: BSD-3-Clause
 *******************************************************************************/
package org.eclipse.rdf4j.console;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.file.Path;
import java.nio.file.Paths;

import org.eclipse.rdf4j.rio.RDFParseException;
import org.jline.reader.EndOfFileException;
import org.jline.reader.History;
import org.jline.reader.LineReader;
import org.jline.reader.LineReaderBuilder;
import org.jline.reader.impl.history.DefaultHistory;
import org.jline.terminal.Terminal;
import org.jline.terminal.TerminalBuilder;

/**
 * @author Dale Visser
 */
public class ConsoleIO {
	private static final String PLEASE_OPEN_FIRST = "please open a repository first";

	private final Terminal terminal;
	private final LineReader input;
	private final ConsoleState appInfo;

	private boolean echo = false;
	private boolean quiet = false;
	private boolean force = false;
	private boolean cautious = false;
	private boolean errorWritten;

	/**
	 * Constructor
	 *
	 * @param input
	 * @param out
	 * @param info
	 * @throws IOException
	 */
	public ConsoleIO(InputStream input, OutputStream out, ConsoleState info) throws IOException {
		this.terminal = TerminalBuilder.builder().system(false).streams(input, out).build();
		this.appInfo = info;
		this.input = buildLineReader();
	}

	/**
	 * Constructor
	 *
	 * @param info
	 * @throws IOException
	 */
	public ConsoleIO(ConsoleState info) throws IOException {
		this.terminal = TerminalBuilder.terminal();
		this.appInfo = info;
		this.input = buildLineReader();
	}

	/**
	 * Build JLine line reader with default history
	 *
	 * @return line reader
	 */
	private LineReader buildLineReader() {
		History history = new DefaultHistory();
		LineReader reader = LineReaderBuilder.builder().terminal(this.terminal).history(history).build();

		Path file = Paths.get(appInfo.getDataDirectory().toString(), "history.txt");
		reader.setVariable(LineReader.HISTORY_FILE, file);

		return reader;
	}

	/**
	 * Get the JLine line reader
	 *
	 * @return line reader
	 */
	public LineReader getLineReader() {
		return this.input;
	}

	/**
	 * Get JLine terminal output stream
	 *
	 * @return output stream
	 */
	public OutputStream getOutputStream() {
		return terminal.output();
	}

	/**
	 * Read a command from input
	 *
	 * @return one line of input, or null on error
	 */
	protected String readCommand() {
		try {
			String line = input.readLine(getPrompt());
			if (line == null) {
				return null;
			}
			line = line.trim();
			if (line.endsWith(".")) {
				line = line.substring(0, line.length() - 1);
			}
			return line;
		} catch (EndOfFileException e) {
			return null;
		}
	}

	/**
	 * Get command prompt.
	 *
	 * Contains the name of the current repository when connected.
	 *
	 * @return command prompt string
	 */
	private String getPrompt() {
		String repositoryID = appInfo.getRepositoryID();
		if (quiet) {
			return "";
		} else if (repositoryID != null) {
			return repositoryID + "> ";
		} else {
			return "> ";
		}
	}

	/**
	 * Reads multiple lines from the input until a line that with a '.' on its own is read.
	 *
	 * @return input string
	 */
	public String readMultiLineInput() {
		return readMultiLineInput("> ");
	}

	/**
	 * Reads multiple lines from the input until a line that with a '.' on its own is read.
	 *
	 * @param prompt
	 * @return input string
	 */
	public String readMultiLineInput(String prompt) {
		String line = input.readLine(prompt);
		String result = null;

		if (line != null) {
			final StringBuilder buf = new StringBuilder(256);
			buf.append(line);

			while (line != null && !(line.length() == 1 && line.endsWith("."))) {
				line = input.readLine("> ");
				buf.append('\n');
				buf.append(line);
			}
			// Remove closing dot
			buf.setLength(buf.length() - 1);
			result = buf.toString().trim();
		}
		if (echo) {
			writeln(result);
		}
		return result;
	}

	/**
	 * Read message from input
	 *
	 * @param message one or multiple messages
	 * @return input string
	 */
	public String readln(String... message) {
		String prompt = !quiet && message.length > 0 && message[0] != null ? message[0] : "";
		String result = input.readLine(prompt);

		if (echo) {
			writeln(result);
		}
		return result;
	}

	/**
	 * Read password from input
	 *
	 * @param prompt prompt to display
	 * @return password string
	 */
	public String readPassword(final String prompt) {
		String result = input.readLine(prompt, '*');
		if (echo && !result.isEmpty()) {
			writeln("************");
		}
		return result;
	}

	/**
	 * Write a string
	 *
	 * @param string string to write
	 */
	public void write(final String string) {
		terminal.writer().print(string);
	}

	/**
	 * Write a newline
	 */
	public void writeln() {
		terminal.writer().println();
	}

	/**
	 * Write a string, followed by a newline
	 *
	 * @param string string to write
	 */
	public void writeln(final String string) {
		terminal.writer().println(string);
	}

	/**
	 * Write an error message
	 *
	 * @param errMsg error message
	 */
	public void writeError(final String errMsg) {
		terminal.writer().println(errMsg);
		errorWritten = true;
	}

	/**
	 * Write a "please open first" error message
	 */
	public void writeUnopenedError() {
		writeError(PLEASE_OPEN_FIRST);
	}

	/**
	 * Write parser error
	 *
	 * @param prefix
	 * @param lineNo line number
	 * @param colNo  column number
	 * @param msg    message to write
	 */
	public void writeParseError(String prefix, long lineNo, long colNo, String msg) {
		String locationString = RDFParseException.getLocationString(lineNo, colNo);
		int locSize = locationString.length();

		StringBuilder builder = new StringBuilder(locSize + prefix.length() + msg.length() + 3);
		builder.append(prefix).append(": ").append(msg);
		if (locSize > 0) {
			builder.append(" ").append(locationString);
		}
		writeError(builder.toString());
	}

	/**
	 * Ask if the user wants to continue
	 *
	 * @param msg          confirmation question
	 * @param defaultValue true when default is yes
	 * @return true when continue
	 */
	public boolean askProceed(String msg, boolean defaultValue) {
		final String defaultString = defaultValue ? "yes" : "no";
		boolean result = force ? true : (cautious ? false : defaultValue);

		if (!force && !cautious) {
			while (true) {
				writeln(msg);
				final String reply = readln("Proceed? (yes|no) [" + defaultString + "]: ");
				if ("no".equalsIgnoreCase(reply) || "no.".equalsIgnoreCase(reply)) {
					result = false;
					break;
				} else if ("yes".equalsIgnoreCase(reply) || "yes.".equalsIgnoreCase(reply)) {
					result = true;
					break;
				} else if (reply.trim().isEmpty()) {
					break;
				}
			}
		}
		return result;
	}

	/**
	 * Whether to echo user input to output stream
	 *
	 * @param echo true to echo input
	 */
	protected void setEcho(boolean echo) {
		this.echo = echo;
	}

	/**
	 * Whether to suppress printing of prompts to output
	 *
	 * @param quiet true to suppress printing
	 */
	public void setQuiet(boolean quiet) {
		this.quiet = quiet;
	}

	/**
	 * Force commands to proceed
	 */
	public void setForce() {
		this.force = true;
	}

	/**
	 * Be cautious when executing commands, opposite of force
	 */
	public void setCautious() {
		this.cautious = true;
	}

	/**
	 * Check if an error was written to the console
	 *
	 * @return true when error was written
	 */
	public boolean wasErrorWritten() {
		return errorWritten;
	}
}