Fix split package between spring-ws-support and spring-ws-core

This commit moves the classes from the o.s.ws.transport.http and
o.s.ws.transport.support packages from spring-ws-support to
spring-ws-core. It turns out these classes don't bring extra
dependencies, which make the move quite straightforward.

Closes gh-1202
This commit is contained in:
Stéphane Nicoll
2025-03-11 14:52:22 +01:00
parent 1213ab4da8
commit 122def7e21
20 changed files with 5 additions and 14 deletions

View File

@@ -0,0 +1,187 @@
/*
* 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.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import javax.xml.namespace.QName;
import com.sun.net.httpserver.HttpExchange;
import jakarta.xml.soap.SOAPConstants;
import org.springframework.util.Assert;
import org.springframework.util.FileCopyUtils;
import org.springframework.ws.WebServiceMessage;
import org.springframework.ws.transport.AbstractReceiverConnection;
import org.springframework.ws.transport.EndpointAwareWebServiceConnection;
import org.springframework.ws.transport.FaultAwareWebServiceConnection;
import org.springframework.ws.transport.WebServiceConnection;
/**
* Implementation of {@link WebServiceConnection} that is based on the Java 6 HttpServer
* {@link HttpExchange}.
*
* @author Arjen Poutsma
* @author Greg Turnquist
* @since 1.5.0
*/
public class HttpExchangeConnection extends AbstractReceiverConnection
implements EndpointAwareWebServiceConnection, FaultAwareWebServiceConnection {
private final HttpExchange httpExchange;
private ByteArrayOutputStream responseBuffer;
private int responseStatusCode = HttpTransportConstants.STATUS_ACCEPTED;
private boolean chunkedEncoding;
/** Constructs a new exchange connection with the given {@code HttpExchange}. */
protected HttpExchangeConnection(HttpExchange httpExchange) {
Assert.notNull(httpExchange, "'httpExchange' must not be null");
this.httpExchange = httpExchange;
}
/** Returns the {@code HttpExchange} for this connection. */
public HttpExchange getHttpExchange() {
return this.httpExchange;
}
@Override
public URI getUri() throws URISyntaxException {
return this.httpExchange.getRequestURI();
}
void setChunkedEncoding(boolean chunkedEncoding) {
this.chunkedEncoding = chunkedEncoding;
}
@Override
public void endpointNotFound() {
this.responseStatusCode = HttpTransportConstants.STATUS_NOT_FOUND;
}
/*
* Errors
*/
@Override
public boolean hasError() throws IOException {
return false;
}
@Override
public String getErrorMessage() throws IOException {
return null;
}
/*
* Receiving request
*/
@Override
public Iterator<String> getRequestHeaderNames() throws IOException {
return this.httpExchange.getRequestHeaders().keySet().iterator();
}
@Override
public Iterator<String> getRequestHeaders(String name) throws IOException {
List<String> headers = this.httpExchange.getRequestHeaders().get(name);
return (headers != null) ? headers.iterator() : Collections.emptyIterator();
}
@Override
protected InputStream getRequestInputStream() throws IOException {
return this.httpExchange.getRequestBody();
}
/*
* Sending response
*/
@Override
public void addResponseHeader(String name, String value) throws IOException {
this.httpExchange.getResponseHeaders().add(name, value);
}
@Override
protected OutputStream getResponseOutputStream() throws IOException {
if (this.chunkedEncoding) {
this.httpExchange.sendResponseHeaders(this.responseStatusCode, 0);
return this.httpExchange.getResponseBody();
}
else {
if (this.responseBuffer == null) {
this.responseBuffer = new ByteArrayOutputStream();
}
return this.responseBuffer;
}
}
@Override
protected void onSendAfterWrite(WebServiceMessage message) throws IOException {
if (!this.chunkedEncoding) {
byte[] buf = this.responseBuffer.toByteArray();
this.httpExchange.sendResponseHeaders(this.responseStatusCode, buf.length);
OutputStream responseBody = this.httpExchange.getResponseBody();
FileCopyUtils.copy(buf, responseBody);
}
this.responseBuffer = null;
}
@Override
public void onClose() throws IOException {
if (this.responseStatusCode == HttpTransportConstants.STATUS_ACCEPTED
|| this.responseStatusCode == HttpTransportConstants.STATUS_NOT_FOUND) {
this.httpExchange.sendResponseHeaders(this.responseStatusCode, -1);
}
this.httpExchange.close();
}
/*
* Faults
*/
@Override
public boolean hasFault() throws IOException {
return this.responseStatusCode == HttpTransportConstants.STATUS_INTERNAL_SERVER_ERROR;
}
@Override
public void setFaultCode(QName faultCode) throws IOException {
if (faultCode != null) {
if (SOAPConstants.SOAP_SENDER_FAULT.equals(faultCode)) {
this.responseStatusCode = HttpTransportConstants.STATUS_BAD_REQUEST;
}
else {
this.responseStatusCode = HttpTransportConstants.STATUS_INTERNAL_SERVER_ERROR;
}
}
else {
this.responseStatusCode = HttpTransportConstants.STATUS_OK;
}
}
}

