diff --git a/spring-ws-core/src/main/java/org/springframework/ws/transport/http/AbstractHttpComponents5MessageSender.java b/spring-ws-core/src/main/java/org/springframework/ws/transport/http/AbstractHttpComponents5MessageSender.java new file mode 100644 index 00000000..1256a5bc --- /dev/null +++ b/spring-ws-core/src/main/java/org/springframework/ws/transport/http/AbstractHttpComponents5MessageSender.java @@ -0,0 +1,110 @@ +/* + * Copyright 2005-2025 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 + * + * https://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 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.core5.http.EntityDetails; +import org.apache.hc.core5.http.HttpException; +import org.apache.hc.core5.http.HttpHeaders; +import org.apache.hc.core5.http.HttpHost; +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.ws.transport.WebServiceConnection; + +/** + * Base {@link AbstractHttpWebServiceMessageSender} implementation that uses + * Apache HttpClient to execute + * POST requests. + *

+ * To configure the underlying {@link HttpClient} consider using + * {@link HttpComponents5MessageSender}. To take control on how the {@link HttpClient} is + * built, use {@link SimpleHttpComponents5MessageSender}. + * + * @author Stephane Nicoll + * @since 4.1.0 + * @see HttpComponents5MessageSender + * @see SimpleHttpComponents5MessageSender + */ +public abstract class AbstractHttpComponents5MessageSender extends AbstractHttpWebServiceMessageSender + implements DisposableBean { + + /** + * Return the {@code HttpClient} used by this message sender. + */ + public abstract HttpClient getHttpClient(); + + @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); + } + HttpHost httpHost = HttpHost.create(uri); + HttpContext httpContext = createContext(uri); + return new HttpComponents5Connection(getHttpClient(), httpHost, httpPost, httpContext); + } + + @Override + public void destroy() throws Exception { + if (getHttpClient() instanceof CloseableHttpClient client) { + client.close(); + } + } + + /** + * Template method that allows for creation of an {@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; + } + + /** + * 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/main/java/org/springframework/ws/transport/http/HttpComponents5MessageSender.java b/spring-ws-core/src/main/java/org/springframework/ws/transport/http/HttpComponents5MessageSender.java index 2cdb52a4..ed1c86b9 100644 --- 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 @@ -16,40 +16,26 @@ package org.springframework.ws.transport.http; -import java.io.IOException; -import java.net.URI; import java.time.Duration; 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.HttpHost; -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. + * {@code AbstractHttpComponents5MessageSender} implementation that configures the + * underlying Apache HttpClient + * that executes 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}). + * To specify the {@link HttpClient}, consider using + * {@link SimpleHttpComponents5MessageSender} instead. * * @author Alan Stewart * @author Barry Pitman @@ -59,8 +45,7 @@ import org.springframework.ws.transport.WebServiceConnection; * @since 4.0.5 * @see HttpClient */ -public class HttpComponents5MessageSender extends AbstractHttpWebServiceMessageSender - implements InitializingBean, DisposableBean { +public class HttpComponents5MessageSender extends AbstractHttpComponents5MessageSender implements InitializingBean { private static final String HTTP_CLIENT_ALREADY_SET = "httpClient already set"; @@ -87,40 +72,43 @@ public class HttpComponents5MessageSender extends AbstractHttpWebServiceMessageS * {@linkplain HttpClientBuilder#addRequestInterceptorFirst(HttpRequestInterceptor) * add} the {@link RemoveSoapHeadersInterceptor}. * @param httpClient the HttpClient instance to use for this sender + * @deprecated as of 4.1.0 in favor of {@link SimpleHttpComponents5MessageSender} */ + @Deprecated(since = "4.1.0", forRemoval = true) public HttpComponents5MessageSender(HttpClient httpClient) { this(); Assert.notNull(httpClient, "httpClient must not be null"); this.httpClient = httpClient; } - /* - * * @see HttpComponents5ClientFactory#setAuthScope(AuthScope) + @Override + public HttpClient getHttpClient() { + return this.httpClient; + } + + /** + * Set the authentication scope to be used. Only used when the {@code credentials} + * property has been set. + * @see HttpComponents5ClientFactory#setAuthScope(AuthScope) */ public void setAuthScope(AuthScope authScope) { - if (getHttpClient() != null) { + if (this.httpClient != null) { throw new IllegalStateException(HTTP_CLIENT_ALREADY_SET); } this.clientFactory.setAuthScope(authScope); } - /* - * * @see HttpComponents5ClientFactory#setCredentials(Credentials) + /** + * Set the credentials to be used. If not set, no authentication is done. + * @see HttpComponents5ClientFactory#setCredentials(Credentials) */ public void setCredentials(Credentials credentials) { - if (getHttpClient() != null) { + if (this.httpClient != null) { throw new IllegalStateException(HTTP_CLIENT_ALREADY_SET); } this.clientFactory.setCredentials(credentials); } - /** - * Returns the {@code HttpClient} used by this message sender. - */ - public HttpClient getHttpClient() { - return this.httpClient; - } - /** * Set the {@code HttpClient} used by this message sender. *

@@ -129,7 +117,9 @@ public class HttpComponents5MessageSender extends AbstractHttpWebServiceMessageS * {@linkplain HttpClientBuilder#addRequestInterceptorFirst(HttpRequestInterceptor) * add} the {@link RemoveSoapHeadersInterceptor}. * @param httpClient the HttpClient to use + * @deprecated as of 4.1.0 in favor of {@link SimpleHttpComponents5MessageSender} */ + @Deprecated(since = "4.1.0", forRemoval = true) public void setHttpClient(HttpClient httpClient) { this.httpClient = httpClient; } @@ -139,7 +129,7 @@ public class HttpComponents5MessageSender extends AbstractHttpWebServiceMessageS * @see HttpComponents5ClientFactory#setConnectionTimeout(Duration) */ public void setConnectionTimeout(Duration timeout) { - if (getHttpClient() != null) { + if (this.httpClient != null) { throw new IllegalStateException(HTTP_CLIENT_ALREADY_SET); } this.clientFactory.setConnectionTimeout(timeout); @@ -150,7 +140,7 @@ public class HttpComponents5MessageSender extends AbstractHttpWebServiceMessageS * @see HttpComponents5ClientFactory#setReadTimeout(Duration) */ public void setReadTimeout(Duration timeout) { - if (getHttpClient() != null) { + if (this.httpClient != null) { throw new IllegalStateException(HTTP_CLIENT_ALREADY_SET); } this.clientFactory.setReadTimeout(timeout); @@ -161,7 +151,7 @@ public class HttpComponents5MessageSender extends AbstractHttpWebServiceMessageS * @see HttpComponents5ClientFactory#setMaxTotalConnections(int) */ public void setMaxTotalConnections(int maxTotalConnections) { - if (getHttpClient() != null) { + if (this.httpClient != null) { throw new IllegalStateException(HTTP_CLIENT_ALREADY_SET); } this.clientFactory.setMaxTotalConnections(maxTotalConnections); @@ -172,7 +162,7 @@ public class HttpComponents5MessageSender extends AbstractHttpWebServiceMessageS * @see HttpComponents5ClientFactory#setMaxConnectionsPerHost(Map) */ public void setMaxConnectionsPerHost(Map maxConnectionsPerHost) { - if (getHttpClient() != null) { + if (this.httpClient != null) { throw new IllegalStateException(HTTP_CLIENT_ALREADY_SET); } this.clientFactory.setMaxConnectionsPerHost(maxConnectionsPerHost); @@ -180,66 +170,9 @@ public class HttpComponents5MessageSender extends AbstractHttpWebServiceMessageS @Override public void afterPropertiesSet() throws Exception { - if (getHttpClient() == null) { + if (this.httpClient == null) { this.httpClient = this.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); - } - - HttpHost httpHost = HttpHost.create(uri); - HttpContext httpContext = createContext(uri); - - return new HttpComponents5Connection(getHttpClient(), httpHost, httpPost, httpContext); - } - - /** - * Template method that allows for creation of an {@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/main/java/org/springframework/ws/transport/http/SimpleHttpComponents5MessageSender.java b/spring-ws-core/src/main/java/org/springframework/ws/transport/http/SimpleHttpComponents5MessageSender.java new file mode 100644 index 00000000..c1fa0225 --- /dev/null +++ b/spring-ws-core/src/main/java/org/springframework/ws/transport/http/SimpleHttpComponents5MessageSender.java @@ -0,0 +1,67 @@ +/* + * Copyright 2005-2025 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 + * + * https://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 org.apache.hc.client5.http.classic.HttpClient; + +import org.springframework.util.Assert; + +/** + * {@code AbstractHttpComponents5MessageSender} implementation that defines the underlying + * Apache HttpClient that + * executes POST requests. + *

+ * The {@link HttpClient} can be provided as-is or configured via the convenient + * {@link HttpComponents5ClientFactory} + *

+ * For convenience method use to customize the underlying {@link HttpClient}, consider + * using {@link HttpComponents5MessageSender} instead. + * + * @author Stephane Nicoll + * @since 4.1.0 + * @see HttpClient + * @see HttpComponents5ClientFactory + */ +public class SimpleHttpComponents5MessageSender extends AbstractHttpComponents5MessageSender { + + private final HttpClient httpClient; + + /** + * Creates an instance with the given {@link HttpClient}. + * @param httpClient the http client to use + */ + public SimpleHttpComponents5MessageSender(HttpClient httpClient) { + Assert.notNull(httpClient, "httpClient must not be null"); + this.httpClient = httpClient; + } + + /** + * Create a new instance with the state of the given + * {@link HttpComponents5ClientFactory}. + * @param factory the factory to use + * @throws Exception if the client fails to build + */ + public SimpleHttpComponents5MessageSender(HttpComponents5ClientFactory factory) throws Exception { + this(factory.getObject()); + } + + @Override + public HttpClient getHttpClient() { + return this.httpClient; + } + +} diff --git a/spring-ws-core/src/test/java/org/springframework/ws/transport/http/HttpComponents5ContentTypeIntegrationTest.java b/spring-ws-core/src/test/java/org/springframework/ws/transport/http/HttpComponents5ContentTypeIntegrationTest.java index 3e8d82e9..930c4952 100644 --- a/spring-ws-core/src/test/java/org/springframework/ws/transport/http/HttpComponents5ContentTypeIntegrationTest.java +++ b/spring-ws-core/src/test/java/org/springframework/ws/transport/http/HttpComponents5ContentTypeIntegrationTest.java @@ -23,22 +23,19 @@ import org.apache.hc.client5.http.impl.classic.HttpClientBuilder; import static org.assertj.core.api.AssertionsForClassTypes.assertThat; class HttpComponents5ContentTypeIntegrationTest - extends AbstractHttpWebServiceMessageSenderIntegrationTest { + extends AbstractHttpWebServiceMessageSenderIntegrationTest { @Override - protected HttpComponents5MessageSender createMessageSender() { - + protected AbstractHttpComponents5MessageSender createMessageSender() { ExecChainHandler testHandler = (request, scope, chain) -> { assertThat(request.getEntity().getContentType()).isNotBlank(); return chain.proceed(request, scope); }; - HttpClient client = HttpClientBuilder.create() .addRequestInterceptorFirst(new HttpComponents5MessageSender.RemoveSoapHeadersInterceptor()) .addExecInterceptorFirst("handler with assertion", testHandler) .build(); - - return new HttpComponents5MessageSender(client); + return new SimpleHttpComponents5MessageSender(client); } } diff --git a/spring-ws-core/src/test/java/org/springframework/ws/transport/http/HttpComponents5MessageSenderTest.java b/spring-ws-core/src/test/java/org/springframework/ws/transport/http/HttpComponents5MessageSenderTest.java index 97d7069f..5355c82e 100644 --- a/spring-ws-core/src/test/java/org/springframework/ws/transport/http/HttpComponents5MessageSenderTest.java +++ b/spring-ws-core/src/test/java/org/springframework/ws/transport/http/HttpComponents5MessageSenderTest.java @@ -44,6 +44,8 @@ class HttpComponents5MessageSenderTest { } @Test + @Deprecated + @SuppressWarnings("removal") void afterPropertiesSetShouldUseAlreadyProvidedHttpClientIfAvailableWithConstructor() throws Exception { CloseableHttpClient httpClient = HttpClientBuilder.create().build(); HttpComponents5MessageSender messageSender = new HttpComponents5MessageSender(httpClient); @@ -54,6 +56,8 @@ class HttpComponents5MessageSenderTest { } @Test + @Deprecated + @SuppressWarnings("removal") void afterPropertiesSetShouldUseAlreadyProvidedHttpClientIfAvailableWithProperty() throws Exception { CloseableHttpClient httpClient = HttpClientBuilder.create().build(); HttpComponents5MessageSender messageSender = new HttpComponents5MessageSender(); diff --git a/spring-ws-core/src/test/java/org/springframework/ws/transport/http/SimpleHttpComponents5MessageSenderTest.java b/spring-ws-core/src/test/java/org/springframework/ws/transport/http/SimpleHttpComponents5MessageSenderTest.java new file mode 100644 index 00000000..8149fec2 --- /dev/null +++ b/spring-ws-core/src/test/java/org/springframework/ws/transport/http/SimpleHttpComponents5MessageSenderTest.java @@ -0,0 +1,72 @@ +/* + * Copyright 2005-2025 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 + * + * https://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 org.apache.hc.client5.http.classic.HttpClient; +import org.apache.hc.client5.http.impl.classic.HttpClientBuilder; +import org.junit.jupiter.api.Test; + +import org.springframework.ws.transport.WebServiceConnection; +import org.springframework.ws.transport.http.HttpComponents5ClientFactory.HttpClientBuilderCustomizer; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +/** + * Tests for {@link SimpleHttpComponents5MessageSender}. + */ +class SimpleHttpComponents5MessageSenderTest { + + @Test + void createExposesHttpClient() { + HttpClient httpClient = HttpClientBuilder.create().build(); + SimpleHttpComponents5MessageSender messageSender = new SimpleHttpComponents5MessageSender(httpClient); + assertThat(messageSender.getHttpClient()).isSameAs(httpClient); + } + + @Test + void createWithNullFails() { + HttpClient httpClient = null; + assertThatIllegalArgumentException().isThrownBy(() -> new SimpleHttpComponents5MessageSender(httpClient)); + } + + @Test + void createWithFactory() throws Exception { + HttpComponents5ClientFactory factory = new HttpComponents5ClientFactory(); + HttpClientBuilderCustomizer builderCustomizer = mock(HttpClientBuilderCustomizer.class); + factory.setClientBuilderCustomizer(builderCustomizer); + new SimpleHttpComponents5MessageSender(factory); + verify(builderCustomizer).customize(any()); + } + + @Test + void createConnectionUsesHttpClient() throws IOException { + HttpClient httpClient = HttpClientBuilder.create().build(); + SimpleHttpComponents5MessageSender messageSender = new SimpleHttpComponents5MessageSender(httpClient); + WebServiceConnection webServiceConnection = messageSender + .createConnection(URI.create("https://ws.example.com")); + assertThat(webServiceConnection).isInstanceOfSatisfying(HttpComponents5Connection.class, + (connection) -> assertThat(connection).hasFieldOrPropertyWithValue("httpClient", httpClient)); + } + +} \ No newline at end of file diff --git a/spring-ws-docs/src/docs/asciidoc/client.adoc b/spring-ws-docs/src/docs/asciidoc/client.adoc index 24f981a6..4569c911 100644 --- a/spring-ws-docs/src/docs/asciidoc/client.adoc +++ b/spring-ws-docs/src/docs/asciidoc/client.adoc @@ -38,7 +38,7 @@ You can set one or more message senders by using the `messageSender` or `message There are three implementations of the `WebServiceMessageSender` interface for sending messages over HTTP. The default implementation is the `HttpUrlConnectionMessageSender`, which uses the facilities provided by Java itself. -The alternatives are either `JdkHttpClientMessageSender` that uses the JDK's `HttpClient`, or `HttpComponents5MessageSender`, which uses the https://hc.apache.org/httpcomponents-client-ga[Apache HttpClient]. +The alternatives are either `JdkHttpClientMessageSender` that uses the JDK's `HttpClient`, or `HttpComponents5MessageSender`/`SimpleHttpComponents5MessageSender`, which use the https://hc.apache.org/httpcomponents-client-ga[Apache HttpClient]. Use the latter if you need more advanced and easy-to-use functionality (such as authentication, HTTP connection pooling, and so forth). To use the HTTP transport, either set the `defaultUri` to something like `http://example.com/services` or supply the `uri` parameter for one of the methods.