INT-1800: Add MTOM Support for WS

JIRA: https://jira.spring.io/browse/INT-1800

The Simple WebService Inbound and Outbound Gateways can operate with `WebServiceMessage`s directly.
This allows to create those messages manually and add attachments to them.
Or, on the other hand, process incoming messages with attachments manually.

For this purpose the `SimpleWebServiceOutboundGateway` is supplied with new `extractPayload` property.
Also the `UnmarshallingTransformer` can now process `MimeMessage` as payload to unmarshal it into object graph with attachments if that
This commit is contained in:
Artem Bilan
2017-02-16 21:22:28 -05:00
committed by Gary Russell
parent 695b168b0b
commit 1e07551294
14 changed files with 373 additions and 112 deletions

View File

@@ -688,6 +688,16 @@ project('spring-integration-xml') {
exclude group: 'org.springframework', module: 'spring-context'
exclude group: 'org.springframework', module: 'spring-core'
}
compile ("org.springframework.ws:spring-ws-core:$springWsVersion") {
optional it
exclude group: 'org.springframework', module: 'spring-aop'
exclude group: 'org.springframework', module: 'spring-beans'
exclude group: 'org.springframework', module: 'spring-context'
exclude group: 'org.springframework', module: 'spring-core'
exclude group: 'org.springframework', module: 'spring-oxm'
exclude group: 'org.springframework', module: 'spring-web'
exclude group: 'org.springframework', module: 'spring-webmvc'
}
testCompile "xmlunit:xmlunit:$xmlUnitVersion"
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2011 the original author or authors.
* Copyright 2002-2017 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.
@@ -34,6 +34,8 @@ import org.springframework.xml.transform.TransformerObjectSupport;
/**
* @author Mark Fisher
* @author Oleg Zhurakousky
* @author Artem Bilan
*
* @since 1.0.2
*/
public class SimpleWebServiceInboundGateway extends AbstractWebServiceInboundGateway {
@@ -62,26 +64,31 @@ public class SimpleWebServiceInboundGateway extends AbstractWebServiceInboundGat
if (replyMessage != null) {
Object replyPayload = replyMessage.getPayload();
Source responseSource = null;
if (replyPayload instanceof Source) {
responseSource = (Source) replyPayload;
}
else if (replyPayload instanceof Document) {
responseSource = new DOMSource((Document) replyPayload);
}
else if (replyPayload instanceof String) {
responseSource = new StringSource((String) replyPayload);
if (replyPayload instanceof WebServiceMessage) {
messageContext.setResponse((WebServiceMessage) replyPayload);
}
else {
throw new IllegalArgumentException("The reply Message payload must be a ["
+ Source.class.getName() + "], [" + Document.class.getName()
+ "], or [java.lang.String]. The actual type was ["
+ replyPayload.getClass().getName() + "]");
if (replyPayload instanceof Source) {
responseSource = (Source) replyPayload;
}
else if (replyPayload instanceof Document) {
responseSource = new DOMSource((Document) replyPayload);
}
else if (replyPayload instanceof String) {
responseSource = new StringSource((String) replyPayload);
}
else {
throw new IllegalArgumentException("The reply Message payload must be a ["
+ Source.class.getName() + "], [" + Document.class.getName()
+ "], [java.lang.String] or [" + WebServiceMessage.class.getName() + "]. " +
"The actual type was [" + replyPayload.getClass().getName() + "]");
}
WebServiceMessage response = messageContext.getResponse();
this.transformerSupportDelegate.transformSourceToResult(responseSource, response.getPayloadResult());
toSoapHeaders(response, replyMessage);
}
WebServiceMessage response = messageContext.getResponse();
this.transformerSupportDelegate.transformSourceToResult(responseSource, response.getPayloadResult());
this.toSoapHeaders(response, replyMessage);
}
}
@@ -95,6 +102,7 @@ public class SimpleWebServiceInboundGateway extends AbstractWebServiceInboundGat
void transformSourceToResult(Source source, Result result) throws TransformerException {
this.transform(source, result);
}
}
}

View File

