Skip to content

Commit be0b69c

Browse files
sdeleuzerstoyanchev
authored andcommitted
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
1 parent 673a497 commit be0b69c

File tree

15 files changed

+610
-16
lines changed

15 files changed

+610
-16
lines changed
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
/*
2+
* Copyright 2002-2014 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.http.converter;
17+
18+
import org.springframework.core.MethodParameter;
19+
import org.springframework.http.HttpInputMessage;
20+
import org.springframework.http.HttpOutputMessage;
21+
import org.springframework.http.MediaType;
22+
23+
import java.io.IOException;
24+
25+
/**
26+
* An HttpMessageConverter that supports converting the value returned from a
27+
* method by incorporating {@link org.springframework.core.MethodParameter}
28+
* information into the conversion. Such a converter can for example take into
29+
* account information from method-level annotations.
30+
*
31+
* @author Rossen Stoyanchev
32+
* @since 4.1
33+
*/
34+
public interface MethodParameterHttpMessageConverter<T> extends HttpMessageConverter<T> {
35+
36+
/**
37+
* This method mirrors {@link HttpMessageConverter#canRead(Class, MediaType)}
38+
* with an additional {@code MethodParameter}.
39+
*/
40+
boolean canRead(Class<?> clazz, MediaType mediaType, MethodParameter parameter);
41+
42+
/**
43+
* This method mirrors {@link HttpMessageConverter#canWrite(Class, MediaType)}
44+
* with an additional {@code MethodParameter}.
45+
*/
46+
boolean canWrite(Class<?> clazz, MediaType mediaType, MethodParameter parameter);
47+
48+
/**
49+
* This method mirrors {@link HttpMessageConverter#read(Class, HttpInputMessage)}
50+
* with an additional {@code MethodParameter}.
51+
*/
52+
T read(Class<? extends T> clazz, HttpInputMessage inputMessage, MethodParameter parameter)
53+
throws IOException, HttpMessageNotReadableException;
54+
55+
/**
56+
* This method mirrors {@link HttpMessageConverter#write(Object, MediaType, HttpOutputMessage)}
57+
* with an additional {@code MethodParameter}.
58+
*/
59+
void write(T t, MediaType contentType, HttpOutputMessage outputMessage, MethodParameter parameter)
60+
throws IOException, HttpMessageNotWritableException;
61+
62+
}

spring-web/src/main/java/org/springframework/http/converter/json/Jackson2ObjectMapperFactoryBean.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -244,6 +244,13 @@ public void setAutoDetectGettersSetters(boolean autoDetectGettersSetters) {
244244
this.features.put(MapperFeature.AUTO_DETECT_SETTERS, autoDetectGettersSetters);
245245
}
246246

247+
/**
248+
* Shortcut for {@link MapperFeature#DEFAULT_VIEW_INCLUSION} option.
249+
*/
250+
public void setDefaultViewInclusion(boolean defaultViewInclusion) {
251+
this.features.put(MapperFeature.DEFAULT_VIEW_INCLUSION, defaultViewInclusion);
252+
}
253+
247254
/**
248255
* Shortcut for {@link SerializationFeature#FAIL_ON_EMPTY_BEANS} option.
249256
*/

spring-web/src/main/java/org/springframework/http/converter/json/MappingJackson2HttpMessageConverter.java

Lines changed: 57 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import java.nio.charset.Charset;
2222
import java.util.concurrent.atomic.AtomicReference;
2323

24+
import com.fasterxml.jackson.annotation.JsonView;
2425
import com.fasterxml.jackson.core.JsonEncoding;
2526
import com.fasterxml.jackson.core.JsonGenerator;
2627
import com.fasterxml.jackson.core.JsonProcessingException;
@@ -29,13 +30,15 @@
2930
import com.fasterxml.jackson.databind.ObjectMapper;
3031
import com.fasterxml.jackson.databind.SerializationFeature;
3132

33+
import org.springframework.core.MethodParameter;
3234
import org.springframework.http.HttpInputMessage;
3335
import org.springframework.http.HttpOutputMessage;
3436
import org.springframework.http.MediaType;
3537
import org.springframework.http.converter.AbstractHttpMessageConverter;
3638
import org.springframework.http.converter.GenericHttpMessageConverter;
3739
import org.springframework.http.converter.HttpMessageNotReadableException;
3840
import org.springframework.http.converter.HttpMessageNotWritableException;
41+
import org.springframework.http.converter.MethodParameterHttpMessageConverter;
3942
import org.springframework.util.Assert;
4043
import org.springframework.util.ClassUtils;
4144

