Add Jackson 2 HttpMessageConverter and View

Jackson 2 uses completely new package names and new maven artifact ids.
This change adds Jackson 2 as an optional dependency and also provides
MappingJackson2HttpMessageConverter and MappingJackson2JsonView for use
with the new version.

The MVC namespace and the MVC Java config detect and use
MappingJackson2HttpMessageConverter if Jackson 2 is present.
Otherwise if Jackson 1.x is present,
then MappingJacksonHttpMessageConverter is used.

Issue: SPR-9302
This commit is contained in:
Rossen Stoyanchev
2012-05-09 13:07:25 -04:00
parent 6cca57afd3
commit e63ca04fdb
14 changed files with 971 additions and 167 deletions

View File

@@ -327,6 +327,7 @@ project('spring-web') {
compile("commons-httpclient:commons-httpclient:3.1", optional) compile("commons-httpclient:commons-httpclient:3.1", optional)
compile("org.apache.httpcomponents:httpclient:4.1.1", optional) compile("org.apache.httpcomponents:httpclient:4.1.1", optional)
compile("org.codehaus.jackson:jackson-mapper-asl:1.4.2", optional) compile("org.codehaus.jackson:jackson-mapper-asl:1.4.2", optional)
compile("com.fasterxml.jackson.core:jackson-databind:2.0.1", optional)
compile("taglibs:standard:1.1.2", optional) compile("taglibs:standard:1.1.2", optional)
compile("org.mortbay.jetty:jetty:6.1.9") { dep -> compile("org.mortbay.jetty:jetty:6.1.9") { dep ->
optional dep optional dep

View File

@@ -0,0 +1,188 @@
/*
* Copyright 2002-2012 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;
import java.io.IOException;
import java.nio.charset.Charset;
import java.util.List;
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.HttpMessageNotReadableException;
import org.springframework.http.converter.HttpMessageNotWritableException;
import org.springframework.util.Assert;
import com.fasterxml.jackson.core.JsonEncoding;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.ObjectMapper;
/**
* Implementation of {@link org.springframework.http.converter.HttpMessageConverter HttpMessageConverter}
* that can read and write JSON using <a href="http://jackson.codehaus.org/">Jackson 2's</a> {@link ObjectMapper}.
*
* <p>This converter can be used to bind to typed beans, or untyped {@link java.util.HashMap HashMap} instances.
*
* <p>By default, this converter supports {@code application/json}. This can be overridden by setting the
* {@link #setSupportedMediaTypes(List) supportedMediaTypes} property.
*
* @author Arjen Poutsma
* @author Keith Donald
* @since 3.2
* @see org.springframework.web.servlet.view.json.MappingJackson2JsonView
*/
public class MappingJackson2HttpMessageConverter extends AbstractHttpMessageConverter<Object> {
public static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8");
private ObjectMapper objectMapper = new ObjectMapper();
private boolean prefixJson = false;
/**
* Construct a new {@code BindingJacksonHttpMessageConverter}.
*/
public MappingJackson2HttpMessageConverter() {
super(new MediaType("application", "json", DEFAULT_CHARSET));
}
/**
* Set the {@code ObjectMapper} for this view. If not set, a default
* {@link ObjectMapper#ObjectMapper() ObjectMapper} is used.
* <p>Setting a custom-configured {@code ObjectMapper} is one way to take further control of the JSON
* serialization process. For example, an extended {@link org.codehaus.jackson.map.SerializerFactory}
* can be configured that provides custom serializers for specific types. The other option for refining
* the serialization process is to use Jackson's provided annotations on the types to be serialized,
* in which case a custom-configured ObjectMapper is unnecessary.
*/
public void setObjectMapper(ObjectMapper objectMapper) {
Assert.notNull(objectMapper, "ObjectMapper must not be null");
this.objectMapper = objectMapper;
}
/**
* Return the underlying {@code ObjectMapper} for this view.
*/
public ObjectMapper getObjectMapper() {
return this.objectMapper;
}
/**
* Indicate whether the JSON output by this view should be prefixed with "{} &&". Default is false.
* <p>Prefixing the JSON string in this manner is used to help prevent JSON Hijacking.
* The prefix renders the string syntactically invalid as a script so that it cannot be hijacked.
* This prefix does not affect the evaluation of JSON, but if JSON validation is performed on the
* string, the prefix would need to be ignored.
*/
public void setPrefixJson(boolean prefixJson) {
this.prefixJson = prefixJson;
}
@Override
public boolean canRead(Class<?> clazz, MediaType mediaType) {
JavaType javaType = getJavaType(clazz);
return (this.objectMapper.canDeserialize(javaType) && canRead(mediaType));
}
@Override
public boolean canWrite(Class<?> clazz, MediaType mediaType) {
return (this.objectMapper.canSerialize(clazz) && canWrite(mediaType));
}
@Override
protected boolean supports(Class<?> clazz) {
// should not be called, since we override canRead/Write instead
throw new UnsupportedOperationException();
}
@Override
protected Object readInternal(Class<?> clazz, HttpInputMessage inputMessage)
throws IOException, HttpMessageNotReadableException {
JavaType javaType = getJavaType(clazz);
try {
return this.objectMapper.readValue(inputMessage.getBody(), javaType);
}
catch (JsonProcessingException ex) {
throw new HttpMessageNotReadableException("Could not read JSON: " + ex.getMessage(), ex);
}
}
@Override
protected void writeInternal(Object object, HttpOutputMessage outputMessage)
throws IOException, HttpMessageNotWritableException {
JsonEncoding encoding = getJsonEncoding(outputMessage.getHeaders().getContentType());
JsonGenerator jsonGenerator =
this.objectMapper.getJsonFactory().createJsonGenerator(outputMessage.getBody(), encoding);
try {
if (this.prefixJson) {
jsonGenerator.writeRaw("{} && ");
}
this.objectMapper.writeValue(jsonGenerator, object);
}
catch (JsonProcessingException ex) {
throw new HttpMessageNotWritableException("Could not write JSON: " + ex.getMessage(), ex);
}
}
/**
* Return the Jackson {@link JavaType} for the specified class.
* <p>The default implementation returns {@link ObjectMapper#constructType(java.lang.reflect.Type)},
* but this can be overridden in subclasses, to allow for custom generic collection handling.
* For instance:
* <pre class="code">
* protected JavaType getJavaType(Class&lt;?&gt; clazz) {
* if (List.class.isAssignableFrom(clazz)) {
* return objectMapper.getTypeFactory().constructCollectionType(ArrayList.class, MyBean.class);
* } else {
* return super.getJavaType(clazz);
* }
* }
* </pre>
* @param clazz the class to return the java type for
* @return the java type
*/
protected JavaType getJavaType(Class<?> clazz) {
return objectMapper.constructType(clazz);
}
/**
* Determine the JSON encoding to use for the given content type.
* @param contentType the media type as requested by the caller
* @return the JSON encoding to use (never <code>null</code>)
*/
protected JsonEncoding getJsonEncoding(MediaType contentType) {
if (contentType != null && contentType.getCharSet() != null) {
Charset charset = contentType.getCharSet();
for (JsonEncoding encoding : JsonEncoding.values()) {
if (charset.name().equals(encoding.getJavaName())) {
return encoding;
}
}
}
return JsonEncoding.UTF8;
}
}

View File

@@ -1,5 +1,5 @@
/* /*
* Copyright 2002-2011 the original author or authors. * Copyright 2002-2012 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@@ -40,6 +40,7 @@ import org.springframework.http.converter.ResourceHttpMessageConverter;
import org.springframework.http.converter.StringHttpMessageConverter; import org.springframework.http.converter.StringHttpMessageConverter;
import org.springframework.http.converter.feed.AtomFeedHttpMessageConverter; import org.springframework.http.converter.feed.AtomFeedHttpMessageConverter;
import org.springframework.http.converter.feed.RssChannelHttpMessageConverter; import org.springframework.http.converter.feed.RssChannelHttpMessageConverter;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.http.converter.json.MappingJacksonHttpMessageConverter; import org.springframework.http.converter.json.MappingJacksonHttpMessageConverter;
import org.springframework.http.converter.xml.Jaxb2RootElementHttpMessageConverter; import org.springframework.http.converter.xml.Jaxb2RootElementHttpMessageConverter;
import org.springframework.http.converter.xml.SourceHttpMessageConverter; import org.springframework.http.converter.xml.SourceHttpMessageConverter;
@@ -118,6 +119,10 @@ public class RestTemplate extends InterceptingHttpAccessor implements RestOperat
private static final boolean jaxb2Present = private static final boolean jaxb2Present =
ClassUtils.isPresent("javax.xml.bind.Binder", RestTemplate.class.getClassLoader()); ClassUtils.isPresent("javax.xml.bind.Binder", RestTemplate.class.getClassLoader());
private static final boolean jackson2Present =
ClassUtils.isPresent("com.fasterxml.jackson.databind.ObjectMapper", RestTemplate.class.getClassLoader()) &&
ClassUtils.isPresent("com.fasterxml.jackson.core.JsonGenerator", RestTemplate.class.getClassLoader());
private static final boolean jacksonPresent = private static final boolean jacksonPresent =
ClassUtils.isPresent("org.codehaus.jackson.map.ObjectMapper", RestTemplate.class.getClassLoader()) && ClassUtils.isPresent("org.codehaus.jackson.map.ObjectMapper", RestTemplate.class.getClassLoader()) &&
ClassUtils.isPresent("org.codehaus.jackson.JsonGenerator", RestTemplate.class.getClassLoader()); ClassUtils.isPresent("org.codehaus.jackson.JsonGenerator", RestTemplate.class.getClassLoader());
@@ -143,7 +148,10 @@ public class RestTemplate extends InterceptingHttpAccessor implements RestOperat
if (jaxb2Present) { if (jaxb2Present) {
this.messageConverters.add(new Jaxb2RootElementHttpMessageConverter()); this.messageConverters.add(new Jaxb2RootElementHttpMessageConverter());
} }
if (jacksonPresent) { if (jackson2Present) {
this.messageConverters.add(new MappingJackson2HttpMessageConverter());
}
else if (jacksonPresent) {
this.messageConverters.add(new MappingJacksonHttpMessageConverter()); this.messageConverters.add(new MappingJacksonHttpMessageConverter());
} }
if (romePresent) { if (romePresent) {

View File

@@ -1,5 +1,5 @@
/* /*
* Copyright 2002-2010 the original author or authors. * Copyright 2002-2012 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@@ -23,29 +23,47 @@ import static org.junit.Assert.assertTrue;
import java.io.IOException; import java.io.IOException;
import java.nio.charset.Charset; import java.nio.charset.Charset;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import org.codehaus.jackson.map.type.TypeFactory; import org.codehaus.jackson.map.type.TypeFactory;
import org.codehaus.jackson.type.JavaType; import org.codehaus.jackson.type.JavaType;
import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.junit.runners.Parameterized.Parameters;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.http.MockHttpInputMessage; import org.springframework.http.MockHttpInputMessage;
import org.springframework.http.MockHttpOutputMessage; import org.springframework.http.MockHttpOutputMessage;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.HttpMessageNotReadableException; import org.springframework.http.converter.HttpMessageNotReadableException;
/** /**
* Jackson conversion tests parameterized with Jackson and Jackson 2 converters.
*
* @author Arjen Poutsma * @author Arjen Poutsma
* @author Rossen Stoyanchev
*/ */
@RunWith(Parameterized.class)
public class MappingJacksonHttpMessageConverterTests { public class MappingJacksonHttpMessageConverterTests {
private MappingJacksonHttpMessageConverter converter; private HttpMessageConverter<Object> converter;
@Before @Parameters
public void setUp() { public static Collection<Object[]> handlerTypes() {
converter = new MappingJacksonHttpMessageConverter(); Object[][] array = new Object[2][1];
array[0] = new Object[] { new MappingJackson2HttpMessageConverter()};
array[1] = new Object[] { new MappingJacksonHttpMessageConverter()};
return Arrays.asList(array);
}
public MappingJacksonHttpMessageConverterTests(HttpMessageConverter<Object> converter) {
this.converter = converter;
} }
@Test @Test

View File

@@ -1,5 +1,5 @@
/* /*
* Copyright 2002-2010 the original author or authors. * Copyright 2002-2012 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@@ -35,6 +35,7 @@ import org.springframework.http.converter.ResourceHttpMessageConverter;
import org.springframework.http.converter.StringHttpMessageConverter; import org.springframework.http.converter.StringHttpMessageConverter;
import org.springframework.http.converter.feed.AtomFeedHttpMessageConverter; import org.springframework.http.converter.feed.AtomFeedHttpMessageConverter;
import org.springframework.http.converter.feed.RssChannelHttpMessageConverter; import org.springframework.http.converter.feed.RssChannelHttpMessageConverter;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.http.converter.json.MappingJacksonHttpMessageConverter; import org.springframework.http.converter.json.MappingJacksonHttpMessageConverter;
import org.springframework.http.converter.xml.Jaxb2RootElementHttpMessageConverter; import org.springframework.http.converter.xml.Jaxb2RootElementHttpMessageConverter;
import org.springframework.http.converter.xml.SourceHttpMessageConverter; import org.springframework.http.converter.xml.SourceHttpMessageConverter;
@@ -125,6 +126,10 @@ class AnnotationDrivenBeanDefinitionParser implements BeanDefinitionParser {
private static final boolean jaxb2Present = private static final boolean jaxb2Present =
ClassUtils.isPresent("javax.xml.bind.Binder", AnnotationDrivenBeanDefinitionParser.class.getClassLoader()); ClassUtils.isPresent("javax.xml.bind.Binder", AnnotationDrivenBeanDefinitionParser.class.getClassLoader());
private static final boolean jackson2Present =
ClassUtils.isPresent("com.fasterxml.jackson.databind.ObjectMapper", AnnotationDrivenBeanDefinitionParser.class.getClassLoader()) &&
ClassUtils.isPresent("com.fasterxml.jackson.core.JsonGenerator", AnnotationDrivenBeanDefinitionParser.class.getClassLoader());
private static final boolean jacksonPresent = private static final boolean jacksonPresent =
ClassUtils.isPresent("org.codehaus.jackson.map.ObjectMapper", AnnotationDrivenBeanDefinitionParser.class.getClassLoader()) && ClassUtils.isPresent("org.codehaus.jackson.map.ObjectMapper", AnnotationDrivenBeanDefinitionParser.class.getClassLoader()) &&
ClassUtils.isPresent("org.codehaus.jackson.JsonGenerator", AnnotationDrivenBeanDefinitionParser.class.getClassLoader()); ClassUtils.isPresent("org.codehaus.jackson.JsonGenerator", AnnotationDrivenBeanDefinitionParser.class.getClassLoader());
@@ -309,7 +314,10 @@ class AnnotationDrivenBeanDefinitionParser implements BeanDefinitionParser {
messageConverters messageConverters
.add(createConverterBeanDefinition(Jaxb2RootElementHttpMessageConverter.class, source)); .add(createConverterBeanDefinition(Jaxb2RootElementHttpMessageConverter.class, source));
} }
if (jacksonPresent) { if (jackson2Present) {
messageConverters.add(createConverterBeanDefinition(MappingJackson2HttpMessageConverter.class, source));
}
else if (jacksonPresent) {
messageConverters.add(createConverterBeanDefinition(MappingJacksonHttpMessageConverter.class, source)); messageConverters.add(createConverterBeanDefinition(MappingJacksonHttpMessageConverter.class, source));
} }
if (romePresent) { if (romePresent) {

View File

@@ -41,6 +41,7 @@ import org.springframework.http.converter.ResourceHttpMessageConverter;
import org.springframework.http.converter.StringHttpMessageConverter; import org.springframework.http.converter.StringHttpMessageConverter;
import org.springframework.http.converter.feed.AtomFeedHttpMessageConverter; import org.springframework.http.converter.feed.AtomFeedHttpMessageConverter;
import org.springframework.http.converter.feed.RssChannelHttpMessageConverter; import org.springframework.http.converter.feed.RssChannelHttpMessageConverter;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.http.converter.json.MappingJacksonHttpMessageConverter; import org.springframework.http.converter.json.MappingJacksonHttpMessageConverter;
import org.springframework.http.converter.xml.Jaxb2RootElementHttpMessageConverter; import org.springframework.http.converter.xml.Jaxb2RootElementHttpMessageConverter;
import org.springframework.http.converter.xml.SourceHttpMessageConverter; import org.springframework.http.converter.xml.SourceHttpMessageConverter;
@@ -374,7 +375,10 @@ public class WebMvcConfigurationSupport implements ApplicationContextAware, Serv
if (ClassUtils.isPresent("javax.xml.bind.Binder", classLoader)) { if (ClassUtils.isPresent("javax.xml.bind.Binder", classLoader)) {
messageConverters.add(new Jaxb2RootElementHttpMessageConverter()); messageConverters.add(new Jaxb2RootElementHttpMessageConverter());
} }
if (ClassUtils.isPresent("org.codehaus.jackson.map.ObjectMapper", classLoader)) { if (ClassUtils.isPresent("com.fasterxml.jackson.databind.ObjectMapper", classLoader)) {
messageConverters.add(new MappingJackson2HttpMessageConverter());
}
else if (ClassUtils.isPresent("org.codehaus.jackson.map.ObjectMapper", classLoader)) {
messageConverters.add(new MappingJacksonHttpMessageConverter()); messageConverters.add(new MappingJacksonHttpMessageConverter());
} }
if (ClassUtils.isPresent("com.sun.syndication.feed.WireFeed", classLoader)) { if (ClassUtils.isPresent("com.sun.syndication.feed.WireFeed", classLoader)) {

View File

@@ -0,0 +1,220 @@
/*
* Copyright 2002-2012 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.web.servlet.view.json;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import com.fasterxml.jackson.core.JsonEncoding;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import org.springframework.validation.BindingResult;
import org.springframework.web.servlet.View;
import org.springframework.web.servlet.view.AbstractView;
/**
* Spring MVC {@link View} that renders JSON content by serializing the model for the current request
* using <a href="http://jackson.codehaus.org/">Jackson 2's</a> {@link ObjectMapper}.
*
* <p>By default, the entire contents of the model map (with the exception of framework-specific classes)
* will be encoded as JSON. If the model contains only one key, you can have it extracted encoded as JSON
* alone via {@link #setExtractValueFromSingleKeyModel}.
*
* @author Jeremy Grelle
* @author Arjen Poutsma
* @author Rossen Stoyanchev
* @since 3.2
* @see org.springframework.http.converter.json.MappingJackson2HttpMessageConverter
*/
public class MappingJackson2JsonView extends AbstractView {
/**
* Default content type. Overridable as bean property.
*/
public static final String DEFAULT_CONTENT_TYPE = "application/json";
private ObjectMapper objectMapper = new ObjectMapper();
private JsonEncoding encoding = JsonEncoding.UTF8;
private boolean prefixJson = false;
private Set<String> modelKeys;
private boolean extractValueFromSingleKeyModel = false;
private boolean disableCaching = true;
/**
* Construct a new {@code JacksonJsonView}, setting the content type to {@code application/json}.
*/
public MappingJackson2JsonView() {
setContentType(DEFAULT_CONTENT_TYPE);
setExposePathVariables(false);
}
/**
* Sets the {@code ObjectMapper} for this view.
* If not set, a default {@link ObjectMapper#ObjectMapper() ObjectMapper} is used.
* <p>Setting a custom-configured {@code ObjectMapper} is one way to take further control
* of the JSON serialization process. For example, an extended {@code SerializerFactory}
* can be configured that provides custom serializers for specific types. The other option
* for refining the serialization process is to use Jackson's provided annotations on the
* types to be serialized, in which case a custom-configured ObjectMapper is unnecessary.
*/
public void setObjectMapper(ObjectMapper objectMapper) {
Assert.notNull(objectMapper, "'objectMapper' must not be null");
this.objectMapper = objectMapper;
}
/**
* Set the {@code JsonEncoding} for this converter.
* By default, {@linkplain JsonEncoding#UTF8 UTF-8} is used.
*/
public void setEncoding(JsonEncoding encoding) {
Assert.notNull(encoding, "'encoding' must not be null");
this.encoding = encoding;
}
/**
* Indicates whether the JSON output by this view should be prefixed with <tt>"{} && "</tt>.
* Default is false.
* <p>Prefixing the JSON string in this manner is used to help prevent JSON Hijacking.
* The prefix renders the string syntactically invalid as a script so that it cannot be hijacked.
* This prefix does not affect the evaluation of JSON, but if JSON validation is performed
* on the string, the prefix would need to be ignored.
*/
public void setPrefixJson(boolean prefixJson) {
this.prefixJson = prefixJson;
}
/**
* Set the attribute in the model that should be rendered by this view.
* When set, all other model attributes will be ignored.
*/
public void setModelKey(String modelKey) {
this.modelKeys = Collections.singleton(modelKey);
}
/**
* Set the attributes in the model that should be rendered by this view.
* When set, all other model attributes will be ignored.
*/
public void setModelKeys(Set<String> modelKeys) {
this.modelKeys = modelKeys;
}
/**
* Return the attributes in the model that should be rendered by this view.
*/
public Set<String> getModelKeys() {
return this.modelKeys;
}
/**
* Set the attributes in the model that should be rendered by this view.
* When set, all other model attributes will be ignored.
* @deprecated use {@link #setModelKeys(Set)} instead
*/
@Deprecated
public void setRenderedAttributes(Set<String> renderedAttributes) {
this.modelKeys = renderedAttributes;
}
/**
* Return the attributes in the model that should be rendered by this view.
* @deprecated use {@link #getModelKeys()} instead
*/
@Deprecated
public Set<String> getRenderedAttributes() {
return this.modelKeys;
}
/**
* Set whether to serialize models containing a single attribute as a map or whether to
* extract the single value from the model and serialize it directly.
* <p>The effect of setting this flag is similar to using {@code MappingJacksonHttpMessageConverter}
* with an {@code @ResponseBody} request-handling method.
* <p>Default is {@code false}.
*/
public void setExtractValueFromSingleKeyModel(boolean extractValueFromSingleKeyModel) {
this.extractValueFromSingleKeyModel = extractValueFromSingleKeyModel;
}
/**
* Disables caching of the generated JSON.
* <p>Default is {@code true}, which will prevent the client from caching the generated JSON.
*/
public void setDisableCaching(boolean disableCaching) {
this.disableCaching = disableCaching;
}
@Override
protected void prepareResponse(HttpServletRequest request, HttpServletResponse response) {
response.setContentType(getContentType());
response.setCharacterEncoding(this.encoding.getJavaName());
if (this.disableCaching) {
response.addHeader("Pragma", "no-cache");
response.addHeader("Cache-Control", "no-cache, no-store, max-age=0");
response.addDateHeader("Expires", 1L);
}
}
@Override
protected void renderMergedOutputModel(Map<String, Object> model, HttpServletRequest request,
HttpServletResponse response) throws Exception {
Object value = filterModel(model);
JsonGenerator generator =
this.objectMapper.getJsonFactory().createJsonGenerator(response.getOutputStream(), this.encoding);
if (this.prefixJson) {
generator.writeRaw("{} && ");
}
this.objectMapper.writeValue(generator, value);
}
/**
* Filters out undesired attributes from the given model.
* The return value can be either another {@link Map} or a single value object.
* <p>The default implementation removes {@link BindingResult} instances and entries
* not included in the {@link #setRenderedAttributes renderedAttributes} property.
* @param model the model, as passed on to {@link #renderMergedOutputModel}
* @return the object to be rendered
*/
protected Object filterModel(Map<String, Object> model) {
Map<String, Object> result = new HashMap<String, Object>(model.size());
Set<String> renderedAttributes = (!CollectionUtils.isEmpty(this.modelKeys) ? this.modelKeys : model.keySet());
for (Map.Entry<String, Object> entry : model.entrySet()) {
if (!(entry.getValue() instanceof BindingResult) && renderedAttributes.contains(entry.getKey())) {
result.put(entry.getKey(), entry.getValue());
}
}
return (this.extractValueFromSingleKeyModel && result.size() == 1 ? result.values().iterator().next() : result);
}
}

View File

@@ -1,5 +1,5 @@
/* /*
* Copyright 2002-2011 the original author or authors. * Copyright 2002-2012 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@@ -35,7 +35,7 @@ import org.springframework.core.io.FileSystemResourceLoader;
import org.springframework.format.FormatterRegistry; import org.springframework.format.FormatterRegistry;
import org.springframework.format.support.FormattingConversionService; import org.springframework.format.support.FormattingConversionService;
import org.springframework.http.converter.HttpMessageConverter; import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.json.MappingJacksonHttpMessageConverter; import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockServletContext; import org.springframework.mock.web.MockServletContext;
import org.springframework.stereotype.Controller; import org.springframework.stereotype.Controller;
@@ -262,7 +262,7 @@ public class WebMvcConfigurationSupportTests {
@Override @Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) { public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
converters.add(new MappingJacksonHttpMessageConverter()); converters.add(new MappingJackson2HttpMessageConverter());
} }
@Override @Override

View File

@@ -0,0 +1,352 @@
/*
* Copyright 2002-2012 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.web.servlet.view.json;
import static org.easymock.EasyMock.createMock;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
import java.io.IOException;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import org.junit.Before;
import org.junit.Test;
import org.mozilla.javascript.Context;
import org.mozilla.javascript.ContextFactory;
import org.mozilla.javascript.ScriptableObject;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.validation.BindingResult;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.BeanProperty;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.databind.cfg.SerializerFactoryConfig;
import com.fasterxml.jackson.databind.ser.BasicSerializerFactory;
import com.fasterxml.jackson.databind.ser.BeanSerializerFactory;
import com.fasterxml.jackson.databind.ser.SerializerFactory;
import com.fasterxml.jackson.databind.ser.Serializers;
/**
* @author Jeremy Grelle
* @author Arjen Poutsma
* @author Rossen Stoyanchev
*/
public class MappingJackson2JsonViewTest {
private MappingJackson2JsonView view;
private MockHttpServletRequest request;
private MockHttpServletResponse response;
private Context jsContext;
private ScriptableObject jsScope;
@Before
public void setUp() {
request = new MockHttpServletRequest();
response = new MockHttpServletResponse();
jsContext = ContextFactory.getGlobal().enterContext();
jsScope = jsContext.initStandardObjects();
view = new MappingJackson2JsonView();
}
@Test
public void isExposePathVars() {
assertEquals("Must not expose path variables", false, view.isExposePathVariables());
}
@Test
public void renderSimpleMap() throws Exception {
Map<String, Object> model = new HashMap<String, Object>();
model.put("bindingResult", createMock("binding_result", BindingResult.class));
model.put("foo", "bar");
view.render(model, request, response);
assertEquals("no-cache", response.getHeader("Pragma"));
assertEquals("no-cache, no-store, max-age=0", response.getHeader("Cache-Control"));
assertNotNull(response.getHeader("Expires"));
assertEquals(MappingJacksonJsonView.DEFAULT_CONTENT_TYPE, response.getContentType());
String jsonResult = response.getContentAsString();
assertTrue(jsonResult.length() > 0);
validateResult();
}
@Test
public void renderCaching() throws Exception {
view.setDisableCaching(false);
Map<String, Object> model = new HashMap<String, Object>();
model.put("bindingResult", createMock("binding_result", BindingResult.class));
model.put("foo", "bar");
view.render(model, request, response);
assertNull(response.getHeader("Pragma"));
assertNull(response.getHeader("Cache-Control"));
assertNull(response.getHeader("Expires"));
}
@Test
public void renderSimpleMapPrefixed() throws Exception {
view.setPrefixJson(true);
renderSimpleMap();
}
@Test
public void renderSimpleBean() throws Exception {
Object bean = new TestBeanSimple();
Map<String, Object> model = new HashMap<String, Object>();
model.put("bindingResult", createMock("binding_result", BindingResult.class));
model.put("foo", bean);
view.render(model, request, response);
assertTrue(response.getContentAsString().length() > 0);
validateResult();
}
@Test
public void renderSimpleBeanPrefixed() throws Exception {
view.setPrefixJson(true);
renderSimpleBean();
}
@Test
public void renderWithCustomSerializerLocatedByAnnotation() throws Exception {
Object bean = new TestBeanSimpleAnnotated();
Map<String, Object> model = new HashMap<String, Object>();
model.put("foo", bean);
view.render(model, request, response);
assertTrue(response.getContentAsString().length() > 0);
assertEquals("{\"foo\":{\"testBeanSimple\":\"custom\"}}", response.getContentAsString());
validateResult();
}
@Test
public void renderWithCustomSerializerLocatedByFactory() throws Exception {
SerializerFactory factory = new DelegatingSerializerFactory(null);
ObjectMapper mapper = new ObjectMapper();
mapper.setSerializerFactory(factory);
view.setObjectMapper(mapper);
Object bean = new TestBeanSimple();
Map<String, Object> model = new HashMap<String, Object>();
model.put("foo", bean);
model.put("bar", new TestChildBean());
view.render(model, request, response);
String result = response.getContentAsString();
assertTrue(result.length() > 0);
assertTrue(result.contains("\"foo\":{\"testBeanSimple\":\"custom\"}"));
validateResult();
}
@Test
public void renderOnlyIncludedAttributes() throws Exception {
Set<String> attrs = new HashSet<String>();
attrs.add("foo");
attrs.add("baz");
attrs.add("nil");
view.setModelKeys(attrs);
Map<String, Object> model = new HashMap<String, Object>();
model.put("foo", "foo");
model.put("bar", "bar");
model.put("baz", "baz");
view.render(model, request, response);
String result = response.getContentAsString();
assertTrue(result.length() > 0);
assertTrue(result.contains("\"foo\":\"foo\""));
assertTrue(result.contains("\"baz\":\"baz\""));
validateResult();
}
@Test
public void filterSingleKeyModel() throws Exception {
view.setExtractValueFromSingleKeyModel(true);
Map<String, Object> model = new HashMap<String, Object>();
TestBeanSimple bean = new TestBeanSimple();
model.put("foo", bean);
Object actual = view.filterModel(model);
assertSame(bean, actual);
}
@SuppressWarnings("rawtypes")
@Test
public void filterTwoKeyModel() throws Exception {
view.setExtractValueFromSingleKeyModel(true);
Map<String, Object> model = new HashMap<String, Object>();
TestBeanSimple bean1 = new TestBeanSimple();
TestBeanSimple bean2 = new TestBeanSimple();
model.put("foo1", bean1);
model.put("foo2", bean2);
Object actual = view.filterModel(model);
assertTrue(actual instanceof Map);
assertSame(bean1, ((Map) actual).get("foo1"));
assertSame(bean2, ((Map) actual).get("foo2"));
}
private void validateResult() throws Exception {
Object jsResult =
jsContext.evaluateString(jsScope, "(" + response.getContentAsString() + ")", "JSON Stream", 1, null);
assertNotNull("Json Result did not eval as valid JavaScript", jsResult);
}
public static class TestBeanSimple {
private String value = "foo";
private boolean test = false;
private long number = 42;
private TestChildBean child = new TestChildBean();
public String getValue() {
return value;
}
public boolean getTest() {
return test;
}
public long getNumber() {
return number;
}
public Date getNow() {
return new Date();
}
public TestChildBean getChild() {
return child;
}
}
@JsonSerialize(using=TestBeanSimpleSerializer.class)
public static class TestBeanSimpleAnnotated extends TestBeanSimple {
}
public static class TestChildBean {
private String value = "bar";
private String baz = null;
private TestBeanSimple parent = null;
public String getValue() {
return value;
}
public String getBaz() {
return baz;
}
public TestBeanSimple getParent() {
return parent;
}
public void setParent(TestBeanSimple parent) {
this.parent = parent;
}
}
public static class TestBeanSimpleSerializer extends JsonSerializer<Object> {
@Override
public void serialize(Object value, JsonGenerator jgen, SerializerProvider provider) throws IOException {
jgen.writeStartObject();
jgen.writeFieldName("testBeanSimple");
jgen.writeString("custom");
jgen.writeEndObject();
}
}
public static class DelegatingSerializerFactory extends BasicSerializerFactory {
private SerializerFactory beanSerializer = BeanSerializerFactory.instance;
protected DelegatingSerializerFactory(SerializerFactoryConfig config) {
super(config);
}
@Override
public JsonSerializer<Object> createSerializer(SerializerProvider prov, JavaType type, BeanProperty property) throws JsonMappingException {
if (type.getRawClass() == TestBeanSimple.class) {
return new TestBeanSimpleSerializer();
}
else {
return beanSerializer.createSerializer(prov, type, property);
}
}
@Override
public SerializerFactory withConfig(SerializerFactoryConfig config) {
return null;
}
@Override
protected Iterable<Serializers> customSerializers() {
return null;
}
}
}

View File

@@ -38,6 +38,7 @@ import org.codehaus.jackson.map.SerializerFactory;
import org.codehaus.jackson.map.SerializerProvider; import org.codehaus.jackson.map.SerializerProvider;
import org.codehaus.jackson.map.annotate.JsonSerialize; import org.codehaus.jackson.map.annotate.JsonSerialize;
import org.codehaus.jackson.map.ser.BeanSerializerFactory; import org.codehaus.jackson.map.ser.BeanSerializerFactory;
import org.junit.Before; import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import org.mozilla.javascript.Context; import org.mozilla.javascript.Context;

View File

@@ -13,6 +13,7 @@ Changes in version 3.2 M1
* fix case-sensitivity issue with some containers on access to 'Content-Disposition' header * fix case-sensitivity issue with some containers on access to 'Content-Disposition' header
* add Servlet 3.0 based async support * add Servlet 3.0 based async support
* fix issue with encoded params in UriComponentsBuilder * fix issue with encoded params in UriComponentsBuilder
* add Jackson 2 HttpMessageConverter and View types
Changes in version 3.1.1 (2012-02-16) Changes in version 3.1.1 (2012-02-16)
------------------------------------- -------------------------------------

View File

@@ -2892,7 +2892,7 @@ public String upload(...) {
&lt;/property&gt; &lt;/property&gt;
&lt;property name="defaultViews"&gt; &lt;property name="defaultViews"&gt;
&lt;list&gt; &lt;list&gt;
&lt;bean class="org.springframework.web.servlet.view.json.MappingJacksonJsonView" /&gt; &lt;bean class="org.springframework.web.servlet.view.json.MappingJackson2JsonView" /&gt;
&lt;/list&gt; &lt;/list&gt;
&lt;/property&gt; &lt;/property&gt;
&lt;/bean&gt; &lt;/bean&gt;
@@ -2924,7 +2924,7 @@ public String upload(...) {
the <classname>SampleContentAtomView</classname> if the view name the <classname>SampleContentAtomView</classname> if the view name
returned is <classname>content</classname>. If the request is made with returned is <classname>content</classname>. If the request is made with
the file extension <literal>.json</literal>, the the file extension <literal>.json</literal>, the
<classname>MappingJacksonJsonView</classname> instance from the <classname>MappingJackson2JsonView</classname>instance from the
<literal>DefaultViews</literal> list will be selected regardless of the <literal>DefaultViews</literal> list will be selected regardless of the
view name. Alternatively, client requests can be made without a file view name. Alternatively, client requests can be made without a file
extension but with the <literal>Accept</literal> header set to the extension but with the <literal>Accept</literal> header set to the
@@ -3588,7 +3588,7 @@ public String onSubmit(<emphasis role="bold">@RequestPart("meta-data") MetaData
However, the <literal>@RequestPart("meta-data") MetaData</literal> However, the <literal>@RequestPart("meta-data") MetaData</literal>
method argument in this case is read as JSON content based on its method argument in this case is read as JSON content based on its
<literal>'Content-Type'</literal> header and converted with the help of <literal>'Content-Type'</literal> header and converted with the help of
the <classname>MappingJacksonHttpMessageConverter</classname>.</para> the <classname>MappingJackson2HttpMessageConverter</classname>.</para>
</section> </section>
</section> </section>
@@ -4256,9 +4256,10 @@ public class WebConfig {
</listitem> </listitem>
<listitem> <listitem>
<para><classname>MappingJacksonHttpMessageConverter</classname> <para><classname>MappingJackson2HttpMessageConverter</classname>
converts to/from JSON — added if Jackson is present on the (or <classname>MappingJacksonHttpMessageConverter</classname>)
classpath.</para> converts to/from JSON — added if Jackson 2 (or Jackson) is present
on the classpath.</para>
</listitem> </listitem>
<listitem> <listitem>

View File

@@ -1363,7 +1363,7 @@ if (HttpStatus.SC_CREATED == post.getStatusCode()) {
these defaults using the <methodname>messageConverters()</methodname> bean these defaults using the <methodname>messageConverters()</methodname> bean
property as would be required if using the property as would be required if using the
<classname>MarshallingHttpMessageConverter</classname> or <classname>MarshallingHttpMessageConverter</classname> or
<classname>MappingJacksonHttpMessageConverter</classname>.</para> <classname>MappingJackson2HttpMessageConverter</classname>.</para>
<para>Each method takes URI template arguments in two forms, either as a <para>Each method takes URI template arguments in two forms, either as a
<literal>String</literal> variable length argument or a <literal>String</literal> variable length argument or a
@@ -1608,7 +1608,7 @@ String body = response.getBody();</programlisting>
</section> </section>
<section id="rest-mapping-json-converter"> <section id="rest-mapping-json-converter">
<title>MappingJacksonHttpMessageConverter</title> <title>MappingJackson2HttpMessageConverter (or MappingJacksonHttpMessageConverter with Jackson 1.x)</title>
<para>An <interfacename>HttpMessageConverter</interfacename> <para>An <interfacename>HttpMessageConverter</interfacename>
implementation that can read and write JSON using Jackson's implementation that can read and write JSON using Jackson's

View File

@@ -2626,7 +2626,9 @@ simpleReport.reportDataKey=myBeanData</programlisting>
<section id="view-json-mapping"> <section id="view-json-mapping">
<title>JSON Mapping View</title> <title>JSON Mapping View</title>
<para>The <classname>MappingJacksonJsonView</classname> uses the Jackson <para>The <classname>MappingJackson2JsonView</classname>
(or <classname>MappingJacksonJsonView</classname> depending on the
the Jackson version you have) uses the Jackson
library's <classname>ObjectMapper</classname> to render the response content library's <classname>ObjectMapper</classname> to render the response content
as JSON. By default, the entire contents of the model map (with the exception as JSON. By default, the entire contents of the model map (with the exception
of framework-specific classes) will be encoded as JSON. For cases where the of framework-specific classes) will be encoded as JSON. For cases where the