You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@taverna.apache.org by st...@apache.org on 2015/02/23 11:17:02 UTC

[21/28] incubator-taverna-common-activities git commit: Revert "temporarily empty repository"

http://git-wip-us.apache.org/repos/asf/incubator-taverna-common-activities/blob/390c286b/taverna-rest-activity/src/main/java/net/sf/taverna/t2/activities/rest/RESTActivityConfigurationBean.java
----------------------------------------------------------------------
diff --git a/taverna-rest-activity/src/main/java/net/sf/taverna/t2/activities/rest/RESTActivityConfigurationBean.java b/taverna-rest-activity/src/main/java/net/sf/taverna/t2/activities/rest/RESTActivityConfigurationBean.java
new file mode 100644
index 0000000..7a0d5d6
--- /dev/null
+++ b/taverna-rest-activity/src/main/java/net/sf/taverna/t2/activities/rest/RESTActivityConfigurationBean.java
@@ -0,0 +1,306 @@
+package net.sf.taverna.t2.activities.rest;
+
+import static net.sf.taverna.t2.activities.rest.RESTActivity.hasMessageBodyInputPort;
+
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+
+import com.fasterxml.jackson.databind.JsonNode;
+
+import net.sf.taverna.t2.activities.rest.RESTActivity.DATA_FORMAT;
+import net.sf.taverna.t2.activities.rest.RESTActivity.HTTP_METHOD;
+import net.sf.taverna.t2.workflowmodel.processor.config.ConfigurationBean;
+import net.sf.taverna.t2.workflowmodel.processor.config.ConfigurationProperty;
+
+/**
+ * Beans of this class store configuration information for REST activities.
+ * Configuration is comprised of the HTTP method to use, URL signature, and MIME
+ * types for Accept and Content-Type HTTP request headers. Additional value is
+ * used to record the format of outgoing data - binary or string. <br/>
+ * <br/>
+ * Also, derived attribute "activityInputs" is generated by identifying all
+ * "input ports" in the provided URL signature. <br/>
+ * <br/>
+ * Complete request URL (obtained by substituting values into the placeholders
+ * of the URL signature) is not stored, as it represents an instantiation of the
+ * activity invocation. The same applies for the input message body sent along
+ * with POST / PUT requests.
+ * 
+ * @author Sergejs Aleksejevs
+ */
+@SuppressWarnings("serial")
+@ConfigurationBean(uri = RESTActivity.URI + "#Config")
+public class RESTActivityConfigurationBean implements Serializable {
+	private static final List<String> knownHeaders = Arrays.asList("Accept",
+			"Content-Type", "Expect");
+	private RESTActivity.DATA_FORMAT outgoingDataFormat;
+
+	private boolean showRedirectionOutputPort;
+	private boolean showActualUrlPort;
+	private boolean showResponseHeadersPort;
+
+	// whether to perform URL escaping of passed parameters, true by default
+	private boolean escapeParameters = true;
+
+	// only need to store the configuration of inputs, as all of them are
+	// dynamic;
+	// only inputs that constitute components of URL signature are to be stored
+	// in this map all outputs are currently fixed, so no need to keep
+	// configuration of those
+	private Map<String, Class<?>> activityInputs;
+
+	private HTTPRequest request;
+
+	/**
+	 * @return An instance of the {@link RESTActivityConfigurationBean}
+	 *         pre-configured with default settings for all parameters.
+	 */
+	public static RESTActivityConfigurationBean getDefaultInstance() {
+		// TODO - set sensible default values here
+		RESTActivityConfigurationBean defaultBean = new RESTActivityConfigurationBean();
+		defaultBean.setRequest(new HTTPRequest());
+		defaultBean.setHttpMethod(RESTActivity.HTTP_METHOD.GET);
+		defaultBean.setAcceptsHeaderValue("application/xml");
+		defaultBean.setContentTypeForUpdates("application/xml");
+		defaultBean.setUrlSignature("http://www.uniprot.org/uniprot/{id}.xml");
+		defaultBean.setOutgoingDataFormat(RESTActivity.DATA_FORMAT.String);
+		// not ticked by default to allow to post to Twitter
+		defaultBean.setSendHTTPExpectRequestHeader(false);
+		// not showing the Redirection output port by default to make processor
+		// look simpler
+		defaultBean.setShowRedirectionOutputPort(false);
+		defaultBean.setShowActualUrlPort(false);
+		defaultBean.setShowResponseHeadersPort(false);
+		defaultBean.setEscapeParameters(true);
+		defaultBean.setOtherHTTPHeaders(new ArrayList<ArrayList<String>>());
+		return (defaultBean);
+	}
+
+	public RESTActivityConfigurationBean() {
+
+	}
+
+	public RESTActivityConfigurationBean(JsonNode json) {
+		JsonNode requestNode = json.get("request");
+		HTTPRequest request = new HTTPRequest();
+		if (requestNode.has("httpMethod")) {
+			request.setMethod(HTTP_METHOD.valueOf(requestNode.get("httpMethod")
+					.textValue()));
+		} else {
+			request.setMethod(HTTP_METHOD.GET);
+		}
+		request.setAbsoluteURITemplate(requestNode.get("absoluteURITemplate")
+				.textValue());
+		setRequest(request);
+		setAcceptsHeaderValue("application/xml");
+		setContentTypeForUpdates("application/xml");
+		setSendHTTPExpectRequestHeader(false);
+		if (requestNode.has("headers")) {
+			for (JsonNode headerNode : requestNode.get("headers")) {
+				String headerName = headerNode.get("header").textValue();
+				String headerValue = headerNode.get("value").textValue();
+				if ("Expect".equals(headerName)) {
+					request.setHeader(headerName, true);
+				} else {
+					request.setHeader(headerName, headerValue);
+				}
+			}
+		}
+		if (json.has("outgoingDataFormat")) {
+			setOutgoingDataFormat(DATA_FORMAT.valueOf(json.get(
+					"outgoingDataFormat").textValue()));
+		} else {
+			setOutgoingDataFormat(DATA_FORMAT.String);
+		}
+		if (json.has("showRedirectionOutputPort")) {
+			setShowRedirectionOutputPort(json.get("showRedirectionOutputPort")
+					.booleanValue());
+		} else {
+			setShowRedirectionOutputPort(false);
+		}
+		if (json.has("showActualURLPort")) {
+			setShowActualUrlPort(json.get("showActualURLPort").booleanValue());
+		} else {
+			setShowActualUrlPort(false);
+		}
+		if (json.has("showResponseHeadersPort")) {
+			setShowResponseHeadersPort(json.get("showResponseHeadersPort")
+					.booleanValue());
+		} else {
+			setShowResponseHeadersPort(false);
+		}
+		if (json.has("escapeParameters")) {
+			setEscapeParameters(json.get("escapeParameters").booleanValue());
+		} else {
+			setEscapeParameters(true);
+		}
+	}
+
+	/**
+	 * Tests validity of the configuration held in this bean. <br/>
+	 * <br/>
+	 * Performed tests are as follows: <br/>
+	 * * <code>httpMethod</code> is known to be valid - it's an enum; <br/>
+	 * * <code>urlSignature</code> - uses
+	 * {@link URISignatureHandler#isValid(String)} to test validity; <br/>
+	 * * <code>acceptsHeaderValue</code> and <code>contentTypeForUpdates</code>
+	 * must not be empty. <br/>
+	 * <br/>
+	 * <code>contentTypeForUpdates</code> is only checked if the
+	 * <code>httpMethod</code> is such that it is meant to use the Content-Type
+	 * header (that is POST / PUT only).
+	 * 
+	 * @return <code>true</code> if the configuration in the bean is valid;
+	 *         <code>false</code> otherwise.
+	 */
+	public boolean isValid() {
+		if (getUrlSignature() == null
+				|| !URISignatureHandler.isValid(getUrlSignature()))
+			return false;
+		return (hasMessageBodyInputPort(getHttpMethod())
+				&& getContentTypeForUpdates() != null
+				&& getContentTypeForUpdates().length() > 0 && outgoingDataFormat != null)
+				|| !hasMessageBodyInputPort(getHttpMethod());
+	}
+
+	public void setHttpMethod(RESTActivity.HTTP_METHOD httpMethod) {
+		request.setMethod(httpMethod);
+	}
+
+	public RESTActivity.HTTP_METHOD getHttpMethod() {
+		return request.getMethod();
+	}
+
+	public String getUrlSignature() {
+		return request.getAbsoluteURITemplate();
+	}
+
+	public void setUrlSignature(String urlSignature) {
+		request.setAbsoluteURITemplate(urlSignature);
+	}
+
+	public String getAcceptsHeaderValue() {
+		HTTPRequestHeader header = request.getHeader("Accept");
+		return header == null ? null : header.getFieldValue();
+	}
+
+	public void setAcceptsHeaderValue(String acceptsHeaderValue) {
+		request.setHeader("Accept", acceptsHeaderValue);
+	}
+
+	public String getContentTypeForUpdates() {
+		HTTPRequestHeader header = request.getHeader("Content-Type");
+		return header == null ? null : header.getFieldValue();
+	}
+
+	public void setContentTypeForUpdates(String contentTypeForUpdates) {
+		request.setHeader("Content-Type", contentTypeForUpdates);
+	}
+
+	public void setActivityInputs(Map<String, Class<?>> activityInputs) {
+		this.activityInputs = activityInputs;
+	}
+
+	public Map<String, Class<?>> getActivityInputs() {
+		return activityInputs;
+	}
+
+	public RESTActivity.DATA_FORMAT getOutgoingDataFormat() {
+		return outgoingDataFormat;
+	}
+
+	@ConfigurationProperty(name = "outgoingDataFormat", label = "Send data as", required = false)
+	public void setOutgoingDataFormat(
+			RESTActivity.DATA_FORMAT outgoingDataFormat) {
+		this.outgoingDataFormat = outgoingDataFormat;
+	}
+
+	public boolean getSendHTTPExpectRequestHeader() {
+		HTTPRequestHeader header = request.getHeader("Expect");
+		return header == null ? false : header.isUse100Continue();
+	}
+
+	public void setSendHTTPExpectRequestHeader(
+			boolean sendHTTPExpectRequestHeader) {
+		request.setHeader("Expect", sendHTTPExpectRequestHeader);
+	}
+
+	public boolean getShowRedirectionOutputPort() {
+		return showRedirectionOutputPort;
+	}
+
+	@ConfigurationProperty(name = "showRedirectionOutputPort", label = "Show 'Redirection' output port", required = false)
+	public void setShowRedirectionOutputPort(boolean showRedirectionOutputPort) {
+		this.showRedirectionOutputPort = showRedirectionOutputPort;
+	}
+
+	@ConfigurationProperty(name = "escapeParameters", label = "Escape URL parameter values", required = false)
+	public void setEscapeParameters(boolean escapeParameters) {
+		this.escapeParameters = Boolean.valueOf(escapeParameters);
+	}
+
+	public boolean getEscapeParameters() {
+		return escapeParameters;
+	}
+
+	public void setOtherHTTPHeaders(
+			ArrayList<ArrayList<String>> otherHTTPHeaders) {
+		for (ArrayList<String> otherHTTPHeader : otherHTTPHeaders)
+			request.setHeader(otherHTTPHeader.get(0), otherHTTPHeader.get(1));
+	}
+
+	public ArrayList<ArrayList<String>> getOtherHTTPHeaders() {
+		ArrayList<ArrayList<String>> otherHTTPHeaders = new ArrayList<>();
+		List<HTTPRequestHeader> headers = request.getHeaders();
+		for (HTTPRequestHeader header : headers)
+			if (!knownHeaders.contains(header.getFieldName())) {
+				ArrayList<String> nameValuePair = new ArrayList<>();
+				nameValuePair.add(header.getFieldName());
+				nameValuePair.add(header.getFieldValue());
+				otherHTTPHeaders.add(nameValuePair);
+			}
+		return otherHTTPHeaders;
+	}
+
+	/**
+	 * @return the showActualUrlPort
+	 */
+	public boolean getShowActualUrlPort() {
+		return showActualUrlPort;
+	}
+
+	/**
+	 * @param showActualUrlPort
+	 *            the showActualUrlPort to set
+	 */
+	public void setShowActualUrlPort(boolean showActualUrlPort) {
+		this.showActualUrlPort = showActualUrlPort;
+	}
+
+	/**
+	 * @return the showResponseHeadersPort
+	 */
+	public boolean getShowResponseHeadersPort() {
+		return showResponseHeadersPort;
+	}
+
+	/**
+	 * @param showResponseHeadersPort
+	 *            the showResponseHeadersPort to set
+	 */
+	public void setShowResponseHeadersPort(boolean showResponseHeadersPort) {
+		this.showResponseHeadersPort = showResponseHeadersPort;
+	}
+
+	public HTTPRequest getRequest() {
+		return request;
+	}
+
+	@ConfigurationProperty(name = "request", label = "HTTP Request")
+	public void setRequest(HTTPRequest request) {
+		this.request = request;
+	}
+}

