You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@taverna.apache.org by st...@apache.org on 2015/02/23 11:17:03 UTC
[22/28] incubator-taverna-common-activities git commit: Revert
"temporarily empty repository"
http://git-wip-us.apache.org/repos/asf/incubator-taverna-common-activities/blob/390c286b/taverna-interaction-activity/src/main/resources/pmrpc.js
----------------------------------------------------------------------
diff --git a/taverna-interaction-activity/src/main/resources/pmrpc.js b/taverna-interaction-activity/src/main/resources/pmrpc.js
new file mode 100644
index 0000000..0edc8cf
--- /dev/null
+++ b/taverna-interaction-activity/src/main/resources/pmrpc.js
@@ -0,0 +1,686 @@
+/*
+ * pmrpc 0.6 - Inter-widget remote procedure call library based on HTML5
+ * postMessage API and JSON-RPC. https://github.com/izuzak/pmrpc
+ *
+ * Copyright 2011 Ivan Zuzak, Marko Ivankovic
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+pmrpc = self.pmrpc = function() {
+ // check if JSON library is available
+ if (typeof JSON === "undefined" || typeof JSON.stringify === "undefined" ||
+ typeof JSON.parse === "undefined") {
+ throw "pmrpc requires the JSON library";
+ }
+
+ // TODO: make "contextType" private variable
+ // check if postMessage APIs are available
+ if (typeof this.postMessage === "undefined" && // window or worker
+ typeof this.onconnect === "undefined") { // shared worker
+ throw "pmrpc requires the HTML5 cross-document messaging and worker APIs";
+ }
+
+ // Generates a version 4 UUID
+ function generateUUID() {
+ var uuid = [], nineteen = "89AB", hex = "0123456789ABCDEF";
+ for (var i=0; i<36; i++) {
+ uuid[i] = hex[Math.floor(Math.random() * 16)];
+ }
+ uuid[14] = '4';
+ uuid[19] = nineteen[Math.floor(Math.random() * 4)];
+ uuid[8] = uuid[13] = uuid[18] = uuid[23] = '-';
+ return uuid.join('');
+ }
+
+ // TODO: remove this - make everything a regex?
+ // Converts a wildcard expression into a regular expression
+ function convertWildcardToRegex(wildcardExpression) {
+ var regex = wildcardExpression.replace(
+ /([\^\$\.\+\?\=\!\:\|\\\/\(\)\[\]\{\}])/g, "\\$1");
+ regex = "^" + regex.replace(/\*/g, ".*") + "$";
+ return regex;
+ }
+
+ // Checks whether a domain satisfies the access control list. The access
+ // control list has a whitelist and a blacklist. In order to satisfy the acl,
+ // the domain must be on the whitelist, and must not be on the blacklist.
+ function checkACL(accessControlList, origin) {
+ var aclWhitelist = accessControlList.whitelist;
+ var aclBlacklist = accessControlList.blacklist;
+
+ var isWhitelisted = false;
+ var isBlacklisted = false;
+
+ for (var i=0; i<aclWhitelist.length; ++i) {
+ var aclRegex = convertWildcardToRegex(aclWhitelist[i]);
+ if(origin.match(aclRegex)) {
+ isWhitelisted = true;
+ break;
+ }
+ }
+
+ for (var j=0; i<aclBlacklist.length; ++j) {
+ var aclRegex = convertWildcardToRegex(aclBlacklist[j]);
+ if(origin.match(aclRegex)) {
+ isBlacklisted = true;
+ break;
+ }
+ }
+
+ return isWhitelisted && !isBlacklisted;
+ }
+
+ // Calls a function with either positional or named parameters
+ // In either case, additionalParams will be appended to the end
+ function invokeProcedure(fn, self, params, additionalParams) {
+ if (!(params instanceof Array)) {
+ // get string representation of function
+ var fnDef = fn.toString();
+
+ // parse the string representation and retrieve order of parameters
+ var argNames = fnDef.substring(fnDef.indexOf("(")+1, fnDef.indexOf(")"));
+ argNames = (argNames === "") ? [] : argNames.split(", ");
+
+ var argIndexes = {};
+ for (var i=0; i<argNames.length; i++) {
+ argIndexes[argNames[i]] = i;
+ }
+
+ // construct an array of arguments from a dictionary
+ var callParameters = [];
+ for (var paramName in params) {
+ if (typeof argIndexes[paramName] !== "undefined") {
+ callParameters[argIndexes[paramName]] = params[paramName];
+ } else {
+ throw "No such param!";
+ }
+ }
+
+ params = callParameters;
+ }
+
+ // append additional parameters
+ if (typeof additionalParams !== "undefined") {
+ params = params.concat(additionalParams);
+ }
+
+ // invoke function with specified context and arguments array
+ return fn.apply(self, params);
+ }
+
+ // JSON encode an object into pmrpc message
+ function encode(obj) {
+ return "pmrpc." + JSON.stringify(obj);
+ }
+
+ // JSON decode a pmrpc message
+ function decode(str) {
+ return JSON.parse(str.substring("pmrpc.".length));
+ }
+
+ // Creates a base JSON-RPC object, usable for both request and response.
+ // As of JSON-RPC 2.0 it only contains one field "jsonrpc" with value "2.0"
+ function createJSONRpcBaseObject() {
+ var call = {};
+ call.jsonrpc = "2.0";
+ return call;
+ }
+
+ // Creates a JSON-RPC request object for the given method and parameters
+ function createJSONRpcRequestObject(procedureName, parameters, id) {
+ var call = createJSONRpcBaseObject();
+ call.method = procedureName;
+ call.params = parameters;
+ if (typeof id !== "undefined") {
+ call.id = id;
+ }
+ return call;
+ }
+
+ // Creates a JSON-RPC error object complete with message and error code
+ function createJSONRpcErrorObject(errorcode, message, data) {
+ var error = {};
+ error.code = errorcode;
+ error.message = message;
+ error.data = data;
+ return error;
+ }
+
+ // Creates a JSON-RPC response object.
+ function createJSONRpcResponseObject(error, result, id) {
+ var response = createJSONRpcBaseObject();
+ response.id = id;
+
+ if (typeof error === "undefined" || error === null) {
+ response.result = (result === "undefined") ? null : result;
+ } else {
+ response.error = error;
+ }
+
+ return response;
+ }
+
+ // dictionary of services registered for remote calls
+ var registeredServices = {};
+ // dictionary of requests being processed on the client side
+ var callQueue = {};
+
+ var reservedProcedureNames = {};
+ // register a service available for remote calls
+ // if no acl is given, assume that it is available to everyone
+ function register(config) {
+ if (config.publicProcedureName in reservedProcedureNames) {
+ return false;
+ } else {
+ registeredServices[config.publicProcedureName] = {
+ "publicProcedureName" : config.publicProcedureName,
+ "procedure" : config.procedure,
+ "context" : config.procedure.context,
+ "isAsync" : typeof config.isAsynchronous !== "undefined" ?
+ config.isAsynchronous : false,
+ "acl" : typeof config.acl !== "undefined" ?
+ config.acl : {whitelist: ["*"], blacklist: []}};
+ return true;
+ }
+ }
+
+ // unregister a previously registered procedure
+ function unregister(publicProcedureName) {
+ if (publicProcedureName in reservedProcedureNames) {
+ return false;
+ } else {
+ delete registeredServices[publicProcedureName];
+ return true;
+ }
+ }
+
+ // retreive service for a specific procedure name
+ function fetchRegisteredService(publicProcedureName){
+ return registeredServices[publicProcedureName];
+ }
+
+ // receive and execute a pmrpc call which may be a request or a response
+ function processPmrpcMessage(eventParams) {
+ var serviceCallEvent = eventParams.event;
+ var eventSource = eventParams.source;
+ var isWorkerComm = typeof eventSource !== "undefined" && eventSource !== null;
+
+ // if the message is not for pmrpc, ignore it.
+ if (serviceCallEvent.data.indexOf("pmrpc.") !== 0) {
+ return;
+ } else {
+ var message = decode(serviceCallEvent.data);
+ //if (typeof console !== "undefined" && console.log !== "undefined" && (typeof this.frames !== "undefined")) { console.log("Received:" + encode(message)); }
+ if (typeof message.method !== "undefined") {
+ // this is a request
+
+ // ako je
+ var newServiceCallEvent = {
+ data : serviceCallEvent.data,
+ source : isWorkerComm ? eventSource : serviceCallEvent.source,
+ origin : isWorkerComm ? "*" : serviceCallEvent.origin,
+ shouldCheckACL : !isWorkerComm
+ };
+
+ response = processJSONRpcRequest(message, newServiceCallEvent);
+
+ // return the response
+ if (response !== null) {
+ sendPmrpcMessage(
+ newServiceCallEvent.source, response, newServiceCallEvent.origin);
+ }
+ } else {
+ // this is a response
+ processJSONRpcResponse(message);
+ }
+ }
+ }
+
+ // Process a single JSON-RPC Request
+ function processJSONRpcRequest(request, serviceCallEvent, shouldCheckACL) {
+ if (request.jsonrpc !== "2.0") {
+ // Invalid JSON-RPC request
+ return createJSONRpcResponseObject(
+ createJSONRpcErrorObject(-32600, "Invalid request.",
+ "The recived JSON is not a valid JSON-RPC 2.0 request."),
+ null,
+ null);
+ }
+
+ var id = request.id;
+ var service = fetchRegisteredService(request.method);
+
+ if (typeof service !== "undefined") {
+ // check the acl rights
+ if (!serviceCallEvent.shouldCheckACL ||
+ checkACL(service.acl, serviceCallEvent.origin)) {
+ try {
+ if (service.isAsync) {
+ // if the service is async, create a callback which the service
+ // must call in order to send a response back
+ var cb = function (returnValue) {
+ sendPmrpcMessage(
+ serviceCallEvent.source,
+ createJSONRpcResponseObject(null, returnValue, id),
+ serviceCallEvent.origin);
+ };
+ invokeProcedure(
+ service.procedure, service.context, request.params, [cb, serviceCallEvent]);
+ return null;
+ } else {
+ // if the service is not async, just call it and return the value
+ var returnValue = invokeProcedure(
+ service.procedure,
+ service.context,
+ request.params, [serviceCallEvent]);
+ return (typeof id === "undefined") ? null :
+ createJSONRpcResponseObject(null, returnValue, id);
+ }
+ } catch (error) {
+ if (typeof id === "undefined") {
+ // it was a notification nobody cares if it fails
+ return null;
+ }
+
+ if (error === "No such param!") {
+ return createJSONRpcResponseObject(
+ createJSONRpcErrorObject(
+ -32602, "Invalid params.", error.description),
+ null,
+ id);
+ }
+
+ // the -1 value is "application defined"
+ return createJSONRpcResponseObject(
+ createJSONRpcErrorObject(
+ -1, "Application error.", error.description),
+ null,
+ id);
+ }
+ } else {
+ // access denied
+ return (typeof id === "undefined") ? null : createJSONRpcResponseObject(
+ createJSONRpcErrorObject(
+ -2, "Application error.", "Access denied on server."),
+ null,
+ id);
+ }
+ } else {
+ // No such method
+ return (typeof id === "undefined") ? null : createJSONRpcResponseObject(
+ createJSONRpcErrorObject(
+ -32601,
+ "Method not found.",
+ "The requestd remote procedure does not exist or is not available."),
+ null,
+ id);
+ }
+ }
+
+ // internal rpc service that receives responses for rpc calls
+ function processJSONRpcResponse(response) {
+ var id = response.id;
+ var callObj = callQueue[id];
+ if (typeof callObj === "undefined" || callObj === null) {
+ return;
+ } else {
+ delete callQueue[id];
+ }
+
+ // check if the call was sucessful or not
+ if (typeof response.error === "undefined") {
+ callObj.onSuccess( {
+ "destination" : callObj.destination,
+ "publicProcedureName" : callObj.publicProcedureName,
+ "params" : callObj.params,
+ "status" : "success",
+ "returnValue" : response.result} );
+ } else {
+ callObj.onError( {
+ "destination" : callObj.destination,
+ "publicProcedureName" : callObj.publicProcedureName,
+ "params" : callObj.params,
+ "status" : "error",
+ "description" : response.error.message + " " + response.error.data} );
+ }
+ }
+
+ // call remote procedure
+ function call(config) {
+ // check that number of retries is not -1, that is a special internal value
+ if (config.retries && config.retries < 0) {
+ throw new Exception("number of retries must be 0 or higher");
+ }
+
+ var destContexts = [];
+
+ if (typeof config.destination === "undefined" || config.destination === null || config.destination === "workerParent") {
+ destContexts = [{context : null, type : "workerParent"}];
+ } else if (config.destination === "publish") {
+ destContexts = findAllReachableContexts();
+ } else if (config.destination instanceof Array) {
+ for (var i=0; i<config.destination.length; i++) {
+ if (config.destination[i] === "workerParent") {
+ destContexts.push({context : null, type : "workerParent"});
+ } else if (typeof config.destination[i].frames !== "undefined") {
+ destContexts.push({context : config.destination[i], type : "window"});
+ } else {
+ destContexts.push({context : config.destination[i], type : "worker"});
+ }
+ }
+ } else {
+ if (typeof config.destination.frames !== "undefined") {
+ destContexts.push({context : config.destination, type : "window"});
+ } else {
+ destContexts.push({context : config.destination, type : "worker"});
+ }
+ }
+
+ for (var i=0; i<destContexts.length; i++) {
+ var callObj = {
+ destination : destContexts[i].context,
+ destinationDomain : typeof config.destinationDomain === "undefined" ? ["*"] : (typeof config.destinationDomain === "string" ? [config.destinationDomain] : config.destinationDomain),
+ publicProcedureName : config.publicProcedureName,
+ onSuccess : typeof config.onSuccess !== "undefined" ?
+ config.onSuccess : function (){},
+ onError : typeof config.onError !== "undefined" ?
+ config.onError : function (){},
+ retries : typeof config.retries !== "undefined" ? config.retries : 5,
+ timeout : typeof config.timeout !== "undefined" ? config.timeout : 500,
+ status : "requestNotSent"
+ };
+
+ isNotification = typeof config.onError === "undefined" && typeof config.onSuccess === "undefined";
+ params = (typeof config.params !== "undefined") ? config.params : [];
+ callId = generateUUID();
+ callQueue[callId] = callObj;
+
+ if (isNotification) {
+ callObj.message = createJSONRpcRequestObject(
+ config.publicProcedureName, params);
+ } else {
+ callObj.message = createJSONRpcRequestObject(
+ config.publicProcedureName, params, callId);
+ }
+
+ waitAndSendRequest(callId);
+ }
+ }
+
+ // Use the postMessage API to send a pmrpc message to a destination
+ function sendPmrpcMessage(destination, message, acl) {
+ //if (typeof console !== "undefined" && console.log !== "undefined" && (typeof this.frames !== "undefined")) { console.log("Sending:" + encode(message)); }
+ if (typeof destination === "undefined" || destination === null) {
+ self.postMessage(encode(message));
+ } else if (typeof destination.frames !== "undefined") {
+ return destination.postMessage(encode(message), acl);
+ } else {
+ destination.postMessage(encode(message));
+ }
+ }
+
+ // Execute a remote call by first pinging the destination and afterwards
+ // sending the request
+ function waitAndSendRequest(callId) {
+ var callObj = callQueue[callId];
+ if (typeof callObj === "undefined") {
+ return;
+ } else if (callObj.retries <= -1) {
+ processJSONRpcResponse(
+ createJSONRpcResponseObject(
+ createJSONRpcErrorObject(
+ -4, "Application error.", "Destination unavailable."),
+ null,
+ callId));
+ } else if (callObj.status === "requestSent") {
+ return;
+ } else if (callObj.retries === 0 || callObj.status === "available") {
+ callObj.status = "requestSent";
+ callObj.retries = -1;
+ callQueue[callId] = callObj;
+ for (var i=0; i<callObj.destinationDomain.length; i++) {
+ sendPmrpcMessage(
+ callObj.destination, callObj.message, callObj.destinationDomain[i], callObj);
+ self.setTimeout(function() { waitAndSendRequest(callId); }, callObj.timeout);
+ }
+ } else {
+ // if we can ping some more - send a new ping request
+ callObj.status = "pinging";
+ callObj.retries = callObj.retries - 1;
+
+ call({
+ "destination" : callObj.destination,
+ "publicProcedureName" : "receivePingRequest",
+ "onSuccess" : function (callResult) {
+ if (callResult.returnValue === true &&
+ typeof callQueue[callId] !== 'undefined') {
+ callQueue[callId].status = "available";
+ waitAndSendRequest(callId);
+ }
+ },
+ "params" : [callObj.publicProcedureName],
+ "retries" : 0,
+ "destinationDomain" : callObj.destinationDomain});
+ callQueue[callId] = callObj;
+ self.setTimeout(function() { waitAndSendRequest(callId); }, callObj.timeout / callObj.retries);
+ }
+ }
+
+ // attach the pmrpc event listener
+ function addCrossBrowserEventListerner(obj, eventName, handler, bubble) {
+ if ("addEventListener" in obj) {
+ // FF
+ obj.addEventListener(eventName, handler, bubble);
+ } else {
+ // IE
+ obj.attachEvent("on" + eventName, handler);
+ }
+ }
+
+ function createHandler(method, source, destinationType) {
+ return function(event) {
+ var params = {event : event, source : source, destinationType : destinationType};
+ method(params);
+ };
+ }
+
+ if ('window' in this) {
+ // window object - window-to-window comm
+ var handler = createHandler(processPmrpcMessage, null, "window");
+ addCrossBrowserEventListerner(this, "message", handler, false);
+ } else if ('onmessage' in this) {
+ // dedicated worker - parent X to worker comm
+ var handler = createHandler(processPmrpcMessage, this, "worker");
+ addCrossBrowserEventListerner(this, "message", handler, false);
+ } else if ('onconnect' in this) {
+ // shared worker - parent X to shared-worker comm
+ var connectHandler = function(e) {
+ //this.sendPort = e.ports[0];
+ var handler = createHandler(processPmrpcMessage, e.ports[0], "sharedWorker");
+ addCrossBrowserEventListerner(e.ports[0], "message", handler, false);
+ e.ports[0].start();
+ };
+ addCrossBrowserEventListerner(this, "connect", connectHandler, false);
+ } else {
+ throw "Pmrpc must be loaded within a browser window or web worker.";
+ }
+
+ // Override Worker and SharedWorker constructors so that pmrpc may relay
+ // messages. For each message received from the worker, call pmrpc processing
+ // method. This is child worker to parent communication.
+
+ var createDedicatedWorker = this.Worker;
+ this.nonPmrpcWorker = createDedicatedWorker;
+ var createSharedWorker = this.SharedWorker;
+ this.nonPmrpcSharedWorker = createSharedWorker;
+
+ var allWorkers = [];
+
+ this.Worker = function(scriptUri) {
+ var newWorker = new createDedicatedWorker(scriptUri);
+ allWorkers.push({context : newWorker, type : 'worker'});
+ var handler = createHandler(processPmrpcMessage, newWorker, "worker");
+ addCrossBrowserEventListerner(newWorker, "message", handler, false);
+ return newWorker;
+ };
+
+ this.SharedWorker = function(scriptUri, workerName) {
+ var newWorker = new createSharedWorker(scriptUri, workerName);
+ allWorkers.push({context : newWorker, type : 'sharedWorker'});
+ var handler = createHandler(processPmrpcMessage, newWorker.port, "sharedWorker");
+ addCrossBrowserEventListerner(newWorker.port, "message", handler, false);
+ newWorker.postMessage = function (msg, portArray) {
+ return newWorker.port.postMessage(msg, portArray);
+ };
+ newWorker.port.start();
+ return newWorker;
+ };
+
+ // function that receives pings for methods and returns responses
+ function receivePingRequest(publicProcedureName) {
+ return typeof fetchRegisteredService(publicProcedureName) !== "undefined";
+ }
+
+ function subscribe(params) {
+ return register(params);
+ }
+
+ function unsubscribe(params) {
+ return unregister(params);
+ }
+
+ function findAllWindows() {
+ var allWindowContexts = [];
+
+ if (typeof window !== 'undefined') {
+ allWindowContexts.push( { context : window.top, type : 'window' } );
+
+ // walk through all iframes, starting with window.top
+ for (var i=0; typeof allWindowContexts[i] !== 'undefined'; i++) {
+ var currentWindow = allWindowContexts[i];
+ for (var j=0; j<currentWindow.context.frames.length; j++) {
+ allWindowContexts.push({
+ context : currentWindow.context.frames[j],
+ type : 'window'
+ });
+ }
+ }
+ } else {
+ allWindowContexts.push( {context : this, type : 'workerParent'} );
+ }
+
+ return allWindowContexts;
+ }
+
+ function findAllWorkers() {
+ return allWorkers;
+ }
+
+ function findAllReachableContexts() {
+ var allWindows = findAllWindows();
+ var allWorkers = findAllWorkers();
+ var allContexts = allWindows.concat(allWorkers);
+
+ return allContexts;
+ }
+
+ // register method for receiving and returning pings
+ register({
+ "publicProcedureName" : "receivePingRequest",
+ "procedure" : receivePingRequest});
+
+ function getRegisteredProcedures() {
+ var regSvcs = [];
+ var origin = typeof this.frames !== "undefined" ? (window.location.protocol + "//" + window.location.host + (window.location.port !== "" ? ":" + window.location.port : "")) : "";
+ for (publicProcedureName in registeredServices) {
+ if (publicProcedureName in reservedProcedureNames) {
+ continue;
+ } else {
+ regSvcs.push( {
+ "publicProcedureName" : registeredServices[publicProcedureName].publicProcedureName,
+ "acl" : registeredServices[publicProcedureName].acl,
+ "origin" : origin
+ } );
+ }
+ }
+ return regSvcs;
+ }
+
+ // register method for returning registered procedures
+ register({
+ "publicProcedureName" : "getRegisteredProcedures",
+ "procedure" : getRegisteredProcedures});
+
+ function discover(params) {
+ var windowsForDiscovery = null;
+
+ if (typeof params.destination === "undefined") {
+ windowsForDiscovery = findAllReachableContexts();
+ for (var i=0; i<windowsForDiscovery.length; i++) {
+ windowsForDiscovery[i] = windowsForDiscovery[i].context;
+ }
+ } else {
+ windowsForDiscovery = params.destination;
+ }
+ var originRegex = typeof params.origin === "undefined" ?
+ ".*" : params.origin;
+ var nameRegex = typeof params.publicProcedureName === "undefined" ?
+ ".*" : params.publicProcedureName;
+
+ var counter = windowsForDiscovery.length;
+
+ var discoveredMethods = [];
+ function addToDiscoveredMethods(methods, destination) {
+ for (var i=0; i<methods.length; i++) {
+ if (methods[i].origin.match(originRegex) && methods[i].publicProcedureName.match(nameRegex)) {
+ discoveredMethods.push({
+ publicProcedureName : methods[i].publicProcedureName,
+ destination : destination,
+ procedureACL : methods[i].acl,
+ destinationOrigin : methods[i].origin
+ });
+ }
+ }
+ }
+
+ pmrpc.call({
+ destination : windowsForDiscovery,
+ destinationDomain : "*",
+ publicProcedureName : "getRegisteredProcedures",
+ onSuccess : function (callResult) {
+ counter--;
+ addToDiscoveredMethods(callResult.returnValue, callResult.destination);
+ if (counter === 0) {
+ params.callback(discoveredMethods);
+ }
+ },
+ onError : function (callResult) {
+ counter--;
+ if (counter === 0) {
+ params.callback(discoveredMethods);
+ }
+ }
+ });
+ }
+
+ reservedProcedureNames = {"getRegisteredProcedures" : null, "receivePingRequest" : null};
+
+ // return public methods
+ return {
+ register : register,
+ unregister : unregister,
+ call : call,
+ discover : discover
+ };
+}();
http://git-wip-us.apache.org/repos/asf/incubator-taverna-common-activities/blob/390c286b/taverna-interaction-activity/src/main/resources/schema.json
----------------------------------------------------------------------
diff --git a/taverna-interaction-activity/src/main/resources/schema.json b/taverna-interaction-activity/src/main/resources/schema.json
new file mode 100644
index 0000000..dcde96e
--- /dev/null
+++ b/taverna-interaction-activity/src/main/resources/schema.json
@@ -0,0 +1,31 @@
+{
+ "$schema": "http://json-schema.org/draft-03/schema#",
+ "id": "http://ns.taverna.org.uk/2010/activity/interaction.schema.json",
+ "title": Interaction activity configuration",
+ "type": "object",
+ "properties": {
+ "@context": {
+ "description": "JSON-LD context for interpreting the configuration as RDF",
+ "required": true,
+ "enum": ["http://ns.taverna.org.uk/2010/activity/interaction.context.json"]
+ },
+ "presentationOrigin": {
+ "type": "string",
+ "required": true,
+ "minLength": 1,
+ "description": "The URL of the presentation page, or the identifier of the standard template"
+ },
+ "interactionActivityType": {
+ "type": "string",
+ "required": true,
+ "minLength": 1,
+ "enum" : [ "VelocityTemplate", "LocallyPresentedHtml"],
+ "description": "Indication of the type of the definition for the interaction"
+ },
+ "progressNotification": {
+ "type": "boolean",
+ "required": true,
+ "description": "True if the interaction should not block the workflow run"
+ }
+ }
+}
\ No newline at end of file
http://git-wip-us.apache.org/repos/asf/incubator-taverna-common-activities/blob/390c286b/taverna-interaction-activity/src/main/resources/select.vm
----------------------------------------------------------------------
diff --git a/taverna-interaction-activity/src/main/resources/select.vm b/taverna-interaction-activity/src/main/resources/select.vm
new file mode 100644
index 0000000..6fe5764
--- /dev/null
+++ b/taverna-interaction-activity/src/main/resources/select.vm
@@ -0,0 +1,61 @@
+#require("valueList",1)
+#require("message")
+#require("title")
+#produce("answer")
+<!doctype html>
+<html>
+ <head>
+ <meta charset="utf-8" />
+ <title></title>
+ <style>
+ </style>
+ </head>
+ <body>
+
+ <script type="text/javascript" src="$pmrpcUrl"></script>
+
+ <script type="text/javascript">
+
+ function reply() {
+ pmrpc.call({
+ destination : "publish",
+ publicProcedureName : "reply",
+ params : ["OK", {"answer" : document.myform.mySelect.options[document.myform.mySelect.selectedIndex].value}],
+ onSuccess : function() {document.getElementsByTagName('body')[0].innerHTML='<h1>Data submitted</h1>';},
+ onFailure: function() {document.getElementsByTagName('body')[0].innerHTML='<h1>Data submission failed</h1>';}
+ });
+ return true;
+ }
+
+ function cancel() {
+ pmrpc.call({
+ destination : "publish",
+ publicProcedureName : "reply",
+ params : ["Cancelled", {}],
+ onSuccess : function() {document.getElementsByTagName('body')[0].innerHTML='<h1>Cancelled</h1>';},
+ onFailure: function() {document.getElementsByTagName('body')[0].innerHTML='<h1>Cancellation failed</h1>';}
+ });
+ return true;
+ }
+
+ pmrpc.call({
+ destination : "publish",
+ publicProcedureName : "setTitle",
+ params : ["$!title"]});
+
+ </script>
+
+ <h2>$!message</h2>
+ <form name="myform" onSubmit="reply(); return false;">
+ <select name="mySelect">
+#foreach( $value in $valueList )
+ <option value="$value">$value</option>
+#end
+ </select>
+ <br/>
+ <input type="button" value="Cancel" onClick = "cancel()"/>
+ <input type="button" value="Submit" onClick = "reply()"/>
+ </form>
+ </body>
+</html>
+
http://git-wip-us.apache.org/repos/asf/incubator-taverna-common-activities/blob/390c286b/taverna-interaction-activity/src/main/resources/tell.vm
----------------------------------------------------------------------
diff --git a/taverna-interaction-activity/src/main/resources/tell.vm b/taverna-interaction-activity/src/main/resources/tell.vm
new file mode 100644
index 0000000..948c023
--- /dev/null
+++ b/taverna-interaction-activity/src/main/resources/tell.vm
@@ -0,0 +1,54 @@
+#require("message")
+#require("title")
+#produce("answer")
+<!doctype html>
+<html>
+ <head>
+ <meta charset="utf-8" />
+ <title></title>
+ <style>
+ </style>
+ </head>
+ <body>
+
+ <script type="text/javascript" src="$pmrpcUrl"></script>
+
+ <script type="text/javascript">
+
+ function reply() {
+ pmrpc.call({
+ destination : "publish",
+ publicProcedureName : "reply",
+ params : ["OK", {"answer" : "answer"}],
+ onSuccess : function() {document.getElementsByTagName('body')[0].innerHTML='<h1>Data submitted</h1>';},
+ onFailure: function() {document.getElementsByTagName('body')[0].innerHTML='<h1>Data submission failed</h1>';}
+ });
+ return true;
+ }
+
+ function cancel() {
+ pmrpc.call({
+ destination : "publish",
+ publicProcedureName : "reply",
+ params : ["Cancelled", {}],
+ onSuccess : function() {document.getElementsByTagName('body')[0].innerHTML='<h1>Cancelled</h1>';},
+ onFailure: function() {document.getElementsByTagName('body')[0].innerHTML='<h1>Cancellation failed</h1>';}
+ });
+ return true;
+ }
+
+ pmrpc.call({
+ destination : "publish",
+ publicProcedureName : "setTitle",
+ params : ["$!title"]});
+
+ </script>
+
+ <h2>Message: $!message</h2>
+ <form name="myform" onSubmit="reply(); return false;">
+ <input type="button" value="Cancel" onClick = "cancel()"/>
+ <input type="button" value="OK" onClick = "reply()"/>
+ </form>
+ </body>
+</html>
+
http://git-wip-us.apache.org/repos/asf/incubator-taverna-common-activities/blob/390c286b/taverna-interaction-activity/src/main/resources/warn.vm
----------------------------------------------------------------------
diff --git a/taverna-interaction-activity/src/main/resources/warn.vm b/taverna-interaction-activity/src/main/resources/warn.vm
new file mode 100644
index 0000000..ae9f573
--- /dev/null
+++ b/taverna-interaction-activity/src/main/resources/warn.vm
@@ -0,0 +1,54 @@
+#require("message")
+#require("title")
+#produce("answer")
+<!doctype html>
+<html>
+ <head>
+ <meta charset="UTF-8" />
+ <title></title>
+ <style>
+ </style>
+ </head>
+ <body>
+
+ <script type="text/javascript" src="$pmrpcUrl"></script>
+
+ <script type="text/javascript">
+
+ function reply() {
+ pmrpc.call({
+ destination : "publish",
+ publicProcedureName : "reply",
+ params : ["OK", {"answer" : "answer"}],
+ onSuccess : function() {document.getElementsByTagName('body')[0].innerHTML='<h1>Data submitted</h1>';},
+ onFailure: function() {document.getElementsByTagName('body')[0].innerHTML='<h1>Data submission failed</h1>';}
+ });
+ return true;
+ }
+
+ function cancel() {
+ pmrpc.call({
+ destination : "publish",
+ publicProcedureName : "reply",
+ params : ["Cancelled", {}],
+ onSuccess : function() {document.getElementsByTagName('body')[0].innerHTML='<h1>Cancelled</h1>';},
+ onFailure: function() {document.getElementsByTagName('body')[0].innerHTML='<h1>Cancellation failed</h1>';}
+ });
+ return true;
+ }
+
+ pmrpc.call({
+ destination : "publish",
+ publicProcedureName : "setTitle",
+ params : ["$!title"]});
+
+ </script>
+
+ <h2>Warning: $!message</h2>
+ <form name="myform" onSubmit="reply(); return false;">
+ <input type="button" value="Cancel" onClick = "cancel()"/>
+ <input type="button" value="OK" onClick = "reply()"/>
+ </form>
+ </body>
+</html>
+
http://git-wip-us.apache.org/repos/asf/incubator-taverna-common-activities/blob/390c286b/taverna-rest-activity/pom.xml
----------------------------------------------------------------------
diff --git a/taverna-rest-activity/pom.xml b/taverna-rest-activity/pom.xml
new file mode 100644
index 0000000..aa0cba3
--- /dev/null
+++ b/taverna-rest-activity/pom.xml
@@ -0,0 +1,96 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+ <modelVersion>4.0.0</modelVersion>
+ <parent>
+ <groupId>org.apache.taverna.commonactivities</groupId>
+ <artifactId>taverna-common-activities</artifactId>
+ <version>2.1.0-incubating-SNAPSHOT</version>
+ </parent>
+ <artifactId>taverna-rest-activity</artifactId>
+ <name>Apache Taverna REST Activity</name>
+ <packaging>bundle</packaging>
+
+ <dependencies>
+ <dependency>
+ <groupId>org.apache.taverna.engine</groupId>
+ <artifactId>taverna-reference-api</artifactId>
+ <version>${taverna.engine.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.taverna.engine</groupId>
+ <artifactId>taverna-workflowmodel-api</artifactId>
+ <version>${taverna.engine.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.taverna.engine</groupId>
+ <artifactId>taverna-credential-manager</artifactId>
+ <version>${taverna.engine.version}</version>
+ </dependency>
+
+ <dependency>
+ <groupId>commons-codec</groupId>
+ <artifactId>commons-codec</artifactId>
+ <version>${commons.codec.version}</version>
+ </dependency>
+
+ <dependency>
+ <groupId>org.apache.httpcomponents</groupId>
+ <artifactId>httpclient-osgi</artifactId>
+ <version>${apache.httpclient.version}</version>
+ <exclusions>
+ <!-- These are all embedded within httpclient-osgi -->
+ <exclusion>
+ <groupId>org.apache.httpcomponents</groupId>
+ <artifactId>httpclient</artifactId>
+ </exclusion>
+ <exclusion>
+ <groupId>commons-codec</groupId>
+ <artifactId>commons-codec</artifactId>
+ </exclusion>
+ <exclusion>
+ <groupId>org.apache.httpcomponents</groupId>
+ <artifactId>httpmime</artifactId>
+ </exclusion>
+ <exclusion>
+ <groupId>org.apache.httpcomponents</groupId>
+ <artifactId>httpclient-cache</artifactId>
+ </exclusion>
+ <exclusion>
+ <groupId>org.apache.httpcomponents</groupId>
+ <artifactId>fluent-hc</artifactId>
+ </exclusion>
+ </exclusions>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.httpcomponents</groupId>
+ <artifactId>httpcore-osgi</artifactId>
+ <version>${apache.httpcore.version}</version>
+ <exclusions>
+ <exclusion>
+ <groupId>org.apache.httpcomponents</groupId>
+ <artifactId>httpcore</artifactId>
+ </exclusion>
+ <exclusion>
+ <groupId>org.apache.httpcomponents</groupId>
+ <artifactId>httpcore-nio</artifactId>
+ </exclusion>
+ </exclusions>
+ </dependency>
+
+
+ <dependency>
+ <groupId>junit</groupId>
+ <artifactId>junit</artifactId>
+ <version>${junit.version}</version>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.taverna.engine</groupId>
+ <artifactId>taverna-activity-test-utils</artifactId>
+ <version>${taverna.engine.version}</version>
+ <scope>test</scope>
+ </dependency>
+
+ </dependencies>
+</project>
http://git-wip-us.apache.org/repos/asf/incubator-taverna-common-activities/blob/390c286b/taverna-rest-activity/src/main/java/net/sf/taverna/t2/activities/rest/HTTPRequest.java
----------------------------------------------------------------------
diff --git a/taverna-rest-activity/src/main/java/net/sf/taverna/t2/activities/rest/HTTPRequest.java b/taverna-rest-activity/src/main/java/net/sf/taverna/t2/activities/rest/HTTPRequest.java
new file mode 100644
index 0000000..9ff240b
--- /dev/null
+++ b/taverna-rest-activity/src/main/java/net/sf/taverna/t2/activities/rest/HTTPRequest.java
@@ -0,0 +1,105 @@
+/*******************************************************************************
+ * 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.net.URI;
+import java.util.ArrayList;
+import java.util.List;
+
+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;
+
+/**
+ * HTTP Request configuration bean.
+ *
+ * @author David Withers
+ */
+@ConfigurationBean(uri = RESTActivity.URI + "#Request")
+public class HTTPRequest {
+
+ private HTTP_METHOD method;
+
+ private String absoluteURITemplate;
+
+ private List<HTTPRequestHeader> headers = new ArrayList<HTTPRequestHeader>();
+
+ public HTTP_METHOD getMethod() {
+ return method;
+ }
+
+ @ConfigurationProperty(name = "mthd", label = "HTTP Method", uri="http://www.w3.org/2011/http#mthd")
+ public void setMethod(URI method) {
+ setMethod(HTTP_METHOD.valueOf(method.getFragment()));
+ }
+
+ public void setMethod(HTTP_METHOD method) {
+ this.method = method;
+ }
+
+ public String getAbsoluteURITemplate() {
+ return absoluteURITemplate;
+ }
+
+ @ConfigurationProperty(name = "absoluteURITemplate", label = "URL Template")
+ public void setAbsoluteURITemplate(String absoluteURITemplate) {
+ this.absoluteURITemplate = absoluteURITemplate;
+ }
+
+ public List<HTTPRequestHeader> getHeaders() {
+ return headers;
+ }
+
+ @ConfigurationProperty(name = "headers", label = "HTTP Request Headers", uri="http://www.w3.org/2011/http#headers")
+ public void setHeaders(List<HTTPRequestHeader> headers) {
+ this.headers = headers;
+ }
+
+ public HTTPRequestHeader getHeader(String name) {
+ for (HTTPRequestHeader httpRequestHeader : headers) {
+ if (httpRequestHeader.getFieldName().equals(name)) {
+ return httpRequestHeader;
+ }
+ }
+ return null;
+ }
+
+ public void setHeader(String name, String value) {
+ HTTPRequestHeader httpRequestHeader = getHeader(name);
+ if (httpRequestHeader == null) {
+ httpRequestHeader = new HTTPRequestHeader();
+ httpRequestHeader.setFieldName(name);
+ headers.add(httpRequestHeader);
+ }
+ httpRequestHeader.setFieldValue(value);
+ }
+
+ public void setHeader(String name, boolean use100Continue) {
+ HTTPRequestHeader httpRequestHeader = getHeader(name);
+ if (httpRequestHeader == null) {
+ httpRequestHeader = new HTTPRequestHeader();
+ httpRequestHeader.setFieldName(name);
+ headers.add(httpRequestHeader);
+ }
+ httpRequestHeader.setUse100Continue(use100Continue);
+ }
+
+}
http://git-wip-us.apache.org/repos/asf/incubator-taverna-common-activities/blob/390c286b/taverna-rest-activity/src/main/java/net/sf/taverna/t2/activities/rest/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/390c286b/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/390c286b/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]);
+ }
+ });
+ }
+}