From 87473307d17209d5f1d03268ecb90a6402598fb0 Mon Sep 17 00:00:00 2001 From: Arjen Poutsma Date: Mon, 11 Apr 2011 13:12:45 +0000 Subject: [PATCH] SPR-6180 - Upgrade Apache HttpClient to version 4.0 git-svn-id: https://src.springframework.org/svn/spring-framework/trunk@4211 50f2f4bb-b051-0410-bef5-90022cba6387 --- org.springframework.web/ivy.xml | 2 + .../http/client/CommonsClientHttpRequest.java | 2 + .../CommonsClientHttpRequestFactory.java | 4 +- .../client/CommonsClientHttpResponse.java | 4 +- .../HttpComponentsClientHttpRequest.java | 85 +++++++++ ...ttpComponentsClientHttpRequestFactory.java | 170 ++++++++++++++++++ .../HttpComponentsClientHttpResponse.java | 86 +++++++++ .../AbstractHttpRequestFactoryTestCase.java | 53 +----- ...mponentsClientHttpRequestFactoryTests.java | 25 +++ .../client/RestTemplateIntegrationTests.java | 6 +- org.springframework.web/template.mf | 1 + 11 files changed, 381 insertions(+), 57 deletions(-) create mode 100644 org.springframework.web/src/main/java/org/springframework/http/client/HttpComponentsClientHttpRequest.java create mode 100644 org.springframework.web/src/main/java/org/springframework/http/client/HttpComponentsClientHttpRequestFactory.java create mode 100644 org.springframework.web/src/main/java/org/springframework/http/client/HttpComponentsClientHttpResponse.java create mode 100644 org.springframework.web/src/test/java/org/springframework/http/client/HttpComponentsClientHttpRequestFactoryTests.java diff --git a/org.springframework.web/ivy.xml b/org.springframework.web/ivy.xml index d6fff677287..44140487a09 100644 --- a/org.springframework.web/ivy.xml +++ b/org.springframework.web/ivy.xml @@ -60,6 +60,8 @@ conf="optional, httpclient->compile"/> + Created via the {@link HttpComponentsClientHttpRequestFactory}. + * + * @author Oleg Kalnichevski + * @author Arjen Poutsma + * @since 3.0 + * @see HttpComponentsClientHttpRequestFactory#createRequest(URI, HttpMethod) + */ +final class HttpComponentsClientHttpRequest extends AbstractBufferingClientHttpRequest { + + private final HttpClient httpClient; + + private final HttpUriRequest httpRequest; + + public HttpComponentsClientHttpRequest(HttpClient httpClient, HttpUriRequest httpRequest) { + this.httpClient = httpClient; + this.httpRequest = httpRequest; + } + + public HttpMethod getMethod() { + return HttpMethod.valueOf(httpRequest.getMethod()); + } + + public URI getURI() { + return httpRequest.getURI(); + } + + @Override + protected ClientHttpResponse executeInternal(HttpHeaders headers, byte[] bufferedOutput) throws IOException { + for (Map.Entry> entry : headers.entrySet()) { + String headerName = entry.getKey(); + if (!headerName.equalsIgnoreCase(HTTP.CONTENT_LEN) && + !headerName.equalsIgnoreCase(HTTP.TRANSFER_ENCODING)) { + for (String headerValue : entry.getValue()) { + httpRequest.addHeader(headerName, headerValue); + } + } + } + if (httpRequest instanceof HttpEntityEnclosingRequest) { + HttpEntityEnclosingRequest entityEnclosingRequest = (HttpEntityEnclosingRequest) httpRequest; + HttpEntity requestEntity = new ByteArrayEntity(bufferedOutput); + entityEnclosingRequest.setEntity(requestEntity); + } + HttpResponse httpResponse = httpClient.execute(httpRequest); + return new HttpComponentsClientHttpResponse(httpResponse); + + } +} diff --git a/org.springframework.web/src/main/java/org/springframework/http/client/HttpComponentsClientHttpRequestFactory.java b/org.springframework.web/src/main/java/org/springframework/http/client/HttpComponentsClientHttpRequestFactory.java new file mode 100644 index 00000000000..3bb3e77051c --- /dev/null +++ b/org.springframework.web/src/main/java/org/springframework/http/client/HttpComponentsClientHttpRequestFactory.java @@ -0,0 +1,170 @@ +/* + * 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.http.client; + +import java.io.IOException; +import java.net.URI; + +import org.apache.http.client.HttpClient; +import org.apache.http.client.methods.HttpDelete; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.methods.HttpHead; +import org.apache.http.client.methods.HttpOptions; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.client.methods.HttpPut; +import org.apache.http.client.methods.HttpTrace; +import org.apache.http.client.methods.HttpUriRequest; +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.impl.client.DefaultHttpClient; +import org.apache.http.impl.conn.tsccm.ThreadSafeClientConnManager; +import org.apache.http.params.CoreConnectionPNames; + +import org.springframework.beans.factory.DisposableBean; +import org.springframework.http.HttpMethod; +import org.springframework.util.Assert; + +/** + * {@link org.springframework.http.client.ClientHttpRequestFactory} implementation that uses + * Http Components HttpClient to create requests. + * + *

