
org.ow2.petals.cli.shell.AbstractShell Maven / Gradle / Ivy
/**
* Copyright (c) 2010-2012 EBM WebSourcing, 2012-2023 Linagora
*
* This program/library is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 2.1 of the License, or (at your
* option) any later version.
*
* This program/library 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 Lesser General Public License
* for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program/library; If not, see http://www.gnu.org/licenses/
* for the GNU Lesser General Public License version 2.1.
*/
package org.ow2.petals.cli.shell;
import java.io.PrintStream;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.commons.lang3.text.StrMatcher;
import org.apache.commons.lang3.text.StrTokenizer;
import org.ow2.petals.cli.api.command.Command;
import org.ow2.petals.cli.api.command.ConnectionCommand;
import org.ow2.petals.cli.api.command.DisconnectionCommand;
import org.ow2.petals.cli.api.command.exception.CommandException;
import org.ow2.petals.cli.api.command.exception.CommandInvalidException;
import org.ow2.petals.cli.api.command.exception.ConnectionErrorException;
import org.ow2.petals.cli.api.command.exception.DisconnectionErrorException;
import org.ow2.petals.cli.api.connection.ConnectionParameters;
import org.ow2.petals.cli.api.exception.NoInteractiveShellException;
import org.ow2.petals.cli.api.pref.Preferences;
import org.ow2.petals.cli.api.shell.Shell;
import org.ow2.petals.cli.api.shell.ShellExtension;
import org.ow2.petals.cli.api.shell.exception.ConnectionCommandAlreadyRegisteredException;
import org.ow2.petals.cli.api.shell.exception.DisconnectionCommandAlreadyRegisteredException;
import org.ow2.petals.cli.api.shell.exception.DuplicatedCommandException;
import org.ow2.petals.cli.api.shell.exception.ShellException;
import org.ow2.petals.cli.shell.exception.UnknownCommandException;
/**
* A shell registers and evaluates commands
*
* @author Sebastien Andre - EBM WebSourcing
* @author Christophe DENEUX - EBM WebSourcing
* @author Alexandre Lagane - Linagora
*/
public abstract class AbstractShell implements Shell {
/**
* The pattern of the default prompt: "base>"
*/
public static final String DEFAULT_PROMPT_PATTERN = "%s>";
/**
* Regular expression that matches comments in scripts
*/
public static final Pattern REGEX_COMMENT = Pattern.compile("^\\s*(#.*)?$");
/**
* Regular expression that matches variables in expressions
*/
public static final Pattern REGEX_VARIABLE = Pattern.compile("(\\\\|\\B)?(\\$\\{([:-_\\.\\w]+)\\})");
/**
* Prefix used to differentiate property and environment variable in variable expressions
*/
public static final String ENV_PREFIX = "env:";
/**
* Group number of the backslash placeholder in {@link #REGEX_VARIABLE}
*/
private static final short BACKSLASH_PLACEHOLDER = 1;
/**
* Group number of the bracket placeholder in {@link #REGEX_VARIABLE}
*/
private static final short BRACKET_PLACEHOLDER = 2;
/**
* Group number of the property name placeholder in {@link #REGEX_VARIABLE}
*/
private static final short PROPERTY_NAME_PLACEHOLDER = 3;
private final Map commands = new HashMap<>();
private ConnectionCommand connectionCommand = null;
private DisconnectionCommand disconnectionCommand = null;
protected final PrintStream printStream;
protected final PrintStream errStream;
/**
* Flag to stop reading command on the input stream
*/
protected boolean isCommandRead = true;
private int exitStatus = 0;
/**
* Flag indicating if the debug mode is enabled
*/
protected final boolean isDebugModeEnable;
/**
* Flag indicating if the automatic confirmation is enabled
*/
protected final boolean isYesFlagEnabled;
protected ConnectionParameters connectionParameters;
private final AtomicInteger asynchronousCommandsInProgress = new AtomicInteger(0);
/**
* Preferences
*/
private final Preferences preferences;
/**
* Shell extensions registered
*/
private final ShellExtension[] shellExtensions;
/**
* Base of the prompt.
*/
private final String basePrompt;
/**
* Flag about the connection state. true
, a connection is established. false
, no connection exists.
*/
private boolean isConnectionEstablished = false;
/**
*
* @param printStream
* @param errStream
* @param isDebugModeEnable
* @param isYesFlagEnable
* Flag to enable the automatic confirmation
* @param shellExtensions
* A list of shell extension, or null
if no shell extension is available
* @param basePrompt
* The base prompt
*/
public AbstractShell(final PrintStream printStream, final PrintStream errStream, final boolean isDebugModeEnable,
final boolean isYesFlagEnable, final Preferences preferences, final ShellExtension[] shellExtensions,
final String basePrompt) {
this.printStream = printStream;
this.errStream = errStream;
this.isDebugModeEnable = isDebugModeEnable;
this.isYesFlagEnabled = isYesFlagEnable;
this.basePrompt = basePrompt;
this.preferences = preferences;
this.shellExtensions = shellExtensions;
if (shellExtensions != null) {
for (final ShellExtension shellExtension : shellExtensions) {
shellExtension.onInit(this);
}
}
}
@Override
public final Map getCommands() {
return this.commands;
}
@Override
public void registersCommand(final Command command) throws DuplicatedCommandException,
IllegalArgumentException {
assert command != null;
final String name = command.getName();
if (this.commands.containsKey(name)) {
throw new DuplicatedCommandException(name);
} else {
this.commands.put(name, command);
}
}
@Override
public ConnectionCommand getConnectionCommand() {
return this.connectionCommand;
}
@Override
public DisconnectionCommand getDisconnectionCommand() {
return this.disconnectionCommand;
}
@Override
public void registersConnectionCommand(final ConnectionCommand connectionCommand)
throws ConnectionCommandAlreadyRegisteredException, DuplicatedCommandException, IllegalArgumentException {
assert connectionCommand != null;
if (this.connectionCommand == null) {
this.registersCommand(connectionCommand);
this.connectionCommand = connectionCommand;
} else {
throw new ConnectionCommandAlreadyRegisteredException(connectionCommand.getName());
}
}
@Override
public void registersDisconnectionCommand(final DisconnectionCommand disconnectionCommand)
throws DisconnectionCommandAlreadyRegisteredException, DuplicatedCommandException, IllegalArgumentException {
assert disconnectionCommand != null;
if (this.disconnectionCommand == null) {
this.registersCommand(disconnectionCommand);
this.disconnectionCommand = disconnectionCommand;
} else {
throw new DisconnectionCommandAlreadyRegisteredException(disconnectionCommand.getName());
}
}
/**
*
* Executes the shell.
*
*
* No exception is thrown. The implementation of the method is responsible
* for printing error messages and set the right exit code.
*
*/
public abstract void run();
/**
* Tests whether a line is a comment or not.
*
* @param line
* the string to be tested
* @return true if the line is a comment, false otherwise
*/
protected boolean isComment(final String line) {
return REGEX_COMMENT.matcher(line).matches();
}
/**
* Interpolates variables and special characters in a string.
*
* @param str
* the string to be interpolated
* @return a new string with interpolated variables
*/
@Override
public String interpolate(final String str) {
final Matcher m = REGEX_VARIABLE.matcher(str);
final StringBuffer sb = new StringBuffer();
while (m.find()) {
final String backslash = m.group(BACKSLASH_PLACEHOLDER);
final String property = m.group(BRACKET_PLACEHOLDER).replace("$", "\\$");
final String name = m.group(PROPERTY_NAME_PLACEHOLDER);
final String value;
if (name != null && name.startsWith(ENV_PREFIX)) {
// Environment variable
value = System.getenv(name.substring(ENV_PREFIX.length()));
} else {
// Property
value = System.getProperty(name);
}
if (value == null || backslash.length() > 0) {
m.appendReplacement(sb, property);
} else {
m.appendReplacement(sb, value);
}
}
m.appendTail(sb);
return sb.toString();
}
/**
* Sets an exit code and exit the current shell.
*
* @param status
* the optional result value
*/
@Override
public void exit(final int status) {
this.exitStatus = status;
this.isCommandRead = false;
// Wait the end of asynchronous commands
try {
while (this.isAsynchronousCommandInProgress()) {
Thread.sleep(1000);
}
} catch (final InterruptedException e) {
// No operation to exit the shell
}
}
/**
* Disconnect, if needed, Petals CLI from the current Petals ESB server.
*/
public void disconnectIfNeeded() {
// Disconnect if needed to prevent resource consumption if the disconnection command was not used
try {
if (this.disconnectionCommand.isConnected()) {
try {
this.disconnectionCommand.disconnect();
} catch (final DisconnectionErrorException e) {
this.errStream.println("WARNING: The following error occurs on disconnection: " + e.getMessage());
e.printStackTrace(this.errStream);
} catch (final CommandException e) {
this.errStream.println("WARNING: The following error occurs on disconnection: " + e.getMessage());
e.printStackTrace(this.errStream);
}
}
} catch (final DisconnectionErrorException e) {
this.errStream
.println("WARNING: The following error occurs checking if a connection is established before to force the disconnection: "
+ e.getMessage());
e.printStackTrace(this.errStream);
}
}
/**
*
* Evaluates a list of arguments and retrieve the associated command. Once
* the command is found, it is executed.
*
*
* @param args
* the command line. Not empty and not null.
* @param shouldIConnect
* Flag to established a connection, if needed, before to execute
* the command. true
: a connection should be
* established.
* @throws UnknownCommandException
* when the command was not found
* @throws ConnectionErrorException
* when an error occurs establishing a connection
* @throws CommandException
* when an error occurs during the execution of the command
*/
protected void evaluate(final String[] args, final boolean shouldIConnect)
throws UnknownCommandException, ConnectionErrorException, CommandException {
assert (args != null && args.length > 0);
final String cmdName = args[0];
if (this.commands.containsKey(cmdName)) {
final Command cmd = this.commands.get(cmdName);
final String[] nargs = new String[args.length - 1];
for (int i = 0; i < nargs.length; i++) {
nargs[i] = this.interpolate(args[i + 1]);
}
try {
if (shouldIConnect) {
// Connection automatically established because the command and shell require it.
this.connectionCommand.connect(this.connectionParameters, this.isYesFlagEnabled);
}
cmd.execute(nargs);
} finally {
cmd.resetOptions();
}
} else {
throw new UnknownCommandException(cmdName);
}
}
/**
*
* Evaluates a command line.
*
*
* Notes:
*
*
* - does nothing if the line is a comment,
* - no connection will be automatically established.
*
*
* @see #isComment(String)
* @param line
* the line to be evaluated
* @throws UnknownCommandException
* when the command was not found
* @throws CommandException
* when an error occurs during the execution of the command
*/
protected final void evaluate(final String line) throws UnknownCommandException, CommandException {
if (!this.isComment(line)) {
final String[] args = AbstractShell.getLineArgs(line);
this.evaluate(args, false);
}
}
private static final String[] getLineArgs(final String line) {
final StrMatcher delim = StrMatcher.splitMatcher();
final StrMatcher quote = StrMatcher.quoteMatcher();
final StrTokenizer tokenizer = new StrTokenizer(line, delim, quote);
return tokenizer.getTokenArray();
}
/**
* Gets the output stream where anything can be written, mainly by commands.
* @return The output stream.
*/
@Override
public PrintStream getPrintStream() {
return this.printStream;
}
@Override
public PrintStream getErrorStream() {
return this.errStream;
}
public int getExitStatus() {
return this.exitStatus;
}
@Override
public void setExitStatus(final int exitStatus) {
this.exitStatus = exitStatus;
}
/**
* Prints the error message of a command syntax error.
*
* The message is made up of the command name,
* the syntax error message and the command usage.
*
*
* @param ce
* The command syntax error to print
*/
public void printCommandSyntaxError(final CommandInvalidException ce) {
final Command command = ce.getCommand();
if (command != null) {
this.errStream.println("ERROR on command '" + ce.getCommand().getName() + "': "
+ ce.getMessage());
this.errStream.println(ce.getCommand().getUsage());
} else {
this.errStream.println("ERROR: " + ce.getMessage());
}
}
/**
* Prints the error message of a command execution error.
*
* The message is made up of the command name, the execution error message and, if debug
* mode is enable, the stack trace.
*
*
* @param ce
* The command syntax error to print
*/
public void printCommandExecutionError(final CommandException ce) {
final Command command = ce.getCommand();
if (command != null) {
this.errStream.println("ERROR on command '" + ce.getCommand().getName() + "': "
+ ce.getMessage());
} else {
this.errStream.println("ERROR: " + ce.getMessage());
}
if (this.isDebugModeEnable) {
ce.printStackTrace(this.errStream);
}
}
/**
* Prints an error message.
* If the debug mode is enabled, the stack trace is also printed.
*
* @param error
* The error. The message to print will be extracted from
* {@link Throwable#getMessage()}.
*/
public void printError(final Throwable error) {
this.printError(error, "ERROR: " + error.getMessage());
}
/**
* Prints an error message.
* If the debug mode is enabled, the stack trace is also printed.
*
* @param error
* The error
* @param msg
* The message to print.
*/
public void printError(final Throwable error, final String msg) {
this.errStream.println(msg);
if (this.isDebugModeEnable) {
error.printStackTrace(this.errStream);
}
}
/**
* @return true if the debug mode is enabled, false otherwise
*/
@Override
public boolean isDebugModeEnable() {
return this.isDebugModeEnable;
}
@Override
public String askQuestion(final String question, final boolean isReplyPassword)
throws NoInteractiveShellException, ShellException {
throw new NoInteractiveShellException();
}
/**
* {@inheritDoc}
*
*
* Special notes for the implementation {@link AbstractShell}:
*
*
* - no message is displayed,
* - the return value is
true
if the 'yes' flag is enable.
*
*/
@Override
public boolean confirms(final String message) throws ShellException {
return this.isYesFlagEnabled;
}
@Override
public boolean isInteractive() {
return false;
}
@Override
public void setPrompt(final String prompt) {
// NOP: By default, shell has no prompt
}
@Override
public void setConnectionParameters(final ConnectionParameters connectionParameters) {
this.connectionParameters = connectionParameters;
}
@Override
public ConnectionParameters getConnectionParameters() {
return this.connectionParameters;
}
@Override
public boolean isAsynchronousCommandInProgress() {
return this.asynchronousCommandsInProgress.get() != 0;
}
@Override
public void addAsynchronousCommand() {
this.asynchronousCommandsInProgress.incrementAndGet();
}
@Override
public void removeAsynchronousCommand() {
this.asynchronousCommandsInProgress.decrementAndGet();
}
@Override
public ShellExtension[] getExtensions() {
return this.shellExtensions;
}
@Override
public void setDefaultPrompt() {
this.setPrompt(String.format(DEFAULT_PROMPT_PATTERN, this.basePrompt));
}
@Override
public Preferences getPreferences() {
return this.preferences;
}
@Override
public void onConnectionEstablished() {
this.isConnectionEstablished = true;
}
@Override
public void onDisconnection() {
this.isConnectionEstablished = false;
}
@Override
public boolean isConnectionEstablished() {
return this.isConnectionEstablished;
}
}