diff --git a/build-spring-framework/resources/changelog.txt b/build-spring-framework/resources/changelog.txt index 52957824ea9..38f92ea0796 100644 --- a/build-spring-framework/resources/changelog.txt +++ b/build-spring-framework/resources/changelog.txt @@ -16,7 +16,7 @@ Changes in version 3.1.2 (2012-06-??) * ServletContextResource's getFile implementation falls back to getRealPath for non-existent files * fixed StandardServletMultipartResolver compatibility with Resin (only deleting actual file parts) * fix issue with parsing invalid Content-Type or Accept headers - +* add Jackson 2 HttpMessageConverter and View types Changes in version 3.1.1 (2012-02-16) ------------------------------------- diff --git a/org.springframework.web.servlet/.classpath b/org.springframework.web.servlet/.classpath index ebeb77d8ec5..a735aa4ded1 100644 --- a/org.springframework.web.servlet/.classpath +++ b/org.springframework.web.servlet/.classpath @@ -23,6 +23,9 @@ + + + diff --git a/org.springframework.web.servlet/ivy.xml b/org.springframework.web.servlet/ivy.xml index c15efda0b22..2729836258b 100644 --- a/org.springframework.web.servlet/ivy.xml +++ b/org.springframework.web.servlet/ivy.xml @@ -69,6 +69,8 @@ conf="optional, velocity->compile"/> + 1.4.2 true + + com.fasterxml.jackson.core + jackson-databind + 2.0.2 + true + org.springframework spring-asm diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/config/AnnotationDrivenBeanDefinitionParser.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/config/AnnotationDrivenBeanDefinitionParser.java index f0d09aaff75..080565ea16a 100644 --- a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/config/AnnotationDrivenBeanDefinitionParser.java +++ b/org.springframework.web.servlet/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/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurationSupport.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurationSupport.java index 03c8480bba0..55d418d8071 100644 --- a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurationSupport.java +++ b/org.springframework.web.servlet/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/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/view/json/MappingJackson2JsonView.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/view/json/MappingJackson2JsonView.java new file mode 100644 index 00000000000..4427b138d6a --- /dev/null +++ b/org.springframework.web.servlet/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/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurationSupportTests.java b/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurationSupportTests.java index 3f5fbef0e96..8dc615c4af6 100644 --- a/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurationSupportTests.java +++ b/org.springframework.web.servlet/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/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/view/json/MappingJackson2JsonViewTest.java b/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/view/json/MappingJackson2JsonViewTest.java new file mode 100644 index 00000000000..93774c21ca8 --- /dev/null +++ b/org.springframework.web.servlet/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/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/view/json/MappingJacksonJsonViewTest.java b/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/view/json/MappingJacksonJsonViewTest.java index 7b0a8e0c7d2..eecc00d28d0 100644 --- a/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/view/json/MappingJacksonJsonViewTest.java +++ b/org.springframework.web.servlet/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/org.springframework.web.servlet/template.mf b/org.springframework.web.servlet/template.mf index e5c88aafc94..ffdcea46922 100644 --- a/org.springframework.web.servlet/template.mf +++ b/org.springframework.web.servlet/template.mf @@ -24,6 +24,7 @@ Import-Template: org.apache.velocity.*;version="[1.5.0, 2.0.0)";resolution:=optional, org.apache.velocity.tools.*;version="[1.4.0, 3.0.0)";resolution:=optional, org.codehaus.jackson.*;version="[1.3.0, 2.0.0)";resolution:=optional, + com.fasterxml.jackson.*;version="[2.0.0, 3.0.0)";resolution:=optional, org.springframework.beans.*;version=${spring.osgi.range}, org.springframework.context.*;version=${spring.osgi.range}, org.springframework.core.*;version=${spring.osgi.range}, diff --git a/org.springframework.web.servlet/web-servlet.iml b/org.springframework.web.servlet/web-servlet.iml index 172c5221930..c2ab746ee86 100644 --- a/org.springframework.web.servlet/web-servlet.iml +++ b/org.springframework.web.servlet/web-servlet.iml @@ -267,6 +267,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/org.springframework.web/.classpath b/org.springframework.web/.classpath index 32843f72bdb..64ee34e9ab3 100644 --- a/org.springframework.web/.classpath +++ b/org.springframework.web/.classpath @@ -28,6 +28,9 @@ + + + diff --git a/org.springframework.web/ivy.xml b/org.springframework.web/ivy.xml index d7cea881b3d..d002c62894e 100644 --- a/org.springframework.web/ivy.xml +++ b/org.springframework.web/ivy.xml @@ -64,9 +64,11 @@ conf="optional, httpclient->compile"/> - - + diff --git a/org.springframework.web/pom.xml b/org.springframework.web/pom.xml index 6f417ddbee9..37b229183d7 100644 --- a/org.springframework.web/pom.xml +++ b/org.springframework.web/pom.xml @@ -124,6 +124,12 @@ 1.4.2 true + + com.fasterxml.jackson.core + jackson-databind + 2.0.2 + true + org.springframework spring-aop diff --git a/org.springframework.web/src/main/java/org/springframework/http/converter/json/MappingJackson2HttpMessageConverter.java b/org.springframework.web/src/main/java/org/springframework/http/converter/json/MappingJackson2HttpMessageConverter.java new file mode 100644 index 00000000000..343b02fe8f4 --- /dev/null +++ b/org.springframework.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/org.springframework.web/src/main/java/org/springframework/web/client/RestTemplate.java b/org.springframework.web/src/main/java/org/springframework/web/client/RestTemplate.java index 94f957c686f..770551895df 100644 --- a/org.springframework.web/src/main/java/org/springframework/web/client/RestTemplate.java +++ b/org.springframework.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/org.springframework.web/src/test/java/org/springframework/http/converter/json/MappingJacksonHttpMessageConverterTests.java b/org.springframework.web/src/test/java/org/springframework/http/converter/json/MappingJacksonHttpMessageConverterTests.java index b26799f60cd..3ba8dea96ed 100644 --- a/org.springframework.web/src/test/java/org/springframework/http/converter/json/MappingJacksonHttpMessageConverterTests.java +++ b/org.springframework.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; + + @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); + } - @Before - public void setUp() { - converter = new MappingJacksonHttpMessageConverter(); + public MappingJacksonHttpMessageConverterTests(HttpMessageConverter converter) { + this.converter = converter; } @Test diff --git a/org.springframework.web/template.mf b/org.springframework.web/template.mf index 095a5cfafc1..16770ab71b9 100644 --- a/org.springframework.web/template.mf +++ b/org.springframework.web/template.mf @@ -6,6 +6,7 @@ Import-Template: com.caucho.*;version="[3.2.0, 5.0.0)";resolution:=optional, com.sun.syndication.*;version="[1.0.0, 2.0.0)";resolution:=optional, org.codehaus.jackson.*;version="[1.3.0, 2.0.0)";resolution:=optional, + com.fasterxml.jackson.*;version="[2.0.0, 3.0.0)";resolution:=optional, com.sun.net.*;version="0";resolution:=optional, javax.activation.*;version="0";resolution:=optional, javax.el.*;version="[1.0.0, 3.0.0)";resolution:=optional, diff --git a/org.springframework.web/web.iml b/org.springframework.web/web.iml index 83f144797b1..300f28af405 100644 --- a/org.springframework.web/web.iml +++ b/org.springframework.web/web.iml @@ -152,6 +152,40 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +