Permalink
Browse files

Add support for Jackson serialization views

Spring MVC now supports Jackon's serialization views for rendering
different subsets of the same POJO from different controller
methods (e.g. detailed page vs summary view).

Issue: SPR-7156
  • Loading branch information...
1 parent 673a497 commit be0b69cbf1377e3fa8d8abc94f24274f4d0945b2 @sdeleuze sdeleuze committed with rstoyanchev May 12, 2014
Showing with 610 additions and 16 deletions.
  1. +62 −0 spring-web/src/main/java/org/springframework/http/converter/MethodParameterHttpMessageConverter.java
  2. +7 −0 ...ng-web/src/main/java/org/springframework/http/converter/json/Jackson2ObjectMapperFactoryBean.java
  3. +57 −2 ...eb/src/main/java/org/springframework/http/converter/json/MappingJackson2HttpMessageConverter.java
  4. +60 −0 spring-web/src/main/java/org/springframework/http/converter/json/MappingJacksonValueHolder.java
  5. +3 −0 ...b/src/test/java/org/springframework/http/converter/json/Jackson2ObjectMapperFactoryBeanTests.java
  6. +55 −0 ...c/test/java/org/springframework/http/converter/json/MappingJackson2HttpMessageConverterTests.java
  7. +39 −6 spring-web/src/test/java/org/springframework/web/client/AbstractJettyServerTestCase.java
  8. +2 −2 spring-web/src/test/java/org/springframework/web/client/AsyncRestTemplateIntegrationTests.java
  9. +78 −1 spring-web/src/test/java/org/springframework/web/client/RestTemplateIntegrationTests.java
  10. +12 −0 ...rg/springframework/web/servlet/mvc/method/annotation/AbstractMessageConverterMethodProcessor.java
  11. +22 −3 spring-webmvc/src/main/java/org/springframework/web/servlet/view/json/MappingJackson2JsonView.java
  12. +104 −0 ...rg/springframework/web/servlet/mvc/method/annotation/RequestResponseBodyMethodProcessorTests.java
  13. +2 −2 spring-webmvc/src/test/java/org/springframework/web/servlet/view/ViewResolverTests.java
  14. +23 −0 ...-webmvc/src/test/java/org/springframework/web/servlet/view/json/MappingJackson2JsonViewTests.java
  15. +84 −0 src/asciidoc/index.adoc
@@ -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<T> extends HttpMessageConverter<T> {
+
+ /**
+ * 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<? extends T> 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;
+
+}
@@ -245,6 +245,13 @@ public void setAutoDetectGettersSetters(boolean autoDetectGettersSetters) {
}
/**
+ * Shortcut for {@link MapperFeature#DEFAULT_VIEW_INCLUSION} option.
+ */
+ public void setDefaultViewInclusion(boolean defaultViewInclusion) {
+ this.features.put(MapperFeature.DEFAULT_VIEW_INCLUSION, defaultViewInclusion);
+ }
+
+ /**
* Shortcut for {@link SerializationFeature#FAIL_ON_EMPTY_BEANS} option.
*/
public void setFailOnEmptyBeans(boolean failOnEmptyBeans) {
@@ -21,6 +21,7 @@
import java.nio.charset.Charset;
import java.util.concurrent.atomic.AtomicReference;
+import com.fasterxml.jackson.annotation.JsonView;
import com.fasterxml.jackson.core.JsonEncoding;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonProcessingException;
@@ -29,13 +30,15 @@
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
+import org.springframework.core.MethodParameter;
import org.springframework.http.HttpInputMessage;
import org.springframework.http.HttpOutputMessage;
import org.springframework.http.MediaType;
import org.springframework.http.converter.AbstractHttpMessageConverter;
import org.springframework.http.converter.GenericHttpMessageConverter;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.http.converter.HttpMessageNotWritableException;
+import org.springframework.http.converter.MethodParameterHttpMessageConverter;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
@@ -57,7 +60,7 @@
* @since 3.1.2
*/
public class MappingJackson2HttpMessageConverter extends AbstractHttpMessageConverter<Object>
- implements GenericHttpMessageConverter<Object> {
+ implements GenericHttpMessageConverter<Object>, MethodParameterHttpMessageConverter<Object> {
public static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8");
@@ -147,6 +150,10 @@ private void configurePrettyPrint() {
}
}
+ @Override
+ public boolean canRead(Class<?> clazz, MediaType mediaType, MethodParameter parameter) {
+ return canRead(clazz, mediaType);
+ }
@Override
public boolean canRead(Class<?> clazz, MediaType mediaType) {
@@ -199,6 +206,11 @@ public boolean canWrite(Class<?> clazz, MediaType mediaType) {
}
@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
throw new UnsupportedOperationException();
@@ -213,6 +225,11 @@ protected Object readInternal(Class<?> clazz, HttpInputMessage 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 @@ protected void writeInternal(Object object, HttpOutputMessage outputMessage)
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.
* <p>The default implementation returns {@code typeFactory.constructType(type, contextClass)},
@@ -298,4 +337,20 @@ protected JsonEncoding getJsonEncoding(MediaType contentType) {
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);
+ }
+
}
@@ -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;
+ }
+
+}
@@ -90,6 +90,7 @@ public void testUnknownFeature() {
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 void testBooleanSetters() {
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 void testCompleteSetup() {
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));
@@ -24,6 +24,7 @@
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 void prefixJsonCustom() throws Exception {
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 void setName(String name) {
}
}
+ 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;
+ }
+ }
+
}
Oops, something went wrong.

0 comments on commit be0b69c

Please sign in to comment.