diff --git a/org.springframework.integration.http/.classpath b/org.springframework.integration.http/.classpath index 2ebfc23195..9338d2c1a1 100644 --- a/org.springframework.integration.http/.classpath +++ b/org.springframework.integration.http/.classpath @@ -5,6 +5,7 @@ + diff --git a/org.springframework.integration.http/ivy.xml b/org.springframework.integration.http/ivy.xml index ce7296c981..6878557e39 100644 --- a/org.springframework.integration.http/ivy.xml +++ b/org.springframework.integration.http/ivy.xml @@ -21,6 +21,7 @@ + diff --git a/org.springframework.integration.http/src/main/java/org/springframework/integration/http/CommonsHttpRequestExecutor.java b/org.springframework.integration.http/src/main/java/org/springframework/integration/http/CommonsHttpRequestExecutor.java new file mode 100644 index 0000000000..51341365bd --- /dev/null +++ b/org.springframework.integration.http/src/main/java/org/springframework/integration/http/CommonsHttpRequestExecutor.java @@ -0,0 +1,274 @@ +/* + * Copyright 2002-2009 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.integration.http; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.zip.GZIPInputStream; + +import org.apache.commons.httpclient.Header; +import org.apache.commons.httpclient.HttpClient; +import org.apache.commons.httpclient.HttpException; +import org.apache.commons.httpclient.HttpMethod; +import org.apache.commons.httpclient.MultiThreadedHttpConnectionManager; +import org.apache.commons.httpclient.methods.ByteArrayRequestEntity; +import org.apache.commons.httpclient.methods.DeleteMethod; +import org.apache.commons.httpclient.methods.EntityEnclosingMethod; +import org.apache.commons.httpclient.methods.GetMethod; +import org.apache.commons.httpclient.methods.HeadMethod; +import org.apache.commons.httpclient.methods.OptionsMethod; +import org.apache.commons.httpclient.methods.PostMethod; +import org.apache.commons.httpclient.methods.PutMethod; +import org.apache.commons.httpclient.methods.TraceMethod; + +import org.springframework.context.i18n.LocaleContext; +import org.springframework.context.i18n.LocaleContextHolder; +import org.springframework.integration.http.AbstractHttpRequestExecutor; +import org.springframework.integration.http.HttpRequest; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +/** + * Implementation of {@link HttpRequestExecutor} that uses a commons-http + * {@link HttpClient} to execute {@link HttpRequest} instances. + * + * @author Mark Fisher + * @author Juergen Hoeller + * @since 1.0.2 + */ +public class CommonsHttpRequestExecutor extends AbstractHttpRequestExecutor { + + /** + * Default timeout value if no HttpClient is explicitly provided. + */ + private static final int DEFAULT_READ_TIMEOUT_MILLISECONDS = (60 * 1000); + + private HttpClient httpClient; + + + /** + * Create a new CommonsHttpRequestExecutor with a default HttpClient that + * uses a default MultiThreadedHttpConnectionManager. + * Sets the socket read timeout to {@link #DEFAULT_READ_TIMEOUT_MILLISECONDS}. + * @see org.apache.commons.httpclient.HttpClient + * @see org.apache.commons.httpclient.MultiThreadedHttpConnectionManager + */ + public CommonsHttpRequestExecutor() { + this.httpClient = new HttpClient(new MultiThreadedHttpConnectionManager()); + this.setReadTimeout(DEFAULT_READ_TIMEOUT_MILLISECONDS); + } + + /** + * Create a new CommonsHttpRequestExecutor with the given HttpClient + * instance. The socket read timeout of the provided HttpClient will not be + * changed. + * @param httpClient the HttpClient instance to use for this request executor + */ + public CommonsHttpRequestExecutor(HttpClient httpClient) { + this.setHttpClient(httpClient); + } + + + /** + * Set the HttpClient instance to use for this request executor. + */ + public void setHttpClient(HttpClient httpClient) { + Assert.notNull(httpClient, "httpClient must not be null"); + this.httpClient = httpClient; + } + + /** + * Return the HttpClient instance that this request executor uses. + */ + public HttpClient getHttpClient() { + return this.httpClient; + } + + /** + * Set the socket read timeout for the underlying HttpClient. A value of 0 + * means never timeout. + * @param timeout the timeout value in milliseconds + * @see org.apache.commons.httpclient.params.HttpConnectionManagerParams#setSoTimeout(int) + * @see #DEFAULT_READ_TIMEOUT_MILLISECONDS + */ + public void setReadTimeout(int timeout) { + if (timeout < 0) { + throw new IllegalArgumentException("timeout must be a non-negative value"); + } + this.httpClient.getHttpConnectionManager().getParams().setSoTimeout(timeout); + } + + @Override + protected InputStream doExecuteRequest(HttpRequest request) throws Exception { + HttpMethod httpMethod = createHttpMethod(request); + try { + if (httpMethod instanceof EntityEnclosingMethod) { + setRequestBody((EntityEnclosingMethod) httpMethod, request.getBody(), request.getContentType()); + } + executeHttpMethod(getHttpClient(), httpMethod); + validateResponse(httpMethod); + return readResponseBody(httpMethod); + } + finally { + // Need to explicitly release because it might be pooled. + httpMethod.releaseConnection(); + } + } + + /** + * Create a HttpMethod for the given {@link HttpRequest}. + *

This implementation creates an HttpMethod with the request's target + * URL as well as the "Accept-Language" and "Accept-Encoding" headers. If + * the method is "POST" or "PUT", the "Content-Type" header will also be + * set as specified in the given request. + * @param request the HTTP request to create a method for + * @return the HttpMethod instance + */ + private HttpMethod createHttpMethod(HttpRequest request) { + String url = request.getTargetUrl().toString(); + String methodName = request.getRequestMethod(); + HttpMethod httpMethod = null; + if ("GET".equals(methodName)) { + httpMethod = new GetMethod(url); + } + else if ("POST".equals(methodName)) { + httpMethod = new PostMethod(url); + } + else if ("PUT".equals(methodName)) { + httpMethod = new PutMethod(url); + } + else if ("DELETE".equals(methodName)) { + httpMethod = new DeleteMethod(url); + } + else if ("TRACE".equals(methodName)) { + httpMethod = new TraceMethod(url); + } + else if ("HEAD".equals(methodName)) { + httpMethod = new HeadMethod(url); + } + else if ("OPTIONS".equals(methodName)) { + httpMethod = new OptionsMethod(url); + } + else { + throw new UnsupportedOperationException("unsupported request method '" + methodName + "'"); + } + LocaleContext locale = LocaleContextHolder.getLocaleContext(); + if (locale != null) { + httpMethod.addRequestHeader(HTTP_HEADER_ACCEPT_LANGUAGE, StringUtils.toLanguageTag(locale.getLocale())); + } + if (isAcceptGzipEncoding()) { + httpMethod.addRequestHeader(HTTP_HEADER_ACCEPT_ENCODING, ENCODING_GZIP); + } + if (httpMethod instanceof EntityEnclosingMethod) { + String contentType = request.getContentType(); + if (contentType != null) { + httpMethod.addRequestHeader(HTTP_HEADER_CONTENT_TYPE, contentType); + } + } + return httpMethod; + } + + /** + * Set the given byte stream as the request body. + *

The default implementation simply sets the byte stream as the + * EntityEnclosingMethod's request body. This can be overridden, for + * example, to write a specific encoding and potentially set appropriate + * HTTP request headers. + * @param httpMethod the EntityEnclosingMethod on which to set the request body + * @param baos the ByteArrayOutputStream that contains the content + * @param contentType the request body's content type + * @throws IOException if thrown by I/O methods + * @see org.apache.commons.httpclient.methods.PostMethod#setRequestBody(java.io.InputStream) + * @see org.apache.commons.httpclient.methods.PostMethod#setRequestEntity + * @see org.apache.commons.httpclient.methods.InputStreamRequestEntity + */ + protected void setRequestBody( + EntityEnclosingMethod httpMethod, ByteArrayOutputStream baos, String contentType) + throws IOException { + httpMethod.setRequestEntity(new ByteArrayRequestEntity(baos.toByteArray(), contentType)); + } + + /** + * Execute the given HttpMethod instance. + * @param httpClient the HttpClient responsible for execution + * @param httpMethod the HttpMethod to be executed + * @throws IOException if thrown by I/O methods + * @see org.apache.commons.httpclient.HttpClient#executeMethod(org.apache.commons.httpclient.HttpMethod) + */ + private void executeHttpMethod(HttpClient httpClient, HttpMethod httpMethod) throws IOException { + httpClient.executeMethod(httpMethod); + } + + /** + * Validate the given response as contained in the HttpMethod object, + * throwing an exception if it does not correspond to a successful HTTP response. + *

This implementation rejects any HTTP status code beyond 2xx, to avoid + * parsing the response body and trying to read from a corrupted stream. + * @param httpMethod the executed HttpMethod to validate + * @throws IOException if validation failed + * @see org.apache.commons.httpclient.methods.PostMethod#getStatusCode() + * @see org.apache.commons.httpclient.HttpException + */ + private void validateResponse(HttpMethod httpMethod) throws IOException { + if (httpMethod.getStatusCode() >= 300) { + throw new HttpException( + "Did not receive successful HTTP response: status code = " + httpMethod.getStatusCode() + + ", status message = [" + httpMethod.getStatusText() + "]"); + } + } + + /** + * Extract the response body from the given executed request. + *

This implementation simply fetches the HttpMethod's response + * body stream. If the response is recognized as a GZIP response, the + * InputStream will be wrapped in a GZIPInputStream. + * @param httpMethod the HttpMethod from which to read the response body + * @return an InputStream for the response body, or null if no response stream is available + * @throws IOException if thrown by I/O methods + * @see #isGzipResponse + * @see java.util.zip.GZIPInputStream + * @see org.apache.commons.httpclient.HttpMethod#getResponseBodyAsStream() + */ + private InputStream readResponseBody(HttpMethod httpMethod) throws IOException { + byte[] responseBody = httpMethod.getResponseBody(); + InputStream responseStream = null; + if (responseBody != null) { + responseStream = new ByteArrayInputStream(responseBody); + if (isGzipResponse(httpMethod)) { + responseStream = new GZIPInputStream(responseStream); + } + } + return responseStream; + } + + /** + * Determine whether the given response indicates a GZIP response. + *

This implementation checks whether the HTTP "Content-Encoding" + * header contains "gzip" (in any casing). + * @param httpMethod the HttpMethod to check + * @return whether the given response indicates a GZIP response + * @see org.apache.commons.httpclient.HttpMethod#getResponseHeader(String) + */ + private boolean isGzipResponse(HttpMethod httpMethod) { + Header encodingHeader = httpMethod.getResponseHeader(HTTP_HEADER_CONTENT_ENCODING); + return (encodingHeader != null && encodingHeader.getValue() != null + && encodingHeader.getValue().toLowerCase().indexOf(ENCODING_GZIP) != -1); + } + +} diff --git a/org.springframework.integration.http/src/main/java/org/springframework/integration/http/SimpleHttpRequestExecutor.java b/org.springframework.integration.http/src/main/java/org/springframework/integration/http/SimpleHttpRequestExecutor.java index f90386c590..d5dc328936 100644 --- a/org.springframework.integration.http/src/main/java/org/springframework/integration/http/SimpleHttpRequestExecutor.java +++ b/org.springframework.integration.http/src/main/java/org/springframework/integration/http/SimpleHttpRequestExecutor.java @@ -32,7 +32,7 @@ import org.springframework.util.StringUtils; /** * Implementation of {@link HttpRequestExecutor} that uses {@link HttpURLConnection} * directly. This version has limited functionality but no additional dependencies. - * For more features, see {@link CommonsHttpRequestExecutor}. (TODO) + * For more features, see {@link CommonsHttpRequestExecutor}. * * @author Juergen Hoeller * @author Iwein Fuld diff --git a/org.springframework.integration.http/template.mf b/org.springframework.integration.http/template.mf index 6a83b7abd3..cce1ea3158 100644 --- a/org.springframework.integration.http/template.mf +++ b/org.springframework.integration.http/template.mf @@ -9,6 +9,7 @@ Import-Template: org.springframework.core.*;version="[2.5.6.A, 3.0.0)", org.springframework.util;version="[2.5.6.A, 3.0.0)", org.springframework.web.*;version="[2.5.6.A, 3.0.0)";resolution:=optional, + org.apache.commons.httpclient.*;version="[3.1.0, 4.0.0)", org.apache.commons.logging;version="[1.1.1, 2.0.0)", javax.servlet.*;version="[2.4.0, 3.0.0)";resolution:=optional Unversioned-Imports: