Browse Source

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
pull/76/head
Rossen Stoyanchev 14 years ago
parent
commit
e63ca04fdb
  1. 1
      build.gradle
  2. 188
      spring-web/src/main/java/org/springframework/http/converter/json/MappingJackson2HttpMessageConverter.java
  3. 16
      spring-web/src/main/java/org/springframework/web/client/RestTemplate.java
  4. 30
      spring-web/src/test/java/org/springframework/http/converter/json/MappingJacksonHttpMessageConverterTests.java
  5. 54
      spring-webmvc/src/main/java/org/springframework/web/servlet/config/AnnotationDrivenBeanDefinitionParser.java
  6. 190
      spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurationSupport.java
  7. 220
      spring-webmvc/src/main/java/org/springframework/web/servlet/view/json/MappingJackson2JsonView.java
  8. 60
      spring-webmvc/src/test/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurationSupportTests.java
  9. 352
      spring-webmvc/src/test/java/org/springframework/web/servlet/view/json/MappingJackson2JsonViewTest.java
  10. 5
      spring-webmvc/src/test/java/org/springframework/web/servlet/view/json/MappingJacksonJsonViewTest.java
  11. 1
      src/dist/changelog.txt
  12. 13
      src/reference/docbook/mvc.xml
  13. 4
      src/reference/docbook/remoting.xml
  14. 4
      src/reference/docbook/view.xml

1
build.gradle

@ -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

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

@ -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;
}
}

16
spring-web/src/main/java/org/springframework/web/client/RestTemplate.java

