Skip to content

Commit

Permalink
Support generic target types in the RestTemplate
Browse files Browse the repository at this point in the history
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
  • Loading branch information
poutsma authored and rstoyanchev committed Aug 22, 2012
1 parent 789e12a commit ed3823b
Show file tree
Hide file tree
Showing 14 changed files with 1,213 additions and 99 deletions.
@@ -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 + ">";
}
}
@@ -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;
}
}
@@ -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;

}
Expand Up @@ -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;
Expand All @@ -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 <a href="http://jackson.codehaus.org/">Jackson 2's</a> {@link ObjectMapper}.
Expand All @@ -50,7 +52,8 @@
* @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");

Expand All @@ -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));
Expand Down Expand Up @@ -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));
}

Expand All @@ -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);
}
Expand All @@ -153,6 +171,7 @@ protected Object readInternal(Class<?> clazz, HttpInputMessage inputMessage)
}
}


@Override
protected void writeInternal(Object object, HttpOutputMessage outputMessage)
throws IOException, HttpMessageNotWritableException {
Expand Down Expand Up @@ -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.
* <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);
}

/**
Expand Down

0 comments on commit ed3823b

Please sign in to comment.