View File

@@ -0,0 +1,36 @@
/*
* 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;
/**
* Exception that is thrown when an error occurs in the HTTP transport.
*
* @author Arjen Poutsma
* @since 1.5.8
*/
@SuppressWarnings("serial")
public class HttpsTransportException extends HttpTransportException {
public HttpsTransportException(String msg) {
super(msg);
}
public HttpsTransportException(String msg, Throwable cause) {
super(msg, cause);
}
}

View File

@@ -0,0 +1,174 @@
/*
* 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.HttpURLConnection;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.SecureRandom;
import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.KeyManager;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManager;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.util.Assert;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
/**
* Extension of {@link HttpUrlConnectionMessageSender} that adds support for (self-signed)
* HTTPS certificates.
*
* @author Alex Marshall
* @author Arjen Poutsma
* @since 1.5.8
*/
public class HttpsUrlConnectionMessageSender extends HttpUrlConnectionMessageSender implements InitializingBean {
/** The default SSL protocol. */
public static final String DEFAULT_SSL_PROTOCOL = "ssl";
private String sslProtocol = DEFAULT_SSL_PROTOCOL;
private String sslProvider;
private KeyManager[] keyManagers;
private TrustManager[] trustManagers;
private HostnameVerifier hostnameVerifier;
private SecureRandom rnd;
private SSLSocketFactory sslSocketFactory;
/**
* Sets the SSL protocol to use. Default is {@code ssl}.
* @see SSLContext#getInstance(String, String)
*/
public void setSslProtocol(String sslProtocol) {
Assert.hasLength(sslProtocol, "'sslProtocol' must not be empty");
this.sslProtocol = sslProtocol;
}
/**
* Sets the SSL provider to use. Default is empty, to use the default provider.
* @see SSLContext#getInstance(String, String)
*/
public void setSslProvider(String sslProvider) {
this.sslProvider = sslProvider;
}
/**
* Specifies the key managers to use for this message sender.
* <p>
* Setting either this property or {@link #setTrustManagers(TrustManager[])
* trustManagers} is required.
* @see SSLContext#init(KeyManager[], TrustManager[], SecureRandom)
*/
public void setKeyManagers(KeyManager[] keyManagers) {
this.keyManagers = keyManagers;
}
/**
* Specifies the trust managers to use for this message sender.
* <p>
* Setting either this property or {@link #setKeyManagers(KeyManager[]) keyManagers}
* is required.
* @see SSLContext#init(KeyManager[], TrustManager[], SecureRandom)
*/
public void setTrustManagers(TrustManager[] trustManagers) {
this.trustManagers = trustManagers;
}
/**
* Specifies the host name verifier to use for this message sender.
* @see HttpsURLConnection#setHostnameVerifier(HostnameVerifier)
*/
public void setHostnameVerifier(HostnameVerifier hostnameVerifier) {
this.hostnameVerifier = hostnameVerifier;
}
/**
* Specifies the secure random to use for this message sender.
* @see SSLContext#init(KeyManager[], TrustManager[], SecureRandom)
*/
public void setSecureRandom(SecureRandom rnd) {
this.rnd = rnd;
}
/**
* Specifies the SSLSocketFactory to use for this message sender.
* @see HttpsURLConnection#setSSLSocketFactory(SSLSocketFactory sf)
*/
public void setSslSocketFactory(SSLSocketFactory sslSocketFactory) {
this.sslSocketFactory = sslSocketFactory;
}
@Override
public void afterPropertiesSet() throws Exception {
Assert.isTrue(
!(ObjectUtils.isEmpty(this.keyManagers) && ObjectUtils.isEmpty(this.trustManagers)
&& (this.sslSocketFactory == null)),
"Setting either 'keyManagers', 'trustManagers' or 'sslSocketFactory' is required");
}
@Override
protected void prepareConnection(HttpURLConnection connection) throws IOException {
super.prepareConnection(connection);
if (connection instanceof HttpsURLConnection httpsConnection) {
httpsConnection.setSSLSocketFactory(createSslSocketFactory());
if (this.hostnameVerifier != null) {
httpsConnection.setHostnameVerifier(this.hostnameVerifier);
}
}
}
private SSLSocketFactory createSslSocketFactory() throws HttpsTransportException {
if (this.sslSocketFactory != null) {
return this.sslSocketFactory;
}
try {
SSLContext sslContext = StringUtils.hasLength(this.sslProvider)
? SSLContext.getInstance(this.sslProtocol, this.sslProvider)
: SSLContext.getInstance(this.sslProtocol);
sslContext.init(this.keyManagers, this.trustManagers, this.rnd);
if (this.logger.isDebugEnabled()) {
this.logger.debug("Initialized SSL Context with key managers ["
+ StringUtils.arrayToCommaDelimitedString(this.keyManagers) + "] trust managers ["
+ StringUtils.arrayToCommaDelimitedString(this.trustManagers) + "] secure random [" + this.rnd
+ "]");
}
return sslContext.getSocketFactory();
}
catch (NoSuchAlgorithmException | NoSuchProviderException ex) {
throw new HttpsTransportException("Could not create SSLContext: " + ex.getMessage(), ex);
}
catch (KeyManagementException ex) {
throw new HttpsTransportException("Could not initialize SSLContext: " + ex.getMessage(), ex);
}
}
}