@@ -57,7 +60,7 @@
5760
* @since 3.1.2
5861
*/
5962
public class MappingJackson2HttpMessageConverter extends AbstractHttpMessageConverter<Object>
60-
implements GenericHttpMessageConverter<Object> {
63+
implements GenericHttpMessageConverter<Object>, MethodParameterHttpMessageConverter<Object> {
6164

6265
public static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8");
6366

@@ -147,6 +150,10 @@ private void configurePrettyPrint() {
147150
}
148151
}
149152

153+
@Override
154+
public boolean canRead(Class<?> clazz, MediaType mediaType, MethodParameter parameter) {
155+
return canRead(clazz, mediaType);
156+
}
150157

151158
@Override
152159
public boolean canRead(Class<?> clazz, MediaType mediaType) {
@@ -198,6 +205,11 @@ public boolean canWrite(Class<?> clazz, MediaType mediaType) {
198205
return false;
199206
}
200207

208+
@Override
209+
public boolean canWrite(Class<?> clazz, MediaType mediaType, MethodParameter parameter) {
210+
return canWrite(clazz, mediaType);
211+
}
212+
201213
@Override
202214
protected boolean supports(Class<?> clazz) {
203215
// should not be called, since we override canRead/Write instead
@@ -212,6 +224,11 @@ protected Object readInternal(Class<?> clazz, HttpInputMessage inputMessage)
212224
return readJavaType(javaType, inputMessage);
213225
}
214226

227+
@Override
228+
public Object read(Class<?> clazz, HttpInputMessage inputMessage, MethodParameter parameter) throws IOException, HttpMessageNotReadableException {
229+
return super.read(clazz, inputMessage);
230+
}
231+
215232
@Override
216233
public Object read(Type type, Class<?> contextClass, HttpInputMessage inputMessage)
217234
throws IOException, HttpMessageNotReadableException {
@@ -250,13 +267,35 @@ protected void writeInternal(Object object, HttpOutputMessage outputMessage)
250267
if (this.jsonPrefix != null) {
251268
jsonGenerator.writeRaw(this.jsonPrefix);
252269
}
253-
this.objectMapper.writeValue(jsonGenerator, object);
270+
if (object instanceof MappingJacksonValueHolder) {
271+
MappingJacksonValueHolder valueHolder = (MappingJacksonValueHolder) object;
272+
object = valueHolder.getValue();
273+
Class<?> serializationView = valueHolder.getSerializationView();
274+
this.objectMapper.writerWithView(serializationView).writeValue(jsonGenerator, object);
275+
}
276+
else {
277+
this.objectMapper.writeValue(jsonGenerator, object);
278+
}
254279
}
255280
catch (JsonProcessingException ex) {
256281
throw new HttpMessageNotWritableException("Could not write JSON: " + ex.getMessage(), ex);
257282
}
258283
}
259284

285+
@Override
286+
public void write(Object object, MediaType contentType, HttpOutputMessage outputMessage, MethodParameter parameter)
287+
throws IOException, HttpMessageNotWritableException {
288+
289+
JsonView annot = parameter.getMethodAnnotation(JsonView.class);
290+
if (annot != null && annot.value().length != 0) {
291+
MappingJacksonValueHolder serializationValue = new MappingJacksonValueHolder(object, annot.value()[0]);
292+
super.write(serializationValue, contentType, outputMessage);
293+
}
294+
else {
295+
super.write(object, contentType, outputMessage);
296+
}
297+
}
298+
260299
/**
261300
* Return the Jackson {@link JavaType} for the specified type and context class.
262301
* <p>The default implementation returns {@code typeFactory.constructType(type, contextClass)},
@@ -298,4 +337,20 @@ protected JsonEncoding getJsonEncoding(MediaType contentType) {
298337
return JsonEncoding.UTF8;
299338
}
300339

340+
@Override
341+
protected MediaType getDefaultContentType(Object object) throws IOException {
342+
if (object instanceof MappingJacksonValueHolder) {
343+
object = ((MappingJacksonValueHolder) object).getValue();
344+
}
345+
return super.getDefaultContentType(object);
346+
}
347+
348+
@Override
349+
protected Long getContentLength(Object object, MediaType contentType) throws IOException {
350+
if (object instanceof MappingJacksonValueHolder) {
351+
object = ((MappingJacksonValueHolder) object).getValue();
352+
}
353+
return super.getContentLength(object, contentType);
354+
}
355+
301356
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
/*
2+
* Copyright 2002-2014 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.http.converter.json;
18+
19+
/**
20+
* Holds an Object to be serialized via Jackson together with a serialization
21+
* view to be applied.
22+
*
23+
* @author Rossen Stoyanchev
24+
* @since 4.1
25+
*
26+
* @see com.fasterxml.jackson.annotation.JsonView
27+
*/
28+
public class MappingJacksonValueHolder {
29+
30+
private final Object value;
31+
32+
private final Class<?> serializationView;
33+
34+
35+
/**
36+
* Create a new instance.
37+
* @param value the Object to be serialized
38+
* @param serializationView the view to be applied
39+
*/
40+
public MappingJacksonValueHolder(Object value, Class<?> serializationView) {
41+
this.value = value;
42+
this.serializationView = serializationView;
43+
}
44+
45+
46+
/**
47+
* Return the value to be serialized.
48+
*/
49+
public Object getValue() {
50+
return this.value;
51+
}
52+
53+
/**
54+
* Return the serialization view to use.
55+
*/
56+
public Class<?> getSerializationView() {
57+
return this.serializationView;
58+
}
59+
60+
}

spring-web/src/test/java/org/springframework/http/converter/json/Jackson2ObjectMapperFactoryBeanTests.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ public void testUnknownFeature() {
9090
public void testBooleanSetters() {
9191
this.factory.setAutoDetectFields(false);
9292
this.factory.setAutoDetectGettersSetters(false);
93+
this.factory.setDefaultViewInclusion(false);
9394
this.factory.setFailOnEmptyBeans(false);
9495
this.factory.setIndentOutput(true);
9596
this.factory.afterPropertiesSet();
@@ -100,6 +101,7 @@ public void testBooleanSetters() {
100101
assertFalse(objectMapper.getDeserializationConfig().isEnabled(MapperFeature.AUTO_DETECT_FIELDS));
101102
assertFalse(objectMapper.getSerializationConfig().isEnabled(MapperFeature.AUTO_DETECT_GETTERS));
102103
assertFalse(objectMapper.getDeserializationConfig().isEnabled(MapperFeature.AUTO_DETECT_SETTERS));
104+
assertFalse(objectMapper.getDeserializationConfig().isEnabled(MapperFeature.DEFAULT_VIEW_INCLUSION));
103105
assertFalse(objectMapper.getSerializationConfig().isEnabled(SerializationFeature.FAIL_ON_EMPTY_BEANS));
104106
assertTrue(objectMapper.getSerializationConfig().isEnabled(SerializationFeature.INDENT_OUTPUT));
105107
assertTrue(objectMapper.getSerializationConfig().getSerializationInclusion() == JsonInclude.Include.ALWAYS);
@@ -253,6 +255,7 @@ public void testCompleteSetup() {
253255
assertTrue(objectMapper.getFactory().isEnabled(JsonGenerator.Feature.WRITE_NUMBERS_AS_STRINGS));
254256

255257
assertFalse(objectMapper.getSerializationConfig().isEnabled(MapperFeature.AUTO_DETECT_GETTERS));
258+
assertTrue(objectMapper.getDeserializationConfig().isEnabled(MapperFeature.DEFAULT_VIEW_INCLUSION));
256259
assertFalse(objectMapper.getDeserializationConfig().isEnabled(MapperFeature.AUTO_DETECT_FIELDS));
257260
assertFalse(objectMapper.getFactory().isEnabled(JsonParser.Feature.AUTO_CLOSE_SOURCE));
258261
assertFalse(objectMapper.getFactory().isEnabled(JsonGenerator.Feature.QUOTE_FIELD_NAMES));

spring-web/src/test/java/org/springframework/http/converter/json/MappingJackson2HttpMessageConverterTests.java

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import java.util.List;
2525
import java.util.Map;
2626

27+
import com.fasterxml.jackson.annotation.JsonView;
2728
import com.fasterxml.jackson.databind.JavaType;
2829
import com.fasterxml.jackson.databind.ObjectMapper;
2930
import org.junit.Test;
@@ -237,6 +238,22 @@ public void prefixJsonCustom() throws Exception {
237238
assertEquals(")]}',\"foo\"", outputMessage.getBodyAsString(Charset.forName("UTF-8")));
238239
}
239240

241+
@Test
242+
public void jsonView() throws Exception {
243+
MockHttpOutputMessage outputMessage = new MockHttpOutputMessage();
244+
JacksonViewBean bean = new JacksonViewBean();
245+
bean.setWithView1("with");
246+
bean.setWithView2("with");
247+
bean.setWithoutView("without");
248+
MappingJacksonValueHolder jsv = new MappingJacksonValueHolder(bean, MyJacksonView1.class);
249+
this.converter.writeInternal(jsv, outputMessage);
250+
251+
String result = outputMessage.getBodyAsString(Charset.forName("UTF-8"));
252+
assertTrue(result.contains("\"withView1\":\"with\""));
253+
assertFalse(result.contains("\"withView2\":\"with\""));
254+
assertTrue(result.contains("\"withoutView\":\"without\""));
255+
}
256+
240257

241258
public static class MyBean {
242259

@@ -315,4 +332,42 @@ public void setName(String name) {
315332
}
316333
}
317334

335+
private interface MyJacksonView1 {};
336+
private interface MyJacksonView2 {};
337+
338+
private static class JacksonViewBean {
339+
340+
@JsonView(MyJacksonView1.class)
341+
private String withView1;
342+
343+
@JsonView(MyJacksonView2.class)
344+
private String withView2;
345+
346+
private String withoutView;
347+
348+
public String getWithView1() {
349+
return withView1;
350+
}
351+
352+
public void setWithView1(String withView1) {
353+
this.withView1 = withView1;
354+
}
355+
356+
public String getWithView2() {
357+
return withView2;
358+
}
359+
360+
public void setWithView2(String withView2) {
361+
this.withView2 = withView2;
362+
}
363+
364+
public String getWithoutView() {
365+
return withoutView;
366+
}
367+
368+
public void setWithoutView(String withoutView) {
369+
this.withoutView = withoutView;
370+
}
371+
}
372+
318373
}

0 commit comments

Comments
 (0)