001/*
002 * Copyright 2002-2018 the original author or authors.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *      https://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016
017package org.springframework.http.client;
018
019import java.io.Closeable;
020import java.io.IOException;
021import java.net.URI;
022
023import org.apache.http.client.HttpClient;
024import org.apache.http.client.config.RequestConfig;
025import org.apache.http.client.methods.Configurable;
026import org.apache.http.client.methods.HttpEntityEnclosingRequestBase;
027import org.apache.http.client.methods.HttpGet;
028import org.apache.http.client.methods.HttpHead;
029import org.apache.http.client.methods.HttpOptions;
030import org.apache.http.client.methods.HttpPatch;
031import org.apache.http.client.methods.HttpPost;
032import org.apache.http.client.methods.HttpPut;
033import org.apache.http.client.methods.HttpTrace;
034import org.apache.http.client.methods.HttpUriRequest;
035import org.apache.http.client.protocol.HttpClientContext;
036import org.apache.http.impl.client.HttpClients;
037import org.apache.http.protocol.HttpContext;
038
039import org.springframework.beans.factory.DisposableBean;
040import org.springframework.http.HttpMethod;
041import org.springframework.util.Assert;
042import org.springframework.util.ClassUtils;
043
044/**
045 * {@link org.springframework.http.client.ClientHttpRequestFactory} implementation that
046 * uses <a href="https://hc.apache.org/httpcomponents-client-ga/">Apache HttpComponents
047 * HttpClient</a> to create requests.
048 *
049 * <p>Allows to use a pre-configured {@link HttpClient} instance -
050 * potentially with authentication, HTTP connection pooling, etc.
051 *
052 * <p><b>NOTE:</b> Requires Apache HttpComponents 4.3 or higher, as of Spring 4.0.
053 *
054 * @author Oleg Kalnichevski
055 * @author Arjen Poutsma
056 * @author Stephane Nicoll
057 * @author Juergen Hoeller
058 * @since 3.1
059 */
060public class HttpComponentsClientHttpRequestFactory implements ClientHttpRequestFactory, DisposableBean {
061
062        private static Class<?> abstractHttpClientClass;
063
064        static {
065                try {
066                        // Looking for AbstractHttpClient class (deprecated as of HttpComponents 4.3)
067                        abstractHttpClientClass = ClassUtils.forName("org.apache.http.impl.client.AbstractHttpClient",
068                                        HttpComponentsClientHttpRequestFactory.class.getClassLoader());
069                }
070                catch (ClassNotFoundException ex) {
071                        // Probably removed from HttpComponents in the meantime...
072                }
073        }
074
075
076        private HttpClient httpClient;
077
078        private RequestConfig requestConfig;
079
080        private boolean bufferRequestBody = true;
081
082
083        /**
084         * Create a new instance of the {@code HttpComponentsClientHttpRequestFactory}
085         * with a default {@link HttpClient}.
086         */
087        public HttpComponentsClientHttpRequestFactory() {
088                this.httpClient = HttpClients.createSystem();
089        }
090
091        /**
092         * Create a new instance of the {@code HttpComponentsClientHttpRequestFactory}
093         * with the given {@link HttpClient} instance.
094         * @param httpClient the HttpClient instance to use for this request factory
095         */
096        public HttpComponentsClientHttpRequestFactory(HttpClient httpClient) {
097                setHttpClient(httpClient);
098        }
099
100
101        /**
102         * Set the {@code HttpClient} used for
103         * {@linkplain #createRequest(URI, HttpMethod) synchronous execution}.
104         */
105        public void setHttpClient(HttpClient httpClient) {
106                Assert.notNull(httpClient, "HttpClient must not be null");
107                this.httpClient = httpClient;
108        }
109
110        /**
111         * Return the {@code HttpClient} used for
112         * {@linkplain #createRequest(URI, HttpMethod) synchronous execution}.
113         */
114        public HttpClient getHttpClient() {
115                return this.httpClient;
116        }
117
118        /**
119         * Set the connection timeout for the underlying HttpClient.
120         * A timeout value of 0 specifies an infinite timeout.
121         * <p>Additional properties can be configured by specifying a
122         * {@link RequestConfig} instance on a custom {@link HttpClient}.
123         * @param timeout the timeout value in milliseconds
124         * @see RequestConfig#getConnectTimeout()
125         */
126        public void setConnectTimeout(int timeout) {
127                Assert.isTrue(timeout >= 0, "Timeout must be a non-negative value");
128                this.requestConfig = requestConfigBuilder().setConnectTimeout(timeout).build();
129                setLegacyConnectionTimeout(getHttpClient(), timeout);
130        }
131
132        /**
133         * Apply the specified connection timeout to deprecated {@link HttpClient}
134         * implementations.
135         * <p>As of HttpClient 4.3, default parameters have to be exposed through a
136         * {@link RequestConfig} instance instead of setting the parameters on the
137         * client. Unfortunately, this behavior is not backward-compatible and older
138         * {@link HttpClient} implementations will ignore the {@link RequestConfig}
139         * object set in the context.
140         * <p>If the specified client is an older implementation, we set the custom
141         * connection timeout through the deprecated API. Otherwise, we just return
142         * as it is set through {@link RequestConfig} with newer clients.
143         * @param client the client to configure
144         * @param timeout the custom connection timeout
145         */
146        @SuppressWarnings("deprecation")
147        private void setLegacyConnectionTimeout(HttpClient client, int timeout) {
148                if (abstractHttpClientClass != null && abstractHttpClientClass.isInstance(client)) {
149                        client.getParams().setIntParameter(org.apache.http.params.CoreConnectionPNames.CONNECTION_TIMEOUT, timeout);
150                }
151        }
152
153        /**
154         * Set the timeout in milliseconds used when requesting a connection from the connection
155         * manager using the underlying HttpClient.
156         * A timeout value of 0 specifies an infinite timeout.
157         * <p>Additional properties can be configured by specifying a
158         * {@link RequestConfig} instance on a custom {@link HttpClient}.
159         * @param connectionRequestTimeout the timeout value to request a connection in milliseconds
160         * @see RequestConfig#getConnectionRequestTimeout()
161         */
162        public void setConnectionRequestTimeout(int connectionRequestTimeout) {
163                this.requestConfig = requestConfigBuilder().setConnectionRequestTimeout(connectionRequestTimeout).build();
164        }
165
166        /**
167         * Set the socket read timeout for the underlying HttpClient.
168         * A timeout value of 0 specifies an infinite timeout.
169         * <p>Additional properties can be configured by specifying a
170         * {@link RequestConfig} instance on a custom {@link HttpClient}.
171         * @param timeout the timeout value in milliseconds
172         * @see RequestConfig#getSocketTimeout()
173         */
174        public void setReadTimeout(int timeout) {
175                Assert.isTrue(timeout >= 0, "Timeout must be a non-negative value");
176                this.requestConfig = requestConfigBuilder().setSocketTimeout(timeout).build();
177                setLegacySocketTimeout(getHttpClient(), timeout);
178        }
179
180        /**
181         * Apply the specified socket timeout to deprecated {@link HttpClient}
182         * implementations. See {@link #setLegacyConnectionTimeout}.
183         * @param client the client to configure
184         * @param timeout the custom socket timeout
185         * @see #setLegacyConnectionTimeout
186         */
187        @SuppressWarnings("deprecation")
188        private void setLegacySocketTimeout(HttpClient client, int timeout) {
189                if (abstractHttpClientClass != null && abstractHttpClientClass.isInstance(client)) {
190                        client.getParams().setIntParameter(org.apache.http.params.CoreConnectionPNames.SO_TIMEOUT, timeout);
191                }
192        }
193
194        /**
195         * Indicates whether this request factory should buffer the request body internally.
196         * <p>Default is {@code true}. When sending large amounts of data via POST or PUT, it is
197         * recommended to change this property to {@code false}, so as not to run out of memory.
198         * @since 4.0
199         */
200        public void setBufferRequestBody(boolean bufferRequestBody) {
201                this.bufferRequestBody = bufferRequestBody;
202        }
203
204
205        @Override
206        public ClientHttpRequest createRequest(URI uri, HttpMethod httpMethod) throws IOException {
207                HttpUriRequest httpRequest = createHttpUriRequest(httpMethod, uri);
208                postProcessHttpRequest(httpRequest);
209                HttpContext context = createHttpContext(httpMethod, uri);
210                if (context == null) {
211                        context = HttpClientContext.create();
212                }
213
214                // Request configuration not set in the context
215                if (context.getAttribute(HttpClientContext.REQUEST_CONFIG) == null) {
216                        // Use request configuration given by the user, when available
217                        RequestConfig config = null;
218                        if (httpRequest instanceof Configurable) {
219                                config = ((Configurable) httpRequest).getConfig();
220                        }
221                        if (config == null) {
222                                config = createRequestConfig(getHttpClient());
223                        }
224                        if (config != null) {
225                                context.setAttribute(HttpClientContext.REQUEST_CONFIG, config);
226                        }
227                }
228
229                if (this.bufferRequestBody) {
230                        return new HttpComponentsClientHttpRequest(getHttpClient(), httpRequest, context);
231                }
232                else {
233                        return new HttpComponentsStreamingClientHttpRequest(getHttpClient(), httpRequest, context);
234                }
235        }
236
237
238        /**
239         * Return a builder for modifying the factory-level {@link RequestConfig}.
240         * @since 4.2
241         */
242        private RequestConfig.Builder requestConfigBuilder() {
243                return (this.requestConfig != null ? RequestConfig.copy(this.requestConfig) : RequestConfig.custom());
244        }
245
246        /**
247         * Create a default {@link RequestConfig} to use with the given client.
248         * Can return {@code null} to indicate that no custom request config should
249         * be set and the defaults of the {@link HttpClient} should be used.
250         * <p>The default implementation tries to merge the defaults of the client
251         * with the local customizations of this factory instance, if any.
252         * @param client the {@link HttpClient} (or {@code HttpAsyncClient}) to check
253         * @return the actual RequestConfig to use (may be {@code null})
254         * @since 4.2
255         * @see #mergeRequestConfig(RequestConfig)
256         */
257        protected RequestConfig createRequestConfig(Object client) {
258                if (client instanceof Configurable) {
259                        RequestConfig clientRequestConfig = ((Configurable) client).getConfig();
260                        return mergeRequestConfig(clientRequestConfig);
261                }
262                return this.requestConfig;
263        }
264
265        /**
266         * Merge the given {@link HttpClient}-level {@link RequestConfig} with
267         * the factory-level {@link RequestConfig}, if necessary.
268         * @param clientConfig the config held by the current
269         * @return the merged request config
270         * @since 4.2
271         */
272        protected RequestConfig mergeRequestConfig(RequestConfig clientConfig) {
273                if (this.requestConfig == null) {  // nothing to merge
274                        return clientConfig;
275                }
276
277                RequestConfig.Builder builder = RequestConfig.copy(clientConfig);
278                int connectTimeout = this.requestConfig.getConnectTimeout();
279                if (connectTimeout >= 0) {
280                        builder.setConnectTimeout(connectTimeout);
281                }
282                int connectionRequestTimeout = this.requestConfig.getConnectionRequestTimeout();
283                if (connectionRequestTimeout >= 0) {
284                        builder.setConnectionRequestTimeout(connectionRequestTimeout);
285                }
286                int socketTimeout = this.requestConfig.getSocketTimeout();
287                if (socketTimeout >= 0) {
288                        builder.setSocketTimeout(socketTimeout);
289                }
290                return builder.build();
291        }
292
293        /**
294         * Create a Commons HttpMethodBase object for the given HTTP method and URI specification.
295         * @param httpMethod the HTTP method
296         * @param uri the URI
297         * @return the Commons HttpMethodBase object
298         */
299        protected HttpUriRequest createHttpUriRequest(HttpMethod httpMethod, URI uri) {
300                switch (httpMethod) {
301                        case GET:
302                                return new HttpGet(uri);
303                        case HEAD:
304                                return new HttpHead(uri);
305                        case POST:
306                                return new HttpPost(uri);
307                        case PUT:
308                                return new HttpPut(uri);
309                        case PATCH:
310                                return new HttpPatch(uri);
311                        case DELETE:
312                                return new HttpDelete(uri);
313                        case OPTIONS:
314                                return new HttpOptions(uri);
315                        case TRACE:
316                                return new HttpTrace(uri);
317                        default:
318                                throw new IllegalArgumentException("Invalid HTTP method: " + httpMethod);
319                }
320        }
321
322        /**
323         * Template method that allows for manipulating the {@link HttpUriRequest} before it is
324         * returned as part of a {@link HttpComponentsClientHttpRequest}.
325         * <p>The default implementation is empty.
326         * @param request the request to process
327         */
328        protected void postProcessHttpRequest(HttpUriRequest request) {
329        }
330
331        /**
332         * Template methods that creates a {@link HttpContext} for the given HTTP method and URI.
333         * <p>The default implementation returns {@code null}.
334         * @param httpMethod the HTTP method
335         * @param uri the URI
336         * @return the http context
337         */
338        protected HttpContext createHttpContext(HttpMethod httpMethod, URI uri) {
339                return null;
340        }
341
342
343        /**
344         * Shutdown hook that closes the underlying
345         * {@link org.apache.http.conn.HttpClientConnectionManager ClientConnectionManager}'s
346         * connection pool, if any.
347         */
348        @Override
349        public void destroy() throws Exception {
350                HttpClient httpClient = getHttpClient();
351                if (httpClient instanceof Closeable) {
352                        ((Closeable) httpClient).close();
353                }
354        }
355
356
357        /**
358         * An alternative to {@link org.apache.http.client.methods.HttpDelete} that
359         * extends {@link org.apache.http.client.methods.HttpEntityEnclosingRequestBase}
360         * rather than {@link org.apache.http.client.methods.HttpRequestBase} and
361         * hence allows HTTP delete with a request body. For use with the RestTemplate
362         * exchange methods which allow the combination of HTTP DELETE with an entity.
363         * @since 4.1.2
364         */
365        private static class HttpDelete extends HttpEntityEnclosingRequestBase {
366
367                public HttpDelete(URI uri) {
368                        super();
369                        setURI(uri);
370                }
371
372                @Override
373                public String getMethod() {
374                        return "DELETE";
375                }
376        }
377
378}