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
This commit is contained in:
Arjen Poutsma 2012-06-08 14:47:22 +02:00 committed by Rossen Stoyanchev
parent 789e12a0c7
commit ed3823b045
14 changed files with 1213 additions and 99 deletions

View File

@ -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:
*
* <pre class="code">
* ParameterizedTypeReference&lt;List&lt;String&gt;&gt; typeRef = new ParameterizedTypeReference&lt;List&lt;String&gt;&gt;() {};
* </pre>
*
* <p>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<T> {
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 + ">";
}
}

View File

@ -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<Map<Object,String>> mapTypeReference = new ParameterizedTypeReference<Map<Object,String>>() {};
assertEquals(mapType, mapTypeReference.getType());
}
@Test
public void list() throws NoSuchMethodException {
Type mapType = getClass().getMethod("listMethod").getGenericReturnType();
ParameterizedTypeReference<List<String>> mapTypeReference = new ParameterizedTypeReference<List<String>>() {};
assertEquals(mapType, mapTypeReference.getType());
}
@Test
public void string() {
ParameterizedTypeReference<String> typeReference = new ParameterizedTypeReference<String>() {};
assertEquals(String.class, typeReference.getType());
}
public static Map<Object, String> mapMethod() {
return null;
}
public static List<String> listMethod() {
return null;
}
}

View File

