diff --git a/pom.xml b/pom.xml index fe923485..c3ef4c78 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 19df245b..7ff83043 100644 --- a/spring-ws-core/pom.xml +++ b/spring-ws-core/pom.xml @@ -160,6 +160,24 @@ + + org.apache.httpcomponents.client5 + httpclient5 + ${httpclient5.version} + true + + + org.apache.httpcomponents + httpclient + ${httpclient.version} + true + + + commons-logging + commons-logging + + + org.eclipse.jetty 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 00000000..82fc6075 --- /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/HttpComponents5Connection.java b/spring-ws-core/src/main/java/org/springframework/ws/transport/http/HttpComponents5Connection.java new file mode 100644 index 00000000..d0e0bdb9 --- /dev/null +++ b/spring-ws-core/src/main/java/org/springframework/ws/transport/http/HttpComponents5Connection.java @@ -0,0 +1,186 @@ +/* + * 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.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.Arrays; +import java.util.Iterator; + +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 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 + * @author Lars Uffmann + * @since 4.0.5 + */ +public class HttpComponents5Connection extends AbstractHttpSenderConnection { + + private final HttpClient httpClient; + + private final HttpPost httpPost; + + private final HttpContext httpContext; + + private HttpResponse httpResponse; + + private ByteArrayOutputStream requestBuffer; + + protected HttpComponents5Connection(HttpClient httpClient, HttpPost httpPost, HttpContext httpContext) { + Assert.notNull(httpClient, "httpClient must not be null"); + Assert.notNull(httpPost, "httpPost must not be null"); + this.httpClient = httpClient; + this.httpPost = httpPost; + this.httpContext = httpContext; + } + + public HttpPost getHttpPost() { + return httpPost; + } + + public HttpResponse getHttpResponse() { + return httpResponse; + } + + @Override + public void onClose() throws IOException { + //XXX: + if (httpResponse instanceof ClassicHttpResponse response) { + if (response.getEntity() != null) { + EntityUtils.consume(response.getEntity()); + } + } + } + + /* + * URI + */ + @Override + public URI getUri() throws URISyntaxException { + return new URI(httpPost.getUri().toString()); + } + + /* + * Sending request + */ + + @Override + protected void onSendBeforeWrite(WebServiceMessage message) throws IOException { + requestBuffer = new ByteArrayOutputStream(); + } + + @Override + public void addRequestHeader(String name, String value) throws IOException { + httpPost.addHeader(name, value); + } + + @Override + protected OutputStream getRequestOutputStream() throws IOException { + return requestBuffer; + } + + @Override + protected void onSendAfterWrite(WebServiceMessage message) throws IOException { + //XXX + httpPost.setEntity(new ByteArrayEntity(requestBuffer.toByteArray(), null)); + requestBuffer = null; + if (httpContext != null) { + httpResponse = httpClient.execute(httpPost, httpContext); + } else { + httpResponse = httpClient.execute(httpPost); + } + } + + /* + * Receiving response + */ + + @Override + protected int getResponseCode() throws IOException { + return httpResponse.getCode(); + } + + @Override + protected String getResponseMessage() throws IOException { + return httpResponse.getReasonPhrase(); + } + + @Override + protected long getResponseContentLength() throws IOException { + //XXX: + if (httpResponse instanceof ClassicHttpResponse response) { + HttpEntity entity = response.getEntity(); + if (entity != null) { + return entity.getContentLength(); + } + } + return 0; + } + + @Override + protected InputStream getRawResponseInputStream() throws IOException { + 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 = httpResponse.getHeaders(); + String[] names = new String[headers.length]; + for (int i = 0; i < headers.length; i++) { + names[i] = headers[i].getName(); + } + return Arrays.asList(names).iterator(); + } + + @Override + public Iterator getResponseHeaders(String name) throws IOException { + 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 00000000..22caf2f1 --- /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/HttpComponents5MessageSenderIntegrationTest.java b/spring-ws-core/src/test/java/org/springframework/ws/transport/http/HttpComponents5MessageSenderIntegrationTest.java new file mode 100644 index 00000000..a8320398 --- /dev/null +++ b/spring-ws-core/src/test/java/org/springframework/ws/transport/http/HttpComponents5MessageSenderIntegrationTest.java @@ -0,0 +1,167 @@ +/* + * 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.HashMap; +import java.util.Map; + +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.*; + +class HttpComponents5MessageSenderIntegrationTest + extends AbstractHttpWebServiceMessageSenderIntegrationTestCase { + + @Override + protected HttpComponents5MessageSender createMessageSender() { + return new HttpComponents5MessageSender(); + } + + @Test + 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 + void testContextClose() throws Exception { + + MessageFactory messageFactory = MessageFactory.newInstance(); + int port = FreePortScanner.getFreePort(); + + Server jettyServer = new Server(port); + Connector connector = new ServerConnector(jettyServer); + jettyServer.addConnector(connector); + + ServletContextHandler jettyContext = new ServletContextHandler(); + jettyContext.setContextPath("/"); + + jettyContext.addServlet(EchoServlet.class, "/"); + + jettyServer.setHandler(jettyContext); + jettyServer.start(); + + WebServiceConnection connection = null; + + try { + + StaticApplicationContext appContext = new StaticApplicationContext(); + appContext.registerSingleton("messageSender", HttpComponents5MessageSender.class); + appContext.refresh(); + + HttpComponents5MessageSender messageSender = appContext.getBean("messageSender", + HttpComponents5MessageSender.class); + connection = messageSender.createConnection(new URI("http://localhost:" + port)); + + connection.send(new SaajSoapMessage(messageFactory.createMessage())); + connection.receive(new SaajSoapMessageFactory(messageFactory)); + + appContext.close(); + } finally { + if (connection != null) { + try { + connection.close(); + } catch (IOException ex) { + // ignore + } + } + if (jettyServer.isRunning()) { + jettyServer.stop(); + } + } + + } + + @SuppressWarnings("serial") + public static class EchoServlet extends HttpServlet { + + @Override + protected void doPost(HttpServletRequest request, HttpServletResponse response) throws IOException { + + response.setContentType("text/xml"); + FileCopyUtils.copy(request.getInputStream(), response.getOutputStream()); + + } + } + +}