View File

@@ -0,0 +1,68 @@
/*
* 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 com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpHandler;
import org.springframework.ws.transport.support.SimpleWebServiceMessageReceiverObjectSupport;
/**
* {@link HttpHandler} that can be used to handle incoming {@link HttpExchange} service
* requests. Designed for Sun's JRE 1.6 HTTP server.
* <p>
* Requires a {@link org.springframework.ws.WebServiceMessageFactory} which is used to
* convert the incoming {@link HttpExchange} into a
* {@link org.springframework.ws.WebServiceMessage}, and passes that to the
* {@link org.springframework.ws.transport.WebServiceMessageReceiver}
* {@link #setMessageReceiver(org.springframework.ws.transport.WebServiceMessageReceiver)
* registered}.
*
* @author Arjen Poutsma
* @since 1.5.0
*/
public class WebServiceMessageReceiverHttpHandler extends SimpleWebServiceMessageReceiverObjectSupport
implements HttpHandler {
private boolean chunkedEncoding = false;
/** Enables chunked encoding on response bodies. Defaults to {@code false}. */
public void setChunkedEncoding(boolean chunkedEncoding) {
this.chunkedEncoding = chunkedEncoding;
}
@Override
public void handle(HttpExchange httpExchange) throws IOException {
if (HttpTransportConstants.METHOD_POST.equals(httpExchange.getRequestMethod())) {
HttpExchangeConnection connection = new HttpExchangeConnection(httpExchange);
connection.setChunkedEncoding(this.chunkedEncoding);
try {
handleConnection(connection);
}
catch (Exception ex) {
this.logger.error(ex);
}
}
else {
httpExchange.sendResponseHeaders(HttpTransportConstants.STATUS_METHOD_NOT_ALLOWED, -1);
httpExchange.close();
}
}
}

View File

