/*
* Copyright 2005-2015 WSO2, Inc. (http://wso2.com)
*
* 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.
*/
package org.wso2.carbon.bpmn.extensions.rest;
import org.activiti.engine.delegate.DelegateExecution;
import org.activiti.engine.delegate.Expression;
import org.activiti.engine.delegate.JavaDelegate;
import org.apache.axiom.om.OMElement;
import org.apache.axiom.om.util.AXIOMUtil;
import org.apache.commons.lang.StringEscapeUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.http.Header;
import org.wso2.carbon.bpmn.core.types.datatypes.json.BPMNJsonException;
import org.wso2.carbon.bpmn.core.types.datatypes.json.JSONUtils;
import org.wso2.carbon.bpmn.core.types.datatypes.json.api.JsonNodeObject;
import org.wso2.carbon.bpmn.core.types.datatypes.xml.BPMNXmlException;
import org.wso2.carbon.bpmn.core.types.datatypes.xml.Utils;
import org.wso2.carbon.bpmn.core.types.datatypes.xml.api.XMLDocument;
import org.wso2.carbon.bpmn.extensions.internal.BPMNExtensionsComponent;
import org.wso2.carbon.registry.api.Registry;
import org.wso2.carbon.registry.api.RegistryException;
import org.wso2.carbon.registry.api.Resource;
import org.wso2.carbon.unifiedendpoint.core.UnifiedEndpoint;
import org.wso2.carbon.unifiedendpoint.core.UnifiedEndpointFactory;
import org.xml.sax.SAXException;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.stream.XMLStreamException;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.charset.Charset;
/**
* Provides REST service invocation support within BPMN processes. It invokes the REST service given by "serviceURL" or "serviceRef" parameters using
* the HTTP method given as "method" parameter. "serviceURL" parameter can be used to give a URL of a REST service endpoint, which cannot be changed after deployment.
* "serviceRef" can point to a registry location which contains an endpoint reference as mentioned in https://docs.wso2.com/display/BPS350/Endpoint+References.
* URLs given in such registry resources can be changed after deployment and the current value of the registry resource will be read before each service invocation.
* <p/>
* Optionally, input payload can be provided using the "input" parameter. Output received from the REST service will be assigned to a
* process variable (as raw content) or parts of the output can be mapped to different process variables. Both these scenarios are illustrated in below examples.
* <p/>
* If a failure occurs in REST task, a BPMN error with error code "RestInvokeError" will be thrown. BPMN process can catch this error using an Error Boundary Event associated
* with the REST service task.
* <p/>
* Example with text input and text output:
* <p/>
* <serviceTask id="servicetask1" name="REST task1" activiti:class="RESTTask">
* <extensionElements>
* <activiti:field name="serviceURL">
* <activiti:expression>http://10.0.3.1:9773/restSample1_1.0.0/services/rest_sample1/${method}</activiti:expression>
* </activiti:field>
* <activiti:field name="basicAuthUsername">
* <activiti:expression>bobcat</activiti:expression>
* </activiti:field>
* <activiti:field name="basicAuthPassword">
* <activiti:expression>bobcat</activiti:expression>
* </activiti:field>
* <activiti:field name="method">
* <activiti:string><![CDATA[POST]]></activiti:string>
* </activiti:field>
* <activiti:field name="input">
* <activiti:expression>Input for task1</activiti:expression>
* </activiti:field>
* <activiti:field name="outputVariable">
* <activiti:string><![CDATA[v1]]></activiti:string>
* </activiti:field>
* <activiti:field name="headers">
* <activiti:string><![CDATA[{"key1":"value1","key2":"value2"}]]></activiti:string>
* </activiti:field>
* </extensionElements>
* </serviceTask>
* <p/>
* Example with JSON input and JSON output mapping and registry based URL:
* <serviceTask id="servicetask2" name="Rest task2" activiti:class="RESTTask">
* <extensionElements>
* <activiti:field name="serviceRef">
* <activiti:expression>conf:/test1/service2</activiti:expression>
* </activiti:field>
* <activiti:field name="method">
* <activiti:string><![CDATA[POST]]></activiti:string>
* </activiti:field>
* <activiti:field name="input">
* <activiti:expression>{
* "companyName":"ibm",
* "industry":"${industry}",
* "address":{
* "country":"USA",
* "state":"${state}"}
* }
* </activiti:expression>
* </activiti:field>
* <activiti:field name="outputMappings">
* <activiti:string><![CDATA[var2:customer.name,var3:item.price]]></activiti:string>
* </activiti:field>
* </extensionElements>
* </serviceTask>
* <p/>
* Registry endpoint format:
* <p/>
* <Endpoint
*/
public class RESTTask implements JavaDelegate {
private static final Log log = LogFactory.getLog(RESTTask.class);
private static final String GOVERNANCE_REGISTRY_PREFIX = "gov:/";
private static final String CONFIGURATION_REGISTRY_PREFIX = "conf:/";
private static final String REST_INVOKE_ERROR = "REST_CLIENT_INVOKE_ERROR";
private static final String GET_METHOD = "GET";
private static final String POST_METHOD = "POST";
private static final String PUT_METHOD = "PUT";
private static final String DELETE_METHOD = "DELETE";
private static final String APPLICATION_JSON = "application/json";
private static final String APPLICATION_XML = "application/xml";
private Expression serviceURL;
private Expression basicAuthUsername;
private Expression basicAuthPassword;
private Expression serviceRef;
private Expression method;
private Expression input;
private Expression outputVariable;
private Expression outputMappings;
private Expression headers;
private Expression responseHeaderVariable;
private Expression httpStatusVariable;
@Override
public void execute(DelegateExecution execution) {
if (log.isDebugEnabled()) {
log.debug("Executing RESTInvokeTask " + method.getValue(execution).toString() + " - " +
serviceURL.getValue(execution).toString());
}
RESTInvoker restInvoker = BPMNRestExtensionHolder.getInstance().getRestInvoker();
RESTResponse response;
String url = null;
String bUsername = null;
String bPassword = null;
JsonNodeObject jsonHeaders = null;
try {
if (serviceURL != null) {
url = serviceURL.getValue(execution).toString();
if (basicAuthUsername != null && basicAuthPassword != null) {
bUsername = basicAuthUsername.getValue(execution).toString();
bPassword = basicAuthPassword.getValue(execution).toString();
}
} else if (serviceRef != null) {
String resourcePath = serviceRef.getValue(execution).toString();
String registryPath;
String tenantId = execution.getTenantId();
Registry registry;
if (resourcePath.startsWith(GOVERNANCE_REGISTRY_PREFIX)) {
registryPath = resourcePath.substring(GOVERNANCE_REGISTRY_PREFIX.length());
registry = BPMNExtensionsComponent.getRegistryService().getGovernanceSystemRegistry(
Integer.parseInt(tenantId));
} else if (resourcePath.startsWith(CONFIGURATION_REGISTRY_PREFIX)) {
registryPath = resourcePath.substring(CONFIGURATION_REGISTRY_PREFIX.length());
registry = BPMNExtensionsComponent.getRegistryService().getConfigSystemRegistry(
Integer.parseInt(tenantId));
} else {
String msg = "Registry type is not specified for service reference in " +
getTaskDetails(execution) +
". serviceRef should begin with gov:/ or conf:/ to indicate the registry type.";
throw new RESTClientException(msg);
}
if (log.isDebugEnabled()) {
log.debug("Reading endpoint from registry location: " + registryPath + " for task " + getTaskDetails(execution));
}
Resource urlResource = registry.get(registryPath);
if (urlResource != null) {
String uepContent = new String((byte[]) urlResource.getContent(), Charset.defaultCharset());
UnifiedEndpointFactory uepFactory = new UnifiedEndpointFactory();
OMElement uepElement = AXIOMUtil.stringToOM(uepContent);
UnifiedEndpoint uep = uepFactory.createEndpoint(uepElement);
url = uep.getAddress();
bUsername = uep.getAuthorizationUserName();
bPassword = uep.getAuthorizationPassword();
} else {
String errorMsg = "Endpoint resource " + registryPath +
" is not found. Failed to execute REST invocation in task " + getTaskDetails(execution);
throw new RESTClientException(errorMsg);
}
} else {
String urlNotFoundErrorMsg = "Service URL is not provided for " +
getTaskDetails(execution) + ". serviceURL or serviceRef must be provided.";
throw new RESTClientException(urlNotFoundErrorMsg);
}
if (headers != null) {
String headerContent = headers.getValue(execution).toString();
jsonHeaders = JSONUtils.parse(headerContent);
}
if (POST_METHOD.equals(method.getValue(execution).toString().trim().toUpperCase())) {
String inputContent = input.getValue(execution).toString();
response = restInvoker.invokePOST(new URI(url), jsonHeaders, bUsername, bPassword, inputContent);
} else if (GET_METHOD.equals(method.getValue(execution).toString().trim().toUpperCase())) {
response = restInvoker.invokeGET(new URI(url), jsonHeaders, bUsername, bPassword);
} else if (PUT_METHOD.equals(method.getValue(execution).toString().trim().toUpperCase())) {
String inputContent = input.getValue(execution).toString();
response = restInvoker.invokePUT(new URI(url), jsonHeaders, bUsername, bPassword, inputContent);
} else if (DELETE_METHOD.equals(method.getValue(execution).toString().trim().toUpperCase())) {
response = restInvoker.invokeDELETE(new URI(url), jsonHeaders, bUsername, bPassword);
} else {
String errorMsg = "Unsupported http method. The REST task only supports GET, POST, PUT and DELETE operations";
throw new RESTClientException(errorMsg);
}
Object output = response.getContent();
boolean contentAvailable = !response.getContent().equals("");
if (contentAvailable && response.getContentType().contains(APPLICATION_JSON)) {
output = JSONUtils.parse(String.valueOf(output));
} else if (contentAvailable && response.getContentType().contains(APPLICATION_XML)) {
output = Utils.parse(String.valueOf(output));
} else {
output = StringEscapeUtils.escapeXml(String.valueOf(output));
}
if (outputVariable != null) {
String outVarName = outputVariable.getValue(execution).toString();
execution.setVariableLocal(outVarName, output);
} else if (outputMappings != null) {
String outMappings = outputMappings.getValue(execution).toString();
outMappings = outMappings.trim();
String[] mappings = outMappings.split(",");
for (String mapping : mappings) {
String[] mappingParts = mapping.split(":");
String varName = mappingParts[0];
String expression = mappingParts[1];
Object value;
if (output instanceof JsonNodeObject) {
value = ((JsonNodeObject) output).jsonPath(expression);
} else {
value = ((XMLDocument) output).xPath(expression);
}
execution.setVariableLocal(varName, value);
}
} else {
String outputNotFoundErrorMsg = "An outputVariable or outputMappings is not provided. " +
"Either an output variable or output mappings must be provided to save " +
"the response.";
throw new RESTClientException(outputNotFoundErrorMsg);
}
if (responseHeaderVariable != null) {
StringBuilder headerJsonStr = new StringBuilder();
headerJsonStr.append("{");
String prefix = "";
for (Header header : response.getHeaders()) {
headerJsonStr.append(prefix);
String name = header.getName().replaceAll("\"", "");
String value = header.getValue().replaceAll("\"", "");
headerJsonStr.append("\"").append(name).append("\":\"")
.append(value).append("\"");
prefix = ",";
}
headerJsonStr.append("}");
JsonNodeObject headerJson = JSONUtils.parse(headerJsonStr.toString());
execution.setVariableLocal(responseHeaderVariable.getValue(execution).toString(), headerJson);
}
if (httpStatusVariable != null) {
execution.setVariableLocal(httpStatusVariable.getValue(execution).toString(), response.getHttpStatus());
}
} catch (RegistryException | XMLStreamException | URISyntaxException | IOException
| SAXException | ParserConfigurationException e) {
String errorMessage = "Failed to execute " + method.getValue(execution).toString() +
" " + url + " within task " + getTaskDetails(execution);
log.error(errorMessage, e);
throw new RESTClientException(REST_INVOKE_ERROR, errorMessage);
} catch (BPMNJsonException | BPMNXmlException e) {
String errorMessage = "Failed to extract values for output mappings, the response content" +
" doesn't support the expression" + method.getValue(execution).toString() + " " +
url + " within task " + getTaskDetails(execution);
log.error(errorMessage, e);
throw new RESTClientException(REST_INVOKE_ERROR, errorMessage);
}
}
private String getTaskDetails(DelegateExecution execution) {
String task = execution.getCurrentActivityId() + ":" + execution.getCurrentActivityName() + " in process instance " + execution.getProcessInstanceId();
return task;
}
public void setServiceURL(Expression serviceURL) {
this.serviceURL = serviceURL;
}
public void setServiceRef(Expression serviceRef) {
this.serviceRef = serviceRef;
}
public void setInput(Expression input) {
this.input = input;
}
public void setOutputVariable(Expression outputVariable) {
this.outputVariable = outputVariable;
}
public void setHeaders(Expression headers) {
this.headers = headers;
}
public void setMethod(Expression method) {
this.method = method;
}
public Expression getOutputMappings() {
return outputMappings;
}
public void setOutputMappings(Expression outputMappings) {
this.outputMappings = outputMappings;
}
public void setResponseHeaderVariable(Expression responseHeaderVariable) {
this.responseHeaderVariable = responseHeaderVariable;
}
public void setHttpStatusVariable(Expression httpStatusVariable) {
this.httpStatusVariable = httpStatusVariable;
}
public void setBasicAuthUsername(Expression basicAuthUsername) {
this.basicAuthUsername = basicAuthUsername;
}
public void setBasicAuthPassword(Expression basicAuthPassword) {
this.basicAuthPassword = basicAuthPassword;
}
}