@ -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<T> extends HttpMessageConverter<T> {
/**
* 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;
}

View File

@ -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.JavaType;
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 <a href="http://jackson.codehaus.org/">Jackson 2's</a> {@link ObjectMapper}.
@ -50,7 +52,8 @@ import com.fasterxml.jackson.databind.SerializationFeature;
* @since 3.1.2
* @see org.springframework.web.servlet.view.json.MappingJackson2JsonView
*/
public class MappingJackson2HttpMessageConverter extends AbstractHttpMessageConverter<Object> {
public class MappingJackson2HttpMessageConverter extends AbstractHttpMessageConverter<Object>
implements GenericHttpMessageConverter<Object> {
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 class MappingJackson2HttpMessageConverter extends AbstractHttpMessageConv
@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 @@ public class MappingJackson2HttpMessageConverter extends AbstractHttpMessageConv
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 @@ public class MappingJackson2HttpMessageConverter extends AbstractHttpMessageConv
}
}
@Override
protected void writeInternal(Object object, HttpOutputMessage outputMessage)
throws IOException, HttpMessageNotWritableException {
@ -180,24 +199,24 @@ public class MappingJackson2HttpMessageConverter extends AbstractHttpMessageConv
/**
* Return the Jackson {@link JavaType} for the specified class.
* Return the Jackson {@link JavaType} for the specified type.
* <p>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:
* <pre class="code">
* protected JavaType getJavaType(Class&lt;?&gt; 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);
* }
* }
* </pre>
* @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);
}
/**

View File

@ -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.ObjectMapper;
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 <a href="http://jackson.codehaus.org/">Jackson's</a> {@link ObjectMapper}.
@ -50,7 +52,8 @@ import com.fasterxml.jackson.core.util.DefaultPrettyPrinter;
* @since 3.0
* @see org.springframework.web.servlet.view.json.MappingJacksonJsonView
*/
public class MappingJacksonHttpMessageConverter extends AbstractHttpMessageConverter<Object> {
public class MappingJacksonHttpMessageConverter extends AbstractHttpMessageConverter<Object>
implements GenericHttpMessageConverter<Object> {
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 class MappingJacksonHttpMessageConverter extends AbstractHttpMessageConve
@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 @@ public class MappingJacksonHttpMessageConverter extends AbstractHttpMessageConve
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 @@ public class MappingJacksonHttpMessageConverter extends AbstractHttpMessageConve
/**
* Return the Jackson {@link JavaType} for the specified class.
* Return the Jackson {@link JavaType} for the specified type.
* <p>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:
* <pre class="code">
* protected JavaType getJavaType(Class&lt;?&gt; 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);
* }
* }
* </pre>
* @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);
}
/**

View File

@ -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.
*
* <p>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<T extends Collection>
extends AbstractJaxb2HttpMessageConverter<T> implements GenericHttpMessageConverter<T> {
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}
* <p>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<? extends T> 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.
* <p/> 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();
}
}

View File

@ -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.apache.commons.logging.LogFactory;
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 <code>T</code>.
* Response extractor that uses the given {@linkplain HttpMessageConverter entity
* converters} to convert the response into a type <code>T</code>.
*
* @author Arjen Poutsma
* @see RestTemplate
@ -38,21 +40,31 @@ import org.springframework.util.Assert;
*/
public class HttpMessageConverterExtractor<T> implements ResponseExtractor<T> {
private final Class<T> responseType;
private final Type responseType;
private final List<HttpMessageConverter<?>> 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<T> responseType, List<HttpMessageConverter<?>> 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<HttpMessageConverter<?>> messageConverters) {
this(responseType, messageConverters, LogFactory.getLog(HttpMessageConverterExtractor.class));
}
HttpMessageConverterExtractor(Class<T> responseType, List<HttpMessageConverter<?>> messageConverters, Log logger) {
HttpMessageConverterExtractor(Type responseType, List<HttpMessageConverter<?>> 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 class HttpMessageConverterExtractor<T> implements ResponseExtractor<T> {
if (!hasMessageBody(response)) {
return null;
}
MediaType contentType = getContentType(response);
Class<T> 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 class HttpMessageConverterExtractor<T> implements ResponseExtractor<T> {
}
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.
* <p>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. <p>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 class HttpMessageConverterExtractor<T> implements ResponseExtractor<T> {
*/
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();

View File

@ -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.net.URI;
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 @@ public interface RestOperations {
<T> ResponseEntity<T> exchange(URI url, HttpMethod method, HttpEntity<?> requestEntity,
Class<T> 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:
*
* <pre class="code">
* ParameterizedTypeReference&lt;List&lt;MyBean&gt;&gt; myBean = new ParameterizedTypeReference&lt;List&lt;MyBean&gt;&gt;() {};
* ResponseEntity&lt;List&lt;MyBean&gt;&gt; response = template.exchange(&quot;http://example.com&quot;,HttpMethod.GET, null, myBean);
* </pre>
*
* @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
*/
<T> ResponseEntity<T> exchange(String url,HttpMethod method, HttpEntity<?> requestEntity,
ParameterizedTypeReference<T> 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:
*
* <pre class="code">
* ParameterizedTypeReference&lt;List&lt;MyBean&gt;&gt; myBean = new ParameterizedTypeReference&lt;List&lt;MyBean&gt;&gt;() {};
* ResponseEntity&lt;List&lt;MyBean&gt;&gt; response = template.exchange(&quot;http://example.com&quot;,HttpMethod.GET, null, myBean);
* </pre>
*
* @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
*/
<T> ResponseEntity<T> exchange(String url, HttpMethod method, HttpEntity<?> requestEntity,
ParameterizedTypeReference<T> responseType, Map<String, ?> 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:
*
* <pre class="code">
* ParameterizedTypeReference&lt;List&lt;MyBean&gt;&gt; myBean = new ParameterizedTypeReference&lt;List&lt;MyBean&gt;&gt;() {};
* ResponseEntity&lt;List&lt;MyBean&gt;&gt; response = template.exchange(&quot;http://example.com&quot;,HttpMethod.GET, null, myBean);
* </pre>
*
* @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
*/
<T> ResponseEntity<T> exchange(URI url, HttpMethod method, HttpEntity<?> requestEntity,
ParameterizedTypeReference<T> responseType) throws RestClientException;
// general execution
/**

View File

@ -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 @@ package org.springframework.web.client;
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.List;
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.ClientHttpRequestFactory;
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 class RestTemplate extends InterceptingHttpAccessor implements RestOperat
public <T> ResponseEntity<T> exchange(String url, HttpMethod method,
HttpEntity<?> requestEntity, Class<T> responseType, Object... uriVariables) throws RestClientException {
HttpEntityRequestCallback requestCallback = new HttpEntityRequestCallback(requestEntity, responseType);
ResponseEntityResponseExtractor<T> responseExtractor = new ResponseEntityResponseExtractor<T>(responseType);
return execute(url, method, requestCallback, responseExtractor, uriVariables);
@ -391,6 +395,7 @@ public class RestTemplate extends InterceptingHttpAccessor implements RestOperat
public <T> ResponseEntity<T> exchange(String url, HttpMethod method,
HttpEntity<?> requestEntity, Class<T> responseType, Map<String, ?> uriVariables) throws RestClientException {
HttpEntityRequestCallback requestCallback = new HttpEntityRequestCallback(requestEntity, responseType);
ResponseEntityResponseExtractor<T> responseExtractor = new ResponseEntityResponseExtractor<T>(responseType);
return execute(url, method, requestCallback, responseExtractor, uriVariables);
@ -398,11 +403,39 @@ public class RestTemplate extends InterceptingHttpAccessor implements RestOperat
public <T> ResponseEntity<T> exchange(URI url, HttpMethod method, HttpEntity<?> requestEntity,
Class<T> responseType) throws RestClientException {
HttpEntityRequestCallback requestCallback = new HttpEntityRequestCallback(requestEntity, responseType);
ResponseEntityResponseExtractor<T> responseExtractor = new ResponseEntityResponseExtractor<T>(responseType);
return execute(url, method, requestCallback, responseExtractor);
}
public <T> ResponseEntity<T> exchange(String url, HttpMethod method, HttpEntity<?> requestEntity,
ParameterizedTypeReference<T> responseType, Object... uriVariables) throws RestClientException {
Type type = responseType.getType();
HttpEntityRequestCallback requestCallback = new HttpEntityRequestCallback(requestEntity, type);
ResponseEntityResponseExtractor<T> responseExtractor = new ResponseEntityResponseExtractor<T>(type);
return execute(url, method, requestCallback, responseExtractor, uriVariables);
}
public <T> ResponseEntity<T> exchange(String url, HttpMethod method, HttpEntity<?> requestEntity,
ParameterizedTypeReference<T> responseType, Map<String, ?> uriVariables) throws RestClientException {
Type type = responseType.getType();
HttpEntityRequestCallback requestCallback = new HttpEntityRequestCallback(requestEntity, type);
ResponseEntityResponseExtractor<T> responseExtractor = new ResponseEntityResponseExtractor<T>(type);
return execute(url, method, requestCallback, responseExtractor, uriVariables);
}
public <T> ResponseEntity<T> exchange(URI url, HttpMethod method, HttpEntity<?> requestEntity,
ParameterizedTypeReference<T> responseType) throws RestClientException {
Type type = responseType.getType();
HttpEntityRequestCallback requestCallback = new HttpEntityRequestCallback(requestEntity, type);
ResponseEntityResponseExtractor<T> responseExtractor = new ResponseEntityResponseExtractor<T>(type);
return execute(url, method, requestCallback, responseExtractor);
}
// general execution
public <T> T execute(String url, HttpMethod method, RequestCallback requestCallback,
@ -504,37 +537,62 @@ public class RestTemplate extends InterceptingHttpAccessor implements RestOperat
*/
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<MediaType> allSupportedMediaTypes = new ArrayList<MediaType>();
for (HttpMessageConverter<?> messageConverter : getMessageConverters()) {
if (messageConverter.canRead(responseType, null)) {
List<MediaType> 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<MediaType> getSupportedMediaTypes(HttpMessageConverter<?> messageConverter) {
List<MediaType> supportedMediaTypes = messageConverter.getSupportedMediaTypes();
List<MediaType> result = new ArrayList<MediaType>(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 @@ public class RestTemplate extends InterceptingHttpAccessor implements RestOperat
}
@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 @@ public class RestTemplate extends InterceptingHttpAccessor implements RestOperat
private final HttpMessageConverterExtractor<T> delegate;
public ResponseEntityResponseExtractor(Class<T> responseType) {
public ResponseEntityResponseExtractor(Type responseType) {
if (responseType != null && !Void.class.equals(responseType)) {
this.delegate = new HttpMessageConverterExtractor<T>(responseType, getMessageConverters(), logger);
} else {

View File

@ -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 class MappingJackson2HttpMessageConverterTests extends AbstractMappingJac
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 @@ public class MappingJackson2HttpMessageConverterTests extends AbstractMappingJac
assertArrayEquals(new byte[]{0x1, 0x2}, result.getBytes());
}
@Test
@SuppressWarnings("unchecked")
public void readParameterizedType() throws IOException {
ParameterizedTypeReference<List<MyBean>> beansList = new ParameterizedTypeReference<List<MyBean>>() {};
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<MyBean> results = (List<MyBean>) 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();

View File

@ -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 @@ public class MappingJacksonHttpMessageConverterTests extends AbstractMappingJack
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 @@ public class MappingJacksonHttpMessageConverterTests extends AbstractMappingJack
assertArrayEquals(new byte[]{0x1, 0x2}, result.getBytes());
}
@Test
@SuppressWarnings("unchecked")
public void readParameterizedType() throws IOException {
ParameterizedTypeReference<List<MyBean>> beansList = new ParameterizedTypeReference<List<MyBean>>() {};
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<MyBean> results = (List<MyBean>) 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();

View File

@ -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<Collection<Object>>();
rootElementListType = new ParameterizedTypeReference<List<RootElement>>() {}.getType();
rootElementSetType = new ParameterizedTypeReference<Set<RootElement>>() {}.getType();
typeListType = new ParameterizedTypeReference<List<TestType>>() {}.getType();
typeSetType = new ParameterizedTypeReference<Set<TestType>>() {}.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 = "<list><rootElement><type s=\"1\"/></rootElement><rootElement><type s=\"2\"/></rootElement></list>";
MockHttpInputMessage inputMessage = new MockHttpInputMessage(content.getBytes("UTF-8"));
List<RootElement> result = (List<RootElement>) 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 = "<set><rootElement><type s=\"1\"/></rootElement><rootElement><type s=\"2\"/></rootElement></set>";
MockHttpInputMessage inputMessage = new MockHttpInputMessage(content.getBytes("UTF-8"));
Set<RootElement> result = (Set<RootElement>) 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 = "<list><foo s=\"1\"/><bar s=\"2\"/></list>";
MockHttpInputMessage inputMessage = new MockHttpInputMessage(content.getBytes("UTF-8"));
List<TestType> result = (List<TestType>) 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 = "<set><foo s=\"1\"/><bar s=\"2\"/></set>";
MockHttpInputMessage inputMessage = new MockHttpInputMessage(content.getBytes("UTF-8"));
Set<TestType> result = (Set<TestType>) 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();
}
}
}

View File

@ -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>(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>(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>(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<String> converter = createMock(HttpMessageConverter.class);
List<HttpMessageConverter<?>> converters = new ArrayList<HttpMessageConverter<?>>();
converters.add(converter);
HttpHeaders responseHeaders = new HttpHeaders();
MediaType contentType = MediaType.TEXT_PLAIN;
responseHeaders.setContentType(contentType);
String expected = "Foo";
extractor = new HttpMessageConverterExtractor<String>(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<String> converter = createMock(HttpMessageConverter.class);
List<HttpMessageConverter<?>> converters = new ArrayList<HttpMessageConverter<?>>();
converters.add(converter);
HttpHeaders responseHeaders = new HttpHeaders();
MediaType contentType = MediaType.TEXT_PLAIN;
responseHeaders.setContentType(contentType);
extractor = new HttpMessageConverterExtractor<String>(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<String> converter = createMock(GenericHttpMessageConverter.class);
List<HttpMessageConverter<?>> converters = createConverterList(converter);
HttpHeaders responseHeaders = new HttpHeaders();
MediaType contentType = MediaType.TEXT_PLAIN;
responseHeaders.setContentType(contentType);
String expected = "Foo";
ParameterizedTypeReference<List<String>> reference = new ParameterizedTypeReference<List<String>>() {};
Type type = reference.getType();
extractor = new HttpMessageConverterExtractor<List<String>>(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<HttpMessageConverter<?>> createConverterList(HttpMessageConverter converter) {
List<HttpMessageConverter<?>> converters = new ArrayList<HttpMessageConverter<?>>(1);
converters.add(converter);
return converters;
}
}

View File

@ -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.net.URI;
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.ResponseEntity;
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 class RestTemplateTests {
@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 class RestTemplateTests {
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 class RestTemplateTests {
HttpEntity<String> requestEntity = new HttpEntity<String>(body, entityHeaders);
ResponseEntity<Integer> 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.<HttpMessageConverter<?>>singletonList(converter));
ParameterizedTypeReference<List<Integer>> intList = new ParameterizedTypeReference<List<Integer>>() {};
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<Integer> 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<String> requestEntity = new HttpEntity<String>(requestBody, entityHeaders);
ResponseEntity<List<Integer>> 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);