@ -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) {
@ -384,7 +392,7 @@ public class RestTemplate extends InterceptingHttpAccessor implements RestOperat
return execute(url, method, requestCallback, responseExtractor, uriVariables); return execute(url, method, requestCallback, responseExtractor, uriVariables);
} }
public <T> ResponseEntity<T> exchange(URI url, HttpMethod method, HttpEntity<?> requestEntity, public <T> ResponseEntity<T> exchange(URI url, HttpMethod method, HttpEntity<?> requestEntity,
Class<T> responseType) throws RestClientException { Class<T> responseType) throws RestClientException {
HttpEntityRequestCallback requestCallback = new HttpEntityRequestCallback(requestEntity, responseType); HttpEntityRequestCallback requestCallback = new HttpEntityRequestCallback(requestEntity, responseType);
ResponseEntityResponseExtractor<T> responseExtractor = new ResponseEntityResponseExtractor<T>(responseType); ResponseEntityResponseExtractor<T> responseExtractor = new ResponseEntityResponseExtractor<T>(responseType);
@ -577,7 +585,7 @@ public class RestTemplate extends InterceptingHttpAccessor implements RestOperat
} }
if (logger.isDebugEnabled()) { if (logger.isDebugEnabled()) {
if (requestContentType != null) { if (requestContentType != null) {
logger.debug("Writing [" + requestBody + "] as \"" + requestContentType + logger.debug("Writing [" + requestBody + "] as \"" + requestContentType +
"\" using [" + messageConverter + "]"); "\" using [" + messageConverter + "]");
} }
else { else {

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

@ -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;
@Parameters
public static Collection<Object[]> handlerTypes() {
Object[][] array = new Object[2][1];
array[0] = new Object[] { new MappingJackson2HttpMessageConverter()};
array[1] = new Object[] { new MappingJacksonHttpMessageConverter()};
return Arrays.asList(array);
}
@Before public MappingJacksonHttpMessageConverterTests(HttpMessageConverter<Object> converter) {
public void setUp() { this.converter = converter;
converter = new MappingJacksonHttpMessageConverter();
} }
@Test @Test

54
spring-webmvc/src/main/java/org/springframework/web/servlet/config/AnnotationDrivenBeanDefinitionParser.java

@ -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;
@ -65,52 +66,52 @@ import org.springframework.web.servlet.mvc.support.DefaultHandlerExceptionResolv
import org.w3c.dom.Element; import org.w3c.dom.Element;
/** /**
* A {@link BeanDefinitionParser} that provides the configuration for the * A {@link BeanDefinitionParser} that provides the configuration for the
* {@code <annotation-driven/>} MVC namespace element. * {@code <annotation-driven/>} MVC namespace element.
* *
* <p>This class registers the following {@link HandlerMapping}s:</p> * <p>This class registers the following {@link HandlerMapping}s:</p>
* <ul> * <ul>
* <li>{@link RequestMappingHandlerMapping} * <li>{@link RequestMappingHandlerMapping}
* ordered at 0 for mapping requests to annotated controller methods. * ordered at 0 for mapping requests to annotated controller methods.
* <li>{@link BeanNameUrlHandlerMapping} * <li>{@link BeanNameUrlHandlerMapping}
* ordered at 2 to map URL paths to controller bean names. * ordered at 2 to map URL paths to controller bean names.
* </ul> * </ul>
* *
* <p><strong>Note:</strong> Additional HandlerMappings may be registered * <p><strong>Note:</strong> Additional HandlerMappings may be registered
* as a result of using the {@code <view-controller>} or the * as a result of using the {@code <view-controller>} or the
* {@code <resources>} MVC namespace elements. * {@code <resources>} MVC namespace elements.
* *
* <p>This class registers the following {@link HandlerAdapter}s: * <p>This class registers the following {@link HandlerAdapter}s:
* <ul> * <ul>
* <li>{@link RequestMappingHandlerAdapter} * <li>{@link RequestMappingHandlerAdapter}
* for processing requests with annotated controller methods. * for processing requests with annotated controller methods.
* <li>{@link HttpRequestHandlerAdapter} * <li>{@link HttpRequestHandlerAdapter}
* for processing requests with {@link HttpRequestHandler}s. * for processing requests with {@link HttpRequestHandler}s.
* <li>{@link SimpleControllerHandlerAdapter} * <li>{@link SimpleControllerHandlerAdapter}
* for processing requests with interface-based {@link Controller}s. * for processing requests with interface-based {@link Controller}s.
* </ul> * </ul>
* *
* <p>This class registers the following {@link HandlerExceptionResolver}s: * <p>This class registers the following {@link HandlerExceptionResolver}s:
* <ul> * <ul>
* <li>{@link ExceptionHandlerExceptionResolver} for handling exceptions * <li>{@link ExceptionHandlerExceptionResolver} for handling exceptions
* through @{@link ExceptionHandler} methods. * through @{@link ExceptionHandler} methods.
* <li>{@link ResponseStatusExceptionResolver} for exceptions annotated * <li>{@link ResponseStatusExceptionResolver} for exceptions annotated
* with @{@link ResponseStatus}. * with @{@link ResponseStatus}.
* <li>{@link DefaultHandlerExceptionResolver} for resolving known Spring * <li>{@link DefaultHandlerExceptionResolver} for resolving known Spring
* exception types * exception types
* </ul> * </ul>
* *
* <p>Both the {@link RequestMappingHandlerAdapter} and the * <p>Both the {@link RequestMappingHandlerAdapter} and the
* {@link ExceptionHandlerExceptionResolver} are configured with default * {@link ExceptionHandlerExceptionResolver} are configured with default
* instances of the following kind, unless custom instances are provided: * instances of the following kind, unless custom instances are provided:
* <ul> * <ul>
* <li>A {@link DefaultFormattingConversionService} * <li>A {@link DefaultFormattingConversionService}
* <li>A {@link LocalValidatorFactoryBean} if a JSR-303 implementation is * <li>A {@link LocalValidatorFactoryBean} if a JSR-303 implementation is
* available on the classpath * available on the classpath
* <li>A range of {@link HttpMessageConverter}s depending on what 3rd party * <li>A range of {@link HttpMessageConverter}s depending on what 3rd party
* libraries are available on the classpath. * libraries are available on the classpath.
* </ul> * </ul>
* *
* @author Keith Donald * @author Keith Donald
* @author Juergen Hoeller * @author Juergen Hoeller
* @author Arjen Poutsma * @author Arjen Poutsma
@ -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());
@ -158,7 +163,7 @@ class AnnotationDrivenBeanDefinitionParser implements BeanDefinitionParser {
ManagedList<?> messageConverters = getMessageConverters(element, source, parserContext); ManagedList<?> messageConverters = getMessageConverters(element, source, parserContext);
ManagedList<?> argumentResolvers = getArgumentResolvers(element, source, parserContext); ManagedList<?> argumentResolvers = getArgumentResolvers(element, source, parserContext);
ManagedList<?> returnValueHandlers = getReturnValueHandlers(element, source, parserContext); ManagedList<?> returnValueHandlers = getReturnValueHandlers(element, source, parserContext);
RootBeanDefinition methodAdapterDef = new RootBeanDefinition(RequestMappingHandlerAdapter.class); RootBeanDefinition methodAdapterDef = new RootBeanDefinition(RequestMappingHandlerAdapter.class);
methodAdapterDef.setSource(source); methodAdapterDef.setSource(source);
methodAdapterDef.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); methodAdapterDef.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
@ -215,7 +220,7 @@ class AnnotationDrivenBeanDefinitionParser implements BeanDefinitionParser {
parserContext.registerComponent(new BeanComponentDefinition(defaultExceptionResolver, defaultExceptionResolverName)); parserContext.registerComponent(new BeanComponentDefinition(defaultExceptionResolver, defaultExceptionResolverName));
parserContext.registerComponent(new BeanComponentDefinition(mappedCsInterceptorDef, mappedInterceptorName)); parserContext.registerComponent(new BeanComponentDefinition(mappedCsInterceptorDef, mappedInterceptorName));
// Ensure BeanNameUrlHandlerMapping (SPR-8289) and default HandlerAdapters are not "turned off" // Ensure BeanNameUrlHandlerMapping (SPR-8289) and default HandlerAdapters are not "turned off"
MvcNamespaceUtils.registerDefaultComponents(parserContext, source); MvcNamespaceUtils.registerDefaultComponents(parserContext, source);
parserContext.popAndRegisterContainingComponent(); parserContext.popAndRegisterContainingComponent();
@ -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) {

190
spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurationSupport.java

@ -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;
@ -74,59 +75,59 @@ import org.springframework.web.servlet.mvc.support.DefaultHandlerExceptionResolv
/** /**
* This is the main class providing the configuration behind the MVC Java config. * This is the main class providing the configuration behind the MVC Java config.
* It is typically imported by adding {@link EnableWebMvc @EnableWebMvc} to an * It is typically imported by adding {@link EnableWebMvc @EnableWebMvc} to an
* application {@link Configuration @Configuration} class. An alternative more * application {@link Configuration @Configuration} class. An alternative more
* advanced option is to extend directly from this class and override methods as * advanced option is to extend directly from this class and override methods as
* necessary remembering to add {@link Configuration @Configuration} to the * necessary remembering to add {@link Configuration @Configuration} to the
* subclass and {@link Bean @Bean} to overridden {@link Bean @Bean} methods. * subclass and {@link Bean @Bean} to overridden {@link Bean @Bean} methods.
* For more details see the Javadoc of {@link EnableWebMvc @EnableWebMvc}. * For more details see the Javadoc of {@link EnableWebMvc @EnableWebMvc}.
* *
* <p>This class registers the following {@link HandlerMapping}s:</p> * <p>This class registers the following {@link HandlerMapping}s:</p>
* <ul> * <ul>
* <li>{@link RequestMappingHandlerMapping} * <li>{@link RequestMappingHandlerMapping}
* ordered at 0 for mapping requests to annotated controller methods. * ordered at 0 for mapping requests to annotated controller methods.
* <li>{@link HandlerMapping} * <li>{@link HandlerMapping}
* ordered at 1 to map URL paths directly to view names. * ordered at 1 to map URL paths directly to view names.
* <li>{@link BeanNameUrlHandlerMapping} * <li>{@link BeanNameUrlHandlerMapping}
* ordered at 2 to map URL paths to controller bean names. * ordered at 2 to map URL paths to controller bean names.
* <li>{@link HandlerMapping} * <li>{@link HandlerMapping}
* ordered at {@code Integer.MAX_VALUE-1} to serve static resource requests. * ordered at {@code Integer.MAX_VALUE-1} to serve static resource requests.
* <li>{@link HandlerMapping} * <li>{@link HandlerMapping}
* ordered at {@code Integer.MAX_VALUE} to forward requests to the default servlet. * ordered at {@code Integer.MAX_VALUE} to forward requests to the default servlet.
* </ul> * </ul>
* *
* <p>Registers these {@link HandlerAdapter}s: * <p>Registers these {@link HandlerAdapter}s:
* <ul> * <ul>
* <li>{@link RequestMappingHandlerAdapter} * <li>{@link RequestMappingHandlerAdapter}
* for processing requests with annotated controller methods. * for processing requests with annotated controller methods.
* <li>{@link HttpRequestHandlerAdapter} * <li>{@link HttpRequestHandlerAdapter}
* for processing requests with {@link HttpRequestHandler}s. * for processing requests with {@link HttpRequestHandler}s.
* <li>{@link SimpleControllerHandlerAdapter} * <li>{@link SimpleControllerHandlerAdapter}
* for processing requests with interface-based {@link Controller}s. * for processing requests with interface-based {@link Controller}s.
* </ul> * </ul>
* *
* <p>Registers a {@link HandlerExceptionResolverComposite} with this chain of * <p>Registers a {@link HandlerExceptionResolverComposite} with this chain of
* exception resolvers: * exception resolvers:
* <ul> * <ul>
* <li>{@link ExceptionHandlerExceptionResolver} for handling exceptions * <li>{@link ExceptionHandlerExceptionResolver} for handling exceptions
* through @{@link ExceptionHandler} methods. * through @{@link ExceptionHandler} methods.
* <li>{@link ResponseStatusExceptionResolver} for exceptions annotated * <li>{@link ResponseStatusExceptionResolver} for exceptions annotated
* with @{@link ResponseStatus}. * with @{@link ResponseStatus}.
* <li>{@link DefaultHandlerExceptionResolver} for resolving known Spring * <li>{@link DefaultHandlerExceptionResolver} for resolving known Spring
* exception types * exception types
* </ul> * </ul>
* *
* <p>Both the {@link RequestMappingHandlerAdapter} and the * <p>Both the {@link RequestMappingHandlerAdapter} and the
* {@link ExceptionHandlerExceptionResolver} are configured with default * {@link ExceptionHandlerExceptionResolver} are configured with default
* instances of the following kind, unless custom instances are provided: * instances of the following kind, unless custom instances are provided:
* <ul> * <ul>
* <li>A {@link DefaultFormattingConversionService} * <li>A {@link DefaultFormattingConversionService}
* <li>A {@link LocalValidatorFactoryBean} if a JSR-303 implementation is * <li>A {@link LocalValidatorFactoryBean} if a JSR-303 implementation is
* available on the classpath * available on the classpath
* <li>A range of {@link HttpMessageConverter}s depending on the 3rd party * <li>A range of {@link HttpMessageConverter}s depending on the 3rd party
* libraries available on the classpath. * libraries available on the classpath.
* </ul> * </ul>
* *
* @see EnableWebMvc * @see EnableWebMvc
* @see WebMvcConfigurer * @see WebMvcConfigurer
* @see WebMvcConfigurerAdapter * @see WebMvcConfigurerAdapter
@ -151,9 +152,9 @@ public class WebMvcConfigurationSupport implements ApplicationContextAware, Serv
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext; this.applicationContext = applicationContext;
} }
/** /**
* Return a {@link RequestMappingHandlerMapping} ordered at 0 for mapping * Return a {@link RequestMappingHandlerMapping} ordered at 0 for mapping
* requests to annotated controllers. * requests to annotated controllers.
*/ */
@Bean @Bean
@ -163,11 +164,11 @@ public class WebMvcConfigurationSupport implements ApplicationContextAware, Serv
handlerMapping.setInterceptors(getInterceptors()); handlerMapping.setInterceptors(getInterceptors());
return handlerMapping; return handlerMapping;
} }
/** /**
* Provide access to the shared handler interceptors used to configure * Provide access to the shared handler interceptors used to configure
* {@link HandlerMapping} instances with. This method cannot be overridden, * {@link HandlerMapping} instances with. This method cannot be overridden,
* use {@link #addInterceptors(InterceptorRegistry)} instead. * use {@link #addInterceptors(InterceptorRegistry)} instead.
*/ */
protected final Object[] getInterceptors() { protected final Object[] getInterceptors() {
if (interceptors == null) { if (interceptors == null) {
@ -178,9 +179,9 @@ public class WebMvcConfigurationSupport implements ApplicationContextAware, Serv
} }
return interceptors.toArray(); return interceptors.toArray();
} }
/** /**
* Override this method to add Spring MVC interceptors for * Override this method to add Spring MVC interceptors for
* pre- and post-processing of controller invocation. * pre- and post-processing of controller invocation.
* @see InterceptorRegistry * @see InterceptorRegistry
*/ */
@ -188,15 +189,15 @@ public class WebMvcConfigurationSupport implements ApplicationContextAware, Serv
} }
/** /**
* Return a handler mapping ordered at 1 to map URL paths directly to * Return a handler mapping ordered at 1 to map URL paths directly to
* view names. To configure view controllers, override * view names. To configure view controllers, override
* {@link #addViewControllers}. * {@link #addViewControllers}.
*/ */
@Bean @Bean
public HandlerMapping viewControllerHandlerMapping() { public HandlerMapping viewControllerHandlerMapping() {
ViewControllerRegistry registry = new ViewControllerRegistry(); ViewControllerRegistry registry = new ViewControllerRegistry();
addViewControllers(registry); addViewControllers(registry);
AbstractHandlerMapping handlerMapping = registry.getHandlerMapping(); AbstractHandlerMapping handlerMapping = registry.getHandlerMapping();
handlerMapping = handlerMapping != null ? handlerMapping : new EmptyHandlerMapping(); handlerMapping = handlerMapping != null ? handlerMapping : new EmptyHandlerMapping();
handlerMapping.setInterceptors(getInterceptors()); handlerMapping.setInterceptors(getInterceptors());
@ -209,9 +210,9 @@ public class WebMvcConfigurationSupport implements ApplicationContextAware, Serv
*/ */
protected void addViewControllers(ViewControllerRegistry registry) { protected void addViewControllers(ViewControllerRegistry registry) {
} }
/** /**
* Return a {@link BeanNameUrlHandlerMapping} ordered at 2 to map URL * Return a {@link BeanNameUrlHandlerMapping} ordered at 2 to map URL
* paths to controller bean names. * paths to controller bean names.
*/ */
@Bean @Bean
@ -223,8 +224,8 @@ public class WebMvcConfigurationSupport implements ApplicationContextAware, Serv
} }
/** /**
* Return a handler mapping ordered at Integer.MAX_VALUE-1 with mapped * Return a handler mapping ordered at Integer.MAX_VALUE-1 with mapped
* resource handlers. To configure resource handling, override * resource handlers. To configure resource handling, override
* {@link #addResourceHandlers}. * {@link #addResourceHandlers}.
*/ */
@Bean @Bean
@ -237,16 +238,16 @@ public class WebMvcConfigurationSupport implements ApplicationContextAware, Serv
} }
/** /**
* Override this method to add resource handlers for serving static resources. * Override this method to add resource handlers for serving static resources.
* @see ResourceHandlerRegistry * @see ResourceHandlerRegistry
*/ */
protected void addResourceHandlers(ResourceHandlerRegistry registry) { protected void addResourceHandlers(ResourceHandlerRegistry registry) {
} }
/** /**
* Return a handler mapping ordered at Integer.MAX_VALUE with a mapped * Return a handler mapping ordered at Integer.MAX_VALUE with a mapped
* default servlet handler. To configure "default" Servlet handling, * default servlet handler. To configure "default" Servlet handling,
* override {@link #configureDefaultServletHandling}. * override {@link #configureDefaultServletHandling}.
*/ */
@Bean @Bean
public HandlerMapping defaultServletHandlerMapping() { public HandlerMapping defaultServletHandlerMapping() {
@ -258,15 +259,15 @@ public class WebMvcConfigurationSupport implements ApplicationContextAware, Serv
} }
/** /**
* Override this method to configure "default" Servlet handling. * Override this method to configure "default" Servlet handling.
* @see DefaultServletHandlerConfigurer * @see DefaultServletHandlerConfigurer
*/ */
protected void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) { protected void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
} }
/** /**
* Returns a {@link RequestMappingHandlerAdapter} for processing requests * Returns a {@link RequestMappingHandlerAdapter} for processing requests
* through annotated controller methods. Consider overriding one of these * through annotated controller methods. Consider overriding one of these
* other more fine-grained methods: * other more fine-grained methods:
* <ul> * <ul>
* <li>{@link #addArgumentResolvers} for adding custom argument resolvers. * <li>{@link #addArgumentResolvers} for adding custom argument resolvers.
@ -279,13 +280,13 @@ public class WebMvcConfigurationSupport implements ApplicationContextAware, Serv
ConfigurableWebBindingInitializer webBindingInitializer = new ConfigurableWebBindingInitializer(); ConfigurableWebBindingInitializer webBindingInitializer = new ConfigurableWebBindingInitializer();
webBindingInitializer.setConversionService(mvcConversionService()); webBindingInitializer.setConversionService(mvcConversionService());
webBindingInitializer.setValidator(mvcValidator()); webBindingInitializer.setValidator(mvcValidator());
List<HandlerMethodArgumentResolver> argumentResolvers = new ArrayList<HandlerMethodArgumentResolver>(); List<HandlerMethodArgumentResolver> argumentResolvers = new ArrayList<HandlerMethodArgumentResolver>();
addArgumentResolvers(argumentResolvers); addArgumentResolvers(argumentResolvers);
List<HandlerMethodReturnValueHandler> returnValueHandlers = new ArrayList<HandlerMethodReturnValueHandler>(); List<HandlerMethodReturnValueHandler> returnValueHandlers = new ArrayList<HandlerMethodReturnValueHandler>();
addReturnValueHandlers(returnValueHandlers); addReturnValueHandlers(returnValueHandlers);
RequestMappingHandlerAdapter adapter = new RequestMappingHandlerAdapter(); RequestMappingHandlerAdapter adapter = new RequestMappingHandlerAdapter();
adapter.setMessageConverters(getMessageConverters()); adapter.setMessageConverters(getMessageConverters());
adapter.setWebBindingInitializer(webBindingInitializer); adapter.setWebBindingInitializer(webBindingInitializer);
@ -295,40 +296,40 @@ public class WebMvcConfigurationSupport implements ApplicationContextAware, Serv
} }
/** /**
* Add custom {@link HandlerMethodArgumentResolver}s to use in addition to * Add custom {@link HandlerMethodArgumentResolver}s to use in addition to
* the ones registered by default. * the ones registered by default.
* <p>Custom argument resolvers are invoked before built-in resolvers * <p>Custom argument resolvers are invoked before built-in resolvers
* except for those that rely on the presence of annotations (e.g. * except for those that rely on the presence of annotations (e.g.
* {@code @RequestParameter}, {@code @PathVariable}, etc.). * {@code @RequestParameter}, {@code @PathVariable}, etc.).
* The latter can be customized by configuring the * The latter can be customized by configuring the
* {@link RequestMappingHandlerAdapter} directly. * {@link RequestMappingHandlerAdapter} directly.
* @param argumentResolvers the list of custom converters; * @param argumentResolvers the list of custom converters;
* initially an empty list. * initially an empty list.
*/ */
protected void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) { protected void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
} }
/** /**
* Add custom {@link HandlerMethodReturnValueHandler}s in addition to the * Add custom {@link HandlerMethodReturnValueHandler}s in addition to the
* ones registered by default. * ones registered by default.
* <p>Custom return value handlers are invoked before built-in ones except * <p>Custom return value handlers are invoked before built-in ones except
* for those that rely on the presence of annotations (e.g. * for those that rely on the presence of annotations (e.g.
* {@code @ResponseBody}, {@code @ModelAttribute}, etc.). * {@code @ResponseBody}, {@code @ModelAttribute}, etc.).
* The latter can be customized by configuring the * The latter can be customized by configuring the
* {@link RequestMappingHandlerAdapter} directly. * {@link RequestMappingHandlerAdapter} directly.
* @param returnValueHandlers the list of custom handlers; * @param returnValueHandlers the list of custom handlers;
* initially an empty list. * initially an empty list.
*/ */
protected void addReturnValueHandlers(List<HandlerMethodReturnValueHandler> returnValueHandlers) { protected void addReturnValueHandlers(List<HandlerMethodReturnValueHandler> returnValueHandlers) {
} }
/** /**
* Provides access to the shared {@link HttpMessageConverter}s used by the * Provides access to the shared {@link HttpMessageConverter}s used by the
* {@link RequestMappingHandlerAdapter} and the * {@link RequestMappingHandlerAdapter} and the
* {@link ExceptionHandlerExceptionResolver}. * {@link ExceptionHandlerExceptionResolver}.
* This method cannot be overridden. * This method cannot be overridden.
* Use {@link #configureMessageConverters(List)} instead. * Use {@link #configureMessageConverters(List)} instead.
* Also see {@link #addDefaultHttpMessageConverters(List)} that can be * Also see {@link #addDefaultHttpMessageConverters(List)} that can be
* used to add default message converters. * used to add default message converters.
*/ */
protected final List<HttpMessageConverter<?>> getMessageConverters() { protected final List<HttpMessageConverter<?>> getMessageConverters() {
@ -343,21 +344,21 @@ public class WebMvcConfigurationSupport implements ApplicationContextAware, Serv
} }
/** /**
* Override this method to add custom {@link HttpMessageConverter}s to use * Override this method to add custom {@link HttpMessageConverter}s to use
* with the {@link RequestMappingHandlerAdapter} and the * with the {@link RequestMappingHandlerAdapter} and the
* {@link ExceptionHandlerExceptionResolver}. Adding converters to the * {@link ExceptionHandlerExceptionResolver}. Adding converters to the
* list turns off the default converters that would otherwise be registered * list turns off the default converters that would otherwise be registered
* by default. Also see {@link #addDefaultHttpMessageConverters(List)} that * by default. Also see {@link #addDefaultHttpMessageConverters(List)} that
* can be used to add default message converters. * can be used to add default message converters.
* @param converters a list to add message converters to; * @param converters a list to add message converters to;
* initially an empty list. * initially an empty list.
*/ */
protected void configureMessageConverters(List<HttpMessageConverter<?>> converters) { protected void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
} }
/** /**
* Adds a set of default HttpMessageConverter instances to the given list. * Adds a set of default HttpMessageConverter instances to the given list.
* Subclasses can call this method from {@link #configureMessageConverters(List)}. * Subclasses can call this method from {@link #configureMessageConverters(List)}.
* @param messageConverters the list to add the default message converters to * @param messageConverters the list to add the default message converters to
*/ */
protected final void addDefaultHttpMessageConverters(List<HttpMessageConverter<?>> messageConverters) { protected final void addDefaultHttpMessageConverters(List<HttpMessageConverter<?>> messageConverters) {
@ -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)) {
@ -382,10 +386,10 @@ public class WebMvcConfigurationSupport implements ApplicationContextAware, Serv
messageConverters.add(new RssChannelHttpMessageConverter()); messageConverters.add(new RssChannelHttpMessageConverter());
} }
} }
/** /**
* Returns a {@link FormattingConversionService} for use with annotated * Returns a {@link FormattingConversionService} for use with annotated
* controller methods and the {@code spring:eval} JSP tag. * controller methods and the {@code spring:eval} JSP tag.
* Also see {@link #addFormatters} as an alternative to overriding this method. * Also see {@link #addFormatters} as an alternative to overriding this method.
*/ */
@Bean @Bean
@ -402,11 +406,11 @@ public class WebMvcConfigurationSupport implements ApplicationContextAware, Serv
} }
/** /**
* Returns a global {@link Validator} instance for example for validating * Returns a global {@link Validator} instance for example for validating
* {@code @ModelAttribute} and {@code @RequestBody} method arguments. * {@code @ModelAttribute} and {@code @RequestBody} method arguments.
* Delegates to {@link #getValidator()} first and if that returns {@code null} * Delegates to {@link #getValidator()} first and if that returns {@code null}
* checks the classpath for the presence of a JSR-303 implementations * checks the classpath for the presence of a JSR-303 implementations
* before creating a {@code LocalValidatorFactoryBean}.If a JSR-303 * before creating a {@code LocalValidatorFactoryBean}.If a JSR-303
* implementation is not available, a no-op {@link Validator} is returned. * implementation is not available, a no-op {@link Validator} is returned.
*/ */
@Bean @Bean
@ -446,7 +450,7 @@ public class WebMvcConfigurationSupport implements ApplicationContextAware, Serv
} }
/** /**
* Returns a {@link HttpRequestHandlerAdapter} for processing requests * Returns a {@link HttpRequestHandlerAdapter} for processing requests
* with {@link HttpRequestHandler}s. * with {@link HttpRequestHandler}s.
*/ */
@Bean @Bean
@ -455,7 +459,7 @@ public class WebMvcConfigurationSupport implements ApplicationContextAware, Serv
} }
/** /**
* Returns a {@link SimpleControllerHandlerAdapter} for processing requests * Returns a {@link SimpleControllerHandlerAdapter} for processing requests
* with interface-based controllers. * with interface-based controllers.
*/ */
@Bean @Bean
@ -465,23 +469,23 @@ public class WebMvcConfigurationSupport implements ApplicationContextAware, Serv
/** /**
* Returns a {@link HandlerExceptionResolverComposite} containing a list * Returns a {@link HandlerExceptionResolverComposite} containing a list
* of exception resolvers obtained either through * of exception resolvers obtained either through
* {@link #configureHandlerExceptionResolvers(List)} or through * {@link #configureHandlerExceptionResolvers(List)} or through
* {@link #addDefaultHandlerExceptionResolvers(List)}. * {@link #addDefaultHandlerExceptionResolvers(List)}.
* <p><strong>Note:</strong> This method cannot be made final due to CGLib * <p><strong>Note:</strong> This method cannot be made final due to CGLib
* constraints. Rather than overriding it, consider overriding * constraints. Rather than overriding it, consider overriding
* {@link #configureHandlerExceptionResolvers(List)}, which allows * {@link #configureHandlerExceptionResolvers(List)}, which allows
* providing a list of resolvers. * providing a list of resolvers.
*/ */
@Bean @Bean
public HandlerExceptionResolver handlerExceptionResolver() { public HandlerExceptionResolver handlerExceptionResolver() {
List<HandlerExceptionResolver> exceptionResolvers = new ArrayList<HandlerExceptionResolver>(); List<HandlerExceptionResolver> exceptionResolvers = new ArrayList<HandlerExceptionResolver>();
configureHandlerExceptionResolvers(exceptionResolvers); configureHandlerExceptionResolvers(exceptionResolvers);
if (exceptionResolvers.isEmpty()) { if (exceptionResolvers.isEmpty()) {
addDefaultHandlerExceptionResolvers(exceptionResolvers); addDefaultHandlerExceptionResolvers(exceptionResolvers);
} }
HandlerExceptionResolverComposite composite = new HandlerExceptionResolverComposite(); HandlerExceptionResolverComposite composite = new HandlerExceptionResolverComposite();
composite.setOrder(0); composite.setOrder(0);
composite.setExceptionResolvers(exceptionResolvers); composite.setExceptionResolvers(exceptionResolvers);
@ -489,27 +493,27 @@ public class WebMvcConfigurationSupport implements ApplicationContextAware, Serv
} }
/** /**
* Override this method to configure the list of * Override this method to configure the list of
* {@link HandlerExceptionResolver}s to use. Adding resolvers to the list * {@link HandlerExceptionResolver}s to use. Adding resolvers to the list
* turns off the default resolvers that would otherwise be registered by * turns off the default resolvers that would otherwise be registered by
* default. Also see {@link #addDefaultHandlerExceptionResolvers(List)} * default. Also see {@link #addDefaultHandlerExceptionResolvers(List)}
* that can be used to add the default exception resolvers. * that can be used to add the default exception resolvers.
* @param exceptionResolvers a list to add exception resolvers to; * @param exceptionResolvers a list to add exception resolvers to;
* initially an empty list. * initially an empty list.
*/ */
protected void configureHandlerExceptionResolvers(List<HandlerExceptionResolver> exceptionResolvers) { protected void configureHandlerExceptionResolvers(List<HandlerExceptionResolver> exceptionResolvers) {
} }
/** /**
* A method available to subclasses for adding default * A method available to subclasses for adding default
* {@link HandlerExceptionResolver}s. * {@link HandlerExceptionResolver}s.
* <p>Adds the following exception resolvers: * <p>Adds the following exception resolvers:
* <ul> * <ul>
* <li>{@link ExceptionHandlerExceptionResolver} * <li>{@link ExceptionHandlerExceptionResolver}
* for handling exceptions through @{@link ExceptionHandler} methods. * for handling exceptions through @{@link ExceptionHandler} methods.
* <li>{@link ResponseStatusExceptionResolver} * <li>{@link ResponseStatusExceptionResolver}
* for exceptions annotated with @{@link ResponseStatus}. * for exceptions annotated with @{@link ResponseStatus}.
* <li>{@link DefaultHandlerExceptionResolver} * <li>{@link DefaultHandlerExceptionResolver}
* for resolving known Spring exception types * for resolving known Spring exception types
* </ul> * </ul>
*/ */
@ -524,11 +528,11 @@ public class WebMvcConfigurationSupport implements ApplicationContextAware, Serv
} }
private final static class EmptyHandlerMapping extends AbstractHandlerMapping { private final static class EmptyHandlerMapping extends AbstractHandlerMapping {
@Override @Override
protected Object getHandlerInternal(HttpServletRequest request) throws Exception { protected Object getHandlerInternal(HttpServletRequest request) throws Exception {
return null; return null;
} }
} }
} }