Allows to use a pre-configured {@link HttpClient} instance - + * potentially with authentication, HTTP connection pooling, etc. + * + * @author Oleg Kalnichevski + * @author Arjen Poutsma + * @since 3.1 + */ +public class HttpComponentsClientHttpRequestFactory implements ClientHttpRequestFactory, DisposableBean { + + 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 {@code HttpComponentsClientHttpRequestFactory} with a default {@link HttpClient} that + * uses a default {@link org.apache.http.impl.conn.tsccm.ThreadSafeClientConnManager} + */ + public HttpComponentsClientHttpRequestFactory() { + 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); + + httpClient = new DefaultHttpClient(connectionManager); + this.setReadTimeout(DEFAULT_READ_TIMEOUT_MILLISECONDS); + } + + /** + * Create a new instance of the {@code HttpComponentsClientHttpRequestFactory} with the given {@link HttpClient} + * instance. + * + * @param httpClient the HttpClient instance to use for this factory + */ + public HttpComponentsClientHttpRequestFactory(HttpClient httpClient) { + Assert.notNull(httpClient, "httpClient must not be null"); + this.httpClient = httpClient; + } + + /** + * Set the {@code HttpClient} used by this factory. + */ + public void setHttpClient(HttpClient httpClient) { + this.httpClient = 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) + */ + public void setReadTimeout(int timeout) { + if (timeout < 0) { + throw new IllegalArgumentException("timeout must be a non-negative value"); + } + getHttpClient().getParams().setIntParameter(CoreConnectionPNames.SO_TIMEOUT, timeout); + } + + /** + * Return the {@code HttpClient} used by this factory. + */ + public HttpClient getHttpClient() { + return this.httpClient; + } + + public ClientHttpRequest createRequest(URI uri, HttpMethod httpMethod) throws IOException { + HttpUriRequest httpRequest = createHttpUriRequest(httpMethod, uri); + postProcessHttpRequest(httpRequest); + return new HttpComponentsClientHttpRequest(getHttpClient(), httpRequest); + } + + /** + * Create a Commons HttpMethodBase object for the given HTTP method and URI specification. + * + * @param httpMethod the HTTP method + * @param uri the URI + * @return the Commons HttpMethodBase object + */ + protected HttpUriRequest createHttpUriRequest(HttpMethod httpMethod, URI uri) { + switch (httpMethod) { + case GET: + return new HttpGet(uri); + case DELETE: + return new HttpDelete(uri); + case HEAD: + return new HttpHead(uri); + case OPTIONS: + return new HttpOptions(uri); + case POST: + return new HttpPost(uri); + case PUT: + return new HttpPut(uri); + case TRACE: + return new HttpTrace(uri); + default: + throw new IllegalArgumentException("Invalid HTTP method: " + httpMethod); + } + } + + /** + * Template method that allows for manipulating the {@link HttpUriRequest} before it is returned as part of a {@link + * HttpComponentsClientHttpRequest}. + *

The default implementation is empty. + * + * @param request the request to process + */ + protected void postProcessHttpRequest(HttpUriRequest request) { + } + + /** + * Shutdown hook that closes the underlying {@link org.apache.http.conn.ClientConnectionManager + * ClientConnectionManager}'s connection pool, if any. + */ + public void destroy() { + getHttpClient().getConnectionManager().shutdown(); + } +} diff --git a/org.springframework.web/src/main/java/org/springframework/http/client/HttpComponentsClientHttpResponse.java b/org.springframework.web/src/main/java/org/springframework/http/client/HttpComponentsClientHttpResponse.java new file mode 100644 index 00000000000..dbe4c57412e --- /dev/null +++ b/org.springframework.web/src/main/java/org/springframework/http/client/HttpComponentsClientHttpResponse.java @@ -0,0 +1,86 @@ +/* + * 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.http.client; + +import java.io.IOException; +import java.io.InputStream; + +import org.apache.http.Header; +import org.apache.http.HttpEntity; +import org.apache.http.HttpResponse; +import org.apache.http.util.EntityUtils; + +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; + +/** + * {@link org.springframework.http.client.ClientHttpResponse} implementation that uses + * Apache Http Components HttpClient to execute requests. + * + *

Created via the {@link HttpComponentsClientHttpRequest}. + * + * @author Oleg Kalnichevski + * @author Arjen Poutsma + * @since 3.0 + * @see HttpComponentsClientHttpRequest#execute() + */ +final class HttpComponentsClientHttpResponse implements ClientHttpResponse { + + private final HttpResponse httpResponse; + + private HttpHeaders headers; + + public HttpComponentsClientHttpResponse(HttpResponse httpResponse) { + this.httpResponse = httpResponse; + } + + public HttpStatus getStatusCode() throws IOException { + return HttpStatus.valueOf(httpResponse.getStatusLine().getStatusCode()); + } + + public String getStatusText() throws IOException { + return httpResponse.getStatusLine().getReasonPhrase(); + } + + public HttpHeaders getHeaders() { + if (headers == null) { + headers = new HttpHeaders(); + for (Header header : httpResponse.getAllHeaders()) { + headers.add(header.getName(), header.getValue()); + } + } + return headers; + } + + public InputStream getBody() throws IOException { + HttpEntity entity = httpResponse.getEntity(); + return entity != null ? entity.getContent() : null; + } + + public void close() { + HttpEntity entity = httpResponse.getEntity(); + if (entity != null) { + try { + // Release underlying connection back to the connection manager + EntityUtils.consume(entity); + } + catch (IOException e) { + // ignore + } + } + } +} diff --git a/org.springframework.web/src/test/java/org/springframework/http/client/AbstractHttpRequestFactoryTestCase.java b/org.springframework.web/src/test/java/org/springframework/http/client/AbstractHttpRequestFactoryTestCase.java index 2f92bd978c3..14d7d43b950 100644 --- a/org.springframework.web/src/test/java/org/springframework/http/client/AbstractHttpRequestFactoryTestCase.java +++ b/org.springframework.web/src/test/java/org/springframework/http/client/AbstractHttpRequestFactoryTestCase.java @@ -67,7 +67,6 @@ public abstract class AbstractHttpRequestFactoryTestCase { jettyContext.addServlet(new ServletHolder(new MethodServlet("OPTIONS")), "/methods/options"); jettyContext.addServlet(new ServletHolder(new PostServlet()), "/methods/post"); jettyContext.addServlet(new ServletHolder(new MethodServlet("PUT")), "/methods/put"); - jettyContext.addServlet(new ServletHolder(new RedirectServlet("/status/ok")), "/redirect"); jettyServer.start(); } @@ -170,35 +169,6 @@ public abstract class AbstractHttpRequestFactoryTestCase { } } - @Test - public void redirect() throws Exception { - ClientHttpResponse response = null; - try { - ClientHttpRequest request = factory.createRequest(new URI(baseUrl + "/redirect"), HttpMethod.PUT); - response = request.execute(); - assertEquals("Invalid Location value", new URI(baseUrl + "/status/ok"), - response.getHeaders().getLocation()); - - } - finally { - if (response != null) { - response.close(); - response = null; - } - } - try { - ClientHttpRequest request = factory.createRequest(new URI(baseUrl + "/redirect"), HttpMethod.GET); - response = request.execute(); - assertNull("Invalid Location value", response.getHeaders().getLocation()); - - } - finally { - if (response != null) { - response.close(); - } - } - } - /** Servlet that sets a given status code. */ private static class StatusServlet extends GenericServlet { @@ -281,25 +251,4 @@ public abstract class AbstractHttpRequestFactoryTestCase { } } - private static class RedirectServlet extends GenericServlet { - - private final String location; - - private RedirectServlet(String location) { - this.location = location; - } - - @Override - public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException { - HttpServletRequest request = (HttpServletRequest) req; - HttpServletResponse response = (HttpServletResponse) res; - response.setStatus(HttpServletResponse.SC_SEE_OTHER); - StringBuilder builder = new StringBuilder(); - builder.append(request.getScheme()).append("://"); - builder.append(request.getServerName()).append(':').append(request.getServerPort()); - builder.append(location); - response.addHeader("Location", builder.toString()); - } - } - -} \ No newline at end of file +} diff --git a/org.springframework.web/src/test/java/org/springframework/http/client/HttpComponentsClientHttpRequestFactoryTests.java b/org.springframework.web/src/test/java/org/springframework/http/client/HttpComponentsClientHttpRequestFactoryTests.java new file mode 100644 index 00000000000..399db694a49 --- /dev/null +++ b/org.springframework.web/src/test/java/org/springframework/http/client/HttpComponentsClientHttpRequestFactoryTests.java @@ -0,0 +1,25 @@ +/* + * 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.http.client; + +public class HttpComponentsClientHttpRequestFactoryTests extends AbstractHttpRequestFactoryTestCase { + + @Override + protected ClientHttpRequestFactory createRequestFactory() { + return new HttpComponentsClientHttpRequestFactory(); + } +} diff --git a/org.springframework.web/src/test/java/org/springframework/web/client/RestTemplateIntegrationTests.java b/org.springframework.web/src/test/java/org/springframework/web/client/RestTemplateIntegrationTests.java index ffde34977ba..16682b0a68a 100644 --- a/org.springframework.web/src/test/java/org/springframework/web/client/RestTemplateIntegrationTests.java +++ b/org.springframework.web/src/test/java/org/springframework/web/client/RestTemplateIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2010 the original author or authors. + * 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. @@ -54,8 +54,8 @@ import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; -import org.springframework.http.client.CommonsClientHttpRequestFactory; import org.springframework.http.client.FreePortScanner; +import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; import org.springframework.util.FileCopyUtils; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; @@ -97,7 +97,7 @@ public class RestTemplateIntegrationTests { @Before public void createTemplate() { - template = new RestTemplate(new CommonsClientHttpRequestFactory()); + template = new RestTemplate(new HttpComponentsClientHttpRequestFactory()); } @AfterClass diff --git a/org.springframework.web/template.mf b/org.springframework.web/template.mf index d6816d10346..a8bc0c66a41 100644 --- a/org.springframework.web/template.mf +++ b/org.springframework.web/template.mf @@ -21,6 +21,7 @@ Import-Template: org.apache.commons.fileupload.*;version="[1.2.0, 2.0.0)";resolution:=optional, org.apache.commons.httpclient.*;version="[3.1.0, 4.0.0)";resolution:=optional, org.apache.commons.logging.*;version="[1.1.1, 2.0.0)", + org.apache.http.*;version="[4.1, 5.0.0)", org.apache.log4j.*;version="[1.2.15, 2.0.0)";resolution:=optional, org.springframework.aop.*;version=${spring.osgi.range}, org.springframework.beans.*;version=${spring.osgi.range},