blob: 76625f896d8dedab9e0ee4b746cf5ff34fa69729 [file] [log] [blame]
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]);
}
});
}
}