@@ -0,0 +1,84 @@
/*
* 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.ByteArrayOutputStream;
import java.io.IOException;
import javax.xml.transform.TransformerException;
import javax.xml.transform.stream.StreamResult;
import com.sun.net.httpserver.Headers;
import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpHandler;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.util.Assert;
import org.springframework.util.FileCopyUtils;
import org.springframework.ws.wsdl.WsdlDefinition;
import org.springframework.xml.transform.TransformerObjectSupport;
/**
* {@link HttpHandler} implementation for WSDL documents.
*
* @author Arjen Poutsma
* @since 1.5.0
*/
public class WsdlDefinitionHttpHandler extends TransformerObjectSupport implements HttpHandler, InitializingBean {
private static final String CONTENT_TYPE = "text/xml";
private WsdlDefinition definition;
public WsdlDefinitionHttpHandler() {
}
public WsdlDefinitionHttpHandler(WsdlDefinition definition) {
this.definition = definition;
}
public void setDefinition(WsdlDefinition definition) {
this.definition = definition;
}
@Override
public void afterPropertiesSet() throws Exception {
Assert.notNull(this.definition, "'definition' is required");
}
@Override
public void handle(HttpExchange httpExchange) throws IOException {
try (httpExchange) {
if (HttpTransportConstants.METHOD_GET.equals(httpExchange.getRequestMethod())) {
Headers headers = httpExchange.getResponseHeaders();
headers.set(HttpTransportConstants.HEADER_CONTENT_TYPE, CONTENT_TYPE);
ByteArrayOutputStream os = new ByteArrayOutputStream();
transform(this.definition.getSource(), new StreamResult(os));
byte[] buf = os.toByteArray();
httpExchange.sendResponseHeaders(HttpTransportConstants.STATUS_OK, buf.length);
FileCopyUtils.copy(buf, httpExchange.getResponseBody());
}
else {
httpExchange.sendResponseHeaders(HttpTransportConstants.STATUS_METHOD_NOT_ALLOWED, -1);
}
}
catch (TransformerException ex) {
this.logger.error(ex, ex);
}
}
}

View File

@@ -0,0 +1,87 @@
/*
* 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.support;
import org.springframework.beans.factory.BeanNameAware;
import org.springframework.core.task.SimpleAsyncTaskExecutor;
import org.springframework.core.task.TaskExecutor;
import org.springframework.util.ClassUtils;
/**
* Abstract base class for asynchronous standalone, server-side transport objects.
* Contains a Spring {@link TaskExecutor}, and various lifecycle callbacks.
*
* @author Arjen Poutsma
*/
public abstract class AbstractAsyncStandaloneMessageReceiver extends AbstractStandaloneMessageReceiver
implements BeanNameAware {
/** Default thread name prefix. */
public final String DEFAULT_THREAD_NAME_PREFIX = ClassUtils.getShortName(getClass()) + "-";
private TaskExecutor taskExecutor;
private String beanName;
/**
* Set the Spring {@link TaskExecutor} to use for running the listener threads.
* Default is {@link SimpleAsyncTaskExecutor}, starting up a number of new threads.
* <p>
* Specify an alternative task executor for integration with an existing thread pool,
* such as the
* {@link org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor}.
*/
public void setTaskExecutor(TaskExecutor taskExecutor) {
this.taskExecutor = taskExecutor;
}
@Override
public void setBeanName(String beanName) {
this.beanName = beanName;
}
@Override
public void afterPropertiesSet() throws Exception {
if (this.taskExecutor == null) {
this.taskExecutor = createDefaultTaskExecutor();
}
super.afterPropertiesSet();
}
/**
* Create a default TaskExecutor. Called if no explicit TaskExecutor has been
* specified.
* <p>
* The default implementation builds a
* {@link org.springframework.core.task.SimpleAsyncTaskExecutor} with the specified
* bean name (or the class name, if no bean name specified) as thread name prefix.
* @see org.springframework.core.task.SimpleAsyncTaskExecutor#SimpleAsyncTaskExecutor(String)
*/
protected TaskExecutor createDefaultTaskExecutor() {
String threadNamePrefix = (this.beanName != null) ? this.beanName + "-" : this.DEFAULT_THREAD_NAME_PREFIX;
return new SimpleAsyncTaskExecutor(threadNamePrefix);
}
/**
* Executes the given {@link Runnable} via this receiver's {@link TaskExecutor}.
* @see #setTaskExecutor(TaskExecutor)
*/
protected void execute(Runnable runnable) {
this.taskExecutor.execute(runnable);
}
}

View File

