diff --git a/org.springframework.web/src/main/java/org/springframework/http/converter/json/MappingJacksonHttpMessageConverter.java b/org.springframework.web/src/main/java/org/springframework/http/converter/json/MappingJacksonHttpMessageConverter.java index b076cc390d8..7e1e78a22d9 100644 --- a/org.springframework.web/src/main/java/org/springframework/http/converter/json/MappingJacksonHttpMessageConverter.java +++ b/org.springframework.web/src/main/java/org/springframework/http/converter/json/MappingJacksonHttpMessageConverter.java @@ -20,14 +20,11 @@ import java.io.IOException; import java.nio.charset.Charset; import java.util.List; -import javax.xml.bind.Marshaller; -import javax.xml.bind.PropertyException; - import org.codehaus.jackson.JsonEncoding; import org.codehaus.jackson.JsonGenerator; -import org.codehaus.jackson.type.JavaType; import org.codehaus.jackson.map.ObjectMapper; import org.codehaus.jackson.map.type.TypeFactory; +import org.codehaus.jackson.type.JavaType; import org.springframework.http.HttpInputMessage; import org.springframework.http.HttpOutputMessage; diff --git a/org.springframework.web/src/main/java/org/springframework/http/converter/xml/AbstractJaxb2HttpMessageConverter.java b/org.springframework.web/src/main/java/org/springframework/http/converter/xml/AbstractJaxb2HttpMessageConverter.java new file mode 100644 index 00000000000..68f67694dba --- /dev/null +++ b/org.springframework.web/src/main/java/org/springframework/http/converter/xml/AbstractJaxb2HttpMessageConverter.java @@ -0,0 +1,99 @@ +/* + * Copyright 2002-2009 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.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import javax.xml.bind.JAXBContext; +import javax.xml.bind.JAXBException; +import javax.xml.bind.Marshaller; +import javax.xml.bind.Unmarshaller; + +import org.springframework.http.converter.HttpMessageConversionException; +import org.springframework.util.Assert; + +/** + * Abstract base class for {@link org.springframework.http.converter.HttpMessageConverter HttpMessageConverters} that + * use JAXB2. Creates {@link JAXBContext} object lazily. + * + * @author Arjen Poutsma + * @since 3.0 + */ +public abstract class AbstractJaxb2HttpMessageConverter extends AbstractXmlHttpMessageConverter { + + private final ConcurrentMap jaxbContexts = new ConcurrentHashMap(); + + /** + * Creates a new {@link Marshaller} for the given class. + * + * @param clazz the class to create the marshaller for + * @return the {@code Marshaller} + * @throws HttpMessageConversionException in case of JAXB errors + */ + protected final Marshaller createMarshaller(Class clazz) { + try { + JAXBContext jaxbContext = getJaxbContext(clazz); + return jaxbContext.createMarshaller(); + } + catch (JAXBException ex) { + throw new HttpMessageConversionException( + "Could not create Marshaller for class [" + clazz + "]: " + ex.getMessage(), ex); + } + } + + /** + * Creates a new {@link Unmarshaller} for the given class. + * + * @param clazz the class to create the unmarshaller for + * @return the {@code Unmarshaller} + * @throws HttpMessageConversionException in case of JAXB errors + */ + protected final Unmarshaller createUnmarshaller(Class clazz) throws JAXBException { + try { + JAXBContext jaxbContext = getJaxbContext(clazz); + return jaxbContext.createUnmarshaller(); + } + catch (JAXBException ex) { + throw new HttpMessageConversionException( + "Could not create Unmarshaller for class [" + clazz + "]: " + ex.getMessage(), ex); + } + } + + /** + * Returns a {@link JAXBContext} for the given class. + * + * @param clazz the class to return the context for + * @return the {@code JAXBContext} + * @throws HttpMessageConversionException in case of JAXB errors + */ + protected final JAXBContext getJaxbContext(Class clazz) { + Assert.notNull(clazz, "'clazz' must not be null"); + JAXBContext jaxbContext = jaxbContexts.get(clazz); + if (jaxbContext == null) { + try { + jaxbContext = JAXBContext.newInstance(clazz); + jaxbContexts.putIfAbsent(clazz, jaxbContext); + } + catch (JAXBException ex) { + throw new HttpMessageConversionException( + "Could not instantiate JAXBContext for class [" + clazz + "]: " + ex.getMessage(), ex); + } + } + return jaxbContext; + } + +} diff --git a/org.springframework.web/src/main/java/org/springframework/http/converter/xml/Jaxb2RootElementHttpMessageConverter.java b/org.springframework.web/src/main/java/org/springframework/http/converter/xml/Jaxb2RootElementHttpMessageConverter.java new file mode 100644 index 00000000000..10b3294be1f --- /dev/null +++ b/org.springframework.web/src/main/java/org/springframework/http/converter/xml/Jaxb2RootElementHttpMessageConverter.java @@ -0,0 +1,112 @@ +/* + * Copyright 2002-2009 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.bind.JAXBElement; +import javax.xml.bind.JAXBException; +import javax.xml.bind.MarshalException; +import javax.xml.bind.Marshaller; +import javax.xml.bind.PropertyException; +import javax.xml.bind.UnmarshalException; +import javax.xml.bind.Unmarshaller; +import javax.xml.bind.annotation.XmlRootElement; +import javax.xml.bind.annotation.XmlType; +import javax.xml.transform.Result; +import javax.xml.transform.Source; + +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.http.converter.HttpMessageConversionException; +import org.springframework.http.converter.HttpMessageNotReadableException; +import org.springframework.http.converter.HttpMessageNotWritableException; +import org.springframework.util.ClassUtils; + +/** + * Implementation of {@link org.springframework.http.converter.HttpMessageConverter HttpMessageConverter} that can read + * and write XML using JAXB2. + * + *

This converter can read classes annotated with {@link XmlRootElement} and {@link XmlType}, and write classes + * annotated with with {@link XmlRootElement}, or subclasses thereof. + * + * @author Arjen Poutsma + * @since 3.0 + */ +public class Jaxb2RootElementHttpMessageConverter extends AbstractJaxb2HttpMessageConverter { + + @Override + public boolean canRead(Class clazz, MediaType mediaType) { + return (clazz.isAnnotationPresent(XmlRootElement.class) || clazz.isAnnotationPresent(XmlType.class)) && + isSupported(mediaType); + } + + @Override + public boolean canWrite(Class clazz, MediaType mediaType) { + return AnnotationUtils.findAnnotation(clazz, XmlRootElement.class) != null && isSupported(mediaType); + } + + @Override + protected boolean supports(Class clazz) { + // should not be called, since we override canRead/Write + throw new UnsupportedOperationException(); + } + + @Override + protected Object readFromSource(Class clazz, HttpHeaders headers, Source source) throws IOException { + try { + Unmarshaller unmarshaller = createUnmarshaller(clazz); + if (clazz.isAnnotationPresent(XmlRootElement.class)) { + return unmarshaller.unmarshal(source); + } + else { + JAXBElement jaxbElement = unmarshaller.unmarshal(source, clazz); + return jaxbElement.getValue(); + } + } + catch (UnmarshalException ex) { + throw new HttpMessageNotReadableException("Could not unmarshal to [" + clazz + "]: " + ex.getMessage(), ex); + + } + catch (JAXBException ex) { + throw new HttpMessageConversionException("Could not instantiate JAXBContext: " + ex.getMessage(), ex); + } + } + + @Override + protected void writeToResult(Object o, HttpHeaders headers, Result result) throws IOException { + try { + Class clazz = ClassUtils.getUserClass(o); + Marshaller marshaller = createMarshaller(clazz); + setCharset(headers.getContentType(), marshaller); + marshaller.marshal(o, result); + } + catch (MarshalException ex) { + throw new HttpMessageNotWritableException("Could not marshal [" + o + "]: " + ex.getMessage(), ex); + } + catch (JAXBException ex) { + throw new HttpMessageConversionException("Could not instantiate JAXBContext: " + ex.getMessage(), ex); + } + } + + private void setCharset(MediaType contentType, Marshaller marshaller) throws PropertyException { + if (contentType != null && contentType.getCharSet() != null) { + marshaller.setProperty(Marshaller.JAXB_ENCODING, contentType.getCharSet().name()); + } + } + +} diff --git a/org.springframework.web/src/main/java/org/springframework/web/client/RestTemplate.java b/org.springframework.web/src/main/java/org/springframework/web/client/RestTemplate.java index d5e8779117b..da82f49c8b9 100644 --- a/org.springframework.web/src/main/java/org/springframework/web/client/RestTemplate.java +++ b/org.springframework.web/src/main/java/org/springframework/web/client/RestTemplate.java @@ -35,8 +35,11 @@ import org.springframework.http.converter.ByteArrayHttpMessageConverter; import org.springframework.http.converter.FormHttpMessageConverter; import org.springframework.http.converter.HttpMessageConverter; import org.springframework.http.converter.StringHttpMessageConverter; +import org.springframework.http.converter.json.MappingJacksonHttpMessageConverter; import org.springframework.http.converter.xml.SourceHttpMessageConverter; +import org.springframework.http.converter.xml.Jaxb2RootElementHttpMessageConverter; import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; import org.springframework.web.util.UriTemplate; /** @@ -98,6 +101,13 @@ import org.springframework.web.util.UriTemplate; */ public class RestTemplate extends HttpAccessor implements RestOperations { + private static final boolean jaxb2Present = + ClassUtils.isPresent("javax.xml.bind.Binder", RestTemplate.class.getClassLoader()); + + private static final boolean jacksonPresent = + ClassUtils.isPresent("org.codehaus.jackson.map.ObjectMapper", RestTemplate.class.getClassLoader()) && + ClassUtils.isPresent("org.codehaus.jackson.JsonGenerator", RestTemplate.class.getClassLoader()); + private final ResponseExtractor headersExtractor = new HeadersExtractor(); private List> messageConverters = new ArrayList>(); @@ -110,6 +120,12 @@ public class RestTemplate extends HttpAccessor implements RestOperations { this.messageConverters.add(new StringHttpMessageConverter()); this.messageConverters.add(new FormHttpMessageConverter()); this.messageConverters.add(new SourceHttpMessageConverter()); + if (jaxb2Present) { + this.messageConverters.add(new Jaxb2RootElementHttpMessageConverter()); + } + if (jacksonPresent) { + this.messageConverters.add(new MappingJacksonHttpMessageConverter()); + } } /** diff --git a/org.springframework.web/src/test/java/org/springframework/http/converter/xml/Jaxb2RootElementHttpMessageConverterTest.java b/org.springframework.web/src/test/java/org/springframework/http/converter/xml/Jaxb2RootElementHttpMessageConverterTest.java new file mode 100644 index 00000000000..98368938923 --- /dev/null +++ b/org.springframework.web/src/test/java/org/springframework/http/converter/xml/Jaxb2RootElementHttpMessageConverterTest.java @@ -0,0 +1,144 @@ +/* + * Copyright 2002-2009 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.nio.charset.Charset; +import javax.xml.bind.annotation.XmlAttribute; +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlRootElement; +import javax.xml.bind.annotation.XmlType; + +import static org.custommonkey.xmlunit.XMLAssert.*; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import org.junit.Before; +import org.junit.Test; + +import org.springframework.aop.framework.AdvisedSupport; +import org.springframework.aop.framework.AopProxy; +import org.springframework.aop.framework.DefaultAopProxyFactory; +import org.springframework.http.MediaType; +import org.springframework.http.MockHttpInputMessage; +import org.springframework.http.MockHttpOutputMessage; + +/** @author Arjen Poutsma */ +public class Jaxb2RootElementHttpMessageConverterTest { + + private Jaxb2RootElementHttpMessageConverter converter; + + private RootElement rootElement; + + private Type type; + + private RootElement rootElementCglib; + + @Before + public void setUp() { + converter = new Jaxb2RootElementHttpMessageConverter(); + rootElement = new RootElement(); + type = new Type(); + DefaultAopProxyFactory proxyFactory = new DefaultAopProxyFactory(); + AdvisedSupport advisedSupport = new AdvisedSupport(); + advisedSupport.setTarget(rootElement); + advisedSupport.setProxyTargetClass(true); + AopProxy proxy = proxyFactory.createAopProxy(advisedSupport); + rootElementCglib = (RootElement) proxy.getProxy(); + } + + @Test + public void canRead() throws Exception { + assertTrue("Converter does not support reading @XmlRootElement", converter.canRead(RootElement.class, null)); + assertTrue("Converter does not support reading @XmlType", converter.canRead(Type.class, null)); + } + + @Test + public void canWrite() throws Exception { + assertTrue("Converter does not support writing @XmlRootElement", converter.canWrite(RootElement.class, null)); + assertTrue("Converter does not support writing @XmlRootElement subclass", converter.canWrite(RootElementSubclass.class, null)); + assertTrue("Converter does not support writing @XmlRootElement subclass", converter.canWrite(rootElementCglib.getClass(), null)); + assertFalse("Converter supports writing @XmlType", converter.canWrite(Type.class, null)); + } + + @Test + @SuppressWarnings("unchecked") + public void readXmlRootElement() throws Exception { + byte[] body = "".getBytes("UTF-8"); + MockHttpInputMessage inputMessage = new MockHttpInputMessage(body); + RootElement result = (RootElement) converter.read((Class) RootElement.class, inputMessage); + assertEquals("Invalid result", "Hello World", result.type.s); + } + + @Test + @SuppressWarnings("unchecked") + public void readXmlRootElementSubclass() throws Exception { + byte[] body = "".getBytes("UTF-8"); + MockHttpInputMessage inputMessage = new MockHttpInputMessage(body); + RootElementSubclass result = (RootElementSubclass) converter.read((Class) RootElementSubclass.class, inputMessage); + assertEquals("Invalid result", "Hello World", result.type.s); + } + + @Test + @SuppressWarnings("unchecked") + public void readXmlType() throws Exception { + byte[] body = "".getBytes("UTF-8"); + MockHttpInputMessage inputMessage = new MockHttpInputMessage(body); + Type result = (Type) converter.read((Class) Type.class, inputMessage); + assertEquals("Invalid result", "Hello World", result.s); + } + + @Test + public void writeXmlRootElement() throws Exception { + MockHttpOutputMessage outputMessage = new MockHttpOutputMessage(); + converter.write(rootElement, null, outputMessage); + assertEquals("Invalid content-type", new MediaType("application", "xml"), + outputMessage.getHeaders().getContentType()); + assertXMLEqual("Invalid result", "", + outputMessage.getBodyAsString(Charset.forName("UTF-8"))); + } + + @Test + public void writeXmlRootElementSubclass() throws Exception { + MockHttpOutputMessage outputMessage = new MockHttpOutputMessage(); + converter.write(rootElementCglib, null, outputMessage); + assertEquals("Invalid content-type", new MediaType("application", "xml"), + outputMessage.getHeaders().getContentType()); + assertXMLEqual("Invalid result", "", + outputMessage.getBodyAsString(Charset.forName("UTF-8"))); + } + + @XmlRootElement + public static class RootElement { + + @XmlElement + public Type type = new Type(); + + } + + @XmlType + public static class Type { + + @XmlAttribute + public String s = "Hello World"; + + } + + public static class RootElementSubclass extends RootElement { + + } + +}