@@ -17,6 +17,7 @@
package org.springframework.integration.ws;
import java.io.IOException;
import java.util.Iterator;
import javax.xml.transform.Result;
import javax.xml.transform.Source;
@@ -34,6 +35,8 @@ import org.springframework.ws.WebServiceMessageFactory;
import org.springframework.ws.client.core.SourceExtractor;
import org.springframework.ws.client.core.WebServiceMessageCallback;
import org.springframework.ws.client.support.destination.DestinationProvider;
import org.springframework.ws.mime.Attachment;
import org.springframework.ws.mime.MimeMessage;
import org.springframework.xml.transform.StringResult;
import org.springframework.xml.transform.StringSource;
import org.springframework.xml.transform.TransformerObjectSupport;
@@ -50,6 +53,8 @@ public class SimpleWebServiceOutboundGateway extends AbstractWebServiceOutboundG
private final SourceExtractor<?> sourceExtractor;
private volatile boolean extractPayload = true;
public SimpleWebServiceOutboundGateway(DestinationProvider destinationProvider) {
this(destinationProvider, null, null);
}
@@ -79,6 +84,15 @@ public class SimpleWebServiceOutboundGateway extends AbstractWebServiceOutboundG
this.sourceExtractor = (sourceExtractor != null) ? sourceExtractor : new DefaultSourceExtractor();
}
/**
*
* @param extractPayload
* @since 5.0
*/
public void setExtractPayload(boolean extractPayload) {
this.extractPayload = extractPayload;
}
@Override
public String getComponentType() {
return "ws:outbound-gateway(simple)";
@@ -95,9 +109,10 @@ public class SimpleWebServiceOutboundGateway extends AbstractWebServiceOutboundG
else if (requestPayload instanceof Document) {
responseResultInstance = new DOMResult();
}
return this.getWebServiceTemplate().sendAndReceive(uri,
new SimpleRequestMessageCallback(requestCallback, requestMessage),
new SimpleResponseMessageExtractor(responseResultInstance));
return getWebServiceTemplate()
.sendAndReceive(uri,
new SimpleRequestMessageCallback(requestCallback, requestMessage),
new SimpleResponseMessageExtractor(responseResultInstance));
}
private final class SimpleRequestMessageCallback extends RequestMessageCallback {
@@ -110,7 +125,10 @@ public class SimpleWebServiceOutboundGateway extends AbstractWebServiceOutboundG
public void doWithMessageInternal(WebServiceMessage message, Object payload)
throws IOException, TransformerException {
Source source = this.extractSource(payload);
this.transform(source, message.getPayloadResult());
transform(source, message.getPayloadResult());
if (message instanceof MimeMessage && payload instanceof MimeMessage) {
copyAttachments((MimeMessage) payload, (MimeMessage) message);
}
}
private Source extractSource(Object requestPayload) throws IOException, TransformerException {
@@ -128,17 +146,28 @@ public class SimpleWebServiceOutboundGateway extends AbstractWebServiceOutboundG
else if (requestPayload instanceof Document) {
source = new DOMSource((Document) requestPayload);
}
else if (requestPayload instanceof WebServiceMessage) {
source = ((WebServiceMessage) requestPayload).getPayloadSource();
}
else {
throw new MessagingException("Unsupported payload type '" + requestPayload.getClass() +
"'. " + this.getClass().getName() + " only supports 'java.lang.String', '" +
Source.class.getName() +
"', and '" + Document.class.getName() + "'. Consider either using the '"
"', '" + Document.class.getName() + "' and '" + WebServiceMessage.class.getName() + "'. " +
"Consider either using the '"
+ MarshallingWebServiceOutboundGateway.class.getName() + "' or a Message Transformer.");
}
return source;
}
private void copyAttachments(MimeMessage source, MimeMessage target) {
for (Iterator<Attachment> attachments = source.getAttachments(); attachments.hasNext(); ) {
Attachment attachment = attachments.next();
target.addAttachment(attachment.getContentId(), attachment.getDataHandler());
}
}
}
private final class SimpleResponseMessageExtractor extends ResponseMessageExtractor {
@@ -152,22 +181,27 @@ public class SimpleWebServiceOutboundGateway extends AbstractWebServiceOutboundG
@Override
public Object doExtractData(WebServiceMessage message) throws IOException, TransformerException {
Source payloadSource = message.getPayloadSource();
if (payloadSource != null && this.result != null) {
this.transform(payloadSource, this.result);
if (this.result instanceof StringResult) {
return this.result.toString();
}
else if (this.result instanceof DOMResult) {
return ((DOMResult) this.result).getNode();
}
else {
return this.result;
}
if (!SimpleWebServiceOutboundGateway.this.extractPayload) {
return message;
}
else {
Source payloadSource = message.getPayloadSource();
return payloadSource;
if (payloadSource != null && this.result != null) {
this.transform(payloadSource, this.result);
if (this.result instanceof StringResult) {
return this.result.toString();
}
else if (this.result instanceof DOMResult) {
return ((DOMResult) this.result).getNode();
}
else {
return this.result;
}
}
return payloadSource;
}
}
}

View File

@@ -97,9 +97,9 @@ public class WebServiceOutboundGatewayParser extends AbstractOutboundGatewayPars
@Override
protected void postProcessGateway(BeanDefinitionBuilder builder, Element element, ParserContext parserContext) {
String marshallerRef = element.getAttribute("marshaller");
String unmarshallerRef = element.getAttribute("unmarshaller");
if (StringUtils.hasText(marshallerRef)) {
builder.addConstructorArgReference(marshallerRef);
String unmarshallerRef = element.getAttribute("unmarshaller");
if (StringUtils.hasText(unmarshallerRef)) {
builder.addConstructorArgReference(unmarshallerRef);
}
@@ -154,6 +154,19 @@ public class WebServiceOutboundGatewayParser extends AbstractOutboundGatewayPars
if (StringUtils.hasText(interceptorListRef)) {
builder.addPropertyReference("interceptors", interceptorListRef);
}
if (StringUtils.hasText(marshallerRef) || StringUtils.hasText(unmarshallerRef)) {
String extractPayload = element.getAttribute("extract-payload");
if (StringUtils.hasText(extractPayload)) {
parserContext.getReaderContext()
.warning("Setting 'extract-payload' attribute has no effect when used with " +
"a marshalling Web Service Outbound Gateway.", element);
}
}
else {
IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, "extract-payload");
}
}
}

