diff --git a/spring-web/src/main/java/org/springframework/http/converter/MethodParameterHttpMessageConverter.java b/spring-web/src/main/java/org/springframework/http/converter/MethodParameterHttpMessageConverter.java new file mode 100644 index 00000000000..e141e48fedf --- /dev/null +++ b/spring-web/src/main/java/org/springframework/http/converter/MethodParameterHttpMessageConverter.java @@ -0,0 +1,62 @@ +/* + * Copyright 2002-2014 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 org.springframework.core.MethodParameter; +import org.springframework.http.HttpInputMessage; +import org.springframework.http.HttpOutputMessage; +import org.springframework.http.MediaType; + +import java.io.IOException; + +/** + * An HttpMessageConverter that supports converting the value returned from a + * method by incorporating {@link org.springframework.core.MethodParameter} + * information into the conversion. Such a converter can for example take into + * account information from method-level annotations. + * + * @author Rossen Stoyanchev + * @since 4.1 + */ +public interface MethodParameterHttpMessageConverter extends HttpMessageConverter { + + /** + * This method mirrors {@link HttpMessageConverter#canRead(Class, MediaType)} + * with an additional {@code MethodParameter}. + */ + boolean canRead(Class clazz, MediaType mediaType, MethodParameter parameter); + + /** + * This method mirrors {@link HttpMessageConverter#canWrite(Class, MediaType)} + * with an additional {@code MethodParameter}. + */ + boolean canWrite(Class clazz, MediaType mediaType, MethodParameter parameter); + + /** + * This method mirrors {@link HttpMessageConverter#read(Class, HttpInputMessage)} + * with an additional {@code MethodParameter}. + */ + T read(Class clazz, HttpInputMessage inputMessage, MethodParameter parameter) + throws IOException, HttpMessageNotReadableException; + + /** + * This method mirrors {@link HttpMessageConverter#write(Object, MediaType, HttpOutputMessage)} + * with an additional {@code MethodParameter}. + */ + void write(T t, MediaType contentType, HttpOutputMessage outputMessage, MethodParameter parameter) + throws IOException, HttpMessageNotWritableException; + +} diff --git a/spring-web/src/main/java/org/springframework/http/converter/json/Jackson2ObjectMapperFactoryBean.java b/spring-web/src/main/java/org/springframework/http/converter/json/Jackson2ObjectMapperFactoryBean.java index e99d8451f2a..9e6933f6dee 100644 --- a/spring-web/src/main/java/org/springframework/http/converter/json/Jackson2ObjectMapperFactoryBean.java +++ b/spring-web/src/main/java/org/springframework/http/converter/json/Jackson2ObjectMapperFactoryBean.java @@ -244,6 +244,13 @@ public class Jackson2ObjectMapperFactoryBean implements FactoryBean - implements GenericHttpMessageConverter { + implements GenericHttpMessageConverter, MethodParameterHttpMessageConverter { public static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8"); @@ -147,6 +150,10 @@ public class MappingJackson2HttpMessageConverter extends AbstractHttpMessageConv } } + @Override + public boolean canRead(Class clazz, MediaType mediaType, MethodParameter parameter) { + return canRead(clazz, mediaType); + } @Override public boolean canRead(Class clazz, MediaType mediaType) { @@ -198,6 +205,11 @@ public class MappingJackson2HttpMessageConverter extends AbstractHttpMessageConv return false; } + @Override + public boolean canWrite(Class clazz, MediaType mediaType, MethodParameter parameter) { + return canWrite(clazz, mediaType); + } + @Override protected boolean supports(Class clazz) { // should not be called, since we override canRead/Write instead @@ -212,6 +224,11 @@ public class MappingJackson2HttpMessageConverter extends AbstractHttpMessageConv return readJavaType(javaType, inputMessage); } + @Override + public Object read(Class clazz, HttpInputMessage inputMessage, MethodParameter parameter) throws IOException, HttpMessageNotReadableException { + return super.read(clazz, inputMessage); + } + @Override public Object read(Type type, Class contextClass, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException { @@ -250,13 +267,35 @@ public class MappingJackson2HttpMessageConverter extends AbstractHttpMessageConv if (this.jsonPrefix != null) { jsonGenerator.writeRaw(this.jsonPrefix); } - this.objectMapper.writeValue(jsonGenerator, object); + if (object instanceof MappingJacksonValueHolder) { + MappingJacksonValueHolder valueHolder = (MappingJacksonValueHolder) object; + object = valueHolder.getValue(); + Class serializationView = valueHolder.getSerializationView(); + this.objectMapper.writerWithView(serializationView).writeValue(jsonGenerator, object); + } + else { + this.objectMapper.writeValue(jsonGenerator, object); + } } catch (JsonProcessingException ex) { throw new HttpMessageNotWritableException("Could not write JSON: " + ex.getMessage(), ex); } } + @Override + public void write(Object object, MediaType contentType, HttpOutputMessage outputMessage, MethodParameter parameter) + throws IOException, HttpMessageNotWritableException { + + JsonView annot = parameter.getMethodAnnotation(JsonView.class); + if (annot != null && annot.value().length != 0) { + MappingJacksonValueHolder serializationValue = new MappingJacksonValueHolder(object, annot.value()[0]); + super.write(serializationValue, contentType, outputMessage); + } + else { + super.write(object, contentType, outputMessage); + } + } + /** * Return the Jackson {@link JavaType} for the specified type and context class. *

The default implementation returns {@code typeFactory.constructType(type, contextClass)}, @@ -298,4 +337,20 @@ public class MappingJackson2HttpMessageConverter extends AbstractHttpMessageConv return JsonEncoding.UTF8; } + @Override + protected MediaType getDefaultContentType(Object object) throws IOException { + if (object instanceof MappingJacksonValueHolder) { + object = ((MappingJacksonValueHolder) object).getValue(); + } + return super.getDefaultContentType(object); + } + + @Override + protected Long getContentLength(Object object, MediaType contentType) throws IOException { + if (object instanceof MappingJacksonValueHolder) { + object = ((MappingJacksonValueHolder) object).getValue(); + } + return super.getContentLength(object, contentType); + } + } diff --git a/spring-web/src/main/java/org/springframework/http/converter/json/MappingJacksonValueHolder.java b/spring-web/src/main/java/org/springframework/http/converter/json/MappingJacksonValueHolder.java new file mode 100644 index 00000000000..a2331b88396 --- /dev/null +++ b/spring-web/src/main/java/org/springframework/http/converter/json/MappingJacksonValueHolder.java @@ -0,0 +1,60 @@ +/* + * Copyright 2002-2014 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.json; + +/** + * Holds an Object to be serialized via Jackson together with a serialization + * view to be applied. + * + * @author Rossen Stoyanchev + * @since 4.1 + * + * @see com.fasterxml.jackson.annotation.JsonView + */ +public class MappingJacksonValueHolder { + + private final Object value; + + private final Class serializationView; + + + /** + * Create a new instance. + * @param value the Object to be serialized + * @param serializationView the view to be applied + */ + public MappingJacksonValueHolder(Object value, Class serializationView) { + this.value = value; + this.serializationView = serializationView; + } + + + /** + * Return the value to be serialized. + */ + public Object getValue() { + return this.value; + } + + /** + * Return the serialization view to use. + */ + public Class getSerializationView() { + return this.serializationView; + } + +} diff --git a/spring-web/src/test/java/org/springframework/http/converter/json/Jackson2ObjectMapperFactoryBeanTests.java b/spring-web/src/test/java/org/springframework/http/converter/json/Jackson2ObjectMapperFactoryBeanTests.java index 2d5e845a35f..0759d07b03b 100644 --- a/spring-web/src/test/java/org/springframework/http/converter/json/Jackson2ObjectMapperFactoryBeanTests.java +++ b/spring-web/src/test/java/org/springframework/http/converter/json/Jackson2ObjectMapperFactoryBeanTests.java @@ -90,6 +90,7 @@ public class Jackson2ObjectMapperFactoryBeanTests { public void testBooleanSetters() { this.factory.setAutoDetectFields(false); this.factory.setAutoDetectGettersSetters(false); + this.factory.setDefaultViewInclusion(false); this.factory.setFailOnEmptyBeans(false); this.factory.setIndentOutput(true); this.factory.afterPropertiesSet(); @@ -100,6 +101,7 @@ public class Jackson2ObjectMapperFactoryBeanTests { assertFalse(objectMapper.getDeserializationConfig().isEnabled(MapperFeature.AUTO_DETECT_FIELDS)); assertFalse(objectMapper.getSerializationConfig().isEnabled(MapperFeature.AUTO_DETECT_GETTERS)); assertFalse(objectMapper.getDeserializationConfig().isEnabled(MapperFeature.AUTO_DETECT_SETTERS)); + assertFalse(objectMapper.getDeserializationConfig().isEnabled(MapperFeature.DEFAULT_VIEW_INCLUSION)); assertFalse(objectMapper.getSerializationConfig().isEnabled(SerializationFeature.FAIL_ON_EMPTY_BEANS)); assertTrue(objectMapper.getSerializationConfig().isEnabled(SerializationFeature.INDENT_OUTPUT)); assertTrue(objectMapper.getSerializationConfig().getSerializationInclusion() == JsonInclude.Include.ALWAYS); @@ -253,6 +255,7 @@ public class Jackson2ObjectMapperFactoryBeanTests { assertTrue(objectMapper.getFactory().isEnabled(JsonGenerator.Feature.WRITE_NUMBERS_AS_STRINGS)); assertFalse(objectMapper.getSerializationConfig().isEnabled(MapperFeature.AUTO_DETECT_GETTERS)); + assertTrue(objectMapper.getDeserializationConfig().isEnabled(MapperFeature.DEFAULT_VIEW_INCLUSION)); assertFalse(objectMapper.getDeserializationConfig().isEnabled(MapperFeature.AUTO_DETECT_FIELDS)); assertFalse(objectMapper.getFactory().isEnabled(JsonParser.Feature.AUTO_CLOSE_SOURCE)); assertFalse(objectMapper.getFactory().isEnabled(JsonGenerator.Feature.QUOTE_FIELD_NAMES)); 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 e741202422e..20096feb385 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 @@ -24,6 +24,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import com.fasterxml.jackson.annotation.JsonView; import com.fasterxml.jackson.databind.JavaType; import com.fasterxml.jackson.databind.ObjectMapper; import org.junit.Test; @@ -237,6 +238,22 @@ public class MappingJackson2HttpMessageConverterTests { assertEquals(")]}',\"foo\"", outputMessage.getBodyAsString(Charset.forName("UTF-8"))); } + @Test + public void jsonView() throws Exception { + MockHttpOutputMessage outputMessage = new MockHttpOutputMessage(); + JacksonViewBean bean = new JacksonViewBean(); + bean.setWithView1("with"); + bean.setWithView2("with"); + bean.setWithoutView("without"); + MappingJacksonValueHolder jsv = new MappingJacksonValueHolder(bean, MyJacksonView1.class); + this.converter.writeInternal(jsv, outputMessage); + + String result = outputMessage.getBodyAsString(Charset.forName("UTF-8")); + assertTrue(result.contains("\"withView1\":\"with\"")); + assertFalse(result.contains("\"withView2\":\"with\"")); + assertTrue(result.contains("\"withoutView\":\"without\"")); + } + public static class MyBean { @@ -315,4 +332,42 @@ public class MappingJackson2HttpMessageConverterTests { } } + private interface MyJacksonView1 {}; + private interface MyJacksonView2 {}; + + private static class JacksonViewBean { + + @JsonView(MyJacksonView1.class) + private String withView1; + + @JsonView(MyJacksonView2.class) + private String withView2; + + private String withoutView; + + public String getWithView1() { + return withView1; + } + + public void setWithView1(String withView1) { + this.withView1 = withView1; + } + + public String getWithView2() { + return withView2; + } + + public void setWithView2(String withView2) { + this.withView2 = withView2; + } + + public String getWithoutView() { + return withoutView; + } + + public void setWithoutView(String withoutView) { + this.withoutView = withoutView; + } + } + } diff --git a/spring-web/src/test/java/org/springframework/web/client/AbstractJettyServerTestCase.java b/spring-web/src/test/java/org/springframework/web/client/AbstractJettyServerTestCase.java index 1739da77727..390acb60c20 100644 --- a/spring-web/src/test/java/org/springframework/web/client/AbstractJettyServerTestCase.java +++ b/spring-web/src/test/java/org/springframework/web/client/AbstractJettyServerTestCase.java @@ -56,7 +56,8 @@ public class AbstractJettyServerTestCase { protected static String baseUrl; - protected static MediaType contentType; + protected static MediaType textContentType; + protected static MediaType jsonContentType; private static Server jettyServer; @@ -67,14 +68,19 @@ public class AbstractJettyServerTestCase { baseUrl = "http://localhost:" + port; ServletContextHandler handler = new ServletContextHandler(); byte[] bytes = helloWorld.getBytes("UTF-8"); - contentType = new MediaType("text", "plain", Collections + textContentType = new MediaType("text", "plain", Collections .singletonMap("charset", "UTF-8")); - handler.addServlet(new ServletHolder(new GetServlet(bytes, contentType)), "/get"); - handler.addServlet(new ServletHolder(new GetServlet(new byte[0], contentType)), "/get/nothing"); + jsonContentType = new MediaType("application", "json", Collections + .singletonMap("charset", "UTF-8")); + handler.addServlet(new ServletHolder(new GetServlet(bytes, textContentType)), "/get"); + handler.addServlet(new ServletHolder(new GetServlet(new byte[0], textContentType)), "/get/nothing"); handler.addServlet(new ServletHolder(new GetServlet(bytes, null)), "/get/nocontenttype"); handler.addServlet( - new ServletHolder(new PostServlet(helloWorld, baseUrl + "/post/1", bytes, contentType)), + new ServletHolder(new PostServlet(helloWorld, baseUrl + "/post/1", bytes, textContentType)), "/post"); + handler.addServlet( + new ServletHolder(new JsonPostServlet(baseUrl + "/jsonpost/1", jsonContentType)), + "/jsonpost"); handler.addServlet(new ServletHolder(new StatusCodeServlet(204)), "/status/nocontent"); handler.addServlet(new ServletHolder(new StatusCodeServlet(304)), "/status/notmodified"); handler.addServlet(new ServletHolder(new ErrorServlet(404)), "/status/notfound"); @@ -83,7 +89,7 @@ public class AbstractJettyServerTestCase { handler.addServlet(new ServletHolder(new MultipartServlet()), "/multipart"); handler.addServlet(new ServletHolder(new DeleteServlet()), "/delete"); handler.addServlet( - new ServletHolder(new PutServlet(helloWorld, bytes, contentType)), + new ServletHolder(new PutServlet(helloWorld, bytes, textContentType)), "/put"); jettyServer.setHandler(handler); jettyServer.start(); @@ -185,6 +191,33 @@ public class AbstractJettyServerTestCase { } } + @SuppressWarnings("serial") + private static class JsonPostServlet extends HttpServlet { + + private final String location; + + private final MediaType contentType; + + private JsonPostServlet(String location, MediaType contentType) { + this.location = location; + this.contentType = contentType; + } + + @Override + protected void doPost(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + assertTrue("Invalid request content-length", request.getContentLength() > 0); + assertNotNull("No content-type", request.getContentType()); + String body = FileCopyUtils.copyToString(request.getReader()); + response.setStatus(HttpServletResponse.SC_CREATED); + response.setHeader("Location", location); + response.setContentType(contentType.toString()); + byte[] bytes = body.getBytes("UTF-8"); + response.setContentLength(bytes.length);; + FileCopyUtils.copy(bytes, response.getOutputStream()); + } + } + @SuppressWarnings("serial") private static class PutServlet extends HttpServlet { diff --git a/spring-web/src/test/java/org/springframework/web/client/AsyncRestTemplateIntegrationTests.java b/spring-web/src/test/java/org/springframework/web/client/AsyncRestTemplateIntegrationTests.java index 2b2083fb090..5813633b939 100644 --- a/spring-web/src/test/java/org/springframework/web/client/AsyncRestTemplateIntegrationTests.java +++ b/spring-web/src/test/java/org/springframework/web/client/AsyncRestTemplateIntegrationTests.java @@ -63,7 +63,7 @@ public class AsyncRestTemplateIntegrationTests extends AbstractJettyServerTestCa ResponseEntity entity = futureEntity.get(); assertEquals("Invalid content", helloWorld, entity.getBody()); assertFalse("No headers", entity.getHeaders().isEmpty()); - assertEquals("Invalid content-type", contentType, entity.getHeaders().getContentType()); + assertEquals("Invalid content-type", textContentType, entity.getHeaders().getContentType()); assertEquals("Invalid status code", HttpStatus.OK, entity.getStatusCode()); } @@ -84,7 +84,7 @@ public class AsyncRestTemplateIntegrationTests extends AbstractJettyServerTestCa public void onSuccess(ResponseEntity entity) { assertEquals("Invalid content", helloWorld, entity.getBody()); assertFalse("No headers", entity.getHeaders().isEmpty()); - assertEquals("Invalid content-type", contentType, entity.getHeaders().getContentType()); + assertEquals("Invalid content-type", textContentType, entity.getHeaders().getContentType()); assertEquals("Invalid status code", HttpStatus.OK, entity.getStatusCode()); } diff --git a/spring-web/src/test/java/org/springframework/web/client/RestTemplateIntegrationTests.java b/spring-web/src/test/java/org/springframework/web/client/RestTemplateIntegrationTests.java index 2364b8e6261..996eff3b399 100644 --- a/spring-web/src/test/java/org/springframework/web/client/RestTemplateIntegrationTests.java +++ b/spring-web/src/test/java/org/springframework/web/client/RestTemplateIntegrationTests.java @@ -23,6 +23,7 @@ import java.nio.charset.Charset; import java.util.EnumSet; import java.util.Set; +import com.fasterxml.jackson.annotation.JsonView; import org.junit.Before; import org.junit.Test; @@ -35,6 +36,7 @@ import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; +import org.springframework.http.converter.json.MappingJacksonValueHolder; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; @@ -63,7 +65,7 @@ public class RestTemplateIntegrationTests extends AbstractJettyServerTestCase { ResponseEntity entity = template.getForEntity(baseUrl + "/{method}", String.class, "get"); assertEquals("Invalid content", helloWorld, entity.getBody()); assertFalse("No headers", entity.getHeaders().isEmpty()); - assertEquals("Invalid content-type", contentType, entity.getHeaders().getContentType()); + assertEquals("Invalid content-type", textContentType, entity.getHeaders().getContentType()); assertEquals("Invalid status code", HttpStatus.OK, entity.getStatusCode()); } @@ -198,4 +200,79 @@ public class RestTemplateIntegrationTests extends AbstractJettyServerTestCase { assertFalse(result.hasBody()); } + @Test + public void jsonPostForObject() throws URISyntaxException { + HttpHeaders entityHeaders = new HttpHeaders(); + entityHeaders.setContentType(new MediaType("application", "json", Charset.forName("UTF-8"))); + MySampleBean bean = new MySampleBean(); + bean.setWith1("with"); + bean.setWith2("with"); + bean.setWithout("without"); + HttpEntity entity = new HttpEntity(bean, entityHeaders); + String s = template.postForObject(baseUrl + "/jsonpost", entity, String.class, "post"); + assertTrue(s.contains("\"with1\":\"with\"")); + assertTrue(s.contains("\"with2\":\"with\"")); + assertTrue(s.contains("\"without\":\"without\"")); + } + + @Test + public void jsonPostForObjectWithJacksonView() throws URISyntaxException { + HttpHeaders entityHeaders = new HttpHeaders(); + entityHeaders.setContentType(new MediaType("application", "json", Charset.forName("UTF-8"))); + MySampleBean bean = new MySampleBean("with", "with", "without"); + MappingJacksonValueHolder jsv = new MappingJacksonValueHolder(bean, MyJacksonView1.class); + HttpEntity entity = new HttpEntity(jsv); + String s = template.postForObject(baseUrl + "/jsonpost", entity, String.class, "post"); + assertTrue(s.contains("\"with1\":\"with\"")); + assertFalse(s.contains("\"with2\":\"with\"")); + assertTrue(s.contains("\"without\":\"without\"")); + } + + public interface MyJacksonView1 {}; + public interface MyJacksonView2 {}; + + public static class MySampleBean { + + @JsonView(MyJacksonView1.class) + private String with1; + + @JsonView(MyJacksonView2.class) + private String with2; + + private String without; + + private MySampleBean() { + } + + private MySampleBean(String with1, String with2, String without) { + this.with1 = with1; + this.with2 = with2; + this.without = without; + } + + public String getWith1() { + return with1; + } + + public void setWith1(String with1) { + this.with1 = with1; + } + + public String getWith2() { + return with2; + } + + public void setWith2(String with2) { + this.with2 = with2; + } + + public String getWithout() { + return without; + } + + public void setWithout(String without) { + this.without = without; + } + } + } 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 83c564bc233..ea798a66824 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 @@ -29,6 +29,7 @@ import org.springframework.core.MethodParameter; import org.springframework.http.HttpOutputMessage; import org.springframework.http.MediaType; import org.springframework.http.converter.HttpMessageConverter; +import org.springframework.http.converter.MethodParameterHttpMessageConverter; import org.springframework.http.server.ServletServerHttpRequest; import org.springframework.http.server.ServletServerHttpResponse; import org.springframework.util.CollectionUtils; @@ -139,6 +140,17 @@ public abstract class AbstractMessageConverterMethodProcessor extends AbstractMe if (selectedMediaType != null) { selectedMediaType = selectedMediaType.removeQualityValue(); for (HttpMessageConverter messageConverter : this.messageConverters) { + if (messageConverter instanceof MethodParameterHttpMessageConverter) { + MethodParameterHttpMessageConverter c = (MethodParameterHttpMessageConverter) messageConverter; + if (c.canWrite(returnValueClass, selectedMediaType, returnType)) { + c.write(returnValue, selectedMediaType, outputMessage, returnType); + if (logger.isDebugEnabled()) { + logger.debug("Written [" + returnValue + "] as \"" + selectedMediaType + "\" using [" + + messageConverter + "]"); + } + return; + } + } if (messageConverter.canWrite(returnValueClass, selectedMediaType)) { ((HttpMessageConverter) messageConverter).write(returnValue, selectedMediaType, outputMessage); if (logger.isDebugEnabled()) { diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/view/json/MappingJackson2JsonView.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/view/json/MappingJackson2JsonView.java index a744e4a73c7..44a54108cb0 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/view/json/MappingJackson2JsonView.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/view/json/MappingJackson2JsonView.java @@ -26,6 +26,7 @@ import java.util.Set; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; +import com.fasterxml.jackson.annotation.JsonView; import com.fasterxml.jackson.core.JsonEncoding; import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.databind.ObjectMapper; @@ -253,7 +254,12 @@ public class MappingJackson2JsonView extends AbstractView { OutputStream stream = (this.updateContentLength ? createTemporaryOutputStream() : response.getOutputStream()); Object value = filterModel(model); - writeContent(stream, value, this.jsonPrefix); + if (model.containsKey(JsonView.class.getName())) { + writeContent(stream, value, this.jsonPrefix, model); + } + else { + writeContent(stream, value, this.jsonPrefix); + } if (this.updateContentLength) { writeToResponse(response, (ByteArrayOutputStream) stream); } @@ -287,6 +293,12 @@ public class MappingJackson2JsonView extends AbstractView { * @throws IOException if writing failed */ protected void writeContent(OutputStream stream, Object value, String jsonPrefix) throws IOException { + writeContent(stream, value, jsonPrefix, Collections.emptyMap()); + } + + protected void writeContent(OutputStream stream, Object value, String jsonPrefix, Map model) + throws IOException { + // The following has been deprecated as late as Jackson 2.2 (April 2013); // preserved for the time being, for Jackson 2.0/2.1 compatibility. @SuppressWarnings("deprecation") @@ -301,7 +313,14 @@ public class MappingJackson2JsonView extends AbstractView { if (jsonPrefix != null) { generator.writeRaw(jsonPrefix); } - this.objectMapper.writeValue(generator, value); + + Class serializationView = (Class) model.get(JsonView.class.getName()); + if (serializationView != null) { + this.objectMapper.writerWithView(serializationView).writeValue(generator, value); + } + else { + this.objectMapper.writeValue(generator, value); + } } -} +} \ No newline at end of file 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 152bd2e5243..2c572e0ad99 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 @@ -21,6 +21,7 @@ import java.lang.reflect.Method; import java.util.ArrayList; import java.util.List; +import com.fasterxml.jackson.annotation.JsonView; import org.junit.Before; import org.junit.Test; @@ -47,6 +48,8 @@ import org.springframework.web.context.request.NativeWebRequest; import org.springframework.web.context.request.ServletWebRequest; import org.springframework.web.method.HandlerMethod; import org.springframework.web.method.support.ModelAndViewContainer; +import org.springframework.web.servlet.ModelAndView; +import org.springframework.web.servlet.view.json.MappingJackson2JsonView; import static org.junit.Assert.*; @@ -283,6 +286,42 @@ public class RequestResponseBodyMethodProcessorTests { assertTrue("Failed to recognize type-level @RestController", processor.supportsReturnType(returnType)); } + @Test + public void handleResponseBodyJacksonView() throws Exception { + Method method = JacksonViewController.class.getMethod("handleResponseBody"); + HandlerMethod handlerMethod = new HandlerMethod(new JacksonViewController(), method); + MethodParameter methodReturnType = handlerMethod.getReturnType(); + + List> converters = new ArrayList>(); + converters.add(new MappingJackson2HttpMessageConverter()); + RequestResponseBodyMethodProcessor processor = new RequestResponseBodyMethodProcessor(converters); + + processor.handleReturnValue(new JacksonViewController().handleResponseBody(), methodReturnType, mavContainer, webRequest); + + String content = servletResponse.getContentAsString(); + assertFalse(content.contains("\"withView1\":\"with\"")); + assertTrue(content.contains("\"withView2\":\"with\"")); + assertTrue(content.contains("\"withoutView\":\"without\"")); + } + + @Test + public void handleResponseBodyJacksonViewAndModelAndView() throws Exception { + Method method = JacksonViewController.class.getMethod("handleResponseBodyWithModelAndView"); + HandlerMethod handlerMethod = new HandlerMethod(new JacksonViewController(), method); + MethodParameter methodReturnType = handlerMethod.getReturnType(); + + List> converters = new ArrayList>(); + converters.add(new MappingJackson2HttpMessageConverter()); + RequestResponseBodyMethodProcessor processor = new RequestResponseBodyMethodProcessor(converters); + + processor.handleReturnValue(new JacksonViewController().handleResponseBody(), methodReturnType, mavContainer, webRequest); + + String content = servletResponse.getContentAsString(); + assertFalse(content.contains("\"withView1\":\"with\"")); + assertTrue(content.contains("\"withView2\":\"with\"")); + assertTrue(content.contains("\"withoutView\":\"without\"")); + } + public String handle( @RequestBody List list, @@ -373,4 +412,69 @@ public class RequestResponseBodyMethodProcessorTests { } } + private interface MyJacksonView1 {}; + private interface MyJacksonView2 {}; + + private static class JacksonViewBean { + + @JsonView(MyJacksonView1.class) + private String withView1; + + @JsonView(MyJacksonView2.class) + private String withView2; + + private String withoutView; + + public String getWithView1() { + return withView1; + } + + public void setWithView1(String withView1) { + this.withView1 = withView1; + } + + public String getWithView2() { + return withView2; + } + + public void setWithView2(String withView2) { + this.withView2 = withView2; + } + + public String getWithoutView() { + return withoutView; + } + + public void setWithoutView(String withoutView) { + this.withoutView = withoutView; + } + } + + private static class JacksonViewController { + + @RequestMapping + @ResponseBody + @JsonView(MyJacksonView2.class) + public JacksonViewBean handleResponseBody() { + JacksonViewBean bean = new JacksonViewBean(); + bean.setWithView1("with"); + bean.setWithView2("with"); + bean.setWithoutView("without"); + return bean; + } + + @RequestMapping + @JsonView(MyJacksonView2.class) + public ModelAndView handleResponseBodyWithModelAndView() { + JacksonViewBean bean = new JacksonViewBean(); + bean.setWithView1("with"); + bean.setWithView2("with"); + bean.setWithoutView("without"); + ModelAndView mav = new ModelAndView(new MappingJackson2JsonView()); + mav.addObject("bean", bean); + return mav; + } + + } + } diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/view/ViewResolverTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/view/ViewResolverTests.java index 3836559480d..a7e0ac0c38e 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/view/ViewResolverTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/view/ViewResolverTests.java @@ -132,12 +132,12 @@ public class ViewResolverTests { View view = vr.resolveViewName("example1", Locale.getDefault()); assertEquals("Correct view class", JstlView.class, view.getClass()); assertEquals("Correct URL", "example1", ((InternalResourceView) view).getUrl()); - assertEquals("Correct contentType", "myContentType", ((InternalResourceView) view).getContentType()); + assertEquals("Correct textContentType", "myContentType", ((InternalResourceView) view).getContentType()); view = vr.resolveViewName("example2", Locale.getDefault()); assertEquals("Correct view class", JstlView.class, view.getClass()); assertEquals("Correct URL", "example2", ((InternalResourceView) view).getUrl()); - assertEquals("Correct contentType", "myContentType", ((InternalResourceView) view).getContentType()); + assertEquals("Correct textContentType", "myContentType", ((InternalResourceView) view).getContentType()); HttpServletRequest request = new MockHttpServletRequest(wac.getServletContext()); HttpServletResponse response = new MockHttpServletResponse(); diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/view/json/MappingJackson2JsonViewTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/view/json/MappingJackson2JsonViewTests.java index eb3bfbb8208..c0742bfd365 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/view/json/MappingJackson2JsonViewTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/view/json/MappingJackson2JsonViewTests.java @@ -23,6 +23,7 @@ import java.util.HashSet; import java.util.Map; import java.util.Set; +import com.fasterxml.jackson.annotation.JsonView; import org.junit.Before; import org.junit.Test; import org.mozilla.javascript.Context; @@ -274,6 +275,24 @@ public class MappingJackson2JsonViewTests { assertSame(bean2, ((Map) actual).get("foo2")); } + @Test + public void renderSimpleBeanWithJsonView() throws Exception { + Object bean = new TestBeanSimple(); + Map model = new HashMap(); + model.put("bindingResult", mock(BindingResult.class, "binding_result")); + model.put("foo", bean); + model.put(JsonView.class.getName(), MyJacksonView1.class); + + view.setUpdateContentLength(true); + view.render(model, request, response); + + String content = response.getContentAsString(); + assertTrue(content.length() > 0); + assertEquals(content.length(), response.getContentLength()); + assertTrue(content.contains("foo")); + assertFalse(content.contains("42")); + } + private void validateResult() throws Exception { Object jsResult = jsContext.evaluateString(jsScope, "(" + response.getContentAsString() + ")", "JSON Stream", 1, null); @@ -281,14 +300,18 @@ public class MappingJackson2JsonViewTests { assertEquals("application/json", response.getContentType()); } + public interface MyJacksonView1 {}; + public interface MyJacksonView2 {}; @SuppressWarnings("unused") public static class TestBeanSimple { + @JsonView(MyJacksonView1.class) private String value = "foo"; private boolean test = false; + @JsonView(MyJacksonView2.class) private long number = 42; private TestChildBean child = new TestChildBean(); diff --git a/src/asciidoc/index.adoc b/src/asciidoc/index.adoc index 71ac9b2cd10..127ff330fd0 100644 --- a/src/asciidoc/index.adoc +++ b/src/asciidoc/index.adoc @@ -29853,6 +29853,76 @@ Check out the {javadoc-baseurl}/org/springframework/web/bind/annotation/ControllerAdvice.html[`@ControllerAdvice` documentation] for more details. +[[mvc-ann-jsonview]] +===== Jackson Serialization View Support + +It can sometimes be useful to filter contextually the object that will be serialized to the +HTTP response body. In order to provide such capability, Spring MVC has built-in support for +rendering with http://wiki.fasterxml.com/JacksonJsonViews[Jackson's Serialization Views]. + +To use it with `@ResponseBody` controller method or methods return `ResponseEntity`, simply +add the `@JsonView` annotation: + +[source,java,indent=0] +[subs="verbatim,quotes"] +---- + @RestController + public class UserController { + + @RequestMapping(value = "/user", method = RequestMethod.GET) + @JsonView(User.WithoutPasswordView.class) + public User getUser() { + return new User("eric", "7!jd#h23"); + } + } + + public class User { + + public interface WithoutPasswordView {}; + public interface WithPasswordView extends WithoutPasswordView {}; + + private String username; + private String password; + + public User() { + } + + public User(String username, String password) { + this.username = username; + this.password = password; + } + + @JsonView(WithoutPasswordView.class) + public String getUsername() { + return this.username; + } + + @JsonView(WithPasswordView.class) + public String getPassword() { + return this.password; + } + } +---- + +For controllers relying on view resolution, simply add the serialization view class +to the model: + +[source,java,indent=0] +[subs="verbatim,quotes"] +---- + @Controller + public class UserController extends AbstractController { + + @RequestMapping(value = "/user", method = RequestMethod.GET) + public String getUser(Model model) { + model.addAttribute("user", new User("eric", "7!jd#h23")); + model.addAttribute(JsonView.class.getName(), User.WithoutPasswordView.class); + return "userView"; + } + } + +---- + [[mvc-ann-async]] ==== Asynchronous Request Processing Spring MVC 3.2 introduced Servlet 3 based asynchronous request processing. Instead of @@ -39812,6 +39882,20 @@ In the above example, we first prepare a request entity that contains the `MyRequestHeader` header. We then retrieve the response, and read the `MyResponseHeader` and body. +[[rest-template-jsonview]] +===== Jackson JSON Views support + +It is possible to specify a http://wiki.fasterxml.com/JacksonJsonViews[Jackson JSON View] +to serialize only a subset of the object properties. For example: + +[source,java,indent=0] +[subs="verbatim,quotes"] +---- + JacksonSerializationValue jsv = new JacksonSerializationValue(new User("eric", "7!jd#h23"), + User.WithoutPasswordView.class); + HttpEntity entity = new HttpEntity(jsv); + String s = template.postForObject("http://example.com/user", entity, String.class); +---- [[rest-message-conversion]]