/*
* Copyright (c) 2013-2017 Cinchapi Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.cinchapi.concourse.shell;
import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import static java.text.MessageFormat.format;
import java.util.List;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import javax.annotation.Nullable;
import groovy.lang.Binding;
import groovy.lang.Closure;
import groovy.lang.GroovyShell;
import groovy.lang.MissingMethodException;
import groovy.lang.Script;
import jline.TerminalFactory;
import jline.console.ConsoleReader;
import jline.console.UserInterruptException;
import jline.console.completer.StringsCompleter;
import jline.console.history.FileHistory;
import org.apache.thrift.transport.TTransportException;
import com.cinchapi.concourse.config.ConcourseClientPreferences;
import org.codehaus.groovy.control.CompilationFailedException;
import org.codehaus.groovy.control.MultipleCompilationErrorsException;
import com.beust.jcommander.JCommander;
import com.beust.jcommander.Parameter;
import com.cinchapi.concourse.Concourse;
import com.cinchapi.concourse.Link;
import com.cinchapi.concourse.Tag;
import com.cinchapi.concourse.Timestamp;
import com.cinchapi.concourse.lang.Criteria;
import com.cinchapi.concourse.lang.StartState;
import com.cinchapi.concourse.thrift.Diff;
import com.cinchapi.concourse.thrift.Operator;
import com.cinchapi.concourse.thrift.ParseException;
import com.cinchapi.concourse.thrift.SecurityException;
import com.cinchapi.concourse.util.FileOps;
import com.cinchapi.concourse.util.Version;
import com.google.common.base.CaseFormat;
import com.google.common.base.Stopwatch;
import com.google.common.base.Strings;
import com.google.common.base.Throwables;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
/**
* The main program runner for the ConcourseShell client. ConcourseShell wraps a
* connection to a ConcourseServer inside of a {@link GroovyShell}, which allows
* for rich interaction with Concourse in a scripting environment.
*
* @author Jeff Nelson
*/
public final class ConcourseShell {
/**
* Run the program...
*
* @param args
* @throws Exception
*/
public static void main(String... args) throws Exception {
try {
ConcourseShell cash = new ConcourseShell();
Options opts = new Options();
JCommander parser = null;
try {
parser = new JCommander(opts, args);
}
catch (Exception e) {
die(e.getMessage());
}
parser.setProgramName("concourse-shell");
if(opts.help) {
parser.usage();
System.exit(1);
}
if(!Strings.isNullOrEmpty(opts.prefs)) {
opts.prefs = FileOps.expandPath(opts.prefs,
System.getProperty("user.dir.real"));
ConcourseClientPreferences prefs = ConcourseClientPreferences
.open(opts.prefs);
opts.username = prefs.getUsername();
opts.password = new String(prefs.getPasswordExplicit());
opts.host = prefs.getHost();
opts.port = prefs.getPort();
opts.environment = prefs.getEnvironment();
}
if(Strings.isNullOrEmpty(opts.password)) {
cash.setExpandEvents(false);
opts.password = cash.console.readLine("Password ["
+ opts.username + "]: ", '*');
}
try {
cash.concourse = Concourse.connect(opts.host, opts.port,
opts.username, opts.password, opts.environment);
cash.whoami = opts.username;
}
catch (Exception e) {
if(e.getCause() instanceof TTransportException) {
die("Unable to connect to the Concourse Server at "
+ opts.host + ":" + opts.port);
}
else if(e.getCause() instanceof SecurityException) {
die("Invalid username/password combination.");
}
else {
die(e.getMessage());
}
}
if(!opts.ignoreRunCommands) {
cash.loadExternalScript(opts.ext);
}
if(!Strings.isNullOrEmpty(opts.run)) {
try {
String result = cash.evaluate(opts.run);
System.out.println(result);
cash.concourse.exit();
System.exit(0);
}
catch (IrregularEvaluationResult e) {
die(e.getMessage());
}
}
else {
cash.enableInteractiveSettings();
boolean running = true;
String input = "";
while (running) {
boolean extraLineBreak = true;
boolean clearInput = true;
boolean clearPrompt = false;
try {
input = input + cash.console.readLine().trim();
String result = cash.evaluate(input);
System.out.println(result);
}
catch (UserInterruptException e) {
if(Strings.isNullOrEmpty(e.getPartialLine())
&& Strings.isNullOrEmpty(input)) {
cash.console.println("Type EXIT to quit.");
}
}
catch (HelpRequest e) {
String text = getHelpText(e.topic);
if(!Strings.isNullOrEmpty(text)) {
Process p = Runtime.getRuntime().exec(
new String[] {
"sh",
"-c",
"echo \"" + text
+ "\" | less > /dev/tty" });
p.waitFor();
}
cash.console.getHistory().removeLast();
}
catch (ExitRequest e) {
running = false;
cash.console.getHistory().removeLast();
}
catch (NewLineRequest e) {
extraLineBreak = false;
}
catch (ProgramCrash e) {
die(e.getMessage());
}
catch (MultiLineRequest e) {
extraLineBreak = false;
clearInput = false;
clearPrompt = true;
}
catch (IrregularEvaluationResult e) {
System.err.println(e.getMessage());
}
finally {
if(extraLineBreak) {
cash.console.print("\n");
}
if(clearInput) {
input = "";
}
if(clearPrompt) {
cash.console.setPrompt("> ");
}
else {
cash.console.setPrompt(cash.defaultPrompt);
}
}
}
cash.concourse.exit();
System.exit(0);
}
}
finally {
TerminalFactory.get().restore();
}
}
/**
* Return a sorted array that contains all the accessible API methods.
*
* @return the accessible API methods
*/
protected static String[] getAccessibleApiMethods() {
if(ACCESSIBLE_API_METHODS == null) {
Set<String> banned = Sets.newHashSet("equals", "getClass",
"hashCode", "notify", "notifyAll", "toString", "wait",
"exit");
Set<String> methods = Sets.newTreeSet();
for (Method method : Concourse.class.getMethods()) {
if(!Modifier.isStatic(method.getModifiers())
&& !banned.contains(method.getName())) {
// NOTE: Even though the StringCompleter strips the
// "concourse." from these method names, we must add it here
// so that we can properly handle short syntax in
// SyntaxTools#handleShortSyntax
methods.add(format("concourse.{0}", method.getName()));
}
}
ACCESSIBLE_API_METHODS = methods
.toArray(new String[methods.size()]);
}
return ACCESSIBLE_API_METHODS;
}
/**
* Return {@code true} if {@code string} contains at last one of the
* {@link #BANNED_CHAR_SEQUENCES} strings.
*
* @param string
* @return {@code true} if string contains a banned character sequence
*/
private static boolean containsBannedCharSequence(String string) {
for (String charSequence : BANNED_CHAR_SEQUENCES) {
if(string.contains(charSequence)) {
return true;
}
}
return false;
}
/**
* Print {@code message} to stderr and exit with a non-zero status.
*
* @param message
*/
private static void die(String message) {
System.err.println("ERROR: " + message);
System.exit(1);
}
/**
* Return a sorted array that contains all the accessible API methods using
* short syntax.
*
* @return the accessible API methods using short syntax
*/
private static String[] getAccessibleApiMethodsUsingShortSyntax() {
Set<String> methods = Sets.newTreeSet();
for (String method : getAccessibleApiMethods()) {
methods.add(method.replace("concourse.", ""));
}
// Add the Showable items
for (Showable showable : Showable.values()) {
methods.add("show " + showable.getName());
}
return methods.toArray(new String[methods.size()]);
}
/**
* Return the help text for a given {@code topic}.
*
* @param topic
* @return the help text
*/
private static String getHelpText(String topic) {
topic = Strings.isNullOrEmpty(topic) ? "cash" : topic;
topic = topic.toLowerCase();
InputStream in = ConcourseShell.class.getResourceAsStream("/" + topic);
if(in == null) {
System.err.println("No help entry for " + topic);
return null;
}
else {
try {
BufferedReader reader = new BufferedReader(
new InputStreamReader(in));
StringBuilder builder = new StringBuilder();
String line;
while ((line = reader.readLine()) != null) {
line = line.replaceAll("\"", "\\\\\"");
builder.append(line).append(
System.getProperty("line.separator"));
}
String text = builder.toString().trim();
reader.close();
return text;
}
catch (IOException e) {
throw Throwables.propagate(e);
}
}
}
/**
* Attempt to return the {@link #ACCESSIBLE_API_METHODS API method} that is
* the closest match for the specified {@code alias}.
* <p>
* This method can be used to take a user supplied method name that does not
* match any of the {@link #ACCESSIBLE_API_METHODS provided} ones, but can
* be reasonably assumed to be a valid alias of some sort (i.e. an API
* method name in underscore case as opposed to camel case).
* </p>
*
* @param alias the method name that may be an alias for one of the provided
* API methods
* @return the actual API method that {@code alias} should resolve to, if it
* is possible to determine that; otherwise {@code null}
*/
@Nullable
private static String tryGetCorrectApiMethod(String alias) {
String camel = CaseFormat.LOWER_UNDERSCORE.to(CaseFormat.LOWER_CAMEL,
alias);
String expanded = com.cinchapi.concourse.util.Strings.ensureStartsWith(
camel, "concourse.");
return methods.contains(expanded) && !camel.equals(alias) ? camel
: null;
}
/**
* A cache of the API methods that are accessible in CaSH.
*/
private static String[] ACCESSIBLE_API_METHODS = null;
/**
* A list of char sequences that we must ban for security and other
* miscellaneous purposes.
*/
private static List<String> BANNED_CHAR_SEQUENCES = ImmutableList.of(
"concourse.exit()", "concourse.username", "concourse.password",
"concourse.client", "concourse.getClass().getDeclaredFields()",
Concourse.class.getName());
/**
* The message to display when a line of input contains a banned character
* sequence.
*/
protected static final String BANNED_CHAR_SEQUENCE_ERROR_MESSAGE = "Cannot evaluate input "
+ "because it contains an illegal character sequence"; // visible
// for
// testing
/**
* A list which contains all of the accessible API methods. This list is
* used to expand short syntax that is used in any evaluatable line.
*/
private static final List<String> methods = Lists
.newArrayList(getAccessibleApiMethods());
/**
* The name of the external script that is
* {@link #loadExternalScript(String)
* loaded}. This name is how the script functions are stored in the
* {@link #groovyBinding}.
*/
private static final String EXTERNAL_SCRIPT_NAME = "ext";
/**
* The list of classes that are imported directly into the
* {@link #groovyBinding} so that they can be used within CaSH the exact
* same way they would in code. Importing classes directly eliminates the
* need to bind custom closures to static methods in the classes.
*/
protected static List<Class<?>> IMPORTED_CLASSES = ImmutableList.of(
Timestamp.class, Diff.class, Link.class, Tag.class, Criteria.class,
Operator.class); // visible for testing
/**
* A closure that converts a string value to a tag.
*
* @deprecated Use the {@link Tag} class directly as it is imported into the
* {@link #groovyBinding} within the {@link #evaluate(String)}
* method.
*/
@Deprecated
private static Closure<Tag> STRING_TO_TAG = new Closure<Tag>(null) {
private static final long serialVersionUID = 1L;
@Override
public Tag call(Object arg) {
return Tag.create(arg.toString());
}
};
/**
* A closure that returns a nwe CriteriaBuilder object.
*
* @deprecated Use the {@link Criteria} class directly as it is imported
* into the {@link #groovyBinding} within the
* {@link #evaluate(String)} method.
*/
@Deprecated
private static Closure<StartState> WHERE = new Closure<StartState>(null) {
private static final long serialVersionUID = 1L;
@Override
public StartState call() {
return Criteria.where();
}
};
/**
* The client connection to Concourse.
*/
protected Concourse concourse;
/**
* The file where the user's CASH history is stored.
*/
protected String historyStore = System.getProperty("user.home")
+ File.separator + ".cash_history";
/**
* The Concourse user that is currently connected to the shell.
*/
protected String whoami;
/**
* The env that the client is connected to.
*/
protected String env;
/**
* The console handles all I/O.
*/
private ConsoleReader console;
/**
* The groovy environment that actually evaluates the user input.
*/
private GroovyShell groovy;
/**
* The binding that contains all the variables that are in scope for the
* groovy environment.
*/
private Binding groovyBinding;
/**
* The stopwatch that is used to time the duration of all evaluations.
*/
private Stopwatch watch = Stopwatch.createUnstarted();
/**
* The shell prompt.
*/
private String defaultPrompt;
/**
* An external script that has been loaded by the
* {@link #loadExternalScript(String)} method.
*/
private Script script = null;
/**
* A closure that responds to the 'show' command and returns information to
* display to the user based on the input argument(s).
*/
private final Closure<Object> showFunction = new Closure<Object>(null) {
private static final long serialVersionUID = 1L;
@Override
public Object call(Object arg) {
if(arg == Showable.RECORDS) {
return concourse.inventory();
}
else {
StringBuilder sb = new StringBuilder();
sb.append("Unable to show ");
sb.append(arg);
sb.append(". Valid options are: ");
sb.append(Showable.OPTIONS);
throw new IllegalArgumentException(sb.toString());
}
}
};
/**
* A closure that responds to time/date alias functions.
*/
private final Closure<Timestamp> timeFunction = new Closure<Timestamp>(null) {
private static final long serialVersionUID = 1L;
@Override
public Timestamp call(Object arg) {
return concourse.time(arg.toString());
}
@Override
public Timestamp call() {
return concourse.time();
}
};
/**
* Construct a new instance. Be sure to call {@link #setClient(Concourse)}
* before performing any
* evaluations.
*
* @throws Exception
*/
protected ConcourseShell() throws Exception {
this.console = new ConsoleReader();
this.groovyBinding = new Binding();
this.groovy = new GroovyShell(groovyBinding);
}
/**
* Evaluate the given {@code input} and return the result that should be
* displayed to the user. If, for some reason, the evaluation of the input
* does not yield a displayable response, a subclass of
* {@link IrregularEvaluationResult} will be thrown to give the caller and
* indication of how to proceed.
*
* @param input
* @return the result of the evaluation
* @throws IrregularEvaluationResult
*/
public String evaluate(String input) throws IrregularEvaluationResult {
input = SyntaxTools.handleShortSyntax(input, methods);
String inputLowerCase = input.toLowerCase();
// NOTE: These must always be set before evaluating a line just in case
// an attempt was made to bind the variables to different values in a
// previous evaluation.
groovyBinding.setVariable("concourse", concourse);
groovyBinding.setVariable("eq", Operator.EQUALS);
groovyBinding.setVariable("ne", Operator.NOT_EQUALS);
groovyBinding.setVariable("gt", Operator.GREATER_THAN);
groovyBinding.setVariable("gte", Operator.GREATER_THAN_OR_EQUALS);
groovyBinding.setVariable("lt", Operator.LESS_THAN);
groovyBinding.setVariable("lte", Operator.LESS_THAN_OR_EQUALS);
groovyBinding.setVariable("bw", Operator.BETWEEN);
groovyBinding.setVariable("regex", Operator.REGEX);
groovyBinding.setVariable("nregex", Operator.NOT_REGEX);
groovyBinding.setVariable("lnk2", Operator.LINKS_TO);
groovyBinding.setVariable("time", timeFunction);
groovyBinding.setVariable("date", timeFunction);
groovyBinding.setVariable("where", WHERE); // deprecated
groovyBinding.setVariable("tag", STRING_TO_TAG); // deprecated
groovyBinding.setVariable("whoami", whoami);
groovyBinding.setVariable("ADDED", Diff.ADDED);
groovyBinding.setVariable("REMOVED", Diff.REMOVED);
// Do direct import of declared classes
for (Class<?> clazz : IMPORTED_CLASSES) {
String variable = clazz.getSimpleName();
groovyBinding.setVariable(variable, clazz);
}
// Add Showable variables
for (Showable showable : Showable.values()) {
groovyBinding.setVariable(showable.getName(), showable);
}
groovyBinding.setVariable("show", showFunction);
if(script != null) {
groovyBinding.setVariable(EXTERNAL_SCRIPT_NAME, script);
}
if(inputLowerCase.equalsIgnoreCase("exit")) {
throw new ExitRequest();
}
else if(inputLowerCase.startsWith("help")
|| inputLowerCase.startsWith("man")) {
String[] toks = input.split(" ");
if(toks.length == 1) {
throw new HelpRequest();
}
else {
String topic = toks[1];
throw new HelpRequest(topic);
}
}
else if(containsBannedCharSequence(input)) {
throw new EvaluationException(BANNED_CHAR_SEQUENCE_ERROR_MESSAGE);
}
else if(Strings.isNullOrEmpty(input)) { // CON-170
throw new NewLineRequest();
}
else {
StringBuilder result = new StringBuilder();
try {
watch.reset().start();
Object value = groovy.evaluate(input, "ConcourseShell");
watch.stop();
long elapsed = watch.elapsed(TimeUnit.MILLISECONDS);
double seconds = elapsed / 1000.0;
if(value != null) {
result.append("Returned '" + value + "' in " + seconds
+ " sec");
}
else {
result.append("Completed in " + seconds + " sec");
}
return result.toString();
}
catch (CompilationFailedException e) {
throw new MultiLineRequest(e.getMessage());
}
catch (Exception e) {
// CON-331: Here we catch a generic Exception and examine
// additional context (i.e. the cause or other environmental
// aspects) to perform additional logic that determines the
// appropriate response. These cases SHOULD NOT be placed in
// their own separate catch block.
String method = null;
String methodCorrected = null;
if(e.getCause() instanceof TTransportException) {
throw new ProgramCrash(e.getMessage());
}
else if(e.getCause() instanceof SecurityException) {
throw new ProgramCrash(
"A security change has occurred and your "
+ "session cannot continue");
}
else if(e instanceof MissingMethodException
&& ErrorCause.determine(e.getMessage()) == ErrorCause.MISSING_CASH_METHOD
&& ((methodCorrected = tryGetCorrectApiMethod((method = ((MissingMethodException) e)
.getMethod()))) != null || hasExternalScript())) {
if(methodCorrected != null) {
input = input.replaceAll(method, methodCorrected);
}
else {
input = input.replaceAll(method, "ext." + method);
}
return evaluate(input);
}
else {
String message = e.getCause() instanceof ParseException ? e
.getCause().getMessage() : e.getMessage();
throw new EvaluationException("ERROR: " + message);
}
}
}
}
/**
* Return {@code true} if this instance has an external script loaded.
*
* @return {@code true} if an external script has been loaded
*/
public boolean hasExternalScript() {
return script != null;
}
/**
* Load an external script and store it so it can be added to the binding
* when {@link #evaluate(String) evaluating} commands. Any functions defined
* in the script must be accessed using the {@code ext} qualifier.
*
* @param script - the path to the external script
*/
public void loadExternalScript(String script) {
try {
Path extPath = Paths.get(script);
if(Files.exists(extPath) && Files.size(extPath) > 0) {
List<String> lines = FileOps.readLines(script);
StringBuilder sb = new StringBuilder();
for (String line : lines) {
line = SyntaxTools.handleShortSyntax(line, methods);
sb.append(line)
.append(System.getProperty("line.separator"));
}
String scriptText = sb.toString();
try {
this.script = groovy
.parse(scriptText, EXTERNAL_SCRIPT_NAME);
evaluate(scriptText);
}
catch (IrregularEvaluationResult e) {
System.err.println(e.getMessage());
}
catch (MultipleCompilationErrorsException e) {
String msg = e.getMessage();
msg = msg.substring(msg.indexOf('\n') + 1);
msg = msg.replaceAll("ext: ", "");
die("A fatal error occurred while parsing the run-commands file at "
+ script
+ System.getProperty("line.separator")
+ msg
+ System.getProperty("line.separator")
+ "Fix these errors or start concourse shell with the --no-run-commands flag");
}
}
}
catch (IOException e) {
throw Throwables.propagate(e);
}
}
/**
* This method calls {@link ConsoleReader#setExpandEvents(boolean)} with the
* specified value.
*
* @param bool
*/
public void setExpandEvents(boolean bool) {
console.setExpandEvents(bool);
}
/**
* Turn on the settings necessary to make the application "interactive"
* (e.g. a REPL). By default, these settings are turned off.
*/
private void enableInteractiveSettings() throws Exception {
console.setExpandEvents(false);
console.setHandleUserInterrupt(true);
File file = new File(historyStore);
file.createNewFile();
console.setHistory(new FileHistory(file));
Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() {
@Override
public void run() {
try {
((FileHistory) console.getHistory()).flush();
}
catch (Exception e) {
e.printStackTrace();
}
}
}));
CommandLine.displayWelcomeBanner();
env = concourse.getServerEnvironment();
setDefaultPrompt();
console.println("Client Version "
+ Version.getVersion(ConcourseShell.class));
console.println("Server Version " + concourse.getServerVersion());
console.println("");
console.println("Connected to the '" + env + "' environment.");
console.println("");
console.println("Type HELP for help.");
console.println("Type EXIT to quit.");
console.println("Use TAB for completion.");
console.println("");
console.setPrompt(defaultPrompt);
console.addCompleter(new StringsCompleter(
getAccessibleApiMethodsUsingShortSyntax()));
}
/**
* Set the {@link #defaultPrompt} variable to account for the current
* {@link #env}.
*/
private void setDefaultPrompt() {
this.defaultPrompt = format("[{0}/cash]$ ", env);
}
/**
* The options that can be passed to the main method of this script.
*
* @author Jeff Nelson
*/
private static class Options {
/**
* A handler for the client preferences that <em>may</em> exist in the
* user's home directory.
*/
private ConcourseClientPreferences prefsHandler = null;
{
String file = System.getProperty("user.home") + File.separator
+ "concourse_client.prefs";
if(Files.exists(Paths.get(file))) { // check to make sure that the
// file exists first, so we
// don't create a blank one if
// it doesn't
prefsHandler = ConcourseClientPreferences.open(file);
}
}
@Parameter(names = { "-e", "--environment" }, description = "The environment of the Concourse Server to use")
public String environment = prefsHandler != null ? prefsHandler
.getEnvironment() : "";
@Parameter(names = "--help", help = true, hidden = true)
public boolean help;
@Parameter(names = { "-h", "--host" }, description = "The hostname where the Concourse Server is located")
public String host = prefsHandler != null ? prefsHandler.getHost()
: "localhost";
@Parameter(names = "--password", description = "The password", password = false, hidden = true)
public String password = prefsHandler != null ? new String(
prefsHandler.getPasswordExplicit()) : null;
@Parameter(names = { "-p", "--port" }, description = "The port on which the Concourse Server is listening")
public int port = prefsHandler != null ? prefsHandler.getPort() : 1717;
@Parameter(names = { "-r", "--run" }, description = "The command to run non-interactively")
public String run = "";
@Parameter(names = { "-u", "--username" }, description = "The username with which to connect")
public String username = prefsHandler != null ? prefsHandler
.getUsername() : "admin";
@Parameter(names = { "--run-commands", "--rc" }, description = "Path to a script that contains commands to run when the shell starts")
public String ext = FileOps.getUserHome() + "/.cashrc";
@Parameter(names = { "--no-run-commands", "--no-rc" }, description = "A flag to disable loading any run commands file")
public boolean ignoreRunCommands = false;
@Parameter(names = "--prefs", description = "Path to the concourse_client.prefs file")
public String prefs;
}
/**
* An enum that summarizes the cause of an error based on the message.
* <p>
* Retrieve an instance by calling {@link ErrorCause#determine(String)} on
* an error message returned from an exception/
* </p>
*
* @author Jeff Nelson
*/
private enum ErrorCause {
MISSING_CASH_METHOD, MISSING_EXTERNAL_METHOD, UNDEFINED;
/**
* Examine an error message to determine the {@link ErrorCause}.
*
* @param message - the error message from an Exception
* @return the {@link ErrorCause} that summarizes the reason the
* Exception occurred
*/
public static ErrorCause determine(String message) {
if(message.startsWith("No signature of method: ConcourseShell.")) {
return MISSING_CASH_METHOD;
}
else if(message.startsWith("No signature of method: ext.")) {
return MISSING_EXTERNAL_METHOD;
}
else {
return UNDEFINED;
}
}
}
/**
* An enum containing the types of things that can be listed using the
* 'show' function.
*
* @author Jeff Nelson
*/
private enum Showable {
RECORDS;
/**
* Return the name of this Showable.
*
* @return the name
*/
public String getName() {
return name().toLowerCase();
}
/**
* Valid options for the 'show' function based on the values defined in
* this enum.
*/
private static String OPTIONS;
static {
StringBuilder sb = new StringBuilder();
for (Showable showable : values()) {
sb.append(showable.toString().toLowerCase()).append(" ");
}
sb.deleteCharAt(sb.length() - 1);
OPTIONS = sb.toString();
}
}
}