View File

@@ -300,6 +300,18 @@ this list can also be simple patterns to be matched against the header names (e.
]]></xsd:documentation>
</xsd:annotation>
</xsd:attribute>
<xsd:attribute name="extract-payload">
<xsd:annotation>
<xsd:documentation>
Set to 'true' to extract the WebServiceMessage payload.
Otherwise the whole WebServiceMessage is used as the integration message payload.
This option is only applied for the simple gateway.
</xsd:documentation>
</xsd:annotation>
<xsd:simpleType>
<xsd:union memberTypes="xsd:boolean xsd:string"/>
</xsd:simpleType>
</xsd:attribute>
<xsd:attributeGroup ref="integration:smartLifeCycleAttributeGroup"/>
</xsd:complexType>
</xsd:element>
@@ -396,7 +408,18 @@ this list can also be simple patterns to be matched against the header names (e.
</xsd:appinfo>
</xsd:annotation>
</xsd:attribute>
<xsd:attribute name="extract-payload" type="xsd:string"/>
<xsd:attribute name="extract-payload">
<xsd:annotation>
<xsd:documentation>
Set to 'true' to extract the WebServiceMessage payload.
Otherwise the whole WebServiceMessage is used as the integration message payload.
This option is only applied for the simple gateway.
</xsd:documentation>
</xsd:annotation>
<xsd:simpleType>
<xsd:union memberTypes="xsd:boolean xsd:string"/>
</xsd:simpleType>
</xsd:attribute>
<xsd:attribute name="header-mapper">
<xsd:annotation>
<xsd:documentation>

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2016 the original author or authors.
* Copyright 2002-2017 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.
@@ -16,32 +16,57 @@
package org.springframework.integration.ws;
import static org.hamcrest.Matchers.instanceOf;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertThat;
import static org.mockito.Mockito.mock;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.net.URI;
import java.nio.charset.Charset;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicReference;
import javax.xml.soap.MessageFactory;
import javax.xml.soap.SOAPException;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import org.hamcrest.Matchers;
import org.junit.Test;
import org.mockito.Mockito;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.core.io.ByteArrayResource;
import org.springframework.integration.handler.ReplyRequiredException;
import org.springframework.integration.support.MessageBuilder;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.PollableChannel;
import org.springframework.messaging.support.GenericMessage;
import org.springframework.util.StreamUtils;
import org.springframework.util.concurrent.SettableListenableFuture;
import org.springframework.ws.WebServiceMessage;
import org.springframework.ws.WebServiceMessageFactory;
import org.springframework.ws.client.WebServiceClientException;
import org.springframework.ws.client.support.destination.DestinationProvider;
import org.springframework.ws.client.support.interceptor.ClientInterceptorAdapter;
import org.springframework.ws.context.MessageContext;
import org.springframework.ws.mime.Attachment;
import org.springframework.ws.mime.MimeMessage;
import org.springframework.ws.soap.SoapMessage;
import org.springframework.ws.soap.saaj.SaajSoapMessageFactory;
import org.springframework.ws.transport.WebServiceConnection;
import org.springframework.ws.transport.WebServiceMessageSender;
import org.springframework.xml.transform.StringResult;
import org.springframework.xml.transform.StringSource;
/**
* @author Mark Fisher
@@ -111,6 +136,65 @@ public class SimpleWebServiceOutboundGatewayTests {
gateway.handleMessage(new GenericMessage<String>("<test>foo</test>"));
}
@Test
public void testAttachments() throws TransformerException, SOAPException, InterruptedException, ExecutionException, TimeoutException, IOException {
String uri = "http://www.example.org";
SimpleWebServiceOutboundGateway gateway = new SimpleWebServiceOutboundGateway(uri);
gateway.setBeanFactory(mock(BeanFactory.class));
final SettableListenableFuture<WebServiceMessage> requestFuture = new SettableListenableFuture<>();
ClientInterceptorAdapter interceptorAdapter = new ClientInterceptorAdapter() {
@Override
public boolean handleRequest(MessageContext messageContext) throws WebServiceClientException {
requestFuture.set(messageContext.getRequest());
return super.handleRequest(messageContext);
}
};
gateway.setInterceptors(interceptorAdapter);
gateway.afterPropertiesSet();
WebServiceMessageFactory messageFactory = new SaajSoapMessageFactory(MessageFactory.newInstance());
MimeMessage webServiceMessage = (MimeMessage) messageFactory.createWebServiceMessage();
String request = "<test>foo</test>";
TransformerFactory transformerFactory = TransformerFactory.newInstance();
Transformer transformer = transformerFactory.newTransformer();
transformer.transform(new StringSource(request), webServiceMessage.getPayloadResult());
webServiceMessage.addAttachment("myAttachment", new ByteArrayResource("my_data".getBytes()), "text/plain");
try {
gateway.handleMessage(new GenericMessage<>(webServiceMessage));
}
catch (Exception e) {
// expected
}
WebServiceMessage requestMessage = requestFuture.get(10, TimeUnit.SECONDS);
assertNotNull(requestMessage);
assertThat(requestMessage, instanceOf(MimeMessage.class));
transformer = transformerFactory.newTransformer();
transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes");
StringResult stringResult = new StringResult();
transformer.transform(requestMessage.getPayloadSource(), stringResult);
assertEquals(request, stringResult.toString());
Attachment myAttachment = ((MimeMessage) requestMessage).getAttachment("myAttachment");
assertNotNull(myAttachment);
assertEquals("text/plain", myAttachment.getContentType());
assertEquals("my_data", StreamUtils.copyToString(myAttachment.getInputStream(), Charset.forName("UTF-8")));
}
public static WebServiceMessageSender createMockMessageSender(final String mockResponseMessage) throws Exception {
WebServiceMessageSender messageSender = Mockito.mock(WebServiceMessageSender.class);
WebServiceConnection wsConnection = Mockito.mock(WebServiceConnection.class);

View File

@@ -38,6 +38,7 @@
request-channel="inputChannel"
uri="http://example.org"
ignore-empty-responses="false"
extract-payload="false"
requires-reply="true"/>
<ws:outbound-gateway id="gatewayWithDefaultSourceExtractor"

View File

@@ -127,7 +127,8 @@ public class WebServiceOutboundGatewayParserTests {
assertEquals(SimpleWebServiceOutboundGateway.class, gateway.getClass());
DirectFieldAccessor accessor = new DirectFieldAccessor(gateway);
assertEquals(Boolean.FALSE, accessor.getPropertyValue("ignoreEmptyResponses"));
Assert.assertEquals(Boolean.TRUE, accessor.getPropertyValue("requiresReply"));
assertEquals(Boolean.TRUE, accessor.getPropertyValue("requiresReply"));
assertEquals(Boolean.FALSE, accessor.getPropertyValue("extractPayload"));
}
@Test

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2016 the original author or authors.
* Copyright 2002-2017 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.
@@ -16,10 +16,12 @@
package org.springframework.integration.ws.config;
import static org.hamcrest.Matchers.instanceOf;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;
import java.io.ByteArrayInputStream;
@@ -34,11 +36,13 @@ import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mockito;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.integration.channel.QueueChannel;
import org.springframework.integration.mapping.AbstractHeaderMapper;
import org.springframework.integration.support.MessageBuilder;
@@ -51,6 +55,8 @@ import org.springframework.messaging.Message;
import org.springframework.messaging.MessageChannel;
import org.springframework.oxm.Unmarshaller;
import org.springframework.oxm.XmlMappingException;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.util.xml.DomUtils;
import org.springframework.ws.WebServiceMessage;
import org.springframework.ws.WebServiceMessageFactory;
@@ -66,26 +72,30 @@ import org.springframework.xml.transform.StringResult;
/**
* @author Oleg Zhurakousky
* @author Gunnar Hillert
*
* @author Artem Bilan
*/
@RunWith(SpringRunner.class)
@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD)
public class WebServiceOutboundGatewayWithHeaderMapperTests {
String responseSoapMessage = "<?xml version=\"1.0\" encoding=\"UTF-8\" ?> " +
private static String responseSoapMessage = "<?xml version=\"1.0\" encoding=\"UTF-8\" ?> " +
"<SOAP-ENV:Envelope xmlns:SOAP-ENV=\"http://schemas.xmlsoap.org/soap/envelope/\"> " +
"<SOAP-ENV:Header/>" +
"<SOAP-ENV:Body> " +
"<root><name>jane</name></root>" +
"</SOAP-ENV:Body> " +
" <SOAP-ENV:Header/>" +
" <SOAP-ENV:Body> " +
" <root><name>jane</name></root>" +
" </SOAP-ENV:Body> " +
"</SOAP-ENV:Envelope>";
String responseNonSoapMessage = "<?xml version=\"1.0\" encoding=\"UTF-8\" ?> " +
private static String responseNonSoapMessage = "<?xml version=\"1.0\" encoding=\"UTF-8\" ?> " +
"<person><name>oleg</name></person>";
@Autowired
private ApplicationContext context;
@Test
public void headerMapperParserTest() throws Exception {
ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext(
"ws-outbound-gateway-with-headermappers.xml", this.getClass());
SimpleWebServiceOutboundGateway gateway = TestUtils.getPropertyValue(context.getBean("withHeaderMapper"),
SimpleWebServiceOutboundGateway gateway =
TestUtils.getPropertyValue(this.context.getBean("withHeaderMapper"),
"handler", SimpleWebServiceOutboundGateway.class);
DefaultSoapHeaderMapper headerMapper = TestUtils.getPropertyValue(gateway, "headerMapper",
DefaultSoapHeaderMapper.class);
@@ -108,22 +118,34 @@ public class WebServiceOutboundGatewayWithHeaderMapperTests {
assertFalse(replyHeaderMatcher.matchHeader("123baz123"));
assertTrue(replyHeaderMatcher.matchHeader("bar"));
assertTrue(replyHeaderMatcher.matchHeader("bar123"));
context.close();
}
@Test
public void withHeaderMapperString() throws Exception {
String payload = "<root><name>bill</name></root>";
Message<?> replyMessage = this.process(payload, "withHeaderMapper", "inputChannel", true);
Message<?> replyMessage = process(payload, "withHeaderMapper", "inputChannel", true);
assertTrue(replyMessage.getPayload() instanceof String);
assertEquals("bar", replyMessage.getHeaders().get("bar"));
assertNull(replyMessage.getHeaders().get("baz"));
}
@Test
public void withHeaderMapperAndExtractPayloadFalse() throws Exception {
SimpleWebServiceOutboundGateway gateway =
this.context.getBean("withHeaderMapper.handler", SimpleWebServiceOutboundGateway.class);
gateway.setExtractPayload(false);
String payload = "<root><name>bill</name></root>";
Message<?> replyMessage = process(payload, "withHeaderMapper", "inputChannel", true);
assertThat(replyMessage.getPayload(), instanceOf(WebServiceMessage.class));
assertEquals("bar", replyMessage.getHeaders().get("bar"));
assertNull(replyMessage.getHeaders().get("baz"));
}
@Test
public void withHeaderMapperStringPOX() throws Exception {
String payload = "<root><name>bill</name></root>";
Message<?> replyMessage = this.process(payload, "withHeaderMapper", "inputChannel", false);
Message<?> replyMessage = process(payload, "withHeaderMapper", "inputChannel", false);
assertTrue(replyMessage.getPayload() instanceof String);
assertTrue(((String) replyMessage.getPayload()).contains("<person><name>oleg</name></person>"));
}
@@ -134,7 +156,7 @@ public class WebServiceOutboundGatewayWithHeaderMapperTests {
DocumentBuilder docBuilder = dbfac.newDocumentBuilder();
Document document = docBuilder.parse(new ByteArrayInputStream("<root><name>bill</name></root>".getBytes()));
DOMSource payload = new DOMSource(document);
Message<?> replyMessage = this.process(payload, "withHeaderMapper", "inputChannel", true);
Message<?> replyMessage = process(payload, "withHeaderMapper", "inputChannel", true);
assertTrue(replyMessage.getPayload() instanceof DOMSource);
assertEquals("bar", replyMessage.getHeaders().get("bar"));
assertNull(replyMessage.getHeaders().get("baz"));
@@ -146,7 +168,7 @@ public class WebServiceOutboundGatewayWithHeaderMapperTests {
DocumentBuilder docBuilder = dbfac.newDocumentBuilder();
Document document = docBuilder.parse(new ByteArrayInputStream("<root><name>bill</name></root>".getBytes()));
DOMSource payload = new DOMSource(document);
Message<?> replyMessage = this.process(payload, "withHeaderMapper", "inputChannel", false);
Message<?> replyMessage = process(payload, "withHeaderMapper", "inputChannel", false);
assertTrue(replyMessage.getPayload() instanceof DOMSource);
assertTrue(this.extractStringResult(replyMessage).contains("<person><name>oleg</name></person>"));
}
@@ -156,7 +178,7 @@ public class WebServiceOutboundGatewayWithHeaderMapperTests {
DocumentBuilderFactory dbfac = DocumentBuilderFactory.newInstance();
DocumentBuilder docBuilder = dbfac.newDocumentBuilder();
Document payload = docBuilder.parse(new ByteArrayInputStream("<root><name>bill</name></root>".getBytes()));
Message<?> replyMessage = this.process(payload, "withHeaderMapper", "inputChannel", true);
Message<?> replyMessage = process(payload, "withHeaderMapper", "inputChannel", true);
assertTrue(replyMessage.getPayload() instanceof Document);
assertEquals("bar", replyMessage.getHeaders().get("bar"));
assertNull(replyMessage.getHeaders().get("baz"));
@@ -167,7 +189,7 @@ public class WebServiceOutboundGatewayWithHeaderMapperTests {
DocumentBuilderFactory dbfac = DocumentBuilderFactory.newInstance();
DocumentBuilder docBuilder = dbfac.newDocumentBuilder();
Document payload = docBuilder.parse(new ByteArrayInputStream("<root><name>bill</name></root>".getBytes()));
Message<?> replyMessage = this.process(payload, "withHeaderMapper", "inputChannel", false);
Message<?> replyMessage = process(payload, "withHeaderMapper", "inputChannel", false);
assertTrue(replyMessage.getPayload() instanceof Document);
assertTrue(this.extractStringResult(replyMessage).contains("<person><name>oleg</name></person>"));
}
@@ -176,16 +198,14 @@ public class WebServiceOutboundGatewayWithHeaderMapperTests {
public void withHeaderMapperAndMarshaller() throws Exception {
Person person = new Person();
person.setName("Bill Clinton");
Message<?> replyMessage = this.process(person, "marshallingWithHeaderMapper", "inputMarshallingChannel", true);
Message<?> replyMessage = process(person, "marshallingWithHeaderMapper", "inputMarshallingChannel", true);
assertEquals("bar", replyMessage.getHeaders().get("bar"));
assertNull(replyMessage.getHeaders().get("baz"));
}
@SuppressWarnings({ "resource" })
public Message<?> process(Object payload, String gatewayName, String channelName, final boolean soap) throws Exception {
ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext(
"ws-outbound-gateway-with-headermappers.xml", this.getClass());
AbstractWebServiceOutboundGateway gateway = TestUtils.getPropertyValue(context.getBean(gatewayName), "handler",
private Message<?> process(Object payload, String gatewayName, String channelName, final boolean soap) throws Exception {
AbstractWebServiceOutboundGateway gateway =
TestUtils.getPropertyValue(this.context.getBean(gatewayName), "handler",
AbstractWebServiceOutboundGateway.class);
if (!soap) {
@@ -199,6 +219,7 @@ public class WebServiceOutboundGatewayWithHeaderMapperTests {
Mockito.when(messageSender.supports(Mockito.any(URI.class))).thenReturn(true);
Mockito.doAnswer(invocation -> {
Object[] args = invocation.getArguments();
WebServiceMessage wsMessage = (WebServiceMessage) args[0];
// try { // uncomment if you want to see a pretty-print of SOAP message
@@ -217,7 +238,9 @@ public class WebServiceOutboundGatewayWithHeaderMapperTests {
assertNull(soapHeader.getAttributeValue(QNameUtils.parseQNameString("bar")));
}
return null;
}).when(wsConnection).send(Mockito.any(WebServiceMessage.class));
}).when(wsConnection)
.send(Mockito.any(WebServiceMessage.class));
Mockito.doAnswer(invocation -> {
Object[] args = invocation.getArguments();
@@ -252,9 +275,23 @@ public class WebServiceOutboundGatewayWithHeaderMapperTests {
setHeader(WebServiceHeaders.SOAP_ACTION, "someAction").build();
inputChannel.send(message);
QueueChannel outputChannel = context.getBean("outputChannel", QueueChannel.class);
Message<?> replyMessage = outputChannel.receive(0);
context.close();
return replyMessage;
return outputChannel.receive(0);
}
private String extractStringResult(Message<?> replyMessage) throws Exception {
Transformer transformer = TransformerFactory.newInstance().newTransformer();
StringResult result = new StringResult();
Object payload = replyMessage.getPayload();
if (payload instanceof DOMSource) {
transformer.transform(((DOMSource) replyMessage.getPayload()), result);
}
else if (payload instanceof Document) {
transformer.transform(new DOMSource((Document) replyMessage.getPayload()), result);
}
else {
throw new IllegalArgumentException("Unsupported payload type: " + payload.getClass().getName());
}
return result.toString();
}
public static class Person {
@@ -268,6 +305,7 @@ public class WebServiceOutboundGatewayWithHeaderMapperTests {
public void setName(String name) {
this.name = name;
}
}
public static class SampleUnmarshaller implements Unmarshaller {
@@ -286,21 +324,7 @@ public class WebServiceOutboundGatewayWithHeaderMapperTests {
person.setName(name);
return person;
}
}
private String extractStringResult(Message<?> replyMessage) throws Exception {
Transformer transformer = TransformerFactory.newInstance().newTransformer();
StringResult result = new StringResult();
Object payload = replyMessage.getPayload();
if (payload instanceof DOMSource) {
transformer.transform(((DOMSource) replyMessage.getPayload()), result);
}
else if (payload instanceof Document) {
transformer.transform(new DOMSource((Document) replyMessage.getPayload()), result);
}
else {
throw new IllegalArgumentException("Unsupported payload type: " + payload.getClass().getName());
}
return result.toString();
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2014 the original author or authors.
* Copyright 2002-2017 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.
@@ -32,6 +32,7 @@ import org.springframework.integration.xml.source.SourceFactory;
import org.springframework.messaging.MessagingException;
import org.springframework.oxm.Unmarshaller;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.xml.transform.StringSource;
/**
@@ -39,14 +40,18 @@ import org.springframework.xml.transform.StringSource;
* {@link Unmarshaller}. Expects the payload to be of type {@link Document},
* {@link String}, {@link File}, {@link Source} or to have an instance of
* {@link SourceFactory} that can convert to a {@link Source}. If
* alwaysUseSourceFactory is set to true, then the {@link SourceFactory}
* {@link #alwaysUseSourceFactory} is set to true, then the {@link SourceFactory}
* will be used to create the {@link Source} regardless of payload type.
* <p>
* The {@link #alwaysUseSourceFactory} is ignored if payload is
* {@link org.springframework.ws.mime.MimeMessage}.
* <p>
* The Unmarshaller may return a Message, but if the return value is not
* already a Message instance, a new Message will be created with that
* return value as its payload.
*
* @author Jonas Partner
* @author Artem Bilan
*/
public class UnmarshallingTransformer extends AbstractPayloadTransformer<Object, Object> {
@@ -56,15 +61,17 @@ public class UnmarshallingTransformer extends AbstractPayloadTransformer<Object,
private volatile boolean alwaysUseSourceFactory = false;
private MimeMessageUnmarshallerHelper mimeMessageUnmarshallerHelper;
public UnmarshallingTransformer(Unmarshaller unmarshaller) {
this.unmarshaller = unmarshaller;
if (ClassUtils.isPresent("org.springframework.ws.mime.MimeMessage", ClassUtils.getDefaultClassLoader())) {
this.mimeMessageUnmarshallerHelper = new MimeMessageUnmarshallerHelper(unmarshaller);
}
}
/**
* Provide the SourceFactory to be used. Must not be null.
*
* @param sourceFactory The source factory.
*/
public void setSourceFactory(SourceFactory sourceFactory) {
@@ -74,7 +81,6 @@ public class UnmarshallingTransformer extends AbstractPayloadTransformer<Object,
/**
* If true always delegate to the {@link SourceFactory}.
*
* @param alwaysUseSourceFactory true to always use the source factory.
*/
public void setAlwaysUseSourceFactory(boolean alwaysUseSourceFactory) {
@@ -89,29 +95,38 @@ public class UnmarshallingTransformer extends AbstractPayloadTransformer<Object,
@Override
public Object transformPayload(Object payload) {
Source source = null;
if (this.alwaysUseSourceFactory) {
source = this.sourceFactory.createSource(payload);
}
else if (payload instanceof String) {
source = new StringSource((String) payload);
}
else if (payload instanceof File) {
source = new StreamSource((File) payload);
}
else if (payload instanceof Document) {
source = new DOMSource((Document) payload);
}
else if (payload instanceof Source) {
source = (Source) payload;
}
else {
source = this.sourceFactory.createSource(payload);
}
if (source == null) {
throw new MessagingException(
"failed to transform message, payload not assignable from javax.xml.transform.Source and no conversion possible");
}
try {
if (this.mimeMessageUnmarshallerHelper != null) {
Object result = this.mimeMessageUnmarshallerHelper.maybeUnmarshalMimeMessage(payload);
if (result != null) {
return result;
}
}
if (this.alwaysUseSourceFactory) {
source = this.sourceFactory.createSource(payload);
}
else if (payload instanceof String) {
source = new StringSource((String) payload);
}
else if (payload instanceof File) {
source = new StreamSource((File) payload);
}
else if (payload instanceof Document) {
source = new DOMSource((Document) payload);
}
else if (payload instanceof Source) {
source = (Source) payload;
}
else {
source = this.sourceFactory.createSource(payload);
}
if (source == null) {
throw new MessagingException(
"failed to transform message, payload not assignable from " + Source.class.getName()
+ "and no conversion possible");
}
return this.unmarshaller.unmarshal(source);
}
catch (IOException e) {
@@ -119,4 +134,24 @@ public class UnmarshallingTransformer extends AbstractPayloadTransformer<Object,
}
}
private static class MimeMessageUnmarshallerHelper {
private Unmarshaller delegate;
MimeMessageUnmarshallerHelper(Unmarshaller unmarshaller) {
this.delegate = unmarshaller;
}
public Object maybeUnmarshalMimeMessage(Object payload) throws IOException {
if (payload instanceof org.springframework.ws.mime.MimeMessage) {
return org.springframework.ws.support.MarshallingUtils.unmarshal(this.delegate,
(org.springframework.ws.mime.MimeMessage) payload);
}
else {
return null;
}
}
}
}

View File

@@ -148,4 +148,6 @@ See <<stomp>> for more information.
- The `DefaultSoapHeaderMapper` can now map a `javax.xml.transform.Source` user-defined header to a SOAP header element.
- Simple WebService Inbound and Outbound gateways can now deal with the complete `WebServiceMessage` as a `payload`, allowing the manipulation of MTOM attachments.
See <<ws>> for more information.

View File

@@ -271,3 +271,25 @@ And in the end we have SOAP envelope as:
</soapenv:Body>
</soapenv:Envelope>
----
[[mtom-support]]
=== MTOM Support
The Marshalling Inbound and Outbound WebService Gateways support attachments directly via built-in functionality of the marshaller, e.g. `Jaxb2Marshaller` provides the `mtomEnabled` option.
Starting with _version 5.0_, the Simple WebService Gateways can operate with inbound and outbound `MimeMessage` s directly, which have an API to manipulate attachments.
When you need to send WebService message with attachments (either a reply from a server, or a client request) you should use the `WebServiceMessageFactory` directly and send a `WebServiceMessage` with attachments as a `payload` to the request or reply channel of the gateway:
[source, java]
----
WebServiceMessageFactory messageFactory = new SaajSoapMessageFactory(MessageFactory.newInstance());
MimeMessage webServiceMessage = (MimeMessage) messageFactory.createWebServiceMessage();
String request = "<test>foo</test>";
TransformerFactory transformerFactory = TransformerFactory.newInstance();
Transformer transformer = transformerFactory.newTransformer();
transformer.transform(new StringSource(request), webServiceMessage.getPayloadResult());
webServiceMessage.addAttachment("myAttachment", new ByteArrayResource("my_data".getBytes()), "plain/text");
this.webServiceChannel.send(new GenericMessage<>(webServiceMessage));
----

View File

@@ -275,6 +275,10 @@ Custom conversion to a `Source` is also supported by injecting an implementation
NOTE: If a `SourceFactory` is not set explicitly, the property on the `UnmarshallingTransformer` will by default be set to a http://docs.spring.io/spring-integration/api/org/springframework/integration/xml/source/DomSourceFactory.html[DomSourceFactory].
Starting with _version 5.0_, the `UnmarshallingTransformer` also supports an `org.springframework.ws.mime.MimeMessage` as the incoming payload.
This can be useful in scenarios when we receive a raw `WebServiceMessage` via SOAP with MTOM attachments.
See <<mtom-support>> for more information.
[source,xml]
----
<bean id="unmarshallingTransformer" class="o.s.i.xml.transformer.UnmarshallingTransformer">