diff --git a/build.gradle b/build.gradle index dbbff2acbc129970ec793d18bd8070ab28cba4b4..7865e3c0683372b8cfce49258fc247b59ac654ef 100644 --- a/build.gradle +++ b/build.gradle @@ -486,6 +486,7 @@ project("spring-messaging") { compile(project(":spring-beans")) compile(project(":spring-core")) compile(project(":spring-context")) + optional(project(":spring-oxm")) optional("io.projectreactor:reactor-core:${reactorVersion}") optional("io.projectreactor:reactor-net:${reactorVersion}") { exclude group: "io.netty", module: "netty-all" @@ -516,6 +517,7 @@ project("spring-messaging") { testCompile("commons-dbcp:commons-dbcp:1.4") testCompile("log4j:log4j:1.2.17") testCompile("org.slf4j:slf4j-jcl:${slf4jVersion}") + testCompile("xmlunit:xmlunit:${xmlunitVersion}") } } diff --git a/spring-messaging/src/main/java/org/springframework/messaging/converter/MarshallingMessageConverter.java b/spring-messaging/src/main/java/org/springframework/messaging/converter/MarshallingMessageConverter.java new file mode 100644 index 0000000000000000000000000000000000000000..06a8b5c19bf52088f901bf8d757ea679ee3387d2 --- /dev/null +++ b/spring-messaging/src/main/java/org/springframework/messaging/converter/MarshallingMessageConverter.java @@ -0,0 +1,209 @@ +/* + * Copyright 2002-2015 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.messaging.converter; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.StringReader; +import java.io.StringWriter; +import java.io.Writer; +import java.util.Arrays; +import javax.xml.transform.Result; +import javax.xml.transform.Source; +import javax.xml.transform.stream.StreamResult; +import javax.xml.transform.stream.StreamSource; + +import org.springframework.beans.TypeMismatchException; +import org.springframework.messaging.Message; +import org.springframework.messaging.MessageHeaders; +import org.springframework.oxm.Marshaller; +import org.springframework.oxm.MarshallingFailureException; +import org.springframework.oxm.Unmarshaller; +import org.springframework.oxm.UnmarshallingFailureException; +import org.springframework.util.Assert; +import org.springframework.util.MimeType; + +/** + * Implementation of {@link MessageConverter} that can read and write XML using Spring's + * {@link Marshaller} and {@link Unmarshaller} abstractions. + * + *

This converter requires a {@code Marshaller} and {@code Unmarshaller} before it can + * be used. These can be injected by the {@linkplain MarshallingMessageConverter(Marshaller) + * constructor} or {@linkplain #setMarshaller(Marshaller) bean properties}. + * + * @author Arjen Poutsma + * @since 4.2 + */ +public class MarshallingMessageConverter extends AbstractMessageConverter { + + private Marshaller marshaller; + + private Unmarshaller unmarshaller; + + + /** + * Construct a {@code MarshallingMessageConverter} supporting one or more custom MIME + * types. + * @param supportedMimeTypes the supported MIME types + */ + public MarshallingMessageConverter(MimeType... supportedMimeTypes) { + super(Arrays.asList(supportedMimeTypes)); + } + + /** + * Construct a new {@code MarshallingMessageConverter} with no {@link Marshaller} or + * {@link Unmarshaller} set. The Marshaller and Unmarshaller must be set after + * construction by invoking {@link #setMarshaller(Marshaller)} and {@link + * #setUnmarshaller(Unmarshaller)} . + */ + public MarshallingMessageConverter() { + this(new MimeType("application", "xml"), new MimeType("text", "xml"), + new MimeType("application", "*+xml")); + } + + /** + * Construct a new {@code MarshallingMessageConverter} with the given {@link + * Marshaller} set. + * + *

If the given {@link Marshaller} also implements the {@link Unmarshaller} + * interface, it is used for both marshalling and unmarshalling. Otherwise, an + * exception is thrown. + * + *

Note that all {@code Marshaller} implementations in Spring also implement the + * {@code Unmarshaller} interface, so that you can safely use this constructor. + * @param marshaller object used as marshaller and unmarshaller + */ + public MarshallingMessageConverter(Marshaller marshaller) { + this(); + Assert.notNull(marshaller, "Marshaller must not be null"); + this.marshaller = marshaller; + if (marshaller instanceof Unmarshaller) { + this.unmarshaller = (Unmarshaller) marshaller; + } + } + + /** + * Construct a new {@code MarshallingMessageConverter} with the given {@code + * Marshaller} and {@code Unmarshaller}. + * @param marshaller the Marshaller to use + * @param unmarshaller the Unmarshaller to use + */ + public MarshallingMessageConverter(Marshaller marshaller, Unmarshaller unmarshaller) { + this(); + Assert.notNull(marshaller, "Marshaller must not be null"); + Assert.notNull(unmarshaller, "Unmarshaller must not be null"); + this.marshaller = marshaller; + this.unmarshaller = unmarshaller; + } + + + /** + * Set the {@link Marshaller} to be used by this message converter. + */ + public void setMarshaller(Marshaller marshaller) { + this.marshaller = marshaller; + } + + /** + * Set the {@link Unmarshaller} to be used by this message converter. + */ + public void setUnmarshaller(Unmarshaller unmarshaller) { + this.unmarshaller = unmarshaller; + } + + @Override + protected boolean canConvertFrom(Message message, Class targetClass) { + return supportsMimeType(message.getHeaders()) && (this.unmarshaller != null) && + this.unmarshaller.supports(targetClass); + } + + @Override + protected boolean canConvertTo(Object payload, MessageHeaders headers) { + return supportsMimeType(headers) && (this.marshaller != null) && + this.marshaller.supports(payload.getClass()); + } + + @Override + protected boolean supports(Class clazz) { + // should not be called, since we override canConvertFrom/canConvertTo instead + throw new UnsupportedOperationException(); + } + + @Override + public Object convertFromInternal(Message message, Class targetClass) { + Assert.notNull(this.unmarshaller, "Property 'unmarshaller' is required"); + try { + Source source = getSource(message.getPayload()); + + Object result = this.unmarshaller.unmarshal(source); + if (!targetClass.isInstance(result)) { + throw new TypeMismatchException(result, targetClass); + } + return result; + } + catch (UnmarshallingFailureException ex) { + throw new MessageConversionException(message, + "Could not unmarshal XML: " + ex.getMessage(), ex); + } + catch (IOException ex) { + throw new MessageConversionException(message, + "Could not unmarshal XML: " + ex.getMessage(), ex); + } + } + + private Source getSource(Object payload) { + if (payload instanceof byte[]) { + return new StreamSource(new ByteArrayInputStream((byte[]) payload)); + } + else { + return new StreamSource(new StringReader((String) payload)); + } + } + + @Override + public Object convertToInternal(Object payload, MessageHeaders headers) { + Assert.notNull(this.marshaller, "Property 'marshaller' is required"); + try { + if (byte[].class.equals(getSerializedPayloadClass())) { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + Result result = new StreamResult(out); + + this.marshaller.marshal(payload, result); + + payload = out.toByteArray(); + } + else { + Writer writer = new StringWriter(); + Result result = new StreamResult(writer); + + this.marshaller.marshal(payload, result); + + payload = writer.toString(); + } + } + catch (MarshallingFailureException ex) { + throw new MessageConversionException( + "Could not marshal XML: " + ex.getMessage(), ex); + } + catch (IOException ex) { + throw new MessageConversionException( + "Could not marshal XML: " + ex.getMessage(), ex); + } + return payload; + } +} diff --git a/spring-messaging/src/test/java/org/springframework/messaging/converter/MarshallingMessageConverterTests.java b/spring-messaging/src/test/java/org/springframework/messaging/converter/MarshallingMessageConverterTests.java new file mode 100644 index 0000000000000000000000000000000000000000..083def0a8f477789bbabbb2fd32e1640e0f8cc2d --- /dev/null +++ b/spring-messaging/src/test/java/org/springframework/messaging/converter/MarshallingMessageConverterTests.java @@ -0,0 +1,101 @@ +/* + * Copyright 2002-2015 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.messaging.converter; + +import java.io.IOException; +import java.nio.charset.Charset; +import javax.xml.bind.annotation.XmlRootElement; + +import org.junit.Before; +import org.junit.Test; + +import org.springframework.messaging.Message; +import org.springframework.messaging.support.MessageBuilder; +import org.springframework.oxm.jaxb.Jaxb2Marshaller; + +import static org.custommonkey.xmlunit.XMLAssert.assertXMLEqual; +import static org.junit.Assert.assertEquals; + +/** + * @author Arjen Poutsma + */ +public class MarshallingMessageConverterTests { + + private static Charset UTF_8 = Charset.forName("UTF-8"); + + + private MarshallingMessageConverter converter; + + + @Before + public void createMarshaller() throws Exception { + Jaxb2Marshaller marshaller = new Jaxb2Marshaller(); + marshaller.setClassesToBeBound(MyBean.class); + marshaller.afterPropertiesSet(); + + converter = new MarshallingMessageConverter(marshaller); + } + @Test + public void fromMessage() throws Exception { + String payload = "Foo"; + Message message = MessageBuilder.withPayload(payload.getBytes(UTF_8)).build(); + MyBean actual = (MyBean) converter.fromMessage(message, MyBean.class); + + assertEquals("Foo", actual.getName()); + } + + @Test(expected = MessageConversionException.class) + public void fromMessageInvalidXml() throws Exception { + String payload = "Foo"; + Message message = MessageBuilder.withPayload(payload.getBytes(UTF_8)).build(); + converter.fromMessage(message, MyBean.class); + } + + @Test(expected = MessageConversionException.class) + public void fromMessageValidXmlWithUnknownProperty() throws IOException { + String payload = "42"; + Message message = MessageBuilder.withPayload(payload.getBytes(UTF_8)).build(); + MyBean myBean = (MyBean)converter.fromMessage(message, MyBean.class); + } + + @Test + public void toMessage() throws Exception { + MyBean payload = new MyBean(); + payload.setName("Foo"); + + Message message = converter.toMessage(payload, null); + String actual = new String((byte[]) message.getPayload(), UTF_8); + + assertXMLEqual("Foo", actual); + } + + + @XmlRootElement + public static class MyBean { + + private String name; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + } + +} \ No newline at end of file