@@ -0,0 +1,141 @@
/*
* 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.support;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.context.Lifecycle;
/**
* Abstract base class for standalone, server-side transport objects. Provides a basic,
* thread-safe implementation of the {@link Lifecycle} interface, and various template
* methods to be implemented by concrete sub classes.
*
* @author Arjen Poutsma
* @since 1.5.0
*/
public abstract class AbstractStandaloneMessageReceiver extends SimpleWebServiceMessageReceiverObjectSupport
implements Lifecycle, DisposableBean {
private volatile boolean active = false;
private boolean autoStartup = true;
private boolean running = false;
private final Object lifecycleMonitor = new Object();
/**
* Return whether this server is currently active, that is, whether it has been set up
* but not shut down yet.
*/
public final boolean isActive() {
synchronized (this.lifecycleMonitor) {
return this.active;
}
}
/**
* Return whether this server is currently running, that is, whether it has been
* started and not stopped yet.
*/
@Override
public final boolean isRunning() {
synchronized (this.lifecycleMonitor) {
return this.running;
}
}
/**
* Set whether to automatically start the receiver after initialization.
* <p>
* Default is {@code true}; set this to {@code false} to allow for manual startup.
*/
public void setAutoStartup(boolean autoStartup) {
this.autoStartup = autoStartup;
}
/**
* Calls {@link #activate()} when the BeanFactory initializes the receiver instance.
*/
@Override
public void afterPropertiesSet() throws Exception {
activate();
}
/** Calls {@link #shutdown()} when the BeanFactory destroys the receiver instance. */
@Override
public void destroy() {
shutdown();
}
/**
* Initialize this server. Starts the server if {@link #setAutoStartup(boolean)
* autoStartup} hasn't been turned off.
*/
public final void activate() throws Exception {
synchronized (this.lifecycleMonitor) {
this.active = true;
}
onActivate();
if (this.autoStartup) {
start();
}
}
/** Start this server. */
@Override
public final void start() {
synchronized (this.lifecycleMonitor) {
this.running = true;
}
onStart();
}
/** Stop this server. */
@Override
public final void stop() {
synchronized (this.lifecycleMonitor) {
this.running = false;
}
onStop();
}
/** Shut down this server. */
public final void shutdown() {
synchronized (this.lifecycleMonitor) {
this.running = false;
this.active = false;
}
onShutdown();
}
/**
* Template method invoked when {@link #activate()} is invoked.
* @throws Exception in case of errors
*/
protected abstract void onActivate() throws Exception;
/** Template method invoked when {@link #start()} is invoked. */
protected abstract void onStart();
/** Template method invoked when {@link #stop()} is invoked. */
protected abstract void onStop();
/** Template method invoked when {@link #shutdown()} is invoked. */
protected abstract void onShutdown();
}

View File

@@ -0,0 +1,61 @@
/*
* 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.support;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.util.Assert;
import org.springframework.ws.transport.WebServiceConnection;
import org.springframework.ws.transport.WebServiceMessageReceiver;
/**
* Base class for server-side transport objects which have a predefined
* {@link WebServiceMessageReceiver}.
*
* @author Arjen Poutsma
* @since 1.5.0
* @see #handleConnection(WebServiceConnection)
*/
public abstract class SimpleWebServiceMessageReceiverObjectSupport extends WebServiceMessageReceiverObjectSupport
implements InitializingBean {
private WebServiceMessageReceiver messageReceiver;
/**
* Returns the {@code WebServiceMessageReceiver} used by this listener.
*/
public WebServiceMessageReceiver getMessageReceiver() {
return this.messageReceiver;
}
/**
* Sets the {@code WebServiceMessageReceiver} used by this listener.
*/
public void setMessageReceiver(WebServiceMessageReceiver messageReceiver) {
this.messageReceiver = messageReceiver;
}
@Override
public void afterPropertiesSet() throws Exception {
super.afterPropertiesSet();
Assert.notNull(getMessageReceiver(), "messageReceiver must not be null");
}
protected final void handleConnection(WebServiceConnection connection) throws Exception {
handleConnection(connection, getMessageReceiver());
}
}

View File

@@ -0,0 +1,34 @@
/*
* 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.util.Locale;
import org.springframework.ws.context.MessageContext;
import org.springframework.ws.server.endpoint.MessageEndpoint;
import org.springframework.ws.soap.SoapMessage;
public class FaultEndpoint implements MessageEndpoint {
@Override
public void invoke(MessageContext messageContext) {
SoapMessage response = (SoapMessage) messageContext.getResponse();
response.getSoapBody().addServerOrReceiverFault("Something went wrong", Locale.ENGLISH);
}
}

View File

@@ -0,0 +1,31 @@
/*
* 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.springframework.ws.context.MessageContext;
import org.springframework.ws.server.endpoint.MessageEndpoint;
/**
* @author Arjen Poutsma
*/
public class NoResponseEndpoint implements MessageEndpoint {
@Override
public void invoke(MessageContext messageContext) {
}
}

View File

