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

org.ow2.petals.cli.shell.AbstractShell Maven / Gradle / Ivy

The newest version!
/**
 * 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; } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy