/* * $Id$ * * Copyright 2006, The jCoderZ.org Project. All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are * met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above * copyright notice, this list of conditions and the following * disclaimer in the documentation and/or other materials * provided with the distribution. * * Neither the name of the jCoderZ.org Project nor the names of * its contributors may be used to endorse or promote products * derived from this software without specific prior written * permission. * * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS "AS IS" AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS AND CONTRIBUTORS * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR * BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ package org.jcoderz.commons.connector.http.transport; import java.io.IOException; import java.io.InputStream; import java.net.ConnectException; import java.security.KeyStore; import java.util.HashMap; import java.util.Iterator; import java.util.Map; import java.util.logging.Level; import java.util.logging.Logger; import org.apache.commons.httpclient.Header; import org.apache.commons.httpclient.HostConfiguration; import org.apache.commons.httpclient.HttpClient; import org.apache.commons.httpclient.HttpConnection; import org.apache.commons.httpclient.HttpConnectionManager; import org.apache.commons.httpclient.HttpException; import org.apache.commons.httpclient.SimpleHttpConnectionManager; import org.apache.commons.httpclient.URI; import org.apache.commons.httpclient.URIException; import org.apache.commons.httpclient.methods.InputStreamRequestEntity; import org.apache.commons.httpclient.methods.PostMethod; import org.apache.commons.httpclient.params.HttpConnectionManagerParams; import org.apache.commons.httpclient.protocol.Protocol; import org.apache.commons.httpclient.protocol.ProtocolSocketFactory; import org.jcoderz.commons.ArgumentMalformedException; import org.jcoderz.commons.util.Assert; import org.jcoderz.commons.util.Constants; import org.jcoderz.commons.util.HexUtil; import org.jcoderz.commons.util.StringUtil; /** * This class implements the HttpConnectionInterface using the Jakarta * commons-httpclient library. * */ public class HttpClientConnectionImpl implements HttpClientConnection { /** The class name used for logging */ private static final String CLASSNAME = HttpClientConnectionImpl.class.getName(); /** The logger in use */ private static final Logger logger = Logger.getLogger(CLASSNAME); /** The default port used for SSL connections */ private static final int DEFAULT_SSL_PORT = 443; /** Constant for line feed. */ private static final String LINE_FEED = "\n"; /** Constant for the HTTP result code 200 */ private static final int HTTP_RESULT_OK = 200; /** Contant for the HTTP result code 300 */ private static final int HTTP_REDIRECT = 300; /** Constant for the HTTP result code 400 */ private static final int HTTP_BAD_REQUEST = 400; /** Constant for the HTTP result code 500 */ private static final int HTTP_INTERNAL_SERVER_ERROR = 500; /** The HttpClient defined in the 3rd party library. */ private HttpClient mHttpClient; /** The HostConfiguration defined in the 3rd party library. */ private HostConfiguration mHostConfiguration; /** The PostMethod defined in the 3rd party library. */ private PostMethod mPostMethod; /** The state identifying the current step of the HTTP send/receive process. */ /** Input stream for the request message used for the httpclient */ private InputStream mRequestBodyInputStream = null; /** Content length of the request message */ private int mRequestContentLength = 0; /** Flag indicating if the content length has been set via interface method. If false the content length will be calculated by the httpclient. */ private boolean mIsRequestContentLengthSet = false; /** Content type of the request message */ private String mRequestContentType = null; /** Current state of the httpclient. Used to check the correct usage of the interface methods defined within HttpClientConnection interface. */ private HttpConnectionState mState = HttpConnectionState.CONNECTION_NOT_ESTABLISHED; /** Keystore filename used for SSL connections. Used to load the keystore from the filesystem if not given within interface method. */ private String mKeyStoreFilename = null; /** Keystore password used for SSL connection. */ private String mKeyStorePassword = null; /** Truststore filename used for SSL connections. Used to load the truststore from the filesystem if not given within interface method. */ private String mTrustStoreFilename = null; /** Truststore password used for SSL connection. */ private String mTrustStorePassword = null; /** Keystore used for SSL connections obtained via interface method. */ private KeyStore mKeyStore = null; private KeyStore mTrustStore = null; /** Key alias of the sending system used for SSL connections. */ private String mKeyAlias = null; /** Key password of the used key. */ private String mKeyAliasPassword = null; /** Path used to load the keystore from file system if not given within interface method calls. */ private String mPath = null; private HttpConnectorEventListener mHttpEventListener = null; private ConnectorContext mConnectorContext = null; private HttpRequestResponseHeader mRequestResponseHeader = null; /** {@inheritDoc} */ public void establishConnection ( String uriAsString, int connectTimeout, int readTimeout) { Assert.notNull(uriAsString, "uriAsString"); mHostConfiguration = new HostConfiguration(); mPostMethod = new PostMethod(); URI uri; try { uri = new URI(uriAsString, false); mPath = uri.getPathQuery(); mPostMethod.setPath(mPath); final String scheme = uri.getScheme(); if (scheme != null && scheme.toLowerCase(Constants.SYSTEM_LOCALE). equals("https")) { SslSocketFactory sslFactory = null; if (mKeyStore != null && mTrustStore != null) { // using the keystore given from signature service sslFactory = new SslSocketFactory( mKeyStore, mTrustStore, mKeyAlias, mKeyAliasPassword); } else { // using a keystore loading from file system sslFactory = new SslSocketFactory( mKeyStoreFilename, mKeyStorePassword, mTrustStoreFilename, mTrustStorePassword, mKeyAlias, mKeyAliasPassword); } final Protocol https = new Protocol( "https", (ProtocolSocketFactory) sslFactory, DEFAULT_SSL_PORT); int port = uri.getPort(); if (port == -1) { port = DEFAULT_SSL_PORT; } final String host = uri.getHost(); mHostConfiguration.setHost(host, port, https); } else { mHostConfiguration.setHost(uri); } } catch (URIException ue) { final ArgumentMalformedException ame = new ArgumentMalformedException( "uriAsString", uriAsString, ue.getMessage(), ue); throw ame; } catch (IllegalArgumentException iae) { final ArgumentMalformedException ame = new ArgumentMalformedException( "uriAsString", uriAsString, iae.getMessage(), iae); throw ame; } mHttpClient = new HttpClient(); final SimpleHttpConnectionManager connectionManager = new SimpleHttpConnectionManager(); final HttpConnectionManagerParams httpParams = new HttpConnectionManagerParams(); httpParams.setConnectionTimeout(connectTimeout); httpParams.setSoTimeout(readTimeout); connectionManager.setParams(httpParams); mHttpClient.setHttpConnectionManager(connectionManager); mState = HttpConnectionState.CONNECTION_ESTABLISHED; } /** {@inheritDoc} */ public void releaseConnection () { if (mState == HttpConnectionState.CONNECTION_RELEASED) { return; } if (mState != HttpConnectionState.CONNECTION_ESTABLISHED && mState != HttpConnectionState.CONNECTION_EXECUTED) { final IllegalStateException ile = new IllegalStateException( "Connection must be established before"); throw ile; } mPostMethod.releaseConnection(); // depcrecated and will be removed in future releases mPostMethod.recycle(); mState = HttpConnectionState.CONNECTION_RELEASED; } /** {@inheritDoc} */ public void closeConnection () { if (mState != HttpConnectionState.CONNECTION_ESTABLISHED && mState != HttpConnectionState.CONNECTION_EXECUTED && mState != HttpConnectionState.CONNECTION_RELEASED) { final IllegalStateException ile = new IllegalStateException( "Connection must be established before"); throw ile; } final HttpConnectionManager connManager = mHttpClient.getHttpConnectionManager(); final HttpConnection connection = connManager.getConnection(mHostConfiguration); connection.close(); mState = HttpConnectionState.CONNECTION_CLOSED; } /** {@inheritDoc} */ public void execute () throws HttpConnectionException { assertStateForExecute(); setRequestHeader(); mPostMethod.setRequestEntity(getRequestEntity()); // reassign the path to the previously released post method if (mState == HttpConnectionState.CONNECTION_RELEASED) { mPostMethod.setPath(mPath); } // dump request if (logger.isLoggable(Level.FINEST)) { dumpRequestHeader(); } int resultCode = 0; try { doCallbackRequestSend(); resultCode = mHttpClient.executeMethod( mHostConfiguration, mPostMethod); doCallbackResponseReceived(resultCode, getResponseBody()); } catch (HttpException he) { final HttpConnectionException hce = new HttpConnectionException( he.getMessage(), he); throw hce; } catch (ConnectException ce) { final HttpConnectConnectionException hce = new HttpConnectConnectionException(ce.getMessage(), ce); throw hce; } catch (IOException ioe) { handleIOExceptionWhilstExecute(ioe); } // dump response if (logger.isLoggable(Level.FINEST)) { dumpResponse(); } assertResultCode(resultCode); assertResponseHeader(); mState = HttpConnectionState.CONNECTION_EXECUTED; } private void assertStateForExecute () { if (mState != HttpConnectionState.CONNECTION_ESTABLISHED && mState != HttpConnectionState.CONNECTION_RELEASED) { final IllegalStateException ile = new IllegalStateException( "Connection must be established before or released"); throw ile; } } private void handleIOExceptionWhilstExecute ( IOException ioe) throws HttpConnectionException { final String message = ioe.getMessage().toLowerCase( Constants.SYSTEM_LOCALE); HttpConnectionException hce; if (message.equals("read timed out")) { hce = new HttpTimeoutConnectionException(ioe.getMessage(), ioe); } else { hce = new HttpConnectionException(ioe.getMessage(), ioe); } throw hce; } /** {@inheritDoc} */ public void setRequestBody (InputStream in) { mRequestBodyInputStream = in; } /** {@inheritDoc} */ public byte[] getResponseBody () throws HttpEmptyResponseException { final byte[] result; try { result = mPostMethod.getResponseBody(); } catch (IOException ioe) { final HttpEmptyResponseException ere = new HttpEmptyResponseException( "IOException while obtaining response body", ioe); throw ere; } if (result == null || result.length == 0) { final HttpEmptyResponseException ere = new HttpEmptyResponseException( "Received empty http body in response"); throw ere; } return result; } /** {@inheritDoc} */ public String getResponseHeader (String key) { String result = null; final Header header = mPostMethod.getResponseHeader(key); if (header != null) { result = (header.getElements())[0].getName(); } return result; } /** {@inheritDoc} */ public void initSsl ( String keyAlias, String keyAliasPassword) { Assert.notNull(keyAlias, "keyAlias"); Assert.notNull(keyAliasPassword, "keyAliasPassword"); mKeyStoreFilename = System.getProperty( "javax.net.ssl.keyStore"); mKeyStorePassword = System.getProperty( "javax.net.ssl.keyStorePassword"); mTrustStoreFilename = System.getProperty( "javax.net.ssl.trustStore"); mTrustStorePassword = System.getProperty( "javax.net.ssl.trustStorePassword"); mKeyStore = null; mTrustStore = null; mKeyAlias = keyAlias; mKeyAliasPassword = keyAliasPassword; } /** {@inheritDoc} */ public void initSsl ( KeyStore keyStore, KeyStore trustStore, String keyAlias, String keyAliasPassword) { Assert.notNull(keyStore, "keyStore"); Assert.notNull(trustStore, "trustStore"); Assert.notNull(keyAlias, "keyAlias"); Assert.notNull(keyAliasPassword, "keyAliasPassword"); mKeyStore = keyStore; mTrustStore = trustStore; mKeyAlias = keyAlias; mKeyAliasPassword = keyAliasPassword; } /** {@inheritDoc} */ public void setEventListener (HttpConnectorEventListener listener, ConnectorContext context) { mHttpEventListener = listener; mConnectorContext = context; } /** {@inheritDoc} */ public void setRequestResponseHeader (HttpRequestResponseHeader header) { mRequestResponseHeader = header; } /** * Prepares the InputStream including the http request for the * httpclient. * * @return InputStreamRequestEntity the InputStream used by httpclient */ private InputStreamRequestEntity getRequestEntity () { InputStreamRequestEntity entity = null; if (mIsRequestContentLengthSet && mRequestContentType != null) { entity = new InputStreamRequestEntity(mRequestBodyInputStream, mRequestContentLength, mRequestContentType); } else if (mIsRequestContentLengthSet) { entity = new InputStreamRequestEntity(mRequestBodyInputStream, mRequestContentLength); } else if (mRequestContentType != null) { entity = new InputStreamRequestEntity(mRequestBodyInputStream, mRequestContentType); } else { entity = new InputStreamRequestEntity(mRequestBodyInputStream); } return entity; } /** * Sets the request header included in the HttpRequestResponseHeader object. */ private void setRequestHeader () { if (mRequestResponseHeader != null) { final Map header = new HashMap(mRequestResponseHeader.getRequestHeader()); if (!header.containsKey("Content-Type")) { // default Content-Type header.put("Content-Type", "text/xml; charset=ISO-8859-1"); } for (final Iterator it = header.keySet().iterator(); it.hasNext();) { final String key = (String) it.next(); final String value = (String) header.get(key); mPostMethod.setRequestHeader(key, value); if (key.equals("Content-Length")) { setRequestContentLength(Integer.parseInt(value)); } else if (key.equals("Content-Type")) { mRequestContentType = value; } } } } /** * Sets the content lenght of the request. * @param contentLength the length to set */ private void setRequestContentLength (int contentLength) { mRequestContentLength = contentLength; mIsRequestContentLengthSet = true; } /** * Checks the http status code in the response message. * Throws appropriate HttpConnectionException if result code is * not 200 (OK). * * @param resultCode the http result code received in response * @throws HttpConnectionException to indicate failure on http level */ private void assertResultCode (int resultCode) throws HttpConnectionException { if (resultCode != HTTP_RESULT_OK) { HttpConnectionException hce; if (resultCode >= HTTP_BAD_REQUEST && resultCode < HTTP_INTERNAL_SERVER_ERROR) { // HTTP 4xx hce = new HttpClientConnectionException( "HTTP Result Code of range 4xx in response", null); } else if (resultCode >= HTTP_INTERNAL_SERVER_ERROR) { // HTTP 5xx hce = new HttpServerConnectionException( "HTTP Result Code of range 5xx in response", null); } else if (resultCode >= HTTP_REDIRECT && resultCode < HTTP_BAD_REQUEST) { // HTTP 3xx hce = new HttpServerConnectionException( "HTTP redirect not supported", null); } else { hce = new HttpServerConnectionException( "Unsupported HTTP Result Code in response", null); } hce.setStatusCode(resultCode); hce.setHttpMessage(StringUtil.asciiToString(getResponseBody())); throw hce; } } /** * Checks if the received http header in response match the * response header defined in the given HttpRequestResponseHeader object. * * @throws HttpInvalidResponseHeaderException if one or more header * values are not like expected */ private void assertResponseHeader () throws HttpInvalidResponseHeaderException { if (mRequestResponseHeader != null) { logger.finest(mRequestResponseHeader.toString()); final Map expectedResponseHeader = mRequestResponseHeader.getResponseHeader(); final StringBuffer message = new StringBuffer(); final Map invalidHeaders = new HashMap(); for (final Iterator it = expectedResponseHeader.keySet().iterator(); it.hasNext();) { final String key = (String) it.next(); final String value = (String) expectedResponseHeader.get(key); final String responseValue = getResponseHeader(key); if (responseValue == null) { if (message.length() == 0) { message.append("One or more invalid header"); message.append(" values detected in response:\n"); } message.append("Expected response header <"); message.append(key); message.append("> not received\n"); invalidHeaders.put(key, null); } else if (!responseValue.equalsIgnoreCase(value)) { if (message.length() == 0) { message.append("One or more invalid header"); message.append(" values detected in response:\n"); } message.append("Value for <"); message.append(key); message.append("> is '"); message.append(responseValue); message.append("' expected was '"); message.append(value); message.append("'\n"); invalidHeaders.put(key, responseValue); } } // end for if (message.length() != 0) { final HttpInvalidResponseHeaderException ihe = new HttpInvalidResponseHeaderException( message.toString(), invalidHeaders); try { ihe.setHttpMessage(HexUtil.dump(mPostMethod.getResponseBody())); } catch (IOException ignore) { // ignore } throw ihe; } } else { logger.finest("No response header for validating set"); } } private void doCallbackRequestSend () { if (mHttpEventListener != null) { mHttpEventListener.requestSend(mConnectorContext); } } private void doCallbackResponseReceived (int resultCode, byte[] responseData) { if (mHttpEventListener != null) { mHttpEventListener.responseReceived( resultCode, responseData, mConnectorContext); } } /** * Dumps the request message. */ private void dumpRequestHeader () { final StringBuffer dumpBuffer = new StringBuffer(); dumpBuffer.append("\n-------------------------DUMP REQUEST HEADER---\n"); dumpBuffer.append("Host: "); dumpBuffer.append(mHostConfiguration.getHost()); dumpBuffer.append(LINE_FEED); dumpBuffer.append("Port: "); dumpBuffer.append(mHostConfiguration.getPort()); dumpBuffer.append(LINE_FEED); dumpBuffer.append("Path: "); dumpBuffer.append(mPostMethod.getPath()); dumpBuffer.append(LINE_FEED); dumpBuffer.append(LINE_FEED); dumpBuffer.append("Header:\n"); final Header[] headers = mPostMethod.getRequestHeaders(); for (int i = 0; i < headers.length; i++) { final String header = headers[i].toString(); dumpBuffer.append(header); } dumpBuffer.append(LINE_FEED); dumpBuffer.append("-----------------------------------------"); logger.finest(dumpBuffer.toString()); } /** * Dumps the response message. */ private void dumpResponse () { final StringBuffer dumpBuffer = new StringBuffer(); dumpBuffer.append("\n-------------------------DUMP RESPONSE---\n"); dumpBuffer.append("StatusLine: "); dumpBuffer.append(mPostMethod.getStatusLine().toString()); dumpBuffer.append(LINE_FEED); dumpBuffer.append(LINE_FEED); dumpBuffer.append("Header:\n"); final Header[] headers = mPostMethod.getResponseHeaders(); for (int i = 0; i < headers.length; i++) { final String header = headers[i].toString(); dumpBuffer.append(header); } dumpBuffer.append(LINE_FEED); dumpBuffer.append("Body: \n"); byte[] response = null; try { response = mPostMethod.getResponseBody(); } catch (IOException ignore) { // ignore } if (response != null) { dumpBuffer.append(HexUtil.dump(response)); } else { dumpBuffer.append("no response body available"); } dumpBuffer.append(LINE_FEED); dumpBuffer.append("-----------------------------------------"); logger.finest(dumpBuffer.toString()); } }