summaryrefslogtreecommitdiff
path: root/spring-webmvc/src/main/java/org
diff options
context:
space:
mode:
authorEmmanuel Bourg <ebourg@apache.org>2016-08-02 11:13:32 +0200
committerEmmanuel Bourg <ebourg@apache.org>2016-08-02 11:13:32 +0200
commitf69f2a4b8ea697b3a631c0dc7a470e3c9793fee3 (patch)
treedb2f25b29aa3e59c463ab41d3f2856f6265bb1a5 /spring-webmvc/src/main/java/org
parent5575b60c30c5a0c308c4ba3a2db93956d8c1746c (diff)
Imported Upstream version 4.2.6
Diffstat (limited to 'spring-webmvc/src/main/java/org')
-rw-r--r--spring-webmvc/src/main/java/org/springframework/web/servlet/AsyncHandlerInterceptor.java49
-rw-r--r--spring-webmvc/src/main/java/org/springframework/web/servlet/DispatcherServlet.java33
-rw-r--r--spring-webmvc/src/main/java/org/springframework/web/servlet/DispatcherServlet.properties24
-rw-r--r--spring-webmvc/src/main/java/org/springframework/web/servlet/FlashMap.java39
-rw-r--r--spring-webmvc/src/main/java/org/springframework/web/servlet/FrameworkServlet.java32
-rw-r--r--spring-webmvc/src/main/java/org/springframework/web/servlet/HandlerExceptionResolver.java24
-rw-r--r--spring-webmvc/src/main/java/org/springframework/web/servlet/HandlerInterceptor.java19
-rw-r--r--spring-webmvc/src/main/java/org/springframework/web/servlet/HandlerMapping.java4
-rw-r--r--spring-webmvc/src/main/java/org/springframework/web/servlet/NoHandlerFoundException.java10
-rw-r--r--spring-webmvc/src/main/java/org/springframework/web/servlet/ViewRendererServlet.java4
-rw-r--r--spring-webmvc/src/main/java/org/springframework/web/servlet/config/AnnotationDrivenBeanDefinitionParser.java12
-rw-r--r--spring-webmvc/src/main/java/org/springframework/web/servlet/config/CorsBeanDefinitionParser.java120
-rw-r--r--spring-webmvc/src/main/java/org/springframework/web/servlet/config/MvcNamespaceHandler.java5
-rw-r--r--spring-webmvc/src/main/java/org/springframework/web/servlet/config/MvcNamespaceUtils.java32
-rw-r--r--spring-webmvc/src/main/java/org/springframework/web/servlet/config/ResourcesBeanDefinitionParser.java56
-rw-r--r--spring-webmvc/src/main/java/org/springframework/web/servlet/config/ScriptTemplateConfigurerBeanDefinitionParser.java91
-rw-r--r--spring-webmvc/src/main/java/org/springframework/web/servlet/config/TilesConfigurerBeanDefinitionParser.java7
-rw-r--r--spring-webmvc/src/main/java/org/springframework/web/servlet/config/ViewControllerBeanDefinitionParser.java3
-rw-r--r--spring-webmvc/src/main/java/org/springframework/web/servlet/config/ViewResolversBeanDefinitionParser.java11
-rw-r--r--spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/ContentNegotiationConfigurer.java165
-rw-r--r--spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/CorsRegistration.java99
-rw-r--r--spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/CorsRegistry.java63
-rw-r--r--spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/DelegatingWebMvcConfiguration.java5
-rw-r--r--spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/EnableWebMvc.java52
-rw-r--r--spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/PathMatchConfigurer.java55
-rw-r--r--spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/ResourceChainRegistration.java11
-rw-r--r--spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/ResourceHandlerRegistration.java25
-rw-r--r--spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/ViewResolverRegistry.java28
-rw-r--r--spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurationSupport.java58
-rw-r--r--spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurer.java6
-rw-r--r--spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurerAdapter.java8
-rw-r--r--spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurerComposite.java7
-rw-r--r--spring-webmvc/src/main/java/org/springframework/web/servlet/handler/AbstractHandlerExceptionResolver.java68
-rw-r--r--spring-webmvc/src/main/java/org/springframework/web/servlet/handler/AbstractHandlerMapping.java184
-rw-r--r--spring-webmvc/src/main/java/org/springframework/web/servlet/handler/AbstractHandlerMethodMapping.java573
-rw-r--r--spring-webmvc/src/main/java/org/springframework/web/servlet/handler/AbstractUrlHandlerMapping.java27
-rw-r--r--spring-webmvc/src/main/java/org/springframework/web/servlet/handler/MappedInterceptor.java30
-rw-r--r--spring-webmvc/src/main/java/org/springframework/web/servlet/handler/SimpleMappingExceptionResolver.java4
-rw-r--r--spring-webmvc/src/main/java/org/springframework/web/servlet/i18n/CookieLocaleResolver.java8
-rw-r--r--spring-webmvc/src/main/java/org/springframework/web/servlet/i18n/LocaleChangeInterceptor.java83
-rw-r--r--spring-webmvc/src/main/java/org/springframework/web/servlet/i18n/SessionLocaleResolver.java18
-rw-r--r--spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/AbstractController.java9
-rw-r--r--spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/WebContentInterceptor.java98
-rw-r--r--spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/annotation/AnnotationMethodHandlerAdapter.java26
-rw-r--r--spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/annotation/AnnotationMethodHandlerExceptionResolver.java18
-rw-r--r--spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/annotation/DefaultAnnotationHandlerMapping.java5
-rw-r--r--spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/annotation/ResponseStatusExceptionResolver.java16
-rw-r--r--spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/annotation/ServletAnnotationMappingUtils.java7
-rw-r--r--spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/condition/AbstractMediaTypeExpression.java2
-rw-r--r--spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/condition/AbstractRequestCondition.java2
-rw-r--r--spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/RequestMappingInfo.java311
-rw-r--r--spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/RequestMappingInfoHandlerMapping.java25
-rw-r--r--spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/AbstractJsonpResponseBodyAdvice.java2
-rw-r--r--spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/AbstractMessageConverterMethodArgumentResolver.java192
-rw-r--r--spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/AbstractMessageConverterMethodProcessor.java118
-rw-r--r--spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/AsyncTaskMethodReturnValueHandler.java11
-rw-r--r--spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/CallableMethodReturnValueHandler.java11
-rw-r--r--spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/CompletionStageReturnValueHandler.java80
-rw-r--r--spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/DeferredResultMethodReturnValueHandler.java11
-rw-r--r--spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ExceptionHandlerExceptionResolver.java11
-rw-r--r--spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/HttpEntityMethodProcessor.java115
-rw-r--r--spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/JsonViewRequestBodyAdvice.java73
-rw-r--r--spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/JsonViewResponseBodyAdvice.java2
-rw-r--r--spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ListenableFutureReturnValueHandler.java11
-rw-r--r--spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/MatrixVariableMapMethodArgumentResolver.java12
-rw-r--r--spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/MatrixVariableMethodArgumentResolver.java32
-rw-r--r--spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/MvcUriComponentsBuilder.java459
-rw-r--r--spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/PathVariableMethodArgumentResolver.java38
-rw-r--r--spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestBodyAdvice.java91
-rw-r--r--spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestBodyAdviceAdapter.java70
-rw-r--r--spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerAdapter.java171
-rw-r--r--spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerMapping.java177
-rw-r--r--spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestPartMethodArgumentResolver.java63
-rw-r--r--spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestResponseBodyAdviceChain.java177
-rw-r--r--spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestResponseBodyMethodProcessor.java114
-rw-r--r--spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ResponseBodyAdvice.java4
-rw-r--r--spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ResponseBodyAdviceChain.java78
-rw-r--r--spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ResponseBodyEmitter.java286
-rw-r--r--spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ResponseBodyEmitterReturnValueHandler.java232
-rw-r--r--spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ResponseEntityExceptionHandler.java108
-rw-r--r--spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ServletInvocableHandlerMethod.java18
-rw-r--r--spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ServletRequestMethodArgumentResolver.java12
-rw-r--r--spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/SseEmitter.java251
-rw-r--r--spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/StreamingResponseBody.java47
-rw-r--r--spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/StreamingResponseBodyReturnValueHandler.java113
-rw-r--r--spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/UriComponentsBuilderMethodArgumentResolver.java7
-rw-r--r--spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ViewNameMethodReturnValueHandler.java24
-rw-r--r--spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/multiaction/MultiActionController.java21
-rw-r--r--spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/support/DefaultHandlerExceptionResolver.java100
-rw-r--r--spring-webmvc/src/main/java/org/springframework/web/servlet/resource/DefaultResourceResolverChain.java5
-rw-r--r--spring-webmvc/src/main/java/org/springframework/web/servlet/resource/PathResourceResolver.java2
-rw-r--r--spring-webmvc/src/main/java/org/springframework/web/servlet/resource/ResourceHttpRequestHandler.java193
-rw-r--r--spring-webmvc/src/main/java/org/springframework/web/servlet/resource/ResourceUrlEncodingFilter.java42
-rw-r--r--spring-webmvc/src/main/java/org/springframework/web/servlet/resource/ResourceUrlProvider.java21
-rw-r--r--spring-webmvc/src/main/java/org/springframework/web/servlet/resource/ResourceUrlProviderExposingInterceptor.java11
-rw-r--r--spring-webmvc/src/main/java/org/springframework/web/servlet/resource/VersionResourceResolver.java107
-rw-r--r--spring-webmvc/src/main/java/org/springframework/web/servlet/resource/VersionedResource.java33
-rw-r--r--spring-webmvc/src/main/java/org/springframework/web/servlet/resource/WebJarsResourceResolver.java107
-rw-r--r--spring-webmvc/src/main/java/org/springframework/web/servlet/support/AbstractDispatcherServletInitializer.java35
-rw-r--r--spring-webmvc/src/main/java/org/springframework/web/servlet/support/AbstractFlashMapManager.java33
-rw-r--r--spring-webmvc/src/main/java/org/springframework/web/servlet/support/RequestContext.java22
-rw-r--r--spring-webmvc/src/main/java/org/springframework/web/servlet/support/RequestContextUtils.java75
-rw-r--r--spring-webmvc/src/main/java/org/springframework/web/servlet/support/ServletUriComponentsBuilder.java2
-rw-r--r--spring-webmvc/src/main/java/org/springframework/web/servlet/support/WebContentGenerator.java359
-rw-r--r--spring-webmvc/src/main/java/org/springframework/web/servlet/tags/TransformTag.java3
-rw-r--r--spring-webmvc/src/main/java/org/springframework/web/servlet/tags/UrlTag.java7
-rw-r--r--spring-webmvc/src/main/java/org/springframework/web/servlet/tags/form/AbstractMultiCheckedElementTag.java2
-rw-r--r--spring-webmvc/src/main/java/org/springframework/web/servlet/tags/form/CheckboxTag.java2
-rw-r--r--spring-webmvc/src/main/java/org/springframework/web/servlet/tags/form/ErrorsTag.java6
-rw-r--r--spring-webmvc/src/main/java/org/springframework/web/servlet/tags/form/FormTag.java14
-rw-r--r--spring-webmvc/src/main/java/org/springframework/web/servlet/tags/form/OptionTag.java12
-rw-r--r--spring-webmvc/src/main/java/org/springframework/web/servlet/tags/form/TextareaTag.java4
-rw-r--r--spring-webmvc/src/main/java/org/springframework/web/servlet/view/ContentNegotiatingViewResolver.java98
-rw-r--r--spring-webmvc/src/main/java/org/springframework/web/servlet/view/InternalResourceViewResolver.java4
-rw-r--r--spring-webmvc/src/main/java/org/springframework/web/servlet/view/RedirectView.java35
-rw-r--r--spring-webmvc/src/main/java/org/springframework/web/servlet/view/document/AbstractExcelView.java3
-rw-r--r--spring-webmvc/src/main/java/org/springframework/web/servlet/view/document/AbstractXlsView.java120
-rw-r--r--spring-webmvc/src/main/java/org/springframework/web/servlet/view/document/AbstractXlsxStreamingView.java59
-rw-r--r--spring-webmvc/src/main/java/org/springframework/web/servlet/view/document/AbstractXlsxView.java54
-rw-r--r--spring-webmvc/src/main/java/org/springframework/web/servlet/view/freemarker/spring.ftl383
-rw-r--r--spring-webmvc/src/main/java/org/springframework/web/servlet/view/jasperreports/JasperReportsMultiFormatView.java3
-rw-r--r--spring-webmvc/src/main/java/org/springframework/web/servlet/view/jasperreports/JasperReportsXlsxView.java50
-rw-r--r--spring-webmvc/src/main/java/org/springframework/web/servlet/view/json/AbstractJackson2View.java53
-rw-r--r--spring-webmvc/src/main/java/org/springframework/web/servlet/view/json/MappingJackson2JsonView.java50
-rw-r--r--spring-webmvc/src/main/java/org/springframework/web/servlet/view/script/ScriptTemplateConfig.java79
-rw-r--r--spring-webmvc/src/main/java/org/springframework/web/servlet/view/script/ScriptTemplateConfigurer.java225
-rw-r--r--spring-webmvc/src/main/java/org/springframework/web/servlet/view/script/ScriptTemplateView.java408
-rw-r--r--spring-webmvc/src/main/java/org/springframework/web/servlet/view/script/ScriptTemplateViewResolver.java47
-rw-r--r--spring-webmvc/src/main/java/org/springframework/web/servlet/view/script/package-info.java6
-rw-r--r--spring-webmvc/src/main/java/org/springframework/web/servlet/view/velocity/VelocityViewResolver.java2
-rw-r--r--spring-webmvc/src/main/java/org/springframework/web/servlet/view/velocity/spring.vm319
-rw-r--r--spring-webmvc/src/main/java/org/springframework/web/servlet/view/xml/MappingJackson2XmlView.java10
132 files changed, 6905 insertions, 2213 deletions
diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/AsyncHandlerInterceptor.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/AsyncHandlerInterceptor.java
index 82c016d7..5284a2d1 100644
--- a/spring-webmvc/src/main/java/org/springframework/web/servlet/AsyncHandlerInterceptor.java
+++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/AsyncHandlerInterceptor.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2012 the original author or authors.
+ * Copyright 2002-2015 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.
@@ -22,20 +22,34 @@ import javax.servlet.http.HttpServletResponse;
import org.springframework.web.method.HandlerMethod;
/**
- * Extends {@code HandlerInterceptor} with a callback method invoked during
- * asynchronous request handling.
+ * Extends {@code HandlerInterceptor} with a callback method invoked after the
+ * start of asynchronous request handling.
*
- * <p>When a handler starts asynchronous request handling, the DispatcherServlet
- * exits without invoking {@code postHandle} and {@code afterCompletion}, as it
- * normally does, since the results of request handling (e.g. ModelAndView)
- * will. be produced concurrently in another thread. In such scenarios,
- * {@link #afterConcurrentHandlingStarted(HttpServletRequest, HttpServletResponse, Object)}
- * is invoked instead allowing implementations to perform tasks such as cleaning
- * up thread bound attributes.
+ * <p>When a handler starts an asynchronous request, the {@link DispatcherServlet}
+ * exits without invoking {@code postHandle} and {@code afterCompletion} as it
+ * normally does for a synchronous request, since the result of request handling
+ * (e.g. ModelAndView) is likely not yet ready and will be produced concurrently
+ * from another thread. In such scenarios, {@link #afterConcurrentHandlingStarted}
+ * is invoked instead, allowing implementations to perform tasks such as cleaning
+ * up thread-bound attributes before releasing the thread to the Servlet container.
*
* <p>When asynchronous handling completes, the request is dispatched to the
- * container for further processing. At this stage the DispatcherServlet invokes
- * {@code preHandle}, {@code postHandle} and {@code afterCompletion} as usual.
+ * container for further processing. At this stage the {@code DispatcherServlet}
+ * invokes {@code preHandle}, {@code postHandle}, and {@code afterCompletion}.
+ * To distinguish between the initial request and the subsequent dispatch
+ * after asynchronous handling completes, interceptors can check whether the
+ * {@code javax.servlet.DispatcherType} of {@link javax.servlet.ServletRequest}
+ * is {@code "REQUEST"} or {@code "ASYNC"}.
+ *
+ * <p>Note that {@code HandlerInterceptor} implementations may need to do work
+ * when an async request times out or completes with a network error. For such
+ * cases the Servlet container does not dispatch and therefore the
+ * {@code postHandle} and {@code afterCompletion} methods will not be invoked.
+ * Instead, interceptors can register to track an asynchronous request through
+ * the {@code registerCallbackInterceptor} and {@code registerDeferredResultInterceptor}
+ * methods on {@link org.springframework.web.context.request.async.WebAsyncManager
+ * WebAsyncManager}. This can be done proactively on every request from
+ * {@code preHandle} regardless of whether async request processing will start.
*
* @author Rossen Stoyanchev
* @since 3.2
@@ -48,14 +62,15 @@ public interface AsyncHandlerInterceptor extends HandlerInterceptor {
/**
* Called instead of {@code postHandle} and {@code afterCompletion}, when
- * the a handler is being executed concurrently. Implementations may use the
- * provided request and response but should avoid modifying them in ways
- * that would conflict with the concurrent execution of the handler. A
- * typical use of this method would be to clean thread local variables.
+ * the a handler is being executed concurrently.
+ * <p>Implementations may use the provided request and response but should
+ * avoid modifying them in ways that would conflict with the concurrent
+ * execution of the handler. A typical use of this method would be to
+ * clean up thread-local variables.
*
* @param request the current request
* @param response the current response
- * @param handler handler (or {@link HandlerMethod}) that started async
+ * @param handler the handler (or {@link HandlerMethod}) that started async
* execution, for type and/or instance examination
* @throws Exception in case of errors
*/
diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/DispatcherServlet.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/DispatcherServlet.java
index 97c1f4f4..7fd94248 100644
--- a/spring-webmvc/src/main/java/org/springframework/web/servlet/DispatcherServlet.java
+++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/DispatcherServlet.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2015 the original author or authors.
+ * Copyright 2002-2016 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.
@@ -42,7 +42,7 @@ import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.i18n.LocaleContext;
-import org.springframework.core.OrderComparator;
+import org.springframework.core.annotation.AnnotationAwareOrderComparator;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.support.PropertiesLoaderUtils;
import org.springframework.http.server.ServletServerHttpRequest;
@@ -206,7 +206,7 @@ public class DispatcherServlet extends FrameworkServlet {
/**
* Request attribute to hold the current web application context.
* Otherwise only the global web app context is obtainable by tags etc.
- * @see org.springframework.web.servlet.support.RequestContextUtils#getWebApplicationContext
+ * @see org.springframework.web.servlet.support.RequestContextUtils#findWebApplicationContext
*/
public static final String WEB_APPLICATION_CONTEXT_ATTRIBUTE = DispatcherServlet.class.getName() + ".CONTEXT";
@@ -328,6 +328,7 @@ public class DispatcherServlet extends FrameworkServlet {
/** List of ViewResolvers used by this servlet */
private List<ViewResolver> viewResolvers;
+
/**
* Create a new {@code DispatcherServlet} that will create its own internal web
* application context based on defaults and values provided through servlet
@@ -392,6 +393,7 @@ public class DispatcherServlet extends FrameworkServlet {
super(webApplicationContext);
}
+
/**
* Set whether to detect all HandlerMapping beans in this servlet's context. Otherwise,
* just a single bean with name "handlerMapping" will be expected.
@@ -463,6 +465,7 @@ public class DispatcherServlet extends FrameworkServlet {
this.cleanupAfterInclude = cleanupAfterInclude;
}
+
/**
* This implementation calls {@link #initStrategies}.
*/
@@ -547,9 +550,8 @@ public class DispatcherServlet extends FrameworkServlet {
// We need to use the default.
this.themeResolver = getDefaultStrategy(context, ThemeResolver.class);
if (logger.isDebugEnabled()) {
- logger.debug(
- "Unable to locate ThemeResolver with name '" + THEME_RESOLVER_BEAN_NAME + "': using default [" +
- this.themeResolver + "]");
+ logger.debug("Unable to locate ThemeResolver with name '" + THEME_RESOLVER_BEAN_NAME +
+ "': using default [" + this.themeResolver + "]");
}
}
}
@@ -569,7 +571,7 @@ public class DispatcherServlet extends FrameworkServlet {
if (!matchingBeans.isEmpty()) {
this.handlerMappings = new ArrayList<HandlerMapping>(matchingBeans.values());
// We keep HandlerMappings in sorted order.
- OrderComparator.sort(this.handlerMappings);
+ AnnotationAwareOrderComparator.sort(this.handlerMappings);
}
}
else {
@@ -607,7 +609,7 @@ public class DispatcherServlet extends FrameworkServlet {
if (!matchingBeans.isEmpty()) {
this.handlerAdapters = new ArrayList<HandlerAdapter>(matchingBeans.values());
// We keep HandlerAdapters in sorted order.
- OrderComparator.sort(this.handlerAdapters);
+ AnnotationAwareOrderComparator.sort(this.handlerAdapters);
}
}
else {
@@ -645,7 +647,7 @@ public class DispatcherServlet extends FrameworkServlet {
if (!matchingBeans.isEmpty()) {
this.handlerExceptionResolvers = new ArrayList<HandlerExceptionResolver>(matchingBeans.values());
// We keep HandlerExceptionResolvers in sorted order.
- OrderComparator.sort(this.handlerExceptionResolvers);
+ AnnotationAwareOrderComparator.sort(this.handlerExceptionResolvers);
}
}
else {
@@ -707,7 +709,7 @@ public class DispatcherServlet extends FrameworkServlet {
if (!matchingBeans.isEmpty()) {
this.viewResolvers = new ArrayList<ViewResolver>(matchingBeans.values());
// We keep ViewResolvers in sorted order.
- OrderComparator.sort(this.viewResolvers);
+ AnnotationAwareOrderComparator.sort(this.viewResolvers);
}
}
else {
@@ -737,8 +739,7 @@ public class DispatcherServlet extends FrameworkServlet {
*/
private void initFlashMapManager(ApplicationContext context) {
try {
- this.flashMapManager =
- context.getBean(FLASH_MAP_MANAGER_BEAN_NAME, FlashMapManager.class);
+ this.flashMapManager = context.getBean(FLASH_MAP_MANAGER_BEAN_NAME, FlashMapManager.class);
if (logger.isDebugEnabled()) {
logger.debug("Using FlashMapManager [" + this.flashMapManager + "]");
}
@@ -838,7 +839,8 @@ public class DispatcherServlet extends FrameworkServlet {
/**
* Create a default strategy.
- * <p>The default implementation uses {@link org.springframework.beans.factory.config.AutowireCapableBeanFactory#createBean}.
+ * <p>The default implementation uses
+ * {@link org.springframework.beans.factory.config.AutowireCapableBeanFactory#createBean}.
* @param context the current WebApplicationContext
* @param clazz the strategy implementation class to instantiate
* @return the fully configured strategy instance
@@ -962,7 +964,7 @@ public class DispatcherServlet extends FrameworkServlet {
return;
}
- applyDefaultViewName(request, mv);
+ applyDefaultViewName(processedRequest, mv);
mappedHandler.applyPostHandle(processedRequest, response, mv);
}
catch (Exception ex) {
@@ -1099,7 +1101,8 @@ public class DispatcherServlet extends FrameworkServlet {
* @see MultipartResolver#cleanupMultipart
*/
protected void cleanupMultipart(HttpServletRequest request) {
- MultipartHttpServletRequest multipartRequest = WebUtils.getNativeRequest(request, MultipartHttpServletRequest.class);
+ MultipartHttpServletRequest multipartRequest =
+ WebUtils.getNativeRequest(request, MultipartHttpServletRequest.class);
if (multipartRequest != null) {
this.multipartResolver.cleanupMultipart(multipartRequest);
}
diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/DispatcherServlet.properties b/spring-webmvc/src/main/java/org/springframework/web/servlet/DispatcherServlet.properties
deleted file mode 100644
index c8e5ab29..00000000
--- a/spring-webmvc/src/main/java/org/springframework/web/servlet/DispatcherServlet.properties
+++ /dev/null
@@ -1,24 +0,0 @@
-# Default implementation classes for DispatcherServlet's strategy interfaces.
-# Used as fallback when no matching beans are found in the DispatcherServlet context.
-# Not meant to be customized by application developers.
-
-org.springframework.web.servlet.LocaleResolver=org.springframework.web.servlet.i18n.AcceptHeaderLocaleResolver
-
-org.springframework.web.servlet.ThemeResolver=org.springframework.web.servlet.theme.FixedThemeResolver
-
-org.springframework.web.servlet.HandlerMapping=org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping,\
- org.springframework.web.servlet.mvc.annotation.DefaultAnnotationHandlerMapping
-
-org.springframework.web.servlet.HandlerAdapter=org.springframework.web.servlet.mvc.HttpRequestHandlerAdapter,\
- org.springframework.web.servlet.mvc.SimpleControllerHandlerAdapter,\
- org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter
-
-org.springframework.web.servlet.HandlerExceptionResolver=org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerExceptionResolver,\
- org.springframework.web.servlet.mvc.annotation.ResponseStatusExceptionResolver,\
- org.springframework.web.servlet.mvc.support.DefaultHandlerExceptionResolver
-
-org.springframework.web.servlet.RequestToViewNameTranslator=org.springframework.web.servlet.view.DefaultRequestToViewNameTranslator
-
-org.springframework.web.servlet.ViewResolver=org.springframework.web.servlet.view.InternalResourceViewResolver
-
-org.springframework.web.servlet.FlashMapManager=org.springframework.web.servlet.support.SessionFlashMapManager \ No newline at end of file
diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/FlashMap.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/FlashMap.java
index 03a2ecfd..cc752af7 100644
--- a/spring-webmvc/src/main/java/org/springframework/web/servlet/FlashMap.java
+++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/FlashMap.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2014 the original author or authors.
+ * Copyright 2002-2015 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.
@@ -50,11 +50,9 @@ public final class FlashMap extends HashMap<String, Object> implements Comparabl
private String targetRequestPath;
- private final MultiValueMap<String, String> targetRequestParams = new LinkedMultiValueMap<String, String>();
+ private final MultiValueMap<String, String> targetRequestParams = new LinkedMultiValueMap<String, String>(4);
- private long expirationStartTime;
-
- private int timeToLive;
+ private long expirationTime = -1;
/**
@@ -112,8 +110,25 @@ public final class FlashMap extends HashMap<String, Object> implements Comparabl
* @param timeToLive the number of seconds before expiration
*/
public void startExpirationPeriod(int timeToLive) {
- this.expirationStartTime = System.currentTimeMillis();
- this.timeToLive = timeToLive;
+ this.expirationTime = System.currentTimeMillis() + timeToLive * 1000;
+ }
+
+ /**
+ * Set the expiration time for the FlashMap. This is provided for serialization
+ * purposes but can also be used instead {@link #startExpirationPeriod(int)}.
+ * @since 4.2
+ */
+ public void setExpirationTime(long expirationTime) {
+ this.expirationTime = expirationTime;
+ }
+
+ /**
+ * Return the expiration time for the FlashMap or -1 if the expiration
+ * period has not started.
+ * @since 4.2
+ */
+ public long getExpirationTime() {
+ return this.expirationTime;
}
/**
@@ -121,8 +136,7 @@ public final class FlashMap extends HashMap<String, Object> implements Comparabl
* elapsed time since the call to {@link #startExpirationPeriod}.
*/
public boolean isExpired() {
- return (this.expirationStartTime != 0 &&
- (System.currentTimeMillis() - this.expirationStartTime > this.timeToLive * 1000));
+ return (this.expirationTime != -1 && System.currentTimeMillis() > this.expirationTime);
}
@@ -167,11 +181,8 @@ public final class FlashMap extends HashMap<String, Object> implements Comparabl
@Override
public String toString() {
- StringBuilder sb = new StringBuilder();
- sb.append("FlashMap [attributes=").append(super.toString());
- sb.append(", targetRequestPath=").append(this.targetRequestPath);
- sb.append(", targetRequestParams=").append(this.targetRequestParams).append("]");
- return sb.toString();
+ return "FlashMap [attributes=" + super.toString() + ", targetRequestPath=" +
+ this.targetRequestPath + ", targetRequestParams=" + this.targetRequestParams + "]";
}
}
diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/FrameworkServlet.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/FrameworkServlet.java
index c68d06b9..0cf705a9 100644
--- a/spring-webmvc/src/main/java/org/springframework/web/servlet/FrameworkServlet.java
+++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/FrameworkServlet.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2015 the original author or authors.
+ * Copyright 2002-2016 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.
@@ -42,11 +42,10 @@ import org.springframework.context.i18n.SimpleLocaleContext;
import org.springframework.core.GenericTypeResolver;
import org.springframework.core.annotation.AnnotationAwareOrderComparator;
import org.springframework.core.env.ConfigurableEnvironment;
-import org.springframework.util.Assert;
+import org.springframework.http.HttpMethod;
import org.springframework.util.ClassUtils;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
-import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.context.ConfigurableWebApplicationContext;
import org.springframework.web.context.ConfigurableWebEnvironment;
import org.springframework.web.context.ContextLoader;
@@ -61,6 +60,7 @@ import org.springframework.web.context.request.async.WebAsyncUtils;
import org.springframework.web.context.support.ServletRequestHandledEvent;
import org.springframework.web.context.support.WebApplicationContextUtils;
import org.springframework.web.context.support.XmlWebApplicationContext;
+import org.springframework.web.cors.CorsUtils;
import org.springframework.web.util.NestedServletException;
import org.springframework.web.util.WebUtils;
@@ -369,9 +369,11 @@ public abstract class FrameworkServlet extends HttpServletBean implements Applic
* @see #applyInitializers
*/
@SuppressWarnings("unchecked")
- public void setContextInitializers(ApplicationContextInitializer<? extends ConfigurableApplicationContext>... initializers) {
- for (ApplicationContextInitializer<? extends ConfigurableApplicationContext> initializer : initializers) {
- this.contextInitializers.add((ApplicationContextInitializer<ConfigurableApplicationContext>) initializer);
+ public void setContextInitializers(ApplicationContextInitializer<?>... initializers) {
+ if (initializers != null) {
+ for (ApplicationContextInitializer<?> initializer : initializers) {
+ this.contextInitializers.add((ApplicationContextInitializer<ConfigurableApplicationContext>) initializer);
+ }
}
}
@@ -734,17 +736,17 @@ public abstract class FrameworkServlet extends HttpServletBean implements Applic
Class<?> initializerClass = ClassUtils.forName(className, wac.getClassLoader());
Class<?> initializerContextClass =
GenericTypeResolver.resolveTypeArgument(initializerClass, ApplicationContextInitializer.class);
- if (initializerContextClass != null) {
- Assert.isAssignable(initializerContextClass, wac.getClass(), String.format(
- "Could not add context initializer [%s] since its generic parameter [%s] " +
+ if (initializerContextClass != null && !initializerContextClass.isInstance(wac)) {
+ throw new ApplicationContextException(String.format(
+ "Could not apply context initializer [%s] since its generic parameter [%s] " +
"is not assignable from the type of application context used by this " +
- "framework servlet [%s]: ", initializerClass.getName(), initializerContextClass.getName(),
+ "framework servlet: [%s]", initializerClass.getName(), initializerContextClass.getName(),
wac.getClass().getName()));
}
return BeanUtils.instantiateClass(initializerClass, ApplicationContextInitializer.class);
}
- catch (Exception ex) {
- throw new IllegalArgumentException(String.format("Could not instantiate class [%s] specified " +
+ catch (ClassNotFoundException ex) {
+ throw new ApplicationContextException(String.format("Could not load class [%s] specified " +
"via 'contextInitializerClasses' init-param", className), ex);
}
}
@@ -834,7 +836,7 @@ public abstract class FrameworkServlet extends HttpServletBean implements Applic
protected void service(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
- if (RequestMethod.PATCH.name().equalsIgnoreCase(request.getMethod())) {
+ if (HttpMethod.PATCH.matches(request.getMethod())) {
processRequest(request, response);
}
else {
@@ -899,7 +901,7 @@ public abstract class FrameworkServlet extends HttpServletBean implements Applic
protected void doOptions(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
- if (this.dispatchOptionsRequest) {
+ if (this.dispatchOptionsRequest || CorsUtils.isPreFlightRequest(request)) {
processRequest(request, response);
if (response.containsHeader("Allow")) {
// Proper OPTIONS response coming from a handler - we're done.
@@ -913,7 +915,7 @@ public abstract class FrameworkServlet extends HttpServletBean implements Applic
@Override
public void setHeader(String name, String value) {
if ("Allow".equals(name)) {
- value = (StringUtils.hasLength(value) ? value + ", " : "") + RequestMethod.PATCH.name();
+ value = (StringUtils.hasLength(value) ? value + ", " : "") + HttpMethod.PATCH.name();
}
super.setHeader(name, value);
}
diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/HandlerExceptionResolver.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/HandlerExceptionResolver.java
index 13407758..d7d1b6d9 100644
--- a/spring-webmvc/src/main/java/org/springframework/web/servlet/HandlerExceptionResolver.java
+++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/HandlerExceptionResolver.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2012 the original author or authors.
+ * Copyright 2002-2015 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.
@@ -20,13 +20,13 @@ import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
- * Interface to be implemented by objects than can resolve exceptions thrown
- * during handler mapping or execution, in the typical case to error views.
- * Implementors are typically registered as beans in the application context.
+ * Interface to be implemented by objects that can resolve exceptions thrown during
+ * handler mapping or execution, in the typical case to error views. Implementors are
+ * typically registered as beans in the application context.
*
- * <p>Error views are analogous to the error page JSPs, but can be used with
- * any kind of exception including any checked exception, with potentially
- * fine-granular mappings for specific handlers.
+ * <p>Error views are analogous to JSP error pages but can be used with any kind of
+ * exception including any checked exception, with potentially fine-grained mappings for
+ * specific handlers.
*
* @author Juergen Hoeller
* @since 22.11.2003
@@ -34,9 +34,9 @@ import javax.servlet.http.HttpServletResponse;
public interface HandlerExceptionResolver {
/**
- * Try to resolve the given exception that got thrown during on handler execution,
- * returning a ModelAndView that represents a specific error page if appropriate.
- * <p>The returned ModelAndView may be {@linkplain ModelAndView#isEmpty() empty}
+ * Try to resolve the given exception that got thrown during handler execution,
+ * returning a {@link ModelAndView} that represents a specific error page if appropriate.
+ * <p>The returned {@code ModelAndView} may be {@linkplain ModelAndView#isEmpty() empty}
* to indicate that the exception has been resolved successfully but that no view
* should be rendered, for instance by setting a status code.
* @param request current HTTP request
@@ -44,8 +44,8 @@ public interface HandlerExceptionResolver {
* @param handler the executed handler, or {@code null} if none chosen at the
* time of the exception (for example, if multipart resolution failed)
* @param ex the exception that got thrown during handler execution
- * @return a corresponding ModelAndView to forward to,
- * or {@code null} for default processing
+ * @return a corresponding {@code ModelAndView} to forward to, or {@code null}
+ * for default processing
*/
ModelAndView resolveException(
HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex);
diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/HandlerInterceptor.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/HandlerInterceptor.java
index a03e5152..2aa28d5d 100644
--- a/spring-webmvc/src/main/java/org/springframework/web/servlet/HandlerInterceptor.java
+++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/HandlerInterceptor.java
@@ -77,10 +77,16 @@ public interface HandlerInterceptor {
/**
* Intercept the execution of a handler. Called after HandlerMapping determined
* an appropriate handler object, but before HandlerAdapter invokes the handler.
+ *
* <p>DispatcherServlet processes a handler in an execution chain, consisting
* of any number of interceptors, with the handler itself at the end.
* With this method, each interceptor can decide to abort the execution chain,
* typically sending a HTTP error or writing a custom response.
+ *
+ * <p><strong>Note:</strong> special considerations apply for asynchronous
+ * request processing. For more details see
+ * {@link org.springframework.web.servlet.AsyncHandlerInterceptor}.
+ *
* @param request current HTTP request
* @param response current HTTP response
* @param handler chosen handler to execute, for type and/or instance evaluation
@@ -96,10 +102,16 @@ public interface HandlerInterceptor {
* Intercept the execution of a handler. Called after HandlerAdapter actually
* invoked the handler, but before the DispatcherServlet renders the view.
* Can expose additional model objects to the view via the given ModelAndView.
+ *
* <p>DispatcherServlet processes a handler in an execution chain, consisting
* of any number of interceptors, with the handler itself at the end.
* With this method, each interceptor can post-process an execution,
* getting applied in inverse order of the execution chain.
+ *
+ * <p><strong>Note:</strong> special considerations apply for asynchronous
+ * request processing. For more details see
+ * {@link org.springframework.web.servlet.AsyncHandlerInterceptor}.
+ *
* @param request current HTTP request
* @param response current HTTP response
* @param handler handler (or {@link HandlerMethod}) that started async
@@ -115,11 +127,18 @@ public interface HandlerInterceptor {
* Callback after completion of request processing, that is, after rendering
* the view. Will be called on any outcome of handler execution, thus allows
* for proper resource cleanup.
+ *
* <p>Note: Will only be called if this interceptor's {@code preHandle}
* method has successfully completed and returned {@code true}!
+ *
* <p>As with the {@code postHandle} method, the method will be invoked on each
* interceptor in the chain in reverse order, so the first interceptor will be
* the last to be invoked.
+ *
+ * <p><strong>Note:</strong> special considerations apply for asynchronous
+ * request processing. For more details see
+ * {@link org.springframework.web.servlet.AsyncHandlerInterceptor}.
+ *
* @param request current HTTP request
* @param response current HTTP response
* @param handler handler (or {@link HandlerMethod}) that started async
diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/HandlerMapping.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/HandlerMapping.java
index 791e169a..117bff35 100644
--- a/spring-webmvc/src/main/java/org/springframework/web/servlet/HandlerMapping.java
+++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/HandlerMapping.java
@@ -24,7 +24,7 @@ import javax.servlet.http.HttpServletRequest;
*
* <p>This class can be implemented by application developers, although this is not
* necessary, as {@link org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping}
- * and {@link org.springframework.web.servlet.handler.SimpleUrlHandlerMapping}
+ * and {@link org.springframework.web.servlet.mvc.annotation.DefaultAnnotationHandlerMapping}
* are included in the framework. The former is the default if no
* HandlerMapping bean is registered in the application context.
*
@@ -49,7 +49,7 @@ import javax.servlet.http.HttpServletRequest;
* @see org.springframework.core.Ordered
* @see org.springframework.web.servlet.handler.AbstractHandlerMapping
* @see org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping
- * @see org.springframework.web.servlet.handler.SimpleUrlHandlerMapping
+ * @see org.springframework.web.servlet.mvc.annotation.DefaultAnnotationHandlerMapping
*/
public interface HandlerMapping {
diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/NoHandlerFoundException.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/NoHandlerFoundException.java
index 959bba67..63f83c1c 100644
--- a/spring-webmvc/src/main/java/org/springframework/web/servlet/NoHandlerFoundException.java
+++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/NoHandlerFoundException.java
@@ -23,9 +23,10 @@ import javax.servlet.http.HttpServletResponse;
import org.springframework.http.HttpHeaders;
/**
- * Exception to be thrown if DispatcherServlet is unable to determine a corresponding
- * handler for an incoming HTTP request. The DispatcherServlet throws this exception
- * only if its "throwExceptionIfNoHandlerFound" property is set to "true".
+ * By default when the DispatcherServlet can't find a handler for a request it
+ * sends a 404 response. However if its property "throwExceptionIfNoHandlerFound"
+ * is set to {@code true} this exception is raised and may be handled with
+ * a configured HandlerExceptionResolver.
*
* @author Brian Clozel
* @since 4.0
@@ -49,12 +50,13 @@ public class NoHandlerFoundException extends ServletException {
* @param headers the HTTP request headers
*/
public NoHandlerFoundException(String httpMethod, String requestURL, HttpHeaders headers) {
- super("No handler found for " + httpMethod + " " + requestURL + ", headers=" + headers);
+ super("No handler found for " + httpMethod + " " + requestURL);
this.httpMethod = httpMethod;
this.requestURL = requestURL;
this.headers = headers;
}
+
public String getHttpMethod() {
return this.httpMethod;
}
diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/ViewRendererServlet.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/ViewRendererServlet.java
index e7574d23..f634ac80 100644
--- a/spring-webmvc/src/main/java/org/springframework/web/servlet/ViewRendererServlet.java
+++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/ViewRendererServlet.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2012 the original author or authors.
+ * Copyright 2002-2015 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.
@@ -48,7 +48,7 @@ public class ViewRendererServlet extends HttpServlet {
/**
* Request attribute to hold current web application context.
* Otherwise only the global web app context is obtainable by tags etc.
- * @see org.springframework.web.servlet.support.RequestContextUtils#getWebApplicationContext
+ * @see org.springframework.web.servlet.support.RequestContextUtils#findWebApplicationContext
*/
public static final String WEB_APPLICATION_CONTEXT_ATTRIBUTE = DispatcherServlet.WEB_APPLICATION_CONTEXT_ATTRIBUTE;
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 243a7561..7c8cbcb4 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
@@ -74,6 +74,7 @@ import org.springframework.web.servlet.mvc.HttpRequestHandlerAdapter;
import org.springframework.web.servlet.mvc.SimpleControllerHandlerAdapter;
import org.springframework.web.servlet.mvc.annotation.ResponseStatusExceptionResolver;
import org.springframework.web.servlet.mvc.method.annotation.ExceptionHandlerExceptionResolver;
+import org.springframework.web.servlet.mvc.method.annotation.JsonViewRequestBodyAdvice;
import org.springframework.web.servlet.mvc.method.annotation.JsonViewResponseBodyAdvice;
import org.springframework.web.servlet.mvc.method.annotation.MvcUriComponentsBuilder;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter;
@@ -202,6 +203,9 @@ class AnnotationDrivenBeanDefinitionParser implements BeanDefinitionParser {
configurePathMatchingProperties(handlerMappingDef, element, parserContext);
readerContext.getRegistry().registerBeanDefinition(HANDLER_MAPPING_BEAN_NAME , handlerMappingDef);
+ RuntimeBeanReference corsConfigurationsRef = MvcNamespaceUtils.registerCorsConfigurations(null, parserContext, source);
+ handlerMappingDef.getPropertyValues().add("corsConfigurations", corsConfigurationsRef);
+
RuntimeBeanReference conversionService = getConversionService(element, source, parserContext);
RuntimeBeanReference validator = getValidator(element, source, parserContext);
RuntimeBeanReference messageCodesResolver = getMessageCodesResolver(element);
@@ -227,6 +231,7 @@ class AnnotationDrivenBeanDefinitionParser implements BeanDefinitionParser {
handlerAdapterDef.getPropertyValues().add("contentNegotiationManager", contentNegotiationManager);
handlerAdapterDef.getPropertyValues().add("webBindingInitializer", bindingDef);
handlerAdapterDef.getPropertyValues().add("messageConverters", messageConverters);
+ addRequestBodyAdvice(handlerAdapterDef);
addResponseBodyAdvice(handlerAdapterDef);
if (element.hasAttribute("ignore-default-model-on-redirect")) {
@@ -313,6 +318,13 @@ class AnnotationDrivenBeanDefinitionParser implements BeanDefinitionParser {
return null;
}
+ protected void addRequestBodyAdvice(RootBeanDefinition beanDef) {
+ if (jackson2Present) {
+ beanDef.getPropertyValues().add("requestBodyAdvice",
+ new RootBeanDefinition(JsonViewRequestBodyAdvice.class));
+ }
+ }
+
protected void addResponseBodyAdvice(RootBeanDefinition beanDef) {
if (jackson2Present) {
beanDef.getPropertyValues().add("responseBodyAdvice",
diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/CorsBeanDefinitionParser.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/CorsBeanDefinitionParser.java
new file mode 100644
index 00000000..c62f70dc
--- /dev/null
+++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/CorsBeanDefinitionParser.java
@@ -0,0 +1,120 @@
+/*
+ * Copyright 2002-2015 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.config;
+
+import java.util.Arrays;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.w3c.dom.Element;
+
+import org.springframework.beans.factory.config.BeanDefinition;
+import org.springframework.beans.factory.xml.BeanDefinitionParser;
+import org.springframework.beans.factory.xml.ParserContext;
+import org.springframework.http.HttpMethod;
+import org.springframework.util.StringUtils;
+import org.springframework.util.xml.DomUtils;
+import org.springframework.web.cors.CorsConfiguration;
+
+/**
+ * {@link org.springframework.beans.factory.xml.BeanDefinitionParser} that parses a
+ * {@code cors} element in order to set the CORS configuration in the various
+ * {AbstractHandlerMapping} beans created by {@link AnnotationDrivenBeanDefinitionParser},
+ * {@link ResourcesBeanDefinitionParser} and {@link ViewControllerBeanDefinitionParser}.
+ *
+ * @author Sebastien Deleuze
+ * @since 4.2
+ */
+public class CorsBeanDefinitionParser implements BeanDefinitionParser {
+
+ private static final List<String> DEFAULT_ALLOWED_ORIGINS = Arrays.asList("*");
+
+ private static final List<String> DEFAULT_ALLOWED_METHODS =
+ Arrays.asList(HttpMethod.GET.name(), HttpMethod.HEAD.name(), HttpMethod.POST.name());
+
+ private static final List<String> DEFAULT_ALLOWED_HEADERS = Arrays.asList("*");
+
+ private static final boolean DEFAULT_ALLOW_CREDENTIALS = true;
+
+ private static final long DEFAULT_MAX_AGE = 1600;
+
+
+ @Override
+ public BeanDefinition parse(Element element, ParserContext parserContext) {
+
+ Map<String, CorsConfiguration> corsConfigurations = new LinkedHashMap<String, CorsConfiguration>();
+ List<Element> mappings = DomUtils.getChildElementsByTagName(element, "mapping");
+
+ if (mappings.isEmpty()) {
+ CorsConfiguration config = new CorsConfiguration();
+ config.setAllowedOrigins(DEFAULT_ALLOWED_ORIGINS);
+ config.setAllowedMethods(DEFAULT_ALLOWED_METHODS);
+ config.setAllowedHeaders(DEFAULT_ALLOWED_HEADERS);
+ config.setAllowCredentials(DEFAULT_ALLOW_CREDENTIALS);
+ config.setMaxAge(DEFAULT_MAX_AGE);
+ corsConfigurations.put("/**", config);
+ }
+ else {
+ for (Element mapping : mappings) {
+ CorsConfiguration config = new CorsConfiguration();
+ if (mapping.hasAttribute("allowed-origins")) {
+ String[] allowedOrigins = StringUtils.tokenizeToStringArray(mapping.getAttribute("allowed-origins"), ",");
+ config.setAllowedOrigins(Arrays.asList(allowedOrigins));
+ }
+ else {
+ config.setAllowedOrigins(DEFAULT_ALLOWED_ORIGINS);
+ }
+ if (mapping.hasAttribute("allowed-methods")) {
+ String[] allowedMethods = StringUtils.tokenizeToStringArray(mapping.getAttribute("allowed-methods"), ",");
+ config.setAllowedMethods(Arrays.asList(allowedMethods));
+ }
+ else {
+ config.setAllowedMethods(DEFAULT_ALLOWED_METHODS);
+ }
+ if (mapping.hasAttribute("allowed-headers")) {
+ String[] allowedHeaders = StringUtils.tokenizeToStringArray(mapping.getAttribute("allowed-headers"), ",");
+ config.setAllowedHeaders(Arrays.asList(allowedHeaders));
+ }
+ else {
+ config.setAllowedHeaders(DEFAULT_ALLOWED_HEADERS);
+ }
+ if (mapping.hasAttribute("exposed-headers")) {
+ String[] exposedHeaders = StringUtils.tokenizeToStringArray(mapping.getAttribute("exposed-headers"), ",");
+ config.setExposedHeaders(Arrays.asList(exposedHeaders));
+ }
+ if (mapping.hasAttribute("allow-credentials")) {
+ config.setAllowCredentials(Boolean.parseBoolean(mapping.getAttribute("allow-credentials")));
+ }
+ else {
+ config.setAllowCredentials(DEFAULT_ALLOW_CREDENTIALS);
+ }
+ if (mapping.hasAttribute("max-age")) {
+ config.setMaxAge(Long.parseLong(mapping.getAttribute("max-age")));
+ }
+ else {
+ config.setMaxAge(DEFAULT_MAX_AGE);
+ }
+ corsConfigurations.put(mapping.getAttribute("path"), config);
+ }
+ }
+
+ MvcNamespaceUtils.registerCorsConfigurations(corsConfigurations, parserContext, parserContext.extractSource(element));
+ return null;
+ }
+
+}
diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/MvcNamespaceHandler.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/MvcNamespaceHandler.java
index 03edc267..1cfe9108 100644
--- a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/MvcNamespaceHandler.java
+++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/MvcNamespaceHandler.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2014 the original author or authors.
+ * Copyright 2002-2015 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.
@@ -24,6 +24,7 @@ import org.springframework.beans.factory.xml.NamespaceHandlerSupport;
*
* @author Keith Donald
* @author Jeremy Grelle
+ * @author Sebastien Deleuze
* @since 3.0
*/
public class MvcNamespaceHandler extends NamespaceHandlerSupport {
@@ -42,6 +43,8 @@ public class MvcNamespaceHandler extends NamespaceHandlerSupport {
registerBeanDefinitionParser("freemarker-configurer", new FreeMarkerConfigurerBeanDefinitionParser());
registerBeanDefinitionParser("velocity-configurer", new VelocityConfigurerBeanDefinitionParser());
registerBeanDefinitionParser("groovy-configurer", new GroovyMarkupConfigurerBeanDefinitionParser());
+ registerBeanDefinitionParser("script-template-configurer", new ScriptTemplateConfigurerBeanDefinitionParser());
+ registerBeanDefinitionParser("cors", new CorsBeanDefinitionParser());
}
}
diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/MvcNamespaceUtils.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/MvcNamespaceUtils.java
index 30e1688d..e6684136 100644
--- a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/MvcNamespaceUtils.java
+++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/MvcNamespaceUtils.java
@@ -16,6 +16,9 @@
package org.springframework.web.servlet.config;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.RuntimeBeanReference;
import org.springframework.beans.factory.parsing.BeanComponentDefinition;
@@ -23,6 +26,7 @@ import org.springframework.beans.factory.support.RootBeanDefinition;
import org.springframework.beans.factory.xml.ParserContext;
import org.springframework.util.AntPathMatcher;
import org.springframework.util.PathMatcher;
+import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping;
import org.springframework.web.servlet.mvc.HttpRequestHandlerAdapter;
import org.springframework.web.servlet.mvc.SimpleControllerHandlerAdapter;
@@ -50,6 +54,8 @@ abstract class MvcNamespaceUtils {
private static final String PATH_MATCHER_BEAN_NAME = "mvcPathMatcher";
+ private static final String CORS_CONFIGURATION_BEAN_NAME = "mvcCorsConfigurations";
+
public static void registerDefaultComponents(ParserContext parserContext, Object source) {
registerBeanNameUrlHandlerMapping(parserContext, source);
@@ -113,6 +119,8 @@ abstract class MvcNamespaceUtils {
beanNameMappingDef.setSource(source);
beanNameMappingDef.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
beanNameMappingDef.getPropertyValues().add("order", 2); // consistent with WebMvcConfigurationSupport
+ RuntimeBeanReference corsConfigurationsRef = MvcNamespaceUtils.registerCorsConfigurations(null, parserContext, source);
+ beanNameMappingDef.getPropertyValues().add("corsConfigurations", corsConfigurationsRef);
parserContext.getRegistry().registerBeanDefinition(BEAN_NAME_URL_HANDLER_MAPPING_BEAN_NAME, beanNameMappingDef);
parserContext.registerComponent(new BeanComponentDefinition(beanNameMappingDef, BEAN_NAME_URL_HANDLER_MAPPING_BEAN_NAME));
}
@@ -146,4 +154,28 @@ abstract class MvcNamespaceUtils {
}
}
+ /**
+ * Registers a {@code Map<String, CorsConfiguration>} (mapped {@code CorsConfiguration}s)
+ * under a well-known name unless already registered. The bean definition may be updated
+ * if a non-null CORS configuration is provided.
+ * @return a RuntimeBeanReference to this {@code Map<String, CorsConfiguration>} instance
+ */
+ public static RuntimeBeanReference registerCorsConfigurations(Map<String, CorsConfiguration> corsConfigurations, ParserContext parserContext, Object source) {
+ if (!parserContext.getRegistry().containsBeanDefinition(CORS_CONFIGURATION_BEAN_NAME)) {
+ RootBeanDefinition corsConfigurationsDef = new RootBeanDefinition(LinkedHashMap.class);
+ corsConfigurationsDef.setSource(source);
+ corsConfigurationsDef.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
+ if (corsConfigurations != null) {
+ corsConfigurationsDef.getConstructorArgumentValues().addIndexedArgumentValue(0, corsConfigurations);
+ }
+ parserContext.getReaderContext().getRegistry().registerBeanDefinition(CORS_CONFIGURATION_BEAN_NAME, corsConfigurationsDef);
+ parserContext.registerComponent(new BeanComponentDefinition(corsConfigurationsDef, CORS_CONFIGURATION_BEAN_NAME));
+ }
+ else if (corsConfigurations != null) {
+ BeanDefinition corsConfigurationsDef = parserContext.getRegistry().getBeanDefinition(CORS_CONFIGURATION_BEAN_NAME);
+ corsConfigurationsDef.getConstructorArgumentValues().addIndexedArgumentValue(0, corsConfigurations);
+ }
+ return new RuntimeBeanReference(CORS_CONFIGURATION_BEAN_NAME);
+ }
+
}
diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/ResourcesBeanDefinitionParser.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/ResourcesBeanDefinitionParser.java
index 26322bf7..f55e287a 100644
--- a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/ResourcesBeanDefinitionParser.java
+++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/ResourcesBeanDefinitionParser.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2014 the original author or authors.
+ * Copyright 2002-2015 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.
@@ -18,6 +18,7 @@ package org.springframework.web.servlet.config;
import java.util.Arrays;
import java.util.Map;
+import java.util.concurrent.TimeUnit;
import org.w3c.dom.Element;
@@ -32,8 +33,10 @@ import org.springframework.beans.factory.xml.BeanDefinitionParser;
import org.springframework.beans.factory.xml.ParserContext;
import org.springframework.cache.concurrent.ConcurrentMapCache;
import org.springframework.core.Ordered;
+import org.springframework.util.ClassUtils;
import org.springframework.util.StringUtils;
import org.springframework.util.xml.DomUtils;
+import org.springframework.http.CacheControl;
import org.springframework.web.servlet.handler.MappedInterceptor;
import org.springframework.web.servlet.handler.SimpleUrlHandlerMapping;
import org.springframework.web.servlet.mvc.HttpRequestHandlerAdapter;
@@ -49,6 +52,7 @@ import org.springframework.web.servlet.resource.ResourceTransformer;
import org.springframework.web.servlet.resource.ResourceUrlProvider;
import org.springframework.web.servlet.resource.ResourceUrlProviderExposingInterceptor;
import org.springframework.web.servlet.resource.VersionResourceResolver;
+import org.springframework.web.servlet.resource.WebJarsResourceResolver;
/**
* {@link org.springframework.beans.factory.xml.BeanDefinitionParser} that parses a
@@ -76,6 +80,9 @@ class ResourcesBeanDefinitionParser implements BeanDefinitionParser {
private static final String RESOURCE_URL_PROVIDER = "mvcResourceUrlProvider";
+ private static final boolean isWebJarsAssetLocatorPresent = ClassUtils.isPresent(
+ "org.webjars.WebJarAssetLocator", ResourcesBeanDefinitionParser.class.getClassLoader());
+
@Override
public BeanDefinition parse(Element element, ParserContext parserContext) {
@@ -109,6 +116,9 @@ class ResourcesBeanDefinitionParser implements BeanDefinitionParser {
// Use a default of near-lowest precedence, still allowing for even lower precedence in other mappings
handlerMappingDef.getPropertyValues().add("order", StringUtils.hasText(order) ? order : Ordered.LOWEST_PRECEDENCE - 1);
+ RuntimeBeanReference corsConfigurationsRef = MvcNamespaceUtils.registerCorsConfigurations(null, parserContext, source);
+ handlerMappingDef.getPropertyValues().add("corsConfigurations", corsConfigurationsRef);
+
String beanName = parserContext.getReaderContext().generateBeanName(handlerMappingDef);
parserContext.getRegistry().registerBeanDefinition(beanName, handlerMappingDef);
parserContext.registerComponent(new BeanComponentDefinition(handlerMappingDef, beanName));
@@ -162,6 +172,12 @@ class ResourcesBeanDefinitionParser implements BeanDefinitionParser {
resourceHandlerDef.getPropertyValues().add("cacheSeconds", cacheSeconds);
}
+ Element cacheControlElement = DomUtils.getChildElementByTagName(element, "cache-control");
+ if (cacheControlElement != null) {
+ CacheControl cacheControl = parseCacheControl(cacheControlElement);
+ resourceHandlerDef.getPropertyValues().add("cacheControl", cacheControl);
+ }
+
Element resourceChainElement = DomUtils.getChildElementByTagName(element, "resource-chain");
if (resourceChainElement != null) {
parseResourceChain(resourceHandlerDef, parserContext, resourceChainElement, source);
@@ -197,6 +213,38 @@ class ResourcesBeanDefinitionParser implements BeanDefinitionParser {
}
}
+ private CacheControl parseCacheControl(Element element) {
+ CacheControl cacheControl = CacheControl.empty();
+ if ("true".equals(element.getAttribute("no-cache"))) {
+ cacheControl = CacheControl.noCache();
+ }
+ else if ("true".equals(element.getAttribute("no-store"))) {
+ cacheControl = CacheControl.noStore();
+ }
+ else if (element.hasAttribute("max-age")) {
+ cacheControl = CacheControl.maxAge(Long.parseLong(element.getAttribute("max-age")), TimeUnit.SECONDS);
+ }
+ if ("true".equals(element.getAttribute("must-revalidate"))) {
+ cacheControl = cacheControl.mustRevalidate();
+ }
+ if ("true".equals(element.getAttribute("no-transform"))) {
+ cacheControl = cacheControl.noTransform();
+ }
+ if ("true".equals(element.getAttribute("cache-public"))) {
+ cacheControl = cacheControl.cachePublic();
+ }
+ if ("true".equals(element.getAttribute("cache-private"))) {
+ cacheControl = cacheControl.cachePrivate();
+ }
+ if ("true".equals(element.getAttribute("proxy-revalidate"))) {
+ cacheControl = cacheControl.proxyRevalidate();
+ }
+ if (element.hasAttribute("s-maxage")) {
+ cacheControl = cacheControl.sMaxAge(Long.parseLong(element.getAttribute("s-maxage")), TimeUnit.SECONDS);
+ }
+ return cacheControl;
+ }
+
private void parseResourceCache(ManagedList<? super Object> resourceResolvers,
ManagedList<? super Object> resourceTransformers, Element element, Object source) {
@@ -262,6 +310,12 @@ class ResourcesBeanDefinitionParser implements BeanDefinitionParser {
}
if (isAutoRegistration) {
+ if (isWebJarsAssetLocatorPresent) {
+ RootBeanDefinition webJarsResolverDef = new RootBeanDefinition(WebJarsResourceResolver.class);
+ webJarsResolverDef.setSource(source);
+ webJarsResolverDef.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
+ resourceResolvers.add(webJarsResolverDef);
+ }
RootBeanDefinition pathResolverDef = new RootBeanDefinition(PathResourceResolver.class);
pathResolverDef.setSource(source);
pathResolverDef.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/ScriptTemplateConfigurerBeanDefinitionParser.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/ScriptTemplateConfigurerBeanDefinitionParser.java
new file mode 100644
index 00000000..13a71ac1
--- /dev/null
+++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/ScriptTemplateConfigurerBeanDefinitionParser.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright 2002-2015 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.config;
+
+import java.nio.charset.Charset;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.w3c.dom.Element;
+
+import org.springframework.beans.factory.support.AbstractBeanDefinition;
+import org.springframework.beans.factory.support.BeanDefinitionBuilder;
+import org.springframework.beans.factory.xml.AbstractSimpleBeanDefinitionParser;
+import org.springframework.beans.factory.xml.ParserContext;
+import org.springframework.util.xml.DomUtils;
+
+/**
+ * Parse the <mvc:script-template-configurer> MVC namespace element and register a
+ * {@code ScriptTemplateConfigurer} bean.
+ *
+ * @author Sebastien Deleuze
+ * @since 4.2
+ */
+public class ScriptTemplateConfigurerBeanDefinitionParser extends AbstractSimpleBeanDefinitionParser {
+
+ public static final String BEAN_NAME = "mvcScriptTemplateConfigurer";
+
+
+ @Override
+ protected String resolveId(Element element, AbstractBeanDefinition definition, ParserContext parserContext) {
+ return BEAN_NAME;
+ }
+
+ @Override
+ protected String getBeanClassName(Element element) {
+ return "org.springframework.web.servlet.view.script.ScriptTemplateConfigurer";
+ }
+
+ @Override
+ protected void doParse(Element element, ParserContext parserContext, BeanDefinitionBuilder builder) {
+ List<Element> childElements = DomUtils.getChildElementsByTagName(element, "script");
+ if (!childElements.isEmpty()) {
+ List<String> locations = new ArrayList<String>(childElements.size());
+ for (Element childElement : childElements) {
+ locations.add(childElement.getAttribute("location"));
+ }
+ builder.addPropertyValue("scripts", locations.toArray(new String[locations.size()]));
+ }
+ builder.addPropertyValue("engineName", element.getAttribute("engine-name"));
+ if (element.hasAttribute("render-object")) {
+ builder.addPropertyValue("renderObject", element.getAttribute("render-object"));
+ }
+ if (element.hasAttribute("render-function")) {
+ builder.addPropertyValue("renderFunction", element.getAttribute("render-function"));
+ }
+ if (element.hasAttribute("content-type")) {
+ builder.addPropertyValue("contentType", element.getAttribute("content-type"));
+ }
+ if (element.hasAttribute("charset")) {
+ builder.addPropertyValue("charset", Charset.forName(element.getAttribute("charset")));
+ }
+ if (element.hasAttribute("resource-loader-path")) {
+ builder.addPropertyValue("resourceLoaderPath", element.getAttribute("resource-loader-path"));
+ }
+ if (element.hasAttribute("shared-engine")) {
+ builder.addPropertyValue("sharedEngine", element.getAttribute("shared-engine"));
+ }
+ }
+
+ @Override
+ protected boolean isEligibleAttribute(String name) {
+ return (name.equals("engine-name") || name.equals("scripts") || name.equals("render-object") ||
+ name.equals("render-function") || name.equals("content-type") ||
+ name.equals("charset") || name.equals("resource-loader-path"));
+ }
+
+}
diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/TilesConfigurerBeanDefinitionParser.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/TilesConfigurerBeanDefinitionParser.java
index 890e5333..3b607993 100644
--- a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/TilesConfigurerBeanDefinitionParser.java
+++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/TilesConfigurerBeanDefinitionParser.java
@@ -32,6 +32,7 @@ import org.springframework.util.xml.DomUtils;
* a corresponding TilesConfigurer bean.
* @author Rossen Stoyanchev
+ * @author Juergen Hoeller
* @since 4.1
*/
public class TilesConfigurerBeanDefinitionParser extends AbstractSingleBeanDefinitionParser {
@@ -65,6 +66,12 @@ public class TilesConfigurerBeanDefinitionParser extends AbstractSingleBeanDefin
if (element.hasAttribute("validate-definitions")) {
builder.addPropertyValue("validateDefinitions", element.getAttribute("validate-definitions"));
}
+ if (element.hasAttribute("definitions-factory")) {
+ builder.addPropertyValue("definitionsFactoryClass", element.getAttribute("definitions-factory"));
+ }
+ if (element.hasAttribute("preparer-factory")) {
+ builder.addPropertyValue("preparerFactoryClass", element.getAttribute("preparer-factory"));
+ }
}
}
diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/ViewControllerBeanDefinitionParser.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/ViewControllerBeanDefinitionParser.java
index 9025ff1f..06d61dc1 100644
--- a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/ViewControllerBeanDefinitionParser.java
+++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/ViewControllerBeanDefinitionParser.java
@@ -21,6 +21,7 @@ import java.util.Map;
import org.w3c.dom.Element;
import org.springframework.beans.factory.config.BeanDefinition;
+import org.springframework.beans.factory.config.RuntimeBeanReference;
import org.springframework.beans.factory.parsing.BeanComponentDefinition;
import org.springframework.beans.factory.support.ManagedMap;
import org.springframework.beans.factory.support.RootBeanDefinition;
@@ -125,6 +126,8 @@ class ViewControllerBeanDefinitionParser implements BeanDefinitionParser {
beanDef.getPropertyValues().add("order", "1");
beanDef.getPropertyValues().add("pathMatcher", MvcNamespaceUtils.registerPathMatcher(null, context, source));
beanDef.getPropertyValues().add("urlPathHelper", MvcNamespaceUtils.registerUrlPathHelper(null, context, source));
+ RuntimeBeanReference corsConfigurationsRef = MvcNamespaceUtils.registerCorsConfigurations(null, context, source);
+ beanDef.getPropertyValues().add("corsConfigurations", corsConfigurationsRef);
return beanDef;
}
diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/ViewResolversBeanDefinitionParser.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/ViewResolversBeanDefinitionParser.java
index 7f07ed9e..06e645f5 100644
--- a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/ViewResolversBeanDefinitionParser.java
+++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/ViewResolversBeanDefinitionParser.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2014 the original author or authors.
+ * Copyright 2002-2015 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.
@@ -37,6 +37,7 @@ import org.springframework.web.servlet.view.InternalResourceViewResolver;
import org.springframework.web.servlet.view.ViewResolverComposite;
import org.springframework.web.servlet.view.freemarker.FreeMarkerViewResolver;
import org.springframework.web.servlet.view.groovy.GroovyMarkupViewResolver;
+import org.springframework.web.servlet.view.script.ScriptTemplateViewResolver;
import org.springframework.web.servlet.view.tiles3.TilesViewResolver;
import org.springframework.web.servlet.view.velocity.VelocityViewResolver;
@@ -60,6 +61,8 @@ import org.springframework.web.servlet.view.velocity.VelocityViewResolver;
* @see TilesConfigurerBeanDefinitionParser
* @see FreeMarkerConfigurerBeanDefinitionParser
* @see VelocityConfigurerBeanDefinitionParser
+ * @see GroovyMarkupConfigurerBeanDefinitionParser
+ * @see ScriptTemplateConfigurerBeanDefinitionParser
*/
public class ViewResolversBeanDefinitionParser implements BeanDefinitionParser {
@@ -72,7 +75,7 @@ public class ViewResolversBeanDefinitionParser implements BeanDefinitionParser {
ManagedList<Object> resolvers = new ManagedList<Object>(4);
resolvers.setSource(context.extractSource(element));
- String[] names = new String[] {"jsp", "tiles", "bean-name", "freemarker", "velocity", "groovy", "bean", "ref"};
+ String[] names = new String[] {"jsp", "tiles", "bean-name", "freemarker", "velocity", "groovy", "script-template", "bean", "ref"};
for (Element resolverElement : DomUtils.getChildElementsByTagName(element, names)) {
String name = resolverElement.getLocalName();
@@ -106,6 +109,10 @@ public class ViewResolversBeanDefinitionParser implements BeanDefinitionParser {
resolverBeanDef.getPropertyValues().add("suffix", ".tpl");
addUrlBasedViewResolverProperties(resolverElement, resolverBeanDef);
}
+ else if ("script-template".equals(name)) {
+ resolverBeanDef = new RootBeanDefinition(ScriptTemplateViewResolver.class);
+ addUrlBasedViewResolverProperties(resolverElement, resolverBeanDef);
+ }
else if ("bean-name".equals(name)) {
resolverBeanDef = new RootBeanDefinition(BeanNameViewResolver.class);
}
diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/ContentNegotiationConfigurer.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/ContentNegotiationConfigurer.java
index 946fda52..c22dda73 100644
--- a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/ContentNegotiationConfigurer.java
+++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/ContentNegotiationConfigurer.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2014 the original author or authors.
+ * Copyright 2002-2015 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,21 +23,70 @@ import org.springframework.http.MediaType;
import org.springframework.web.accept.ContentNegotiationManager;
import org.springframework.web.accept.ContentNegotiationManagerFactoryBean;
import org.springframework.web.accept.ContentNegotiationStrategy;
+import org.springframework.web.accept.FixedContentNegotiationStrategy;
+import org.springframework.web.accept.HeaderContentNegotiationStrategy;
+import org.springframework.web.accept.ParameterContentNegotiationStrategy;
+import org.springframework.web.accept.PathExtensionContentNegotiationStrategy;
/**
- * Helps with configuring a {@link ContentNegotiationManager}.
+ * Creates a {@code ContentNegotiationManager} and configures it with
+ * one or more {@link ContentNegotiationStrategy} instances. The following shows
+ * the resulting strategy instances, the methods used to configured them, and
+ * whether enabled by default:
*
- * <p>By default strategies for checking the extension of the request path and
- * the {@code Accept} header are registered. The path extension check will perform
- * lookups through the {@link ServletContext} and the Java Activation Framework
- * (if present) unless {@linkplain #mediaTypes(Map) media types} are configured.
+ * <table>
+ * <tr>
+ * <th>Configurer Property</th>
+ * <th>Underlying Strategy</th>
+ * <th>Default Setting</th>
+ * </tr>
+ * <tr>
+ * <td>{@link #favorPathExtension}</td>
+ * <td>{@link PathExtensionContentNegotiationStrategy Path Extension strategy}</td>
+ * <td>On</td>
+ * </tr>
+ * <tr>
+ * <td>{@link #favorParameter}</td>
+ * <td>{@link ParameterContentNegotiationStrategy Parameter strategy}</td>
+ * <td>Off</td>
+ * </tr>
+ * <tr>
+ * <td>{@link #ignoreAcceptHeader}</td>
+ * <td>{@link HeaderContentNegotiationStrategy Header strategy}</td>
+ * <td>On</td>
+ * </tr>
+ * <tr>
+ * <td>{@link #defaultContentType}</td>
+ * <td>{@link FixedContentNegotiationStrategy Fixed content strategy}</td>
+ * <td>Not set</td>
+ * </tr>
+ * <tr>
+ * <td>{@link #defaultContentTypeStrategy}</td>
+ * <td>{@link ContentNegotiationStrategy}</td>
+ * <td>Not set</td>
+ * </tr>
+ * </table>
+ *
+ * <p>The order in which strategies are configured is fixed. You can only turn
+ * them on or off.
+ *
+ * <p>For the path extension and parameter strategies you may explicitly add
+ * {@link #mediaType MediaType mappings}. Those will be used to resolve path
+ * extensions and/or a query parameter value such as "json" to a concrete media
+ * type such as "application/json".
+ *
+ * <p>The path extension strategy will also use {@link ServletContext#getMimeType}
+ * and the Java Activation framework (JAF), if available, to resolve a path
+ * extension to a MediaType. You may however {@link #useJaf suppress} the use
+ * of JAF.
*
* @author Rossen Stoyanchev
* @since 3.2
*/
public class ContentNegotiationConfigurer {
- private final ContentNegotiationManagerFactoryBean factoryBean = new ContentNegotiationManagerFactoryBean();
+ private final ContentNegotiationManagerFactoryBean factory =
+ new ContentNegotiationManagerFactoryBean();
private final Map<String, MediaType> mediaTypes = new HashMap<String, MediaType>();
@@ -46,18 +95,19 @@ public class ContentNegotiationConfigurer {
* Class constructor with {@link javax.servlet.ServletContext}.
*/
public ContentNegotiationConfigurer(ServletContext servletContext) {
- this.factoryBean.setServletContext(servletContext);
+ this.factory.setServletContext(servletContext);
}
+
/**
- * Indicate whether the extension of the request path should be used to determine
- * the requested media type with the <em>highest priority</em>.
- * <p>By default this value is set to {@code true} in which case a request
+ * Whether the path extension in the URL path should be used to determine
+ * the requested media type.
+ * <p>By default this is set to {@code true} in which case a request
* for {@code /hotels.pdf} will be interpreted as a request for
- * {@code "application/pdf"} regardless of the {@code Accept} header.
+ * {@code "application/pdf"} regardless of the 'Accept' header.
*/
public ContentNegotiationConfigurer favorPathExtension(boolean favorPathExtension) {
- this.factoryBean.setFavorPathExtension(favorPathExtension);
+ this.factory.setFavorPathExtension(favorPathExtension);
return this;
}
@@ -82,7 +132,9 @@ public class ContentNegotiationConfigurer {
}
/**
- * An alternative to {@link #mediaType} with a Map of registrations to add.
+ * An alternative to {@link #mediaType}.
+ * @see #mediaType(String, MediaType)
+ * @see #replaceMediaTypes(Map)
*/
public ContentNegotiationConfigurer mediaTypes(Map<String, MediaType> mediaTypes) {
if (mediaTypes != null) {
@@ -92,9 +144,9 @@ public class ContentNegotiationConfigurer {
}
/**
- * Add mappings from file extensions to media types replacing any previous mappings.
- * <p>If this property is not set, the Java Action Framework, if available, may
- * still be used in conjunction with {@link #favorPathExtension(boolean)}.
+ * Similar to {@link #mediaType} but for replacing existing mappings.
+ * @see #mediaType(String, MediaType)
+ * @see #mediaTypes(Map)
*/
public ContentNegotiationConfigurer replaceMediaTypes(Map<String, MediaType> mediaTypes) {
this.mediaTypes.clear();
@@ -103,101 +155,84 @@ public class ContentNegotiationConfigurer {
}
/**
- * Whether to ignore requests that have a file extension that does not match
- * any mapped media types. Setting this to {@code false} will result in a
- * {@code HttpMediaTypeNotAcceptableException} when there is no match.
- *
+ * Whether to ignore requests with path extension that cannot be resolved
+ * to any media type. Setting this to {@code false} will result in an
+ * {@code HttpMediaTypeNotAcceptableException} if there is no match.
* <p>By default this is set to {@code true}.
*/
public ContentNegotiationConfigurer ignoreUnknownPathExtensions(boolean ignore) {
- this.factoryBean.setIgnoreUnknownPathExtensions(ignore);
+ this.factory.setIgnoreUnknownPathExtensions(ignore);
return this;
}
/**
- * Indicate whether to use the Java Activation Framework as a fallback option
- * to map from file extensions to media types. This is used only when
- * {@link #favorPathExtension(boolean)} is set to {@code true}.
- * <p>The default value is {@code true}.
- * @see #parameterName
- * @see #mediaTypes(Map)
+ * When {@link #favorPathExtension} is set, this property determines whether
+ * to allow use of JAF (Java Activation Framework) to resolve a path
+ * extension to a specific MediaType.
+ * <p>By default this is not set in which case
+ * {@code PathExtensionContentNegotiationStrategy} will use JAF if available.
*/
public ContentNegotiationConfigurer useJaf(boolean useJaf) {
- this.factoryBean.setUseJaf(useJaf);
+ this.factory.setUseJaf(useJaf);
return this;
}
/**
- * Indicate whether a request parameter should be used to determine the
- * requested media type with the <em>2nd highest priority</em>, i.e.
- * after path extensions but before the {@code Accept} header.
- * <p>The default value is {@code false}. If set to to {@code true}, a request
- * for {@code /hotels?format=pdf} will be interpreted as a request for
- * {@code "application/pdf"} regardless of the {@code Accept} header.
- * <p>To use this option effectively you must also configure the MediaType
- * type mappings via {@link #mediaTypes(Map)}.
+ * Whether a request parameter ("format" by default) should be used to
+ * determine the requested media type. For this option to work you must
+ * register {@link #mediaType(String, MediaType) media type mappings}.
+ * <p>By default this is set to {@code false}.
* @see #parameterName(String)
*/
public ContentNegotiationConfigurer favorParameter(boolean favorParameter) {
- this.factoryBean.setFavorParameter(favorParameter);
+ this.factory.setFavorParameter(favorParameter);
return this;
}
/**
- * Set the parameter name that can be used to determine the requested media type
- * if the {@link #favorParameter(boolean)} property is {@code true}.
+ * Set the query parameter name to use when {@link #favorParameter} is on.
* <p>The default parameter name is {@code "format"}.
*/
public ContentNegotiationConfigurer parameterName(String parameterName) {
- this.factoryBean.setParameterName(parameterName);
+ this.factory.setParameterName(parameterName);
return this;
}
/**
- * Indicate whether the HTTP {@code Accept} header should be ignored altogether.
- * If set the {@code Accept} header is checked at the
- * <em>3rd highest priority</em>, i.e. after the request path extension and
- * possibly a request parameter if configured.
+ * Whether to disable checking the 'Accept' request header.
* <p>By default this value is set to {@code false}.
*/
public ContentNegotiationConfigurer ignoreAcceptHeader(boolean ignoreAcceptHeader) {
- this.factoryBean.setIgnoreAcceptHeader(ignoreAcceptHeader);
+ this.factory.setIgnoreAcceptHeader(ignoreAcceptHeader);
return this;
}
/**
- * Set the default content type to use when no content type was requested.
- * <p>Note that internally this method creates and adds a
- * {@link org.springframework.web.accept.FixedContentNegotiationStrategy
- * FixedContentNegotiationStrategy}. Alternatively you can also provide a
- * custom strategy via {@link #defaultContentTypeStrategy}.
+ * Set the default content type to use when no content type is requested.
+ * <p>By default this is not set.
+ * @see #defaultContentTypeStrategy
*/
public ContentNegotiationConfigurer defaultContentType(MediaType defaultContentType) {
- this.factoryBean.setDefaultContentType(defaultContentType);
+ this.factory.setDefaultContentType(defaultContentType);
return this;
}
/**
- * Configure a custom {@link ContentNegotiationStrategy} to use to determine
- * the default content type to use when no content type was requested.
- * <p>However also consider using {@link #defaultContentType} which provides
- * a simpler alternative to doing the same.
+ * Set a custom {@link ContentNegotiationStrategy} to use to determine
+ * the content type to use when no content type is requested.
+ * <p>By default this is not set.
+ * @see #defaultContentType
* @since 4.1.2
*/
public ContentNegotiationConfigurer defaultContentTypeStrategy(ContentNegotiationStrategy defaultStrategy) {
- this.factoryBean.setDefaultContentTypeStrategy(defaultStrategy);
+ this.factory.setDefaultContentTypeStrategy(defaultStrategy);
return this;
}
- /**
- * Return the configured {@link ContentNegotiationManager} instance
- */
protected ContentNegotiationManager getContentNegotiationManager() throws Exception {
- if (!this.mediaTypes.isEmpty()) {
- this.factoryBean.addMediaTypes(mediaTypes);
- }
- this.factoryBean.afterPropertiesSet();
- return this.factoryBean.getObject();
+ this.factory.addMediaTypes(this.mediaTypes);
+ this.factory.afterPropertiesSet();
+ return this.factory.getObject();
}
}
diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/CorsRegistration.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/CorsRegistration.java
new file mode 100644
index 00000000..75d315b5
--- /dev/null
+++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/CorsRegistration.java
@@ -0,0 +1,99 @@
+/*
+ * Copyright 2002-2015 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.config.annotation;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+
+import org.springframework.http.HttpMethod;
+import org.springframework.web.bind.annotation.CrossOrigin;
+import org.springframework.web.cors.CorsConfiguration;
+
+/**
+ * {@code CorsRegistration} assists with the creation of a
+ * {@link CorsConfiguration} instance mapped to a path pattern.
+ *
+ * <p>If no path pattern is specified, cross-origin request handling is
+ * mapped to {@code "/**"}.
+ *
+ * <p>By default, all origins, all headers, credentials and {@code GET},
+ * {@code HEAD}, and {@code POST} methods are allowed, and the max age is
+ * set to 30 minutes.
+ *
+ * @author Sebastien Deleuze
+ * @author Sam Brannen
+ * @since 4.2
+ * @see CorsConfiguration
+ * @see CorsRegistry
+ */
+public class CorsRegistration {
+
+ private final String pathPattern;
+
+ private final CorsConfiguration config;
+
+ public CorsRegistration(String pathPattern) {
+ this.pathPattern = pathPattern;
+ // Same implicit default values as the @CrossOrigin annotation + allows simple methods
+ this.config = new CorsConfiguration();
+ this.config.setAllowedOrigins(Arrays.asList(CrossOrigin.DEFAULT_ORIGINS));
+ this.config.setAllowedMethods(Arrays.asList(HttpMethod.GET.name(),
+ HttpMethod.HEAD.name(), HttpMethod.POST.name()));
+ this.config.setAllowedHeaders(Arrays.asList(CrossOrigin.DEFAULT_ALLOWED_HEADERS));
+ this.config.setAllowCredentials(CrossOrigin.DEFAULT_ALLOW_CREDENTIALS);
+ this.config.setMaxAge(CrossOrigin.DEFAULT_MAX_AGE);
+ }
+
+ public CorsRegistration allowedOrigins(String... origins) {
+ this.config.setAllowedOrigins(new ArrayList<String>(Arrays.asList(origins)));
+ return this;
+ }
+
+ public CorsRegistration allowedMethods(String... methods) {
+ this.config.setAllowedMethods(new ArrayList<String>(Arrays.asList(methods)));
+ return this;
+ }
+
+ public CorsRegistration allowedHeaders(String... headers) {
+ this.config.setAllowedHeaders(new ArrayList<String>(Arrays.asList(headers)));
+ return this;
+ }
+
+ public CorsRegistration exposedHeaders(String... headers) {
+ this.config.setExposedHeaders(new ArrayList<String>(Arrays.asList(headers)));
+ return this;
+ }
+
+ public CorsRegistration maxAge(long maxAge) {
+ this.config.setMaxAge(maxAge);
+ return this;
+ }
+
+ public CorsRegistration allowCredentials(boolean allowCredentials) {
+ this.config.setAllowCredentials(allowCredentials);
+ return this;
+ }
+
+ protected String getPathPattern() {
+ return this.pathPattern;
+ }
+
+ protected CorsConfiguration getCorsConfiguration() {
+ return this.config;
+ }
+
+}
diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/CorsRegistry.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/CorsRegistry.java
new file mode 100644
index 00000000..3f4e334e
--- /dev/null
+++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/CorsRegistry.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright 2002-2015 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.config.annotation;
+
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.springframework.web.cors.CorsConfiguration;
+
+/**
+ * {@code CorsRegistry} assists with the registration of {@link CorsConfiguration}
+ * mapped to a path pattern.
+ *
+ * @author Sebastien Deleuze
+ * @since 4.2
+ * @see CorsRegistration
+ */
+public class CorsRegistry {
+
+ private final List<CorsRegistration> registrations = new ArrayList<CorsRegistration>();
+
+
+ /**
+ * Enable cross origin request handling for the specified path pattern.
+ *
+ * <p>Exact path mapping URIs (such as {@code "/admin"}) are supported as
+ * well as Ant-style path patterns (such as {@code "/admin/**"}).
+ *
+ * <p>By default, all origins, all headers, credentials and {@code GET},
+ * {@code HEAD}, and {@code POST} methods are allowed, and the max age
+ * is set to 30 minutes.
+ */
+ public CorsRegistration addMapping(String pathPattern) {
+ CorsRegistration registration = new CorsRegistration(pathPattern);
+ this.registrations.add(registration);
+ return registration;
+ }
+
+ protected Map<String, CorsConfiguration> getCorsConfigurations() {
+ Map<String, CorsConfiguration> configs = new LinkedHashMap<String, CorsConfiguration>(this.registrations.size());
+ for (CorsRegistration registration : this.registrations) {
+ configs.put(registration.getPathPattern(), registration.getCorsConfiguration());
+ }
+ return configs;
+ }
+
+}
diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/DelegatingWebMvcConfiguration.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/DelegatingWebMvcConfiguration.java
index bf384ce8..084acd90 100644
--- a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/DelegatingWebMvcConfiguration.java
+++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/DelegatingWebMvcConfiguration.java
@@ -132,4 +132,9 @@ public class DelegatingWebMvcConfiguration extends WebMvcConfigurationSupport {
this.configurers.configureHandlerExceptionResolvers(exceptionResolvers);
}
+ @Override
+ protected void addCorsMappings(CorsRegistry registry) {
+ this.configurers.addCorsMappings(registry);
+ }
+
}
diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/EnableWebMvc.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/EnableWebMvc.java
index 17dd890f..8de751cc 100644
--- a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/EnableWebMvc.java
+++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/EnableWebMvc.java
@@ -1,15 +1,19 @@
/*
- * Copyright 2002-2014 the original author or authors.
+ * Copyright 2002-2016 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
+ * 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.
+ * 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.config.annotation;
import java.lang.annotation.Documented;
@@ -43,17 +47,17 @@ import org.springframework.context.annotation.Import;
* &#064;ComponentScan(basePackageClasses = { MyConfiguration.class })
* public class MyConfiguration extends WebMvcConfigurerAdapter {
*
- * &#064;Override
- * public void addFormatters(FormatterRegistry formatterRegistry) {
- * formatterRegistry.addConverter(new MyConverter());
- * }
+ * &#064;Override
+ * public void addFormatters(FormatterRegistry formatterRegistry) {
+ * formatterRegistry.addConverter(new MyConverter());
+ * }
*
- * &#064;Override
- * public void configureMessageConverters(List&lt;HttpMessageConverter&lt;?&gt;&gt; converters) {
- * converters.add(new MyHttpMessageConverter());
- * }
+ * &#064;Override
+ * public void configureMessageConverters(List&lt;HttpMessageConverter&lt;?&gt;&gt; converters) {
+ * converters.add(new MyHttpMessageConverter());
+ * }
*
- * // More overridden methods ...
+ * // More overridden methods ...
* }
* </pre>
*
@@ -67,16 +71,16 @@ import org.springframework.context.annotation.Import;
* &#064;ComponentScan(basePackageClasses = { MyConfiguration.class })
* public class MyConfiguration extends WebMvcConfigurationSupport {
*
- * &#064;Override
- * public void addFormatters(FormatterRegistry formatterRegistry) {
- * formatterRegistry.addConverter(new MyConverter());
- * }
+ * &#064;Override
+ * public void addFormatters(FormatterRegistry formatterRegistry) {
+ * formatterRegistry.addConverter(new MyConverter());
+ * }
*
- * &#064;Bean
- * public RequestMappingHandlerAdapter requestMappingHandlerAdapter() {
- * // Create or delegate to "super" to create and
- * // customize properties of RequestMapingHandlerAdapter
- * }
+ * &#064;Bean
+ * public RequestMappingHandlerAdapter requestMappingHandlerAdapter() {
+ * // Create or delegate to "super" to create and
+ * // customize properties of RequestMappingHandlerAdapter
+ * }
* }
* </pre>
*
diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/PathMatchConfigurer.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/PathMatchConfigurer.java
index 8203ea20..35fa13e3 100644
--- a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/PathMatchConfigurer.java
+++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/PathMatchConfigurer.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2014 the original author or authors.
+ * Copyright 2002-2015 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.
@@ -20,8 +20,8 @@ import org.springframework.util.PathMatcher;
import org.springframework.web.util.UrlPathHelper;
/**
- * Helps with configuring HandlerMappings path matching options such as trailing slash match,
- * suffix registration, path matcher and path helper.
+ * Helps with configuring HandlerMappings path matching options such as trailing
+ * slash match, suffix registration, path matcher and path helper.
*
* <p>Configured path matcher and path helper instances are shared for:
* <ul>
@@ -37,11 +37,11 @@ import org.springframework.web.util.UrlPathHelper;
*/
public class PathMatchConfigurer {
- private Boolean useSuffixPatternMatch;
+ private Boolean suffixPatternMatch;
- private Boolean useTrailingSlashMatch;
+ private Boolean trailingSlashMatch;
- private Boolean useRegisteredSuffixPatternMatch;
+ private Boolean registeredSuffixPatternMatch;
private UrlPathHelper urlPathHelper;
@@ -51,10 +51,11 @@ public class PathMatchConfigurer {
/**
* Whether to use suffix pattern match (".*") when matching patterns to
* requests. If enabled a method mapped to "/users" also matches to "/users.*".
- * <p>The default value is {@code true}.
+ * <p>By default this is set to {@code true}.
+ * @see #registeredSuffixPatternMatch
*/
- public PathMatchConfigurer setUseSuffixPatternMatch(Boolean useSuffixPatternMatch) {
- this.useSuffixPatternMatch = useSuffixPatternMatch;
+ public PathMatchConfigurer setUseSuffixPatternMatch(Boolean suffixPatternMatch) {
+ this.suffixPatternMatch = suffixPatternMatch;
return this;
}
@@ -63,28 +64,24 @@ public class PathMatchConfigurer {
* If enabled a method mapped to "/users" also matches to "/users/".
* <p>The default value is {@code true}.
*/
- public PathMatchConfigurer setUseTrailingSlashMatch(Boolean useTrailingSlashMatch) {
- this.useTrailingSlashMatch = useTrailingSlashMatch;
+ public PathMatchConfigurer setUseTrailingSlashMatch(Boolean trailingSlashMatch) {
+ this.trailingSlashMatch = trailingSlashMatch;
return this;
}
/**
- * Whether to use suffix pattern match for registered file extensions only
- * when matching patterns to requests.
- * <p>If enabled, a controller method mapped to "/users" also matches to
- * "/users.json" assuming ".json" is a file extension registered with the
- * provided {@link org.springframework.web.accept.ContentNegotiationManager}.</p>
- * <p>The {@link org.springframework.web.accept.ContentNegotiationManager} can be customized
- * using a {@link ContentNegotiationConfigurer}.</p>
- * <p>If enabled, this flag also enables
- * {@link #setUseSuffixPatternMatch(Boolean) useSuffixPatternMatch}. The
- * default value is {@code false}.</p>
- * @see org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping
- * @see ContentNegotiationConfigurer
- *
+ * Whether suffix pattern matching should work only against path extensions
+ * explicitly registered when you
+ * {@link WebMvcConfigurer#configureContentNegotiation configure content
+ * negotiation}. This is generally recommended to reduce ambiguity and to
+ * avoid issues such as when a "." appears in the path for other reasons.
+ * <p>By default this is set to "false".
+ * @see WebMvcConfigurer#configureContentNegotiation
*/
- public PathMatchConfigurer setUseRegisteredSuffixPatternMatch(Boolean useRegisteredSuffixPatternMatch) {
- this.useRegisteredSuffixPatternMatch = useRegisteredSuffixPatternMatch;
+ public PathMatchConfigurer setUseRegisteredSuffixPatternMatch(
+ Boolean registeredSuffixPatternMatch) {
+
+ this.registeredSuffixPatternMatch = registeredSuffixPatternMatch;
return this;
}
@@ -110,15 +107,15 @@ public class PathMatchConfigurer {
}
public Boolean isUseSuffixPatternMatch() {
- return this.useSuffixPatternMatch;
+ return this.suffixPatternMatch;
}
public Boolean isUseTrailingSlashMatch() {
- return this.useTrailingSlashMatch;
+ return this.trailingSlashMatch;
}
public Boolean isUseRegisteredSuffixPatternMatch() {
- return this.useRegisteredSuffixPatternMatch;
+ return this.registeredSuffixPatternMatch;
}
public UrlPathHelper getUrlPathHelper() {
diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/ResourceChainRegistration.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/ResourceChainRegistration.java
index 7749491f..92a75bed 100644
--- a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/ResourceChainRegistration.java
+++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/ResourceChainRegistration.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2014 the original author or authors.
+ * Copyright 2002-2015 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.
@@ -22,6 +22,7 @@ import java.util.List;
import org.springframework.cache.Cache;
import org.springframework.cache.concurrent.ConcurrentMapCache;
import org.springframework.util.Assert;
+import org.springframework.util.ClassUtils;
import org.springframework.web.servlet.resource.CachingResourceResolver;
import org.springframework.web.servlet.resource.CachingResourceTransformer;
import org.springframework.web.servlet.resource.CssLinkResourceTransformer;
@@ -29,6 +30,7 @@ import org.springframework.web.servlet.resource.PathResourceResolver;
import org.springframework.web.servlet.resource.ResourceResolver;
import org.springframework.web.servlet.resource.ResourceTransformer;
import org.springframework.web.servlet.resource.VersionResourceResolver;
+import org.springframework.web.servlet.resource.WebJarsResourceResolver;
/**
* Assists with the registration of resource resolvers and transformers.
@@ -40,6 +42,10 @@ public class ResourceChainRegistration {
private static final String DEFAULT_CACHE_NAME = "spring-resource-chain-cache";
+ private static final boolean isWebJarsAssetLocatorPresent = ClassUtils.isPresent(
+ "org.webjars.WebJarAssetLocator", ResourceChainRegistration.class.getClassLoader());
+
+
private final List<ResourceResolver> resolvers = new ArrayList<ResourceResolver>(4);
private final List<ResourceTransformer> transformers = new ArrayList<ResourceTransformer>(4);
@@ -98,6 +104,9 @@ public class ResourceChainRegistration {
protected List<ResourceResolver> getResourceResolvers() {
if (!this.hasPathResolver) {
List<ResourceResolver> result = new ArrayList<ResourceResolver>(this.resolvers);
+ if (isWebJarsAssetLocatorPresent) {
+ result.add(new WebJarsResourceResolver());
+ }
result.add(new PathResourceResolver());
return result;
}
diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/ResourceHandlerRegistration.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/ResourceHandlerRegistration.java
index 152617cd..54c15642 100644
--- a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/ResourceHandlerRegistration.java
+++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/ResourceHandlerRegistration.java
@@ -23,6 +23,7 @@ import org.springframework.cache.Cache;
import org.springframework.core.io.Resource;
import org.springframework.core.io.ResourceLoader;
import org.springframework.util.Assert;
+import org.springframework.http.CacheControl;
import org.springframework.web.servlet.resource.PathResourceResolver;
import org.springframework.web.servlet.resource.ResourceHttpRequestHandler;
@@ -44,6 +45,8 @@ public class ResourceHandlerRegistration {
private Integer cachePeriod;
+ private CacheControl cacheControl;
+
private ResourceChainRegistration resourceChainRegistration;
@@ -68,7 +71,7 @@ public class ResourceHandlerRegistration {
* {@code /META-INF/public-web-resources/} directory, with resources in the web application root taking precedence.
* @return the same {@link ResourceHandlerRegistration} instance, for chained method invocation
*/
- public ResourceHandlerRegistration addResourceLocations(String...resourceLocations) {
+ public ResourceHandlerRegistration addResourceLocations(String... resourceLocations) {
for (String location : resourceLocations) {
this.locations.add(resourceLoader.getResource(location));
}
@@ -88,6 +91,21 @@ public class ResourceHandlerRegistration {
}
/**
+ * Specify the {@link org.springframework.http.CacheControl} which should be used
+ * by the resource handler.
+ *
+ * <p>Setting a custom value here will override the configuration set with {@link #setCachePeriod}.
+ *
+ * @param cacheControl the CacheControl configuration to use
+ * @return the same {@link ResourceHandlerRegistration} instance, for chained method invocation
+ * @since 4.2
+ */
+ public ResourceHandlerRegistration setCacheControl(CacheControl cacheControl) {
+ this.cacheControl = cacheControl;
+ return this;
+ }
+
+ /**
* Configure a chain of resource resolvers and transformers to use. This
* can be useful, for example, to apply a version strategy to resource URLs.
*
@@ -147,7 +165,10 @@ public class ResourceHandlerRegistration {
handler.setResourceTransformers(this.resourceChainRegistration.getResourceTransformers());
}
handler.setLocations(this.locations);
- if (this.cachePeriod != null) {
+ if (this.cacheControl != null) {
+ handler.setCacheControl(this.cacheControl);
+ }
+ else if (this.cachePeriod != null) {
handler.setCacheSeconds(this.cachePeriod);
}
return handler;
diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/ViewResolverRegistry.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/ViewResolverRegistry.java
index 6af073c5..c4fedda8 100644
--- a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/ViewResolverRegistry.java
+++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/ViewResolverRegistry.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2014 the original author or authors.
+ * Copyright 2002-2015 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.
@@ -37,6 +37,8 @@ import org.springframework.web.servlet.view.freemarker.FreeMarkerConfigurer;
import org.springframework.web.servlet.view.freemarker.FreeMarkerViewResolver;
import org.springframework.web.servlet.view.groovy.GroovyMarkupConfigurer;
import org.springframework.web.servlet.view.groovy.GroovyMarkupViewResolver;
+import org.springframework.web.servlet.view.script.ScriptTemplateConfigurer;
+import org.springframework.web.servlet.view.script.ScriptTemplateViewResolver;
import org.springframework.web.servlet.view.tiles3.TilesConfigurer;
import org.springframework.web.servlet.view.tiles3.TilesViewResolver;
import org.springframework.web.servlet.view.velocity.VelocityConfigurer;
@@ -234,6 +236,22 @@ public class ViewResolverRegistry {
}
/**
+ * Register a script template view resolver with an empty default view name prefix and suffix.
+ * @since 4.2
+ */
+ public UrlBasedViewResolverRegistration scriptTemplate() {
+ if (this.applicationContext != null && !hasBeanOfType(ScriptTemplateConfigurer.class)) {
+ throw new BeanInitializationException("In addition to a script template view resolver " +
+ "there must also be a single ScriptTemplateConfig bean in this web application context " +
+ "(or its parent): ScriptTemplateConfigurer is the usual implementation. " +
+ "This bean may be given any name.");
+ }
+ ScriptRegistration registration = new ScriptRegistration();
+ this.viewResolvers.add(registration.getViewResolver());
+ return registration;
+ }
+
+ /**
* Register a bean name view resolver that interprets view names as the names
* of {@link org.springframework.web.servlet.View} beans.
*/
@@ -324,4 +342,12 @@ public class ViewResolverRegistry {
}
}
+ private static class ScriptRegistration extends UrlBasedViewResolverRegistration {
+
+ private ScriptRegistration() {
+ super(new ScriptTemplateViewResolver());
+ getViewResolver();
+ }
+ }
+
}
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 46112ed3..37976905 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
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2014 the original author or authors.
+ * Copyright 2002-2015 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.
@@ -66,6 +66,7 @@ import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.support.ConfigurableWebBindingInitializer;
import org.springframework.web.context.ServletContextAware;
+import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.method.support.CompositeUriComponentsContributor;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.method.support.HandlerMethodReturnValueHandler;
@@ -83,7 +84,9 @@ import org.springframework.web.servlet.mvc.HttpRequestHandlerAdapter;
import org.springframework.web.servlet.mvc.SimpleControllerHandlerAdapter;
import org.springframework.web.servlet.mvc.annotation.ResponseStatusExceptionResolver;
import org.springframework.web.servlet.mvc.method.annotation.ExceptionHandlerExceptionResolver;
+import org.springframework.web.servlet.mvc.method.annotation.JsonViewRequestBodyAdvice;
import org.springframework.web.servlet.mvc.method.annotation.JsonViewResponseBodyAdvice;
+import org.springframework.web.servlet.mvc.method.annotation.RequestBodyAdvice;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;
@@ -198,6 +201,8 @@ public class WebMvcConfigurationSupport implements ApplicationContextAware, Serv
private List<HttpMessageConverter<?>> messageConverters;
+ private Map<String, CorsConfiguration> corsConfigurations;
+
/**
* Set the Spring {@link ApplicationContext}, e.g. for resource loading.
@@ -207,6 +212,10 @@ public class WebMvcConfigurationSupport implements ApplicationContextAware, Serv
this.applicationContext = applicationContext;
}
+ public ApplicationContext getApplicationContext() {
+ return this.applicationContext;
+ }
+
/**
* Set the {@link javax.servlet.ServletContext}, e.g. for resource handling,
* looking up file extensions, etc.
@@ -216,6 +225,9 @@ public class WebMvcConfigurationSupport implements ApplicationContextAware, Serv
this.servletContext = servletContext;
}
+ public ServletContext getServletContext() {
+ return this.servletContext;
+ }
/**
* Return a {@link RequestMappingHandlerMapping} ordered at 0 for mapping
@@ -223,10 +235,11 @@ public class WebMvcConfigurationSupport implements ApplicationContextAware, Serv
*/
@Bean
public RequestMappingHandlerMapping requestMappingHandlerMapping() {
- RequestMappingHandlerMapping handlerMapping = new RequestMappingHandlerMapping();
+ RequestMappingHandlerMapping handlerMapping = createRequestMappingHandlerMapping();
handlerMapping.setOrder(0);
handlerMapping.setInterceptors(getInterceptors());
handlerMapping.setContentNegotiationManager(mvcContentNegotiationManager());
+ handlerMapping.setCorsConfigurations(getCorsConfigurations());
PathMatchConfigurer configurer = getPathMatchConfigurer();
if (configurer.isUseSuffixPatternMatch() != null) {
@@ -249,6 +262,14 @@ public class WebMvcConfigurationSupport implements ApplicationContextAware, Serv
}
/**
+ * Protected method for plugging in a custom sub-class of
+ * {@link RequestMappingHandlerMapping}.
+ */
+ protected RequestMappingHandlerMapping createRequestMappingHandlerMapping() {
+ return new RequestMappingHandlerMapping();
+ }
+
+ /**
* Provide access to the shared handler interceptors used to configure
* {@link HandlerMapping} instances with. This method cannot be overridden,
* use {@link #addInterceptors(InterceptorRegistry)} instead.
@@ -351,6 +372,7 @@ public class WebMvcConfigurationSupport implements ApplicationContextAware, Serv
handlerMapping.setPathMatcher(mvcPathMatcher());
handlerMapping.setUrlPathHelper(mvcUrlPathHelper());
handlerMapping.setInterceptors(getInterceptors());
+ handlerMapping.setCorsConfigurations(getCorsConfigurations());
return handlerMapping;
}
@@ -370,6 +392,7 @@ public class WebMvcConfigurationSupport implements ApplicationContextAware, Serv
BeanNameUrlHandlerMapping mapping = new BeanNameUrlHandlerMapping();
mapping.setOrder(2);
mapping.setInterceptors(getInterceptors());
+ mapping.setCorsConfigurations(getCorsConfigurations());
return mapping;
}
@@ -389,6 +412,7 @@ public class WebMvcConfigurationSupport implements ApplicationContextAware, Serv
handlerMapping.setUrlPathHelper(mvcUrlPathHelper());
handlerMapping.setInterceptors(new HandlerInterceptor[] {
new ResourceUrlProviderExposingInterceptor(mvcResourceUrlProvider())});
+ handlerMapping.setCorsConfigurations(getCorsConfigurations());
}
else {
handlerMapping = new EmptyHandlerMapping();
@@ -464,9 +488,13 @@ public class WebMvcConfigurationSupport implements ApplicationContextAware, Serv
adapter.setCustomReturnValueHandlers(returnValueHandlers);
if (jackson2Present) {
- List<ResponseBodyAdvice<?>> interceptors = new ArrayList<ResponseBodyAdvice<?>>();
- interceptors.add(new JsonViewResponseBodyAdvice());
- adapter.setResponseBodyAdvice(interceptors);
+ List<RequestBodyAdvice> requestBodyAdvices = new ArrayList<RequestBodyAdvice>();
+ requestBodyAdvices.add(new JsonViewRequestBodyAdvice());
+ adapter.setRequestBodyAdvice(requestBodyAdvices);
+
+ List<ResponseBodyAdvice<?>> responseBodyAdvices = new ArrayList<ResponseBodyAdvice<?>>();
+ responseBodyAdvices.add(new JsonViewResponseBodyAdvice());
+ adapter.setResponseBodyAdvice(responseBodyAdvices);
}
AsyncSupportConfigurer configurer = new AsyncSupportConfigurer();
@@ -843,6 +871,26 @@ public class WebMvcConfigurationSupport implements ApplicationContextAware, Serv
protected void configureViewResolvers(ViewResolverRegistry registry) {
}
+ /**
+ * @since 4.2
+ */
+ protected final Map<String, CorsConfiguration> getCorsConfigurations() {
+ if (this.corsConfigurations == null) {
+ CorsRegistry registry = new CorsRegistry();
+ addCorsMappings(registry);
+ this.corsConfigurations = registry.getCorsConfigurations();
+ }
+ return this.corsConfigurations;
+ }
+
+ /**
+ * Override this method to configure cross origin requests processing.
+ * @since 4.2
+ * @see CorsRegistry
+ */
+ protected void addCorsMappings(CorsRegistry registry) {
+ }
+
private static final class EmptyHandlerMapping extends AbstractHandlerMapping {
diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurer.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurer.java
index 85c09d38..3e9c509f 100644
--- a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurer.java
+++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurer.java
@@ -182,4 +182,10 @@ public interface WebMvcConfigurer {
*/
void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer);
+ /**
+ * Configure cross origin requests processing.
+ * @since 4.2
+ */
+ void addCorsMappings(CorsRegistry registry);
+
}
diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurerAdapter.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurerAdapter.java
index 7f4c7c34..90d4b6ae 100644
--- a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurerAdapter.java
+++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurerAdapter.java
@@ -165,4 +165,12 @@ public abstract class WebMvcConfigurerAdapter implements WebMvcConfigurer {
public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
}
+ /**
+ * {@inheritDoc}
+ * <p>This implementation is empty.
+ */
+ @Override
+ public void addCorsMappings(CorsRegistry registry) {
+ }
+
}
diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurerComposite.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurerComposite.java
index af08decd..c88ba0ff 100644
--- a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurerComposite.java
+++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurerComposite.java
@@ -153,6 +153,13 @@ class WebMvcConfigurerComposite implements WebMvcConfigurer {
return selectSingleInstance(candidates, Validator.class);
}
+ @Override
+ public void addCorsMappings(CorsRegistry registry) {
+ for (WebMvcConfigurer delegate : this.delegates) {
+ delegate.addCorsMappings(registry);
+ }
+ }
+
private <T> T selectSingleInstance(List<T> instances, Class<T> instanceType) {
if (instances.size() > 1) {
throw new IllegalStateException(
diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/AbstractHandlerExceptionResolver.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/AbstractHandlerExceptionResolver.java
index 764bec2c..9ae1cb42 100644
--- a/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/AbstractHandlerExceptionResolver.java
+++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/AbstractHandlerExceptionResolver.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2013 the original author or authors.
+ * Copyright 2002-2015 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.
@@ -30,19 +30,17 @@ import org.springframework.web.servlet.ModelAndView;
/**
* Abstract base class for {@link HandlerExceptionResolver} implementations.
*
- * <p>Provides a set of mapped handlers that the resolver should map to,
- * and the {@link Ordered} implementation.
+ * <p>Supports mapped {@linkplain #setMappedHandlers handlers} and
+ * {@linkplain #setMappedHandlerClasses handler classes} that the resolver
+ * should be applied to and implements the {@link Ordered} interface.
*
* @author Arjen Poutsma
* @author Juergen Hoeller
+ * @author Sam Brannen
* @since 3.0
*/
public abstract class AbstractHandlerExceptionResolver implements HandlerExceptionResolver, Ordered {
- private static final String HEADER_PRAGMA = "Pragma";
-
- private static final String HEADER_EXPIRES = "Expires";
-
private static final String HEADER_CACHE_CONTROL = "Cache-Control";
@@ -71,10 +69,10 @@ public abstract class AbstractHandlerExceptionResolver implements HandlerExcepti
/**
* Specify the set of handlers that this exception resolver should apply to.
- * The exception mappings and the default error view will only apply to the specified handlers.
- * <p>If no handlers and handler classes are set, the exception mappings and the default error
+ * <p>The exception mappings and the default error view will only apply to the specified handlers.
+ * <p>If no handlers or handler classes are set, the exception mappings and the default error
* view will apply to all handlers. This means that a specified default error view will be used
- * as fallback for all exceptions; any further HandlerExceptionResolvers in the chain will be
+ * as a fallback for all exceptions; any further HandlerExceptionResolvers in the chain will be
* ignored in this case.
*/
public void setMappedHandlers(Set<?> mappedHandlers) {
@@ -83,20 +81,20 @@ public abstract class AbstractHandlerExceptionResolver implements HandlerExcepti
/**
* Specify the set of classes that this exception resolver should apply to.
- * The exception mappings and the default error view will only apply to handlers of the
- * specified type; the specified types may be interfaces and superclasses of handlers as well.
- * <p>If no handlers and handler classes are set, the exception mappings and the default error
+ * <p>The exception mappings and the default error view will only apply to handlers of the
+ * specified types; the specified types may be interfaces or superclasses of handlers as well.
+ * <p>If no handlers or handler classes are set, the exception mappings and the default error
* view will apply to all handlers. This means that a specified default error view will be used
- * as fallback for all exceptions; any further HandlerExceptionResolvers in the chain will be
+ * as a fallback for all exceptions; any further HandlerExceptionResolvers in the chain will be
* ignored in this case.
*/
- public void setMappedHandlerClasses(Class<?>[] mappedHandlerClasses) {
+ public void setMappedHandlerClasses(Class<?>... mappedHandlerClasses) {
this.mappedHandlerClasses = mappedHandlerClasses;
}
/**
* Set the log category for warn logging. The name will be passed to the underlying logger
- * implementation through Commons Logging, getting interpreted as log category according
+ * implementation through Commons Logging, getting interpreted as a log category according
* to the logger's configuration.
* <p>Default is no warn logging. Specify this setting to activate warn logging into a specific
* category. Alternatively, override the {@link #logException} method for custom logging.
@@ -110,9 +108,9 @@ public abstract class AbstractHandlerExceptionResolver implements HandlerExcepti
/**
* Specify whether to prevent HTTP response caching for any view resolved
- * by this HandlerExceptionResolver.
- * <p>Default is "false". Switch this to "true" in order to automatically
- * generate HTTP response headers that suppress response caching.
+ * by this exception resolver.
+ * <p>Default is {@code false}. Switch this to {@code true} in order to
+ * automatically generate HTTP response headers that suppress response caching.
*/
public void setPreventResponseCaching(boolean preventResponseCaching) {
this.preventResponseCaching = preventResponseCaching;
@@ -120,9 +118,10 @@ public abstract class AbstractHandlerExceptionResolver implements HandlerExcepti
/**
- * Checks whether this resolver is supposed to apply (i.e. the handler matches
- * in case of "mappedHandlers" having been specified), then delegates to the
- * {@link #doResolveException} template method.
+ * Check whether this resolver is supposed to apply (i.e. if the supplied handler
+ * matches any of the configured {@linkplain #setMappedHandlers handlers} or
+ * {@linkplain #setMappedHandlerClasses handler classes}), and then delegate
+ * to the {@link #doResolveException} template method.
*/
@Override
public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response,
@@ -130,8 +129,8 @@ public abstract class AbstractHandlerExceptionResolver implements HandlerExcepti
if (shouldApplyTo(request, handler)) {
// Log exception, both at debug log level and at warn level, if desired.
- if (logger.isDebugEnabled()) {
- logger.debug("Resolving exception from handler [" + handler + "]: " + ex);
+ if (this.logger.isDebugEnabled()) {
+ this.logger.debug("Resolving exception from handler [" + handler + "]: " + ex);
}
logException(ex, request);
prepareResponse(ex, response);
@@ -144,8 +143,9 @@ public abstract class AbstractHandlerExceptionResolver implements HandlerExcepti
/**
* Check whether this resolver is supposed to apply to the given handler.
- * <p>The default implementation checks against the specified mapped handlers
- * and handler classes, if any.
+ * <p>The default implementation checks against the configured
+ * {@linkplain #setMappedHandlers handlers} and
+ * {@linkplain #setMappedHandlerClasses handler classes}, if any.
* @param request current HTTP request
* @param handler the executed handler, or {@code null} if none chosen
* at the time of the exception (for example, if multipart resolution failed)
@@ -175,7 +175,6 @@ public abstract class AbstractHandlerExceptionResolver implements HandlerExcepti
* Log the given exception at warn level, provided that warn logging has been
* activated through the {@link #setWarnLogCategory "warnLogCategory"} property.
* <p>Calls {@link #buildLogMessage} in order to determine the concrete message to log.
- * Always passes the full exception to the logger.
* @param ex the exception that got thrown during handler execution
* @param request current HTTP request (useful for obtaining metadata)
* @see #setWarnLogCategory
@@ -184,7 +183,7 @@ public abstract class AbstractHandlerExceptionResolver implements HandlerExcepti
*/
protected void logException(Exception ex, HttpServletRequest request) {
if (this.warnLogger != null && this.warnLogger.isWarnEnabled()) {
- this.warnLogger.warn(buildLogMessage(ex, request), ex);
+ this.warnLogger.warn(buildLogMessage(ex, request));
}
}
@@ -195,7 +194,7 @@ public abstract class AbstractHandlerExceptionResolver implements HandlerExcepti
* @return the log message to use
*/
protected String buildLogMessage(Exception ex, HttpServletRequest request) {
- return "Handler execution resulted in exception";
+ return "Handler execution resulted in exception: " + ex;
}
/**
@@ -215,20 +214,17 @@ public abstract class AbstractHandlerExceptionResolver implements HandlerExcepti
/**
* Prevents the response from being cached, through setting corresponding
- * HTTP headers. See {@code http://www.mnot.net/cache_docs}.
+ * HTTP {@code Cache-Control: no-store} header.
* @param response current HTTP response
*/
protected void preventCaching(HttpServletResponse response) {
- response.setHeader(HEADER_PRAGMA, "no-cache");
- response.setDateHeader(HEADER_EXPIRES, 1L);
- response.setHeader(HEADER_CACHE_CONTROL, "no-cache");
response.addHeader(HEADER_CACHE_CONTROL, "no-store");
}
/**
- * Actually resolve the given exception that got thrown during on handler execution,
- * returning a ModelAndView that represents a specific error page if appropriate.
+ * Actually resolve the given exception that got thrown during handler execution,
+ * returning a {@link ModelAndView} that represents a specific error page if appropriate.
* <p>May be overridden in subclasses, in order to apply specific exception checks.
* Note that this template method will be invoked <i>after</i> checking whether this
* resolved applies ("mappedHandlers" etc), so an implementation may simply proceed
@@ -238,7 +234,7 @@ public abstract class AbstractHandlerExceptionResolver implements HandlerExcepti
* @param handler the executed handler, or {@code null} if none chosen at the time
* of the exception (for example, if multipart resolution failed)
* @param ex the exception that got thrown during handler execution
- * @return a corresponding ModelAndView to forward to, or {@code null} for default processing
+ * @return a corresponding {@code ModelAndView} to forward to, or {@code null} for default processing
*/
protected abstract ModelAndView doResolveException(HttpServletRequest request,
HttpServletResponse response, Object handler, Exception ex);
diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/AbstractHandlerMapping.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/AbstractHandlerMapping.java
index f0ac1307..1877f194 100644
--- a/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/AbstractHandlerMapping.java
+++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/AbstractHandlerMapping.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2014 the original author or authors.
+ * Copyright 2002-2015 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.
@@ -16,14 +16,22 @@
package org.springframework.web.servlet.handler;
+import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
+import java.util.Map;
import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanFactoryUtils;
import org.springframework.core.Ordered;
+import org.springframework.web.HttpRequestHandler;
+import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
+import org.springframework.web.cors.CorsProcessor;
+import org.springframework.web.cors.CorsConfiguration;
+import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.util.AntPathMatcher;
import org.springframework.util.Assert;
import org.springframework.util.PathMatcher;
@@ -32,6 +40,8 @@ import org.springframework.web.context.support.WebApplicationObjectSupport;
import org.springframework.web.servlet.HandlerExecutionChain;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.HandlerMapping;
+import org.springframework.web.cors.DefaultCorsProcessor;
+import org.springframework.web.cors.CorsUtils;
import org.springframework.web.util.UrlPathHelper;
/**
@@ -69,7 +79,9 @@ public abstract class AbstractHandlerMapping extends WebApplicationObjectSupport
private final List<HandlerInterceptor> adaptedInterceptors = new ArrayList<HandlerInterceptor>();
- private final List<MappedInterceptor> mappedInterceptors = new ArrayList<MappedInterceptor>();
+ private CorsProcessor corsProcessor = new DefaultCorsProcessor();
+
+ private final UrlBasedCorsConfigurationSource corsConfigSource = new UrlBasedCorsConfigurationSource();
/**
@@ -112,6 +124,7 @@ public abstract class AbstractHandlerMapping extends WebApplicationObjectSupport
*/
public void setAlwaysUseFullPath(boolean alwaysUseFullPath) {
this.urlPathHelper.setAlwaysUseFullPath(alwaysUseFullPath);
+ this.corsConfigSource.setAlwaysUseFullPath(alwaysUseFullPath);
}
/**
@@ -123,6 +136,7 @@ public abstract class AbstractHandlerMapping extends WebApplicationObjectSupport
*/
public void setUrlDecode(boolean urlDecode) {
this.urlPathHelper.setUrlDecode(urlDecode);
+ this.corsConfigSource.setUrlDecode(urlDecode);
}
/**
@@ -132,6 +146,7 @@ public abstract class AbstractHandlerMapping extends WebApplicationObjectSupport
*/
public void setRemoveSemicolonContent(boolean removeSemicolonContent) {
this.urlPathHelper.setRemoveSemicolonContent(removeSemicolonContent);
+ this.corsConfigSource.setRemoveSemicolonContent(removeSemicolonContent);
}
/**
@@ -143,6 +158,7 @@ public abstract class AbstractHandlerMapping extends WebApplicationObjectSupport
public void setUrlPathHelper(UrlPathHelper urlPathHelper) {
Assert.notNull(urlPathHelper, "UrlPathHelper must not be null");
this.urlPathHelper = urlPathHelper;
+ this.corsConfigSource.setUrlPathHelper(urlPathHelper);
}
/**
@@ -160,6 +176,7 @@ public abstract class AbstractHandlerMapping extends WebApplicationObjectSupport
public void setPathMatcher(PathMatcher pathMatcher) {
Assert.notNull(pathMatcher, "PathMatcher must not be null");
this.pathMatcher = pathMatcher;
+ this.corsConfigSource.setPathMatcher(pathMatcher);
}
/**
@@ -184,6 +201,40 @@ public abstract class AbstractHandlerMapping extends WebApplicationObjectSupport
this.interceptors.addAll(Arrays.asList(interceptors));
}
+ /**
+ * Configure a custom {@link CorsProcessor} to use to apply the matched
+ * {@link CorsConfiguration} for a request.
+ * <p>By default {@link DefaultCorsProcessor} is used.
+ * @since 4.2
+ */
+ public void setCorsProcessor(CorsProcessor corsProcessor) {
+ Assert.notNull(corsProcessor, "CorsProcessor must not be null");
+ this.corsProcessor = corsProcessor;
+ }
+
+ /**
+ * Return the configured {@link CorsProcessor}.
+ */
+ public CorsProcessor getCorsProcessor() {
+ return this.corsProcessor;
+ }
+
+ /**
+ * Set "global" CORS configuration based on URL patterns. By default the first
+ * matching URL pattern is combined with the CORS configuration for the
+ * handler, if any.
+ * @since 4.2
+ */
+ public void setCorsConfigurations(Map<String, CorsConfiguration> corsConfigurations) {
+ this.corsConfigSource.setCorsConfigurations(corsConfigurations);
+ }
+
+ /**
+ * Get the CORS configuration.
+ */
+ public Map<String, CorsConfiguration> getCorsConfigurations() {
+ return this.corsConfigSource.getCorsConfigurations();
+ }
/**
* Initializes the interceptors.
@@ -193,7 +244,7 @@ public abstract class AbstractHandlerMapping extends WebApplicationObjectSupport
@Override
protected void initApplicationContext() throws BeansException {
extendInterceptors(this.interceptors);
- detectMappedInterceptors(this.mappedInterceptors);
+ detectMappedInterceptors(this.adaptedInterceptors);
initInterceptors();
}
@@ -216,7 +267,7 @@ public abstract class AbstractHandlerMapping extends WebApplicationObjectSupport
* from the current context and its ancestors. Subclasses can override and refine this policy.
* @param mappedInterceptors an empty list to add {@link MappedInterceptor} instances to
*/
- protected void detectMappedInterceptors(List<MappedInterceptor> mappedInterceptors) {
+ protected void detectMappedInterceptors(List<HandlerInterceptor> mappedInterceptors) {
mappedInterceptors.addAll(
BeanFactoryUtils.beansOfTypeIncludingAncestors(
getApplicationContext(), MappedInterceptor.class, true, false).values());
@@ -235,12 +286,7 @@ public abstract class AbstractHandlerMapping extends WebApplicationObjectSupport
if (interceptor == null) {
throw new IllegalArgumentException("Entry number " + i + " in interceptors array is null");
}
- if (interceptor instanceof MappedInterceptor) {
- this.mappedInterceptors.add((MappedInterceptor) interceptor);
- }
- else {
- this.adaptedInterceptors.add(adaptInterceptor(interceptor));
- }
+ this.adaptedInterceptors.add(adaptInterceptor(interceptor));
}
}
}
@@ -283,8 +329,14 @@ public abstract class AbstractHandlerMapping extends WebApplicationObjectSupport
* @return the array of {@link MappedInterceptor}s, or {@code null} if none
*/
protected final MappedInterceptor[] getMappedInterceptors() {
- int count = this.mappedInterceptors.size();
- return (count > 0 ? this.mappedInterceptors.toArray(new MappedInterceptor[count]) : null);
+ List<MappedInterceptor> mappedInterceptors = new ArrayList<MappedInterceptor>();
+ for (HandlerInterceptor interceptor : this.adaptedInterceptors) {
+ if (interceptor instanceof MappedInterceptor) {
+ mappedInterceptors.add((MappedInterceptor) interceptor);
+ }
+ }
+ int count = mappedInterceptors.size();
+ return (count > 0 ? mappedInterceptors.toArray(new MappedInterceptor[count]) : null);
}
/**
@@ -308,13 +360,26 @@ public abstract class AbstractHandlerMapping extends WebApplicationObjectSupport
String handlerName = (String) handler;
handler = getApplicationContext().getBean(handlerName);
}
- return getHandlerExecutionChain(handler, request);
+
+ HandlerExecutionChain executionChain = getHandlerExecutionChain(handler, request);
+ if (CorsUtils.isCorsRequest(request)) {
+ CorsConfiguration globalConfig = this.corsConfigSource.getCorsConfiguration(request);
+ CorsConfiguration handlerConfig = getCorsConfiguration(handler, request);
+ CorsConfiguration config = (globalConfig != null ? globalConfig.combine(handlerConfig) : handlerConfig);
+ executionChain = getCorsHandlerExecutionChain(request, executionChain, config);
+ }
+ return executionChain;
}
/**
* Look up a handler for the given request, returning {@code null} if no
* specific one is found. This method is called by {@link #getHandler};
* a {@code null} return value will lead to the default handler, if one is set.
+ * <p>On CORS pre-flight requests this method should return a match not for
+ * the pre-flight request but for the expected actual request based on the URL
+ * path, the HTTP methods from the "Access-Control-Request-Method" header, and
+ * the headers from the "Access-Control-Request-Headers" header thus allowing
+ * the CORS configuration to be obtained via {@link #getCorsConfigurations},
* <p>Note: This method may also return a pre-built {@link HandlerExecutionChain},
* combining a handler object with dynamically determined interceptors.
* Statically specified interceptors will get merged into such an existing chain.
@@ -329,8 +394,9 @@ public abstract class AbstractHandlerMapping extends WebApplicationObjectSupport
* applicable interceptors.
* <p>The default implementation builds a standard {@link HandlerExecutionChain}
* with the given handler, the handler mapping's common interceptors, and any
- * {@link MappedInterceptor}s matching to the current request URL. Subclasses
- * may override this in order to extend/rearrange the list of interceptors.
+ * {@link MappedInterceptor}s matching to the current request URL. Interceptors
+ * are added in the order they were registered. Subclasses may override this
+ * in order to extend/rearrange the list of interceptors.
* <p><b>NOTE:</b> The passed-in handler object may be a raw handler or a
* pre-built {@link HandlerExecutionChain}. This method should handle those
* two cases explicitly, either building a new {@link HandlerExecutionChain}
@@ -346,16 +412,96 @@ public abstract class AbstractHandlerMapping extends WebApplicationObjectSupport
protected HandlerExecutionChain getHandlerExecutionChain(Object handler, HttpServletRequest request) {
HandlerExecutionChain chain = (handler instanceof HandlerExecutionChain ?
(HandlerExecutionChain) handler : new HandlerExecutionChain(handler));
- chain.addInterceptors(getAdaptedInterceptors());
String lookupPath = this.urlPathHelper.getLookupPathForRequest(request);
- for (MappedInterceptor mappedInterceptor : this.mappedInterceptors) {
- if (mappedInterceptor.matches(lookupPath, this.pathMatcher)) {
- chain.addInterceptor(mappedInterceptor.getInterceptor());
+ for (HandlerInterceptor interceptor : this.adaptedInterceptors) {
+ if (interceptor instanceof MappedInterceptor) {
+ MappedInterceptor mappedInterceptor = (MappedInterceptor) interceptor;
+ if (mappedInterceptor.matches(lookupPath, this.pathMatcher)) {
+ chain.addInterceptor(mappedInterceptor.getInterceptor());
+ }
}
+ else {
+ chain.addInterceptor(interceptor);
+ }
+ }
+ return chain;
+ }
+
+ /**
+ * Retrieve the CORS configuration for the given handler.
+ * @param handler the handler to check (never {@code null}).
+ * @param request the current request.
+ * @return the CORS configuration for the handler or {@code null}.
+ * @since 4.2
+ */
+ protected CorsConfiguration getCorsConfiguration(Object handler, HttpServletRequest request) {
+ if (handler instanceof HandlerExecutionChain) {
+ handler = ((HandlerExecutionChain) handler).getHandler();
}
+ if (handler instanceof CorsConfigurationSource) {
+ return ((CorsConfigurationSource) handler).getCorsConfiguration(request);
+ }
+ return null;
+ }
+
+ /**
+ * Update the HandlerExecutionChain for CORS-related handling.
+ * <p>For pre-flight requests, the default implementation replaces the selected
+ * handler with a simple HttpRequestHandler that invokes the configured
+ * {@link #setCorsProcessor}.
+ * <p>For actual requests, the default implementation inserts a
+ * HandlerInterceptor that makes CORS-related checks and adds CORS headers.
+ * @param request the current request
+ * @param chain the handler chain
+ * @param config the applicable CORS configuration, possibly {@code null}
+ * @since 4.2
+ */
+ protected HandlerExecutionChain getCorsHandlerExecutionChain(HttpServletRequest request,
+ HandlerExecutionChain chain, CorsConfiguration config) {
+ if (CorsUtils.isPreFlightRequest(request)) {
+ HandlerInterceptor[] interceptors = chain.getInterceptors();
+ chain = new HandlerExecutionChain(new PreFlightHandler(config), interceptors);
+ }
+ else {
+ chain.addInterceptor(new CorsInterceptor(config));
+ }
return chain;
}
+
+ private class PreFlightHandler implements HttpRequestHandler {
+
+ private final CorsConfiguration config;
+
+ public PreFlightHandler(CorsConfiguration config) {
+ this.config = config;
+ }
+
+ @Override
+ public void handleRequest(HttpServletRequest request, HttpServletResponse response)
+ throws IOException {
+
+ corsProcessor.processRequest(this.config, request, response);
+ }
+ }
+
+
+ private class CorsInterceptor extends HandlerInterceptorAdapter {
+
+ private final CorsConfiguration config;
+
+ public CorsInterceptor(CorsConfiguration config) {
+ this.config = config;
+ }
+
+ @Override
+ public boolean preHandle(HttpServletRequest request, HttpServletResponse response,
+ Object handler) throws Exception {
+
+ return corsProcessor.processRequest(this.config, request, response);
+ }
+ }
+
}
diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/AbstractHandlerMethodMapping.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/AbstractHandlerMethodMapping.java
index 6acaccfd..d84d1601 100644
--- a/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/AbstractHandlerMethodMapping.java
+++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/AbstractHandlerMethodMapping.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2015 the original author or authors.
+ * Copyright 2002-2016 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.
@@ -21,22 +21,26 @@ import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
-import java.util.IdentityHashMap;
+import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.locks.ReentrantReadWriteLock;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import org.springframework.beans.factory.BeanFactoryUtils;
import org.springframework.beans.factory.InitializingBean;
+import org.springframework.core.MethodIntrospector;
+import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
-import org.springframework.util.ReflectionUtils.MethodFilter;
+import org.springframework.web.cors.CorsConfiguration;
+import org.springframework.web.cors.CorsUtils;
import org.springframework.web.method.HandlerMethod;
-import org.springframework.web.method.HandlerMethodSelector;
import org.springframework.web.servlet.HandlerMapping;
/**
@@ -67,16 +71,24 @@ public abstract class AbstractHandlerMethodMapping<T> extends AbstractHandlerMap
*/
private static final String SCOPED_TARGET_NAME_PREFIX = "scopedTarget.";
+ private static final HandlerMethod PREFLIGHT_AMBIGUOUS_MATCH =
+ new HandlerMethod(new EmptyHandler(), ClassUtils.getMethod(EmptyHandler.class, "handle"));
- private boolean detectHandlerMethodsInAncestorContexts = false;
+ private static final CorsConfiguration ALLOW_CORS_CONFIG = new CorsConfiguration();
+
+ static {
+ ALLOW_CORS_CONFIG.addAllowedOrigin("*");
+ ALLOW_CORS_CONFIG.addAllowedMethod("*");
+ ALLOW_CORS_CONFIG.addAllowedHeader("*");
+ ALLOW_CORS_CONFIG.setAllowCredentials(true);
+ }
- private HandlerMethodMappingNamingStrategy<T> namingStrategy;
- private final Map<T, HandlerMethod> handlerMethods = new LinkedHashMap<T, HandlerMethod>();
+ private boolean detectHandlerMethodsInAncestorContexts = false;
- private final MultiValueMap<String, T> urlMap = new LinkedMultiValueMap<String, T>();
+ private HandlerMethodMappingNamingStrategy<T> namingStrategy;
- private final MultiValueMap<String, HandlerMethod> nameMap = new LinkedMultiValueMap<String, HandlerMethod>();
+ private final MappingRegistry mappingRegistry = new MappingRegistry();
/**
@@ -94,27 +106,75 @@ public abstract class AbstractHandlerMethodMapping<T> extends AbstractHandlerMap
/**
* Configure the naming strategy to use for assigning a default name to every
* mapped handler method.
+ * <p>The default naming strategy is based on the capital letters of the
+ * class name followed by "#" and then the method name, e.g. "TC#getFoo"
+ * for a class named TestController with method getFoo.
*/
public void setHandlerMethodMappingNamingStrategy(HandlerMethodMappingNamingStrategy<T> namingStrategy) {
this.namingStrategy = namingStrategy;
}
/**
- * Return a map with all handler methods and their mappings.
+ * Return the configured naming strategy or {@code null}.
+ */
+ public HandlerMethodMappingNamingStrategy<T> getNamingStrategy() {
+ return this.namingStrategy;
+ }
+
+ /**
+ * Return a (read-only) map with all mappings and HandlerMethod's.
*/
public Map<T, HandlerMethod> getHandlerMethods() {
- return Collections.unmodifiableMap(this.handlerMethods);
+ this.mappingRegistry.acquireReadLock();
+ try {
+ return Collections.unmodifiableMap(this.mappingRegistry.getMappings());
+ }
+ finally {
+ this.mappingRegistry.releaseReadLock();
+ }
}
/**
- * Return the handler methods mapped to the mapping with the given name.
+ * Return the handler methods for the given mapping name.
* @param mappingName the mapping name
+ * @return a list of matching HandlerMethod's or {@code null}; the returned
+ * list will never be modified and is safe to iterate.
+ * @see #setHandlerMethodMappingNamingStrategy
*/
public List<HandlerMethod> getHandlerMethodsForMappingName(String mappingName) {
- return this.nameMap.get(mappingName);
+ return this.mappingRegistry.getHandlerMethodsByMappingName(mappingName);
+ }
+
+ /**
+ * Return the internal mapping registry. Provided for testing purposes.
+ */
+ MappingRegistry getMappingRegistry() {
+ return this.mappingRegistry;
+ }
+
+ /**
+ * Register the given mapping.
+ * <p>This method may be invoked at runtime after initialization has completed.
+ * @param mapping the mapping for the handler method
+ * @param handler the handler
+ * @param method the method
+ */
+ public void registerMapping(T mapping, Object handler, Method method) {
+ this.mappingRegistry.register(mapping, handler, method);
+ }
+
+ /**
+ * Un-register the given mapping.
+ * <p>This method may be invoked at runtime after initialization has completed.
+ * @param mapping the mapping to unregister
+ */
+ public void unregisterMapping(T mapping) {
+ this.mappingRegistry.unregister(mapping);
}
+ // Handler method detection
+
/**
* Detects handler methods at initialization.
*/
@@ -133,69 +193,56 @@ public abstract class AbstractHandlerMethodMapping<T> extends AbstractHandlerMap
if (logger.isDebugEnabled()) {
logger.debug("Looking for request mappings in application context: " + getApplicationContext());
}
-
String[] beanNames = (this.detectHandlerMethodsInAncestorContexts ?
BeanFactoryUtils.beanNamesForTypeIncludingAncestors(getApplicationContext(), Object.class) :
getApplicationContext().getBeanNamesForType(Object.class));
for (String beanName : beanNames) {
- if (!beanName.startsWith(SCOPED_TARGET_NAME_PREFIX) &&
- isHandler(getApplicationContext().getType(beanName))){
- detectHandlerMethods(beanName);
+ if (!beanName.startsWith(SCOPED_TARGET_NAME_PREFIX)) {
+ Class<?> beanType = null;
+ try {
+ beanType = getApplicationContext().getType(beanName);
+ }
+ catch (Throwable ex) {
+ // An unresolvable bean type, probably from a lazy bean - let's ignore it.
+ if (logger.isDebugEnabled()) {
+ logger.debug("Could not resolve target class for bean with name '" + beanName + "'", ex);
+ }
+ }
+ if (beanType != null && isHandler(beanType)) {
+ detectHandlerMethods(beanName);
+ }
}
}
handlerMethodsInitialized(getHandlerMethods());
}
/**
- * Whether the given type is a handler with handler methods.
- * @param beanType the type of the bean being checked
- * @return "true" if this a handler type, "false" otherwise.
- */
- protected abstract boolean isHandler(Class<?> beanType);
-
- /**
* Look for handler methods in a handler.
* @param handler the bean name of a handler or a handler instance
*/
protected void detectHandlerMethods(final Object handler) {
- Class<?> handlerType =
- (handler instanceof String ? getApplicationContext().getType((String) handler) : handler.getClass());
-
- // Avoid repeated calls to getMappingForMethod which would rebuild RequestMappingInfo instances
- final Map<Method, T> mappings = new IdentityHashMap<Method, T>();
+ Class<?> handlerType = (handler instanceof String ?
+ getApplicationContext().getType((String) handler) : handler.getClass());
final Class<?> userType = ClassUtils.getUserClass(handlerType);
- Set<Method> methods = HandlerMethodSelector.selectMethods(userType, new MethodFilter() {
- @Override
- public boolean matches(Method method) {
- T mapping = getMappingForMethod(method, userType);
- if (mapping != null) {
- mappings.put(method, mapping);
- return true;
- }
- else {
- return false;
- }
- }
- });
+ Map<Method, T> methods = MethodIntrospector.selectMethods(userType,
+ new MethodIntrospector.MetadataLookup<T>() {
+ @Override
+ public T inspect(Method method) {
+ return getMappingForMethod(method, userType);
+ }
+ });
- for (Method method : methods) {
- registerHandlerMethod(handler, method, mappings.get(method));
+ if (logger.isDebugEnabled()) {
+ logger.debug(methods.size() + " request handler methods found on " + userType + ": " + methods);
+ }
+ for (Map.Entry<Method, T> entry : methods.entrySet()) {
+ registerHandlerMethod(handler, entry.getKey(), entry.getValue());
}
}
/**
- * Provide the mapping for a handler method. A method for which no
- * mapping can be provided is not a handler method.
- * @param method the method to provide a mapping for
- * @param handlerType the handler type, possibly a sub-type of the method's
- * declaring class
- * @return the mapping, or {@code null} if the method is not mapped
- */
- protected abstract T getMappingForMethod(Method method, Class<?> handlerType);
-
- /**
* Register a handler method and its unique mapping. Invoked at startup for
* each detected handler method.
* @param handler the bean name of the handler or the handler instance
@@ -205,52 +252,7 @@ public abstract class AbstractHandlerMethodMapping<T> extends AbstractHandlerMap
* under the same mapping
*/
protected void registerHandlerMethod(Object handler, Method method, T mapping) {
- HandlerMethod newHandlerMethod = createHandlerMethod(handler, method);
- HandlerMethod oldHandlerMethod = this.handlerMethods.get(mapping);
- if (oldHandlerMethod != null && !oldHandlerMethod.equals(newHandlerMethod)) {
- throw new IllegalStateException("Ambiguous mapping found. Cannot map '" + newHandlerMethod.getBean() +
- "' bean method \n" + newHandlerMethod + "\nto " + mapping + ": There is already '" +
- oldHandlerMethod.getBean() + "' bean method\n" + oldHandlerMethod + " mapped.");
- }
-
- this.handlerMethods.put(mapping, newHandlerMethod);
- if (logger.isInfoEnabled()) {
- logger.info("Mapped \"" + mapping + "\" onto " + newHandlerMethod);
- }
-
- Set<String> patterns = getMappingPathPatterns(mapping);
- for (String pattern : patterns) {
- if (!getPathMatcher().isPattern(pattern)) {
- this.urlMap.add(pattern, mapping);
- }
- }
-
- if (this.namingStrategy != null) {
- String name = this.namingStrategy.getName(newHandlerMethod, mapping);
- updateNameMap(name, newHandlerMethod);
- }
- }
-
- private void updateNameMap(String name, HandlerMethod newHandlerMethod) {
- List<HandlerMethod> handlerMethods = this.nameMap.get(name);
- if (handlerMethods != null) {
- for (HandlerMethod handlerMethod : handlerMethods) {
- if (handlerMethod.getMethod().equals(newHandlerMethod.getMethod())) {
- logger.trace("Mapping name already registered. Multiple controller instances perhaps?");
- return;
- }
- }
- }
-
- logger.trace("Mapping name=" + name);
- this.nameMap.add(name, newHandlerMethod);
-
- if (this.nameMap.get(name).size() > 1) {
- if (logger.isDebugEnabled()) {
- logger.debug("Mapping name clash for handlerMethods=" + this.nameMap.get(name) +
- ". Consider assigning explicit names.");
- }
- }
+ this.mappingRegistry.register(mapping, handler, method);
}
/**
@@ -273,9 +275,11 @@ public abstract class AbstractHandlerMethodMapping<T> extends AbstractHandlerMap
}
/**
- * Extract and return the URL paths contained in a mapping.
+ * Extract and return the CORS configuration for the mapping.
*/
- protected abstract Set<String> getMappingPathPatterns(T mapping);
+ protected CorsConfiguration initCorsConfiguration(Object handler, Method method, T mapping) {
+ return null;
+ }
/**
* Invoked after all handler methods have been detected.
@@ -285,6 +289,8 @@ public abstract class AbstractHandlerMethodMapping<T> extends AbstractHandlerMap
}
+ // Handler method lookup
+
/**
* Look up a handler method for the given request.
*/
@@ -294,16 +300,22 @@ public abstract class AbstractHandlerMethodMapping<T> extends AbstractHandlerMap
if (logger.isDebugEnabled()) {
logger.debug("Looking up handler method for path " + lookupPath);
}
- HandlerMethod handlerMethod = lookupHandlerMethod(lookupPath, request);
- if (logger.isDebugEnabled()) {
- if (handlerMethod != null) {
- logger.debug("Returning handler method [" + handlerMethod + "]");
- }
- else {
- logger.debug("Did not find handler method for [" + lookupPath + "]");
+ this.mappingRegistry.acquireReadLock();
+ try {
+ HandlerMethod handlerMethod = lookupHandlerMethod(lookupPath, request);
+ if (logger.isDebugEnabled()) {
+ if (handlerMethod != null) {
+ logger.debug("Returning handler method [" + handlerMethod + "]");
+ }
+ else {
+ logger.debug("Did not find handler method for [" + lookupPath + "]");
+ }
}
+ return (handlerMethod != null ? handlerMethod.createWithResolvedBean() : null);
+ }
+ finally {
+ this.mappingRegistry.releaseReadLock();
}
- return (handlerMethod != null ? handlerMethod.createWithResolvedBean() : null);
}
/**
@@ -317,37 +329,40 @@ public abstract class AbstractHandlerMethodMapping<T> extends AbstractHandlerMap
*/
protected HandlerMethod lookupHandlerMethod(String lookupPath, HttpServletRequest request) throws Exception {
List<Match> matches = new ArrayList<Match>();
- List<T> directPathMatches = this.urlMap.get(lookupPath);
+ List<T> directPathMatches = this.mappingRegistry.getMappingsByUrl(lookupPath);
if (directPathMatches != null) {
addMatchingMappings(directPathMatches, matches, request);
}
if (matches.isEmpty()) {
// No choice but to go through all mappings...
- addMatchingMappings(this.handlerMethods.keySet(), matches, request);
+ addMatchingMappings(this.mappingRegistry.getMappings().keySet(), matches, request);
}
if (!matches.isEmpty()) {
Comparator<Match> comparator = new MatchComparator(getMappingComparator(request));
Collections.sort(matches, comparator);
if (logger.isTraceEnabled()) {
- logger.trace("Found " + matches.size() + " matching mapping(s) for [" + lookupPath + "] : " + matches);
+ logger.trace("Found " + matches.size() + " matching mapping(s) for [" +
+ lookupPath + "] : " + matches);
}
Match bestMatch = matches.get(0);
if (matches.size() > 1) {
+ if (CorsUtils.isPreFlightRequest(request)) {
+ return PREFLIGHT_AMBIGUOUS_MATCH;
+ }
Match secondBestMatch = matches.get(1);
if (comparator.compare(bestMatch, secondBestMatch) == 0) {
Method m1 = bestMatch.handlerMethod.getMethod();
Method m2 = secondBestMatch.handlerMethod.getMethod();
- throw new IllegalStateException(
- "Ambiguous handler methods mapped for HTTP path '" + request.getRequestURL() + "': {" +
- m1 + ", " + m2 + "}");
+ throw new IllegalStateException("Ambiguous handler methods mapped for HTTP path '" +
+ request.getRequestURL() + "': {" + m1 + ", " + m2 + "}");
}
}
handleMatch(bestMatch.mapping, lookupPath, request);
return bestMatch.handlerMethod;
}
else {
- return handleNoMatch(this.handlerMethods.keySet(), lookupPath, request);
+ return handleNoMatch(this.mappingRegistry.getMappings().keySet(), lookupPath, request);
}
}
@@ -355,11 +370,75 @@ public abstract class AbstractHandlerMethodMapping<T> extends AbstractHandlerMap
for (T mapping : mappings) {
T match = getMatchingMapping(mapping, request);
if (match != null) {
- matches.add(new Match(match, this.handlerMethods.get(mapping)));
+ matches.add(new Match(match, this.mappingRegistry.getMappings().get(mapping)));
+ }
+ }
+ }
+
+ /**
+ * Invoked when a matching mapping is found.
+ * @param mapping the matching mapping
+ * @param lookupPath mapping lookup path within the current servlet mapping
+ * @param request the current request
+ */
+ protected void handleMatch(T mapping, String lookupPath, HttpServletRequest request) {
+ request.setAttribute(HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE, lookupPath);
+ }
+
+ /**
+ * Invoked when no matching mapping is not found.
+ * @param mappings all registered mappings
+ * @param lookupPath mapping lookup path within the current servlet mapping
+ * @param request the current request
+ * @throws ServletException in case of errors
+ */
+ protected HandlerMethod handleNoMatch(Set<T> mappings, String lookupPath, HttpServletRequest request)
+ throws Exception {
+
+ return null;
+ }
+
+ @Override
+ protected CorsConfiguration getCorsConfiguration(Object handler, HttpServletRequest request) {
+ CorsConfiguration corsConfig = super.getCorsConfiguration(handler, request);
+ if (handler instanceof HandlerMethod) {
+ HandlerMethod handlerMethod = (HandlerMethod) handler;
+ if (handlerMethod.equals(PREFLIGHT_AMBIGUOUS_MATCH)) {
+ return AbstractHandlerMethodMapping.ALLOW_CORS_CONFIG;
+ }
+ else {
+ CorsConfiguration corsConfigFromMethod = this.mappingRegistry.getCorsConfiguration(handlerMethod);
+ corsConfig = (corsConfig != null ? corsConfig.combine(corsConfigFromMethod) : corsConfigFromMethod);
}
}
+ return corsConfig;
}
+
+ // Abstract template methods
+
+ /**
+ * Whether the given type is a handler with handler methods.
+ * @param beanType the type of the bean being checked
+ * @return "true" if this a handler type, "false" otherwise.
+ */
+ protected abstract boolean isHandler(Class<?> beanType);
+
+ /**
+ * Provide the mapping for a handler method. A method for which no
+ * mapping can be provided is not a handler method.
+ * @param method the method to provide a mapping for
+ * @param handlerType the handler type, possibly a sub-type of the method's
+ * declaring class
+ * @return the mapping, or {@code null} if the method is not mapped
+ */
+ protected abstract T getMappingForMethod(Method method, Class<?> handlerType);
+
+ /**
+ * Extract and return the URL paths contained in a mapping.
+ */
+ protected abstract Set<String> getMappingPathPatterns(T mapping);
+
/**
* Check if a mapping matches the current request and return a (potentially
* new) mapping with conditions relevant to the current request.
@@ -377,27 +456,245 @@ public abstract class AbstractHandlerMethodMapping<T> extends AbstractHandlerMap
*/
protected abstract Comparator<T> getMappingComparator(HttpServletRequest request);
+
/**
- * Invoked when a matching mapping is found.
- * @param mapping the matching mapping
- * @param lookupPath mapping lookup path within the current servlet mapping
- * @param request the current request
+ * A registry that maintains all mappings to handler methods, exposing methods
+ * to perform lookups and providing concurrent access.
+ *
+ * <p>Package-private for testing purposes.
*/
- protected void handleMatch(T mapping, String lookupPath, HttpServletRequest request) {
- request.setAttribute(HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE, lookupPath);
+ class MappingRegistry {
+
+ private final Map<T, MappingRegistration<T>> registry = new HashMap<T, MappingRegistration<T>>();
+
+ private final Map<T, HandlerMethod> mappingLookup = new LinkedHashMap<T, HandlerMethod>();
+
+ private final MultiValueMap<String, T> urlLookup = new LinkedMultiValueMap<String, T>();
+
+ private final Map<String, List<HandlerMethod>> nameLookup =
+ new ConcurrentHashMap<String, List<HandlerMethod>>();
+
+ private final Map<HandlerMethod, CorsConfiguration> corsLookup =
+ new ConcurrentHashMap<HandlerMethod, CorsConfiguration>();
+
+ private final ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
+
+ /**
+ * Return all mappings and handler methods. Not thread-safe.
+ * @see #acquireReadLock()
+ */
+ public Map<T, HandlerMethod> getMappings() {
+ return this.mappingLookup;
+ }
+
+ /**
+ * Return matches for the given URL path. Not thread-safe.
+ * @see #acquireReadLock()
+ */
+ public List<T> getMappingsByUrl(String urlPath) {
+ return this.urlLookup.get(urlPath);
+ }
+
+ /**
+ * Return handler methods by mapping name. Thread-safe for concurrent use.
+ */
+ public List<HandlerMethod> getHandlerMethodsByMappingName(String mappingName) {
+ return this.nameLookup.get(mappingName);
+ }
+
+ /**
+ * Return CORS configuration. Thread-safe for concurrent use.
+ */
+ public CorsConfiguration getCorsConfiguration(HandlerMethod handlerMethod) {
+ HandlerMethod original = handlerMethod.getResolvedFromHandlerMethod();
+ return this.corsLookup.get(original != null ? original : handlerMethod);
+ }
+
+ /**
+ * Acquire the read lock when using getMappings and getMappingsByUrl.
+ */
+ public void acquireReadLock() {
+ this.readWriteLock.readLock().lock();
+ }
+
+ /**
+ * Release the read lock after using getMappings and getMappingsByUrl.
+ */
+ public void releaseReadLock() {
+ this.readWriteLock.readLock().unlock();
+ }
+
+ public void register(T mapping, Object handler, Method method) {
+ this.readWriteLock.writeLock().lock();
+ try {
+ HandlerMethod handlerMethod = createHandlerMethod(handler, method);
+ assertUniqueMethodMapping(handlerMethod, mapping);
+
+ if (logger.isInfoEnabled()) {
+ logger.info("Mapped \"" + mapping + "\" onto " + handlerMethod);
+ }
+ this.mappingLookup.put(mapping, handlerMethod);
+
+ List<String> directUrls = getDirectUrls(mapping);
+ for (String url : directUrls) {
+ this.urlLookup.add(url, mapping);
+ }
+
+ String name = null;
+ if (getNamingStrategy() != null) {
+ name = getNamingStrategy().getName(handlerMethod, mapping);
+ addMappingName(name, handlerMethod);
+ }
+
+ CorsConfiguration corsConfig = initCorsConfiguration(handler, method, mapping);
+ if (corsConfig != null) {
+ this.corsLookup.put(handlerMethod, corsConfig);
+ }
+
+ this.registry.put(mapping, new MappingRegistration<T>(mapping, handlerMethod, directUrls, name));
+ }
+ finally {
+ this.readWriteLock.writeLock().unlock();
+ }
+ }
+
+ private void assertUniqueMethodMapping(HandlerMethod newHandlerMethod, T mapping) {
+ HandlerMethod handlerMethod = this.mappingLookup.get(mapping);
+ if (handlerMethod != null && !handlerMethod.equals(newHandlerMethod)) {
+ throw new IllegalStateException(
+ "Ambiguous mapping. Cannot map '" + newHandlerMethod.getBean() + "' method \n" +
+ newHandlerMethod + "\nto " + mapping + ": There is already '" +
+ handlerMethod.getBean() + "' bean method\n" + handlerMethod + " mapped.");
+ }
+ }
+
+ private List<String> getDirectUrls(T mapping) {
+ List<String> urls = new ArrayList<String>(1);
+ for (String path : getMappingPathPatterns(mapping)) {
+ if (!getPathMatcher().isPattern(path)) {
+ urls.add(path);
+ }
+ }
+ return urls;
+ }
+
+ private void addMappingName(String name, HandlerMethod handlerMethod) {
+ List<HandlerMethod> oldList = this.nameLookup.get(name);
+ if (oldList == null) {
+ oldList = Collections.<HandlerMethod>emptyList();
+ }
+
+ for (HandlerMethod current : oldList) {
+ if (handlerMethod.equals(current)) {
+ return;
+ }
+ }
+
+ if (logger.isTraceEnabled()) {
+ logger.trace("Mapping name '" + name + "'");
+ }
+
+ List<HandlerMethod> newList = new ArrayList<HandlerMethod>(oldList.size() + 1);
+ newList.addAll(oldList);
+ newList.add(handlerMethod);
+ this.nameLookup.put(name, newList);
+
+ if (newList.size() > 1) {
+ if (logger.isTraceEnabled()) {
+ logger.trace("Mapping name clash for handlerMethods " + newList +
+ ". Consider assigning explicit names.");
+ }
+ }
+ }
+
+ public void unregister(T mapping) {
+ this.readWriteLock.writeLock().lock();
+ try {
+ MappingRegistration<T> definition = this.registry.remove(mapping);
+ if (definition == null) {
+ return;
+ }
+
+ this.mappingLookup.remove(definition.getMapping());
+
+ for (String url : definition.getDirectUrls()) {
+ List<T> list = this.urlLookup.get(url);
+ if (list != null) {
+ list.remove(definition.getMapping());
+ if (list.isEmpty()) {
+ this.urlLookup.remove(url);
+ }
+ }
+ }
+
+ removeMappingName(definition);
+
+ this.corsLookup.remove(definition.getHandlerMethod());
+ }
+ finally {
+ this.readWriteLock.writeLock().unlock();
+ }
+ }
+
+ private void removeMappingName(MappingRegistration<T> definition) {
+ String name = definition.getMappingName();
+ if (name == null) {
+ return;
+ }
+ HandlerMethod handlerMethod = definition.getHandlerMethod();
+ List<HandlerMethod> oldList = this.nameLookup.get(name);
+ if (oldList == null) {
+ return;
+ }
+ if (oldList.size() <= 1) {
+ this.nameLookup.remove(name);
+ return;
+ }
+ List<HandlerMethod> newList = new ArrayList<HandlerMethod>(oldList.size() - 1);
+ for (HandlerMethod current : oldList) {
+ if (!current.equals(handlerMethod)) {
+ newList.add(current);
+ }
+ }
+ this.nameLookup.put(name, newList);
+ }
}
- /**
- * Invoked when no matching mapping is not found.
- * @param mappings all registered mappings
- * @param lookupPath mapping lookup path within the current servlet mapping
- * @param request the current request
- * @throws ServletException in case of errors
- */
- protected HandlerMethod handleNoMatch(Set<T> mappings, String lookupPath, HttpServletRequest request)
- throws Exception {
- return null;
+ private static class MappingRegistration<T> {
+
+ private final T mapping;
+
+ private final HandlerMethod handlerMethod;
+
+ private final List<String> directUrls;
+
+ private final String mappingName;
+
+ public MappingRegistration(T mapping, HandlerMethod handlerMethod, List<String> directUrls, String mappingName) {
+ Assert.notNull(mapping);
+ Assert.notNull(handlerMethod);
+ this.mapping = mapping;
+ this.handlerMethod = handlerMethod;
+ this.directUrls = (directUrls != null ? directUrls : Collections.<String>emptyList());
+ this.mappingName = mappingName;
+ }
+
+ public T getMapping() {
+ return this.mapping;
+ }
+
+ public HandlerMethod getHandlerMethod() {
+ return this.handlerMethod;
+ }
+
+ public List<String> getDirectUrls() {
+ return this.directUrls;
+ }
+
+ public String getMappingName() {
+ return this.mappingName;
+ }
}
@@ -437,4 +734,12 @@ public abstract class AbstractHandlerMethodMapping<T> extends AbstractHandlerMap
}
}
+
+ private static class EmptyHandler {
+
+ public void handle() {
+ throw new UnsupportedOperationException("not implemented");
+ }
+ }
+
}
diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/AbstractUrlHandlerMapping.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/AbstractUrlHandlerMapping.java
index 1817dae3..5d6406e5 100644
--- a/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/AbstractUrlHandlerMapping.java
+++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/AbstractUrlHandlerMapping.java
@@ -54,6 +54,8 @@ public abstract class AbstractUrlHandlerMapping extends AbstractHandlerMapping {
private Object rootHandler;
+ private boolean useTrailingSlashMatch = false;
+
private boolean lazyInitHandlers = false;
private final Map<String, Object> handlerMap = new LinkedHashMap<String, Object>();
@@ -77,6 +79,22 @@ public abstract class AbstractUrlHandlerMapping extends AbstractHandlerMapping {
}
/**
+ * Whether to match to URLs irrespective of the presence of a trailing slash.
+ * If enabled a URL pattern such as "/users" also matches to "/users/".
+ * <p>The default value is {@code false}.
+ */
+ public void setUseTrailingSlashMatch(boolean useTrailingSlashMatch) {
+ this.useTrailingSlashMatch = useTrailingSlashMatch;
+ }
+
+ /**
+ * Whether to match to URLs irrespective of the presence of a trailing slash.
+ */
+ public boolean useTrailingSlashMatch() {
+ return this.useTrailingSlashMatch;
+ }
+
+ /**
* Set whether to lazily initialize handlers. Only applicable to
* singleton handlers, as prototypes are always lazily initialized.
* Default is "false", as eager initialization allows for more efficiency
@@ -159,6 +177,11 @@ public abstract class AbstractUrlHandlerMapping extends AbstractHandlerMapping {
if (getPathMatcher().match(registeredPattern, urlPath)) {
matchingPatterns.add(registeredPattern);
}
+ else if (useTrailingSlashMatch()) {
+ if (!registeredPattern.endsWith("/") && getPathMatcher().match(registeredPattern + "/", urlPath)) {
+ matchingPatterns.add(registeredPattern +"/");
+ }
+ }
}
String bestPatternMatch = null;
Comparator<String> patternComparator = getPathMatcher().getPatternComparator(urlPath);
@@ -171,6 +194,10 @@ public abstract class AbstractUrlHandlerMapping extends AbstractHandlerMapping {
}
if (bestPatternMatch != null) {
handler = this.handlerMap.get(bestPatternMatch);
+ if (handler == null) {
+ Assert.isTrue(bestPatternMatch.endsWith("/"));
+ handler = this.handlerMap.get(bestPatternMatch.substring(0, bestPatternMatch.length() - 1));
+ }
// Bean name or resolved handler?
if (handler instanceof String) {
String handlerName = (String) handler;
diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/MappedInterceptor.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/MappedInterceptor.java
index 88b1b86d..a238d537 100644
--- a/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/MappedInterceptor.java
+++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/MappedInterceptor.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2012 the original author or authors.
+ * Copyright 2002-2015 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.
@@ -16,14 +16,18 @@
package org.springframework.web.servlet.handler;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
import org.springframework.util.PathMatcher;
import org.springframework.web.context.request.WebRequestInterceptor;
import org.springframework.web.servlet.HandlerInterceptor;
+import org.springframework.web.servlet.ModelAndView;
/**
- * Contains a {@link HandlerInterceptor} along with include (and optionally
- * exclude) path patterns to which the interceptor should apply. Also provides
- * matching logic to test if the interceptor applies to a given request path.
+ * Contains and delegates calls to a {@link HandlerInterceptor} along with
+ * include (and optionally exclude) path patterns to which the interceptor should apply.
+ * Also provides matching logic to test if the interceptor applies to a given request path.
*
* <p>A MappedInterceptor can be registered directly with any
* {@link org.springframework.web.servlet.handler.AbstractHandlerMethodMapping
@@ -34,9 +38,10 @@ import org.springframework.web.servlet.HandlerInterceptor;
*
* @author Keith Donald
* @author Rossen Stoyanchev
+ * @author Brian Clozel
* @since 3.0
*/
-public final class MappedInterceptor {
+public final class MappedInterceptor implements HandlerInterceptor {
private final String[] includePatterns;
@@ -122,6 +127,21 @@ public final class MappedInterceptor {
return this.interceptor;
}
+ @Override
+ public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
+ return this.interceptor.preHandle(request, response, handler);
+ }
+
+ @Override
+ public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
+ this.interceptor.postHandle(request, response, handler, modelAndView);
+ }
+
+ @Override
+ public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
+ this.interceptor.afterCompletion(request, response, handler, ex);
+ }
+
/**
* Returns {@code true} if the interceptor applies to the given request path.
* @param lookupPath the current request path
diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/SimpleMappingExceptionResolver.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/SimpleMappingExceptionResolver.java
index 8ca6708d..b24363f1 100644
--- a/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/SimpleMappingExceptionResolver.java
+++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/SimpleMappingExceptionResolver.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2013 the original author or authors.
+ * Copyright 2002-2015 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.
@@ -267,7 +267,7 @@ public class SimpleMappingExceptionResolver extends AbstractHandlerExceptionReso
return depth;
}
// If we've gone as far as we can go and haven't found it...
- if (exceptionClass.equals(Throwable.class)) {
+ if (exceptionClass == Throwable.class) {
return -1;
}
return getDepth(exceptionMapping, exceptionClass.getSuperclass(), depth + 1);
diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/i18n/CookieLocaleResolver.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/i18n/CookieLocaleResolver.java
index 4bf43e28..547a91fc 100644
--- a/spring-webmvc/src/main/java/org/springframework/web/servlet/i18n/CookieLocaleResolver.java
+++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/i18n/CookieLocaleResolver.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2013 the original author or authors.
+ * Copyright 2002-2015 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.
@@ -111,6 +111,7 @@ public class CookieLocaleResolver extends CookieGenerator implements LocaleConte
/**
* Set a fixed TimeZone that this resolver will return if no cookie found.
+ * @since 4.0
*/
public void setDefaultTimeZone(TimeZone defaultTimeZone) {
this.defaultTimeZone = defaultTimeZone;
@@ -119,6 +120,7 @@ public class CookieLocaleResolver extends CookieGenerator implements LocaleConte
/**
* Return the fixed TimeZone that this resolver will return if no cookie found,
* if any.
+ * @since 4.0
*/
protected TimeZone getDefaultTimeZone() {
return this.defaultTimeZone;
@@ -171,7 +173,7 @@ public class CookieLocaleResolver extends CookieGenerator implements LocaleConte
}
}
request.setAttribute(LOCALE_REQUEST_ATTRIBUTE_NAME,
- (locale != null ? locale: determineDefaultLocale(request)));
+ (locale != null ? locale : determineDefaultLocale(request)));
request.setAttribute(TIME_ZONE_REQUEST_ATTRIBUTE_NAME,
(timeZone != null ? timeZone : determineDefaultTimeZone(request)));
}
@@ -197,7 +199,7 @@ public class CookieLocaleResolver extends CookieGenerator implements LocaleConte
removeCookie(response);
}
request.setAttribute(LOCALE_REQUEST_ATTRIBUTE_NAME,
- (locale != null ? locale: determineDefaultLocale(request)));
+ (locale != null ? locale : determineDefaultLocale(request)));
request.setAttribute(TIME_ZONE_REQUEST_ATTRIBUTE_NAME,
(timeZone != null ? timeZone : determineDefaultTimeZone(request)));
}
diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/i18n/LocaleChangeInterceptor.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/i18n/LocaleChangeInterceptor.java
index 833c2962..099e4b46 100644
--- a/spring-webmvc/src/main/java/org/springframework/web/servlet/i18n/LocaleChangeInterceptor.java
+++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/i18n/LocaleChangeInterceptor.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2013 the original author or authors.
+ * Copyright 2002-2015 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.
@@ -20,6 +20,10 @@ import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.servlet.LocaleResolver;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
@@ -30,6 +34,7 @@ import org.springframework.web.servlet.support.RequestContextUtils;
* via a configurable request parameter (default parameter name: "locale").
*
* @author Juergen Hoeller
+ * @author Rossen Stoyanchev
* @since 20.06.2003
* @see org.springframework.web.servlet.LocaleResolver
*/
@@ -40,8 +45,15 @@ public class LocaleChangeInterceptor extends HandlerInterceptorAdapter {
*/
public static final String DEFAULT_PARAM_NAME = "locale";
+
+ protected final Log logger = LogFactory.getLog(getClass());
+
private String paramName = DEFAULT_PARAM_NAME;
+ private String[] httpMethods;
+
+ private boolean ignoreInvalidLocale = false;
+
/**
* Set the name of the parameter that contains a locale specification
@@ -59,21 +71,80 @@ public class LocaleChangeInterceptor extends HandlerInterceptorAdapter {
return this.paramName;
}
+ /**
+ * Configure the HTTP method(s) over which the locale can be changed.
+ * @param httpMethods the methods
+ * @since 4.2
+ */
+ public void setHttpMethods(String... httpMethods) {
+ this.httpMethods = httpMethods;
+ }
+
+ /**
+ * Return the configured HTTP methods.
+ * @since 4.2
+ */
+ public String[] getHttpMethods() {
+ return this.httpMethods;
+ }
+
+ /**
+ * Set whether to ignore an invalid value for the locale parameter.
+ * @since 4.2.2
+ */
+ public void setIgnoreInvalidLocale(boolean ignoreInvalidLocale) {
+ this.ignoreInvalidLocale = ignoreInvalidLocale;
+ }
+
+ /**
+ * Return whether to ignore an invalid value for the locale parameter.
+ * @since 4.2.2
+ */
+ public boolean isIgnoreInvalidLocale() {
+ return this.ignoreInvalidLocale;
+ }
+
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws ServletException {
- String newLocale = request.getParameter(this.paramName);
+ String newLocale = request.getParameter(getParamName());
if (newLocale != null) {
- LocaleResolver localeResolver = RequestContextUtils.getLocaleResolver(request);
- if (localeResolver == null) {
- throw new IllegalStateException("No LocaleResolver found: not in a DispatcherServlet request?");
+ if (checkHttpMethod(request.getMethod())) {
+ LocaleResolver localeResolver = RequestContextUtils.getLocaleResolver(request);
+ if (localeResolver == null) {
+ throw new IllegalStateException(
+ "No LocaleResolver found: not in a DispatcherServlet request?");
+ }
+ try {
+ localeResolver.setLocale(request, response, StringUtils.parseLocaleString(newLocale));
+ }
+ catch (IllegalArgumentException ex) {
+ if (isIgnoreInvalidLocale()) {
+ logger.debug("Ignoring invalid locale value [" + newLocale + "]: " + ex.getMessage());
+ }
+ else {
+ throw ex;
+ }
+ }
}
- localeResolver.setLocale(request, response, StringUtils.parseLocaleString(newLocale));
}
// Proceed in any case.
return true;
}
+ private boolean checkHttpMethod(String currentMethod) {
+ String[] configuredMethods = getHttpMethods();
+ if (ObjectUtils.isEmpty(configuredMethods)) {
+ return true;
+ }
+ for (String configuredMethod : configuredMethods) {
+ if (configuredMethod.equalsIgnoreCase(currentMethod)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
}
diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/i18n/SessionLocaleResolver.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/i18n/SessionLocaleResolver.java
index 1aa94df6..6437f87b 100644
--- a/spring-webmvc/src/main/java/org/springframework/web/servlet/i18n/SessionLocaleResolver.java
+++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/i18n/SessionLocaleResolver.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2013 the original author or authors.
+ * Copyright 2002-2015 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.
@@ -32,15 +32,25 @@ import org.springframework.web.util.WebUtils;
* accept-header locale.
*
* <p>This is most appropriate if the application needs user sessions anyway,
- * that is, when the HttpSession does not have to be created for the locale.
- * The session may optionally contain an associated time zone attribute as well;
- * alternatively, you may specify a default time zone.
+ * i.e. when the {@code HttpSession} does not have to be created just for storing
+ * the user's locale. The session may optionally contain an associated time zone
+ * attribute as well; alternatively, you may specify a default time zone.
*
* <p>Custom controllers can override the user's locale and time zone by calling
* {@code #setLocale(Context)} on the resolver, e.g. responding to a locale change
* request. As a more convenient alternative, consider using
* {@link org.springframework.web.servlet.support.RequestContext#changeLocale}.
*
+ * <p>In contrast to {@link CookieLocaleResolver}, this strategy stores locally
+ * chosen locale settings in the Servlet container's {@code HttpSession}. As a
+ * consequence, those settings are just temporary for each session and therefore
+ * lost when each session terminates.
+ *
+ * <p>Note that there is no direct relationship with external session management
+ * mechanisms such as the "Spring Session" project. This {@code LocaleResolver}
+ * will simply evaluate and modify corresponding {@code HttpSession} attributes
+ * against the current {@code HttpServletRequest}.
+ *
* @author Juergen Hoeller
* @since 27.02.2003
* @see #setDefaultLocale
diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/AbstractController.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/AbstractController.java
index 772953ff..46fab982 100644
--- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/AbstractController.java
+++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/AbstractController.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2014 the original author or authors.
+ * Copyright 2002-2015 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.
@@ -25,7 +25,7 @@ import org.springframework.web.servlet.support.WebContentGenerator;
import org.springframework.web.util.WebUtils;
/**
- * <p>Convenient superclass for controller implementations, using the Template Method
+ * Convenient superclass for controller implementations, using the Template Method
* design pattern.
*
* <p><b><a name="workflow">Workflow
@@ -130,7 +130,8 @@ public abstract class AbstractController extends WebContentGenerator implements
throws Exception {
// Delegate to WebContentGenerator for checking and preparing.
- checkAndPrepare(request, response, this instanceof LastModified);
+ checkRequest(request);
+ prepareResponse(response);
// Execute handleRequestInternal in synchronized block if required.
if (this.synchronizeOnSession) {
@@ -152,6 +153,6 @@ public abstract class AbstractController extends WebContentGenerator implements
* @see #handleRequest
*/
protected abstract ModelAndView handleRequestInternal(HttpServletRequest request, HttpServletResponse response)
- throws Exception;
+ throws Exception;
}
diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/WebContentInterceptor.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/WebContentInterceptor.java
index f9c23f84..038667f4 100644
--- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/WebContentInterceptor.java
+++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/WebContentInterceptor.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2012 the original author or authors.
+ * Copyright 2002-2015 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.
@@ -20,6 +20,7 @@ import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
+
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@@ -27,21 +28,24 @@ import javax.servlet.http.HttpServletResponse;
import org.springframework.util.AntPathMatcher;
import org.springframework.util.Assert;
import org.springframework.util.PathMatcher;
+import org.springframework.http.CacheControl;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.support.WebContentGenerator;
import org.springframework.web.util.UrlPathHelper;
/**
- * Interceptor that checks and prepares request and response. Checks for supported
- * methods and a required session, and applies the specified number of cache seconds.
+ * Handler interceptor that checks the request and prepares the response.
+ * Checks for supported methods and a required session, and applies the
+ * specified {@link org.springframework.http.CacheControl} builder.
* See superclass bean properties for configuration options.
*
- * <p>All the settings supported by this interceptor can also be set on AbstractController.
- * This interceptor is mainly intended for applying checks and preparations to a set of
- * controllers mapped by a HandlerMapping.
+ * <p>All the settings supported by this interceptor can also be set on
+ * {@link AbstractController}. This interceptor is mainly intended for applying
+ * checks and preparations to a set of controllers mapped by a HandlerMapping.
*
* @author Juergen Hoeller
+ * @author Brian Clozel
* @since 27.11.2003
* @see AbstractController
*/
@@ -49,14 +53,16 @@ public class WebContentInterceptor extends WebContentGenerator implements Handle
private UrlPathHelper urlPathHelper = new UrlPathHelper();
+ private PathMatcher pathMatcher = new AntPathMatcher();
+
private Map<String, Integer> cacheMappings = new HashMap<String, Integer>();
- private PathMatcher pathMatcher = new AntPathMatcher();
+ private Map<String, CacheControl> cacheControlMappings = new HashMap<String, CacheControl>();
public WebContentInterceptor() {
- // no restriction of HTTP methods by default,
- // in particular for use with annotated controllers
+ // No restriction of HTTP methods by default,
+ // in particular for use with annotated controllers...
super(false);
}
@@ -120,7 +126,28 @@ public class WebContentInterceptor extends WebContentGenerator implements Handle
Enumeration<?> propNames = cacheMappings.propertyNames();
while (propNames.hasMoreElements()) {
String path = (String) propNames.nextElement();
- this.cacheMappings.put(path, Integer.valueOf(cacheMappings.getProperty(path)));
+ int cacheSeconds = Integer.valueOf(cacheMappings.getProperty(path));
+ this.cacheMappings.put(path, cacheSeconds);
+ }
+ }
+
+ /**
+ * Map specific URL paths to a specific {@link org.springframework.http.CacheControl}.
+ * <p>Overrides the default cache seconds setting of this interceptor.
+ * Can specify a empty {@link org.springframework.http.CacheControl} instance
+ * to exclude a URL path from default caching.
+ * <p>Supports direct matches, e.g. a registered "/test" matches "/test",
+ * and a various Ant-style pattern matches, e.g. a registered "/t*" matches
+ * both "/test" and "/team". For details, see the AntPathMatcher javadoc.
+ * @param cacheControl the {@code CacheControl} to use
+ * @param paths URL paths that will map to the given {@code CacheControl}
+ * @see #setCacheSeconds
+ * @see org.springframework.util.AntPathMatcher
+ * @since 4.2
+ */
+ public void addCacheMapping(CacheControl cacheControl, String... paths) {
+ for (String path : paths) {
+ this.cacheControlMappings.put(path, cacheControl);
}
}
@@ -128,6 +155,7 @@ public class WebContentInterceptor extends WebContentGenerator implements Handle
* Set the PathMatcher implementation to use for matching URL paths
* against registered URL patterns, for determining cache mappings.
* Default is AntPathMatcher.
+ * @see #addCacheMapping
* @see #setCacheMappings
* @see org.springframework.util.AntPathMatcher
*/
@@ -139,44 +167,76 @@ public class WebContentInterceptor extends WebContentGenerator implements Handle
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
- throws ServletException {
+ throws ServletException {
+
+ checkRequest(request);
String lookupPath = this.urlPathHelper.getLookupPathForRequest(request);
if (logger.isDebugEnabled()) {
logger.debug("Looking up cache seconds for [" + lookupPath + "]");
}
+ CacheControl cacheControl = lookupCacheControl(lookupPath);
Integer cacheSeconds = lookupCacheSeconds(lookupPath);
- if (cacheSeconds != null) {
+ if (cacheControl != null) {
if (logger.isDebugEnabled()) {
- logger.debug("Applying " + cacheSeconds + " cache seconds to [" + lookupPath + "]");
+ logger.debug("Applying CacheControl to [" + lookupPath + "]");
}
- checkAndPrepare(request, response, cacheSeconds, handler instanceof LastModified);
+ applyCacheControl(response, cacheControl);
+ }
+ else if (cacheSeconds != null) {
+ if (logger.isDebugEnabled()) {
+ logger.debug("Applying CacheControl to [" + lookupPath + "]");
+ }
+ applyCacheSeconds(response, cacheSeconds);
}
else {
if (logger.isDebugEnabled()) {
logger.debug("Applying default cache seconds to [" + lookupPath + "]");
}
- checkAndPrepare(request, response, handler instanceof LastModified);
+ prepareResponse(response);
}
return true;
}
/**
- * Look up a cache seconds value for the given URL path.
+ * Look up a {@link org.springframework.http.CacheControl} instance for the given URL path.
+ * <p>Supports direct matches, e.g. a registered "/test" matches "/test",
+ * and various Ant-style pattern matches, e.g. a registered "/t*" matches
+ * both "/test" and "/team". For details, see the AntPathMatcher class.
+ * @param urlPath URL the bean is mapped to
+ * @return the associated {@code CacheControl}, or {@code null} if not found
+ * @see org.springframework.util.AntPathMatcher
+ */
+ protected CacheControl lookupCacheControl(String urlPath) {
+ // Direct match?
+ CacheControl cacheControl = this.cacheControlMappings.get(urlPath);
+ if (cacheControl == null) {
+ // Pattern match?
+ for (String registeredPath : this.cacheControlMappings.keySet()) {
+ if (this.pathMatcher.match(registeredPath, urlPath)) {
+ cacheControl = this.cacheControlMappings.get(registeredPath);
+ }
+ }
+ }
+ return cacheControl;
+ }
+
+ /**
+ * Look up a cacheSeconds integer value for the given URL path.
* <p>Supports direct matches, e.g. a registered "/test" matches "/test",
* and various Ant-style pattern matches, e.g. a registered "/t*" matches
* both "/test" and "/team". For details, see the AntPathMatcher class.
* @param urlPath URL the bean is mapped to
- * @return the associated cache seconds, or {@code null} if not found
+ * @return the cacheSeconds integer value, or {@code null} if not found
* @see org.springframework.util.AntPathMatcher
*/
protected Integer lookupCacheSeconds(String urlPath) {
- // direct match?
+ // Direct match?
Integer cacheSeconds = this.cacheMappings.get(urlPath);
if (cacheSeconds == null) {
- // pattern match?
+ // Pattern match?
for (String registeredPath : this.cacheMappings.keySet()) {
if (this.pathMatcher.match(registeredPath, urlPath)) {
cacheSeconds = this.cacheMappings.get(registeredPath);
diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/annotation/AnnotationMethodHandlerAdapter.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/annotation/AnnotationMethodHandlerAdapter.java
index 28cf8ce0..f3cdd873 100644
--- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/annotation/AnnotationMethodHandlerAdapter.java
+++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/annotation/AnnotationMethodHandlerAdapter.java
@@ -56,6 +56,7 @@ import org.springframework.beans.factory.config.ConfigurableBeanFactory;
import org.springframework.core.DefaultParameterNameDiscoverer;
import org.springframework.core.Ordered;
import org.springframework.core.ParameterNameDiscoverer;
+import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
@@ -119,7 +120,7 @@ import org.springframework.web.util.WebUtils;
/**
* Implementation of the {@link org.springframework.web.servlet.HandlerAdapter} interface
- * that maps handler methods based on HTTP paths, HTTP methods and request parameters
+ * that maps handler methods based on HTTP paths, HTTP methods, and request parameters
* expressed through the {@link RequestMapping} annotation.
*
* <p>Supports request parameter binding through the {@link RequestParam} annotation.
@@ -133,13 +134,13 @@ import org.springframework.web.util.WebUtils;
*
* @author Juergen Hoeller
* @author Arjen Poutsma
+ * @author Sam Brannen
* @since 2.5
* @see #setPathMatcher
* @see #setMethodNameResolver
* @see #setWebBindingInitializer
* @see #setSessionAttributeStore
- *
- * @deprecated in Spring 3.2 in favor of
+ * @deprecated as of Spring 3.2, in favor of
* {@link org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter RequestMappingHandlerAdapter}
*/
@Deprecated
@@ -411,12 +412,9 @@ public class AnnotationMethodHandlerAdapter extends WebContentGenerator
}
if (annotatedWithSessionAttributes) {
- // Always prevent caching in case of session attribute management.
checkAndPrepare(request, response, this.cacheSecondsForSessionAttributeHandlers, true);
- // Prepare cached set of session attributes names.
}
else {
- // Uses configured default cacheSeconds setting.
checkAndPrepare(request, response, true);
}
@@ -891,7 +889,7 @@ public class AnnotationMethodHandlerAdapter extends WebContentGenerator
else if (Principal.class.isAssignableFrom(parameterType)) {
return request.getUserPrincipal();
}
- else if (Locale.class.equals(parameterType)) {
+ else if (Locale.class == parameterType) {
return RequestContextUtils.getLocale(request);
}
else if (InputStream.class.isAssignableFrom(parameterType)) {
@@ -915,19 +913,19 @@ public class AnnotationMethodHandlerAdapter extends WebContentGenerator
public ModelAndView getModelAndView(Method handlerMethod, Class<?> handlerType, Object returnValue,
ExtendedModelMap implicitModel, ServletWebRequest webRequest) throws Exception {
- ResponseStatus responseStatusAnn = AnnotationUtils.findAnnotation(handlerMethod, ResponseStatus.class);
- if (responseStatusAnn != null) {
- HttpStatus responseStatus = responseStatusAnn.value();
- String reason = responseStatusAnn.reason();
+ ResponseStatus responseStatus = AnnotatedElementUtils.findMergedAnnotation(handlerMethod, ResponseStatus.class);
+ if (responseStatus != null) {
+ HttpStatus statusCode = responseStatus.code();
+ String reason = responseStatus.reason();
if (!StringUtils.hasText(reason)) {
- webRequest.getResponse().setStatus(responseStatus.value());
+ webRequest.getResponse().setStatus(statusCode.value());
}
else {
- webRequest.getResponse().sendError(responseStatus.value(), reason);
+ webRequest.getResponse().sendError(statusCode.value(), reason);
}
// to be picked up by the RedirectView
- webRequest.getRequest().setAttribute(View.RESPONSE_STATUS_ATTRIBUTE, responseStatus);
+ webRequest.getRequest().setAttribute(View.RESPONSE_STATUS_ATTRIBUTE, statusCode);
this.responseArgumentUsed = true;
}
diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/annotation/AnnotationMethodHandlerExceptionResolver.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/annotation/AnnotationMethodHandlerExceptionResolver.java
index 49c6ea90..70a59702 100644
--- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/annotation/AnnotationMethodHandlerExceptionResolver.java
+++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/annotation/AnnotationMethodHandlerExceptionResolver.java
@@ -43,7 +43,9 @@ import javax.xml.transform.Source;
import org.springframework.core.ExceptionDepthComparator;
import org.springframework.core.GenericTypeResolver;
import org.springframework.core.MethodParameter;
+import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.core.annotation.AnnotationUtils;
+import org.springframework.core.annotation.SynthesizingMethodParameter;
import org.springframework.http.HttpInputMessage;
import org.springframework.http.HttpOutputMessage;
import org.springframework.http.HttpStatus;
@@ -260,7 +262,7 @@ public class AnnotationMethodHandlerExceptionResolver extends AbstractHandlerExc
Object[] args = new Object[paramTypes.length];
Class<?> handlerType = handler.getClass();
for (int i = 0; i < args.length; i++) {
- MethodParameter methodParam = new MethodParameter(handlerMethod, i);
+ MethodParameter methodParam = new SynthesizingMethodParameter(handlerMethod, i);
GenericTypeResolver.resolveParameterType(methodParam, handlerType);
Class<?> paramType = methodParam.getParameterType();
Object argValue = resolveCommonArgument(methodParam, webRequest, thrownException);
@@ -343,7 +345,7 @@ public class AnnotationMethodHandlerExceptionResolver extends AbstractHandlerExc
else if (Principal.class.isAssignableFrom(parameterType)) {
return request.getUserPrincipal();
}
- else if (Locale.class.equals(parameterType)) {
+ else if (Locale.class == parameterType) {
return RequestContextUtils.getLocale(request);
}
else if (InputStream.class.isAssignableFrom(parameterType)) {
@@ -379,15 +381,15 @@ public class AnnotationMethodHandlerExceptionResolver extends AbstractHandlerExc
private ModelAndView getModelAndView(Method handlerMethod, Object returnValue, ServletWebRequest webRequest)
throws Exception {
- ResponseStatus responseStatusAnn = AnnotationUtils.findAnnotation(handlerMethod, ResponseStatus.class);
- if (responseStatusAnn != null) {
- HttpStatus responseStatus = responseStatusAnn.value();
- String reason = responseStatusAnn.reason();
+ ResponseStatus responseStatus = AnnotatedElementUtils.findMergedAnnotation(handlerMethod, ResponseStatus.class);
+ if (responseStatus != null) {
+ HttpStatus statusCode = responseStatus.code();
+ String reason = responseStatus.reason();
if (!StringUtils.hasText(reason)) {
- webRequest.getResponse().setStatus(responseStatus.value());
+ webRequest.getResponse().setStatus(statusCode.value());
}
else {
- webRequest.getResponse().sendError(responseStatus.value(), reason);
+ webRequest.getResponse().sendError(statusCode.value(), reason);
}
}
diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/annotation/DefaultAnnotationHandlerMapping.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/annotation/DefaultAnnotationHandlerMapping.java
index 77c99cde..a1275c9c 100644
--- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/annotation/DefaultAnnotationHandlerMapping.java
+++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/annotation/DefaultAnnotationHandlerMapping.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2012 the original author or authors.
+ * Copyright 2002-2015 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.
@@ -78,8 +78,7 @@ import org.springframework.web.servlet.handler.AbstractDetectingUrlHandlerMappin
* @since 2.5
* @see RequestMapping
* @see AnnotationMethodHandlerAdapter
- *
- * @deprecated in Spring 3.2 in favor of
+ * @deprecated as of Spring 3.2, in favor of
* {@link org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping RequestMappingHandlerMapping}
*/
@Deprecated
diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/annotation/ResponseStatusExceptionResolver.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/annotation/ResponseStatusExceptionResolver.java
index 28fcb63d..b9b9f788 100644
--- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/annotation/ResponseStatusExceptionResolver.java
+++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/annotation/ResponseStatusExceptionResolver.java
@@ -22,7 +22,7 @@ import javax.servlet.http.HttpServletResponse;
import org.springframework.context.MessageSource;
import org.springframework.context.MessageSourceAware;
import org.springframework.context.i18n.LocaleContextHolder;
-import org.springframework.core.annotation.AnnotationUtils;
+import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.servlet.ModelAndView;
@@ -37,9 +37,15 @@ import org.springframework.web.servlet.handler.AbstractHandlerExceptionResolver;
* {@link org.springframework.web.servlet.DispatcherServlet DispatcherServlet}
* and the MVC Java config and the MVC namespace.
*
+ * <p>As of 4.2 this resolver also looks recursively for {@code @ResponseStatus}
+ * present on cause exceptions, and as of 4.2.2 this resolver supports
+ * attribute overrides for {@code @ResponseStatus} in custom composed annotations.
+ *
* @author Arjen Poutsma
* @author Rossen Stoyanchev
+ * @author Sam Brannen
* @since 3.0
+ * @see AnnotatedElementUtils#findMergedAnnotation
*/
public class ResponseStatusExceptionResolver extends AbstractHandlerExceptionResolver implements MessageSourceAware {
@@ -56,7 +62,7 @@ public class ResponseStatusExceptionResolver extends AbstractHandlerExceptionRes
protected ModelAndView doResolveException(HttpServletRequest request, HttpServletResponse response,
Object handler, Exception ex) {
- ResponseStatus responseStatus = AnnotationUtils.findAnnotation(ex.getClass(), ResponseStatus.class);
+ ResponseStatus responseStatus = AnnotatedElementUtils.findMergedAnnotation(ex.getClass(), ResponseStatus.class);
if (responseStatus != null) {
try {
return resolveResponseStatus(responseStatus, request, response, handler, ex);
@@ -65,6 +71,10 @@ public class ResponseStatusExceptionResolver extends AbstractHandlerExceptionRes
logger.warn("Handling of @ResponseStatus resulted in Exception", resolveEx);
}
}
+ else if (ex.getCause() instanceof Exception) {
+ ex = (Exception) ex.getCause();
+ return doResolveException(request, response, handler, ex);
+ }
return null;
}
@@ -87,7 +97,7 @@ public class ResponseStatusExceptionResolver extends AbstractHandlerExceptionRes
protected ModelAndView resolveResponseStatus(ResponseStatus responseStatus, HttpServletRequest request,
HttpServletResponse response, Object handler, Exception ex) throws Exception {
- int statusCode = responseStatus.value().value();
+ int statusCode = responseStatus.code().value();
String reason = responseStatus.reason();
if (this.messageSource != null) {
reason = this.messageSource.getMessage(reason, null, reason, LocaleContextHolder.getLocale());
diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/annotation/ServletAnnotationMappingUtils.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/annotation/ServletAnnotationMappingUtils.java
index 5c0282f5..6c1288fc 100644
--- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/annotation/ServletAnnotationMappingUtils.java
+++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/annotation/ServletAnnotationMappingUtils.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2012 the original author or authors.
+ * Copyright 2002-2015 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.
@@ -31,8 +31,7 @@ import org.springframework.web.util.WebUtils;
* @author Juergen Hoeller
* @author Arjen Poutsma
* @since 2.5.2
- *
- * @deprecated in 3.2 together with {@link DefaultAnnotationHandlerMapping},
+ * @deprecated as of Spring 3.2, together with {@link DefaultAnnotationHandlerMapping},
* {@link AnnotationMethodHandlerAdapter}, and {@link AnnotationMethodHandlerExceptionResolver}.
*/
@Deprecated
@@ -154,7 +153,7 @@ abstract class ServletAnnotationMappingUtils {
}
private static boolean isMediaTypeHeader(String headerName) {
- return "Accept".equalsIgnoreCase(headerName) || "Content-Type".equalsIgnoreCase(headerName);
+ return ("Accept".equalsIgnoreCase(headerName) || "Content-Type".equalsIgnoreCase(headerName));
}
}
diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/condition/AbstractMediaTypeExpression.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/condition/AbstractMediaTypeExpression.java
index 532f6fbc..e6708f27 100644
--- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/condition/AbstractMediaTypeExpression.java
+++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/condition/AbstractMediaTypeExpression.java
@@ -93,7 +93,7 @@ abstract class AbstractMediaTypeExpression implements Comparable<AbstractMediaTy
if (this == obj) {
return true;
}
- if (obj != null && getClass().equals(obj.getClass())) {
+ if (obj != null && getClass() == obj.getClass()) {
AbstractMediaTypeExpression other = (AbstractMediaTypeExpression) obj;
return (this.mediaType.equals(other.mediaType) && this.isNegated == other.isNegated);
}
diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/condition/AbstractRequestCondition.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/condition/AbstractRequestCondition.java
index aa86fe50..92dd4e87 100644
--- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/condition/AbstractRequestCondition.java
+++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/condition/AbstractRequestCondition.java
@@ -33,7 +33,7 @@ public abstract class AbstractRequestCondition<T extends AbstractRequestConditio
if (this == obj) {
return true;
}
- if (obj != null && getClass().equals(obj.getClass())) {
+ if (obj != null && getClass() == obj.getClass()) {
AbstractRequestCondition<?> other = (AbstractRequestCondition<?>) obj;
return getContent().equals(other.getContent());
}
diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/RequestMappingInfo.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/RequestMappingInfo.java
index bf73f151..395ecb9c 100644
--- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/RequestMappingInfo.java
+++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/RequestMappingInfo.java
@@ -16,9 +16,15 @@
package org.springframework.web.servlet.mvc.method;
+import java.util.List;
import javax.servlet.http.HttpServletRequest;
+import org.springframework.http.HttpHeaders;
+import org.springframework.util.PathMatcher;
import org.springframework.util.StringUtils;
+import org.springframework.web.accept.ContentNegotiationManager;
+import org.springframework.web.bind.annotation.RequestMethod;
+import org.springframework.web.cors.CorsUtils;
import org.springframework.web.servlet.mvc.condition.ConsumesRequestCondition;
import org.springframework.web.servlet.mvc.condition.HeadersRequestCondition;
import org.springframework.web.servlet.mvc.condition.ParamsRequestCondition;
@@ -27,6 +33,7 @@ import org.springframework.web.servlet.mvc.condition.ProducesRequestCondition;
import org.springframework.web.servlet.mvc.condition.RequestCondition;
import org.springframework.web.servlet.mvc.condition.RequestConditionHolder;
import org.springframework.web.servlet.mvc.condition.RequestMethodsRequestCondition;
+import org.springframework.web.util.UrlPathHelper;
/**
* Encapsulates the following request mapping conditions:
@@ -208,7 +215,15 @@ public final class RequestMappingInfo implements RequestCondition<RequestMapping
ProducesRequestCondition produces = this.producesCondition.getMatchingCondition(request);
if (methods == null || params == null || headers == null || consumes == null || produces == null) {
- return null;
+ if (CorsUtils.isPreFlightRequest(request)) {
+ methods = getAccessControlRequestMethodCondition(request);
+ if (methods == null || params == null) {
+ return null;
+ }
+ }
+ else {
+ return null;
+ }
}
PatternsRequestCondition patterns = this.patternsCondition.getMatchingCondition(request);
@@ -225,6 +240,21 @@ public final class RequestMappingInfo implements RequestCondition<RequestMapping
methods, params, headers, consumes, produces, custom.getCondition());
}
+ /**
+ * Return a matching RequestMethodsRequestCondition based on the expected
+ * HTTP method specified in a CORS pre-flight request.
+ */
+ private RequestMethodsRequestCondition getAccessControlRequestMethodCondition(HttpServletRequest request) {
+ String expectedMethod = request.getHeader(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD);
+ if (StringUtils.hasText(expectedMethod)) {
+ for (RequestMethod method : getMethodsCondition().getMethods()) {
+ if (expectedMethod.equalsIgnoreCase(method.name())) {
+ return new RequestMethodsRequestCondition(method);
+ }
+ }
+ }
+ return null;
+ }
/**
* Compares "this" info (i.e. the current instance) with another info in the context of a request.
@@ -317,4 +347,283 @@ public final class RequestMappingInfo implements RequestCondition<RequestMapping
return builder.toString();
}
+
+ /**
+ * Create a new {@code RequestMappingInfo.Builder} with the given paths.
+ * @param paths the paths to use
+ * @since 4.2
+ */
+ public static Builder paths(String... paths) {
+ return new DefaultBuilder(paths);
+ }
+
+
+ /**
+ * Defines a builder for creating a RequestMappingInfo.
+ * @since 4.2
+ */
+ public interface Builder {
+
+ /**
+ * Set the path patterns.
+ */
+ Builder paths(String... paths);
+
+ /**
+ * Set the request method conditions.
+ */
+ Builder methods(RequestMethod... methods);
+
+ /**
+ * Set the request param conditions.
+ */
+ Builder params(String... params);
+
+ /**
+ * Set the header conditions.
+ * <p>By default this is not set.
+ */
+ Builder headers(String... headers);
+
+ /**
+ * Set the consumes conditions.
+ */
+ Builder consumes(String... consumes);
+
+ /**
+ * Set the produces conditions.
+ */
+ Builder produces(String... produces);
+
+ /**
+ * Set the mapping name.
+ */
+ Builder mappingName(String name);
+
+ /**
+ * Set a custom condition to use.
+ */
+ Builder customCondition(RequestCondition<?> condition);
+
+ /**
+ * Provide additional configuration needed for request mapping purposes.
+ */
+ Builder options(BuilderConfiguration options);
+
+ /**
+ * Build the RequestMappingInfo.
+ */
+ RequestMappingInfo build();
+ }
+
+
+ private static class DefaultBuilder implements Builder {
+
+ private String[] paths;
+
+ private RequestMethod[] methods;
+
+ private String[] params;
+
+ private String[] headers;
+
+ private String[] consumes;
+
+ private String[] produces;
+
+ private String mappingName;
+
+ private RequestCondition<?> customCondition;
+
+ private BuilderConfiguration options = new BuilderConfiguration();
+
+ public DefaultBuilder(String... paths) {
+ this.paths = paths;
+ }
+
+ @Override
+ public Builder paths(String... paths) {
+ this.paths = paths;
+ return this;
+ }
+
+ @Override
+ public DefaultBuilder methods(RequestMethod... methods) {
+ this.methods = methods;
+ return this;
+ }
+
+ @Override
+ public DefaultBuilder params(String... params) {
+ this.params = params;
+ return this;
+ }
+
+ @Override
+ public DefaultBuilder headers(String... headers) {
+ this.headers = headers;
+ return this;
+ }
+
+ @Override
+ public DefaultBuilder consumes(String... consumes) {
+ this.consumes = consumes;
+ return this;
+ }
+
+ @Override
+ public DefaultBuilder produces(String... produces) {
+ this.produces = produces;
+ return this;
+ }
+
+ @Override
+ public DefaultBuilder mappingName(String name) {
+ this.mappingName = name;
+ return this;
+ }
+
+ @Override
+ public DefaultBuilder customCondition(RequestCondition<?> condition) {
+ this.customCondition = condition;
+ return this;
+ }
+
+ @Override
+ public Builder options(BuilderConfiguration options) {
+ this.options = options;
+ return this;
+ }
+
+ @Override
+ public RequestMappingInfo build() {
+ ContentNegotiationManager manager = this.options.getContentNegotiationManager();
+
+ PatternsRequestCondition patternsCondition = new PatternsRequestCondition(
+ this.paths, this.options.getUrlPathHelper(), this.options.getPathMatcher(),
+ this.options.useSuffixPatternMatch(), this.options.useTrailingSlashMatch(),
+ this.options.getFileExtensions());
+
+ return new RequestMappingInfo(this.mappingName, patternsCondition,
+ new RequestMethodsRequestCondition(methods),
+ new ParamsRequestCondition(this.params),
+ new HeadersRequestCondition(this.headers),
+ new ConsumesRequestCondition(this.consumes, this.headers),
+ new ProducesRequestCondition(this.produces, this.headers, manager),
+ this.customCondition);
+ }
+ }
+
+
+ /**
+ * Container for configuration options used for request mapping purposes.
+ * Such configuration is required to create RequestMappingInfo instances but
+ * is typically used across all RequestMappingInfo instances.
+ * @since 4.2
+ * @see Builder#options
+ */
+ public static class BuilderConfiguration {
+
+ private UrlPathHelper urlPathHelper;
+
+ private PathMatcher pathMatcher;
+
+ private boolean trailingSlashMatch = true;
+
+ private boolean suffixPatternMatch = true;
+
+ private boolean registeredSuffixPatternMatch = false;
+
+ private ContentNegotiationManager contentNegotiationManager;
+
+ /**
+ * Set a custom UrlPathHelper to use for the PatternsRequestCondition.
+ * <p>By default this is not set.
+ */
+ public void setPathHelper(UrlPathHelper pathHelper) {
+ this.urlPathHelper = pathHelper;
+ }
+
+ public UrlPathHelper getUrlPathHelper() {
+ return this.urlPathHelper;
+ }
+
+ /**
+ * Set a custom PathMatcher to use for the PatternsRequestCondition.
+ * <p>By default this is not set.
+ */
+ public void setPathMatcher(PathMatcher pathMatcher) {
+ this.pathMatcher = pathMatcher;
+ }
+
+ public PathMatcher getPathMatcher() {
+ return this.pathMatcher;
+ }
+
+ /**
+ * Whether to apply trailing slash matching in PatternsRequestCondition.
+ * <p>By default this is set to 'true'.
+ */
+ public void setTrailingSlashMatch(boolean trailingSlashMatch) {
+ this.trailingSlashMatch = trailingSlashMatch;
+ }
+
+ public boolean useTrailingSlashMatch() {
+ return this.trailingSlashMatch;
+ }
+
+ /**
+ * Whether to apply suffix pattern matching in PatternsRequestCondition.
+ * <p>By default this is set to 'true'.
+ * @see #setRegisteredSuffixPatternMatch(boolean)
+ */
+ public void setSuffixPatternMatch(boolean suffixPatternMatch) {
+ this.suffixPatternMatch = suffixPatternMatch;
+ }
+
+ public boolean useSuffixPatternMatch() {
+ return this.suffixPatternMatch;
+ }
+
+ /**
+ * Whether suffix pattern matching should be restricted to registered
+ * file extensions only. Setting this property also sets
+ * suffixPatternMatch=true and requires that a
+ * {@link #setContentNegotiationManager} is also configured in order to
+ * obtain the registered file extensions.
+ */
+ public void setRegisteredSuffixPatternMatch(boolean registeredSuffixPatternMatch) {
+ this.registeredSuffixPatternMatch = registeredSuffixPatternMatch;
+ this.suffixPatternMatch = (registeredSuffixPatternMatch || this.suffixPatternMatch);
+ }
+
+ public boolean useRegisteredSuffixPatternMatch() {
+ return this.registeredSuffixPatternMatch;
+ }
+
+ /**
+ * Return the file extensions to use for suffix pattern matching. If
+ * {@code registeredSuffixPatternMatch=true}, the extensions are obtained
+ * from the configured {@code contentNegotiationManager}.
+ */
+ public List<String> getFileExtensions() {
+ if (useRegisteredSuffixPatternMatch() && getContentNegotiationManager() != null) {
+ return this.contentNegotiationManager.getAllFileExtensions();
+ }
+ return null;
+ }
+
+ /**
+ * Set the ContentNegotiationManager to use for the ProducesRequestCondition.
+ * <p>By default this is not set.
+ */
+ public void setContentNegotiationManager(ContentNegotiationManager manager) {
+ this.contentNegotiationManager = manager;
+ }
+
+ public ContentNegotiationManager getContentNegotiationManager() {
+ return this.contentNegotiationManager;
+ }
+ }
+
}
diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/RequestMappingInfoHandlerMapping.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/RequestMappingInfoHandlerMapping.java
index 5574ccfe..5c18540a 100644
--- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/RequestMappingInfoHandlerMapping.java
+++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/RequestMappingInfoHandlerMapping.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2014 the original author or authors.
+ * Copyright 2002-2015 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.
@@ -22,6 +22,7 @@ import java.util.Comparator;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
+import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
@@ -205,7 +206,7 @@ public abstract class RequestMappingInfoHandlerMapping extends AbstractHandlerMe
Set<MediaType> consumableMediaTypes;
Set<MediaType> producibleMediaTypes;
- Set<String> paramConditions;
+ List<String[]> paramConditions;
if (patternAndMethodMatches.isEmpty()) {
consumableMediaTypes = getConsumableMediaTypes(request, patternMatches);
@@ -234,8 +235,7 @@ public abstract class RequestMappingInfoHandlerMapping extends AbstractHandlerMe
throw new HttpMediaTypeNotAcceptableException(new ArrayList<MediaType>(producibleMediaTypes));
}
else if (!CollectionUtils.isEmpty(paramConditions)) {
- String[] params = paramConditions.toArray(new String[paramConditions.size()]);
- throw new UnsatisfiedServletRequestParameterException(params, request.getParameterMap());
+ throw new UnsatisfiedServletRequestParameterException(paramConditions, request.getParameterMap());
}
else {
return null;
@@ -262,18 +262,21 @@ public abstract class RequestMappingInfoHandlerMapping extends AbstractHandlerMe
return result;
}
- private Set<String> getRequestParams(HttpServletRequest request, Set<RequestMappingInfo> partialMatches) {
+ private List<String[]> getRequestParams(HttpServletRequest request, Set<RequestMappingInfo> partialMatches) {
+ List<String[]> result = new ArrayList<String[]>();
for (RequestMappingInfo partialMatch : partialMatches) {
ParamsRequestCondition condition = partialMatch.getParamsCondition();
- if (!CollectionUtils.isEmpty(condition.getExpressions()) && (condition.getMatchingCondition(request) == null)) {
- Set<String> expressions = new HashSet<String>();
- for (NameValueExpression<String> expr : condition.getExpressions()) {
- expressions.add(expr.toString());
+ Set<NameValueExpression<String>> expressions = condition.getExpressions();
+ if (!CollectionUtils.isEmpty(expressions) && condition.getMatchingCondition(request) == null) {
+ int i = 0;
+ String[] array = new String[expressions.size()];
+ for (NameValueExpression<String> expression : expressions) {
+ array[i++] = expression.toString();
}
- return expressions;
+ result.add(array);
}
}
- return null;
+ return result;
}
}
diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/AbstractJsonpResponseBodyAdvice.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/AbstractJsonpResponseBodyAdvice.java
index 85f1e699..0a383c6c 100644
--- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/AbstractJsonpResponseBodyAdvice.java
+++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/AbstractJsonpResponseBodyAdvice.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2014 the original author or authors.
+ * Copyright 2002-2015 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.
diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/AbstractMessageConverterMethodArgumentResolver.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/AbstractMessageConverterMethodArgumentResolver.java
index 6357551b..75c78a91 100644
--- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/AbstractMessageConverterMethodArgumentResolver.java
+++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/AbstractMessageConverterMethodArgumentResolver.java
@@ -17,10 +17,13 @@
package org.springframework.web.servlet.mvc.method.annotation;
import java.io.IOException;
+import java.io.InputStream;
+import java.io.PushbackInputStream;
import java.lang.annotation.Annotation;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Collections;
+import java.util.EnumSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
@@ -32,11 +35,15 @@ import org.apache.commons.logging.LogFactory;
import org.springframework.core.MethodParameter;
import org.springframework.core.ResolvableType;
import org.springframework.core.annotation.AnnotationUtils;
+import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpInputMessage;
+import org.springframework.http.HttpMethod;
+import org.springframework.http.HttpRequest;
import org.springframework.http.InvalidMediaTypeException;
import org.springframework.http.MediaType;
import org.springframework.http.converter.GenericHttpMessageConverter;
import org.springframework.http.converter.HttpMessageConverter;
+import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.http.server.ServletServerHttpRequest;
import org.springframework.util.Assert;
import org.springframework.validation.Errors;
@@ -56,17 +63,39 @@ import org.springframework.web.method.support.HandlerMethodArgumentResolver;
*/
public abstract class AbstractMessageConverterMethodArgumentResolver implements HandlerMethodArgumentResolver {
+ private static final Set<HttpMethod> SUPPORTED_METHODS =
+ EnumSet.of(HttpMethod.POST, HttpMethod.PUT, HttpMethod.PATCH);
+
+ private static final Object NO_VALUE = new Object();
+
+
protected final Log logger = LogFactory.getLog(getClass());
protected final List<HttpMessageConverter<?>> messageConverters;
protected final List<MediaType> allSupportedMediaTypes;
+ private final RequestResponseBodyAdviceChain advice;
+
+
+ /**
+ * Basic constructor with converters only.
+ */
+ public AbstractMessageConverterMethodArgumentResolver(List<HttpMessageConverter<?>> converters) {
+ this(converters, null);
+ }
+
+ /**
+ * Constructor with converters and {@code Request~} and {@code ResponseBodyAdvice}.
+ * @since 4.2
+ */
+ public AbstractMessageConverterMethodArgumentResolver(List<HttpMessageConverter<?>> converters,
+ List<Object> requestResponseBodyAdvice) {
- public AbstractMessageConverterMethodArgumentResolver(List<HttpMessageConverter<?>> messageConverters) {
- Assert.notEmpty(messageConverters, "'messageConverters' must not be empty");
- this.messageConverters = messageConverters;
- this.allSupportedMediaTypes = getAllSupportedMediaTypes(messageConverters);
+ Assert.notEmpty(converters, "'messageConverters' must not be empty");
+ this.messageConverters = converters;
+ this.allSupportedMediaTypes = getAllSupportedMediaTypes(converters);
+ this.advice = new RequestResponseBodyAdviceChain(requestResponseBodyAdvice);
}
@@ -86,6 +115,15 @@ public abstract class AbstractMessageConverterMethodArgumentResolver implements
/**
+ * Return the configured {@link RequestBodyAdvice} and
+ * {@link RequestBodyAdvice} where each instance may be wrapped as a
+ * {@link org.springframework.web.method.ControllerAdviceBean ControllerAdviceBean}.
+ */
+ protected RequestResponseBodyAdviceChain getAdvice() {
+ return this.advice;
+ }
+
+ /**
* Create the method argument value of the expected parameter type by
* reading from the given request.
* @param <T> the expected type of the argument value to be created
@@ -96,8 +134,8 @@ public abstract class AbstractMessageConverterMethodArgumentResolver implements
* @throws IOException if the reading from the request fails
* @throws HttpMediaTypeNotSupportedException if no suitable message converter is found
*/
- protected <T> Object readWithMessageConverters(NativeWebRequest webRequest,
- MethodParameter methodParam, Type paramType) throws IOException, HttpMediaTypeNotSupportedException {
+ protected <T> Object readWithMessageConverters(NativeWebRequest webRequest, MethodParameter methodParam,
+ Type paramType) throws IOException, HttpMediaTypeNotSupportedException, HttpMessageNotReadableException {
HttpInputMessage inputMessage = createInputMessage(webRequest);
return readWithMessageConverters(inputMessage, methodParam, paramType);
@@ -108,19 +146,19 @@ public abstract class AbstractMessageConverterMethodArgumentResolver implements
* from the given HttpInputMessage.
* @param <T> the expected type of the argument value to be created
* @param inputMessage the HTTP input message representing the current request
- * @param methodParam the method parameter descriptor
- * @param targetType the type of object to create, not necessarily the same as
- * the method parameter type (e.g. for {@code HttpEntity<String>} method
- * parameter the target type is String)
+ * @param param the method parameter descriptor (may be {@code null})
+ * @param targetType the target type, not necessarily the same as the method
+ * parameter type, e.g. for {@code HttpEntity<String>}.
* @return the created method argument value
* @throws IOException if the reading from the request fails
* @throws HttpMediaTypeNotSupportedException if no suitable message converter is found
*/
@SuppressWarnings("unchecked")
- protected <T> Object readWithMessageConverters(HttpInputMessage inputMessage,
- MethodParameter methodParam, Type targetType) throws IOException, HttpMediaTypeNotSupportedException {
+ protected <T> Object readWithMessageConverters(HttpInputMessage inputMessage, MethodParameter param,
+ Type targetType) throws IOException, HttpMediaTypeNotSupportedException, HttpMessageNotReadableException {
MediaType contentType;
+ boolean noContentType = false;
try {
contentType = inputMessage.getHeaders().getContentType();
}
@@ -128,34 +166,76 @@ public abstract class AbstractMessageConverterMethodArgumentResolver implements
throw new HttpMediaTypeNotSupportedException(ex.getMessage());
}
if (contentType == null) {
+ noContentType = true;
contentType = MediaType.APPLICATION_OCTET_STREAM;
}
- Class<?> contextClass = methodParam.getContainingClass();
- Class<T> targetClass = (Class<T>)
- ResolvableType.forMethodParameter(methodParam, targetType).resolve(Object.class);
-
- for (HttpMessageConverter<?> converter : this.messageConverters) {
- if (converter instanceof GenericHttpMessageConverter) {
- GenericHttpMessageConverter<?> genericConverter = (GenericHttpMessageConverter<?>) converter;
- if (genericConverter.canRead(targetType, contextClass, contentType)) {
- if (logger.isDebugEnabled()) {
- logger.debug("Reading [" + targetType + "] as \"" +
- contentType + "\" using [" + converter + "]");
+ Class<?> contextClass = (param != null ? param.getContainingClass() : null);
+ Class<T> targetClass = (targetType instanceof Class<?> ? (Class<T>) targetType : null);
+ if (targetClass == null) {
+ ResolvableType resolvableType = (param != null ?
+ ResolvableType.forMethodParameter(param) : ResolvableType.forType(targetType));
+ targetClass = (Class<T>) resolvableType.resolve();
+ }
+
+ HttpMethod httpMethod = ((HttpRequest) inputMessage).getMethod();
+ Object body = NO_VALUE;
+
+ try {
+ inputMessage = new EmptyBodyCheckingHttpInputMessage(inputMessage);
+
+ for (HttpMessageConverter<?> converter : this.messageConverters) {
+ Class<HttpMessageConverter<?>> converterType = (Class<HttpMessageConverter<?>>) converter.getClass();
+ if (converter instanceof GenericHttpMessageConverter) {
+ GenericHttpMessageConverter<?> genericConverter = (GenericHttpMessageConverter<?>) converter;
+ if (genericConverter.canRead(targetType, contextClass, contentType)) {
+ if (logger.isDebugEnabled()) {
+ logger.debug("Read [" + targetType + "] as \"" + contentType + "\" with [" + converter + "]");
+ }
+ if (inputMessage.getBody() != null) {
+ inputMessage = getAdvice().beforeBodyRead(inputMessage, param, targetType, converterType);
+ body = genericConverter.read(targetType, contextClass, inputMessage);
+ body = getAdvice().afterBodyRead(body, inputMessage, param, targetType, converterType);
+ }
+ else {
+ body = null;
+ body = getAdvice().handleEmptyBody(body, inputMessage, param, targetType, converterType);
+ }
+ break;
}
- return genericConverter.read(targetType, contextClass, inputMessage);
}
- }
- if (converter.canRead(targetClass, contentType)) {
- if (logger.isDebugEnabled()) {
- logger.debug("Reading [" + targetClass.getName() + "] as \"" +
- contentType + "\" using [" + converter + "]");
+ else if (targetClass != null) {
+ if (converter.canRead(targetClass, contentType)) {
+ if (logger.isDebugEnabled()) {
+ logger.debug("Read [" + targetType + "] as \"" + contentType + "\" with [" + converter + "]");
+ }
+ if (inputMessage.getBody() != null) {
+ inputMessage = getAdvice().beforeBodyRead(inputMessage, param, targetType, converterType);
+ body = ((HttpMessageConverter<T>) converter).read(targetClass, inputMessage);
+ body = getAdvice().afterBodyRead(body, inputMessage, param, targetType, converterType);
+ }
+ else {
+ body = null;
+ body = getAdvice().handleEmptyBody(body, inputMessage, param, targetType, converterType);
+ }
+ break;
+ }
}
- return ((HttpMessageConverter<T>) converter).read(targetClass, inputMessage);
}
}
+ catch (IOException ex) {
+ throw new HttpMessageNotReadableException("Could not read document: " + ex.getMessage(), ex);
+ }
+
+ if (body == NO_VALUE) {
+ if (httpMethod == null || !SUPPORTED_METHODS.contains(httpMethod) ||
+ (noContentType && inputMessage.getBody() == null)) {
+ return null;
+ }
+ throw new HttpMediaTypeNotSupportedException(contentType, this.allSupportedMediaTypes);
+ }
- throw new HttpMediaTypeNotSupportedException(contentType, this.allSupportedMediaTypes);
+ return body;
}
/**
@@ -205,4 +285,54 @@ public abstract class AbstractMessageConverterMethodArgumentResolver implements
return !hasBindingResult;
}
+
+ private static class EmptyBodyCheckingHttpInputMessage implements HttpInputMessage {
+
+ private final HttpHeaders headers;
+
+ private final InputStream body;
+
+ private final HttpMethod method;
+
+
+ public EmptyBodyCheckingHttpInputMessage(HttpInputMessage inputMessage) throws IOException {
+ this.headers = inputMessage.getHeaders();
+ InputStream inputStream = inputMessage.getBody();
+ if (inputStream == null) {
+ this.body = null;
+ }
+ else if (inputStream.markSupported()) {
+ inputStream.mark(1);
+ this.body = (inputStream.read() != -1 ? inputStream : null);
+ inputStream.reset();
+ }
+ else {
+ PushbackInputStream pushbackInputStream = new PushbackInputStream(inputStream);
+ int b = pushbackInputStream.read();
+ if (b == -1) {
+ this.body = null;
+ }
+ else {
+ this.body = pushbackInputStream;
+ pushbackInputStream.unread(b);
+ }
+ }
+ this.method = ((HttpRequest) inputMessage).getMethod();
+ }
+
+ @Override
+ public HttpHeaders getHeaders() {
+ return this.headers;
+ }
+
+ @Override
+ public InputStream getBody() throws IOException {
+ return this.body;
+ }
+
+ public HttpMethod getMethod() {
+ return this.method;
+ }
+ }
+
}
diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/AbstractMessageConverterMethodProcessor.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/AbstractMessageConverterMethodProcessor.java
index 0982a873..f4034995 100644
--- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/AbstractMessageConverterMethodProcessor.java
+++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/AbstractMessageConverterMethodProcessor.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2014 the original author or authors.
+ * Copyright 2002-2015 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.
@@ -17,6 +17,7 @@
package org.springframework.web.servlet.mvc.method.annotation;
import java.io.IOException;
+import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
@@ -29,10 +30,13 @@ import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.core.MethodParameter;
+import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpOutputMessage;
import org.springframework.http.MediaType;
+import org.springframework.http.converter.GenericHttpMessageConverter;
import org.springframework.http.converter.HttpMessageConverter;
+import org.springframework.http.converter.HttpMessageNotWritableException;
import org.springframework.http.server.ServletServerHttpRequest;
import org.springframework.http.server.ServletServerHttpResponse;
import org.springframework.util.CollectionUtils;
@@ -83,27 +87,36 @@ public abstract class AbstractMessageConverterMethodProcessor extends AbstractMe
private final PathExtensionContentNegotiationStrategy pathStrategy;
- private final ResponseBodyAdviceChain adviceChain;
-
private final Set<String> safeExtensions = new HashSet<String>();
- protected AbstractMessageConverterMethodProcessor(List<HttpMessageConverter<?>> messageConverters) {
- this(messageConverters, null);
+
+ /**
+ * Constructor with list of converters only.
+ */
+ protected AbstractMessageConverterMethodProcessor(List<HttpMessageConverter<?>> converters) {
+ this(converters, null);
}
- protected AbstractMessageConverterMethodProcessor(List<HttpMessageConverter<?>> messageConverters,
- ContentNegotiationManager manager) {
- this(messageConverters, manager, null);
+ /**
+ * Constructor with list of converters and ContentNegotiationManager.
+ */
+ protected AbstractMessageConverterMethodProcessor(List<HttpMessageConverter<?>> converters,
+ ContentNegotiationManager contentNegotiationManager) {
+
+ this(converters, contentNegotiationManager, null);
}
- protected AbstractMessageConverterMethodProcessor(List<HttpMessageConverter<?>> messageConverters,
- ContentNegotiationManager manager, List<Object> responseBodyAdvice) {
+ /**
+ * Constructor with list of converters and ContentNegotiationManager as well
+ * as request/response body advice instances.
+ */
+ protected AbstractMessageConverterMethodProcessor(List<HttpMessageConverter<?>> converters,
+ ContentNegotiationManager manager, List<Object> requestResponseBodyAdvice) {
- super(messageConverters);
+ super(converters, requestResponseBodyAdvice);
this.contentNegotiationManager = (manager != null ? manager : new ContentNegotiationManager());
this.pathStrategy = initPathStrategy(this.contentNegotiationManager);
- this.adviceChain = new ResponseBodyAdviceChain(responseBodyAdvice);
this.safeExtensions.addAll(this.contentNegotiationManager.getAllFileExtensions());
this.safeExtensions.addAll(WHITELISTED_EXTENSIONS);
}
@@ -118,10 +131,6 @@ public abstract class AbstractMessageConverterMethodProcessor extends AbstractMe
}
- protected ResponseBodyAdviceChain getAdviceChain() {
- return this.adviceChain;
- }
-
/**
* Creates a new {@link HttpOutputMessage} from the given {@link NativeWebRequest}.
* @param webRequest the web request to create an output message from
@@ -137,7 +146,7 @@ public abstract class AbstractMessageConverterMethodProcessor extends AbstractMe
* {@link #writeWithMessageConverters(Object, MethodParameter, ServletServerHttpRequest, ServletServerHttpResponse)}
*/
protected <T> void writeWithMessageConverters(T returnValue, MethodParameter returnType, NativeWebRequest webRequest)
- throws IOException, HttpMediaTypeNotAcceptableException {
+ throws IOException, HttpMediaTypeNotAcceptableException, HttpMessageNotWritableException {
ServletServerHttpRequest inputMessage = createInputMessage(webRequest);
ServletServerHttpResponse outputMessage = createOutputMessage(webRequest);
@@ -157,12 +166,17 @@ public abstract class AbstractMessageConverterMethodProcessor extends AbstractMe
@SuppressWarnings("unchecked")
protected <T> void writeWithMessageConverters(T returnValue, MethodParameter returnType,
ServletServerHttpRequest inputMessage, ServletServerHttpResponse outputMessage)
- throws IOException, HttpMediaTypeNotAcceptableException {
+ throws IOException, HttpMediaTypeNotAcceptableException, HttpMessageNotWritableException {
Class<?> returnValueClass = getReturnValueType(returnValue, returnType);
+ Type returnValueType = getGenericType(returnType);
HttpServletRequest servletRequest = inputMessage.getServletRequest();
List<MediaType> requestedMediaTypes = getAcceptableMediaTypes(servletRequest);
- List<MediaType> producibleMediaTypes = getProducibleMediaTypes(servletRequest, returnValueClass);
+ List<MediaType> producibleMediaTypes = getProducibleMediaTypes(servletRequest, returnValueClass, returnValueType);
+
+ if (returnValue != null && producibleMediaTypes.isEmpty()) {
+ throw new IllegalArgumentException("No converter found for return value of type: " + returnValueClass);
+ }
Set<MediaType> compatibleMediaTypes = new LinkedHashSet<MediaType>();
for (MediaType requestedType : requestedMediaTypes) {
@@ -197,15 +211,35 @@ public abstract class AbstractMessageConverterMethodProcessor extends AbstractMe
if (selectedMediaType != null) {
selectedMediaType = selectedMediaType.removeQualityValue();
for (HttpMessageConverter<?> messageConverter : this.messageConverters) {
- if (messageConverter.canWrite(returnValueClass, selectedMediaType)) {
- returnValue = this.adviceChain.invoke(returnValue, returnType, selectedMediaType,
- (Class<HttpMessageConverter<?>>) messageConverter.getClass(), inputMessage, outputMessage);
+ if (messageConverter instanceof GenericHttpMessageConverter) {
+ if (((GenericHttpMessageConverter<T>) messageConverter).canWrite(returnValueType,
+ returnValueClass, selectedMediaType)) {
+ returnValue = (T) getAdvice().beforeBodyWrite(returnValue, returnType, selectedMediaType,
+ (Class<? extends HttpMessageConverter<?>>) messageConverter.getClass(),
+ inputMessage, outputMessage);
+ if (returnValue != null) {
+ addContentDispositionHeader(inputMessage, outputMessage);
+ ((GenericHttpMessageConverter<T>) messageConverter).write(returnValue,
+ returnValueType, selectedMediaType, outputMessage);
+ if (logger.isDebugEnabled()) {
+ logger.debug("Written [" + returnValue + "] as \"" +
+ selectedMediaType + "\" using [" + messageConverter + "]");
+ }
+ }
+ return;
+ }
+ }
+ else if (messageConverter.canWrite(returnValueClass, selectedMediaType)) {
+ returnValue = (T) getAdvice().beforeBodyWrite(returnValue, returnType, selectedMediaType,
+ (Class<? extends HttpMessageConverter<?>>) messageConverter.getClass(),
+ inputMessage, outputMessage);
if (returnValue != null) {
addContentDispositionHeader(inputMessage, outputMessage);
- ((HttpMessageConverter<T>) messageConverter).write(returnValue, selectedMediaType, outputMessage);
+ ((HttpMessageConverter<T>) messageConverter).write(returnValue,
+ selectedMediaType, outputMessage);
if (logger.isDebugEnabled()) {
- logger.debug("Written [" + returnValue + "] as \"" + selectedMediaType + "\" using [" +
- messageConverter + "]");
+ logger.debug("Written [" + returnValue + "] as \"" +
+ selectedMediaType + "\" using [" + messageConverter + "]");
}
}
return;
@@ -229,15 +263,40 @@ public abstract class AbstractMessageConverterMethodProcessor extends AbstractMe
}
/**
+ * Return the generic type of the {@code returnType} (or of the nested type if it is
+ * a {@link HttpEntity}).
+ */
+ private Type getGenericType(MethodParameter returnType) {
+ Type type;
+ if (HttpEntity.class.isAssignableFrom(returnType.getParameterType())) {
+ returnType.increaseNestingLevel();
+ type = returnType.getNestedGenericParameterType();
+ }
+ else {
+ type = returnType.getGenericParameterType();
+ }
+ return type;
+ }
+
+ /**
+ * @see #getProducibleMediaTypes(HttpServletRequest, Class, Type)
+ */
+ @SuppressWarnings({"unchecked", "unused"})
+ protected List<MediaType> getProducibleMediaTypes(HttpServletRequest request, Class<?> returnValueClass) {
+ return getProducibleMediaTypes(request, returnValueClass, null);
+ }
+
+ /**
* Returns the media types that can be produced:
* <ul>
* <li>The producible media types specified in the request mappings, or
* <li>Media types of configured converters that can write the specific return value, or
* <li>{@link MediaType#ALL}
* </ul>
+ * @since 4.2
*/
@SuppressWarnings("unchecked")
- protected List<MediaType> getProducibleMediaTypes(HttpServletRequest request, Class<?> returnValueClass) {
+ protected List<MediaType> getProducibleMediaTypes(HttpServletRequest request, Class<?> returnValueClass, Type returnValueType) {
Set<MediaType> mediaTypes = (Set<MediaType>) request.getAttribute(HandlerMapping.PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE);
if (!CollectionUtils.isEmpty(mediaTypes)) {
return new ArrayList<MediaType>(mediaTypes);
@@ -245,7 +304,12 @@ public abstract class AbstractMessageConverterMethodProcessor extends AbstractMe
else if (!this.allSupportedMediaTypes.isEmpty()) {
List<MediaType> result = new ArrayList<MediaType>();
for (HttpMessageConverter<?> converter : this.messageConverters) {
- if (converter.canWrite(returnValueClass, null)) {
+ if (converter instanceof GenericHttpMessageConverter && returnValueType != null) {
+ if (((GenericHttpMessageConverter<?>) converter).canWrite(returnValueType, returnValueClass, null)) {
+ result.addAll(converter.getSupportedMediaTypes());
+ }
+ }
+ else if (converter.canWrite(returnValueClass, null)) {
result.addAll(converter.getSupportedMediaTypes());
}
}
diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/AsyncTaskMethodReturnValueHandler.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/AsyncTaskMethodReturnValueHandler.java
index 88c7c04c..6691d65c 100644
--- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/AsyncTaskMethodReturnValueHandler.java
+++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/AsyncTaskMethodReturnValueHandler.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2014 the original author or authors.
+ * Copyright 2002-2015 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.
@@ -21,7 +21,7 @@ import org.springframework.core.MethodParameter;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.context.request.async.WebAsyncTask;
import org.springframework.web.context.request.async.WebAsyncUtils;
-import org.springframework.web.method.support.HandlerMethodReturnValueHandler;
+import org.springframework.web.method.support.AsyncHandlerMethodReturnValueHandler;
import org.springframework.web.method.support.ModelAndViewContainer;
/**
@@ -30,7 +30,7 @@ import org.springframework.web.method.support.ModelAndViewContainer;
* @author Rossen Stoyanchev
* @since 3.2
*/
-public class AsyncTaskMethodReturnValueHandler implements HandlerMethodReturnValueHandler {
+public class AsyncTaskMethodReturnValueHandler implements AsyncHandlerMethodReturnValueHandler {
private final BeanFactory beanFactory;
@@ -46,6 +46,11 @@ public class AsyncTaskMethodReturnValueHandler implements HandlerMethodReturnVal
}
@Override
+ public boolean isAsyncReturnValue(Object returnValue, MethodParameter returnType) {
+ return (returnValue != null && returnValue instanceof WebAsyncTask);
+ }
+
+ @Override
public void handleReturnValue(Object returnValue, MethodParameter returnType,
ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception {
diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/CallableMethodReturnValueHandler.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/CallableMethodReturnValueHandler.java
index a38c05bd..cfd5dade 100644
--- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/CallableMethodReturnValueHandler.java
+++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/CallableMethodReturnValueHandler.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2014 the original author or authors.
+ * Copyright 2002-2015 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.
@@ -21,7 +21,7 @@ import java.util.concurrent.Callable;
import org.springframework.core.MethodParameter;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.context.request.async.WebAsyncUtils;
-import org.springframework.web.method.support.HandlerMethodReturnValueHandler;
+import org.springframework.web.method.support.AsyncHandlerMethodReturnValueHandler;
import org.springframework.web.method.support.ModelAndViewContainer;
/**
@@ -30,7 +30,7 @@ import org.springframework.web.method.support.ModelAndViewContainer;
* @author Rossen Stoyanchev
* @since 3.2
*/
-public class CallableMethodReturnValueHandler implements HandlerMethodReturnValueHandler {
+public class CallableMethodReturnValueHandler implements AsyncHandlerMethodReturnValueHandler {
@Override
public boolean supportsReturnType(MethodParameter returnType) {
@@ -38,6 +38,11 @@ public class CallableMethodReturnValueHandler implements HandlerMethodReturnValu
}
@Override
+ public boolean isAsyncReturnValue(Object returnValue, MethodParameter returnType) {
+ return (returnValue != null && returnValue instanceof Callable);
+ }
+
+ @Override
public void handleReturnValue(Object returnValue, MethodParameter returnType,
ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception {
diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/CompletionStageReturnValueHandler.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/CompletionStageReturnValueHandler.java
new file mode 100644
index 00000000..6543b76c
--- /dev/null
+++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/CompletionStageReturnValueHandler.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright 2002-2015 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.mvc.method.annotation;
+
+import java.util.concurrent.CompletionStage;
+import java.util.function.Consumer;
+import java.util.function.Function;
+
+import org.springframework.core.MethodParameter;
+import org.springframework.lang.UsesJava8;
+import org.springframework.web.context.request.NativeWebRequest;
+import org.springframework.web.context.request.async.DeferredResult;
+import org.springframework.web.context.request.async.WebAsyncUtils;
+import org.springframework.web.method.support.AsyncHandlerMethodReturnValueHandler;
+import org.springframework.web.method.support.ModelAndViewContainer;
+
+/**
+ * Handles return values of type {@link CompletionStage} (implemented by
+ * {@link java.util.concurrent.CompletableFuture} for example).
+ *
+ * @author Sebastien Deleuze
+ * @since 4.2
+ */
+@UsesJava8
+public class CompletionStageReturnValueHandler implements AsyncHandlerMethodReturnValueHandler {
+
+ @Override
+ public boolean supportsReturnType(MethodParameter returnType) {
+ return CompletionStage.class.isAssignableFrom(returnType.getParameterType());
+ }
+
+ @Override
+ public boolean isAsyncReturnValue(Object returnValue, MethodParameter returnType) {
+ return (returnValue != null && returnValue instanceof CompletionStage);
+ }
+
+ @Override
+ public void handleReturnValue(Object returnValue, MethodParameter returnType,
+ ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception {
+
+ if (returnValue == null) {
+ mavContainer.setRequestHandled(true);
+ return;
+ }
+
+ final DeferredResult<Object> deferredResult = new DeferredResult<Object>();
+ WebAsyncUtils.getAsyncManager(webRequest).startDeferredResultProcessing(deferredResult, mavContainer);
+
+ @SuppressWarnings("unchecked")
+ CompletionStage<Object> future = (CompletionStage<Object>) returnValue;
+ future.thenAccept(new Consumer<Object>() {
+ @Override
+ public void accept(Object result) {
+ deferredResult.setResult(result);
+ }
+ });
+ future.exceptionally(new Function<Throwable, Object>() {
+ @Override
+ public Object apply(Throwable ex) {
+ deferredResult.setErrorResult(ex);
+ return null;
+ }
+ });
+ }
+
+}
diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/DeferredResultMethodReturnValueHandler.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/DeferredResultMethodReturnValueHandler.java
index 3b66aec9..147e6aff 100644
--- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/DeferredResultMethodReturnValueHandler.java
+++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/DeferredResultMethodReturnValueHandler.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2014 the original author or authors.
+ * Copyright 2002-2015 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.
@@ -20,7 +20,7 @@ import org.springframework.core.MethodParameter;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.context.request.async.DeferredResult;
import org.springframework.web.context.request.async.WebAsyncUtils;
-import org.springframework.web.method.support.HandlerMethodReturnValueHandler;
+import org.springframework.web.method.support.AsyncHandlerMethodReturnValueHandler;
import org.springframework.web.method.support.ModelAndViewContainer;
/**
@@ -29,7 +29,7 @@ import org.springframework.web.method.support.ModelAndViewContainer;
* @author Rossen Stoyanchev
* @since 3.2
*/
-public class DeferredResultMethodReturnValueHandler implements HandlerMethodReturnValueHandler {
+public class DeferredResultMethodReturnValueHandler implements AsyncHandlerMethodReturnValueHandler {
@Override
public boolean supportsReturnType(MethodParameter returnType) {
@@ -37,6 +37,11 @@ public class DeferredResultMethodReturnValueHandler implements HandlerMethodRetu
}
@Override
+ public boolean isAsyncReturnValue(Object returnValue, MethodParameter returnType) {
+ return (returnValue != null && returnValue instanceof DeferredResult);
+ }
+
+ @Override
public void handleReturnValue(Object returnValue, MethodParameter returnType,
ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception {
diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ExceptionHandlerExceptionResolver.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ExceptionHandlerExceptionResolver.java
index a708cc2c..c897a419 100644
--- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ExceptionHandlerExceptionResolver.java
+++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ExceptionHandlerExceptionResolver.java
@@ -31,7 +31,7 @@ import javax.xml.transform.Source;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
-import org.springframework.core.OrderComparator;
+import org.springframework.core.annotation.AnnotationAwareOrderComparator;
import org.springframework.http.converter.ByteArrayHttpMessageConverter;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.StringHttpMessageConverter;
@@ -258,7 +258,7 @@ public class ExceptionHandlerExceptionResolver extends AbstractHandlerMethodExce
}
List<ControllerAdviceBean> adviceBeans = ControllerAdviceBean.findAnnotatedBeans(getApplicationContext());
- OrderComparator.sort(adviceBeans);
+ AnnotationAwareOrderComparator.sort(adviceBeans);
for (ControllerAdviceBean adviceBean : adviceBeans) {
ExceptionHandlerMethodResolver resolver = new ExceptionHandlerMethodResolver(adviceBean.getBeanType());
@@ -293,6 +293,7 @@ public class ExceptionHandlerExceptionResolver extends AbstractHandlerMethodExce
// Type-based argument resolution
resolvers.add(new ServletRequestMethodArgumentResolver());
resolvers.add(new ServletResponseMethodArgumentResolver());
+ resolvers.add(new ModelMethodProcessor());
// Custom arguments
if (getCustomArgumentResolvers() != null) {
@@ -359,11 +360,11 @@ public class ExceptionHandlerExceptionResolver extends AbstractHandlerMethodExce
if (logger.isDebugEnabled()) {
logger.debug("Invoking @ExceptionHandler method: " + exceptionHandlerMethod);
}
- exceptionHandlerMethod.invokeAndHandle(webRequest, mavContainer, exception);
+ exceptionHandlerMethod.invokeAndHandle(webRequest, mavContainer, exception, handlerMethod);
}
catch (Exception invocationEx) {
- if (logger.isErrorEnabled()) {
- logger.error("Failed to invoke @ExceptionHandler method: " + exceptionHandlerMethod, invocationEx);
+ if (logger.isDebugEnabled()) {
+ logger.debug("Failed to invoke @ExceptionHandler method: " + exceptionHandlerMethod, invocationEx);
}
return null;
}
diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/HttpEntityMethodProcessor.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/HttpEntityMethodProcessor.java
index 26d06eff..7741b1c4 100644
--- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/HttpEntityMethodProcessor.java
+++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/HttpEntityMethodProcessor.java
@@ -25,12 +25,15 @@ import org.springframework.core.MethodParameter;
import org.springframework.core.ResolvableType;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
+import org.springframework.http.HttpMethod;
+import org.springframework.http.HttpStatus;
import org.springframework.http.RequestEntity;
import org.springframework.http.ResponseEntity;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.server.ServletServerHttpRequest;
import org.springframework.http.server.ServletServerHttpResponse;
import org.springframework.util.Assert;
+import org.springframework.util.StringUtils;
import org.springframework.web.HttpMediaTypeNotSupportedException;
import org.springframework.web.accept.ContentNegotiationManager;
import org.springframework.web.bind.support.WebDataBinderFactory;
@@ -48,31 +51,58 @@ import org.springframework.web.method.support.ModelAndViewContainer;
*
* @author Arjen Poutsma
* @author Rossen Stoyanchev
+ * @author Brian Clozel
* @since 3.1
*/
public class HttpEntityMethodProcessor extends AbstractMessageConverterMethodProcessor {
- public HttpEntityMethodProcessor(List<HttpMessageConverter<?>> messageConverters) {
- super(messageConverters);
+ /**
+ * Basic constructor with converters only. Suitable for resolving
+ * {@code HttpEntity}. For handling {@code ResponseEntity} consider also
+ * providing a {@code ContentNegotiationManager}.
+ */
+ public HttpEntityMethodProcessor(List<HttpMessageConverter<?>> converters) {
+ super(converters);
}
- public HttpEntityMethodProcessor(List<HttpMessageConverter<?>> messageConverters,
- ContentNegotiationManager contentNegotiationManager) {
+ /**
+ * Basic constructor with converters and {@code ContentNegotiationManager}.
+ * Suitable for resolving {@code HttpEntity} and handling {@code ResponseEntity}
+ * without {@code Request~} or {@code ResponseBodyAdvice}.
+ */
+ public HttpEntityMethodProcessor(List<HttpMessageConverter<?>> converters,
+ ContentNegotiationManager manager) {
- super(messageConverters, contentNegotiationManager);
+ super(converters, manager);
}
- public HttpEntityMethodProcessor(List<HttpMessageConverter<?>> messageConverters,
- ContentNegotiationManager contentNegotiationManager, List<Object> responseBodyAdvice) {
+ /**
+ * Complete constructor for resolving {@code HttpEntity} method arguments.
+ * For handling {@code ResponseEntity} consider also providing a
+ * {@code ContentNegotiationManager}.
+ * @since 4.2
+ */
+ public HttpEntityMethodProcessor(List<HttpMessageConverter<?>> converters,
+ List<Object> requestResponseBodyAdvice) {
- super(messageConverters, contentNegotiationManager, responseBodyAdvice);
+ super(converters, null, requestResponseBodyAdvice);
+ }
+
+ /**
+ * Complete constructor for resolving {@code HttpEntity} and handling
+ * {@code ResponseEntity}.
+ */
+ public HttpEntityMethodProcessor(List<HttpMessageConverter<?>> converters,
+ ContentNegotiationManager manager, List<Object> requestResponseBodyAdvice) {
+
+ super(converters, manager, requestResponseBodyAdvice);
}
@Override
public boolean supportsParameter(MethodParameter parameter) {
- return (HttpEntity.class.equals(parameter.getParameterType()) ||
- RequestEntity.class.equals(parameter.getParameterType()));
+ return (HttpEntity.class == parameter.getParameterType() ||
+ RequestEntity.class == parameter.getParameterType());
}
@Override
@@ -90,7 +120,7 @@ public class HttpEntityMethodProcessor extends AbstractMessageConverterMethodPro
Type paramType = getHttpEntityType(parameter);
Object body = readWithMessageConverters(webRequest, parameter, paramType);
- if (RequestEntity.class.equals(parameter.getParameterType())) {
+ if (RequestEntity.class == parameter.getParameterType()) {
return new RequestEntity<Object>(body, inputMessage.getHeaders(),
inputMessage.getMethod(), inputMessage.getURI());
}
@@ -131,9 +161,6 @@ public class HttpEntityMethodProcessor extends AbstractMessageConverterMethodPro
Assert.isInstanceOf(HttpEntity.class, returnValue);
HttpEntity<?> responseEntity = (HttpEntity<?>) returnValue;
- if (responseEntity instanceof ResponseEntity) {
- outputMessage.setStatusCode(((ResponseEntity<?>) responseEntity).getStatusCode());
- }
HttpHeaders entityHeaders = responseEntity.getHeaders();
if (!entityHeaders.isEmpty()) {
@@ -141,12 +168,70 @@ public class HttpEntityMethodProcessor extends AbstractMessageConverterMethodPro
}
Object body = responseEntity.getBody();
+ if (responseEntity instanceof ResponseEntity) {
+ outputMessage.setStatusCode(((ResponseEntity<?>) responseEntity).getStatusCode());
+ if (HttpMethod.GET == inputMessage.getMethod() && isResourceNotModified(inputMessage, outputMessage)) {
+ outputMessage.setStatusCode(HttpStatus.NOT_MODIFIED);
+ // Ensure headers are flushed, no body should be written.
+ outputMessage.flush();
+ // Skip call to converters, as they may update the body.
+ return;
+ }
+ }
// Try even with null body. ResponseBodyAdvice could get involved.
writeWithMessageConverters(body, returnType, inputMessage, outputMessage);
// Ensure headers are flushed even if no body was written.
- outputMessage.getBody();
+ outputMessage.flush();
+ }
+
+ private boolean isResourceNotModified(ServletServerHttpRequest inputMessage, ServletServerHttpResponse outputMessage) {
+ List<String> ifNoneMatch = inputMessage.getHeaders().getIfNoneMatch();
+ long ifModifiedSince = inputMessage.getHeaders().getIfModifiedSince();
+ String eTag = addEtagPadding(outputMessage.getHeaders().getETag());
+ long lastModified = outputMessage.getHeaders().getLastModified();
+ boolean notModified = false;
+
+ if (!ifNoneMatch.isEmpty() && (inputMessage.getHeaders().containsKey(HttpHeaders.IF_UNMODIFIED_SINCE)
+ || inputMessage.getHeaders().containsKey(HttpHeaders.IF_MATCH))) {
+ // invalid conditional request, do not process
+ }
+ else if (lastModified != -1 && StringUtils.hasLength(eTag)) {
+ notModified = isETagNotModified(ifNoneMatch, eTag) && isTimeStampNotModified(ifModifiedSince, lastModified);
+ }
+ else if (lastModified != -1) {
+ notModified = isTimeStampNotModified(ifModifiedSince, lastModified);
+ }
+ else if (StringUtils.hasLength(eTag)) {
+ notModified = isETagNotModified(ifNoneMatch, eTag);
+ }
+ return notModified;
+ }
+
+ private boolean isETagNotModified(List<String> ifNoneMatch, String etag) {
+ if (StringUtils.hasLength(etag)) {
+ for (String clientETag : ifNoneMatch) {
+ // Compare weak/strong ETags as per https://tools.ietf.org/html/rfc7232#section-2.3
+ if (StringUtils.hasLength(clientETag) &&
+ (clientETag.replaceFirst("^W/", "").equals(etag.replaceFirst("^W/", "")))) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ private boolean isTimeStampNotModified(long ifModifiedSince, long lastModifiedTimestamp) {
+ return (ifModifiedSince >= (lastModifiedTimestamp / 1000 * 1000));
+ }
+
+ private String addEtagPadding(String etag) {
+ if (StringUtils.hasLength(etag) &&
+ (!(etag.startsWith("\"") || etag.startsWith("W/\"")) || !etag.endsWith("\"")) ) {
+ etag = "\"" + etag + "\"";
+ }
+ return etag;
}
@Override
diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/JsonViewRequestBodyAdvice.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/JsonViewRequestBodyAdvice.java
new file mode 100644
index 00000000..255e969a
--- /dev/null
+++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/JsonViewRequestBodyAdvice.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright 2002-2015 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.mvc.method.annotation;
+
+import java.io.IOException;
+import java.lang.reflect.Type;
+
+import com.fasterxml.jackson.annotation.JsonView;
+
+import org.springframework.core.MethodParameter;
+import org.springframework.http.HttpInputMessage;
+import org.springframework.http.converter.HttpMessageConverter;
+import org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter;
+import org.springframework.http.converter.json.MappingJacksonInputMessage;
+
+/**
+ * A {@link RequestBodyAdvice} implementation that adds support for Jackson's
+ * {@code @JsonView} annotation declared on a Spring MVC {@code @HttpEntity}
+ * or {@code @RequestBody} method parameter.
+ *
+ * <p>The deserialization view specified in the annotation will be passed in to the
+ * {@link org.springframework.http.converter.json.MappingJackson2HttpMessageConverter}
+ * which will then use it to deserialize the request body with.
+ *
+ * <p>Note that despite {@code @JsonView} allowing for more than one class to
+ * be specified, the use for a request body advice is only supported with
+ * exactly one class argument. Consider the use of a composite interface.
+ *
+ * <p>Jackson 2.5 or later is required for parameter-level use of {@code @JsonView}.
+ *
+ * @author Sebastien Deleuze
+ * @since 4.2
+ * @see com.fasterxml.jackson.annotation.JsonView
+ * @see com.fasterxml.jackson.databind.ObjectMapper#readerWithView(Class)
+ */
+public class JsonViewRequestBodyAdvice extends RequestBodyAdviceAdapter {
+
+ @Override
+ public boolean supports(MethodParameter methodParameter, Type targetType,
+ Class<? extends HttpMessageConverter<?>> converterType) {
+
+ return (AbstractJackson2HttpMessageConverter.class.isAssignableFrom(converterType) &&
+ methodParameter.getParameterAnnotation(JsonView.class) != null);
+ }
+
+ @Override
+ public HttpInputMessage beforeBodyRead(HttpInputMessage inputMessage, MethodParameter methodParameter,
+ Type targetType, Class<? extends HttpMessageConverter<?>> selectedConverterType) throws IOException {
+
+ JsonView annotation = methodParameter.getParameterAnnotation(JsonView.class);
+ Class<?>[] classes = annotation.value();
+ if (classes.length != 1) {
+ throw new IllegalArgumentException(
+ "@JsonView only supported for request body advice with exactly 1 class argument: " + methodParameter);
+ }
+ return new MappingJacksonInputMessage(inputMessage.getBody(), inputMessage.getHeaders(), classes[0]);
+ }
+
+}
diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/JsonViewResponseBodyAdvice.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/JsonViewResponseBodyAdvice.java
index 944706db..3ed6bb8f 100644
--- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/JsonViewResponseBodyAdvice.java
+++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/JsonViewResponseBodyAdvice.java
@@ -32,7 +32,7 @@ import org.springframework.http.server.ServerHttpResponse;
*
* <p>The serialization view specified in the annotation will be passed in to the
* {@link org.springframework.http.converter.json.MappingJackson2HttpMessageConverter}
- * which will then use it to serialize the response body with.
+ * which will then use it to serialize the response body.
*
* <p>Note that despite {@code @JsonView} allowing for more than one class to
* be specified, the use for a response body advice is only supported with
diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ListenableFutureReturnValueHandler.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ListenableFutureReturnValueHandler.java
index af9fb1a3..18d25a4f 100644
--- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ListenableFutureReturnValueHandler.java
+++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ListenableFutureReturnValueHandler.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2014 the original author or authors.
+ * Copyright 2002-2015 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.
@@ -22,7 +22,7 @@ import org.springframework.util.concurrent.ListenableFutureCallback;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.context.request.async.DeferredResult;
import org.springframework.web.context.request.async.WebAsyncUtils;
-import org.springframework.web.method.support.HandlerMethodReturnValueHandler;
+import org.springframework.web.method.support.AsyncHandlerMethodReturnValueHandler;
import org.springframework.web.method.support.ModelAndViewContainer;
/**
@@ -32,7 +32,7 @@ import org.springframework.web.method.support.ModelAndViewContainer;
* @author Rossen Stoyanchev
* @since 4.1
*/
-public class ListenableFutureReturnValueHandler implements HandlerMethodReturnValueHandler {
+public class ListenableFutureReturnValueHandler implements AsyncHandlerMethodReturnValueHandler {
@Override
public boolean supportsReturnType(MethodParameter returnType) {
@@ -40,6 +40,11 @@ public class ListenableFutureReturnValueHandler implements HandlerMethodReturnVa
}
@Override
+ public boolean isAsyncReturnValue(Object returnValue, MethodParameter returnType) {
+ return (returnValue != null && returnValue instanceof ListenableFuture);
+ }
+
+ @Override
public void handleReturnValue(Object returnValue, MethodParameter returnType,
ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception {
diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/MatrixVariableMapMethodArgumentResolver.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/MatrixVariableMapMethodArgumentResolver.java
index ffefd917..59a8e987 100644
--- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/MatrixVariableMapMethodArgumentResolver.java
+++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/MatrixVariableMapMethodArgumentResolver.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2012 the original author or authors.
+ * Copyright 2002-2015 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,8 +35,8 @@ import org.springframework.web.servlet.HandlerMapping;
/**
* Resolves method arguments of type Map annotated with
- * {@link MatrixVariable @MatrixVariable} where the annotation the does not
- * specify a name. If a name specified then the argument will by resolved by the
+ * {@link MatrixVariable @MatrixVariable} where the annotation does not
+ * specify a name. If a name is specified then the argument will by resolved by the
* {@link MatrixVariableMethodArgumentResolver} instead.
*
* @author Rossen Stoyanchev
@@ -46,10 +46,10 @@ public class MatrixVariableMapMethodArgumentResolver implements HandlerMethodArg
@Override
public boolean supportsParameter(MethodParameter parameter) {
- MatrixVariable paramAnnot = parameter.getParameterAnnotation(MatrixVariable.class);
- if (paramAnnot != null) {
+ MatrixVariable matrixVariable = parameter.getParameterAnnotation(MatrixVariable.class);
+ if (matrixVariable != null) {
if (Map.class.isAssignableFrom(parameter.getParameterType())) {
- return !StringUtils.hasText(paramAnnot.value());
+ return !StringUtils.hasText(matrixVariable.name());
}
}
return false;
diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/MatrixVariableMethodArgumentResolver.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/MatrixVariableMethodArgumentResolver.java
index 8e563d22..6e971176 100644
--- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/MatrixVariableMethodArgumentResolver.java
+++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/MatrixVariableMethodArgumentResolver.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2014 the original author or authors.
+ * Copyright 2002-2016 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.
@@ -33,12 +33,13 @@ import org.springframework.web.method.annotation.AbstractNamedValueMethodArgumen
import org.springframework.web.servlet.HandlerMapping;
/**
- * Resolves method arguments annotated with an {@link MatrixVariable @PathParam}.
+ * Resolves method arguments annotated with {@link MatrixVariable @MatrixVariable}.
*
* <p>If the method parameter is of type Map and no name is specified, then it will
* by resolved by the {@link MatrixVariableMapMethodArgumentResolver} instead.
*
* @author Rossen Stoyanchev
+ * @author Sam Brannen
* @since 3.2
*/
public class MatrixVariableMethodArgumentResolver extends AbstractNamedValueMethodArgumentResolver {
@@ -47,14 +48,15 @@ public class MatrixVariableMethodArgumentResolver extends AbstractNamedValueMeth
super(null);
}
+
@Override
public boolean supportsParameter(MethodParameter parameter) {
if (!parameter.hasParameterAnnotation(MatrixVariable.class)) {
return false;
}
if (Map.class.isAssignableFrom(parameter.getParameterType())) {
- String paramName = parameter.getParameterAnnotation(MatrixVariable.class).value();
- return StringUtils.hasText(paramName);
+ String variableName = parameter.getParameterAnnotation(MatrixVariable.class).name();
+ return StringUtils.hasText(variableName);
}
return true;
}
@@ -62,17 +64,14 @@ public class MatrixVariableMethodArgumentResolver extends AbstractNamedValueMeth
@Override
protected NamedValueInfo createNamedValueInfo(MethodParameter parameter) {
MatrixVariable annotation = parameter.getParameterAnnotation(MatrixVariable.class);
- return new PathParamNamedValueInfo(annotation);
+ return new MatrixVariableNamedValueInfo(annotation);
}
@Override
+ @SuppressWarnings("unchecked")
protected Object resolveName(String name, MethodParameter parameter, NativeWebRequest request) throws Exception {
-
- @SuppressWarnings("unchecked")
- Map<String, MultiValueMap<String, String>> pathParameters =
- (Map<String, MultiValueMap<String, String>>) request.getAttribute(
- HandlerMapping.MATRIX_VARIABLES_ATTRIBUTE, RequestAttributes.SCOPE_REQUEST);
-
+ Map<String, MultiValueMap<String, String>> pathParameters = (Map<String, MultiValueMap<String, String>>)
+ request.getAttribute(HandlerMapping.MATRIX_VARIABLES_ATTRIBUTE, RequestAttributes.SCOPE_REQUEST);
if (CollectionUtils.isEmpty(pathParameters)) {
return null;
}
@@ -94,7 +93,7 @@ public class MatrixVariableMethodArgumentResolver extends AbstractNamedValueMeth
String paramType = parameter.getParameterType().getName();
throw new ServletRequestBindingException(
"Found more than one match for URI path parameter '" + name +
- "' for parameter type [" + paramType + "]. Use pathVar attribute to disambiguate.");
+ "' for parameter type [" + paramType + "]. Use 'pathVar' attribute to disambiguate.");
}
paramValues.addAll(params.get(name));
found = true;
@@ -120,10 +119,11 @@ public class MatrixVariableMethodArgumentResolver extends AbstractNamedValueMeth
}
- private static class PathParamNamedValueInfo extends NamedValueInfo {
+ private static class MatrixVariableNamedValueInfo extends NamedValueInfo {
- private PathParamNamedValueInfo(MatrixVariable annotation) {
- super(annotation.value(), annotation.required(), annotation.defaultValue());
+ private MatrixVariableNamedValueInfo(MatrixVariable annotation) {
+ super(annotation.name(), annotation.required(), annotation.defaultValue());
}
}
-} \ No newline at end of file
+
+}
diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/MvcUriComponentsBuilder.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/MvcUriComponentsBuilder.java
index abdc8049..e8ff7ee9 100644
--- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/MvcUriComponentsBuilder.java
+++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/MvcUriComponentsBuilder.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2014 the original author or authors.
+ * Copyright 2002-2015 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.
@@ -21,6 +21,7 @@ import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
+import java.util.Set;
import javax.servlet.http.HttpServletRequest;
import org.aopalliance.intercept.MethodInterceptor;
@@ -38,14 +39,18 @@ import org.springframework.cglib.proxy.Factory;
import org.springframework.cglib.proxy.MethodProxy;
import org.springframework.core.DefaultParameterNameDiscoverer;
import org.springframework.core.MethodParameter;
+import org.springframework.core.MethodIntrospector;
import org.springframework.core.ParameterNameDiscoverer;
-import org.springframework.core.annotation.AnnotationUtils;
-import org.springframework.objenesis.ObjenesisStd;
+import org.springframework.core.annotation.AnnotatedElementUtils;
+import org.springframework.core.annotation.SynthesizingMethodParameter;
+import org.springframework.objenesis.ObjenesisException;
+import org.springframework.objenesis.SpringObjenesis;
import org.springframework.util.AntPathMatcher;
import org.springframework.util.Assert;
import org.springframework.util.ObjectUtils;
import org.springframework.util.PathMatcher;
import org.springframework.util.ReflectionUtils;
+import org.springframework.util.ReflectionUtils.MethodFilter;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.context.WebApplicationContext;
@@ -62,14 +67,26 @@ import org.springframework.web.util.UriComponents;
import org.springframework.web.util.UriComponentsBuilder;
/**
- * A UriComponentsBuilder that helps to build URIs to Spring MVC controllers
- * and methods from their request mappings.
+ * Creates instances of {@link org.springframework.web.util.UriComponentsBuilder}
+ * by pointing to Spring MVC controllers and {@code @RequestMapping} methods.
+ *
+ * <p>The static {@code fromXxx(...)} methods prepare links relative to the
+ * current request as determined by a call to
+ * {@link org.springframework.web.servlet.support.ServletUriComponentsBuilder#fromCurrentServletMapping()}.
+ *
+ * <p>The static {@code fromXxx(UriComponentsBuilder,...)} methods can be given
+ * the baseUrl when operating outside the context of a request.
+ *
+ * <p>You can also create an MvcUriComponentsBuilder instance with a baseUrl
+ * via {@link #relativeTo(org.springframework.web.util.UriComponentsBuilder)}
+ * and then use the non-static {@code withXxx(...)} method variants.
*
* @author Oliver Gierke
* @author Rossen Stoyanchev
+ * @author Sam Brannen
* @since 4.0
*/
-public class MvcUriComponentsBuilder extends UriComponentsBuilder {
+public class MvcUriComponentsBuilder {
/**
* Well-known name for the {@link CompositeUriComponentsContributor} object in the bean factory.
@@ -79,7 +96,7 @@ public class MvcUriComponentsBuilder extends UriComponentsBuilder {
private static final Log logger = LogFactory.getLog(MvcUriComponentsBuilder.class);
- private static final ObjenesisStd objenesis = new ObjenesisStd(true);
+ private static final SpringObjenesis objenesis = new SpringObjenesis();
private static final PathMatcher pathMatcher = new AntPathMatcher();
@@ -92,27 +109,33 @@ public class MvcUriComponentsBuilder extends UriComponentsBuilder {
new PathVariableMethodArgumentResolver(), new RequestParamMethodArgumentResolver(false));
}
+ private final UriComponentsBuilder baseUrl;
+
/**
* Default constructor. Protected to prevent direct instantiation.
- *
* @see #fromController(Class)
* @see #fromMethodName(Class, String, Object...)
* @see #fromMethodCall(Object)
* @see #fromMappingName(String)
* @see #fromMethod(java.lang.reflect.Method, Object...)
*/
- protected MvcUriComponentsBuilder() {
+ protected MvcUriComponentsBuilder(UriComponentsBuilder baseUrl) {
+ Assert.notNull(baseUrl, "'baseUrl' is required");
+ this.baseUrl = baseUrl;
}
+
/**
- * Create a deep copy of the given MvcUriComponentsBuilder.
- * @param other the other builder to copy from
+ * Create an instance of this class with a base URL. After that calls to one
+ * of the instance based {@code withXxx(...}} methods will create URLs relative
+ * to the given base URL.
*/
- protected MvcUriComponentsBuilder(MvcUriComponentsBuilder other) {
- super(other);
+ public static MvcUriComponentsBuilder relativeTo(UriComponentsBuilder baseUrl) {
+ return new MvcUriComponentsBuilder(baseUrl);
}
+
/**
* Create a {@link UriComponentsBuilder} from the mapping of a controller class
* and current request information including Servlet mapping. If the controller
@@ -121,20 +144,25 @@ public class MvcUriComponentsBuilder extends UriComponentsBuilder {
* @return a UriComponentsBuilder instance (never {@code null})
*/
public static UriComponentsBuilder fromController(Class<?> controllerType) {
- String mapping = getTypeRequestMapping(controllerType);
- return ServletUriComponentsBuilder.fromCurrentServletMapping().path(mapping);
+ return fromController(null, controllerType);
}
- private static String getTypeRequestMapping(Class<?> controllerType) {
- Assert.notNull(controllerType, "'controllerType' must not be null");
- RequestMapping annot = AnnotationUtils.findAnnotation(controllerType, RequestMapping.class);
- if (annot == null || ObjectUtils.isEmpty(annot.value()) || StringUtils.isEmpty(annot.value()[0])) {
- return "/";
- }
- if (annot.value().length > 1 && logger.isWarnEnabled()) {
- logger.warn("Multiple paths on controller " + controllerType.getName() + ", using first one");
- }
- return annot.value()[0];
+ /**
+ * An alternative to {@link #fromController(Class)} that accepts a
+ * {@code UriComponentsBuilder} representing the base URL. This is useful
+ * when using MvcUriComponentsBuilder outside the context of processing a
+ * request or to apply a custom baseUrl not matching the current request.
+ * @param builder the builder for the base URL; the builder will be cloned
+ * and therefore not modified and may be re-used for further calls.
+ * @param controllerType the controller to build a URI for
+ * @return a UriComponentsBuilder instance (never {@code null})
+ */
+ public static UriComponentsBuilder fromController(UriComponentsBuilder builder,
+ Class<?> controllerType) {
+
+ builder = getBaseUrlToUse(builder);
+ String mapping = getTypeRequestMapping(controllerType);
+ return builder.path(mapping);
}
/**
@@ -143,32 +171,37 @@ public class MvcUriComponentsBuilder extends UriComponentsBuilder {
* to {@link #fromMethod(java.lang.reflect.Method, Object...)}.
* @param controllerType the controller
* @param methodName the method name
- * @param argumentValues the argument values
+ * @param args the argument values
* @return a UriComponentsBuilder instance, never {@code null}
* @throws IllegalArgumentException if there is no matching or
* if there is more than one matching method
*/
- public static UriComponentsBuilder fromMethodName(Class<?> controllerType, String methodName, Object... argumentValues) {
- Method method = getMethod(controllerType, methodName, argumentValues);
- return fromMethod(method, argumentValues);
- }
-
- private static Method getMethod(Class<?> controllerType, String methodName, Object... argumentValues) {
- Method match = null;
- for (Method method : controllerType.getDeclaredMethods()) {
- if (method.getName().equals(methodName) && method.getParameterTypes().length == argumentValues.length) {
- if (match != null) {
- throw new IllegalArgumentException("Found two methods named '" + methodName + "' having " +
- Arrays.asList(argumentValues) + " arguments, controller " + controllerType.getName());
- }
- match = method;
- }
- }
- if (match == null) {
- throw new IllegalArgumentException("No method '" + methodName + "' with " + argumentValues.length +
- " parameters found in " + controllerType.getName());
- }
- return match;
+ public static UriComponentsBuilder fromMethodName(Class<?> controllerType,
+ String methodName, Object... args) {
+
+ Method method = getMethod(controllerType, methodName, args);
+ return fromMethodInternal(null, controllerType, method, args);
+ }
+
+ /**
+ * An alternative to {@link #fromMethodName(Class, String, Object...)} that
+ * accepts a {@code UriComponentsBuilder} representing the base URL. This is
+ * useful when using MvcUriComponentsBuilder outside the context of processing
+ * a request or to apply a custom baseUrl not matching the current request.
+ * @param builder the builder for the base URL; the builder will be cloned
+ * and therefore not modified and may be re-used for further calls.
+ * @param controllerType the controller
+ * @param methodName the method name
+ * @param args the argument values
+ * @return a UriComponentsBuilder instance, never {@code null}
+ * @throws IllegalArgumentException if there is no matching or
+ * if there is more than one matching method
+ */
+ public static UriComponentsBuilder fromMethodName(UriComponentsBuilder builder,
+ Class<?> controllerType, String methodName, Object... args) {
+
+ Method method = getMethod(controllerType, methodName, args);
+ return fromMethodInternal(builder, controllerType, method, args);
}
/**
@@ -192,29 +225,51 @@ public class MvcUriComponentsBuilder extends UriComponentsBuilder {
* // Inline style with static import of "MvcUriComponentsBuilder.on"
*
* MvcUriComponentsBuilder.fromMethodCall(
- * on(CustomerController.class).showAddresses("US")).buildAndExpand(1);
+ * on(AddressController.class).getAddressesForCountry("US")).buildAndExpand(1);
*
* // Longer form useful for repeated invocation (and void controller methods)
*
- * CustomerController controller = MvcUriComponentsBuilder.on(CustomController.class);
+ * AddressController controller = MvcUriComponentsBuilder.on(AddressController.class);
* controller.addAddress(null);
* builder = MvcUriComponentsBuilder.fromMethodCall(controller);
* controller.getAddressesForCountry("US")
* builder = MvcUriComponentsBuilder.fromMethodCall(controller);
* </pre>
- * @param invocationInfo either the value returned from a "mock" controller
+ * @param info either the value returned from a "mock" controller
* invocation or the "mock" controller itself after an invocation
* @return a UriComponents instance
*/
- public static UriComponentsBuilder fromMethodCall(Object invocationInfo) {
- Assert.isInstanceOf(MethodInvocationInfo.class, invocationInfo);
- MethodInvocationInfo info = (MethodInvocationInfo) invocationInfo;
- return fromMethod(info.getControllerMethod(), info.getArgumentValues());
+ public static UriComponentsBuilder fromMethodCall(Object info) {
+ Assert.isInstanceOf(MethodInvocationInfo.class, info);
+ MethodInvocationInfo invocationInfo = (MethodInvocationInfo) info;
+ Class<?> controllerType = invocationInfo.getControllerType();
+ Method method = invocationInfo.getControllerMethod();
+ Object[] arguments = invocationInfo.getArgumentValues();
+ return fromMethodInternal(null, controllerType, method, arguments);
+ }
+
+ /**
+ * An alternative to {@link #fromMethodCall(Object)} that accepts a
+ * {@code UriComponentsBuilder} representing the base URL. This is useful
+ * when using MvcUriComponentsBuilder outside the context of processing a
+ * request or to apply a custom baseUrl not matching the current request.
+ * @param builder the builder for the base URL; the builder will be cloned
+ * and therefore not modified and may be re-used for further calls.
+ * @param info either the value returned from a "mock" controller
+ * invocation or the "mock" controller itself after an invocation
+ * @return a UriComponents instance
+ */
+ public static UriComponentsBuilder fromMethodCall(UriComponentsBuilder builder, Object info) {
+ Assert.isInstanceOf(MethodInvocationInfo.class, info);
+ MethodInvocationInfo invocationInfo = (MethodInvocationInfo) info;
+ Class<?> controllerType = invocationInfo.getControllerType();
+ Method method = invocationInfo.getControllerMethod();
+ Object[] arguments = invocationInfo.getArgumentValues();
+ return fromMethodInternal(builder, controllerType, method, arguments);
}
/**
* Create a URL from the name of a Spring MVC controller method's request mapping.
- *
* <p>The configured
* {@link org.springframework.web.servlet.handler.HandlerMethodMappingNamingStrategy
* HandlerMethodMappingNamingStrategy} determines the names of controller
@@ -225,11 +280,9 @@ public class MvcUriComponentsBuilder extends UriComponentsBuilder {
* naming convention does not produce unique results, an explicit name may
* be assigned through the name attribute of the {@code @RequestMapping}
* annotation.
- *
* <p>This is aimed primarily for use in view rendering technologies and EL
* expressions. The Spring URL tag library registers this method as a function
* called "mvcUrl".
- *
* <p>For example, given this controller:
* <pre class="code">
* &#064;RequestMapping("/people")
@@ -248,10 +301,8 @@ public class MvcUriComponentsBuilder extends UriComponentsBuilder {
*
* &lt;a href="${s:mvcUrl('PC#getPerson').arg(0,"123").build()}"&gt;Get Person&lt;/a&gt;
* </pre>
- *
* <p>Note that it's not necessary to specify all arguments. Only the ones
* required to prepare the URL, mainly {@code @RequestParam} and {@code @PathVariable}).
- *
* @param mappingName the mapping name
* @return a builder to to prepare the URI String
* @throws IllegalArgumentException if the mapping name is not found or
@@ -259,16 +310,36 @@ public class MvcUriComponentsBuilder extends UriComponentsBuilder {
* @since 4.1
*/
public static MethodArgumentBuilder fromMappingName(String mappingName) {
+ return fromMappingName(null, mappingName);
+ }
+
+ /**
+ * An alternative to {@link #fromMappingName(String)} that accepts a
+ * {@code UriComponentsBuilder} representing the base URL. This is useful
+ * when using MvcUriComponentsBuilder outside the context of processing a
+ * request or to apply a custom baseUrl not matching the current request.
+ * @param builder the builder for the base URL; the builder will be cloned
+ * and therefore not modified and may be re-used for further calls.
+ * @param name the mapping name
+ * @return a builder to to prepare the URI String
+ * @throws IllegalArgumentException if the mapping name is not found or
+ * if there is no unique match
+ * @since 4.2
+ */
+ public static MethodArgumentBuilder fromMappingName(UriComponentsBuilder builder, String name) {
RequestMappingInfoHandlerMapping handlerMapping = getRequestMappingInfoHandlerMapping();
- List<HandlerMethod> handlerMethods = handlerMapping.getHandlerMethodsForMappingName(mappingName);
+ List<HandlerMethod> handlerMethods = handlerMapping.getHandlerMethodsForMappingName(name);
if (handlerMethods == null) {
- throw new IllegalArgumentException("Mapping mappingName not found: " + mappingName);
+ throw new IllegalArgumentException("Mapping mappingName not found: " + name);
}
if (handlerMethods.size() != 1) {
- throw new IllegalArgumentException(
- "No unique match for mapping mappingName " + mappingName + ": " + handlerMethods);
+ throw new IllegalArgumentException("No unique match for mapping mappingName " +
+ name + ": " + handlerMethods);
}
- return new MethodArgumentBuilder(handlerMethods.get(0).getMethod());
+ HandlerMethod handlerMethod = handlerMethods.get(0);
+ Class<?> controllerType = handlerMethod.getBeanType();
+ Method method = handlerMethod.getMethod();
+ return new MethodArgumentBuilder(builder, controllerType, method);
}
/**
@@ -276,34 +347,126 @@ public class MvcUriComponentsBuilder extends UriComponentsBuilder {
* and an array of method argument values. The array of values must match the
* signature of the controller method. Values for {@code @RequestParam} and
* {@code @PathVariable} are used for building the URI (via implementations of
- * {@link org.springframework.web.method.support.UriComponentsContributor})
- * while remaining argument values are ignored and can be {@code null}.
+ * {@link org.springframework.web.method.support.UriComponentsContributor
+ * UriComponentsContributor}) while remaining argument values are ignored and
+ * can be {@code null}.
+ * @param controllerType the controller type
* @param method the controller method
- * @param argumentValues argument values for the controller method
+ * @param args argument values for the controller method
* @return a UriComponentsBuilder instance, never {@code null}
+ * @since 4.2
+ */
+ public static UriComponentsBuilder fromMethod(Class<?> controllerType, Method method, Object... args) {
+ return fromMethodInternal(null, controllerType, method, args);
+ }
+
+ /**
+ * An alternative to {@link #fromMethod(java.lang.reflect.Method, Object...)}
+ * that accepts a {@code UriComponentsBuilder} representing the base URL.
+ * This is useful when using MvcUriComponentsBuilder outside the context of
+ * processing a request or to apply a custom baseUrl not matching the
+ * current request.
+ * @param baseUrl the builder for the base URL; the builder will be cloned
+ * and therefore not modified and may be re-used for further calls.
+ * @param controllerType the controller type
+ * @param method the controller method
+ * @param args argument values for the controller method
+ * @return a UriComponentsBuilder instance (never {@code null})
+ * @since 4.2
+ */
+ public static UriComponentsBuilder fromMethod(UriComponentsBuilder baseUrl,
+ Class<?> controllerType, Method method, Object... args) {
+
+ return fromMethodInternal(baseUrl,
+ (controllerType != null ? controllerType : method.getDeclaringClass()), method, args);
+ }
+
+ /**
+ * @see #fromMethod(Class, Method, Object...)
+ * @see #fromMethod(UriComponentsBuilder, Class, Method, Object...)
+ * @deprecated as of 4.2, this is deprecated in favor of the overloaded
+ * method that also accepts a controllerType argument
*/
- public static UriComponentsBuilder fromMethod(Method method, Object... argumentValues) {
- String typePath = getTypeRequestMapping(method.getDeclaringClass());
+ @Deprecated
+ public static UriComponentsBuilder fromMethod(Method method, Object... args) {
+ return fromMethodInternal(null, method.getDeclaringClass(), method, args);
+ }
+
+ private static UriComponentsBuilder fromMethodInternal(UriComponentsBuilder baseUrl,
+ Class<?> controllerType, Method method, Object... args) {
+
+ baseUrl = getBaseUrlToUse(baseUrl);
+ String typePath = getTypeRequestMapping(controllerType);
String methodPath = getMethodRequestMapping(method);
String path = pathMatcher.combine(typePath, methodPath);
+ baseUrl.path(path);
+ UriComponents uriComponents = applyContributors(baseUrl, method, args);
+ return UriComponentsBuilder.newInstance().uriComponents(uriComponents);
+ }
- UriComponentsBuilder builder = ServletUriComponentsBuilder.fromCurrentServletMapping().path(path);
- UriComponents uriComponents = applyContributors(builder, method, argumentValues);
- return ServletUriComponentsBuilder.newInstance().uriComponents(uriComponents);
+ private static UriComponentsBuilder getBaseUrlToUse(UriComponentsBuilder baseUrl) {
+ if (baseUrl != null) {
+ return (UriComponentsBuilder) baseUrl.clone();
+ }
+ else {
+ return ServletUriComponentsBuilder.fromCurrentServletMapping();
+ }
+ }
+
+ private static String getTypeRequestMapping(Class<?> controllerType) {
+ Assert.notNull(controllerType, "'controllerType' must not be null");
+ RequestMapping requestMapping = AnnotatedElementUtils.findMergedAnnotation(controllerType, RequestMapping.class);
+ if (requestMapping == null) {
+ return "/";
+ }
+ String[] paths = requestMapping.path();
+ if (ObjectUtils.isEmpty(paths) || StringUtils.isEmpty(paths[0])) {
+ return "/";
+ }
+ if (paths.length > 1 && logger.isWarnEnabled()) {
+ logger.warn("Multiple paths on controller " + controllerType.getName() + ", using first one");
+ }
+ return paths[0];
}
private static String getMethodRequestMapping(Method method) {
- RequestMapping annot = AnnotationUtils.findAnnotation(method, RequestMapping.class);
- if (annot == null) {
+ Assert.notNull(method, "'method' must not be null");
+ RequestMapping requestMapping = AnnotatedElementUtils.findMergedAnnotation(method, RequestMapping.class);
+ if (requestMapping == null) {
throw new IllegalArgumentException("No @RequestMapping on: " + method.toGenericString());
}
- if (ObjectUtils.isEmpty(annot.value()) || StringUtils.isEmpty(annot.value()[0])) {
+ String[] paths = requestMapping.path();
+ if (ObjectUtils.isEmpty(paths) || StringUtils.isEmpty(paths[0])) {
return "/";
}
- if (annot.value().length > 1 && logger.isWarnEnabled()) {
+ if (paths.length > 1 && logger.isWarnEnabled()) {
logger.warn("Multiple paths on method " + method.toGenericString() + ", using first one");
}
- return annot.value()[0];
+ return paths[0];
+ }
+
+ private static Method getMethod(Class<?> controllerType, final String methodName, final Object... args) {
+ MethodFilter selector = new MethodFilter() {
+ @Override
+ public boolean matches(Method method) {
+ String name = method.getName();
+ int argLength = method.getParameterTypes().length;
+ return (name.equals(methodName) && argLength == args.length);
+ }
+ };
+ Set<Method> methods = MethodIntrospector.selectMethods(controllerType, selector);
+ if (methods.size() == 1) {
+ return methods.iterator().next();
+ }
+ else if (methods.size() > 1) {
+ throw new IllegalArgumentException(String.format(
+ "Found two methods named '%s' accepting arguments %s in controller %s: [%s]",
+ methodName, Arrays.asList(args), controllerType.getName(), methods));
+ }
+ else {
+ throw new IllegalArgumentException("No method named '" + methodName + "' with " + args.length +
+ " arguments found in controller " + controllerType.getName());
+ }
}
private static UriComponents applyContributors(UriComponentsBuilder builder, Method method, Object... args) {
@@ -322,7 +485,7 @@ public class MvcUriComponentsBuilder extends UriComponentsBuilder {
final Map<String, Object> uriVars = new HashMap<String, Object>();
for (int i = 0; i < paramCount; i++) {
- MethodParameter param = new MethodParameter(method, i);
+ MethodParameter param = new SynthesizingMethodParameter(method, i);
param.initParameterNameDiscovery(parameterNameDiscoverer);
contributor.contributeMethodArgument(param, args[i], builder, uriVars);
}
@@ -336,7 +499,7 @@ public class MvcUriComponentsBuilder extends UriComponentsBuilder {
});
}
- protected static CompositeUriComponentsContributor getConfiguredUriComponentsContributor() {
+ private static CompositeUriComponentsContributor getConfiguredUriComponentsContributor() {
WebApplicationContext wac = getWebApplicationContext();
if (wac == null) {
return null;
@@ -353,7 +516,7 @@ public class MvcUriComponentsBuilder extends UriComponentsBuilder {
}
}
- protected static RequestMappingInfoHandlerMapping getRequestMappingInfoHandlerMapping() {
+ private static RequestMappingInfoHandlerMapping getRequestMappingInfoHandlerMapping() {
WebApplicationContext wac = getWebApplicationContext();
Assert.notNull(wac, "Cannot lookup handler method mappings without WebApplicationContext");
try {
@@ -427,7 +590,7 @@ public class MvcUriComponentsBuilder extends UriComponentsBuilder {
*/
public static <T> T controller(Class<T> controllerType) {
Assert.notNull(controllerType, "'controllerType' must not be null");
- return initProxy(controllerType, new ControllerMethodInvocationInterceptor());
+ return initProxy(controllerType, new ControllerMethodInvocationInterceptor(controllerType));
}
@SuppressWarnings("unchecked")
@@ -439,21 +602,85 @@ public class MvcUriComponentsBuilder extends UriComponentsBuilder {
factory.addAdvice(interceptor);
return (T) factory.getProxy();
}
+
else {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(type);
enhancer.setInterfaces(new Class<?>[] {MethodInvocationInfo.class});
enhancer.setNamingPolicy(SpringNamingPolicy.INSTANCE);
enhancer.setCallbackType(org.springframework.cglib.proxy.MethodInterceptor.class);
- Factory factory = (Factory) objenesis.newInstance(enhancer.createClass());
- factory.setCallbacks(new Callback[] {interceptor});
- return (T) factory;
+
+ Class<?> proxyClass = enhancer.createClass();
+ Object proxy = null;
+
+ if (objenesis.isWorthTrying()) {
+ try {
+ proxy = objenesis.newInstance(proxyClass, enhancer.getUseCache());
+ }
+ catch (ObjenesisException ex) {
+ logger.debug("Unable to instantiate controller proxy using Objenesis, " +
+ "falling back to regular construction", ex);
+ }
+ }
+
+ if (proxy == null) {
+ try {
+ proxy = proxyClass.newInstance();
+ }
+ catch (Exception ex) {
+ throw new IllegalStateException("Unable to instantiate controller proxy using Objenesis, " +
+ "and regular controller instantiation via default constructor fails as well", ex);
+ }
+ }
+
+ ((Factory) proxy).setCallbacks(new Callback[] {interceptor});
+ return (T) proxy;
}
}
- @Override
- protected Object clone() {
- return new MvcUriComponentsBuilder(this);
+ /**
+ * An alternative to {@link #fromController(Class)} for use with an instance
+ * of this class created via a call to {@link #relativeTo}.
+ * @since 4.2
+ */
+ public UriComponentsBuilder withController(Class<?> controllerType) {
+ return fromController(this.baseUrl, controllerType);
+ }
+
+ /**
+ * An alternative to {@link #fromMethodName(Class, String, Object...)}} for
+ * use with an instance of this class created via {@link #relativeTo}.
+ * @since 4.2
+ */
+ public UriComponentsBuilder withMethodName(Class<?> controllerType, String methodName, Object... args) {
+ return fromMethodName(this.baseUrl, controllerType, methodName, args);
+ }
+
+ /**
+ * An alternative to {@link #fromMethodCall(Object)} for use with an instance
+ * of this class created via {@link #relativeTo}.
+ * @since 4.2
+ */
+ public UriComponentsBuilder withMethodCall(Object invocationInfo) {
+ return fromMethodCall(this.baseUrl, invocationInfo);
+ }
+
+ /**
+ * An alternative to {@link #fromMappingName(String)} for use with an instance
+ * of this class created via {@link #relativeTo}.
+ * @since 4.2
+ */
+ public MethodArgumentBuilder withMappingName(String mappingName) {
+ return fromMappingName(this.baseUrl, mappingName);
+ }
+
+ /**
+ * An alternative to {@link #fromMethod(Class, Method, Object...)}
+ * for use with an instance of this class created via {@link #relativeTo}.
+ * @since 4.2
+ */
+ public UriComponentsBuilder withMethod(Class<?> controllerType, Method method, Object... args) {
+ return fromMethod(this.baseUrl, controllerType, method, args);
}
@@ -466,10 +693,18 @@ public class MvcUriComponentsBuilder extends UriComponentsBuilder {
private static final Method getArgumentValues =
ReflectionUtils.findMethod(MethodInvocationInfo.class, "getArgumentValues");
+ private static final Method getControllerType =
+ ReflectionUtils.findMethod(MethodInvocationInfo.class, "getControllerType");
+
private Method controllerMethod;
private Object[] argumentValues;
+ private Class<?> controllerType;
+
+ ControllerMethodInvocationInterceptor(Class<?> controllerType) {
+ this.controllerType = controllerType;
+ }
@Override
public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) {
@@ -479,6 +714,9 @@ public class MvcUriComponentsBuilder extends UriComponentsBuilder {
else if (getArgumentValues.equals(method)) {
return this.argumentValues;
}
+ else if (getControllerType.equals(method)) {
+ return this.controllerType;
+ }
else if (ReflectionUtils.isObjectMethod(method)) {
return ReflectionUtils.invokeMethod(method, obj, args);
}
@@ -486,7 +724,7 @@ public class MvcUriComponentsBuilder extends UriComponentsBuilder {
this.controllerMethod = method;
this.argumentValues = args;
Class<?> returnType = method.getReturnType();
- return (void.class.equals(returnType) ? null : returnType.cast(initProxy(returnType, this)));
+ return (void.class == returnType ? null : returnType.cast(initProxy(returnType, this)));
}
}
@@ -502,18 +740,36 @@ public class MvcUriComponentsBuilder extends UriComponentsBuilder {
Method getControllerMethod();
Object[] getArgumentValues();
+
+ Class<?> getControllerType();
}
public static class MethodArgumentBuilder {
+ private final Class<?> controllerType;
+
private final Method method;
private final Object[] argumentValues;
+ private final UriComponentsBuilder baseUrl;
- public MethodArgumentBuilder(Method method) {
+ /**
+ * @since 4.2
+ */
+ public MethodArgumentBuilder(Class<?> controllerType, Method method) {
+ this(null, controllerType, method);
+ }
+
+ /**
+ * @since 4.2
+ */
+ public MethodArgumentBuilder(UriComponentsBuilder baseUrl, Class<?> controllerType, Method method) {
+ Assert.notNull(controllerType, "'controllerType' is required");
Assert.notNull(method, "'method' is required");
+ this.baseUrl = (baseUrl != null ? baseUrl : initBaseUrl());
+ this.controllerType = controllerType;
this.method = method;
this.argumentValues = new Object[method.getParameterTypes().length];
for (int i = 0; i < this.argumentValues.length; i++) {
@@ -521,19 +777,34 @@ public class MvcUriComponentsBuilder extends UriComponentsBuilder {
}
}
+ /**
+ * @see #MethodArgumentBuilder(Class, Method)
+ * @deprecated as of 4.2, this is deprecated in favor of alternative constructors
+ * that accept a controllerType argument
+ */
+ @Deprecated
+ public MethodArgumentBuilder(Method method) {
+ this(method.getDeclaringClass(), method);
+ }
+
+ private static UriComponentsBuilder initBaseUrl() {
+ UriComponentsBuilder builder = ServletUriComponentsBuilder.fromCurrentServletMapping();
+ return UriComponentsBuilder.fromPath(builder.build().getPath());
+ }
+
public MethodArgumentBuilder arg(int index, Object value) {
this.argumentValues[index] = value;
return this;
}
public String build() {
- return MvcUriComponentsBuilder.fromMethod(this.method, this.argumentValues)
- .build(false).encode().toUriString();
+ return fromMethodInternal(this.baseUrl, this.controllerType, this.method,
+ this.argumentValues).build(false).encode().toUriString();
}
- public String buildAndExpand(Object... uriVariables) {
- return MvcUriComponentsBuilder.fromMethod(this.method, this.argumentValues)
- .build(false).expand(uriVariables).encode().toString();
+ public String buildAndExpand(Object... uriVars) {
+ return fromMethodInternal(this.baseUrl, this.controllerType, this.method,
+ this.argumentValues).build(false).expand(uriVars).encode().toString();
}
}
diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/PathVariableMethodArgumentResolver.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/PathVariableMethodArgumentResolver.java
index 3aa5143c..0475e03b 100644
--- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/PathVariableMethodArgumentResolver.java
+++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/PathVariableMethodArgumentResolver.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2014 the original author or authors.
+ * Copyright 2002-2016 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.
@@ -25,6 +25,7 @@ import org.springframework.core.convert.ConversionService;
import org.springframework.core.convert.TypeDescriptor;
import org.springframework.core.convert.converter.Converter;
import org.springframework.util.StringUtils;
+import org.springframework.web.bind.MissingPathVariableException;
import org.springframework.web.bind.ServletRequestBindingException;
import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.annotation.PathVariable;
@@ -32,7 +33,6 @@ import org.springframework.web.bind.annotation.ValueConstants;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.method.annotation.AbstractNamedValueMethodArgumentResolver;
-import org.springframework.web.method.annotation.RequestParamMapMethodArgumentResolver;
import org.springframework.web.method.support.ModelAndViewContainer;
import org.springframework.web.method.support.UriComponentsContributor;
import org.springframework.web.servlet.HandlerMapping;
@@ -42,22 +42,18 @@ import org.springframework.web.util.UriComponentsBuilder;
/**
* Resolves method arguments annotated with an @{@link PathVariable}.
*
- * <p>An @{@link PathVariable} is a named value that gets resolved from a URI
- * template variable. It is always required and does not have a default value
- * to fall back on. See the base class
+ * <p>An @{@link PathVariable} is a named value that gets resolved from a URI template variable.
+ * It is always required and does not have a default value to fall back on. See the base class
* {@link org.springframework.web.method.annotation.AbstractNamedValueMethodArgumentResolver}
* for more information on how named values are processed.
*
- * <p>If the method parameter type is {@link Map}, the name specified in the
- * annotation is used to resolve the URI variable String value. The value is
- * then converted to a {@link Map} via type conversion assuming a suitable
- * {@link Converter} or {@link PropertyEditor} has been registered.
- * Or if the annotation does not specify name the
- * {@link RequestParamMapMethodArgumentResolver} is used instead to provide
- * access to all URI variables in a map.
+ * <p>If the method parameter type is {@link Map}, the name specified in the annotation is used
+ * to resolve the URI variable String value. The value is then converted to a {@link Map} via
+ * type conversion, assuming a suitable {@link Converter} or {@link PropertyEditor} has been
+ * registered.
*
- * <p>A {@link WebDataBinder} is invoked to apply type conversion to resolved
- * path variable values that don't yet match the method parameter type.
+ * <p>A {@link WebDataBinder} is invoked to apply type conversion to resolved path variable
+ * values that don't yet match the method parameter type.
*
* @author Rossen Stoyanchev
* @author Arjen Poutsma
@@ -94,16 +90,16 @@ public class PathVariableMethodArgumentResolver extends AbstractNamedValueMethod
@Override
@SuppressWarnings("unchecked")
protected Object resolveName(String name, MethodParameter parameter, NativeWebRequest request) throws Exception {
- Map<String, String> uriTemplateVars =
- (Map<String, String>) request.getAttribute(
- HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE, RequestAttributes.SCOPE_REQUEST);
- return (uriTemplateVars != null) ? uriTemplateVars.get(name) : null;
+ Map<String, String> uriTemplateVars = (Map<String, String>) request.getAttribute(
+ HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE, RequestAttributes.SCOPE_REQUEST);
+ return (uriTemplateVars != null ? uriTemplateVars.get(name) : null);
}
@Override
- protected void handleMissingValue(String name, MethodParameter parameter) throws ServletRequestBindingException {
- throw new ServletRequestBindingException("Missing URI template variable '" + name +
- "' for method parameter of type " + parameter.getParameterType().getSimpleName());
+ protected void handleMissingValue(String name, MethodParameter parameter)
+ throws ServletRequestBindingException {
+
+ throw new MissingPathVariableException(name, parameter);
}
@Override
diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestBodyAdvice.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestBodyAdvice.java
new file mode 100644
index 00000000..ee0be650
--- /dev/null
+++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestBodyAdvice.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright 2002-2015 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.mvc.method.annotation;
+
+import java.io.IOException;
+import java.lang.reflect.Type;
+
+import org.springframework.core.MethodParameter;
+import org.springframework.http.HttpInputMessage;
+import org.springframework.http.converter.HttpMessageConverter;
+
+/**
+ * Allows customizing the request before its body is read and converted into an
+ * Object and also allows for processing of the resulting Object before it is
+ * passed into a controller method as an {@code @RequestBody} or an
+ * {@code HttpEntity} method argument.
+ *
+ * <p>Implementations of this contract may be registered directly with the
+ * {@code RequestMappingHandlerAdapter} or more likely annotated with
+ * {@code @ControllerAdvice} in which case they are auto-detected.
+ *
+ * @author Rossen Stoyanchev
+ * @since 4.2
+ */
+public interface RequestBodyAdvice {
+
+ /**
+ * Invoked first to determine if this interceptor applies.
+ * @param methodParameter the method parameter
+ * @param targetType the target type, not necessarily the same as the method
+ * parameter type, e.g. for {@code HttpEntity<String>}.
+ * @param converterType the selected converter type
+ * @return whether this interceptor should be invoked or not
+ */
+ boolean supports(MethodParameter methodParameter, Type targetType,
+ Class<? extends HttpMessageConverter<?>> converterType);
+
+ /**
+ * Invoked second (and last) if the body is empty.
+ * @param body set to {@code null} before the first advice is called
+ * @param inputMessage the request
+ * @param parameter the method parameter
+ * @param targetType the target type, not necessarily the same as the method
+ * parameter type, e.g. for {@code HttpEntity<String>}.
+ * @param converterType the selected converter type
+ * @return the value to use or {@code null} which may then raise an
+ * {@code HttpMessageNotReadableException} if the argument is required.
+ */
+ Object handleEmptyBody(Object body, HttpInputMessage inputMessage, MethodParameter parameter,
+ Type targetType, Class<? extends HttpMessageConverter<?>> converterType);
+
+ /**
+ * Invoked second before the request body is read and converted.
+ * @param inputMessage the request
+ * @param parameter the target method parameter
+ * @param targetType the target type, not necessarily the same as the method
+ * parameter type, e.g. for {@code HttpEntity<String>}.
+ * @param converterType the converter used to deserialize the body
+ * @return the input request or a new instance, never {@code null}
+ */
+ HttpInputMessage beforeBodyRead(HttpInputMessage inputMessage, MethodParameter parameter,
+ Type targetType, Class<? extends HttpMessageConverter<?>> converterType) throws IOException;
+
+ /**
+ * Invoked third (and last) after the request body is converted to an Object.
+ * @param body set to the converter Object before the 1st advice is called
+ * @param inputMessage the request
+ * @param parameter the target method parameter
+ * @param targetType the target type, not necessarily the same as the method
+ * parameter type, e.g. for {@code HttpEntity<String>}.
+ * @param converterType the converter used to deserialize the body
+ * @return the same body or a new instance
+ */
+ Object afterBodyRead(Object body, HttpInputMessage inputMessage, MethodParameter parameter,
+ Type targetType, Class<? extends HttpMessageConverter<?>> converterType);
+
+}
diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestBodyAdviceAdapter.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestBodyAdviceAdapter.java
new file mode 100644
index 00000000..ee110ee9
--- /dev/null
+++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestBodyAdviceAdapter.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright 2002-2015 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.mvc.method.annotation;
+
+import java.io.IOException;
+import java.lang.reflect.Type;
+
+import org.springframework.core.MethodParameter;
+import org.springframework.http.HttpInputMessage;
+import org.springframework.http.converter.HttpMessageConverter;
+
+/**
+ * A convenient starting point for implementing
+ * {@link org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice
+ * ResponseBodyAdvice} with default method implementations.
+ *
+ * <p>Sub-classes are required to implement {@link #supports} to return true
+ * depending on when the advice applies.
+ *
+ * @author Rossen Stoyanchev
+ * @since 4.2
+ */
+public abstract class RequestBodyAdviceAdapter implements RequestBodyAdvice {
+
+ /**
+ * The default implementation returns the body that was passed in.
+ */
+ @Override
+ public Object handleEmptyBody(Object body, HttpInputMessage inputMessage,
+ MethodParameter parameter, Type targetType,
+ Class<? extends HttpMessageConverter<?>> converterType) {
+
+ return body;
+ }
+
+ /**
+ * The default implementation returns the InputMessage that was passed in.
+ */
+ @Override
+ public HttpInputMessage beforeBodyRead(HttpInputMessage inputMessage, MethodParameter parameter,
+ Type targetType, Class<? extends HttpMessageConverter<?>> converterType)
+ throws IOException {
+
+ return inputMessage;
+ }
+
+ /**
+ * The default implementation returns the body that was passed in.
+ */
+ @Override
+ public Object afterBodyRead(Object body, HttpInputMessage inputMessage, MethodParameter parameter,
+ Type targetType, Class<? extends HttpMessageConverter<?>> converterType) {
+
+ return body;
+ }
+
+}
diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerAdapter.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerAdapter.java
index 2c87b61d..ce031bd5 100644
--- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerAdapter.java
+++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerAdapter.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2015 the original author or authors.
+ * Copyright 2002-2016 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,8 +35,9 @@ import org.springframework.beans.factory.BeanFactoryAware;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
import org.springframework.core.DefaultParameterNameDiscoverer;
-import org.springframework.core.OrderComparator;
+import org.springframework.core.MethodIntrospector;
import org.springframework.core.ParameterNameDiscoverer;
+import org.springframework.core.annotation.AnnotationAwareOrderComparator;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.core.task.AsyncTaskExecutor;
import org.springframework.core.task.SimpleAsyncTaskExecutor;
@@ -47,6 +48,7 @@ import org.springframework.http.converter.support.AllEncompassingFormHttpMessage
import org.springframework.http.converter.xml.SourceHttpMessageConverter;
import org.springframework.ui.ModelMap;
import org.springframework.util.Assert;
+import org.springframework.util.ClassUtils;
import org.springframework.util.CollectionUtils;
import org.springframework.util.ReflectionUtils.MethodFilter;
import org.springframework.web.accept.ContentNegotiationManager;
@@ -69,7 +71,6 @@ import org.springframework.web.context.request.async.WebAsyncTask;
import org.springframework.web.context.request.async.WebAsyncUtils;
import org.springframework.web.method.ControllerAdviceBean;
import org.springframework.web.method.HandlerMethod;
-import org.springframework.web.method.HandlerMethodSelector;
import org.springframework.web.method.annotation.ErrorsMethodArgumentResolver;
import org.springframework.web.method.annotation.ExpressionValueMethodArgumentResolver;
import org.springframework.web.method.annotation.InitBinderDataBinderFactory;
@@ -108,6 +109,7 @@ import org.springframework.web.util.WebUtils;
* use {@link #setArgumentResolvers} and {@link #setReturnValueHandlers}.
*
* @author Rossen Stoyanchev
+ * @author Juergen Hoeller
* @since 3.1
* @see HandlerMethodArgumentResolver
* @see HandlerMethodReturnValueHandler
@@ -115,6 +117,10 @@ import org.springframework.web.util.WebUtils;
public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter
implements BeanFactoryAware, InitializingBean {
+ private static final boolean completionStagePresent = ClassUtils.isPresent(
+ "java.util.concurrent.CompletionStage", RequestMappingHandlerAdapter.class.getClassLoader());
+
+
private List<HandlerMethodArgumentResolver> customArgumentResolvers;
private HandlerMethodArgumentResolverComposite argumentResolvers;
@@ -131,7 +137,7 @@ public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter
private List<HttpMessageConverter<?>> messageConverters;
- private List<Object> responseBodyAdvice = new ArrayList<Object>();
+ private List<Object> requestResponseBodyAdvice = new ArrayList<Object>();
private WebBindingInitializer webBindingInitializer;
@@ -329,15 +335,24 @@ public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter
}
/**
- * Add one or more components to modify the response after the execution of a
- * controller method annotated with {@code @ResponseBody}, or a method returning
- * {@code ResponseEntity} and before the body is written to the response with
- * the selected {@code HttpMessageConverter}.
+ * Add one or more {@code RequestBodyAdvice} instances to intercept the
+ * request before it is read and converted for {@code @RequestBody} and
+ * {@code HttpEntity} method arguments.
+ */
+ public void setRequestBodyAdvice(List<RequestBodyAdvice> requestBodyAdvice) {
+ if (requestBodyAdvice != null) {
+ this.requestResponseBodyAdvice.addAll(requestBodyAdvice);
+ }
+ }
+
+ /**
+ * Add one or more {@code ResponseBodyAdvice} instances to intercept the
+ * response before {@code @ResponseBody} or {@code ResponseEntity} return
+ * values are written to the response body.
*/
public void setResponseBodyAdvice(List<ResponseBodyAdvice<?>> responseBodyAdvice) {
- this.responseBodyAdvice.clear();
if (responseBodyAdvice != null) {
- this.responseBodyAdvice.addAll(responseBodyAdvice);
+ this.requestResponseBodyAdvice.addAll(responseBodyAdvice);
}
}
@@ -429,7 +444,14 @@ public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter
/**
* Cache content produced by {@code @SessionAttributes} annotated handlers
- * for the given number of seconds. Default is 0, preventing caching completely.
+ * for the given number of seconds.
+ * <p>Possible values are:
+ * <ul>
+ * <li>-1: no generation of cache-related headers</li>
+ * <li>0 (default value): "Cache-Control: no-store" will prevent caching</li>
+ * <li>1 or higher: "Cache-Control: max-age=seconds" will ask to cache content;
+ * not advised when dealing with session attributes</li>
+ * </ul>
* <p>In contrast to the "cacheSeconds" property which will apply to all general
* handlers (but not to {@code @SessionAttributes} annotated handlers),
* this setting will apply to {@code @SessionAttributes} handlers only.
@@ -518,29 +540,41 @@ public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter
}
List<ControllerAdviceBean> beans = ControllerAdviceBean.findAnnotatedBeans(getApplicationContext());
- OrderComparator.sort(beans);
+ AnnotationAwareOrderComparator.sort(beans);
- List<Object> responseBodyAdviceBeans = new ArrayList<Object>();
+ List<Object> requestResponseBodyAdviceBeans = new ArrayList<Object>();
for (ControllerAdviceBean bean : beans) {
- Set<Method> attrMethods = HandlerMethodSelector.selectMethods(bean.getBeanType(), MODEL_ATTRIBUTE_METHODS);
+ Set<Method> attrMethods = MethodIntrospector.selectMethods(bean.getBeanType(), MODEL_ATTRIBUTE_METHODS);
if (!attrMethods.isEmpty()) {
this.modelAttributeAdviceCache.put(bean, attrMethods);
- logger.info("Detected @ModelAttribute methods in " + bean);
+ if (logger.isInfoEnabled()) {
+ logger.info("Detected @ModelAttribute methods in " + bean);
+ }
}
- Set<Method> binderMethods = HandlerMethodSelector.selectMethods(bean.getBeanType(), INIT_BINDER_METHODS);
+ Set<Method> binderMethods = MethodIntrospector.selectMethods(bean.getBeanType(), INIT_BINDER_METHODS);
if (!binderMethods.isEmpty()) {
this.initBinderAdviceCache.put(bean, binderMethods);
- logger.info("Detected @InitBinder methods in " + bean);
+ if (logger.isInfoEnabled()) {
+ logger.info("Detected @InitBinder methods in " + bean);
+ }
+ }
+ if (RequestBodyAdvice.class.isAssignableFrom(bean.getBeanType())) {
+ requestResponseBodyAdviceBeans.add(bean);
+ if (logger.isInfoEnabled()) {
+ logger.info("Detected RequestBodyAdvice bean in " + bean);
+ }
}
if (ResponseBodyAdvice.class.isAssignableFrom(bean.getBeanType())) {
- responseBodyAdviceBeans.add(bean);
- logger.info("Detected ResponseBodyAdvice bean in " + bean);
+ requestResponseBodyAdviceBeans.add(bean);
+ if (logger.isInfoEnabled()) {
+ logger.info("Detected ResponseBodyAdvice bean in " + bean);
+ }
}
}
- if (!responseBodyAdviceBeans.isEmpty()) {
- this.responseBodyAdvice.addAll(0, responseBodyAdviceBeans);
+ if (!requestResponseBodyAdviceBeans.isEmpty()) {
+ this.requestResponseBodyAdvice.addAll(0, requestResponseBodyAdviceBeans);
}
}
@@ -559,8 +593,8 @@ public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter
resolvers.add(new MatrixVariableMethodArgumentResolver());
resolvers.add(new MatrixVariableMapMethodArgumentResolver());
resolvers.add(new ServletModelAttributeMethodProcessor(false));
- resolvers.add(new RequestResponseBodyMethodProcessor(getMessageConverters()));
- resolvers.add(new RequestPartMethodArgumentResolver(getMessageConverters()));
+ resolvers.add(new RequestResponseBodyMethodProcessor(getMessageConverters(), this.requestResponseBodyAdvice));
+ resolvers.add(new RequestPartMethodArgumentResolver(getMessageConverters(), this.requestResponseBodyAdvice));
resolvers.add(new RequestHeaderMethodArgumentResolver(getBeanFactory()));
resolvers.add(new RequestHeaderMapMethodArgumentResolver());
resolvers.add(new ServletCookieValueMethodArgumentResolver(getBeanFactory()));
@@ -569,7 +603,7 @@ public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter
// Type-based argument resolution
resolvers.add(new ServletRequestMethodArgumentResolver());
resolvers.add(new ServletResponseMethodArgumentResolver());
- resolvers.add(new HttpEntityMethodProcessor(getMessageConverters()));
+ resolvers.add(new HttpEntityMethodProcessor(getMessageConverters(), this.requestResponseBodyAdvice));
resolvers.add(new RedirectAttributesMethodArgumentResolver());
resolvers.add(new ModelMethodProcessor());
resolvers.add(new MapMethodProcessor());
@@ -631,18 +665,23 @@ public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter
handlers.add(new ModelAndViewMethodReturnValueHandler());
handlers.add(new ModelMethodProcessor());
handlers.add(new ViewMethodReturnValueHandler());
- handlers.add(new HttpEntityMethodProcessor(
- getMessageConverters(), this.contentNegotiationManager, this.responseBodyAdvice));
+ handlers.add(new ResponseBodyEmitterReturnValueHandler(getMessageConverters()));
+ handlers.add(new StreamingResponseBodyReturnValueHandler());
+ handlers.add(new HttpEntityMethodProcessor(getMessageConverters(),
+ this.contentNegotiationManager, this.requestResponseBodyAdvice));
handlers.add(new HttpHeadersReturnValueHandler());
handlers.add(new CallableMethodReturnValueHandler());
handlers.add(new DeferredResultMethodReturnValueHandler());
handlers.add(new AsyncTaskMethodReturnValueHandler(this.beanFactory));
handlers.add(new ListenableFutureReturnValueHandler());
+ if (completionStagePresent) {
+ handlers.add(new CompletionStageReturnValueHandler());
+ }
// Annotation-based return value types
handlers.add(new ModelAttributeMethodProcessor(false));
- handlers.add(new RequestResponseBodyMethodProcessor(
- getMessageConverters(), this.contentNegotiationManager, this.responseBodyAdvice));
+ handlers.add(new RequestResponseBodyMethodProcessor(getMessageConverters(),
+ this.contentNegotiationManager, this.requestResponseBodyAdvice));
// Multi-purpose return value types
handlers.add(new ViewNameMethodReturnValueHandler());
@@ -682,14 +721,8 @@ public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter
protected ModelAndView handleInternal(HttpServletRequest request,
HttpServletResponse response, HandlerMethod handlerMethod) throws Exception {
- if (getSessionAttributesHandler(handlerMethod).hasSessionAttributes()) {
- // Always prevent caching in case of session attribute management.
- checkAndPrepare(request, response, this.cacheSecondsForSessionAttributeHandlers, true);
- }
- else {
- // Uses configured default cacheSeconds setting.
- checkAndPrepare(request, response, true);
- }
+ ModelAndView mav;
+ checkRequest(request);
// Execute invokeHandlerMethod in synchronized block if required.
if (this.synchronizeOnSession) {
@@ -697,12 +730,29 @@ public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter
if (session != null) {
Object mutex = WebUtils.getSessionMutex(session);
synchronized (mutex) {
- return invokeHandlerMethod(request, response, handlerMethod);
+ mav = invokeHandlerMethod(request, response, handlerMethod);
}
}
+ else {
+ // No HttpSession available -> no mutex necessary
+ mav = invokeHandlerMethod(request, response, handlerMethod);
+ }
+ }
+ else {
+ // No synchronization on session demanded at all...
+ mav = invokeHandlerMethod(request, response, handlerMethod);
+ }
+
+ if (!response.containsHeader(HEADER_CACHE_CONTROL)) {
+ if (getSessionAttributesHandler(handlerMethod).hasSessionAttributes()) {
+ applyCacheSeconds(response, this.cacheSecondsForSessionAttributeHandlers);
+ }
+ else {
+ prepareResponse(response);
+ }
}
- return invokeHandlerMethod(request, response, handlerMethod);
+ return mav;
}
/**
@@ -738,19 +788,26 @@ public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter
/**
* Invoke the {@link RequestMapping} handler method preparing a {@link ModelAndView}
* if view resolution is required.
+ * @since 4.2
+ * @see #createInvocableHandlerMethod(HandlerMethod)
*/
- private ModelAndView invokeHandlerMethod(HttpServletRequest request, HttpServletResponse response,
- HandlerMethod handlerMethod) throws Exception {
+ protected ModelAndView invokeHandlerMethod(HttpServletRequest request,
+ HttpServletResponse response, HandlerMethod handlerMethod) throws Exception {
ServletWebRequest webRequest = new ServletWebRequest(request, response);
WebDataBinderFactory binderFactory = getDataBinderFactory(handlerMethod);
ModelFactory modelFactory = getModelFactory(handlerMethod, binderFactory);
- ServletInvocableHandlerMethod requestMappingMethod = createRequestMappingMethod(handlerMethod, binderFactory);
+
+ ServletInvocableHandlerMethod invocableMethod = createInvocableHandlerMethod(handlerMethod);
+ invocableMethod.setHandlerMethodArgumentResolvers(this.argumentResolvers);
+ invocableMethod.setHandlerMethodReturnValueHandlers(this.returnValueHandlers);
+ invocableMethod.setDataBinderFactory(binderFactory);
+ invocableMethod.setParameterNameDiscoverer(this.parameterNameDiscoverer);
ModelAndViewContainer mavContainer = new ModelAndViewContainer();
mavContainer.addAllAttributes(RequestContextUtils.getInputFlashMap(request));
- modelFactory.initModel(webRequest, mavContainer, requestMappingMethod);
+ modelFactory.initModel(webRequest, mavContainer, invocableMethod);
mavContainer.setIgnoreDefaultModelOnRedirect(this.ignoreDefaultModelOnRedirect);
AsyncWebRequest asyncWebRequest = WebAsyncUtils.createAsyncWebRequest(request, response);
@@ -769,10 +826,10 @@ public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter
if (logger.isDebugEnabled()) {
logger.debug("Found concurrent result value [" + result + "]");
}
- requestMappingMethod = requestMappingMethod.wrapConcurrentResult(result);
+ invocableMethod = invocableMethod.wrapConcurrentResult(result);
}
- requestMappingMethod.invokeAndHandle(webRequest, mavContainer);
+ invocableMethod.invokeAndHandle(webRequest, mavContainer);
if (asyncManager.isConcurrentHandlingStarted()) {
return null;
}
@@ -780,16 +837,14 @@ public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter
return getModelAndView(mavContainer, modelFactory, webRequest);
}
- private ServletInvocableHandlerMethod createRequestMappingMethod(
- HandlerMethod handlerMethod, WebDataBinderFactory binderFactory) {
-
- ServletInvocableHandlerMethod requestMethod;
- requestMethod = new ServletInvocableHandlerMethod(handlerMethod);
- requestMethod.setHandlerMethodArgumentResolvers(this.argumentResolvers);
- requestMethod.setHandlerMethodReturnValueHandlers(this.returnValueHandlers);
- requestMethod.setDataBinderFactory(binderFactory);
- requestMethod.setParameterNameDiscoverer(this.parameterNameDiscoverer);
- return requestMethod;
+ /**
+ * Create a {@link ServletInvocableHandlerMethod} from the given {@link HandlerMethod} definition.
+ * @param handlerMethod the {@link HandlerMethod} definition
+ * @return the corresponding {@link ServletInvocableHandlerMethod} (or custom subclass thereof)
+ * @since 4.2
+ */
+ protected ServletInvocableHandlerMethod createInvocableHandlerMethod(HandlerMethod handlerMethod) {
+ return new ServletInvocableHandlerMethod(handlerMethod);
}
private ModelFactory getModelFactory(HandlerMethod handlerMethod, WebDataBinderFactory binderFactory) {
@@ -797,7 +852,7 @@ public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter
Class<?> handlerType = handlerMethod.getBeanType();
Set<Method> methods = this.modelAttributeCache.get(handlerType);
if (methods == null) {
- methods = HandlerMethodSelector.selectMethods(handlerType, MODEL_ATTRIBUTE_METHODS);
+ methods = MethodIntrospector.selectMethods(handlerType, MODEL_ATTRIBUTE_METHODS);
this.modelAttributeCache.put(handlerType, methods);
}
List<InvocableHandlerMethod> attrMethods = new ArrayList<InvocableHandlerMethod>();
@@ -829,12 +884,12 @@ public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter
Class<?> handlerType = handlerMethod.getBeanType();
Set<Method> methods = this.initBinderCache.get(handlerType);
if (methods == null) {
- methods = HandlerMethodSelector.selectMethods(handlerType, INIT_BINDER_METHODS);
+ methods = MethodIntrospector.selectMethods(handlerType, INIT_BINDER_METHODS);
this.initBinderCache.put(handlerType, methods);
}
List<InvocableHandlerMethod> initBinderMethods = new ArrayList<InvocableHandlerMethod>();
// Global methods first
- for (Entry<ControllerAdviceBean, Set<Method>> entry : this.initBinderAdviceCache .entrySet()) {
+ for (Entry<ControllerAdviceBean, Set<Method>> entry : this.initBinderAdviceCache.entrySet()) {
if (entry.getKey().isApplicableToBeanType(handlerType)) {
Object bean = entry.getKey().resolveBean();
for (Method method : entry.getValue()) {
@@ -896,7 +951,6 @@ public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter
* MethodFilter that matches {@link InitBinder @InitBinder} methods.
*/
public static final MethodFilter INIT_BINDER_METHODS = new MethodFilter() {
-
@Override
public boolean matches(Method method) {
return AnnotationUtils.findAnnotation(method, InitBinder.class) != null;
@@ -907,7 +961,6 @@ public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter
* MethodFilter that matches {@link ModelAttribute @ModelAttribute} methods.
*/
public static final MethodFilter MODEL_ATTRIBUTE_METHODS = new MethodFilter() {
-
@Override
public boolean matches(Method method) {
return ((AnnotationUtils.findAnnotation(method, RequestMapping.class) == null) &&
diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerMapping.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerMapping.java
index dd28d139..5ffb6aaf 100644
--- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerMapping.java
+++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerMapping.java
@@ -16,26 +16,27 @@
package org.springframework.web.servlet.mvc.method.annotation;
+import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.Method;
-import java.util.ArrayList;
+import java.util.Arrays;
import java.util.List;
import org.springframework.context.EmbeddedValueResolverAware;
+import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.stereotype.Controller;
import org.springframework.util.Assert;
+import org.springframework.util.CollectionUtils;
import org.springframework.util.StringValueResolver;
import org.springframework.web.accept.ContentNegotiationManager;
+import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestMethod;
+import org.springframework.web.cors.CorsConfiguration;
+import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.mvc.condition.AbstractRequestCondition;
import org.springframework.web.servlet.mvc.condition.CompositeRequestCondition;
-import org.springframework.web.servlet.mvc.condition.ConsumesRequestCondition;
-import org.springframework.web.servlet.mvc.condition.HeadersRequestCondition;
-import org.springframework.web.servlet.mvc.condition.ParamsRequestCondition;
-import org.springframework.web.servlet.mvc.condition.PatternsRequestCondition;
-import org.springframework.web.servlet.mvc.condition.ProducesRequestCondition;
import org.springframework.web.servlet.mvc.condition.RequestCondition;
-import org.springframework.web.servlet.mvc.condition.RequestMethodsRequestCondition;
import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
import org.springframework.web.servlet.mvc.method.RequestMappingInfoHandlerMapping;
@@ -46,6 +47,7 @@ import org.springframework.web.servlet.mvc.method.RequestMappingInfoHandlerMappi
*
* @author Arjen Poutsma
* @author Rossen Stoyanchev
+ * @author Sam Brannen
* @since 3.1
*/
public class RequestMappingHandlerMapping extends RequestMappingInfoHandlerMapping
@@ -59,10 +61,10 @@ public class RequestMappingHandlerMapping extends RequestMappingInfoHandlerMappi
private ContentNegotiationManager contentNegotiationManager = new ContentNegotiationManager();
- private final List<String> fileExtensions = new ArrayList<String>();
-
private StringValueResolver embeddedValueResolver;
+ private RequestMappingInfo.BuilderConfiguration config = new RequestMappingInfo.BuilderConfiguration();
+
/**
* Whether to use suffix pattern match (".*") when matching patterns to
@@ -76,19 +78,11 @@ public class RequestMappingHandlerMapping extends RequestMappingInfoHandlerMappi
}
/**
- * Whether to use suffix pattern match for registered file extensions only
- * when matching patterns to requests.
- * <p>If enabled, a controller method mapped to "/users" also matches to
- * "/users.json" assuming ".json" is a file extension registered with the
- * provided {@link #setContentNegotiationManager(ContentNegotiationManager)
- * contentNegotiationManager}. This can be useful for allowing only specific
- * URL extensions to be used as well as in cases where a "." in the URL path
- * can lead to ambiguous interpretation of path variable content, (e.g. given
- * "/users/{user}" and incoming URLs such as "/users/john.j.joe" and
- * "/users/john.j.joe.json").
- * <p>If enabled, this flag also enables
- * {@link #setUseSuffixPatternMatch(boolean) useSuffixPatternMatch}. The
- * default value is {@code false}.
+ * Whether suffix pattern matching should work only against path extensions
+ * explicitly registered with the {@link ContentNegotiationManager}. This
+ * is generally recommended to reduce ambiguity and to avoid issues such as
+ * when a "." appears in the path for other reasons.
+ * <p>By default this is set to "false".
*/
public void setUseRegisteredSuffixPatternMatch(boolean useRegisteredSuffixPatternMatch) {
this.useRegisteredSuffixPatternMatch = useRegisteredSuffixPatternMatch;
@@ -120,9 +114,14 @@ public class RequestMappingHandlerMapping extends RequestMappingInfoHandlerMappi
@Override
public void afterPropertiesSet() {
- if (this.useRegisteredSuffixPatternMatch) {
- this.fileExtensions.addAll(this.contentNegotiationManager.getAllFileExtensions());
- }
+ this.config = new RequestMappingInfo.BuilderConfiguration();
+ this.config.setPathHelper(getUrlPathHelper());
+ this.config.setPathMatcher(getPathMatcher());
+ this.config.setSuffixPatternMatch(this.useSuffixPatternMatch);
+ this.config.setTrailingSlashMatch(this.useTrailingSlashMatch);
+ this.config.setRegisteredSuffixPatternMatch(this.useRegisteredSuffixPatternMatch);
+ this.config.setContentNegotiationManager(getContentNegotiationManager());
+
super.afterPropertiesSet();
}
@@ -159,7 +158,7 @@ public class RequestMappingHandlerMapping extends RequestMappingInfoHandlerMappi
* Return the file extensions to use for suffix pattern matching.
*/
public List<String> getFileExtensions() {
- return this.fileExtensions;
+ return this.config.getFileExtensions();
}
@@ -183,21 +182,31 @@ public class RequestMappingHandlerMapping extends RequestMappingInfoHandlerMappi
*/
@Override
protected RequestMappingInfo getMappingForMethod(Method method, Class<?> handlerType) {
- RequestMappingInfo info = null;
- RequestMapping methodAnnotation = AnnotationUtils.findAnnotation(method, RequestMapping.class);
- if (methodAnnotation != null) {
- RequestCondition<?> methodCondition = getCustomMethodCondition(method);
- info = createRequestMappingInfo(methodAnnotation, methodCondition);
- RequestMapping typeAnnotation = AnnotationUtils.findAnnotation(handlerType, RequestMapping.class);
- if (typeAnnotation != null) {
- RequestCondition<?> typeCondition = getCustomTypeCondition(handlerType);
- info = createRequestMappingInfo(typeAnnotation, typeCondition).combine(info);
+ RequestMappingInfo info = createRequestMappingInfo(method);
+ if (info != null) {
+ RequestMappingInfo typeInfo = createRequestMappingInfo(handlerType);
+ if (typeInfo != null) {
+ info = typeInfo.combine(info);
}
}
return info;
}
/**
+ * Delegates to {@link #createRequestMappingInfo(RequestMapping, RequestCondition)},
+ * supplying the appropriate custom {@link RequestCondition} depending on whether
+ * the supplied {@code annotatedElement} is a class or method.
+ * @see #getCustomTypeCondition(Class)
+ * @see #getCustomMethodCondition(Method)
+ */
+ private RequestMappingInfo createRequestMappingInfo(AnnotatedElement element) {
+ RequestMapping requestMapping = AnnotatedElementUtils.findMergedAnnotation(element, RequestMapping.class);
+ RequestCondition<?> condition = (element instanceof Class<?> ?
+ getCustomTypeCondition((Class<?>) element) : getCustomMethodCondition((Method) element));
+ return (requestMapping != null ? createRequestMappingInfo(requestMapping, condition) : null);
+ }
+
+ /**
* Provide a custom type-level request condition.
* The custom {@link RequestCondition} can be of any type so long as the
* same condition type is returned from all calls to this method in order
@@ -229,20 +238,24 @@ public class RequestMappingHandlerMapping extends RequestMappingInfoHandlerMappi
/**
* Create a {@link RequestMappingInfo} from the supplied
- * {@link RequestMapping @RequestMapping} annotation.
+ * {@link RequestMapping @RequestMapping} annotation, which is either
+ * a directly declared annotation, a meta-annotation, or the synthesized
+ * result of merging annotation attributes within an annotation hierarchy.
*/
- protected RequestMappingInfo createRequestMappingInfo(RequestMapping annotation, RequestCondition<?> customCondition) {
- String[] patterns = resolveEmbeddedValuesInPatterns(annotation.value());
- return new RequestMappingInfo(
- annotation.name(),
- new PatternsRequestCondition(patterns, getUrlPathHelper(), getPathMatcher(),
- this.useSuffixPatternMatch, this.useTrailingSlashMatch, this.fileExtensions),
- new RequestMethodsRequestCondition(annotation.method()),
- new ParamsRequestCondition(annotation.params()),
- new HeadersRequestCondition(annotation.headers()),
- new ConsumesRequestCondition(annotation.consumes(), annotation.headers()),
- new ProducesRequestCondition(annotation.produces(), annotation.headers(), this.contentNegotiationManager),
- customCondition);
+ protected RequestMappingInfo createRequestMappingInfo(
+ RequestMapping requestMapping, RequestCondition<?> customCondition) {
+
+ return RequestMappingInfo
+ .paths(resolveEmbeddedValuesInPatterns(requestMapping.path()))
+ .methods(requestMapping.method())
+ .params(requestMapping.params())
+ .headers(requestMapping.headers())
+ .consumes(requestMapping.consumes())
+ .produces(requestMapping.produces())
+ .mappingName(requestMapping.name())
+ .customCondition(customCondition)
+ .options(this.config)
+ .build();
}
/**
@@ -262,4 +275,72 @@ public class RequestMappingHandlerMapping extends RequestMappingInfoHandlerMappi
}
}
+ @Override
+ protected CorsConfiguration initCorsConfiguration(Object handler, Method method, RequestMappingInfo mappingInfo) {
+ HandlerMethod handlerMethod = createHandlerMethod(handler, method);
+ CrossOrigin typeAnnotation = AnnotatedElementUtils.findMergedAnnotation(handlerMethod.getBeanType(), CrossOrigin.class);
+ CrossOrigin methodAnnotation = AnnotatedElementUtils.findMergedAnnotation(method, CrossOrigin.class);
+
+ if (typeAnnotation == null && methodAnnotation == null) {
+ return null;
+ }
+
+ CorsConfiguration config = new CorsConfiguration();
+ updateCorsConfig(config, typeAnnotation);
+ updateCorsConfig(config, methodAnnotation);
+
+ if (CollectionUtils.isEmpty(config.getAllowedOrigins())) {
+ config.setAllowedOrigins(Arrays.asList(CrossOrigin.DEFAULT_ORIGINS));
+ }
+ if (CollectionUtils.isEmpty(config.getAllowedMethods())) {
+ for (RequestMethod allowedMethod : mappingInfo.getMethodsCondition().getMethods()) {
+ config.addAllowedMethod(allowedMethod.name());
+ }
+ }
+ if (CollectionUtils.isEmpty(config.getAllowedHeaders())) {
+ config.setAllowedHeaders(Arrays.asList(CrossOrigin.DEFAULT_ALLOWED_HEADERS));
+ }
+ if (config.getAllowCredentials() == null) {
+ config.setAllowCredentials(CrossOrigin.DEFAULT_ALLOW_CREDENTIALS);
+ }
+ if (config.getMaxAge() == null) {
+ config.setMaxAge(CrossOrigin.DEFAULT_MAX_AGE);
+ }
+ return config;
+ }
+
+ private void updateCorsConfig(CorsConfiguration config, CrossOrigin annotation) {
+ if (annotation == null) {
+ return;
+ }
+ for (String origin : annotation.origins()) {
+ config.addAllowedOrigin(origin);
+ }
+ for (RequestMethod method : annotation.methods()) {
+ config.addAllowedMethod(method.name());
+ }
+ for (String header : annotation.allowedHeaders()) {
+ config.addAllowedHeader(header);
+ }
+ for (String header : annotation.exposedHeaders()) {
+ config.addExposedHeader(header);
+ }
+
+ String allowCredentials = annotation.allowCredentials();
+ if ("true".equalsIgnoreCase(allowCredentials)) {
+ config.setAllowCredentials(true);
+ }
+ else if ("false".equalsIgnoreCase(allowCredentials)) {
+ config.setAllowCredentials(false);
+ }
+ else if (!allowCredentials.isEmpty()) {
+ throw new IllegalStateException("@CrossOrigin's allowCredentials value must be \"true\", \"false\", "
+ + "or an empty string (\"\"); current value is [" + allowCredentials + "].");
+ }
+
+ if (annotation.maxAge() >= 0 && config.getMaxAge() == null) {
+ config.setMaxAge(annotation.maxAge());
+ }
+ }
+
}
diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestPartMethodArgumentResolver.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestPartMethodArgumentResolver.java
index 6bd1c5a4..ba34c2e4 100644
--- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestPartMethodArgumentResolver.java
+++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestPartMethodArgumentResolver.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2015 the original author or authors.
+ * Copyright 2002-2016 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.
@@ -19,6 +19,7 @@ package org.springframework.web.servlet.mvc.method.annotation;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
+import java.util.Optional;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.Part;
@@ -26,6 +27,7 @@ import org.springframework.core.GenericCollectionTypeResolver;
import org.springframework.core.MethodParameter;
import org.springframework.http.HttpInputMessage;
import org.springframework.http.converter.HttpMessageConverter;
+import org.springframework.lang.UsesJava8;
import org.springframework.util.Assert;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.MethodArgumentNotValidException;
@@ -72,10 +74,23 @@ import org.springframework.web.util.WebUtils;
*/
public class RequestPartMethodArgumentResolver extends AbstractMessageConverterMethodArgumentResolver {
+ /**
+ * Basic constructor with converters only.
+ */
public RequestPartMethodArgumentResolver(List<HttpMessageConverter<?>> messageConverters) {
super(messageConverters);
}
+ /**
+ * Constructor with converters and {@code Request~} and
+ * {@code ResponseBodyAdvice}.
+ */
+ public RequestPartMethodArgumentResolver(List<HttpMessageConverter<?>> messageConverters,
+ List<Object> requestResponseBodyAdvice) {
+
+ super(messageConverters, requestResponseBodyAdvice);
+ }
+
/**
* Supports the following:
@@ -94,7 +109,7 @@ public class RequestPartMethodArgumentResolver extends AbstractMessageConverterM
if (parameter.hasParameterAnnotation(RequestParam.class)){
return false;
}
- else if (MultipartFile.class.equals(parameter.getParameterType())) {
+ else if (MultipartFile.class == parameter.getParameterType()) {
return true;
}
else if ("javax.servlet.http.Part".equals(parameter.getParameterType().getName())) {
@@ -112,14 +127,21 @@ public class RequestPartMethodArgumentResolver extends AbstractMessageConverterM
HttpServletRequest servletRequest = request.getNativeRequest(HttpServletRequest.class);
assertIsMultipartRequest(servletRequest);
-
MultipartHttpServletRequest multipartRequest =
WebUtils.getNativeRequest(servletRequest, MultipartHttpServletRequest.class);
String partName = getPartName(parameter);
+ Class<?> paramType = parameter.getParameterType();
+ boolean optional = paramType.getName().equals("java.util.Optional");
+ if (optional) {
+ parameter = new MethodParameter(parameter);
+ parameter.increaseNestingLevel();
+ paramType = parameter.getNestedParameterType();
+ }
+
Object arg;
- if (MultipartFile.class.equals(parameter.getParameterType())) {
+ if (MultipartFile.class == paramType) {
Assert.notNull(multipartRequest, "Expected MultipartHttpServletRequest: is a MultipartResolver configured?");
arg = multipartRequest.getFile(partName);
}
@@ -132,7 +154,7 @@ public class RequestPartMethodArgumentResolver extends AbstractMessageConverterM
List<MultipartFile> files = multipartRequest.getFiles(partName);
arg = files.toArray(new MultipartFile[files.size()]);
}
- else if ("javax.servlet.http.Part".equals(parameter.getParameterType().getName())) {
+ else if ("javax.servlet.http.Part".equals(paramType.getName())) {
assertIsMultipartRequest(servletRequest);
arg = servletRequest.getPart(partName);
}
@@ -147,7 +169,7 @@ public class RequestPartMethodArgumentResolver extends AbstractMessageConverterM
else {
try {
HttpInputMessage inputMessage = new RequestPartServletServerHttpRequest(servletRequest, partName);
- arg = readWithMessageConverters(inputMessage, parameter, parameter.getParameterType());
+ arg = readWithMessageConverters(inputMessage, parameter, parameter.getNestedGenericParameterType());
WebDataBinder binder = binderFactory.createBinder(request, arg, partName);
if (arg != null) {
validateIfApplicable(binder, parameter);
@@ -163,12 +185,15 @@ public class RequestPartMethodArgumentResolver extends AbstractMessageConverterM
}
}
- RequestPart ann = parameter.getParameterAnnotation(RequestPart.class);
- boolean isRequired = (ann == null || ann.required());
+ RequestPart requestPart = parameter.getParameterAnnotation(RequestPart.class);
+ boolean isRequired = ((requestPart == null || requestPart.required()) && !optional);
if (arg == null && isRequired) {
throw new MissingServletRequestPartException(partName);
}
+ if (optional) {
+ arg = OptionalResolver.resolveValue(arg);
+ }
return arg;
}
@@ -181,8 +206,8 @@ public class RequestPartMethodArgumentResolver extends AbstractMessageConverterM
}
private String getPartName(MethodParameter methodParam) {
- RequestPart ann = methodParam.getParameterAnnotation(RequestPart.class);
- String partName = (ann != null ? ann.value() : "");
+ RequestPart requestPart = methodParam.getParameterAnnotation(RequestPart.class);
+ String partName = (requestPart != null ? requestPart.name() : "");
if (partName.length() == 0) {
partName = methodParam.getParameterName();
if (partName == null) {
@@ -196,12 +221,12 @@ public class RequestPartMethodArgumentResolver extends AbstractMessageConverterM
private boolean isMultipartFileCollection(MethodParameter methodParam) {
Class<?> collectionType = getCollectionParameterType(methodParam);
- return MultipartFile.class.equals(collectionType);
+ return MultipartFile.class == collectionType;
}
private boolean isMultipartFileArray(MethodParameter methodParam) {
Class<?> paramType = methodParam.getNestedParameterType().getComponentType();
- return MultipartFile.class.equals(paramType);
+ return MultipartFile.class == paramType;
}
private boolean isPartCollection(MethodParameter methodParam) {
@@ -216,7 +241,7 @@ public class RequestPartMethodArgumentResolver extends AbstractMessageConverterM
private Class<?> getCollectionParameterType(MethodParameter methodParam) {
Class<?> paramType = methodParam.getNestedParameterType();
- if (Collection.class.equals(paramType) || List.class.isAssignableFrom(paramType)){
+ if (Collection.class == paramType || List.class.isAssignableFrom(paramType)){
Class<?> valueType = GenericCollectionTypeResolver.getCollectionParameterType(methodParam);
if (valueType != null) {
return valueType;
@@ -237,4 +262,16 @@ public class RequestPartMethodArgumentResolver extends AbstractMessageConverterM
}
}
+
+ /**
+ * Inner class to avoid hard-coded dependency on Java 8 Optional type...
+ */
+ @UsesJava8
+ private static class OptionalResolver {
+
+ public static Object resolveValue(Object value) {
+ return Optional.ofNullable(value);
+ }
+ }
+
}
diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestResponseBodyAdviceChain.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestResponseBodyAdviceChain.java
new file mode 100644
index 00000000..d41295cf
--- /dev/null
+++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestResponseBodyAdviceChain.java
@@ -0,0 +1,177 @@
+/*
+ * Copyright 2002-2015 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.mvc.method.annotation;
+
+import java.io.IOException;
+import java.lang.reflect.Type;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import org.springframework.core.MethodParameter;
+import org.springframework.http.HttpInputMessage;
+import org.springframework.http.MediaType;
+import org.springframework.http.converter.HttpMessageConverter;
+import org.springframework.http.server.ServerHttpRequest;
+import org.springframework.http.server.ServerHttpResponse;
+import org.springframework.util.CollectionUtils;
+import org.springframework.web.method.ControllerAdviceBean;
+
+
+/**
+ * Invokes {@link RequestBodyAdvice} and {@link ResponseBodyAdvice} where each
+ * instance may be (and is most likely) wrapped with
+ * {@link org.springframework.web.method.ControllerAdviceBean ControllerAdviceBean}.
+ *
+ * @author Rossen Stoyanchev
+ * @since 4.2
+ */
+class RequestResponseBodyAdviceChain implements RequestBodyAdvice, ResponseBodyAdvice<Object> {
+
+ private final List<Object> requestBodyAdvice = new ArrayList<Object>(4);
+
+ private final List<Object> responseBodyAdvice = new ArrayList<Object>(4);
+
+
+ /**
+ * Create an instance from a list of objects that are either of type
+ * {@code ControllerAdviceBean} or {@code RequestBodyAdvice}.
+ */
+ public RequestResponseBodyAdviceChain(List<Object> requestResponseBodyAdvice) {
+ initAdvice(requestResponseBodyAdvice);
+ }
+
+ private void initAdvice(List<Object> requestResponseBodyAdvice) {
+ if (requestResponseBodyAdvice == null) {
+ return;
+ }
+ for (Object advice : requestResponseBodyAdvice) {
+ Class<?> beanType = (advice instanceof ControllerAdviceBean ?
+ ((ControllerAdviceBean) advice).getBeanType() : advice.getClass());
+ if (RequestBodyAdvice.class.isAssignableFrom(beanType)) {
+ this.requestBodyAdvice.add(advice);
+ }
+ else if (ResponseBodyAdvice.class.isAssignableFrom(beanType)) {
+ this.responseBodyAdvice.add(advice);
+ }
+ }
+ }
+
+ private List<Object> getAdvice(Class<?> adviceType) {
+ if (RequestBodyAdvice.class == adviceType) {
+ return this.requestBodyAdvice;
+ }
+ else if (ResponseBodyAdvice.class == adviceType) {
+ return this.responseBodyAdvice;
+ }
+ else {
+ throw new IllegalArgumentException("Unexpected adviceType: " + adviceType);
+ }
+ }
+
+
+ @Override
+ public boolean supports(MethodParameter param, Type type, Class<? extends HttpMessageConverter<?>> converterType) {
+ throw new UnsupportedOperationException("Not implemented");
+ }
+
+ @Override
+ public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
+ throw new UnsupportedOperationException("Not implemented");
+ }
+
+ @Override
+ public Object handleEmptyBody(Object body, HttpInputMessage inputMessage, MethodParameter parameter,
+ Type targetType, Class<? extends HttpMessageConverter<?>> converterType) {
+
+ for (RequestBodyAdvice advice : getMatchingAdvice(parameter, RequestBodyAdvice.class)) {
+ if (advice.supports(parameter, targetType, converterType)) {
+ body = advice.handleEmptyBody(body, inputMessage, parameter, targetType, converterType);
+ }
+ }
+ return body;
+ }
+
+ @Override
+ public HttpInputMessage beforeBodyRead(HttpInputMessage request, MethodParameter parameter,
+ Type targetType, Class<? extends HttpMessageConverter<?>> converterType) throws IOException {
+
+ for (RequestBodyAdvice advice : getMatchingAdvice(parameter, RequestBodyAdvice.class)) {
+ if (advice.supports(parameter, targetType, converterType)) {
+ request = advice.beforeBodyRead(request, parameter, targetType, converterType);
+ }
+ }
+ return request;
+ }
+
+ @Override
+ public Object afterBodyRead(Object body, HttpInputMessage inputMessage, MethodParameter parameter,
+ Type targetType, Class<? extends HttpMessageConverter<?>> converterType) {
+
+ for (RequestBodyAdvice advice : getMatchingAdvice(parameter, RequestBodyAdvice.class)) {
+ if (advice.supports(parameter, targetType, converterType)) {
+ body = advice.afterBodyRead(body, inputMessage, parameter, targetType, converterType);
+ }
+ }
+ return body;
+ }
+
+ @Override
+ public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType contentType,
+ Class<? extends HttpMessageConverter<?>> converterType,
+ ServerHttpRequest request, ServerHttpResponse response) {
+
+ return processBody(body, returnType, contentType, converterType, request, response);
+ }
+
+ @SuppressWarnings("unchecked")
+ private <T> Object processBody(Object body, MethodParameter returnType, MediaType contentType,
+ Class<? extends HttpMessageConverter<?>> converterType,
+ ServerHttpRequest request, ServerHttpResponse response) {
+
+ for (ResponseBodyAdvice<?> advice : getMatchingAdvice(returnType, ResponseBodyAdvice.class)) {
+ if (advice.supports(returnType, converterType)) {
+ body = ((ResponseBodyAdvice<T>) advice).beforeBodyWrite((T) body, returnType,
+ contentType, converterType, request, response);
+ }
+ }
+ return body;
+ }
+
+ @SuppressWarnings("unchecked")
+ private <A> List<A> getMatchingAdvice(MethodParameter parameter, Class<? extends A> adviceType) {
+ List<Object> availableAdvice = getAdvice(adviceType);
+ if (CollectionUtils.isEmpty(availableAdvice)) {
+ return Collections.emptyList();
+ }
+ List<A> result = new ArrayList<A>(availableAdvice.size());
+ for (Object advice : availableAdvice) {
+ if (advice instanceof ControllerAdviceBean) {
+ ControllerAdviceBean adviceBean = (ControllerAdviceBean) advice;
+ if (!adviceBean.isApplicableToBeanType(parameter.getContainingClass())) {
+ continue;
+ }
+ advice = adviceBean.resolveBean();
+ }
+ if (adviceType.isAssignableFrom(advice.getClass())) {
+ result.add((A) advice);
+ }
+ }
+ return result;
+ }
+
+}
diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestResponseBodyMethodProcessor.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestResponseBodyMethodProcessor.java
index fccf6ee6..bc187962 100644
--- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestResponseBodyMethodProcessor.java
+++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestResponseBodyMethodProcessor.java
@@ -17,20 +17,16 @@
package org.springframework.web.servlet.mvc.method.annotation;
import java.io.IOException;
-import java.io.InputStream;
-import java.io.PushbackInputStream;
import java.lang.reflect.Type;
import java.util.List;
-import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletRequestWrapper;
import org.springframework.core.Conventions;
import org.springframework.core.MethodParameter;
import org.springframework.core.annotation.AnnotationUtils;
-import org.springframework.http.HttpInputMessage;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.HttpMessageNotReadableException;
+import org.springframework.http.converter.HttpMessageNotWritableException;
import org.springframework.http.server.ServletServerHttpRequest;
import org.springframework.validation.BindingResult;
import org.springframework.web.HttpMediaTypeNotAcceptableException;
@@ -61,20 +57,47 @@ import org.springframework.web.servlet.mvc.support.DefaultHandlerExceptionResolv
*/
public class RequestResponseBodyMethodProcessor extends AbstractMessageConverterMethodProcessor {
- public RequestResponseBodyMethodProcessor(List<HttpMessageConverter<?>> messageConverters) {
- super(messageConverters);
+ /**
+ * Basic constructor with converters only. Suitable for resolving
+ * {@code @RequestBody}. For handling {@code @ResponseBody} consider also
+ * providing a {@code ContentNegotiationManager}.
+ */
+ public RequestResponseBodyMethodProcessor(List<HttpMessageConverter<?>> converters) {
+ super(converters);
}
- public RequestResponseBodyMethodProcessor(List<HttpMessageConverter<?>> messageConverters,
- ContentNegotiationManager contentNegotiationManager) {
+ /**
+ * Basic constructor with converters and {@code ContentNegotiationManager}.
+ * Suitable for resolving {@code @RequestBody} and handling
+ * {@code @ResponseBody} without {@code Request~} or
+ * {@code ResponseBodyAdvice}.
+ */
+ public RequestResponseBodyMethodProcessor(List<HttpMessageConverter<?>> converters,
+ ContentNegotiationManager manager) {
- super(messageConverters, contentNegotiationManager);
+ super(converters, manager);
}
- public RequestResponseBodyMethodProcessor(List<HttpMessageConverter<?>> messageConverters,
- ContentNegotiationManager contentNegotiationManager, List<Object> responseBodyAdvice) {
+ /**
+ * Complete constructor for resolving {@code @RequestBody} method arguments.
+ * For handling {@code @ResponseBody} consider also providing a
+ * {@code ContentNegotiationManager}.
+ * @since 4.2
+ */
+ public RequestResponseBodyMethodProcessor(List<HttpMessageConverter<?>> converters,
+ List<Object> requestResponseBodyAdvice) {
+
+ super(converters, null, requestResponseBodyAdvice);
+ }
+
+ /**
+ * Complete constructor for resolving {@code @RequestBody} and handling
+ * {@code @ResponseBody}.
+ */
+ public RequestResponseBodyMethodProcessor(List<HttpMessageConverter<?>> converters,
+ ContentNegotiationManager manager, List<Object> requestResponseBodyAdvice) {
- super(messageConverters, contentNegotiationManager, responseBodyAdvice);
+ super(converters, manager, requestResponseBodyAdvice);
}
@@ -101,6 +124,7 @@ public class RequestResponseBodyMethodProcessor extends AbstractMessageConverter
Object arg = readWithMessageConverters(webRequest, parameter, parameter.getGenericParameterType());
String name = Conventions.getVariableNameForParameter(parameter);
+
WebDataBinder binder = binderFactory.createBinder(webRequest, arg, name);
if (arg != null) {
validateIfApplicable(binder, parameter);
@@ -109,75 +133,31 @@ public class RequestResponseBodyMethodProcessor extends AbstractMessageConverter
}
}
mavContainer.addAttribute(BindingResult.MODEL_KEY_PREFIX + name, binder.getBindingResult());
+
return arg;
}
@Override
protected <T> Object readWithMessageConverters(NativeWebRequest webRequest, MethodParameter methodParam,
- Type paramType) throws IOException, HttpMediaTypeNotSupportedException {
+ Type paramType) throws IOException, HttpMediaTypeNotSupportedException, HttpMessageNotReadableException {
HttpServletRequest servletRequest = webRequest.getNativeRequest(HttpServletRequest.class);
- HttpInputMessage inputMessage = new ServletServerHttpRequest(servletRequest);
+ ServletServerHttpRequest inputMessage = new ServletServerHttpRequest(servletRequest);
- InputStream inputStream = inputMessage.getBody();
- if (inputStream == null) {
- return handleEmptyBody(methodParam);
- }
- else if (inputStream.markSupported()) {
- inputStream.mark(1);
- if (inputStream.read() == -1) {
- return handleEmptyBody(methodParam);
- }
- inputStream.reset();
- }
- else {
- final PushbackInputStream pushbackInputStream = new PushbackInputStream(inputStream);
- int b = pushbackInputStream.read();
- if (b == -1) {
- return handleEmptyBody(methodParam);
- }
- else {
- pushbackInputStream.unread(b);
+ Object arg = readWithMessageConverters(inputMessage, methodParam, paramType);
+ if (arg == null) {
+ if (methodParam.getParameterAnnotation(RequestBody.class).required()) {
+ throw new HttpMessageNotReadableException("Required request body is missing: " +
+ methodParam.getMethod().toGenericString());
}
- HttpServletRequest wrappedRequest = new HttpServletRequestWrapper(servletRequest) {
- @Override
- public ServletInputStream getInputStream() throws IOException {
- return new ServletInputStream() {
- @Override
- public int read() throws IOException {
- return pushbackInputStream.read();
- }
- @Override
- public void close() throws IOException {
- super.close();
- pushbackInputStream.close();
- }
- };
- }
- };
- inputMessage = new ServletServerHttpRequest(wrappedRequest) {
- @Override
- public InputStream getBody() {
- // Form POST should not get here
- return pushbackInputStream;
- }
- };
}
-
- return super.readWithMessageConverters(inputMessage, methodParam, paramType);
- }
-
- private Object handleEmptyBody(MethodParameter param) {
- if (param.getParameterAnnotation(RequestBody.class).required()) {
- throw new HttpMessageNotReadableException("Required request body content is missing: " + param);
- }
- return null;
+ return arg;
}
@Override
public void handleReturnValue(Object returnValue, MethodParameter returnType,
ModelAndViewContainer mavContainer, NativeWebRequest webRequest)
- throws IOException, HttpMediaTypeNotAcceptableException {
+ throws IOException, HttpMediaTypeNotAcceptableException, HttpMessageNotWritableException {
mavContainer.setRequestHandled(true);
diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ResponseBodyAdvice.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ResponseBodyAdvice.java
index 01ae9cd0..b9c8c83d 100644
--- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ResponseBodyAdvice.java
+++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ResponseBodyAdvice.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2014 the original author or authors.
+ * Copyright 2002-2015 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.
@@ -24,7 +24,7 @@ import org.springframework.http.server.ServerHttpResponse;
/**
* Allows customizing the response after the execution of an {@code @ResponseBody}
- * or an {@code ResponseEntity} controller method but before the body is written
+ * or a {@code ResponseEntity} controller method but before the body is written
* with an {@code HttpMessageConverter}.
*
* <p>Implementations may be may be registered directly with
diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ResponseBodyAdviceChain.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ResponseBodyAdviceChain.java
deleted file mode 100644
index 4b17fce7..00000000
--- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ResponseBodyAdviceChain.java
+++ /dev/null
@@ -1,78 +0,0 @@
-/*
- * Copyright 2002-2015 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.mvc.method.annotation;
-
-import java.util.List;
-
-import org.springframework.core.MethodParameter;
-import org.springframework.http.MediaType;
-import org.springframework.http.converter.HttpMessageConverter;
-import org.springframework.http.server.ServerHttpRequest;
-import org.springframework.http.server.ServerHttpResponse;
-import org.springframework.util.CollectionUtils;
-import org.springframework.web.method.ControllerAdviceBean;
-
-/**
- * Invokes a a list of {@link ResponseBodyAdvice} beans.
- *
- * @author Rossen Stoyanchev
- * @since 4.1
- */
-class ResponseBodyAdviceChain {
-
- private final List<Object> advice;
-
-
- public ResponseBodyAdviceChain(List<Object> advice) {
- this.advice = advice;
- }
-
-
- public boolean hasAdvice() {
- return !CollectionUtils.isEmpty(this.advice);
- }
-
- @SuppressWarnings("unchecked")
- public <T> T invoke(T body, MethodParameter returnType,
- MediaType selectedContentType, Class<? extends HttpMessageConverter<?>> selectedConverterType,
- ServerHttpRequest request, ServerHttpResponse response) {
-
- if (this.advice != null) {
- for (Object advice : this.advice) {
- if (advice instanceof ControllerAdviceBean) {
- ControllerAdviceBean adviceBean = (ControllerAdviceBean) advice;
- if (!adviceBean.isApplicableToBeanType(returnType.getContainingClass())) {
- continue;
- }
- advice = adviceBean.resolveBean();
- }
- if (advice instanceof ResponseBodyAdvice) {
- ResponseBodyAdvice<T> typedAdvice = (ResponseBodyAdvice<T>) advice;
- if (typedAdvice.supports(returnType, selectedConverterType)) {
- body = typedAdvice.beforeBodyWrite(body, returnType,
- selectedContentType, selectedConverterType, request, response);
- }
- }
- else {
- throw new IllegalStateException("Expected ResponseBodyAdvice: " + advice);
- }
- }
- }
- return body;
- }
-
-}
diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ResponseBodyEmitter.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ResponseBodyEmitter.java
new file mode 100644
index 00000000..18d96b07
--- /dev/null
+++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ResponseBodyEmitter.java
@@ -0,0 +1,286 @@
+/*
+ * Copyright 2002-2015 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.mvc.method.annotation;
+
+import java.io.IOException;
+import java.util.LinkedHashSet;
+import java.util.Set;
+
+import org.springframework.http.MediaType;
+import org.springframework.http.server.ServerHttpResponse;
+import org.springframework.util.Assert;
+
+/**
+ * A controller method return value type for asynchronous request processing
+ * where one or more objects are written to the response.
+ *
+ * <p>While {@link org.springframework.web.context.request.async.DeferredResult}
+ * is used to produce a single result, a {@code ResponseBodyEmitter} can be used
+ * to send multiple objects where each object is written with a compatible
+ * {@link org.springframework.http.converter.HttpMessageConverter}.
+ *
+ * <p>Supported as a return type on its own as well as within a
+ * {@link org.springframework.http.ResponseEntity}.
+ *
+ * <pre>
+ * &#064;RequestMapping(value="/stream", method=RequestMethod.GET)
+ * public ResponseBodyEmitter handle() {
+ * ResponseBodyEmitter emitter = new ResponseBodyEmitter();
+ * // Pass the emitter to another component...
+ * return emitter;
+ * }
+ *
+ * // in another thread
+ * emitter.send(foo1);
+ *
+ * // and again
+ * emitter.send(foo2);
+ *
+ * // and done
+ * emitter.complete();
+ * </pre>
+ *
+ * @author Rossen Stoyanchev
+ * @author Juergen Hoeller
+ * @since 4.2
+ */
+public class ResponseBodyEmitter {
+
+ private final Long timeout;
+
+ private final Set<DataWithMediaType> earlySendAttempts = new LinkedHashSet<DataWithMediaType>(8);
+
+ private Handler handler;
+
+ private boolean complete;
+
+ private Throwable failure;
+
+ private final DefaultCallback timeoutCallback = new DefaultCallback();
+
+ private final DefaultCallback completionCallback = new DefaultCallback();
+
+
+ /**
+ * Create a new ResponseBodyEmitter instance.
+ */
+ public ResponseBodyEmitter() {
+ this.timeout = null;
+ }
+
+ /**
+ * Create a ResponseBodyEmitter with a custom timeout value.
+ * <p>By default not set in which case the default configured in the MVC
+ * Java Config or the MVC namespace is used, or if that's not set, then the
+ * timeout depends on the default of the underlying server.
+ * @param timeout timeout value in milliseconds
+ */
+ public ResponseBodyEmitter(Long timeout) {
+ this.timeout = timeout;
+ }
+
+
+ /**
+ * Return the configured timeout value, if any.
+ */
+ public Long getTimeout() {
+ return this.timeout;
+ }
+
+
+ synchronized void initialize(Handler handler) throws IOException {
+ this.handler = handler;
+
+ for (DataWithMediaType sendAttempt : this.earlySendAttempts) {
+ sendInternal(sendAttempt.getData(), sendAttempt.getMediaType());
+ }
+ this.earlySendAttempts.clear();
+
+ if (this.complete) {
+ if (this.failure != null) {
+ this.handler.completeWithError(this.failure);
+ }
+ else {
+ this.handler.complete();
+ }
+ }
+ else {
+ this.handler.onTimeout(this.timeoutCallback);
+ this.handler.onCompletion(this.completionCallback);
+ }
+ }
+
+ /**
+ * Invoked after the response is updated with the status code and headers,
+ * if the ResponseBodyEmitter is wrapped in a ResponseEntity, but before the
+ * response is committed, i.e. before the response body has been written to.
+ * <p>The default implementation is empty.
+ */
+ protected void extendResponse(ServerHttpResponse outputMessage) {
+ }
+
+ /**
+ * Write the given object to the response.
+ * <p>If any exception occurs a dispatch is made back to the app server where
+ * Spring MVC will pass the exception through its exception handling mechanism.
+ * @param object the object to write
+ * @throws IOException raised when an I/O error occurs
+ * @throws java.lang.IllegalStateException wraps any other errors
+ */
+ public void send(Object object) throws IOException {
+ send(object, null);
+ }
+
+ /**
+ * Write the given object to the response also using a MediaType hint.
+ * <p>If any exception occurs a dispatch is made back to the app server where
+ * Spring MVC will pass the exception through its exception handling mechanism.
+ * @param object the object to write
+ * @param mediaType a MediaType hint for selecting an HttpMessageConverter
+ * @throws IOException raised when an I/O error occurs
+ * @throws java.lang.IllegalStateException wraps any other errors
+ */
+ public synchronized void send(Object object, MediaType mediaType) throws IOException {
+ Assert.state(!this.complete, "ResponseBodyEmitter is already set complete");
+ sendInternal(object, mediaType);
+ }
+
+ private void sendInternal(Object object, MediaType mediaType) throws IOException {
+ if (object != null) {
+ if (this.handler != null) {
+ try {
+ this.handler.send(object, mediaType);
+ }
+ catch (IOException ex) {
+ completeWithError(ex);
+ throw ex;
+ }
+ catch (Throwable ex) {
+ completeWithError(ex);
+ throw new IllegalStateException("Failed to send " + object, ex);
+ }
+ }
+ else {
+ this.earlySendAttempts.add(new DataWithMediaType(object, mediaType));
+ }
+ }
+ }
+
+ /**
+ * Complete request processing.
+ * <p>A dispatch is made into the app server where Spring MVC completes
+ * asynchronous request processing.
+ */
+ public synchronized void complete() {
+ this.complete = true;
+ if (this.handler != null) {
+ this.handler.complete();
+ }
+ }
+
+ /**
+ * Complete request processing with an error.
+ * <p>A dispatch is made into the app server where Spring MVC will pass the
+ * exception through its exception handling mechanism.
+ */
+ public synchronized void completeWithError(Throwable ex) {
+ this.complete = true;
+ this.failure = ex;
+ if (this.handler != null) {
+ this.handler.completeWithError(ex);
+ }
+ }
+
+ /**
+ * Register code to invoke when the async request times out. This method is
+ * called from a container thread when an async request times out.
+ */
+ public synchronized void onTimeout(Runnable callback) {
+ this.timeoutCallback.setDelegate(callback);
+ }
+
+ /**
+ * Register code to invoke when the async request completes. This method is
+ * called from a container thread when an async request completed for any
+ * reason including timeout and network error. This method is useful for
+ * detecting that a {@code ResponseBodyEmitter} instance is no longer usable.
+ */
+ public synchronized void onCompletion(Runnable callback) {
+ this.completionCallback.setDelegate(callback);
+ }
+
+
+ /**
+ * Handle sent objects and complete request processing.
+ */
+ interface Handler {
+
+ void send(Object data, MediaType mediaType) throws IOException;
+
+ void complete();
+
+ void completeWithError(Throwable failure);
+
+ void onTimeout(Runnable callback);
+
+ void onCompletion(Runnable callback);
+ }
+
+
+ /**
+ * A simple holder of data to be written along with a MediaType hint for
+ * selecting a message converter to write with.
+ */
+ public static class DataWithMediaType {
+
+ private final Object data;
+
+ private final MediaType mediaType;
+
+ public DataWithMediaType(Object data, MediaType mediaType) {
+ this.data = data;
+ this.mediaType = mediaType;
+ }
+
+ public Object getData() {
+ return this.data;
+ }
+
+ public MediaType getMediaType() {
+ return this.mediaType;
+ }
+ }
+
+
+ private class DefaultCallback implements Runnable {
+
+ private Runnable delegate;
+
+ public void setDelegate(Runnable delegate) {
+ this.delegate = delegate;
+ }
+
+ @Override
+ public void run() {
+ ResponseBodyEmitter.this.complete = true;
+ if (this.delegate != null) {
+ this.delegate.run();
+ }
+ }
+ }
+
+}
diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ResponseBodyEmitterReturnValueHandler.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ResponseBodyEmitterReturnValueHandler.java
new file mode 100644
index 00000000..9a216058
--- /dev/null
+++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ResponseBodyEmitterReturnValueHandler.java
@@ -0,0 +1,232 @@
+/*
+ * Copyright 2002-2015 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.mvc.method.annotation;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.util.List;
+
+import javax.servlet.ServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+import org.springframework.core.MethodParameter;
+import org.springframework.core.ResolvableType;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.MediaType;
+import org.springframework.http.ResponseEntity;
+import org.springframework.http.converter.HttpMessageConverter;
+import org.springframework.http.server.ServerHttpResponse;
+import org.springframework.http.server.ServletServerHttpResponse;
+import org.springframework.util.Assert;
+import org.springframework.web.context.request.NativeWebRequest;
+import org.springframework.web.context.request.async.DeferredResult;
+import org.springframework.web.context.request.async.WebAsyncUtils;
+import org.springframework.web.filter.ShallowEtagHeaderFilter;
+import org.springframework.web.method.support.AsyncHandlerMethodReturnValueHandler;
+import org.springframework.web.method.support.ModelAndViewContainer;
+
+/**
+ * Supports return values of type {@link ResponseBodyEmitter} and also
+ * {@code ResponseEntity<ResponseBodyEmitter>}.
+ *
+ * @author Rossen Stoyanchev
+ * @since 4.2
+ */
+public class ResponseBodyEmitterReturnValueHandler implements AsyncHandlerMethodReturnValueHandler {
+
+ private static final Log logger = LogFactory.getLog(ResponseBodyEmitterReturnValueHandler.class);
+
+ private final List<HttpMessageConverter<?>> messageConverters;
+
+
+ public ResponseBodyEmitterReturnValueHandler(List<HttpMessageConverter<?>> messageConverters) {
+ Assert.notEmpty(messageConverters, "'messageConverters' must not be empty");
+ this.messageConverters = messageConverters;
+ }
+
+
+ @Override
+ public boolean supportsReturnType(MethodParameter returnType) {
+ if (ResponseBodyEmitter.class.isAssignableFrom(returnType.getParameterType())) {
+ return true;
+ }
+ else if (ResponseEntity.class.isAssignableFrom(returnType.getParameterType())) {
+ Class<?> bodyType = ResolvableType.forMethodParameter(returnType).getGeneric(0).resolve();
+ return (bodyType != null && ResponseBodyEmitter.class.isAssignableFrom(bodyType));
+ }
+ return false;
+ }
+
+ @Override
+ public boolean isAsyncReturnValue(Object returnValue, MethodParameter returnType) {
+ if (returnValue != null) {
+ if (returnValue instanceof ResponseBodyEmitter) {
+ return true;
+ }
+ else if (returnValue instanceof ResponseEntity) {
+ Object body = ((ResponseEntity) returnValue).getBody();
+ return (body != null && body instanceof ResponseBodyEmitter);
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public void handleReturnValue(Object returnValue, MethodParameter returnType,
+ ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception {
+
+ if (returnValue == null) {
+ mavContainer.setRequestHandled(true);
+ return;
+ }
+
+ HttpServletResponse response = webRequest.getNativeResponse(HttpServletResponse.class);
+ ServerHttpResponse outputMessage = new ServletServerHttpResponse(response);
+
+ if (ResponseEntity.class.isAssignableFrom(returnValue.getClass())) {
+ ResponseEntity<?> responseEntity = (ResponseEntity<?>) returnValue;
+ outputMessage.setStatusCode(responseEntity.getStatusCode());
+ outputMessage.getHeaders().putAll(responseEntity.getHeaders());
+ returnValue = responseEntity.getBody();
+ if (returnValue == null) {
+ mavContainer.setRequestHandled(true);
+ return;
+ }
+ }
+
+ ServletRequest request = webRequest.getNativeRequest(ServletRequest.class);
+ ShallowEtagHeaderFilter.disableContentCaching(request);
+
+ Assert.isInstanceOf(ResponseBodyEmitter.class, returnValue);
+ ResponseBodyEmitter emitter = (ResponseBodyEmitter) returnValue;
+ emitter.extendResponse(outputMessage);
+
+ // Commit the response and wrap to ignore further header changes
+ outputMessage.getBody();
+ outputMessage.flush();
+ outputMessage = new StreamingServletServerHttpResponse(outputMessage);
+
+ DeferredResult<?> deferredResult = new DeferredResult<Object>(emitter.getTimeout());
+ WebAsyncUtils.getAsyncManager(webRequest).startDeferredResultProcessing(deferredResult, mavContainer);
+
+ HttpMessageConvertingHandler handler = new HttpMessageConvertingHandler(outputMessage, deferredResult);
+ emitter.initialize(handler);
+ }
+
+
+ /**
+ * ResponseBodyEmitter.Handler that writes with HttpMessageConverter's.
+ */
+ private class HttpMessageConvertingHandler implements ResponseBodyEmitter.Handler {
+
+ private final ServerHttpResponse outputMessage;
+
+ private final DeferredResult<?> deferredResult;
+
+ public HttpMessageConvertingHandler(ServerHttpResponse outputMessage, DeferredResult<?> deferredResult) {
+ this.outputMessage = outputMessage;
+ this.deferredResult = deferredResult;
+ }
+
+ @Override
+ public void send(Object data, MediaType mediaType) throws IOException {
+ sendInternal(data, mediaType);
+ }
+
+ @SuppressWarnings("unchecked")
+ private <T> void sendInternal(T data, MediaType mediaType) throws IOException {
+ for (HttpMessageConverter<?> converter : ResponseBodyEmitterReturnValueHandler.this.messageConverters) {
+ if (converter.canWrite(data.getClass(), mediaType)) {
+ ((HttpMessageConverter<T>) converter).write(data, mediaType, this.outputMessage);
+ this.outputMessage.flush();
+ if (logger.isDebugEnabled()) {
+ logger.debug("Written [" + data + "] using [" + converter + "]");
+ }
+ return;
+ }
+ }
+ throw new IllegalArgumentException("No suitable converter for " + data.getClass());
+ }
+
+ @Override
+ public void complete() {
+ this.deferredResult.setResult(null);
+ }
+
+ @Override
+ public void completeWithError(Throwable failure) {
+ this.deferredResult.setErrorResult(failure);
+ }
+
+ @Override
+ public void onTimeout(Runnable callback) {
+ this.deferredResult.onTimeout(callback);
+ }
+
+ @Override
+ public void onCompletion(Runnable callback) {
+ this.deferredResult.onCompletion(callback);
+ }
+ }
+
+
+ /**
+ * Wrap to silently ignore header changes HttpMessageConverter's that would
+ * otherwise cause HttpHeaders to raise exceptions.
+ */
+ private static class StreamingServletServerHttpResponse implements ServerHttpResponse {
+
+ private final ServerHttpResponse delegate;
+
+ private final HttpHeaders mutableHeaders = new HttpHeaders();
+
+ public StreamingServletServerHttpResponse(ServerHttpResponse delegate) {
+ this.delegate = delegate;
+ this.mutableHeaders.putAll(delegate.getHeaders());
+ }
+
+ @Override
+ public void setStatusCode(HttpStatus status) {
+ this.delegate.setStatusCode(status);
+ }
+
+ @Override
+ public HttpHeaders getHeaders() {
+ return this.mutableHeaders;
+ }
+
+ @Override
+ public OutputStream getBody() throws IOException {
+ return this.delegate.getBody();
+ }
+
+ @Override
+ public void flush() throws IOException {
+ this.delegate.flush();
+ }
+
+ @Override
+ public void close() {
+ this.delegate.close();
+ }
+ }
+
+}
diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ResponseEntityExceptionHandler.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ResponseEntityExceptionHandler.java
index 7ac762cc..0be4cb9b 100644
--- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ResponseEntityExceptionHandler.java
+++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ResponseEntityExceptionHandler.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2013 the original author or authors.
+ * Copyright 2002-2016 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.
@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+
package org.springframework.web.servlet.mvc.method.annotation;
import java.util.List;
@@ -37,6 +38,7 @@ import org.springframework.web.HttpMediaTypeNotAcceptableException;
import org.springframework.web.HttpMediaTypeNotSupportedException;
import org.springframework.web.HttpRequestMethodNotSupportedException;
import org.springframework.web.bind.MethodArgumentNotValidException;
+import org.springframework.web.bind.MissingPathVariableException;
import org.springframework.web.bind.MissingServletRequestParameterException;
import org.springframework.web.bind.ServletRequestBindingException;
import org.springframework.web.bind.annotation.ControllerAdvice;
@@ -45,34 +47,35 @@ import org.springframework.web.context.request.WebRequest;
import org.springframework.web.multipart.support.MissingServletRequestPartException;
import org.springframework.web.servlet.NoHandlerFoundException;
import org.springframework.web.servlet.mvc.multiaction.NoSuchRequestHandlingMethodException;
+import org.springframework.web.util.WebUtils;
/**
* A convenient base class for {@link ControllerAdvice @ControllerAdvice} classes
* that wish to provide centralized exception handling across all
* {@code @RequestMapping} methods through {@code @ExceptionHandler} methods.
*
- * <p>This base class provides an {@code @ExceptionHandler} for handling standard
- * Spring MVC exceptions that returns a {@code ResponseEntity} to be written with
- * {@link HttpMessageConverter message converters}. This is in contrast to
+ * <p>This base class provides an {@code @ExceptionHandler} method for handling
+ * internal Spring MVC exceptions. This method returns a {@code ResponseEntity}
+ * for writing to the response with a {@link HttpMessageConverter message converter}.
+ * in contrast to
* {@link org.springframework.web.servlet.mvc.support.DefaultHandlerExceptionResolver
- * DefaultHandlerExceptionResolver} which returns a {@code ModelAndView} instead.
+ * DefaultHandlerExceptionResolver} which returns a
+ * {@link org.springframework.web.servlet.ModelAndView ModelAndView}.
*
- * <p>If there is no need to write error content to the response body, or if using
- * view resolution, e.g. {@code ContentNegotiatingViewResolver}, then use
- * {@code DefaultHandlerExceptionResolver} instead.
+ * <p>If there is no need to write error content to the response body, or when
+ * using view resolution (e.g., via {@code ContentNegotiatingViewResolver}),
+ * then {@code DefaultHandlerExceptionResolver} is good enough.
*
* <p>Note that in order for an {@code @ControllerAdvice} sub-class to be
* detected, {@link ExceptionHandlerExceptionResolver} must be configured.
*
* @author Rossen Stoyanchev
* @since 3.2
- *
+ * @see #handleException(Exception, WebRequest)
* @see org.springframework.web.servlet.mvc.support.DefaultHandlerExceptionResolver
*/
public abstract class ResponseEntityExceptionHandler {
- protected final Log logger = LogFactory.getLog(getClass());
-
/**
* Log category to use when no mapped handler is found for a request.
* @see #pageNotFoundLogger
@@ -80,22 +83,28 @@ public abstract class ResponseEntityExceptionHandler {
public static final String PAGE_NOT_FOUND_LOG_CATEGORY = "org.springframework.web.servlet.PageNotFound";
/**
- * Additional logger to use when no mapped handler is found for a request.
+ * Specific logger to use when no mapped handler is found for a request.
* @see #PAGE_NOT_FOUND_LOG_CATEGORY
*/
protected static final Log pageNotFoundLogger = LogFactory.getLog(PAGE_NOT_FOUND_LOG_CATEGORY);
+ /**
+ * Common logger for use in subclasses.
+ */
+ protected final Log logger = LogFactory.getLog(getClass());
+
/**
* Provides handling for standard Spring MVC exceptions.
* @param ex the target exception
* @param request the current request
*/
- @ExceptionHandler(value={
+ @ExceptionHandler({
NoSuchRequestHandlingMethodException.class,
HttpRequestMethodNotSupportedException.class,
HttpMediaTypeNotSupportedException.class,
HttpMediaTypeNotAcceptableException.class,
+ MissingPathVariableException.class,
MissingServletRequestParameterException.class,
ServletRequestBindingException.class,
ConversionNotSupportedException.class,
@@ -108,9 +117,7 @@ public abstract class ResponseEntityExceptionHandler {
NoHandlerFoundException.class
})
public final ResponseEntity<Object> handleException(Exception ex, WebRequest request) {
-
HttpHeaders headers = new HttpHeaders();
-
if (ex instanceof NoSuchRequestHandlingMethodException) {
HttpStatus status = HttpStatus.NOT_FOUND;
return handleNoSuchRequestHandlingMethod((NoSuchRequestHandlingMethodException) ex, headers, status, request);
@@ -127,6 +134,10 @@ public abstract class ResponseEntityExceptionHandler {
HttpStatus status = HttpStatus.NOT_ACCEPTABLE;
return handleHttpMediaTypeNotAcceptable((HttpMediaTypeNotAcceptableException) ex, headers, status, request);
}
+ else if (ex instanceof MissingPathVariableException) {
+ HttpStatus status = HttpStatus.INTERNAL_SERVER_ERROR;
+ return handleMissingPathVariable((MissingPathVariableException) ex, headers, status, request);
+ }
else if (ex instanceof MissingServletRequestParameterException) {
HttpStatus status = HttpStatus.BAD_REQUEST;
return handleMissingServletRequestParameter((MissingServletRequestParameterException) ex, headers, status, request);
@@ -176,27 +187,27 @@ public abstract class ResponseEntityExceptionHandler {
/**
* A single place to customize the response body of all Exception types.
- * This method returns {@code null} by default.
+ * <p>The default implementation sets the {@link WebUtils#ERROR_EXCEPTION_ATTRIBUTE}
+ * request attribute and creates a {@link ResponseEntity} from the given
+ * body, headers, and status.
* @param ex the exception
- * @param body the body to use for the response
- * @param headers the headers to be written to the response
- * @param status the selected response status
+ * @param body the body for the response
+ * @param headers the headers for the response
+ * @param status the response status
* @param request the current request
*/
protected ResponseEntity<Object> handleExceptionInternal(Exception ex, Object body,
HttpHeaders headers, HttpStatus status, WebRequest request) {
if (HttpStatus.INTERNAL_SERVER_ERROR.equals(status)) {
- request.setAttribute("javax.servlet.error.exception", ex, WebRequest.SCOPE_REQUEST);
+ request.setAttribute(WebUtils.ERROR_EXCEPTION_ATTRIBUTE, ex, WebRequest.SCOPE_REQUEST);
}
-
return new ResponseEntity<Object>(body, headers, status);
}
/**
* Customize the response for NoSuchRequestHandlingMethodException.
- * This method logs a warning and delegates to
- * {@link #handleExceptionInternal(Exception, Object, HttpHeaders, HttpStatus, WebRequest)}.
+ * <p>This method logs a warning and delegates to {@link #handleExceptionInternal}.
* @param ex the exception
* @param headers the headers to be written to the response
* @param status the selected response status
@@ -213,8 +224,8 @@ public abstract class ResponseEntityExceptionHandler {
/**
* Customize the response for HttpRequestMethodNotSupportedException.
- * This method logs a warning, sets the "Allow" header, and delegates to
- * {@link #handleExceptionInternal(Exception, Object, HttpHeaders, HttpStatus, WebRequest)}.
+ * <p>This method logs a warning, sets the "Allow" header, and delegates to
+ * {@link #handleExceptionInternal}.
* @param ex the exception
* @param headers the headers to be written to the response
* @param status the selected response status
@@ -230,14 +241,13 @@ public abstract class ResponseEntityExceptionHandler {
if (!supportedMethods.isEmpty()) {
headers.setAllow(supportedMethods);
}
-
return handleExceptionInternal(ex, null, headers, status, request);
}
/**
* Customize the response for HttpMediaTypeNotSupportedException.
- * This method sets the "Accept" header and delegates to
- * {@link #handleExceptionInternal(Exception, Object, HttpHeaders, HttpStatus, WebRequest)}.
+ * <p>This method sets the "Accept" header and delegates to
+ * {@link #handleExceptionInternal}.
* @param ex the exception
* @param headers the headers to be written to the response
* @param status the selected response status
@@ -257,7 +267,7 @@ public abstract class ResponseEntityExceptionHandler {
/**
* Customize the response for HttpMediaTypeNotAcceptableException.
- * This method delegates to {@link #handleExceptionInternal(Exception, Object, HttpHeaders, HttpStatus, WebRequest)}.
+ * <p>This method delegates to {@link #handleExceptionInternal}.
* @param ex the exception
* @param headers the headers to be written to the response
* @param status the selected response status
@@ -271,8 +281,24 @@ public abstract class ResponseEntityExceptionHandler {
}
/**
+ * Customize the response for MissingPathVariableException.
+ * <p>This method delegates to {@link #handleExceptionInternal}.
+ * @param ex the exception
+ * @param headers the headers to be written to the response
+ * @param status the selected response status
+ * @param request the current request
+ * @return a {@code ResponseEntity} instance
+ * @since 4.2
+ */
+ protected ResponseEntity<Object> handleMissingPathVariable(MissingPathVariableException ex,
+ HttpHeaders headers, HttpStatus status, WebRequest request) {
+
+ return handleExceptionInternal(ex, null, headers, status, request);
+ }
+
+ /**
* Customize the response for MissingServletRequestParameterException.
- * This method delegates to {@link #handleExceptionInternal(Exception, Object, HttpHeaders, HttpStatus, WebRequest)}.
+ * <p>This method delegates to {@link #handleExceptionInternal}.
* @param ex the exception
* @param headers the headers to be written to the response
* @param status the selected response status
@@ -287,7 +313,7 @@ public abstract class ResponseEntityExceptionHandler {
/**
* Customize the response for ServletRequestBindingException.
- * This method delegates to {@link #handleExceptionInternal(Exception, Object, HttpHeaders, HttpStatus, WebRequest)}.
+ * <p>This method delegates to {@link #handleExceptionInternal}.
* @param ex the exception
* @param headers the headers to be written to the response
* @param status the selected response status
@@ -302,7 +328,7 @@ public abstract class ResponseEntityExceptionHandler {
/**
* Customize the response for ConversionNotSupportedException.
- * This method delegates to {@link #handleExceptionInternal(Exception, Object, HttpHeaders, HttpStatus, WebRequest)}.
+ * <p>This method delegates to {@link #handleExceptionInternal}.
* @param ex the exception
* @param headers the headers to be written to the response
* @param status the selected response status
@@ -317,7 +343,7 @@ public abstract class ResponseEntityExceptionHandler {
/**
* Customize the response for TypeMismatchException.
- * This method delegates to {@link #handleExceptionInternal(Exception, Object, HttpHeaders, HttpStatus, WebRequest)}.
+ * <p>This method delegates to {@link #handleExceptionInternal}.
* @param ex the exception
* @param headers the headers to be written to the response
* @param status the selected response status
@@ -332,7 +358,7 @@ public abstract class ResponseEntityExceptionHandler {
/**
* Customize the response for HttpMessageNotReadableException.
- * This method delegates to {@link #handleExceptionInternal(Exception, Object, HttpHeaders, HttpStatus, WebRequest)}.
+ * <p>This method delegates to {@link #handleExceptionInternal}.
* @param ex the exception
* @param headers the headers to be written to the response
* @param status the selected response status
@@ -347,7 +373,7 @@ public abstract class ResponseEntityExceptionHandler {
/**
* Customize the response for HttpMessageNotWritableException.
- * This method delegates to {@link #handleExceptionInternal(Exception, Object, HttpHeaders, HttpStatus, WebRequest)}.
+ * <p>This method delegates to {@link #handleExceptionInternal}.
* @param ex the exception
* @param headers the headers to be written to the response
* @param status the selected response status
@@ -362,7 +388,7 @@ public abstract class ResponseEntityExceptionHandler {
/**
* Customize the response for MethodArgumentNotValidException.
- * This method delegates to {@link #handleExceptionInternal(Exception, Object, HttpHeaders, HttpStatus, WebRequest)}.
+ * <p>This method delegates to {@link #handleExceptionInternal}.
* @param ex the exception
* @param headers the headers to be written to the response
* @param status the selected response status
@@ -377,7 +403,7 @@ public abstract class ResponseEntityExceptionHandler {
/**
* Customize the response for MissingServletRequestPartException.
- * This method delegates to {@link #handleExceptionInternal(Exception, Object, HttpHeaders, HttpStatus, WebRequest)}.
+ * <p>This method delegates to {@link #handleExceptionInternal}.
* @param ex the exception
* @param headers the headers to be written to the response
* @param status the selected response status
@@ -392,7 +418,7 @@ public abstract class ResponseEntityExceptionHandler {
/**
* Customize the response for BindException.
- * This method delegates to {@link #handleExceptionInternal(Exception, Object, HttpHeaders, HttpStatus, WebRequest)}.
+ * <p>This method delegates to {@link #handleExceptionInternal}.
* @param ex the exception
* @param headers the headers to be written to the response
* @param status the selected response status
@@ -407,7 +433,7 @@ public abstract class ResponseEntityExceptionHandler {
/**
* Customize the response for NoHandlerFoundException.
- * This method delegates to {@link #handleExceptionInternal(Exception, Object, HttpHeaders, HttpStatus, WebRequest)}.
+ * <p>This method delegates to {@link #handleExceptionInternal}.
* @param ex the exception
* @param headers the headers to be written to the response
* @param status the selected response status
@@ -415,8 +441,8 @@ public abstract class ResponseEntityExceptionHandler {
* @return a {@code ResponseEntity} instance
* @since 4.0
*/
- protected ResponseEntity<Object> handleNoHandlerFoundException(NoHandlerFoundException ex, HttpHeaders headers,
- HttpStatus status, WebRequest request) {
+ protected ResponseEntity<Object> handleNoHandlerFoundException(
+ NoHandlerFoundException ex, HttpHeaders headers, HttpStatus status, WebRequest request) {
return handleExceptionInternal(ex, null, headers, status, request);
}
diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ServletInvocableHandlerMethod.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ServletInvocableHandlerMethod.java
index d48b6502..05cbba3c 100644
--- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ServletInvocableHandlerMethod.java
+++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ServletInvocableHandlerMethod.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2014 the original author or authors.
+ * Copyright 2002-2015 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.
@@ -83,7 +83,7 @@ public class ServletInvocableHandlerMethod extends InvocableHandlerMethod {
private void initResponseStatus() {
ResponseStatus annotation = getMethodAnnotation(ResponseStatus.class);
if (annotation != null) {
- this.responseStatus = annotation.value();
+ this.responseStatus = annotation.code();
this.responseReason = annotation.reason();
}
}
@@ -260,7 +260,19 @@ public class ServletInvocableHandlerMethod extends InvocableHandlerMethod {
@Override
public Class<?> getParameterType() {
- return (this.returnValue != null ? this.returnValue.getClass() : this.returnType.getRawClass());
+ if (this.returnValue != null) {
+ return this.returnValue.getClass();
+ }
+ Class<?> parameterType = super.getParameterType();
+ if (ResponseBodyEmitter.class.isAssignableFrom(parameterType) ||
+ StreamingResponseBody.class.isAssignableFrom(parameterType)) {
+ return parameterType;
+ }
+ if (ResolvableType.NONE.equals(this.returnType)) {
+ throw new IllegalArgumentException("Expected one of Callable, DeferredResult, or ListenableFuture: " +
+ super.getParameterType());
+ }
+ return this.returnType.getRawClass();
}
@Override
diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ServletRequestMethodArgumentResolver.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ServletRequestMethodArgumentResolver.java
index 6c0db89c..1ec50e65 100644
--- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ServletRequestMethodArgumentResolver.java
+++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ServletRequestMethodArgumentResolver.java
@@ -69,12 +69,12 @@ public class ServletRequestMethodArgumentResolver implements HandlerMethodArgume
MultipartRequest.class.isAssignableFrom(paramType) ||
HttpSession.class.isAssignableFrom(paramType) ||
Principal.class.isAssignableFrom(paramType) ||
- Locale.class.equals(paramType) ||
- TimeZone.class.equals(paramType) ||
+ Locale.class == paramType ||
+ TimeZone.class == paramType ||
"java.time.ZoneId".equals(paramType.getName()) ||
InputStream.class.isAssignableFrom(paramType) ||
Reader.class.isAssignableFrom(paramType) ||
- HttpMethod.class.equals(paramType));
+ HttpMethod.class == paramType);
}
@Override
@@ -98,16 +98,16 @@ public class ServletRequestMethodArgumentResolver implements HandlerMethodArgume
else if (HttpSession.class.isAssignableFrom(paramType)) {
return request.getSession();
}
- else if (HttpMethod.class.equals(paramType)) {
+ else if (HttpMethod.class == paramType) {
return ((ServletWebRequest) webRequest).getHttpMethod();
}
else if (Principal.class.isAssignableFrom(paramType)) {
return request.getUserPrincipal();
}
- else if (Locale.class.equals(paramType)) {
+ else if (Locale.class == paramType) {
return RequestContextUtils.getLocale(request);
}
- else if (TimeZone.class.equals(paramType)) {
+ else if (TimeZone.class == paramType) {
TimeZone timeZone = RequestContextUtils.getTimeZone(request);
return (timeZone != null ? timeZone : TimeZone.getDefault());
}
diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/SseEmitter.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/SseEmitter.java
new file mode 100644
index 00000000..4bc3267e
--- /dev/null
+++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/SseEmitter.java
@@ -0,0 +1,251 @@
+/*
+ * Copyright 2002-2015 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.mvc.method.annotation;
+
+import java.io.IOException;
+import java.nio.charset.Charset;
+import java.util.Collections;
+import java.util.LinkedHashSet;
+import java.util.Set;
+
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.MediaType;
+import org.springframework.http.server.ServerHttpResponse;
+
+/**
+ * A specialization of {@link ResponseBodyEmitter} for sending
+ * <a href="http://www.w3.org/TR/eventsource/">Server-Sent Events</a>.
+ *
+ * @author Rossen Stoyanchev
+ * @author Juergen Hoeller
+ * @since 4.2
+ */
+public class SseEmitter extends ResponseBodyEmitter {
+
+ static final MediaType TEXT_PLAIN = new MediaType("text", "plain", Charset.forName("UTF-8"));
+
+
+ /**
+ * Create a new SseEmitter instance.
+ */
+ public SseEmitter() {
+ super();
+ }
+
+ /**
+ * Create a SseEmitter with a custom timeout value.
+ * <p>By default not set in which case the default configured in the MVC
+ * Java Config or the MVC namespace is used, or if that's not set, then the
+ * timeout depends on the default of the underlying server.
+ * @param timeout timeout value in milliseconds
+ * @since 4.2.2
+ */
+ public SseEmitter(Long timeout) {
+ super(timeout);
+ }
+
+
+ @Override
+ protected void extendResponse(ServerHttpResponse outputMessage) {
+ super.extendResponse(outputMessage);
+
+ HttpHeaders headers = outputMessage.getHeaders();
+ if (headers.getContentType() == null) {
+ headers.setContentType(new MediaType("text", "event-stream"));
+ }
+ }
+
+ /**
+ * Send the object formatted as a single SSE "data" line. It's equivalent to:
+ * <pre>
+ * // static import of SseEmitter.*
+ *
+ * SseEmitter emitter = new SseEmitter();
+ * emitter.send(event().data(myObject));
+ * </pre>
+ * @param object the object to write
+ * @throws IOException raised when an I/O error occurs
+ * @throws java.lang.IllegalStateException wraps any other errors
+ */
+ @Override
+ public void send(Object object) throws IOException {
+ send(object, null);
+ }
+
+ /**
+ * Send the object formatted as a single SSE "data" line. It's equivalent to:
+ * <pre>
+ * // static import of SseEmitter.*
+ *
+ * SseEmitter emitter = new SseEmitter();
+ * emitter.send(event().data(myObject, MediaType.APPLICATION_JSON));
+ * </pre>
+ * @param object the object to write
+ * @param mediaType a MediaType hint for selecting an HttpMessageConverter
+ * @throws IOException raised when an I/O error occurs
+ */
+ @Override
+ public void send(Object object, MediaType mediaType) throws IOException {
+ if (object != null) {
+ send(event().data(object, mediaType));
+ }
+ }
+
+ /**
+ * Send an SSE event prepared with the given builder. For example:
+ * <pre>
+ * // static import of SseEmitter
+ *
+ * SseEmitter emitter = new SseEmitter();
+ * emitter.send(event().name("update").id("1").data(myObject));
+ * </pre>
+ * @param builder a builder for an SSE formatted event.
+ * @throws IOException raised when an I/O error occurs
+ */
+ public void send(SseEventBuilder builder) throws IOException {
+ Set<DataWithMediaType> dataToSend = builder.build();
+ synchronized (this) {
+ for (DataWithMediaType entry : dataToSend) {
+ super.send(entry.getData(), entry.getMediaType());
+ }
+ }
+ }
+
+
+ public static SseEventBuilder event() {
+ return new SseEventBuilderImpl();
+ }
+
+
+ /**
+ * A builder for an SSE event.
+ */
+ public interface SseEventBuilder {
+
+ /**
+ * Add an SSE "comment" line.
+ */
+ SseEventBuilder comment(String comment);
+
+ /**
+ * Add an SSE "event" line.
+ */
+ SseEventBuilder name(String eventName);
+
+ /**
+ * Add an SSE "id" line.
+ */
+ SseEventBuilder id(String id);
+
+ /**
+ * Add an SSE "event" line.
+ */
+ SseEventBuilder reconnectTime(long reconnectTimeMillis);
+
+ /**
+ * Add an SSE "data" line.
+ */
+ SseEventBuilder data(Object object);
+
+ /**
+ * Add an SSE "data" line.
+ */
+ SseEventBuilder data(Object object, MediaType mediaType);
+
+ /**
+ * Return one or more Object-MediaType pairs to write via
+ * {@link #send(Object, MediaType)}.
+ * @since 4.2.3
+ */
+ Set<DataWithMediaType> build();
+ }
+
+
+ /**
+ * Default implementation of SseEventBuilder.
+ */
+ private static class SseEventBuilderImpl implements SseEventBuilder {
+
+ private final Set<DataWithMediaType> dataToSend = new LinkedHashSet<DataWithMediaType>(4);
+
+ private StringBuilder sb;
+
+ @Override
+ public SseEventBuilder comment(String comment) {
+ append(":").append(comment != null ? comment : "").append("\n");
+ return this;
+ }
+
+ @Override
+ public SseEventBuilder name(String name) {
+ append("event:").append(name != null ? name : "").append("\n");
+ return this;
+ }
+
+ @Override
+ public SseEventBuilder id(String id) {
+ append("id:").append(id != null ? id : "").append("\n");
+ return this;
+ }
+
+ @Override
+ public SseEventBuilder reconnectTime(long reconnectTimeMillis) {
+ append("retry:").append(String.valueOf(reconnectTimeMillis)).append("\n");
+ return this;
+ }
+
+ @Override
+ public SseEventBuilder data(Object object) {
+ return data(object, null);
+ }
+
+ @Override
+ public SseEventBuilder data(Object object, MediaType mediaType) {
+ append("data:");
+ saveAppendedText();
+ this.dataToSend.add(new DataWithMediaType(object, mediaType));
+ append("\n");
+ return this;
+ }
+
+ SseEventBuilderImpl append(String text) {
+ if (this.sb == null) {
+ this.sb = new StringBuilder();
+ }
+ this.sb.append(text);
+ return this;
+ }
+
+ @Override
+ public Set<DataWithMediaType> build() {
+ if ((this.sb == null || this.sb.length() == 0) && this.dataToSend.isEmpty()) {
+ return Collections.<DataWithMediaType>emptySet();
+ }
+ append("\n");
+ saveAppendedText();
+ return this.dataToSend;
+ }
+
+ private void saveAppendedText() {
+ if (this.sb != null) {
+ this.dataToSend.add(new DataWithMediaType(this.sb.toString(), TEXT_PLAIN));
+ this.sb = null;
+ }
+ }
+ }
+
+}
diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/StreamingResponseBody.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/StreamingResponseBody.java
new file mode 100644
index 00000000..a3a4240c
--- /dev/null
+++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/StreamingResponseBody.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2002-2015 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.mvc.method.annotation;
+
+
+import java.io.IOException;
+import java.io.OutputStream;
+
+/**
+ * A controller method return value type for asynchronous request processing
+ * where the application can write directly to the response {@code OutputStream}
+ * without holding up the Servlet container thread.
+ *
+ * <p><strong>Note:</strong> when using this option it is highly recommended to
+ * configure explicitly the TaskExecutor used in Spring MVC for executing
+ * asynchronous requests. Both the MVC Java config and the MVC namespaces provide
+ * options to configure asynchronous handling. If not using those, an application
+ * can set the {@code taskExecutor} property of
+ * {@link org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter
+ * RequestMappingHandlerAdapter}.
+ *
+ * @author Rossen Stoyanchev
+ * @since 4.2
+ */
+public interface StreamingResponseBody {
+
+ /**
+ * A callback for writing to the response body.
+ * @param outputStream the stream for the response body
+ * @throws IOException an exception while writing
+ */
+ void writeTo(OutputStream outputStream) throws IOException;
+
+}
diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/StreamingResponseBodyReturnValueHandler.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/StreamingResponseBodyReturnValueHandler.java
new file mode 100644
index 00000000..40852899
--- /dev/null
+++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/StreamingResponseBodyReturnValueHandler.java
@@ -0,0 +1,113 @@
+/*
+ * Copyright 2002-2015 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.mvc.method.annotation;
+
+import java.io.OutputStream;
+import java.util.concurrent.Callable;
+
+import javax.servlet.ServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.springframework.core.MethodParameter;
+import org.springframework.core.ResolvableType;
+import org.springframework.http.ResponseEntity;
+import org.springframework.http.server.ServerHttpResponse;
+import org.springframework.http.server.ServletServerHttpResponse;
+import org.springframework.util.Assert;
+import org.springframework.web.context.request.NativeWebRequest;
+import org.springframework.web.context.request.async.WebAsyncUtils;
+import org.springframework.web.filter.ShallowEtagHeaderFilter;
+import org.springframework.web.method.support.HandlerMethodReturnValueHandler;
+import org.springframework.web.method.support.ModelAndViewContainer;
+
+
+/**
+ * Supports return values of type
+ * {@link org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody}
+ * and also {@code ResponseEntity<StreamingResponseBody>}.
+ *
+ * @author Rossen Stoyanchev
+ * @since 4.2
+ */
+public class StreamingResponseBodyReturnValueHandler implements HandlerMethodReturnValueHandler {
+
+ @Override
+ public boolean supportsReturnType(MethodParameter returnType) {
+ if (StreamingResponseBody.class.isAssignableFrom(returnType.getParameterType())) {
+ return true;
+ }
+ else if (ResponseEntity.class.isAssignableFrom(returnType.getParameterType())) {
+ Class<?> bodyType = ResolvableType.forMethodParameter(returnType).getGeneric(0).resolve();
+ return (bodyType != null && StreamingResponseBody.class.isAssignableFrom(bodyType));
+ }
+ return false;
+ }
+
+ @Override
+ public void handleReturnValue(Object returnValue, MethodParameter returnType,
+ ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception {
+
+ if (returnValue == null) {
+ mavContainer.setRequestHandled(true);
+ return;
+ }
+
+ HttpServletResponse response = webRequest.getNativeResponse(HttpServletResponse.class);
+ ServerHttpResponse outputMessage = new ServletServerHttpResponse(response);
+
+ if (ResponseEntity.class.isAssignableFrom(returnValue.getClass())) {
+ ResponseEntity<?> responseEntity = (ResponseEntity<?>) returnValue;
+ outputMessage.setStatusCode(responseEntity.getStatusCode());
+ outputMessage.getHeaders().putAll(responseEntity.getHeaders());
+
+ returnValue = responseEntity.getBody();
+ if (returnValue == null) {
+ mavContainer.setRequestHandled(true);
+ return;
+ }
+ }
+
+ ServletRequest request = webRequest.getNativeRequest(ServletRequest.class);
+ ShallowEtagHeaderFilter.disableContentCaching(request);
+
+ Assert.isInstanceOf(StreamingResponseBody.class, returnValue);
+ StreamingResponseBody streamingBody = (StreamingResponseBody) returnValue;
+
+ Callable<Void> callable = new StreamingResponseBodyTask(outputMessage.getBody(), streamingBody);
+ WebAsyncUtils.getAsyncManager(webRequest).startCallableProcessing(callable, mavContainer);
+ }
+
+
+ private static class StreamingResponseBodyTask implements Callable<Void> {
+
+ private final OutputStream outputStream;
+
+ private final StreamingResponseBody streamingBody;
+
+
+ public StreamingResponseBodyTask(OutputStream outputStream, StreamingResponseBody streamingBody) {
+ this.outputStream = outputStream;
+ this.streamingBody = streamingBody;
+ }
+
+ @Override
+ public Void call() throws Exception {
+ this.streamingBody.writeTo(this.outputStream);
+ return null;
+ }
+ }
+
+}
diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/UriComponentsBuilderMethodArgumentResolver.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/UriComponentsBuilderMethodArgumentResolver.java
index 74e632f2..d8d32084 100644
--- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/UriComponentsBuilderMethodArgumentResolver.java
+++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/UriComponentsBuilderMethodArgumentResolver.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2014 the original author or authors.
+ * Copyright 2002-2015 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.
@@ -26,6 +26,7 @@ import org.springframework.web.method.support.ModelAndViewContainer;
import org.springframework.web.servlet.support.ServletUriComponentsBuilder;
import org.springframework.web.util.UriComponentsBuilder;
+
/**
* Resolvers argument values of type {@link UriComponentsBuilder}.
*
@@ -37,9 +38,11 @@ import org.springframework.web.util.UriComponentsBuilder;
*/
public class UriComponentsBuilderMethodArgumentResolver implements HandlerMethodArgumentResolver {
+
@Override
public boolean supportsParameter(MethodParameter parameter) {
- return UriComponentsBuilder.class.isAssignableFrom(parameter.getParameterType());
+ Class<?> type = parameter.getParameterType();
+ return (UriComponentsBuilder.class == type || ServletUriComponentsBuilder.class == type);
}
@Override
diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ViewNameMethodReturnValueHandler.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ViewNameMethodReturnValueHandler.java
index 65919d22..9601ce60 100644
--- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ViewNameMethodReturnValueHandler.java
+++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ViewNameMethodReturnValueHandler.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2014 the original author or authors.
+ * Copyright 2002-2015 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.
@@ -24,8 +24,9 @@ import org.springframework.web.method.support.ModelAndViewContainer;
import org.springframework.web.servlet.RequestToViewNameTranslator;
/**
- * Handles return values of types {@code void} and {@code String} interpreting
- * them as view name reference.
+ * Handles return values of types {@code void} and {@code String} interpreting them
+ * as view name reference. As of 4.2, it also handles general {@code CharSequence}
+ * types, e.g. {@code StringBuilder} or Groovy's {@code GString}, as view names.
*
* <p>A {@code null} return value, either due to a {@code void} return type or
* as the actual return value is left as-is allowing the configured
@@ -37,6 +38,7 @@ import org.springframework.web.servlet.RequestToViewNameTranslator;
* the handlers that support these annotations.
*
* @author Rossen Stoyanchev
+ * @author Juergen Hoeller
* @since 3.1
*/
public class ViewNameMethodReturnValueHandler implements HandlerMethodReturnValueHandler {
@@ -68,24 +70,21 @@ public class ViewNameMethodReturnValueHandler implements HandlerMethodReturnValu
@Override
public boolean supportsReturnType(MethodParameter returnType) {
Class<?> paramType = returnType.getParameterType();
- return (void.class.equals(paramType) || String.class.equals(paramType));
+ return (void.class == paramType || CharSequence.class.isAssignableFrom(paramType));
}
@Override
public void handleReturnValue(Object returnValue, MethodParameter returnType,
ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception {
- if (returnValue == null) {
- return;
- }
- else if (returnValue instanceof String) {
- String viewName = (String) returnValue;
+ if (returnValue instanceof CharSequence) {
+ String viewName = returnValue.toString();
mavContainer.setViewName(viewName);
if (isRedirectViewName(viewName)) {
mavContainer.setRedirectModelScenario(true);
}
}
- else {
+ else if (returnValue != null){
// should not happen
throw new UnsupportedOperationException("Unexpected return type: " +
returnType.getParameterType().getName() + " in method: " + returnType.getMethod());
@@ -101,10 +100,7 @@ public class ViewNameMethodReturnValueHandler implements HandlerMethodReturnValu
* reference; "false" otherwise.
*/
protected boolean isRedirectViewName(String viewName) {
- if (PatternMatchUtils.simpleMatch(this.redirectPatterns, viewName)) {
- return true;
- }
- return viewName.startsWith("redirect:");
+ return (PatternMatchUtils.simpleMatch(this.redirectPatterns, viewName) || viewName.startsWith("redirect:"));
}
}
diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/multiaction/MultiActionController.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/multiaction/MultiActionController.java
index a47920ae..188c9667 100644
--- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/multiaction/MultiActionController.java
+++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/multiaction/MultiActionController.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2012 the original author or authors.
+ * Copyright 2002-2015 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.
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package org.springframework.web.servlet.mvc.multiaction;
+package org.springframework.web.servlet. mvc.multiaction;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
@@ -288,12 +288,12 @@ public class MultiActionController extends AbstractController implements LastMod
*/
private boolean isHandlerMethod(Method method) {
Class<?> returnType = method.getReturnType();
- if (ModelAndView.class.equals(returnType) || Map.class.equals(returnType) || String.class.equals(returnType) ||
- void.class.equals(returnType)) {
+ if (ModelAndView.class == returnType || Map.class == returnType || String.class == returnType ||
+ void.class == returnType) {
Class<?>[] parameterTypes = method.getParameterTypes();
return (parameterTypes.length >= 2 &&
- HttpServletRequest.class.equals(parameterTypes[0]) &&
- HttpServletResponse.class.equals(parameterTypes[1]) &&
+ HttpServletRequest.class == parameterTypes[0] &&
+ HttpServletResponse.class == parameterTypes[1] &&
!("handleRequest".equals(method.getName()) && parameterTypes.length == 2));
}
return false;
@@ -329,7 +329,7 @@ public class MultiActionController extends AbstractController implements LastMod
method.getName() + LAST_MODIFIED_METHOD_SUFFIX,
new Class<?>[] {HttpServletRequest.class});
Class<?> returnType = lastModifiedMethod.getReturnType();
- if (!(long.class.equals(returnType) || Long.class.equals(returnType))) {
+ if (!(long.class == returnType || Long.class == returnType)) {
throw new IllegalStateException("last-modified method [" + lastModifiedMethod +
"] declares an invalid return type - needs to be 'long' or 'Long'");
}
@@ -452,7 +452,7 @@ public class MultiActionController extends AbstractController implements LastMod
params.add(request);
params.add(response);
- if (paramTypes.length >= 3 && paramTypes[2].equals(HttpSession.class)) {
+ if (paramTypes.length >= 3 && HttpSession.class == paramTypes[2]) {
HttpSession session = request.getSession(false);
if (session == null) {
throw new HttpSessionRequiredException(
@@ -462,8 +462,7 @@ public class MultiActionController extends AbstractController implements LastMod
}
// If last parameter isn't of HttpSession type, it's a command.
- if (paramTypes.length >= 3 &&
- !paramTypes[paramTypes.length - 1].equals(HttpSession.class)) {
+ if (paramTypes.length >= 3 && HttpSession.class != paramTypes[paramTypes.length - 1]) {
Object command = newCommandObject(paramTypes[paramTypes.length - 1]);
params.add(command);
bind(request, command);
@@ -608,7 +607,7 @@ public class MultiActionController extends AbstractController implements LastMod
logger.debug("Trying to find handler for exception class [" + exceptionClass.getName() + "]");
}
Method handler = this.exceptionHandlerMap.get(exceptionClass);
- while (handler == null && !exceptionClass.equals(Throwable.class)) {
+ while (handler == null && exceptionClass != Throwable.class) {
if (logger.isDebugEnabled()) {
logger.debug("Trying to find handler for exception superclass [" + exceptionClass.getName() + "]");
}
diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/support/DefaultHandlerExceptionResolver.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/support/DefaultHandlerExceptionResolver.java
index 26a69b16..77290e56 100644
--- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/support/DefaultHandlerExceptionResolver.java
+++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/support/DefaultHandlerExceptionResolver.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2013 the original author or authors.
+ * Copyright 2002-2015 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.
@@ -38,6 +38,7 @@ import org.springframework.web.HttpMediaTypeNotAcceptableException;
import org.springframework.web.HttpMediaTypeNotSupportedException;
import org.springframework.web.HttpRequestMethodNotSupportedException;
import org.springframework.web.bind.MethodArgumentNotValidException;
+import org.springframework.web.bind.MissingPathVariableException;
import org.springframework.web.bind.MissingServletRequestParameterException;
import org.springframework.web.bind.ServletRequestBindingException;
import org.springframework.web.bind.annotation.ModelAttribute;
@@ -55,12 +56,13 @@ import org.springframework.web.servlet.mvc.multiaction.NoSuchRequestHandlingMeth
* HandlerExceptionResolver} interface that resolves standard Spring exceptions and translates
* them to corresponding HTTP status codes.
*
- * <p>This exception resolver is enabled by default in the {@link org.springframework.web.servlet.DispatcherServlet}.
+ * <p>This exception resolver is enabled by default in the common Spring
+ * {@link org.springframework.web.servlet.DispatcherServlet}.
*
* @author Arjen Poutsma
* @author Rossen Stoyanchev
+ * @author Juergen Hoeller
* @since 3.0
- *
* @see org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler
* @see #handleNoSuchRequestHandlingMethod
* @see #handleHttpRequestMethodNotSupported
@@ -119,6 +121,10 @@ public class DefaultHandlerExceptionResolver extends AbstractHandlerExceptionRes
return handleHttpMediaTypeNotAcceptable((HttpMediaTypeNotAcceptableException) ex, request, response,
handler);
}
+ else if (ex instanceof MissingPathVariableException) {
+ return handleMissingPathVariable((MissingPathVariableException) ex, request,
+ response, handler);
+ }
else if (ex instanceof MissingServletRequestParameterException) {
return handleMissingServletRequestParameter((MissingServletRequestParameterException) ex, request,
response, handler);
@@ -140,10 +146,12 @@ public class DefaultHandlerExceptionResolver extends AbstractHandlerExceptionRes
return handleHttpMessageNotWritable((HttpMessageNotWritableException) ex, request, response, handler);
}
else if (ex instanceof MethodArgumentNotValidException) {
- return handleMethodArgumentNotValidException((MethodArgumentNotValidException) ex, request, response, handler);
+ return handleMethodArgumentNotValidException((MethodArgumentNotValidException) ex, request, response,
+ handler);
}
else if (ex instanceof MissingServletRequestPartException) {
- return handleMissingServletRequestPartException((MissingServletRequestPartException) ex, request, response, handler);
+ return handleMissingServletRequestPartException((MissingServletRequestPartException) ex, request,
+ response, handler);
}
else if (ex instanceof BindException) {
return handleBindException((BindException) ex, request, response, handler);
@@ -153,7 +161,9 @@ public class DefaultHandlerExceptionResolver extends AbstractHandlerExceptionRes
}
}
catch (Exception handlerException) {
- logger.warn("Handling of [" + ex.getClass().getName() + "] resulted in Exception", handlerException);
+ if (logger.isWarnEnabled()) {
+ logger.warn("Handling of [" + ex.getClass().getName() + "] resulted in Exception", handlerException);
+ }
}
return null;
}
@@ -206,9 +216,10 @@ public class DefaultHandlerExceptionResolver extends AbstractHandlerExceptionRes
/**
* Handle the case where no {@linkplain org.springframework.http.converter.HttpMessageConverter message converters}
- * were found for the PUT or POSTed content. <p>The default implementation sends an HTTP 415 error,
- * sets the "Accept" header, and returns an empty {@code ModelAndView}. Alternatively, a fallback
- * view could be chosen, or the HttpMediaTypeNotSupportedException could be rethrown as-is.
+ * were found for the PUT or POSTed content.
+ * <p>The default implementation sends an HTTP 415 error, sets the "Accept" header,
+ * and returns an empty {@code ModelAndView}. Alternatively, a fallback view could
+ * be chosen, or the HttpMediaTypeNotSupportedException could be rethrown as-is.
* @param ex the HttpMediaTypeNotSupportedException to be handled
* @param request current HTTP request
* @param response current HTTP response
@@ -248,6 +259,26 @@ public class DefaultHandlerExceptionResolver extends AbstractHandlerExceptionRes
}
/**
+ * Handle the case when a declared path variable does not match any extracted URI variable.
+ * <p>The default implementation sends an HTTP 500 error, and returns an empty {@code ModelAndView}.
+ * Alternatively, a fallback view could be chosen, or the MissingPathVariableException
+ * could be rethrown as-is.
+ * @param ex the MissingPathVariableException to be handled
+ * @param request current HTTP request
+ * @param response current HTTP response
+ * @param handler the executed handler
+ * @return an empty ModelAndView indicating the exception was handled
+ * @throws IOException potentially thrown from response.sendError()
+ * @since 4.2
+ */
+ protected ModelAndView handleMissingPathVariable(MissingPathVariableException ex,
+ HttpServletRequest request, HttpServletResponse response, Object handler) throws IOException {
+
+ response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, ex.getMessage());
+ return new ModelAndView();
+ }
+
+ /**
* Handle the case when a required parameter is missing.
* <p>The default implementation sends an HTTP 400 error, and returns an empty {@code ModelAndView}.
* Alternatively, a fallback view could be chosen, or the MissingServletRequestParameterException
@@ -280,7 +311,7 @@ public class DefaultHandlerExceptionResolver extends AbstractHandlerExceptionRes
protected ModelAndView handleServletRequestBindingException(ServletRequestBindingException ex,
HttpServletRequest request, HttpServletResponse response, Object handler) throws IOException {
- response.sendError(HttpServletResponse.SC_BAD_REQUEST);
+ response.sendError(HttpServletResponse.SC_BAD_REQUEST, ex.getMessage());
return new ModelAndView();
}
@@ -298,22 +329,14 @@ public class DefaultHandlerExceptionResolver extends AbstractHandlerExceptionRes
protected ModelAndView handleConversionNotSupported(ConversionNotSupportedException ex,
HttpServletRequest request, HttpServletResponse response, Object handler) throws IOException {
+ if (logger.isWarnEnabled()) {
+ logger.warn("Failed to convert request element: " + ex);
+ }
sendServerError(ex, request, response);
return new ModelAndView();
}
/**
- * Invoked to send a server error. Sets the status to 500 and also sets the
- * request attribute "javax.servlet.error.exception" to the Exception.
- */
- protected void sendServerError(Exception ex,
- HttpServletRequest request, HttpServletResponse response) throws IOException {
-
- request.setAttribute("javax.servlet.error.exception", ex);
- response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
- }
-
- /**
* Handle the case when a {@link org.springframework.web.bind.WebDataBinder} conversion error occurs.
* <p>The default implementation sends an HTTP 400 error, and returns an empty {@code ModelAndView}.
* Alternatively, a fallback view could be chosen, or the TypeMismatchException could be rethrown as-is.
@@ -327,6 +350,9 @@ public class DefaultHandlerExceptionResolver extends AbstractHandlerExceptionRes
protected ModelAndView handleTypeMismatch(TypeMismatchException ex,
HttpServletRequest request, HttpServletResponse response, Object handler) throws IOException {
+ if (logger.isWarnEnabled()) {
+ logger.warn("Failed to bind request element: " + ex);
+ }
response.sendError(HttpServletResponse.SC_BAD_REQUEST);
return new ModelAndView();
}
@@ -347,6 +373,9 @@ public class DefaultHandlerExceptionResolver extends AbstractHandlerExceptionRes
protected ModelAndView handleHttpMessageNotReadable(HttpMessageNotReadableException ex,
HttpServletRequest request, HttpServletResponse response, Object handler) throws IOException {
+ if (logger.isWarnEnabled()) {
+ logger.warn("Failed to read HTTP message: " + ex);
+ }
response.sendError(HttpServletResponse.SC_BAD_REQUEST);
return new ModelAndView();
}
@@ -367,6 +396,9 @@ public class DefaultHandlerExceptionResolver extends AbstractHandlerExceptionRes
protected ModelAndView handleHttpMessageNotWritable(HttpMessageNotWritableException ex,
HttpServletRequest request, HttpServletResponse response, Object handler) throws IOException {
+ if (logger.isWarnEnabled()) {
+ logger.warn("Failed to write HTTP message: " + ex);
+ }
sendServerError(ex, request, response);
return new ModelAndView();
}
@@ -383,6 +415,7 @@ public class DefaultHandlerExceptionResolver extends AbstractHandlerExceptionRes
*/
protected ModelAndView handleMethodArgumentNotValidException(MethodArgumentNotValidException ex,
HttpServletRequest request, HttpServletResponse response, Object handler) throws IOException {
+
response.sendError(HttpServletResponse.SC_BAD_REQUEST);
return new ModelAndView();
}
@@ -399,6 +432,7 @@ public class DefaultHandlerExceptionResolver extends AbstractHandlerExceptionRes
*/
protected ModelAndView handleMissingServletRequestPartException(MissingServletRequestPartException ex,
HttpServletRequest request, HttpServletResponse response, Object handler) throws IOException {
+
response.sendError(HttpServletResponse.SC_BAD_REQUEST, ex.getMessage());
return new ModelAndView();
}
@@ -407,7 +441,7 @@ public class DefaultHandlerExceptionResolver extends AbstractHandlerExceptionRes
* Handle the case where an {@linkplain ModelAttribute @ModelAttribute} method
* argument has binding or validation errors and is not followed by another
* method argument of type {@link BindingResult}.
- * By default an HTTP 400 error is sent back to the client.
+ * By default, an HTTP 400 error is sent back to the client.
* @param request current HTTP request
* @param response current HTTP response
* @param handler the executed handler
@@ -416,14 +450,15 @@ public class DefaultHandlerExceptionResolver extends AbstractHandlerExceptionRes
*/
protected ModelAndView handleBindException(BindException ex, HttpServletRequest request,
HttpServletResponse response, Object handler) throws IOException {
+
response.sendError(HttpServletResponse.SC_BAD_REQUEST);
return new ModelAndView();
}
/**
* Handle the case where no handler was found during the dispatch.
- * <p>The default sends an HTTP 404 error, and returns
- * an empty {@code ModelAndView}. Alternatively, a fallback view could be chosen,
+ * <p>The default implementation sends an HTTP 404 error and returns an empty
+ * {@code ModelAndView}. Alternatively, a fallback view could be chosen,
* or the NoHandlerFoundException could be rethrown as-is.
* @param ex the NoHandlerFoundException to be handled
* @param request current HTTP request
@@ -434,10 +469,23 @@ public class DefaultHandlerExceptionResolver extends AbstractHandlerExceptionRes
* @throws IOException potentially thrown from response.sendError()
* @since 4.0
*/
- protected ModelAndView handleNoHandlerFoundException(NoHandlerFoundException ex, HttpServletRequest request,
- HttpServletResponse response, Object handler) throws IOException {
+ protected ModelAndView handleNoHandlerFoundException(NoHandlerFoundException ex,
+ HttpServletRequest request, HttpServletResponse response, Object handler) throws IOException {
+
response.sendError(HttpServletResponse.SC_NOT_FOUND);
return new ModelAndView();
}
+
+ /**
+ * Invoked to send a server error. Sets the status to 500 and also sets the
+ * request attribute "javax.servlet.error.exception" to the Exception.
+ */
+ protected void sendServerError(Exception ex, HttpServletRequest request, HttpServletResponse response)
+ throws IOException {
+
+ request.setAttribute("javax.servlet.error.exception", ex);
+ response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
+ }
+
}
diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/DefaultResourceResolverChain.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/DefaultResourceResolverChain.java
index 7bb638f2..501f2371 100644
--- a/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/DefaultResourceResolverChain.java
+++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/DefaultResourceResolverChain.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2015 the original author or authors.
+ * Copyright 2002-2016 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.
@@ -78,12 +78,11 @@ class DefaultResourceResolverChain implements ResourceResolverChain {
private ResourceResolver getNext() {
Assert.state(this.index <= this.resolvers.size(),
- "Current index exceeds the number of configured ResourceResolver's");
+ "Current index exceeds the number of configured ResourceResolvers");
if (this.index == (this.resolvers.size() - 1)) {
return null;
}
-
this.index++;
return this.resolvers.get(this.index);
}
diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/PathResourceResolver.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/PathResourceResolver.java
index fc964907..2119b7a7 100644
--- a/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/PathResourceResolver.java
+++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/PathResourceResolver.java
@@ -158,7 +158,7 @@ public class PathResourceResolver extends AbstractResourceResolver {
}
private boolean isResourceUnderLocation(Resource resource, Resource location) throws IOException {
- if (!resource.getClass().equals(location.getClass())) {
+ if (resource.getClass() != location.getClass()) {
return false;
}
diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/ResourceHttpRequestHandler.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/ResourceHttpRequestHandler.java
index d36c6d3f..f5319893 100644
--- a/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/ResourceHttpRequestHandler.java
+++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/ResourceHttpRequestHandler.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2015 the original author or authors.
+ * Copyright 2002-2016 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.
@@ -19,12 +19,14 @@ package org.springframework.web.servlet.resource;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
+import java.io.OutputStream;
import java.net.URLDecoder;
import java.util.ArrayList;
import java.util.List;
import javax.activation.FileTypeMap;
import javax.activation.MimetypesFileTypeMap;
import javax.servlet.ServletException;
+import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@@ -34,16 +36,22 @@ import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.HttpRange;
import org.springframework.http.MediaType;
+import org.springframework.http.server.ServletServerHttpRequest;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.CollectionUtils;
+import org.springframework.util.MimeTypeUtils;
import org.springframework.util.ObjectUtils;
import org.springframework.util.ResourceUtils;
import org.springframework.util.StreamUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.HttpRequestHandler;
import org.springframework.web.context.request.ServletWebRequest;
+import org.springframework.web.cors.CorsConfiguration;
+import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.servlet.HandlerMapping;
import org.springframework.web.servlet.support.WebContentGenerator;
@@ -55,8 +63,8 @@ import org.springframework.web.servlet.support.WebContentGenerator;
* <p>The {@linkplain #setLocations "locations" property} takes a list of Spring {@link Resource}
* locations from which static resources are allowed to be served by this handler. For a given request,
* the list of locations will be consulted in order for the presence of the requested resource, and the
- * first found match will be written to the response, with {@code Expires} and {@code Cache-Control}
- * headers set as configured. The handler also properly evaluates the {@code Last-Modified} header
+ * first found match will be written to the response, with a HTTP Caching headers
+ * set as configured. The handler also properly evaluates the {@code Last-Modified} header
* (if present) so that a {@code 304} status code will be returned as appropriate, avoiding unnecessary
* overhead for resources that are already cached by the client. The use of {@code Resource} locations
* allows resource requests to easily be mapped to locations other than the web application root.
@@ -80,11 +88,12 @@ import org.springframework.web.servlet.support.WebContentGenerator;
* @author Keith Donald
* @author Jeremy Grelle
* @author Juergen Hoeller
+ * @author Arjen Poutsma
+ * @author Brian Clozel
* @since 3.0.4
*/
-public class ResourceHttpRequestHandler extends WebContentGenerator implements HttpRequestHandler, InitializingBean {
-
- private static final String CONTENT_ENCODING = "Content-Encoding";
+public class ResourceHttpRequestHandler extends WebContentGenerator
+ implements HttpRequestHandler, InitializingBean, CorsConfigurationSource {
private static final Log logger = LogFactory.getLog(ResourceHttpRequestHandler.class);
@@ -98,6 +107,8 @@ public class ResourceHttpRequestHandler extends WebContentGenerator implements H
private final List<ResourceTransformer> resourceTransformers = new ArrayList<ResourceTransformer>(4);
+ private CorsConfiguration corsConfiguration;
+
public ResourceHttpRequestHandler() {
super(METHOD_GET, METHOD_HEAD);
@@ -156,6 +167,15 @@ public class ResourceHttpRequestHandler extends WebContentGenerator implements H
return this.resourceTransformers;
}
+ public void setCorsConfiguration(CorsConfiguration corsConfiguration) {
+ this.corsConfiguration = corsConfiguration;
+ }
+
+ @Override
+ public CorsConfiguration getCorsConfiguration(HttpServletRequest request) {
+ return this.corsConfiguration;
+ }
+
@Override
public void afterPropertiesSet() throws Exception {
@@ -167,17 +187,15 @@ public class ResourceHttpRequestHandler extends WebContentGenerator implements H
}
/**
- * Look for a {@link org.springframework.web.servlet.resource.PathResourceResolver}
- * among the {@link #getResourceResolvers() resource resolvers} and configure
- * its {@code "allowedLocations"} to match the value of the
- * {@link #setLocations(java.util.List) locations} property unless the "allowed
- * locations" of the {@code PathResourceResolver} is non-empty.
+ * Look for a {@code PathResourceResolver} among the configured resource
+ * resolvers and set its {@code allowedLocations} property (if empty) to
+ * match the {@link #setLocations locations} configured on this class.
*/
protected void initAllowedLocations() {
if (CollectionUtils.isEmpty(this.locations)) {
return;
}
- for (int i = getResourceResolvers().size()-1; i >= 0; i--) {
+ for (int i = getResourceResolvers().size() - 1; i >= 0; i--) {
if (getResourceResolvers().get(i) instanceof PathResourceResolver) {
PathResourceResolver pathResolver = (PathResourceResolver) getResourceResolvers().get(i);
if (ObjectUtils.isEmpty(pathResolver.getAllowedLocations())) {
@@ -188,6 +206,7 @@ public class ResourceHttpRequestHandler extends WebContentGenerator implements H
}
}
+
/**
* Processes a resource request.
* <p>Checks for the existence of the requested resource in the configured list of locations.
@@ -204,9 +223,10 @@ public class ResourceHttpRequestHandler extends WebContentGenerator implements H
public void handleRequest(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
- checkAndPrepare(request, response, true);
+ // Supported methods and required session
+ checkRequest(request);
- // check whether a matching resource exists
+ // Check whether a matching resource exists
Resource resource = getResource(request);
if (resource == null) {
logger.trace("No matching resource found - returning 404");
@@ -214,7 +234,16 @@ public class ResourceHttpRequestHandler extends WebContentGenerator implements H
return;
}
- // check the resource's media type
+ // Header phase
+ if (new ServletWebRequest(request, response).checkNotModified(resource.lastModified())) {
+ logger.trace("Resource not modified - returning 304");
+ return;
+ }
+
+ // Apply cache settings, if any
+ prepareResponse(response);
+
+ // Check the media type for the resource
MediaType mediaType = getMediaType(resource);
if (mediaType != null) {
if (logger.isTraceEnabled()) {
@@ -227,19 +256,20 @@ public class ResourceHttpRequestHandler extends WebContentGenerator implements H
}
}
- // header phase
- if (new ServletWebRequest(request, response).checkNotModified(resource.lastModified())) {
- logger.trace("Resource not modified - returning 304");
- return;
- }
- setHeaders(response, resource, mediaType);
-
- // content phase
+ // Content phase
if (METHOD_HEAD.equals(request.getMethod())) {
+ setHeaders(response, resource, mediaType);
logger.trace("HEAD request - skipping content");
return;
}
- writeContent(response, resource);
+
+ if (request.getHeader(HttpHeaders.RANGE) == null) {
+ setHeaders(response, resource, mediaType);
+ writeContent(response, resource);
+ }
+ else {
+ writePartialContent(request, response, resource, mediaType);
+ }
}
protected Resource getResource(HttpServletRequest request) throws IOException {
@@ -387,14 +417,16 @@ public class ResourceHttpRequestHandler extends WebContentGenerator implements H
throw new IOException("Resource content too long (beyond Integer.MAX_VALUE): " + resource);
}
response.setContentLength((int) length);
-
if (mediaType != null) {
response.setContentType(mediaType.toString());
}
-
if (resource instanceof EncodedResource) {
- response.setHeader(CONTENT_ENCODING, ((EncodedResource) resource).getContentEncoding());
+ response.setHeader(HttpHeaders.CONTENT_ENCODING, ((EncodedResource) resource).getContentEncoding());
+ }
+ if (resource instanceof VersionedResource) {
+ response.setHeader(HttpHeaders.ETAG, "\"" + ((VersionedResource) resource).getVersion() + "\"");
}
+ response.setHeader(HttpHeaders.ACCEPT_RANGES, "bytes");
}
/**
@@ -427,10 +459,113 @@ public class ResourceHttpRequestHandler extends WebContentGenerator implements H
}
}
+ /**
+ * Write parts of the resource as indicated by the request {@code Range} header.
+ * @param request current servlet request
+ * @param response current servlet response
+ * @param resource the identified resource (never {@code null})
+ * @param contentType the content type
+ * @throws IOException in case of errors while writing the content
+ */
+ protected void writePartialContent(HttpServletRequest request, HttpServletResponse response,
+ Resource resource, MediaType contentType) throws IOException {
+
+ long length = resource.contentLength();
+
+ List<HttpRange> ranges;
+ try {
+ HttpHeaders headers = new ServletServerHttpRequest(request).getHeaders();
+ ranges = headers.getRange();
+ }
+ catch (IllegalArgumentException ex) {
+ response.addHeader("Content-Range", "bytes */" + length);
+ response.sendError(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE);
+ return;
+ }
+
+ response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT);
+
+ if (ranges.size() == 1) {
+ HttpRange range = ranges.get(0);
+
+ long start = range.getRangeStart(length);
+ long end = range.getRangeEnd(length);
+ long rangeLength = end - start + 1;
+
+ setHeaders(response, resource, contentType);
+ response.addHeader("Content-Range", "bytes " + start + "-" + end + "/" + length);
+ response.setContentLength((int) rangeLength);
+
+ InputStream in = resource.getInputStream();
+ try {
+ copyRange(in, response.getOutputStream(), start, end);
+ }
+ finally {
+ try {
+ in.close();
+ }
+ catch (IOException ex) {
+ // ignore
+ }
+ }
+ }
+ else {
+ String boundaryString = MimeTypeUtils.generateMultipartBoundaryString();
+ response.setContentType("multipart/byteranges; boundary=" + boundaryString);
+
+ ServletOutputStream out = response.getOutputStream();
+
+ for (HttpRange range : ranges) {
+ long start = range.getRangeStart(length);
+ long end = range.getRangeEnd(length);
+
+ InputStream in = resource.getInputStream();
+
+ // Writing MIME header.
+ out.println();
+ out.println("--" + boundaryString);
+ if (contentType != null) {
+ out.println("Content-Type: " + contentType);
+ }
+ out.println("Content-Range: bytes " + start + "-" + end + "/" + length);
+ out.println();
+
+ // Printing content
+ copyRange(in, out, start, end);
+ }
+ out.println();
+ out.print("--" + boundaryString + "--");
+ }
+ }
+
+ private void copyRange(InputStream in, OutputStream out, long start, long end) throws IOException {
+ long skipped = in.skip(start);
+ if (skipped < start) {
+ throw new IOException("Skipped only " + skipped + " bytes out of " + start + " required.");
+ }
+
+ long bytesToCopy = end - start + 1;
+ byte buffer[] = new byte[StreamUtils.BUFFER_SIZE];
+ while (bytesToCopy > 0) {
+ int bytesRead = in.read(buffer);
+ if (bytesRead <= bytesToCopy) {
+ out.write(buffer, 0, bytesRead);
+ bytesToCopy -= bytesRead;
+ }
+ else {
+ out.write(buffer, 0, (int) bytesToCopy);
+ bytesToCopy = 0;
+ }
+ if (bytesRead == -1) {
+ break;
+ }
+ }
+ }
+
+
@Override
public String toString() {
- return "ResourceHttpRequestHandler [locations=" +
- getLocations() + ", resolvers=" + getResourceResolvers() + "]";
+ return "ResourceHttpRequestHandler [locations=" + getLocations() + ", resolvers=" + getResourceResolvers() + "]";
}
diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/ResourceUrlEncodingFilter.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/ResourceUrlEncodingFilter.java
index 9a053450..471ae154 100644
--- a/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/ResourceUrlEncodingFilter.java
+++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/ResourceUrlEncodingFilter.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2015 the original author or authors.
+ * Copyright 2002-2016 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.
@@ -37,6 +37,7 @@ import org.springframework.web.filter.OncePerRequestFilter;
* @author Jeremy Grelle
* @author Rossen Stoyanchev
* @author Sam Brannen
+ * @author Brian Clozel
* @since 4.1
*/
public class ResourceUrlEncodingFilter extends OncePerRequestFilter {
@@ -54,11 +55,13 @@ public class ResourceUrlEncodingFilter extends OncePerRequestFilter {
private static class ResourceUrlEncodingResponseWrapper extends HttpServletResponseWrapper {
- private HttpServletRequest request;
+ private final HttpServletRequest request;
- /* Cache the index of the path within the DispatcherServlet mapping. */
+ /* Cache the index and prefix of the path within the DispatcherServlet mapping */
private Integer indexLookupPath;
+ private String prefixLookupPath;
+
public ResourceUrlEncodingResponseWrapper(HttpServletRequest request, HttpServletResponse wrapped) {
super(wrapped);
this.request = request;
@@ -71,30 +74,47 @@ public class ResourceUrlEncodingFilter extends OncePerRequestFilter {
logger.debug("Request attribute exposing ResourceUrlProvider not found");
return super.encodeURL(url);
}
- initIndexLookupPath(resourceUrlProvider);
- if (url.length() >= this.indexLookupPath) {
- String prefix = url.substring(0, this.indexLookupPath);
- String lookupPath = url.substring(this.indexLookupPath);
+
+ initLookupPath(resourceUrlProvider);
+ if (url.startsWith(this.prefixLookupPath)) {
+ int suffixIndex = getQueryParamsIndex(url);
+ String suffix = url.substring(suffixIndex);
+ String lookupPath = url.substring(this.indexLookupPath, suffixIndex);
lookupPath = resourceUrlProvider.getForLookupPath(lookupPath);
if (lookupPath != null) {
- return super.encodeURL(prefix + lookupPath);
+ return super.encodeURL(this.prefixLookupPath + lookupPath + suffix);
}
}
+
return super.encodeURL(url);
}
private ResourceUrlProvider getResourceUrlProvider() {
- String name = ResourceUrlProviderExposingInterceptor.RESOURCE_URL_PROVIDER_ATTR;
- return (ResourceUrlProvider) this.request.getAttribute(name);
+ return (ResourceUrlProvider) this.request.getAttribute(
+ ResourceUrlProviderExposingInterceptor.RESOURCE_URL_PROVIDER_ATTR);
}
- private void initIndexLookupPath(ResourceUrlProvider urlProvider) {
+ private void initLookupPath(ResourceUrlProvider urlProvider) {
if (this.indexLookupPath == null) {
String requestUri = urlProvider.getPathHelper().getRequestUri(this.request);
String lookupPath = urlProvider.getPathHelper().getLookupPathForRequest(this.request);
this.indexLookupPath = requestUri.lastIndexOf(lookupPath);
+ this.prefixLookupPath = requestUri.substring(0, this.indexLookupPath);
+
+ if ("/".equals(lookupPath) && !"/".equals(requestUri)) {
+ String contextPath = urlProvider.getPathHelper().getContextPath(this.request);
+ if (requestUri.equals(contextPath)) {
+ this.indexLookupPath = requestUri.length();
+ this.prefixLookupPath = requestUri;
+ }
+ }
}
}
+
+ private int getQueryParamsIndex(String url) {
+ int index = url.indexOf("?");
+ return (index > 0 ? index : url.length());
+ }
}
}
diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/ResourceUrlProvider.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/ResourceUrlProvider.java
index a79000d8..9ee0c5a8 100644
--- a/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/ResourceUrlProvider.java
+++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/ResourceUrlProvider.java
@@ -30,7 +30,7 @@ import org.apache.commons.logging.LogFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationListener;
import org.springframework.context.event.ContextRefreshedEvent;
-import org.springframework.core.OrderComparator;
+import org.springframework.core.annotation.AnnotationAwareOrderComparator;
import org.springframework.util.AntPathMatcher;
import org.springframework.util.PathMatcher;
import org.springframework.web.servlet.handler.SimpleUrlHandlerMapping;
@@ -140,7 +140,7 @@ public class ResourceUrlProvider implements ApplicationListener<ContextRefreshed
Map<String, SimpleUrlHandlerMapping> map = appContext.getBeansOfType(SimpleUrlHandlerMapping.class);
List<SimpleUrlHandlerMapping> handlerMappings = new ArrayList<SimpleUrlHandlerMapping>(map.values());
- OrderComparator.sort(handlerMappings);
+ AnnotationAwareOrderComparator.sort(handlerMappings);
for (SimpleUrlHandlerMapping hm : handlerMappings) {
for (String pattern : hm.getHandlerMap().keySet()) {
@@ -171,11 +171,13 @@ public class ResourceUrlProvider implements ApplicationListener<ContextRefreshed
if (logger.isTraceEnabled()) {
logger.trace("Getting resource URL for request URL \"" + requestUrl + "\"");
}
- int index = getLookupPathIndex(request);
- String prefix = requestUrl.substring(0, index);
- String lookupPath = requestUrl.substring(index);
+ int prefixIndex = getLookupPathIndex(request);
+ int suffixIndex = getQueryParamsIndex(requestUrl);
+ String prefix = requestUrl.substring(0, prefixIndex);
+ String suffix = requestUrl.substring(suffixIndex);
+ String lookupPath = requestUrl.substring(prefixIndex, suffixIndex);
String resolvedLookupPath = getForLookupPath(lookupPath);
- return (resolvedLookupPath != null ? prefix + resolvedLookupPath : null);
+ return (resolvedLookupPath != null ? prefix + resolvedLookupPath + suffix : null);
}
private int getLookupPathIndex(HttpServletRequest request) {
@@ -184,6 +186,11 @@ public class ResourceUrlProvider implements ApplicationListener<ContextRefreshed
return requestUri.indexOf(lookupPath);
}
+ private int getQueryParamsIndex(String lookupPath) {
+ int index = lookupPath.indexOf("?");
+ return index > 0 ? index : lookupPath.length();
+ }
+
/**
* Compare the given path against configured resource handler mappings and
* if a match is found use the {@code ResourceResolver} chain of the matched
@@ -211,7 +218,7 @@ public class ResourceUrlProvider implements ApplicationListener<ContextRefreshed
if (!matchingPatterns.isEmpty()) {
Comparator<String> patternComparator = getPathMatcher().getPatternComparator(lookupPath);
Collections.sort(matchingPatterns, patternComparator);
- for(String pattern : matchingPatterns) {
+ for (String pattern : matchingPatterns) {
String pathWithinMapping = getPathMatcher().extractPathWithinPattern(pattern, lookupPath);
String pathMapping = lookupPath.substring(0, lookupPath.indexOf(pathWithinMapping));
if (logger.isTraceEnabled()) {
diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/ResourceUrlProviderExposingInterceptor.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/ResourceUrlProviderExposingInterceptor.java
index 0c69345e..3c55ec21 100644
--- a/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/ResourceUrlProviderExposingInterceptor.java
+++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/ResourceUrlProviderExposingInterceptor.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2014 the original author or authors.
+ * Copyright 2002-2015 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.
@@ -34,20 +34,19 @@ public class ResourceUrlProviderExposingInterceptor extends HandlerInterceptorAd
/**
* Name of the request attribute that holds the {@link ResourceUrlProvider}.
*/
- public static final String RESOURCE_URL_PROVIDER_ATTR = ResourceUrlProvider.class.getName().toString();
-
+ public static final String RESOURCE_URL_PROVIDER_ATTR = ResourceUrlProvider.class.getName();
private final ResourceUrlProvider resourceUrlProvider;
public ResourceUrlProviderExposingInterceptor(ResourceUrlProvider resourceUrlProvider) {
- Assert.notNull(resourceUrlProvider, "'resourceUrlProvider' is required");
+ Assert.notNull(resourceUrlProvider, "ResourceUrlProvider is required");
this.resourceUrlProvider = resourceUrlProvider;
}
@Override
- public boolean preHandle(HttpServletRequest request, HttpServletResponse response,
- Object handler) throws Exception {
+ public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
+ throws Exception {
request.setAttribute(RESOURCE_URL_PROVIDER_ATTR, this.resourceUrlProvider);
return true;
diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/VersionResourceResolver.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/VersionResourceResolver.java
index 45f831bf..f7eb7cb0 100644
--- a/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/VersionResourceResolver.java
+++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/VersionResourceResolver.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2015 the original author or authors.
+ * Copyright 2002-2016 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.
@@ -16,7 +16,13 @@
package org.springframework.web.servlet.resource;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URI;
+import java.net.URL;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.LinkedHashMap;
@@ -24,6 +30,7 @@ import java.util.List;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
+import org.springframework.core.io.AbstractResource;
import org.springframework.core.io.Resource;
import org.springframework.util.AntPathMatcher;
import org.springframework.util.StringUtils;
@@ -103,14 +110,26 @@ public class VersionResourceResolver extends AbstractResourceResolver {
* fetched from a git commit sha, a property file, or environment variable
* and set with SpEL expressions in the configuration (e.g. see {@code @Value}
* in Java config).
+ * <p>If not done already, variants of the given {@code pathPatterns}, prefixed with
+ * the {@code version} will be also configured. For example, adding a {@code "/js/**"} path pattern
+ * will also cofigure automatically a {@code "/v1.0.0/js/**"} with {@code "v1.0.0"} the
+ * {@code version} String given as an argument.
* @param version a version string
* @param pathPatterns one or more resource URL path patterns
* @return the current instance for chained method invocation
* @see FixedVersionStrategy
*/
public VersionResourceResolver addFixedVersionStrategy(String version, String... pathPatterns) {
- addVersionStrategy(new FixedVersionStrategy(version), pathPatterns);
- return this;
+ List<String> patternsList = Arrays.asList(pathPatterns);
+ List<String> prefixedPatterns = new ArrayList<String>(pathPatterns.length);
+ String versionPrefix = "/" + version;
+ for (String pattern : patternsList) {
+ prefixedPatterns.add(pattern);
+ if (!pattern.startsWith(versionPrefix) && !patternsList.contains(versionPrefix + pattern)) {
+ prefixedPatterns.add(versionPrefix + pattern);
+ }
+ }
+ return addVersionStrategy(new FixedVersionStrategy(version), prefixedPatterns.toArray(new String[0]));
}
/**
@@ -164,9 +183,9 @@ public class VersionResourceResolver extends AbstractResourceResolver {
String actualVersion = versionStrategy.getResourceVersion(baseResource);
if (candidateVersion.equals(actualVersion)) {
if (logger.isTraceEnabled()) {
- logger.trace("Resource matches extracted version ["+ candidateVersion + "]");
+ logger.trace("Resource matches extracted version [" + candidateVersion + "]");
}
- return baseResource;
+ return new FileNameVersionedResource(baseResource, candidateVersion);
}
else {
if (logger.isTraceEnabled()) {
@@ -218,4 +237,82 @@ public class VersionResourceResolver extends AbstractResourceResolver {
return null;
}
+
+ private class FileNameVersionedResource extends AbstractResource implements VersionedResource {
+
+ private final Resource original;
+
+ private final String version;
+
+ public FileNameVersionedResource(Resource original, String version) {
+ this.original = original;
+ this.version = version;
+ }
+
+ @Override
+ public boolean exists() {
+ return this.original.exists();
+ }
+
+ @Override
+ public boolean isReadable() {
+ return this.original.isReadable();
+ }
+
+ @Override
+ public boolean isOpen() {
+ return this.original.isOpen();
+ }
+
+ @Override
+ public URL getURL() throws IOException {
+ return this.original.getURL();
+ }
+
+ @Override
+ public URI getURI() throws IOException {
+ return this.original.getURI();
+ }
+
+ @Override
+ public File getFile() throws IOException {
+ return this.original.getFile();
+ }
+
+ @Override
+ public String getFilename() {
+ return this.original.getFilename();
+ }
+
+ @Override
+ public long contentLength() throws IOException {
+ return this.original.contentLength();
+ }
+
+ @Override
+ public long lastModified() throws IOException {
+ return this.original.lastModified();
+ }
+
+ @Override
+ public Resource createRelative(String relativePath) throws IOException {
+ return this.original.createRelative(relativePath);
+ }
+
+ @Override
+ public String getDescription() {
+ return original.getDescription();
+ }
+
+ @Override
+ public InputStream getInputStream() throws IOException {
+ return original.getInputStream();
+ }
+
+ @Override
+ public String getVersion() {
+ return this.version;
+ }
+ }
+
}
diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/VersionedResource.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/VersionedResource.java
new file mode 100644
index 00000000..c780df8c
--- /dev/null
+++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/VersionedResource.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2002-2016 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.resource;
+
+import org.springframework.core.io.Resource;
+
+/**
+ * Interface for a resource descriptor that describes its version with a
+ * version string that can be derived from its content and/or metadata.
+ *
+ * @author Brian Clozel
+ * @since 4.2.5
+ * @see VersionResourceResolver
+ */
+public interface VersionedResource extends Resource {
+
+ String getVersion();
+
+}
diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/WebJarsResourceResolver.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/WebJarsResourceResolver.java
new file mode 100644
index 00000000..8d08bbd7
--- /dev/null
+++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/WebJarsResourceResolver.java
@@ -0,0 +1,107 @@
+/*
+ * Copyright 2002-2015 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.resource;
+
+import java.util.List;
+import javax.servlet.http.HttpServletRequest;
+
+import org.webjars.MultipleMatchesException;
+import org.webjars.WebJarAssetLocator;
+
+import org.springframework.core.io.Resource;
+
+/**
+ * A {@code ResourceResolver} that delegates to the chain to locate a resource and then
+ * attempts to find a matching versioned resource contained in a WebJar JAR file.
+ *
+ * <p>This allows WebJars.org users to write version agnostic paths in their templates,
+ * like {@code <script src="/jquery/jquery.min.js"/>}.
+ * This path will be resolved to the unique version {@code <script src="/jquery/1.2.0/jquery.min.js"/>},
+ * which is a better fit for HTTP caching and version management in applications.
+ *
+ * <p>This also resolves resources for version agnostic HTTP requests {@code "GET /jquery/jquery.min.js"}.
+ *
+ * <p>This resolver requires the "org.webjars:webjars-locator" library on classpath,
+ * and is automatically registered if that library is present.
+ *
+ * @author Brian Clozel
+ * @since 4.2
+ * @see org.springframework.web.servlet.config.annotation.ResourceChainRegistration
+ * @see <a href="http://www.webjars.org">webjars.org</a>
+ */
+public class WebJarsResourceResolver extends AbstractResourceResolver {
+
+ private final static String WEBJARS_LOCATION = "META-INF/resources/webjars";
+
+ private final static int WEBJARS_LOCATION_LENGTH = WEBJARS_LOCATION.length();
+
+ private final WebJarAssetLocator webJarAssetLocator = new WebJarAssetLocator();
+
+
+ @Override
+ protected Resource resolveResourceInternal(HttpServletRequest request, String requestPath,
+ List<? extends Resource> locations, ResourceResolverChain chain) {
+
+ Resource resolved = chain.resolveResource(request, requestPath, locations);
+ if (resolved == null) {
+ String webJarResourcePath = findWebJarResourcePath(requestPath);
+ if (webJarResourcePath != null) {
+ return chain.resolveResource(request, webJarResourcePath, locations);
+ }
+ }
+ return resolved;
+ }
+
+ @Override
+ protected String resolveUrlPathInternal(String resourceUrlPath,
+ List<? extends Resource> locations, ResourceResolverChain chain) {
+
+ String path = chain.resolveUrlPath(resourceUrlPath, locations);
+ if (path == null) {
+ String webJarResourcePath = findWebJarResourcePath(resourceUrlPath);
+ if (webJarResourcePath != null) {
+ return chain.resolveUrlPath(webJarResourcePath, locations);
+ }
+ }
+ return path;
+ }
+
+ protected String findWebJarResourcePath(String path) {
+ try {
+ int startOffset = (path.startsWith("/") ? 1 : 0);
+ int endOffset = path.indexOf("/", 1);
+ if (endOffset != -1) {
+ String webjar = path.substring(startOffset, endOffset);
+ String partialPath = path.substring(endOffset);
+ String webJarPath = webJarAssetLocator.getFullPath(webjar, partialPath);
+ return webJarPath.substring(WEBJARS_LOCATION_LENGTH);
+ }
+ }
+ catch (MultipleMatchesException ex) {
+ if (logger.isWarnEnabled()) {
+ logger.warn("WebJar version conflict for \"" + path + "\"", ex);
+ }
+ }
+ catch (IllegalArgumentException ex) {
+ if (logger.isTraceEnabled()) {
+ logger.trace("No WebJar resource found for \"" + path + "\"");
+ }
+ }
+ return null;
+ }
+
+}
diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/support/AbstractDispatcherServletInitializer.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/support/AbstractDispatcherServletInitializer.java
index e7a969f9..3fe22004 100644
--- a/spring-webmvc/src/main/java/org/springframework/web/servlet/support/AbstractDispatcherServletInitializer.java
+++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/support/AbstractDispatcherServletInitializer.java
@@ -5,7 +5,7 @@
* 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
+ * 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,
@@ -25,12 +25,14 @@ import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.ServletRegistration;
+import org.springframework.context.ApplicationContextInitializer;
import org.springframework.core.Conventions;
import org.springframework.util.Assert;
import org.springframework.util.ObjectUtils;
import org.springframework.web.context.AbstractContextLoaderInitializer;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.servlet.DispatcherServlet;
+import org.springframework.web.servlet.FrameworkServlet;
/**
* Base class for {@link org.springframework.web.WebApplicationInitializer}
@@ -51,6 +53,8 @@ import org.springframework.web.servlet.DispatcherServlet;
* @author Arjen Poutsma
* @author Chris Beams
* @author Rossen Stoyanchev
+ * @author Juergen Hoeller
+ * @author Stephane Nicoll
* @since 3.2
*/
public abstract class AbstractDispatcherServletInitializer extends AbstractContextLoaderInitializer {
@@ -74,7 +78,8 @@ public abstract class AbstractDispatcherServletInitializer extends AbstractConte
* from {@link #createServletApplicationContext()}, and mapping it to the patterns
* returned from {@link #getServletMappings()}.
* <p>Further customization can be achieved by overriding {@link
- * #customizeRegistration(ServletRegistration.Dynamic)}.
+ * #customizeRegistration(ServletRegistration.Dynamic)} or
+ * {@link #createDispatcherServlet(WebApplicationContext)}.
* @param servletContext the context to register the servlet against
*/
protected void registerDispatcherServlet(ServletContext servletContext) {
@@ -86,7 +91,9 @@ public abstract class AbstractDispatcherServletInitializer extends AbstractConte
"createServletApplicationContext() did not return an application " +
"context for servlet [" + servletName + "]");
- DispatcherServlet dispatcherServlet = new DispatcherServlet(servletAppContext);
+ FrameworkServlet dispatcherServlet = createDispatcherServlet(servletAppContext);
+ dispatcherServlet.setContextInitializers(getServletApplicationContextInitializers());
+
ServletRegistration.Dynamic registration = servletContext.addServlet(servletName, dispatcherServlet);
Assert.notNull(registration,
"Failed to register servlet with name '" + servletName + "'." +
@@ -126,6 +133,28 @@ public abstract class AbstractDispatcherServletInitializer extends AbstractConte
protected abstract WebApplicationContext createServletApplicationContext();
/**
+ * Create a {@link DispatcherServlet} (or other kind of {@link FrameworkServlet}-derived
+ * dispatcher) with the specified {@link WebApplicationContext}.
+ * <p>Note: This allows for any {@link FrameworkServlet} subclass as of 4.2.3.
+ * Previously, it insisted on returning a {@link DispatcherServlet} or subclass thereof.
+ */
+ protected FrameworkServlet createDispatcherServlet(WebApplicationContext servletAppContext) {
+ return new DispatcherServlet(servletAppContext);
+ }
+
+ /**
+ * Specify application context initializers to be applied to the servlet-specific
+ * application context that the {@code DispatcherServlet} is being created with.
+ * @since 4.2
+ * @see #createServletApplicationContext()
+ * @see DispatcherServlet#setContextInitializers
+ * @see #getRootApplicationContextInitializers()
+ */
+ protected ApplicationContextInitializer<?>[] getServletApplicationContextInitializers() {
+ return null;
+ }
+
+ /**
* Specify the servlet mapping(s) for the {@code DispatcherServlet} &mdash;
* for example {@code "/"}, {@code "/app"}, etc.
* @see #registerDispatcherServlet(ServletContext)
diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/support/AbstractFlashMapManager.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/support/AbstractFlashMapManager.java
index 50366287..c859a230 100644
--- a/spring-webmvc/src/main/java/org/springframework/web/servlet/support/AbstractFlashMapManager.java
+++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/support/AbstractFlashMapManager.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2014 the original author or authors.
+ * Copyright 2002-2015 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.
@@ -16,11 +16,11 @@
package org.springframework.web.servlet.support;
-import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
+
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@@ -30,12 +30,13 @@ import org.apache.commons.logging.LogFactory;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import org.springframework.util.MultiValueMap;
-import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.servlet.FlashMap;
import org.springframework.web.servlet.FlashMapManager;
+import org.springframework.web.util.UriComponents;
import org.springframework.web.util.UrlPathHelper;
+
/**
* A base class for {@link FlashMapManager} implementations.
*
@@ -172,10 +173,16 @@ public abstract class AbstractFlashMapManager implements FlashMapManager {
return false;
}
}
- MultiValueMap<String, String> targetParams = flashMap.getTargetRequestParams();
- for (String expectedName : targetParams.keySet()) {
- for (String expectedValue : targetParams.get(expectedName)) {
- if (!ObjectUtils.containsElement(request.getParameterValues(expectedName), expectedValue)) {
+ UriComponents uriComponents = ServletUriComponentsBuilder.fromRequest(request).build();
+ MultiValueMap<String, String> actualParams = uriComponents.getQueryParams();
+ MultiValueMap<String, String> expectedParams = flashMap.getTargetRequestParams();
+ for (String expectedName : expectedParams.keySet()) {
+ List<String> actualValues = actualParams.get(expectedName);
+ if (actualValues == null) {
+ return false;
+ }
+ for (String expectedValue : expectedParams.get(expectedName)) {
+ if (!actualValues.contains(expectedValue)) {
return false;
}
}
@@ -191,7 +198,6 @@ public abstract class AbstractFlashMapManager implements FlashMapManager {
String path = decodeAndNormalizePath(flashMap.getTargetRequestPath(), request);
flashMap.setTargetRequestPath(path);
- decodeParameters(flashMap.getTargetRequestParams(), request);
if (logger.isDebugEnabled()) {
logger.debug("Saving FlashMap=" + flashMap);
@@ -227,17 +233,6 @@ public abstract class AbstractFlashMapManager implements FlashMapManager {
return path;
}
- private void decodeParameters(MultiValueMap<String, String> params, HttpServletRequest request) {
- for (String name : new ArrayList<String>(params.keySet())) {
- for (String value : new ArrayList<String>(params.remove(name))) {
- name = getUrlPathHelper().decodeRequestString(request, name);
- value = getUrlPathHelper().decodeRequestString(request, value);
- params.add(name, value);
- }
- }
- }
-
-
/**
* Retrieve saved FlashMap instances from the underlying storage.
* @param request the current request
diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/support/RequestContext.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/support/RequestContext.java
index ec328fb4..9fae8679 100644
--- a/spring-webmvc/src/main/java/org/springframework/web/servlet/support/RequestContext.java
+++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/support/RequestContext.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2014 the original author or authors.
+ * Copyright 2002-2015 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.
@@ -89,12 +89,6 @@ public class RequestContext {
*/
public static final String WEB_APPLICATION_CONTEXT_ATTRIBUTE = RequestContext.class.getName() + ".CONTEXT";
- /**
- * The name of the bean to use to look up in an implementation of
- * {@link RequestDataValueProcessor} has been configured.
- */
- private static final String REQUEST_DATA_VALUE_PROCESSOR_BEAN_NAME = "requestDataValueProcessor";
-
protected static final boolean jstlPresent = ClassUtils.isPresent("javax.servlet.jsp.jstl.core.Config",
RequestContext.class.getClassLoader());
@@ -236,7 +230,11 @@ public class RequestContext {
// ServletContext needs to be specified to be able to fall back to the root context!
this.webApplicationContext = (WebApplicationContext) request.getAttribute(WEB_APPLICATION_CONTEXT_ATTRIBUTE);
if (this.webApplicationContext == null) {
- this.webApplicationContext = RequestContextUtils.getWebApplicationContext(request, servletContext);
+ this.webApplicationContext = RequestContextUtils.findWebApplicationContext(request, servletContext);
+ if (this.webApplicationContext == null) {
+ throw new IllegalStateException("No WebApplicationContext found: not in a DispatcherServlet " +
+ "request and no ContextLoaderListener registered?");
+ }
}
// Determine locale to use for this RequestContext.
@@ -271,9 +269,9 @@ public class RequestContext {
this.urlPathHelper = new UrlPathHelper();
- if (this.webApplicationContext.containsBean(REQUEST_DATA_VALUE_PROCESSOR_BEAN_NAME)) {
+ if (this.webApplicationContext.containsBean(RequestContextUtils.REQUEST_DATA_VALUE_PROCESSOR_BEAN_NAME)) {
this.requestDataValueProcessor = this.webApplicationContext.getBean(
- REQUEST_DATA_VALUE_PROCESSOR_BEAN_NAME, RequestDataValueProcessor.class);
+ RequestContextUtils.REQUEST_DATA_VALUE_PROCESSOR_BEAN_NAME, RequestDataValueProcessor.class);
}
}
@@ -493,11 +491,11 @@ public class RequestContext {
/**
* Is HTML escaping using the response encoding by default?
* If enabled, only XML markup significant characters will be escaped with UTF-* encodings.
- * <p>Falls back to {@code false} in case of no explicit default given.
+ * <p>Falls back to {@code true} in case of no explicit default given, as of Spring 4.2.
* @since 4.1.2
*/
public boolean isResponseEncodedHtmlEscape() {
- return (this.responseEncodedHtmlEscape != null && this.responseEncodedHtmlEscape.booleanValue());
+ return (this.responseEncodedHtmlEscape == null || this.responseEncodedHtmlEscape.booleanValue());
}
/**
diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/support/RequestContextUtils.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/support/RequestContextUtils.java
index e9c25b93..ac6e0769 100644
--- a/spring-webmvc/src/main/java/org/springframework/web/servlet/support/RequestContextUtils.java
+++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/support/RequestContextUtils.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2013 the original author or authors.
+ * Copyright 2002-2015 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.
@@ -27,6 +27,7 @@ import org.springframework.context.i18n.LocaleContext;
import org.springframework.context.i18n.TimeZoneAwareLocaleContext;
import org.springframework.ui.context.Theme;
import org.springframework.ui.context.ThemeSource;
+import org.springframework.web.context.ContextLoader;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.context.support.WebApplicationContextUtils;
import org.springframework.web.servlet.DispatcherServlet;
@@ -52,15 +53,25 @@ import org.springframework.web.servlet.ThemeResolver;
public abstract class RequestContextUtils {
/**
+ * The name of the bean to use to look up in an implementation of
+ * {@link RequestDataValueProcessor} has been configured.
+ * @since 4.2.1
+ */
+ public static final String REQUEST_DATA_VALUE_PROCESSOR_BEAN_NAME = "requestDataValueProcessor";
+
+
+ /**
* Look for the WebApplicationContext associated with the DispatcherServlet
* that has initiated request processing.
* @param request current HTTP request
* @return the request-specific web application context
* @throws IllegalStateException if no servlet-specific context has been found
+ * @see #getWebApplicationContext(ServletRequest, ServletContext)
+ * @deprecated as of Spring 4.2.1, in favor of
+ * {@link #findWebApplicationContext(HttpServletRequest)}
*/
- public static WebApplicationContext getWebApplicationContext(ServletRequest request)
- throws IllegalStateException {
-
+ @Deprecated
+ public static WebApplicationContext getWebApplicationContext(ServletRequest request) throws IllegalStateException {
return getWebApplicationContext(request, null);
}
@@ -76,7 +87,12 @@ public abstract class RequestContextUtils {
* if no request-specific context has been found
* @throws IllegalStateException if neither a servlet-specific nor a
* global context has been found
+ * @see DispatcherServlet#WEB_APPLICATION_CONTEXT_ATTRIBUTE
+ * @see WebApplicationContextUtils#getRequiredWebApplicationContext(ServletContext)
+ * @deprecated as of Spring 4.2.1, in favor of
+ * {@link #findWebApplicationContext(HttpServletRequest, ServletContext)}
*/
+ @Deprecated
public static WebApplicationContext getWebApplicationContext(
ServletRequest request, ServletContext servletContext) throws IllegalStateException {
@@ -92,6 +108,57 @@ public abstract class RequestContextUtils {
}
/**
+ * Look for the WebApplicationContext associated with the DispatcherServlet
+ * that has initiated request processing, and for the global context if none
+ * was found associated with the current request. The global context will
+ * be found via the ServletContext or via ContextLoader's current context.
+ * <p>NOTE: This variant remains compatible with Servlet 2.5, explicitly
+ * checking a given ServletContext instead of deriving it from the request.
+ * @param request current HTTP request
+ * @param servletContext current servlet context
+ * @return the request-specific WebApplicationContext, or the global one
+ * if no request-specific context has been found, or {@code null} if none
+ * @since 4.2.1
+ * @see DispatcherServlet#WEB_APPLICATION_CONTEXT_ATTRIBUTE
+ * @see WebApplicationContextUtils#getWebApplicationContext(ServletContext)
+ * @see ContextLoader#getCurrentWebApplicationContext()
+ */
+ public static WebApplicationContext findWebApplicationContext(
+ HttpServletRequest request, ServletContext servletContext) {
+
+ WebApplicationContext webApplicationContext = (WebApplicationContext) request.getAttribute(
+ DispatcherServlet.WEB_APPLICATION_CONTEXT_ATTRIBUTE);
+ if (webApplicationContext == null) {
+ if (servletContext != null) {
+ webApplicationContext = WebApplicationContextUtils.getWebApplicationContext(servletContext);
+ }
+ if (webApplicationContext == null) {
+ webApplicationContext = ContextLoader.getCurrentWebApplicationContext();
+ }
+ }
+ return webApplicationContext;
+ }
+
+ /**
+ * Look for the WebApplicationContext associated with the DispatcherServlet
+ * that has initiated request processing, and for the global context if none
+ * was found associated with the current request. The global context will
+ * be found via the ServletContext or via ContextLoader's current context.
+ * <p>NOTE: This variant requires Servlet 3.0+ and is generally recommended
+ * for forward-looking custom user code.
+ * @param request current HTTP request
+ * @return the request-specific WebApplicationContext, or the global one
+ * if no request-specific context has been found, or {@code null} if none
+ * @since 4.2.1
+ * @see #findWebApplicationContext(HttpServletRequest, ServletContext)
+ * @see ServletRequest#getServletContext()
+ * @see ContextLoader#getCurrentWebApplicationContext()
+ */
+ public static WebApplicationContext findWebApplicationContext(HttpServletRequest request) {
+ return findWebApplicationContext(request, request.getServletContext());
+ }
+
+ /**
* Return the LocaleResolver that has been bound to the request by the
* DispatcherServlet.
* @param request current HTTP request
diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/support/ServletUriComponentsBuilder.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/support/ServletUriComponentsBuilder.java
index d0591ce2..74c804f4 100644
--- a/spring-webmvc/src/main/java/org/springframework/web/servlet/support/ServletUriComponentsBuilder.java
+++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/support/ServletUriComponentsBuilder.java
@@ -223,7 +223,7 @@ public class ServletUriComponentsBuilder extends UriComponentsBuilder {
}
@Override
- protected Object clone() {
+ public Object clone() {
return new ServletUriComponentsBuilder(this);
}
diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/support/WebContentGenerator.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/support/WebContentGenerator.java
index 087bcc0f..20cf89d2 100644
--- a/spring-webmvc/src/main/java/org/springframework/web/servlet/support/WebContentGenerator.java
+++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/support/WebContentGenerator.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2014 the original author or authors.
+ * Copyright 2002-2016 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.
@@ -18,15 +18,18 @@ package org.springframework.web.servlet.support;
import java.util.Arrays;
import java.util.HashSet;
+import java.util.LinkedHashSet;
import java.util.Set;
+import java.util.concurrent.TimeUnit;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
+import org.springframework.http.CacheControl;
+import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.HttpRequestMethodNotSupportedException;
import org.springframework.web.HttpSessionRequiredException;
-import org.springframework.web.context.request.WebRequest;
import org.springframework.web.context.support.WebApplicationObjectSupport;
/**
@@ -36,13 +39,22 @@ import org.springframework.web.context.support.WebApplicationObjectSupport;
* Can also be used for custom handlers that have their own
* {@link org.springframework.web.servlet.HandlerAdapter}.
*
- * <p>Supports HTTP cache control options. The usage of corresponding
- * HTTP headers can be controlled via the "useExpiresHeader",
- * "useCacheControlHeader" and "useCacheControlNoStore" properties.
+ * <p>Supports HTTP cache control options. The usage of corresponding HTTP
+ * headers can be controlled via the {@link #setCacheSeconds "cacheSeconds"}
+ * and {@link #setCacheControl "cacheControl"} properties.
+ *
+ * <p><b>NOTE:</b> As of Spring 4.2, this generator's default behavior changed when
+ * using only {@link #setCacheSeconds}, sending HTTP response headers that are in line
+ * with current browsers and proxies implementations (i.e. no HTTP 1.0 headers anymore)
+ * Reverting to the previous behavior can be easily done by using one of the newly
+ * deprecated methods {@link #setUseExpiresHeader}, {@link #setUseCacheControlHeader},
+ * {@link #setUseCacheControlNoStore} or {@link #setAlwaysMustRevalidate}.
*
* @author Rod Johnson
* @author Juergen Hoeller
+ * @author Brian Clozel
* @see #setCacheSeconds
+ * @see #setCacheControl
* @see #setRequireSession
*/
public abstract class WebContentGenerator extends WebApplicationObjectSupport {
@@ -56,21 +68,24 @@ public abstract class WebContentGenerator extends WebApplicationObjectSupport {
/** HTTP method "POST" */
public static final String METHOD_POST = "POST";
-
private static final String HEADER_PRAGMA = "Pragma";
private static final String HEADER_EXPIRES = "Expires";
- private static final String HEADER_CACHE_CONTROL = "Cache-Control";
+ protected static final String HEADER_CACHE_CONTROL = "Cache-Control";
/** Set of supported HTTP methods */
- private Set<String> supportedMethods;
+ private Set<String> supportedMethods;
private boolean requireSession = false;
+ private CacheControl cacheControl;
+
+ private int cacheSeconds = -1;
+
/** Use HTTP 1.0 expires header? */
- private boolean useExpiresHeader = true;
+ private boolean useExpiresHeader = false;
/** Use HTTP 1.1 cache-control header? */
private boolean useCacheControlHeader = true;
@@ -78,8 +93,6 @@ public abstract class WebContentGenerator extends WebApplicationObjectSupport {
/** Use HTTP 1.1 cache-control header value "no-store"? */
private boolean useCacheControlNoStore = true;
- private int cacheSeconds = -1;
-
private boolean alwaysMustRevalidate = false;
@@ -121,8 +134,8 @@ public abstract class WebContentGenerator extends WebApplicationObjectSupport {
* unrestricted for general controllers and interceptors.
*/
public final void setSupportedMethods(String... methods) {
- if (methods != null) {
- this.supportedMethods = new HashSet<String>(Arrays.asList(methods));
+ if (!ObjectUtils.isEmpty(methods)) {
+ this.supportedMethods = new LinkedHashSet<String>(Arrays.asList(methods));
}
else {
this.supportedMethods = null;
@@ -151,17 +164,64 @@ public abstract class WebContentGenerator extends WebApplicationObjectSupport {
}
/**
- * Set whether to use the HTTP 1.0 expires header. Default is "true".
+ * Set the {@link org.springframework.http.CacheControl} instance to build
+ * the Cache-Control HTTP response header.
+ * @since 4.2
+ */
+ public final void setCacheControl(CacheControl cacheControl) {
+ this.cacheControl = cacheControl;
+ }
+
+ /**
+ * Get the {@link org.springframework.http.CacheControl} instance
+ * that builds the Cache-Control HTTP response header.
+ * @since 4.2
+ */
+ public final CacheControl getCacheControl() {
+ return this.cacheControl;
+ }
+
+ /**
+ * Cache content for the given number of seconds, by writing
+ * cache-related HTTP headers to the response:
+ * <ul>
+ * <li>seconds == -1 (default value): no generation cache-related headers</li>
+ * <li>seconds == 0: "Cache-Control: no-store" will prevent caching</li>
+ * <li>seconds > 0: "Cache-Control: max-age=seconds" will ask to cache content</li>
+ * </ul>
+ * <p>For more specific needs, a custom {@link org.springframework.http.CacheControl}
+ * should be used.
+ * @see #setCacheControl
+ */
+ public final void setCacheSeconds(int seconds) {
+ this.cacheSeconds = seconds;
+ }
+
+ /**
+ * Return the number of seconds that content is cached.
+ */
+ public final int getCacheSeconds() {
+ return this.cacheSeconds;
+ }
+
+ /**
+ * Set whether to use the HTTP 1.0 expires header. Default is "false",
+ * as of 4.2.
* <p>Note: Cache headers will only get applied if caching is enabled
* (or explicitly prevented) for the current request.
+ * @deprecated as of 4.2, since going forward, the HTTP 1.1 cache-control
+ * header will be required, with the HTTP 1.0 headers disappearing
*/
+ @Deprecated
public final void setUseExpiresHeader(boolean useExpiresHeader) {
this.useExpiresHeader = useExpiresHeader;
}
/**
* Return whether the HTTP 1.0 expires header is used.
+ * @deprecated as of 4.2, in favor of {@link #getCacheControl()}
*/
+ @Deprecated
public final boolean isUseExpiresHeader() {
return this.useExpiresHeader;
}
@@ -170,14 +230,19 @@ public abstract class WebContentGenerator extends WebApplicationObjectSupport {
* Set whether to use the HTTP 1.1 cache-control header. Default is "true".
* <p>Note: Cache headers will only get applied if caching is enabled
* (or explicitly prevented) for the current request.
+ * @deprecated as of 4.2, since going forward, the HTTP 1.1 cache-control
+ * header will be required, with the HTTP 1.0 headers disappearing
*/
+ @Deprecated
public final void setUseCacheControlHeader(boolean useCacheControlHeader) {
this.useCacheControlHeader = useCacheControlHeader;
}
/**
* Return whether the HTTP 1.1 cache-control header is used.
+ * @deprecated as of 4.2, in favor of {@link #getCacheControl()}
*/
+ @Deprecated
public final boolean isUseCacheControlHeader() {
return this.useCacheControlHeader;
}
@@ -185,123 +250,191 @@ public abstract class WebContentGenerator extends WebApplicationObjectSupport {
/**
* Set whether to use the HTTP 1.1 cache-control header value "no-store"
* when preventing caching. Default is "true".
+ * @deprecated as of 4.2, in favor of {@link #setCacheControl}
*/
+ @Deprecated
public final void setUseCacheControlNoStore(boolean useCacheControlNoStore) {
this.useCacheControlNoStore = useCacheControlNoStore;
}
/**
* Return whether the HTTP 1.1 cache-control header value "no-store" is used.
+ * @deprecated as of 4.2, in favor of {@link #getCacheControl()}
*/
+ @Deprecated
public final boolean isUseCacheControlNoStore() {
return this.useCacheControlNoStore;
}
/**
- * An option to add 'must-revalidate' to every Cache-Control header. This
- * may be useful with annotated controller methods, which can
- * programmatically do a lastModified calculation as described in
- * {@link WebRequest#checkNotModified(long)}. Default is "false",
- * effectively relying on whether the handler implements
- * {@link org.springframework.web.servlet.mvc.LastModified} or not.
+ * An option to add 'must-revalidate' to every Cache-Control header.
+ * This may be useful with annotated controller methods, which can
+ * programmatically do a last-modified calculation as described in
+ * {@link org.springframework.web.context.request.WebRequest#checkNotModified(long)}.
+ * <p>Default is "false".
+ * @deprecated as of 4.2, in favor of {@link #setCacheControl}
*/
- public void setAlwaysMustRevalidate(boolean mustRevalidate) {
+ @Deprecated
+ public final void setAlwaysMustRevalidate(boolean mustRevalidate) {
this.alwaysMustRevalidate = mustRevalidate;
}
/**
* Return whether 'must-revalidate' is added to every Cache-Control header.
+ * @deprecated as of 4.2, in favor of {@link #getCacheControl()}
*/
- public boolean isAlwaysMustRevalidate() {
- return alwaysMustRevalidate;
+ @Deprecated
+ public final boolean isAlwaysMustRevalidate() {
+ return this.alwaysMustRevalidate;
}
+
/**
- * Cache content for the given number of seconds. Default is -1,
- * indicating no generation of cache-related headers.
- * <p>Only if this is set to 0 (no cache) or a positive value (cache for
- * this many seconds) will this class generate cache headers.
- * <p>The headers can be overwritten by subclasses, before content is generated.
+ * Check the given request for supported methods and a required session, if any.
+ * @param request current HTTP request
+ * @throws ServletException if the request cannot be handled because a check failed
+ * @since 4.2
*/
- public final void setCacheSeconds(int seconds) {
- this.cacheSeconds = seconds;
+ protected final void checkRequest(HttpServletRequest request) throws ServletException {
+ // Check whether we should support the request method.
+ String method = request.getMethod();
+ if (this.supportedMethods != null && !this.supportedMethods.contains(method)) {
+ throw new HttpRequestMethodNotSupportedException(
+ method, StringUtils.toStringArray(this.supportedMethods));
+ }
+
+ // Check whether a session is required.
+ if (this.requireSession && request.getSession(false) == null) {
+ throw new HttpSessionRequiredException("Pre-existing session required but none found");
+ }
}
/**
- * Return the number of seconds that content is cached.
+ * Prepare the given response according to the settings of this generator.
+ * Applies the number of cache seconds specified for this generator.
+ * @param response current HTTP response
+ * @since 4.2
*/
- public final int getCacheSeconds() {
- return this.cacheSeconds;
+ protected final void prepareResponse(HttpServletResponse response) {
+ if (this.cacheControl != null) {
+ applyCacheControl(response, this.cacheControl);
+ }
+ else {
+ applyCacheSeconds(response, this.cacheSeconds);
+ }
}
-
/**
- * Check and prepare the given request and response according to the settings
- * of this generator. Checks for supported methods and a required session,
- * and applies the number of cache seconds specified for this generator.
- * @param request current HTTP request
+ * Set the HTTP Cache-Control header according to the given settings.
* @param response current HTTP response
- * @param lastModified if the mapped handler provides Last-Modified support
- * @throws ServletException if the request cannot be handled because a check failed
+ * @param cacheControl the pre-configured cache control settings
+ * @since 4.2
*/
- protected final void checkAndPrepare(
- HttpServletRequest request, HttpServletResponse response, boolean lastModified)
- throws ServletException {
-
- checkAndPrepare(request, response, this.cacheSeconds, lastModified);
+ protected final void applyCacheControl(HttpServletResponse response, CacheControl cacheControl) {
+ String ccValue = cacheControl.getHeaderValue();
+ if (ccValue != null) {
+ // Set computed HTTP 1.1 Cache-Control header
+ response.setHeader(HEADER_CACHE_CONTROL, ccValue);
+
+ if (response.containsHeader(HEADER_PRAGMA)) {
+ // Reset HTTP 1.0 Pragma header if present
+ response.setHeader(HEADER_PRAGMA, "");
+ }
+ if (response.containsHeader(HEADER_EXPIRES)) {
+ // Reset HTTP 1.0 Expires header if present
+ response.setHeader(HEADER_EXPIRES, "");
+ }
+ }
}
/**
- * Check and prepare the given request and response according to the settings
- * of this generator. Checks for supported methods and a required session,
- * and applies the given number of cache seconds.
- * @param request current HTTP request
+ * Apply the given cache seconds and generate corresponding HTTP headers,
+ * i.e. allow caching for the given number of seconds in case of a positive
+ * value, prevent caching if given a 0 value, do nothing else.
+ * Does not tell the browser to revalidate the resource.
* @param response current HTTP response
* @param cacheSeconds positive number of seconds into the future that the
* response should be cacheable for, 0 to prevent caching
- * @param lastModified if the mapped handler provides Last-Modified support
- * @throws ServletException if the request cannot be handled because a check failed
*/
- protected final void checkAndPrepare(
- HttpServletRequest request, HttpServletResponse response, int cacheSeconds, boolean lastModified)
- throws ServletException {
-
- // Check whether we should support the request method.
- String method = request.getMethod();
- if (this.supportedMethods != null && !this.supportedMethods.contains(method)) {
- throw new HttpRequestMethodNotSupportedException(
- method, StringUtils.toStringArray(this.supportedMethods));
+ @SuppressWarnings("deprecation")
+ protected final void applyCacheSeconds(HttpServletResponse response, int cacheSeconds) {
+ if (this.useExpiresHeader || !this.useCacheControlHeader) {
+ // Deprecated HTTP 1.0 cache behavior, as in previous Spring versions
+ if (cacheSeconds > 0) {
+ cacheForSeconds(response, cacheSeconds);
+ }
+ else if (cacheSeconds == 0) {
+ preventCaching(response);
+ }
}
-
- // Check whether a session is required.
- if (this.requireSession) {
- if (request.getSession(false) == null) {
- throw new HttpSessionRequiredException("Pre-existing session required but none found");
+ else {
+ CacheControl cControl;
+ if (cacheSeconds > 0) {
+ cControl = CacheControl.maxAge(cacheSeconds, TimeUnit.SECONDS);
+ if (this.alwaysMustRevalidate) {
+ cControl = cControl.mustRevalidate();
+ }
+ }
+ else if (cacheSeconds == 0) {
+ cControl = (this.useCacheControlNoStore ? CacheControl.noStore() : CacheControl.noCache());
}
+ else {
+ cControl = CacheControl.empty();
+ }
+ applyCacheControl(response, cControl);
}
+ }
+
- // Do declarative cache control.
- // Revalidate if the controller supports last-modified.
- applyCacheSeconds(response, cacheSeconds, lastModified);
+ /**
+ * @see #checkRequest(HttpServletRequest)
+ * @see #prepareResponse(HttpServletResponse)
+ * @deprecated as of 4.2, since the {@code lastModified} flag is effectively ignored,
+ * with a must-revalidate header only generated if explicitly configured
+ */
+ @Deprecated
+ protected final void checkAndPrepare(
+ HttpServletRequest request, HttpServletResponse response, boolean lastModified) throws ServletException {
+
+ checkRequest(request);
+ prepareResponse(response);
}
/**
- * Prevent the response from being cached.
- * See {@code http://www.mnot.net/cache_docs}.
+ * @see #checkRequest(HttpServletRequest)
+ * @see #applyCacheSeconds(HttpServletResponse, int)
+ * @deprecated as of 4.2, since the {@code lastModified} flag is effectively ignored,
+ * with a must-revalidate header only generated if explicitly configured
*/
- protected final void preventCaching(HttpServletResponse response) {
- response.setHeader(HEADER_PRAGMA, "no-cache");
- if (this.useExpiresHeader) {
- // HTTP 1.0 header
- response.setDateHeader(HEADER_EXPIRES, 1L);
+ @Deprecated
+ protected final void checkAndPrepare(
+ HttpServletRequest request, HttpServletResponse response, int cacheSeconds, boolean lastModified)
+ throws ServletException {
+
+ checkRequest(request);
+ applyCacheSeconds(response, cacheSeconds);
+ }
+
+ /**
+ * Apply the given cache seconds and generate respective HTTP headers.
+ * <p>That is, allow caching for the given number of seconds in the
+ * case of a positive value, prevent caching if given a 0 value, else
+ * do nothing (i.e. leave caching to the client).
+ * @param response the current HTTP response
+ * @param cacheSeconds the (positive) number of seconds into the future
+ * that the response should be cacheable for; 0 to prevent caching; and
+ * a negative value to leave caching to the client.
+ * @param mustRevalidate whether the client should revalidate the resource
+ * (typically only necessary for controllers with last-modified support)
+ * @deprecated as of 4.2, in favor of {@link #applyCacheControl}
+ */
+ @Deprecated
+ protected final void applyCacheSeconds(HttpServletResponse response, int cacheSeconds, boolean mustRevalidate) {
+ if (cacheSeconds > 0) {
+ cacheForSeconds(response, cacheSeconds, mustRevalidate);
}
- if (this.useCacheControlHeader) {
- // HTTP 1.1 header: "no-cache" is the standard value,
- // "no-store" is necessary to prevent caching on FireFox.
- response.setHeader(HEADER_CACHE_CONTROL, "no-cache");
- if (this.useCacheControlNoStore) {
- response.addHeader(HEADER_CACHE_CONTROL, "no-store");
- }
+ else if (cacheSeconds == 0) {
+ preventCaching(response);
}
}
@@ -311,8 +444,9 @@ public abstract class WebContentGenerator extends WebApplicationObjectSupport {
* @param response current HTTP response
* @param seconds number of seconds into the future that the response
* should be cacheable for
- * @see #cacheForSeconds(javax.servlet.http.HttpServletResponse, int, boolean)
+ * @deprecated as of 4.2, in favor of {@link #applyCacheControl}
*/
+ @Deprecated
protected final void cacheForSeconds(HttpServletResponse response, int seconds) {
cacheForSeconds(response, seconds, false);
}
@@ -326,12 +460,19 @@ public abstract class WebContentGenerator extends WebApplicationObjectSupport {
* should be cacheable for
* @param mustRevalidate whether the client should revalidate the resource
* (typically only necessary for controllers with last-modified support)
+ * @deprecated as of 4.2, in favor of {@link #applyCacheControl}
*/
+ @Deprecated
protected final void cacheForSeconds(HttpServletResponse response, int seconds, boolean mustRevalidate) {
if (this.useExpiresHeader) {
// HTTP 1.0 header
response.setDateHeader(HEADER_EXPIRES, System.currentTimeMillis() + seconds * 1000L);
}
+ else if (response.containsHeader(HEADER_EXPIRES)) {
+ // Reset HTTP 1.0 Expires header if present
+ response.setHeader(HEADER_EXPIRES, "");
+ }
+
if (this.useCacheControlHeader) {
// HTTP 1.1 header
String headerValue = "max-age=" + seconds;
@@ -340,42 +481,36 @@ public abstract class WebContentGenerator extends WebApplicationObjectSupport {
}
response.setHeader(HEADER_CACHE_CONTROL, headerValue);
}
- }
- /**
- * Apply the given cache seconds and generate corresponding HTTP headers,
- * i.e. allow caching for the given number of seconds in case of a positive
- * value, prevent caching if given a 0 value, do nothing else.
- * Does not tell the browser to revalidate the resource.
- * @param response current HTTP response
- * @param seconds positive number of seconds into the future that the
- * response should be cacheable for, 0 to prevent caching
- * @see #cacheForSeconds(javax.servlet.http.HttpServletResponse, int, boolean)
- */
- protected final void applyCacheSeconds(HttpServletResponse response, int seconds) {
- applyCacheSeconds(response, seconds, false);
+ if (response.containsHeader(HEADER_PRAGMA)) {
+ // Reset HTTP 1.0 Pragma header if present
+ response.setHeader(HEADER_PRAGMA, "");
+ }
}
/**
- * Apply the given cache seconds and generate respective HTTP headers.
- * <p>That is, allow caching for the given number of seconds in the
- * case of a positive value, prevent caching if given a 0 value, else
- * do nothing (i.e. leave caching to the client).
- * @param response the current HTTP response
- * @param seconds the (positive) number of seconds into the future that
- * the response should be cacheable for; 0 to prevent caching; and
- * a negative value to leave caching to the client.
- * @param mustRevalidate whether the client should revalidate the resource
- * (typically only necessary for controllers with last-modified support)
+ * Prevent the response from being cached.
+ * Only called in HTTP 1.0 compatibility mode.
+ * <p>See {@code http://www.mnot.net/cache_docs}.
+ * @deprecated as of 4.2, in favor of {@link #applyCacheControl}
*/
- protected final void applyCacheSeconds(HttpServletResponse response, int seconds, boolean mustRevalidate) {
- if (seconds > 0) {
- cacheForSeconds(response, seconds, mustRevalidate);
+ @Deprecated
+ protected final void preventCaching(HttpServletResponse response) {
+ response.setHeader(HEADER_PRAGMA, "no-cache");
+
+ if (this.useExpiresHeader) {
+ // HTTP 1.0 Expires header
+ response.setDateHeader(HEADER_EXPIRES, 1L);
}
- else if (seconds == 0) {
- preventCaching(response);
+
+ if (this.useCacheControlHeader) {
+ // HTTP 1.1 Cache-Control header: "no-cache" is the standard value,
+ // "no-store" is necessary to prevent caching on Firefox.
+ response.setHeader(HEADER_CACHE_CONTROL, "no-cache");
+ if (this.useCacheControlNoStore) {
+ response.addHeader(HEADER_CACHE_CONTROL, "no-store");
+ }
}
- // Leave caching to the client otherwise.
}
}
diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/tags/TransformTag.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/tags/TransformTag.java
index 05d662b4..6cb54379 100644
--- a/spring-webmvc/src/main/java/org/springframework/web/servlet/tags/TransformTag.java
+++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/tags/TransformTag.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2013 the original author or authors.
+ * Copyright 2002-2015 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.
@@ -21,7 +21,6 @@ import java.io.IOException;
import javax.servlet.jsp.JspException;
import javax.servlet.jsp.tagext.TagSupport;
-import org.springframework.web.util.HtmlUtils;
import org.springframework.web.util.TagUtils;
/**
diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/tags/UrlTag.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/tags/UrlTag.java
index 622846af..201a5fb4 100644
--- a/spring-webmvc/src/main/java/org/springframework/web/servlet/tags/UrlTag.java
+++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/tags/UrlTag.java
@@ -208,7 +208,12 @@ public class UrlTag extends HtmlEscapingAwareTag implements ParamAware {
url.append(request.getContextPath());
}
else {
- url.append(this.context);
+ if (this.context.endsWith("/")) {
+ url.append(this.context.substring(0, this.context.length() - 1));
+ }
+ else {
+ url.append(this.context);
+ }
}
}
if (this.type != UrlType.RELATIVE && this.type != UrlType.ABSOLUTE && !this.value.startsWith("/")) {
diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/tags/form/AbstractMultiCheckedElementTag.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/tags/form/AbstractMultiCheckedElementTag.java
index cb9c6963..58d4cc79 100644
--- a/spring-webmvc/src/main/java/org/springframework/web/servlet/tags/form/AbstractMultiCheckedElementTag.java
+++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/tags/form/AbstractMultiCheckedElementTag.java
@@ -149,7 +149,7 @@ public abstract class AbstractMultiCheckedElementTag extends AbstractCheckedElem
/**
* Set the HTML element used to enclose the
* '{@code input type="checkbox/radio"}' tag.
- * <p>Defaults to an HTML '{@code &lt;span/&gt;}' tag.
+ * <p>Defaults to an HTML '{@code <span/>}' tag.
*/
public void setElement(String element) {
Assert.hasText(element, "'element' cannot be null or blank");
diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/tags/form/CheckboxTag.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/tags/form/CheckboxTag.java
index c7abcaa4..ebde41ea 100644
--- a/spring-webmvc/src/main/java/org/springframework/web/servlet/tags/form/CheckboxTag.java
+++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/tags/form/CheckboxTag.java
@@ -71,7 +71,7 @@ public class CheckboxTag extends AbstractSingleCheckedElementTag {
Object boundValue = getBoundValue();
Class<?> valueType = getBindStatus().getValueType();
- if (Boolean.class.equals(valueType) || boolean.class.equals(valueType)) {
+ if (Boolean.class == valueType || boolean.class == valueType) {
// the concrete type may not be a Boolean - can be String
if (boundValue instanceof String) {
boundValue = Boolean.valueOf((String) boundValue);
diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/tags/form/ErrorsTag.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/tags/form/ErrorsTag.java
index c05a4468..1673f18f 100644
--- a/spring-webmvc/src/main/java/org/springframework/web/servlet/tags/form/ErrorsTag.java
+++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/tags/form/ErrorsTag.java
@@ -72,7 +72,7 @@ public class ErrorsTag extends AbstractHtmlElementBodyTag implements BodyTag {
/**
* Set the HTML element must be used to render the error messages.
- * <p>Defaults to an HTML '{@code &lt;span/&gt;}' tag.
+ * <p>Defaults to an HTML '{@code <span/>}' tag.
*/
public void setElement(String element) {
Assert.hasText(element, "'element' cannot be null or blank");
@@ -88,7 +88,7 @@ public class ErrorsTag extends AbstractHtmlElementBodyTag implements BodyTag {
/**
* Set the delimiter to be used between error messages.
- * <p>Defaults to an HTML '{@code &lt;br/&gt;}' tag.
+ * <p>Defaults to an HTML '{@code <br/>}' tag.
*/
public void setDelimiter(String delimiter) {
this.delimiter = delimiter;
@@ -105,7 +105,7 @@ public class ErrorsTag extends AbstractHtmlElementBodyTag implements BodyTag {
/**
* Get the value for the HTML '{@code id}' attribute.
* <p>Appends '{@code .errors}' to the value returned by {@link #getPropertyPath()}
- * or to the model attribute name if the {@code &lt;form:errors/&gt;} tag's
+ * or to the model attribute name if the {@code <form:errors/>} tag's
* '{@code path}' attribute has been omitted.
* @return the value for the HTML '{@code id}' attribute
* @see #getPropertyPath()
diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/tags/form/FormTag.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/tags/form/FormTag.java
index 5e3a5404..5aa9d232 100644
--- a/spring-webmvc/src/main/java/org/springframework/web/servlet/tags/form/FormTag.java
+++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/tags/form/FormTag.java
@@ -325,7 +325,19 @@ public class FormTag extends AbstractHtmlElementTag {
/**
* Get the name of the request param for non-browser supported HTTP methods.
+ * @since 4.2.3
*/
+ @SuppressWarnings("deprecation")
+ protected String getMethodParam() {
+ return getMethodParameter();
+ }
+
+ /**
+ * Get the name of the request param for non-browser supported HTTP methods.
+ * @deprecated as of 4.2.3, in favor of {@link #getMethodParam()} which is
+ * a proper pairing for {@link #setMethodParam(String)}
+ */
+ @Deprecated
protected String getMethodParameter() {
return this.methodParam;
}
@@ -363,7 +375,7 @@ public class FormTag extends AbstractHtmlElementTag {
if (!isMethodBrowserSupported(getMethod())) {
assertHttpMethod(getMethod());
- String inputName = getMethodParameter();
+ String inputName = getMethodParam();
String inputType = "hidden";
tagWriter.startTag(INPUT_TAG);
writeOptionalAttribute(tagWriter, TYPE_ATTRIBUTE, inputType);
diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/tags/form/OptionTag.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/tags/form/OptionTag.java
index 2e97f9e0..36e381d0 100644
--- a/spring-webmvc/src/main/java/org/springframework/web/servlet/tags/form/OptionTag.java
+++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/tags/form/OptionTag.java
@@ -76,12 +76,12 @@ public class OptionTag extends AbstractHtmlElementBodyTag implements BodyTag {
/**
- * The 'value' attribute of the rendered HTML {@code &lt;option&gt;} tag.
+ * The 'value' attribute of the rendered HTML {@code <option>} tag.
*/
private Object value;
/**
- * The text body of the rendered HTML {@code &lt;option&gt;} tag.
+ * The text body of the rendered HTML {@code <option>} tag.
*/
private String label;
@@ -93,14 +93,14 @@ public class OptionTag extends AbstractHtmlElementBodyTag implements BodyTag {
/**
- * Set the 'value' attribute of the rendered HTML {@code &lt;option&gt;} tag.
+ * Set the 'value' attribute of the rendered HTML {@code <option>} tag.
*/
public void setValue(Object value) {
this.value = value;
}
/**
- * Get the 'value' attribute of the rendered HTML {@code &lt;option&gt;} tag.
+ * Get the 'value' attribute of the rendered HTML {@code <option>} tag.
*/
protected Object getValue() {
return this.value;
@@ -121,7 +121,7 @@ public class OptionTag extends AbstractHtmlElementBodyTag implements BodyTag {
}
/**
- * Set the text body of the rendered HTML {@code &lt;option&gt;} tag.
+ * Set the text body of the rendered HTML {@code <option>} tag.
* <p>May be a runtime expression.
*/
public void setLabel(String label) {
@@ -130,7 +130,7 @@ public class OptionTag extends AbstractHtmlElementBodyTag implements BodyTag {
}
/**
- * Get the text body of the rendered HTML {@code &lt;option&gt;} tag.
+ * Get the text body of the rendered HTML {@code <option>} tag.
*/
protected String getLabel() {
return this.label;
diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/tags/form/TextareaTag.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/tags/form/TextareaTag.java
index 09539fbb..16987862 100644
--- a/spring-webmvc/src/main/java/org/springframework/web/servlet/tags/form/TextareaTag.java
+++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/tags/form/TextareaTag.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2012 the original author or authors.
+ * Copyright 2002-2016 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.
@@ -98,7 +98,7 @@ public class TextareaTag extends AbstractHtmlInputElementTag {
writeOptionalAttribute(tagWriter, COLS_ATTRIBUTE, getCols());
writeOptionalAttribute(tagWriter, ONSELECT_ATTRIBUTE, getOnselect());
String value = getDisplayString(getBoundValue(), getPropertyEditor());
- tagWriter.appendValue(processFieldValue(getName(), value, "textarea"));
+ tagWriter.appendValue("\r\n" + processFieldValue(getName(), value, "textarea"));
tagWriter.endTag();
return SKIP_BODY;
}
diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/view/ContentNegotiatingViewResolver.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/view/ContentNegotiatingViewResolver.java
index 2b902c36..511cd4c4 100644
--- a/spring-webmvc/src/main/java/org/springframework/web/servlet/view/ContentNegotiatingViewResolver.java
+++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/view/ContentNegotiatingViewResolver.java
@@ -23,17 +23,15 @@ import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
-import java.util.Properties;
import java.util.Set;
-import javax.activation.FileTypeMap;
import javax.servlet.ServletContext;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.BeanFactoryUtils;
import org.springframework.beans.factory.InitializingBean;
-import org.springframework.core.OrderComparator;
import org.springframework.core.Ordered;
+import org.springframework.core.annotation.AnnotationAwareOrderComparator;
import org.springframework.http.MediaType;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
@@ -93,7 +91,7 @@ public class ContentNegotiatingViewResolver extends WebApplicationObjectSupport
private ContentNegotiationManager contentNegotiationManager;
- private final ContentNegotiationManagerFactoryBean cnManagerFactoryBean = new ContentNegotiationManagerFactoryBean();
+ private final ContentNegotiationManagerFactoryBean cnmFactoryBean = new ContentNegotiationManagerFactoryBean();
private boolean useNotAcceptableStatusCode = false;
@@ -130,90 +128,6 @@ public class ContentNegotiatingViewResolver extends WebApplicationObjectSupport
}
/**
- * Indicate whether the extension of the request path should be used to determine the requested media type,
- * in favor of looking at the {@code Accept} header. The default value is {@code true}.
- * <p>For instance, when this flag is {@code true} (the default), a request for {@code /hotels.pdf}
- * will result in an {@code AbstractPdfView} being resolved, while the {@code Accept} header can be the
- * browser-defined {@code text/html,application/xhtml+xml}.
- * @deprecated use {@link #setContentNegotiationManager(ContentNegotiationManager)}
- */
- @Deprecated
- public void setFavorPathExtension(boolean favorPathExtension) {
- this.cnManagerFactoryBean.setFavorPathExtension(favorPathExtension);
- }
-
- /**
- * Indicate whether to use the Java Activation Framework to map from file extensions to media types.
- * <p>Default is {@code true}, i.e. the Java Activation Framework is used (if available).
- * @deprecated use {@link #setContentNegotiationManager(ContentNegotiationManager)}
- */
- @Deprecated
- public void setUseJaf(boolean useJaf) {
- this.cnManagerFactoryBean.setUseJaf(useJaf);
- }
-
- /**
- * Indicate whether a request parameter should be used to determine the requested media type,
- * in favor of looking at the {@code Accept} header. The default value is {@code false}.
- * <p>For instance, when this flag is {@code true}, a request for {@code /hotels?format=pdf} will result
- * in an {@code AbstractPdfView} being resolved, while the {@code Accept} header can be the browser-defined
- * {@code text/html,application/xhtml+xml}.
- * @deprecated use {@link #setContentNegotiationManager(ContentNegotiationManager)}
- */
- @Deprecated
- public void setFavorParameter(boolean favorParameter) {
- this.cnManagerFactoryBean.setFavorParameter(favorParameter);
- }
-
- /**
- * Set the parameter name that can be used to determine the requested media type if the {@link
- * #setFavorParameter} property is {@code true}. The default parameter name is {@code format}.
- * @deprecated use {@link #setContentNegotiationManager(ContentNegotiationManager)}
- */
- @Deprecated
- public void setParameterName(String parameterName) {
- this.cnManagerFactoryBean.setParameterName(parameterName);
- }
-
- /**
- * Indicate whether the HTTP {@code Accept} header should be ignored. Default is {@code false}.
- * <p>If set to {@code true}, this view resolver will only refer to the file extension and/or
- * parameter, as indicated by the {@link #setFavorPathExtension favorPathExtension} and
- * {@link #setFavorParameter favorParameter} properties.
- * @deprecated use {@link #setContentNegotiationManager(ContentNegotiationManager)}
- */
- @Deprecated
- public void setIgnoreAcceptHeader(boolean ignoreAcceptHeader) {
- this.cnManagerFactoryBean.setIgnoreAcceptHeader(ignoreAcceptHeader);
- }
-
- /**
- * Set the mapping from file extensions to media types.
- * <p>When this mapping is not set or when an extension is not present, this view resolver
- * will fall back to using a {@link FileTypeMap} when the Java Action Framework is available.
- * @deprecated use {@link #setContentNegotiationManager(ContentNegotiationManager)}
- */
- @Deprecated
- public void setMediaTypes(Map<String, String> mediaTypes) {
- if (mediaTypes != null) {
- Properties props = new Properties();
- props.putAll(mediaTypes);
- this.cnManagerFactoryBean.setMediaTypes(props);
- }
- }
-
- /**
- * Set the default content type.
- * <p>This content type will be used when file extension, parameter, nor {@code Accept}
- * header define a content-type, either through being disabled or empty.
- * @deprecated use {@link #setContentNegotiationManager(ContentNegotiationManager)}
- */
- @Deprecated
- public void setDefaultContentType(MediaType defaultContentType) {
- this.cnManagerFactoryBean.setDefaultContentType(defaultContentType);
- }
-
- /**
* Indicate whether a {@link HttpServletResponse#SC_NOT_ACCEPTABLE 406 Not Acceptable}
* status code should be returned if no suitable view can be found.
* <p>Default is {@code false}, meaning that this view resolver returns {@code null} for
@@ -284,15 +198,15 @@ public class ContentNegotiatingViewResolver extends WebApplicationObjectSupport
logger.warn("Did not find any ViewResolvers to delegate to; please configure them using the " +
"'viewResolvers' property on the ContentNegotiatingViewResolver");
}
- OrderComparator.sort(this.viewResolvers);
- this.cnManagerFactoryBean.setServletContext(servletContext);
+ AnnotationAwareOrderComparator.sort(this.viewResolvers);
+ this.cnmFactoryBean.setServletContext(servletContext);
}
@Override
public void afterPropertiesSet() {
if (this.contentNegotiationManager == null) {
- this.cnManagerFactoryBean.afterPropertiesSet();
- this.contentNegotiationManager = this.cnManagerFactoryBean.getObject();
+ this.cnmFactoryBean.afterPropertiesSet();
+ this.contentNegotiationManager = this.cnmFactoryBean.getObject();
}
}
diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/view/InternalResourceViewResolver.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/view/InternalResourceViewResolver.java
index eb9a1bf5..679e8a95 100644
--- a/spring-webmvc/src/main/java/org/springframework/web/servlet/view/InternalResourceViewResolver.java
+++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/view/InternalResourceViewResolver.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2014 the original author or authors.
+ * Copyright 2002-2015 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.
@@ -60,7 +60,7 @@ public class InternalResourceViewResolver extends UrlBasedViewResolver {
*/
public InternalResourceViewResolver() {
Class<?> viewClass = requiredViewClass();
- if (viewClass.equals(InternalResourceView.class) && jstlPresent) {
+ if (InternalResourceView.class == viewClass && jstlPresent) {
viewClass = JstlView.class;
}
setViewClass(viewClass);
diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/view/RedirectView.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/view/RedirectView.java
index c3a4e380..0a188696 100644
--- a/spring-webmvc/src/main/java/org/springframework/web/servlet/view/RedirectView.java
+++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/view/RedirectView.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2014 the original author or authors.
+ * Copyright 2002-2015 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.
@@ -36,14 +36,12 @@ import org.springframework.http.HttpStatus;
import org.springframework.util.CollectionUtils;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
-import org.springframework.web.context.ContextLoader;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.servlet.FlashMap;
import org.springframework.web.servlet.FlashMapManager;
import org.springframework.web.servlet.HandlerMapping;
import org.springframework.web.servlet.SmartView;
import org.springframework.web.servlet.View;
-import org.springframework.web.servlet.support.RequestContext;
import org.springframework.web.servlet.support.RequestContextUtils;
import org.springframework.web.servlet.support.RequestDataValueProcessor;
import org.springframework.web.util.UriComponents;
@@ -299,7 +297,7 @@ public class RedirectView extends AbstractUrlBasedView implements SmartView {
}
/**
- * Creates the target URL by checking if the redirect string is a URI template first,
+ * Create the target URL by checking if the redirect string is a URI template first,
* expanding it with the given model, and then optionally appending simple type model
* attributes as query String parameters.
*/
@@ -554,27 +552,23 @@ public class RedirectView extends AbstractUrlBasedView implements SmartView {
/**
* Find the registered {@link RequestDataValueProcessor}, if any, and allow
* it to update the redirect target URL.
+ * @param targetUrl the given redirect URL
* @return the updated URL or the same as URL as the one passed in
*/
protected String updateTargetUrl(String targetUrl, Map<String, Object> model,
HttpServletRequest request, HttpServletResponse response) {
- RequestContext requestContext = null;
- if (getWebApplicationContext() != null) {
- requestContext = createRequestContext(request, response, model);
+ WebApplicationContext wac = getWebApplicationContext();
+ if (wac == null) {
+ wac = RequestContextUtils.findWebApplicationContext(request, getServletContext());
}
- else {
- WebApplicationContext wac = ContextLoader.getCurrentWebApplicationContext();
- if (wac != null && wac.getServletContext() != null) {
- requestContext = new RequestContext(request, response, wac.getServletContext(), model);
- }
- }
- if (requestContext != null) {
- RequestDataValueProcessor processor = requestContext.getRequestDataValueProcessor();
- if (processor != null) {
- targetUrl = processor.processUrl(request, targetUrl);
- }
+
+ if (wac != null && wac.containsBean(RequestContextUtils.REQUEST_DATA_VALUE_PROCESSOR_BEAN_NAME)) {
+ RequestDataValueProcessor processor = wac.getBean(
+ RequestContextUtils.REQUEST_DATA_VALUE_PROCESSOR_BEAN_NAME, RequestDataValueProcessor.class);
+ return processor.processUrl(request, targetUrl);
}
+
return targetUrl;
}
@@ -591,10 +585,15 @@ public class RedirectView extends AbstractUrlBasedView implements SmartView {
String encodedRedirectURL = response.encodeRedirectURL(targetUrl);
if (http10Compatible) {
+ HttpStatus attributeStatusCode = (HttpStatus) request.getAttribute(View.RESPONSE_STATUS_ATTRIBUTE);
if (this.statusCode != null) {
response.setStatus(this.statusCode.value());
response.setHeader("Location", encodedRedirectURL);
}
+ else if (attributeStatusCode != null) {
+ response.setStatus(attributeStatusCode.value());
+ response.setHeader("Location", encodedRedirectURL);
+ }
else {
// Send status code 302 by default.
response.sendRedirect(encodedRedirectURL);
diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/view/document/AbstractExcelView.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/view/document/AbstractExcelView.java
index 425ac888..c5a1d619 100644
--- a/spring-webmvc/src/main/java/org/springframework/web/servlet/view/document/AbstractExcelView.java
+++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/view/document/AbstractExcelView.java
@@ -91,7 +91,10 @@ import org.springframework.web.servlet.view.AbstractView;
* @author Jean-Pierre Pawlak
* @author Juergen Hoeller
* @see AbstractPdfView
+ * @deprecated as of Spring 4.2, in favor of {@link AbstractXlsView} and its
+ * {@link AbstractXlsxView} and {@link AbstractXlsxStreamingView} variants
*/
+@Deprecated
public abstract class AbstractExcelView extends AbstractView {
/** The content type for an Excel response */
diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/view/document/AbstractXlsView.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/view/document/AbstractXlsView.java
new file mode 100644
index 00000000..4279bb3e
--- /dev/null
+++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/view/document/AbstractXlsView.java
@@ -0,0 +1,120 @@
+/*
+ * Copyright 2002-2016 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.document;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.util.Map;
+import javax.servlet.ServletOutputStream;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.apache.poi.hssf.usermodel.HSSFWorkbook;
+import org.apache.poi.ss.usermodel.Workbook;
+
+import org.springframework.web.servlet.view.AbstractView;
+
+/**
+ * Convenient superclass for Excel document views in traditional XLS format.
+ * Compatible with Apache POI 3.5 and higher.
+ *
+ * <p>For working with the workbook in the subclass, see
+ * <a href="http://poi.apache.org">Apache's POI site</a>
+ *
+ * @author Juergen Hoeller
+ * @since 4.2
+ */
+public abstract class AbstractXlsView extends AbstractView {
+
+ /**
+ * Default Constructor.
+ * Sets the content type of the view to "application/vnd.ms-excel".
+ */
+ public AbstractXlsView() {
+ setContentType("application/vnd.ms-excel");
+ }
+
+
+ @Override
+ protected boolean generatesDownloadContent() {
+ return true;
+ }
+
+ /**
+ * Renders the Excel view, given the specified model.
+ */
+ @Override
+ protected final void renderMergedOutputModel(
+ Map<String, Object> model, HttpServletRequest request, HttpServletResponse response) throws Exception {
+
+ // Create a fresh workbook instance for this render step.
+ Workbook workbook = createWorkbook(model, request);
+
+ // Delegate to application-provided document code.
+ buildExcelDocument(model, workbook, request, response);
+
+ // Set the content type.
+ response.setContentType(getContentType());
+
+ // Flush byte array to servlet output stream.
+ renderWorkbook(workbook, response);;
+ }
+
+
+ /**
+ * Template method for creating the POI {@link Workbook} instance.
+ * <p>The default implementation creates a traditional {@link HSSFWorkbook}.
+ * Spring-provided subclasses are overriding this for the OOXML-based variants;
+ * custom subclasses may override this for reading a workbook from a file.
+ * @param model the model Map
+ * @param request current HTTP request (for taking the URL or headers into account)
+ * @return the new {@link Workbook} instance
+ */
+ protected Workbook createWorkbook(Map<String, Object> model, HttpServletRequest request) {
+ return new HSSFWorkbook();
+ }
+
+ /**
+ * The actual render step: taking the POI {@link Workbook} and rendering
+ * it to the given response.
+ * @param workbook the POI Workbook to render
+ * @param response current HTTP response
+ * @throws IOException when thrown by I/O methods that we're delegating to
+ */
+ protected void renderWorkbook(Workbook workbook, HttpServletResponse response) throws IOException {
+ ServletOutputStream out = response.getOutputStream();
+ workbook.write(out);
+
+ // Closeable only implemented as of POI 3.10
+ if (workbook instanceof Closeable) {
+ ((Closeable) workbook).close();
+ }
+ }
+
+ /**
+ * Application-provided subclasses must implement this method to populate
+ * the Excel workbook document, given the model.
+ * @param model the model Map
+ * @param workbook the Excel workbook to populate
+ * @param request in case we need locale etc. Shouldn't look at attributes.
+ * @param response in case we need to set cookies. Shouldn't write to it.
+ */
+ protected abstract void buildExcelDocument(
+ Map<String, Object> model, Workbook workbook, HttpServletRequest request, HttpServletResponse response)
+ throws Exception;
+
+}
diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/view/document/AbstractXlsxStreamingView.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/view/document/AbstractXlsxStreamingView.java
new file mode 100644
index 00000000..2db6b646
--- /dev/null
+++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/view/document/AbstractXlsxStreamingView.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright 2002-2015 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.document;
+
+import java.io.IOException;
+import java.util.Map;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.apache.poi.ss.usermodel.Workbook;
+import org.apache.poi.xssf.streaming.SXSSFWorkbook;
+
+/**
+ * Convenient superclass for Excel document views in the Office 2007 XLSX format,
+ * using POI's streaming variant. Compatible with Apache POI 3.9 and higher.
+ *
+ * <p>For working with the workbook in subclasses, see
+ * <a href="http://poi.apache.org">Apache's POI site</a>.
+ *
+ * @author Juergen Hoeller
+ * @since 4.2
+ */
+public abstract class AbstractXlsxStreamingView extends AbstractXlsxView {
+
+ /**
+ * This implementation creates a {@link SXSSFWorkbook} for streaming the XLSX format.
+ */
+ @Override
+ protected SXSSFWorkbook createWorkbook(Map<String, Object> model, HttpServletRequest request) {
+ return new SXSSFWorkbook();
+ }
+
+ /**
+ * This implementation disposes of the {@link SXSSFWorkbook} when done with rendering.
+ * @see org.apache.poi.xssf.streaming.SXSSFWorkbook#dispose()
+ */
+ @Override
+ protected void renderWorkbook(Workbook workbook, HttpServletResponse response) throws IOException {
+ super.renderWorkbook(workbook, response);
+
+ // Dispose of temporary files in case of streaming variant...
+ ((SXSSFWorkbook) workbook).dispose();
+ }
+
+}
diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/view/document/AbstractXlsxView.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/view/document/AbstractXlsxView.java
new file mode 100644
index 00000000..4c26ffa7
--- /dev/null
+++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/view/document/AbstractXlsxView.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2002-2015 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.document;
+
+import java.util.Map;
+import javax.servlet.http.HttpServletRequest;
+
+import org.apache.poi.ss.usermodel.Workbook;
+import org.apache.poi.xssf.usermodel.XSSFWorkbook;
+
+/**
+ * Convenient superclass for Excel document views in the Office 2007 XLSX format
+ * (as supported by POI-OOXML). Compatible with Apache POI 3.5 and higher.
+ *
+ * <p>For working with the workbook in subclasses, see
+ * <a href="http://poi.apache.org">Apache's POI site</a>.
+ *
+ * @author Juergen Hoeller
+ * @since 4.2
+ */
+public abstract class AbstractXlsxView extends AbstractXlsView {
+
+ /**
+ * Default Constructor.
+ * <p>Sets the content type of the view to
+ * {@code "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"}.
+ */
+ public AbstractXlsxView() {
+ setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
+ }
+
+ /**
+ * This implementation creates an {@link XSSFWorkbook} for the XLSX format.
+ */
+ @Override
+ protected Workbook createWorkbook(Map<String, Object> model, HttpServletRequest request) {
+ return new XSSFWorkbook();
+ }
+
+}
diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/view/freemarker/spring.ftl b/spring-webmvc/src/main/java/org/springframework/web/servlet/view/freemarker/spring.ftl
deleted file mode 100644
index bfb41221..00000000
--- a/spring-webmvc/src/main/java/org/springframework/web/servlet/view/freemarker/spring.ftl
+++ /dev/null
@@ -1,383 +0,0 @@
-<#ftl strip_whitespace=true>
-<#--
- * spring.ftl
- *
- * This file consists of a collection of FreeMarker macros aimed at easing
- * some of the common requirements of web applications - in particular
- * handling of forms.
- *
- * Spring's FreeMarker support will automatically make this file and therefore
- * all macros within it available to any application using Spring's
- * FreeMarkerConfigurer.
- *
- * To take advantage of these macros, the "exposeSpringMacroHelpers" property
- * of the FreeMarker class needs to be set to "true". This will expose a
- * RequestContext under the name "springMacroRequestContext", as needed by
- * the macros in this library.
- *
- * @author Darren Davison
- * @author Juergen Hoeller
- * @since 1.1
- -->
-
-<#--
- * message
- *
- * Macro to translate a message code into a message.
- -->
-<#macro message code>${springMacroRequestContext.getMessage(code)}</#macro>
-
-<#--
- * messageText
- *
- * Macro to translate a message code into a message,
- * using the given default text if no message found.
- -->
-<#macro messageText code, text>${springMacroRequestContext.getMessage(code, text)}</#macro>
-
-<#--
- * messageArgs
- *
- * Macro to translate a message code with arguments into a message.
- -->
-<#macro messageArgs code, args>${springMacroRequestContext.getMessage(code, args)}</#macro>
-
-<#--
- * messageArgsText
- *
- * Macro to translate a message code with arguments into a message,
- * using the given default text if no message found.
- -->
-<#macro messageArgsText code, args, text>${springMacroRequestContext.getMessage(code, args, text)}</#macro>
-
-<#--
- * theme
- *
- * Macro to translate a theme message code into a message.
- -->
-<#macro theme code>${springMacroRequestContext.getThemeMessage(code)}</#macro>
-
-<#--
- * themeText
- *
- * Macro to translate a theme message code into a message,
- * using the given default text if no message found.
- -->
-<#macro themeText code, text>${springMacroRequestContext.getThemeMessage(code, text)}</#macro>
-
-<#--
- * themeArgs
- *
- * Macro to translate a theme message code with arguments into a message.
- -->
-<#macro themeArgs code, args>${springMacroRequestContext.getThemeMessage(code, args)}</#macro>
-
-<#--
- * themeArgsText
- *
- * Macro to translate a theme message code with arguments into a message,
- * using the given default text if no message found.
- -->
-<#macro themeArgsText code, args, text>${springMacroRequestContext.getThemeMessage(code, args, text)}</#macro>
-
-<#--
- * url
- *
- * Takes a relative URL and makes it absolute from the server root by
- * adding the context root for the web application.
- -->
-<#macro url relativeUrl extra...><#if extra?? && extra?size!=0>${springMacroRequestContext.getContextUrl(relativeUrl,extra)}<#else>${springMacroRequestContext.getContextUrl(relativeUrl)}</#if></#macro>
-
-<#--
- * bind
- *
- * Exposes a BindStatus object for the given bind path, which can be
- * a bean (e.g. "person") to get global errors, or a bean property
- * (e.g. "person.name") to get field errors. Can be called multiple times
- * within a form to bind to multiple command objects and/or field names.
- *
- * This macro will participate in the default HTML escape setting for the given
- * RequestContext. This can be customized by calling "setDefaultHtmlEscape"
- * on the "springMacroRequestContext" context variable, or via the
- * "defaultHtmlEscape" context-param in web.xml (same as for the JSP bind tag).
- * Also regards a "htmlEscape" variable in the namespace of this library.
- *
- * Producing no output, the following context variable will be available
- * each time this macro is referenced (assuming you import this library in
- * your templates with the namespace 'spring'):
- *
- * spring.status : a BindStatus instance holding the command object name,
- * expression, value, and error messages and codes for the path supplied
- *
- * @param path : the path (string value) of the value required to bind to.
- * Spring defaults to a command name of "command" but this can be overridden
- * by user config.
- -->
-<#macro bind path>
- <#if htmlEscape?exists>
- <#assign status = springMacroRequestContext.getBindStatus(path, htmlEscape)>
- <#else>
- <#assign status = springMacroRequestContext.getBindStatus(path)>
- </#if>
- <#-- assign a temporary value, forcing a string representation for any
- kind of variable. This temp value is only used in this macro lib -->
- <#if status.value?exists && status.value?is_boolean>
- <#assign stringStatusValue=status.value?string>
- <#else>
- <#assign stringStatusValue=status.value?default("")>
- </#if>
-</#macro>
-
-<#--
- * bindEscaped
- *
- * Similar to spring:bind, but takes an explicit HTML escape flag rather
- * than relying on the default HTML escape setting.
- -->
-<#macro bindEscaped path, htmlEscape>
- <#assign status = springMacroRequestContext.getBindStatus(path, htmlEscape)>
- <#-- assign a temporary value, forcing a string representation for any
- kind of variable. This temp value is only used in this macro lib -->
- <#if status.value?exists && status.value?is_boolean>
- <#assign stringStatusValue=status.value?string>
- <#else>
- <#assign stringStatusValue=status.value?default("")>
- </#if>
-</#macro>
-
-<#--
- * formInput
- *
- * Display a form input field of type 'text' and bind it to an attribute
- * of a command or bean.
- *
- * @param path the name of the field to bind to
- * @param attributes any additional attributes for the element (such as class
- * or CSS styles or size
- -->
-<#macro formInput path attributes="" fieldType="text">
- <@bind path/>
- <input type="${fieldType}" id="${status.expression?replace('[','')?replace(']','')}" name="${status.expression}" value="<#if fieldType!="password">${stringStatusValue}</#if>" ${attributes}<@closeTag/>
-</#macro>
-
-<#--
- * formPasswordInput
- *
- * Display a form input field of type 'password' and bind it to an attribute
- * of a command or bean. No value will ever be displayed. This functionality
- * can also be obtained by calling the formInput macro with a 'type' parameter
- * of 'password'.
- *
- * @param path the name of the field to bind to
- * @param attributes any additional attributes for the element (such as class
- * or CSS styles or size
- -->
-<#macro formPasswordInput path attributes="">
- <@formInput path, attributes, "password"/>
-</#macro>
-
-<#--
- * formHiddenInput
- *
- * Generate a form input field of type 'hidden' and bind it to an attribute
- * of a command or bean. This functionality can also be obtained by calling
- * the formInput macro with a 'type' parameter of 'hidden'.
- *
- * @param path the name of the field to bind to
- * @param attributes any additional attributes for the element (such as class
- * or CSS styles or size
- -->
-<#macro formHiddenInput path attributes="">
- <@formInput path, attributes, "hidden"/>
-</#macro>
-
-<#--
- * formTextarea
- *
- * Display a text area and bind it to an attribute of a command or bean.
- *
- * @param path the name of the field to bind to
- * @param attributes any additional attributes for the element (such as class
- * or CSS styles or size
- -->
-<#macro formTextarea path attributes="">
- <@bind path/>
- <textarea id="${status.expression?replace('[','')?replace(']','')}" name="${status.expression}" ${attributes}>${stringStatusValue}</textarea>
-</#macro>
-
-<#--
- * formSingleSelect
- *
- * Show a selectbox (dropdown) input element allowing a single value to be chosen
- * from a list of options.
- *
- * @param path the name of the field to bind to
- * @param options a map (value=label) of all the available options
- * @param attributes any additional attributes for the element (such as class
- * or CSS styles or size
--->
-<#macro formSingleSelect path options attributes="">
- <@bind path/>
- <select id="${status.expression?replace('[','')?replace(']','')}" name="${status.expression}" ${attributes}>
- <#if options?is_hash>
- <#list options?keys as value>
- <option value="${value?html}"<@checkSelected value/>>${options[value]?html}</option>
- </#list>
- <#else>
- <#list options as value>
- <option value="${value?html}"<@checkSelected value/>>${value?html}</option>
- </#list>
- </#if>
- </select>
-</#macro>
-
-<#--
- * formMultiSelect
- *
- * Show a listbox of options allowing the user to make 0 or more choices from
- * the list of options.
- *
- * @param path the name of the field to bind to
- * @param options a map (value=label) of all the available options
- * @param attributes any additional attributes for the element (such as class
- * or CSS styles or size
--->
-<#macro formMultiSelect path options attributes="">
- <@bind path/>
- <select multiple="multiple" id="${status.expression?replace('[','')?replace(']','')}" name="${status.expression}" ${attributes}>
- <#list options?keys as value>
- <#assign isSelected = contains(status.actualValue?default([""]), value)>
- <option value="${value?html}"<#if isSelected> selected="selected"</#if>>${options[value]?html}</option>
- </#list>
- </select>
-</#macro>
-
-<#--
- * formRadioButtons
- *
- * Show radio buttons.
- *
- * @param path the name of the field to bind to
- * @param options a map (value=label) of all the available options
- * @param separator the html tag or other character list that should be used to
- * separate each option. Typically '&nbsp;' or '<br>'
- * @param attributes any additional attributes for the element (such as class
- * or CSS styles or size
--->
-<#macro formRadioButtons path options separator attributes="">
- <@bind path/>
- <#list options?keys as value>
- <#assign id="${status.expression?replace('[','')?replace(']','')}${value_index}">
- <input type="radio" id="${id}" name="${status.expression}" value="${value?html}"<#if stringStatusValue == value> checked="checked"</#if> ${attributes}<@closeTag/>
- <label for="${id}">${options[value]?html}</label>${separator}
- </#list>
-</#macro>
-
-<#--
- * formCheckboxes
- *
- * Show checkboxes.
- *
- * @param path the name of the field to bind to
- * @param options a map (value=label) of all the available options
- * @param separator the html tag or other character list that should be used to
- * separate each option. Typically '&nbsp;' or '<br>'
- * @param attributes any additional attributes for the element (such as class
- * or CSS styles or size
--->
-<#macro formCheckboxes path options separator attributes="">
- <@bind path/>
- <#list options?keys as value>
- <#assign id="${status.expression?replace('[','')?replace(']','')}${value_index}">
- <#assign isSelected = contains(status.actualValue?default([""]), value)>
- <input type="checkbox" id="${id}" name="${status.expression}" value="${value?html}"<#if isSelected> checked="checked"</#if> ${attributes}<@closeTag/>
- <label for="${id}">${options[value]?html}</label>${separator}
- </#list>
- <input type="hidden" name="_${status.expression}" value="on"/>
-</#macro>
-
-<#--
- * formCheckbox
- *
- * Show a single checkbox.
- *
- * @param path the name of the field to bind to
- * @param attributes any additional attributes for the element (such as class
- * or CSS styles or size
--->
-<#macro formCheckbox path attributes="">
- <@bind path />
- <#assign id="${status.expression?replace('[','')?replace(']','')}">
- <#assign isSelected = status.value?? && status.value?string=="true">
- <input type="hidden" name="_${status.expression}" value="on"/>
- <input type="checkbox" id="${id}" name="${status.expression}"<#if isSelected> checked="checked"</#if> ${attributes}/>
-</#macro>
-
-<#--
- * showErrors
- *
- * Show validation errors for the currently bound field, with
- * optional style attributes.
- *
- * @param separator the html tag or other character list that should be used to
- * separate each option. Typically '<br>'.
- * @param classOrStyle either the name of a CSS class element (which is defined in
- * the template or an external CSS file) or an inline style. If the value passed in here
- * contains a colon (:) then a 'style=' attribute will be used, else a 'class=' attribute
- * will be used.
--->
-<#macro showErrors separator classOrStyle="">
- <#list status.errorMessages as error>
- <#if classOrStyle == "">
- <b>${error}</b>
- <#else>
- <#if classOrStyle?index_of(":") == -1><#assign attr="class"><#else><#assign attr="style"></#if>
- <span ${attr}="${classOrStyle}">${error}</span>
- </#if>
- <#if error_has_next>${separator}</#if>
- </#list>
-</#macro>
-
-<#--
- * checkSelected
- *
- * Check a value in a list to see if it is the currently selected value.
- * If so, add the 'selected="selected"' text to the output.
- * Handles values of numeric and string types.
- * This function is used internally but can be accessed by user code if required.
- *
- * @param value the current value in a list iteration
--->
-<#macro checkSelected value>
- <#if stringStatusValue?is_number && stringStatusValue == value?number>selected="selected"</#if>
- <#if stringStatusValue?is_string && stringStatusValue == value>selected="selected"</#if>
-</#macro>
-
-<#--
- * contains
- *
- * Macro to return true if the list contains the scalar, false if not.
- * Surprisingly not a FreeMarker builtin.
- * This function is used internally but can be accessed by user code if required.
- *
- * @param list the list to search for the item
- * @param item the item to search for in the list
- * @return true if item is found in the list, false otherwise
--->
-<#function contains list item>
- <#list list as nextInList>
- <#if nextInList == item><#return true></#if>
- </#list>
- <#return false>
-</#function>
-
-<#--
- * closeTag
- *
- * Simple macro to close an HTML tag that has no body with '>' or '/>',
- * depending on the value of a 'xhtmlCompliant' variable in the namespace
- * of this library.
--->
-<#macro closeTag>
- <#if xhtmlCompliant?exists && xhtmlCompliant>/><#else>></#if>
-</#macro>
diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/view/jasperreports/JasperReportsMultiFormatView.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/view/jasperreports/JasperReportsMultiFormatView.java
index 2ae6bf8a..7e9af07d 100644
--- a/spring-webmvc/src/main/java/org/springframework/web/servlet/view/jasperreports/JasperReportsMultiFormatView.java
+++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/view/jasperreports/JasperReportsMultiFormatView.java
@@ -53,6 +53,7 @@ import org.springframework.util.CollectionUtils;
* <li>{@code html} - {@code JasperReportsHtmlView}</li>
* <li>{@code pdf} - {@code JasperReportsPdfView}</li>
* <li>{@code xls} - {@code JasperReportsXlsView}</li>
+ * <li>{@code xlsx} - {@code JasperReportsXlsxView}</li> (as of Spring 4.2)
* </ul>
*
* <p>The format key can be changed using the {@code formatKey} property.
@@ -100,6 +101,7 @@ public class JasperReportsMultiFormatView extends AbstractJasperReportsView {
this.formatMappings.put("html", JasperReportsHtmlView.class);
this.formatMappings.put("pdf", JasperReportsPdfView.class);
this.formatMappings.put("xls", JasperReportsXlsView.class);
+ this.formatMappings.put("xlsx", JasperReportsXlsxView.class);
}
@@ -119,6 +121,7 @@ public class JasperReportsMultiFormatView extends AbstractJasperReportsView {
* <li>{@code html} - {@code JasperReportsHtmlView}</li>
* <li>{@code pdf} - {@code JasperReportsPdfView}</li>
* <li>{@code xls} - {@code JasperReportsXlsView}</li>
+ * <li>{@code xlsx} - {@code JasperReportsXlsxView}</li> (as of Spring 4.2)
* </ul>
*/
public void setFormatMappings(Map<String, Class<? extends AbstractJasperReportsView>> formatMappings) {
diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/view/jasperreports/JasperReportsXlsxView.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/view/jasperreports/JasperReportsXlsxView.java
new file mode 100644
index 00000000..f86f3ce2
--- /dev/null
+++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/view/jasperreports/JasperReportsXlsxView.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2002-2015 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.jasperreports;
+
+import net.sf.jasperreports.engine.export.ooxml.JRXlsxExporter;
+
+/**
+ * Implementation of {@code AbstractJasperReportsSingleFormatView}
+ * that renders report results in XLSX format.
+ *
+ * <p><b>This class is compatible with classic JasperReports releases back until 2.x.</b>
+ * As a consequence, it keeps using the {@link net.sf.jasperreports.engine.JRExporter}
+ * API which got deprecated as of JasperReports 5.5.2 (early 2014).
+ *
+ * @author Rob Harrop
+ * @author Juergen Hoeller
+ * @since 4.2
+ */
+@SuppressWarnings({"deprecation", "rawtypes"})
+public class JasperReportsXlsxView extends AbstractJasperReportsSingleFormatView {
+
+ public JasperReportsXlsxView() {
+ setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
+ }
+
+ @Override
+ protected net.sf.jasperreports.engine.JRExporter createExporter() {
+ return new JRXlsxExporter();
+ }
+
+ @Override
+ protected boolean useWriter() {
+ return false;
+ }
+
+}
diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/view/json/AbstractJackson2View.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/view/json/AbstractJackson2View.java
index 3ce9ecdf..34b75fa2 100644
--- a/spring-webmvc/src/main/java/org/springframework/web/servlet/view/json/AbstractJackson2View.java
+++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/view/json/AbstractJackson2View.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2014 the original author or authors.
+ * Copyright 2002-2015 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.
@@ -28,6 +28,7 @@ import com.fasterxml.jackson.core.JsonEncoding;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
+import com.fasterxml.jackson.databind.ser.FilterProvider;
import org.springframework.http.converter.json.MappingJacksonValue;
import org.springframework.util.Assert;
@@ -60,7 +61,7 @@ public abstract class AbstractJackson2View extends AbstractView {
protected AbstractJackson2View(ObjectMapper objectMapper, String contentType) {
- this.objectMapper = objectMapper;
+ setObjectMapper(objectMapper);
setContentType(contentType);
setExposePathVariables(false);
}
@@ -122,12 +123,6 @@ public abstract class AbstractJackson2View extends AbstractView {
}
/**
- * Set the attribute in the model that should be rendered by this view.
- * When set, all other model attributes will be ignored.
- */
- public abstract void setModelKey(String modelKey);
-
- /**
* Disables caching of the generated JSON.
* <p>Default is {@code true}, which will prevent the client from caching the generated JSON.
*/
@@ -150,9 +145,7 @@ public abstract class AbstractJackson2View extends AbstractView {
setResponseContentType(request, response);
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);
+ response.addHeader("Cache-Control", "no-store");
}
}
@@ -178,45 +171,42 @@ public abstract class AbstractJackson2View extends AbstractView {
protected Object filterAndWrapModel(Map<String, Object> model, HttpServletRequest request) {
Object value = filterModel(model);
Class<?> serializationView = (Class<?>) model.get(JsonView.class.getName());
- if (serializationView != null) {
+ FilterProvider filters = (FilterProvider) model.get(FilterProvider.class.getName());
+ if (serializationView != null || filters != null) {
MappingJacksonValue container = new MappingJacksonValue(value);
container.setSerializationView(serializationView);
+ container.setFilters(filters);
value = container;
}
return value;
}
/**
- * Filter out undesired attributes from the given model.
- * The return value can be either another {@link Map} or a single value object.
- * @param model the model, as passed on to {@link #renderMergedOutputModel}
- * @return the value to be rendered
- */
- protected abstract Object filterModel(Map<String, Object> model);
-
- /**
* Write the actual JSON content to the stream.
* @param stream the output stream to use
* @param object the value to be rendered, as returned from {@link #filterModel}
* @throws IOException if writing failed
*/
- protected void writeContent(OutputStream stream, Object object)
- throws IOException {
-
+ protected void writeContent(OutputStream stream, Object object) throws IOException {
JsonGenerator generator = this.objectMapper.getFactory().createGenerator(stream, this.encoding);
writePrefix(generator, object);
Class<?> serializationView = null;
+ FilterProvider filters = null;
Object value = object;
if (value instanceof MappingJacksonValue) {
MappingJacksonValue container = (MappingJacksonValue) value;
value = container.getValue();
serializationView = container.getSerializationView();
+ filters = container.getFilters();
}
if (serializationView != null) {
this.objectMapper.writerWithView(serializationView).writeValue(generator, value);
}
+ else if (filters != null) {
+ this.objectMapper.writer(filters).writeValue(generator, value);
+ }
else {
this.objectMapper.writeValue(generator, value);
}
@@ -224,13 +214,27 @@ public abstract class AbstractJackson2View extends AbstractView {
generator.flush();
}
+
+ /**
+ * Set the attribute in the model that should be rendered by this view.
+ * When set, all other model attributes will be ignored.
+ */
+ public abstract void setModelKey(String modelKey);
+
+ /**
+ * Filter out undesired attributes from the given model.
+ * The return value can be either another {@link Map} or a single value object.
+ * @param model the model, as passed on to {@link #renderMergedOutputModel}
+ * @return the value to be rendered
+ */
+ protected abstract Object filterModel(Map<String, Object> model);
+
/**
* Write a prefix before the main content.
* @param generator the generator to use for writing content.
* @param object the object to write to the output message.
*/
protected void writePrefix(JsonGenerator generator, Object object) throws IOException {
-
}
/**
@@ -239,7 +243,6 @@ public abstract class AbstractJackson2View extends AbstractView {
* @param object the object to write to the output message.
*/
protected void writeSuffix(JsonGenerator generator, Object object) throws IOException {
-
}
}
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
index 31c76c14..2039e8c0 100644
--- 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
@@ -30,6 +30,7 @@ import javax.servlet.http.HttpServletResponse;
import com.fasterxml.jackson.annotation.JsonView;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.ser.FilterProvider;
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
import org.springframework.http.converter.json.MappingJacksonValue;
@@ -40,7 +41,7 @@ import org.springframework.web.servlet.View;
/**
* Spring MVC {@link View} that renders JSON content by serializing the model for the current request
- * using <a href="http://jackson.codehaus.org/">Jackson 2's</a> {@link ObjectMapper}.
+ * using <a href="http://wiki.fasterxml.com/JacksonHome">Jackson 2's</a> {@link ObjectMapper}.
*
* <p>By default, the entire contents of the model map (with the exception of framework-specific classes)
* will be encoded as JSON. If the model contains only one key, you can have it extracted encoded as JSON
@@ -94,6 +95,15 @@ public class MappingJackson2JsonView extends AbstractJackson2View {
super(Jackson2ObjectMapperBuilder.json().build(), DEFAULT_CONTENT_TYPE);
}
+ /**
+ * Construct a new {@code MappingJackson2JsonView} using the provided
+ * {@link ObjectMapper} and setting the content type to {@code application/json}.
+ * @since 4.2.1
+ */
+ public MappingJackson2JsonView(ObjectMapper objectMapper) {
+ super(objectMapper, DEFAULT_CONTENT_TYPE);
+ }
+
/**
* Specify a custom prefix to use for this view's JSON output.
@@ -105,16 +115,15 @@ public class MappingJackson2JsonView extends AbstractJackson2View {
}
/**
- * Indicates whether the JSON output by this view should be prefixed with <tt>"{} && "</tt>.
+ * Indicates whether the JSON output by this view should be prefixed with <tt>")]}', "</tt>.
* Default is {@code false}.
* <p>Prefixing the JSON string in this manner is used to help prevent JSON Hijacking.
* The prefix renders the string syntactically invalid as a script so that it cannot be hijacked.
- * This prefix does not affect the evaluation of JSON, but if JSON validation is performed
- * on the string, the prefix would need to be ignored.
+ * This prefix should be stripped before parsing the string as JSON.
* @see #setJsonPrefix
*/
public void setPrefixJson(boolean prefixJson) {
- this.jsonPrefix = (prefixJson ? "{} && " : null);
+ this.jsonPrefix = (prefixJson ? ")]}', " : null);
}
/**
@@ -141,29 +150,11 @@ public class MappingJackson2JsonView extends AbstractJackson2View {
}
/**
- * Set the attributes in the model that should be rendered by this view.
- * When set, all other model attributes will be ignored.
- * @deprecated use {@link #setModelKeys(Set)} instead
- */
- @Deprecated
- public void setRenderedAttributes(Set<String> renderedAttributes) {
- this.modelKeys = renderedAttributes;
- }
-
- /**
- * Return the attributes in the model that should be rendered by this view.
- * @deprecated use {@link #getModelKeys()} instead
- */
- @Deprecated
- public final Set<String> getRenderedAttributes() {
- return this.modelKeys;
- }
-
- /**
- * Set whether to serialize models containing a single attribute as a map or whether to
- * extract the single value from the model and serialize it directly.
- * <p>The effect of setting this flag is similar to using {@code MappingJackson2HttpMessageConverter}
- * with an {@code @ResponseBody} request-handling method.
+ * Set whether to serialize models containing a single attribute as a map or
+ * whether to extract the single value from the model and serialize it directly.
+ * <p>The effect of setting this flag is similar to using
+ * {@code MappingJackson2HttpMessageConverter} with an {@code @ResponseBody}
+ * request-handling method.
* <p>Default is {@code false}.
*/
public void setExtractValueFromSingleKeyModel(boolean extractValueFromSingleKeyModel) {
@@ -226,7 +217,8 @@ public class MappingJackson2JsonView extends AbstractJackson2View {
Set<String> modelKeys = (!CollectionUtils.isEmpty(this.modelKeys) ? this.modelKeys : model.keySet());
for (Map.Entry<String, Object> entry : model.entrySet()) {
if (!(entry.getValue() instanceof BindingResult) && modelKeys.contains(entry.getKey()) &&
- !entry.getKey().equals(JsonView.class.getName())) {
+ !entry.getKey().equals(JsonView.class.getName()) &&
+ !entry.getKey().equals(FilterProvider.class.getName())) {
result.put(entry.getKey(), entry.getValue());
}
}
diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/view/script/ScriptTemplateConfig.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/view/script/ScriptTemplateConfig.java
new file mode 100644
index 00000000..a11968bd
--- /dev/null
+++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/view/script/ScriptTemplateConfig.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright 2002-2015 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.script;
+
+import java.nio.charset.Charset;
+import javax.script.ScriptEngine;
+
+/**
+ * Interface to be implemented by objects that configure and manage a
+ * JSR-223 {@link ScriptEngine} for automatic lookup in a web environment.
+ * Detected and used by {@link ScriptTemplateView}.
+ *
+ * @author Sebastien Deleuze
+ * @since 4.2
+ */
+public interface ScriptTemplateConfig {
+
+ /**
+ * Return the {@link ScriptEngine} to use by the views.
+ */
+ ScriptEngine getEngine();
+
+ /**
+ * Return the engine name that will be used to instantiate the {@link ScriptEngine}.
+ */
+ String getEngineName();
+
+ /**
+ * Return whether to use a shared engine for all threads or whether to create
+ * thread-local engine instances for each thread.
+ */
+ Boolean isSharedEngine();
+
+ /**
+ * Return the scripts to be loaded by the script engine (library or user provided).
+ */
+ String[] getScripts();
+
+ /**
+ * Return the object where the render function belongs (optional).
+ */
+ String getRenderObject();
+
+ /**
+ * Return the render function name (mandatory).
+ */
+ String getRenderFunction();
+
+ /**
+ * Return the content type to use for the response.
+ * @since 4.2.1
+ */
+ String getContentType();
+
+ /**
+ * Return the charset used to read script and template files.
+ */
+ Charset getCharset();
+
+ /**
+ * Return the resource loader path(s) via a Spring resource location.
+ */
+ String getResourceLoaderPath();
+
+}
diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/view/script/ScriptTemplateConfigurer.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/view/script/ScriptTemplateConfigurer.java
new file mode 100644
index 00000000..33c4d654
--- /dev/null
+++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/view/script/ScriptTemplateConfigurer.java
@@ -0,0 +1,225 @@
+/*
+ * Copyright 2002-2015 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.script;
+
+import java.nio.charset.Charset;
+import javax.script.ScriptEngine;
+
+/**
+ * An implementation of Spring MVC's {@link ScriptTemplateConfig} for creating
+ * a {@code ScriptEngine} for use in a web application.
+ *
+ * <pre class="code">
+ *
+ * // Add the following to an &#64;Configuration class
+ * &#64;Bean
+ * public ScriptTemplateConfigurer mustacheConfigurer() {
+ * ScriptTemplateConfigurer configurer = new ScriptTemplateConfigurer();
+ * configurer.setEngineName("nashorn");
+ * configurer.setScripts("mustache.js");
+ * configurer.setRenderObject("Mustache");
+ * configurer.setRenderFunction("render");
+ * return configurer;
+ * }
+ * </pre>
+ *
+ * <p><b>NOTE:</b> It is possible to use non thread-safe script engines with
+ * templating libraries not designed for concurrency, like Handlebars or React running on
+ * Nashorn, by setting the {@link #setSharedEngine sharedEngine} property to {@code false}.
+ *
+ * @author Sebastien Deleuze
+ * @since 4.2
+ * @see ScriptTemplateView
+ */
+public class ScriptTemplateConfigurer implements ScriptTemplateConfig {
+
+ private ScriptEngine engine;
+
+ private String engineName;
+
+ private Boolean sharedEngine;
+
+ private String[] scripts;
+
+ private String renderObject;
+
+ private String renderFunction;
+
+ private String contentType;
+
+ private Charset charset;
+
+ private String resourceLoaderPath;
+
+
+ /**
+ * Set the {@link ScriptEngine} to use by the view.
+ * The script engine must implement {@code Invocable}.
+ * You must define {@code engine} or {@code engineName}, not both.
+ * <p>When the {@code sharedEngine} flag is set to {@code false}, you should not specify
+ * the script engine with this setter, but with the {@link #setEngineName(String)}
+ * one (since it implies multiple lazy instantiations of the script engine).
+ * @see #setEngineName(String)
+ */
+ public void setEngine(ScriptEngine engine) {
+ this.engine = engine;
+ }
+
+ @Override
+ public ScriptEngine getEngine() {
+ return this.engine;
+ }
+
+ /**
+ * Set the engine name that will be used to instantiate the {@link ScriptEngine}.
+ * The script engine must implement {@code Invocable}.
+ * You must define {@code engine} or {@code engineName}, not both.
+ * @see #setEngine(ScriptEngine)
+ */
+ public void setEngineName(String engineName) {
+ this.engineName = engineName;
+ }
+
+ @Override
+ public String getEngineName() {
+ return this.engineName;
+ }
+
+ /**
+ * When set to {@code false}, use thread-local {@link ScriptEngine} instances instead
+ * of one single shared instance. This flag should be set to {@code false} for those
+ * using non thread-safe script engines with templating libraries not designed for
+ * concurrency, like Handlebars or React running on Nashorn for example.
+ * In this case, Java 8u60 or greater is required due to
+ * <a href="https://bugs.openjdk.java.net/browse/JDK-8076099">this bug</a>.
+ * <p>When this flag is set to {@code false}, the script engine must be specified using
+ * {@link #setEngineName(String)}. Using {@link #setEngine(ScriptEngine)} is not
+ * possible because multiple instances of the script engine need to be created lazily
+ * (one per thread).
+ * @see <a href="http://docs.oracle.com/javase/8/docs/api/javax/script/ScriptEngineFactory.html#getParameter-java.lang.String-">THREADING ScriptEngine parameter<a/>
+ */
+ public void setSharedEngine(Boolean sharedEngine) {
+ this.sharedEngine = sharedEngine;
+ }
+
+ @Override
+ public Boolean isSharedEngine() {
+ return this.sharedEngine;
+ }
+
+ /**
+ * Set the scripts to be loaded by the script engine (library or user provided).
+ * Since {@code resourceLoaderPath} default value is "classpath:", you can load easily
+ * any script available on the classpath.
+ * <p>For example, in order to use a JavaScript library available as a WebJars dependency
+ * and a custom "render.js" file, you should call
+ * {@code configurer.setScripts("/META-INF/resources/webjars/library/version/library.js",
+ * "com/myproject/script/render.js");}.
+ * @see #setResourceLoaderPath
+ * @see <a href="http://www.webjars.org">WebJars</a>
+ */
+ public void setScripts(String... scriptNames) {
+ this.scripts = scriptNames;
+ }
+
+ @Override
+ public String[] getScripts() {
+ return this.scripts;
+ }
+
+ /**
+ * Set the object where the render function belongs (optional).
+ * For example, in order to call {@code Mustache.render()}, {@code renderObject}
+ * should be set to {@code "Mustache"} and {@code renderFunction} to {@code "render"}.
+ */
+ public void setRenderObject(String renderObject) {
+ this.renderObject = renderObject;
+ }
+
+ @Override
+ public String getRenderObject() {
+ return this.renderObject;
+ }
+
+ /**
+ * Set the render function name (mandatory).
+ *
+ * <p>This function will be called with the following parameters:
+ * <ol>
+ * <li>{@code String template}: the template content</li>
+ * <li>{@code Map model}: the view model</li>
+ * <li>{@code String url}: the template url (since 4.2.2)</li>
+ * </ol>
+ */
+ public void setRenderFunction(String renderFunction) {
+ this.renderFunction = renderFunction;
+ }
+
+ @Override
+ public String getRenderFunction() {
+ return this.renderFunction;
+ }
+
+ /**
+ * Set the content type to use for the response.
+ * ({@code text/html} by default).
+ * @since 4.2.1
+ */
+ public void setContentType(String contentType) {
+ this.contentType = contentType;
+ }
+
+ /**
+ * Return the content type to use for the response.
+ * @since 4.2.1
+ */
+ @Override
+ public String getContentType() {
+ return this.contentType;
+ }
+
+ /**
+ * Set the charset used to read script and template files.
+ * ({@code UTF-8} by default).
+ */
+ public void setCharset(Charset charset) {
+ this.charset = charset;
+ }
+
+ @Override
+ public Charset getCharset() {
+ return this.charset;
+ }
+
+ /**
+ * Set the resource loader path(s) via a Spring resource location.
+ * Accepts multiple locations as a comma-separated list of paths.
+ * Standard URLs like "file:" and "classpath:" and pseudo URLs are supported
+ * as understood by Spring's {@link org.springframework.core.io.ResourceLoader}.
+ * Relative paths are allowed when running in an ApplicationContext.
+ * <p>Default is "classpath:".
+ */
+ public void setResourceLoaderPath(String resourceLoaderPath) {
+ this.resourceLoaderPath = resourceLoaderPath;
+ }
+
+ @Override
+ public String getResourceLoaderPath() {
+ return this.resourceLoaderPath;
+ }
+
+}
diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/view/script/ScriptTemplateView.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/view/script/ScriptTemplateView.java
new file mode 100644
index 00000000..908b273e
--- /dev/null
+++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/view/script/ScriptTemplateView.java
@@ -0,0 +1,408 @@
+/*
+ * Copyright 2002-2015 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.script;
+
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.net.URL;
+import java.net.URLClassLoader;
+import java.nio.charset.Charset;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import javax.script.Invocable;
+import javax.script.ScriptEngine;
+import javax.script.ScriptEngineManager;
+import javax.script.ScriptException;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.springframework.beans.BeansException;
+import org.springframework.beans.factory.BeanFactoryUtils;
+import org.springframework.beans.factory.NoSuchBeanDefinitionException;
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.ApplicationContextException;
+import org.springframework.core.NamedThreadLocal;
+import org.springframework.core.io.DefaultResourceLoader;
+import org.springframework.core.io.Resource;
+import org.springframework.core.io.ResourceLoader;
+import org.springframework.scripting.support.StandardScriptEvalException;
+import org.springframework.scripting.support.StandardScriptUtils;
+import org.springframework.util.Assert;
+import org.springframework.util.FileCopyUtils;
+import org.springframework.util.ObjectUtils;
+import org.springframework.util.StringUtils;
+import org.springframework.web.servlet.view.AbstractUrlBasedView;
+
+/**
+ * An {@link AbstractUrlBasedView} subclass designed to run any template library
+ * based on a JSR-223 script engine.
+ *
+ * <p>If not set, each property is auto-detected by looking up up a single
+ * {@link ScriptTemplateConfig} bean in the web application context and using
+ * it to obtain the configured properties.
+ *
+ * <p>Nashorn Javascript engine requires Java 8+, and may require setting the
+ * {@code sharedEngine} property to {@code false} in order to run properly. See
+ * {@link ScriptTemplateConfigurer#setSharedEngine(Boolean)} for more details.
+ *
+ * @author Sebastien Deleuze
+ * @author Juergen Hoeller
+ * @since 4.2
+ * @see ScriptTemplateConfigurer
+ * @see ScriptTemplateViewResolver
+ */
+public class ScriptTemplateView extends AbstractUrlBasedView {
+
+ public static final String DEFAULT_CONTENT_TYPE = "text/html";
+
+ private static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8");
+
+ private static final String DEFAULT_RESOURCE_LOADER_PATH = "classpath:";
+
+
+ private static final ThreadLocal<Map<Object, ScriptEngine>> enginesHolder =
+ new NamedThreadLocal<Map<Object, ScriptEngine>>("ScriptTemplateView engines");
+
+
+ private ScriptEngine engine;
+
+ private String engineName;
+
+ private Boolean sharedEngine;
+
+ private String[] scripts;
+
+ private String renderObject;
+
+ private String renderFunction;
+
+ private Charset charset;
+
+ private String resourceLoaderPath;
+
+ private ResourceLoader resourceLoader;
+
+ private volatile ScriptEngineManager scriptEngineManager;
+
+
+ /**
+ * Constructor for use as a bean.
+ * @see #setUrl
+ */
+ public ScriptTemplateView() {
+ setContentType(null);
+ }
+
+ /**
+ * Create a new ScriptTemplateView with the given URL.
+ * @since 4.2.1
+ */
+ public ScriptTemplateView(String url) {
+ super(url);
+ setContentType(null);
+ }
+
+
+ /**
+ * See {@link ScriptTemplateConfigurer#setEngine(ScriptEngine)} documentation.
+ */
+ public void setEngine(ScriptEngine engine) {
+ Assert.isInstanceOf(Invocable.class, engine);
+ this.engine = engine;
+ }
+
+ /**
+ * See {@link ScriptTemplateConfigurer#setEngineName(String)} documentation.
+ */
+ public void setEngineName(String engineName) {
+ this.engineName = engineName;
+ }
+
+ /**
+ * See {@link ScriptTemplateConfigurer#setSharedEngine(Boolean)} documentation.
+ */
+ public void setSharedEngine(Boolean sharedEngine) {
+ this.sharedEngine = sharedEngine;
+ }
+
+ /**
+ * See {@link ScriptTemplateConfigurer#setScripts(String...)} documentation.
+ */
+ public void setScripts(String... scripts) {
+ this.scripts = scripts;
+ }
+
+ /**
+ * See {@link ScriptTemplateConfigurer#setRenderObject(String)} documentation.
+ */
+ public void setRenderObject(String renderObject) {
+ this.renderObject = renderObject;
+ }
+
+ /**
+ * See {@link ScriptTemplateConfigurer#setRenderFunction(String)} documentation.
+ */
+ public void setRenderFunction(String functionName) {
+ this.renderFunction = functionName;
+ }
+
+ /**
+ * See {@link ScriptTemplateConfigurer#setContentType(String)}} documentation.
+ * @since 4.2.1
+ */
+ @Override
+ public void setContentType(String contentType) {
+ super.setContentType(contentType);
+ }
+
+ /**
+ * See {@link ScriptTemplateConfigurer#setCharset(Charset)} documentation.
+ */
+ public void setCharset(Charset charset) {
+ this.charset = charset;
+ }
+
+ /**
+ * See {@link ScriptTemplateConfigurer#setResourceLoaderPath(String)} documentation.
+ */
+ public void setResourceLoaderPath(String resourceLoaderPath) {
+ this.resourceLoaderPath = resourceLoaderPath;
+ }
+
+
+ @Override
+ protected void initApplicationContext(ApplicationContext context) {
+ super.initApplicationContext(context);
+
+ ScriptTemplateConfig viewConfig = autodetectViewConfig();
+ if (this.engine == null && viewConfig.getEngine() != null) {
+ setEngine(viewConfig.getEngine());
+ }
+ if (this.engineName == null && viewConfig.getEngineName() != null) {
+ this.engineName = viewConfig.getEngineName();
+ }
+ if (this.scripts == null && viewConfig.getScripts() != null) {
+ this.scripts = viewConfig.getScripts();
+ }
+ if (this.renderObject == null && viewConfig.getRenderObject() != null) {
+ this.renderObject = viewConfig.getRenderObject();
+ }
+ if (this.renderFunction == null && viewConfig.getRenderFunction() != null) {
+ this.renderFunction = viewConfig.getRenderFunction();
+ }
+ if (this.getContentType() == null) {
+ setContentType(viewConfig.getContentType() != null ? viewConfig.getContentType() : DEFAULT_CONTENT_TYPE);
+ }
+ if (this.charset == null) {
+ this.charset = (viewConfig.getCharset() != null ? viewConfig.getCharset() : DEFAULT_CHARSET);
+ }
+ if (this.resourceLoaderPath == null) {
+ this.resourceLoaderPath = (viewConfig.getResourceLoaderPath() != null ?
+ viewConfig.getResourceLoaderPath() : DEFAULT_RESOURCE_LOADER_PATH);
+ }
+ if (this.resourceLoader == null) {
+ this.resourceLoader = new DefaultResourceLoader(createClassLoader());
+ }
+ if (this.sharedEngine == null && viewConfig.isSharedEngine() != null) {
+ this.sharedEngine = viewConfig.isSharedEngine();
+ }
+
+ Assert.isTrue(!(this.engine != null && this.engineName != null),
+ "You should define either 'engine' or 'engineName', not both.");
+ Assert.isTrue(!(this.engine == null && this.engineName == null),
+ "No script engine found, please specify either 'engine' or 'engineName'.");
+
+ if (Boolean.FALSE.equals(this.sharedEngine)) {
+ Assert.isTrue(this.engineName != null,
+ "When 'sharedEngine' is set to false, you should specify the " +
+ "script engine using the 'engineName' property, not the 'engine' one.");
+ }
+ else if (this.engine != null) {
+ loadScripts(this.engine);
+ }
+ else {
+ setEngine(createEngineFromName());
+ }
+
+ Assert.isTrue(this.renderFunction != null, "The 'renderFunction' property must be defined.");
+ }
+
+
+ protected ScriptEngine getEngine() {
+ if (Boolean.FALSE.equals(this.sharedEngine)) {
+ Map<Object, ScriptEngine> engines = enginesHolder.get();
+ if (engines == null) {
+ engines = new HashMap<Object, ScriptEngine>(4);
+ enginesHolder.set(engines);
+ }
+ Object engineKey = (!ObjectUtils.isEmpty(this.scripts) ?
+ new EngineKey(this.engineName, this.scripts) : this.engineName);
+ ScriptEngine engine = engines.get(engineKey);
+ if (engine == null) {
+ engine = createEngineFromName();
+ engines.put(engineKey, engine);
+ }
+ return engine;
+ }
+ else {
+ // Simply return the configured ScriptEngine...
+ return this.engine;
+ }
+ }
+
+ protected ScriptEngine createEngineFromName() {
+ if (this.scriptEngineManager == null) {
+ this.scriptEngineManager = new ScriptEngineManager(getApplicationContext().getClassLoader());
+ }
+
+ ScriptEngine engine = StandardScriptUtils.retrieveEngineByName(this.scriptEngineManager, this.engineName);
+ loadScripts(engine);
+ return engine;
+ }
+
+ protected void loadScripts(ScriptEngine engine) {
+ if (!ObjectUtils.isEmpty(this.scripts)) {
+ try {
+ for (String script : this.scripts) {
+ Resource resource = this.resourceLoader.getResource(script);
+ if (!resource.exists()) {
+ throw new IllegalStateException("Script resource [" + script + "] not found");
+ }
+ engine.eval(new InputStreamReader(resource.getInputStream()));
+ }
+ }
+ catch (Exception ex) {
+ throw new IllegalStateException("Failed to load script", ex);
+ }
+ }
+ }
+
+ protected ClassLoader createClassLoader() {
+ String[] paths = StringUtils.commaDelimitedListToStringArray(this.resourceLoaderPath);
+ List<URL> urls = new ArrayList<URL>();
+ try {
+ for (String path : paths) {
+ Resource[] resources = getApplicationContext().getResources(path);
+ if (resources.length > 0) {
+ for (Resource resource : resources) {
+ if (resource.exists()) {
+ urls.add(resource.getURL());
+ }
+ }
+ }
+ }
+ }
+ catch (IOException ex) {
+ throw new IllegalStateException("Cannot create class loader: " + ex.getMessage());
+ }
+ ClassLoader classLoader = getApplicationContext().getClassLoader();
+ return (urls.size() > 0 ? new URLClassLoader(urls.toArray(new URL[urls.size()]), classLoader) : classLoader);
+ }
+
+ protected ScriptTemplateConfig autodetectViewConfig() throws BeansException {
+ try {
+ return BeanFactoryUtils.beanOfTypeIncludingAncestors(
+ getApplicationContext(), ScriptTemplateConfig.class, true, false);
+ }
+ catch (NoSuchBeanDefinitionException ex) {
+ throw new ApplicationContextException("Expected a single ScriptTemplateConfig bean in the current " +
+ "Servlet web application context or the parent root context: ScriptTemplateConfigurer is " +
+ "the usual implementation. This bean may have any name.", ex);
+ }
+ }
+
+
+ @Override
+ protected void prepareResponse(HttpServletRequest request, HttpServletResponse response) {
+ super.prepareResponse(request, response);
+
+ setResponseContentType(request, response);
+ response.setCharacterEncoding(this.charset.name());
+ }
+
+ @Override
+ protected void renderMergedOutputModel(Map<String, Object> model, HttpServletRequest request,
+ HttpServletResponse response) throws Exception {
+
+ try {
+ ScriptEngine engine = getEngine();
+ Invocable invocable = (Invocable) engine;
+ String url = getUrl();
+ String template = getTemplate(url);
+
+ Object html;
+ if (this.renderObject != null) {
+ Object thiz = engine.eval(this.renderObject);
+ html = invocable.invokeMethod(thiz, this.renderFunction, template, model, url);
+ }
+ else {
+ html = invocable.invokeFunction(this.renderFunction, template, model, url);
+ }
+
+ response.getWriter().write(String.valueOf(html));
+ }
+ catch (ScriptException ex) {
+ throw new ServletException("Failed to render script template", new StandardScriptEvalException(ex));
+ }
+ }
+
+ protected String getTemplate(String path) throws IOException {
+ Resource resource = this.resourceLoader.getResource(path);
+ InputStreamReader reader = new InputStreamReader(resource.getInputStream(), this.charset);
+ return FileCopyUtils.copyToString(reader);
+ }
+
+
+ /**
+ * Key class for the {@code enginesHolder ThreadLocal}.
+ * Only used if scripts have been specified; otherwise, the
+ * {@code engineName String} will be used as cache key directly.
+ */
+ private static class EngineKey {
+
+ private final String engineName;
+
+ private final String[] scripts;
+
+ public EngineKey(String engineName, String[] scripts) {
+ this.engineName = engineName;
+ this.scripts = scripts;
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (this == other) {
+ return true;
+ }
+ if (!(other instanceof EngineKey)) {
+ return false;
+ }
+ EngineKey otherKey = (EngineKey) other;
+ return (this.engineName.equals(otherKey.engineName) && Arrays.equals(this.scripts, otherKey.scripts));
+ }
+
+ @Override
+ public int hashCode() {
+ return (this.engineName.hashCode() * 29 + Arrays.hashCode(this.scripts));
+ }
+ }
+
+}
diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/view/script/ScriptTemplateViewResolver.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/view/script/ScriptTemplateViewResolver.java
new file mode 100644
index 00000000..54f61470
--- /dev/null
+++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/view/script/ScriptTemplateViewResolver.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2002-2015 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.script;
+
+import org.springframework.web.servlet.view.UrlBasedViewResolver;
+
+/**
+ * Convenience subclass of {@link UrlBasedViewResolver} that supports
+ * {@link ScriptTemplateView} and custom subclasses of it.
+ *
+ * <p>The view class for all views created by this resolver can be specified
+ * via the {@link #setViewClass(Class)} property.
+ *
+ * <p><b>Note:</b> When chaining ViewResolvers this resolver will check for the
+ * existence of the specified template resources and only return a non-null
+ * View object if a template is actually found.
+ *
+ * @author Sebastien Deleuze
+ * @since 4.2
+ * @see ScriptTemplateConfigurer
+ */
+public class ScriptTemplateViewResolver extends UrlBasedViewResolver {
+
+ public ScriptTemplateViewResolver() {
+ setViewClass(requiredViewClass());
+ }
+
+ @Override
+ protected Class<?> requiredViewClass() {
+ return ScriptTemplateView.class;
+ }
+
+}
diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/view/script/package-info.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/view/script/package-info.java
new file mode 100644
index 00000000..141d3537
--- /dev/null
+++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/view/script/package-info.java
@@ -0,0 +1,6 @@
+/**
+ * Support classes for views based on the JSR-223 script engine abstraction
+ * (as included in Java 6+), e.g. using JavaScript via Nashorn on JDK 8.
+ * Contains a View implementation for scripted templates.
+ */
+package org.springframework.web.servlet.view.script;
diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/view/velocity/VelocityViewResolver.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/view/velocity/VelocityViewResolver.java
index d1c903cc..e812ec92 100644
--- a/spring-webmvc/src/main/java/org/springframework/web/servlet/view/velocity/VelocityViewResolver.java
+++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/view/velocity/VelocityViewResolver.java
@@ -106,7 +106,7 @@ public class VelocityViewResolver extends AbstractTemplateViewResolver {
super.initApplicationContext();
if (this.toolboxConfigLocation != null) {
- if (VelocityView.class.equals(getViewClass())) {
+ if (VelocityView.class == getViewClass()) {
logger.info("Using VelocityToolboxView instead of default VelocityView " +
"due to specified toolboxConfigLocation");
setViewClass(VelocityToolboxView.class);
diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/view/velocity/spring.vm b/spring-webmvc/src/main/java/org/springframework/web/servlet/view/velocity/spring.vm
deleted file mode 100644
index 9064115e..00000000
--- a/spring-webmvc/src/main/java/org/springframework/web/servlet/view/velocity/spring.vm
+++ /dev/null
@@ -1,319 +0,0 @@
-#**
- * spring.vm
- *
- * This file consists of a collection of Velocity macros aimed at easing
- * some of the common requirements of web applications - in particular
- * handling of forms.
- *
- * Spring's Velocity support will automatically make this file and therefore
- * all macros within it available to any application using Spring's
- * VelocityConfigurer.
- *
- * To take advantage of these macros, the "exposeSpringMacroHelpers" property
- * of the VelocityView class needs to be set to "true". This will expose a
- * RequestContext under the name "springMacroRequestContext", as needed by
- * the macros in this library.
- *
- * @author Darren Davison
- * @author Juergen Hoeller
- * @since 1.1
- *#
-
-#**
- * springMessage
- *
- * Macro to translate a message code into a message.
- *#
-#macro( springMessage $code )$springMacroRequestContext.getMessage($code)#end
-
-#**
- * springMessageText
- *
- * Macro to translate a message code into a message,
- * using the given default text if no message found.
- *#
-#macro( springMessageText $code $text )$springMacroRequestContext.getMessage($code, $text)#end
-
-#**
- * springTheme
- *
- * Macro to translate a theme message code into a string.
- *#
-#macro( springTheme $code )$springMacroRequestContext.getThemeMessage($code)#end
-
-#**
- * springThemeText
- *
- * Macro to translate a theme message code into a string,
- * using the given default text if no message found.
- *#
-#macro( springThemeText $code $text )$springMacroRequestContext.getThemeMessage($code, $text)#end
-
-#**
- * springUrl
- *
- * Takes a relative URL and makes it absolute from the server root by
- * adding the context root for the web application.
- *#
-#macro( springUrl $relativeUrl )$springMacroRequestContext.getContextPath()${relativeUrl}#end
-
-#**
- * springBind
- *
- * Exposes a BindStatus object for the given bind path, which can be
- * a bean (e.g. "person") to get global errors, or a bean property
- * (e.g. "person.name") to get field errors. Can be called multiple times
- * within a form to bind to multiple command objects and/or field names.
- *
- * This macro will participate in the default HTML escape setting for the given
- * RequestContext. This can be customized by calling "setDefaultHtmlEscape"
- * on the "springMacroRequestContext" context variable, or via the
- * "defaultHtmlEscape" context-param in web.xml (same as for the JSP bind tag).
- * Also regards a "springHtmlEscape" variable in the template context.
- *
- * Producing no output, the following context variable will be available
- * each time this macro is referenced:
- *
- * $status : a BindStatus instance holding the command object name,
- * expression, value, and error codes and messages for the path supplied
- *
- * @param $path : the path (string value) of the value required to bind to.
- * Spring defaults to a command name of "command" but this can be overridden
- * by user config.
- *#
-#macro( springBind $path )
- #if("$!springHtmlEscape"!="")
- #set( $status = $springMacroRequestContext.getBindStatus($path, $springHtmlEscape) )
- #else
- #set( $status = $springMacroRequestContext.getBindStatus($path) )
- #end
-#end
-
-#**
- * springBindEscaped
- *
- * Similar to springBind, but takes an explicit HTML escape flag rather
- * than relying on the default HTML escape setting.
- *#
-#macro( springBindEscaped $path $htmlEscape )
- #set( $status = $springMacroRequestContext.getBindStatus($path, $htmlEscape) )
-#end
-
-#**
- * springFormInput
- *
- * Display a form input field of type 'text' and bind it to an attribute
- * of a command or bean.
- *
- * @param path the name of the field to bind to
- * @param attributes any additional attributes for the element (such as class
- * or CSS styles or size)
- *
- *#
-#macro( springFormInput $path $attributes )
- #springBind($path)
- <input type="text" id="#springXmlId(${status.expression})" name="${status.expression}" value="$!status.value" ${attributes}#springCloseTag()
-#end
-
-#**
- * springFormPasswordInput
- *
- * Display a form input field of type 'password' and bind it to an attribute
- * of a command or bean. No value will ever be specified for this field regardless
- * of whether one exists or not. For hopefully obvious reasons!
- *
- * @param path the name of the field to bind to
- * @param attributes any additional attributes for the element (such as class
- * or CSS styles or size)
- *
- *#
-#macro( springFormPasswordInput $path $attributes )
- #springBind($path)
- <input type="password" id="#springXmlId(${status.expression})" name="${status.expression}" value="" ${attributes}#springCloseTag()
-#end
-
-#**
- * springFormHiddenInput
- *
- * Generate a form input field of type 'hidden' and bind it to an attribute
- * of a command or bean.
- *
- * @param path the name of the field to bind to
- * @param attributes any additional attributes for the element (such as class
- * or CSS styles or size)
- *
- *#
-#macro( springFormHiddenInput $path $attributes )
- #springBind($path)
- <input type="hidden" id="#springXmlId(${status.expression})" name="${status.expression}" value="$!status.value" ${attributes}#springCloseTag()
-#end
-
-#**
- * formTextArea
- *
- * display a text area and bind it to an attribute
- * of a command or bean
- *
- * @param path the name of the field to bind to
- * @param attributes any additional attributes for the element (such as class
- * or CSS styles or size)
- *
- *#
-#macro( springFormTextarea $path $attributes )
- #springBind($path)
- <textarea id="#springXmlId(${status.expression})" name="${status.expression}" ${attributes}>$!status.value</textarea>
-#end
-
-#**
- * springFormSingleSelect
- *
- * Show a selectbox (dropdown) input element allowing a single value to be chosen
- * from a list of options.
- *
- * The null check for $status.value leverages Velocity's 'quiet' notation rather
- * than the more common #if($status.value) since this method evaluates to the
- * boolean 'false' if the content of $status.value is the String "false" - not
- * what we want.
- *
- * @param path the name of the field to bind to
- * @param options a map (value=label) of all the available options
- * @param attributes any additional attributes for the element (such as class
- * or CSS styles or size)
-*#
-#macro( springFormSingleSelect $path $options $attributes )
- #springBind($path)
- <select id="#springXmlId(${status.expression})" name="${status.expression}" ${attributes}>
- #foreach($option in $options.keySet())
- <option value="${option}"
- #if("$!status.value"=="$option") selected="selected" #end>
- ${options.get($option)}</option>
- #end
- </select>
-#end
-
-#**
- * springFormMultiSelect
- *
- * Show a listbox of options allowing the user to make 0 or more choices from
- * the list of options.
- *
- * @param path the name of the field to bind to
- * @param options a map (value=label) of all the available options
- * @param attributes any additional attributes for the element (such as class
- * or CSS styles or size)
-*#
-#macro( springFormMultiSelect $path $options $attributes )
- #springBind($path)
- <select multiple="multiple" id="#springXmlId(${status.expression})" name="${status.expression}" ${attributes}>
- #foreach($option in $options.keySet())
- <option value="${option}"
- #foreach($item in $status.actualValue)
- #if($item==$option) selected="selected" #end
- #end
- >${options.get($option)}</option>
- #end
- </select>
-#end
-
-#**
- * springFormRadioButtons
- *
- * Show radio buttons.
- *
- * @param path the name of the field to bind to
- * @param options a map (value=label) of all the available options
- * @param separator the html tag or other character list that should be used to
- * separate each option. Typically '&nbsp;' or '<br>'
- * @param attributes any additional attributes for the element (such as class
- * or CSS styles or size)
-*#
-#macro( springFormRadioButtons $path $options $separator $attributes )
- #springBind($path)
- #foreach($option in $options.keySet())
- <input type="radio" name="${status.expression}" value="${option}"
- #if("$!status.value"=="$option") checked="checked" #end
- ${attributes}
- #springCloseTag()
- ${options.get($option)} ${separator}
- #end
-#end
-
-#**
- * springFormCheckboxes
- *
- * Show checkboxes.
- *
- * @param path the name of the field to bind to
- * @param options a map (value=label) of all the available options
- * @param separator the html tag or other character list that should be used to
- * separate each option. Typically '&nbsp;' or '<br>'.
- * @param attributes any additional attributes for the element (such as class
- * or CSS styles or size)
-*#
-#macro( springFormCheckboxes $path $options $separator $attributes )
- #springBind($path)
- #foreach($option in $options.keySet())
- <input type="checkbox" name="${status.expression}" value="${option}"
- #foreach($item in $status.actualValue)
- #if($item==$option) checked="checked" #end
- #end
- ${attributes} #springCloseTag()
- ${options.get($option)} ${separator}
- #end
- <input type="hidden" name="_${status.expression}" value="on"/>
-#end
-
-#**
- * springFormCheckbox
- *
- * Show a single checkbox.
- *
- * @param path the name of the field to bind to
- * @param attributes any additional attributes for the element (such as class
- * or CSS styles or size)
-*#
-#macro( springFormCheckbox $path $attributes )
- #springBind($path)
- <input type="hidden" name="_#springXmlId(${status.expression})" value="on"/>
- <input type="checkbox" id="#springXmlId(${status.expression})" name="${status.expression}"#if("$!{status.value}"=="true") checked="checked"#end ${attributes}/>
-#end
-
-#**
- * springShowErrors
- *
- * Show validation errors for the currently bound field, with
- * optional style attributes.
- *
- * @param separator the html tag or other character list that should be used to
- * separate each option. Typically '<br>'.
- * @param classOrStyle either the name of a CSS class element (which is defined in
- * the template or an external CSS file) or an inline style. If the value passed in here
- * contains a colon (:) then a 'style=' attribute will be used, else a 'class=' attribute
- * will be used.
-*#
-#macro( springShowErrors $separator $classOrStyle )
- #foreach($error in $status.errorMessages)
- #if($classOrStyle=="")
- <b>${error}</b>
- #else
- #if($classOrStyle.indexOf(":")==-1)
- #set($attr="class")
- #else
- #set($attr="style")
- #end
- <span ${attr}="${classOrStyle}">${error}</span>
- #end
- ${separator}
- #end
-#end
-
-#**
- * springCloseTag
- *
- * Simple macro to close an HTML tag that has no body with '>' or '/>',
- * depending on the value of a 'springXhtmlCompliant' variable in the
- * template context.
- *#
-#macro( springCloseTag )#if($springXhtmlCompliant)/>#else>#end#end
-
-#macro( springXmlId $id)#if($id)$id.replaceAll("\[","").replaceAll("\]","")#else$id#end#end
diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/view/xml/MappingJackson2XmlView.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/view/xml/MappingJackson2XmlView.java
index 66b9d4c4..dfcf59e5 100644
--- a/spring-webmvc/src/main/java/org/springframework/web/servlet/view/xml/MappingJackson2XmlView.java
+++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/view/xml/MappingJackson2XmlView.java
@@ -28,7 +28,7 @@ import org.springframework.web.servlet.view.json.AbstractJackson2View;
/**
* Spring MVC {@link View} that renders XML content by serializing the model for the current request
- * using <a href="http://jackson.codehaus.org/">Jackson 2's</a> {@link XmlMapper}.
+ * using <a href="http://wiki.fasterxml.com/JacksonHome">Jackson 2's</a> {@link XmlMapper}.
*
* <p>The Object to be serialized is supplied as a parameter in the model. The first serializable
* entry is used. Users can either specify a specific entry in the model via the
@@ -58,6 +58,14 @@ public class MappingJackson2XmlView extends AbstractJackson2View {
super(Jackson2ObjectMapperBuilder.xml().build(), DEFAULT_CONTENT_TYPE);
}
+ /**
+ * Construct a new {@code MappingJackson2XmlView} using the provided {@link XmlMapper}
+ * and setting the content type to {@code application/xml}.
+ * @since 4.2.1
+ */
+ public MappingJackson2XmlView(XmlMapper xmlMapper) {
+ super(xmlMapper, DEFAULT_CONTENT_TYPE);
+ }
/**
* {@inheritDoc}