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