diff --git a/spring-core/src/main/java/org/springframework/core/ParameterizedTypeReference.java b/spring-core/src/main/java/org/springframework/core/ParameterizedTypeReference.java
new file mode 100644
index 00000000000..8c52650414d
--- /dev/null
+++ b/spring-core/src/main/java/org/springframework/core/ParameterizedTypeReference.java
@@ -0,0 +1,99 @@
+/*
+ * Copyright 2002-2012 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.core;
+
+import java.lang.reflect.ParameterizedType;
+import java.lang.reflect.Type;
+
+import org.springframework.util.Assert;
+
+/**
+ * The purpose of this class is to enable capturing and passing a generic
+ * {@link Type}. In order to capture the generic type and retain it at runtime,
+ * you need to create a sub-class as follows:
+ *
+ *
+ * ParameterizedTypeReference<List<String>> typeRef = new ParameterizedTypeReference<List<String>>() {};
+ *
+ *
+ * The resulting {@code typeReference} instance can then be used to obtain a
+ * {@link Type} instance that carries parameterized type information.
+ * For more information on "super type tokens" see the link to Neal Gafter's blog post.
+ *
+ * @author Arjen Poutsma
+ * @author Rossen Stoyanchev
+ * @since 3.2
+ *
+ * @see http://gafter.blogspot.nl/2006/12/super-type-tokens.html
+ */
+public abstract class ParameterizedTypeReference {
+
+ private final Type type;
+
+ protected ParameterizedTypeReference() {
+ Class> parameterizedTypeReferenceSubClass = findParameterizedTypeReferenceSubClass(getClass());
+
+ Type type = parameterizedTypeReferenceSubClass.getGenericSuperclass();
+ Assert.isInstanceOf(ParameterizedType.class, type);
+
+ ParameterizedType parameterizedType = (ParameterizedType) type;
+ Assert.isTrue(parameterizedType.getActualTypeArguments().length == 1);
+
+ this.type = parameterizedType.getActualTypeArguments()[0];
+ }
+
+ private static Class> findParameterizedTypeReferenceSubClass(Class> child) {
+
+ Class> parent = child.getSuperclass();
+
+ if (Object.class.equals(parent)) {
+ throw new IllegalStateException("Expected ParameterizedTypeReference superclass");
+ }
+ else if (ParameterizedTypeReference.class.equals(parent)) {
+ return child;
+ }
+ else {
+ return findParameterizedTypeReferenceSubClass(parent);
+ }
+ }
+
+ public Type getType() {
+ return this.type;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o instanceof ParameterizedTypeReference) {
+ ParameterizedTypeReference> other = (ParameterizedTypeReference>) o;
+ return this.type.equals(other.type);
+ }
+ return false;
+ }
+
+ @Override
+ public int hashCode() {
+ return this.type.hashCode();
+ }
+
+ @Override
+ public String toString() {
+ return "ParameterizedTypeReference<" + this.type + ">";
+ }
+}
diff --git a/spring-core/src/test/java/org/springframework/core/ParameterizedTypeReferenceTest.java b/spring-core/src/test/java/org/springframework/core/ParameterizedTypeReferenceTest.java
new file mode 100644
index 00000000000..7162d9ad93c
--- /dev/null
+++ b/spring-core/src/test/java/org/springframework/core/ParameterizedTypeReferenceTest.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright 2002-2012 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.core;
+
+import java.lang.reflect.Type;
+import java.util.List;
+import java.util.Map;
+
+import static org.junit.Assert.assertEquals;
+import org.junit.Test;
+
+/**
+ * Test fixture for {@link ParameterizedTypeReference}.
+ *
+ * @author Arjen Poutsma
+ * @author Rossen Stoyanchev
+ */
+public class ParameterizedTypeReferenceTest {
+
+ @Test
+ public void map() throws NoSuchMethodException {
+ Type mapType = getClass().getMethod("mapMethod").getGenericReturnType();
+ ParameterizedTypeReference> mapTypeReference = new ParameterizedTypeReference>() {};
+ assertEquals(mapType, mapTypeReference.getType());
+ }
+
+ @Test
+ public void list() throws NoSuchMethodException {
+ Type mapType = getClass().getMethod("listMethod").getGenericReturnType();
+ ParameterizedTypeReference> mapTypeReference = new ParameterizedTypeReference>() {};
+ assertEquals(mapType, mapTypeReference.getType());
+ }
+
+ @Test
+ public void string() {
+ ParameterizedTypeReference typeReference = new ParameterizedTypeReference() {};
+ assertEquals(String.class, typeReference.getType());
+ }
+
+ public static Map mapMethod() {
+ return null;
+ }
+
+ public static List listMethod() {
+ return null;
+ }
+}
diff --git a/spring-web/src/main/java/org/springframework/http/converter/GenericHttpMessageConverter.java b/spring-web/src/main/java/org/springframework/http/converter/GenericHttpMessageConverter.java
new file mode 100644
index 00000000000..89bc5909b4c
--- /dev/null
+++ b/spring-web/src/main/java/org/springframework/http/converter/GenericHttpMessageConverter.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright 2002-2012 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.http.converter;
+
+import java.io.IOException;
+import java.lang.reflect.Type;
+
+import org.springframework.core.ParameterizedTypeReference;
+import org.springframework.http.HttpInputMessage;
+import org.springframework.http.MediaType;
+
+/**
+ * A specialization of {@link HttpMessageConverter} that can convert an HTTP
+ * request into a target object of a specified generic type.
+ *
+ * @author Arjen Poutsma
+ * @since 3.2
+ *
+ * @see ParameterizedTypeReference
+ */
+public interface GenericHttpMessageConverter extends HttpMessageConverter {
+
+ /**
+ * Indicates whether the given type can be read by this converter.
+ * @param type the type to test for readability
+ * @param mediaType the media type to read, can be {@code null} if not specified.
+ * Typically the value of a {@code Content-Type} header.
+ * @return {@code true} if readable; {@code false} otherwise
+ */
+ boolean canRead(Type type, MediaType mediaType);
+
+ /**
+ * Read an object of the given type form the given input message, and returns it.
+ * @param clazz the type of object to return. This type must have previously
+ * been passed to the {@link #canRead canRead} method of this interface,
+ * which must have returned {@code true}.
+ * @param type the type of the target object
+ * @param inputMessage the HTTP input message to read from
+ * @return the converted object
+ * @throws IOException in case of I/O errors
+ * @throws HttpMessageNotReadableException in case of conversion errors
+ */
+ T read(Type type, HttpInputMessage inputMessage)
+ throws IOException, HttpMessageNotReadableException;
+
+}
diff --git a/spring-web/src/main/java/org/springframework/http/converter/json/MappingJackson2HttpMessageConverter.java b/spring-web/src/main/java/org/springframework/http/converter/json/MappingJackson2HttpMessageConverter.java
index 327f529dd23..31f236a6bd5 100644
--- a/spring-web/src/main/java/org/springframework/http/converter/json/MappingJackson2HttpMessageConverter.java
+++ b/spring-web/src/main/java/org/springframework/http/converter/json/MappingJackson2HttpMessageConverter.java
@@ -17,17 +17,10 @@
package org.springframework.http.converter.json;
import java.io.IOException;
+import java.lang.reflect.Type;
import java.nio.charset.Charset;
import java.util.List;
-import org.springframework.http.HttpInputMessage;
-import org.springframework.http.HttpOutputMessage;
-import org.springframework.http.MediaType;
-import org.springframework.http.converter.AbstractHttpMessageConverter;
-import org.springframework.http.converter.HttpMessageNotReadableException;
-import org.springframework.http.converter.HttpMessageNotWritableException;
-import org.springframework.util.Assert;
-
import com.fasterxml.jackson.core.JsonEncoding;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonProcessingException;
@@ -36,6 +29,15 @@ import com.fasterxml.jackson.databind.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 Jackson 2's {@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 {
+public class MappingJackson2HttpMessageConverter extends AbstractHttpMessageConverter
+ implements GenericHttpMessageConverter {
public static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8");
@@ -63,7 +66,7 @@ public class MappingJackson2HttpMessageConverter extends AbstractHttpMessageConv
/**
- * Construct a new {@code BindingJacksonHttpMessageConverter}.
+ * Construct a new {@code MappingJackson2HttpMessageConverter}.
*/
public MappingJackson2HttpMessageConverter() {
super(new MediaType("application", "json", DEFAULT_CHARSET));
@@ -125,7 +128,11 @@ public 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.
* The default implementation returns {@link ObjectMapper#constructType(java.lang.reflect.Type)},
* but this can be overridden in subclasses, to allow for custom generic collection handling.
* For instance:
*
- * protected JavaType getJavaType(Class<?> clazz) {
- * if (List.class.isAssignableFrom(clazz)) {
- * return objectMapper.getTypeFactory().constructCollectionType(ArrayList.class, MyBean.class);
+ * protected JavaType getJavaType(Type type) {
+ * if (type instanceof Class && List.class.isAssignableFrom((Class)type)) {
+ * return TypeFactory.collectionType(ArrayList.class, MyBean.class);
* } else {
- * return super.getJavaType(clazz);
+ * return super.getJavaType(type);
* }
* }
*
- * @param clazz the class to return the java type for
+ * @param type the type to return the java type for
* @return the java type
*/
- protected JavaType getJavaType(Class> clazz) {
- return objectMapper.constructType(clazz);
+ protected JavaType getJavaType(Type type) {
+ return this.objectMapper.constructType(type);
}
/**
diff --git a/spring-web/src/main/java/org/springframework/http/converter/json/MappingJacksonHttpMessageConverter.java b/spring-web/src/main/java/org/springframework/http/converter/json/MappingJacksonHttpMessageConverter.java
index 51bc4d50376..51976c380bc 100644
--- a/spring-web/src/main/java/org/springframework/http/converter/json/MappingJacksonHttpMessageConverter.java
+++ b/spring-web/src/main/java/org/springframework/http/converter/json/MappingJacksonHttpMessageConverter.java
@@ -1,11 +1,11 @@
/*
- * Copyright 2002-2011 the original author or authors.
+ * Copyright 2002-2012 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
@@ -17,9 +17,11 @@
package org.springframework.http.converter.json;
import java.io.IOException;
+import java.lang.reflect.Type;
import java.nio.charset.Charset;
import java.util.List;
+import com.fasterxml.jackson.core.util.DefaultPrettyPrinter;
import org.codehaus.jackson.JsonEncoding;
import org.codehaus.jackson.JsonGenerator;
import org.codehaus.jackson.JsonProcessingException;
@@ -27,16 +29,16 @@ import org.codehaus.jackson.map.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 Jackson's {@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 {
+public class MappingJacksonHttpMessageConverter extends AbstractHttpMessageConverter
+ implements GenericHttpMessageConverter {
public static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8");
@@ -63,7 +66,7 @@ public class MappingJacksonHttpMessageConverter extends AbstractHttpMessageConve
/**
- * Construct a new {@code BindingJacksonHttpMessageConverter}.
+ * Construct a new {@code MappingJacksonHttpMessageConverter}.
*/
public MappingJacksonHttpMessageConverter() {
super(new MediaType("application", "json", DEFAULT_CHARSET));
@@ -125,7 +128,11 @@ public 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.
* The default implementation returns {@link TypeFactory#type(java.lang.reflect.Type)},
* but this can be overridden in subclasses, to allow for custom generic collection handling.
* For instance:
*
- * protected JavaType getJavaType(Class<?> clazz) {
- * if (List.class.isAssignableFrom(clazz)) {
+ * protected JavaType getJavaType(Type type) {
+ * if (type instanceof Class && List.class.isAssignableFrom((Class)type)) {
* return TypeFactory.collectionType(ArrayList.class, MyBean.class);
* } else {
- * return super.getJavaType(clazz);
+ * return super.getJavaType(type);
* }
* }
*
- * @param clazz the class to return the java type for
+ * @param type the type to return the java type for
* @return the java type
*/
- protected JavaType getJavaType(Class> clazz) {
- return TypeFactory.type(clazz);
+ protected JavaType getJavaType(Type type) {
+ return TypeFactory.type(type);
}
/**
diff --git a/spring-web/src/main/java/org/springframework/http/converter/xml/Jaxb2CollectionHttpMessageConverter.java b/spring-web/src/main/java/org/springframework/http/converter/xml/Jaxb2CollectionHttpMessageConverter.java
new file mode 100644
index 00000000000..376becb1dfd
--- /dev/null
+++ b/spring-web/src/main/java/org/springframework/http/converter/xml/Jaxb2CollectionHttpMessageConverter.java
@@ -0,0 +1,228 @@
+/*
+ * Copyright 2002-2012 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.http.converter.xml;
+
+import java.io.IOException;
+import java.lang.reflect.ParameterizedType;
+import java.lang.reflect.Type;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.SortedSet;
+import java.util.TreeSet;
+
+import javax.xml.bind.JAXBException;
+import javax.xml.bind.UnmarshalException;
+import javax.xml.bind.Unmarshaller;
+import javax.xml.bind.annotation.XmlRootElement;
+import javax.xml.bind.annotation.XmlType;
+import javax.xml.stream.XMLInputFactory;
+import javax.xml.stream.XMLStreamException;
+import javax.xml.stream.XMLStreamReader;
+import javax.xml.transform.Result;
+import javax.xml.transform.Source;
+
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.HttpInputMessage;
+import org.springframework.http.MediaType;
+import org.springframework.http.converter.GenericHttpMessageConverter;
+import org.springframework.http.converter.HttpMessageConversionException;
+import org.springframework.http.converter.HttpMessageNotReadableException;
+
+/**
+ * An {@code HttpMessageConverter} that can read XML collections using JAXB2.
+ *
+ * This converter can read {@linkplain Collection collections} that contain classes
+ * annotated with {@link XmlRootElement} and {@link XmlType}. Note that this converter
+ * does not support writing.
+ *
+ * @author Arjen Poutsma
+ * @since 3.2
+ */
+public class Jaxb2CollectionHttpMessageConverter
+ extends AbstractJaxb2HttpMessageConverter implements GenericHttpMessageConverter {
+
+ private final XMLInputFactory inputFactory = createXmlInputFactory();
+
+ /**
+ * Always returns {@code false} since Jaxb2CollectionHttpMessageConverter
+ * required generic type information in order to read a Collection.
+ */
+ @Override
+ public boolean canRead(Class> clazz, MediaType mediaType) {
+ return false;
+ }
+
+ /**
+ * {@inheritDoc}
+ * Jaxb2CollectionHttpMessageConverter can read a generic
+ * {@link Collection} where the generic type is a JAXB type annotated with
+ * {@link XmlRootElement} or {@link XmlType}.
+ */
+ public boolean canRead(Type type, MediaType mediaType) {
+ if (!(type instanceof ParameterizedType)) {
+ return false;
+ }
+ ParameterizedType parameterizedType = (ParameterizedType) type;
+ if (!(parameterizedType.getRawType() instanceof Class)) {
+ return false;
+ }
+ Class> rawType = (Class>) parameterizedType.getRawType();
+ if (!(Collection.class.isAssignableFrom(rawType))) {
+ return false;
+ }
+ if (parameterizedType.getActualTypeArguments().length != 1) {
+ return false;
+ }
+ Type typeArgument = parameterizedType.getActualTypeArguments()[0];
+ if (!(typeArgument instanceof Class)) {
+ return false;
+ }
+ Class> typeArgumentClass = (Class>) typeArgument;
+ return (typeArgumentClass.isAnnotationPresent(XmlRootElement.class) ||
+ typeArgumentClass.isAnnotationPresent(XmlType.class)) && canRead(mediaType);
+ }
+
+ /**
+ * Always returns {@code false} since Jaxb2CollectionHttpMessageConverter
+ * does not convert collections to XML.
+ */
+ @Override
+ public boolean canWrite(Class> clazz, MediaType mediaType) {
+ return false;
+ }
+
+ @Override
+ protected boolean supports(Class> clazz) {
+ // should not be called, since we override canRead/Write
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ protected T readFromSource(Class 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.
+ *
Can be overridden in subclasses, adding further initialization of the factory.
+ * The resulting factory is cached, so this method will only be called once.
+ *
+ * @return the created factory
+ */
+ protected XMLInputFactory createXmlInputFactory() {
+ return XMLInputFactory.newInstance();
+ }
+
+}
diff --git a/spring-web/src/main/java/org/springframework/web/client/HttpMessageConverterExtractor.java b/spring-web/src/main/java/org/springframework/web/client/HttpMessageConverterExtractor.java
index 7dd17bae422..dd9de11d6d9 100644
--- a/spring-web/src/main/java/org/springframework/web/client/HttpMessageConverterExtractor.java
+++ b/spring-web/src/main/java/org/springframework/web/client/HttpMessageConverterExtractor.java
@@ -17,6 +17,7 @@
package org.springframework.web.client;
import java.io.IOException;
+import java.lang.reflect.Type;
import java.util.List;
import org.apache.commons.logging.Log;
@@ -25,12 +26,13 @@ import org.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 T.
+ * Response extractor that uses the given {@linkplain HttpMessageConverter entity
+ * converters} to convert the response into a type T.
*
* @author Arjen Poutsma
* @see RestTemplate
@@ -38,21 +40,31 @@ import org.springframework.util.Assert;
*/
public class HttpMessageConverterExtractor implements ResponseExtractor {
- private final Class responseType;
+ private final Type responseType;
private final List> messageConverters;
private final Log logger;
/**
- * Creates a new instance of the {@code HttpMessageConverterExtractor} with the given response type and message
- * converters. The given converters must support the response type.
+ * Creates a new instance of the {@code HttpMessageConverterExtractor} with the given
+ * response type and message converters. The given converters must support the response
+ * type.
*/
public HttpMessageConverterExtractor(Class responseType, List> messageConverters) {
+ this((Type) responseType, messageConverters);
+ }
+
+ /**
+ * Creates a new instance of the {@code HttpMessageConverterExtractor} with the given
+ * response type and message converters. The given converters must support the response
+ * type.
+ */
+ public HttpMessageConverterExtractor(Type responseType, List> messageConverters) {
this(responseType, messageConverters, LogFactory.getLog(HttpMessageConverterExtractor.class));
}
- HttpMessageConverterExtractor(Class responseType, List> messageConverters, Log logger) {
+ HttpMessageConverterExtractor(Type responseType, List> messageConverters, Log logger) {
Assert.notNull(responseType, "'responseType' must not be null");
Assert.notEmpty(messageConverters, "'messageConverters' must not be empty");
this.responseType = responseType;
@@ -65,6 +77,39 @@ public class HttpMessageConverterExtractor implements ResponseExtractor {
if (!hasMessageBody(response)) {
return null;
}
+ MediaType contentType = getContentType(response);
+
+ Class responseClass = null;
+ if (this.responseType instanceof Class) {
+ responseClass = (Class) this.responseType;
+ }
+ for (HttpMessageConverter messageConverter : this.messageConverters) {
+ if (responseClass != null) {
+ if (messageConverter.canRead(responseClass, contentType)) {
+ if (logger.isDebugEnabled()) {
+ logger.debug("Reading [" + responseClass.getName() + "] as \"" +
+ contentType + "\" using [" + messageConverter + "]");
+ }
+ return (T) messageConverter.read(responseClass, response);
+ }
+ }
+ else if (messageConverter instanceof GenericHttpMessageConverter) {
+ GenericHttpMessageConverter genericMessageConverter = (GenericHttpMessageConverter) messageConverter;
+ if (genericMessageConverter.canRead(this.responseType, contentType)) {
+ if (logger.isDebugEnabled()) {
+ logger.debug("Reading [" + this.responseType + "] as \"" +
+ contentType + "\" using [" + messageConverter + "]");
+ }
+ return (T) genericMessageConverter.read(this.responseType, response);
+ }
+ }
+ }
+ throw new RestClientException(
+ "Could not extract response: no suitable HttpMessageConverter found for response type [" +
+ this.responseType + "] and content type [" + contentType + "]");
+ }
+
+ private MediaType getContentType(ClientHttpResponse response) {
MediaType contentType = response.getHeaders().getContentType();
if (contentType == null) {
if (logger.isTraceEnabled()) {
@@ -72,24 +117,13 @@ public class HttpMessageConverterExtractor implements ResponseExtractor {
}
contentType = MediaType.APPLICATION_OCTET_STREAM;
}
- for (HttpMessageConverter messageConverter : messageConverters) {
- if (messageConverter.canRead(responseType, contentType)) {
- if (logger.isDebugEnabled()) {
- logger.debug("Reading [" + responseType.getName() + "] as \"" + contentType
- +"\" using [" + messageConverter + "]");
- }
- return (T) messageConverter.read(this.responseType, response);
- }
- }
- throw new RestClientException(
- "Could not extract response: no suitable HttpMessageConverter found for response type [" +
- this.responseType.getName() + "] and content type [" + contentType + "]");
+ return contentType;
}
/**
- * Indicates whether the given response has a message body.
- * Default implementation returns {@code false} for a response status of {@code 204} or {@code 304}, or a
- * {@code Content-Length} of {@code 0}.
+ * Indicates whether the given response has a message body.
Default implementation
+ * returns {@code false} for a response status of {@code 204} or {@code 304}, or a {@code
+ * Content-Length} of {@code 0}.
*
* @param response the response to check for a message body
* @return {@code true} if the response has a body, {@code false} otherwise
@@ -97,7 +131,8 @@ public class HttpMessageConverterExtractor implements ResponseExtractor {
*/
protected boolean hasMessageBody(ClientHttpResponse response) throws IOException {
HttpStatus responseStatus = response.getStatusCode();
- if (responseStatus == HttpStatus.NO_CONTENT || responseStatus == HttpStatus.NOT_MODIFIED) {
+ if (responseStatus == HttpStatus.NO_CONTENT ||
+ responseStatus == HttpStatus.NOT_MODIFIED) {
return false;
}
long contentLength = response.getHeaders().getContentLength();
diff --git a/spring-web/src/main/java/org/springframework/web/client/RestOperations.java b/spring-web/src/main/java/org/springframework/web/client/RestOperations.java
index 9d5b23ce7bb..1528643e76b 100644
--- a/spring-web/src/main/java/org/springframework/web/client/RestOperations.java
+++ b/spring-web/src/main/java/org/springframework/web/client/RestOperations.java
@@ -1,11 +1,11 @@
/*
- * Copyright 2002-2010 the original author or authors.
+ * Copyright 2002-2012 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
@@ -20,6 +20,7 @@ import java.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 {
ResponseEntity exchange(URI url, HttpMethod method, HttpEntity> requestEntity,
Class responseType) throws RestClientException;
+ /**
+ * Execute the HTTP method to the given URI template, writing the given
+ * request entity to the request, and returns the response as {@link ResponseEntity}.
+ * The given {@link ParameterizedTypeReference} is used to pass generic type information:
+ *
+ *
+ * ParameterizedTypeReference<List<MyBean>> myBean = new ParameterizedTypeReference<List<MyBean>>() {};
+ * ResponseEntity<List<MyBean>> response = template.exchange("http://example.com",HttpMethod.GET, null, myBean);
+ *
+ *
+ * @param url the URL
+ * @param method the HTTP method (GET, POST, etc)
+ * @param requestEntity the entity (headers and/or body) to write to the
+ * request, may be {@code null}
+ * @param responseType the type of the return value
+ * @param uriVariables the variables to expand in the template
+ * @return the response as entity
+ * @since 3.2.0
+ */
+ ResponseEntity exchange(String url,HttpMethod method, HttpEntity> requestEntity,
+ ParameterizedTypeReference responseType, Object... uriVariables) throws RestClientException;
+
+ /**
+ * Execute the HTTP method to the given URI template, writing the given
+ * request entity to the request, and returns the response as {@link ResponseEntity}.
+ * The given {@link ParameterizedTypeReference} is used to pass generic type information:
+ *
+ *
+ * ParameterizedTypeReference<List<MyBean>> myBean = new ParameterizedTypeReference<List<MyBean>>() {};
+ * ResponseEntity<List<MyBean>> response = template.exchange("http://example.com",HttpMethod.GET, null, myBean);
+ *
+ *
+ * @param url the URL
+ * @param method the HTTP method (GET, POST, etc)
+ * @param requestEntity the entity (headers and/or body) to write to the request, may be {@code null}
+ * @param responseType the type of the return value
+ * @param uriVariables the variables to expand in the template
+ * @return the response as entity
+ * @since 3.2.0
+ */
+ ResponseEntity exchange(String url, HttpMethod method, HttpEntity> requestEntity,
+ ParameterizedTypeReference responseType, Map uriVariables) throws RestClientException;
+
+ /**
+ * Execute the HTTP method to the given URI template, writing the given
+ * request entity to the request, and returns the response as {@link ResponseEntity}.
+ * The given {@link ParameterizedTypeReference} is used to pass generic type information:
+ *
+ *
+ * ParameterizedTypeReference<List<MyBean>> myBean = new ParameterizedTypeReference<List<MyBean>>() {};
+ * ResponseEntity<List<MyBean>> response = template.exchange("http://example.com",HttpMethod.GET, null, myBean);
+ *
+ *
+ * @param url the URL
+ * @param method the HTTP method (GET, POST, etc)
+ * @param requestEntity the entity (headers and/or body) to write to the request, may be {@code null}
+ * @param responseType the type of the return value
+ * @return the response as entity
+ * @since 3.2.0
+ */
+ ResponseEntity exchange(URI url, HttpMethod method, HttpEntity> requestEntity,
+ ParameterizedTypeReference responseType) throws RestClientException;
+
// general execution
/**
diff --git a/spring-web/src/main/java/org/springframework/web/client/RestTemplate.java b/spring-web/src/main/java/org/springframework/web/client/RestTemplate.java
index ce0f5bdf6e9..032ea53b960 100644
--- a/spring-web/src/main/java/org/springframework/web/client/RestTemplate.java
+++ b/spring-web/src/main/java/org/springframework/web/client/RestTemplate.java
@@ -5,7 +5,7 @@
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
@@ -18,6 +18,7 @@ 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 ResponseEntity exchange(String url, HttpMethod method,
HttpEntity> requestEntity, Class responseType, Object... uriVariables) throws RestClientException {
+
HttpEntityRequestCallback requestCallback = new HttpEntityRequestCallback(requestEntity, responseType);
ResponseEntityResponseExtractor responseExtractor = new ResponseEntityResponseExtractor(responseType);
return execute(url, method, requestCallback, responseExtractor, uriVariables);
@@ -391,6 +395,7 @@ public class RestTemplate extends InterceptingHttpAccessor implements RestOperat
public ResponseEntity exchange(String url, HttpMethod method,
HttpEntity> requestEntity, Class responseType, Map uriVariables) throws RestClientException {
+
HttpEntityRequestCallback requestCallback = new HttpEntityRequestCallback(requestEntity, responseType);
ResponseEntityResponseExtractor responseExtractor = new ResponseEntityResponseExtractor(responseType);
return execute(url, method, requestCallback, responseExtractor, uriVariables);
@@ -398,11 +403,39 @@ public class RestTemplate extends InterceptingHttpAccessor implements RestOperat
public ResponseEntity exchange(URI url, HttpMethod method, HttpEntity> requestEntity,
Class responseType) throws RestClientException {
+
HttpEntityRequestCallback requestCallback = new HttpEntityRequestCallback(requestEntity, responseType);
ResponseEntityResponseExtractor responseExtractor = new ResponseEntityResponseExtractor(responseType);
return execute(url, method, requestCallback, responseExtractor);
}
+ public ResponseEntity exchange(String url, HttpMethod method, HttpEntity> requestEntity,
+ ParameterizedTypeReference responseType, Object... uriVariables) throws RestClientException {
+
+ Type type = responseType.getType();
+ HttpEntityRequestCallback requestCallback = new HttpEntityRequestCallback(requestEntity, type);
+ ResponseEntityResponseExtractor responseExtractor = new ResponseEntityResponseExtractor(type);
+ return execute(url, method, requestCallback, responseExtractor, uriVariables);
+ }
+
+ public ResponseEntity exchange(String url, HttpMethod method, HttpEntity> requestEntity,
+ ParameterizedTypeReference responseType, Map uriVariables) throws RestClientException {
+
+ Type type = responseType.getType();
+ HttpEntityRequestCallback requestCallback = new HttpEntityRequestCallback(requestEntity, type);
+ ResponseEntityResponseExtractor responseExtractor = new ResponseEntityResponseExtractor(type);
+ return execute(url, method, requestCallback, responseExtractor, uriVariables);
+ }
+
+ public ResponseEntity exchange(URI url, HttpMethod method, HttpEntity> requestEntity,
+ ParameterizedTypeReference responseType) throws RestClientException {
+
+ Type type = responseType.getType();
+ HttpEntityRequestCallback requestCallback = new HttpEntityRequestCallback(requestEntity, type);
+ ResponseEntityResponseExtractor responseExtractor = new ResponseEntityResponseExtractor(type);
+ return execute(url, method, requestCallback, responseExtractor);
+ }
+
// general execution
public T execute(String url, HttpMethod method, RequestCallback requestCallback,
@@ -504,37 +537,62 @@ 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 allSupportedMediaTypes = new ArrayList();
for (HttpMessageConverter> messageConverter : getMessageConverters()) {
- if (messageConverter.canRead(responseType, null)) {
- List supportedMediaTypes = messageConverter.getSupportedMediaTypes();
- for (MediaType supportedMediaType : supportedMediaTypes) {
- if (supportedMediaType.getCharSet() != null) {
- supportedMediaType =
- new MediaType(supportedMediaType.getType(), supportedMediaType.getSubtype());
- }
- allSupportedMediaTypes.add(supportedMediaType);
+ if (responseClass != null) {
+ if (messageConverter.canRead(responseClass, null)) {
+ allSupportedMediaTypes
+ .addAll(getSupportedMediaTypes(messageConverter));
}
}
+ else if (messageConverter instanceof GenericHttpMessageConverter) {
+
+ GenericHttpMessageConverter genericMessageConverter =
+ (GenericHttpMessageConverter) messageConverter;
+ if (genericMessageConverter.canRead(responseType, null)) {
+ allSupportedMediaTypes
+ .addAll(getSupportedMediaTypes(messageConverter));
+ }
+ }
+
}
if (!allSupportedMediaTypes.isEmpty()) {
MediaType.sortBySpecificity(allSupportedMediaTypes);
if (logger.isDebugEnabled()) {
- logger.debug("Setting request Accept header to " + allSupportedMediaTypes);
+ logger.debug("Setting request Accept header to " +
+ allSupportedMediaTypes);
}
request.getHeaders().setAccept(allSupportedMediaTypes);
}
}
}
+
+ private List getSupportedMediaTypes(HttpMessageConverter> messageConverter) {
+ List supportedMediaTypes = messageConverter.getSupportedMediaTypes();
+ List result = new ArrayList(supportedMediaTypes.size());
+ for (MediaType supportedMediaType : supportedMediaTypes) {
+ if (supportedMediaType.getCharSet() != null) {
+ supportedMediaType =
+ new MediaType(supportedMediaType.getType(), supportedMediaType.getSubtype());
+ }
+ result.add(supportedMediaType);
+ }
+ return result;
+ }
}
@@ -550,7 +608,7 @@ 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 delegate;
- public ResponseEntityResponseExtractor(Class responseType) {
+ public ResponseEntityResponseExtractor(Type responseType) {
if (responseType != null && !Void.class.equals(responseType)) {
this.delegate = new HttpMessageConverterExtractor(responseType, getMessageConverters(), logger);
} else {
diff --git a/spring-web/src/test/java/org/springframework/http/converter/json/MappingJackson2HttpMessageConverterTests.java b/spring-web/src/test/java/org/springframework/http/converter/json/MappingJackson2HttpMessageConverterTests.java
index b449545f15d..603b6613abe 100644
--- a/spring-web/src/test/java/org/springframework/http/converter/json/MappingJackson2HttpMessageConverterTests.java
+++ b/spring-web/src/test/java/org/springframework/http/converter/json/MappingJackson2HttpMessageConverterTests.java
@@ -16,23 +16,22 @@
package org.springframework.http.converter.json;
-import static org.junit.Assert.assertArrayEquals;
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertTrue;
-
import java.io.IOException;
+import java.lang.reflect.Type;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.List;
+import com.fasterxml.jackson.databind.JavaType;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import static org.junit.Assert.*;
import org.junit.Test;
+
+import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.MediaType;
import org.springframework.http.MockHttpInputMessage;
import org.springframework.http.MockHttpOutputMessage;
-import com.fasterxml.jackson.databind.JavaType;
-import com.fasterxml.jackson.databind.ObjectMapper;
-
/**
* Jackson 2.x converter tests.
*
@@ -52,12 +51,12 @@ public 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> beansList = new ParameterizedTypeReference>() {};
+
+ String body =
+ "[{\"bytes\":\"AQI=\",\"array\":[\"Foo\",\"Bar\"],\"number\":42,\"string\":\"Foo\",\"bool\":true,\"fraction\":42.0}]";
+ MockHttpInputMessage inputMessage = new MockHttpInputMessage(body.getBytes("UTF-8"));
+ inputMessage.getHeaders().setContentType(new MediaType("application", "json"));
+
+ MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();
+ List results = (List) converter.read(beansList.getType(), inputMessage);
+ assertEquals(1, results.size());
+ MyBean result = results.get(0);
+ assertEquals("Foo", result.getString());
+ assertEquals(42, result.getNumber());
+ assertEquals(42F, result.getFraction(), 0F);
+ assertArrayEquals(new String[]{"Foo", "Bar"}, result.getArray());
+ assertTrue(result.isBool());
+ assertArrayEquals(new byte[]{0x1, 0x2}, result.getBytes());
+ }
+
+
@Test
public void prettyPrint() throws Exception {
MockHttpOutputMessage outputMessage = new MockHttpOutputMessage();
diff --git a/spring-web/src/test/java/org/springframework/http/converter/json/MappingJacksonHttpMessageConverterTests.java b/spring-web/src/test/java/org/springframework/http/converter/json/MappingJacksonHttpMessageConverterTests.java
index e5fb9f32905..c3fe567a69c 100644
--- a/spring-web/src/test/java/org/springframework/http/converter/json/MappingJacksonHttpMessageConverterTests.java
+++ b/spring-web/src/test/java/org/springframework/http/converter/json/MappingJacksonHttpMessageConverterTests.java
@@ -16,18 +16,18 @@
package org.springframework.http.converter.json;
-import static org.junit.Assert.assertArrayEquals;
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertTrue;
-
import java.io.IOException;
+import java.lang.reflect.Type;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.List;
import org.codehaus.jackson.map.type.TypeFactory;
import org.codehaus.jackson.type.JavaType;
+import static org.junit.Assert.*;
import org.junit.Test;
+
+import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.MediaType;
import org.springframework.http.MockHttpInputMessage;
import org.springframework.http.MockHttpOutputMessage;
@@ -49,12 +49,12 @@ 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> beansList = new ParameterizedTypeReference>() {};
+
+ String body =
+ "[{\"bytes\":\"AQI=\",\"array\":[\"Foo\",\"Bar\"],\"number\":42,\"string\":\"Foo\",\"bool\":true,\"fraction\":42.0}]";
+ MockHttpInputMessage inputMessage = new MockHttpInputMessage(body.getBytes("UTF-8"));
+ inputMessage.getHeaders().setContentType(new MediaType("application", "json"));
+
+ MappingJacksonHttpMessageConverter converter = new MappingJacksonHttpMessageConverter();
+ List results = (List) converter.read(beansList.getType(), inputMessage);
+ assertEquals(1, results.size());
+ MyBean result = results.get(0);
+ assertEquals("Foo", result.getString());
+ assertEquals(42, result.getNumber());
+ assertEquals(42F, result.getFraction(), 0F);
+ assertArrayEquals(new String[]{"Foo", "Bar"}, result.getArray());
+ assertTrue(result.isBool());
+ assertArrayEquals(new byte[]{0x1, 0x2}, result.getBytes());
+ }
+
@Test
public void prettyPrint() throws Exception {
MockHttpOutputMessage outputMessage = new MockHttpOutputMessage();
diff --git a/spring-web/src/test/java/org/springframework/http/converter/xml/Jaxb2CollectionHttpMessageConverterTests.java b/spring-web/src/test/java/org/springframework/http/converter/xml/Jaxb2CollectionHttpMessageConverterTests.java
new file mode 100644
index 00000000000..44715ac6cbe
--- /dev/null
+++ b/spring-web/src/test/java/org/springframework/http/converter/xml/Jaxb2CollectionHttpMessageConverterTests.java
@@ -0,0 +1,189 @@
+/*
+ * Copyright 2002-2012 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.http.converter.xml;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import java.lang.reflect.Type;
+import java.util.Collection;
+import java.util.List;
+import java.util.Set;
+
+import javax.xml.bind.annotation.XmlAttribute;
+import javax.xml.bind.annotation.XmlElement;
+import javax.xml.bind.annotation.XmlRootElement;
+import javax.xml.bind.annotation.XmlType;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.springframework.core.ParameterizedTypeReference;
+import org.springframework.http.MockHttpInputMessage;
+
+/**
+ * Test fixture for {@link Jaxb2CollectionHttpMessageConverter}.
+ *
+ * @author Arjen Poutsma
+ */
+public class Jaxb2CollectionHttpMessageConverterTests {
+
+ private Jaxb2CollectionHttpMessageConverter> converter;
+
+ private Type rootElementListType;
+
+ private Type rootElementSetType;
+
+ private Type typeListType;
+
+ private Type typeSetType;
+
+
+ @Before
+ public void setUp() {
+ converter = new Jaxb2CollectionHttpMessageConverter>();
+ rootElementListType = new ParameterizedTypeReference>() {}.getType();
+ rootElementSetType = new ParameterizedTypeReference>() {}.getType();
+ typeListType = new ParameterizedTypeReference>() {}.getType();
+ typeSetType = new ParameterizedTypeReference>() {}.getType();
+ }
+
+ @Test
+ public void canRead() throws Exception {
+ assertTrue(converter.canRead(rootElementListType, null));
+ assertTrue(converter.canRead(rootElementSetType, null));
+ assertTrue(converter.canRead(typeSetType, null));
+ }
+
+ @Test
+ @SuppressWarnings("unchecked")
+ public void readXmlRootElementList() throws Exception {
+ String content = "
";
+ MockHttpInputMessage inputMessage = new MockHttpInputMessage(content.getBytes("UTF-8"));
+
+ List result = (List) converter.read(rootElementListType, inputMessage);
+
+ assertEquals("Invalid result", 2, result.size());
+ assertEquals("Invalid result", "1", result.get(0).type.s);
+ assertEquals("Invalid result", "2", result.get(1).type.s);
+ }
+
+ @Test
+ @SuppressWarnings("unchecked")
+ public void readXmlRootElementSet() throws Exception {
+ String content = " ";
+ MockHttpInputMessage inputMessage = new MockHttpInputMessage(content.getBytes("UTF-8"));
+
+ Set result = (Set) converter.read(rootElementSetType, inputMessage);
+
+ assertEquals("Invalid result", 2, result.size());
+ assertTrue("Invalid result", result.contains(new RootElement("1")));
+ assertTrue("Invalid result", result.contains(new RootElement("2")));
+ }
+
+ @Test
+ @SuppressWarnings("unchecked")
+ public void readXmlTypeList() throws Exception {
+ String content = "
";
+ MockHttpInputMessage inputMessage = new MockHttpInputMessage(content.getBytes("UTF-8"));
+
+ List result = (List) converter.read(typeListType, inputMessage);
+
+ assertEquals("Invalid result", 2, result.size());
+ assertEquals("Invalid result", "1", result.get(0).s);
+ assertEquals("Invalid result", "2", result.get(1).s);
+ }
+
+ @Test
+ @SuppressWarnings("unchecked")
+ public void readXmlTypeSet() throws Exception {
+ String content = " ";
+ MockHttpInputMessage inputMessage = new MockHttpInputMessage(content.getBytes("UTF-8"));
+
+ Set result = (Set) converter.read(typeSetType, inputMessage);
+
+ assertEquals("Invalid result", 2, result.size());
+ assertTrue("Invalid result", result.contains(new TestType("1")));
+ assertTrue("Invalid result", result.contains(new TestType("2")));
+ }
+
+
+ @XmlRootElement
+ public static class RootElement {
+
+ public RootElement() {
+ }
+
+ public RootElement(String s) {
+ this.type = new TestType(s);
+ }
+
+ @XmlElement
+ public TestType type = new TestType();
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o instanceof RootElement) {
+ RootElement other = (RootElement) o;
+ return this.type.equals(other.type);
+ }
+ return false;
+ }
+
+ @Override
+ public int hashCode() {
+ return type.hashCode();
+ }
+ }
+
+ @XmlType
+ public static class TestType {
+
+ public TestType() {
+ }
+
+ public TestType(String s) {
+ this.s = s;
+ }
+
+ @XmlAttribute
+ public String s = "Hello World";
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o instanceof TestType) {
+ TestType other = (TestType) o;
+ return this.s.equals(other.s);
+ }
+ return false;
+ }
+
+ @Override
+ public int hashCode() {
+ return s.hashCode();
+ }
+
+
+
+ }
+
+}
diff --git a/spring-web/src/test/java/org/springframework/web/client/HttpMessageConverterExtractorTests.java b/spring-web/src/test/java/org/springframework/web/client/HttpMessageConverterExtractorTests.java
new file mode 100644
index 00000000000..a8e9a418e66
--- /dev/null
+++ b/spring-web/src/test/java/org/springframework/web/client/HttpMessageConverterExtractorTests.java
@@ -0,0 +1,196 @@
+/*
+ * Copyright 2002-2012 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.web.client;
+
+import static org.easymock.EasyMock.createMock;
+import static org.easymock.EasyMock.expect;
+import static org.easymock.EasyMock.replay;
+import static org.easymock.EasyMock.verify;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.fail;
+
+import java.io.IOException;
+import java.lang.reflect.Type;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.springframework.core.ParameterizedTypeReference;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.MediaType;
+import org.springframework.http.client.ClientHttpResponse;
+import org.springframework.http.converter.GenericHttpMessageConverter;
+import org.springframework.http.converter.HttpMessageConverter;
+
+/**
+ * Test fixture for {@link HttpMessageConverter}.
+ *
+ * @author Arjen Poutsma
+ */
+public class HttpMessageConverterExtractorTests {
+
+ private HttpMessageConverterExtractor extractor;
+
+ private ClientHttpResponse response;
+
+ @Before
+ public void createMocks() {
+ response = createMock(ClientHttpResponse.class);
+ }
+
+ @Test
+ public void noContent() throws IOException {
+ HttpMessageConverter> converter = createMock(HttpMessageConverter.class);
+
+ extractor = new HttpMessageConverterExtractor(String.class, createConverterList(converter));
+
+ expect(response.getStatusCode()).andReturn(HttpStatus.NO_CONTENT);
+
+ replay(response, converter);
+ Object result = extractor.extractData(response);
+
+ assertNull(result);
+ verify(response, converter);
+ }
+
+ @Test
+ public void notModified() throws IOException {
+ HttpMessageConverter> converter = createMock(HttpMessageConverter.class);
+
+ extractor = new HttpMessageConverterExtractor(String.class, createConverterList(converter));
+
+ expect(response.getStatusCode()).andReturn(HttpStatus.NOT_MODIFIED);
+
+ replay(response, converter);
+ Object result = extractor.extractData(response);
+
+ assertNull(result);
+ verify(response, converter);
+ }
+
+ @Test
+ public void zeroContentLength() throws IOException {
+ HttpMessageConverter> converter = createMock(HttpMessageConverter.class);
+ HttpHeaders responseHeaders = new HttpHeaders();
+ responseHeaders.setContentLength(0);
+
+ extractor = new HttpMessageConverterExtractor(String.class, createConverterList(converter));
+
+ expect(response.getStatusCode()).andReturn(HttpStatus.OK);
+ expect(response.getHeaders()).andReturn(responseHeaders);
+
+ replay(response, converter);
+ Object result = extractor.extractData(response);
+
+ assertNull(result);
+ verify(response, converter);
+ }
+
+ @Test
+ @SuppressWarnings("unchecked")
+ public void normal() throws IOException {
+ HttpMessageConverter converter = createMock(HttpMessageConverter.class);
+ List> converters = new ArrayList>();
+ converters.add(converter);
+
+ HttpHeaders responseHeaders = new HttpHeaders();
+ MediaType contentType = MediaType.TEXT_PLAIN;
+ responseHeaders.setContentType(contentType);
+ String expected = "Foo";
+
+ extractor = new HttpMessageConverterExtractor(String.class, converters);
+
+ expect(response.getStatusCode()).andReturn(HttpStatus.OK);
+ expect(response.getHeaders()).andReturn(responseHeaders).times(2);
+ expect(converter.canRead(String.class, contentType)).andReturn(true);
+ expect(converter.read(String.class, response)).andReturn(expected);
+
+ replay(response, converter);
+ Object result = extractor.extractData(response);
+
+ assertEquals(expected, result);
+ verify(response, converter);
+ }
+
+ @Test
+ @SuppressWarnings("unchecked")
+ public void cannotRead() throws IOException {
+ HttpMessageConverter converter = createMock(HttpMessageConverter.class);
+ List> converters = new ArrayList>();
+ converters.add(converter);
+
+ HttpHeaders responseHeaders = new HttpHeaders();
+ MediaType contentType = MediaType.TEXT_PLAIN;
+ responseHeaders.setContentType(contentType);
+
+ extractor = new HttpMessageConverterExtractor(String.class, converters);
+
+ expect(response.getStatusCode()).andReturn(HttpStatus.OK);
+ expect(response.getHeaders()).andReturn(responseHeaders).times(2);
+ expect(converter.canRead(String.class, contentType)).andReturn(false);
+
+ replay(response, converter);
+ try {
+ extractor.extractData(response);
+ fail("RestClientException expected");
+ }
+ catch (RestClientException expected) {
+ // expected
+ }
+
+ verify(response, converter);
+ }
+
+ @Test
+ @SuppressWarnings("unchecked")
+ public void generics() throws IOException {
+ GenericHttpMessageConverter converter = createMock(GenericHttpMessageConverter.class);
+ List> converters = createConverterList(converter);
+
+ HttpHeaders responseHeaders = new HttpHeaders();
+ MediaType contentType = MediaType.TEXT_PLAIN;
+ responseHeaders.setContentType(contentType);
+ String expected = "Foo";
+
+ ParameterizedTypeReference> reference = new ParameterizedTypeReference>() {};
+ Type type = reference.getType();
+
+ extractor = new HttpMessageConverterExtractor>(type, converters);
+
+ expect(response.getStatusCode()).andReturn(HttpStatus.OK);
+ expect(response.getHeaders()).andReturn(responseHeaders).times(2);
+ expect(converter.canRead(type, contentType)).andReturn(true);
+ expect(converter.read(type, response)).andReturn(expected);
+
+ replay(response, converter);
+ Object result = extractor.extractData(response);
+
+ assertEquals(expected, result);
+ verify(response, converter);
+ }
+
+ private List> createConverterList(HttpMessageConverter converter) {
+ List> converters = new ArrayList>(1);
+ converters.add(converter);
+ return converters;
+ }
+
+
+}
diff --git a/spring-web/src/test/java/org/springframework/web/client/RestTemplateTests.java b/spring-web/src/test/java/org/springframework/web/client/RestTemplateTests.java
index ff58437bfeb..d12c5cf8e39 100644
--- a/spring-web/src/test/java/org/springframework/web/client/RestTemplateTests.java
+++ b/spring-web/src/test/java/org/springframework/web/client/RestTemplateTests.java
@@ -1,11 +1,11 @@
/*
- * Copyright 2002-2011 the original author or authors.
+ * Copyright 2002-2012 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
@@ -21,12 +21,16 @@ import java.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 requestEntity = new HttpEntity(body, entityHeaders);
ResponseEntity result = template.exchange("http://example.com", HttpMethod.POST, requestEntity, Integer.class);
assertEquals("Invalid POST result", expected, result.getBody());
- assertEquals("Invalid Content-Type", textPlain, result.getHeaders().getContentType());
- assertEquals("Invalid Accept header", textPlain.toString(), requestHeaders.getFirst("Accept"));
+ assertEquals("Invalid Content-Type", MediaType.TEXT_PLAIN, result.getHeaders().getContentType());
+ assertEquals("Invalid Accept header", MediaType.TEXT_PLAIN_VALUE, requestHeaders.getFirst("Accept"));
assertEquals("Invalid custom header", "MyValue", requestHeaders.getFirst("MyHeader"));
assertEquals("Invalid status code", HttpStatus.OK, result.getStatusCode());
verifyMocks();
}
+ @Test
+ public void exchangeParameterizedType() throws Exception {
+ GenericHttpMessageConverter converter = createMock(GenericHttpMessageConverter.class);
+ template.setMessageConverters(Collections.>singletonList(converter));
+
+ ParameterizedTypeReference> intList = new ParameterizedTypeReference>() {};
+ expect(converter.canRead(intList.getType(), null)).andReturn(true);
+ expect(converter.getSupportedMediaTypes()).andReturn(Collections.singletonList(MediaType.TEXT_PLAIN));
+ expect(requestFactory.createRequest(new URI("http://example.com"), HttpMethod.POST)).andReturn(this.request);
+ HttpHeaders requestHeaders = new HttpHeaders();
+ expect(this.request.getHeaders()).andReturn(requestHeaders).times(2);
+ expect(converter.canWrite(String.class, null)).andReturn(true);
+ String requestBody = "Hello World";
+ converter.write(requestBody, null, this.request);
+ expect(this.request.execute()).andReturn(response);
+ expect(errorHandler.hasError(response)).andReturn(false);
+ HttpHeaders responseHeaders = new HttpHeaders();
+ responseHeaders.setContentType(MediaType.TEXT_PLAIN);
+ responseHeaders.setContentLength(10);
+ expect(response.getStatusCode()).andReturn(HttpStatus.OK);
+ expect(response.getHeaders()).andReturn(responseHeaders).times(3);
+ List expected = Collections.singletonList(42);
+ expect(converter.canRead(intList.getType(), MediaType.TEXT_PLAIN)).andReturn(true);
+ expect(converter.read(intList.getType(), response)).andReturn(expected);
+ expect(response.getStatusCode()).andReturn(HttpStatus.OK);
+ response.close();
+
+ replay(requestFactory, request, response, errorHandler, converter);
+
+ HttpHeaders entityHeaders = new HttpHeaders();
+ entityHeaders.set("MyHeader", "MyValue");
+ HttpEntity requestEntity = new HttpEntity(requestBody, entityHeaders);
+ ResponseEntity> result = template.exchange("http://example.com", HttpMethod.POST, requestEntity, intList);
+ assertEquals("Invalid POST result", expected, result.getBody());
+ assertEquals("Invalid Content-Type", MediaType.TEXT_PLAIN, result.getHeaders().getContentType());
+ assertEquals("Invalid Accept header", MediaType.TEXT_PLAIN_VALUE, requestHeaders.getFirst("Accept"));
+ assertEquals("Invalid custom header", "MyValue", requestHeaders.getFirst("MyHeader"));
+ assertEquals("Invalid status code", HttpStatus.OK, result.getStatusCode());
+
+ verify(requestFactory, request, response, errorHandler, converter);
+ }
+
private void replayMocks() {
replay(requestFactory, request, response, errorHandler, converter);