org.apache.taverna.activities.rest.HTTPRequestHandler.java Source code

Java tutorial

Introduction

Here is the source code for org.apache.taverna.activities.rest.HTTPRequestHandler.java

Source

/*
* 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.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 org.apache.taverna.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);
        }
    }
}