@@ -0,0 +1,32 @@
/*
* 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.springframework.ws.context.MessageContext;
import org.springframework.ws.server.endpoint.MessageEndpoint;
/**
* @author Arjen Poutsma
*/
public class ResponseEndpoint implements MessageEndpoint {
@Override
public void invoke(MessageContext messageContext) {
messageContext.getResponse();
}
}

View File

@@ -0,0 +1,172 @@
/*
* 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.InetSocketAddress;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Executor;
import com.sun.net.httpserver.Authenticator;
import com.sun.net.httpserver.Filter;
import com.sun.net.httpserver.HttpContext;
import com.sun.net.httpserver.HttpHandler;
import com.sun.net.httpserver.HttpServer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.FactoryBean;
import org.springframework.beans.factory.InitializingBean;
/**
* Utility class ONLY used for testing SOAP interactions through the JRE's built-in
* {@link HttpServer}.
*/
class SimpleHttpServerFactoryBean implements FactoryBean<HttpServer>, InitializingBean, DisposableBean {
protected final Logger logger = LoggerFactory.getLogger(getClass());
private int port = 8080;
private String hostname;
private int backlog = -1;
private int shutdownDelay = 0;
private Executor executor;
private Map<String, HttpHandler> contexts;
private List<Filter> filters;
private Authenticator authenticator;
private HttpServer server;
/**
* Specify the HTTP server's port. Default is 8080.
*/
public void setPort(int port) {
this.port = port;
}
/**
* Specify the HTTP server's hostname to bind to. Default is localhost; can be
* overridden with a specific network address to bind to.
*/
public void setHostname(String hostname) {
this.hostname = hostname;
}
/**
* Specify the HTTP server's TCP backlog. Default is -1, indicating the system's
* default value.
*/
public void setBacklog(int backlog) {
this.backlog = backlog;
}
/**
* Specify the number of seconds to wait until HTTP exchanges have completed when
* shutting down the HTTP server. Default is 0.
*/
public void setShutdownDelay(int shutdownDelay) {
this.shutdownDelay = shutdownDelay;
}
/**
* Set the JDK concurrent executor to use for dispatching incoming requests.
* @see HttpServer#setExecutor
*/
public void setExecutor(Executor executor) {
this.executor = executor;
}
/**
* Register {@link HttpHandler HttpHandlers} for specific context paths.
* @param contexts a Map with context paths as keys and HttpHandler objects as values
*/
public void setContexts(Map<String, HttpHandler> contexts) {
this.contexts = contexts;
}
/**
* Register common {@link Filter Filters} to be applied to all locally registered
* {@link #setContexts contexts}.
*/
public void setFilters(List<Filter> filters) {
this.filters = filters;
}
/**
* Register a common {@link Authenticator} to be applied to all locally registered
* {@link #setContexts contexts}.
*/
public void setAuthenticator(Authenticator authenticator) {
this.authenticator = authenticator;
}
@Override
public void afterPropertiesSet() throws IOException {
InetSocketAddress address = (this.hostname != null ? new InetSocketAddress(this.hostname, this.port)
: new InetSocketAddress(this.port));
this.server = HttpServer.create(address, this.backlog);
if (this.executor != null) {
this.server.setExecutor(this.executor);
}
if (this.contexts != null) {
this.contexts.forEach((key, context) -> {
HttpContext httpContext = this.server.createContext(key, context);
if (this.filters != null) {
httpContext.getFilters().addAll(this.filters);
}
if (this.authenticator != null) {
httpContext.setAuthenticator(this.authenticator);
}
});
}
if (this.logger.isInfoEnabled()) {
this.logger.info("Starting HttpServer at address " + address);
}
this.server.start();
}
@Override
public HttpServer getObject() {
return this.server;
}
@Override
public Class<? extends HttpServer> getObjectType() {
return (this.server != null ? this.server.getClass() : HttpServer.class);
}
@Override
public boolean isSingleton() {
return true;
}
@Override
public void destroy() {
this.logger.info("Stopping HttpServer");
this.server.stop(this.shutdownDelay);
}
}

View File

