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/17 12:37:00 UTC
[40/70] [abbrv] incubator-taverna-common-activities git commit:
taverna-rest-activity/
http://git-wip-us.apache.org/repos/asf/incubator-taverna-common-activities/blob/598ad6de/taverna-rest-activity/src/main/java/net/sf/taverna/t2/activities/rest/HTTPRequestHandler.java
----------------------------------------------------------------------
diff --git a/taverna-rest-activity/src/main/java/net/sf/taverna/t2/activities/rest/HTTPRequestHandler.java b/taverna-rest-activity/src/main/java/net/sf/taverna/t2/activities/rest/HTTPRequestHandler.java
new file mode 100644
index 0000000..6393550
--- /dev/null
+++ b/taverna-rest-activity/src/main/java/net/sf/taverna/t2/activities/rest/HTTPRequestHandler.java
@@ -0,0 +1,586 @@
+package net.sf.taverna.t2.activities.rest;
+
+import java.io.BufferedInputStream;
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.UnsupportedEncodingException;
+import java.net.MalformedURLException;
+import java.net.ProxySelector;
+import java.net.URL;
+import java.security.NoSuchAlgorithmException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+import javax.net.ssl.SSLContext;
+
+import net.sf.taverna.t2.activities.rest.RESTActivity.DATA_FORMAT;
+
+import org.apache.http.Header;
+import org.apache.http.HttpEntity;
+import org.apache.http.HttpHost;
+import org.apache.http.HttpResponse;
+import org.apache.http.auth.AuthScope;
+import org.apache.http.auth.UsernamePasswordCredentials;
+import org.apache.http.client.CredentialsProvider;
+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.conn.ClientConnectionManager;
+import org.apache.http.conn.scheme.Scheme;
+import org.apache.http.conn.scheme.SchemeRegistry;
+import org.apache.http.entity.ByteArrayEntity;
+import org.apache.http.entity.StringEntity;
+import org.apache.http.impl.client.DefaultHttpClient;
+import org.apache.http.impl.conn.ProxySelectorRoutePlanner;
+import org.apache.http.impl.conn.SingleClientConnManager;
+import org.apache.http.protocol.BasicHttpContext;
+import org.apache.http.protocol.ExecutionContext;
+import org.apache.http.protocol.HttpContext;
+import org.apache.log4j.Logger;
+
+/**
+ * This class deals with the actual remote REST service invocation. The main
+ * four HTTP methods (GET | POST | PUT | DELETE) are supported. <br/>
+ * <br/>
+ *
+ * Configuration for request execution is obtained from the related REST
+ * activity - encapsulated in a configuration bean.
+ *
+ * @author Sergejs Aleksejevs
+ * @author Alex Nenadic
+ */
+public class HTTPRequestHandler {
+ private static final int HTTPS_DEFAULT_PORT = 443;
+ private static final String CONTENT_TYPE_HEADER_NAME = "Content-Type";
+ private static final String ACCEPT_HEADER_NAME = "Accept";
+ private static Logger logger = Logger.getLogger(HTTPRequestHandler.class);
+
+ public static String PROXY_HOST = "http.proxyHost";
+ public static String PROXY_PORT = "http.proxyPort";
+ public static String PROXY_USERNAME = "http.proxyUser";
+ public static String PROXY_PASSWORD = "http.proxyPassword";
+
+ /**
+ * This method is the entry point to the invocation of a remote REST
+ * service. It accepts a number of parameters from the related REST activity
+ * and uses those to assemble, execute and fetch results of a relevant HTTP
+ * request.
+ *
+ * @param requestURL
+ * The URL for the request to be made. This cannot be taken from
+ * the <code>configBean</code>, because this should be the
+ * complete URL which may be directly used to make the request (
+ * <code>configBean</code> would only contain the URL signature
+ * associated with the REST activity).
+ * @param configBean
+ * Configuration of the associated REST activity is passed to
+ * this class as a configuration bean. Settings such as HTTP
+ * method, MIME types for "Content-Type" and "Accept" headers,
+ * etc are taken from the bean.
+ * @param inputMessageBody
+ * Body of the message to be sent to the server - only needed for
+ * POST and PUT requests; for GET and DELETE it will be
+ * discarded.
+ * @return
+ */
+ @SuppressWarnings("deprecation")
+ public static HTTPRequestResponse initiateHTTPRequest(String requestURL,
+ RESTActivityConfigurationBean configBean, Object inputMessageBody,
+ Map<String, String> urlParameters, CredentialsProvider credentialsProvider) {
+ ClientConnectionManager connectionManager = null;
+ if (requestURL.toLowerCase().startsWith("https")) {
+ // Register a protocol scheme for https that uses Taverna's
+ // SSLSocketFactory
+ try {
+ URL url = new URL(requestURL); // the URL object which will
+ // parse the port out for us
+ int port = url.getPort();
+ if (port == -1) // no port was defined in the URL
+ port = HTTPS_DEFAULT_PORT; // default HTTPS port
+ Scheme https = new Scheme("https", new org.apache.http.conn.ssl.SSLSocketFactory(
+ SSLContext.getDefault()), port);
+ SchemeRegistry schemeRegistry = new SchemeRegistry();
+ schemeRegistry.register(https);
+ connectionManager = new SingleClientConnManager(null,
+ schemeRegistry);
+ } catch (MalformedURLException ex) {
+ logger.error("Failed to extract port from the REST service URL: the URL "
+ + requestURL + " is malformed.", ex);
+ // This will cause the REST activity to fail but this method
+ // seems not to throw an exception so we'll just log the error
+ // and let it go through
+ } catch (NoSuchAlgorithmException ex2) {
+ // This will cause the REST activity to fail but this method
+ // seems not to throw an exception so we'll just log the error
+ // and let it go through
+ logger.error(
+ "Failed to create SSLContext for invoking the REST service over https.",
+ ex2);
+ }
+ }
+
+ switch (configBean.getHttpMethod()) {
+ case GET:
+ return doGET(connectionManager, requestURL, configBean, urlParameters, credentialsProvider);
+ case POST:
+ return doPOST(connectionManager, requestURL, configBean, inputMessageBody, urlParameters, credentialsProvider);
+ case PUT:
+ return doPUT(connectionManager, requestURL, configBean, inputMessageBody, urlParameters, credentialsProvider);
+ case DELETE:
+ return doDELETE(connectionManager, requestURL, configBean, urlParameters, credentialsProvider);
+ default:
+ return new HTTPRequestResponse(new Exception("Error: something went wrong; "
+ + "no failure has occurred, but but unexpected HTTP method (\""
+ + configBean.getHttpMethod() + "\") encountered."));
+ }
+ }
+
+ private static HTTPRequestResponse doGET(ClientConnectionManager connectionManager,
+ String requestURL, RESTActivityConfigurationBean configBean,
+ Map<String, String> urlParameters, CredentialsProvider credentialsProvider) {
+ HttpGet httpGet = new HttpGet(requestURL);
+ return performHTTPRequest(connectionManager, httpGet, configBean, urlParameters, credentialsProvider);
+ }
+
+ private static HTTPRequestResponse doPOST(ClientConnectionManager connectionManager,
+ String requestURL, RESTActivityConfigurationBean configBean, Object inputMessageBody,
+ Map<String, String> urlParameters, CredentialsProvider credentialsProvider) {
+ HttpPost httpPost = new HttpPost(requestURL);
+
+ // TODO - decide whether this is needed for PUT requests, too (or just
+ // here, for POST)
+ // check whether to send the HTTP Expect header or not
+ if (!configBean.getSendHTTPExpectRequestHeader())
+ httpPost.getParams().setBooleanParameter("http.protocol.expect-continue", false);
+
+ // If the user wants to set MIME type for the 'Content-Type' header
+ if (!configBean.getContentTypeForUpdates().isEmpty())
+ httpPost.setHeader(CONTENT_TYPE_HEADER_NAME, configBean.getContentTypeForUpdates());
+ try {
+ HttpEntity entity = null;
+ if (inputMessageBody == null) {
+ entity = new StringEntity("");
+ } else if (configBean.getOutgoingDataFormat() == DATA_FORMAT.String) {
+ entity = new StringEntity((String) inputMessageBody);
+ } else {
+ entity = new ByteArrayEntity((byte[]) inputMessageBody);
+ }
+ httpPost.setEntity(entity);
+ } catch (UnsupportedEncodingException e) {
+ return (new HTTPRequestResponse(new Exception("Error occurred while trying to "
+ + "attach a message body to the POST request. See attached cause of this "
+ + "exception for details.")));
+ }
+ return performHTTPRequest(connectionManager, httpPost, configBean, urlParameters, credentialsProvider);
+ }
+
+ private static HTTPRequestResponse doPUT(ClientConnectionManager connectionManager,
+ String requestURL, RESTActivityConfigurationBean configBean, Object inputMessageBody,
+ Map<String, String> urlParameters, CredentialsProvider credentialsProvider) {
+ HttpPut httpPut = new HttpPut(requestURL);
+ if (!configBean.getContentTypeForUpdates().isEmpty())
+ httpPut.setHeader(CONTENT_TYPE_HEADER_NAME, configBean.getContentTypeForUpdates());
+ try {
+ HttpEntity entity = null;
+ if (inputMessageBody == null) {
+ entity = new StringEntity("");
+ } else if (configBean.getOutgoingDataFormat() == DATA_FORMAT.String) {
+ entity = new StringEntity((String) inputMessageBody);
+ } else {
+ entity = new ByteArrayEntity((byte[]) inputMessageBody);
+ }
+ httpPut.setEntity(entity);
+ } catch (UnsupportedEncodingException e) {
+ return new HTTPRequestResponse(new Exception("Error occurred while trying to "
+ + "attach a message body to the PUT request. See attached cause of this "
+ + "exception for details."));
+ }
+ return performHTTPRequest(connectionManager, httpPut, configBean, urlParameters, credentialsProvider);
+ }
+
+ private static HTTPRequestResponse doDELETE(ClientConnectionManager connectionManager,
+ String requestURL, RESTActivityConfigurationBean configBean,
+ Map<String, String> urlParameters, CredentialsProvider credentialsProvider) {
+ HttpDelete httpDelete = new HttpDelete(requestURL);
+ return performHTTPRequest(connectionManager, httpDelete, configBean, urlParameters, credentialsProvider);
+ }
+
+ /**
+ * TODO - REDIRECTION output:: if there was no redirection, should just show
+ * the actual initial URL?
+ *
+ * @param httpRequest
+ * @param acceptHeaderValue
+ */
+ private static HTTPRequestResponse performHTTPRequest(
+ ClientConnectionManager connectionManager, HttpRequestBase httpRequest,
+ RESTActivityConfigurationBean configBean,
+ Map<String, String> urlParameters, CredentialsProvider credentialsProvider) {
+ // headers are set identically for all HTTP methods, therefore can do
+ // centrally - here
+
+ // If the user wants to set MIME type for the 'Accepts' header
+ String acceptsHeaderValue = configBean.getAcceptsHeaderValue();
+ if ((acceptsHeaderValue != null) && !acceptsHeaderValue.isEmpty()) {
+ httpRequest.setHeader(ACCEPT_HEADER_NAME,
+ URISignatureHandler.generateCompleteURI(acceptsHeaderValue, urlParameters, configBean.getEscapeParameters()));
+ }
+
+ // See if user wanted to set any other HTTP headers
+ ArrayList<ArrayList<String>> otherHTTPHeaders = configBean.getOtherHTTPHeaders();
+ if (!otherHTTPHeaders.isEmpty())
+ for (ArrayList<String> httpHeaderNameValuePair : otherHTTPHeaders)
+ if (httpHeaderNameValuePair.get(0) != null
+ && !httpHeaderNameValuePair.get(0).isEmpty()) {
+ String headerParameterizedValue = httpHeaderNameValuePair.get(1);
+ String headerValue = URISignatureHandler.generateCompleteURI(headerParameterizedValue, urlParameters, configBean.getEscapeParameters());
+ httpRequest.setHeader(httpHeaderNameValuePair.get(0), headerValue);
+ }
+
+ try {
+ HTTPRequestResponse requestResponse = new HTTPRequestResponse();
+ DefaultHttpClient httpClient = new DefaultHttpClient(connectionManager, null);
+ ((DefaultHttpClient) httpClient).setCredentialsProvider(credentialsProvider);
+ HttpContext localContext = new BasicHttpContext();
+
+ // Set the proxy settings, if any
+ if (System.getProperty(PROXY_HOST) != null
+ && !System.getProperty(PROXY_HOST).isEmpty()) {
+ // Instruct HttpClient to use the standard
+ // JRE proxy selector to obtain proxy information
+ ProxySelectorRoutePlanner routePlanner = new ProxySelectorRoutePlanner(httpClient
+ .getConnectionManager().getSchemeRegistry(), ProxySelector.getDefault());
+ httpClient.setRoutePlanner(routePlanner);
+ // Do we need to authenticate the user to the proxy?
+ if (System.getProperty(PROXY_USERNAME) != null
+ && !System.getProperty(PROXY_USERNAME).isEmpty())
+ // Add the proxy username and password to the list of
+ // credentials
+ httpClient.getCredentialsProvider().setCredentials(
+ new AuthScope(System.getProperty(PROXY_HOST), Integer.parseInt(System
+ .getProperty(PROXY_PORT))),
+ new UsernamePasswordCredentials(System.getProperty(PROXY_USERNAME),
+ System.getProperty(PROXY_PASSWORD)));
+ }
+
+ // execute the request
+ HttpResponse response = httpClient.execute(httpRequest, localContext);
+
+ // record response code
+ requestResponse.setStatusCode(response.getStatusLine().getStatusCode());
+ requestResponse.setReasonPhrase(response.getStatusLine().getReasonPhrase());
+
+ // record header values for Content-Type of the response
+ requestResponse.setResponseContentTypes(response.getHeaders(CONTENT_TYPE_HEADER_NAME));
+
+ // track where did the final redirect go to (if there was any)
+ HttpHost targetHost = (HttpHost) localContext
+ .getAttribute(ExecutionContext.HTTP_TARGET_HOST);
+ HttpUriRequest targetRequest = (HttpUriRequest) localContext
+ .getAttribute(ExecutionContext.HTTP_REQUEST);
+ requestResponse.setRedirectionURL("" + targetHost + targetRequest.getURI());
+ requestResponse.setRedirectionHTTPMethod(targetRequest.getMethod());
+ requestResponse.setHeaders(response.getAllHeaders());
+
+ /* read and store response body
+ (check there is some content - negative length of content means
+ unknown length;
+ zero definitely means no content...)*/
+ // TODO - make sure that this test is sufficient to determine if
+ // there is no response entity
+ if (response.getEntity() != null && response.getEntity().getContentLength() != 0)
+ requestResponse.setResponseBody(readResponseBody(response.getEntity()));
+
+ // release resources (e.g. connection pool, etc)
+ httpClient.getConnectionManager().shutdown();
+ return requestResponse;
+ } catch (Exception ex) {
+ return new HTTPRequestResponse(ex);
+ }
+ }
+
+ /**
+ * Dispatcher method that decides on the method of reading the server
+ * response data - either as a string or as binary data.
+ *
+ * @param entity
+ * @return
+ * @throws IOException
+ */
+ private static Object readResponseBody(HttpEntity entity) throws IOException {
+ if (entity == null)
+ return null;
+
+ /*
+ * test whether the data is binary or textual - for binary data will
+ * read just as it is, for textual data will attempt to perform charset
+ * conversion from the original one into UTF-8
+ */
+
+ if (entity.getContentType() == null)
+ // HTTP message contains a body but content type is null??? - we
+ // have seen services like this
+ return readFromInputStreamAsBinary(entity.getContent());
+
+ String contentType = entity.getContentType().getValue().toLowerCase();
+ if (contentType.startsWith("text") || contentType.contains("charset="))
+ // read as text
+ return readResponseBodyAsString(entity);
+ // read as binary - enough to pass the input stream, not the
+ // whole entity
+ return readFromInputStreamAsBinary(entity.getContent());
+ }
+
+ /**
+ * Worker method that extracts the content of the received HTTP message as a
+ * string. It also makes use of the charset that is specified in the
+ * Content-Type header of the received data to read it appropriately.
+ *
+ * @param entity
+ * @return
+ * @throws IOException
+ */
+ private static String readResponseBodyAsString(HttpEntity entity) throws IOException {
+ /*
+ * From RFC2616 http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html
+ * Content-Type = "Content-Type" ":" media-type, where media-type = type
+ * "/" subtype *( ";" parameter ) can have 0 or more parameters such as
+ * "charset", etc. Linear white space (LWS) MUST NOT be used between the
+ * type and subtype, nor between an attribute and its value. e.g.
+ * Content-Type: text/html; charset=ISO-8859-4
+ */
+
+ // get charset name
+ String charset = null;
+ String contentType = entity.getContentType().getValue().toLowerCase();
+
+ String[] contentTypeParts = contentType.split(";");
+ for (String contentTypePart : contentTypeParts) {
+ contentTypePart = contentTypePart.trim();
+ if (contentTypePart.startsWith("charset="))
+ charset = contentTypePart.substring("charset=".length());
+ }
+
+ // read the data line by line
+ StringBuilder responseBodyString = new StringBuilder();
+ try (BufferedReader reader = new BufferedReader(new InputStreamReader(
+ entity.getContent(), charset != null ? charset : "UTF-8"))) {
+
+ String str;
+ while ((str = reader.readLine()) != null)
+ responseBodyString.append(str + "\n");
+
+ return responseBodyString.toString();
+ }
+ }
+
+ /**
+ * Worker method that extracts the content of the input stream as binary
+ * data.
+ *
+ * @param inputStream
+ * @return
+ * @throws IOException
+ */
+ public static byte[] readFromInputStreamAsBinary(InputStream inputStream) throws IOException {
+ // use BufferedInputStream for better performance
+ try (BufferedInputStream in = new BufferedInputStream(inputStream)) {
+ // this list is to hold all fetched data
+ List<byte[]> data = new ArrayList<byte[]>();
+
+ // set up buffers for reading the data
+ int bufLength = 100 * 1024; // 100K
+ byte[] buf = new byte[bufLength];
+ byte[] currentPortionOfData = null;
+ int currentlyReadByteCount = 0;
+
+ // read the data portion by portion into a list
+ while ((currentlyReadByteCount = in.read(buf, 0, bufLength)) != -1) {
+ currentPortionOfData = new byte[currentlyReadByteCount];
+ System.arraycopy(buf, 0, currentPortionOfData, 0, currentlyReadByteCount);
+ data.add(currentPortionOfData);
+ }
+
+ // now check how much data was read and return that as a single byte
+ // array
+ if (data.size() == 1)
+ // just a single block of data - return it as it is
+ return data.get(0);
+
+ // there is more than one block of data -- calculate total
+ // length of data
+ bufLength = 0;
+ for (byte[] portionOfData : data)
+ bufLength += portionOfData.length;
+
+ // allocate a single large byte array that could contain all
+ // data
+ buf = new byte[bufLength];
+
+ // fill this byte array with data from all fragments
+ int lastFilledPositionInOutputArray = 0;
+ for (byte[] portionOfData : data) {
+ System.arraycopy(portionOfData, 0, buf,
+ lastFilledPositionInOutputArray, portionOfData.length);
+ lastFilledPositionInOutputArray += portionOfData.length;
+ }
+
+ return buf;
+ }
+ }
+
+ /**
+ * All fields have public accessor, but private mutators. This is because it
+ * should only be allowed to modify the HTTPRequestResponse partially inside
+ * the HTTPRequestHandler class only. For users of this class it will behave
+ * as immutable.
+ *
+ * @author Sergejs Aleksejevs
+ */
+ public static class HTTPRequestResponse {
+ private int statusCode;
+ private String reasonPhrase;
+ private String redirectionURL;
+ private String redirectionHTTPMethod;
+ private Header[] responseContentTypes;
+ private Object responseBody;
+
+ private Exception exception;
+ private Header[] allHeaders;
+
+ /**
+ * Private default constructor - will only be accessible from
+ * HTTPRequestHandler. Values for the entity will then be set using the
+ * private mutator methods.
+ */
+ private HTTPRequestResponse() {
+ /*
+ * do nothing here - values will need to be manually set later by
+ * using private mutator methods
+ */
+ }
+
+ public void setHeaders(Header[] allHeaders) {
+ this.allHeaders = allHeaders;
+ }
+
+ public Header[] getHeaders() {
+ return allHeaders;
+ }
+
+ public List<String> getHeadersAsStrings() {
+ List<String> headerStrings = new ArrayList<String>();
+ for (Header h : getHeaders()) {
+ headerStrings.add(h.toString());
+ }
+ return headerStrings;
+ }
+
+ /**
+ * Standard public constructor for a regular case, where all values are
+ * known and the request has succeeded.
+ *
+ * @param statusCode
+ * @param reasonPhrase
+ * @param redirection
+ * @param responseContentTypes
+ * @param responseBody
+ */
+ public HTTPRequestResponse(int statusCode, String reasonPhrase, String redirectionURL,
+ String redirectionHTTPMethod, Header[] responseContentTypes, String responseBody) {
+ this.statusCode = statusCode;
+ this.reasonPhrase = reasonPhrase;
+ this.redirectionURL = redirectionURL;
+ this.redirectionHTTPMethod = redirectionHTTPMethod;
+ this.responseContentTypes = responseContentTypes;
+ this.responseBody = responseBody;
+ }
+
+ /**
+ * Standard public constructor for an error case, where an error has
+ * occurred and request couldn't be executed because of an internal
+ * exception (rather than an error received from the remote server).
+ *
+ * @param exception
+ */
+ public HTTPRequestResponse(Exception exception) {
+ this.exception = exception;
+ }
+
+ private void setStatusCode(int statusCode) {
+ this.statusCode = statusCode;
+ }
+
+ public int getStatusCode() {
+ return statusCode;
+ }
+
+ public String getReasonPhrase() {
+ return reasonPhrase;
+ }
+
+ private void setReasonPhrase(String reasonPhrase) {
+ this.reasonPhrase = reasonPhrase;
+ }
+
+ public String getRedirectionURL() {
+ return redirectionURL;
+ }
+
+ private void setRedirectionURL(String redirectionURL) {
+ this.redirectionURL = redirectionURL;
+ }
+
+ public String getRedirectionHTTPMethod() {
+ return redirectionHTTPMethod;
+ }
+
+ private void setRedirectionHTTPMethod(String redirectionHTTPMethod) {
+ this.redirectionHTTPMethod = redirectionHTTPMethod;
+ }
+
+ public Header[] getResponseContentTypes() {
+ return responseContentTypes;
+ }
+
+ private void setResponseContentTypes(Header[] responseContentTypes) {
+ this.responseContentTypes = responseContentTypes;
+ }
+
+ public Object getResponseBody() {
+ return responseBody;
+ }
+
+ private void setResponseBody(Object outputBody) {
+ this.responseBody = outputBody;
+ }
+
+ /**
+ * @return <code>true</code> if an exception has occurred while the HTTP
+ * request was executed. (E.g. this doesn't indicate a server
+ * error - just that the request couldn't be successfully
+ * executed. It could have been a network timeout, etc).
+ */
+ public boolean hasException() {
+ return (this.exception != null);
+ }
+
+ public Exception getException() {
+ return exception;
+ }
+
+ /**
+ * @return <code>true</code> if HTTP code of server response is either
+ * 4xx or 5xx.
+ */
+ public boolean hasServerError() {
+ return (statusCode >= 400 && statusCode < 600);
+ }
+ }
+}
http://git-wip-us.apache.org/repos/asf/incubator-taverna-common-activities/blob/598ad6de/taverna-rest-activity/src/main/java/net/sf/taverna/t2/activities/rest/HTTPRequestHeader.java
----------------------------------------------------------------------
diff --git a/taverna-rest-activity/src/main/java/net/sf/taverna/t2/activities/rest/HTTPRequestHeader.java b/taverna-rest-activity/src/main/java/net/sf/taverna/t2/activities/rest/HTTPRequestHeader.java
new file mode 100644
index 0000000..5885870
--- /dev/null
+++ b/taverna-rest-activity/src/main/java/net/sf/taverna/t2/activities/rest/HTTPRequestHeader.java
@@ -0,0 +1,66 @@
+/*******************************************************************************
+ * 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 net.sf.taverna.t2.workflowmodel.processor.config.ConfigurationBean;
+import net.sf.taverna.t2.workflowmodel.processor.config.ConfigurationProperty;
+
+/**
+ * HTTP Request Header configuration bean
+ *
+ * @author David Withers
+ */
+@ConfigurationBean(uri = "http://www.w3.org/2011/http#RequestHeader")
+public class HTTPRequestHeader {
+
+ private String fieldName, fieldValue;
+
+ private boolean use100Continue;
+
+ public String getFieldName() {
+ return fieldName;
+ }
+
+ @ConfigurationProperty(name = "fieldName", label = "HTTP Header Name")
+ public void setFieldName(String fieldName) {
+ this.fieldName = fieldName;
+ }
+
+ public String getFieldValue() {
+ return fieldValue;
+ }
+
+ @ConfigurationProperty(name = "fieldValue", label = "HTTP Header Value")
+ public void setFieldValue(String fieldValue) {
+ this.fieldValue = fieldValue;
+ }
+
+ public boolean isUse100Continue() {
+ return use100Continue;
+ }
+
+ @ConfigurationProperty(name = "use100Continue", label = "Use 100 Continue", required = false, uri = RESTActivity.URI
+ + "#use100Continue")
+ public void setUse100Continue(boolean use100Continue) {
+ this.use100Continue = use100Continue;
+ }
+
+}
http://git-wip-us.apache.org/repos/asf/incubator-taverna-common-activities/blob/598ad6de/taverna-rest-activity/src/main/java/net/sf/taverna/t2/activities/rest/RESTActivity.java
----------------------------------------------------------------------
diff --git a/taverna-rest-activity/src/main/java/net/sf/taverna/t2/activities/rest/RESTActivity.java b/taverna-rest-activity/src/main/java/net/sf/taverna/t2/activities/rest/RESTActivity.java
new file mode 100644
index 0000000..76625f8
--- /dev/null
+++ b/taverna-rest-activity/src/main/java/net/sf/taverna/t2/activities/rest/RESTActivity.java
@@ -0,0 +1,345 @@
+package net.sf.taverna.t2.activities.rest;
+
+import java.io.UnsupportedEncodingException;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import net.sf.taverna.t2.activities.rest.HTTPRequestHandler.HTTPRequestResponse;
+import net.sf.taverna.t2.activities.rest.URISignatureHandler.URISignatureParsingException;
+import net.sf.taverna.t2.invocation.InvocationContext;
+import net.sf.taverna.t2.reference.ErrorDocument;
+import net.sf.taverna.t2.reference.ReferenceService;
+import net.sf.taverna.t2.reference.T2Reference;
+import net.sf.taverna.t2.workflowmodel.processor.activity.AbstractAsynchronousActivity;
+import net.sf.taverna.t2.workflowmodel.processor.activity.ActivityConfigurationException;
+import net.sf.taverna.t2.workflowmodel.processor.activity.AsynchronousActivityCallback;
+
+import org.apache.http.client.CredentialsProvider;
+import org.apache.log4j.Logger;
+
+import com.fasterxml.jackson.databind.JsonNode;
+
+/**
+ * Generic REST activity that is capable to perform all four HTTP methods.
+ *
+ * @author Sergejs Aleksejevs
+ */
+public class RESTActivity extends AbstractAsynchronousActivity<JsonNode> {
+
+ public static final String URI = "http://ns.taverna.org.uk/2010/activity/rest";
+
+ private static Logger logger = Logger.getLogger(RESTActivity.class);
+
+ // This generic activity can deal with any of the four HTTP methods
+ public static enum HTTP_METHOD {
+ GET, POST, PUT, DELETE
+ };
+
+ // Default choice of data format (especially, for outgoing data)
+ public static enum DATA_FORMAT {
+ String(String.class), Binary(byte[].class);
+
+ private final Class<?> dataFormat;
+
+ DATA_FORMAT(Class<?> dataFormat) {
+ this.dataFormat = dataFormat;
+ }
+
+ public Class<?> getDataFormat() {
+ return this.dataFormat;
+ }
+ };
+
+ // These ports are default ones; additional ports will be dynamically
+ // generated from the
+ // URI signature used to configure the activity
+ public static final String IN_BODY = "inputBody";
+ public static final String OUT_RESPONSE_BODY = "responseBody";
+ public static final String OUT_RESPONSE_HEADERS = "responseHeaders";
+ public static final String OUT_STATUS = "status";
+ public static final String OUT_REDIRECTION = "redirection";
+ public static final String OUT_COMPLETE_URL = "actualURL";
+
+ // Configuration bean for this activity - essentially defines a particular
+ // instance
+ // of the activity through the values of its parameters
+ private RESTActivityConfigurationBean configBean;
+ private JsonNode json;
+
+ private CredentialsProvider credentialsProvider;
+
+ public RESTActivity(CredentialsProvider credentialsProvider) {
+ this.credentialsProvider = credentialsProvider;
+ }
+
+ @Override
+ public JsonNode getConfiguration() {
+ return json;
+ }
+
+ public RESTActivityConfigurationBean getConfigurationBean() {
+ return configBean;
+ }
+
+ @Override
+ public void configure(JsonNode json) throws ActivityConfigurationException {
+ this.json = json;
+ configBean = new RESTActivityConfigurationBean(json);
+ // Check configBean is valid - mainly check the URI signature for being
+ // well-formed and
+ // other details being present and valid;
+ //
+ // NB! The URI signature will still be valid if there are no
+ // placeholders at all - in this
+ // case for GET and DELETE methods no input ports will be generated and
+ // a single input
+ // port for input message body will be created for POST / PUT methods.
+ if (!configBean.isValid()) {
+ throw new ActivityConfigurationException(
+ "Bad data in the REST activity configuration bean - "
+ + "possible causes are: missing or ill-formed URI signature, missing or invalid MIME types for the "
+ + "specified HTTP headers ('Accept' | 'Content-Type'). This should not have happened, as validation "
+ + "on the UI had to be performed prior to accepting this configuration.");
+ }
+
+ // (Re)create input/output ports depending on configuration
+ configurePorts();
+ }
+
+ protected void configurePorts() {
+ // all input ports are dynamic and depend on the configuration
+ // of the particular instance of the REST activity
+
+ // now process the URL signature - extract all placeholders and create
+ // an input data type for each
+ Map<String, Class<?>> activityInputs = new HashMap<>();
+ 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
+ activityInputs.put(placeholder, String.class);
+
+ // all inputs have now been configured - store the resulting set-up in
+ // the config bean;
+ // this configuration will be reused during the execution of activity,
+ // so that existing
+ // set-up could simply be referred to, rather than "re-calculated"
+ configBean.setActivityInputs(activityInputs);
+
+ // ---- 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;
+ addOutput(OUT_RESPONSE_BODY, 0);
+ addOutput(OUT_STATUS, 0);
+ if (configBean.getShowActualUrlPort())
+ addOutput(OUT_COMPLETE_URL, 0);
+ if (configBean.getShowResponseHeadersPort())
+ addOutput(OUT_RESPONSE_HEADERS, 1);
+
+ // Redirection port may be hidden/shown
+ if (configBean.getShowRedirectionOutputPort())
+ addOutput(OUT_REDIRECTION, 0);
+ }
+
+ /**
+ * Uses HTTP method value of the config bean of the current instance of
+ * RESTActivity.
+ *
+ * @see RESTActivity#hasMessageBodyInputPort(HTTP_METHOD)
+ */
+ public boolean hasMessageBodyInputPort() {
+ return hasMessageBodyInputPort(configBean.getHttpMethod());
+ }
+
+ /**
+ * Return value of this method has a number of implications - various input
+ * ports and configuration options for this activity are applied based on
+ * the selected HTTP method.
+ *
+ * @param httpMethod
+ * HTTP method to make the decision for.
+ * @return True if this instance of the REST activity uses HTTP POST / PUT
+ * methods; false otherwise.
+ */
+ public static boolean hasMessageBodyInputPort(HTTP_METHOD httpMethod) {
+ return httpMethod == HTTP_METHOD.POST || httpMethod == HTTP_METHOD.PUT;
+ }
+
+ /**
+ * This method executes pre-configured instance of REST activity. It
+ * resolves inputs of the activity and registers its outputs; the real
+ * invocation of the HTTP request is performed by
+ * {@link HTTPRequestHandler#initiateHTTPRequest(String, RESTActivityConfigurationBean, String)}
+ * .
+ */
+ @Override
+ public void executeAsynch(final Map<String, T2Reference> inputs,
+ final AsynchronousActivityCallback callback) {
+ // Don't execute service directly now, request to be run asynchronously
+ callback.requestRun(new Runnable() {
+ private Logger logger = Logger.getLogger(RESTActivity.class);
+
+ @Override
+ public void run() {
+
+ InvocationContext context = callback.getContext();
+ ReferenceService referenceService = context.getReferenceService();
+
+ // ---- RESOLVE INPUTS ----
+
+ // RE-ASSEMBLE REQUEST URL FROM SIGNATURE AND PARAMETERS
+ // (just use the configuration that was determined in
+ // configurePorts() - all ports in this set are required)
+ Map<String, String> urlParameters = new HashMap<>();
+ try {
+ for (String inputName : configBean.getActivityInputs().keySet())
+ urlParameters.put(inputName, (String) referenceService.renderIdentifier(
+ inputs.get(inputName), configBean.getActivityInputs()
+ .get(inputName), context));
+ } catch (Exception e) {
+ // problem occurred while resolving the inputs
+ callback.fail("REST activity was unable to resolve all necessary inputs"
+ + "that contain values for populating the URI signature placeholders "
+ + "with values.", e);
+
+ // make sure we don't call callback.receiveResult later
+ return;
+ }
+ String completeURL = URISignatureHandler.generateCompleteURI(
+ configBean.getUrlSignature(), urlParameters,
+ configBean.getEscapeParameters());
+
+ // OBTAIN THE INPUT BODY IF NECESSARY
+ // ("IN_BODY" is treated as *optional* for now)
+ Object inputMessageBody = null;
+ if (hasMessageBodyInputPort() && inputs.containsKey(IN_BODY)) {
+ inputMessageBody = referenceService.renderIdentifier(inputs.get(IN_BODY),
+ configBean.getOutgoingDataFormat().getDataFormat(), context);
+ }
+
+ // ---- DO THE ACTUAL SERVICE INVOCATION ----
+ HTTPRequestResponse requestResponse = HTTPRequestHandler.initiateHTTPRequest(
+ completeURL, configBean, inputMessageBody, urlParameters,
+ credentialsProvider);
+
+ // test if an internal failure has occurred
+ if (requestResponse.hasException()) {
+ callback.fail(
+ "Internal error has occurred while trying to execute the REST activity",
+ requestResponse.getException());
+
+ // make sure we don't call callback.receiveResult later
+ return;
+ }
+
+ // ---- REGISTER OUTPUTS ----
+ Map<String, T2Reference> outputs = new HashMap<String, T2Reference>();
+
+ T2Reference responseBodyRef = null;
+ if (requestResponse.hasServerError()) {
+ // test if a server error has occurred -- if so, return
+ // output as an error document
+
+ // Check if error returned is a string - sometimes services return byte[]
+ ErrorDocument errorDocument = null;
+ if (requestResponse.getResponseBody() == null) {
+ // No response body - register empty string
+ errorDocument = referenceService.getErrorDocumentService().registerError(
+ "", 0, context);
+ } else {
+ if (requestResponse.getResponseBody() instanceof String) {
+ errorDocument = referenceService.getErrorDocumentService()
+ .registerError((String) requestResponse.getResponseBody(), 0,
+ context);
+ } else if (requestResponse.getResponseBody() instanceof byte[]) {
+ // Do the only thing we can - try to convert to
+ // UTF-8 encoded string
+ // and hope we'll get back something intelligible
+ String str = null;
+ try {
+ str = new String((byte[]) requestResponse.getResponseBody(),
+ "UTF-8");
+ } catch (UnsupportedEncodingException e) {
+ logger.error(
+ "Failed to reconstruct the response body byte[]"
+ + " into string using UTF-8 encoding",
+ e);
+ // try with no encoding, probably will get garbage
+ str = new String((byte[]) requestResponse.getResponseBody());
+ }
+ errorDocument = referenceService.getErrorDocumentService()
+ .registerError(str, 0, context);
+ } else {
+ // Do what we can - call toString() method and hope
+ // for the best
+ errorDocument = referenceService.getErrorDocumentService()
+ .registerError(requestResponse.getResponseBody().toString(), 0,
+ context);
+ }
+ }
+ responseBodyRef = referenceService.register(errorDocument, 0, true, context);
+ } else if (requestResponse.getResponseBody() != null) {
+ // some response data is available
+ responseBodyRef = referenceService.register(requestResponse.getResponseBody(),
+ 0, true, context);
+ } else {
+ // no data was received in response to the request - must
+ // have been just a response header...
+ responseBodyRef = referenceService.register("", 0, true, context);
+ }
+ outputs.put(OUT_RESPONSE_BODY, responseBodyRef);
+
+ T2Reference statusRef = referenceService.register(requestResponse.getStatusCode(),
+ 0, true, context);
+ outputs.put(OUT_STATUS, statusRef);
+
+ if (configBean.getShowActualUrlPort()) {
+ T2Reference completeURLRef = referenceService.register(completeURL, 0, true,
+ context);
+ outputs.put(OUT_COMPLETE_URL, completeURLRef);
+ }
+ if (configBean.getShowResponseHeadersPort())
+ outputs.put(OUT_RESPONSE_HEADERS, referenceService.register(
+ requestResponse.getHeadersAsStrings(), 1, true, context));
+
+ // only put an output to the Redirection port if the processor
+ // is configured to display that port
+ if (configBean.getShowRedirectionOutputPort()) {
+ T2Reference redirectionRef = referenceService.register(
+ requestResponse.getRedirectionURL(), 0, true, context);
+ outputs.put(OUT_REDIRECTION, redirectionRef);
+ }
+
+ // return map of output data, with empty index array as this is
+ // the only and final result (this index parameter is used if
+ // pipelining output)
+ callback.receiveResult(outputs, new int[0]);
+ }
+ });
+ }
+}
http://git-wip-us.apache.org/repos/asf/incubator-taverna-common-activities/blob/598ad6de/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/598ad6de/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/598ad6de/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/598ad6de/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/598ad6de/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/598ad6de/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/598ad6de/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