From ca01cb4df61c1b41afe0ae5ad178181c4cabdf20 Mon Sep 17 00:00:00 2001 From: Arjen Poutsma Date: Tue, 3 Mar 2009 11:34:41 +0000 Subject: [PATCH] SPR-5539: Add XML HttpMessageConverters --- org.springframework.web/ivy.xml | 4 + .../xml/AbstractXmlHttpMessageConverter.java | 107 +++++++++++++++++ .../xml/MarshallingHttpMessageConverter.java | 108 ++++++++++++++++++ .../xml/SourceHttpMessageConverter.java | 58 ++++++++++ .../http/converter/xml/package.html | 7 ++ .../MarshallingHttpMessageConverterTest.java | 60 ++++++++++ .../xml/SourceHttpMessageConverterTest.java | 89 +++++++++++++++ org.springframework.web/web.iml | 12 ++ 8 files changed, 445 insertions(+) create mode 100644 org.springframework.web/src/main/java/org/springframework/http/converter/xml/AbstractXmlHttpMessageConverter.java create mode 100644 org.springframework.web/src/main/java/org/springframework/http/converter/xml/MarshallingHttpMessageConverter.java create mode 100644 org.springframework.web/src/main/java/org/springframework/http/converter/xml/SourceHttpMessageConverter.java create mode 100644 org.springframework.web/src/main/java/org/springframework/http/converter/xml/package.html create mode 100644 org.springframework.web/src/test/java/org/springframework/http/converter/xml/MarshallingHttpMessageConverterTest.java create mode 100644 org.springframework.web/src/test/java/org/springframework/http/converter/xml/SourceHttpMessageConverterTest.java diff --git a/org.springframework.web/ivy.xml b/org.springframework.web/ivy.xml index a7f0eb6578..c46f7fb88e 100644 --- a/org.springframework.web/ivy.xml +++ b/org.springframework.web/ivy.xml @@ -59,6 +59,8 @@ conf="compile->compile"/> + @@ -67,6 +69,8 @@ + diff --git a/org.springframework.web/src/main/java/org/springframework/http/converter/xml/AbstractXmlHttpMessageConverter.java b/org.springframework.web/src/main/java/org/springframework/http/converter/xml/AbstractXmlHttpMessageConverter.java new file mode 100644 index 0000000000..bb8af50284 --- /dev/null +++ b/org.springframework.web/src/main/java/org/springframework/http/converter/xml/AbstractXmlHttpMessageConverter.java @@ -0,0 +1,107 @@ +/* + * Copyright 2008 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.http.converter.xml; + +import java.io.IOException; +import javax.xml.transform.Result; +import javax.xml.transform.Source; +import javax.xml.transform.Transformer; +import javax.xml.transform.TransformerException; +import javax.xml.transform.TransformerFactory; +import javax.xml.transform.stream.StreamResult; +import javax.xml.transform.stream.StreamSource; + +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpInputMessage; +import org.springframework.http.HttpOutputMessage; +import org.springframework.http.MediaType; +import org.springframework.http.converter.AbstractHttpMessageConverter; +import org.springframework.http.converter.HttpMessageConversionException; + +/** + * Abstract base class for {@link org.springframework.http.converter.HttpMessageConverter HttpMessageConverters} that + * convert from/to XML. + * + *

By default, subclasses of this converter support {@code text/xml} and {@code application/xml}. This can be + * overridden by setting the {@link #setSupportedMediaTypes(java.util.List) supportedMediaTypes} property. + * + * @author Arjen Poutsma + * @since 3.0 + */ +public abstract class AbstractXmlHttpMessageConverter extends AbstractHttpMessageConverter { + + private final TransformerFactory transformerFactory = TransformerFactory.newInstance(); + + /** + * Protected constructor that sets the {@link #setSupportedMediaTypes(java.util.List) supportedMediaTypes} to {@code + * text/xml} and {@code application/xml}. + */ + protected AbstractXmlHttpMessageConverter() { + super(new MediaType("application", "xml"), new MediaType("text", "xml")); + } + + /** Invokes {@link #readFromSource(Class, HttpHeaders, Source)}. */ + public final T read(Class clazz, HttpInputMessage inputMessage) throws IOException { + return readFromSource(clazz, inputMessage.getHeaders(), new StreamSource(inputMessage.getBody())); + } + + /** + * Abstract template method called from {@link #read(Class, HttpInputMessage)}. + * + * @param clazz the type of object to return + * @param headers the HTTP input headers + * @param source the HTTP input body + * @return the converted object + * @throws IOException in case of I/O errors + * @throws org.springframework.http.converter.HttpMessageConversionException in case of conversion errors + */ + protected abstract T readFromSource(Class clazz, HttpHeaders headers, Source source) throws IOException; + + @Override + protected final void writeToInternal(T t, HttpOutputMessage outputMessage) throws IOException { + writeToResult(t, outputMessage.getHeaders(), new StreamResult(outputMessage.getBody())); + } + + /** + * Abstract template method called from {@link #writeToInternal(Object, HttpOutputMessage)}. + * + * @param t the object to write to the output message + * @param headers the HTTP output headers + * @param result the HTTP output body + * @throws IOException in case of I/O errors + * @throws HttpMessageConversionException in case of conversion errors + */ + protected abstract void writeToResult(T t, HttpHeaders headers, Result result) throws IOException; + + /** + * Transforms the given {@code Source} to the {@code Result}. + * + * @param source the source to transform from + * @param result the result to transform to + * @throws HttpMessageConversionException in case of transformation errors + */ + protected void transform(Source source, Result result) { + try { + Transformer transformer = transformerFactory.newTransformer(); + transformer.transform(source, result); + } + catch (TransformerException ex) { + throw new HttpMessageConversionException("Could not transform XML", ex); + } + } + +} diff --git a/org.springframework.web/src/main/java/org/springframework/http/converter/xml/MarshallingHttpMessageConverter.java b/org.springframework.web/src/main/java/org/springframework/http/converter/xml/MarshallingHttpMessageConverter.java new file mode 100644 index 0000000000..15231b57da --- /dev/null +++ b/org.springframework.web/src/main/java/org/springframework/http/converter/xml/MarshallingHttpMessageConverter.java @@ -0,0 +1,108 @@ +package org.springframework.http.converter.xml; + +import java.io.IOException; +import javax.xml.transform.Result; +import javax.xml.transform.Source; + +import org.springframework.beans.factory.InitializingBean; +import org.springframework.http.HttpHeaders; +import org.springframework.oxm.Marshaller; +import org.springframework.oxm.Unmarshaller; +import org.springframework.util.Assert; + +/** + * Implementation of {@link org.springframework.http.converter.HttpMessageConverter HttpMessageConverter} 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 #MarshallingHttpMessageConverter(Marshaller) constructor} or {@linkplain + * #setMarshaller(Marshaller) bean properties}. + * + *

By default, this converter supports {@code text/xml} and {@code application/xml}. This can be overridden by + * setting the {@link #setSupportedMediaTypes(java.util.List) supportedMediaTypes} property. + * + * @author Arjen Poutsma + * @since 3.0 + */ + +public class MarshallingHttpMessageConverter extends AbstractXmlHttpMessageConverter + implements InitializingBean { + + private Marshaller marshaller; + + private Unmarshaller unmarshaller; + + /** + * Construct a new {@code MarshallingHttpMessageConverter} 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 MarshallingHttpMessageConverter() { + } + + /** + * 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 + * @throws IllegalArgumentException when marshaller does not implement the {@link Unmarshaller} interface + * as well + */ + public MarshallingHttpMessageConverter(Marshaller marshaller) { + Assert.notNull(marshaller, "marshaller must not be null"); + if (!(marshaller instanceof Unmarshaller)) { + throw new IllegalArgumentException("Marshaller [" + marshaller + "] does not implement the Unmarshaller " + + "interface. Please set an Unmarshaller explicitely by using the " + + "MarshallingHttpMessageConverter(Marshaller, Unmarshaller) constructor."); + } + else { + this.marshaller = marshaller; + this.unmarshaller = (Unmarshaller) marshaller; + } + } + + /** + * Construct a new MarshallingMessageConverter with the given {@code Marshaller} and {@code + * Unmarshaller}. + * + * @param marshaller the Marshaller to use + * @param unmarshaller the Unmarshaller to use + */ + public MarshallingHttpMessageConverter(Marshaller marshaller, Unmarshaller unmarshaller) { + 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; + } + + public void afterPropertiesSet() { + Assert.notNull(this.marshaller, "Property 'marshaller' is required"); + Assert.notNull(this.unmarshaller, "Property 'unmarshaller' is required"); + } + + public boolean supports(Class clazz) { + return unmarshaller.supports(clazz); + } + + @Override + protected Object readFromSource(Class clazz, HttpHeaders headers, Source source) throws IOException { + return unmarshaller.unmarshal(source); + } + + @Override + protected void writeToResult(Object o, HttpHeaders headers, Result result) throws IOException { + marshaller.marshal(o, result); + } +} diff --git a/org.springframework.web/src/main/java/org/springframework/http/converter/xml/SourceHttpMessageConverter.java b/org.springframework.web/src/main/java/org/springframework/http/converter/xml/SourceHttpMessageConverter.java new file mode 100644 index 0000000000..3546978b41 --- /dev/null +++ b/org.springframework.web/src/main/java/org/springframework/http/converter/xml/SourceHttpMessageConverter.java @@ -0,0 +1,58 @@ +package org.springframework.http.converter.xml; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import javax.xml.transform.Result; +import javax.xml.transform.Source; +import javax.xml.transform.dom.DOMResult; +import javax.xml.transform.dom.DOMSource; +import javax.xml.transform.sax.SAXSource; +import javax.xml.transform.stream.StreamResult; +import javax.xml.transform.stream.StreamSource; + +import org.xml.sax.InputSource; + +import org.springframework.http.HttpHeaders; +import org.springframework.http.converter.HttpMessageConversionException; + +/** @author Arjen Poutsma */ +public class SourceHttpMessageConverter extends AbstractXmlHttpMessageConverter { + + public boolean supports(Class clazz) { + return Source.class.isAssignableFrom(clazz); + } + + @Override + @SuppressWarnings("unchecked") + protected T readFromSource(Class clazz, HttpHeaders headers, Source source) throws IOException { + if (DOMSource.class.equals(clazz)) { + DOMResult domResult = new DOMResult(); + transform(source, domResult); + return (T) new DOMSource(domResult.getNode()); + } + else if (SAXSource.class.equals(clazz)) { + ByteArrayInputStream bis = transformToByteArray(source); + return (T) new SAXSource(new InputSource(bis)); + } + else if (StreamSource.class.equals(clazz) || Source.class.equals(clazz)) { + ByteArrayInputStream bis = transformToByteArray(source); + return (T) new StreamSource(bis); + } + else { + throw new HttpMessageConversionException( + "Could not read class [" + clazz + "]. Only DOMSource, SAXSource, and StreamSource are supported."); + } + } + + private ByteArrayInputStream transformToByteArray(Source source) { + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + transform(source, new StreamResult(bos)); + return new ByteArrayInputStream(bos.toByteArray()); + } + + @Override + protected void writeToResult(T t, HttpHeaders headers, Result result) throws IOException { + transform(t, result); + } +} diff --git a/org.springframework.web/src/main/java/org/springframework/http/converter/xml/package.html b/org.springframework.web/src/main/java/org/springframework/http/converter/xml/package.html new file mode 100644 index 0000000000..66b805df13 --- /dev/null +++ b/org.springframework.web/src/main/java/org/springframework/http/converter/xml/package.html @@ -0,0 +1,7 @@ + + + +Provides a HttpMessageConverter implementations for handling XML. + + + diff --git a/org.springframework.web/src/test/java/org/springframework/http/converter/xml/MarshallingHttpMessageConverterTest.java b/org.springframework.web/src/test/java/org/springframework/http/converter/xml/MarshallingHttpMessageConverterTest.java new file mode 100644 index 0000000000..c03e582d1d --- /dev/null +++ b/org.springframework.web/src/test/java/org/springframework/http/converter/xml/MarshallingHttpMessageConverterTest.java @@ -0,0 +1,60 @@ +package org.springframework.http.converter.xml; + +import javax.xml.transform.stream.StreamResult; +import javax.xml.transform.stream.StreamSource; + +import static org.easymock.EasyMock.*; +import static org.junit.Assert.*; +import org.junit.Before; +import org.junit.Test; + +import org.springframework.http.MediaType; +import org.springframework.http.MockHttpInputMessage; +import org.springframework.http.MockHttpOutputMessage; +import org.springframework.oxm.Marshaller; +import org.springframework.oxm.Unmarshaller; + +/** @author Arjen Poutsma */ +public class MarshallingHttpMessageConverterTest { + + private MarshallingHttpMessageConverter converter; + + private Marshaller marshaller; + + private Unmarshaller unmarshaller; + + @Before + public void setUp() { + marshaller = createMock(Marshaller.class); + unmarshaller = createMock(Unmarshaller.class); + + converter = new MarshallingHttpMessageConverter(marshaller, unmarshaller); + } + + @Test + public void read() throws Exception { + String body = "Hello World"; + MockHttpInputMessage inputMessage = new MockHttpInputMessage(body.getBytes("UTF-8")); + + expect(unmarshaller.unmarshal(isA(StreamSource.class))).andReturn(body); + + replay(marshaller, unmarshaller); + String result = (String) converter.read(Object.class, inputMessage); + assertEquals("Invalid result", body, result); + verify(marshaller, unmarshaller); + } + + @Test + public void write() throws Exception { + String body = "Hello World"; + MockHttpOutputMessage outputMessage = new MockHttpOutputMessage(); + + marshaller.marshal(eq(body), isA(StreamResult.class)); + + replay(marshaller, unmarshaller); + converter.write(body, outputMessage); + assertEquals("Invalid content-type", new MediaType("application", "xml"), + outputMessage.getHeaders().getContentType()); + verify(marshaller, unmarshaller); + } +} diff --git a/org.springframework.web/src/test/java/org/springframework/http/converter/xml/SourceHttpMessageConverterTest.java b/org.springframework.web/src/test/java/org/springframework/http/converter/xml/SourceHttpMessageConverterTest.java new file mode 100644 index 0000000000..a6ddf2f3e3 --- /dev/null +++ b/org.springframework.web/src/test/java/org/springframework/http/converter/xml/SourceHttpMessageConverterTest.java @@ -0,0 +1,89 @@ +package org.springframework.http.converter.xml; + +import java.io.InputStreamReader; +import java.nio.charset.Charset; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.transform.Source; +import javax.xml.transform.dom.DOMSource; +import javax.xml.transform.sax.SAXSource; +import javax.xml.transform.stream.StreamSource; + +import static org.custommonkey.xmlunit.XMLAssert.*; +import static org.junit.Assert.assertEquals; +import org.junit.Test; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.xml.sax.InputSource; + +import org.springframework.http.MediaType; +import org.springframework.http.MockHttpInputMessage; +import org.springframework.http.MockHttpOutputMessage; +import org.springframework.util.FileCopyUtils; + +/** @author Arjen Poutsma */ +public class SourceHttpMessageConverterTest { + + @Test + public void readDOMSource() throws Exception { + SourceHttpMessageConverter converter = new SourceHttpMessageConverter(); + String body = "Hello World"; + MockHttpInputMessage inputMessage = new MockHttpInputMessage(body.getBytes("UTF-8")); + inputMessage.getHeaders().setContentType(new MediaType("application", "xml")); + DOMSource result = converter.read(DOMSource.class, inputMessage); + Document document = (Document) result.getNode(); + assertEquals("Invalid result", "root", document.getDocumentElement().getLocalName()); + } + + @Test + public void readSAXSource() throws Exception { + SourceHttpMessageConverter converter = new SourceHttpMessageConverter(); + String body = "Hello World"; + MockHttpInputMessage inputMessage = new MockHttpInputMessage(body.getBytes("UTF-8")); + inputMessage.getHeaders().setContentType(new MediaType("application", "xml")); + SAXSource result = converter.read(SAXSource.class, inputMessage); + InputSource inputSource = result.getInputSource(); + String s = FileCopyUtils.copyToString(new InputStreamReader(inputSource.getByteStream())); + assertXMLEqual("Invalid result", body, s); + } + + @Test + public void readStreamSource() throws Exception { + SourceHttpMessageConverter converter = new SourceHttpMessageConverter(); + String body = "Hello World"; + MockHttpInputMessage inputMessage = new MockHttpInputMessage(body.getBytes("UTF-8")); + inputMessage.getHeaders().setContentType(new MediaType("application", "xml")); + StreamSource result = converter.read(StreamSource.class, inputMessage); + String s = FileCopyUtils.copyToString(new InputStreamReader(result.getInputStream())); + assertXMLEqual("Invalid result", body, s); + } + + @Test + public void readSource() throws Exception { + SourceHttpMessageConverter converter = new SourceHttpMessageConverter(); + String body = "Hello World"; + MockHttpInputMessage inputMessage = new MockHttpInputMessage(body.getBytes("UTF-8")); + inputMessage.getHeaders().setContentType(new MediaType("application", "xml")); + converter.read(Source.class, inputMessage); + } + + @Test + public void write() throws Exception { + DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance(); + documentBuilderFactory.setNamespaceAware(true); + Document document = documentBuilderFactory.newDocumentBuilder().newDocument(); + Element rootElement = document.createElement("root"); + document.appendChild(rootElement); + rootElement.setTextContent("Hello World"); + DOMSource domSource = new DOMSource(document); + + SourceHttpMessageConverter converter = new SourceHttpMessageConverter(); + MockHttpOutputMessage outputMessage = new MockHttpOutputMessage(); + converter.write(domSource, outputMessage); + assertXMLEqual("Invalid result", "Hello World", + outputMessage.getBodyAsString(Charset.forName("UTF-8"))); + assertEquals("Invalid content-type", new MediaType("application", "xml"), + outputMessage.getHeaders().getContentType()); + } + + +} diff --git a/org.springframework.web/web.iml b/org.springframework.web/web.iml index 6310bd0183..5b7e806452 100644 --- a/org.springframework.web/web.iml +++ b/org.springframework.web/web.iml @@ -15,6 +15,7 @@ + @@ -222,6 +223,17 @@ + + + + + + + + + + +