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