diff --git a/pom.xml b/pom.xml index fe923485d..c3ef4c783 100644 --- a/pom.xml +++ b/pom.xml @@ -96,6 +96,7 @@ 2.10.9.2 2.0.0-alpha-2 4.5.3 + 5.2.1 2.1.1 3.1.0 2.1.0 diff --git a/spring-ws-core/pom.xml b/spring-ws-core/pom.xml index 19df245bb..eb5c9e843 100644 --- a/spring-ws-core/pom.xml +++ b/spring-ws-core/pom.xml @@ -133,27 +133,17 @@ - org.apache.httpcomponents - httpclient - ${httpclient.version} + org.apache.httpcomponents.client5 + httpclient5 + ${httpclient5.version} true - - - commons-logging - commons-logging - - - commons-httpclient - commons-httpclient - ${commons-httpclient.version} + org.apache.httpcomponents + httpclient + ${httpclient.version} true - - commons-codec - commons-codec - commons-logging commons-logging diff --git a/spring-ws-core/src/main/java/org/springframework/ws/transport/http/CommonsHttpMessageSender.java b/spring-ws-core/src/main/java/org/springframework/ws/transport/http/CommonsHttpMessageSender.java deleted file mode 100644 index f2850601f..000000000 --- a/spring-ws-core/src/main/java/org/springframework/ws/transport/http/CommonsHttpMessageSender.java +++ /dev/null @@ -1,237 +0,0 @@ -/* - * Copyright 2005-2022 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.ws.transport.http; - -import java.io.IOException; -import java.net.URI; -import java.util.Map; - -import org.apache.commons.httpclient.Credentials; -import org.apache.commons.httpclient.HostConfiguration; -import org.apache.commons.httpclient.HttpClient; -import org.apache.commons.httpclient.HttpConnectionManager; -import org.apache.commons.httpclient.HttpURL; -import org.apache.commons.httpclient.HttpsURL; -import org.apache.commons.httpclient.MultiThreadedHttpConnectionManager; -import org.apache.commons.httpclient.NTCredentials; -import org.apache.commons.httpclient.URIException; -import org.apache.commons.httpclient.UsernamePasswordCredentials; -import org.apache.commons.httpclient.auth.AuthScope; -import org.apache.commons.httpclient.methods.PostMethod; -import org.springframework.beans.factory.DisposableBean; -import org.springframework.beans.factory.InitializingBean; -import org.springframework.util.Assert; -import org.springframework.ws.transport.WebServiceConnection; - -/** - * {@code WebServiceMessageSender} implementation that uses - * Jakarta Commons HttpClient to execute POST requests. - *

- * Allows to use a preconfigured HttpClient instance, potentially with authentication, HTTP connection pooling, etc. - * Authentication can also be set by injecting a {@link Credentials} instance (such as the - * {@link UsernamePasswordCredentials}). - * - * @author Arjen Poutsma - * @see HttpUrlConnectionMessageSender - * @see HttpClient - * @see #setCredentials(Credentials) - * @since 1.0.0 - * @deprecated In favor of {@link HttpComponentsMessageSender} - */ -@Deprecated -public class CommonsHttpMessageSender extends AbstractHttpWebServiceMessageSender - implements InitializingBean, DisposableBean { - - private static final int DEFAULT_CONNECTION_TIMEOUT_MILLISECONDS = (60 * 1000); - - private static final int DEFAULT_READ_TIMEOUT_MILLISECONDS = (60 * 1000); - - private HttpClient httpClient; - - private Credentials credentials; - - private AuthScope authScope; - - /** - * Create a new instance of the {@code CommonsHttpMessageSender} with a default {@link HttpClient} that uses a default - * {@link MultiThreadedHttpConnectionManager}. - */ - public CommonsHttpMessageSender() { - httpClient = new HttpClient(new MultiThreadedHttpConnectionManager()); - setConnectionTimeout(DEFAULT_CONNECTION_TIMEOUT_MILLISECONDS); - setReadTimeout(DEFAULT_READ_TIMEOUT_MILLISECONDS); - } - - /** - * Create a new instance of the {@code CommonsHttpMessageSender} with the given {@link HttpClient} instance. - * - * @param httpClient the HttpClient instance to use for this sender - */ - public CommonsHttpMessageSender(HttpClient httpClient) { - Assert.notNull(httpClient, "httpClient must not be null"); - this.httpClient = httpClient; - } - - /** Returns the {@code HttpClient} used by this message sender. */ - public HttpClient getHttpClient() { - return httpClient; - } - - /** Set the {@code HttpClient} used by this message sender. */ - public void setHttpClient(HttpClient httpClient) { - this.httpClient = httpClient; - } - - /** Returns the credentials to be used. */ - public Credentials getCredentials() { - return credentials; - } - - /** - * Sets the credentials to be used. If not set, no authentication is done. - * - * @see UsernamePasswordCredentials - * @see NTCredentials - */ - public void setCredentials(Credentials credentials) { - this.credentials = credentials; - } - - /** - * Sets the timeout until a connection is etablished. A value of 0 means never timeout. - * - * @param timeout the timeout value in milliseconds - * @see org.apache.commons.httpclient.params.HttpConnectionManagerParams#setConnectionTimeout(int) - */ - public void setConnectionTimeout(int timeout) { - if (timeout < 0) { - throw new IllegalArgumentException("timeout must be a non-negative value"); - } - getHttpClient().getHttpConnectionManager().getParams().setConnectionTimeout(timeout); - } - - /** - * 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().getHttpConnectionManager().getParams().setSoTimeout(timeout); - } - - /** - * Sets the maximum number of connections allowed for the underlying HttpClient. - * - * @param maxTotalConnections the maximum number of connections allowed - * @see org.apache.commons.httpclient.params.HttpConnectionManagerParams#setMaxTotalConnections(int) - */ - public void setMaxTotalConnections(int maxTotalConnections) { - if (maxTotalConnections <= 0) { - throw new IllegalArgumentException("maxTotalConnections must be a positive value"); - } - getHttpClient().getHttpConnectionManager().getParams().setMaxTotalConnections(maxTotalConnections); - } - - /** - * Sets the maximum number of connections per host for the underlying HttpClient. The maximum number of connections - * per host can be set in a form accepted by the {@code java.util.Properties} class, like as follows: - * - *

-	 * https://www.example.com=1
-	 * http://www.example.com:8080=7
-	 * www.springframework.org=10
-	 * *=5
-	 * 
- * - * The host can be specified as hostname, or as URI (with scheme and port). The special host name {@code *} can be - * used to specify {@link org.apache.commons.httpclient.HostConfiguration#ANY_HOST_CONFIGURATION}. - * - * @param maxConnectionsPerHost a properties object specifying the maximum number of connection - * @see org.apache.commons.httpclient.params.HttpConnectionManagerParams#setMaxConnectionsPerHost(org.apache.commons.httpclient.HostConfiguration, - * int) - */ - public void setMaxConnectionsPerHost(Map maxConnectionsPerHost) throws URIException { - for (String host : maxConnectionsPerHost.keySet()) { - HostConfiguration hostConfiguration = new HostConfiguration(); - if ("*".equals(host)) { - hostConfiguration = HostConfiguration.ANY_HOST_CONFIGURATION; - } else if (host.startsWith("http://")) { - HttpURL httpURL = new HttpURL(host); - hostConfiguration.setHost(httpURL); - } else if (host.startsWith("https://")) { - HttpsURL httpsURL = new HttpsURL(host); - hostConfiguration.setHost(httpsURL); - } else { - hostConfiguration.setHost(host); - } - int maxHostConnections = Integer.parseInt(maxConnectionsPerHost.get(host)); - getHttpClient().getHttpConnectionManager().getParams().setMaxConnectionsPerHost(hostConfiguration, - maxHostConnections); - } - } - - /** - * Returns the authentication scope to be used. Only used when the {@code credentials} property has been set. - *

- * By default, the {@link AuthScope#ANY} is returned. - */ - public AuthScope getAuthScope() { - return authScope != null ? authScope : AuthScope.ANY; - } - - /** - * Sets the authentication scope to be used. Only used when the {@code credentials} property has been set. - *

- * By default, the {@link AuthScope#ANY} is used. - * - * @see #setCredentials(Credentials) - */ - public void setAuthScope(AuthScope authScope) { - this.authScope = authScope; - } - - @Override - public void afterPropertiesSet() throws Exception { - if (getCredentials() != null) { - getHttpClient().getState().setCredentials(getAuthScope(), getCredentials()); - getHttpClient().getParams().setAuthenticationPreemptive(true); - } - } - - @Override - public void destroy() throws Exception { - HttpConnectionManager connectionManager = getHttpClient().getHttpConnectionManager(); - if (connectionManager instanceof MultiThreadedHttpConnectionManager) { - ((MultiThreadedHttpConnectionManager) connectionManager).shutdown(); - } - } - - @Override - public WebServiceConnection createConnection(URI uri) throws IOException { - PostMethod postMethod = new PostMethod(uri.toString()); - if (isAcceptGzipEncoding()) { - postMethod.addRequestHeader(HttpTransportConstants.HEADER_ACCEPT_ENCODING, - HttpTransportConstants.CONTENT_ENCODING_GZIP); - } - return new CommonsHttpConnection(getHttpClient(), postMethod); - } - -} diff --git a/spring-ws-core/src/main/java/org/springframework/ws/transport/http/HttpComponents5ClientFactory.java b/spring-ws-core/src/main/java/org/springframework/ws/transport/http/HttpComponents5ClientFactory.java new file mode 100644 index 000000000..82fc6075d --- /dev/null +++ b/spring-ws-core/src/main/java/org/springframework/ws/transport/http/HttpComponents5ClientFactory.java @@ -0,0 +1,239 @@ +/* + * Copyright 2005-2023 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.ws.transport.http; + +import java.net.URI; +import java.net.URISyntaxException; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +import org.apache.hc.client5.http.HttpRoute; +import org.apache.hc.client5.http.auth.AuthScope; +import org.apache.hc.client5.http.auth.Credentials; +import org.apache.hc.client5.http.config.RequestConfig; +import org.apache.hc.client5.http.impl.auth.BasicCredentialsProvider; +import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; +import org.apache.hc.client5.http.impl.classic.HttpClientBuilder; +import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManager; +import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManagerBuilder; +import org.apache.hc.core5.http.HttpHost; + +import org.springframework.beans.factory.FactoryBean; + +/** + * {@code FactoryBean} to set up a Apache + * CloseableHttpClient + * + * @author Lars Uffmann + * @since 4.0.5 + */ +public class HttpComponents5ClientFactory implements FactoryBean { + private static final int DEFAULT_CONNECTION_TIMEOUT_MILLISECONDS = (60 * 1000); + + private int connectionTimeout = DEFAULT_CONNECTION_TIMEOUT_MILLISECONDS; + private static final int DEFAULT_READ_TIMEOUT_MILLISECONDS = (60 * 1000); + + private int readTimeout = DEFAULT_READ_TIMEOUT_MILLISECONDS; + + private int maxTotalConnections = -1; + private AuthScope authScope = null; + + private Credentials credentials = null; + + private Map maxConnectionsPerHost = Map.of(); + + private PoolingHttpClientConnectionManager connectionManager; + + private HttpClientBuilderCustomizer clientBuilderCustomizer; + + private PoolingHttpClientConnectionManagerBuilderCustomizer connectionManagerBuilderCustomizer; + + /** + * Sets the credentials to be used. If not set, no authentication is done. + * + * @see org.apache.hc.client5.http.auth.UsernamePasswordCredentials + * @see org.apache.hc.client5.http.auth.NTCredentials + */ + public void setCredentials(Credentials credentials) { + this.credentials = credentials; + } + + /** + * Sets the authentication scope to be used. Only used when the {@code credentials} property has been set. + *

+ * By default, the {@link AuthScope#ANY} is used. + * + * @see #setCredentials(Credentials) + */ + public void setAuthScope(AuthScope authScope) { + this.authScope = authScope; + } + + /** + * Sets the timeout until a connection is established. A value of 0 means never timeout. + * + * @param timeout the timeout value in milliseconds + */ + public void setConnectionTimeout(int timeout) { + if (timeout < 0) { + throw new IllegalArgumentException("timeout must be a non-negative value"); + } + this.connectionTimeout = timeout; + } + + /** + * Set the socket read timeout for the underlying HttpClient. A value of 0 means never timeout. + * + * @param timeout the timeout value in milliseconds + */ + public void setReadTimeout(int timeout) { + if (timeout < 0) { + throw new IllegalArgumentException("timeout must be a non-negative value"); + } + this.readTimeout = timeout; + } + + /** + * Sets the maximum number of connections allowed for the underlying HttpClient. + * + * @param maxTotalConnections the maximum number of connections allowed + * @see PoolingHttpClientConnectionManager... + */ + public void setMaxTotalConnections(int maxTotalConnections) { + if (maxTotalConnections <= 0) { + throw new IllegalArgumentException("maxTotalConnections must be a positive value"); + } + this.maxTotalConnections = maxTotalConnections; + } + + /** + * Sets the maximum number of connections per host for the underlying HttpClient. The maximum number of connections + * per host can be set in a form accepted by the {@code java.util.Properties} class, like as follows: + * + *

+	 * https://www.example.com=1
+	 * http://www.example.com:8080=7
+	 * http://www.springframework.org=10
+	 * 
+ *

+ * The host can be specified as a URI (with scheme and port). + * + * @param maxConnectionsPerHost a properties object specifying the maximum number of connection + * @see PoolingHttpClientConnectionManager... + */ + public void setMaxConnectionsPerHost(Map maxConnectionsPerHost) { + this.maxConnectionsPerHost = maxConnectionsPerHost; + } + + void applyMaxConnectionsPerHost(PoolingHttpClientConnectionManager connectionManager) throws URISyntaxException { + + for (Map.Entry entry : maxConnectionsPerHost.entrySet()) { + URI uri = new URI(entry.getKey()); + HttpHost host = new HttpHost(uri.getScheme(), uri.getHost(), getPort(uri)); + final HttpRoute route; + + if (uri.getScheme().equals("https")) { + route = new HttpRoute(host, null, true); + } + else { + route = new HttpRoute(host); + } + int max = Integer.parseInt(entry.getValue()); + connectionManager.setMaxPerRoute(route, max); + } + } + + static int getPort(URI uri) { + if (uri.getPort() == -1) { + if ("https".equalsIgnoreCase(uri.getScheme())) { + return 443; + } + if ("http".equalsIgnoreCase(uri.getScheme())) { + return 80; + } + } + return uri.getPort(); + } + + @Override + public boolean isSingleton() { + return true; + } + + @Override + public CloseableHttpClient getObject() throws Exception { + PoolingHttpClientConnectionManagerBuilder connectionManagerBuilder = PoolingHttpClientConnectionManagerBuilder.create(); + if (this.maxTotalConnections != -1) { + connectionManagerBuilder.setMaxConnTotal(this.maxTotalConnections); + } + + if (null != this.connectionManagerBuilderCustomizer) { + this.connectionManagerBuilderCustomizer.customize(connectionManagerBuilder); + } + + this.connectionManager = connectionManagerBuilder.build(); + + applyMaxConnectionsPerHost(connectionManager); + + RequestConfig.Builder requestConfigBuilder = RequestConfig.custom() + .setConnectTimeout(connectionTimeout, TimeUnit.MILLISECONDS) + .setResponseTimeout(readTimeout, TimeUnit.MILLISECONDS); + + HttpClientBuilder httpClientBuilder = HttpClientBuilder.create() + .setDefaultRequestConfig(requestConfigBuilder.build()) + .setConnectionManager(connectionManager); + + if (null != credentials && null != authScope) { + BasicCredentialsProvider basicCredentialsProvider = new BasicCredentialsProvider(); + basicCredentialsProvider.setCredentials(authScope, credentials); + httpClientBuilder.setDefaultCredentialsProvider(basicCredentialsProvider); + } + + if (null != this.clientBuilderCustomizer) { + clientBuilderCustomizer.customize(httpClientBuilder); + } + + return httpClientBuilder.build(); + } + + @Override + public Class getObjectType() { + return CloseableHttpClient.class; + } + + PoolingHttpClientConnectionManager getConnectionManager() { + return this.connectionManager; + } + + public void setClientBuilderCustomizer(HttpClientBuilderCustomizer clientBuilderCustomizer) { + this.clientBuilderCustomizer = clientBuilderCustomizer; + } + + public void setConnectionManagerBuilderCustomizer(PoolingHttpClientConnectionManagerBuilderCustomizer connectionManagerBuilderCustomizer) { + this.connectionManagerBuilderCustomizer = connectionManagerBuilderCustomizer; + } + + @FunctionalInterface + public interface HttpClientBuilderCustomizer { + void customize(HttpClientBuilder httpClientBuilder); + } + + @FunctionalInterface + public interface PoolingHttpClientConnectionManagerBuilderCustomizer { + void customize(PoolingHttpClientConnectionManagerBuilder poolingHttpClientConnectionManagerBuilder); + } +} diff --git a/spring-ws-core/src/main/java/org/springframework/ws/transport/http/CommonsHttpConnection.java b/spring-ws-core/src/main/java/org/springframework/ws/transport/http/HttpComponents5Connection.java similarity index 53% rename from spring-ws-core/src/main/java/org/springframework/ws/transport/http/CommonsHttpConnection.java rename to spring-ws-core/src/main/java/org/springframework/ws/transport/http/HttpComponents5Connection.java index 6ace8061c..d0e0bdb9c 100644 --- a/spring-ws-core/src/main/java/org/springframework/ws/transport/http/CommonsHttpConnection.java +++ b/spring-ws-core/src/main/java/org/springframework/ws/transport/http/HttpComponents5Connection.java @@ -1,5 +1,5 @@ /* - * Copyright 2005-2022 the original author or authors. + * Copyright 2005-2023 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. @@ -25,71 +25,80 @@ import java.util.Arrays; import java.util.Iterator; -import org.apache.commons.httpclient.Header; -import org.apache.commons.httpclient.HttpClient; -import org.apache.commons.httpclient.MultiThreadedHttpConnectionManager; -import org.apache.commons.httpclient.URIException; -import org.apache.commons.httpclient.methods.ByteArrayRequestEntity; -import org.apache.commons.httpclient.methods.PostMethod; +import org.apache.hc.client5.http.classic.HttpClient; +import org.apache.hc.client5.http.classic.methods.HttpPost; +import org.apache.hc.core5.http.ClassicHttpResponse; +import org.apache.hc.core5.http.Header; +import org.apache.hc.core5.http.HttpEntity; +import org.apache.hc.core5.http.HttpResponse; +import org.apache.hc.core5.http.io.entity.ByteArrayEntity; +import org.apache.hc.core5.http.io.entity.EntityUtils; +import org.apache.hc.core5.http.protocol.HttpContext; + import org.springframework.util.Assert; import org.springframework.ws.WebServiceMessage; import org.springframework.ws.transport.WebServiceConnection; /** - * Implementation of {@link WebServiceConnection} that is based on Jakarta Commons HttpClient. Exposes a - * {@link PostMethod}. + * Implementation of {@link WebServiceConnection} that is based on Apache HttpClient. Exposes a {@link org.apache.hc.client5.http.classic.methods.HttpPost} and + * {@link org.apache.hc.core5.http.HttpResponse * + * @author Alan Stewart + * @author Barry Pitman * @author Arjen Poutsma * @author Greg Turnquist - * @since 1.0.0 - * @deprecated In favor of {@link HttpComponentsConnection} + * @author Lars Uffmann + * @since 4.0.5 */ -@Deprecated -public class CommonsHttpConnection extends AbstractHttpSenderConnection { +public class HttpComponents5Connection extends AbstractHttpSenderConnection { private final HttpClient httpClient; - private final PostMethod postMethod; + private final HttpPost httpPost; - private ByteArrayOutputStream requestBuffer; + private final HttpContext httpContext; - private MultiThreadedHttpConnectionManager connectionManager; + private HttpResponse httpResponse; - protected CommonsHttpConnection(HttpClient httpClient, PostMethod postMethod) { + private ByteArrayOutputStream requestBuffer; + + protected HttpComponents5Connection(HttpClient httpClient, HttpPost httpPost, HttpContext httpContext) { Assert.notNull(httpClient, "httpClient must not be null"); - Assert.notNull(postMethod, "postMethod must not be null"); + Assert.notNull(httpPost, "httpPost must not be null"); this.httpClient = httpClient; - this.postMethod = postMethod; + this.httpPost = httpPost; + this.httpContext = httpContext; } - public PostMethod getPostMethod() { - return postMethod; + public HttpPost getHttpPost() { + return httpPost; + } + + public HttpResponse getHttpResponse() { + return httpResponse; } @Override public void onClose() throws IOException { - postMethod.releaseConnection(); - if (connectionManager != null) { - connectionManager.shutdown(); + //XXX: + if (httpResponse instanceof ClassicHttpResponse response) { + if (response.getEntity() != null) { + EntityUtils.consume(response.getEntity()); + } } } /* - * URI - */ - + * URI + */ @Override public URI getUri() throws URISyntaxException { - try { - return new URI(postMethod.getURI().toString()); - } catch (URIException ex) { - throw new URISyntaxException("", ex.getMessage()); - } + return new URI(httpPost.getUri().toString()); } /* - * Sending request - */ + * Sending request + */ @Override protected void onSendBeforeWrite(WebServiceMessage message) throws IOException { @@ -98,7 +107,7 @@ protected void onSendBeforeWrite(WebServiceMessage message) throws IOException { @Override public void addRequestHeader(String name, String value) throws IOException { - postMethod.addRequestHeader(name, value); + httpPost.addHeader(name, value); } @Override @@ -108,20 +117,13 @@ protected OutputStream getRequestOutputStream() throws IOException { @Override protected void onSendAfterWrite(WebServiceMessage message) throws IOException { - postMethod.setRequestEntity(new ByteArrayRequestEntity(requestBuffer.toByteArray())); + //XXX + httpPost.setEntity(new ByteArrayEntity(requestBuffer.toByteArray(), null)); requestBuffer = null; - try { - httpClient.executeMethod(postMethod); - } catch (IllegalStateException ex) { - if ("Connection factory has been shutdown.".equals(ex.getMessage())) { - // The application context has been closed, resulting in a connection factory shutdown and an ISE. - // Let's create a new connection factory for this connection only. - connectionManager = new MultiThreadedHttpConnectionManager(); - httpClient.setHttpConnectionManager(connectionManager); - httpClient.executeMethod(postMethod); - } else { - throw ex; - } + if (httpContext != null) { + httpResponse = httpClient.execute(httpPost, httpContext); + } else { + httpResponse = httpClient.execute(httpPost); } } @@ -131,27 +133,40 @@ protected void onSendAfterWrite(WebServiceMessage message) throws IOException { @Override protected int getResponseCode() throws IOException { - return postMethod.getStatusCode(); + return httpResponse.getCode(); } @Override protected String getResponseMessage() throws IOException { - return postMethod.getStatusText(); + return httpResponse.getReasonPhrase(); } @Override protected long getResponseContentLength() throws IOException { - return postMethod.getResponseContentLength(); + //XXX: + if (httpResponse instanceof ClassicHttpResponse response) { + HttpEntity entity = response.getEntity(); + if (entity != null) { + return entity.getContentLength(); + } + } + return 0; } @Override protected InputStream getRawResponseInputStream() throws IOException { - return postMethod.getResponseBodyAsStream(); + if (httpResponse instanceof ClassicHttpResponse response) { + HttpEntity entity = response.getEntity(); + if (entity != null) { + return entity.getContent(); + } + } + throw new IllegalStateException("Response has no enclosing response entity, cannot create input stream"); } @Override public Iterator getResponseHeaderNames() throws IOException { - Header[] headers = postMethod.getResponseHeaders(); + Header[] headers = httpResponse.getHeaders(); String[] names = new String[headers.length]; for (int i = 0; i < headers.length; i++) { names[i] = headers[i].getName(); @@ -161,12 +176,11 @@ public Iterator getResponseHeaderNames() throws IOException { @Override public Iterator getResponseHeaders(String name) throws IOException { - Header[] headers = postMethod.getResponseHeaders(name); + Header[] headers = httpResponse.getHeaders(name); String[] values = new String[headers.length]; for (int i = 0; i < headers.length; i++) { values[i] = headers[i].getValue(); } return Arrays.asList(values).iterator(); } - } diff --git a/spring-ws-core/src/main/java/org/springframework/ws/transport/http/HttpComponents5MessageSender.java b/spring-ws-core/src/main/java/org/springframework/ws/transport/http/HttpComponents5MessageSender.java new file mode 100644 index 000000000..22caf2f13 --- /dev/null +++ b/spring-ws-core/src/main/java/org/springframework/ws/transport/http/HttpComponents5MessageSender.java @@ -0,0 +1,213 @@ +/* + * Copyright 2005-2023 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.ws.transport.http; + +import java.io.IOException; +import java.net.URI; +import java.util.Map; + +import org.apache.hc.client5.http.auth.AuthScope; +import org.apache.hc.client5.http.auth.Credentials; +import org.apache.hc.client5.http.classic.HttpClient; +import org.apache.hc.client5.http.classic.methods.HttpPost; +import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; +import org.apache.hc.client5.http.impl.classic.HttpClientBuilder; +import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManager; +import org.apache.hc.core5.http.EntityDetails; +import org.apache.hc.core5.http.HttpException; +import org.apache.hc.core5.http.HttpHeaders; +import org.apache.hc.core5.http.HttpRequest; +import org.apache.hc.core5.http.HttpRequestInterceptor; +import org.apache.hc.core5.http.protocol.HttpContext; + +import org.springframework.beans.factory.DisposableBean; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.util.Assert; +import org.springframework.ws.transport.WebServiceConnection; + +/** + * {@code WebServiceMessageSender} implementation that uses Apache + * HttpClient to execute POST requests. + *

+ * Allows to use a pre-configured HttpClient instance, potentially with authentication, HTTP connection pooling, etc. + * Authentication can also be set by injecting a {@link Credentials} instance (such as the + * {@link org.apache.hc.client5.http.auth.UsernamePasswordCredentials}). + * + * @author Alan Stewart + * @author Barry Pitman + * @author Arjen Poutsma + * @author Greg Turnquist + * @author Lars Uffmann + * @see HttpClient + * @since 4.0.5 + */ +public class HttpComponents5MessageSender extends AbstractHttpWebServiceMessageSender + implements InitializingBean, DisposableBean { + private static final String HTTP_CLIENT_ALREADY_SET = "httpClient already set"; + private HttpClient httpClient; + + private HttpComponents5ClientFactory clientFactory; + + /** + * Create a new instance of the {@code HttpClientMessageSender} with a default {@link HttpClient} that uses a default + * {@link PoolingHttpClientConnectionManager}. + */ + public HttpComponents5MessageSender() { + this.clientFactory = new HttpComponents5ClientFactory(); + this.clientFactory.setClientBuilderCustomizer(httpClientBuilder -> + httpClientBuilder.addRequestInterceptorFirst(new RemoveSoapHeadersInterceptor())); + } + + /** + * Create a new instance of the {@code HttpClientMessageSender} with the given {@link HttpClient} instance. + *

+ * This constructor does not change the given {@code HttpClient} in any way. As such, it does not set timeouts, nor + * does it + * {@linkplain HttpClientBuilder#addRequestInterceptorFirst(HttpRequestInterceptor) + * add} the {@link RemoveSoapHeadersInterceptor}. + * + * @param httpClient the HttpClient instance to use for this sender + */ + public HttpComponents5MessageSender(HttpClient httpClient) { + Assert.notNull(httpClient, "httpClient must not be null"); + this.httpClient = httpClient; + } + + /** + * @see HttpComponents5ClientFactory#setAuthScope(AuthScope) + */ + public void setAuthScope(AuthScope authScope) { + if (null != getHttpClient()) { + throw new IllegalStateException(HTTP_CLIENT_ALREADY_SET); + } + this.clientFactory.setAuthScope(authScope); + } + + /** + * @see HttpComponents5ClientFactory#setCredentials(Credentials) + */ + public void setCredentials(Credentials credentials) { + if (null != getHttpClient()) { + throw new IllegalStateException(HTTP_CLIENT_ALREADY_SET); + } + this.clientFactory.setCredentials(credentials); + } + + /** + * Returns the {@code HttpClient} used by this message sender. + */ + public HttpClient getHttpClient() { + return httpClient; + } + + /** + * Set the {@code HttpClient} used by this message sender. + */ + public void setHttpClient(HttpClient httpClient) { + this.httpClient = httpClient; + } + + /** + * @see HttpComponents5ClientFactory#setConnectionTimeout(int) + */ + public void setConnectionTimeout(int timeout) { + if (null != getHttpClient()) { + throw new IllegalStateException(HTTP_CLIENT_ALREADY_SET); + } + this.clientFactory.setConnectionTimeout(timeout); + } + + /** + * @see HttpComponents5ClientFactory#setReadTimeout(int) + */ + public void setReadTimeout(int timeout) { + if (null != getHttpClient()) { + throw new IllegalStateException(HTTP_CLIENT_ALREADY_SET); + } + this.clientFactory.setReadTimeout(timeout); + } + + /** + * @see HttpComponents5ClientFactory#setMaxTotalConnections(int) + */ + public void setMaxTotalConnections(int maxTotalConnections) { + if (null != getHttpClient()) { + throw new IllegalStateException(HTTP_CLIENT_ALREADY_SET); + } + this.clientFactory.setMaxTotalConnections(maxTotalConnections); + } + + /** + * @see HttpComponents5ClientFactory#setMaxConnectionsPerHost(Map) + */ + public void setMaxConnectionsPerHost(Map maxConnectionsPerHost) { + if (null != getHttpClient()) { + throw new IllegalStateException(HTTP_CLIENT_ALREADY_SET); + } + this.clientFactory.setMaxConnectionsPerHost(maxConnectionsPerHost); + } + + @Override + public void afterPropertiesSet() throws Exception { + this.httpClient = clientFactory.getObject(); + } + + @Override + public WebServiceConnection createConnection(URI uri) throws IOException { + HttpPost httpPost = new HttpPost(uri); + if (isAcceptGzipEncoding()) { + httpPost.addHeader(HttpTransportConstants.HEADER_ACCEPT_ENCODING, HttpTransportConstants.CONTENT_ENCODING_GZIP); + } + HttpContext httpContext = createContext(uri); + return new HttpComponents5Connection(getHttpClient(), httpPost, httpContext); + } + + /** + * Template method that allows for creation of a {@link HttpContext} for the given uri. Default implementation returns + * {@code null}. + * + * @param uri the URI to create the context for + * @return the context, or {@code null} + */ + protected HttpContext createContext(URI uri) { + return null; + } + + @Override + public void destroy() throws Exception { + if (getHttpClient() instanceof CloseableHttpClient client) { + client.close(); + } + } + + /** + * HttpClient {@link HttpRequestInterceptor} implementation that removes {@code Content-Length} and + * {@code Transfer-Encoding} headers from the request. Necessary, because some SAAJ and other SOAP implementations set + * these headers themselves, and HttpClient throws an exception if they have been set. + */ + public static class RemoveSoapHeadersInterceptor implements HttpRequestInterceptor { + @Override + public void process(HttpRequest request, EntityDetails entityDetails, HttpContext httpContext) throws HttpException, IOException { + if (request.containsHeader(HttpHeaders.TRANSFER_ENCODING)) { + request.removeHeaders(HttpHeaders.TRANSFER_ENCODING); + } + if (request.containsHeader(HttpHeaders.CONTENT_LENGTH)) { + request.removeHeaders(HttpHeaders.CONTENT_LENGTH); + } + } + } +} diff --git a/spring-ws-core/src/test/java/org/springframework/ws/transport/http/CommonsHttpMessageSenderIntegrationTest.java b/spring-ws-core/src/test/java/org/springframework/ws/transport/http/HttpComponents5MessageSenderIntegrationTest.java similarity index 50% rename from spring-ws-core/src/test/java/org/springframework/ws/transport/http/CommonsHttpMessageSenderIntegrationTest.java rename to spring-ws-core/src/test/java/org/springframework/ws/transport/http/HttpComponents5MessageSenderIntegrationTest.java index 16bb673e6..a83203983 100644 --- a/spring-ws-core/src/test/java/org/springframework/ws/transport/http/CommonsHttpMessageSenderIntegrationTest.java +++ b/spring-ws-core/src/test/java/org/springframework/ws/transport/http/HttpComponents5MessageSenderIntegrationTest.java @@ -16,52 +16,95 @@ package org.springframework.ws.transport.http; -import jakarta.servlet.http.HttpServlet; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; -import jakarta.xml.soap.MessageFactory; - import java.io.IOException; import java.net.URI; import java.util.HashMap; import java.util.Map; -import org.apache.commons.httpclient.URIException; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.xml.soap.MessageFactory; +import org.apache.hc.client5.http.HttpRoute; +import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; +import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManager; +import org.apache.hc.core5.http.HttpHost; import org.eclipse.jetty.server.Connector; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.ServerConnector; import org.eclipse.jetty.servlet.ServletContextHandler; import org.junit.jupiter.api.Test; + import org.springframework.context.support.StaticApplicationContext; import org.springframework.util.FileCopyUtils; import org.springframework.ws.soap.saaj.SaajSoapMessage; import org.springframework.ws.soap.saaj.SaajSoapMessageFactory; import org.springframework.ws.transport.WebServiceConnection; import org.springframework.ws.transport.support.FreePortScanner; +import static org.assertj.core.api.AssertionsForClassTypes.*; +import static org.springframework.ws.transport.http.HttpComponents5ClientFactory.*; -public class CommonsHttpMessageSenderIntegrationTest - extends AbstractHttpWebServiceMessageSenderIntegrationTestCase { +class HttpComponents5MessageSenderIntegrationTest + extends AbstractHttpWebServiceMessageSenderIntegrationTestCase { @Override - protected CommonsHttpMessageSender createMessageSender() { - return new CommonsHttpMessageSender(); + protected HttpComponents5MessageSender createMessageSender() { + return new HttpComponents5MessageSender(); } @Test - public void testMaxConnections() throws URIException { - - CommonsHttpMessageSender messageSender = new CommonsHttpMessageSender(); - messageSender.setMaxTotalConnections(2); - Map maxConnectionsPerHost = new HashMap(); - maxConnectionsPerHost.put("https://www.example.com", "1"); - maxConnectionsPerHost.put("http://www.example.com:8080", "7"); - maxConnectionsPerHost.put("www.springframework.org", "10"); - maxConnectionsPerHost.put("*", "5"); - messageSender.setMaxConnectionsPerHost(maxConnectionsPerHost); + void testMaxConnections() throws Exception { + + final String url1 = "https://www.example.com"; + URI uri1 = new URI(url1); + HttpHost host1 = new HttpHost(uri1.getScheme(), uri1.getHost(), getPort(uri1)); + HttpRoute route1 = new HttpRoute(host1, null, true); + + assertThat(route1.isSecure()).isTrue(); + assertThat(route1.getTargetHost().getHostName()).isEqualTo("www.example.com"); + assertThat(route1.getTargetHost().getPort()).isEqualTo(443); + + final String url2 = "http://www.example.com:8080"; + URI uri2 = new URI(url2); + HttpHost host2 = new HttpHost(uri2.getScheme(), uri2.getHost(), getPort(uri2)); + HttpRoute route2 = new HttpRoute(host2); + + assertThat(route2.isSecure()).isFalse(); + assertThat(route2.getTargetHost().getHostName()).isEqualTo("www.example.com"); + assertThat(route2.getTargetHost().getPort()).isEqualTo(8080); + + final String url3 = "http://www.springframework.org"; + URI uri3 = new URI(url3); + HttpHost host3 = new HttpHost(uri3.getScheme(), uri3.getHost(), getPort(uri3)); + HttpRoute route3 = new HttpRoute(host3); + + assertThat(route3.isSecure()).isFalse(); + assertThat(route3.getTargetHost().getHostName()).isEqualTo("www.springframework.org"); + assertThat(route3.getTargetHost().getPort()).isEqualTo(80); + + HttpComponents5ClientFactory clientFactory = new HttpComponents5ClientFactory(); + + Map maxConnectionsPerHost = new HashMap<>(); + maxConnectionsPerHost.put(url1, "1"); + maxConnectionsPerHost.put(url2, "7"); + maxConnectionsPerHost.put(url3, "10"); + + clientFactory.setMaxTotalConnections(2); + clientFactory.setMaxConnectionsPerHost(maxConnectionsPerHost); + + CloseableHttpClient client = clientFactory.getObject(); + assertThat(client).isNotNull(); + + // It is no longer possible way to get connection manager from client + PoolingHttpClientConnectionManager poolingHttpClientConnectionManager = clientFactory.getConnectionManager(); + + assertThat(poolingHttpClientConnectionManager.getMaxPerRoute(route1)).isEqualTo(1); + assertThat(poolingHttpClientConnectionManager.getMaxPerRoute(route2)).isEqualTo(7); + assertThat(poolingHttpClientConnectionManager.getMaxPerRoute(route3)).isEqualTo(10); } @Test - public void testContextClose() throws Exception { + void testContextClose() throws Exception { MessageFactory messageFactory = MessageFactory.newInstance(); int port = FreePortScanner.getFreePort(); @@ -83,18 +126,18 @@ public void testContextClose() throws Exception { try { StaticApplicationContext appContext = new StaticApplicationContext(); - appContext.registerSingleton("messageSender", CommonsHttpMessageSender.class); + appContext.registerSingleton("messageSender", HttpComponents5MessageSender.class); appContext.refresh(); - CommonsHttpMessageSender messageSender = appContext.getBean("messageSender", CommonsHttpMessageSender.class); + HttpComponents5MessageSender messageSender = appContext.getBean("messageSender", + HttpComponents5MessageSender.class); connection = messageSender.createConnection(new URI("http://localhost:" + port)); - appContext.close(); - connection.send(new SaajSoapMessage(messageFactory.createMessage())); connection.receive(new SaajSoapMessageFactory(messageFactory)); - } finally { + appContext.close(); + } finally { if (connection != null) { try { connection.close(); @@ -117,6 +160,8 @@ protected void doPost(HttpServletRequest request, HttpServletResponse response) response.setContentType("text/xml"); FileCopyUtils.copy(request.getInputStream(), response.getOutputStream()); + } } + }