Java tutorial
/* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you 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.apache.taverna.activities.rest; import java.io.UnsupportedEncodingException; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import org.apache.taverna.activities.rest.HTTPRequestHandler.HTTPRequestResponse; import org.apache.taverna.activities.rest.URISignatureHandler.URISignatureParsingException; import org.apache.taverna.invocation.InvocationContext; import org.apache.taverna.reference.ErrorDocument; import org.apache.taverna.reference.ReferenceService; import org.apache.taverna.reference.T2Reference; import org.apache.taverna.workflowmodel.processor.activity.AbstractAsynchronousActivity; import org.apache.taverna.workflowmodel.processor.activity.ActivityConfigurationException; import org.apache.taverna.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]); } }); } }