diff --git a/build.gradle b/build.gradle index 07983850661..32d5ce3ef0c 100644 --- a/build.gradle +++ b/build.gradle @@ -327,6 +327,7 @@ project('spring-web') { compile("commons-httpclient:commons-httpclient:3.1", optional) compile("org.apache.httpcomponents:httpclient:4.1.1", 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("org.mortbay.jetty:jetty:6.1.9") { dep -> optional dep diff --git a/spring-web/src/main/java/org/springframework/http/converter/json/MappingJackson2HttpMessageConverter.java b/spring-web/src/main/java/org/springframework/http/converter/json/MappingJackson2HttpMessageConverter.java new file mode 100644 index 00000000000..343b02fe8f4 --- /dev/null +++ b/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 Jackson 2's {@link ObjectMapper}. + * + *

This converter can be used to bind to typed beans, or untyped {@link java.util.HashMap HashMap} instances. + * + *

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 { + + 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. + *

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. + *

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. + *

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: + *

+	 * protected JavaType getJavaType(Class<?> clazz) {
+	 *   if (List.class.isAssignableFrom(clazz)) {
+	 *     return objectMapper.getTypeFactory().constructCollectionType(ArrayList.class, MyBean.class);
+	 *   } else {
+	 *     return super.getJavaType(clazz);
+	 *   }
+	 * }
+	 * 
+ * @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 null) + */ + 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; + } + +} diff --git a/spring-web/src/main/java/org/springframework/web/client/RestTemplate.java b/spring-web/src/main/java/org/springframework/web/client/RestTemplate.java index 94f957c686f..770551895df 100644 --- a/spring-web/src/main/java/org/springframework/web/client/RestTemplate.java +++ b/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"); * 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.feed.AtomFeedHttpMessageConverter; 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.xml.Jaxb2RootElementHttpMessageConverter; import org.springframework.http.converter.xml.SourceHttpMessageConverter; @@ -118,6 +119,10 @@ public class RestTemplate extends InterceptingHttpAccessor implements RestOperat private static final boolean jaxb2Present = 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 = ClassUtils.isPresent("org.codehaus.jackson.map.ObjectMapper", 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) { this.messageConverters.add(new Jaxb2RootElementHttpMessageConverter()); } - if (jacksonPresent) { + if (jackson2Present) { + this.messageConverters.add(new MappingJackson2HttpMessageConverter()); + } + else if (jacksonPresent) { this.messageConverters.add(new MappingJacksonHttpMessageConverter()); } if (romePresent) { @@ -384,7 +392,7 @@ public class RestTemplate extends InterceptingHttpAccessor implements RestOperat return execute(url, method, requestCallback, responseExtractor, uriVariables); } - public ResponseEntity exchange(URI url, HttpMethod method, HttpEntity requestEntity, + public ResponseEntity exchange(URI url, HttpMethod method, HttpEntity requestEntity, Class responseType) throws RestClientException { HttpEntityRequestCallback requestCallback = new HttpEntityRequestCallback(requestEntity, responseType); ResponseEntityResponseExtractor responseExtractor = new ResponseEntityResponseExtractor(responseType); @@ -577,7 +585,7 @@ public class RestTemplate extends InterceptingHttpAccessor implements RestOperat } if (logger.isDebugEnabled()) { if (requestContentType != null) { - logger.debug("Writing [" + requestBody + "] as \"" + requestContentType + + logger.debug("Writing [" + requestBody + "] as \"" + requestContentType + "\" using [" + messageConverter + "]"); } else { diff --git a/spring-web/src/test/java/org/springframework/http/converter/json/MappingJacksonHttpMessageConverterTests.java b/spring-web/src/test/java/org/springframework/http/converter/json/MappingJacksonHttpMessageConverterTests.java index b26799f60cd..3ba8dea96ed 100644 --- a/spring-web/src/test/java/org/springframework/http/converter/json/MappingJacksonHttpMessageConverterTests.java +++ b/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"); * 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.nio.charset.Charset; import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Map; import org.codehaus.jackson.map.type.TypeFactory; import org.codehaus.jackson.type.JavaType; -import org.junit.Before; 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.MockHttpInputMessage; import org.springframework.http.MockHttpOutputMessage; +import org.springframework.http.converter.HttpMessageConverter; import org.springframework.http.converter.HttpMessageNotReadableException; /** + * Jackson conversion tests parameterized with Jackson and Jackson 2 converters. + * * @author Arjen Poutsma + * @author Rossen Stoyanchev */ +@RunWith(Parameterized.class) public class MappingJacksonHttpMessageConverterTests { - private MappingJacksonHttpMessageConverter converter; + private HttpMessageConverter converter; - @Before - public void setUp() { - converter = new MappingJacksonHttpMessageConverter(); + @Parameters + public static Collection handlerTypes() { + 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 converter) { + this.converter = converter; } @Test diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/AnnotationDrivenBeanDefinitionParser.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/AnnotationDrivenBeanDefinitionParser.java index f0d09aaff75..080565ea16a 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/AnnotationDrivenBeanDefinitionParser.java +++ b/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"); * 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.feed.AtomFeedHttpMessageConverter; 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.xml.Jaxb2RootElementHttpMessageConverter; import org.springframework.http.converter.xml.SourceHttpMessageConverter; @@ -65,52 +66,52 @@ import org.springframework.web.servlet.mvc.support.DefaultHandlerExceptionResolv import org.w3c.dom.Element; /** - * A {@link BeanDefinitionParser} that provides the configuration for the + * A {@link BeanDefinitionParser} that provides the configuration for the * {@code } MVC namespace element. * *

This class registers the following {@link HandlerMapping}s:

*
    - *
  • {@link RequestMappingHandlerMapping} + *
  • {@link RequestMappingHandlerMapping} * ordered at 0 for mapping requests to annotated controller methods. - *
  • {@link BeanNameUrlHandlerMapping} + *
  • {@link BeanNameUrlHandlerMapping} * ordered at 2 to map URL paths to controller bean names. *
* - *

Note: Additional HandlerMappings may be registered - * as a result of using the {@code } or the + *

Note: Additional HandlerMappings may be registered + * as a result of using the {@code } or the * {@code } MVC namespace elements. - * + * *

This class registers the following {@link HandlerAdapter}s: *

    - *
  • {@link RequestMappingHandlerAdapter} + *
  • {@link RequestMappingHandlerAdapter} * for processing requests with annotated controller methods. - *
  • {@link HttpRequestHandlerAdapter} + *
  • {@link HttpRequestHandlerAdapter} * for processing requests with {@link HttpRequestHandler}s. - *
  • {@link SimpleControllerHandlerAdapter} + *
  • {@link SimpleControllerHandlerAdapter} * for processing requests with interface-based {@link Controller}s. *
- * + * *

This class registers the following {@link HandlerExceptionResolver}s: *

    - *
  • {@link ExceptionHandlerExceptionResolver} for handling exceptions + *
  • {@link ExceptionHandlerExceptionResolver} for handling exceptions * through @{@link ExceptionHandler} methods. - *
  • {@link ResponseStatusExceptionResolver} for exceptions annotated + *
  • {@link ResponseStatusExceptionResolver} for exceptions annotated * with @{@link ResponseStatus}. - *
  • {@link DefaultHandlerExceptionResolver} for resolving known Spring + *
  • {@link DefaultHandlerExceptionResolver} for resolving known Spring * exception types *
- * - *

Both the {@link RequestMappingHandlerAdapter} and the - * {@link ExceptionHandlerExceptionResolver} are configured with default + * + *

Both the {@link RequestMappingHandlerAdapter} and the + * {@link ExceptionHandlerExceptionResolver} are configured with default * instances of the following kind, unless custom instances are provided: *

    *
  • A {@link DefaultFormattingConversionService} - *
  • A {@link LocalValidatorFactoryBean} if a JSR-303 implementation is + *
  • A {@link LocalValidatorFactoryBean} if a JSR-303 implementation is * available on the classpath - *
  • A range of {@link HttpMessageConverter}s depending on what 3rd party + *
  • A range of {@link HttpMessageConverter}s depending on what 3rd party * libraries are available on the classpath. *
- * + * * @author Keith Donald * @author Juergen Hoeller * @author Arjen Poutsma @@ -125,6 +126,10 @@ class AnnotationDrivenBeanDefinitionParser implements BeanDefinitionParser { private static final boolean jaxb2Present = 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 = ClassUtils.isPresent("org.codehaus.jackson.map.ObjectMapper", 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 argumentResolvers = getArgumentResolvers(element, source, parserContext); ManagedList returnValueHandlers = getReturnValueHandlers(element, source, parserContext); - + RootBeanDefinition methodAdapterDef = new RootBeanDefinition(RequestMappingHandlerAdapter.class); methodAdapterDef.setSource(source); methodAdapterDef.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); @@ -215,7 +220,7 @@ class AnnotationDrivenBeanDefinitionParser implements BeanDefinitionParser { parserContext.registerComponent(new BeanComponentDefinition(defaultExceptionResolver, defaultExceptionResolverName)); 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); parserContext.popAndRegisterContainingComponent(); @@ -309,7 +314,10 @@ class AnnotationDrivenBeanDefinitionParser implements BeanDefinitionParser { messageConverters .add(createConverterBeanDefinition(Jaxb2RootElementHttpMessageConverter.class, source)); } - if (jacksonPresent) { + if (jackson2Present) { + messageConverters.add(createConverterBeanDefinition(MappingJackson2HttpMessageConverter.class, source)); + } + else if (jacksonPresent) { messageConverters.add(createConverterBeanDefinition(MappingJacksonHttpMessageConverter.class, source)); } if (romePresent) { diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurationSupport.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurationSupport.java index 03c8480bba0..55d418d8071 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurationSupport.java +++ b/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.feed.AtomFeedHttpMessageConverter; 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.xml.Jaxb2RootElementHttpMessageConverter; 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. - * It is typically imported by adding {@link EnableWebMvc @EnableWebMvc} to an - * application {@link Configuration @Configuration} class. An alternative more + * It is typically imported by adding {@link EnableWebMvc @EnableWebMvc} to an + * application {@link Configuration @Configuration} class. An alternative more * 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. * For more details see the Javadoc of {@link EnableWebMvc @EnableWebMvc}. - * + * *

This class registers the following {@link HandlerMapping}s:

*
    - *
  • {@link RequestMappingHandlerMapping} + *
  • {@link RequestMappingHandlerMapping} * ordered at 0 for mapping requests to annotated controller methods. - *
  • {@link HandlerMapping} + *
  • {@link HandlerMapping} * ordered at 1 to map URL paths directly to view names. - *
  • {@link BeanNameUrlHandlerMapping} + *
  • {@link BeanNameUrlHandlerMapping} * ordered at 2 to map URL paths to controller bean names. - *
  • {@link HandlerMapping} + *
  • {@link HandlerMapping} * ordered at {@code Integer.MAX_VALUE-1} to serve static resource requests. - *
  • {@link HandlerMapping} + *
  • {@link HandlerMapping} * ordered at {@code Integer.MAX_VALUE} to forward requests to the default servlet. *
* *

Registers these {@link HandlerAdapter}s: *

    - *
  • {@link RequestMappingHandlerAdapter} + *
  • {@link RequestMappingHandlerAdapter} * for processing requests with annotated controller methods. - *
  • {@link HttpRequestHandlerAdapter} + *
  • {@link HttpRequestHandlerAdapter} * for processing requests with {@link HttpRequestHandler}s. - *
  • {@link SimpleControllerHandlerAdapter} + *
  • {@link SimpleControllerHandlerAdapter} * for processing requests with interface-based {@link Controller}s. *
* *

Registers a {@link HandlerExceptionResolverComposite} with this chain of * exception resolvers: *

    - *
  • {@link ExceptionHandlerExceptionResolver} for handling exceptions + *
  • {@link ExceptionHandlerExceptionResolver} for handling exceptions * through @{@link ExceptionHandler} methods. - *
  • {@link ResponseStatusExceptionResolver} for exceptions annotated + *
  • {@link ResponseStatusExceptionResolver} for exceptions annotated * with @{@link ResponseStatus}. - *
  • {@link DefaultHandlerExceptionResolver} for resolving known Spring + *
  • {@link DefaultHandlerExceptionResolver} for resolving known Spring * exception types *
* - *

Both the {@link RequestMappingHandlerAdapter} and the - * {@link ExceptionHandlerExceptionResolver} are configured with default + *

Both the {@link RequestMappingHandlerAdapter} and the + * {@link ExceptionHandlerExceptionResolver} are configured with default * instances of the following kind, unless custom instances are provided: *

    *
  • A {@link DefaultFormattingConversionService} - *
  • A {@link LocalValidatorFactoryBean} if a JSR-303 implementation is + *
  • A {@link LocalValidatorFactoryBean} if a JSR-303 implementation is * available on the classpath - *
  • A range of {@link HttpMessageConverter}s depending on the 3rd party + *
  • A range of {@link HttpMessageConverter}s depending on the 3rd party * libraries available on the classpath. *
- * + * * @see EnableWebMvc * @see WebMvcConfigurer * @see WebMvcConfigurerAdapter @@ -151,9 +152,9 @@ public class WebMvcConfigurationSupport implements ApplicationContextAware, Serv public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { 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. */ @Bean @@ -163,11 +164,11 @@ public class WebMvcConfigurationSupport implements ApplicationContextAware, Serv handlerMapping.setInterceptors(getInterceptors()); return handlerMapping; } - + /** - * Provide access to the shared handler interceptors used to configure - * {@link HandlerMapping} instances with. This method cannot be overridden, - * use {@link #addInterceptors(InterceptorRegistry)} instead. + * Provide access to the shared handler interceptors used to configure + * {@link HandlerMapping} instances with. This method cannot be overridden, + * use {@link #addInterceptors(InterceptorRegistry)} instead. */ protected final Object[] getInterceptors() { if (interceptors == null) { @@ -178,9 +179,9 @@ public class WebMvcConfigurationSupport implements ApplicationContextAware, Serv } 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. * @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 - * view names. To configure view controllers, override - * {@link #addViewControllers}. + * Return a handler mapping ordered at 1 to map URL paths directly to + * view names. To configure view controllers, override + * {@link #addViewControllers}. */ @Bean public HandlerMapping viewControllerHandlerMapping() { ViewControllerRegistry registry = new ViewControllerRegistry(); addViewControllers(registry); - + AbstractHandlerMapping handlerMapping = registry.getHandlerMapping(); handlerMapping = handlerMapping != null ? handlerMapping : new EmptyHandlerMapping(); handlerMapping.setInterceptors(getInterceptors()); @@ -209,9 +210,9 @@ public class WebMvcConfigurationSupport implements ApplicationContextAware, Serv */ 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. */ @Bean @@ -223,8 +224,8 @@ public class WebMvcConfigurationSupport implements ApplicationContextAware, Serv } /** - * Return a handler mapping ordered at Integer.MAX_VALUE-1 with mapped - * resource handlers. To configure resource handling, override + * Return a handler mapping ordered at Integer.MAX_VALUE-1 with mapped + * resource handlers. To configure resource handling, override * {@link #addResourceHandlers}. */ @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 */ protected void addResourceHandlers(ResourceHandlerRegistry registry) { } /** - * Return a handler mapping ordered at Integer.MAX_VALUE with a mapped - * default servlet handler. To configure "default" Servlet handling, - * override {@link #configureDefaultServletHandling}. + * Return a handler mapping ordered at Integer.MAX_VALUE with a mapped + * default servlet handler. To configure "default" Servlet handling, + * override {@link #configureDefaultServletHandling}. */ @Bean 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 */ protected void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) { } /** - * Returns a {@link RequestMappingHandlerAdapter} for processing requests - * through annotated controller methods. Consider overriding one of these + * Returns a {@link RequestMappingHandlerAdapter} for processing requests + * through annotated controller methods. Consider overriding one of these * other more fine-grained methods: *
    *
  • {@link #addArgumentResolvers} for adding custom argument resolvers. @@ -279,13 +280,13 @@ public class WebMvcConfigurationSupport implements ApplicationContextAware, Serv ConfigurableWebBindingInitializer webBindingInitializer = new ConfigurableWebBindingInitializer(); webBindingInitializer.setConversionService(mvcConversionService()); webBindingInitializer.setValidator(mvcValidator()); - + List argumentResolvers = new ArrayList(); addArgumentResolvers(argumentResolvers); List returnValueHandlers = new ArrayList(); addReturnValueHandlers(returnValueHandlers); - + RequestMappingHandlerAdapter adapter = new RequestMappingHandlerAdapter(); adapter.setMessageConverters(getMessageConverters()); 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. - *

    Custom argument resolvers are invoked before built-in resolvers - * except for those that rely on the presence of annotations (e.g. - * {@code @RequestParameter}, {@code @PathVariable}, etc.). - * The latter can be customized by configuring the - * {@link RequestMappingHandlerAdapter} directly. - * @param argumentResolvers the list of custom converters; + *

    Custom argument resolvers are invoked before built-in resolvers + * except for those that rely on the presence of annotations (e.g. + * {@code @RequestParameter}, {@code @PathVariable}, etc.). + * The latter can be customized by configuring the + * {@link RequestMappingHandlerAdapter} directly. + * @param argumentResolvers the list of custom converters; * initially an empty list. */ protected void addArgumentResolvers(List argumentResolvers) { } /** - * Add custom {@link HandlerMethodReturnValueHandler}s in addition to the + * Add custom {@link HandlerMethodReturnValueHandler}s in addition to the * ones registered by default. - *

    Custom return value handlers are invoked before built-in ones except - * for those that rely on the presence of annotations (e.g. - * {@code @ResponseBody}, {@code @ModelAttribute}, etc.). - * The latter can be customized by configuring the + *

    Custom return value handlers are invoked before built-in ones except + * for those that rely on the presence of annotations (e.g. + * {@code @ResponseBody}, {@code @ModelAttribute}, etc.). + * The latter can be customized by configuring the * {@link RequestMappingHandlerAdapter} directly. - * @param returnValueHandlers the list of custom handlers; + * @param returnValueHandlers the list of custom handlers; * initially an empty list. */ protected void addReturnValueHandlers(List returnValueHandlers) { } /** - * Provides access to the shared {@link HttpMessageConverter}s used by the - * {@link RequestMappingHandlerAdapter} and the - * {@link ExceptionHandlerExceptionResolver}. - * This method cannot be overridden. + * Provides access to the shared {@link HttpMessageConverter}s used by the + * {@link RequestMappingHandlerAdapter} and the + * {@link ExceptionHandlerExceptionResolver}. + * This method cannot be overridden. * 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. */ protected final List> getMessageConverters() { @@ -343,21 +344,21 @@ public class WebMvcConfigurationSupport implements ApplicationContextAware, Serv } /** - * Override this method to add custom {@link HttpMessageConverter}s to use - * with the {@link RequestMappingHandlerAdapter} and the - * {@link ExceptionHandlerExceptionResolver}. Adding converters to the + * Override this method to add custom {@link HttpMessageConverter}s to use + * with the {@link RequestMappingHandlerAdapter} and the + * {@link ExceptionHandlerExceptionResolver}. Adding converters to the * 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. - * @param converters a list to add message converters to; + * @param converters a list to add message converters to; * initially an empty list. */ protected void configureMessageConverters(List> converters) { } /** - * Adds a set of default HttpMessageConverter instances to the given list. - * Subclasses can call this method from {@link #configureMessageConverters(List)}. + * Adds a set of default HttpMessageConverter instances to the given list. + * Subclasses can call this method from {@link #configureMessageConverters(List)}. * @param messageConverters the list to add the default message converters to */ protected final void addDefaultHttpMessageConverters(List> messageConverters) { @@ -374,7 +375,10 @@ public class WebMvcConfigurationSupport implements ApplicationContextAware, Serv if (ClassUtils.isPresent("javax.xml.bind.Binder", classLoader)) { 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()); } if (ClassUtils.isPresent("com.sun.syndication.feed.WireFeed", classLoader)) { @@ -382,10 +386,10 @@ public class WebMvcConfigurationSupport implements ApplicationContextAware, Serv messageConverters.add(new RssChannelHttpMessageConverter()); } } - + /** - * Returns a {@link FormattingConversionService} for use with annotated - * controller methods and the {@code spring:eval} JSP tag. + * Returns a {@link FormattingConversionService} for use with annotated + * controller methods and the {@code spring:eval} JSP tag. * Also see {@link #addFormatters} as an alternative to overriding this method. */ @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. * Delegates to {@link #getValidator()} first and if that returns {@code null} * 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. */ @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. */ @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. */ @Bean @@ -465,23 +469,23 @@ public class WebMvcConfigurationSupport implements ApplicationContextAware, Serv /** * 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 #addDefaultHandlerExceptionResolvers(List)}. *

    Note: This method cannot be made final due to CGLib * constraints. Rather than overriding it, consider overriding - * {@link #configureHandlerExceptionResolvers(List)}, which allows + * {@link #configureHandlerExceptionResolvers(List)}, which allows * providing a list of resolvers. */ @Bean public HandlerExceptionResolver handlerExceptionResolver() { List exceptionResolvers = new ArrayList(); configureHandlerExceptionResolvers(exceptionResolvers); - + if (exceptionResolvers.isEmpty()) { addDefaultHandlerExceptionResolvers(exceptionResolvers); } - + HandlerExceptionResolverComposite composite = new HandlerExceptionResolverComposite(); composite.setOrder(0); composite.setExceptionResolvers(exceptionResolvers); @@ -489,27 +493,27 @@ public class WebMvcConfigurationSupport implements ApplicationContextAware, Serv } /** - * Override this method to configure the list of - * {@link HandlerExceptionResolver}s to use. Adding resolvers to the list - * turns off the default resolvers that would otherwise be registered by - * default. Also see {@link #addDefaultHandlerExceptionResolvers(List)} + * Override this method to configure the list of + * {@link HandlerExceptionResolver}s to use. Adding resolvers to the list + * turns off the default resolvers that would otherwise be registered by + * default. Also see {@link #addDefaultHandlerExceptionResolvers(List)} * 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. */ protected void configureHandlerExceptionResolvers(List exceptionResolvers) { } /** - * A method available to subclasses for adding default + * A method available to subclasses for adding default * {@link HandlerExceptionResolver}s. *

    Adds the following exception resolvers: *

      - *
    • {@link ExceptionHandlerExceptionResolver} + *
    • {@link ExceptionHandlerExceptionResolver} * for handling exceptions through @{@link ExceptionHandler} methods. - *
    • {@link ResponseStatusExceptionResolver} + *
    • {@link ResponseStatusExceptionResolver} * for exceptions annotated with @{@link ResponseStatus}. - *
    • {@link DefaultHandlerExceptionResolver} + *
    • {@link DefaultHandlerExceptionResolver} * for resolving known Spring exception types *
    */ @@ -524,11 +528,11 @@ public class WebMvcConfigurationSupport implements ApplicationContextAware, Serv } private final static class EmptyHandlerMapping extends AbstractHandlerMapping { - + @Override protected Object getHandlerInternal(HttpServletRequest request) throws Exception { return null; } } - + } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/view/json/MappingJackson2JsonView.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/view/json/MappingJackson2JsonView.java new file mode 100644 index 00000000000..4427b138d6a --- /dev/null +++ b/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 Jackson 2's {@link ObjectMapper}. + * + *

    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 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. + *

    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 "{} && ". + * Default is false. + *

    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 modelKeys) { + this.modelKeys = modelKeys; + } + + /** + * Return the attributes in the model that should be rendered by this view. + */ + public Set 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 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 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. + *

    The effect of setting this flag is similar to using {@code MappingJacksonHttpMessageConverter} + * with an {@code @ResponseBody} request-handling method. + *

    Default is {@code false}. + */ + public void setExtractValueFromSingleKeyModel(boolean extractValueFromSingleKeyModel) { + this.extractValueFromSingleKeyModel = extractValueFromSingleKeyModel; + } + + /** + * Disables caching of the generated JSON. + *

    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 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. + *

    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 model) { + Map result = new HashMap(model.size()); + Set renderedAttributes = (!CollectionUtils.isEmpty(this.modelKeys) ? this.modelKeys : model.keySet()); + for (Map.Entry 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); + } + +} diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurationSupportTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurationSupportTests.java index 3f5fbef0e96..8dc615c4af6 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurationSupportTests.java +++ b/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"); * 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.support.FormattingConversionService; 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.MockServletContext; import org.springframework.stereotype.Controller; @@ -73,12 +73,12 @@ public class WebMvcConfigurationSupportTests { public void setUp() { mvcConfiguration = new TestWebMvcConfiguration(); } - + @Test public void requestMappingHandlerMapping() throws Exception { StaticWebApplicationContext cxt = new StaticWebApplicationContext(); cxt.registerSingleton("controller", TestController.class); - + RequestMappingHandlerMapping handlerMapping = mvcConfiguration.requestMappingHandlerMapping(); assertEquals(0, handlerMapping.getOrder()); @@ -95,7 +95,7 @@ public class WebMvcConfigurationSupportTests { assertEquals(Integer.MAX_VALUE, handlerMapping.getOrder()); assertTrue(handlerMapping.getClass().getName().endsWith("EmptyHandlerMapping")); } - + @Test public void beanNameHandlerMapping() throws Exception { StaticWebApplicationContext cxt = new StaticWebApplicationContext(); @@ -112,7 +112,7 @@ public class WebMvcConfigurationSupportTests { assertEquals(2, chain.getInterceptors().length); assertEquals(ConversionServiceExposingInterceptor.class, chain.getInterceptors()[1].getClass()); } - + @Test public void emptyResourceHandlerMapping() { mvcConfiguration.setApplicationContext(new StaticWebApplicationContext()); @@ -121,7 +121,7 @@ public class WebMvcConfigurationSupportTests { assertEquals(Integer.MAX_VALUE, handlerMapping.getOrder()); assertTrue(handlerMapping.getClass().getName().endsWith("EmptyHandlerMapping")); } - + @Test public void emptyDefaultServletHandlerMapping() { mvcConfiguration.setServletContext(new MockServletContext()); @@ -130,7 +130,7 @@ public class WebMvcConfigurationSupportTests { assertEquals(Integer.MAX_VALUE, handlerMapping.getOrder()); assertTrue(handlerMapping.getClass().getName().endsWith("EmptyHandlerMapping")); } - + @Test public void requestMappingHandlerAdapter() throws Exception { RequestMappingHandlerAdapter adapter = mvcConfiguration.requestMappingHandlerAdapter(); @@ -145,29 +145,29 @@ public class WebMvcConfigurationSupportTests { ConversionService conversionService = initializer.getConversionService(); assertNotNull(conversionService); assertTrue(conversionService instanceof FormattingConversionService); - + Validator validator = initializer.getValidator(); assertNotNull(validator); assertTrue(validator instanceof LocalValidatorFactoryBean); - + assertEquals(false, new DirectFieldAccessor(adapter).getPropertyValue("ignoreDefaultModelOnRedirect")); } - + @Test public void handlerExceptionResolver() throws Exception { - HandlerExceptionResolverComposite compositeResolver = + HandlerExceptionResolverComposite compositeResolver = (HandlerExceptionResolverComposite) mvcConfiguration.handlerExceptionResolver(); - + assertEquals(0, compositeResolver.getOrder()); List expectedResolvers = new ArrayList(); mvcConfiguration.addDefaultHandlerExceptionResolvers(expectedResolvers); assertEquals(expectedResolvers.size(), compositeResolver.getExceptionResolvers().size()); } - - @Test + + @Test public void webMvcConfigurerExtensionHooks() throws Exception { - + StaticWebApplicationContext appCxt = new StaticWebApplicationContext(); appCxt.setServletContext(new MockServletContext(new FileSystemResourceLoader())); appCxt.registerSingleton("controller", TestController.class); @@ -175,33 +175,33 @@ public class WebMvcConfigurationSupportTests { WebConfig webConfig = new WebConfig(); webConfig.setApplicationContext(appCxt); webConfig.setServletContext(appCxt.getServletContext()); - + String actual = webConfig.mvcConversionService().convert(new TestBean(), String.class); assertEquals("converted", actual); RequestMappingHandlerAdapter adapter = webConfig.requestMappingHandlerAdapter(); assertEquals(1, adapter.getMessageConverters().size()); - + ConfigurableWebBindingInitializer initializer = (ConfigurableWebBindingInitializer) adapter.getWebBindingInitializer(); assertNotNull(initializer); - + BeanPropertyBindingResult bindingResult = new BeanPropertyBindingResult(null, ""); initializer.getValidator().validate(null, bindingResult); assertEquals("invalid", bindingResult.getAllErrors().get(0).getCode()); @SuppressWarnings("unchecked") - List argResolvers= (List) + List argResolvers= (List) new DirectFieldAccessor(adapter).getPropertyValue("customArgumentResolvers"); assertEquals(1, argResolvers.size()); @SuppressWarnings("unchecked") - List handlers = (List) + List handlers = (List) new DirectFieldAccessor(adapter).getPropertyValue("customReturnValueHandlers"); assertEquals(1, handlers.size()); - + HandlerExceptionResolverComposite composite = (HandlerExceptionResolverComposite) webConfig.handlerExceptionResolver(); assertEquals(1, composite.getExceptionResolvers().size()); - + RequestMappingHandlerMapping rmHandlerMapping = webConfig.requestMappingHandlerMapping(); rmHandlerMapping.setApplicationContext(appCxt); HandlerExecutionChain chain = rmHandlerMapping.getHandler(new MockHttpServletRequest("GET", "/")); @@ -234,7 +234,7 @@ public class WebMvcConfigurationSupportTests { @Controller private static class TestController { - + @SuppressWarnings("unused") @RequestMapping("/") public void handle() { @@ -242,15 +242,15 @@ public class WebMvcConfigurationSupportTests { } 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}. */ private class WebConfig extends WebMvcConfigurationSupport implements WebMvcConfigurer { - + @Override public void addFormatters(FormatterRegistry registry) { registry.addConverter(new Converter() { @@ -262,7 +262,7 @@ public class WebMvcConfigurationSupportTests { @Override public void configureMessageConverters(List> converters) { - converters.add(new MappingJacksonHttpMessageConverter()); + converters.add(new MappingJackson2HttpMessageConverter()); } @Override @@ -312,5 +312,5 @@ public class WebMvcConfigurationSupportTests { configurer.enable("default"); } } - + } diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/view/json/MappingJackson2JsonViewTest.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/view/json/MappingJackson2JsonViewTest.java new file mode 100644 index 00000000000..93774c21ca8 --- /dev/null +++ b/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 model = new HashMap(); + 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 model = new HashMap(); + 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 model = new HashMap(); + 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 model = new HashMap(); + 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 model = new HashMap(); + 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 attrs = new HashSet(); + attrs.add("foo"); + attrs.add("baz"); + attrs.add("nil"); + + view.setModelKeys(attrs); + Map model = new HashMap(); + 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 model = new HashMap(); + 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 model = new HashMap(); + 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 { + + @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 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 customSerializers() { + return null; + } + } +} diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/view/json/MappingJacksonJsonViewTest.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/view/json/MappingJacksonJsonViewTest.java index 7b0a8e0c7d2..eecc00d28d0 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/view/json/MappingJacksonJsonViewTest.java +++ b/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.annotate.JsonSerialize; import org.codehaus.jackson.map.ser.BeanSerializerFactory; + import org.junit.Before; import org.junit.Test; import org.mozilla.javascript.Context; @@ -213,10 +214,10 @@ public class MappingJacksonJsonViewTest { model.put("foo", bean); Object actual = view.filterModel(model); - + assertSame(bean, actual); } - + @SuppressWarnings("rawtypes") @Test public void filterTwoKeyModel() throws Exception { diff --git a/src/dist/changelog.txt b/src/dist/changelog.txt index 3a56a0576b2..a534caeb080 100644 --- a/src/dist/changelog.txt +++ b/src/dist/changelog.txt @@ -13,6 +13,7 @@ Changes in version 3.2 M1 * fix case-sensitivity issue with some containers on access to 'Content-Disposition' header * add Servlet 3.0 based async support * fix issue with encoded params in UriComponentsBuilder +* add Jackson 2 HttpMessageConverter and View types Changes in version 3.1.1 (2012-02-16) ------------------------------------- diff --git a/src/reference/docbook/mvc.xml b/src/reference/docbook/mvc.xml index 38ee9fe58cc..59051321828 100644 --- a/src/reference/docbook/mvc.xml +++ b/src/reference/docbook/mvc.xml @@ -2892,7 +2892,7 @@ public String upload(...) { </property> <property name="defaultViews"> <list> - <bean class="org.springframework.web.servlet.view.json.MappingJacksonJsonView" /> + <bean class="org.springframework.web.servlet.view.json.MappingJackson2JsonView" /> </list> </property> </bean> @@ -2924,7 +2924,7 @@ public String upload(...) { the SampleContentAtomView if the view name returned is content. If the request is made with the file extension .json, the - MappingJacksonJsonView instance from the + MappingJackson2JsonViewinstance from the DefaultViews list will be selected regardless of the view name. Alternatively, client requests can be made without a file extension but with the Accept header set to the @@ -3588,7 +3588,7 @@ public String onSubmit(@RequestPart("meta-data") MetaData However, the @RequestPart("meta-data") MetaData method argument in this case is read as JSON content based on its 'Content-Type' header and converted with the help of - the MappingJacksonHttpMessageConverter. + the MappingJackson2HttpMessageConverter. @@ -4256,9 +4256,10 @@ public class WebConfig { - MappingJacksonHttpMessageConverter - converts to/from JSON — added if Jackson is present on the - classpath. + MappingJackson2HttpMessageConverter + (or MappingJacksonHttpMessageConverter) + converts to/from JSON — added if Jackson 2 (or Jackson) is present + on the classpath. diff --git a/src/reference/docbook/remoting.xml b/src/reference/docbook/remoting.xml index d7e537c4f31..006dd621371 100644 --- a/src/reference/docbook/remoting.xml +++ b/src/reference/docbook/remoting.xml @@ -1363,7 +1363,7 @@ if (HttpStatus.SC_CREATED == post.getStatusCode()) { these defaults using the messageConverters() bean property as would be required if using the MarshallingHttpMessageConverter or - MappingJacksonHttpMessageConverter. + MappingJackson2HttpMessageConverter. Each method takes URI template arguments in two forms, either as a String variable length argument or a @@ -1608,7 +1608,7 @@ String body = response.getBody();
    - MappingJacksonHttpMessageConverter + MappingJackson2HttpMessageConverter (or MappingJacksonHttpMessageConverter with Jackson 1.x) An HttpMessageConverter implementation that can read and write JSON using Jackson's diff --git a/src/reference/docbook/view.xml b/src/reference/docbook/view.xml index 8629a256928..e1e6e4e2b04 100644 --- a/src/reference/docbook/view.xml +++ b/src/reference/docbook/view.xml @@ -2626,7 +2626,9 @@ simpleReport.reportDataKey=myBeanData
    JSON Mapping View - The MappingJacksonJsonView uses the Jackson + The MappingJackson2JsonView + (or MappingJacksonJsonView depending on the + the Jackson version you have) uses the Jackson library's ObjectMapper to render the response content 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