http://git-wip-us.apache.org/repos/asf/incubator-taverna-common-activities/blob/390c286b/taverna-rest-activity/src/main/java/net/sf/taverna/t2/activities/rest/RESTActivityCredentialsProvider.java
----------------------------------------------------------------------
diff --git a/taverna-rest-activity/src/main/java/net/sf/taverna/t2/activities/rest/RESTActivityCredentialsProvider.java b/taverna-rest-activity/src/main/java/net/sf/taverna/t2/activities/rest/RESTActivityCredentialsProvider.java
new file mode 100644
index 0000000..050cdd8
--- /dev/null
+++ b/taverna-rest-activity/src/main/java/net/sf/taverna/t2/activities/rest/RESTActivityCredentialsProvider.java
@@ -0,0 +1,178 @@
+package net.sf.taverna.t2.activities.rest;
+
+import java.net.URI;
+import java.net.URLEncoder;
+import java.security.Principal;
+
+import javax.management.remote.JMXPrincipal;
+
+import net.sf.taverna.t2.security.credentialmanager.CredentialManager;
+import net.sf.taverna.t2.security.credentialmanager.UsernamePassword;
+
+import org.apache.http.auth.AuthScope;
+import org.apache.http.auth.Credentials;
+//import org.apache.http.client.CredentialsProvider;
+import org.apache.http.impl.client.BasicCredentialsProvider;
+import org.apache.log4j.Logger;
+
+/**
+ * This CredentialsProvider acts as a mediator between the Apache HttpClient and
+ * Taverna's CredentialManager that stores all user's credentials.
+ *
+ * The only role of it is to retrieve stored details from CredentialManager when
+ * they are required for HTTP authentication.
+ *
+ * @author Sergejs Aleksejevs
+ * @author Alex Nenadic
+ */
+public class RESTActivityCredentialsProvider extends BasicCredentialsProvider {
+	private static Logger logger = Logger.getLogger(RESTActivityCredentialsProvider.class);
+	
+	private static final int DEFAULT_HTTP_PORT = 80;
+	private static final int DEFAULT_HTTPS_PORT = 443;
+	
+	private static final String HTTP_PROTOCOL = "http";
+	private static final String HTTPS_PROTOCOL = "https";
+	
+	private CredentialManager credentialManager;
+	
+	public RESTActivityCredentialsProvider(CredentialManager credentialManager) {
+		this.credentialManager = credentialManager;
+	}
+	
+	@Override
+	public Credentials getCredentials(AuthScope authscope) {
+		logger.info("Looking for credentials for: Host - " + authscope.getHost() + ";" + "Port - "
+				+ authscope.getPort() + ";" + "Realm - " + authscope.getRealm() + ";"
+				+ "Authentication scheme - " + authscope.getScheme());
+		
+		// Ask the superclass first
+		Credentials creds = super.getCredentials(authscope);
+		if (creds != null) {
+			/*
+			 * We have used setCredentials() on this class (for proxy host,
+			 * port, username,password) just before we invoked the http request,
+			 * which will then pick the proxy credentials up from here.
+			 */
+			return creds;
+		}
+		
+		// Otherwise, ask Credential Manager if is can provide the credential
+		String AUTHENTICATION_REQUEST_MSG = "This REST service requires authentication in "
+				+ authscope.getRealm();
+
+		try {
+			UsernamePassword credentials = null;
+
+			/*
+			 * if port is 80 - use HTTP, don't append port if port is 443 - use
+			 * HTTPS, don't append port any other port - append port + do 2
+			 * tests:
+			 * 
+			 * --- test HTTPS first has...()
+			 * --- if not there, do get...() for HTTP (which will save the thing)
+			 *
+			 * (save both these entries for HTTP + HTTPS if not there)
+			 */
+
+			// build the service URI back to front
+			StringBuilder serviceURI = new StringBuilder();
+			serviceURI.insert(0, "/#" + URLEncoder.encode(authscope.getRealm(), "UTF-16"));
+			if (authscope.getPort() != DEFAULT_HTTP_PORT
+					&& authscope.getPort() != DEFAULT_HTTPS_PORT) {
+				// non-default port - add port name to the URI
+				serviceURI.insert(0, ":" + authscope.getPort());
+			}
+			serviceURI.insert(0, authscope.getHost());
+			serviceURI.insert(0, "://");
+
+			// now the URI is complete, apart from the protocol name
+			if (authscope.getPort() == DEFAULT_HTTP_PORT
+					|| authscope.getPort() == DEFAULT_HTTPS_PORT) {
+				// definitely HTTP or HTTPS
+				serviceURI.insert(0, (authscope.getPort() == DEFAULT_HTTP_PORT ? HTTP_PROTOCOL
+						: HTTPS_PROTOCOL));
+
+				// request credentials from CrendentialManager
+				credentials = credentialManager.getUsernameAndPasswordForService(
+						URI.create(serviceURI.toString()), true, AUTHENTICATION_REQUEST_MSG);
+			} else {
+				/*
+				 * non-default port - will need to try both HTTP and HTTPS; just
+				 * check (no pop-up will be shown) if credentials are there -
+				 * one protocol that matched will be used; if
+				 */
+				if (credentialManager.hasUsernamePasswordForService(URI.create(HTTPS_PROTOCOL
+						+ serviceURI.toString()))) {
+					credentials = credentialManager.getUsernameAndPasswordForService(
+							URI.create(HTTPS_PROTOCOL + serviceURI.toString()), true,
+							AUTHENTICATION_REQUEST_MSG);
+				} else if (credentialManager.hasUsernamePasswordForService(URI.create(HTTP_PROTOCOL
+						+ serviceURI.toString()))) {
+					credentials = credentialManager.getUsernameAndPasswordForService(
+							URI.create(HTTP_PROTOCOL + serviceURI.toString()), true,
+							AUTHENTICATION_REQUEST_MSG);
+				} else {
+					/*
+					 * Neither of the two options succeeded, request details with a
+					 * popup for HTTP...
+					 */
+					credentials = credentialManager.getUsernameAndPasswordForService(
+							URI.create(HTTP_PROTOCOL + serviceURI.toString()), true,
+							AUTHENTICATION_REQUEST_MSG);
+
+					/*
+					 * ...then save a second entry with HTTPS protocol (if the
+					 * user has chosen to save the credentials)
+					 */
+					if (credentials != null && credentials.isShouldSave()) {
+						credentialManager.addUsernameAndPasswordForService(credentials,
+								URI.create(HTTPS_PROTOCOL + serviceURI.toString()));
+					}
+				}
+			}
+
+			if (credentials != null) {
+				logger.info("Credentials obtained successfully");
+				return new RESTActivityCredentials(credentials.getUsername(),
+						credentials.getPasswordAsString());
+			}
+		} catch (Exception e) {
+			logger.error(
+					"Unexpected error while trying to obtain user's credential from CredentialManager",
+					e);
+		}
+
+		// error or nothing was found
+		logger.info("Credentials not found - the user must have refused to enter them.");
+		return null;
+	}
+
+	/**
+	 * This class encapsulates user's credentials that this CredentialsProvider
+	 * can pass to Apache HttpClient.
+	 *
+	 * @author Sergejs Aleksejevs
+	 */
+	public class RESTActivityCredentials implements Credentials {
+		// this seems to be the simplest existing standard implementation of
+		// Principal interface
+		private final JMXPrincipal user;
+		private final String password;
+
+		public RESTActivityCredentials(String username, String password) {
+			this.user = new JMXPrincipal(username);
+			this.password = password;
+		}
+
+		@Override
+		public String getPassword() {
+			return password;
+		}
+
+		@Override
+		public Principal getUserPrincipal() {
+			return user;
+		}
+	}
+}

