/* MonkeyTalk - a cross-platform functional testing tool Copyright (C) 2012 Gorilla Logic, Inc. This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program 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 Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <http://www.gnu.org/licenses/>. */ package com.gorillalogic.monkeytalk; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.regex.Matcher; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import com.gorillalogic.monkeytalk.CommandValidator.CommandStatus; import com.gorillalogic.monkeytalk.parser.MonkeyTalkParser; /** * This class represents a MonkeyTalk command, with componentType, monkeyId, action, args, * modifiers, etc. It can convert various inputs into a command, and provides various outputs (like * JSON). */ public class Command implements Cloneable { /** * The comment prefix. MonkeyTalk command strings that begin with this are comments. */ public static final String COMMENT_PREFIX = "#"; /** * The command modifier prefix. Arguments that begin with this are MonkeyTalk command modifiers, * like timeout, thinktime, etc. */ private static final String MODIFIER_PREFIX = "%"; /** * Default timeout (in ms) -- how long to retry before failing. */ public static final int DEFAULT_TIMEOUT = 2000; /** * Default thinktime (in ms) -- how long to wait before playing the command. */ public static final int DEFAULT_THINKTIME = 500; /** * Default retry delay (in ms) -- how long to wait before retrying. */ public static final int DEFAULT_RETRYDELAY = 100; public static final String SCREENSHOT_ON_ERROR = "screenshotonerror"; public static final String ABORT_MODIFIER = "abort"; public static final String IGNORE_MODIFIER = "ignore"; public static final String SHOULD_FAIL_MODIFIER = "shouldfail"; private String command; private String componentType; private String monkeyId; private String action; private List<String> args; private Map<String, String> modifiers; private boolean comment; private int defaultTimeout = -1; private int defaultThinktime = -1; /** * Instantiate a null MonkeyTalk Command object. */ public Command() { initCommand(); } /** * Instantiate a MonkeyTalk Command object from string. If the incoming string begins with * {@code #}, then it's a comment. * * @param command * the MonkeyTalk command string */ public Command(String command) { initCommand(); parseCommand(command); } /** * Instantiate a MonkeyTalk Command object directly from its parts. If the {@code componentType} * begins with {@code #}, then it's a comment and all the other parts are set to null. * * @param componentType * the MonkeyTalk componentType * @param monkeyId * the MonkeyTalk monkeyId * @param action * the MonkeyTalk action * @param args * the MonkeyTalk args * @param modifiers * the MonkeyTalk command modifiers */ public Command(String componentType, String monkeyId, String action, List<String> args, Map<String, String> modifiers) { initCommand(); setProperties(componentType, monkeyId, action, args, modifiers); } /** * * Instantiate a MonkeyTalk Command object from JSON. * * @param json * the JSON object */ public Command(JSONObject json) { String componentType = json.optString("componentType", "*"); String monkeyId = json.optString("monkeyId", "*"); String action = json.optString("action"); List<String> args = new ArrayList<String>(); JSONArray jsonArgs = json.optJSONArray("args"); if (jsonArgs != null) { for (int i = 0; i < jsonArgs.length(); i++) { args.add(jsonArgs.optString(i)); } } Map<String, String> mods = new HashMap<String, String>(); JSONObject jsonModifiers = json.optJSONObject("modifiers"); if (jsonModifiers != null) { @SuppressWarnings("unchecked") Iterator<String> keys = jsonModifiers.keys(); while (keys.hasNext()) { String key = keys.next(); mods.put(key, jsonModifiers.optString(key)); } } initCommand(); setProperties(componentType, monkeyId, action, args, mods); } /** * Get the MonkeyTalk command string. * * @return the MonkeyTalk command string */ public String getCommand() { return getCommand(false); } /** * Get the MonkeyTalk command string, optionally showing or hiding the timing modifiers. * * @param showDefaultTimings * true to display the <code>%timeout</code> and <code>%thinktime</code>, otherwise * hide them * * @return the MonkeyTalk command string */ public String getCommand(boolean showDefaultTimings) { if (command == null) { return null; } else if (showDefaultTimings) { return new Command(getCommandAsJSON()).getRawCommand(); } else { return command.replaceAll(" %(timeout=" + getDefaultTimeout() + "|thinktime=" + getDefaultThinktime() + ")\\b", ""); } } /** * Get the raw command string. * * @return the command string */ public String getRawCommand() { return command; } /** * Helper to re-build the MonkeyTalk command string from its parts. */ private void setCommand() { setCommand(componentType, monkeyId, action, getArgsAsString(), getModifiersAsString()); } /** * Helper to re-build the MonkeyTalk command string from its parts. * * @param componentType * the MonkeyTalk componentType * @param monkeyId * the MonkeyTalk monkeyId * @param action * the MonkeyTalk action * @param args * the MonkeyTalk args string * @param modifiers * the MonkeyTalk modifiers string */ private void setCommand(String componentType, String monkeyId, String action, String args, String modifiers) { if (comment) { return; } StringBuilder sb = new StringBuilder(); String part; do { if (componentType == null || componentType.length() == 0) { break; } sb.append(componentType); part = exportStr(monkeyId); if (part.length() == 0) { break; } sb.append(' ').append(part); if (action == null || action.length() == 0) { break; } sb.append(' ').append(action); if (args.length() > 0) { sb.append(' ').append(args); } if (modifiers.length() > 0) { sb.append(' ').append(modifiers); } } while (false); String cmd = sb.toString().trim(); command = (cmd.length() == 0 ? null : cmd); } /** * Helper to compute the command name, just {@code componentType.action} lowercased. * * @return the command name */ public String getCommandName() { return (componentType + "." + action).toLowerCase(); } /** * Get the MonkeyTalk componentType. * * @return the MonkeyTalk componentType */ public String getComponentType() { return componentType; } /** * Set the MonkeyTalk componentType, stripping any quotes or spaces. * * @param componentType * the MonkeyTalk componentType */ public void setComponentType(String componentType) { if (componentType != null) { componentType = componentType.trim(); if (componentType.startsWith(COMMENT_PREFIX)) { // we are a comment, so reset & store comment unaltered initCommand(); comment = true; command = componentType; } else { // not a comment, so clean it & store comment = false; componentType = importStr(componentType.replaceAll("\\s+", "")); // default componentType is * if (componentType.length() == 0) { componentType = "*"; } // store component type & update the command string this.componentType = componentType; setCommand(); } } } /** * Get the MonkeyTalk monkeyId. * * @return the MonkeyTalk monkeyId */ public String getMonkeyId() { return escape(monkeyId); } /** * Set the MonkeyTalk monkeyId, first trimming any spaces, then trimming any quotes. * * @param monkeyId * the MonkeyTalk monkeyId */ public void setMonkeyId(String monkeyId) { if (monkeyId != null) { monkeyId = importStr(monkeyId.trim()); if (monkeyId.length() == 0) { monkeyId = "*"; } this.monkeyId = monkeyId; setCommand(); } } /** * Get the MonkeyTalk action. * * @return the MonkeyTalk action */ public String getAction() { return action; } /** * Set the MonkeyTalk action, stripping any quotes or spaces. * * @param action * the MonkeyTalk action */ public void setAction(String action) { if (action != null) { action = importStr(action.replaceAll("\\s+", "")); if (action.length() == 0) { action = "*"; } this.action = action; setCommand(); } } /** * Get the list of arguments. If some of the args were originally quoted in the MonkeyTalk * command string, they are returned in unquoted form. For example, the MonkeyTalk command * string {@code Input password EnterText "i like cheese" "me too" bacon} with be stored as * {@code i like cheese}, {@code me too}, {@code bacon} in the args list. * * @return the list of MonkeyTalk Args */ public List<String> getArgs() { return Collections.unmodifiableList(args); } /** * Get the list of MonkeyTalk command args as a javascript array eg. ['arg1','arg2','arg3']. * * @return the MonkeyTalk args as a single string */ public String getArgsAsJsArray() { // if no args, return empty if (args.size() == 0) { return ""; } StringBuilder sb = new StringBuilder(); for (String arg : args) { // if argument isn't a quoted key/value pair // NOTE: Assumes quoted pairs are escaped if (!arg.matches("[^\\s]+=\"[^\"]*\"")) { // format for export arg = exportStr(arg); // Surround arg with single quote arg = "'" + arg + "'"; } sb.append(arg).append(','); } return "[" + sb.substring(0, sb.length() - 1) + "]"; } /** * Get the list of MonkeyTalk command args as a single string with quoted args as necessary (aka * only when they contain spaces). * * @return the MonkeyTalk args as a single string */ public String getArgsAsString() { // if no args, return empty if (args.size() == 0) { return ""; } StringBuilder sb = new StringBuilder(); for (String arg : args) { // if argument isn't a quoted key/value pair // NOTE: Assumes quoted pairs are escaped if (!arg.matches("[^\\s]+=\"[^\"]*\"")) { // format for export arg = exportStr(arg); if (arg.startsWith(MODIFIER_PREFIX) && arg.contains("=")) { // arg looks like a modifier, so it must be quoted arg = "\"" + arg + "\""; } } sb.append(arg).append(' '); } return sb.substring(0, sb.length() - 1); } /** * Set the MonkeyTalk command args and MonkeyTalk command modifiers from the given string. * * @param s * the MonkeyTalk args and modifiers as a string */ public void setArgsAndModifiers(String s) { if (s != null) { parseArgsAndModifiers(MonkeyTalkParser.parse(s)); setCommand(); } } /** * Get the map of MonkeyTalk command modifiers. If some of the modifier values were originally * quoted in the MonkeyTalk command string, they are returned in unquoted form. Additionally, * the {@code %} prefix signifying a modifier key-value pair, is removed from the key name. For * example, the modifier {@code %thinktime=123} would be stored with key {@code thinktime} and * the value {@code 123} . * * @return the map of MonkeyTalk command modifiers */ public Map<String, String> getModifiers() { return Collections.unmodifiableMap(modifiers); } /** * Get the map of MonkeyTalk command modifiers as a single string with quoted values as * necessary (aka only when they contain spaces). * * @return the MonkeyTalk modifiers as a single string */ public String getModifiersAsString() { if (modifiers.size() == 0) { return ""; } StringBuilder sb = new StringBuilder(); for (String key : modifiers.keySet()) { String val = exportStr(modifiers.get(key)); sb.append(MODIFIER_PREFIX).append(key).append('=').append(val).append(' '); } return sb.substring(0, sb.length() - 1); } /** * Set a single MonkeyTalk command modifier with a named-value pair. A {@code null} key does * nothing, but a valid key with a {@code null} value will cause the key to be deleted from the * map. * * @param key * the MonkeyTalk command modifier name * @param value * the MonkeyTalk command modifier value */ public void setModifier(String key, String value) { if (key != null) { if (value != null) { // do NOT escaped modifiers.put(key, value); setCommand(); } else if (modifiers.containsKey(key)) { modifiers.remove(key); setCommand(); } } } /** * Get the default timeout. * * @return the timeout */ public int getDefaultTimeout() { return (defaultTimeout < 0 ? DEFAULT_TIMEOUT : defaultTimeout); } /** * Set the default timeout. Typically by the processor to the global timeout. * * @param defaultTimeout * the timeout */ public void setDefaultTimeout(int defaultTimeout) { this.defaultTimeout = defaultTimeout; } /** * Get the default thinktime. * * @return the thinktime */ public int getDefaultThinktime() { return (defaultThinktime < 0 ? DEFAULT_THINKTIME : defaultThinktime); } /** * Set the default thinktime. Typically set by the processor to the global thinktime. * * @param defaultThinktime */ public void setDefaultThinktime(int defaultThinktime) { this.defaultThinktime = defaultThinktime; } /** * Get the timeout MonkeyTalk command modifier, or {@link Command#DEFAULT_TIMEOUT} if not set. * * @return the timeout */ public int getTimeout() { return getIntModiferValueWithDefault("timeout", getDefaultTimeout()); } /** * Get the timeout MonkeyTalk command modifier * * @param noDefault * whether to return default if no value set * @return timeout or -1 if unset */ public int getTimeoutRaw() { return getIntModiferValueWithDefault("timeout", -1); } /** * Get the thinktime MonkeyTalk command modifier, or {@link Command#DEFAULT_THINKTIME} if not * set. * * @return the thinktime */ public int getThinktime() { return getIntModiferValueWithDefault("thinktime", getDefaultThinktime()); } /** * Get the thinktime MonkeyTalk command modifier * * @return thinktime, or -1 if unset */ public int getThinktimeRaw() { return getIntModiferValueWithDefault("thinktime", -1); } /** * Get the retry delay MonkeyTalk command modifier, or {@link Command#DEFAULT_RETRYDELAY} if not * set. * * @return the retry delay */ public int getRetryDelay() { return getIntModiferValueWithDefault("retrydelay", DEFAULT_RETRYDELAY); } /** * Helper to get the value of a MonkeyTalk command modifier given its name (aka the key). If the * modifier doesn't exist, return the given default value. * * @param key * the MonkeyTalk command modifier * @param defaultValue * the value to return if the modifier doesn't exist * @return the value of the given key, or the default value if the modifier doesn't exist */ private int getIntModiferValueWithDefault(String key, int defaultValue) { int val = defaultValue; if (modifiers.containsKey(key)) { try { val = Integer.parseInt(modifiers.get(key).replaceAll("\\D+", "").toString()); } catch (Exception ex) { val = defaultValue; } } return val; } /** * If a MonkeyTalk command begins with {@code #}, the pound symbol, it is considered to be a * single-line comment. * * @return true if the MonkeyTalk command is a comment, otherwise false */ public boolean isComment() { return comment; } /** * True if screenshot on error is on (aka if this command causes an error, then the Agent will * return a screenshot along with the response), otherwise false. Defaults to {@code true}. * * @return true if screenshot on error is on */ public boolean isScreenshotOnError() { String val = modifiers.get(SCREENSHOT_ON_ERROR); return (val == null ? true : !val.equalsIgnoreCase("false")); } /** * True if ignore modifier is {@code true}, otherwise false. Defaults to {@code false}. If a * command is ignored, it will be skipped by the processor during playback. * * @return true if ignore is on */ public boolean isIgnored() { String val = modifiers.get(IGNORE_MODIFIER); return (val == null ? false : val.equalsIgnoreCase("true")); } /** * True if ignore modifier contains the given value, otherwise false. Defaults to {@code false}. * If a command is ignored, it will be skipped by the processor during playback. * * @return true if ignore is on */ public boolean isIgnored(String value) { String val = modifiers.get(IGNORE_MODIFIER); return (val == null || value == null ? false : val.toLowerCase().indexOf( value.toLowerCase()) != -1); } /** * True if should fail modifier is {@code true}, otherwise false. Defaults to {@code false}. If * a command is set to should fail, the processor expects it to generate a failure result during * playback (and then it doesn't fail and returns ok), otherwise an ok result generates a * failure. * * @return true if should fail is on */ public boolean shouldFail() { String val = modifiers.get(SHOULD_FAIL_MODIFIER); return (val == null ? false : val.equalsIgnoreCase("true")); } /** * Set screenshot on error, true to turn on, false to turn off. * * @param screenshotOnError * true to turn on screenshot on error */ public void setScreenshotOnError(boolean screenshotOnError) { setModifier(SCREENSHOT_ON_ERROR, Boolean.toString(screenshotOnError)); } private void initCommand() { componentType = null; monkeyId = null; action = null; args = new ArrayList<String>(); modifiers = new HashMap<String, String>(); comment = false; command = null; } private void setProperties(String componentType, String monkeyId, String action, List<String> args, Map<String, String> modifiers) { // process component type first, and abort if we are a comment setComponentType(componentType); if (comment) { return; } setMonkeyId(monkeyId); setAction(action); if (args != null) { for (String arg : args) { // escape arg and append this.args.add(importStr(arg)); } } if (modifiers != null) { for (Map.Entry<String, String> mod : modifiers.entrySet()) { // do NOT escape modifiers, just append this.modifiers.put(mod.getKey(), mod.getValue()); } } // update command string setCommand(); } /** * Parse a MonkeyTalk command string, and update all the properties of this Command object. * * @param command * the MonkeyTalk command string */ private void parseCommand(String command) { List<String> tokens = MonkeyTalkParser.parse(command); if (tokens.size() > 0) { setComponentType(tokens.get(0)); } if (tokens.size() > 1) { setMonkeyId(tokens.get(1)); } if (tokens.size() > 2) { setAction(tokens.get(2)); } if (tokens.size() > 3) { parseArgsAndModifiers(tokens.subList(3, tokens.size())); } } /** * Helper to parse the given tokens into MonkeyTalk command args and command modifiers. * * @param tokens * the tokens */ private void parseArgsAndModifiers(List<String> tokens) { args = new ArrayList<String>(); for (String token : tokens) { if (token.startsWith(MODIFIER_PREFIX) && token.contains("=")) { // token is a modifier token = token.substring(1, token.length()); String[] m = token.split("="); String key = m[0].toLowerCase(); String val = (m.length > 1 ? m[1] : null); modifiers.put(key, importStr(val)); } else { // otherwise, token is an arg args.add(importStr(token)); } } // update command string setCommand(); } /** * Given all substitution parameters, search the MonkeyTalk command string for all instances and * replace them with the corresponding value. The built-in variables {@code componentType}, * {@code monkeyId}, {@code action} are substituted as <code>%{componentType}</code>, * <code>%{monkeyId}</code>, <code>%{action}</code>. The argument list is substituted as * <code>%{1}</code>, <code>%{2}</code>, etc. The variables map (of name-value pairs) is * substituted as <code>${varname...}</code>. <b>NOTE:</b> all the built-in variables and args * are percent-curly bracket, <code>%{...}</code>, and the named variables are dollar-curly * bracket, <code>${...}</code>. * * @param componentType * the value to be substituted for <code>%{componentType}</code> * @param monkeyId * the value to be substituted for <code>%{monkeyId}</code> * @param action * the value to be substituted for <code>%{action}</code> * @param args * the list of values to be substituted for <code>%{1}</code>, <code>%{2}</code>, * etc. * @param variables * the map of name-value pairs to be substituted for <code>${varname...}</code> * @return a new, fully substituted MonkeyTalk command */ public Command substitute(String componentType, String monkeyId, String action, List<String> args, Map<String, String> variables) { String newComponentType = substituteString(this.componentType, componentType, monkeyId, action, args, variables); String newMonkeyId = substituteString(this.monkeyId, componentType, monkeyId, action, args, variables); String newAction = substituteString(this.action, componentType, monkeyId, action, args, variables); List<String> newArgs = new ArrayList<String>(); for (String arg : this.args) { newArgs.add(substituteString(arg, componentType, monkeyId, action, args, variables)); } Map<String, String> newModifiers = new HashMap<String, String>(); for (Map.Entry<String, String> mod : modifiers.entrySet()) { // do NOT escape modifiers newModifiers.put( mod.getKey(), substituteString(mod.getValue(), componentType, monkeyId, action, args, variables)); } return new Command(newComponentType, newMonkeyId, newAction, newArgs, newModifiers); } /** * Helper to replace the given string with all built-in vars, built-in args, and named * variables. * * @param s * the target string * @param componentType * the value to be substituted for <code>%{componentType}</code> * @param monkeyId * the value to be substituted for <code>%{monkeyId}</code> * @param action * the value to be substituted for <code>%{action}</code> * @param args * the list of values to be substituted for <code>%{1}</code>, <code>%{2}</code>, * etc. * @param variables * the map of name-value pairs to be substituted for <code>${varname...}</code> * @return the fully-substituted string */ private String substituteString(String s, String componentType, String monkeyId, String action, List<String> args, Map<String, String> variables) { if (s == null) { return null; } if (componentType != null) { s = s.replaceAll("\\%\\{componentType\\}", Matcher.quoteReplacement(componentType)); } if (monkeyId != null) { s = s.replaceAll("\\%\\{monkeyId\\}", Matcher.quoteReplacement(monkeyId)); } if (action != null) { s = s.replaceAll("\\%\\{action\\}", Matcher.quoteReplacement(action)); } if (args != null) { for (int i = 0; i < args.size(); i++) { String target = "\\%\\{" + (i + 1) + "\\}"; s = s.replaceAll(target, Matcher.quoteReplacement(args.get(i))); } } if (variables != null) { for (Map.Entry<String, String> var : variables.entrySet()) { String target = "\\$\\{" + var.getKey() + "\\}"; s = s.replaceAll(target, Matcher.quoteReplacement(var.getValue())); } } return s; } /** * Get the JSON representation of the MonkeyTalk command. * * @return the MonkeyTalk command as JSON */ public JSONObject getCommandAsJSON() { return getCommandAsJSON(true); } /** * Get the JSON representation of the MonkeyTalk command, optionally without thinktime and * timeout. * * @return the MonkeyTalk command as JSON */ public JSONObject getCommandAsJSON(boolean withTimings) { JSONObject json = new JSONObject(); try { json.put("componentType", componentType); json.put("monkeyId", monkeyId); json.put("action", action); json.put("args", new JSONArray(args)); JSONObject mods = new JSONObject(modifiers); if (withTimings) { mods.put("timeout", getTimeout()); mods.put("thinktime", getThinktime()); if (!isScreenshotOnError()) { mods.put("screenshotonerror", "false"); } } json.put("modifiers", mods); } catch (JSONException ex) { return new JSONObject(); } return json; } /** * Convert an internal value to export-able format. Token characters are escaped as needed. The * string is quoted if it contains any spaces. * * @param s * MonkeyTalk command token in internal format * @return MonkeyTalk token in export-able format */ private String exportStr(Object s) { if (s == null) { return ""; } String e = escape(s.toString()); return e.matches("\\S+") ? e : "\"" + e + "\""; } /*** * Convert the specified string into escaped form. Instances of the following are escaped: * <ul> * <li><code><tab> => "\t"</code> * <li><code><backspace> => "\b"</code> * <li><code><newline> => "\n"</code> * <li><code><carriage return> => "\r"</code> * <li><code><formfeed> => "\f"</code> * <li><code><double quote> => "\""</code> * <li><code><backslash> => "\\"</code> * </ul> * * @param s * string to be escaped * @return the specified string in escaped form */ private String escape(String s) { // FIXME: don't escape anything for now... if (s == null || s.matches(".*")) { // s.matches("[^\\t\\010\\n\\r\\f\'\"\\\\]+")) { return s; } int len = s.length(); StringBuilder sb = new StringBuilder(len); for (int i = 0; i < len; i++) { char c = s.charAt(i); switch (c) { case '\t': sb.append("\\t"); break; case '\b': sb.append("\\b"); break; case '\n': sb.append("\\n"); break; case '\r': sb.append("\\r"); break; case '\f': sb.append("\\f"); break; case '\"': sb.append("\\\""); break; case '\\': sb.append("\\\\"); break; default: sb.append(c); break; } } return sb.toString(); } /** * Convert the specified string to internal format. All escape sequences are replaced with the * characters they represent and any bounding quotes are removed. * * @param s * string to convert to internal format * @return the specified string in internal format */ private String importStr(String s) { if (s == null) { return null; } s = s.trim(); return unescape(s.matches("\".*\"") ? s.substring(1, s.length() - 1) : s); } /** * Replace escape sequenced in the specified string with the characters they represent. The * following sequences are supported: * <ul> * <li><code>"\t" => <tab></code> * <li><code>"\b" => <backspace></code> * <li><code>"\n" => <newline></code> * <li><code>"\r" => <carriage return></code> * <li><code>"\f" => <formfeed></code> * <li><code>"\"" => <double quote></code> * <li><code>"\\" => <backslash></code> * </ul> * * @param s * string to be un-escaped * @return the specified string in un-escaped form */ private String unescape(String s) { // FIXME: don't unescape anything for now... if (s == null || s.matches(".*")) { // s.matches("[^\\\\]+")) { return s; } int len = s.length(); StringBuilder sb = new StringBuilder(len); for (int i = 0; i < len; i++) { char c = s.charAt(i); if (c == '\\') { c = s.charAt(++i); switch (c) { case 't': c = '\t'; break; case 'b': c = '\b'; break; case 'n': c = '\n'; break; case 'r': c = '\r'; break; case 'f': c = '\f'; break; case '\"': case '\\': break; default: break; } } sb.append(c); } return sb.toString(); } /** * Validate the command and return true if valid, otherwise false. * * @return true if the command is valid, otherwise false. */ public boolean isValid() { return (CommandStatus.OK == CommandValidator.validate(this).getStatus()); } @Override public Command clone() { if (comment) { return new Command(command); } else { return new Command(componentType, monkeyId, action, args, modifiers); } } @Override public int hashCode() { int result = 17; result = 31 * result + (getComponentType() == null ? 0 : getComponentType().toLowerCase().hashCode()); result = 31 * result + (getMonkeyId() == null ? 0 : getMonkeyId().hashCode()); result = 31 * result + (getAction() == null ? 0 : getAction().toLowerCase().hashCode()); result = 31 * result + getArgsAsString().hashCode(); result = 31 * result + getModifiersAsString().hashCode(); return result; } @Override public boolean equals(Object obj) { if (obj == this) return true; if (!(obj instanceof Command)) return false; Command that = (Command) obj; boolean checkComponentType = (getComponentType() == null ? that.getComponentType() == null : getComponentType().equalsIgnoreCase(that.getComponentType())); boolean checkMonkeyId = (getMonkeyId() == null ? that.getMonkeyId() == null : getMonkeyId() .equals(that.getMonkeyId())); boolean checkAction = (getAction() == null ? that.getAction() == null : getAction() .equalsIgnoreCase(that.getAction())); boolean checkArgs = getArgsAsString().equals(that.getArgsAsString()); boolean checkModifiers = getModifiersAsString().equals(that.getModifiersAsString()); return checkComponentType && checkMonkeyId && checkAction && checkArgs && checkModifiers; } @Override /** * Get the MonkeyTalk command string. * @return the MonkeyTalk command string */ public String toString() { return command; } }