001/*
002 * Copyright 2002-2017 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.remoting.httpinvoker;
018
019import java.io.ByteArrayOutputStream;
020import java.io.IOException;
021import java.io.InputStream;
022import java.util.Locale;
023import java.util.zip.GZIPInputStream;
024
025import org.apache.http.Header;
026import org.apache.http.HttpResponse;
027import org.apache.http.NoHttpResponseException;
028import org.apache.http.StatusLine;
029import org.apache.http.client.HttpClient;
030import org.apache.http.client.config.RequestConfig;
031import org.apache.http.client.methods.Configurable;
032import org.apache.http.client.methods.HttpPost;
033import org.apache.http.config.Registry;
034import org.apache.http.config.RegistryBuilder;
035import org.apache.http.conn.socket.ConnectionSocketFactory;
036import org.apache.http.conn.socket.PlainConnectionSocketFactory;
037import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
038import org.apache.http.entity.ByteArrayEntity;
039import org.apache.http.impl.client.HttpClientBuilder;
040import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
041
042import org.springframework.context.i18n.LocaleContext;
043import org.springframework.context.i18n.LocaleContextHolder;
044import org.springframework.remoting.support.RemoteInvocationResult;
045import org.springframework.util.Assert;
046import org.springframework.util.ClassUtils;
047import org.springframework.util.StringUtils;
048
049/**
050 * {@link org.springframework.remoting.httpinvoker.HttpInvokerRequestExecutor} implementation that uses
051 * <a href="https://hc.apache.org/httpcomponents-client-ga/httpclient/">Apache HttpComponents HttpClient</a>
052 * to execute POST requests.
053 *
054 * <p>Allows to use a pre-configured {@link org.apache.http.client.HttpClient}
055 * instance, potentially with authentication, HTTP connection pooling, etc.
056 * Also designed for easy subclassing, providing specific template methods.
057 *
058 * <p>As of Spring 4.1, this request executor requires Apache HttpComponents 4.3 or higher.
059 *
060 * @author Juergen Hoeller
061 * @author Stephane Nicoll
062 * @since 3.1
063 * @see org.springframework.remoting.httpinvoker.SimpleHttpInvokerRequestExecutor
064 */
065public class HttpComponentsHttpInvokerRequestExecutor extends AbstractHttpInvokerRequestExecutor {
066
067        private static final int DEFAULT_MAX_TOTAL_CONNECTIONS = 100;
068
069        private static final int DEFAULT_MAX_CONNECTIONS_PER_ROUTE = 5;
070
071        private static final int DEFAULT_READ_TIMEOUT_MILLISECONDS = (60 * 1000);
072
073
074        private static Class<?> abstractHttpClientClass;
075
076        static {
077                try {
078                        // Looking for AbstractHttpClient class (deprecated as of HttpComponents 4.3)
079                        abstractHttpClientClass = ClassUtils.forName("org.apache.http.impl.client.AbstractHttpClient",
080                                        HttpComponentsHttpInvokerRequestExecutor.class.getClassLoader());
081                }
082                catch (ClassNotFoundException ex) {
083                        // Probably removed from HttpComponents in the meantime...
084                }
085        }
086
087
088        private HttpClient httpClient;
089
090        private RequestConfig requestConfig;
091
092
093        /**
094         * Create a new instance of the HttpComponentsHttpInvokerRequestExecutor with a default
095         * {@link HttpClient} that uses a default {@code org.apache.http.impl.conn.PoolingClientConnectionManager}.
096         */
097        public HttpComponentsHttpInvokerRequestExecutor() {
098                this(createDefaultHttpClient(), RequestConfig.custom()
099                                .setSocketTimeout(DEFAULT_READ_TIMEOUT_MILLISECONDS).build());
100        }
101
102        /**
103         * Create a new instance of the HttpComponentsClientHttpRequestFactory
104         * with the given {@link HttpClient} instance.
105         * @param httpClient the HttpClient instance to use for this request executor
106         */
107        public HttpComponentsHttpInvokerRequestExecutor(HttpClient httpClient) {
108                this(httpClient, null);
109        }
110
111        private HttpComponentsHttpInvokerRequestExecutor(HttpClient httpClient, RequestConfig requestConfig) {
112                this.httpClient = httpClient;
113                this.requestConfig = requestConfig;
114        }
115
116
117        private static HttpClient createDefaultHttpClient() {
118                Registry<ConnectionSocketFactory> schemeRegistry = RegistryBuilder.<ConnectionSocketFactory>create()
119                                .register("http", PlainConnectionSocketFactory.getSocketFactory())
120                                .register("https", SSLConnectionSocketFactory.getSocketFactory())
121                                .build();
122
123                PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager(schemeRegistry);
124                connectionManager.setMaxTotal(DEFAULT_MAX_TOTAL_CONNECTIONS);
125                connectionManager.setDefaultMaxPerRoute(DEFAULT_MAX_CONNECTIONS_PER_ROUTE);
126
127                return HttpClientBuilder.create().setConnectionManager(connectionManager).build();
128        }
129
130
131        /**
132         * Set the {@link HttpClient} instance to use for this request executor.
133         */
134        public void setHttpClient(HttpClient httpClient) {
135                this.httpClient = httpClient;
136        }
137
138        /**
139         * Return the {@link HttpClient} instance that this request executor uses.
140         */
141        public HttpClient getHttpClient() {
142                return this.httpClient;
143        }
144
145        /**
146         * Set the connection timeout for the underlying HttpClient.
147         * A timeout value of 0 specifies an infinite timeout.
148         * <p>Additional properties can be configured by specifying a
149         * {@link RequestConfig} instance on a custom {@link HttpClient}.
150         * @param timeout the timeout value in milliseconds
151         * @see RequestConfig#getConnectTimeout()
152         */
153        public void setConnectTimeout(int timeout) {
154                Assert.isTrue(timeout >= 0, "Timeout must be a non-negative value");
155                this.requestConfig = cloneRequestConfig().setConnectTimeout(timeout).build();
156                setLegacyConnectionTimeout(getHttpClient(), timeout);
157        }
158
159        /**
160         * Apply the specified connection timeout to deprecated {@link HttpClient}
161         * implementations.
162         * <p>As of HttpClient 4.3, default parameters have to be exposed through a
163         * {@link RequestConfig} instance instead of setting the parameters on the
164         * client. Unfortunately, this behavior is not backward-compatible and older
165         * {@link HttpClient} implementations will ignore the {@link RequestConfig}
166         * object set in the context.
167         * <p>If the specified client is an older implementation, we set the custom
168         * connection timeout through the deprecated API. Otherwise, we just return
169         * as it is set through {@link RequestConfig} with newer clients.
170         * @param client the client to configure
171         * @param timeout the custom connection timeout
172         */
173        @SuppressWarnings("deprecation")
174        private void setLegacyConnectionTimeout(HttpClient client, int timeout) {
175                if (abstractHttpClientClass != null && abstractHttpClientClass.isInstance(client)) {
176                        client.getParams().setIntParameter(org.apache.http.params.CoreConnectionPNames.CONNECTION_TIMEOUT, timeout);
177                }
178        }
179
180        /**
181         * Set the timeout in milliseconds used when requesting a connection from the connection
182         * manager using the underlying HttpClient.
183         * A timeout value of 0 specifies an infinite timeout.
184         * <p>Additional properties can be configured by specifying a
185         * {@link RequestConfig} instance on a custom {@link HttpClient}.
186         * @param connectionRequestTimeout the timeout value to request a connection in milliseconds
187         * @see RequestConfig#getConnectionRequestTimeout()
188         */
189        public void setConnectionRequestTimeout(int connectionRequestTimeout) {
190                this.requestConfig = cloneRequestConfig().setConnectionRequestTimeout(connectionRequestTimeout).build();
191        }
192
193        /**
194         * Set the socket read timeout for the underlying HttpClient.
195         * A timeout value of 0 specifies an infinite timeout.
196         * <p>Additional properties can be configured by specifying a
197         * {@link RequestConfig} instance on a custom {@link HttpClient}.
198         * @param timeout the timeout value in milliseconds
199         * @see #DEFAULT_READ_TIMEOUT_MILLISECONDS
200         * @see RequestConfig#getSocketTimeout()
201         */
202        public void setReadTimeout(int timeout) {
203                Assert.isTrue(timeout >= 0, "Timeout must be a non-negative value");
204                this.requestConfig = cloneRequestConfig().setSocketTimeout(timeout).build();
205                setLegacySocketTimeout(getHttpClient(), timeout);
206        }
207
208        /**
209         * Apply the specified socket timeout to deprecated {@link HttpClient}
210         * implementations. See {@link #setLegacyConnectionTimeout}.
211         * @param client the client to configure
212         * @param timeout the custom socket timeout
213         * @see #setLegacyConnectionTimeout
214         */
215        @SuppressWarnings("deprecation")
216        private void setLegacySocketTimeout(HttpClient client, int timeout) {
217                if (abstractHttpClientClass != null && abstractHttpClientClass.isInstance(client)) {
218                        client.getParams().setIntParameter(org.apache.http.params.CoreConnectionPNames.SO_TIMEOUT, timeout);
219                }
220        }
221
222        private RequestConfig.Builder cloneRequestConfig() {
223                return (this.requestConfig != null ? RequestConfig.copy(this.requestConfig) : RequestConfig.custom());
224        }
225
226
227        /**
228         * Execute the given request through the HttpClient.
229         * <p>This method implements the basic processing workflow:
230         * The actual work happens in this class's template methods.
231         * @see #createHttpPost
232         * @see #setRequestBody
233         * @see #executeHttpPost
234         * @see #validateResponse
235         * @see #getResponseBody
236         */
237        @Override
238        protected RemoteInvocationResult doExecuteRequest(
239                        HttpInvokerClientConfiguration config, ByteArrayOutputStream baos)
240                        throws IOException, ClassNotFoundException {
241
242                HttpPost postMethod = createHttpPost(config);
243                setRequestBody(config, postMethod, baos);
244                try {
245                        HttpResponse response = executeHttpPost(config, getHttpClient(), postMethod);
246                        validateResponse(config, response);
247                        InputStream responseBody = getResponseBody(config, response);
248                        return readRemoteInvocationResult(responseBody, config.getCodebaseUrl());
249                }
250                finally {
251                        postMethod.releaseConnection();
252                }
253        }
254
255        /**
256         * Create a HttpPost for the given configuration.
257         * <p>The default implementation creates a standard HttpPost with
258         * "application/x-java-serialized-object" as "Content-Type" header.
259         * @param config the HTTP invoker configuration that specifies the
260         * target service
261         * @return the HttpPost instance
262         * @throws java.io.IOException if thrown by I/O methods
263         */
264        protected HttpPost createHttpPost(HttpInvokerClientConfiguration config) throws IOException {
265                HttpPost httpPost = new HttpPost(config.getServiceUrl());
266
267                RequestConfig requestConfig = createRequestConfig(config);
268                if (requestConfig != null) {
269                        httpPost.setConfig(requestConfig);
270                }
271
272                LocaleContext localeContext = LocaleContextHolder.getLocaleContext();
273                if (localeContext != null) {
274                        Locale locale = localeContext.getLocale();
275                        if (locale != null) {
276                                httpPost.addHeader(HTTP_HEADER_ACCEPT_LANGUAGE, StringUtils.toLanguageTag(locale));
277                        }
278                }
279
280                if (isAcceptGzipEncoding()) {
281                        httpPost.addHeader(HTTP_HEADER_ACCEPT_ENCODING, ENCODING_GZIP);
282                }
283
284                return httpPost;
285        }
286
287        /**
288         * Create a {@link RequestConfig} for the given configuration. Can return {@code null}
289         * to indicate that no custom request config should be set and the defaults of the
290         * {@link HttpClient} should be used.
291         * <p>The default implementation tries to merge the defaults of the client with the
292         * local customizations of the instance, if any.
293         * @param config the HTTP invoker configuration that specifies the
294         * target service
295         * @return the RequestConfig to use
296         */
297        protected RequestConfig createRequestConfig(HttpInvokerClientConfiguration config) {
298                HttpClient client = getHttpClient();
299                if (client instanceof Configurable) {
300                        RequestConfig clientRequestConfig = ((Configurable) client).getConfig();
301                        return mergeRequestConfig(clientRequestConfig);
302                }
303                return this.requestConfig;
304        }
305
306        private RequestConfig mergeRequestConfig(RequestConfig defaultRequestConfig) {
307                if (this.requestConfig == null) {  // nothing to merge
308                        return defaultRequestConfig;
309                }
310
311                RequestConfig.Builder builder = RequestConfig.copy(defaultRequestConfig);
312                int connectTimeout = this.requestConfig.getConnectTimeout();
313                if (connectTimeout >= 0) {
314                        builder.setConnectTimeout(connectTimeout);
315                }
316                int connectionRequestTimeout = this.requestConfig.getConnectionRequestTimeout();
317                if (connectionRequestTimeout >= 0) {
318                        builder.setConnectionRequestTimeout(connectionRequestTimeout);
319                }
320                int socketTimeout = this.requestConfig.getSocketTimeout();
321                if (socketTimeout >= 0) {
322                        builder.setSocketTimeout(socketTimeout);
323                }
324                return builder.build();
325        }
326
327        /**
328         * Set the given serialized remote invocation as request body.
329         * <p>The default implementation simply sets the serialized invocation as the
330         * HttpPost's request body. This can be overridden, for example, to write a
331         * specific encoding and to potentially set appropriate HTTP request headers.
332         * @param config the HTTP invoker configuration that specifies the target service
333         * @param httpPost the HttpPost to set the request body on
334         * @param baos the ByteArrayOutputStream that contains the serialized
335         * RemoteInvocation object
336         * @throws java.io.IOException if thrown by I/O methods
337         */
338        protected void setRequestBody(
339                        HttpInvokerClientConfiguration config, HttpPost httpPost, ByteArrayOutputStream baos)
340                        throws IOException {
341
342                ByteArrayEntity entity = new ByteArrayEntity(baos.toByteArray());
343                entity.setContentType(getContentType());
344                httpPost.setEntity(entity);
345        }
346
347        /**
348         * Execute the given HttpPost instance.
349         * @param config the HTTP invoker configuration that specifies the target service
350         * @param httpClient the HttpClient to execute on
351         * @param httpPost the HttpPost to execute
352         * @return the resulting HttpResponse
353         * @throws java.io.IOException if thrown by I/O methods
354         */
355        protected HttpResponse executeHttpPost(
356                        HttpInvokerClientConfiguration config, HttpClient httpClient, HttpPost httpPost)
357                        throws IOException {
358
359                return httpClient.execute(httpPost);
360        }
361
362        /**
363         * Validate the given response as contained in the HttpPost object,
364         * throwing an exception if it does not correspond to a successful HTTP response.
365         * <p>Default implementation rejects any HTTP status code beyond 2xx, to avoid
366         * parsing the response body and trying to deserialize from a corrupted stream.
367         * @param config the HTTP invoker configuration that specifies the target service
368         * @param response the resulting HttpResponse to validate
369         * @throws java.io.IOException if validation failed
370         */
371        protected void validateResponse(HttpInvokerClientConfiguration config, HttpResponse response)
372                        throws IOException {
373
374                StatusLine status = response.getStatusLine();
375                if (status.getStatusCode() >= 300) {
376                        throw new NoHttpResponseException(
377                                        "Did not receive successful HTTP response: status code = " + status.getStatusCode() +
378                                        ", status message = [" + status.getReasonPhrase() + "]");
379                }
380        }
381
382        /**
383         * Extract the response body from the given executed remote invocation request.
384         * <p>The default implementation simply fetches the HttpPost's response body stream.
385         * If the response is recognized as GZIP response, the InputStream will get wrapped
386         * in a GZIPInputStream.
387         * @param config the HTTP invoker configuration that specifies the target service
388         * @param httpResponse the resulting HttpResponse to read the response body from
389         * @return an InputStream for the response body
390         * @throws java.io.IOException if thrown by I/O methods
391         * @see #isGzipResponse
392         * @see java.util.zip.GZIPInputStream
393         */
394        protected InputStream getResponseBody(HttpInvokerClientConfiguration config, HttpResponse httpResponse)
395                        throws IOException {
396
397                if (isGzipResponse(httpResponse)) {
398                        return new GZIPInputStream(httpResponse.getEntity().getContent());
399                }
400                else {
401                        return httpResponse.getEntity().getContent();
402                }
403        }
404
405        /**
406         * Determine whether the given response indicates a GZIP response.
407         * <p>The default implementation checks whether the HTTP "Content-Encoding"
408         * header contains "gzip" (in any casing).
409         * @param httpResponse the resulting HttpResponse to check
410         * @return whether the given response indicates a GZIP response
411         */
412        protected boolean isGzipResponse(HttpResponse httpResponse) {
413                Header encodingHeader = httpResponse.getFirstHeader(HTTP_HEADER_CONTENT_ENCODING);
414                return (encodingHeader != null && encodingHeader.getValue() != null &&
415                                encodingHeader.getValue().toLowerCase().contains(ENCODING_GZIP));
416        }
417
418}