001/*
002 * Copyright 2002-2020 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;
022import java.util.function.BiFunction;
023
024import org.apache.http.client.HttpClient;
025import org.apache.http.client.config.RequestConfig;
026import org.apache.http.client.methods.Configurable;
027import org.apache.http.client.methods.HttpEntityEnclosingRequestBase;
028import org.apache.http.client.methods.HttpGet;
029import org.apache.http.client.methods.HttpHead;
030import org.apache.http.client.methods.HttpOptions;
031import org.apache.http.client.methods.HttpPatch;
032import org.apache.http.client.methods.HttpPost;
033import org.apache.http.client.methods.HttpPut;
034import org.apache.http.client.methods.HttpTrace;
035import org.apache.http.client.methods.HttpUriRequest;
036import org.apache.http.client.protocol.HttpClientContext;
037import org.apache.http.impl.client.HttpClients;
038import org.apache.http.protocol.HttpContext;
039
040import org.springframework.beans.factory.DisposableBean;
041import org.springframework.http.HttpMethod;
042import org.springframework.lang.Nullable;
043import org.springframework.util.Assert;
044
045/**
046 * {@link org.springframework.http.client.ClientHttpRequestFactory} implementation that
047 * uses <a href="https://hc.apache.org/httpcomponents-client-ga/">Apache HttpComponents
048 * HttpClient</a> to create requests.
049 *
050 * <p>Allows to use a pre-configured {@link HttpClient} instance -
051 * potentially with authentication, HTTP connection pooling, etc.
052 *
053 * <p><b>NOTE:</b> Requires Apache HttpComponents 4.3 or higher, as of Spring 4.0.
054 *
055 * @author Oleg Kalnichevski
056 * @author Arjen Poutsma
057 * @author Stephane Nicoll
058 * @author Juergen Hoeller
059 * @since 3.1
060 */
061public class HttpComponentsClientHttpRequestFactory implements ClientHttpRequestFactory, DisposableBean {
062
063        private HttpClient httpClient;
064
065        @Nullable
066        private RequestConfig requestConfig;
067
068        private boolean bufferRequestBody = true;
069
070        @Nullable
071        private BiFunction<HttpMethod, URI, HttpContext> httpContextFactory;
072
073
074        /**
075         * Create a new instance of the {@code HttpComponentsClientHttpRequestFactory}
076         * with a default {@link HttpClient} based on system properties.
077         */
078        public HttpComponentsClientHttpRequestFactory() {
079                this.httpClient = HttpClients.createSystem();
080        }
081
082        /**
083         * Create a new instance of the {@code HttpComponentsClientHttpRequestFactory}
084         * with the given {@link HttpClient} instance.
085         * @param httpClient the HttpClient instance to use for this request factory
086         */
087        public HttpComponentsClientHttpRequestFactory(HttpClient httpClient) {
088                this.httpClient = httpClient;
089        }
090
091
092        /**
093         * Set the {@code HttpClient} used for
094         * {@linkplain #createRequest(URI, HttpMethod) synchronous execution}.
095         */
096        public void setHttpClient(HttpClient httpClient) {
097                Assert.notNull(httpClient, "HttpClient must not be null");
098                this.httpClient = httpClient;
099        }
100
101        /**
102         * Return the {@code HttpClient} used for
103         * {@linkplain #createRequest(URI, HttpMethod) synchronous execution}.
104         */
105        public HttpClient getHttpClient() {
106                return this.httpClient;
107        }
108
109        /**
110         * Set the connection timeout for the underlying {@link RequestConfig}.
111         * A timeout value of 0 specifies an infinite timeout.
112         * <p>Additional properties can be configured by specifying a
113         * {@link RequestConfig} instance on a custom {@link HttpClient}.
114         * <p>This options does not affect connection timeouts for SSL
115         * handshakes or CONNECT requests; for that, it is required to
116         * use the {@link org.apache.http.config.SocketConfig} on the
117         * {@link HttpClient} itself.
118         * @param timeout the timeout value in milliseconds
119         * @see RequestConfig#getConnectTimeout()
120         * @see org.apache.http.config.SocketConfig#getSoTimeout
121         */
122        public void setConnectTimeout(int timeout) {
123                Assert.isTrue(timeout >= 0, "Timeout must be a non-negative value");
124                this.requestConfig = requestConfigBuilder().setConnectTimeout(timeout).build();
125        }
126
127        /**
128         * Set the timeout in milliseconds used when requesting a connection
129         * from the connection manager using the underlying {@link RequestConfig}.
130         * A timeout value of 0 specifies an infinite timeout.
131         * <p>Additional properties can be configured by specifying a
132         * {@link RequestConfig} instance on a custom {@link HttpClient}.
133         * @param connectionRequestTimeout the timeout value to request a connection in milliseconds
134         * @see RequestConfig#getConnectionRequestTimeout()
135         */
136        public void setConnectionRequestTimeout(int connectionRequestTimeout) {
137                this.requestConfig = requestConfigBuilder()
138                                .setConnectionRequestTimeout(connectionRequestTimeout).build();
139        }
140
141        /**
142         * Set the socket read timeout for the underlying {@link RequestConfig}.
143         * A timeout value of 0 specifies an infinite timeout.
144         * <p>Additional properties can be configured by specifying a
145         * {@link RequestConfig} instance on a custom {@link HttpClient}.
146         * @param timeout the timeout value in milliseconds
147         * @see RequestConfig#getSocketTimeout()
148         */
149        public void setReadTimeout(int timeout) {
150                Assert.isTrue(timeout >= 0, "Timeout must be a non-negative value");
151                this.requestConfig = requestConfigBuilder().setSocketTimeout(timeout).build();
152        }
153
154        /**
155         * Indicates whether this request factory should buffer the request body internally.
156         * <p>Default is {@code true}. When sending large amounts of data via POST or PUT, it is
157         * recommended to change this property to {@code false}, so as not to run out of memory.
158         * @since 4.0
159         */
160        public void setBufferRequestBody(boolean bufferRequestBody) {
161                this.bufferRequestBody = bufferRequestBody;
162        }
163
164        /**
165         * Configure a factory to pre-create the {@link HttpContext} for each request.
166         * <p>This may be useful for example in mutual TLS authentication where a
167         * different {@code RestTemplate} for each client certificate such that
168         * all calls made through a given {@code RestTemplate} instance as associated
169         * for the same client identity. {@link HttpClientContext#setUserToken(Object)}
170         * can be used to specify a fixed user token for all requests.
171         * @param httpContextFactory the context factory to use
172         * @since 5.2.7
173         */
174        public void setHttpContextFactory(BiFunction<HttpMethod, URI, HttpContext> httpContextFactory) {
175                this.httpContextFactory = httpContextFactory;
176        }
177
178        @Override
179        public ClientHttpRequest createRequest(URI uri, HttpMethod httpMethod) throws IOException {
180                HttpClient client = getHttpClient();
181
182                HttpUriRequest httpRequest = createHttpUriRequest(httpMethod, uri);
183                postProcessHttpRequest(httpRequest);
184                HttpContext context = createHttpContext(httpMethod, uri);
185                if (context == null) {
186                        context = HttpClientContext.create();
187                }
188
189                // Request configuration not set in the context
190                if (context.getAttribute(HttpClientContext.REQUEST_CONFIG) == null) {
191                        // Use request configuration given by the user, when available
192                        RequestConfig config = null;
193                        if (httpRequest instanceof Configurable) {
194                                config = ((Configurable) httpRequest).getConfig();
195                        }
196                        if (config == null) {
197                                config = createRequestConfig(client);
198                        }
199                        if (config != null) {
200                                context.setAttribute(HttpClientContext.REQUEST_CONFIG, config);
201                        }
202                }
203
204                if (this.bufferRequestBody) {
205                        return new HttpComponentsClientHttpRequest(client, httpRequest, context);
206                }
207                else {
208                        return new HttpComponentsStreamingClientHttpRequest(client, httpRequest, context);
209                }
210        }
211
212
213        /**
214         * Return a builder for modifying the factory-level {@link RequestConfig}.
215         * @since 4.2
216         */
217        private RequestConfig.Builder requestConfigBuilder() {
218                return (this.requestConfig != null ? RequestConfig.copy(this.requestConfig) : RequestConfig.custom());
219        }
220
221        /**
222         * Create a default {@link RequestConfig} to use with the given client.
223         * Can return {@code null} to indicate that no custom request config should
224         * be set and the defaults of the {@link HttpClient} should be used.
225         * <p>The default implementation tries to merge the defaults of the client
226         * with the local customizations of this factory instance, if any.
227         * @param client the {@link HttpClient} (or {@code HttpAsyncClient}) to check
228         * @return the actual RequestConfig to use (may be {@code null})
229         * @since 4.2
230         * @see #mergeRequestConfig(RequestConfig)
231         */
232        @Nullable
233        protected RequestConfig createRequestConfig(Object client) {
234                if (client instanceof Configurable) {
235                        RequestConfig clientRequestConfig = ((Configurable) client).getConfig();
236                        return mergeRequestConfig(clientRequestConfig);
237                }
238                return this.requestConfig;
239        }
240
241        /**
242         * Merge the given {@link HttpClient}-level {@link RequestConfig} with
243         * the factory-level {@link RequestConfig}, if necessary.
244         * @param clientConfig the config held by the current
245         * @return the merged request config
246         * @since 4.2
247         */
248        protected RequestConfig mergeRequestConfig(RequestConfig clientConfig) {
249                if (this.requestConfig == null) {  // nothing to merge
250                        return clientConfig;
251                }
252
253                RequestConfig.Builder builder = RequestConfig.copy(clientConfig);
254                int connectTimeout = this.requestConfig.getConnectTimeout();
255                if (connectTimeout >= 0) {
256                        builder.setConnectTimeout(connectTimeout);
257                }
258                int connectionRequestTimeout = this.requestConfig.getConnectionRequestTimeout();
259                if (connectionRequestTimeout >= 0) {
260                        builder.setConnectionRequestTimeout(connectionRequestTimeout);
261                }
262                int socketTimeout = this.requestConfig.getSocketTimeout();
263                if (socketTimeout >= 0) {
264                        builder.setSocketTimeout(socketTimeout);
265                }
266                return builder.build();
267        }
268
269        /**
270         * Create a Commons HttpMethodBase object for the given HTTP method and URI specification.
271         * @param httpMethod the HTTP method
272         * @param uri the URI
273         * @return the Commons HttpMethodBase object
274         */
275        protected HttpUriRequest createHttpUriRequest(HttpMethod httpMethod, URI uri) {
276                switch (httpMethod) {
277                        case GET:
278                                return new HttpGet(uri);
279                        case HEAD:
280                                return new HttpHead(uri);
281                        case POST:
282                                return new HttpPost(uri);
283                        case PUT:
284                                return new HttpPut(uri);
285                        case PATCH:
286                                return new HttpPatch(uri);
287                        case DELETE:
288                                return new HttpDelete(uri);
289                        case OPTIONS:
290                                return new HttpOptions(uri);
291                        case TRACE:
292                                return new HttpTrace(uri);
293                        default:
294                                throw new IllegalArgumentException("Invalid HTTP method: " + httpMethod);
295                }
296        }
297
298        /**
299         * Template method that allows for manipulating the {@link HttpUriRequest} before it is
300         * returned as part of a {@link HttpComponentsClientHttpRequest}.
301         * <p>The default implementation is empty.
302         * @param request the request to process
303         */
304        protected void postProcessHttpRequest(HttpUriRequest request) {
305        }
306
307        /**
308         * Template methods that creates a {@link HttpContext} for the given HTTP method and URI.
309         * <p>The default implementation returns {@code null}.
310         * @param httpMethod the HTTP method
311         * @param uri the URI
312         * @return the http context
313         */
314        @Nullable
315        protected HttpContext createHttpContext(HttpMethod httpMethod, URI uri) {
316                return (this.httpContextFactory != null ? this.httpContextFactory.apply(httpMethod, uri) : null);
317        }
318
319
320        /**
321         * Shutdown hook that closes the underlying
322         * {@link org.apache.http.conn.HttpClientConnectionManager ClientConnectionManager}'s
323         * connection pool, if any.
324         */
325        @Override
326        public void destroy() throws Exception {
327                HttpClient httpClient = getHttpClient();
328                if (httpClient instanceof Closeable) {
329                        ((Closeable) httpClient).close();
330                }
331        }
332
333
334        /**
335         * An alternative to {@link org.apache.http.client.methods.HttpDelete} that
336         * extends {@link org.apache.http.client.methods.HttpEntityEnclosingRequestBase}
337         * rather than {@link org.apache.http.client.methods.HttpRequestBase} and
338         * hence allows HTTP delete with a request body. For use with the RestTemplate
339         * exchange methods which allow the combination of HTTP DELETE with an entity.
340         * @since 4.1.2
341         */
342        private static class HttpDelete extends HttpEntityEnclosingRequestBase {
343
344                public HttpDelete(URI uri) {
345                        super();
346                        setURI(uri);
347                }
348
349                @Override
350                public String getMethod() {
351                        return "DELETE";
352                }
353        }
354
355}