From cce1e2010da4330346ecb66c4aa8ef028d2d7572 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Mon, 15 Aug 2011 21:23:41 +0000 Subject: [PATCH] added HttpComponentsHttpInvokerRequestExecutor --- ...pComponentsHttpInvokerRequestExecutor.java | 267 ++++++++++++++++++ 1 file changed, 267 insertions(+) create mode 100644 org.springframework.web/src/main/java/org/springframework/remoting/httpinvoker/HttpComponentsHttpInvokerRequestExecutor.java diff --git a/org.springframework.web/src/main/java/org/springframework/remoting/httpinvoker/HttpComponentsHttpInvokerRequestExecutor.java b/org.springframework.web/src/main/java/org/springframework/remoting/httpinvoker/HttpComponentsHttpInvokerRequestExecutor.java new file mode 100644 index 0000000000..0d5e179e9a --- /dev/null +++ b/org.springframework.web/src/main/java/org/springframework/remoting/httpinvoker/HttpComponentsHttpInvokerRequestExecutor.java @@ -0,0 +1,267 @@ +/* + * Copyright 2002-2011 the original author or authors. + * + * Licensed 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.springframework.remoting.httpinvoker; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.zip.GZIPInputStream; + +import org.apache.http.Header; +import org.apache.http.HttpResponse; +import org.apache.http.NoHttpResponseException; +import org.apache.http.StatusLine; +import org.apache.http.client.HttpClient; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.conn.scheme.PlainSocketFactory; +import org.apache.http.conn.scheme.Scheme; +import org.apache.http.conn.scheme.SchemeRegistry; +import org.apache.http.conn.ssl.SSLSocketFactory; +import org.apache.http.entity.ByteArrayEntity; +import org.apache.http.impl.client.DefaultHttpClient; +import org.apache.http.impl.conn.tsccm.ThreadSafeClientConnManager; +import org.apache.http.params.CoreConnectionPNames; + +import org.springframework.context.i18n.LocaleContext; +import org.springframework.context.i18n.LocaleContextHolder; +import org.springframework.remoting.support.RemoteInvocationResult; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +/** + * {@link org.springframework.remoting.httpinvoker.HttpInvokerRequestExecutor} implementation that uses + * Apache HttpComponents HttpClient + * to execute POST requests. + * + *

Allows to use a pre-configured {@link org.apache.http.client.HttpClient} + * instance, potentially with authentication, HTTP connection pooling, etc. + * Also designed for easy subclassing, providing specific template methods. + * + * @author Juergen Hoeller + * @since 3.1 + * @see org.springframework.remoting.httpinvoker.SimpleHttpInvokerRequestExecutor + */ +public class HttpComponentsHttpInvokerRequestExecutor extends AbstractHttpInvokerRequestExecutor { + + private static final int DEFAULT_MAX_TOTAL_CONNECTIONS = 100; + + private static final int DEFAULT_MAX_CONNECTIONS_PER_ROUTE = 5; + + private static final int DEFAULT_READ_TIMEOUT_MILLISECONDS = (60 * 1000); + + private HttpClient httpClient; + + + /** + * Create a new instance of the HttpComponentsHttpInvokerRequestExecutor with a default + * {@link HttpClient} that uses a default {@link org.apache.http.impl.conn.tsccm.ThreadSafeClientConnManager}. + */ + public HttpComponentsHttpInvokerRequestExecutor() { + SchemeRegistry schemeRegistry = new SchemeRegistry(); + schemeRegistry.register(new Scheme("http", 80, PlainSocketFactory.getSocketFactory())); + schemeRegistry.register(new Scheme("https", 443, SSLSocketFactory.getSocketFactory())); + + ThreadSafeClientConnManager connectionManager = new ThreadSafeClientConnManager(schemeRegistry); + connectionManager.setMaxTotal(DEFAULT_MAX_TOTAL_CONNECTIONS); + connectionManager.setDefaultMaxPerRoute(DEFAULT_MAX_CONNECTIONS_PER_ROUTE); + + this.httpClient = new DefaultHttpClient(connectionManager); + setReadTimeout(DEFAULT_READ_TIMEOUT_MILLISECONDS); + } + + /** + * Create a new instance of the HttpComponentsClientHttpRequestFactory + * with the given {@link HttpClient} instance. + * @param httpClient the HttpClient instance to use for this request executor + */ + public HttpComponentsHttpInvokerRequestExecutor(HttpClient httpClient) { + this.httpClient = httpClient; + } + + + /** + * Set the {@link HttpClient} instance to use for this request executor. + */ + public void setHttpClient(HttpClient httpClient) { + this.httpClient = httpClient; + } + + /** + * Return the {@link HttpClient} instance that this request executor uses. + */ + public HttpClient getHttpClient() { + return this.httpClient; + } + + /** + * Set the connection timeout for the underlying HttpClient. + * A timeout value of 0 specifies an infinite timeout. + * @param timeout the timeout value in milliseconds + */ + public void setConnectTimeout(int timeout) { + Assert.isTrue(timeout < 0, "Timeout must be a non-negative value"); + getHttpClient().getParams().setIntParameter(CoreConnectionPNames.CONNECTION_TIMEOUT, timeout); + } + + /** + * Set the socket read timeout for the underlying HttpClient. + * A timeout value of 0 specifies an infinite timeout. + * @param timeout the timeout value in milliseconds + * @see #DEFAULT_READ_TIMEOUT_MILLISECONDS + */ + public void setReadTimeout(int timeout) { + Assert.isTrue(timeout < 0, "Timeout must be a non-negative value"); + getHttpClient().getParams().setIntParameter(CoreConnectionPNames.SO_TIMEOUT, timeout); + } + + + /** + * Execute the given request through the HttpClient. + *

This method implements the basic processing workflow: + * The actual work happens in this class's template methods. + * @see #createHttpPost + * @see #setRequestBody + * @see #executeHttpPost + * @see #validateResponse + * @see #getResponseBody + */ + @Override + protected RemoteInvocationResult doExecuteRequest( + HttpInvokerClientConfiguration config, ByteArrayOutputStream baos) + throws IOException, ClassNotFoundException { + + HttpPost postMethod = createHttpPost(config); + setRequestBody(config, postMethod, baos); + HttpResponse response = executeHttpPost(config, getHttpClient(), postMethod); + validateResponse(config, response); + InputStream responseBody = getResponseBody(config, response); + return readRemoteInvocationResult(responseBody, config.getCodebaseUrl()); + } + + /** + * Create a HttpPost for the given configuration. + *

The default implementation creates a standard HttpPost with + * "application/x-java-serialized-object" as "Content-Type" header. + * @param config the HTTP invoker configuration that specifies the + * target service + * @return the HttpPost instance + * @throws java.io.IOException if thrown by I/O methods + */ + protected HttpPost createHttpPost(HttpInvokerClientConfiguration config) throws IOException { + HttpPost httpPost = new HttpPost(config.getServiceUrl()); + LocaleContext locale = LocaleContextHolder.getLocaleContext(); + if (locale != null) { + httpPost.addHeader(HTTP_HEADER_ACCEPT_LANGUAGE, StringUtils.toLanguageTag(locale.getLocale())); + } + if (isAcceptGzipEncoding()) { + httpPost.addHeader(HTTP_HEADER_ACCEPT_ENCODING, ENCODING_GZIP); + } + return httpPost; + } + + /** + * Set the given serialized remote invocation as request body. + *

The default implementation simply sets the serialized invocation as the + * HttpPost's request body. This can be overridden, for example, to write a + * specific encoding and to potentially set appropriate HTTP request headers. + * @param config the HTTP invoker configuration that specifies the target service + * @param httpPost the HttpPost to set the request body on + * @param baos the ByteArrayOutputStream that contains the serialized + * RemoteInvocation object + * @throws java.io.IOException if thrown by I/O methods + */ + protected void setRequestBody( + HttpInvokerClientConfiguration config, HttpPost httpPost, ByteArrayOutputStream baos) + throws IOException { + + ByteArrayEntity entity = new ByteArrayEntity(baos.toByteArray()); + entity.setContentType(getContentType()); + httpPost.setEntity(entity); + } + + /** + * Execute the given HttpPost instance. + * @param config the HTTP invoker configuration that specifies the target service + * @param httpClient the HttpClient to execute on + * @param httpPost the HttpPost to execute + * @return the resulting HttpResponse + * @throws java.io.IOException if thrown by I/O methods + */ + protected HttpResponse executeHttpPost( + HttpInvokerClientConfiguration config, HttpClient httpClient, HttpPost httpPost) + throws IOException { + + return httpClient.execute(httpPost); + } + + /** + * Validate the given response as contained in the HttpPost object, + * throwing an exception if it does not correspond to a successful HTTP response. + *

Default implementation rejects any HTTP status code beyond 2xx, to avoid + * parsing the response body and trying to deserialize from a corrupted stream. + * @param config the HTTP invoker configuration that specifies the target service + * @param response the resulting HttpResponse to validate + * @throws java.io.IOException if validation failed + */ + protected void validateResponse(HttpInvokerClientConfiguration config, HttpResponse response) + throws IOException { + + StatusLine status = response.getStatusLine(); + if (status.getStatusCode() >= 300) { + throw new NoHttpResponseException( + "Did not receive successful HTTP response: status code = " + status.getStatusCode() + + ", status message = [" + status.getReasonPhrase() + "]"); + } + } + + /** + * Extract the response body from the given executed remote invocation request. + *

The default implementation simply fetches the HttpPost's response body stream. + * If the response is recognized as GZIP response, the InputStream will get wrapped + * in a GZIPInputStream. + * @param config the HTTP invoker configuration that specifies the target service + * @param httpResponse the resulting HttpResponse to read the response body from + * @return an InputStream for the response body + * @throws java.io.IOException if thrown by I/O methods + * @see #isGzipResponse + * @see java.util.zip.GZIPInputStream + */ + protected InputStream getResponseBody(HttpInvokerClientConfiguration config, HttpResponse httpResponse) + throws IOException { + + if (isGzipResponse(httpResponse)) { + return new GZIPInputStream(httpResponse.getEntity().getContent()); + } + else { + return httpResponse.getEntity().getContent(); + } + } + + /** + * Determine whether the given response indicates a GZIP response. + *

The default implementation checks whether the HTTP "Content-Encoding" + * header contains "gzip" (in any casing). + * @param httpResponse the resulting HttpResponse to check + * @return whether the given response indicates a GZIP response + */ + protected boolean isGzipResponse(HttpResponse httpResponse) { + Header encodingHeader = httpResponse.getFirstHeader(HTTP_HEADER_CONTENT_ENCODING); + return (encodingHeader != null && encodingHeader.getValue() != null && + encodingHeader.getValue().toLowerCase().contains(ENCODING_GZIP)); + } + +}