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},