@@ -0,0 +1,127 @@
/*
* 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 org.apache.hc.client5.http.classic.methods.HttpGet;
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.HttpClients;
import org.apache.hc.core5.http.ClassicHttpRequest;
import org.apache.hc.core5.http.ClassicHttpResponse;
import org.apache.hc.core5.http.ContentType;
import org.apache.hc.core5.http.HttpHeaders;
import org.apache.hc.core5.http.io.HttpClientResponseHandler;
import org.apache.hc.core5.http.io.entity.InputStreamEntity;
import org.assertj.core.api.ThrowingConsumer;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.ws.transport.TransportConstants;
import static org.assertj.core.api.Assertions.assertThat;
@ExtendWith(SpringExtension.class)
@ContextConfiguration("httpserver-applicationContext.xml")
public class WebServiceHttpHandlerIntegrationTest {
@Autowired
private int port;
@Test
public void testInvalidMethod() {
HttpGet httpRequest = new HttpGet(serviceUrl());
execute(httpRequest, response -> {
assertThat(response.getCode()).isEqualTo(HttpTransportConstants.STATUS_METHOD_NOT_ALLOWED);
assertThat(response.containsHeader(HttpHeaders.CONTENT_LENGTH)).isTrue();
assertThat(response.getHeader(HttpHeaders.CONTENT_LENGTH).getValue()).isEqualTo("0");
});
}
@Test
public void testNoResponse() throws IOException {
HttpPost httpRequest = new HttpPost(serviceUrl());
httpRequest.addHeader(TransportConstants.HEADER_SOAP_ACTION, "http://springframework.org/spring-ws/NoResponse");
Resource soapRequest = new ClassPathResource("soapRequest.xml", WebServiceHttpHandlerIntegrationTest.class);
httpRequest.setEntity(new InputStreamEntity(soapRequest.getInputStream(), ContentType.TEXT_XML));
execute(httpRequest, response -> {
assertThat(response.getCode()).isEqualTo(HttpTransportConstants.STATUS_ACCEPTED);
assertThat(response.containsHeader(HttpHeaders.CONTENT_LENGTH)).isTrue();
assertThat(response.getHeader(HttpHeaders.CONTENT_LENGTH).getValue()).isEqualTo("0");
});
}
@Test
public void testResponse() throws IOException {
HttpPost httpRequest = new HttpPost(serviceUrl());
httpRequest.addHeader(TransportConstants.HEADER_SOAP_ACTION, "http://springframework.org/spring-ws/Response");
Resource soapRequest = new ClassPathResource("soapRequest.xml", WebServiceHttpHandlerIntegrationTest.class);
httpRequest.setEntity(new InputStreamEntity(soapRequest.getInputStream(), ContentType.TEXT_XML));
execute(httpRequest, response -> {
assertThat(response.getCode()).isEqualTo(HttpTransportConstants.STATUS_OK);
assertThat(response.containsHeader(HttpHeaders.CONTENT_LENGTH)).isTrue();
assertThat(response.getHeader(HttpHeaders.CONTENT_LENGTH).getValue()).asInt().isGreaterThan(0);
});
}
@Test
public void testNoEndpoint() throws IOException {
HttpPost httpRequest = new HttpPost(serviceUrl());
httpRequest.addHeader(TransportConstants.HEADER_SOAP_ACTION, "http://springframework.org/spring-ws/NoEndpoint");
Resource soapRequest = new ClassPathResource("soapRequest.xml", WebServiceHttpHandlerIntegrationTest.class);
httpRequest.setEntity(new InputStreamEntity(soapRequest.getInputStream(), ContentType.TEXT_XML));
execute(httpRequest, response -> {
assertThat(response.getCode()).isEqualTo(HttpTransportConstants.STATUS_NOT_FOUND);
assertThat(response.containsHeader(HttpHeaders.CONTENT_LENGTH)).isTrue();
assertThat(response.getHeader(HttpHeaders.CONTENT_LENGTH).getValue()).isEqualTo("0");
});
}
@Test
public void testFault() throws IOException {
HttpPost httpRequest = new HttpPost(serviceUrl());
httpRequest.addHeader(TransportConstants.HEADER_SOAP_ACTION, "http://springframework.org/spring-ws/Fault");
Resource soapRequest = new ClassPathResource("soapRequest.xml", WebServiceHttpHandlerIntegrationTest.class);
httpRequest.setEntity(new InputStreamEntity(soapRequest.getInputStream(), ContentType.TEXT_XML));
execute(httpRequest, response -> assertThat(response.getCode())
.isEqualTo(HttpTransportConstants.STATUS_INTERNAL_SERVER_ERROR));
}
private String serviceUrl() {
return "http://localhost:%s/service".formatted(this.port);
}
private void execute(ClassicHttpRequest request, ThrowingConsumer<ClassicHttpResponse> responseHandler) {
try (CloseableHttpClient httpclient = HttpClients.createDefault()) {
HttpClientResponseHandler<Object> rh = httpResponse -> {
responseHandler.accept(httpResponse);
return null;
};
httpclient.execute(request, rh);
}
catch (IOException ex) {
throw new IllegalStateException(ex);
}
}
}

View File

@@ -0,0 +1,104 @@
/*
* 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.test;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.util.Random;
import org.springframework.util.Assert;
/**
* Utility class that finds free BSD ports for use in testing scenario's.
*
* @author Ben Hale
* @author Arjen Poutsma
*/
public abstract class FreePortScanner {
private static final int MIN_SAFE_PORT = 1024;
private static final int MAX_PORT = 65535;
private static final Random random = new Random();
/**
* Returns the number of a free port in the default range.
*/
public static int getFreePort() {
return getFreePort(MIN_SAFE_PORT, MAX_PORT);
}
/**
* Returns the number of a free port in the given range.
*/
public static int getFreePort(int minPort, int maxPort) {
Assert.isTrue(minPort > 0, "'minPort' must be larger than 0");
Assert.isTrue(maxPort > minPort, "'maxPort' must be larger than minPort");
int portRange = maxPort - minPort;
int candidatePort;
int searchCounter = 0;
do {
if (++searchCounter > portRange) {
throw new IllegalStateException(
String.format("There were no ports available in the range %d to %d", minPort, maxPort));
}
candidatePort = getRandomPort(minPort, portRange);
}
while (!isPortAvailable(candidatePort));
return candidatePort;
}
private static int getRandomPort(int minPort, int portRange) {
return minPort + random.nextInt(portRange);
}
private static boolean isPortAvailable(int port) {
ServerSocket serverSocket;
try {
serverSocket = new ServerSocket();
}
catch (IOException ex) {
throw new IllegalStateException("Unable to create ServerSocket.", ex);
}
try {
InetSocketAddress sa = new InetSocketAddress(port);
serverSocket.bind(sa);
return true;
}
catch (IOException ex) {
return false;
}
finally {
try {
serverSocket.close();
}
catch (IOException ex) {
// ignore
}
}
}
}