220
spring-webmvc/src/main/java/org/springframework/web/servlet/view/json/MappingJackson2JsonView.java

@ -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);
}
}

60
spring-webmvc/src/test/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurationSupportTests.java

@ -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;
@ -73,12 +73,12 @@ public class WebMvcConfigurationSupportTests {
public void setUp() { public void setUp() {
mvcConfiguration = new TestWebMvcConfiguration(); mvcConfiguration = new TestWebMvcConfiguration();
} }
@Test @Test
public void requestMappingHandlerMapping() throws Exception { public void requestMappingHandlerMapping() throws Exception {
StaticWebApplicationContext cxt = new StaticWebApplicationContext(); StaticWebApplicationContext cxt = new StaticWebApplicationContext();
cxt.registerSingleton("controller", TestController.class); cxt.registerSingleton("controller", TestController.class);
RequestMappingHandlerMapping handlerMapping = mvcConfiguration.requestMappingHandlerMapping(); RequestMappingHandlerMapping handlerMapping = mvcConfiguration.requestMappingHandlerMapping();
assertEquals(0, handlerMapping.getOrder()); assertEquals(0, handlerMapping.getOrder());
@ -95,7 +95,7 @@ public class WebMvcConfigurationSupportTests {
assertEquals(Integer.MAX_VALUE, handlerMapping.getOrder()); assertEquals(Integer.MAX_VALUE, handlerMapping.getOrder());
assertTrue(handlerMapping.getClass().getName().endsWith("EmptyHandlerMapping")); assertTrue(handlerMapping.getClass().getName().endsWith("EmptyHandlerMapping"));
} }
@Test @Test
public void beanNameHandlerMapping() throws Exception { public void beanNameHandlerMapping() throws Exception {
StaticWebApplicationContext cxt = new StaticWebApplicationContext(); StaticWebApplicationContext cxt = new StaticWebApplicationContext();
@ -112,7 +112,7 @@ public class WebMvcConfigurationSupportTests {
assertEquals(2, chain.getInterceptors().length); assertEquals(2, chain.getInterceptors().length);
assertEquals(ConversionServiceExposingInterceptor.class, chain.getInterceptors()[1].getClass()); assertEquals(ConversionServiceExposingInterceptor.class, chain.getInterceptors()[1].getClass());
} }
@Test @Test
public void emptyResourceHandlerMapping() { public void emptyResourceHandlerMapping() {
mvcConfiguration.setApplicationContext(new StaticWebApplicationContext()); mvcConfiguration.setApplicationContext(new StaticWebApplicationContext());
@ -121,7 +121,7 @@ public class WebMvcConfigurationSupportTests {
assertEquals(Integer.MAX_VALUE, handlerMapping.getOrder()); assertEquals(Integer.MAX_VALUE, handlerMapping.getOrder());
assertTrue(handlerMapping.getClass().getName().endsWith("EmptyHandlerMapping")); assertTrue(handlerMapping.getClass().getName().endsWith("EmptyHandlerMapping"));
} }
@Test @Test
public void emptyDefaultServletHandlerMapping() { public void emptyDefaultServletHandlerMapping() {
mvcConfiguration.setServletContext(new MockServletContext()); mvcConfiguration.setServletContext(new MockServletContext());
@ -130,7 +130,7 @@ public class WebMvcConfigurationSupportTests {
assertEquals(Integer.MAX_VALUE, handlerMapping.getOrder()); assertEquals(Integer.MAX_VALUE, handlerMapping.getOrder());
assertTrue(handlerMapping.getClass().getName().endsWith("EmptyHandlerMapping")); assertTrue(handlerMapping.getClass().getName().endsWith("EmptyHandlerMapping"));
} }
@Test @Test
public void requestMappingHandlerAdapter() throws Exception { public void requestMappingHandlerAdapter() throws Exception {
RequestMappingHandlerAdapter adapter = mvcConfiguration.requestMappingHandlerAdapter(); RequestMappingHandlerAdapter adapter = mvcConfiguration.requestMappingHandlerAdapter();
@ -145,29 +145,29 @@ public class WebMvcConfigurationSupportTests {
ConversionService conversionService = initializer.getConversionService(); ConversionService conversionService = initializer.getConversionService();
assertNotNull(conversionService); assertNotNull(conversionService);
assertTrue(conversionService instanceof FormattingConversionService); assertTrue(conversionService instanceof FormattingConversionService);
Validator validator = initializer.getValidator(); Validator validator = initializer.getValidator();
assertNotNull(validator); assertNotNull(validator);
assertTrue(validator instanceof LocalValidatorFactoryBean); assertTrue(validator instanceof LocalValidatorFactoryBean);
assertEquals(false, new DirectFieldAccessor(adapter).getPropertyValue("ignoreDefaultModelOnRedirect")); assertEquals(false, new DirectFieldAccessor(adapter).getPropertyValue("ignoreDefaultModelOnRedirect"));
} }
@Test @Test
public void handlerExceptionResolver() throws Exception { public void handlerExceptionResolver() throws Exception {
HandlerExceptionResolverComposite compositeResolver = HandlerExceptionResolverComposite compositeResolver =
(HandlerExceptionResolverComposite) mvcConfiguration.handlerExceptionResolver(); (HandlerExceptionResolverComposite) mvcConfiguration.handlerExceptionResolver();
assertEquals(0, compositeResolver.getOrder()); assertEquals(0, compositeResolver.getOrder());
List<HandlerExceptionResolver> expectedResolvers = new ArrayList<HandlerExceptionResolver>(); List<HandlerExceptionResolver> expectedResolvers = new ArrayList<HandlerExceptionResolver>();
mvcConfiguration.addDefaultHandlerExceptionResolvers(expectedResolvers); mvcConfiguration.addDefaultHandlerExceptionResolvers(expectedResolvers);
assertEquals(expectedResolvers.size(), compositeResolver.getExceptionResolvers().size()); assertEquals(expectedResolvers.size(), compositeResolver.getExceptionResolvers().size());
} }
@Test @Test
public void webMvcConfigurerExtensionHooks() throws Exception { public void webMvcConfigurerExtensionHooks() throws Exception {
StaticWebApplicationContext appCxt = new StaticWebApplicationContext(); StaticWebApplicationContext appCxt = new StaticWebApplicationContext();
appCxt.setServletContext(new MockServletContext(new FileSystemResourceLoader())); appCxt.setServletContext(new MockServletContext(new FileSystemResourceLoader()));
appCxt.registerSingleton("controller", TestController.class); appCxt.registerSingleton("controller", TestController.class);
@ -175,33 +175,33 @@ public class WebMvcConfigurationSupportTests {
WebConfig webConfig = new WebConfig(); WebConfig webConfig = new WebConfig();
webConfig.setApplicationContext(appCxt); webConfig.setApplicationContext(appCxt);
webConfig.setServletContext(appCxt.getServletContext()); webConfig.setServletContext(appCxt.getServletContext());
String actual = webConfig.mvcConversionService().convert(new TestBean(), String.class); String actual = webConfig.mvcConversionService().convert(new TestBean(), String.class);
assertEquals("converted", actual); assertEquals("converted", actual);
RequestMappingHandlerAdapter adapter = webConfig.requestMappingHandlerAdapter(); RequestMappingHandlerAdapter adapter = webConfig.requestMappingHandlerAdapter();
assertEquals(1, adapter.getMessageConverters().size()); assertEquals(1, adapter.getMessageConverters().size());
ConfigurableWebBindingInitializer initializer = (ConfigurableWebBindingInitializer) adapter.getWebBindingInitializer(); ConfigurableWebBindingInitializer initializer = (ConfigurableWebBindingInitializer) adapter.getWebBindingInitializer();
assertNotNull(initializer); assertNotNull(initializer);
BeanPropertyBindingResult bindingResult = new BeanPropertyBindingResult(null, ""); BeanPropertyBindingResult bindingResult = new BeanPropertyBindingResult(null, "");
initializer.getValidator().validate(null, bindingResult); initializer.getValidator().validate(null, bindingResult);
assertEquals("invalid", bindingResult.getAllErrors().get(0).getCode()); assertEquals("invalid", bindingResult.getAllErrors().get(0).getCode());
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
List<HandlerMethodArgumentResolver> argResolvers= (List<HandlerMethodArgumentResolver>) List<HandlerMethodArgumentResolver> argResolvers= (List<HandlerMethodArgumentResolver>)
new DirectFieldAccessor(adapter).getPropertyValue("customArgumentResolvers"); new DirectFieldAccessor(adapter).getPropertyValue("customArgumentResolvers");
assertEquals(1, argResolvers.size()); assertEquals(1, argResolvers.size());
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
List<HandlerMethodReturnValueHandler> handlers = (List<HandlerMethodReturnValueHandler>) List<HandlerMethodReturnValueHandler> handlers = (List<HandlerMethodReturnValueHandler>)
new DirectFieldAccessor(adapter).getPropertyValue("customReturnValueHandlers"); new DirectFieldAccessor(adapter).getPropertyValue("customReturnValueHandlers");
assertEquals(1, handlers.size()); assertEquals(1, handlers.size());
HandlerExceptionResolverComposite composite = (HandlerExceptionResolverComposite) webConfig.handlerExceptionResolver(); HandlerExceptionResolverComposite composite = (HandlerExceptionResolverComposite) webConfig.handlerExceptionResolver();
assertEquals(1, composite.getExceptionResolvers().size()); assertEquals(1, composite.getExceptionResolvers().size());
RequestMappingHandlerMapping rmHandlerMapping = webConfig.requestMappingHandlerMapping(); RequestMappingHandlerMapping rmHandlerMapping = webConfig.requestMappingHandlerMapping();
rmHandlerMapping.setApplicationContext(appCxt); rmHandlerMapping.setApplicationContext(appCxt);
HandlerExecutionChain chain = rmHandlerMapping.getHandler(new MockHttpServletRequest("GET", "/")); HandlerExecutionChain chain = rmHandlerMapping.getHandler(new MockHttpServletRequest("GET", "/"));
@ -234,7 +234,7 @@ public class WebMvcConfigurationSupportTests {
@Controller @Controller
private static class TestController { private static class TestController {
@SuppressWarnings("unused") @SuppressWarnings("unused")
@RequestMapping("/") @RequestMapping("/")
public void handle() { public void handle() {
@ -242,15 +242,15 @@ public class WebMvcConfigurationSupportTests {
} }
private static class TestWebMvcConfiguration extends WebMvcConfigurationSupport { private static class TestWebMvcConfiguration extends WebMvcConfigurationSupport {
} }
/** /**
* The purpose of this class is to test that an implementation of a {@link WebMvcConfigurer} * The purpose of this class is to test that an implementation of a {@link WebMvcConfigurer}
* can also apply customizations by extension from {@link WebMvcConfigurationSupport}. * can also apply customizations by extension from {@link WebMvcConfigurationSupport}.
*/ */
private class WebConfig extends WebMvcConfigurationSupport implements WebMvcConfigurer { private class WebConfig extends WebMvcConfigurationSupport implements WebMvcConfigurer {
@Override @Override
public void addFormatters(FormatterRegistry registry) { public void addFormatters(FormatterRegistry registry) {
registry.addConverter(new Converter<TestBean, String>() { registry.addConverter(new Converter<TestBean, String>() {
@ -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
@ -312,5 +312,5 @@ public class WebMvcConfigurationSupportTests {
configurer.enable("default"); configurer.enable("default");
} }
} }
} }

352
spring-webmvc/src/test/java/org/springframework/web/servlet/view/json/MappingJackson2JsonViewTest.java

@ -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;
}
}
}

5
spring-webmvc/src/test/java/org/springframework/web/servlet/view/json/MappingJacksonJsonViewTest.java

@ -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;
@ -213,10 +214,10 @@ public class MappingJacksonJsonViewTest {
model.put("foo", bean); model.put("foo", bean);
Object actual = view.filterModel(model); Object actual = view.filterModel(model);
assertSame(bean, actual); assertSame(bean, actual);
} }
@SuppressWarnings("rawtypes") @SuppressWarnings("rawtypes")
@Test @Test
public void filterTwoKeyModel() throws Exception { public void filterTwoKeyModel() throws Exception {

1
src/dist/changelog.txt vendored

@ -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)
------------------------------------- -------------------------------------

13
src/reference/docbook/mvc.xml

@ -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>

4
src/reference/docbook/remoting.xml

@ -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

4
src/reference/docbook/view.xml

@ -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

Loading…
Cancel
Save