From 31a5434ea433bdec2283797bf9415c02bb2f41c1 Mon Sep 17 00:00:00 2001 From: Sebastien Deleuze Date: Mon, 22 Jun 2015 13:52:17 +0200 Subject: [PATCH] Make @ResponseBody method return type available for message converters This commit adds canWrite() and write() methods to the GenericHttpMessageConverter interface. These are type aware variants of the methods available in HttpMessageConverter, in order to keep parametrized type information when serializing objects. AbstractMessageConverterMethodProcessor now calls those type aware methods when the message converter implements GenericHttpMessageConverter. AbstractJackson2HttpMessageConverter and GsonHttpMessageConverter uses these new methods to make @ResponseBody method return type available for type resolution instead of just letting the JSON serializer trying to guess the type to use from the object to serialize. Issue: SPR-12811 --- .../AbstractGenericHttpMessageConverter.java | 118 ++++++++++++++++++ .../AbstractHttpMessageConverter.java | 55 ++++---- .../GenericHttpMessageConverter.java | 37 +++++- .../AbstractJackson2HttpMessageConverter.java | 30 +++-- .../json/GsonHttpMessageConverter.java | 17 ++- .../Jaxb2CollectionHttpMessageConverter.java | 19 ++- .../json/GsonHttpMessageConverterTests.java | 6 +- ...pingJackson2HttpMessageConverterTests.java | 14 +-- ...gJackson2XmlHttpMessageConverterTests.java | 6 +- ...stractMessageConverterMethodProcessor.java | 18 ++- .../HttpEntityMethodProcessorTests.java | 82 +++++++++++- ...questResponseBodyMethodProcessorTests.java | 116 +++++++++++++---- 12 files changed, 442 insertions(+), 76 deletions(-) create mode 100644 spring-web/src/main/java/org/springframework/http/converter/AbstractGenericHttpMessageConverter.java diff --git a/spring-web/src/main/java/org/springframework/http/converter/AbstractGenericHttpMessageConverter.java b/spring-web/src/main/java/org/springframework/http/converter/AbstractGenericHttpMessageConverter.java new file mode 100644 index 00000000000..ec0e8e1b81a --- /dev/null +++ b/spring-web/src/main/java/org/springframework/http/converter/AbstractGenericHttpMessageConverter.java @@ -0,0 +1,118 @@ +/* + * Copyright 2002-2015 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.io.OutputStream; +import java.lang.reflect.Type; + +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpOutputMessage; +import org.springframework.http.MediaType; +import org.springframework.http.StreamingHttpOutputMessage; + +/** + * Abstract base class for most {@link GenericHttpMessageConverter} implementations. + * + * @author Sebastien Deleuze + * @since 4.2 + */ +public abstract class AbstractGenericHttpMessageConverter extends AbstractHttpMessageConverter + implements GenericHttpMessageConverter { + + /** + * Construct an {@code AbstractGenericHttpMessageConverter} with no supported media types. + * @see #setSupportedMediaTypes + */ + protected AbstractGenericHttpMessageConverter() { + } + + /** + * Construct an {@code AbstractGenericHttpMessageConverter} with one supported media type. + * @param supportedMediaType the supported media type + */ + protected AbstractGenericHttpMessageConverter(MediaType supportedMediaType) { + super(supportedMediaType); + } + + /** + * Construct an {@code AbstractGenericHttpMessageConverter} with multiple supported media type. + * @param supportedMediaTypes the supported media types + */ + protected AbstractGenericHttpMessageConverter(MediaType... supportedMediaTypes) { + super(supportedMediaTypes); + } + + @Override + public boolean canWrite(Class contextClass, MediaType mediaType) { + return canWrite(null, contextClass, mediaType); + } + + /** + * This implementation sets the default headers by calling {@link #addDefaultHeaders}, + * and then calls {@link #writeInternal}. + */ + public final void write(final T t, final Type type, MediaType contentType, HttpOutputMessage outputMessage) + throws IOException, HttpMessageNotWritableException { + + final HttpHeaders headers = outputMessage.getHeaders(); + addDefaultHeaders(headers, t, contentType); + + if (outputMessage instanceof StreamingHttpOutputMessage) { + StreamingHttpOutputMessage streamingOutputMessage = + (StreamingHttpOutputMessage) outputMessage; + streamingOutputMessage.setBody(new StreamingHttpOutputMessage.Body() { + @Override + public void writeTo(final OutputStream outputStream) throws IOException { + writeInternal(t, type, new HttpOutputMessage() { + @Override + public OutputStream getBody() throws IOException { + return outputStream; + } + @Override + public HttpHeaders getHeaders() { + return headers; + } + }); + } + }); + } + else { + writeInternal(t, type, outputMessage); + outputMessage.getBody().flush(); + } + } + + + @Override + protected void writeInternal(T t, HttpOutputMessage outputMessage) + throws IOException, HttpMessageNotWritableException { + writeInternal(t, null, outputMessage); + } + + /** + * Abstract template method that writes the actual body. Invoked from {@link #write}. + * @param t the object to write to the output message + * @param type the type of object to write, can be {@code null} if not specified. + * @param outputMessage the HTTP output message to write to + * @throws IOException in case of I/O errors + * @throws HttpMessageNotWritableException in case of conversion errors + */ + protected abstract void writeInternal(T t, Type type, HttpOutputMessage outputMessage) + throws IOException, HttpMessageNotWritableException; + +} diff --git a/spring-web/src/main/java/org/springframework/http/converter/AbstractHttpMessageConverter.java b/spring-web/src/main/java/org/springframework/http/converter/AbstractHttpMessageConverter.java index 1190341e9df..311482d119a 100644 --- a/spring-web/src/main/java/org/springframework/http/converter/AbstractHttpMessageConverter.java +++ b/spring-web/src/main/java/org/springframework/http/converter/AbstractHttpMessageConverter.java @@ -160,34 +160,15 @@ public abstract class AbstractHttpMessageConverter implements HttpMessageConv } /** - * This implementation delegates to {@link #getDefaultContentType(Object)} if a content - * type was not provided, calls {@link #getContentLength}, and sets the corresponding headers - * on the output message. It then calls {@link #writeInternal}. + * This implementation sets the default headers by calling {@link #addDefaultHeaders}, + * and then calls {@link #writeInternal}. */ @Override public final void write(final T t, MediaType contentType, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException { final HttpHeaders headers = outputMessage.getHeaders(); - if (headers.getContentType() == null) { - MediaType contentTypeToUse = contentType; - if (contentType == null || contentType.isWildcardType() || contentType.isWildcardSubtype()) { - contentTypeToUse = getDefaultContentType(t); - } - else if (MediaType.APPLICATION_OCTET_STREAM.equals(contentType)) { - MediaType type = getDefaultContentType(t); - contentTypeToUse = (type != null ? type : contentTypeToUse); - } - if (contentTypeToUse != null) { - headers.setContentType(contentTypeToUse); - } - } - if (headers.getContentLength() == -1) { - Long contentLength = getContentLength(t, headers.getContentType()); - if (contentLength != null) { - headers.setContentLength(contentLength); - } - } + addDefaultHeaders(headers, t, contentType); if (outputMessage instanceof StreamingHttpOutputMessage) { StreamingHttpOutputMessage streamingOutputMessage = @@ -214,6 +195,36 @@ public abstract class AbstractHttpMessageConverter implements HttpMessageConv } } + /** + * Add default headers to the output message. + *

This implementation delegates to {@link #getDefaultContentType(Object)} if a content + * type was not provided, calls {@link #getContentLength}, and sets the corresponding headers + * @since 4.2 + */ + protected void addDefaultHeaders(final HttpHeaders headers, final T t, MediaType contentType) + throws IOException{ + + if (headers.getContentType() == null) { + MediaType contentTypeToUse = contentType; + if (contentType == null || contentType.isWildcardType() || contentType.isWildcardSubtype()) { + contentTypeToUse = getDefaultContentType(t); + } + else if (MediaType.APPLICATION_OCTET_STREAM.equals(contentType)) { + MediaType mediaType = getDefaultContentType(t); + contentTypeToUse = (mediaType != null ? mediaType : contentTypeToUse); + } + if (contentTypeToUse != null) { + headers.setContentType(contentTypeToUse); + } + } + if (headers.getContentLength() == -1) { + Long contentLength = getContentLength(t, headers.getContentType()); + if (contentLength != null) { + headers.setContentLength(contentLength); + } + } + } + /** * Returns the default content type for the given type. Called when {@link #write} * is invoked without a specified content type parameter. 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 index 27600b71303..56de4dac71d 100644 --- a/spring-web/src/main/java/org/springframework/http/converter/GenericHttpMessageConverter.java +++ b/spring-web/src/main/java/org/springframework/http/converter/GenericHttpMessageConverter.java @@ -20,14 +20,17 @@ import java.io.IOException; import java.lang.reflect.Type; import org.springframework.http.HttpInputMessage; +import org.springframework.http.HttpOutputMessage; 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. + * A specialization of {@link HttpMessageConverter} that can convert an HTTP request + * into a target object of a specified generic type and a source object of a specified + * generic type into an HTTP response. * * @author Arjen Poutsma * @author Rossen Stoyanchev + * @author Sebastien Deleuze * @since 3.2 * @see org.springframework.core.ParameterizedTypeReference */ @@ -59,4 +62,34 @@ public interface GenericHttpMessageConverter extends HttpMessageConverter T read(Type type, Class contextClass, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException; + /** + * Indicates whether the given class can be written by this converter. + * @param type the type to test for writability, can be {@code null} if not specified. + * @param contextClass the class to test for writability + * @param mediaType the media type to write, can be {@code null} if not specified. + * Typically the value of an {@code Accept} header. + * @return {@code true} if writable; {@code false} otherwise + * @since 4.2 + */ + boolean canWrite(Type type, Class contextClass, MediaType mediaType); + + /** + * Write an given object to the given output message. + * @param t the object to write to the output message. The type of this object must have previously been + * passed to the {@link #canWrite canWrite} method of this interface, which must have returned {@code true}. + * @param type the type of object to write. This type must have previously + * been passed to the {@link #canWrite canWrite} method of this interface, + * which must have returned {@code true}. Can be {@code null} if not specified. + * @param contentType the content type to use when writing. May be {@code null} to indicate that the + * default content type of the converter must be used. If not {@code null}, this media type must have + * previously been passed to the {@link #canWrite canWrite} method of this interface, which must have + * returned {@code true}. + * @param outputMessage the message to write to + * @throws IOException in case of I/O errors + * @throws HttpMessageNotWritableException in case of conversion errors + * @since 4.2 + */ + void write(T t, Type type, MediaType contentType, HttpOutputMessage outputMessage) + throws IOException, HttpMessageNotWritableException; + } diff --git a/spring-web/src/main/java/org/springframework/http/converter/json/AbstractJackson2HttpMessageConverter.java b/spring-web/src/main/java/org/springframework/http/converter/json/AbstractJackson2HttpMessageConverter.java index 47d460dca5f..0086dfd5a81 100644 --- a/spring-web/src/main/java/org/springframework/http/converter/json/AbstractJackson2HttpMessageConverter.java +++ b/spring-web/src/main/java/org/springframework/http/converter/json/AbstractJackson2HttpMessageConverter.java @@ -27,19 +27,21 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.util.DefaultPrettyPrinter; import com.fasterxml.jackson.databind.JavaType; import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.ObjectWriter; import com.fasterxml.jackson.databind.SerializationFeature; import com.fasterxml.jackson.databind.ser.FilterProvider; 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.AbstractGenericHttpMessageConverter; import org.springframework.http.converter.GenericHttpMessageConverter; import org.springframework.http.converter.HttpMessageConverter; import org.springframework.http.converter.HttpMessageNotReadableException; import org.springframework.http.converter.HttpMessageNotWritableException; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; +import org.springframework.util.TypeUtils; /** * Abstract base class for Jackson based and content type independent @@ -54,7 +56,7 @@ import org.springframework.util.ClassUtils; * @author Sebastien Deleuze * @since 4.1 */ -public abstract class AbstractJackson2HttpMessageConverter extends AbstractHttpMessageConverter +public abstract class AbstractJackson2HttpMessageConverter extends AbstractGenericHttpMessageConverter implements GenericHttpMessageConverter { public static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8"); @@ -158,7 +160,7 @@ public abstract class AbstractJackson2HttpMessageConverter extends AbstractHttpM } @Override - public boolean canWrite(Class clazz, MediaType mediaType) { + public boolean canWrite(Type type, Class clazz, MediaType mediaType) { if (!jackson23Available || !logger.isWarnEnabled()) { return (this.objectMapper.canSerialize(clazz) && canWrite(mediaType)); } @@ -218,31 +220,43 @@ public abstract class AbstractJackson2HttpMessageConverter extends AbstractHttpM } @Override - protected void writeInternal(Object object, HttpOutputMessage outputMessage) + @SuppressWarnings("deprecation") + protected void writeInternal(Object object, Type type, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException { JsonEncoding encoding = getJsonEncoding(outputMessage.getHeaders().getContentType()); JsonGenerator generator = this.objectMapper.getFactory().createGenerator(outputMessage.getBody(), encoding); try { writePrefix(generator, object); + Class serializationView = null; FilterProvider filters = null; Object value = object; - if (value instanceof MappingJacksonValue) { + JavaType javaType = null; + if (type != null) { + javaType = getJavaType(type, null); + } + if (object instanceof MappingJacksonValue) { MappingJacksonValue container = (MappingJacksonValue) object; value = container.getValue(); serializationView = container.getSerializationView(); filters = container.getFilters(); } + ObjectWriter objectWriter; if (serializationView != null) { - this.objectMapper.writerWithView(serializationView).writeValue(generator, value); + objectWriter = this.objectMapper.writerWithView(serializationView); } else if (filters != null) { - this.objectMapper.writer(filters).writeValue(generator, value); + objectWriter = this.objectMapper.writer(filters); } else { - this.objectMapper.writeValue(generator, value); + objectWriter = this.objectMapper.writer(); } + if (javaType != null && value != null && TypeUtils.isAssignable(type, value.getClass())) { + objectWriter = objectWriter.withType(javaType); + } + objectWriter.writeValue(generator, value); + writeSuffix(generator, object); generator.flush(); diff --git a/spring-web/src/main/java/org/springframework/http/converter/json/GsonHttpMessageConverter.java b/spring-web/src/main/java/org/springframework/http/converter/json/GsonHttpMessageConverter.java index f6b37361fa4..e69c29ec52a 100644 --- a/spring-web/src/main/java/org/springframework/http/converter/json/GsonHttpMessageConverter.java +++ b/spring-web/src/main/java/org/springframework/http/converter/json/GsonHttpMessageConverter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2014 the original author or authors. + * Copyright 2002-2015 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. @@ -32,7 +32,7 @@ import org.springframework.http.HttpHeaders; 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.AbstractGenericHttpMessageConverter; import org.springframework.http.converter.GenericHttpMessageConverter; import org.springframework.http.converter.HttpMessageNotReadableException; import org.springframework.http.converter.HttpMessageNotWritableException; @@ -54,7 +54,7 @@ import org.springframework.util.Assert; * @see #setGson * @see #setSupportedMediaTypes */ -public class GsonHttpMessageConverter extends AbstractHttpMessageConverter +public class GsonHttpMessageConverter extends AbstractGenericHttpMessageConverter implements GenericHttpMessageConverter { public static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8"); @@ -125,7 +125,7 @@ public class GsonHttpMessageConverter extends AbstractHttpMessageConverter clazz, MediaType mediaType) { + public boolean canWrite(Type type, Class clazz, MediaType mediaType) { return canWrite(mediaType); } @@ -191,7 +191,7 @@ public class GsonHttpMessageConverter extends AbstractHttpMessageConverter return false; } + /** + * Always returns {@code false} since Jaxb2CollectionHttpMessageConverter + * does not convert collections to XML. + */ + @Override + public boolean canWrite(Type type, Class contextClass, MediaType mediaType) { + return false; + } + @Override protected boolean supports(Class clazz) { // should not be called, since we override canRead/Write @@ -216,6 +227,12 @@ public class Jaxb2CollectionHttpMessageConverter return event; } + @Override + public void write(T t, Type type, MediaType contentType, HttpOutputMessage outputMessage) + throws IOException, HttpMessageNotWritableException { + throw new UnsupportedOperationException(); + } + @Override protected void writeToResult(T t, HttpHeaders headers, Result result) throws IOException { throw new UnsupportedOperationException(); diff --git a/spring-web/src/test/java/org/springframework/http/converter/json/GsonHttpMessageConverterTests.java b/spring-web/src/test/java/org/springframework/http/converter/json/GsonHttpMessageConverterTests.java index fce8cb3ab70..c71f750b33a 100644 --- a/spring-web/src/test/java/org/springframework/http/converter/json/GsonHttpMessageConverterTests.java +++ b/spring-web/src/test/java/org/springframework/http/converter/json/GsonHttpMessageConverterTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2014 the original author or authors. + * Copyright 2002-2015 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. @@ -209,7 +209,7 @@ public class GsonHttpMessageConverterTests { public void prefixJson() throws Exception { MockHttpOutputMessage outputMessage = new MockHttpOutputMessage(); this.converter.setPrefixJson(true); - this.converter.writeInternal("foo", outputMessage); + this.converter.writeInternal("foo", null, outputMessage); assertEquals(")]}', \"foo\"", outputMessage.getBodyAsString(UTF8)); } @@ -217,7 +217,7 @@ public class GsonHttpMessageConverterTests { public void prefixJsonCustom() throws Exception { MockHttpOutputMessage outputMessage = new MockHttpOutputMessage(); this.converter.setJsonPrefix(")))"); - this.converter.writeInternal("foo", outputMessage); + this.converter.writeInternal("foo", null, outputMessage); assertEquals(")))\"foo\"", outputMessage.getBodyAsString(UTF8)); } 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 aaa9fa0f786..59aff6cf776 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 @@ -221,7 +221,7 @@ public class MappingJackson2HttpMessageConverterTests { bean.setName("Jason"); this.converter.setPrettyPrint(true); - this.converter.writeInternal(bean, outputMessage); + this.converter.writeInternal(bean, null, outputMessage); String result = outputMessage.getBodyAsString(Charset.forName("UTF-8")); assertEquals("{" + NEWLINE_SYSTEM_PROPERTY + " \"name\" : \"Jason\"" + NEWLINE_SYSTEM_PROPERTY + "}", result); @@ -231,7 +231,7 @@ public class MappingJackson2HttpMessageConverterTests { public void prefixJson() throws Exception { MockHttpOutputMessage outputMessage = new MockHttpOutputMessage(); this.converter.setPrefixJson(true); - this.converter.writeInternal("foo", outputMessage); + this.converter.writeInternal("foo", null, outputMessage); assertEquals(")]}', \"foo\"", outputMessage.getBodyAsString(Charset.forName("UTF-8"))); } @@ -240,7 +240,7 @@ public class MappingJackson2HttpMessageConverterTests { public void prefixJsonCustom() throws Exception { MockHttpOutputMessage outputMessage = new MockHttpOutputMessage(); this.converter.setJsonPrefix(")))"); - this.converter.writeInternal("foo", outputMessage); + this.converter.writeInternal("foo", null, outputMessage); assertEquals(")))\"foo\"", outputMessage.getBodyAsString(Charset.forName("UTF-8"))); } @@ -255,7 +255,7 @@ public class MappingJackson2HttpMessageConverterTests { MappingJacksonValue jacksonValue = new MappingJacksonValue(bean); jacksonValue.setSerializationView(MyJacksonView1.class); - this.converter.writeInternal(jacksonValue, outputMessage); + this.converter.writeInternal(jacksonValue, null, outputMessage); String result = outputMessage.getBodyAsString(Charset.forName("UTF-8")); assertThat(result, containsString("\"withView1\":\"with\"")); @@ -274,7 +274,7 @@ public class MappingJackson2HttpMessageConverterTests { FilterProvider filters = new SimpleFilterProvider().addFilter("myJacksonFilter", SimpleBeanPropertyFilter.serializeAllExcept("property2")); jacksonValue.setFilters(filters); - this.converter.writeInternal(jacksonValue, outputMessage); + this.converter.writeInternal(jacksonValue, null, outputMessage); String result = outputMessage.getBodyAsString(Charset.forName("UTF-8")); assertThat(result, containsString("\"property1\":\"value\"")); @@ -288,7 +288,7 @@ public class MappingJackson2HttpMessageConverterTests { jacksonValue.setJsonpFunction("callback"); MockHttpOutputMessage outputMessage = new MockHttpOutputMessage(); - this.converter.writeInternal(jacksonValue, outputMessage); + this.converter.writeInternal(jacksonValue, null, outputMessage); assertEquals("callback(\"foo\");", outputMessage.getBodyAsString(Charset.forName("UTF-8"))); } @@ -304,7 +304,7 @@ public class MappingJackson2HttpMessageConverterTests { MappingJacksonValue jacksonValue = new MappingJacksonValue(bean); jacksonValue.setSerializationView(MyJacksonView1.class); jacksonValue.setJsonpFunction("callback"); - this.converter.writeInternal(jacksonValue, outputMessage); + this.converter.writeInternal(jacksonValue, null, outputMessage); String result = outputMessage.getBodyAsString(Charset.forName("UTF-8")); assertThat(result, startsWith("callback(")); diff --git a/spring-web/src/test/java/org/springframework/http/converter/xml/MappingJackson2XmlHttpMessageConverterTests.java b/spring-web/src/test/java/org/springframework/http/converter/xml/MappingJackson2XmlHttpMessageConverterTests.java index e561f585e90..6b3d9d7c1a2 100644 --- a/spring-web/src/test/java/org/springframework/http/converter/xml/MappingJackson2XmlHttpMessageConverterTests.java +++ b/spring-web/src/test/java/org/springframework/http/converter/xml/MappingJackson2XmlHttpMessageConverterTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2014 the original author or authors. + * Copyright 2002-2015 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. @@ -29,8 +29,8 @@ import org.springframework.http.HttpOutputMessage; import org.springframework.http.MediaType; import org.springframework.http.MockHttpInputMessage; import org.springframework.http.MockHttpOutputMessage; +import org.springframework.http.converter.AbstractHttpMessageConverter; import org.springframework.http.converter.HttpMessageNotReadableException; -import org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter; import org.springframework.http.converter.json.MappingJacksonValue; import static org.hamcrest.CoreMatchers.*; @@ -142,7 +142,7 @@ public class MappingJackson2XmlHttpMessageConverterTests { private void writeInternal(Object object, HttpOutputMessage outputMessage) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException { - Method method = AbstractJackson2HttpMessageConverter.class.getDeclaredMethod( + Method method = AbstractHttpMessageConverter.class.getDeclaredMethod( "writeInternal", Object.class, HttpOutputMessage.class); method.setAccessible(true); method.invoke(this.converter, object, outputMessage); diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/AbstractMessageConverterMethodProcessor.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/AbstractMessageConverterMethodProcessor.java index ddd6931c282..9ef59822595 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/AbstractMessageConverterMethodProcessor.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/AbstractMessageConverterMethodProcessor.java @@ -17,6 +17,7 @@ package org.springframework.web.servlet.mvc.method.annotation; import java.io.IOException; +import java.lang.reflect.Type; import java.util.ArrayList; import java.util.Collections; import java.util.LinkedHashSet; @@ -27,8 +28,10 @@ import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.springframework.core.MethodParameter; +import org.springframework.http.HttpEntity; import org.springframework.http.HttpOutputMessage; import org.springframework.http.MediaType; +import org.springframework.http.converter.GenericHttpMessageConverter; import org.springframework.http.converter.HttpMessageConverter; import org.springframework.http.server.ServletServerHttpRequest; import org.springframework.http.server.ServletServerHttpResponse; @@ -158,7 +161,20 @@ public abstract class AbstractMessageConverterMethodProcessor extends AbstractMe (Class>) messageConverter.getClass(), inputMessage, outputMessage); if (returnValue != null) { - ((HttpMessageConverter) messageConverter).write(returnValue, selectedMediaType, outputMessage); + if (messageConverter instanceof GenericHttpMessageConverter) { + Type type; + if (HttpEntity.class.isAssignableFrom(returnType.getParameterType())) { + returnType.increaseNestingLevel(); + type = returnType.getNestedGenericParameterType(); + } + else { + type = returnType.getGenericParameterType(); + } + ((GenericHttpMessageConverter) messageConverter).write(returnValue, type, selectedMediaType, outputMessage); + } + else { + ((HttpMessageConverter) messageConverter).write(returnValue, selectedMediaType, outputMessage); + } if (logger.isDebugEnabled()) { logger.debug("Written [" + returnValue + "] as \"" + selectedMediaType + "\" using [" + messageConverter + "]"); diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/HttpEntityMethodProcessorTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/HttpEntityMethodProcessorTests.java index 83d55a2aa32..e69889fb2bc 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/HttpEntityMethodProcessorTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/HttpEntityMethodProcessorTests.java @@ -16,6 +16,8 @@ package org.springframework.web.servlet.mvc.method.annotation; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import com.fasterxml.jackson.annotation.JsonTypeName; import static org.junit.Assert.*; import java.io.Serializable; @@ -36,6 +38,8 @@ import org.springframework.mock.web.test.MockHttpServletRequest; import org.springframework.mock.web.test.MockHttpServletResponse; import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean; import org.springframework.web.bind.WebDataBinder; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.bind.support.WebDataBinderFactory; import org.springframework.web.context.request.NativeWebRequest; import org.springframework.web.context.request.ServletWebRequest; @@ -64,6 +68,8 @@ public class HttpEntityMethodProcessorTests { private ServletWebRequest webRequest; + private MockHttpServletResponse servletResponse; + @Before public void setUp() throws Exception { @@ -74,7 +80,8 @@ public class HttpEntityMethodProcessorTests { mavContainer = new ModelAndViewContainer(); binderFactory = new ValidatingBinderFactory(); servletRequest = new MockHttpServletRequest(); - webRequest = new ServletWebRequest(servletRequest, new MockHttpServletResponse()); + servletResponse = new MockHttpServletResponse(); + webRequest = new ServletWebRequest(servletRequest, servletResponse); } @Test @@ -153,6 +160,24 @@ public class HttpEntityMethodProcessorTests { assertEquals("Jad", result.getBody().getName()); } + @Test // SPR-12811 + public void jacksonTypeInfoList() throws Exception { + Method method = JacksonController.class.getMethod("handleList"); + HandlerMethod handlerMethod = new HandlerMethod(new JacksonController(), method); + MethodParameter methodReturnType = handlerMethod.getReturnType(); + + List> converters = new ArrayList>(); + converters.add(new MappingJackson2HttpMessageConverter()); + HttpEntityMethodProcessor processor = new HttpEntityMethodProcessor(converters); + + Object returnValue = new JacksonController().handleList(); + processor.handleReturnValue(returnValue, methodReturnType, this.mavContainer, this.webRequest); + + String content = this.servletResponse.getContentAsString(); + assertTrue(content.contains("\"type\":\"foo\"")); + assertTrue(content.contains("\"type\":\"bar\"")); + } + @SuppressWarnings("unused") public void handle(HttpEntity> arg1, HttpEntity arg2) { @@ -217,4 +242,59 @@ public class HttpEntityMethodProcessorTests { } } + @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "type") + private static class ParentClass { + + private String parentProperty; + + public ParentClass() { + } + + public ParentClass(String parentProperty) { + this.parentProperty = parentProperty; + } + + public String getParentProperty() { + return parentProperty; + } + + public void setParentProperty(String parentProperty) { + this.parentProperty = parentProperty; + } + } + + @JsonTypeName("foo") + private static class Foo extends ParentClass { + + public Foo() { + } + + public Foo(String parentProperty) { + super(parentProperty); + } + } + + @JsonTypeName("bar") + private static class Bar extends ParentClass { + + public Bar() { + } + + public Bar(String parentProperty) { + super(parentProperty); + } + } + + private static class JacksonController { + + @RequestMapping + @ResponseBody + public HttpEntity> handleList() { + List list = new ArrayList<>(); + list.add(new Foo("foo")); + list.add(new Bar("bar")); + return new HttpEntity<>(list); + } + } + } diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/RequestResponseBodyMethodProcessorTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/RequestResponseBodyMethodProcessorTests.java index 7414a63e4a7..c4684f9ccd8 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/RequestResponseBodyMethodProcessorTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/RequestResponseBodyMethodProcessorTests.java @@ -27,6 +27,8 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import com.fasterxml.jackson.annotation.JsonTypeName; import com.fasterxml.jackson.annotation.JsonView; import org.junit.Before; import org.junit.Test; @@ -349,8 +351,8 @@ public class RequestResponseBodyMethodProcessorTests { @Test public void jacksonJsonViewWithResponseBodyAndJsonMessageConverter() throws Exception { - Method method = JacksonViewController.class.getMethod("handleResponseBody"); - HandlerMethod handlerMethod = new HandlerMethod(new JacksonViewController(), method); + Method method = JacksonController.class.getMethod("handleResponseBody"); + HandlerMethod handlerMethod = new HandlerMethod(new JacksonController(), method); MethodParameter methodReturnType = handlerMethod.getReturnType(); List> converters = new ArrayList>(); @@ -359,7 +361,7 @@ public class RequestResponseBodyMethodProcessorTests { RequestResponseBodyMethodProcessor processor = new RequestResponseBodyMethodProcessor( converters, null, Collections.singletonList(new JsonViewResponseBodyAdvice())); - Object returnValue = new JacksonViewController().handleResponseBody(); + Object returnValue = new JacksonController().handleResponseBody(); processor.handleReturnValue(returnValue, methodReturnType, this.mavContainer, this.webRequest); String content = this.servletResponse.getContentAsString(); @@ -370,8 +372,8 @@ public class RequestResponseBodyMethodProcessorTests { @Test public void jacksonJsonViewWithResponseEntityAndJsonMessageConverter() throws Exception { - Method method = JacksonViewController.class.getMethod("handleResponseEntity"); - HandlerMethod handlerMethod = new HandlerMethod(new JacksonViewController(), method); + Method method = JacksonController.class.getMethod("handleResponseEntity"); + HandlerMethod handlerMethod = new HandlerMethod(new JacksonController(), method); MethodParameter methodReturnType = handlerMethod.getReturnType(); List> converters = new ArrayList>(); @@ -380,7 +382,7 @@ public class RequestResponseBodyMethodProcessorTests { HttpEntityMethodProcessor processor = new HttpEntityMethodProcessor( converters, null, Collections.singletonList(new JsonViewResponseBodyAdvice())); - Object returnValue = new JacksonViewController().handleResponseEntity(); + Object returnValue = new JacksonController().handleResponseEntity(); processor.handleReturnValue(returnValue, methodReturnType, this.mavContainer, this.webRequest); String content = this.servletResponse.getContentAsString(); @@ -391,8 +393,8 @@ public class RequestResponseBodyMethodProcessorTests { @Test // SPR-12149 public void jacksonJsonViewWithResponseBodyAndXmlMessageConverter() throws Exception { - Method method = JacksonViewController.class.getMethod("handleResponseBody"); - HandlerMethod handlerMethod = new HandlerMethod(new JacksonViewController(), method); + Method method = JacksonController.class.getMethod("handleResponseBody"); + HandlerMethod handlerMethod = new HandlerMethod(new JacksonController(), method); MethodParameter methodReturnType = handlerMethod.getReturnType(); List> converters = new ArrayList>(); @@ -401,7 +403,7 @@ public class RequestResponseBodyMethodProcessorTests { RequestResponseBodyMethodProcessor processor = new RequestResponseBodyMethodProcessor( converters, null, Collections.singletonList(new JsonViewResponseBodyAdvice())); - Object returnValue = new JacksonViewController().handleResponseBody(); + Object returnValue = new JacksonController().handleResponseBody(); processor.handleReturnValue(returnValue, methodReturnType, this.mavContainer, this.webRequest); String content = this.servletResponse.getContentAsString(); @@ -412,8 +414,8 @@ public class RequestResponseBodyMethodProcessorTests { @Test // SPR-12149 public void jacksonJsonViewWithResponseEntityAndXmlMessageConverter() throws Exception { - Method method = JacksonViewController.class.getMethod("handleResponseEntity"); - HandlerMethod handlerMethod = new HandlerMethod(new JacksonViewController(), method); + Method method = JacksonController.class.getMethod("handleResponseEntity"); + HandlerMethod handlerMethod = new HandlerMethod(new JacksonController(), method); MethodParameter methodReturnType = handlerMethod.getReturnType(); List> converters = new ArrayList>(); @@ -422,7 +424,7 @@ public class RequestResponseBodyMethodProcessorTests { HttpEntityMethodProcessor processor = new HttpEntityMethodProcessor( converters, null, Collections.singletonList(new JsonViewResponseBodyAdvice())); - Object returnValue = new JacksonViewController().handleResponseEntity(); + Object returnValue = new JacksonController().handleResponseEntity(); processor.handleReturnValue(returnValue, methodReturnType, this.mavContainer, this.webRequest); String content = this.servletResponse.getContentAsString(); @@ -437,8 +439,8 @@ public class RequestResponseBodyMethodProcessorTests { this.servletRequest.setContent(content.getBytes("UTF-8")); this.servletRequest.setContentType(MediaType.APPLICATION_JSON_VALUE); - Method method = JacksonViewController.class.getMethod("handleRequestBody", JacksonViewBean.class); - HandlerMethod handlerMethod = new HandlerMethod(new JacksonViewController(), method); + Method method = JacksonController.class.getMethod("handleRequestBody", JacksonViewBean.class); + HandlerMethod handlerMethod = new HandlerMethod(new JacksonController(), method); MethodParameter methodParameter = handlerMethod.getMethodParameters()[0]; List> converters = new ArrayList>(); @@ -463,8 +465,8 @@ public class RequestResponseBodyMethodProcessorTests { this.servletRequest.setContent(content.getBytes("UTF-8")); this.servletRequest.setContentType(MediaType.APPLICATION_JSON_VALUE); - Method method = JacksonViewController.class.getMethod("handleHttpEntity", HttpEntity.class); - HandlerMethod handlerMethod = new HandlerMethod(new JacksonViewController(), method); + Method method = JacksonController.class.getMethod("handleHttpEntity", HttpEntity.class); + HandlerMethod handlerMethod = new HandlerMethod(new JacksonController(), method); MethodParameter methodParameter = handlerMethod.getMethodParameters()[0]; List> converters = new ArrayList>(); @@ -490,8 +492,8 @@ public class RequestResponseBodyMethodProcessorTests { this.servletRequest.setContent(content.getBytes("UTF-8")); this.servletRequest.setContentType(MediaType.APPLICATION_XML_VALUE); - Method method = JacksonViewController.class.getMethod("handleRequestBody", JacksonViewBean.class); - HandlerMethod handlerMethod = new HandlerMethod(new JacksonViewController(), method); + Method method = JacksonController.class.getMethod("handleRequestBody", JacksonViewBean.class); + HandlerMethod handlerMethod = new HandlerMethod(new JacksonController(), method); MethodParameter methodParameter = handlerMethod.getMethodParameters()[0]; List> converters = new ArrayList>(); @@ -516,8 +518,8 @@ public class RequestResponseBodyMethodProcessorTests { this.servletRequest.setContent(content.getBytes("UTF-8")); this.servletRequest.setContentType(MediaType.APPLICATION_XML_VALUE); - Method method = JacksonViewController.class.getMethod("handleHttpEntity", HttpEntity.class); - HandlerMethod handlerMethod = new HandlerMethod(new JacksonViewController(), method); + Method method = JacksonController.class.getMethod("handleHttpEntity", HttpEntity.class); + HandlerMethod handlerMethod = new HandlerMethod(new JacksonController(), method); MethodParameter methodParameter = handlerMethod.getMethodParameters()[0]; List> converters = new ArrayList>(); @@ -537,6 +539,24 @@ public class RequestResponseBodyMethodProcessorTests { assertNull(result.getBody().getWithoutView()); } + @Test // SPR-12811 + public void jacksonTypeInfoList() throws Exception { + Method method = JacksonController.class.getMethod("handleList"); + HandlerMethod handlerMethod = new HandlerMethod(new JacksonController(), method); + MethodParameter methodReturnType = handlerMethod.getReturnType(); + + List> converters = new ArrayList>(); + converters.add(new MappingJackson2HttpMessageConverter()); + RequestResponseBodyMethodProcessor processor = new RequestResponseBodyMethodProcessor(converters); + + Object returnValue = new JacksonController().handleList(); + processor.handleReturnValue(returnValue, methodReturnType, this.mavContainer, this.webRequest); + + String content = this.servletResponse.getContentAsString(); + assertTrue(content.contains("\"type\":\"foo\"")); + assertTrue(content.contains("\"type\":\"bar\"")); + } + String handle( @RequestBody List list, @@ -670,7 +690,50 @@ public class RequestResponseBodyMethodProcessorTests { } } - private static class JacksonViewController { + @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "type") + public static class ParentClass { + + private String parentProperty; + + public ParentClass() { + } + + public ParentClass(String parentProperty) { + this.parentProperty = parentProperty; + } + + public String getParentProperty() { + return parentProperty; + } + + public void setParentProperty(String parentProperty) { + this.parentProperty = parentProperty; + } + } + + @JsonTypeName("foo") + public static class Foo extends ParentClass { + + public Foo() { + } + + public Foo(String parentProperty) { + super(parentProperty); + } + } + + @JsonTypeName("bar") + public static class Bar extends ParentClass { + + public Bar() { + } + + public Bar(String parentProperty) { + super(parentProperty); + } + } + + private static class JacksonController { @RequestMapping @ResponseBody @@ -706,8 +769,17 @@ public class RequestResponseBodyMethodProcessorTests { public JacksonViewBean handleHttpEntity(@JsonView(MyJacksonView1.class) HttpEntity entity) { return entity.getBody(); } - } + @RequestMapping + @ResponseBody + public List handleList() { + List list = new ArrayList<>(); + list.add(new Foo("foo")); + list.add(new Bar("bar")); + return list; + } + } + private static class EmptyRequestBodyAdvice implements RequestBodyAdvice { @Override