View File

@@ -0,0 +1,64 @@
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.0.xsd">
<bean id="port"
class="org.springframework.beans.factory.config.MethodInvokingFactoryBean">
<property name="targetClass"
value="org.springframework.ws.transport.http.test.FreePortScanner"/>
<property name="targetMethod" value="getFreePort"/>
</bean>
<bean id="httpServer"
class="org.springframework.ws.transport.http.SimpleHttpServerFactoryBean">
<property name="port" ref="port"/>
<property name="contexts">
<map>
<entry key="/service" value-ref="webServiceHandler"/>
</map>
</property>
</bean>
<bean id="webServiceHandler"
class="org.springframework.ws.transport.http.WebServiceMessageReceiverHttpHandler">
<property name="chunkedEncoding" value="true"/>
<property name="messageFactory" ref="messageFactory"/>
<property name="messageReceiver" ref="messageDispatcher"/>
</bean>
<bean id="messageFactory"
class="org.springframework.ws.soap.saaj.SaajSoapMessageFactory"/>
<bean id="messageDispatcher"
class="org.springframework.ws.soap.server.SoapMessageDispatcher">
<property name="endpointMappings" ref="payloadMapping"/>
</bean>
<bean id="payloadMapping"
class="org.springframework.ws.soap.server.endpoint.mapping.SoapActionEndpointMapping">
<property name="mappings">
<props>
<prop key="http://springframework.org/spring-ws/NoResponse">
noResponseEndpoint
</prop>
<prop key="http://springframework.org/spring-ws/Response">
responseEndpoint
</prop>
<prop key="http://springframework.org/spring-ws/Fault">faultEndpoint
</prop>
</props>
</property>
</bean>
<bean id="noResponseEndpoint"
class="org.springframework.ws.transport.http.NoResponseEndpoint"/>
<bean id="responseEndpoint"
class="org.springframework.ws.transport.http.ResponseEndpoint"/>
<bean id="faultEndpoint"
class="org.springframework.ws.transport.http.FaultEndpoint"/>
</beans>