/* 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.sender;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.json.JSONException;
import org.json.JSONObject;
import com.gorillalogic.monkeytalk.Command;
import com.gorillalogic.monkeytalk.server.JsonServer.HttpStatus;
/**
* Helper class to send a MonkeyTalk {@link Command} object to the target url via a HTTP POST, where
* the {@code Command} is encoded as JSON in the POST body.
*/
public class CommandSender extends Sender {
/**
* MonkeyTalk wire protocol PLAY command.
*/
public static final String PLAY = "PLAY";
/**
* MonkeyTalk wire protocol RECORD command.
*/
public static final String RECORD = "RECORD";
/**
* MonkeyTalk wire protocol PING command.
*/
public static final String PING = "PING";
/**
* MonkeyTalk wire protocol DUMPTREE command.
*/
public static final String DUMPTREE = "DUMPTREE";
/**
* MonkeyTalk wire protocol version.
*/
private static final int VERSION = 1;
/**
* Default target host
*/
private static final String DEFAULT_TARGET_HOST = "localhost";
/**
* Default target path
*/
private static final String DEFAULT_CONTEXT_PATH = "fonemonkey";
/**
* Playback ignores (aka doesn't put on the wire) any of these components.
*/
private static final Set<String> IGNORE_COMPONENTS_FOR_PLAY = new HashSet<String>(
Arrays.asList("doc", "vars", "script", "test", "setup", "teardown"));
private URL url;
/**
* PACKAGE ACCESS ONLY --- use CommandSenderFactory
*
* Instantiate a new command sender from the given host and port, where path defaults to
* {@link CommandSender#DEFAULT_CONTEXT_PATH}.
*
* @param host
* the target host
* @param port
* the target port
*/
protected CommandSender(String host, int port) {
this(host, port, DEFAULT_CONTEXT_PATH);
}
public CommandSender() {} // for factory ONLY do not call elsewhere
public void init(CommandSenderFactory factory, String host, int port, String path) { // for factory ONLY do not call elsewhere
if (host == null) {
host = DEFAULT_TARGET_HOST;
}
if (path == null) {
path = DEFAULT_CONTEXT_PATH;
}
if (!path.startsWith("/")) {
path = "/" + path;
}
if (port < 0) {
port = 0;
}
try {
url = new URL("http", host, port, path);
} catch (MalformedURLException ex) {
url = null;
}
}
/**
* Instantiate a new command sender from the given host, port, agent, and path that targets the
* url <code>http://<host>:<port>/<path></code> for sending all commands.
*
* @param host
* the target host
* @param port
* the target port
* @param path
* the target context path
*/
protected CommandSender(String host, int port, String path) {
init(null, host, port, path);
}
/**
* Send the dump tree command to the agent. Returns the UI component tree of the app (native
* components, plus web components).
*/
public Response dumpTree() {
return sendCommand(DUMPTREE, new JSONObject());
}
/**
* Send the given MonkeyTalk command as a PLAY to the given url. If the command is a comment,
* sending is short-circuited and an OK response is returned immediately.
*
* @param command
* the MonkeyTalk command
* @return the response
*/
public Response play(Command command) {
if (command == null || command.getCommand() == null) {
return new Response(HttpStatus.OK.getCode(),
"{result:\"OK\",message:\"ignore blank command\"}");
} else if (command.isComment()) {
return new Response(HttpStatus.OK.getCode(),
"{result:\"OK\",message:\"ignore comment\"}");
} else if (IGNORE_COMPONENTS_FOR_PLAY.contains(command.getComponentType().toLowerCase())) {
return new Response(HttpStatus.OK.getCode(), "{result:\"OK\",message:\"ignore "
+ command.getCommandName() + "\"}");
} else {
return sendCommand(PLAY, command.getCommandAsJSON());
}
}
/**
* Build the MonkeyTalk command from its parts ({@code componentType}, {@code monkeyId},
* {@code action}, etc.), and play it.
*
* @see CommandSender#play(Command)
* @see Command#Command(String, String, String, List, Map)
*
* @param componentType
* the MonkeyTalk componentType
* @param monkeyId
* the MonkeyTalk monkeyId
* @param action
* the MonkeyTalk action
* @param args
* the MonkeyTalk command args
* @param modifiers
* the MonkeyTalk command modifiers
* @return the response
*/
public Response play(String componentType, String monkeyId, String action, List<String> args,
Map<String, String> modifiers) {
return play(new Command(componentType, monkeyId, action, args, modifiers));
}
/**
* Send the given MonkeyTalk command as a RECORD to the given url.
*
* @param command
* the MonkeyTalk command
* @return the response
*/
public Response record(Command command) {
return sendCommand(RECORD, command.getCommandAsJSON(false));
}
/**
* Send the given MonkeyTalk command as a PING to the given url. Ping is used to start and stop
* recording, and as a keep-alive to make sure agent exists.
*
* @param record
* true turns on agent-side recording (aka agent now sends all recorded commands to
* the IDE), false turns off recording
* @return the response
*/
public Response ping(boolean record) {
return ping(record, null, 0);
}
/**
* Send the given MonkeyTalk command as a PING to the given url. Ping is used to start and stop
* recording, and as a keep-alive to make sure agent exists.
*
* @param record
* true turns on agent-side recording (aka agent now sends all recorded commands to
* the IDE), false turns off recording
* @param recordHost
* the target host for recording (aka the ip addr of the MonkeyTalk IDE record
* server)
* @param recordPort
* the target port for recording (aka the port of the MonkeyTalk IDE record server)
* @return the response
*/
public Response ping(boolean record, String recordHost, int recordPort) {
JSONObject json = new JSONObject();
try {
json.put("record", (record ? "ON" : "OFF"));
if (record) {
if (recordHost != null && recordHost.length() > 0) {
json.put("recordhost", recordHost);
json.put("recordport", recordPort);
}
}
} catch (JSONException e) {
return new Response(0, "failed to build outbound JSON message for PING");
}
return sendCommand(PING, json);
}
/**
* Send the given MonkeyTalk command as a JSON message via HTTP POST to the given url. Append to
* the given JSON object the {@code mtversion}, {@code mtcommand}, and {@code timestamp}.
*
* @param url
* the target url
* @param mtcommand
* the MonkeyTalk wire protocol command (PLAY, PING, RECORD)
* @param command
* the MonkeyTalk command
* @return the response
*/
private Response sendCommand(String mtcommand, JSONObject json) {
try {
json.put("mtversion", VERSION);
json.put("mtcommand", mtcommand);
json.put("timestamp", System.currentTimeMillis());
} catch (JSONException ex) {
return new Response(0, "failed to build outbound JSON message");
}
URL targetURL = getURLforCommand(mtcommand, json);
return sendJSON(targetURL, json);
}
protected URL getURLforCommand(String mtcommand, JSONObject json) {
return url;
}
@Override
public String toString() {
return "CommandSender: url=" + url;
}
protected URL getUrl() {
return url;
}
}