package vnet.sms.common.shell.springshell; import static vnet.sms.common.shell.springshell.internal.util.StringUtils.LINE_SEPARATOR; import java.io.BufferedInputStream; import java.io.BufferedReader; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.net.URL; import java.text.DateFormat; import java.util.Collection; import java.util.Date; import java.util.Locale; import java.util.Map.Entry; import java.util.Set; import java.util.TreeSet; import java.util.logging.Level; import java.util.logging.Logger; import vnet.sms.common.shell.springshell.command.CliCommand; import vnet.sms.common.shell.springshell.command.CliOption; import vnet.sms.common.shell.springshell.event.AbstractShellStatusPublisher; import vnet.sms.common.shell.springshell.event.ShellStatus; import vnet.sms.common.shell.springshell.event.ShellStatus.Status; import vnet.sms.common.shell.springshell.internal.logging.HandlerUtils; import vnet.sms.common.shell.springshell.internal.util.Assert; import vnet.sms.common.shell.springshell.internal.util.IOUtils; import vnet.sms.common.shell.springshell.internal.util.MathUtils; import vnet.sms.common.shell.springshell.internal.util.StringUtils; import vnet.sms.common.shell.springshell.internal.util.VersionUtils; /** * Provides a base {@link Shell} implementation. * * @author Ben Alex */ public abstract class AbstractShell extends AbstractShellStatusPublisher implements Shell { // Constants private static final String MY_SLOT = AbstractShell.class .getName(); protected static final String DEFAULT_PROMPT = "vnet-sms> "; public static String completionKeys = "TAB"; // Instance fields protected final Logger logger = HandlerUtils .getLogger(getClass()); protected String shellPrompt = DEFAULT_PROMPT; protected boolean inBlockComment; protected ExitShellRequest exitShellRequest; /** * 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); protected abstract String getHomeAsString(); protected abstract ExecutionStrategy getExecutionStrategy(); protected abstract Parser getParser(); @CliCommand(value = { "script" }, help = "Parses the specified resource file and executes its commands") 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") final boolean lineNumbers) { Assert.notNull(script, "Script file to parse is required"); final double startedNanoseconds = System.nanoTime(); final InputStream inputStream = openScript(script); BufferedReader in = null; try { in = new BufferedReader(new InputStreamReader(inputStream)); String line; int i = 0; while ((line = in.readLine()) != null) { i++; if (lineNumbers) { this.logger.fine("Line " + i + ": " + line); } else { this.logger.fine(line); } if (!"".equals(line.trim())) { final boolean success = executeScriptLine(line); if (success && ((line.trim().startsWith("q") || line.trim() .startsWith("ex")))) { 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, in); final double executionDurationInSeconds = (System.nanoTime() - startedNanoseconds) / 1000000000D; this.logger.fine("Script required " + MathUtils.round(executionDurationInSeconds, 3) + " seconds to execute"); } } /** * 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 Assert.notNull(urls, "Unexpected error looking for '" + script.getName() + "'"); // Handle the search being OK but the file simply not being present Assert.notEmpty(urls, "Script '" + script + "' not found on disk or in classpath"); Assert.isTrue(urls.size() == 1, "More than one '" + script + "' was found in the classpath; unable to continue"); try { return urls.iterator().next().openStream(); } catch (final IOException e) { throw new IllegalStateException(e); } } } /** * 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); } @Override public boolean executeCommand(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 (!this.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 (this.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 (!this.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; } setShellStatus(Status.EXECUTING); final Object result = executionStrategy.execute(parseResult); setShellStatus(Status.EXECUTION_RESULT_PROCESSING); if (result != null) { if (result instanceof ExitShellRequest) { this.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) { this.logger.info(o.toString()); } } else { this.logger.info(result.toString()); } } logCommandIfRequired(line, true); setShellStatus(Status.EXECUTION_SUCCESS, line, parseResult); return true; } catch (final RuntimeException e) { setShellStatus(Status.EXECUTION_FAILED, line, parseResult); // We rely on execution strategy to log it try { logCommandIfRequired(line, false); } catch (final Exception ignored) { } return false; } finally { setShellStatus(Status.USER_INPUT); } } /** * 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) { } /** * 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) */ @Override public void setPromptPath(final String path) { if ((path == null) || "".equals(path)) { this.shellPrompt = DEFAULT_PROMPT; } else { this.shellPrompt = path + " " + DEFAULT_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 */ @Override public void setPromptPath(final String path, final boolean overrideStyle) { setPromptPath(path); } @Override public ExitShellRequest getExitShellRequest() { return this.exitShellRequest; } @CliCommand(value = { "//", ";" }, help = "Inline comment markers (start of line only)") public void inlineComment() { } @CliCommand(value = { "/*" }, help = "Start of block comment") public void blockCommentBegin() { Assert.isTrue(!this.inBlockComment, "Cannot open a new block comment when one already active"); this.inBlockComment = true; } @CliCommand(value = { "*/" }, help = "End of block comment") public void blockCommentFinish() { Assert.isTrue(this.inBlockComment, "Cannot close a block comment when it has not been opened"); this.inBlockComment = false; } @CliCommand(value = { "system properties" }, help = "Shows the shell's properties") public String props() { final Set<String> data = new TreeSet<String>(); // For repeatability for (final Entry<Object, Object> entry : System.getProperties() .entrySet()) { data.add(entry.getKey() + " = " + entry.getValue()); } return StringUtils.collectionToDelimitedString(data, LINE_SEPARATOR) + LINE_SEPARATOR; } @CliCommand(value = { "date" }, help = "Displays the local date and time") public String date() { return DateFormat.getDateTimeInstance(DateFormat.FULL, DateFormat.FULL, Locale.US).format(new Date()); } // @CliCommand(value = { "version" }, help = "Displays shell version") public String version( @CliOption(key = "", help = "Special version flags") final String extra) { final StringBuilder sb = new StringBuilder(); // @formatter:off sb.append(" ---- ---- ----- ---------- ----------- --------- ---- --------- ").append(LINE_SEPARATOR); sb.append(" \\ \\ / /\\ \\ \\_ _____/\\__ ___/ / _____/ / \\ / _____/ ").append(LINE_SEPARATOR); sb.append(" \\ Y / / | \\ | __)_ | | ______ \\_____ \\ / \\ / \\ \\_____ \\ ").append(LINE_SEPARATOR); sb.append(" \\ / / | \\| \\ | | /_____/ / \\/ Y \\/ \\ ").append(LINE_SEPARATOR); sb.append(" \\___/ \\____|__ /_______ / |____| /_______ /\\____|__ /_______ / ").append(LINE_SEPARATOR); sb.append(" \\/ \\/ \\/ \\/ \\/ ").append(" ").append(versionInfo()).append(LINE_SEPARATOR); sb.append(LINE_SEPARATOR); // @formatter:on return sb.toString(); } public String versionInfo() { return VersionUtils.versionInfo(); } @Override public String getShellPrompt() { return this.shellPrompt; } /** * 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) */ @Override public File getHome() { final String shellHome = getHomeAsString(); final File f = new File(shellHome); Assert.isTrue(!f.exists() || (f.exists() && f.isDirectory()), "Path '" + f.getAbsolutePath() + "' must be a directory, or it must not exist"); if (!f.exists()) { f.mkdirs(); } Assert.isTrue( f.exists() && f.isDirectory(), "Path '" + f.getAbsolutePath() + "' is not a directory; please specify shell.home system property correctly"); return f; } /** * 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. */ @Override public void flash(final Level level, final String message, final String slot) { Assert.notNull(level, "Level is required for a flash message"); Assert.notNull(message, "Message is required for a flash message"); Assert.hasText(slot, "Slot name must be specified for a flash message"); if (!("".equals(message))) { this.logger.log(level, message); } } }