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
This commit is contained in:
parent
673a497923
commit
be0b69cbf1
|
|
@ -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;
|
||||
|
||||
}
|
||||
|
|
@ -244,6 +244,13 @@ public class Jackson2ObjectMapperFactoryBean implements FactoryBean<ObjectMapper
|
|||
this.features.put(MapperFeature.AUTO_DETECT_SETTERS, 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.
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ import java.lang.reflect.Type;
|
|||
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,6 +30,7 @@ import com.fasterxml.jackson.databind.JavaType;
|
|||
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;
|
||||
|
|
@ -36,6 +38,7 @@ 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 @@ import org.springframework.util.ClassUtils;
|
|||
* @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 @@ 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.
|
||||
* <p>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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 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));
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
||||
|
|
|
|||
|
|
@ -63,7 +63,7 @@ public class AsyncRestTemplateIntegrationTests extends AbstractJettyServerTestCa
|
|||
ResponseEntity<String> 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<String> 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());
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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<String> 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<MySampleBean> entity = new HttpEntity<MySampleBean>(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<MappingJacksonValueHolder> entity = new HttpEntity<MappingJacksonValueHolder>(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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<T> c = (MethodParameterHttpMessageConverter<T>) 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<T>) messageConverter).write(returnValue, selectedMediaType, outputMessage);
|
||||
if (logger.isDebugEnabled()) {
|
||||
|
|
|
|||
|
|
@ -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.<String, Object>emptyMap());
|
||||
}
|
||||
|
||||
protected void writeContent(OutputStream stream, Object value, String jsonPrefix, Map<String, Object> 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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -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<HttpMessageConverter<?>> converters = new ArrayList<HttpMessageConverter<?>>();
|
||||
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<HttpMessageConverter<?>> converters = new ArrayList<HttpMessageConverter<?>>();
|
||||
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<SimpleBean> 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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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<String, Object> model = new HashMap<String, Object>();
|
||||
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();
|
||||
|
|
|
|||
|
|
@ -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<JacksonSerializationValue> entity = new HttpEntity<JacksonSerializationValue>(jsv);
|
||||
String s = template.postForObject("http://example.com/user", entity, String.class);
|
||||
----
|
||||
|
||||
|
||||
[[rest-message-conversion]]
|
||||
|
|
|
|||
Loading…
Reference in New Issue