From ed3823b045fd19cdb801609eb034c93dd4d75c3f Mon Sep 17 00:00:00 2001 From: Arjen Poutsma Date: Fri, 8 Jun 2012 14:47:22 +0200 Subject: [PATCH] Support generic target types in the RestTemplate This change makes it possible to use the RestTemplate to read an HTTP response into a target generic type object. The RestTemplate has three new exchange(...) methods that accept ParameterizedTypeReference -- a new class that enables capturing and passing generic type info. See the Javadoc of the three new methods in RestOperations for a short example. To support this feature, the HttpMessageConverter is now extended by GenericHttpMessageConverter, which adds a method for reading an HttpInputMessage to a specific generic type. The new interface is implemented by the MappingJacksonHttpMessageConverter and also by a new Jaxb2CollectionHttpMessageConverter that can read read a generic Collection where the generic type is a JAXB type annotated with @XmlRootElement or @XmlType. Issue: SPR-7023 --- .../core/ParameterizedTypeReference.java | 99 ++++++++ .../core/ParameterizedTypeReferenceTest.java | 61 +++++ .../GenericHttpMessageConverter.java | 60 +++++ .../MappingJackson2HttpMessageConverter.java | 57 +++-- .../MappingJacksonHttpMessageConverter.java | 46 ++-- .../Jaxb2CollectionHttpMessageConverter.java | 228 ++++++++++++++++++ .../client/HttpMessageConverterExtractor.java | 79 ++++-- .../web/client/RestOperations.java | 68 +++++- .../web/client/RestTemplate.java | 86 +++++-- ...pingJackson2HttpMessageConverterTests.java | 42 +++- ...ppingJacksonHttpMessageConverterTests.java | 36 ++- ...b2CollectionHttpMessageConverterTests.java | 189 +++++++++++++++ .../HttpMessageConverterExtractorTests.java | 196 +++++++++++++++ .../web/client/RestTemplateTests.java | 65 ++++- 14 files changed, 1213 insertions(+), 99 deletions(-) create mode 100644 spring-core/src/main/java/org/springframework/core/ParameterizedTypeReference.java create mode 100644 spring-core/src/test/java/org/springframework/core/ParameterizedTypeReferenceTest.java create mode 100644 spring-web/src/main/java/org/springframework/http/converter/GenericHttpMessageConverter.java create mode 100644 spring-web/src/main/java/org/springframework/http/converter/xml/Jaxb2CollectionHttpMessageConverter.java create mode 100644 spring-web/src/test/java/org/springframework/http/converter/xml/Jaxb2CollectionHttpMessageConverterTests.java create mode 100644 spring-web/src/test/java/org/springframework/web/client/HttpMessageConverterExtractorTests.java diff --git a/spring-core/src/main/java/org/springframework/core/ParameterizedTypeReference.java b/spring-core/src/main/java/org/springframework/core/ParameterizedTypeReference.java new file mode 100644 index 000000000000..8c52650414da --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/ParameterizedTypeReference.java @@ -0,0 +1,99 @@ +/* + * Copyright 2002-2012 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.core; + +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; + +import org.springframework.util.Assert; + +/** + * The purpose of this class is to enable capturing and passing a generic + * {@link Type}. In order to capture the generic type and retain it at runtime, + * you need to create a sub-class as follows: + * + *
+ * ParameterizedTypeReference<List<String>> typeRef = new ParameterizedTypeReference<List<String>>() {};
+ * 
+ * + *

The resulting {@code typeReference} instance can then be used to obtain a + * {@link Type} instance that carries parameterized type information. + * For more information on "super type tokens" see the link to Neal Gafter's blog post. + * + * @author Arjen Poutsma + * @author Rossen Stoyanchev + * @since 3.2 + * + * @see http://gafter.blogspot.nl/2006/12/super-type-tokens.html + */ +public abstract class ParameterizedTypeReference { + + private final Type type; + + protected ParameterizedTypeReference() { + Class parameterizedTypeReferenceSubClass = findParameterizedTypeReferenceSubClass(getClass()); + + Type type = parameterizedTypeReferenceSubClass.getGenericSuperclass(); + Assert.isInstanceOf(ParameterizedType.class, type); + + ParameterizedType parameterizedType = (ParameterizedType) type; + Assert.isTrue(parameterizedType.getActualTypeArguments().length == 1); + + this.type = parameterizedType.getActualTypeArguments()[0]; + } + + private static Class findParameterizedTypeReferenceSubClass(Class child) { + + Class parent = child.getSuperclass(); + + if (Object.class.equals(parent)) { + throw new IllegalStateException("Expected ParameterizedTypeReference superclass"); + } + else if (ParameterizedTypeReference.class.equals(parent)) { + return child; + } + else { + return findParameterizedTypeReferenceSubClass(parent); + } + } + + public Type getType() { + return this.type; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o instanceof ParameterizedTypeReference) { + ParameterizedTypeReference other = (ParameterizedTypeReference) o; + return this.type.equals(other.type); + } + return false; + } + + @Override + public int hashCode() { + return this.type.hashCode(); + } + + @Override + public String toString() { + return "ParameterizedTypeReference<" + this.type + ">"; + } +} diff --git a/spring-core/src/test/java/org/springframework/core/ParameterizedTypeReferenceTest.java b/spring-core/src/test/java/org/springframework/core/ParameterizedTypeReferenceTest.java new file mode 100644 index 000000000000..7162d9ad93c6 --- /dev/null +++ b/spring-core/src/test/java/org/springframework/core/ParameterizedTypeReferenceTest.java @@ -0,0 +1,61 @@ +/* + * Copyright 2002-2012 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.core; + +import java.lang.reflect.Type; +import java.util.List; +import java.util.Map; + +import static org.junit.Assert.assertEquals; +import org.junit.Test; + +/** + * Test fixture for {@link ParameterizedTypeReference}. + * + * @author Arjen Poutsma + * @author Rossen Stoyanchev + */ +public class ParameterizedTypeReferenceTest { + + @Test + public void map() throws NoSuchMethodException { + Type mapType = getClass().getMethod("mapMethod").getGenericReturnType(); + ParameterizedTypeReference> mapTypeReference = new ParameterizedTypeReference>() {}; + assertEquals(mapType, mapTypeReference.getType()); + } + + @Test + public void list() throws NoSuchMethodException { + Type mapType = getClass().getMethod("listMethod").getGenericReturnType(); + ParameterizedTypeReference> mapTypeReference = new ParameterizedTypeReference>() {}; + assertEquals(mapType, mapTypeReference.getType()); + } + + @Test + public void string() { + ParameterizedTypeReference typeReference = new ParameterizedTypeReference() {}; + assertEquals(String.class, typeReference.getType()); + } + + public static Map mapMethod() { + return null; + } + + public static List listMethod() { + return null; + } +} diff --git a/spring-web/src/main/java/org/springframework/http/converter/GenericHttpMessageConverter.java b/spring-web/src/main/java/org/springframework/http/converter/GenericHttpMessageConverter.java new file mode 100644 index 000000000000..89bc5909b4c3 --- /dev/null +++ b/spring-web/src/main/java/org/springframework/http/converter/GenericHttpMessageConverter.java @@ -0,0 +1,60 @@ +/* + * Copyright 2002-2012 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; + +import java.io.IOException; +import java.lang.reflect.Type; + +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.http.HttpInputMessage; +import org.springframework.http.MediaType; + +/** + * A specialization of {@link HttpMessageConverter} that can convert an HTTP + * request into a target object of a specified generic type. + * + * @author Arjen Poutsma + * @since 3.2 + * + * @see ParameterizedTypeReference + */ +public interface GenericHttpMessageConverter extends HttpMessageConverter { + + /** + * Indicates whether the given type can be read by this converter. + * @param type the type to test for readability + * @param mediaType the media type to read, can be {@code null} if not specified. + * Typically the value of a {@code Content-Type} header. + * @return {@code true} if readable; {@code false} otherwise + */ + boolean canRead(Type type, MediaType mediaType); + + /** + * Read an object of the given type form the given input message, and returns it. + * @param clazz the type of object to return. This type must have previously + * been passed to the {@link #canRead canRead} method of this interface, + * which must have returned {@code true}. + * @param type the type of the target object + * @param inputMessage the HTTP input message to read from + * @return the converted object + * @throws IOException in case of I/O errors + * @throws HttpMessageNotReadableException in case of conversion errors + */ + T read(Type type, HttpInputMessage inputMessage) + throws IOException, HttpMessageNotReadableException; + +} diff --git a/spring-web/src/main/java/org/springframework/http/converter/json/MappingJackson2HttpMessageConverter.java b/spring-web/src/main/java/org/springframework/http/converter/json/MappingJackson2HttpMessageConverter.java index 327f529dd237..31f236a6bd53 100644 --- a/spring-web/src/main/java/org/springframework/http/converter/json/MappingJackson2HttpMessageConverter.java +++ b/spring-web/src/main/java/org/springframework/http/converter/json/MappingJackson2HttpMessageConverter.java @@ -17,17 +17,10 @@ package org.springframework.http.converter.json; import java.io.IOException; +import java.lang.reflect.Type; import java.nio.charset.Charset; import java.util.List; -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.HttpMessageNotReadableException; -import org.springframework.http.converter.HttpMessageNotWritableException; -import org.springframework.util.Assert; - import com.fasterxml.jackson.core.JsonEncoding; import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.core.JsonProcessingException; @@ -36,6 +29,15 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.SerializationFeature; +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.GenericHttpMessageConverter; +import org.springframework.http.converter.HttpMessageNotReadableException; +import org.springframework.http.converter.HttpMessageNotWritableException; +import org.springframework.util.Assert; + /** * Implementation of {@link org.springframework.http.converter.HttpMessageConverter HttpMessageConverter} * that can read and write JSON using Jackson 2's {@link ObjectMapper}. @@ -50,7 +52,8 @@ * @since 3.1.2 * @see org.springframework.web.servlet.view.json.MappingJackson2JsonView */ -public class MappingJackson2HttpMessageConverter extends AbstractHttpMessageConverter { +public class MappingJackson2HttpMessageConverter extends AbstractHttpMessageConverter + implements GenericHttpMessageConverter { public static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8"); @@ -63,7 +66,7 @@ public class MappingJackson2HttpMessageConverter extends AbstractHttpMessageConv /** - * Construct a new {@code BindingJacksonHttpMessageConverter}. + * Construct a new {@code MappingJackson2HttpMessageConverter}. */ public MappingJackson2HttpMessageConverter() { super(new MediaType("application", "json", DEFAULT_CHARSET)); @@ -125,7 +128,11 @@ public void setPrettyPrint(boolean prettyPrint) { @Override public boolean canRead(Class clazz, MediaType mediaType) { - JavaType javaType = getJavaType(clazz); + return canRead((Type) clazz, mediaType); + } + + public boolean canRead(Type type, MediaType mediaType) { + JavaType javaType = getJavaType(type); return (this.objectMapper.canDeserialize(javaType) && canRead(mediaType)); } @@ -145,6 +152,17 @@ protected Object readInternal(Class clazz, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException { JavaType javaType = getJavaType(clazz); + return readJavaType(javaType, inputMessage); + } + + public Object read(Type type, HttpInputMessage inputMessage) + throws IOException, HttpMessageNotReadableException { + + JavaType javaType = getJavaType(type); + return readJavaType(javaType, inputMessage); + } + + private Object readJavaType(JavaType javaType, HttpInputMessage inputMessage) { try { return this.objectMapper.readValue(inputMessage.getBody(), javaType); } @@ -153,6 +171,7 @@ protected Object readInternal(Class clazz, HttpInputMessage inputMessage) } } + @Override protected void writeInternal(Object object, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException { @@ -180,24 +199,24 @@ protected void writeInternal(Object object, HttpOutputMessage outputMessage) /** - * Return the Jackson {@link JavaType} for the specified class. + * Return the Jackson {@link JavaType} for the specified type. *

The default implementation returns {@link ObjectMapper#constructType(java.lang.reflect.Type)}, * but this can be overridden in subclasses, to allow for custom generic collection handling. * For instance: *

-	 * protected JavaType getJavaType(Class<?> clazz) {
-	 *   if (List.class.isAssignableFrom(clazz)) {
-	 *     return objectMapper.getTypeFactory().constructCollectionType(ArrayList.class, MyBean.class);
+	 * protected JavaType getJavaType(Type type) {
+	 *   if (type instanceof Class && List.class.isAssignableFrom((Class)type)) {
+	 *     return TypeFactory.collectionType(ArrayList.class, MyBean.class);
 	 *   } else {
-	 *     return super.getJavaType(clazz);
+	 *     return super.getJavaType(type);
 	 *   }
 	 * }
 	 * 
- * @param clazz the class to return the java type for + * @param type the type to return the java type for * @return the java type */ - protected JavaType getJavaType(Class clazz) { - return objectMapper.constructType(clazz); + protected JavaType getJavaType(Type type) { + return this.objectMapper.constructType(type); } /** diff --git a/spring-web/src/main/java/org/springframework/http/converter/json/MappingJacksonHttpMessageConverter.java b/spring-web/src/main/java/org/springframework/http/converter/json/MappingJacksonHttpMessageConverter.java index 51bc4d503763..51976c380bc7 100644 --- a/spring-web/src/main/java/org/springframework/http/converter/json/MappingJacksonHttpMessageConverter.java +++ b/spring-web/src/main/java/org/springframework/http/converter/json/MappingJacksonHttpMessageConverter.java @@ -1,11 +1,11 @@ /* - * Copyright 2002-2011 the original author or authors. + * Copyright 2002-2012 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 + * 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, @@ -17,9 +17,11 @@ package org.springframework.http.converter.json; import java.io.IOException; +import java.lang.reflect.Type; import java.nio.charset.Charset; import java.util.List; +import com.fasterxml.jackson.core.util.DefaultPrettyPrinter; import org.codehaus.jackson.JsonEncoding; import org.codehaus.jackson.JsonGenerator; import org.codehaus.jackson.JsonProcessingException; @@ -27,16 +29,16 @@ import org.codehaus.jackson.map.SerializationConfig; import org.codehaus.jackson.map.type.TypeFactory; import org.codehaus.jackson.type.JavaType; + 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.GenericHttpMessageConverter; import org.springframework.http.converter.HttpMessageNotReadableException; import org.springframework.http.converter.HttpMessageNotWritableException; import org.springframework.util.Assert; -import com.fasterxml.jackson.core.util.DefaultPrettyPrinter; - /** * Implementation of {@link org.springframework.http.converter.HttpMessageConverter HttpMessageConverter} * that can read and write JSON using Jackson's {@link ObjectMapper}. @@ -50,7 +52,8 @@ * @since 3.0 * @see org.springframework.web.servlet.view.json.MappingJacksonJsonView */ -public class MappingJacksonHttpMessageConverter extends AbstractHttpMessageConverter { +public class MappingJacksonHttpMessageConverter extends AbstractHttpMessageConverter + implements GenericHttpMessageConverter { public static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8"); @@ -63,7 +66,7 @@ public class MappingJacksonHttpMessageConverter extends AbstractHttpMessageConve /** - * Construct a new {@code BindingJacksonHttpMessageConverter}. + * Construct a new {@code MappingJacksonHttpMessageConverter}. */ public MappingJacksonHttpMessageConverter() { super(new MediaType("application", "json", DEFAULT_CHARSET)); @@ -125,7 +128,11 @@ public void setPrettyPrint(boolean prettyPrint) { @Override public boolean canRead(Class clazz, MediaType mediaType) { - JavaType javaType = getJavaType(clazz); + return canRead((Type) clazz, mediaType); + } + + public boolean canRead(Type type, MediaType mediaType) { + JavaType javaType = getJavaType(type); return (this.objectMapper.canDeserialize(javaType) && canRead(mediaType)); } @@ -145,6 +152,17 @@ protected Object readInternal(Class clazz, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException { JavaType javaType = getJavaType(clazz); + return readJavaType(javaType, inputMessage); + } + + public Object read(Type type, HttpInputMessage inputMessage) + throws IOException, HttpMessageNotReadableException { + + JavaType javaType = getJavaType(type); + return readJavaType(javaType, inputMessage); + } + + private Object readJavaType(JavaType javaType, HttpInputMessage inputMessage) { try { return this.objectMapper.readValue(inputMessage.getBody(), javaType); } @@ -180,24 +198,24 @@ protected void writeInternal(Object object, HttpOutputMessage outputMessage) /** - * Return the Jackson {@link JavaType} for the specified class. + * Return the Jackson {@link JavaType} for the specified type. *

The default implementation returns {@link TypeFactory#type(java.lang.reflect.Type)}, * but this can be overridden in subclasses, to allow for custom generic collection handling. * For instance: *

-	 * protected JavaType getJavaType(Class<?> clazz) {
-	 *   if (List.class.isAssignableFrom(clazz)) {
+	 * protected JavaType getJavaType(Type type) {
+	 *   if (type instanceof Class && List.class.isAssignableFrom((Class)type)) {
 	 *     return TypeFactory.collectionType(ArrayList.class, MyBean.class);
 	 *   } else {
-	 *     return super.getJavaType(clazz);
+	 *     return super.getJavaType(type);
 	 *   }
 	 * }
 	 * 
- * @param clazz the class to return the java type for + * @param type the type to return the java type for * @return the java type */ - protected JavaType getJavaType(Class clazz) { - return TypeFactory.type(clazz); + protected JavaType getJavaType(Type type) { + return TypeFactory.type(type); } /** diff --git a/spring-web/src/main/java/org/springframework/http/converter/xml/Jaxb2CollectionHttpMessageConverter.java b/spring-web/src/main/java/org/springframework/http/converter/xml/Jaxb2CollectionHttpMessageConverter.java new file mode 100644 index 000000000000..376becb1dfda --- /dev/null +++ b/spring-web/src/main/java/org/springframework/http/converter/xml/Jaxb2CollectionHttpMessageConverter.java @@ -0,0 +1,228 @@ +/* + * Copyright 2002-2012 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 java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.Collection; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.SortedSet; +import java.util.TreeSet; + +import javax.xml.bind.JAXBException; +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.stream.XMLInputFactory; +import javax.xml.stream.XMLStreamException; +import javax.xml.stream.XMLStreamReader; +import javax.xml.transform.Result; +import javax.xml.transform.Source; + +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpInputMessage; +import org.springframework.http.MediaType; +import org.springframework.http.converter.GenericHttpMessageConverter; +import org.springframework.http.converter.HttpMessageConversionException; +import org.springframework.http.converter.HttpMessageNotReadableException; + +/** + * An {@code HttpMessageConverter} that can read XML collections using JAXB2. + * + *

This converter can read {@linkplain Collection collections} that contain classes + * annotated with {@link XmlRootElement} and {@link XmlType}. Note that this converter + * does not support writing. + * + * @author Arjen Poutsma + * @since 3.2 + */ +public class Jaxb2CollectionHttpMessageConverter + extends AbstractJaxb2HttpMessageConverter implements GenericHttpMessageConverter { + + private final XMLInputFactory inputFactory = createXmlInputFactory(); + + /** + * Always returns {@code false} since Jaxb2CollectionHttpMessageConverter + * required generic type information in order to read a Collection. + */ + @Override + public boolean canRead(Class clazz, MediaType mediaType) { + return false; + } + + /** + * {@inheritDoc} + *

Jaxb2CollectionHttpMessageConverter can read a generic + * {@link Collection} where the generic type is a JAXB type annotated with + * {@link XmlRootElement} or {@link XmlType}. + */ + public boolean canRead(Type type, MediaType mediaType) { + if (!(type instanceof ParameterizedType)) { + return false; + } + ParameterizedType parameterizedType = (ParameterizedType) type; + if (!(parameterizedType.getRawType() instanceof Class)) { + return false; + } + Class rawType = (Class) parameterizedType.getRawType(); + if (!(Collection.class.isAssignableFrom(rawType))) { + return false; + } + if (parameterizedType.getActualTypeArguments().length != 1) { + return false; + } + Type typeArgument = parameterizedType.getActualTypeArguments()[0]; + if (!(typeArgument instanceof Class)) { + return false; + } + Class typeArgumentClass = (Class) typeArgument; + return (typeArgumentClass.isAnnotationPresent(XmlRootElement.class) || + typeArgumentClass.isAnnotationPresent(XmlType.class)) && canRead(mediaType); + } + + /** + * Always returns {@code false} since Jaxb2CollectionHttpMessageConverter + * does not convert collections to XML. + */ + @Override + public boolean canWrite(Class clazz, MediaType mediaType) { + return false; + } + + @Override + protected boolean supports(Class clazz) { + // should not be called, since we override canRead/Write + throw new UnsupportedOperationException(); + } + + @Override + protected T readFromSource(Class clazz, HttpHeaders headers, Source source) throws IOException { + // should not be called, since we return false for canRead(Class) + throw new UnsupportedOperationException(); + } + + public T read(Type type, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException { + ParameterizedType parameterizedType = (ParameterizedType) type; + T result = createCollection((Class) parameterizedType.getRawType()); + Class elementClass = (Class) parameterizedType.getActualTypeArguments()[0]; + + try { + Unmarshaller unmarshaller = createUnmarshaller(elementClass); + XMLStreamReader streamReader = this.inputFactory.createXMLStreamReader(inputMessage.getBody()); + int event = moveToFirstChildOfRootElement(streamReader); + + while (event != XMLStreamReader.END_DOCUMENT) { + if (elementClass.isAnnotationPresent(XmlRootElement.class)) { + result.add(unmarshaller.unmarshal(streamReader)); + } + else if (elementClass.isAnnotationPresent(XmlType.class)) { + result.add(unmarshaller.unmarshal(streamReader, elementClass).getValue()); + } + else { + // should not happen, since we check in canRead(Type) + throw new HttpMessageConversionException("Could not unmarshal to [" + elementClass + "]"); + } + event = moveToNextElement(streamReader); + } + return result; + } + catch (UnmarshalException ex) { + throw new HttpMessageNotReadableException("Could not unmarshal to [" + elementClass + "]: " + ex.getMessage(), ex); + } + catch (JAXBException ex) { + throw new HttpMessageConversionException("Could not instantiate JAXBContext: " + ex.getMessage(), ex); + } + catch (XMLStreamException ex) { + throw new HttpMessageConversionException(ex.getMessage(), ex); + } + } + + /** + * Create a Collection of the given type, with the given initial capacity + * (if supported by the Collection type). + * + * @param collectionClass the type of Collection to instantiate + * @return the created Collection instance + */ + @SuppressWarnings("unchecked") + protected T createCollection(Class collectionClass) { + if (!collectionClass.isInterface()) { + try { + return (T) collectionClass.newInstance(); + } + catch (Exception ex) { + throw new IllegalArgumentException( + "Could not instantiate collection class [" + + collectionClass.getName() + "]: " + ex.getMessage()); + } + } + else if (List.class.equals(collectionClass)) { + return (T) new ArrayList(); + } + else if (SortedSet.class.equals(collectionClass)) { + return (T) new TreeSet(); + } + else { + return (T) new LinkedHashSet(); + } + } + + private int moveToFirstChildOfRootElement(XMLStreamReader streamReader) throws XMLStreamException { + // root + int event = streamReader.next(); + while (event != XMLStreamReader.START_ELEMENT) { + event = streamReader.next(); + } + + // first child + event = streamReader.next(); + while ((event != XMLStreamReader.START_ELEMENT) && (event != XMLStreamReader.END_DOCUMENT)) { + event = streamReader.next(); + } + return event; + } + + private int moveToNextElement(XMLStreamReader streamReader) throws XMLStreamException { + int event = streamReader.getEventType(); + while (event != XMLStreamReader.START_ELEMENT && event != XMLStreamReader.END_DOCUMENT) { + event = streamReader.next(); + } + return event; + } + + @Override + protected void writeToResult(T t, HttpHeaders headers, Result result) throws IOException { + throw new UnsupportedOperationException(); + } + + /** + * Create a {@code XMLInputFactory} that this converter will use to create {@link + * javax.xml.stream.XMLStreamReader} and {@link javax.xml.stream.XMLEventReader} objects. + *

Can be overridden in subclasses, adding further initialization of the factory. + * The resulting factory is cached, so this method will only be called once. + * + * @return the created factory + */ + protected XMLInputFactory createXmlInputFactory() { + return XMLInputFactory.newInstance(); + } + +} diff --git a/spring-web/src/main/java/org/springframework/web/client/HttpMessageConverterExtractor.java b/spring-web/src/main/java/org/springframework/web/client/HttpMessageConverterExtractor.java index 7dd17bae4227..dd9de11d6d9b 100644 --- a/spring-web/src/main/java/org/springframework/web/client/HttpMessageConverterExtractor.java +++ b/spring-web/src/main/java/org/springframework/web/client/HttpMessageConverterExtractor.java @@ -17,6 +17,7 @@ package org.springframework.web.client; import java.io.IOException; +import java.lang.reflect.Type; import java.util.List; import org.apache.commons.logging.Log; @@ -25,12 +26,13 @@ import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.client.ClientHttpResponse; +import org.springframework.http.converter.GenericHttpMessageConverter; import org.springframework.http.converter.HttpMessageConverter; import org.springframework.util.Assert; /** - * Response extractor that uses the given {@linkplain HttpMessageConverter entity converters} to convert the response - * into a type T. + * Response extractor that uses the given {@linkplain HttpMessageConverter entity + * converters} to convert the response into a type T. * * @author Arjen Poutsma * @see RestTemplate @@ -38,21 +40,31 @@ */ public class HttpMessageConverterExtractor implements ResponseExtractor { - private final Class responseType; + private final Type responseType; private final List> messageConverters; private final Log logger; /** - * Creates a new instance of the {@code HttpMessageConverterExtractor} with the given response type and message - * converters. The given converters must support the response type. + * Creates a new instance of the {@code HttpMessageConverterExtractor} with the given + * response type and message converters. The given converters must support the response + * type. */ public HttpMessageConverterExtractor(Class responseType, List> messageConverters) { + this((Type) responseType, messageConverters); + } + + /** + * Creates a new instance of the {@code HttpMessageConverterExtractor} with the given + * response type and message converters. The given converters must support the response + * type. + */ + public HttpMessageConverterExtractor(Type responseType, List> messageConverters) { this(responseType, messageConverters, LogFactory.getLog(HttpMessageConverterExtractor.class)); } - HttpMessageConverterExtractor(Class responseType, List> messageConverters, Log logger) { + HttpMessageConverterExtractor(Type responseType, List> messageConverters, Log logger) { Assert.notNull(responseType, "'responseType' must not be null"); Assert.notEmpty(messageConverters, "'messageConverters' must not be empty"); this.responseType = responseType; @@ -65,6 +77,39 @@ public T extractData(ClientHttpResponse response) throws IOException { if (!hasMessageBody(response)) { return null; } + MediaType contentType = getContentType(response); + + Class responseClass = null; + if (this.responseType instanceof Class) { + responseClass = (Class) this.responseType; + } + for (HttpMessageConverter messageConverter : this.messageConverters) { + if (responseClass != null) { + if (messageConverter.canRead(responseClass, contentType)) { + if (logger.isDebugEnabled()) { + logger.debug("Reading [" + responseClass.getName() + "] as \"" + + contentType + "\" using [" + messageConverter + "]"); + } + return (T) messageConverter.read(responseClass, response); + } + } + else if (messageConverter instanceof GenericHttpMessageConverter) { + GenericHttpMessageConverter genericMessageConverter = (GenericHttpMessageConverter) messageConverter; + if (genericMessageConverter.canRead(this.responseType, contentType)) { + if (logger.isDebugEnabled()) { + logger.debug("Reading [" + this.responseType + "] as \"" + + contentType + "\" using [" + messageConverter + "]"); + } + return (T) genericMessageConverter.read(this.responseType, response); + } + } + } + throw new RestClientException( + "Could not extract response: no suitable HttpMessageConverter found for response type [" + + this.responseType + "] and content type [" + contentType + "]"); + } + + private MediaType getContentType(ClientHttpResponse response) { MediaType contentType = response.getHeaders().getContentType(); if (contentType == null) { if (logger.isTraceEnabled()) { @@ -72,24 +117,13 @@ public T extractData(ClientHttpResponse response) throws IOException { } contentType = MediaType.APPLICATION_OCTET_STREAM; } - for (HttpMessageConverter messageConverter : messageConverters) { - if (messageConverter.canRead(responseType, contentType)) { - if (logger.isDebugEnabled()) { - logger.debug("Reading [" + responseType.getName() + "] as \"" + contentType - +"\" using [" + messageConverter + "]"); - } - return (T) messageConverter.read(this.responseType, response); - } - } - throw new RestClientException( - "Could not extract response: no suitable HttpMessageConverter found for response type [" + - this.responseType.getName() + "] and content type [" + contentType + "]"); + return contentType; } /** - * Indicates whether the given response has a message body. - *

Default implementation returns {@code false} for a response status of {@code 204} or {@code 304}, or a - * {@code Content-Length} of {@code 0}. + * Indicates whether the given response has a message body.

Default implementation + * returns {@code false} for a response status of {@code 204} or {@code 304}, or a {@code + * Content-Length} of {@code 0}. * * @param response the response to check for a message body * @return {@code true} if the response has a body, {@code false} otherwise @@ -97,7 +131,8 @@ public T extractData(ClientHttpResponse response) throws IOException { */ protected boolean hasMessageBody(ClientHttpResponse response) throws IOException { HttpStatus responseStatus = response.getStatusCode(); - if (responseStatus == HttpStatus.NO_CONTENT || responseStatus == HttpStatus.NOT_MODIFIED) { + if (responseStatus == HttpStatus.NO_CONTENT || + responseStatus == HttpStatus.NOT_MODIFIED) { return false; } long contentLength = response.getHeaders().getContentLength(); diff --git a/spring-web/src/main/java/org/springframework/web/client/RestOperations.java b/spring-web/src/main/java/org/springframework/web/client/RestOperations.java index 9d5b23ce7bbc..1528643e76b9 100644 --- a/spring-web/src/main/java/org/springframework/web/client/RestOperations.java +++ b/spring-web/src/main/java/org/springframework/web/client/RestOperations.java @@ -1,11 +1,11 @@ /* - * Copyright 2002-2010 the original author or authors. + * Copyright 2002-2012 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 + * 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, @@ -20,6 +20,7 @@ import java.util.Map; import java.util.Set; +import org.springframework.core.ParameterizedTypeReference; import org.springframework.http.HttpEntity; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; @@ -396,6 +397,69 @@ ResponseEntity exchange(String url, HttpMethod method, HttpEntity requ ResponseEntity exchange(URI url, HttpMethod method, HttpEntity requestEntity, Class responseType) throws RestClientException; + /** + * Execute the HTTP method to the given URI template, writing the given + * request entity to the request, and returns the response as {@link ResponseEntity}. + * The given {@link ParameterizedTypeReference} is used to pass generic type information: + * + *

+	 * ParameterizedTypeReference<List<MyBean>> myBean = new ParameterizedTypeReference<List<MyBean>>() {};
+	 * ResponseEntity<List<MyBean>> response = template.exchange("http://example.com",HttpMethod.GET, null, myBean);
+	 * 
+ * + * @param url the URL + * @param method the HTTP method (GET, POST, etc) + * @param requestEntity the entity (headers and/or body) to write to the + * request, may be {@code null} + * @param responseType the type of the return value + * @param uriVariables the variables to expand in the template + * @return the response as entity + * @since 3.2.0 + */ + ResponseEntity exchange(String url,HttpMethod method, HttpEntity requestEntity, + ParameterizedTypeReference responseType, Object... uriVariables) throws RestClientException; + + /** + * Execute the HTTP method to the given URI template, writing the given + * request entity to the request, and returns the response as {@link ResponseEntity}. + * The given {@link ParameterizedTypeReference} is used to pass generic type information: + * + *
+	 * ParameterizedTypeReference<List<MyBean>> myBean = new ParameterizedTypeReference<List<MyBean>>() {};
+	 * ResponseEntity<List<MyBean>> response = template.exchange("http://example.com",HttpMethod.GET, null, myBean);
+	 * 
+ * + * @param url the URL + * @param method the HTTP method (GET, POST, etc) + * @param requestEntity the entity (headers and/or body) to write to the request, may be {@code null} + * @param responseType the type of the return value + * @param uriVariables the variables to expand in the template + * @return the response as entity + * @since 3.2.0 + */ + ResponseEntity exchange(String url, HttpMethod method, HttpEntity requestEntity, + ParameterizedTypeReference responseType, Map uriVariables) throws RestClientException; + + /** + * Execute the HTTP method to the given URI template, writing the given + * request entity to the request, and returns the response as {@link ResponseEntity}. + * The given {@link ParameterizedTypeReference} is used to pass generic type information: + * + *
+	 * ParameterizedTypeReference<List<MyBean>> myBean = new ParameterizedTypeReference<List<MyBean>>() {};
+	 * ResponseEntity<List<MyBean>> response = template.exchange("http://example.com",HttpMethod.GET, null, myBean);
+	 * 
+ * + * @param url the URL + * @param method the HTTP method (GET, POST, etc) + * @param requestEntity the entity (headers and/or body) to write to the request, may be {@code null} + * @param responseType the type of the return value + * @return the response as entity + * @since 3.2.0 + */ + ResponseEntity exchange(URI url, HttpMethod method, HttpEntity requestEntity, + ParameterizedTypeReference responseType) throws RestClientException; + // general execution /** diff --git a/spring-web/src/main/java/org/springframework/web/client/RestTemplate.java b/spring-web/src/main/java/org/springframework/web/client/RestTemplate.java index ce0f5bdf6e9e..032ea53b9608 100644 --- a/spring-web/src/main/java/org/springframework/web/client/RestTemplate.java +++ b/spring-web/src/main/java/org/springframework/web/client/RestTemplate.java @@ -5,7 +5,7 @@ * 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 + * 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, @@ -18,6 +18,7 @@ import java.io.IOException; import java.io.UnsupportedEncodingException; +import java.lang.reflect.Type; import java.net.URI; import java.net.URISyntaxException; import java.util.ArrayList; @@ -25,6 +26,7 @@ import java.util.Map; import java.util.Set; +import org.springframework.core.ParameterizedTypeReference; import org.springframework.http.HttpEntity; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; @@ -35,6 +37,7 @@ import org.springframework.http.client.ClientHttpResponse; import org.springframework.http.client.support.InterceptingHttpAccessor; import org.springframework.http.converter.ByteArrayHttpMessageConverter; +import org.springframework.http.converter.GenericHttpMessageConverter; import org.springframework.http.converter.HttpMessageConverter; import org.springframework.http.converter.ResourceHttpMessageConverter; import org.springframework.http.converter.StringHttpMessageConverter; @@ -384,6 +387,7 @@ public Set optionsForAllow(URI url) throws RestClientException { public ResponseEntity exchange(String url, HttpMethod method, HttpEntity requestEntity, Class responseType, Object... uriVariables) throws RestClientException { + HttpEntityRequestCallback requestCallback = new HttpEntityRequestCallback(requestEntity, responseType); ResponseEntityResponseExtractor responseExtractor = new ResponseEntityResponseExtractor(responseType); return execute(url, method, requestCallback, responseExtractor, uriVariables); @@ -391,6 +395,7 @@ public ResponseEntity exchange(String url, HttpMethod method, public ResponseEntity exchange(String url, HttpMethod method, HttpEntity requestEntity, Class responseType, Map uriVariables) throws RestClientException { + HttpEntityRequestCallback requestCallback = new HttpEntityRequestCallback(requestEntity, responseType); ResponseEntityResponseExtractor responseExtractor = new ResponseEntityResponseExtractor(responseType); return execute(url, method, requestCallback, responseExtractor, uriVariables); @@ -398,11 +403,39 @@ public ResponseEntity exchange(String url, HttpMethod method, public ResponseEntity exchange(URI url, HttpMethod method, HttpEntity requestEntity, Class responseType) throws RestClientException { + HttpEntityRequestCallback requestCallback = new HttpEntityRequestCallback(requestEntity, responseType); ResponseEntityResponseExtractor responseExtractor = new ResponseEntityResponseExtractor(responseType); return execute(url, method, requestCallback, responseExtractor); } + public ResponseEntity exchange(String url, HttpMethod method, HttpEntity requestEntity, + ParameterizedTypeReference responseType, Object... uriVariables) throws RestClientException { + + Type type = responseType.getType(); + HttpEntityRequestCallback requestCallback = new HttpEntityRequestCallback(requestEntity, type); + ResponseEntityResponseExtractor responseExtractor = new ResponseEntityResponseExtractor(type); + return execute(url, method, requestCallback, responseExtractor, uriVariables); + } + + public ResponseEntity exchange(String url, HttpMethod method, HttpEntity requestEntity, + ParameterizedTypeReference responseType, Map uriVariables) throws RestClientException { + + Type type = responseType.getType(); + HttpEntityRequestCallback requestCallback = new HttpEntityRequestCallback(requestEntity, type); + ResponseEntityResponseExtractor responseExtractor = new ResponseEntityResponseExtractor(type); + return execute(url, method, requestCallback, responseExtractor, uriVariables); + } + + public ResponseEntity exchange(URI url, HttpMethod method, HttpEntity requestEntity, + ParameterizedTypeReference responseType) throws RestClientException { + + Type type = responseType.getType(); + HttpEntityRequestCallback requestCallback = new HttpEntityRequestCallback(requestEntity, type); + ResponseEntityResponseExtractor responseExtractor = new ResponseEntityResponseExtractor(type); + return execute(url, method, requestCallback, responseExtractor); + } + // general execution public T execute(String url, HttpMethod method, RequestCallback requestCallback, @@ -504,37 +537,62 @@ private void handleResponseError(HttpMethod method, URI url, ClientHttpResponse */ private class AcceptHeaderRequestCallback implements RequestCallback { - private final Class responseType; + private final Type responseType; - private AcceptHeaderRequestCallback(Class responseType) { + private AcceptHeaderRequestCallback(Type responseType) { this.responseType = responseType; } @SuppressWarnings("unchecked") public void doWithRequest(ClientHttpRequest request) throws IOException { if (responseType != null) { + Class responseClass = null; + if (responseType instanceof Class) { + responseClass = (Class) responseType; + } + List allSupportedMediaTypes = new ArrayList(); for (HttpMessageConverter messageConverter : getMessageConverters()) { - if (messageConverter.canRead(responseType, null)) { - List supportedMediaTypes = messageConverter.getSupportedMediaTypes(); - for (MediaType supportedMediaType : supportedMediaTypes) { - if (supportedMediaType.getCharSet() != null) { - supportedMediaType = - new MediaType(supportedMediaType.getType(), supportedMediaType.getSubtype()); - } - allSupportedMediaTypes.add(supportedMediaType); + if (responseClass != null) { + if (messageConverter.canRead(responseClass, null)) { + allSupportedMediaTypes + .addAll(getSupportedMediaTypes(messageConverter)); } } + else if (messageConverter instanceof GenericHttpMessageConverter) { + + GenericHttpMessageConverter genericMessageConverter = + (GenericHttpMessageConverter) messageConverter; + if (genericMessageConverter.canRead(responseType, null)) { + allSupportedMediaTypes + .addAll(getSupportedMediaTypes(messageConverter)); + } + } + } if (!allSupportedMediaTypes.isEmpty()) { MediaType.sortBySpecificity(allSupportedMediaTypes); if (logger.isDebugEnabled()) { - logger.debug("Setting request Accept header to " + allSupportedMediaTypes); + logger.debug("Setting request Accept header to " + + allSupportedMediaTypes); } request.getHeaders().setAccept(allSupportedMediaTypes); } } } + + private List getSupportedMediaTypes(HttpMessageConverter messageConverter) { + List supportedMediaTypes = messageConverter.getSupportedMediaTypes(); + List result = new ArrayList(supportedMediaTypes.size()); + for (MediaType supportedMediaType : supportedMediaTypes) { + if (supportedMediaType.getCharSet() != null) { + supportedMediaType = + new MediaType(supportedMediaType.getType(), supportedMediaType.getSubtype()); + } + result.add(supportedMediaType); + } + return result; + } } @@ -550,7 +608,7 @@ private HttpEntityRequestCallback(Object requestBody) { } @SuppressWarnings("unchecked") - private HttpEntityRequestCallback(Object requestBody, Class responseType) { + private HttpEntityRequestCallback(Object requestBody, Type responseType) { super(responseType); if (requestBody instanceof HttpEntity) { this.requestEntity = (HttpEntity) requestBody; @@ -618,7 +676,7 @@ private class ResponseEntityResponseExtractor implements ResponseExtractor delegate; - public ResponseEntityResponseExtractor(Class responseType) { + public ResponseEntityResponseExtractor(Type responseType) { if (responseType != null && !Void.class.equals(responseType)) { this.delegate = new HttpMessageConverterExtractor(responseType, getMessageConverters(), logger); } else { diff --git a/spring-web/src/test/java/org/springframework/http/converter/json/MappingJackson2HttpMessageConverterTests.java b/spring-web/src/test/java/org/springframework/http/converter/json/MappingJackson2HttpMessageConverterTests.java index b449545f15d3..603b6613abec 100644 --- a/spring-web/src/test/java/org/springframework/http/converter/json/MappingJackson2HttpMessageConverterTests.java +++ b/spring-web/src/test/java/org/springframework/http/converter/json/MappingJackson2HttpMessageConverterTests.java @@ -16,23 +16,22 @@ package org.springframework.http.converter.json; -import static org.junit.Assert.assertArrayEquals; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; - import java.io.IOException; +import java.lang.reflect.Type; import java.nio.charset.Charset; import java.util.ArrayList; import java.util.List; +import com.fasterxml.jackson.databind.JavaType; +import com.fasterxml.jackson.databind.ObjectMapper; +import static org.junit.Assert.*; import org.junit.Test; + +import org.springframework.core.ParameterizedTypeReference; import org.springframework.http.MediaType; import org.springframework.http.MockHttpInputMessage; import org.springframework.http.MockHttpOutputMessage; -import com.fasterxml.jackson.databind.JavaType; -import com.fasterxml.jackson.databind.ObjectMapper; - /** * Jackson 2.x converter tests. * @@ -52,12 +51,12 @@ public void readGenerics() throws IOException { MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter() { @Override - protected JavaType getJavaType(Class clazz) { - if (List.class.isAssignableFrom(clazz)) { + protected JavaType getJavaType(Type type) { + if (type instanceof Class && List.class.isAssignableFrom((Class)type)) { return new ObjectMapper().getTypeFactory().constructCollectionType(ArrayList.class, MyBean.class); } else { - return super.getJavaType(clazz); + return super.getJavaType(type); } } }; @@ -77,6 +76,29 @@ protected JavaType getJavaType(Class clazz) { assertArrayEquals(new byte[]{0x1, 0x2}, result.getBytes()); } + @Test + @SuppressWarnings("unchecked") + public void readParameterizedType() throws IOException { + ParameterizedTypeReference> beansList = new ParameterizedTypeReference>() {}; + + String body = + "[{\"bytes\":\"AQI=\",\"array\":[\"Foo\",\"Bar\"],\"number\":42,\"string\":\"Foo\",\"bool\":true,\"fraction\":42.0}]"; + MockHttpInputMessage inputMessage = new MockHttpInputMessage(body.getBytes("UTF-8")); + inputMessage.getHeaders().setContentType(new MediaType("application", "json")); + + MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter(); + List results = (List) converter.read(beansList.getType(), inputMessage); + assertEquals(1, results.size()); + MyBean result = results.get(0); + assertEquals("Foo", result.getString()); + assertEquals(42, result.getNumber()); + assertEquals(42F, result.getFraction(), 0F); + assertArrayEquals(new String[]{"Foo", "Bar"}, result.getArray()); + assertTrue(result.isBool()); + assertArrayEquals(new byte[]{0x1, 0x2}, result.getBytes()); + } + + @Test public void prettyPrint() throws Exception { MockHttpOutputMessage outputMessage = new MockHttpOutputMessage(); diff --git a/spring-web/src/test/java/org/springframework/http/converter/json/MappingJacksonHttpMessageConverterTests.java b/spring-web/src/test/java/org/springframework/http/converter/json/MappingJacksonHttpMessageConverterTests.java index e5fb9f32905e..c3fe567a69cb 100644 --- a/spring-web/src/test/java/org/springframework/http/converter/json/MappingJacksonHttpMessageConverterTests.java +++ b/spring-web/src/test/java/org/springframework/http/converter/json/MappingJacksonHttpMessageConverterTests.java @@ -16,18 +16,18 @@ package org.springframework.http.converter.json; -import static org.junit.Assert.assertArrayEquals; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; - import java.io.IOException; +import java.lang.reflect.Type; import java.nio.charset.Charset; import java.util.ArrayList; import java.util.List; import org.codehaus.jackson.map.type.TypeFactory; import org.codehaus.jackson.type.JavaType; +import static org.junit.Assert.*; import org.junit.Test; + +import org.springframework.core.ParameterizedTypeReference; import org.springframework.http.MediaType; import org.springframework.http.MockHttpInputMessage; import org.springframework.http.MockHttpOutputMessage; @@ -49,12 +49,12 @@ protected MappingJacksonHttpMessageConverter createConverter() { public void readGenerics() throws IOException { MappingJacksonHttpMessageConverter converter = new MappingJacksonHttpMessageConverter() { @Override - protected JavaType getJavaType(Class clazz) { - if (List.class.isAssignableFrom(clazz)) { + protected JavaType getJavaType(Type type) { + if (type instanceof Class && List.class.isAssignableFrom((Class)type)) { return TypeFactory.collectionType(ArrayList.class, MyBean.class); } else { - return super.getJavaType(clazz); + return super.getJavaType(type); } } }; @@ -74,6 +74,28 @@ protected JavaType getJavaType(Class clazz) { assertArrayEquals(new byte[]{0x1, 0x2}, result.getBytes()); } + @Test + @SuppressWarnings("unchecked") + public void readParameterizedType() throws IOException { + ParameterizedTypeReference> beansList = new ParameterizedTypeReference>() {}; + + String body = + "[{\"bytes\":\"AQI=\",\"array\":[\"Foo\",\"Bar\"],\"number\":42,\"string\":\"Foo\",\"bool\":true,\"fraction\":42.0}]"; + MockHttpInputMessage inputMessage = new MockHttpInputMessage(body.getBytes("UTF-8")); + inputMessage.getHeaders().setContentType(new MediaType("application", "json")); + + MappingJacksonHttpMessageConverter converter = new MappingJacksonHttpMessageConverter(); + List results = (List) converter.read(beansList.getType(), inputMessage); + assertEquals(1, results.size()); + MyBean result = results.get(0); + assertEquals("Foo", result.getString()); + assertEquals(42, result.getNumber()); + assertEquals(42F, result.getFraction(), 0F); + assertArrayEquals(new String[]{"Foo", "Bar"}, result.getArray()); + assertTrue(result.isBool()); + assertArrayEquals(new byte[]{0x1, 0x2}, result.getBytes()); + } + @Test public void prettyPrint() throws Exception { MockHttpOutputMessage outputMessage = new MockHttpOutputMessage(); diff --git a/spring-web/src/test/java/org/springframework/http/converter/xml/Jaxb2CollectionHttpMessageConverterTests.java b/spring-web/src/test/java/org/springframework/http/converter/xml/Jaxb2CollectionHttpMessageConverterTests.java new file mode 100644 index 000000000000..44715ac6cbe3 --- /dev/null +++ b/spring-web/src/test/java/org/springframework/http/converter/xml/Jaxb2CollectionHttpMessageConverterTests.java @@ -0,0 +1,189 @@ +/* + * Copyright 2002-2012 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 static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import java.lang.reflect.Type; +import java.util.Collection; +import java.util.List; +import java.util.Set; + +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 org.junit.Before; +import org.junit.Test; +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.http.MockHttpInputMessage; + +/** + * Test fixture for {@link Jaxb2CollectionHttpMessageConverter}. + * + * @author Arjen Poutsma + */ +public class Jaxb2CollectionHttpMessageConverterTests { + + private Jaxb2CollectionHttpMessageConverter converter; + + private Type rootElementListType; + + private Type rootElementSetType; + + private Type typeListType; + + private Type typeSetType; + + + @Before + public void setUp() { + converter = new Jaxb2CollectionHttpMessageConverter>(); + rootElementListType = new ParameterizedTypeReference>() {}.getType(); + rootElementSetType = new ParameterizedTypeReference>() {}.getType(); + typeListType = new ParameterizedTypeReference>() {}.getType(); + typeSetType = new ParameterizedTypeReference>() {}.getType(); + } + + @Test + public void canRead() throws Exception { + assertTrue(converter.canRead(rootElementListType, null)); + assertTrue(converter.canRead(rootElementSetType, null)); + assertTrue(converter.canRead(typeSetType, null)); + } + + @Test + @SuppressWarnings("unchecked") + public void readXmlRootElementList() throws Exception { + String content = ""; + MockHttpInputMessage inputMessage = new MockHttpInputMessage(content.getBytes("UTF-8")); + + List result = (List) converter.read(rootElementListType, inputMessage); + + assertEquals("Invalid result", 2, result.size()); + assertEquals("Invalid result", "1", result.get(0).type.s); + assertEquals("Invalid result", "2", result.get(1).type.s); + } + + @Test + @SuppressWarnings("unchecked") + public void readXmlRootElementSet() throws Exception { + String content = ""; + MockHttpInputMessage inputMessage = new MockHttpInputMessage(content.getBytes("UTF-8")); + + Set result = (Set) converter.read(rootElementSetType, inputMessage); + + assertEquals("Invalid result", 2, result.size()); + assertTrue("Invalid result", result.contains(new RootElement("1"))); + assertTrue("Invalid result", result.contains(new RootElement("2"))); + } + + @Test + @SuppressWarnings("unchecked") + public void readXmlTypeList() throws Exception { + String content = ""; + MockHttpInputMessage inputMessage = new MockHttpInputMessage(content.getBytes("UTF-8")); + + List result = (List) converter.read(typeListType, inputMessage); + + assertEquals("Invalid result", 2, result.size()); + assertEquals("Invalid result", "1", result.get(0).s); + assertEquals("Invalid result", "2", result.get(1).s); + } + + @Test + @SuppressWarnings("unchecked") + public void readXmlTypeSet() throws Exception { + String content = ""; + MockHttpInputMessage inputMessage = new MockHttpInputMessage(content.getBytes("UTF-8")); + + Set result = (Set) converter.read(typeSetType, inputMessage); + + assertEquals("Invalid result", 2, result.size()); + assertTrue("Invalid result", result.contains(new TestType("1"))); + assertTrue("Invalid result", result.contains(new TestType("2"))); + } + + + @XmlRootElement + public static class RootElement { + + public RootElement() { + } + + public RootElement(String s) { + this.type = new TestType(s); + } + + @XmlElement + public TestType type = new TestType(); + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o instanceof RootElement) { + RootElement other = (RootElement) o; + return this.type.equals(other.type); + } + return false; + } + + @Override + public int hashCode() { + return type.hashCode(); + } + } + + @XmlType + public static class TestType { + + public TestType() { + } + + public TestType(String s) { + this.s = s; + } + + @XmlAttribute + public String s = "Hello World"; + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o instanceof TestType) { + TestType other = (TestType) o; + return this.s.equals(other.s); + } + return false; + } + + @Override + public int hashCode() { + return s.hashCode(); + } + + + + } + +} diff --git a/spring-web/src/test/java/org/springframework/web/client/HttpMessageConverterExtractorTests.java b/spring-web/src/test/java/org/springframework/web/client/HttpMessageConverterExtractorTests.java new file mode 100644 index 000000000000..a8e9a418e661 --- /dev/null +++ b/spring-web/src/test/java/org/springframework/web/client/HttpMessageConverterExtractorTests.java @@ -0,0 +1,196 @@ +/* + * Copyright 2002-2012 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.web.client; + +import static org.easymock.EasyMock.createMock; +import static org.easymock.EasyMock.expect; +import static org.easymock.EasyMock.replay; +import static org.easymock.EasyMock.verify; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.fail; + +import java.io.IOException; +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.List; + +import org.junit.Before; +import org.junit.Test; +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.client.ClientHttpResponse; +import org.springframework.http.converter.GenericHttpMessageConverter; +import org.springframework.http.converter.HttpMessageConverter; + +/** + * Test fixture for {@link HttpMessageConverter}. + * + * @author Arjen Poutsma + */ +public class HttpMessageConverterExtractorTests { + + private HttpMessageConverterExtractor extractor; + + private ClientHttpResponse response; + + @Before + public void createMocks() { + response = createMock(ClientHttpResponse.class); + } + + @Test + public void noContent() throws IOException { + HttpMessageConverter converter = createMock(HttpMessageConverter.class); + + extractor = new HttpMessageConverterExtractor(String.class, createConverterList(converter)); + + expect(response.getStatusCode()).andReturn(HttpStatus.NO_CONTENT); + + replay(response, converter); + Object result = extractor.extractData(response); + + assertNull(result); + verify(response, converter); + } + + @Test + public void notModified() throws IOException { + HttpMessageConverter converter = createMock(HttpMessageConverter.class); + + extractor = new HttpMessageConverterExtractor(String.class, createConverterList(converter)); + + expect(response.getStatusCode()).andReturn(HttpStatus.NOT_MODIFIED); + + replay(response, converter); + Object result = extractor.extractData(response); + + assertNull(result); + verify(response, converter); + } + + @Test + public void zeroContentLength() throws IOException { + HttpMessageConverter converter = createMock(HttpMessageConverter.class); + HttpHeaders responseHeaders = new HttpHeaders(); + responseHeaders.setContentLength(0); + + extractor = new HttpMessageConverterExtractor(String.class, createConverterList(converter)); + + expect(response.getStatusCode()).andReturn(HttpStatus.OK); + expect(response.getHeaders()).andReturn(responseHeaders); + + replay(response, converter); + Object result = extractor.extractData(response); + + assertNull(result); + verify(response, converter); + } + + @Test + @SuppressWarnings("unchecked") + public void normal() throws IOException { + HttpMessageConverter converter = createMock(HttpMessageConverter.class); + List> converters = new ArrayList>(); + converters.add(converter); + + HttpHeaders responseHeaders = new HttpHeaders(); + MediaType contentType = MediaType.TEXT_PLAIN; + responseHeaders.setContentType(contentType); + String expected = "Foo"; + + extractor = new HttpMessageConverterExtractor(String.class, converters); + + expect(response.getStatusCode()).andReturn(HttpStatus.OK); + expect(response.getHeaders()).andReturn(responseHeaders).times(2); + expect(converter.canRead(String.class, contentType)).andReturn(true); + expect(converter.read(String.class, response)).andReturn(expected); + + replay(response, converter); + Object result = extractor.extractData(response); + + assertEquals(expected, result); + verify(response, converter); + } + + @Test + @SuppressWarnings("unchecked") + public void cannotRead() throws IOException { + HttpMessageConverter converter = createMock(HttpMessageConverter.class); + List> converters = new ArrayList>(); + converters.add(converter); + + HttpHeaders responseHeaders = new HttpHeaders(); + MediaType contentType = MediaType.TEXT_PLAIN; + responseHeaders.setContentType(contentType); + + extractor = new HttpMessageConverterExtractor(String.class, converters); + + expect(response.getStatusCode()).andReturn(HttpStatus.OK); + expect(response.getHeaders()).andReturn(responseHeaders).times(2); + expect(converter.canRead(String.class, contentType)).andReturn(false); + + replay(response, converter); + try { + extractor.extractData(response); + fail("RestClientException expected"); + } + catch (RestClientException expected) { + // expected + } + + verify(response, converter); + } + + @Test + @SuppressWarnings("unchecked") + public void generics() throws IOException { + GenericHttpMessageConverter converter = createMock(GenericHttpMessageConverter.class); + List> converters = createConverterList(converter); + + HttpHeaders responseHeaders = new HttpHeaders(); + MediaType contentType = MediaType.TEXT_PLAIN; + responseHeaders.setContentType(contentType); + String expected = "Foo"; + + ParameterizedTypeReference> reference = new ParameterizedTypeReference>() {}; + Type type = reference.getType(); + + extractor = new HttpMessageConverterExtractor>(type, converters); + + expect(response.getStatusCode()).andReturn(HttpStatus.OK); + expect(response.getHeaders()).andReturn(responseHeaders).times(2); + expect(converter.canRead(type, contentType)).andReturn(true); + expect(converter.read(type, response)).andReturn(expected); + + replay(response, converter); + Object result = extractor.extractData(response); + + assertEquals(expected, result); + verify(response, converter); + } + + private List> createConverterList(HttpMessageConverter converter) { + List> converters = new ArrayList>(1); + converters.add(converter); + return converters; + } + + +} diff --git a/spring-web/src/test/java/org/springframework/web/client/RestTemplateTests.java b/spring-web/src/test/java/org/springframework/web/client/RestTemplateTests.java index ff58437bfeb7..d12c5cf8e394 100644 --- a/spring-web/src/test/java/org/springframework/web/client/RestTemplateTests.java +++ b/spring-web/src/test/java/org/springframework/web/client/RestTemplateTests.java @@ -1,11 +1,11 @@ /* - * Copyright 2002-2011 the original author or authors. + * Copyright 2002-2012 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 + * 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, @@ -21,12 +21,16 @@ import java.util.Collections; import java.util.EnumSet; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.Set; +import static org.easymock.EasyMock.*; +import static org.junit.Assert.*; import org.junit.Before; import org.junit.Test; +import org.springframework.core.ParameterizedTypeReference; import org.springframework.http.HttpEntity; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; @@ -36,11 +40,9 @@ import org.springframework.http.client.ClientHttpRequest; import org.springframework.http.client.ClientHttpRequestFactory; import org.springframework.http.client.ClientHttpResponse; +import org.springframework.http.converter.GenericHttpMessageConverter; import org.springframework.http.converter.HttpMessageConverter; -import static org.easymock.EasyMock.*; -import static org.junit.Assert.*; - /** @author Arjen Poutsma */ @SuppressWarnings("unchecked") public class RestTemplateTests { @@ -600,9 +602,8 @@ public void ioException() throws Exception { @Test public void exchange() throws Exception { - MediaType textPlain = new MediaType("text", "plain"); expect(converter.canRead(Integer.class, null)).andReturn(true); - expect(converter.getSupportedMediaTypes()).andReturn(Collections.singletonList(textPlain)); + expect(converter.getSupportedMediaTypes()).andReturn(Collections.singletonList(MediaType.TEXT_PLAIN)); expect(requestFactory.createRequest(new URI("http://example.com"), HttpMethod.POST)).andReturn(this.request); HttpHeaders requestHeaders = new HttpHeaders(); expect(this.request.getHeaders()).andReturn(requestHeaders).times(2); @@ -612,12 +613,12 @@ public void exchange() throws Exception { expect(this.request.execute()).andReturn(response); expect(errorHandler.hasError(response)).andReturn(false); HttpHeaders responseHeaders = new HttpHeaders(); - responseHeaders.setContentType(textPlain); + responseHeaders.setContentType(MediaType.TEXT_PLAIN); responseHeaders.setContentLength(10); expect(response.getStatusCode()).andReturn(HttpStatus.OK); expect(response.getHeaders()).andReturn(responseHeaders).times(3); Integer expected = 42; - expect(converter.canRead(Integer.class, textPlain)).andReturn(true); + expect(converter.canRead(Integer.class, MediaType.TEXT_PLAIN)).andReturn(true); expect(converter.read(Integer.class, response)).andReturn(expected); expect(response.getStatusCode()).andReturn(HttpStatus.OK); response.close(); @@ -629,14 +630,56 @@ public void exchange() throws Exception { HttpEntity requestEntity = new HttpEntity(body, entityHeaders); ResponseEntity result = template.exchange("http://example.com", HttpMethod.POST, requestEntity, Integer.class); assertEquals("Invalid POST result", expected, result.getBody()); - assertEquals("Invalid Content-Type", textPlain, result.getHeaders().getContentType()); - assertEquals("Invalid Accept header", textPlain.toString(), requestHeaders.getFirst("Accept")); + assertEquals("Invalid Content-Type", MediaType.TEXT_PLAIN, result.getHeaders().getContentType()); + assertEquals("Invalid Accept header", MediaType.TEXT_PLAIN_VALUE, requestHeaders.getFirst("Accept")); assertEquals("Invalid custom header", "MyValue", requestHeaders.getFirst("MyHeader")); assertEquals("Invalid status code", HttpStatus.OK, result.getStatusCode()); verifyMocks(); } + @Test + public void exchangeParameterizedType() throws Exception { + GenericHttpMessageConverter converter = createMock(GenericHttpMessageConverter.class); + template.setMessageConverters(Collections.>singletonList(converter)); + + ParameterizedTypeReference> intList = new ParameterizedTypeReference>() {}; + expect(converter.canRead(intList.getType(), null)).andReturn(true); + expect(converter.getSupportedMediaTypes()).andReturn(Collections.singletonList(MediaType.TEXT_PLAIN)); + expect(requestFactory.createRequest(new URI("http://example.com"), HttpMethod.POST)).andReturn(this.request); + HttpHeaders requestHeaders = new HttpHeaders(); + expect(this.request.getHeaders()).andReturn(requestHeaders).times(2); + expect(converter.canWrite(String.class, null)).andReturn(true); + String requestBody = "Hello World"; + converter.write(requestBody, null, this.request); + expect(this.request.execute()).andReturn(response); + expect(errorHandler.hasError(response)).andReturn(false); + HttpHeaders responseHeaders = new HttpHeaders(); + responseHeaders.setContentType(MediaType.TEXT_PLAIN); + responseHeaders.setContentLength(10); + expect(response.getStatusCode()).andReturn(HttpStatus.OK); + expect(response.getHeaders()).andReturn(responseHeaders).times(3); + List expected = Collections.singletonList(42); + expect(converter.canRead(intList.getType(), MediaType.TEXT_PLAIN)).andReturn(true); + expect(converter.read(intList.getType(), response)).andReturn(expected); + expect(response.getStatusCode()).andReturn(HttpStatus.OK); + response.close(); + + replay(requestFactory, request, response, errorHandler, converter); + + HttpHeaders entityHeaders = new HttpHeaders(); + entityHeaders.set("MyHeader", "MyValue"); + HttpEntity requestEntity = new HttpEntity(requestBody, entityHeaders); + ResponseEntity> result = template.exchange("http://example.com", HttpMethod.POST, requestEntity, intList); + assertEquals("Invalid POST result", expected, result.getBody()); + assertEquals("Invalid Content-Type", MediaType.TEXT_PLAIN, result.getHeaders().getContentType()); + assertEquals("Invalid Accept header", MediaType.TEXT_PLAIN_VALUE, requestHeaders.getFirst("Accept")); + assertEquals("Invalid custom header", "MyValue", requestHeaders.getFirst("MyHeader")); + assertEquals("Invalid status code", HttpStatus.OK, result.getStatusCode()); + + verify(requestFactory, request, response, errorHandler, converter); + } + private void replayMocks() { replay(requestFactory, request, response, errorHandler, converter);