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.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); } } }