http://git-wip-us.apache.org/repos/asf/incubator-taverna-common-activities/blob/390c286b/taverna-rest-activity/src/main/java/net/sf/taverna/t2/activities/rest/RESTActivityFactory.java
----------------------------------------------------------------------
diff --git a/taverna-rest-activity/src/main/java/net/sf/taverna/t2/activities/rest/RESTActivityFactory.java b/taverna-rest-activity/src/main/java/net/sf/taverna/t2/activities/rest/RESTActivityFactory.java
new file mode 100644
index 0000000..abf9bc8
--- /dev/null
+++ b/taverna-rest-activity/src/main/java/net/sf/taverna/t2/activities/rest/RESTActivityFactory.java
@@ -0,0 +1,165 @@
+/*******************************************************************************
+ * Copyright (C) 2011 The University of Manchester
+ *
+ *  Modifications to the initial code base are copyright of their
+ *  respective authors, or their employers as appropriate.
+ *
+ *  This program is free software; you can redistribute it and/or
+ *  modify it under the terms of the GNU Lesser General Public License
+ *  as published by the Free Software Foundation; either version 2.1 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
+ *  Lesser General Public License for more details.
+ *
+ *  You should have received a copy of the GNU Lesser General Public
+ *  License along with this program; if not, write to the Free Software
+ *  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
+ ******************************************************************************/
+package net.sf.taverna.t2.activities.rest;
+
+import java.io.IOException;
+import java.net.URI;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+import net.sf.taverna.t2.activities.rest.URISignatureHandler.URISignatureParsingException;
+import net.sf.taverna.t2.security.credentialmanager.CredentialManager;
+import net.sf.taverna.t2.workflowmodel.Edits;
+import net.sf.taverna.t2.workflowmodel.processor.activity.ActivityConfigurationException;
+import net.sf.taverna.t2.workflowmodel.processor.activity.ActivityFactory;
+import net.sf.taverna.t2.workflowmodel.processor.activity.ActivityInputPort;
+import net.sf.taverna.t2.workflowmodel.processor.activity.ActivityOutputPort;
+
+import org.apache.http.client.CredentialsProvider;
+import org.apache.log4j.Logger;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+/**
+ * An {@link ActivityFactory} for creating <code>RESTActivity</code>.
+ *
+ * @author David Withers
+ */
+public class RESTActivityFactory implements ActivityFactory {
+
+	private static Logger logger = Logger.getLogger(RESTActivityFactory.class);
+
+	private CredentialsProvider credentialsProvider;
+	private Edits edits;
+
+	@Override
+	public RESTActivity createActivity() {
+                RESTActivity activity = new RESTActivity(credentialsProvider);
+                activity.setEdits(edits);
+		return activity; 
+	}
+
+	@Override
+	public URI getActivityType() {
+		return URI.create(RESTActivity.URI);
+	}
+
+	@Override
+	public JsonNode getActivityConfigurationSchema() {
+		ObjectMapper objectMapper = new ObjectMapper();
+		try {
+ 			return objectMapper.readTree(getClass().getResource("/schema.json"));
+		} catch (IOException e) {
+			return objectMapper.createObjectNode();
+		}
+	}
+
+	public void setCredentialManager(CredentialManager credentialManager) {
+		credentialsProvider = new RESTActivityCredentialsProvider(credentialManager);
+	}
+
+	@Override
+	public Set<ActivityInputPort> getInputPorts(JsonNode configuration)
+			throws ActivityConfigurationException {
+		Set<ActivityInputPort> activityInputPorts = new HashSet<>();
+		RESTActivityConfigurationBean configBean = new RESTActivityConfigurationBean(configuration);
+		// ---- CREATE INPUTS ----
+
+		// all input ports are dynamic and depend on the configuration
+		// of the particular instance of the REST activity
+
+		// POST and PUT operations send data, so an input for the message body
+		// is required
+		if (RESTActivity.hasMessageBodyInputPort(configBean.getHttpMethod())) {
+			// the input message will be just an XML string for now
+			activityInputPorts.add(edits.createActivityInputPort(RESTActivity.IN_BODY, 0, true, null, configBean.getOutgoingDataFormat()
+					.getDataFormat()));
+		}
+
+		// now process the URL signature - extract all placeholders and create
+		// an input port for each
+		List<String> placeholders = URISignatureHandler
+				.extractPlaceholders(configBean.getUrlSignature());
+		String acceptsHeaderValue = configBean.getAcceptsHeaderValue();
+		if (acceptsHeaderValue != null && !acceptsHeaderValue.isEmpty()) {
+			try {
+			List<String> acceptsPlaceHolders = URISignatureHandler
+				.extractPlaceholders(acceptsHeaderValue);
+			acceptsPlaceHolders.removeAll(placeholders);
+			placeholders.addAll(acceptsPlaceHolders);
+			}
+			catch (URISignatureParsingException e) {
+				logger.error(e);
+			}
+		}
+		for (ArrayList<String> httpHeaderNameValuePair : configBean.getOtherHTTPHeaders()) {
+			try {
+				List<String> headerPlaceHolders = URISignatureHandler
+				.extractPlaceholders(httpHeaderNameValuePair.get(1));
+				headerPlaceHolders.removeAll(placeholders);
+				placeholders.addAll(headerPlaceHolders);
+			}
+			catch (URISignatureParsingException e) {
+				logger.error(e);
+			}
+		}
+		for (String placeholder : placeholders) {
+			// these inputs will have a dynamic name each;
+			// the data type is string as they are the values to be
+			// substituted into the URL signature at the execution time
+			activityInputPorts.add(edits.createActivityInputPort(placeholder, 0, true, null, String.class));
+		}
+		return activityInputPorts;
+	}
+
+	@Override
+	public Set<ActivityOutputPort> getOutputPorts(JsonNode configuration)
+			throws ActivityConfigurationException {
+		Set<ActivityOutputPort> activityOutputPorts = new HashSet<>();
+		RESTActivityConfigurationBean configBean = new RESTActivityConfigurationBean(configuration);
+		// ---- CREATE OUTPUTS ----
+		// all outputs are of depth 0 - i.e. just a single value on each;
+
+		// output ports for Response Body and Status are static - they don't
+		// depend on the configuration of the activity;
+		activityOutputPorts.add(edits.createActivityOutputPort(RESTActivity.OUT_RESPONSE_BODY, 0, 0));
+		activityOutputPorts.add(edits.createActivityOutputPort(RESTActivity.OUT_STATUS, 0, 0));
+		if (configBean.getShowActualUrlPort()) {
+			activityOutputPorts.add(edits.createActivityOutputPort(RESTActivity.OUT_COMPLETE_URL, 0, 0));
+			}
+			if (configBean.getShowResponseHeadersPort()) {
+				activityOutputPorts.add(edits.createActivityOutputPort(RESTActivity.OUT_RESPONSE_HEADERS, 1, 1));
+			}
+		// Redirection port may be hidden/shown
+		if (configBean.getShowRedirectionOutputPort()) {
+			activityOutputPorts.add(edits.createActivityOutputPort(RESTActivity.OUT_REDIRECTION, 0, 0));
+		}
+		return activityOutputPorts;
+	}
+
+	public void setEdits(Edits edits) {
+		this.edits = edits;
+	}
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-taverna-common-activities/blob/390c286b/taverna-rest-activity/src/main/java/net/sf/taverna/t2/activities/rest/RESTActivityHealthCheck.java
----------------------------------------------------------------------
diff --git a/taverna-rest-activity/src/main/java/net/sf/taverna/t2/activities/rest/RESTActivityHealthCheck.java b/taverna-rest-activity/src/main/java/net/sf/taverna/t2/activities/rest/RESTActivityHealthCheck.java
new file mode 100644
index 0000000..1a80810
--- /dev/null
+++ b/taverna-rest-activity/src/main/java/net/sf/taverna/t2/activities/rest/RESTActivityHealthCheck.java
@@ -0,0 +1,32 @@
+package net.sf.taverna.t2.activities.rest;
+
+import net.sf.taverna.t2.visit.VisitKind;
+import net.sf.taverna.t2.visit.Visitor;
+
+/**
+ * A <code>RESTActivityHealthCheck</code> is a kind of visit that determines
+ * if the corresponding REST activity in a workflow will work during a workflow run.
+ *
+ * @author Sergejs Aleksejevs
+ */
+public class RESTActivityHealthCheck extends VisitKind {
+
+	// The following values indicate the type of results that can be associated
+	// with a VisitReport generated by a health-checking visitor.
+
+	public static final int CORRECTLY_CONFIGURED = 0;
+	public static final int GENERAL_CONFIG_PROBLEM = 10;
+
+	@Override
+	public Class<? extends Visitor<?>> getVisitorClass() {
+		return RESTActivityHealthChecker.class;
+	}
+
+	private static class Singleton {
+		private static RESTActivityHealthCheck instance = new RESTActivityHealthCheck();
+	}
+
+	public static RESTActivityHealthCheck getInstance() {
+		return Singleton.instance;
+	}
+}

http://git-wip-us.apache.org/repos/asf/incubator-taverna-common-activities/blob/390c286b/taverna-rest-activity/src/main/java/net/sf/taverna/t2/activities/rest/RESTActivityHealthChecker.java
----------------------------------------------------------------------
diff --git a/taverna-rest-activity/src/main/java/net/sf/taverna/t2/activities/rest/RESTActivityHealthChecker.java b/taverna-rest-activity/src/main/java/net/sf/taverna/t2/activities/rest/RESTActivityHealthChecker.java
new file mode 100644
index 0000000..cbca70a
--- /dev/null
+++ b/taverna-rest-activity/src/main/java/net/sf/taverna/t2/activities/rest/RESTActivityHealthChecker.java
@@ -0,0 +1,58 @@
+package net.sf.taverna.t2.activities.rest;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import net.sf.taverna.t2.visit.VisitReport;
+import net.sf.taverna.t2.visit.VisitReport.Status;
+import net.sf.taverna.t2.workflowmodel.health.HealthCheck;
+import net.sf.taverna.t2.workflowmodel.health.HealthChecker;
+
+/**
+ * A {@link HealthChecker} for a {@link RESTActivity}.
+ *
+ * @author Sergejs Aleksejevs
+ */
+public class RESTActivityHealthChecker implements HealthChecker<RESTActivity> {
+	@Override
+	public boolean canVisit(Object subject) {
+		return (subject instanceof RESTActivity);
+	}
+
+	@Override
+	public VisitReport visit(RESTActivity activity, List<Object> ancestors) {
+		// collection of validation reports that this health checker will create
+		List<VisitReport> reports = new ArrayList<VisitReport>();
+
+		RESTActivityConfigurationBean configBean = activity.getConfigurationBean();
+		if (configBean.isValid()) {
+			reports.add(new VisitReport(RESTActivityHealthCheck.getInstance(), activity,
+					"REST Activity is configured correctly",
+					RESTActivityHealthCheck.CORRECTLY_CONFIGURED, Status.OK));
+		} else {
+			reports.add(new VisitReport(RESTActivityHealthCheck.getInstance(), activity,
+					"REST Activity - bad configuration",
+					RESTActivityHealthCheck.GENERAL_CONFIG_PROBLEM, Status.SEVERE));
+		}
+
+		// (possibly other types of reports could be added later)
+
+		// collection all reports together
+		Status worstStatus = VisitReport.getWorstStatus(reports);
+		VisitReport report = new VisitReport(RESTActivityHealthCheck.getInstance(), activity,
+				"REST Activity Report", HealthCheck.NO_PROBLEM, worstStatus, reports);
+
+		return report;
+	}
+
+	/**
+	 * Health check for the REST activity only involves
+	 * verifying details in the configuration bean -
+	 * that is quick.
+	 */
+	@Override
+	public boolean isTimeConsuming() {
+		return false;
+	}
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-taverna-common-activities/blob/390c286b/taverna-rest-activity/src/main/java/net/sf/taverna/t2/activities/rest/URISignatureHandler.java
----------------------------------------------------------------------
diff --git a/taverna-rest-activity/src/main/java/net/sf/taverna/t2/activities/rest/URISignatureHandler.java b/taverna-rest-activity/src/main/java/net/sf/taverna/t2/activities/rest/URISignatureHandler.java
new file mode 100644
index 0000000..6a669f6
--- /dev/null
+++ b/taverna-rest-activity/src/main/java/net/sf/taverna/t2/activities/rest/URISignatureHandler.java
@@ -0,0 +1,412 @@
+package net.sf.taverna.t2.activities.rest;
+
+import java.io.UnsupportedEncodingException;
+import java.text.Normalizer;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.log4j.Logger;
+
+
+/**
+ * This class deals with URI signatures - essentially, strings that represent
+ * some resource's URI with zero or more placeholders. URI signatures are known
+ * at workflow definition time and represent the pattern of the complete URIs
+ * that will be used at workflow run time.
+ * 
+ * An example of the URI signature is:
+ * http://sysmo-db.org/sops/{sop_id}/experimental_conditions
+ * /{cond_id}?condition_unit={unit}
+ * 
+ * Placeholders "{sop_id}", "{cond_id}" and "{unit}" will be replaced by the
+ * real values prior to using the URI for a real request.
+ * 
+ * This class is concerned with validation of URI signatures, extraction of
+ * placeholders and substituting placeholders to generate complete URIs.
+ * 
+ * @author Sergejs Aleksejevs
+ */
+public class URISignatureHandler {
+	
+	private static Logger logger = Logger.getLogger(URISignatureHandler.class);
+	
+
+	public static final char PLACEHOLDER_START_SYMBOL = '{';
+	public static final char PLACEHOLDER_END_SYMBOL = '}';
+
+	/**
+	 * Extracts placeholders of the given URI signature with their positions in
+	 * the signature in the order of their occurrence.
+	 * 
+	 * Extraction is done in a robust way with signature validity checks being
+	 * carried out simultaneously. This makes sure that even with no explicit
+	 * validation (see {@link URISignatureHandler#isValid(String)}) no
+	 * unexpected faults occur.
+	 * 
+	 * @param uriSignature
+	 *            The URI signature to process.
+	 * @return A map of placeholders as they are encountered (from start to end)
+	 *         in the URI signature and their start positions. Keys of the map
+	 *         are the "titles" of the placeholders without opening and closing
+	 *         placeholder symbols; values are the URI signature string indices,
+	 *         where the title of the corresponding placeholder starts in the
+	 *         string.
+	 */
+	public static LinkedHashMap<String, Integer> extractPlaceholdersWithPositions(
+			String uriSignature) {
+		// no signature - nothing to process
+		if (uriSignature == null || uriSignature.isEmpty())
+			throw new URISignatureParsingException(
+					"URI signature is null or empty - nothing to process.");
+
+		LinkedHashMap<String, Integer> foundPlaceholdersWithPositions = new LinkedHashMap<>();
+
+		int nestingLevel = 0;
+		int startSymbolIdx = -1;
+
+		// go through the signature character by character trying to extract
+		// placeholders
+		for (int i = 0; i < uriSignature.length(); i++) {
+			switch (uriSignature.charAt(i)) {
+			case PLACEHOLDER_START_SYMBOL:
+				if (++nestingLevel != 1)
+					throw new URISignatureParsingException(
+							"Malformed URL: at least two parameter placeholder opening\n" +
+									"symbols follow each other without being closed appropriately\n" +
+							"(possibly the signature contains nested placeholders).");
+				startSymbolIdx = i;
+				break;
+
+			case PLACEHOLDER_END_SYMBOL:
+				if (--nestingLevel < 0)
+					throw new URISignatureParsingException(
+							"Malformed URL: parameter placeholder closing symbol found before the opening one.");
+				if (nestingLevel == 0) {
+					// correctly opened and closed placeholder found; check if
+					// it is a "fresh" one
+					String placeholderCandidate = uriSignature.substring(
+							startSymbolIdx + 1, i);
+					if (!foundPlaceholdersWithPositions
+							.containsKey(placeholderCandidate)) {
+						foundPlaceholdersWithPositions.put(
+								placeholderCandidate, startSymbolIdx + 1);
+					} else {
+						throw new URISignatureParsingException(
+								"Malformed URL: duplicate parameter placeholder \""
+										+ placeholderCandidate + "\" found.");
+					}
+				}
+				break;
+
+			default:
+				continue;
+			}
+		}
+
+		// the final check - make sure that after traversing the string, we are
+		// not "inside" one of the placeholders (e.g. this could happen if a
+		// placeholder
+		// opening symbol was found, but the closing one never occurred after
+		// that)
+		if (nestingLevel > 0)
+			throw new URISignatureParsingException(
+					"Malformed URL: parameter placeholder opening symbol found,\n"
+							+ "but the closing one has not been encountered.");
+
+		return foundPlaceholdersWithPositions;
+	}
+
+	/**
+	 * Works identical to
+	 * {@link URISignatureHandler#extractPlaceholdersWithPositions(String)}
+	 * except for returning only the list of placeholder titles - without
+	 * positions.
+	 * 
+	 * @param uriSignature
+	 *            The URI signature to process.
+	 * @return List of the placeholder titles in the order of their occurrence
+	 *         in the provided URI signature.
+	 */
+	public static List<String> extractPlaceholders(String uriSignature) {
+		return new ArrayList<>(extractPlaceholdersWithPositions(uriSignature)
+				.keySet());
+	}
+
+	/**
+	 * This method performs explicit validation of the URI signature. If the
+	 * validation succeeds, the method terminates quietly; in case of any
+	 * identified problems a {@link URISignatureParsingException} is thrown.
+	 * 
+	 * @param uriSignature
+	 *            The URI signature to validate.
+	 * @throws URISignatureParsingException
+	 */
+	public static void validate(String uriSignature)
+			throws URISignatureParsingException {
+		// this method essentially needs to do exactly the same thing
+		// as the method to extract the placeholders with their corresponding
+		// positions; all necessary validation is already performed there -
+		// hence the trick is simply to call that method (discarding its
+		// output),
+		// while keeping track of any exceptions that may be generated by the
+		// called method;
+		//
+		// for this simply call the placeholder extraction method - any
+		// exceptions
+		// will be forwarded up the method call stack; in case of success, the
+		// method
+		// will terminate quietly
+		extractPlaceholdersWithPositions(uriSignature);
+	}
+	
+	 /**
+	  * Check if the URL string contains "unsafe" characters, i.e. characters 
+	  * that need URL-encoding.
+	  * From RFC 1738: "...Only alphanumerics [0-9a-zA-Z], the special 
+	  * characters "$-_.+!*'()," (not including the quotes) and reserved 
+	  * characters used for their reserved purposes may be 
+	  * used unencoded within a URL." 
+	  * Reserved characters are: ";/?:@&=" ..." and "%" used for escaping.
+	 */
+	public static void checkForUnsafeCharacters(String candidateURLSignature) throws URISignatureParsingException{
+		String allowedURLCharactersString = new String("abcdefghijklmnopqrstuvwxyz0123456789$-_.+!*'(),;/?:@&=%");
+		char[] allowedURLCharactersArray = allowedURLCharactersString.toCharArray();
+		List<Character> allowedURLCharactersList = new ArrayList<Character>();
+		    for (char value : allowedURLCharactersArray)
+		    	allowedURLCharactersList.add(new Character(value));
+
+		int index = 0;
+		String unsafeCharactersDetected = "";
+		while (index < candidateURLSignature.length()){
+			char character = candidateURLSignature.charAt(index);
+			if (character == '{') { // a start of a parameter
+				// This is a paramater name - ignore until we find the closing '}'
+				index++;
+				while(character != '}' && index < candidateURLSignature.length()){
+					character = candidateURLSignature.charAt(index);
+					index++;
+				}
+			} else if (!allowedURLCharactersList.contains(Character
+					.valueOf(Character.toLowerCase(character)))) {
+				// We found an unsafe character in the URL - add to the list of unsafe characters
+				unsafeCharactersDetected += "'" + character + "', ";
+				index++;
+			} else {
+				index++;
+			}
+		}
+		String message = "";
+		unsafeCharactersDetected = unsafeCharactersDetected.trim();
+		if (unsafeCharactersDetected.endsWith(",")){ // remove the last ","
+			unsafeCharactersDetected = unsafeCharactersDetected.substring(0, unsafeCharactersDetected.lastIndexOf(','));
+		}
+		if  (!unsafeCharactersDetected.equals("")){
+			message += "REST service's URL contains unsafe characters that need\nto be URL-encoded or the service will most probably fail:\n"+ unsafeCharactersDetected;	
+			throw new URISignatureParsingException(message);
+		}
+	}
+
+
+	/**
+	 * Tests whether the provided URI signature is valid or not.
+	 * 
+	 * @param uriSignature
+	 *            URI signature to check for validity.
+	 * @return <code>true</code> if the URI signature is valid;
+	 *         <code>false</code> otherwise.
+	 */
+	public static boolean isValid(String uriSignature) {
+		try {
+			// no exceptions are generated by validate(), the validation has
+			// succeeded
+			validate(uriSignature);
+			return (true);
+		} catch (URISignatureParsingException e) {
+			return false;
+		}
+	}
+
+	/**
+	 * Substitutes real values for all placeholders encountered in the URI
+	 * signature and produces a complete URI that can be used directly.
+	 * 
+	 * @param uriSignature
+	 *            The URI signature to use as a basis.
+	 * @param parameters
+	 *            Map of {name,value} pairs for all placeholders in the
+	 *            signature. These values will be used to replace the
+	 *            placeholders in the signature.
+	 * @param escapeParameters
+	 *            Whether to URL-escape paramaters before placing them in the
+	 *            final URL.
+	 * @return A complete URI with all placeholders replaced by the provided
+	 *         values.
+	 * @throws URISignatureParsingException
+	 *             Thrown if there is a problem with the provided URI signature
+	 *             (e.g. null, empty, ill-formed, etc).
+	 * @throws URIGenerationFromSignatureException
+	 *             Thrown if there is a problem with the provided parameter map
+	 *             (e.g. null, empty, not containing enough values for some of
+	 *             the placeholders found in <code>uriSignature</code>.
+	 */
+	public static String generateCompleteURI(String uriSignature,
+			Map<String, String> specifiedParameters, boolean escapeParameters)
+			throws URISignatureParsingException,
+			URIGenerationFromSignatureException {
+		StringBuilder completeURI = new StringBuilder(uriSignature);
+
+		// no need to make any checks on the uriSignature - it is
+		// already handled by extractPlaceholdersWithPositions() --
+		// if something goes wrong a runtime exception will be thrown
+		// during placeholder extraction
+		LinkedHashMap<String, Integer> placeholdersWithPositions = extractPlaceholdersWithPositions(uriSignature);
+
+		// check that the URI signature contains some placeholders
+		if (placeholdersWithPositions.keySet().size() > 0) {
+			Map<String, String> parameters;
+			// some work will actually have to be done to replace placeholders
+			// with real values;
+			// check that the parameter map contains some values
+			if (specifiedParameters == null || specifiedParameters.isEmpty()) {
+				parameters = Collections.emptyMap();
+			} else {
+				parameters = specifiedParameters;
+			}
+
+			// the 'placeholders' linked list is guaranteed to be in the order
+			// of occurrence of placeholders in the URI signature;
+			// this will allow to traverse the URI signature and replace the
+			// placeholders in the reverse order --
+			// this way it is possible to use the indices of placeholders that
+			// were already found during their extraction to
+			// improve performance
+			LinkedList<String> placeholders = new LinkedList<String>(
+					placeholdersWithPositions.keySet());
+			Collections.reverse(placeholders);
+			Iterator<String> placeholdersIterator = placeholders.iterator();
+
+			while (placeholdersIterator.hasNext()) {
+				String placeholder = placeholdersIterator.next();
+				int placeholderStartPos = placeholdersWithPositions
+						.get(placeholder) - 1;
+				int placeholderEndPos = placeholderStartPos
+						+ placeholder.length() + 2;
+				if (parameters.containsKey(placeholder)) {
+					if (escapeParameters) {
+						completeURI.replace(placeholderStartPos,
+								placeholderEndPos, urlEncodeQuery(parameters
+										.get(placeholder)));
+					} else {
+						completeURI.replace(placeholderStartPos,
+								placeholderEndPos, parameters.get(placeholder));
+					}
+				} else {
+					int qnPos = completeURI.lastIndexOf("?", placeholderStartPos);
+ 					int ampPos = completeURI.lastIndexOf("&", placeholderStartPos);
+ 					int slashPos = completeURI.lastIndexOf("/", placeholderStartPos);
+ 					int startParamPos = Math.max(qnPos, ampPos);
+					if (startParamPos > -1 && startParamPos > slashPos) {
+ 						// We found an optional parameter, so delete all of it
+ 						if (qnPos > ampPos) {
+ 							// It might be the first or only parameter so delete carefully
+ 							if (placeholderEndPos >= (completeURI.length() - 1)) {
+ 								// No parameters
+ 								completeURI.replace(startParamPos, placeholderEndPos, "");
+ 							} else {
+ 								// Remove the & from the following parameter, not the ? that starts
+ 								completeURI.replace(startParamPos + 1, placeholderEndPos + 1, "");
+ 							}
+						} else {
+ 							// Just delete the optional parameter in total
+							completeURI.replace(startParamPos, placeholderEndPos, "");
+ 						}
+ 					} else {
+ 						throw new URIGenerationFromSignatureException(
+ 								"Parameter map does not contain a key/value for \""
+ 										+ placeholder + "\" mandatory placeholder");
+ 					}
+				}
+			}
+		}
+		/*
+		 * else { NO PLACEHOLDERS, SO NOTHING TO REPLACE WITH REAL VALUES - JUST
+		 * RETURN THE ORIGINAL 'uriSignature' }
+		 */
+
+		return (completeURI.toString());
+	}
+
+	/**
+	 * Exceptions of this type may be thrown when errors occur during URI
+	 * signature parsing - these will often indicate the reason for failure
+	 * (e.g. missing URI signature, nested placeholders, ill-formed signature,
+	 * etc).
+	 * 
+	 * @author Sergejs Aleksejevs
+	 */
+	@SuppressWarnings("serial")
+	public static class URISignatureParsingException extends
+			IllegalArgumentException {
+		public URISignatureParsingException() {
+		}
+
+		public URISignatureParsingException(String message) {
+			super(message);
+		}
+	}
+
+	/**
+	 * Exceptions of this type may be thrown during generation of a complete URI
+	 * from the provided signature and parameter hash. These may occur because
+	 * of wrong parameters, etc.
+	 * 
+	 * @author Sergejs Aleksejevs
+	 */
+	@SuppressWarnings("serial")
+	public static class URIGenerationFromSignatureException extends
+			RuntimeException {
+		public URIGenerationFromSignatureException() {
+		}
+
+		public URIGenerationFromSignatureException(String message) {
+			super(message);
+		}
+	}
+
+	/**
+	 * Prepares the string to serve as a part of url query to the server.
+	 * 
+	 * @param query
+	 *            The string that needs URL encoding.
+	 * @return URL encoded string that can be inserted into the request URL.
+	 */
+	public static String urlEncodeQuery(String query) {
+		String ns = Normalizer.normalize(query, Normalizer.Form.NFC);
+		byte[] bb = null;
+		try {
+			bb = ns.getBytes("UTF-8");
+		} catch (UnsupportedEncodingException e) {
+			logger.error(e);
+			return query;
+		}
+		
+		StringBuffer sb = new StringBuffer();
+		
+		for (int i = 0; i < bb.length; i++) {
+		    int b = bb[i] & 0xff;
+		    if (!Character.isLetterOrDigit(b) || (b >= 0x80)) {
+		    	sb.append("%");
+		    	sb.append(Integer.toHexString(b).toUpperCase());
+			} else {
+		    	sb.append((char)b);
+		    }
+		}
+		return sb.toString();
+	}
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-taverna-common-activities/blob/390c286b/taverna-rest-activity/src/main/resources/META-INF/services/net.sf.taverna.t2.workbench.report.explainer.VisitExplainer
----------------------------------------------------------------------
diff --git a/taverna-rest-activity/src/main/resources/META-INF/services/net.sf.taverna.t2.workbench.report.explainer.VisitExplainer b/taverna-rest-activity/src/main/resources/META-INF/services/net.sf.taverna.t2.workbench.report.explainer.VisitExplainer
new file mode 100644
index 0000000..86b3ad6
--- /dev/null
+++ b/taverna-rest-activity/src/main/resources/META-INF/services/net.sf.taverna.t2.workbench.report.explainer.VisitExplainer
@@ -0,0 +1 @@
+net.sf.taverna.t2.activities.rest.RESTActivityHealthCheckVisitExplainer
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-taverna-common-activities/blob/390c286b/taverna-rest-activity/src/main/resources/META-INF/services/net.sf.taverna.t2.workflowmodel.health.HealthChecker
----------------------------------------------------------------------
diff --git a/taverna-rest-activity/src/main/resources/META-INF/services/net.sf.taverna.t2.workflowmodel.health.HealthChecker b/taverna-rest-activity/src/main/resources/META-INF/services/net.sf.taverna.t2.workflowmodel.health.HealthChecker
new file mode 100644
index 0000000..737de29
--- /dev/null
+++ b/taverna-rest-activity/src/main/resources/META-INF/services/net.sf.taverna.t2.workflowmodel.health.HealthChecker
@@ -0,0 +1 @@
+net.sf.taverna.t2.activities.rest.RESTActivityHealthChecker
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-taverna-common-activities/blob/390c286b/taverna-rest-activity/src/main/resources/META-INF/spring/rest-activity-context-osgi.xml
----------------------------------------------------------------------
diff --git a/taverna-rest-activity/src/main/resources/META-INF/spring/rest-activity-context-osgi.xml b/taverna-rest-activity/src/main/resources/META-INF/spring/rest-activity-context-osgi.xml
new file mode 100644
index 0000000..44fa5c4
--- /dev/null
+++ b/taverna-rest-activity/src/main/resources/META-INF/spring/rest-activity-context-osgi.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<beans:beans xmlns="http://www.springframework.org/schema/osgi" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+	xmlns:beans="http://www.springframework.org/schema/beans"
+	xsi:schemaLocation="http://www.springframework.org/schema/beans
+                      http://www.springframework.org/schema/beans/spring-beans.xsd
+                      http://www.springframework.org/schema/osgi
+                      http://www.springframework.org/schema/osgi/spring-osgi.xsd">
+
+	<service ref="RESTActivityHealthChecker" interface="net.sf.taverna.t2.workflowmodel.health.HealthChecker" />
+
+	<service ref="restActivityFactory" interface="net.sf.taverna.t2.workflowmodel.processor.activity.ActivityFactory" />
+
+	<reference id="credentialManager" interface="net.sf.taverna.t2.security.credentialmanager.CredentialManager" />
+	<reference id="edits" interface="net.sf.taverna.t2.workflowmodel.Edits" />
+
+</beans:beans>

http://git-wip-us.apache.org/repos/asf/incubator-taverna-common-activities/blob/390c286b/taverna-rest-activity/src/main/resources/META-INF/spring/rest-activity-context.xml
----------------------------------------------------------------------
diff --git a/taverna-rest-activity/src/main/resources/META-INF/spring/rest-activity-context.xml b/taverna-rest-activity/src/main/resources/META-INF/spring/rest-activity-context.xml
new file mode 100644
index 0000000..f1c900e
--- /dev/null
+++ b/taverna-rest-activity/src/main/resources/META-INF/spring/rest-activity-context.xml
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+	xsi:schemaLocation="http://www.springframework.org/schema/beans
+                      http://www.springframework.org/schema/beans/spring-beans.xsd">
+
+	<bean id="RESTActivityHealthChecker" class="net.sf.taverna.t2.activities.rest.RESTActivityHealthChecker" />
+
+	<bean id="restActivityFactory" class="net.sf.taverna.t2.activities.rest.RESTActivityFactory">
+		<property name="credentialManager" ref="credentialManager" />
+		<property name="edits" ref="edits" />
+	</bean>
+
+</beans>

http://git-wip-us.apache.org/repos/asf/incubator-taverna-common-activities/blob/390c286b/taverna-rest-activity/src/main/resources/schema.json
----------------------------------------------------------------------
diff --git a/taverna-rest-activity/src/main/resources/schema.json b/taverna-rest-activity/src/main/resources/schema.json
new file mode 100644
index 0000000..376b479
--- /dev/null
+++ b/taverna-rest-activity/src/main/resources/schema.json
@@ -0,0 +1,75 @@
+{
+    "$schema": "http://json-schema.org/draft-03/schema#",
+    "id": "http://ns.taverna.org.uk/2010/activity/rest.schema.json",
+    "title": "REST activity configuration",
+    "type": "object",
+    "properties": {
+        "@context": {
+            "description": "JSON-LD context for interpreting the configuration as RDF",
+            "required": true,
+            "enum": ["http://ns.taverna.org.uk/2010/activity/rest.context.json"]
+        },
+        "request": {
+            "type": "object",
+            "description": "The HTTP request the REST activity is to perform",
+            "required": true,
+            "properties": {
+                "httpMethod": {
+                    "description": "The HTTP method of the REST request",
+                    "default": "GET",
+                    "enum": ["GET", "POST", "PUT", "DELETE"]
+                },
+                "absoluteURITemplate": {
+                    "type": "string",
+                    "required": true,
+                    "description": "The URI template for this request.  The template is an URI with 0 or more {variables}, e.g. http://example.com/user/{username}?page={page} (RFC6570 Level 1). Matching activity input ports are created for each variable, so the variable name must be a valid Taverna input port name. "
+                },
+                "headers": {
+                    "description": "HTTP headers of the request. GET requests SHOULD include Accept. PUT and POST request SHOULD include Content-Type. The header Expect: 100-continue is recognized and supported by the REST activity for POST and PUT requests.",
+                    "type": "array",
+                    "items": {
+                        "type": "object",
+                        "properties": {
+                            "header": {
+                                "type": "string",
+                                "required": true,
+                                "description": "The HTTP header name, e.g. Accept or Content-Type"
+                            },
+                            "value": {
+                                "type": "string",
+                                "required": true,
+                                "description": "The value of the HTTP header, e.g. text/plain"
+                            }
+                        }
+                    }
+                }
+            }
+        },
+        "outgoingDataFormat": {
+            "description": "Format of the POSTed content",
+            "default": "String",
+            "enum": ["String", "Binary"]
+        },
+        "showRedirectionOutputPort": {
+            "type": "boolean",
+            "default": false,
+            "description": "If set, the output port 'redirection' is added, showing the URL of the requested body. This is particularly useful for following redirection. "
+        },
+        "showActualURLPort": {
+            "type": "boolean",
+            "default": false,
+            "description": "If true, the output port 'actualURL' is added to show the requested URL after expanding the template."
+        },
+        "showResponseHeadersPort": {
+            "type": "boolean",
+            "default": false,
+            "description": "If true, the output port 'responseHeaders' is added to show the response header from the HTTP server."
+        },
+        "escapeParameters": {
+            "type": "boolean",
+            "default": true,
+            "description": "If true (default), input port values are URI-escaped (RFC 2396) before expanding variables in the URI template."
+        }
+        
+    }
+}

http://git-wip-us.apache.org/repos/asf/incubator-taverna-common-activities/blob/390c286b/taverna-rest-activity/src/test/java/net/sf/taverna/t2/activities/rest/ApacheHttpClientUsageTest.java.bak
----------------------------------------------------------------------
diff --git a/taverna-rest-activity/src/test/java/net/sf/taverna/t2/activities/rest/ApacheHttpClientUsageTest.java.bak b/taverna-rest-activity/src/test/java/net/sf/taverna/t2/activities/rest/ApacheHttpClientUsageTest.java.bak
new file mode 100644
index 0000000..518fbc3
--- /dev/null
+++ b/taverna-rest-activity/src/test/java/net/sf/taverna/t2/activities/rest/ApacheHttpClientUsageTest.java.bak
@@ -0,0 +1,215 @@
+package net.sf.taverna.t2.activities.rest;
+
+import java.awt.BorderLayout;
+import java.awt.Container;
+import java.awt.Dimension;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.io.BufferedReader;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.UnsupportedEncodingException;
+
+import javax.swing.BorderFactory;
+import javax.swing.JButton;
+import javax.swing.JComboBox;
+import javax.swing.JFrame;
+import javax.swing.JPanel;
+import javax.swing.JScrollPane;
+import javax.swing.JTextArea;
+import javax.swing.JTextField;
+
+import org.apache.commons.codec.binary.Base64;
+import org.apache.http.HttpEntity;
+import org.apache.http.HttpHost;
+import org.apache.http.HttpResponse;
+import org.apache.http.client.HttpClient;
+import org.apache.http.client.methods.HttpDelete;
+import org.apache.http.client.methods.HttpGet;
+import org.apache.http.client.methods.HttpPost;
+import org.apache.http.client.methods.HttpPut;
+import org.apache.http.client.methods.HttpRequestBase;
+import org.apache.http.client.methods.HttpUriRequest;
+import org.apache.http.entity.StringEntity;
+import org.apache.http.impl.client.DefaultHttpClient;
+import org.apache.http.protocol.BasicHttpContext;
+import org.apache.http.protocol.ExecutionContext;
+import org.apache.http.protocol.HttpContext;
+
+/**
+ * 
+ * @author Sergejs Aleksejevs
+ */
+public class ApacheHttpClientUsageTest extends JFrame implements ActionListener
+{
+  private JComboBox cbHttpMethod;
+  private JTextField tfAddressBar;
+  private JButton bGo;
+  private JTextArea taResponse;
+  private JScrollPane spResponse;
+  
+  
+  /**
+   * Constructor is solely involved in the UI initialisation.
+   */
+  public ApacheHttpClientUsageTest()
+  {
+    Container contentPane = this.getContentPane();
+    contentPane.setLayout(new BorderLayout(5,5));
+    
+    bGo = new JButton("GO!");
+    bGo.addActionListener(this);
+    bGo.setDefaultCapable(true);
+    this.getRootPane().setDefaultButton(bGo);
+    
+    tfAddressBar = new JTextField(50);
+    tfAddressBar.setPreferredSize(new Dimension(0,bGo.getPreferredSize().height));
+    tfAddressBar.setText("http://test.biocatalogue.org/");
+    
+    JPanel jpAddressBar = new JPanel();
+    jpAddressBar.add(tfAddressBar);
+    jpAddressBar.add(bGo);
+    
+    cbHttpMethod = new JComboBox(RESTActivity.HTTP_METHOD.values());
+    cbHttpMethod.setBorder(BorderFactory.createCompoundBorder(
+        BorderFactory.createEmptyBorder(5, 5, 5, 5),
+        cbHttpMethod.getBorder()));
+    
+    JPanel jpAllNavigation = new JPanel(new BorderLayout());
+    jpAllNavigation.add(jpAddressBar, BorderLayout.NORTH);
+    jpAllNavigation.add(cbHttpMethod, BorderLayout.CENTER);
+    
+    contentPane.add(jpAllNavigation, BorderLayout.NORTH);
+    
+    taResponse = new JTextArea(20, 20);
+    taResponse.setEditable(true);
+    
+    spResponse = new JScrollPane(taResponse);
+    spResponse.setBorder(BorderFactory.createCompoundBorder(
+        BorderFactory.createEmptyBorder(0, 5, 5, 5),
+        BorderFactory.createEtchedBorder()));
+    contentPane.add(spResponse, BorderLayout.CENTER);
+    
+    this.pack();
+    this.setLocationRelativeTo(null); // center on screen
+  }
+  
+  
+  /**
+   * Click handler for the only button there is - the "GO!" button 
+   */
+  public void actionPerformed(ActionEvent e)
+  {
+    if (e.getSource().equals(bGo))
+    {
+      try { 
+        switch ((RESTActivity.HTTP_METHOD)cbHttpMethod.getSelectedItem()) {
+          case GET:    doGET(); break;
+          case POST:   doPOST(); break;
+          case PUT:    doPUT(); break;
+          case DELETE: doDELETE(); break;
+        }
+      }
+      catch (Exception ex) {
+        taResponse.setText(ex + "\n\n" + ex.getStackTrace());
+      }
+    }
+  }
+  
+  
+  private void doGET() {
+    HttpGet httpGet = new HttpGet(tfAddressBar.getText());
+    httpGet.addHeader("Accept", "application/xml");
+    performHTTPRequest(httpGet);
+  }
+  
+  
+  private void doPOST() throws UnsupportedEncodingException {
+//    // POST TO MYEXPERIMENT - basic auth
+//    HttpPost httpPost = new HttpPost("http://sandbox.myexperiment.org/comment.xml");
+//    httpPost.addHeader("Authorization", "Basic " + new String(Base64.encodeBase64(("LOGIN" + ":" + "PASSWORD").getBytes())));
+//    httpPost.addHeader("Accept", "application/xml");
+//    httpPost.addHeader("Content-Type", "application/xml");
+//    httpPost.setEntity(new StringEntity("<comment><subject resource=\"http://sandbox.myexperiment.org/files/226\"/><comment>1234567</comment></comment>"));
+//    performHTTPRequest(httpPost);
+    
+    // POST TO BIOCATALOGUE - no auth
+    HttpPost httpPost = new HttpPost(tfAddressBar.getText());
+    httpPost.addHeader("Accept", "application/xml");
+    httpPost.addHeader("Content-Type", "application/xml");
+    httpPost.setEntity(new StringEntity("<searchByData><searchType>input</searchType><limit>20</limit><data>test</data></searchByData>"));
+    performHTTPRequest(httpPost);
+  }
+  
+  
+  private void doPUT() throws UnsupportedEncodingException {
+    HttpPut httpPut = new HttpPut("http://sandbox.myexperiment.org/comment.xml?id=251");
+    httpPut.addHeader("Authorization", "Basic " + new String(Base64.encodeBase64(("LOGIN" + ":" + "PASSWORD").getBytes())));
+    httpPut.addHeader("Accept", "application/xml");
+    httpPut.addHeader("Content-Type", "application/xml");
+    httpPut.setEntity(new StringEntity("<comment><subject resource=\"http://sandbox.myexperiment.org/files/226\"/><comment>12345678</comment></comment>"));
+    performHTTPRequest(httpPut);
+  }
+  
+  
+  private void doDELETE() {
+    HttpDelete httpDelete = new HttpDelete("http://sandbox.myexperiment.org/comment.xml?id=251");
+    httpDelete.addHeader("Authorization", "Basic " + new String(Base64.encodeBase64(("LOGIN" + ":" + "PASSWORD").getBytes())));
+    httpDelete.addHeader("Accept", "application/xml");
+    performHTTPRequest(httpDelete);
+  }
+  
+  
+  private void performHTTPRequest(HttpRequestBase httpRequest) {
+    try {
+      StringBuilder responseStr = new StringBuilder();
+      // ---------------------------------------------
+      
+      HttpClient httpClient = new DefaultHttpClient();
+      HttpContext localContext = new BasicHttpContext();
+      
+      HttpResponse response = httpClient.execute(httpRequest, localContext);
+      // ---
+      // TRACK WHERE THE FINAL REDIRECT ENDS UP - target host + URI
+      HttpHost target = (HttpHost) localContext.getAttribute(ExecutionContext.HTTP_TARGET_HOST);
+      HttpUriRequest req = (HttpUriRequest) localContext.getAttribute(ExecutionContext.HTTP_REQUEST);
+
+      responseStr.append("Final request URI: " + req.getMethod() + " " + target + req.getURI() + "\n");
+//      System.out.println("Target host: " + target);
+//      System.out.println("Final request URI: " + req.getURI());
+//      System.out.println("Final request method: " + req.getMethod());
+      // ---
+      responseStr.append(response.getStatusLine() + "\n");
+      
+      HttpEntity entity = response.getEntity();
+      responseStr.append(entity.getContentType() + "\n\n");
+      
+      if (entity != null) {
+        InputStream in = entity.getContent();
+        BufferedReader reader = new BufferedReader(new InputStreamReader(in));
+        
+        String str;
+        while ((str = reader.readLine()) != null) {
+          responseStr.append(str + "\n");
+        }
+        
+        taResponse.setText(responseStr.toString());
+        taResponse.setCaretPosition(0);
+      }
+      
+      httpClient.getConnectionManager().shutdown();
+    }
+    catch (Exception ex) {
+      taResponse.setText(ex.getMessage() + "\n\n" + ex.getStackTrace());
+    }
+  }
+  
+  
+  public static void main(String[] args)
+  {
+    ApacheHttpClientUsageTest frame = new ApacheHttpClientUsageTest();
+    frame.setVisible(true);
+    frame.tfAddressBar.setCaretPosition(frame.tfAddressBar.getText().length());
+  }
+  
+}

http://git-wip-us.apache.org/repos/asf/incubator-taverna-common-activities/blob/390c286b/taverna-rest-activity/src/test/java/net/sf/taverna/t2/activities/rest/ExampleActivityTest.java.bak
----------------------------------------------------------------------
diff --git a/taverna-rest-activity/src/test/java/net/sf/taverna/t2/activities/rest/ExampleActivityTest.java.bak b/taverna-rest-activity/src/test/java/net/sf/taverna/t2/activities/rest/ExampleActivityTest.java.bak
new file mode 100644
index 0000000..56b6904
--- /dev/null
+++ b/taverna-rest-activity/src/test/java/net/sf/taverna/t2/activities/rest/ExampleActivityTest.java.bak
@@ -0,0 +1,153 @@
+package net.sf.taverna.t2.activities.rest;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import java.net.URI;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+import net.sf.taverna.t2.activities.testutils.ActivityInvoker;
+import net.sf.taverna.t2.reference.ErrorDocument;
+import net.sf.taverna.t2.reference.ExternalReferenceSPI;
+import net.sf.taverna.t2.workflowmodel.OutputPort;
+import net.sf.taverna.t2.workflowmodel.processor.activity.ActivityConfigurationException;
+import net.sf.taverna.t2.workflowmodel.processor.activity.ActivityInputPort;
+
+import org.junit.Before;
+import org.junit.Test;
+
+public class ExampleActivityTest {
+
+	private RESTActivityConfigurationBean configBean;
+
+	private RESTActivity activity = new RESTActivity();
+
+	@Before
+	public void makeConfigBean() throws Exception {
+		configBean = new RESTActivityConfigurationBean();
+		configBean.setExampleString("something");
+		configBean
+				.setExampleUri(URI.create("http://localhost:8080/myEndPoint"));
+	}
+
+	@Test(expected = ActivityConfigurationException.class)
+	public void invalidConfiguration() throws ActivityConfigurationException {
+		RESTActivityConfigurationBean invalidBean = new RESTActivityConfigurationBean();
+		invalidBean.setExampleString("invalidExample");
+		// Should throw ActivityConfigurationException
+		activity.configure(invalidBean);
+	}
+
+	@Test
+	public void executeAsynch() throws Exception {
+		activity.configure(configBean);
+
+		Map<String, Object> inputs = new HashMap<String, Object>();
+		inputs.put("firstInput", "hello");
+
+		Map<String, Class<?>> expectedOutputTypes = new HashMap<String, Class<?>>();
+		expectedOutputTypes.put("simpleOutput", String.class);
+		expectedOutputTypes.put("moreOutputs", ExternalReferenceSPI.class);
+
+		Map<String, Object> outputs = ActivityInvoker.invokeAsyncActivity(
+				activity, inputs, expectedOutputTypes);
+
+		assertEquals("Unexpected outputs", 2, outputs.size());
+		assertEquals("da39a3ee5e6b4b0d3255bfef95601890afd80709", outputs.get("simpleOutput"));
+		ErrorDocument errorDoc = (ErrorDocument) outputs.get("moreOutputs");
+		assertEquals("java.lang.Exception: There are no more values",
+		             errorDoc.getExceptionMessage());
+
+	}
+	
+	
+	
+	@Test
+  public void checksumOfExtraData() throws Exception {
+    configBean.setExampleString("specialCase");
+	  activity.configure(configBean);
+
+    Map<String, Object> inputs = new HashMap<String, Object>();
+    inputs.put("firstInput", "hello");
+    inputs.put("extraData", Arrays.asList("Test1".getBytes("utf8"),
+        "Test2".getBytes("utf8")));
+
+
+    Map<String, Class<?>> expectedOutputTypes = new HashMap<String, Class<?>>();
+    expectedOutputTypes.put("simpleOutput", String.class);
+    expectedOutputTypes.put("moreOutputs", ExternalReferenceSPI.class);
+    expectedOutputTypes.put("report", String.class);
+
+
+    Map<String, Object> outputs = ActivityInvoker.invokeAsyncActivity(
+        activity, inputs, expectedOutputTypes);
+
+    assertEquals("Unexpected outputs", 3, outputs.size());
+    assertEquals("35bceb434ff8e69fb89b829e461c921a28b423b3", outputs.get("simpleOutput"));
+    ErrorDocument errorDoc = (ErrorDocument) outputs.get("moreOutputs");
+    assertEquals("java.lang.Exception: There are no more values",
+                 errorDoc.getExceptionMessage());
+
+  }
+	
+	
+
+	@Test
+	public void reConfiguredActivity() throws Exception {
+		assertEquals("Unexpected inputs", 0, activity.getInputPorts().size());
+		assertEquals("Unexpected outputs", 0, activity.getOutputPorts().size());
+
+		activity.configure(configBean);
+		assertEquals("Unexpected inputs", 1, activity.getInputPorts().size());
+		assertEquals("Unexpected outputs", 2, activity.getOutputPorts().size());
+
+		activity.configure(configBean);
+		// Should not change on reconfigure
+		assertEquals("Unexpected inputs", 1, activity.getInputPorts().size());
+		assertEquals("Unexpected outputs", 2, activity.getOutputPorts().size());
+	}
+
+	@Test
+	public void reConfiguredSpecialPorts() throws Exception {
+		activity.configure(configBean);
+
+		RESTActivityConfigurationBean specialBean = new RESTActivityConfigurationBean();
+		specialBean.setExampleString("specialCase");
+		specialBean.setExampleUri(URI
+				.create("http://localhost:8080/myEndPoint"));
+		activity.configure(specialBean);		
+		// Should now have added the optional ports
+		assertEquals("Unexpected inputs", 2, activity.getInputPorts().size());
+		assertEquals("Unexpected outputs", 3, activity.getOutputPorts().size());
+	}
+
+	@Test
+	public void configureActivity() throws Exception {
+		Set<String> expectedInputs = new HashSet<String>();
+		expectedInputs.add("firstInput");
+
+		Set<String> expectedOutputs = new HashSet<String>();
+		expectedOutputs.add("simpleOutput");
+		expectedOutputs.add("moreOutputs");
+
+		activity.configure(configBean);
+
+		Set<ActivityInputPort> inputPorts = activity.getInputPorts();
+		assertEquals(expectedInputs.size(), inputPorts.size());
+		for (ActivityInputPort inputPort : inputPorts) {
+			assertTrue("Wrong input : " + inputPort.getName(), expectedInputs
+					.remove(inputPort.getName()));
+		}
+
+		Set<OutputPort> outputPorts = activity.getOutputPorts();
+		assertEquals(expectedOutputs.size(), outputPorts.size());
+		for (OutputPort outputPort : outputPorts) {
+			assertTrue("Wrong output : " + outputPort.getName(),
+					expectedOutputs.remove(outputPort.getName()));
+		}
+	}
+}