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,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 {