/* * Copyright 2002-2014 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.config.RequestConfig; import org.apache.http.client.methods.Configurable; import org.apache.http.client.methods.HttpDeleteHC4; import org.apache.http.client.methods.HttpGetHC4; import org.apache.http.client.methods.HttpHeadHC4; import org.apache.http.client.methods.HttpOptionsHC4; import org.apache.http.client.methods.HttpPatch; import org.apache.http.client.methods.HttpPostHC4; import org.apache.http.client.methods.HttpPutHC4; import org.apache.http.client.methods.HttpTraceHC4; import org.apache.http.client.methods.HttpUriRequest; import org.apache.http.client.protocol.HttpClientContext; import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.HttpClients; import org.apache.http.protocol.HttpContext; 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 <a href="https://hc.apache.org/httpcomponents-client-4.3.x/android-port.html">Apache * HttpComponents Android HttpClient</a> to create requests. * * <p>Allows to use a pre-configured {@link HttpClient} instance - potentially with * authentication, HTTP connection pooling, etc. * * <p><b>NOTE:</b> Requires Apache HttpComponents Android HttpClient 4.3 or higher. * * @author Oleg Kalnichevski * @author Arjen Poutsma * @author Stephane Nicoll * @since 1.0 */ public class HttpComponentsClientHttpRequestFactory implements ClientHttpRequestFactory, DisposableBean { private CloseableHttpClient httpClient; private int connectTimeout; private int socketTimeout; private boolean bufferRequestBody = true; /** * Create a new instance of the {@code HttpComponentsClientHttpRequestFactory} with a * default {@link HttpClient}. */ public HttpComponentsClientHttpRequestFactory() { this(HttpClients.createSystem()); } /** * Create a new instance of the {@code HttpComponentsClientHttpRequestFactory} with * the given {@link HttpClient} instance. * <p> * As of Spring for Android 2.0, the given client is expected to be of type * {@link CloseableHttpClient} (requiring Android HttpClient 4.3+). * @param httpClient the HttpClient instance to use for this request factory */ public HttpComponentsClientHttpRequestFactory(HttpClient httpClient) { Assert.notNull(httpClient, "'httpClient' must not be null"); Assert.isInstanceOf(CloseableHttpClient.class, httpClient, "'httpClient' is not of type CloseableHttpClient"); this.httpClient = (CloseableHttpClient) httpClient; } /** * Set the {@code HttpClient} used for * <p> * As of Spring Framework 4.0, the given client is expected to be of type * {@link CloseableHttpClient} (requiring HttpClient 4.3+). */ public void setHttpClient(HttpClient httpClient) { Assert.isInstanceOf(CloseableHttpClient.class, httpClient, "'httpClient' is not of type CloseableHttpClient"); this.httpClient = (CloseableHttpClient) httpClient; } /** * Return the {@code HttpClient} used for {@linkplain #createRequest(URI, HttpMethod) * synchronous execution}. */ 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"); this.connectTimeout = timeout; setLegacyConnectionTimeout(getHttpClient(), timeout); } /** * Apply the specified connection timeout to deprecated {@link HttpClient} * implementations. * <p> * As of HttpClient 4.3, default parameters have to be exposed through a * {@link RequestConfig} instance instead of setting the parameters on the client. * Unfortunately, this behavior is not backward-compatible and older * {@link HttpClient} implementations will ignore the {@link RequestConfig} object set * in the context. * <p> * If the specified client is an older implementation, we set the custom connection * timeout through the deprecated API. Otherwise, we just return as it is set through * {@link RequestConfig} with newer clients. * @param client the client to configure * @param timeout the custom connection timeout */ @SuppressWarnings("deprecation") private void setLegacyConnectionTimeout(HttpClient client, int timeout) { if (org.apache.http.impl.client.AbstractHttpClient.class.isInstance(client)) { client.getParams().setIntParameter( org.apache.http.params.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 */ public void setReadTimeout(int timeout) { Assert.isTrue(timeout >= 0, "Timeout must be a non-negative value"); this.socketTimeout = timeout; setLegacySocketTimeout(getHttpClient(), timeout); } /** * Apply the specified socket timeout to deprecated {@link HttpClient} * implementations. See {@link #setLegacyConnectionTimeout}. * @param client the client to configure * @param timeout the custom socket timeout * @see #setLegacyConnectionTimeout */ @SuppressWarnings("deprecation") private void setLegacySocketTimeout(HttpClient client, int timeout) { if (org.apache.http.impl.client.AbstractHttpClient.class.isInstance(client)) { client.getParams().setIntParameter( org.apache.http.params.CoreConnectionPNames.SO_TIMEOUT, timeout); } } /** * Indicates whether this request factory should buffer the request body internally. * <p> * Default is {@code true}. When sending large amounts of data via POST or PUT, it is * recommended to change this property to {@code false}, so as not to run out of * memory. */ public void setBufferRequestBody(boolean bufferRequestBody) { this.bufferRequestBody = bufferRequestBody; } @Override public ClientHttpRequest createRequest(URI uri, HttpMethod httpMethod) throws IOException { CloseableHttpClient client = (CloseableHttpClient) getHttpClient(); Assert.state(client != null, "Synchronous execution requires an HttpClient to be set"); HttpUriRequest httpRequest = createHttpUriRequest(httpMethod, uri); postProcessHttpRequest(httpRequest); HttpContext context = createHttpContext(httpMethod, uri); if (context == null) { context = HttpClientContext.create(); } // Request configuration not set in the context if (context.getAttribute(HttpClientContext.REQUEST_CONFIG) == null) { // Use request configuration given by the user, when available RequestConfig config = null; if (httpRequest instanceof Configurable) { config = ((Configurable) httpRequest).getConfig(); } if (config == null) { if (this.socketTimeout > 0 || this.connectTimeout > 0) { config = RequestConfig.custom() .setConnectTimeout(this.connectTimeout) .setSocketTimeout(this.socketTimeout).build(); } else { config = RequestConfig.DEFAULT; } } context.setAttribute(HttpClientContext.REQUEST_CONFIG, config); } if (this.bufferRequestBody) { return new HttpComponentsClientHttpRequest(client, httpRequest, context); } else { return new HttpComponentsStreamingClientHttpRequest(client, httpRequest, context); } } /** * 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 HttpGetHC4(uri); case DELETE: return new HttpDeleteHC4(uri); case HEAD: return new HttpHeadHC4(uri); case OPTIONS: return new HttpOptionsHC4(uri); case POST: return new HttpPostHC4(uri); case PUT: return new HttpPutHC4(uri); case TRACE: return new HttpTraceHC4(uri); case PATCH: return new HttpPatch(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}. * <p> * The default implementation is empty. * @param request the request to process */ protected void postProcessHttpRequest(HttpUriRequest request) { } /** * Template methods that creates a {@link HttpContext} for the given HTTP method and * URI. * <p> * The default implementation returns {@code null}. * @param httpMethod the HTTP method * @param uri the URI * @return the http context */ protected HttpContext createHttpContext(HttpMethod httpMethod, URI uri) { return null; } /** * Shutdown hook that closes the underlying * {@link org.apache.http.conn.HttpClientConnectionManager ClientConnectionManager}'s * connection pool, if any. */ @Override public void destroy() throws Exception { this.httpClient.close(); } }