package org.springframework.roo.shell;
import static org.apache.commons.io.IOUtils.LINE_SEPARATOR;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.Validate;
import org.osgi.service.component.annotations.Component;
import org.springframework.roo.shell.event.AbstractShellStatusPublisher;
import org.springframework.roo.shell.event.ShellStatus;
import org.springframework.roo.shell.event.ShellStatus.Status;
import org.springframework.roo.support.logging.HandlerUtils;
import org.springframework.roo.support.util.CollectionUtils;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.net.URISyntaxException;
import java.net.URL;
import java.text.DateFormat;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.List;
import java.util.Map.Entry;
import java.util.Set;
import java.util.TreeSet;
import java.util.jar.JarFile;
import java.util.jar.Manifest;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.zip.ZipEntry;
/**
* Provides a base {@link Shell} implementation.
*
* @author Ben Alex
*/
@Component
public abstract class AbstractShell extends AbstractShellStatusPublisher implements Shell {
List<CommandListener> commandListeners = new ArrayList<CommandListener>();
private static final Logger LOGGER = HandlerUtils.getLogger(AbstractShell.class);
private CommandListener commandListener;
private static final String MY_SLOT = AbstractShell.class.getName();
protected static final String ROO_PROMPT = "roo> ";
// Public static fields; don't rename, make final, or make non-public, as
// they are part of the public API, e.g. are changed by STS.
public static String completionKeys = "TAB";
public static String shellPrompt = ROO_PROMPT;
public static String versionInfo() {
// Try to determine the bundle version
String bundleVersion = null;
String gitCommitHash = null;
JarFile jarFile = null;
try {
final URL classContainer =
AbstractShell.class.getProtectionDomain().getCodeSource().getLocation();
if (classContainer.toString().endsWith(".jar")) {
// Attempt to obtain the "Bundle-Version" version from the
// manifest
jarFile = new JarFile(new File(classContainer.toURI()), false);
final ZipEntry manifestEntry = jarFile.getEntry("META-INF/MANIFEST.MF");
final Manifest manifest = new Manifest(jarFile.getInputStream(manifestEntry));
bundleVersion = manifest.getMainAttributes().getValue("Bundle-Version");
gitCommitHash = manifest.getMainAttributes().getValue("Git-Commit-Hash");
}
} catch (final IOException ignoreAndMoveOn) {
} catch (final URISyntaxException ignoreAndMoveOn) {
} finally {
if (jarFile != null) {
try {
jarFile.close();
} catch (final IOException ignored) {
}
}
}
final StringBuilder sb = new StringBuilder();
if (bundleVersion != null) {
sb.append(bundleVersion);
}
if (gitCommitHash != null && gitCommitHash.length() > 7) {
if (sb.length() > 0) {
sb.append(" ");
}
sb.append("[rev ");
sb.append(gitCommitHash.substring(0, 7));
sb.append("]");
}
if (sb.length() == 0) {
sb.append("UNKNOWN VERSION");
}
return sb.toString();
}
public static String buildInfo() {
// Try to determine the bundle version
String bundleVersion = null;
String gitCommitHash = null;
JarFile jarFile = null;
try {
final URL classContainer =
AbstractShell.class.getProtectionDomain().getCodeSource().getLocation();
if (classContainer.toString().endsWith(".jar")) {
// Attempt to obtain the "Bundle-Version" version from the
// manifest
jarFile = new JarFile(new File(classContainer.toURI()), false);
final ZipEntry manifestEntry = jarFile.getEntry("META-INF/MANIFEST.MF");
final Manifest manifest = new Manifest(jarFile.getInputStream(manifestEntry));
bundleVersion = manifest.getMainAttributes().getValue("Bundle-Version");
gitCommitHash = manifest.getMainAttributes().getValue("Git-Commit-Hash");
}
} catch (final IOException ignoreAndMoveOn) {
} catch (final URISyntaxException ignoreAndMoveOn) {
} finally {
if (jarFile != null) {
try {
jarFile.close();
} catch (final IOException ignored) {
}
}
}
final StringBuilder sb = new StringBuilder();
if (gitCommitHash != null && gitCommitHash.length() > 7) {
if (sb.length() > 0) {
sb.append(" ");
}
sb.append("[rev ");
sb.append(gitCommitHash.substring(0, 7));
sb.append("]");
}
if (sb.length() == 0) {
sb.append("UNKNOWN BUILD ID");
}
return sb.toString();
}
public static String versionInfoWithoutGit() {
// Try to determine the bundle version
String bundleVersion = null;
JarFile jarFile = null;
try {
final URL classContainer =
AbstractShell.class.getProtectionDomain().getCodeSource().getLocation();
if (classContainer.toString().endsWith(".jar")) {
// Attempt to obtain the "Bundle-Version" version from the
// manifest
jarFile = new JarFile(new File(classContainer.toURI()), false);
final ZipEntry manifestEntry = jarFile.getEntry("META-INF/MANIFEST.MF");
final Manifest manifest = new Manifest(jarFile.getInputStream(manifestEntry));
bundleVersion = manifest.getMainAttributes().getValue("Bundle-Version");
}
} catch (final IOException ignoreAndMoveOn) {
} catch (final URISyntaxException ignoreAndMoveOn) {
} finally {
if (jarFile != null) {
try {
jarFile.close();
} catch (final IOException ignored) {
}
}
}
final StringBuilder sb = new StringBuilder();
if (bundleVersion != null) {
sb.append(bundleVersion);
}
if (sb.length() == 0) {
sb.append("UNKNOWN VERSION");
}
return sb.toString();
}
protected final Logger logger = HandlerUtils.getLogger(getClass());
protected boolean inBlockComment;
protected ExitShellRequest exitShellRequest;
private Tailor tailor;
@CliCommand(value = {"/*"}, help = "Start of block comment")
public void blockCommentBegin() {
Validate.isTrue(!inBlockComment, "Cannot open a new block comment when one already active");
inBlockComment = true;
}
@CliCommand(value = {"*/"}, help = "End of block comment")
public void blockCommentFinish() {
Validate.isTrue(inBlockComment, "Cannot close a block comment when it has not been opened");
inBlockComment = false;
}
// @CliCommand(value = {"date"}, help = "Displays the local date and time")
public String date() {
return DateFormat.getDateTimeInstance(DateFormat.FULL, DateFormat.FULL).format(new Date());
}
public boolean executeCommand(final String line) {
if (tailor == null) {
return executeCommandImpl(line);
}
/*
* If getTailor() is not null, then try to transform input command and
* execute all outputs sequentially
*/
List<String> commands = null;
commands = tailor.sew(line);
if (CollectionUtils.isEmpty(commands)) {
return executeCommandImpl(line);
}
for (final String command : commands) {
logger.info("roo-tailor> " + command);
if (!executeCommandImpl(command)) {
return false;
}
}
return true;
}
/**
* Runs the specified command. Control will return to the caller after the
* command is run.
*/
private boolean executeCommandImpl(String line) {
// Another command was attempted
setShellStatus(ShellStatus.Status.PARSING);
final ExecutionStrategy executionStrategy = getExecutionStrategy();
boolean flashedMessage = false;
while (executionStrategy == null || !executionStrategy.isReadyForCommands()) {
// Wait
try {
Thread.sleep(500);
} catch (final InterruptedException ignore) {
}
if (!flashedMessage) {
flash(Level.INFO, "Please wait - still loading", MY_SLOT);
flashedMessage = true;
}
}
if (flashedMessage) {
flash(Level.INFO, "", MY_SLOT);
}
ParseResult parseResult = null;
try {
// We support simple block comments; ie a single pair per line
if (!inBlockComment && line.contains("/*") && line.contains("*/")) {
blockCommentBegin();
final String lhs = line.substring(0, line.lastIndexOf("/*"));
if (line.contains("*/")) {
line = lhs + line.substring(line.lastIndexOf("*/") + 2);
blockCommentFinish();
} else {
line = lhs;
}
}
if (inBlockComment) {
if (!line.contains("*/")) {
return true;
}
blockCommentFinish();
line = line.substring(line.lastIndexOf("*/") + 2);
}
// We also support inline comments (but only at start of line,
// otherwise valid
// command options like http://www.helloworld.com will fail as per
// ROO-517)
if (!inBlockComment && (line.trim().startsWith("//") || line.trim().startsWith("#"))) { // # support in ROO-1116
line = "";
}
// Convert any TAB characters to whitespace (ROO-527)
line = line.replace('\t', ' ');
if ("".equals(line.trim())) {
setShellStatus(Status.EXECUTION_SUCCESS);
return true;
}
parseResult = getParser().parse(line);
if (parseResult == null) {
return false;
}
try {
notifyBeginExecute(parseResult);
} catch (final Exception ignored) {
}
setShellStatus(Status.EXECUTING);
final Object result = executionStrategy.execute(parseResult);
setShellStatus(Status.EXECUTION_RESULT_PROCESSING);
if (result != null) {
if (result instanceof ExitShellRequest) {
exitShellRequest = (ExitShellRequest) result;
// Give ProcessManager a chance to close down its threads
// before the overall OSGi framework is terminated
// (ROO-1938)
executionStrategy.terminate();
} else if (result instanceof Iterable<?>) {
for (final Object o : (Iterable<?>) result) {
logger.info(o.toString());
}
} else {
logger.info(result.toString());
}
}
logCommandIfRequired(line, true);
setShellStatus(Status.EXECUTION_SUCCESS, line, parseResult);
// ROO-3581: When command success, execute command listener SUCCESS
try {
notifyExecutionSuccess();
} catch (final Exception ignored) {
}
return true;
} catch (final RuntimeException e) {
setShellStatus(Status.EXECUTION_FAILED, line, parseResult);
if (isDevelopmentMode()) {
logger.severe(e.getMessage());
}
try {
// ROO-3581: When command fails, execute command listener FAILS
notifyExecutionFailed();
} catch (final Exception ignored) {
}
// We rely on execution strategy to log it
try {
logCommandIfRequired(line, false);
} catch (final Exception ignored) {
}
return false;
} finally {
setShellStatus(Status.USER_INPUT);
}
}
/**
* Execute the single line from a script.
* <p>
* This method can be overridden by sub-classes to pre-process script lines.
*/
protected boolean executeScriptLine(final String line) {
return executeCommand(line);
}
/**
* Returns any classpath resources with the given path
*
* @param path the path for which to search (never null)
* @return <code>null</code> if the search can't be performed
* @since 1.2.0
*/
protected abstract Collection<URL> findResources(String path);
/**
* Simple implementation of {@link #flash(Level, String, String)} that
* simply displays the message via the logger. It is strongly recommended
* shell implementations override this method with a more effective
* approach.
*/
public void flash(final Level level, final String message, final String slot) {
Validate.notNull(level, "Level is required for a flash message");
Validate.notNull(message, "Message is required for a flash message");
Validate.notBlank(slot, "Slot name must be specified for a flash message");
if (!"".equals(message)) {
logger.log(level, message);
}
}
protected abstract ExecutionStrategy getExecutionStrategy();
public ExitShellRequest getExitShellRequest() {
return exitShellRequest;
}
/**
* Obtains the home directory for the current shell instance.
* <p>
* Note: calls the {@link #getHomeAsString()} method to allow subclasses to
* provide the home directory location as string using different
* environment-specific strategies.
* <p>
* If the path indicated by {@link #getHomeAsString()} exists and refers to
* a directory, that directory is returned.
* <p>
* If the path indicated by {@link #getHomeAsString()} exists and refers to
* a file, an exception is thrown.
* <p>
* If the path indicated by {@link #getHomeAsString()} does not exist, it
* will be created as a directory. If this fails, an exception will be
* thrown.
*
* @return the home directory for the current shell instance (which is
* guaranteed to exist and be a directory)
*/
public File getHome() {
final String rooHome = getHomeAsString();
final File f = new File(rooHome);
Validate.isTrue(!f.exists() || f.exists() && f.isDirectory(),
"Path '%s' must be a directory, or it must not exist", f.getAbsolutePath());
if (!f.exists()) {
f.mkdirs();
}
Validate.isTrue(f.exists() && f.isDirectory(),
"Path '%s' is not a directory; please specify roo.home system property correctly",
f.getAbsolutePath());
return f;
}
protected abstract String getHomeAsString();
protected abstract Parser getParser();
public String getShellPrompt() {
return shellPrompt;
}
@CliCommand(value = {"//", ";"}, help = "Inline comment markers (start of line only)")
public void inlineComment() {}
/**
* Allows a subclass to log the execution of a well-formed command. This is
* invoked after a command has completed, and indicates whether the command
* returned normally or returned an exception. Note that attempted commands
* that are not well-formed (eg they are missing a mandatory argument) will
* never be presented to this method, as the command execution is never
* actually attempted in those cases. This method is only invoked if an
* attempt is made to execute a particular command.
* <p>
* Implementations should consider specially handling the "script" commands,
* and also indicating whether a command was successful or not.
* Implementations that wish to behave consistently with other
* {@link AbstractShell} subclasses are encouraged to simply override
* {@link #logCommandToOutput(String)} instead, and only override this
* method if you actually need to fine-tune the output logic.
*
* @param line the parsed line (any comments have been removed; never null)
* @param successful if the command was successful or not
*/
protected void logCommandIfRequired(final String line, final boolean successful) {
if (line.startsWith("script")) {
logCommandToOutput((successful ? "// " : "// [failed] ") + line);
} else {
logCommandToOutput((successful ? "" : "// [failed] ") + line);
}
}
/**
* Allows a subclass to actually write the resulting logged command to some
* form of output. This frees subclasses from needing to implement the logic
* within {@link #logCommandIfRequired(String, boolean)}.
* <p>
* Implementations should invoke {@link #getExitShellRequest()} to monitor
* any attempts to exit the shell and release resources such as output log
* files.
*
* @param processedLine the line that should be appended to some type of
* output (excluding the \n character)
*/
protected void logCommandToOutput(final String processedLine) {}
/**
* Opens the given script for reading
*
* @param script the script to read (required)
* @return a non-<code>null</code> input stream
*/
private InputStream openScript(final File script) {
try {
return new BufferedInputStream(new FileInputStream(script));
} catch (final FileNotFoundException fnfe) {
// Try to find the script via the classloader
final Collection<URL> urls = findResources(script.getName());
// Handle search failure
Validate.notNull(urls, "Unexpected error looking for '%s'", script.getName());
// Handle the search being OK but the file simply not being present
Validate.notEmpty(urls, "Script '%s' not found on disk or in classpath", script);
Validate.isTrue(urls.size() == 1,
"More than one '%s' was found in the classpath; unable to continue", script);
try {
return urls.iterator().next().openStream();
} catch (final IOException e) {
throw new IllegalStateException(e);
}
}
}
@CliCommand(value = {"system properties"}, help = "Shows the shell's properties such as if "
+ "'addon development mode' is enabled, JVM version, file encoding...")
public String props() {
final Set<String> data = new TreeSet<String>();
for (final Entry<Object, Object> entry : System.getProperties().entrySet()) {
data.add(entry.getKey() + " = " + entry.getValue());
}
return StringUtils.join(data, LINE_SEPARATOR) + LINE_SEPARATOR;
}
private double round(final double valueToRound, final int numberOfDecimalPlaces) {
final double multiplicationFactor = Math.pow(10, numberOfDecimalPlaces);
final double interestedInZeroDPs = valueToRound * multiplicationFactor;
return Math.round(interestedInZeroDPs) / multiplicationFactor;
}
@CliCommand(
value = {"script"},
help = "Parses the specified resource file and executes its Roo commands. You can as well execute "
+ "_*.roo_ example scripts in the Roo classpath. Ex.: `script --file clinic.roo`.")
public void script(
@CliOption(key = {"", "file"}, help = "The file to locate and execute.", mandatory = true) final File script,
@CliOption(key = "lineNumbers", mandatory = false, specifiedDefaultValue = "true",
unspecifiedDefaultValue = "false",
help = "Display line numbers when executing the script. "
+ "Default if option present: `true`; default if option not present: `false`.") final boolean lineNumbers,
@CliOption(key = "ignoreLines", mandatory = false,
help = "Comma-list of prefixes to ignore the lines that starts with any of the provided "
+ "case-sensitive prefixes.") final String ignoreLines) {
Validate.notNull(script, "Script file to parse is required");
final double startedNanoseconds = System.nanoTime();
String[] ignoreLinesPrefixes = StringUtils.split(ignoreLines == null ? "" : ignoreLines, ",");
final InputStream inputStream = openScript(script);
try {
int i = 0;
for (final String line : IOUtils.readLines(inputStream)) {
i++;
if (lineNumbers) {
logger.fine("Line " + i + ": " + line);
} else {
logger.fine(line);
}
// ROO-3836
boolean ignoreLine = StringUtils.startsWithAny(line, ignoreLinesPrefixes);
if (ignoreLine) {
if (lineNumbers) {
logger.fine("Ignoring line " + i + ": " + line);
} else {
logger.fine("Ignoring: " + line);
}
}
if (!"".equals(line.trim()) && !ignoreLine) {
final boolean success = executeScriptLine(line);
if (success && (line.trim().startsWith("quit") || line.trim().startsWith("exit"))) {
break;
} else if (!success) {
// Abort script processing, given something went wrong
throw new IllegalStateException("Script execution aborted");
}
}
}
} catch (final IOException e) {
throw new IllegalStateException(e);
} finally {
IOUtils.closeQuietly(inputStream);
final double executionDurationInSeconds =
(System.nanoTime() - startedNanoseconds) / 1000000000D;
logger
.fine("Script required " + round(executionDurationInSeconds, 3) + " seconds to execute");
}
}
/**
* Base implementation of the {@link Shell#setPromptPath(String)} method,
* designed for simple shell implementations. Advanced implementations (eg
* those that support ANSI codes etc) will likely want to override this
* method and set the {@link #shellPrompt} variable directly.
*
* @param path to set (can be null or empty; must NOT be formatted in any
* special way eg ANSI codes)
*/
public void setPromptPath(final String path) {
shellPrompt = (StringUtils.isNotBlank(path) ? path + " " : "") + ROO_PROMPT;
}
/**
* This method changes setPromptPath with a new one
*
* @param path
*/
public void setRooPrompt(final String prompt) {
shellPrompt = StringUtils.isNotBlank(prompt) ? prompt + "> " : ROO_PROMPT;
}
/**
* Default implementation of {@link Shell#setPromptPath(String, boolean))}
* method to satisfy STS compatibility.
*
* @param path to set (can be null or empty)
* @param overrideStyle
*/
public void setPromptPath(final String path, final boolean overrideStyle) {
setPromptPath(path);
}
public void setTailor(final Tailor tailor) {
this.tailor = tailor;
}
@Override
public void addListerner(CommandListener listener) {
commandListeners.add(listener);
}
@Override
public void removeListener(CommandListener listener) {
commandListeners.remove(listener);
}
private void notifyExecutionFailed() {
if (commandListeners.isEmpty()) {
return;
}
for (CommandListener listener : commandListeners) {
listener.onCommandFails();
}
}
private void notifyExecutionSuccess() {
if (commandListeners.isEmpty()) {
return;
}
for (CommandListener listener : commandListeners) {
listener.onCommandSuccess();
}
}
private void notifyBeginExecute(ParseResult parseResult) {
if (commandListeners.isEmpty()) {
return;
}
for (CommandListener listener : commandListeners) {
listener.onCommandBegin(parseResult);
}
}
@CliCommand(value = {"version"}, help = "Displays Roo Shell banner and version.")
public String version(
@CliOption(key = "", help = "Special version flags, like `roorocks`.") final String extra) {
final StringBuilder sb = new StringBuilder();
// By default show version without Git commit ID. To see the Git commit
// ID use the "version" command.
String versionInfo = versionInfoWithoutGit();
if ("roorocks".equals(extra) || "about".equals(extra)) {
sb.append(" /\\ /l").append(LINE_SEPARATOR);
sb.append(" ((.Y(!").append(LINE_SEPARATOR);
sb.append(" \\ |/").append(LINE_SEPARATOR);
sb.append(" / 6~6,").append(LINE_SEPARATOR);
sb.append(" \\ _ +-.").append(LINE_SEPARATOR);
sb.append(" \\`-=--^-' \\").append(LINE_SEPARATOR);
sb.append(" \\ \\ |\\--------------------------+").append(LINE_SEPARATOR);
sb.append(" _/ \\ | About Spring Roo |").append(LINE_SEPARATOR);
sb.append(" ( . Y +---------------------------+").append(LINE_SEPARATOR);
sb.append(" /\"\\ `---^--v---.").append(LINE_SEPARATOR);
sb.append(" / _ `---\"T~~\\/~\\/ Version: ").append(versionInfo)
.append(LINE_SEPARATOR);
sb.append(" / \" ~\\. ! Build ID: ").append(buildInfo())
.append(LINE_SEPARATOR);
sb.append(" _ Y Y.~~~ /' Platform: OSGi R6 - Java").append(LINE_SEPARATOR);
sb.append(" Y^| | | Roo 7 Created By: DISID Corporation S.L.").append(
LINE_SEPARATOR);
sb.append(" | | | | | Visit http://www.disid.com").append(
LINE_SEPARATOR);
sb.append(" | l | / . /'").append(LINE_SEPARATOR);
sb.append(" | `L | Y .^/ ~T Copyright (c) 2016 the original author or authors")
.append(LINE_SEPARATOR);
sb.append(" | l ! | |/ | | All rights reserved.").append(LINE_SEPARATOR);
sb.append(" | .`\\/' | Y | !").append(LINE_SEPARATOR);
sb.append(" l \"~ j l j L______ Visit http://projects.spring.io/spring-roo")
.append(LINE_SEPARATOR);
sb.append(" \\,____{ __\"\" ~ __ ,\\_,\\_").append(LINE_SEPARATOR);
sb.append(" ~~~~~~~~~~~~~~~~~~~~~~~~~~ Licensed under the Apache License, v2.0").append(
LINE_SEPARATOR);
return sb.toString();
}
sb.append(" _ ").append(LINE_SEPARATOR);
sb.append(" ___ _ __ _ __(_)_ __ __ _ _ __ ___ ___ ").append(LINE_SEPARATOR);
sb.append("/ __| '_ \\| '__| | '_ \\ / _` | | '__/ _ \\ / _ \\ ").append(LINE_SEPARATOR);
sb.append("\\__ \\ |_) | | | | | | | (_| | | | | (_) | (_) |").append(LINE_SEPARATOR);
sb.append("|___/ .__/|_| |_|_| |_|\\__, | |_| \\___/ \\___/ ").append(LINE_SEPARATOR);
sb.append(" |_| |___/ ");
// We have only an space of 17 chars to put the version info.
if (versionInfo.length() < 17) {
// Align version info to the right
sb.append(StringUtils.repeat(" ", 17 - versionInfo.length()));
}
sb.append(versionInfo).append(LINE_SEPARATOR);
return sb.toString();
}
}