diff options
author | Emmanuel Bourg <ebourg@apache.org> | 2016-08-03 19:55:01 +0200 |
---|---|---|
committer | Emmanuel Bourg <ebourg@apache.org> | 2016-08-03 19:55:01 +0200 |
commit | 75a721d1019da2a2fa86e24ff439df4a224e5b19 (patch) | |
tree | 2c44c00ce2c8641cccad177177e5682e187a17ea /spring-webmvc/src/main/java | |
parent | 9eaca6a06af3cbceb3754de19d477be770614265 (diff) |
Imported Upstream version 4.3.2
Diffstat (limited to 'spring-webmvc/src/main/java')
124 files changed, 2695 insertions, 1021 deletions
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 38d16f9e..e18f57e2 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 @@ -348,6 +348,7 @@ public class DispatcherServlet extends FrameworkServlet { */ public DispatcherServlet() { super(); + setDispatchOptionsRequest(true); } /** @@ -391,6 +392,7 @@ public class DispatcherServlet extends FrameworkServlet { */ public DispatcherServlet(WebApplicationContext webApplicationContext) { super(webApplicationContext); + setDispatchOptionsRequest(true); } @@ -970,13 +972,19 @@ public class DispatcherServlet extends FrameworkServlet { catch (Exception ex) { dispatchException = ex; } + catch (Throwable err) { + // As of 4.3, we're processing Errors thrown from handler methods as well, + // making them available for @ExceptionHandler methods and other scenarios. + dispatchException = new NestedServletException("Handler dispatch failed", err); + } processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException); } catch (Exception ex) { triggerAfterCompletion(processedRequest, response, mappedHandler, ex); } catch (Throwable err) { - triggerAfterCompletionWithError(processedRequest, response, mappedHandler, err); + triggerAfterCompletion(processedRequest, response, mappedHandler, + new NestedServletException("Handler processing failed", err)); } finally { if (asyncManager.isConcurrentHandlingStarted()) { @@ -1243,6 +1251,9 @@ public class DispatcherServlet extends FrameworkServlet { logger.debug("Rendering view [" + view + "] in DispatcherServlet with name '" + getServletName() + "'"); } try { + if (mv.getStatus() != null) { + response.setStatus(mv.getStatus().value()); + } view.render(mv.getModelInternal(), request, response); } catch (Exception ex) { @@ -1299,16 +1310,6 @@ public class DispatcherServlet extends FrameworkServlet { throw ex; } - private void triggerAfterCompletionWithError(HttpServletRequest request, HttpServletResponse response, - HandlerExecutionChain mappedHandler, Throwable error) throws Exception { - - ServletException ex = new NestedServletException("Handler processing failed", error); - if (mappedHandler != null) { - mappedHandler.triggerAfterCompletion(request, response, ex); - } - throw ex; - } - /** * Restore the request attributes after an include. * @param request current HTTP request 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 0cf705a9..5b56e210 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 @@ -426,9 +426,11 @@ public abstract class FrameworkServlet extends HttpServletBean implements Applic /** * Set whether this servlet should dispatch an HTTP OPTIONS request to * the {@link #doService} method. - * <p>Default is "false", applying {@link javax.servlet.http.HttpServlet}'s - * default behavior (i.e. enumerating all standard HTTP request methods - * as a response to the OPTIONS request). + * <p>Default in the {@code FrameworkServlet} is "false", applying + * {@link javax.servlet.http.HttpServlet}'s default behavior (i.e.enumerating + * all standard HTTP request methods as a response to the OPTIONS request). + * Note however that as of 4.3 the {@code DispatcherServlet} sets this + * property to "true" by default due to its built-in support for OPTIONS. * <p>Turn this flag on if you prefer OPTIONS requests to go through the * regular dispatching chain, just like other HTTP requests. This usually * means that your controllers will receive those requests; make sure @@ -836,7 +838,8 @@ public abstract class FrameworkServlet extends HttpServletBean implements Applic protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { - if (HttpMethod.PATCH.matches(request.getMethod())) { + HttpMethod httpMethod = HttpMethod.resolve(request.getMethod()); + if (HttpMethod.PATCH == httpMethod || httpMethod == null) { processRequest(request, response); } else { 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 6f8e8036..e49eeafb 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 @@ -93,7 +93,7 @@ public interface HandlerInterceptor { * @throws Exception in case of errors */ boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) - throws Exception; + throws Exception; /** * Intercept the execution of a handler. Called after HandlerAdapter actually @@ -114,7 +114,8 @@ public interface HandlerInterceptor { * (can also be {@code null}) * @throws Exception in case of errors */ - void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) + void postHandle( + HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception; /** @@ -136,7 +137,8 @@ public interface HandlerInterceptor { * @param ex exception thrown on handler execution, if any * @throws Exception in case of errors */ - void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) + void afterCompletion( + HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception; } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/LocaleResolver.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/LocaleResolver.java index 80998d0f..2d0f9540 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/LocaleResolver.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/LocaleResolver.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. @@ -51,8 +51,8 @@ import javax.servlet.http.HttpServletResponse; public interface LocaleResolver { /** - * Resolve the current locale via the given request. Can return a default locale as - * fallback in any case. + * Resolve the current locale via the given request. + * Can return a default locale as fallback in any case. * @param request the request to resolve the locale for * @return the current locale (never {@code null}) */ @@ -63,8 +63,8 @@ public interface LocaleResolver { * @param request the request to be used for locale modification * @param response the response to be used for locale modification * @param locale the new locale, or {@code null} to clear the locale - * @throws UnsupportedOperationException if the LocaleResolver implementation does not - * support dynamic changing of the locale + * @throws UnsupportedOperationException if the LocaleResolver + * implementation does not support dynamic changing of the locale */ void setLocale(HttpServletRequest request, HttpServletResponse response, Locale locale); diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/ModelAndView.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/ModelAndView.java index d58ea7de..66fd7429 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/ModelAndView.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/ModelAndView.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. @@ -18,6 +18,7 @@ package org.springframework.web.servlet; import java.util.Map; +import org.springframework.http.HttpStatus; import org.springframework.ui.ModelMap; import org.springframework.util.CollectionUtils; @@ -36,6 +37,7 @@ import org.springframework.util.CollectionUtils; * @author Rod Johnson * @author Juergen Hoeller * @author Rob Harrop + * @author Rossen Stoyanchev * @see DispatcherServlet * @see ViewResolver * @see HandlerAdapter#handle @@ -49,6 +51,9 @@ public class ModelAndView { /** Model Map */ private ModelMap model; + /** Optional HTTP status for the response */ + private HttpStatus status; + /** Indicates whether or not this instance has been cleared with a call to {@link #clear()} */ private boolean cleared = false; @@ -116,6 +121,24 @@ public class ModelAndView { } /** + * Creates new ModelAndView given a view name, model, and status. + * @param viewName name of the View to render, to be resolved + * by the DispatcherServlet's ViewResolver + * @param model Map of model names (Strings) to model objects + * (Objects). Model entries may not be {@code null}, but the + * model Map may be {@code null} if there is no model data. + * @param status an alternative status code to use for the response. + * @since 4.3 + */ + public ModelAndView(String viewName, Map<String, ?> model, HttpStatus status) { + this.view = viewName; + if (model != null) { + getModelMap().addAllAttributes(model); + } + this.status = status; + } + + /** * Convenient constructor to take a single model object. * @param viewName name of the View to render, to be resolved * by the DispatcherServlet's ViewResolver @@ -215,6 +238,22 @@ public class ModelAndView { return getModelMap(); } + /** + * Set the HTTP status to use for the response. + * @since 4.3 + */ + public void setStatus(HttpStatus status) { + this.status = status; + } + + /** + * Return the configured HTTP status for the response, if any. + * @since 4.3 + */ + public HttpStatus getStatus() { + return this.status; + } + /** * Add an attribute to the model. 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 7c8cbcb4..dd80a15a 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/AnnotationDrivenBeanDefinitionParser.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/AnnotationDrivenBeanDefinitionParser.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-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. @@ -286,6 +286,13 @@ class AnnotationDrivenBeanDefinitionParser implements BeanDefinitionParser { exceptionHandlerExceptionResolver.getPropertyValues().add("order", 0); addResponseBodyAdvice(exceptionHandlerExceptionResolver); + if (argumentResolvers != null) { + exceptionHandlerExceptionResolver.getPropertyValues().add("customArgumentResolvers", argumentResolvers); + } + if (returnValueHandlers != null) { + exceptionHandlerExceptionResolver.getPropertyValues().add("customReturnValueHandlers", returnValueHandlers); + } + String methodExceptionResolverName = readerContext.registerWithGeneratedName(exceptionHandlerExceptionResolver); RootBeanDefinition responseStatusExceptionResolver = new RootBeanDefinition(ResponseStatusExceptionResolver.class); 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 e6684136..fb787d1a 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 @@ -178,4 +178,23 @@ abstract class MvcNamespaceUtils { return new RuntimeBeanReference(CORS_CONFIGURATION_BEAN_NAME); } + /** + * Find the {@code ContentNegotiationManager} bean created by or registered + * with the {@code annotation-driven} element. + * @return a bean definition, bean reference, or null. + */ + public static Object getContentNegotiationManager(ParserContext context) { + String name = AnnotationDrivenBeanDefinitionParser.HANDLER_MAPPING_BEAN_NAME; + if (context.getRegistry().containsBeanDefinition(name)) { + BeanDefinition handlerMappingBeanDef = context.getRegistry().getBeanDefinition(name); + return handlerMappingBeanDef.getPropertyValues().get("contentNegotiationManager"); + } + name = AnnotationDrivenBeanDefinitionParser.CONTENT_NEGOTIATION_MANAGER_BEAN_NAME; + if (context.getRegistry().containsBeanDefinition(name)) { + return new RuntimeBeanReference(name); + } + return null; + } + + } 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 f55e287a..e8059186 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-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. @@ -22,6 +22,7 @@ import java.util.concurrent.TimeUnit; import org.w3c.dom.Element; +import org.springframework.beans.MutablePropertyValues; import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.config.ConstructorArgumentValues; import org.springframework.beans.factory.config.RuntimeBeanReference; @@ -165,17 +166,19 @@ class ResourcesBeanDefinitionParser implements BeanDefinitionParser { RootBeanDefinition resourceHandlerDef = new RootBeanDefinition(ResourceHttpRequestHandler.class); resourceHandlerDef.setSource(source); resourceHandlerDef.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); - resourceHandlerDef.getPropertyValues().add("locations", locations); + + MutablePropertyValues values = resourceHandlerDef.getPropertyValues(); + values.add("locations", locations); String cacheSeconds = element.getAttribute("cache-period"); if (StringUtils.hasText(cacheSeconds)) { - resourceHandlerDef.getPropertyValues().add("cacheSeconds", cacheSeconds); + values.add("cacheSeconds", cacheSeconds); } Element cacheControlElement = DomUtils.getChildElementByTagName(element, "cache-control"); if (cacheControlElement != null) { CacheControl cacheControl = parseCacheControl(cacheControlElement); - resourceHandlerDef.getPropertyValues().add("cacheControl", cacheControl); + values.add("cacheControl", cacheControl); } Element resourceChainElement = DomUtils.getChildElementByTagName(element, "resource-chain"); @@ -183,6 +186,11 @@ class ResourcesBeanDefinitionParser implements BeanDefinitionParser { parseResourceChain(resourceHandlerDef, parserContext, resourceChainElement, source); } + Object manager = MvcNamespaceUtils.getContentNegotiationManager(parserContext); + if (manager != null) { + values.add("contentNegotiationManager", manager); + } + String beanName = parserContext.getReaderContext().generateBeanName(resourceHandlerDef); parserContext.getRegistry().registerBeanDefinition(beanName, resourceHandlerDef); parserContext.registerComponent(new BeanComponentDefinition(resourceHandlerDef, beanName)); @@ -242,6 +250,14 @@ class ResourcesBeanDefinitionParser implements BeanDefinitionParser { if (element.hasAttribute("s-maxage")) { cacheControl = cacheControl.sMaxAge(Long.parseLong(element.getAttribute("s-maxage")), TimeUnit.SECONDS); } + if (element.hasAttribute("stale-while-revalidate")) { + cacheControl = cacheControl.staleWhileRevalidate( + Long.parseLong(element.getAttribute("stale-while-revalidate")), TimeUnit.SECONDS); + } + if (element.hasAttribute("stale-if-error")) { + cacheControl = cacheControl.staleIfError( + Long.parseLong(element.getAttribute("stale-if-error")), TimeUnit.SECONDS); + } return cacheControl; } 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 06e645f5..a91c937d 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-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. @@ -22,7 +22,6 @@ import org.w3c.dom.Element; import org.springframework.beans.MutablePropertyValues; 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.parsing.CompositeComponentDefinition; import org.springframework.beans.factory.support.ManagedList; @@ -39,7 +38,6 @@ 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; /** * Parse the {@code view-resolvers} MVC namespace element and register @@ -69,6 +67,7 @@ public class ViewResolversBeanDefinitionParser implements BeanDefinitionParser { public static final String VIEW_RESOLVER_BEAN_NAME = "mvcViewResolver"; + @SuppressWarnings("deprecation") public BeanDefinition parse(Element element, ParserContext context) { Object source = context.extractSource(element); context.pushContainingComponent(new CompositeComponentDefinition(element.getTagName(), source)); @@ -100,7 +99,7 @@ public class ViewResolversBeanDefinitionParser implements BeanDefinitionParser { addUrlBasedViewResolverProperties(resolverElement, resolverBeanDef); } else if ("velocity".equals(name)) { - resolverBeanDef = new RootBeanDefinition(VelocityViewResolver.class); + resolverBeanDef = new RootBeanDefinition(org.springframework.web.servlet.view.velocity.VelocityViewResolver.class); resolverBeanDef.getPropertyValues().add("suffix", ".vm"); addUrlBasedViewResolverProperties(resolverElement, resolverBeanDef); } @@ -192,24 +191,11 @@ public class ViewResolversBeanDefinitionParser implements BeanDefinitionParser { if (resolverElement.hasAttribute("use-not-acceptable")) { values.add("useNotAcceptableStatusCode", resolverElement.getAttribute("use-not-acceptable")); } - Object manager = getContentNegotiationManager(context); + Object manager = MvcNamespaceUtils.getContentNegotiationManager(context); if (manager != null) { values.add("contentNegotiationManager", manager); } return beanDef; } - private Object getContentNegotiationManager(ParserContext context) { - String name = AnnotationDrivenBeanDefinitionParser.HANDLER_MAPPING_BEAN_NAME; - if (context.getRegistry().containsBeanDefinition(name)) { - BeanDefinition handlerMappingBeanDef = context.getRegistry().getBeanDefinition(name); - return handlerMappingBeanDef.getPropertyValues().get("contentNegotiationManager"); - } - name = AnnotationDrivenBeanDefinitionParser.CONTENT_NEGOTIATION_MANAGER_BEAN_NAME; - if (context.getRegistry().containsBeanDefinition(name)) { - return new RuntimeBeanReference(name); - } - return null; - } - } 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 92a75bed..93a21b89 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-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. @@ -56,6 +56,8 @@ public class ResourceChainRegistration { private boolean hasCssLinkTransformer; + private boolean hasWebjarsResolver; + public ResourceChainRegistration(boolean cacheResources) { this(cacheResources, cacheResources ? new ConcurrentMapCache(DEFAULT_CACHE_NAME) : null); @@ -84,6 +86,9 @@ public class ResourceChainRegistration { else if (resolver instanceof PathResourceResolver) { this.hasPathResolver = true; } + else if (resolver instanceof WebJarsResourceResolver) { + this.hasWebjarsResolver = true; + } return this; } @@ -104,7 +109,7 @@ public class ResourceChainRegistration { protected List<ResourceResolver> getResourceResolvers() { if (!this.hasPathResolver) { List<ResourceResolver> result = new ArrayList<ResourceResolver>(this.resolvers); - if (isWebJarsAssetLocatorPresent) { + if (isWebJarsAssetLocatorPresent && !this.hasWebjarsResolver) { result.add(new WebJarsResourceResolver()); } result.add(new PathResourceResolver()); @@ -114,7 +119,7 @@ public class ResourceChainRegistration { } protected List<ResourceTransformer> getResourceTransformers() { - if (this.hasVersionResolver && !this.hasCssLinkTransformer) { + if (this.hasVersionResolver && !this.hasCssLinkTransformer) { List<ResourceTransformer> result = new ArrayList<ResourceTransformer>(this.transformers); boolean hasTransformers = !this.transformers.isEmpty(); boolean hasCaching = hasTransformers && this.transformers.get(0) instanceof CachingResourceTransformer; diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/ResourceHandlerRegistry.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/ResourceHandlerRegistry.java index f9145d4a..a4c7c02a 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/ResourceHandlerRegistry.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/ResourceHandlerRegistry.java @@ -27,6 +27,7 @@ import org.springframework.beans.factory.BeanInitializationException; import org.springframework.context.ApplicationContext; import org.springframework.util.Assert; import org.springframework.web.HttpRequestHandler; +import org.springframework.web.accept.ContentNegotiationManager; import org.springframework.web.servlet.HandlerMapping; import org.springframework.web.servlet.handler.AbstractHandlerMapping; import org.springframework.web.servlet.handler.SimpleUrlHandlerMapping; @@ -55,15 +56,23 @@ public class ResourceHandlerRegistry { private final ApplicationContext appContext; + private final ContentNegotiationManager contentNegotiationManager; + private final List<ResourceHandlerRegistration> registrations = new ArrayList<ResourceHandlerRegistration>(); private int order = Integer.MAX_VALUE -1; public ResourceHandlerRegistry(ApplicationContext applicationContext, ServletContext servletContext) { + this(applicationContext, servletContext, null); + } + + public ResourceHandlerRegistry(ApplicationContext applicationContext, ServletContext servletContext, + ContentNegotiationManager contentNegotiationManager) { Assert.notNull(applicationContext, "ApplicationContext is required"); this.appContext = applicationContext; this.servletContext = servletContext; + this.contentNegotiationManager = contentNegotiationManager; } @@ -113,6 +122,7 @@ public class ResourceHandlerRegistry { ResourceHttpRequestHandler handler = registration.getRequestHandler(); handler.setServletContext(this.servletContext); handler.setApplicationContext(this.appContext); + handler.setContentNegotiationManager(this.contentNegotiationManager); try { handler.afterPropertiesSet(); } 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 c4fedda8..bc21e6e0 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 @@ -41,8 +41,6 @@ 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; -import org.springframework.web.servlet.view.velocity.VelocityViewResolver; /** * Assist with the configuration of a chain of @@ -86,10 +84,8 @@ public class ViewResolverRegistry { * Enable use of a {@link ContentNegotiatingViewResolver} to front all other * configured view resolvers and select among all selected Views based on * media types requested by the client (e.g. in the Accept header). - * * <p>If invoked multiple times the provided default views will be added to * any other default views that may have been configured already. - * * @see ContentNegotiatingViewResolver#setDefaultViews */ public void enableContentNegotiation(View... defaultViews) { @@ -100,7 +96,6 @@ public class ViewResolverRegistry { * Enable use of a {@link ContentNegotiatingViewResolver} to front all other * configured view resolvers and select among all selected Views based on * media types requested by the client (e.g. in the Accept header). - * * <p>If invoked multiple times the provided default views will be added to * any other default views that may have been configured already. * @@ -112,9 +107,8 @@ public class ViewResolverRegistry { } private void initContentNegotiatingViewResolver(View[] defaultViews) { - // ContentNegotiatingResolver in the registry: elevate its precedence! - this.order = (this.order == null ? Ordered.HIGHEST_PRECEDENCE : this.order); + this.order = (this.order != null ? this.order : Ordered.HIGHEST_PRECEDENCE); if (this.contentNegotiatingResolver != null) { if (!ObjectUtils.isEmpty(defaultViews)) { @@ -136,7 +130,6 @@ public class ViewResolverRegistry { /** * Register JSP view resolver using a default view name prefix of "/WEB-INF/" * and a default suffix of ".jsp". - * * <p>When this method is invoked more than once, each call will register a * new ViewResolver instance. Note that since it's not easy to determine * if a JSP exists without forwarding to it, using multiple JSP-based view @@ -149,7 +142,6 @@ public class ViewResolverRegistry { /** * Register JSP view resolver with the specified prefix and suffix. - * * <p>When this method is invoked more than once, each call will register a * new ViewResolver instance. Note that since it's not easy to determine * if a JSP exists without forwarding to it, using multiple JSP-based view @@ -166,7 +158,6 @@ public class ViewResolverRegistry { /** * Register Tiles 3.x view resolver. - * * <p><strong>Note</strong> that you must also configure Tiles by adding a * {@link org.springframework.web.servlet.view.tiles3.TilesConfigurer} bean. */ @@ -184,7 +175,6 @@ public class ViewResolverRegistry { /** * Register a FreeMarker view resolver with an empty default view name * prefix and a default suffix of ".ftl". - * * <p><strong>Note</strong> that you must also configure FreeMarker by adding a * {@link org.springframework.web.servlet.view.freemarker.FreeMarkerConfigurer} bean. */ @@ -203,12 +193,13 @@ public class ViewResolverRegistry { /** * Register Velocity view resolver with an empty default view name * prefix and a default suffix of ".vm". - * * <p><strong>Note</strong> that you must also configure Velocity by adding a * {@link org.springframework.web.servlet.view.velocity.VelocityConfigurer} bean. + * @deprecated as of Spring 4.3, in favor of FreeMarker */ + @Deprecated public UrlBasedViewResolverRegistration velocity() { - if (this.applicationContext != null && !hasBeanOfType(VelocityConfigurer.class)) { + if (this.applicationContext != null && !hasBeanOfType(org.springframework.web.servlet.view.velocity.VelocityConfigurer.class)) { throw new BeanInitializationException("In addition to a Velocity view resolver " + "there must also be a single VelocityConfig bean in this web application context " + "(or its parent): VelocityConfigurer is the usual implementation. " + @@ -313,22 +304,23 @@ public class ViewResolverRegistry { private static class TilesRegistration extends UrlBasedViewResolverRegistration { - private TilesRegistration() { + public TilesRegistration() { super(new TilesViewResolver()); } } private static class VelocityRegistration extends UrlBasedViewResolverRegistration { - private VelocityRegistration() { - super(new VelocityViewResolver()); + @SuppressWarnings("deprecation") + public VelocityRegistration() { + super(new org.springframework.web.servlet.view.velocity.VelocityViewResolver()); getViewResolver().setSuffix(".vm"); } } private static class FreeMarkerRegistration extends UrlBasedViewResolverRegistration { - private FreeMarkerRegistration() { + public FreeMarkerRegistration() { super(new FreeMarkerViewResolver()); getViewResolver().setSuffix(".ftl"); } @@ -336,7 +328,7 @@ public class ViewResolverRegistry { private static class GroovyMarkupRegistration extends UrlBasedViewResolverRegistration { - private GroovyMarkupRegistration() { + public GroovyMarkupRegistration() { super(new GroovyMarkupViewResolver()); getViewResolver().setSuffix(".tpl"); } @@ -344,7 +336,7 @@ public class ViewResolverRegistry { private static class ScriptRegistration extends UrlBasedViewResolverRegistration { - private ScriptRegistration() { + public 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 37976905..3a114972 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-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. @@ -199,6 +199,10 @@ public class WebMvcConfigurationSupport implements ApplicationContextAware, Serv private ContentNegotiationManager contentNegotiationManager; + private List<HandlerMethodArgumentResolver> argumentResolvers; + + private List<HandlerMethodReturnValueHandler> returnValueHandlers; + private List<HttpMessageConverter<?>> messageConverters; private Map<String, CorsConfiguration> corsConfigurations; @@ -403,7 +407,8 @@ public class WebMvcConfigurationSupport implements ApplicationContextAware, Serv */ @Bean public HandlerMapping resourceHandlerMapping() { - ResourceHandlerRegistry registry = new ResourceHandlerRegistry(this.applicationContext, this.servletContext); + ResourceHandlerRegistry registry = new ResourceHandlerRegistry(this.applicationContext, + this.servletContext, mvcContentNegotiationManager()); addResourceHandlers(registry); AbstractHandlerMapping handlerMapping = registry.getHandlerMapping(); @@ -474,18 +479,12 @@ public class WebMvcConfigurationSupport implements ApplicationContextAware, Serv */ @Bean public RequestMappingHandlerAdapter requestMappingHandlerAdapter() { - List<HandlerMethodArgumentResolver> argumentResolvers = new ArrayList<HandlerMethodArgumentResolver>(); - addArgumentResolvers(argumentResolvers); - - List<HandlerMethodReturnValueHandler> returnValueHandlers = new ArrayList<HandlerMethodReturnValueHandler>(); - addReturnValueHandlers(returnValueHandlers); - - RequestMappingHandlerAdapter adapter = new RequestMappingHandlerAdapter(); + RequestMappingHandlerAdapter adapter = createRequestMappingHandlerAdapter(); adapter.setContentNegotiationManager(mvcContentNegotiationManager()); adapter.setMessageConverters(getMessageConverters()); adapter.setWebBindingInitializer(getConfigurableWebBindingInitializer()); - adapter.setCustomArgumentResolvers(argumentResolvers); - adapter.setCustomReturnValueHandlers(returnValueHandlers); + adapter.setCustomArgumentResolvers(getArgumentResolvers()); + adapter.setCustomReturnValueHandlers(getReturnValueHandlers()); if (jackson2Present) { List<RequestBodyAdvice> requestBodyAdvices = new ArrayList<RequestBodyAdvice>(); @@ -513,6 +512,14 @@ public class WebMvcConfigurationSupport implements ApplicationContextAware, Serv } /** + * Protected method for plugging in a custom sub-class of + * {@link RequestMappingHandlerAdapter}. + */ + protected RequestMappingHandlerAdapter createRequestMappingHandlerAdapter() { + return new RequestMappingHandlerAdapter(); + } + + /** * Return the {@link ConfigurableWebBindingInitializer} to use for * initializing all {@link WebDataBinder} instances. */ @@ -618,6 +625,20 @@ public class WebMvcConfigurationSupport implements ApplicationContextAware, Serv } /** + * Provide access to the shared custom argument resolvers used by the + * {@link RequestMappingHandlerAdapter} and the + * {@link ExceptionHandlerExceptionResolver}. This method cannot be + * overridden, use {@link #addArgumentResolvers(List)} instead. + */ + protected final List<HandlerMethodArgumentResolver> getArgumentResolvers() { + if (this.argumentResolvers == null) { + this.argumentResolvers = new ArrayList<HandlerMethodArgumentResolver>(); + addArgumentResolvers(this.argumentResolvers); + } + return this.argumentResolvers; + } + + /** * Add custom {@link HandlerMethodArgumentResolver}s to use in addition to * the ones registered by default. * <p>Custom argument resolvers are invoked before built-in resolvers @@ -632,6 +653,20 @@ public class WebMvcConfigurationSupport implements ApplicationContextAware, Serv } /** + * Provide access to the shared return value handlers used by the + * {@link RequestMappingHandlerAdapter} and the + * {@link ExceptionHandlerExceptionResolver}. This method cannot be + * overridden, use {@link #addReturnValueHandlers(List)} instead. + */ + protected final List<HandlerMethodReturnValueHandler> getReturnValueHandlers() { + if (this.returnValueHandlers == null) { + this.returnValueHandlers = new ArrayList<HandlerMethodReturnValueHandler>(); + addReturnValueHandlers(this.returnValueHandlers); + } + return this.returnValueHandlers; + } + + /** * Add custom {@link HandlerMethodReturnValueHandler}s in addition to the * ones registered by default. * <p>Custom return value handlers are invoked before built-in ones except @@ -679,6 +714,14 @@ public class WebMvcConfigurationSupport implements ApplicationContextAware, Serv protected void configureMessageConverters(List<HttpMessageConverter<?>> converters) { } + /** + * Override this method to extend or modify the list of converters after it + * has been configured. This may be useful for example to allow default + * converters to be registered and then insert a custom converter through + * this method. + * @param converters the list of configured converters to extend. + * @since 4.1.3 + */ protected void extendMessageConverters(List<HttpMessageConverter<?>> converters) { } @@ -779,6 +822,7 @@ public class WebMvcConfigurationSupport implements ApplicationContextAware, Serv addDefaultHandlerExceptionResolvers(exceptionResolvers); } + extendHandlerExceptionResolvers(exceptionResolvers); HandlerExceptionResolverComposite composite = new HandlerExceptionResolverComposite(); composite.setOrder(0); composite.setExceptionResolvers(exceptionResolvers); @@ -798,6 +842,17 @@ public class WebMvcConfigurationSupport implements ApplicationContextAware, Serv } /** + * Override this method to extend or modify the list of + * {@link HandlerExceptionResolver}s after it has been configured. This may + * be useful for example to allow default resolvers to be registered and then + * insert a custom one through this method. + * @param exceptionResolvers the list of configured resolvers to extend. + * @since 4.3 + */ + protected void extendHandlerExceptionResolvers(List<HandlerExceptionResolver> exceptionResolvers) { + } + + /** * A method available to subclasses for adding default {@link HandlerExceptionResolver}s. * <p>Adds the following exception resolvers: * <ul> @@ -810,26 +865,36 @@ public class WebMvcConfigurationSupport implements ApplicationContextAware, Serv * </ul> */ protected final void addDefaultHandlerExceptionResolvers(List<HandlerExceptionResolver> exceptionResolvers) { - ExceptionHandlerExceptionResolver exceptionHandlerExceptionResolver = new ExceptionHandlerExceptionResolver(); - exceptionHandlerExceptionResolver.setContentNegotiationManager(mvcContentNegotiationManager()); - exceptionHandlerExceptionResolver.setMessageConverters(getMessageConverters()); + ExceptionHandlerExceptionResolver exceptionHandlerResolver = createExceptionHandlerExceptionResolver(); + exceptionHandlerResolver.setContentNegotiationManager(mvcContentNegotiationManager()); + exceptionHandlerResolver.setMessageConverters(getMessageConverters()); + exceptionHandlerResolver.setCustomArgumentResolvers(getArgumentResolvers()); + exceptionHandlerResolver.setCustomReturnValueHandlers(getReturnValueHandlers()); if (jackson2Present) { List<ResponseBodyAdvice<?>> interceptors = new ArrayList<ResponseBodyAdvice<?>>(); interceptors.add(new JsonViewResponseBodyAdvice()); - exceptionHandlerExceptionResolver.setResponseBodyAdvice(interceptors); + exceptionHandlerResolver.setResponseBodyAdvice(interceptors); } - exceptionHandlerExceptionResolver.setApplicationContext(this.applicationContext); - exceptionHandlerExceptionResolver.afterPropertiesSet(); - exceptionResolvers.add(exceptionHandlerExceptionResolver); + exceptionHandlerResolver.setApplicationContext(this.applicationContext); + exceptionHandlerResolver.afterPropertiesSet(); + exceptionResolvers.add(exceptionHandlerResolver); - ResponseStatusExceptionResolver responseStatusExceptionResolver = new ResponseStatusExceptionResolver(); - responseStatusExceptionResolver.setMessageSource(this.applicationContext); - exceptionResolvers.add(responseStatusExceptionResolver); + ResponseStatusExceptionResolver responseStatusResolver = new ResponseStatusExceptionResolver(); + responseStatusResolver.setMessageSource(this.applicationContext); + exceptionResolvers.add(responseStatusResolver); exceptionResolvers.add(new DefaultHandlerExceptionResolver()); } /** + * Protected method for plugging in a custom sub-class of + * {@link ExceptionHandlerExceptionResolver}. + */ + protected ExceptionHandlerExceptionResolver createExceptionHandlerExceptionResolver() { + return new ExceptionHandlerExceptionResolver(); + } + + /** * Register a {@link ViewResolverComposite} that contains a chain of view resolvers * to use for view resolution. * By default this resolver is ordered at 0 unless content negotiation view 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 3e9c509f..6ab3eb41 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 @@ -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. @@ -131,6 +131,16 @@ public interface WebMvcConfigurer { void configureHandlerExceptionResolvers(List<HandlerExceptionResolver> exceptionResolvers); /** + * A hook for extending or modifying the list of + * {@link HandlerExceptionResolver}s after it has been configured. This may + * be useful for example to allow default resolvers to be registered and then + * insert a custom one through this method. + * @param exceptionResolvers the list of configured resolvers to extend. + * @since 4.3 + */ + void extendHandlerExceptionResolvers(List<HandlerExceptionResolver> exceptionResolvers); + + /** * Add Spring MVC lifecycle interceptors for pre- and post-processing of * controller method invocations. Interceptors can be registered to apply * to all requests or be limited to a subset of URL patterns. 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 90d4b6ae..1c9e3e0c 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 @@ -121,6 +121,14 @@ public abstract class WebMvcConfigurerAdapter implements WebMvcConfigurer { * <p>This implementation is empty. */ @Override + public void extendHandlerExceptionResolvers(List<HandlerExceptionResolver> exceptionResolvers) { + } + + /** + * {@inheritDoc} + * <p>This implementation is empty. + */ + @Override public MessageCodesResolver getMessageCodesResolver() { return null; } 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 c88ba0ff..1b6d6e53 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 @@ -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. @@ -28,7 +28,7 @@ import org.springframework.web.method.support.HandlerMethodReturnValueHandler; import org.springframework.web.servlet.HandlerExceptionResolver; /** - * An {@link WebMvcConfigurer} implementation that delegates to other {@link WebMvcConfigurer} instances. + * A {@link WebMvcConfigurer} that delegates to one or more others. * * @author Rossen Stoyanchev * @since 3.1 @@ -107,6 +107,13 @@ class WebMvcConfigurerComposite implements WebMvcConfigurer { } @Override + public void extendHandlerExceptionResolvers(List<HandlerExceptionResolver> exceptionResolvers) { + for (WebMvcConfigurer delegate : this.delegates) { + delegate.configureHandlerExceptionResolvers(exceptionResolvers); + } + } + + @Override public void addInterceptors(InterceptorRegistry registry) { for (WebMvcConfigurer delegate : this.delegates) { delegate.addInterceptors(registry); 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 9ae1cb42..50997f55 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-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. @@ -17,6 +17,7 @@ package org.springframework.web.servlet.handler; import java.util.Set; + import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; @@ -128,13 +129,15 @@ public abstract class AbstractHandlerExceptionResolver implements HandlerExcepti Object handler, Exception ex) { if (shouldApplyTo(request, handler)) { - // Log exception, both at debug log level and at warn level, if desired. if (this.logger.isDebugEnabled()) { this.logger.debug("Resolving exception from handler [" + handler + "]: " + ex); } - logException(ex, request); prepareResponse(ex, response); - return doResolveException(request, response, handler, ex); + ModelAndView result = doResolveException(request, response, handler, ex); + if (result != null) { + logException(ex, request); + } + return result; } else { return null; @@ -194,7 +197,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: " + ex; + return "Resolved exception caused by Handler execution: " + 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 1877f194..1af14127 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 @@ -27,21 +27,21 @@ 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; +import org.springframework.web.HttpRequestHandler; import org.springframework.web.context.request.WebRequestInterceptor; import org.springframework.web.context.support.WebApplicationObjectSupport; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.CorsConfigurationSource; +import org.springframework.web.cors.CorsProcessor; +import org.springframework.web.cors.CorsUtils; +import org.springframework.web.cors.DefaultCorsProcessor; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; 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; /** @@ -471,7 +471,7 @@ public abstract class AbstractHandlerMapping extends WebApplicationObjectSupport } - private class PreFlightHandler implements HttpRequestHandler { + private class PreFlightHandler implements HttpRequestHandler, CorsConfigurationSource { private final CorsConfiguration config; @@ -485,10 +485,15 @@ public abstract class AbstractHandlerMapping extends WebApplicationObjectSupport corsProcessor.processRequest(this.config, request, response); } + + @Override + public CorsConfiguration getCorsConfiguration(HttpServletRequest request) { + return this.config; + } } - private class CorsInterceptor extends HandlerInterceptorAdapter { + private class CorsInterceptor extends HandlerInterceptorAdapter implements CorsConfigurationSource { private final CorsConfiguration config; @@ -502,6 +507,11 @@ public abstract class AbstractHandlerMapping extends WebApplicationObjectSupport return corsProcessor.processRequest(this.config, request, response); } + + @Override + public CorsConfiguration getCorsConfiguration(HttpServletRequest request) { + return this.config; + } } } 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 d84d1601..65a8db26 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 @@ -31,6 +31,7 @@ import java.util.concurrent.locks.ReentrantReadWriteLock; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; +import org.springframework.aop.support.AopUtils; import org.springframework.beans.factory.BeanFactoryUtils; import org.springframework.beans.factory.InitializingBean; import org.springframework.core.MethodIntrospector; @@ -230,7 +231,13 @@ public abstract class AbstractHandlerMethodMapping<T> extends AbstractHandlerMap new MethodIntrospector.MetadataLookup<T>() { @Override public T inspect(Method method) { - return getMappingForMethod(method, userType); + try { + return getMappingForMethod(method, userType); + } + catch (Throwable ex) { + throw new IllegalStateException("Invalid mapping on handler class [" + + userType.getName() + "]: " + method, ex); + } } }); @@ -238,7 +245,9 @@ public abstract class AbstractHandlerMethodMapping<T> extends AbstractHandlerMap 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()); + Method invocableMethod = AopUtils.selectInvocableMethod(entry.getKey(), userType); + T mapping = entry.getValue(); + registerHandlerMethod(handler, invocableMethod, mapping); } } 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 5d6406e5..51bed445 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 @@ -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. @@ -29,7 +29,6 @@ import org.springframework.beans.BeansException; import org.springframework.util.Assert; import org.springframework.util.CollectionUtils; import org.springframework.web.servlet.HandlerExecutionChain; -import org.springframework.web.servlet.HandlerMapping; /** * Abstract base class for URL-mapped {@link org.springframework.web.servlet.HandlerMapping} @@ -50,7 +49,7 @@ import org.springframework.web.servlet.HandlerMapping; * @author Arjen Poutsma * @since 16.04.2003 */ -public abstract class AbstractUrlHandlerMapping extends AbstractHandlerMapping { +public abstract class AbstractUrlHandlerMapping extends AbstractHandlerMapping implements MatchableHandlerMapping { private Object rootHandler; @@ -265,8 +264,8 @@ public abstract class AbstractUrlHandlerMapping extends AbstractHandlerMapping { * @see #PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE */ protected void exposePathWithinMapping(String bestMatchingPattern, String pathWithinMapping, HttpServletRequest request) { - request.setAttribute(HandlerMapping.BEST_MATCHING_PATTERN_ATTRIBUTE, bestMatchingPattern); - request.setAttribute(HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE, pathWithinMapping); + request.setAttribute(BEST_MATCHING_PATTERN_ATTRIBUTE, bestMatchingPattern); + request.setAttribute(PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE, pathWithinMapping); } /** @@ -276,7 +275,21 @@ public abstract class AbstractUrlHandlerMapping extends AbstractHandlerMapping { * @see #PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE */ protected void exposeUriTemplateVariables(Map<String, String> uriTemplateVariables, HttpServletRequest request) { - request.setAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE, uriTemplateVariables); + request.setAttribute(URI_TEMPLATE_VARIABLES_ATTRIBUTE, uriTemplateVariables); + } + + @Override + public RequestMatchResult match(HttpServletRequest request, String pattern) { + String lookupPath = getUrlPathHelper().getLookupPathForRequest(request); + if (getPathMatcher().match(pattern, lookupPath)) { + return new RequestMatchResult(pattern, lookupPath, getPathMatcher()); + } + else if (useTrailingSlashMatch()) { + if (!pattern.endsWith("/") && getPathMatcher().match(pattern + "/", lookupPath)) { + return new RequestMatchResult(pattern + "/", lookupPath, getPathMatcher()); + } + } + return null; } /** @@ -386,7 +399,7 @@ public abstract class AbstractUrlHandlerMapping extends AbstractHandlerMapping { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { exposePathWithinMapping(this.bestMatchingPattern, this.pathWithinMapping, request); - request.setAttribute(HandlerMapping.INTROSPECT_TYPE_LEVEL_MAPPING, supportsTypeLevelMappings()); + request.setAttribute(INTROSPECT_TYPE_LEVEL_MAPPING, supportsTypeLevelMappings()); return true; } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/BeanNameUrlHandlerMapping.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/BeanNameUrlHandlerMapping.java index 397c179b..44376cbd 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/BeanNameUrlHandlerMapping.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/BeanNameUrlHandlerMapping.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2010 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. @@ -28,9 +28,9 @@ import org.springframework.util.StringUtils; * * <p>This is the default implementation used by the * {@link org.springframework.web.servlet.DispatcherServlet}, along with - * {@link org.springframework.web.servlet.mvc.annotation.DefaultAnnotationHandlerMapping} - * (on Java 5 and higher). Alternatively, {@link SimpleUrlHandlerMapping} allows for - * customizing a handler mapping declaratively. + * {@link org.springframework.web.servlet.mvc.annotation.DefaultAnnotationHandlerMapping}. + * Alternatively, {@link SimpleUrlHandlerMapping} allows for customizing a + * handler mapping declaratively. * * <p>The mapping is from URL to bean name. Thus an incoming URL "/foo" would map * to a handler named "/foo", or to "/foo /foo2" in case of multiple mappings to diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/HandlerInterceptorAdapter.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/HandlerInterceptorAdapter.java index 602886ba..b00d4689 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/HandlerInterceptorAdapter.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/HandlerInterceptorAdapter.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. @@ -23,7 +23,7 @@ import org.springframework.web.servlet.AsyncHandlerInterceptor; import org.springframework.web.servlet.ModelAndView; /** - * Abstract adapter class for the HandlerInterceptor interface, + * Abstract adapter class for the {@link AsyncHandlerInterceptor} interface, * for simplified implementation of pre-only/post-only interceptors. * * @author Juergen Hoeller @@ -36,7 +36,8 @@ public abstract class HandlerInterceptorAdapter implements AsyncHandlerIntercept */ @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) - throws Exception { + throws Exception { + return true; } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/HandlerMappingIntrospector.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/HandlerMappingIntrospector.java new file mode 100644 index 00000000..1c7fa9b6 --- /dev/null +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/HandlerMappingIntrospector.java @@ -0,0 +1,187 @@ +/* + * 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.handler; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Properties; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletRequestWrapper; + +import org.springframework.beans.factory.BeanFactoryUtils; +import org.springframework.context.ApplicationContext; +import org.springframework.core.annotation.AnnotationAwareOrderComparator; +import org.springframework.core.io.ClassPathResource; +import org.springframework.core.io.Resource; +import org.springframework.core.io.support.PropertiesLoaderUtils; +import org.springframework.util.ClassUtils; +import org.springframework.util.StringUtils; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.CorsConfigurationSource; +import org.springframework.web.servlet.DispatcherServlet; +import org.springframework.web.servlet.HandlerExecutionChain; +import org.springframework.web.servlet.HandlerInterceptor; +import org.springframework.web.servlet.HandlerMapping; + +/** + * Helper class to get information from the {@code HandlerMapping} that would + * serve a specific request. + * + * <p>Provides the following methods: + * <ul> + * <li>{@link #getMatchableHandlerMapping} — obtain a {@code HandlerMapping} + * to check request-matching criteria against. + * <li>{@link #getCorsConfiguration} — obtain the CORS configuration for the + * request. + * </ul> + * + * @author Rossen Stoyanchev + * @since 4.3.1 + */ +public class HandlerMappingIntrospector implements CorsConfigurationSource { + + private final List<HandlerMapping> handlerMappings; + + + /** + * Constructor that detects the configured {@code HandlerMapping}s in the + * given {@code ApplicationContext} or falls back on + * "DispatcherServlet.properties" like the {@code DispatcherServlet}. + */ + public HandlerMappingIntrospector(ApplicationContext context) { + this.handlerMappings = initHandlerMappings(context); + } + + + private static List<HandlerMapping> initHandlerMappings(ApplicationContext context) { + Map<String, HandlerMapping> beans = BeanFactoryUtils.beansOfTypeIncludingAncestors( + context, HandlerMapping.class, true, false); + if (!beans.isEmpty()) { + List<HandlerMapping> mappings = new ArrayList<HandlerMapping>(beans.values()); + AnnotationAwareOrderComparator.sort(mappings); + return mappings; + } + return initDefaultHandlerMappings(context); + } + + private static List<HandlerMapping> initDefaultHandlerMappings(ApplicationContext context) { + Properties props; + String path = "DispatcherServlet.properties"; + try { + Resource resource = new ClassPathResource(path, DispatcherServlet.class); + props = PropertiesLoaderUtils.loadProperties(resource); + } + catch (IOException ex) { + throw new IllegalStateException("Could not load '" + path + "': " + ex.getMessage()); + } + + String value = props.getProperty(HandlerMapping.class.getName()); + String[] names = StringUtils.commaDelimitedListToStringArray(value); + List<HandlerMapping> result = new ArrayList<HandlerMapping>(names.length); + for (String name : names) { + try { + Class<?> clazz = ClassUtils.forName(name, DispatcherServlet.class.getClassLoader()); + Object mapping = context.getAutowireCapableBeanFactory().createBean(clazz); + result.add((HandlerMapping) mapping); + } + catch (ClassNotFoundException ex) { + throw new IllegalStateException("Could not find default HandlerMapping [" + name + "]"); + } + } + return result; + } + + + /** + * Return the configured HandlerMapping's. + */ + public List<HandlerMapping> getHandlerMappings() { + return this.handlerMappings; + } + + /** + * Find the {@link HandlerMapping} that would handle the given request and + * return it as a {@link MatchableHandlerMapping} that can be used to test + * request-matching criteria. + * <p>If the matching HandlerMapping is not an instance of + * {@link MatchableHandlerMapping}, an IllegalStateException is raised. + * @param request the current request + * @return the resolved matcher, or {@code null} + * @throws Exception if any of the HandlerMapping's raise an exception + */ + public MatchableHandlerMapping getMatchableHandlerMapping(HttpServletRequest request) throws Exception { + HttpServletRequest wrapper = new RequestAttributeChangeIgnoringWrapper(request); + for (HandlerMapping handlerMapping : this.handlerMappings) { + Object handler = handlerMapping.getHandler(wrapper); + if (handler == null) { + continue; + } + if (handlerMapping instanceof MatchableHandlerMapping) { + return ((MatchableHandlerMapping) handlerMapping); + } + throw new IllegalStateException("HandlerMapping is not a MatchableHandlerMapping"); + } + return null; + } + + @Override + public CorsConfiguration getCorsConfiguration(HttpServletRequest request) { + HttpServletRequest wrapper = new RequestAttributeChangeIgnoringWrapper(request); + for (HandlerMapping handlerMapping : this.handlerMappings) { + HandlerExecutionChain handler = null; + try { + handler = handlerMapping.getHandler(wrapper); + } + catch (Exception ex) { + // Ignore + } + if (handler == null) { + continue; + } + if (handler.getInterceptors() != null) { + for (HandlerInterceptor interceptor : handler.getInterceptors()) { + if (interceptor instanceof CorsConfigurationSource) { + return ((CorsConfigurationSource) interceptor).getCorsConfiguration(wrapper); + } + } + } + if (handler.getHandler() instanceof CorsConfigurationSource) { + return ((CorsConfigurationSource) handler.getHandler()).getCorsConfiguration(wrapper); + } + } + return null; + } + + + /** + * Request wrapper that ignores request attribute changes. + */ + private static class RequestAttributeChangeIgnoringWrapper extends HttpServletRequestWrapper { + + public RequestAttributeChangeIgnoringWrapper(HttpServletRequest request) { + super(request); + } + + @Override + public void setAttribute(String name, Object value) { + // Ignore attribute change + } + } + +} diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/MatchableHandlerMapping.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/MatchableHandlerMapping.java new file mode 100644 index 00000000..af5788c7 --- /dev/null +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/MatchableHandlerMapping.java @@ -0,0 +1,42 @@ +/* + * 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.handler; + +import javax.servlet.http.HttpServletRequest; + +import org.springframework.web.servlet.HandlerMapping; + +/** + * Additional interface that a {@link HandlerMapping} can implement to expose + * a request matching API aligned with its internal request matching + * configuration and implementation. + * + * @author Rossen Stoyanchev + * @since 4.3.1 + * @see HandlerMappingIntrospector + */ +public interface MatchableHandlerMapping extends HandlerMapping { + + /** + * Determine whether the given request matches the request criteria. + * @param request the current request + * @param pattern the pattern to match + * @return the result from request matching, or {@code null} if none + */ + RequestMatchResult match(HttpServletRequest request, String pattern); + +} diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/RequestMatchResult.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/RequestMatchResult.java new file mode 100644 index 00000000..380be418 --- /dev/null +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/RequestMatchResult.java @@ -0,0 +1,67 @@ +/* + * 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.handler; + +import java.util.Map; + +import org.springframework.util.Assert; +import org.springframework.util.PathMatcher; + +/** + * Container for the result from request pattern matching via + * {@link MatchableHandlerMapping} with a method to further extract + * URI template variables from the pattern. + * + * @author Rossen Stoyanchev + * @since 4.3.1 + */ +public class RequestMatchResult { + + private final String matchingPattern; + + private final String lookupPath; + + private final PathMatcher pathMatcher; + + + /** + * Create an instance with a matching pattern. + * @param matchingPattern the matching pattern, possibly not the same as the + * input pattern, e.g. inputPattern="/foo" and matchingPattern="/foo/". + * @param lookupPath the lookup path extracted from the request + * @param pathMatcher the PathMatcher used + */ + public RequestMatchResult(String matchingPattern, String lookupPath, PathMatcher pathMatcher) { + Assert.hasText(matchingPattern, "'matchingPattern' is required"); + Assert.hasText(lookupPath, "'lookupPath' is required"); + Assert.notNull(pathMatcher, "'pathMatcher' is required"); + this.matchingPattern = matchingPattern; + this.lookupPath = lookupPath; + this.pathMatcher = pathMatcher; + } + + + /** + * Extract URI template variables from the matching pattern as defined in + * {@link PathMatcher#extractUriTemplateVariables}. + * @return a map with URI template variables + */ + public Map<String, String> extractUriTemplateVariables() { + return this.pathMatcher.extractUriTemplateVariables(this.matchingPattern, this.lookupPath); + } + +} 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 b24363f1..7bf2c47e 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-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. @@ -46,6 +46,7 @@ public class SimpleMappingExceptionResolver extends AbstractHandlerExceptionReso /** The default name of the exception attribute: "exception". */ public static final String DEFAULT_EXCEPTION_ATTRIBUTE = "exception"; + private Properties exceptionMappings; private Class<?>[] excludedExceptions; @@ -108,7 +109,7 @@ public class SimpleMappingExceptionResolver extends AbstractHandlerExceptionReso public void setStatusCodes(Properties statusCodes) { for (Enumeration<?> enumeration = statusCodes.propertyNames(); enumeration.hasMoreElements();) { String viewName = (String) enumeration.nextElement(); - Integer statusCode = new Integer(statusCodes.getProperty(viewName)); + Integer statusCode = Integer.valueOf(statusCodes.getProperty(viewName)); this.statusCodes.put(viewName, statusCode); } } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/SimpleServletPostProcessor.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/SimpleServletPostProcessor.java index 49e2e3c8..d6fef3b6 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/SimpleServletPostProcessor.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/SimpleServletPostProcessor.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. @@ -18,7 +18,6 @@ package org.springframework.web.servlet.handler; import java.util.Collections; import java.util.Enumeration; -import java.util.HashSet; import javax.servlet.Servlet; import javax.servlet.ServletConfig; import javax.servlet.ServletContext; @@ -126,6 +125,11 @@ public class SimpleServletPostProcessor implements } } + @Override + public boolean requiresDestruction(Object bean) { + return (bean instanceof Servlet); + } + /** * Internal implementation of the {@link ServletConfig} interface, @@ -159,7 +163,7 @@ public class SimpleServletPostProcessor implements @Override public Enumeration<String> getInitParameterNames() { - return Collections.enumeration(new HashSet<String>()); + return Collections.enumeration(Collections.<String>emptySet()); } } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/i18n/AcceptHeaderLocaleResolver.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/i18n/AcceptHeaderLocaleResolver.java index c28f686e..c8bfa23c 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/i18n/AcceptHeaderLocaleResolver.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/i18n/AcceptHeaderLocaleResolver.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. @@ -16,6 +16,9 @@ package org.springframework.web.servlet.i18n; +import java.util.ArrayList; +import java.util.Enumeration; +import java.util.List; import java.util.Locale; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; @@ -31,14 +34,88 @@ import org.springframework.web.servlet.LocaleResolver; * can only be changed through changing the client's locale settings. * * @author Juergen Hoeller + * @author Rossen Stoyanchev * @since 27.02.2003 * @see javax.servlet.http.HttpServletRequest#getLocale() */ public class AcceptHeaderLocaleResolver implements LocaleResolver { + private final List<Locale> supportedLocales = new ArrayList<Locale>(4); + + private Locale defaultLocale; + + + /** + * Configure supported locales to check against the requested locales + * determined via {@link HttpServletRequest#getLocales()}. If this is not + * configured then {@link HttpServletRequest#getLocale()} is used instead. + * @param locales the supported locales + * @since 4.3 + */ + public void setSupportedLocales(List<Locale> locales) { + this.supportedLocales.clear(); + if (locales != null) { + this.supportedLocales.addAll(locales); + } + } + + /** + * Return the configured list of supported locales. + * @since 4.3 + */ + public List<Locale> getSupportedLocales() { + return this.supportedLocales; + } + + /** + * Configure a fixed default locale to fall back on if the request does not + * have an "Accept-Language" header. + * <p>By default this is not set in which case when there is "Accept-Language" + * header, the default locale for the server is used as defined in + * {@link HttpServletRequest#getLocale()}. + * @param defaultLocale the default locale to use + * @since 4.3 + */ + public void setDefaultLocale(Locale defaultLocale) { + this.defaultLocale = defaultLocale; + } + + /** + * The configured default locale, if any. + * @since 4.3 + */ + public Locale getDefaultLocale() { + return this.defaultLocale; + } + + @Override public Locale resolveLocale(HttpServletRequest request) { - return request.getLocale(); + Locale defaultLocale = getDefaultLocale(); + if (defaultLocale != null && request.getHeader("Accept-Language") == null) { + return defaultLocale; + } + Locale locale = request.getLocale(); + if (!isSupportedLocale(locale)) { + locale = findSupportedLocale(request, locale); + } + return locale; + } + + private boolean isSupportedLocale(Locale locale) { + List<Locale> supportedLocales = getSupportedLocales(); + return (supportedLocales.isEmpty() || supportedLocales.contains(locale)); + } + + private Locale findSupportedLocale(HttpServletRequest request, Locale fallback) { + Enumeration<Locale> requestLocales = request.getLocales(); + while (requestLocales.hasMoreElements()) { + Locale locale = requestLocales.nextElement(); + if (getSupportedLocales().contains(locale)) { + return locale; + } + } + return fallback; } @Override 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 547a91fc..f216e9f6 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 @@ -25,6 +25,7 @@ import javax.servlet.http.HttpServletResponse; import org.springframework.context.i18n.LocaleContext; import org.springframework.context.i18n.SimpleLocaleContext; import org.springframework.context.i18n.TimeZoneAwareLocaleContext; +import org.springframework.lang.UsesJava7; import org.springframework.util.StringUtils; import org.springframework.web.servlet.LocaleContextResolver; import org.springframework.web.servlet.LocaleResolver; @@ -81,6 +82,8 @@ public class CookieLocaleResolver extends CookieGenerator implements LocaleConte public static final String DEFAULT_COOKIE_NAME = CookieLocaleResolver.class.getName() + ".LOCALE"; + private boolean languageTagCompliant = false; + private Locale defaultLocale; private TimeZone defaultTimeZone; @@ -94,6 +97,30 @@ public class CookieLocaleResolver extends CookieGenerator implements LocaleConte setCookieName(DEFAULT_COOKIE_NAME); } + + /** + * Specify whether this resolver's cookies should be compliant with BCP 47 + * language tags instead of Java's legacy locale specification format. + * The default is {@code false}. + * <p>Note: This mode requires JDK 7 or higher. Set this flag to {@code true} + * for BCP 47 compliance on JDK 7+ only. + * @since 4.3 + * @see Locale#forLanguageTag(String) + * @see Locale#toLanguageTag() + */ + public void setLanguageTagCompliant(boolean languageTagCompliant) { + this.languageTagCompliant = languageTagCompliant; + } + + /** + * Return whether this resolver's cookies should be compliant with BCP 47 + * language tags instead of Java's legacy locale specification format. + * @since 4.3 + */ + public boolean isLanguageTagCompliant() { + return this.languageTagCompliant; + } + /** * Set a fixed Locale that this resolver will return if no cookie found. */ @@ -163,7 +190,7 @@ public class CookieLocaleResolver extends CookieGenerator implements LocaleConte localePart = value.substring(0, spaceIndex); timeZonePart = value.substring(spaceIndex + 1); } - locale = (!"-".equals(localePart) ? StringUtils.parseLocaleString(localePart) : null); + locale = (!"-".equals(localePart) ? parseLocaleValue(localePart) : null); if (timeZonePart != null) { timeZone = StringUtils.parseTimeZoneString(timeZonePart); } @@ -193,7 +220,8 @@ public class CookieLocaleResolver extends CookieGenerator implements LocaleConte if (localeContext instanceof TimeZoneAwareLocaleContext) { timeZone = ((TimeZoneAwareLocaleContext) localeContext).getTimeZone(); } - addCookie(response, (locale != null ? locale : "-") + (timeZone != null ? ' ' + timeZone.getID() : "")); + addCookie(response, + (locale != null ? toLocaleValue(locale) : "-") + (timeZone != null ? ' ' + timeZone.getID() : "")); } else { removeCookie(response); @@ -206,6 +234,34 @@ public class CookieLocaleResolver extends CookieGenerator implements LocaleConte /** + * Parse the given locale value coming from an incoming cookie. + * <p>The default implementation calls {@link StringUtils#parseLocaleString(String)} + * or JDK 7's {@link Locale#forLanguageTag(String)}, depending on the + * {@link #setLanguageTagCompliant "languageTagCompliant"} configuration property. + * @param locale the locale value to parse + * @return the corresponding {@code Locale} instance + * @since 4.3 + */ + @UsesJava7 + protected Locale parseLocaleValue(String locale) { + return (isLanguageTagCompliant() ? Locale.forLanguageTag(locale) : StringUtils.parseLocaleString(locale)); + } + + /** + * Render the given locale as a text value for inclusion in a cookie. + * <p>The default implementation calls {@link Locale#toString()} + * or JDK 7's {@link Locale#toLanguageTag()}, depending on the + * {@link #setLanguageTagCompliant "languageTagCompliant"} configuration property. + * @param locale the locale to stringify + * @return a String representation for the given locale + * @since 4.3 + */ + @UsesJava7 + protected String toLocaleValue(Locale locale) { + return (isLanguageTagCompliant() ? locale.toLanguageTag() : locale.toString()); + } + + /** * Determine the default locale for the given request, * Called if no locale cookie has been found. * <p>The default implementation returns the specified default locale, 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 099e4b46..7642bcd0 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 @@ -16,6 +16,7 @@ package org.springframework.web.servlet.i18n; +import java.util.Locale; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; @@ -23,6 +24,7 @@ import javax.servlet.http.HttpServletResponse; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.springframework.lang.UsesJava7; import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; import org.springframework.web.servlet.LocaleResolver; @@ -54,6 +56,8 @@ public class LocaleChangeInterceptor extends HandlerInterceptorAdapter { private boolean ignoreInvalidLocale = false; + private boolean languageTagCompliant = false; + /** * Set the name of the parameter that contains a locale specification @@ -104,6 +108,29 @@ public class LocaleChangeInterceptor extends HandlerInterceptorAdapter { return this.ignoreInvalidLocale; } + /** + * Specify whether to parse request parameter values as BCP 47 language tags + * instead of Java's legacy locale specification format. + * The default is {@code false}. + * <p>Note: This mode requires JDK 7 or higher. Set this flag to {@code true} + * for BCP 47 compliance on JDK 7+ only. + * @since 4.3 + * @see Locale#forLanguageTag(String) + * @see Locale#toLanguageTag() + */ + public void setLanguageTagCompliant(boolean languageTagCompliant) { + this.languageTagCompliant = languageTagCompliant; + } + + /** + * Return whether to use BCP 47 language tags instead of Java's legacy + * locale specification format. + * @since 4.3 + */ + public boolean isLanguageTagCompliant() { + return this.languageTagCompliant; + } + @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) @@ -118,7 +145,7 @@ public class LocaleChangeInterceptor extends HandlerInterceptorAdapter { "No LocaleResolver found: not in a DispatcherServlet request?"); } try { - localeResolver.setLocale(request, response, StringUtils.parseLocaleString(newLocale)); + localeResolver.setLocale(request, response, parseLocaleValue(newLocale)); } catch (IllegalArgumentException ex) { if (isIgnoreInvalidLocale()) { @@ -147,4 +174,18 @@ public class LocaleChangeInterceptor extends HandlerInterceptorAdapter { return false; } + /** + * Parse the given locale value as coming from a request parameter. + * <p>The default implementation calls {@link StringUtils#parseLocaleString(String)} + * or JDK 7's {@link Locale#forLanguageTag(String)}, depending on the + * {@link #setLanguageTagCompliant "languageTagCompliant"} configuration property. + * @param locale the locale value to parse + * @return the corresponding {@code Locale} instance + * @since 4.3 + */ + @UsesJava7 + protected Locale parseLocaleValue(String locale) { + return (isLanguageTagCompliant() ? Locale.forLanguageTag(locale) : StringUtils.parseLocaleString(locale)); + } + } 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 46fab982..c37e656a 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-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. @@ -20,6 +20,7 @@ import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; +import org.springframework.http.HttpMethod; import org.springframework.web.servlet.ModelAndView; import org.springframework.web.servlet.support.WebContentGenerator; import org.springframework.web.util.WebUtils; @@ -87,6 +88,7 @@ import org.springframework.web.util.WebUtils; * * @author Rod Johnson * @author Juergen Hoeller + * @author Rossen Stoyanchev * @see WebContentInterceptor */ public abstract class AbstractController extends WebContentGenerator implements Controller { @@ -95,6 +97,26 @@ public abstract class AbstractController extends WebContentGenerator implements /** + * Create a new AbstractController which supports + * HTTP methods GET, HEAD and POST by default. + */ + public AbstractController() { + this(true); + } + + /** + * Create a new AbstractController. + * @param restrictDefaultSupportedMethods {@code true} if this + * controller should support HTTP methods GET, HEAD and POST by default, + * or {@code false} if it should be unrestricted + * @since 4.3 + */ + public AbstractController(boolean restrictDefaultSupportedMethods) { + super(restrictDefaultSupportedMethods); + } + + + /** * Set if controller execution should be synchronized on the session, * to serialize parallel invocations from the same client. * <p>More specifically, the execution of the {@code handleRequestInternal} @@ -129,6 +151,11 @@ public abstract class AbstractController extends WebContentGenerator implements public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception { + if (HttpMethod.OPTIONS.matches(request.getMethod())) { + response.setHeader("Allow", getAllowHeader()); + return null; + } + // Delegate to WebContentGenerator for checking and preparing. checkRequest(request); prepareResponse(response); diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/ParameterizableViewController.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/ParameterizableViewController.java index bee18861..5d512887 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/ParameterizableViewController.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/ParameterizableViewController.java @@ -19,6 +19,7 @@ package org.springframework.web.servlet.mvc; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; +import org.springframework.http.HttpMethod; import org.springframework.http.HttpStatus; import org.springframework.web.servlet.ModelAndView; import org.springframework.web.servlet.View; @@ -43,6 +44,11 @@ public class ParameterizableViewController extends AbstractController { private boolean statusOnly; + public ParameterizableViewController() { + super(false); + setSupportedMethods(HttpMethod.GET.name(), HttpMethod.HEAD.name()); + } + /** * Set a view name for the ModelAndView to return, to be resolved by the * DispatcherServlet via a ViewResolver. Will override any pre-existing diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/ServletForwardingController.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/ServletForwardingController.java index 50cf151b..3df9b642 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/ServletForwardingController.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/ServletForwardingController.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. @@ -90,6 +90,11 @@ public class ServletForwardingController extends AbstractController implements B private String beanName; + public ServletForwardingController() { + super(false); + } + + /** * Set the name of the servlet to forward to, * i.e. the "servlet-name" of the target servlet in web.xml. diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/ServletWrappingController.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/ServletWrappingController.java index 8ca0ef0a..c16534ff 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/ServletWrappingController.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/ServletWrappingController.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. @@ -78,13 +78,11 @@ import org.springframework.web.servlet.ModelAndView; * @author Juergen Hoeller * @since 1.1.1 * @see ServletForwardingController - * @see org.springframework.orm.jpa.support.OpenEntityManagerInViewInterceptor - * @see org.springframework.orm.jpa.support.OpenEntityManagerInViewFilter */ public class ServletWrappingController extends AbstractController implements BeanNameAware, InitializingBean, DisposableBean { - private Class<?> servletClass; + private Class<? extends Servlet> servletClass; private String servletName; @@ -95,12 +93,17 @@ public class ServletWrappingController extends AbstractController private Servlet servletInstance; + public ServletWrappingController() { + super(false); + } + + /** * Set the class of the servlet to wrap. * Needs to implement {@code javax.servlet.Servlet}. * @see javax.servlet.Servlet */ - public void setServletClass(Class<?> servletClass) { + public void setServletClass(Class<? extends Servlet> servletClass) { this.servletClass = servletClass; } @@ -133,27 +136,23 @@ public class ServletWrappingController extends AbstractController @Override public void afterPropertiesSet() throws Exception { if (this.servletClass == null) { - throw new IllegalArgumentException("servletClass is required"); - } - if (!Servlet.class.isAssignableFrom(this.servletClass)) { - throw new IllegalArgumentException("servletClass [" + this.servletClass.getName() + - "] needs to implement interface [javax.servlet.Servlet]"); + throw new IllegalArgumentException("'servletClass' is required"); } if (this.servletName == null) { this.servletName = this.beanName; } - this.servletInstance = (Servlet) this.servletClass.newInstance(); + this.servletInstance = this.servletClass.newInstance(); this.servletInstance.init(new DelegatingServletConfig()); } /** - * Invoke the the wrapped Servlet instance. + * Invoke the wrapped Servlet instance. * @see javax.servlet.Servlet#service(javax.servlet.ServletRequest, javax.servlet.ServletResponse) */ @Override protected ModelAndView handleRequestInternal(HttpServletRequest request, HttpServletResponse response) - throws Exception { + throws Exception { this.servletInstance.service(request, response); return null; 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 038667f4..5e5810e5 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-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. @@ -102,7 +102,6 @@ public class WebContentInterceptor extends WebContentGenerator implements Handle * <p>Only relevant for the "cacheMappings" setting. * @see #setCacheMappings * @see org.springframework.web.servlet.handler.AbstractUrlHandlerMapping#setUrlPathHelper - * @see org.springframework.web.servlet.mvc.multiaction.AbstractUrlMethodNameResolver#setUrlPathHelper */ public void setUrlPathHelper(UrlPathHelper urlPathHelper) { Assert.notNull(urlPathHelper, "UrlPathHelper must not be null"); 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 f3cdd873..2a26985c 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 @@ -96,8 +96,6 @@ import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.SessionAttributes; -import org.springframework.web.bind.annotation.support.HandlerMethodInvoker; -import org.springframework.web.bind.annotation.support.HandlerMethodResolver; import org.springframework.web.bind.support.DefaultSessionAttributeStore; import org.springframework.web.bind.support.SessionAttributeStore; import org.springframework.web.bind.support.WebArgumentResolver; @@ -110,9 +108,6 @@ import org.springframework.web.servlet.HandlerAdapter; import org.springframework.web.servlet.HandlerMapping; import org.springframework.web.servlet.ModelAndView; import org.springframework.web.servlet.View; -import org.springframework.web.servlet.mvc.multiaction.InternalPathMethodNameResolver; -import org.springframework.web.servlet.mvc.multiaction.MethodNameResolver; -import org.springframework.web.servlet.mvc.multiaction.NoSuchRequestHandlingMethodException; import org.springframework.web.servlet.support.RequestContextUtils; import org.springframework.web.servlet.support.WebContentGenerator; import org.springframework.web.util.UrlPathHelper; @@ -164,7 +159,8 @@ public class AnnotationMethodHandlerAdapter extends WebContentGenerator private PathMatcher pathMatcher = new AntPathMatcher(); - private MethodNameResolver methodNameResolver = new InternalPathMethodNameResolver(); + private org.springframework.web.servlet.mvc.multiaction.MethodNameResolver methodNameResolver = + new org.springframework.web.servlet.mvc.multiaction.InternalPathMethodNameResolver(); private WebBindingInitializer webBindingInitializer; @@ -255,7 +251,7 @@ public class AnnotationMethodHandlerAdapter extends WebContentGenerator * <p>Will only kick in when the handler method cannot be resolved uniquely * through the annotation metadata already. */ - public void setMethodNameResolver(MethodNameResolver methodNameResolver) { + public void setMethodNameResolver(org.springframework.web.servlet.mvc.multiaction.MethodNameResolver methodNameResolver) { this.methodNameResolver = methodNameResolver; } @@ -524,9 +520,10 @@ public class AnnotationMethodHandlerAdapter extends WebContentGenerator /** - * Servlet-specific subclass of {@link HandlerMethodResolver}. + * Servlet-specific subclass of {@code HandlerMethodResolver}. */ - private class ServletHandlerMethodResolver extends HandlerMethodResolver { + @SuppressWarnings("deprecation") + private class ServletHandlerMethodResolver extends org.springframework.web.bind.annotation.support.HandlerMethodResolver { private final Map<Method, RequestMappingInfo> mappings = new HashMap<Method, RequestMappingInfo>(); @@ -674,7 +671,8 @@ public class AnnotationMethodHandlerAdapter extends WebContentGenerator if (!allowedMethods.isEmpty()) { throw new HttpRequestMethodNotSupportedException(request.getMethod(), StringUtils.toStringArray(allowedMethods)); } - throw new NoSuchRequestHandlingMethodException(lookupPath, request.getMethod(), request.getParameterMap()); + throw new org.springframework.web.servlet.mvc.multiaction.NoSuchRequestHandlingMethodException( + lookupPath, request.getMethod(), request.getParameterMap()); } } @@ -768,13 +766,14 @@ public class AnnotationMethodHandlerAdapter extends WebContentGenerator /** - * Servlet-specific subclass of {@link HandlerMethodInvoker}. + * Servlet-specific subclass of {@code HandlerMethodInvoker}. */ - private class ServletHandlerMethodInvoker extends HandlerMethodInvoker { + @SuppressWarnings("deprecation") + private class ServletHandlerMethodInvoker extends org.springframework.web.bind.annotation.support.HandlerMethodInvoker { private boolean responseArgumentUsed = false; - private ServletHandlerMethodInvoker(HandlerMethodResolver resolver) { + private ServletHandlerMethodInvoker(org.springframework.web.bind.annotation.support.HandlerMethodResolver resolver) { super(resolver, webBindingInitializer, sessionAttributeStore, parameterNameDiscoverer, customArgumentResolvers, messageConverters); } 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 6c1288fc..331da205 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-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. @@ -30,6 +30,7 @@ import org.springframework.web.util.WebUtils; * * @author Juergen Hoeller * @author Arjen Poutsma + * @author Rossen Stoyanchev * @since 2.5.2 * @deprecated as of Spring 3.2, together with {@link DefaultAnnotationHandlerMapping}, * {@link AnnotationMethodHandlerAdapter}, and {@link AnnotationMethodHandlerExceptionResolver}. @@ -43,11 +44,12 @@ abstract class ServletAnnotationMappingUtils { * @param request the current HTTP request to check */ public static boolean checkRequestMethod(RequestMethod[] methods, HttpServletRequest request) { - if (ObjectUtils.isEmpty(methods)) { + String inputMethod = request.getMethod(); + if (ObjectUtils.isEmpty(methods) && !RequestMethod.OPTIONS.name().equals(inputMethod)) { return true; } for (RequestMethod method : methods) { - if (method.name().equals(request.getMethod())) { + if (method.name().equals(inputMethod)) { return true; } } 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 e6708f27..d3114c13 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 @@ -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,13 +16,10 @@ package org.springframework.web.servlet.mvc.condition; -import javax.servlet.http.HttpServletRequest; - import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.http.MediaType; -import org.springframework.web.HttpMediaTypeException; import org.springframework.web.bind.annotation.RequestMapping; /** @@ -33,7 +30,7 @@ import org.springframework.web.bind.annotation.RequestMapping; * @author Rossen Stoyanchev * @since 3.1 */ -abstract class AbstractMediaTypeExpression implements Comparable<AbstractMediaTypeExpression>, MediaTypeExpression { +abstract class AbstractMediaTypeExpression implements MediaTypeExpression, Comparable<AbstractMediaTypeExpression> { protected final Log logger = LogFactory.getLog(getClass()); @@ -70,19 +67,6 @@ abstract class AbstractMediaTypeExpression implements Comparable<AbstractMediaTy } - public final boolean match(HttpServletRequest request) { - try { - boolean match = matchMediaType(request); - return (!this.isNegated ? match : !match); - } - catch (HttpMediaTypeException ex) { - return false; - } - } - - protected abstract boolean matchMediaType(HttpServletRequest request) throws HttpMediaTypeException; - - @Override public int compareTo(AbstractMediaTypeExpression other) { return MediaType.SPECIFICITY_COMPARATOR.compare(this.getMediaType(), other.getMediaType()); 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 92dd4e87..26fb5641 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 @@ -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. diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/condition/ConsumesRequestCondition.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/condition/ConsumesRequestCondition.java index f53e34b9..e7a6af41 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/condition/ConsumesRequestCondition.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/condition/ConsumesRequestCondition.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. @@ -28,8 +28,10 @@ import javax.servlet.http.HttpServletRequest; import org.springframework.http.InvalidMediaTypeException; import org.springframework.http.MediaType; import org.springframework.util.StringUtils; +import org.springframework.web.HttpMediaTypeException; import org.springframework.web.HttpMediaTypeNotSupportedException; import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.cors.CorsUtils; import org.springframework.web.servlet.mvc.condition.HeadersRequestCondition.HeaderExpression; /** @@ -46,6 +48,9 @@ import org.springframework.web.servlet.mvc.condition.HeadersRequestCondition.Hea */ public final class ConsumesRequestCondition extends AbstractRequestCondition<ConsumesRequestCondition> { + private final static ConsumesRequestCondition PRE_FLIGHT_MATCH = new ConsumesRequestCondition(); + + private final List<ConsumeMediaTypeExpression> expressions; @@ -160,13 +165,25 @@ public final class ConsumesRequestCondition extends AbstractRequestCondition<Con */ @Override public ConsumesRequestCondition getMatchingCondition(HttpServletRequest request) { + if (CorsUtils.isPreFlightRequest(request)) { + return PRE_FLIGHT_MATCH; + } if (isEmpty()) { return this; } - Set<ConsumeMediaTypeExpression> result = new LinkedHashSet<ConsumeMediaTypeExpression>(expressions); + MediaType contentType; + try { + contentType = StringUtils.hasLength(request.getContentType()) ? + MediaType.parseMediaType(request.getContentType()) : + MediaType.APPLICATION_OCTET_STREAM; + } + catch (InvalidMediaTypeException ex) { + return null; + } + Set<ConsumeMediaTypeExpression> result = new LinkedHashSet<ConsumeMediaTypeExpression>(this.expressions); for (Iterator<ConsumeMediaTypeExpression> iterator = result.iterator(); iterator.hasNext();) { ConsumeMediaTypeExpression expression = iterator.next(); - if (!expression.match(request)) { + if (!expression.match(contentType)) { iterator.remove(); } } @@ -214,18 +231,9 @@ public final class ConsumesRequestCondition extends AbstractRequestCondition<Con super(mediaType, negated); } - @Override - protected boolean matchMediaType(HttpServletRequest request) throws HttpMediaTypeNotSupportedException { - try { - MediaType contentType = StringUtils.hasLength(request.getContentType()) ? - MediaType.parseMediaType(request.getContentType()) : - MediaType.APPLICATION_OCTET_STREAM; - return getMediaType().includes(contentType); - } - catch (InvalidMediaTypeException ex) { - throw new HttpMediaTypeNotSupportedException( - "Can't parse Content-Type [" + request.getContentType() + "]: " + ex.getMessage()); - } + public final boolean match(MediaType contentType) { + boolean match = getMediaType().includes(contentType); + return (!isNegated() ? match : !match); } } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/condition/HeadersRequestCondition.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/condition/HeadersRequestCondition.java index ffa47be0..aaa0847c 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/condition/HeadersRequestCondition.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/condition/HeadersRequestCondition.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. @@ -23,6 +23,7 @@ import java.util.Set; import javax.servlet.http.HttpServletRequest; import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.cors.CorsUtils; /** * A logical conjunction (' && ') request condition that matches a request against @@ -38,6 +39,9 @@ import org.springframework.web.bind.annotation.RequestMapping; */ public final class HeadersRequestCondition extends AbstractRequestCondition<HeadersRequestCondition> { + private final static HeadersRequestCondition PRE_FLIGHT_MATCH = new HeadersRequestCondition(); + + private final Set<HeaderExpression> expressions; @@ -105,6 +109,9 @@ public final class HeadersRequestCondition extends AbstractRequestCondition<Head */ @Override public HeadersRequestCondition getMatchingCondition(HttpServletRequest request) { + if (CorsUtils.isPreFlightRequest(request)) { + return PRE_FLIGHT_MATCH; + } for (HeaderExpression expression : expressions) { if (!expression.match(request)) { return null; diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/condition/ProducesRequestCondition.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/condition/ProducesRequestCondition.java index bc99eae4..55931d4b 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/condition/ProducesRequestCondition.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/condition/ProducesRequestCondition.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. @@ -26,10 +26,12 @@ import java.util.Set; import javax.servlet.http.HttpServletRequest; import org.springframework.http.MediaType; +import org.springframework.web.HttpMediaTypeException; import org.springframework.web.HttpMediaTypeNotAcceptableException; import org.springframework.web.accept.ContentNegotiationManager; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.context.request.ServletWebRequest; +import org.springframework.web.cors.CorsUtils; import org.springframework.web.servlet.mvc.condition.HeadersRequestCondition.HeaderExpression; /** @@ -45,6 +47,11 @@ import org.springframework.web.servlet.mvc.condition.HeadersRequestCondition.Hea */ public final class ProducesRequestCondition extends AbstractRequestCondition<ProducesRequestCondition> { + private final static ProducesRequestCondition PRE_FLIGHT_MATCH = new ProducesRequestCondition(); + + private static final ProducesRequestCondition EMPTY_CONDITION = new ProducesRequestCondition(); + + private final List<ProduceMediaTypeExpression> MEDIA_TYPE_ALL_LIST = Collections.singletonList(new ProduceMediaTypeExpression("*/*")); @@ -176,17 +183,35 @@ public final class ProducesRequestCondition extends AbstractRequestCondition<Pro */ @Override public ProducesRequestCondition getMatchingCondition(HttpServletRequest request) { + if (CorsUtils.isPreFlightRequest(request)) { + return PRE_FLIGHT_MATCH; + } if (isEmpty()) { return this; } + List<MediaType> acceptedMediaTypes; + try { + acceptedMediaTypes = getAcceptedMediaTypes(request); + } + catch (HttpMediaTypeException ex) { + return null; + } Set<ProduceMediaTypeExpression> result = new LinkedHashSet<ProduceMediaTypeExpression>(expressions); for (Iterator<ProduceMediaTypeExpression> iterator = result.iterator(); iterator.hasNext();) { ProduceMediaTypeExpression expression = iterator.next(); - if (!expression.match(request)) { + if (!expression.match(acceptedMediaTypes)) { iterator.remove(); } } - return (result.isEmpty()) ? null : new ProducesRequestCondition(result, this.contentNegotiationManager); + if (!result.isEmpty()) { + return new ProducesRequestCondition(result, this.contentNegotiationManager); + } + else if (acceptedMediaTypes.contains(MediaType.ALL)) { + return EMPTY_CONDITION; + } + else { + return null; + } } /** @@ -295,9 +320,12 @@ public final class ProducesRequestCondition extends AbstractRequestCondition<Pro super(expression); } - @Override - protected boolean matchMediaType(HttpServletRequest request) throws HttpMediaTypeNotAcceptableException { - List<MediaType> acceptedMediaTypes = getAcceptedMediaTypes(request); + public final boolean match(List<MediaType> acceptedMediaTypes) { + boolean match = matchMediaType(acceptedMediaTypes); + return (!isNegated() ? match : !match); + } + + private boolean matchMediaType(List<MediaType> acceptedMediaTypes) { for (MediaType acceptedMediaType : acceptedMediaTypes) { if (getMediaType().isCompatibleWith(acceptedMediaType)) { return true; diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/condition/RequestCondition.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/condition/RequestCondition.java index 1487381c..542e1a2d 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/condition/RequestCondition.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/condition/RequestCondition.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. @@ -18,27 +18,25 @@ package org.springframework.web.servlet.mvc.condition; import javax.servlet.http.HttpServletRequest; -import org.springframework.web.bind.annotation.RequestMapping; - /** - * The contract for request conditions in Spring MVC's mapping infrastructure. + * Contract for request mapping conditions. * * <p>Request conditions can be combined via {@link #combine(Object)}, matched to * a request via {@link #getMatchingCondition(HttpServletRequest)}, and compared * to each other via {@link #compareTo(Object, HttpServletRequest)} to determine - * which matches a request more closely. + * which is a closer match for a given request. * * @author Rossen Stoyanchev * @author Arjen Poutsma * @since 3.1 - * @param <T> the type of objects that this RequestCondition can be combined with and compared to + * @param <T> the type of objects that this RequestCondition can be combined + * with and compared to */ public interface RequestCondition<T> { /** - * Defines the rules for combining this condition (i.e. the current instance) - * with another condition. For example combining type- and method-level - * {@link RequestMapping} conditions. + * Combine this condition with another such as conditions from a + * type-level and method-level {@code @RequestMapping} annotation. * @param other the condition to combine with. * @return a request condition instance that is the result of combining * the two condition instances. @@ -46,18 +44,21 @@ public interface RequestCondition<T> { T combine(T other); /** - * Checks if this condition matches the given request and returns a - * potentially new request condition with content tailored to the - * current request. For example a condition with URL patterns might - * return a new condition that contains matching patterns sorted - * with best matching patterns on top. - * @return a condition instance in case of a match; - * or {@code null} if there is no match + * Check if the condition matches the request returning a potentially new + * instance created for the current request. For example a condition with + * multiple URL patterns may return a new instance only with those patterns + * that match the request. + * <p>For CORS pre-flight requests, conditions should match to the would-be, + * actual request (e.g. URL pattern, query parameters, and the HTTP method + * from the "Access-Control-Request-Method" header). If a condition cannot + * be matched to a pre-flight request it should return an instance with + * empty content thus not causing a failure to match. + * @return a condition instance in case of a match or {@code null} otherwise. */ T getMatchingCondition(HttpServletRequest request); /** - * Compares this condition to another condition in the context of + * Compare this condition to another condition in the context of * a specific request. This method assumes both instances have * been obtained via {@link #getMatchingCondition(HttpServletRequest)} * to ensure they have content relevant to current request only. diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/condition/RequestMethodsRequestCondition.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/condition/RequestMethodsRequestCondition.java index 6d18fcf2..b044ae5b 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/condition/RequestMethodsRequestCondition.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/condition/RequestMethodsRequestCondition.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. @@ -22,9 +22,13 @@ import java.util.Collections; import java.util.LinkedHashSet; import java.util.List; import java.util.Set; +import javax.servlet.DispatcherType; import javax.servlet.http.HttpServletRequest; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.cors.CorsUtils; /** * A logical disjunction (' || ') request condition that matches a request @@ -36,6 +40,10 @@ import org.springframework.web.bind.annotation.RequestMethod; */ public final class RequestMethodsRequestCondition extends AbstractRequestCondition<RequestMethodsRequestCondition> { + private static final RequestMethodsRequestCondition GET_CONDITION = + new RequestMethodsRequestCondition(RequestMethod.GET); + + private final Set<RequestMethod> methods; @@ -90,32 +98,55 @@ public final class RequestMethodsRequestCondition extends AbstractRequestConditi * Check if any of the HTTP request methods match the given request and * return an instance that contains the matching HTTP request method only. * @param request the current request - * @return the same instance if the condition is empty, a new condition with - * the matched request method, or {@code null} if no request methods match + * @return the same instance if the condition is empty (unless the request + * method is HTTP OPTIONS), a new condition with the matched request method, + * or {@code null} if there is no match or the condition is empty and the + * request method is OPTIONS. */ @Override public RequestMethodsRequestCondition getMatchingCondition(HttpServletRequest request) { - if (this.methods.isEmpty()) { - return this; + if (CorsUtils.isPreFlightRequest(request)) { + return matchPreFlight(request); } - RequestMethod incomingRequestMethod = getRequestMethod(request); - if (incomingRequestMethod != null) { - for (RequestMethod method : this.methods) { - if (method.equals(incomingRequestMethod)) { - return new RequestMethodsRequestCondition(method); - } + + if (getMethods().isEmpty()) { + if (RequestMethod.OPTIONS.name().equals(request.getMethod()) && + !DispatcherType.ERROR.equals(request.getDispatcherType())) { + + return null; // No implicit match for OPTIONS (we handle it) } + return this; } - return null; + + return matchRequestMethod(request.getMethod()); } - private RequestMethod getRequestMethod(HttpServletRequest request) { - try { - return RequestMethod.valueOf(request.getMethod()); + /** + * On a pre-flight request match to the would-be, actual request. + * Hence empty conditions is a match, otherwise try to match to the HTTP + * method in the "Access-Control-Request-Method" header. + */ + private RequestMethodsRequestCondition matchPreFlight(HttpServletRequest request) { + if (getMethods().isEmpty()) { + return this; } - catch (IllegalArgumentException ex) { - return null; + String expectedMethod = request.getHeader(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD); + return matchRequestMethod(expectedMethod); + } + + private RequestMethodsRequestCondition matchRequestMethod(String httpMethodValue) { + HttpMethod httpMethod = HttpMethod.resolve(httpMethodValue); + if (httpMethod != null) { + for (RequestMethod method : getMethods()) { + if (httpMethod.matches(method.name())) { + return new RequestMethodsRequestCondition(method); + } + } + if (httpMethod == HttpMethod.HEAD && getMethods().contains(RequestMethod.GET)) { + return GET_CONDITION; + } } + return null; } /** @@ -131,7 +162,18 @@ public final class RequestMethodsRequestCondition extends AbstractRequestConditi */ @Override public int compareTo(RequestMethodsRequestCondition other, HttpServletRequest request) { - return (other.methods.size() - this.methods.size()); + if (other.methods.size() != this.methods.size()) { + return other.methods.size() - this.methods.size(); + } + else if (this.methods.size() == 1) { + if (this.methods.contains(RequestMethod.HEAD) && other.methods.contains(RequestMethod.GET)) { + return -1; + } + else if (this.methods.contains(RequestMethod.GET) && other.methods.contains(RequestMethod.HEAD)) { + return 1; + } + } + return 0; } } 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 395ecb9c..6ecc7687 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 @@ -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,11 @@ package org.springframework.web.servlet.mvc.method; import java.util.List; import javax.servlet.http.HttpServletRequest; -import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; 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; @@ -36,7 +35,7 @@ import org.springframework.web.servlet.mvc.condition.RequestMethodsRequestCondit import org.springframework.web.util.UrlPathHelper; /** - * Encapsulates the following request mapping conditions: + * A {@link RequestCondition} that consists of the following other conditions: * <ol> * <li>{@link PatternsRequestCondition} * <li>{@link RequestMethodsRequestCondition} @@ -215,15 +214,7 @@ public final class RequestMappingInfo implements RequestCondition<RequestMapping ProducesRequestCondition produces = this.producesCondition.getMatchingCondition(request); if (methods == null || params == null || headers == null || consumes == null || produces == null) { - if (CorsUtils.isPreFlightRequest(request)) { - methods = getAccessControlRequestMethodCondition(request); - if (methods == null || params == null) { - return null; - } - } - else { - return null; - } + return null; } PatternsRequestCondition patterns = this.patternsCondition.getMatchingCondition(request); @@ -241,22 +232,6 @@ public final class RequestMappingInfo implements RequestCondition<RequestMapping } /** - * 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. * <p>Note: It is assumed both instances have been obtained via * {@link #getMatchingCondition(HttpServletRequest)} to ensure they have conditions with @@ -264,7 +239,15 @@ public final class RequestMappingInfo implements RequestCondition<RequestMapping */ @Override public int compareTo(RequestMappingInfo other, HttpServletRequest request) { - int result = this.patternsCondition.compareTo(other.getPatternsCondition(), request); + int result; + // Automatic vs explicit HTTP HEAD mapping + if (HttpMethod.HEAD.matches(request.getMethod())) { + result = this.methodsCondition.compareTo(other.getMethodsCondition(), request); + if (result != 0) { + return result; + } + } + result = this.patternsCondition.compareTo(other.getPatternsCondition(), request); if (result != 0) { return result; } @@ -284,6 +267,7 @@ public final class RequestMappingInfo implements RequestCondition<RequestMapping if (result != 0) { return result; } + // Implicit (no method) vs explicit HTTP method mappings result = this.methodsCondition.compareTo(other.getMethodsCondition(), request); if (result != 0) { return result; @@ -537,13 +521,25 @@ public final class RequestMappingInfo implements RequestCondition<RequestMapping private ContentNegotiationManager contentNegotiationManager; /** - * Set a custom UrlPathHelper to use for the PatternsRequestCondition. - * <p>By default this is not set. + * @deprecated as of Spring 4.2.8, in favor of {@link #setUrlPathHelper} */ + @Deprecated public void setPathHelper(UrlPathHelper pathHelper) { this.urlPathHelper = pathHelper; } + /** + * Set a custom UrlPathHelper to use for the PatternsRequestCondition. + * <p>By default this is not set. + * @since 4.2.8 + */ + public void setUrlPathHelper(UrlPathHelper urlPathHelper) { + this.urlPathHelper = urlPathHelper; + } + + /** + * Return a custom UrlPathHelper to use for the PatternsRequestCondition, if any. + */ public UrlPathHelper getUrlPathHelper() { return this.urlPathHelper; } @@ -556,24 +552,30 @@ public final class RequestMappingInfo implements RequestCondition<RequestMapping this.pathMatcher = pathMatcher; } + /** + * Return a custom PathMatcher to use for the PatternsRequestCondition, if any. + */ public PathMatcher getPathMatcher() { return this.pathMatcher; } /** - * Whether to apply trailing slash matching in PatternsRequestCondition. + * Set whether to apply trailing slash matching in PatternsRequestCondition. * <p>By default this is set to 'true'. */ public void setTrailingSlashMatch(boolean trailingSlashMatch) { this.trailingSlashMatch = trailingSlashMatch; } + /** + * Return whether to apply trailing slash matching in PatternsRequestCondition. + */ public boolean useTrailingSlashMatch() { return this.trailingSlashMatch; } /** - * Whether to apply suffix pattern matching in PatternsRequestCondition. + * Set whether to apply suffix pattern matching in PatternsRequestCondition. * <p>By default this is set to 'true'. * @see #setRegisteredSuffixPatternMatch(boolean) */ @@ -581,14 +583,17 @@ public final class RequestMappingInfo implements RequestCondition<RequestMapping this.suffixPatternMatch = suffixPatternMatch; } + /** + * Return whether to apply suffix pattern matching in PatternsRequestCondition. + */ public boolean useSuffixPatternMatch() { return this.suffixPatternMatch; } /** - * Whether suffix pattern matching should be restricted to registered + * Set whether suffix pattern matching should be restricted to registered * file extensions only. Setting this property also sets - * suffixPatternMatch=true and requires that a + * {@code suffixPatternMatch=true} and requires that a * {@link #setContentNegotiationManager} is also configured in order to * obtain the registered file extensions. */ @@ -597,6 +602,10 @@ public final class RequestMappingInfo implements RequestCondition<RequestMapping this.suffixPatternMatch = (registeredSuffixPatternMatch || this.suffixPatternMatch); } + /** + * Return whether suffix pattern matching should be restricted to registered + * file extensions only. + */ public boolean useRegisteredSuffixPatternMatch() { return this.registeredSuffixPatternMatch; } @@ -617,10 +626,14 @@ public final class RequestMappingInfo implements RequestCondition<RequestMapping * Set the ContentNegotiationManager to use for the ProducesRequestCondition. * <p>By default this is not set. */ - public void setContentNegotiationManager(ContentNegotiationManager manager) { - this.contentNegotiationManager = manager; + public void setContentNegotiationManager(ContentNegotiationManager contentNegotiationManager) { + this.contentNegotiationManager = contentNegotiationManager; } + /** + * Return the ContentNegotiationManager to use for the ProducesRequestCondition, + * if any. + */ 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 5c18540a..2c57650d 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-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,10 +16,10 @@ package org.springframework.web.servlet.mvc.method; +import java.lang.reflect.Method; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; -import java.util.HashSet; import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; @@ -29,6 +29,8 @@ import java.util.Set; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; import org.springframework.http.InvalidMediaTypeException; import org.springframework.http.MediaType; import org.springframework.util.CollectionUtils; @@ -43,7 +45,6 @@ import org.springframework.web.method.HandlerMethod; import org.springframework.web.servlet.HandlerMapping; import org.springframework.web.servlet.handler.AbstractHandlerMethodMapping; import org.springframework.web.servlet.mvc.condition.NameValueExpression; -import org.springframework.web.servlet.mvc.condition.ParamsRequestCondition; import org.springframework.web.util.WebUtils; /** @@ -56,6 +57,19 @@ import org.springframework.web.util.WebUtils; */ public abstract class RequestMappingInfoHandlerMapping extends AbstractHandlerMethodMapping<RequestMappingInfo> { + private static final Method HTTP_OPTIONS_HANDLE_METHOD; + + static { + try { + HTTP_OPTIONS_HANDLE_METHOD = HttpOptionsHandler.class.getMethod("handle"); + } + catch (NoSuchMethodException ex) { + // Should never happen + throw new IllegalStateException("Failed to retrieve internal handler method for HTTP OPTIONS", ex); + } + } + + protected RequestMappingInfoHandlerMapping() { setHandlerMethodMappingNamingStrategy(new RequestMappingInfoHandlerMethodMappingNamingStrategy()); } @@ -167,116 +181,284 @@ public abstract class RequestMappingInfoHandlerMapping extends AbstractHandlerMe } /** - * Iterate all RequestMappingInfos once again, look if any match by URL at - * least and raise exceptions accordingly. + * Iterate all RequestMappingInfo's once again, look if any match by URL at + * least and raise exceptions according to what doesn't match. * @throws HttpRequestMethodNotSupportedException if there are matches by URL * but not by HTTP method * @throws HttpMediaTypeNotAcceptableException if there are matches by URL * but not by consumable/producible media types */ @Override - protected HandlerMethod handleNoMatch(Set<RequestMappingInfo> requestMappingInfos, - String lookupPath, HttpServletRequest request) throws ServletException { + protected HandlerMethod handleNoMatch(Set<RequestMappingInfo> infos, String lookupPath, + HttpServletRequest request) throws ServletException { - Set<String> allowedMethods = new LinkedHashSet<String>(4); + PartialMatchHelper helper = new PartialMatchHelper(infos, request); - Set<RequestMappingInfo> patternMatches = new HashSet<RequestMappingInfo>(); - Set<RequestMappingInfo> patternAndMethodMatches = new HashSet<RequestMappingInfo>(); + if (helper.isEmpty()) { + return null; + } + + if (helper.hasMethodsMismatch()) { + Set<String> methods = helper.getAllowedMethods(); + if (HttpMethod.OPTIONS.matches(request.getMethod())) { + HttpOptionsHandler handler = new HttpOptionsHandler(methods); + return new HandlerMethod(handler, HTTP_OPTIONS_HANDLE_METHOD); + } + throw new HttpRequestMethodNotSupportedException(request.getMethod(), methods); + } - for (RequestMappingInfo info : requestMappingInfos) { - if (info.getPatternsCondition().getMatchingCondition(request) != null) { - patternMatches.add(info); - if (info.getMethodsCondition().getMatchingCondition(request) != null) { - patternAndMethodMatches.add(info); + if (helper.hasConsumesMismatch()) { + Set<MediaType> mediaTypes = helper.getConsumableMediaTypes(); + MediaType contentType = null; + if (StringUtils.hasLength(request.getContentType())) { + try { + contentType = MediaType.parseMediaType(request.getContentType()); } - else { - for (RequestMethod method : info.getMethodsCondition().getMethods()) { - allowedMethods.add(method.name()); - } + catch (InvalidMediaTypeException ex) { + throw new HttpMediaTypeNotSupportedException(ex.getMessage()); } } + throw new HttpMediaTypeNotSupportedException(contentType, new ArrayList<MediaType>(mediaTypes)); } - if (patternMatches.isEmpty()) { - return null; + if (helper.hasProducesMismatch()) { + Set<MediaType> mediaTypes = helper.getProducibleMediaTypes(); + throw new HttpMediaTypeNotAcceptableException(new ArrayList<MediaType>(mediaTypes)); } - else if (patternAndMethodMatches.isEmpty() && !allowedMethods.isEmpty()) { - throw new HttpRequestMethodNotSupportedException(request.getMethod(), allowedMethods); + + if (helper.hasParamsMismatch()) { + List<String[]> conditions = helper.getParamConditions(); + throw new UnsatisfiedServletRequestParameterException(conditions, request.getParameterMap()); } - Set<MediaType> consumableMediaTypes; - Set<MediaType> producibleMediaTypes; - List<String[]> paramConditions; + return null; + } + + + /** + * Aggregate all partial matches and expose methods checking across them. + */ + private static class PartialMatchHelper { + + private final List<PartialMatch> partialMatches = new ArrayList<PartialMatch>(); - if (patternAndMethodMatches.isEmpty()) { - consumableMediaTypes = getConsumableMediaTypes(request, patternMatches); - producibleMediaTypes = getProducibleMediaTypes(request, patternMatches); - paramConditions = getRequestParams(request, patternMatches); + public PartialMatchHelper(Set<RequestMappingInfo> infos, HttpServletRequest request) { + for (RequestMappingInfo info : infos) { + if (info.getPatternsCondition().getMatchingCondition(request) != null) { + this.partialMatches.add(new PartialMatch(info, request)); + } + } } - else { - consumableMediaTypes = getConsumableMediaTypes(request, patternAndMethodMatches); - producibleMediaTypes = getProducibleMediaTypes(request, patternAndMethodMatches); - paramConditions = getRequestParams(request, patternAndMethodMatches); + + /** + * Whether there any partial matches. + */ + public boolean isEmpty() { + return this.partialMatches.isEmpty(); } - if (!consumableMediaTypes.isEmpty()) { - MediaType contentType = null; - if (StringUtils.hasLength(request.getContentType())) { - try { - contentType = MediaType.parseMediaType(request.getContentType()); + /** + * Any partial matches for "methods"? + */ + public boolean hasMethodsMismatch() { + for (PartialMatch match : this.partialMatches) { + if (match.hasMethodsMatch()) { + return false; } - catch (InvalidMediaTypeException ex) { - throw new HttpMediaTypeNotSupportedException(ex.getMessage()); + } + return true; + } + + /** + * Any partial matches for "methods" and "consumes"? + */ + public boolean hasConsumesMismatch() { + for (PartialMatch match : this.partialMatches) { + if (match.hasConsumesMatch()) { + return false; } } - throw new HttpMediaTypeNotSupportedException(contentType, new ArrayList<MediaType>(consumableMediaTypes)); + return true; } - else if (!producibleMediaTypes.isEmpty()) { - throw new HttpMediaTypeNotAcceptableException(new ArrayList<MediaType>(producibleMediaTypes)); + + /** + * Any partial matches for "methods", "consumes", and "produces"? + */ + public boolean hasProducesMismatch() { + for (PartialMatch match : this.partialMatches) { + if (match.hasProducesMatch()) { + return false; + } + } + return true; } - else if (!CollectionUtils.isEmpty(paramConditions)) { - throw new UnsatisfiedServletRequestParameterException(paramConditions, request.getParameterMap()); + + /** + * Any partial matches for "methods", "consumes", "produces", and "params"? + */ + public boolean hasParamsMismatch() { + for (PartialMatch match : this.partialMatches) { + if (match.hasParamsMatch()) { + return false; + } + } + return true; } - else { - return null; + + /** + * Return declared HTTP methods. + */ + public Set<String> getAllowedMethods() { + Set<String> result = new LinkedHashSet<String>(); + for (PartialMatch match : this.partialMatches) { + for (RequestMethod method : match.getInfo().getMethodsCondition().getMethods()) { + result.add(method.name()); + } + } + return result; } - } - private Set<MediaType> getConsumableMediaTypes(HttpServletRequest request, Set<RequestMappingInfo> partialMatches) { - Set<MediaType> result = new HashSet<MediaType>(); - for (RequestMappingInfo partialMatch : partialMatches) { - if (partialMatch.getConsumesCondition().getMatchingCondition(request) == null) { - result.addAll(partialMatch.getConsumesCondition().getConsumableMediaTypes()); + /** + * Return declared "consumable" types but only among those that also + * match the "methods" condition. + */ + public Set<MediaType> getConsumableMediaTypes() { + Set<MediaType> result = new LinkedHashSet<MediaType>(); + for (PartialMatch match : this.partialMatches) { + if (match.hasMethodsMatch()) { + result.addAll(match.getInfo().getConsumesCondition().getConsumableMediaTypes()); + } } + return result; } - return result; - } - private Set<MediaType> getProducibleMediaTypes(HttpServletRequest request, Set<RequestMappingInfo> partialMatches) { - Set<MediaType> result = new HashSet<MediaType>(); - for (RequestMappingInfo partialMatch : partialMatches) { - if (partialMatch.getProducesCondition().getMatchingCondition(request) == null) { - result.addAll(partialMatch.getProducesCondition().getProducibleMediaTypes()); + /** + * Return declared "producible" types but only among those that also + * match the "methods" and "consumes" conditions. + */ + public Set<MediaType> getProducibleMediaTypes() { + Set<MediaType> result = new LinkedHashSet<MediaType>(); + for (PartialMatch match : this.partialMatches) { + if (match.hasConsumesMatch()) { + result.addAll(match.getInfo().getProducesCondition().getProducibleMediaTypes()); + } + } + return result; + } + + /** + * Return declared "params" conditions but only among those that also + * match the "methods", "consumes", and "params" conditions. + */ + public List<String[]> getParamConditions() { + List<String[]> result = new ArrayList<String[]>(); + for (PartialMatch match : this.partialMatches) { + if (match.hasProducesMatch()) { + Set<NameValueExpression<String>> set = match.getInfo().getParamsCondition().getExpressions(); + if (!CollectionUtils.isEmpty(set)) { + int i = 0; + String[] array = new String[set.size()]; + for (NameValueExpression<String> expression : set) { + array[i++] = expression.toString(); + } + result.add(array); + } + } + } + return result; + } + + + /** + * Container for a RequestMappingInfo that matches the URL path at least. + */ + private static class PartialMatch { + + private final RequestMappingInfo info; + + private final boolean methodsMatch; + + private final boolean consumesMatch; + + private final boolean producesMatch; + + private final boolean paramsMatch; + + /** + * @param info RequestMappingInfo that matches the URL path. + * @param request the current request + */ + public PartialMatch(RequestMappingInfo info, HttpServletRequest request) { + this.info = info; + this.methodsMatch = (info.getMethodsCondition().getMatchingCondition(request) != null); + this.consumesMatch = (info.getConsumesCondition().getMatchingCondition(request) != null); + this.producesMatch = (info.getProducesCondition().getMatchingCondition(request) != null); + this.paramsMatch = (info.getParamsCondition().getMatchingCondition(request) != null); + } + + public RequestMappingInfo getInfo() { + return this.info; + } + + public boolean hasMethodsMatch() { + return this.methodsMatch; + } + + public boolean hasConsumesMatch() { + return (hasMethodsMatch() && this.consumesMatch); + } + + public boolean hasProducesMatch() { + return (hasConsumesMatch() && this.producesMatch); + } + + public boolean hasParamsMatch() { + return (hasProducesMatch() && this.paramsMatch); + } + + @Override + public String toString() { + return this.info.toString(); } } - return result; } - private List<String[]> getRequestParams(HttpServletRequest request, Set<RequestMappingInfo> partialMatches) { - List<String[]> result = new ArrayList<String[]>(); - for (RequestMappingInfo partialMatch : partialMatches) { - ParamsRequestCondition condition = partialMatch.getParamsCondition(); - 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(); + + /** + * Default handler for HTTP OPTIONS. + */ + private static class HttpOptionsHandler { + + private final HttpHeaders headers = new HttpHeaders(); + + public HttpOptionsHandler(Set<String> declaredMethods) { + this.headers.setAllow(initAllowedHttpMethods(declaredMethods)); + } + + private static Set<HttpMethod> initAllowedHttpMethods(Set<String> declaredMethods) { + Set<HttpMethod> result = new LinkedHashSet<HttpMethod>(declaredMethods.size()); + if (declaredMethods.isEmpty()) { + for (HttpMethod method : HttpMethod.values()) { + if (!HttpMethod.TRACE.equals(method)) { + result.add(method); + } + } + } + else { + boolean hasHead = declaredMethods.contains("HEAD"); + for (String method : declaredMethods) { + result.add(HttpMethod.valueOf(method)); + if (!hasHead && "GET".equals(method)) { + result.add(HttpMethod.HEAD); + } } - result.add(array); } + return result; + } + + public HttpHeaders handle() { + return this.headers; } - return result; } } 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 296caaf8..c0510172 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 @@ -44,7 +44,6 @@ import org.springframework.util.CollectionUtils; import org.springframework.util.StringUtils; import org.springframework.web.HttpMediaTypeNotAcceptableException; import org.springframework.web.accept.ContentNegotiationManager; -import org.springframework.web.accept.ContentNegotiationStrategy; import org.springframework.web.accept.PathExtensionContentNegotiationStrategy; import org.springframework.web.context.request.NativeWebRequest; import org.springframework.web.context.request.ServletWebRequest; @@ -123,12 +122,9 @@ public abstract class AbstractMessageConverterMethodProcessor extends AbstractMe } private static PathExtensionContentNegotiationStrategy initPathStrategy(ContentNegotiationManager manager) { - for (ContentNegotiationStrategy strategy : manager.getStrategies()) { - if (strategy instanceof PathExtensionContentNegotiationStrategy) { - return (PathExtensionContentNegotiationStrategy) strategy; - } - } - return new PathExtensionContentNegotiationStrategy(); + Class<PathExtensionContentNegotiationStrategy> clazz = PathExtensionContentNegotiationStrategy.class; + PathExtensionContentNegotiationStrategy strategy = manager.getStrategy(clazz); + return (strategy != null ? strategy : new PathExtensionContentNegotiationStrategy()); } @@ -169,13 +165,26 @@ public abstract class AbstractMessageConverterMethodProcessor extends AbstractMe ServletServerHttpRequest inputMessage, ServletServerHttpResponse outputMessage) throws IOException, HttpMediaTypeNotAcceptableException, HttpMessageNotWritableException { - Class<?> valueType = getReturnValueType(value, returnType); - Type declaredType = getGenericType(returnType); + Object outputValue; + Class<?> valueType; + Type declaredType; + + if (value instanceof CharSequence) { + outputValue = value.toString(); + valueType = String.class; + declaredType = String.class; + } + else { + outputValue = value; + valueType = getReturnValueType(outputValue, returnType); + declaredType = getGenericType(returnType); + } + HttpServletRequest request = inputMessage.getServletRequest(); List<MediaType> requestedMediaTypes = getAcceptableMediaTypes(request); List<MediaType> producibleMediaTypes = getProducibleMediaTypes(request, valueType, declaredType); - if (value != null && producibleMediaTypes.isEmpty()) { + if (outputValue != null && producibleMediaTypes.isEmpty()) { throw new IllegalArgumentException("No converter found for return value of type: " + valueType); } @@ -188,7 +197,7 @@ public abstract class AbstractMessageConverterMethodProcessor extends AbstractMe } } if (compatibleMediaTypes.isEmpty()) { - if (value != null) { + if (outputValue != null) { throw new HttpMediaTypeNotAcceptableException(producibleMediaTypes); } return; @@ -213,17 +222,17 @@ public abstract class AbstractMessageConverterMethodProcessor extends AbstractMe selectedMediaType = selectedMediaType.removeQualityValue(); for (HttpMessageConverter<?> messageConverter : this.messageConverters) { if (messageConverter instanceof GenericHttpMessageConverter) { - if (((GenericHttpMessageConverter<T>) messageConverter).canWrite( + if (((GenericHttpMessageConverter) messageConverter).canWrite( declaredType, valueType, selectedMediaType)) { - value = (T) getAdvice().beforeBodyWrite(value, returnType, selectedMediaType, + outputValue = (T) getAdvice().beforeBodyWrite(outputValue, returnType, selectedMediaType, (Class<? extends HttpMessageConverter<?>>) messageConverter.getClass(), inputMessage, outputMessage); - if (value != null) { + if (outputValue != null) { addContentDispositionHeader(inputMessage, outputMessage); - ((GenericHttpMessageConverter<T>) messageConverter).write( - value, declaredType, selectedMediaType, outputMessage); + ((GenericHttpMessageConverter) messageConverter).write( + outputValue, declaredType, selectedMediaType, outputMessage); if (logger.isDebugEnabled()) { - logger.debug("Written [" + value + "] as \"" + selectedMediaType + + logger.debug("Written [" + outputValue + "] as \"" + selectedMediaType + "\" using [" + messageConverter + "]"); } } @@ -231,14 +240,14 @@ public abstract class AbstractMessageConverterMethodProcessor extends AbstractMe } } else if (messageConverter.canWrite(valueType, selectedMediaType)) { - value = (T) getAdvice().beforeBodyWrite(value, returnType, selectedMediaType, + outputValue = (T) getAdvice().beforeBodyWrite(outputValue, returnType, selectedMediaType, (Class<? extends HttpMessageConverter<?>>) messageConverter.getClass(), inputMessage, outputMessage); - if (value != null) { + if (outputValue != null) { addContentDispositionHeader(inputMessage, outputMessage); - ((HttpMessageConverter<T>) messageConverter).write(value, selectedMediaType, outputMessage); + ((HttpMessageConverter) messageConverter).write(outputValue, selectedMediaType, outputMessage); if (logger.isDebugEnabled()) { - logger.debug("Written [" + value + "] as \"" + selectedMediaType + + logger.debug("Written [" + outputValue + "] as \"" + selectedMediaType + "\" using [" + messageConverter + "]"); } } @@ -247,7 +256,7 @@ public abstract class AbstractMessageConverterMethodProcessor extends AbstractMe } } - if (value != null) { + if (outputValue != null) { throw new HttpMediaTypeNotAcceptableException(this.allSupportedMediaTypes); } } 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 index 6543b76c..c3c7b1d0 100644 --- 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 @@ -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. @@ -34,7 +34,10 @@ import org.springframework.web.method.support.ModelAndViewContainer; * * @author Sebastien Deleuze * @since 4.2 + * @deprecated as of 4.3 {@link DeferredResultMethodReturnValueHandler} supports + * CompletionStage return values via an adapter mechanism. */ +@Deprecated @UsesJava8 public class CompletionStageReturnValueHandler implements AsyncHandlerMethodReturnValueHandler { diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/DeferredResultAdapter.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/DeferredResultAdapter.java new file mode 100644 index 00000000..a743b09e --- /dev/null +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/DeferredResultAdapter.java @@ -0,0 +1,36 @@ +/* + * 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.mvc.method.annotation; + +import org.springframework.web.context.request.async.DeferredResult; + +/** + * Contract to adapt a single-value async return value to {@code DeferredResult}. + * + * @author Rossen Stoyanchev + * @since 4.3 + */ +public interface DeferredResultAdapter { + + /** + * Create a {@code DeferredResult} for the given return value. + * @param returnValue the return value (never {@code null}) + * @return the DeferredResult + */ + DeferredResult<?> adaptToDeferredResult(Object returnValue); + +} 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 147e6aff..789606dc 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-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,17 @@ package org.springframework.web.servlet.mvc.method.annotation; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.CompletionStage; +import java.util.function.BiFunction; + import org.springframework.core.MethodParameter; +import org.springframework.lang.UsesJava8; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; +import org.springframework.util.concurrent.ListenableFuture; +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; @@ -24,21 +34,56 @@ import org.springframework.web.method.support.AsyncHandlerMethodReturnValueHandl import org.springframework.web.method.support.ModelAndViewContainer; /** - * Handles return values of type {@link DeferredResult}. + * Handler for return values of type {@link DeferredResult}, {@link ListenableFuture}, + * {@link CompletionStage} and any other async type with a {@link #getAdapterMap() + * registered adapter}. * * @author Rossen Stoyanchev * @since 3.2 */ public class DeferredResultMethodReturnValueHandler implements AsyncHandlerMethodReturnValueHandler { + private final Map<Class<?>, DeferredResultAdapter> adapterMap; + + + public DeferredResultMethodReturnValueHandler() { + this.adapterMap = new HashMap<Class<?>, DeferredResultAdapter>(5); + this.adapterMap.put(DeferredResult.class, new SimpleDeferredResultAdapter()); + this.adapterMap.put(ListenableFuture.class, new ListenableFutureAdapter()); + if (ClassUtils.isPresent("java.util.concurrent.CompletionStage", getClass().getClassLoader())) { + this.adapterMap.put(CompletionStage.class, new CompletionStageAdapter()); + } + } + + + /** + * Return the map with {@code DeferredResult} adapters. + * <p>By default the map contains adapters for {@code DeferredResult}, which + * simply downcasts, {@link ListenableFuture}, and {@link CompletionStage}. + * @return the map of adapters + */ + public Map<Class<?>, DeferredResultAdapter> getAdapterMap() { + return this.adapterMap; + } + + private DeferredResultAdapter getAdapterFor(Class<?> type) { + for (Class<?> adapteeType : getAdapterMap().keySet()) { + if (adapteeType.isAssignableFrom(type)) { + return getAdapterMap().get(adapteeType); + } + } + return null; + } + + @Override public boolean supportsReturnType(MethodParameter returnType) { - return DeferredResult.class.isAssignableFrom(returnType.getParameterType()); + return (getAdapterFor(returnType.getParameterType()) != null); } @Override public boolean isAsyncReturnValue(Object returnValue, MethodParameter returnType) { - return (returnValue != null && returnValue instanceof DeferredResult); + return (returnValue != null && (getAdapterFor(returnValue.getClass()) != null)); } @Override @@ -50,8 +95,76 @@ public class DeferredResultMethodReturnValueHandler implements AsyncHandlerMetho return; } - DeferredResult<?> deferredResult = (DeferredResult<?>) returnValue; - WebAsyncUtils.getAsyncManager(webRequest).startDeferredResultProcessing(deferredResult, mavContainer); + DeferredResultAdapter adapter = getAdapterFor(returnValue.getClass()); + Assert.notNull(adapter); + DeferredResult<?> result = adapter.adaptToDeferredResult(returnValue); + WebAsyncUtils.getAsyncManager(webRequest).startDeferredResultProcessing(result, mavContainer); + } + + + /** + * Adapter for {@code DeferredResult} return values. + */ + private static class SimpleDeferredResultAdapter implements DeferredResultAdapter { + + @Override + public DeferredResult<?> adaptToDeferredResult(Object returnValue) { + Assert.isInstanceOf(DeferredResult.class, returnValue); + return (DeferredResult<?>) returnValue; + } + } + + + /** + * Adapter for {@code ListenableFuture} return values. + */ + private static class ListenableFutureAdapter implements DeferredResultAdapter { + + @Override + public DeferredResult<?> adaptToDeferredResult(Object returnValue) { + Assert.isInstanceOf(ListenableFuture.class, returnValue); + final DeferredResult<Object> result = new DeferredResult<Object>(); + ((ListenableFuture<?>) returnValue).addCallback(new ListenableFutureCallback<Object>() { + @Override + public void onSuccess(Object value) { + result.setResult(value); + } + @Override + public void onFailure(Throwable ex) { + result.setErrorResult(ex); + } + }); + return result; + } + } + + + /** + * Adapter for {@code CompletionStage} return values. + */ + @UsesJava8 + private static class CompletionStageAdapter implements DeferredResultAdapter { + + @Override + public DeferredResult<?> adaptToDeferredResult(Object returnValue) { + Assert.isInstanceOf(CompletionStage.class, returnValue); + final DeferredResult<Object> result = new DeferredResult<Object>(); + @SuppressWarnings("unchecked") + CompletionStage<?> future = (CompletionStage<?>) returnValue; + future.handle(new BiFunction<Object, Throwable, Object>() { + @Override + public Object apply(Object value, Throwable ex) { + if (ex != null) { + result.setErrorResult(ex); + } + else { + result.setResult(value); + } + return null; + } + }); + return result; + } } } 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 572f0b7c..8e554773 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 @@ -65,6 +65,7 @@ import org.springframework.web.servlet.handler.AbstractHandlerMethodExceptionRes * {@link #setArgumentResolvers} and {@link #setReturnValueHandlers(List)}. * * @author Rossen Stoyanchev + * @author Juergen Hoeller * @since 3.1 */ public class ExceptionHandlerExceptionResolver extends AbstractHandlerMethodExceptionResolver @@ -294,6 +295,10 @@ public class ExceptionHandlerExceptionResolver extends AbstractHandlerMethodExce protected List<HandlerMethodArgumentResolver> getDefaultArgumentResolvers() { List<HandlerMethodArgumentResolver> resolvers = new ArrayList<HandlerMethodArgumentResolver>(); + // Annotation-based argument resolution + resolvers.add(new SessionAttributeMethodArgumentResolver()); + resolvers.add(new RequestAttributeMethodArgumentResolver()); + // Type-based argument resolution resolvers.add(new ServletRequestMethodArgumentResolver()); resolvers.add(new ServletResponseMethodArgumentResolver()); @@ -364,7 +369,15 @@ public class ExceptionHandlerExceptionResolver extends AbstractHandlerMethodExce if (logger.isDebugEnabled()) { logger.debug("Invoking @ExceptionHandler method: " + exceptionHandlerMethod); } - exceptionHandlerMethod.invokeAndHandle(webRequest, mavContainer, exception, handlerMethod); + Throwable cause = exception.getCause(); + if (cause != null) { + // Expose cause as provided argument as well + exceptionHandlerMethod.invokeAndHandle(webRequest, mavContainer, exception, cause, handlerMethod); + } + else { + // Otherwise, just the given exception as-is + exceptionHandlerMethod.invokeAndHandle(webRequest, mavContainer, exception, handlerMethod); + } } catch (Exception invocationEx) { if (logger.isDebugEnabled()) { 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 7741b1c4..6545d4a8 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 @@ -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,7 +19,10 @@ package org.springframework.web.servlet.mvc.method.annotation; import java.io.IOException; import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.Collections; import java.util.List; +import java.util.Map; import org.springframework.core.MethodParameter; import org.springframework.core.ResolvableType; @@ -162,15 +165,27 @@ public class HttpEntityMethodProcessor extends AbstractMessageConverterMethodPro Assert.isInstanceOf(HttpEntity.class, returnValue); HttpEntity<?> responseEntity = (HttpEntity<?>) returnValue; + HttpHeaders outputHeaders = outputMessage.getHeaders(); HttpHeaders entityHeaders = responseEntity.getHeaders(); + if (outputHeaders.containsKey(HttpHeaders.VARY) && entityHeaders.containsKey(HttpHeaders.VARY)) { + List<String> values = getVaryRequestHeadersToAdd(outputHeaders, entityHeaders); + if (!values.isEmpty()) { + outputHeaders.setVary(values); + } + } if (!entityHeaders.isEmpty()) { - outputMessage.getHeaders().putAll(entityHeaders); + for (Map.Entry<String, List<String>> entry : entityHeaders.entrySet()) { + if (!outputHeaders.containsKey(entry.getKey())) { + outputHeaders.put(entry.getKey(), entry.getValue()); + } + } } - Object body = responseEntity.getBody(); if (responseEntity instanceof ResponseEntity) { - outputMessage.setStatusCode(((ResponseEntity<?>) responseEntity).getStatusCode()); - if (HttpMethod.GET == inputMessage.getMethod() && isResourceNotModified(inputMessage, outputMessage)) { + outputMessage.getServletResponse().setStatus(((ResponseEntity<?>) responseEntity).getStatusCodeValue()); + HttpMethod method = inputMessage.getMethod(); + boolean isGetOrHead = (HttpMethod.GET == method || HttpMethod.HEAD == method); + if (isGetOrHead && isResourceNotModified(inputMessage, outputMessage)) { outputMessage.setStatusCode(HttpStatus.NOT_MODIFIED); // Ensure headers are flushed, no body should be written. outputMessage.flush(); @@ -180,12 +195,33 @@ public class HttpEntityMethodProcessor extends AbstractMessageConverterMethodPro } // Try even with null body. ResponseBodyAdvice could get involved. - writeWithMessageConverters(body, returnType, inputMessage, outputMessage); + writeWithMessageConverters(responseEntity.getBody(), returnType, inputMessage, outputMessage); // Ensure headers are flushed even if no body was written. outputMessage.flush(); } + private List<String> getVaryRequestHeadersToAdd(HttpHeaders responseHeaders, HttpHeaders entityHeaders) { + if (!responseHeaders.containsKey(HttpHeaders.VARY)) { + return entityHeaders.getVary(); + } + List<String> entityHeadersVary = entityHeaders.getVary(); + List<String> result = new ArrayList<String>(entityHeadersVary); + for (String header : responseHeaders.get(HttpHeaders.VARY)) { + for (String existing : StringUtils.tokenizeToStringArray(header, ",")) { + if ("*".equals(existing)) { + return Collections.emptyList(); + } + for (String value : entityHeadersVary) { + if (value.equalsIgnoreCase(existing)) { + result.remove(value); + } + } + } + } + return result; + } + private boolean isResourceNotModified(ServletServerHttpRequest inputMessage, ServletServerHttpResponse outputMessage) { List<String> ifNoneMatch = inputMessage.getHeaders().getIfNoneMatch(); long ifModifiedSince = inputMessage.getHeaders().getIfModifiedSince(); 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 3ed6bb8f..2cb56bc2 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 @@ -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. @@ -47,7 +47,7 @@ public class JsonViewResponseBodyAdvice extends AbstractMappingJacksonResponseBo @Override public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) { - return (super.supports(returnType, converterType) && returnType.getMethodAnnotation(JsonView.class) != null); + return super.supports(returnType, converterType) && returnType.hasMethodAnnotation(JsonView.class); } @Override 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 18d25a4f..d4a8fe62 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-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. @@ -31,7 +31,10 @@ import org.springframework.web.method.support.ModelAndViewContainer; * * @author Rossen Stoyanchev * @since 4.1 + * @deprecated as of 4.3 {@link DeferredResultMethodReturnValueHandler} supports + * ListenableFuture return values via an adapter mechanism. */ +@Deprecated public class ListenableFutureReturnValueHandler implements AsyncHandlerMethodReturnValueHandler { @Override 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 59a8e987..212a8269 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-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. @@ -17,9 +17,11 @@ package org.springframework.web.servlet.mvc.method.annotation; import java.util.Collections; +import java.util.List; import java.util.Map; import org.springframework.core.MethodParameter; +import org.springframework.core.ResolvableType; import org.springframework.util.CollectionUtils; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; @@ -68,23 +70,37 @@ public class MatrixVariableMapMethodArgumentResolver implements HandlerMethodArg return Collections.emptyMap(); } + MultiValueMap<String, String> map = new LinkedMultiValueMap<String, String>(); String pathVariable = parameter.getParameterAnnotation(MatrixVariable.class).pathVar(); if (!pathVariable.equals(ValueConstants.DEFAULT_NONE)) { - MultiValueMap<String, String> map = matrixVariables.get(pathVariable); - return (map != null) ? map : Collections.emptyMap(); + MultiValueMap<String, String> mapForPathVariable = matrixVariables.get(pathVariable); + if (mapForPathVariable == null) { + return Collections.emptyMap(); + } + map.putAll(mapForPathVariable); } - - MultiValueMap<String, String> map = new LinkedMultiValueMap<String, String>(); - for (MultiValueMap<String, String> vars : matrixVariables.values()) { - for (String name : vars.keySet()) { - for (String value : vars.get(name)) { - map.add(name, value); + else { + for (MultiValueMap<String, String> vars : matrixVariables.values()) { + for (String name : vars.keySet()) { + for (String value : vars.get(name)) { + map.add(name, value); + } } } } - return map; + return (isSingleValueMap(parameter) ? map.toSingleValueMap() : map); + } + + private boolean isSingleValueMap(MethodParameter parameter) { + if (!MultiValueMap.class.isAssignableFrom(parameter.getParameterType())) { + ResolvableType[] genericTypes = ResolvableType.forMethodParameter(parameter).getGenerics(); + if (genericTypes.length == 2) { + return !List.class.isAssignableFrom(genericTypes[1].getRawClass()); + } + } + return false; } }
\ No newline at end of file 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 6e971176..1dd9c354 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 @@ -54,7 +54,7 @@ public class MatrixVariableMethodArgumentResolver extends AbstractNamedValueMeth if (!parameter.hasParameterAnnotation(MatrixVariable.class)) { return false; } - if (Map.class.isAssignableFrom(parameter.getParameterType())) { + if (Map.class.isAssignableFrom(parameter.nestedIfOptional().getNestedParameterType())) { String variableName = parameter.getParameterAnnotation(MatrixVariable.class).name(); return StringUtils.hasText(variableName); } @@ -90,7 +90,7 @@ public class MatrixVariableMethodArgumentResolver extends AbstractNamedValueMeth for (MultiValueMap<String, String> params : pathParameters.values()) { if (params.containsKey(name)) { if (found) { - String paramType = parameter.getParameterType().getName(); + String paramType = parameter.getNestedParameterType().getName(); throw new ServletRequestBindingException( "Found more than one match for URI path parameter '" + name + "' for parameter type [" + paramType + "]. Use 'pathVar' attribute to disambiguate."); @@ -115,7 +115,7 @@ public class MatrixVariableMethodArgumentResolver extends AbstractNamedValueMeth @Override protected void handleMissingValue(String name, MethodParameter parameter) throws ServletRequestBindingException { throw new ServletRequestBindingException("Missing matrix variable '" + name + - "' for method parameter of type " + parameter.getParameterType().getSimpleName()); + "' for method parameter of type " + parameter.getNestedParameterType().getSimpleName()); } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ModelAndViewMethodReturnValueHandler.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ModelAndViewMethodReturnValueHandler.java index a848a970..10ea4028 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ModelAndViewMethodReturnValueHandler.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ModelAndViewMethodReturnValueHandler.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. @@ -98,6 +98,7 @@ public class ModelAndViewMethodReturnValueHandler implements HandlerMethodReturn } } } + mavContainer.setStatus(mav.getStatus()); mavContainer.addAllAttributes(mav.getModel()); } 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 25018ff9..e9e302a5 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 @@ -38,8 +38,8 @@ import org.springframework.cglib.proxy.Enhancer; 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.MethodParameter; import org.springframework.core.ParameterNameDiscoverer; import org.springframework.core.annotation.AnnotatedElementUtils; import org.springframework.core.annotation.SynthesizingMethodParameter; @@ -118,7 +118,7 @@ public class MvcUriComponentsBuilder { * @see #fromMethodName(Class, String, Object...) * @see #fromMethodCall(Object) * @see #fromMappingName(String) - * @see #fromMethod(java.lang.reflect.Method, Object...) + * @see #fromMethod(Class, Method, Object...) */ protected MvcUriComponentsBuilder(UriComponentsBuilder baseUrl) { Assert.notNull(baseUrl, "'baseUrl' is required"); @@ -168,7 +168,7 @@ public class MvcUriComponentsBuilder { /** * Create a {@link UriComponentsBuilder} from the mapping of a controller * method and an array of method argument values. This method delegates - * to {@link #fromMethod(java.lang.reflect.Method, Object...)}. + * to {@link #fromMethod(Class, Method, Object...)}. * @param controllerType the controller * @param methodName the method name * @param args the argument values @@ -207,7 +207,7 @@ public class MvcUriComponentsBuilder { /** * Create a {@link UriComponentsBuilder} by invoking a "mock" controller method. * The controller method and the supplied argument values are then used to - * delegate to {@link #fromMethod(java.lang.reflect.Method, Object...)}. + * delegate to {@link #fromMethod(Class, Method, Object...)}. * <p>For example, given this controller: * <pre class="code"> * @RequestMapping("/people/{id}/addresses") @@ -304,7 +304,7 @@ public class MvcUriComponentsBuilder { * <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 + * @return a builder to prepare the URI String * @throws IllegalArgumentException if the mapping name is not found or * if there is no unique match * @since 4.1 @@ -321,7 +321,7 @@ public class MvcUriComponentsBuilder { * @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 + * @return a builder to prepare the URI String * @throws IllegalArgumentException if the mapping name is not found or * if there is no unique match * @since 4.2 @@ -361,7 +361,7 @@ public class MvcUriComponentsBuilder { } /** - * An alternative to {@link #fromMethod(java.lang.reflect.Method, Object...)} + * An alternative to {@link #fromMethod(Class, 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 @@ -557,8 +557,7 @@ public class MvcUriComponentsBuilder { * on the controller is invoked, the supplied argument values are remembered * and the result can then be used to create a {@code UriComponentsBuilder} * via {@link #fromMethodCall(Object)}. - * <p> - * Note that this is a shorthand version of {@link #controller(Class)} intended + * <p>Note that this is a shorthand version of {@link #controller(Class)} intended * for inline use (with a static import), for example: * <pre class="code"> * MvcUriComponentsBuilder.fromMethodCall(on(FooController.class).getFoo(1)).build(); @@ -574,8 +573,7 @@ public class MvcUriComponentsBuilder { * on the controller is invoked, the supplied argument values are remembered * and the result can then be used to create {@code UriComponentsBuilder} via * {@link #fromMethodCall(Object)}. - * <p> - * This is a longer version of {@link #on(Class)}. It is needed with controller + * <p>This is a longer version of {@link #on(Class)}. It is needed with controller * methods returning void as well for repeated invocations. * <pre class="code"> * FooController fooController = controller(FooController.class); @@ -627,7 +625,7 @@ public class MvcUriComponentsBuilder { try { proxy = proxyClass.newInstance(); } - catch (Exception ex) { + catch (Throwable ex) { throw new IllegalStateException("Unable to instantiate controller proxy using Objenesis, " + "and regular controller instantiation via default constructor fails as well", ex); } @@ -778,7 +776,6 @@ public class MvcUriComponentsBuilder { } /** - * @see #MethodArgumentBuilder(Class, Method) * @deprecated as of 4.2, this is deprecated in favor of alternative constructors * that accept a controllerType argument */ 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 0475e03b..d7b1cb52 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 @@ -74,7 +74,7 @@ public class PathVariableMethodArgumentResolver extends AbstractNamedValueMethod if (!parameter.hasParameterAnnotation(PathVariable.class)) { return false; } - if (Map.class.isAssignableFrom(parameter.getParameterType())) { + if (Map.class.isAssignableFrom(parameter.nestedIfOptional().getNestedParameterType())) { String paramName = parameter.getParameterAnnotation(PathVariable.class).value(); return StringUtils.hasText(paramName); } @@ -121,7 +121,7 @@ public class PathVariableMethodArgumentResolver extends AbstractNamedValueMethod public void contributeMethodArgument(MethodParameter parameter, Object value, UriComponentsBuilder builder, Map<String, Object> uriVariables, ConversionService conversionService) { - if (Map.class.isAssignableFrom(parameter.getParameterType())) { + if (Map.class.isAssignableFrom(parameter.getNestedParameterType())) { return; } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestAttributeMethodArgumentResolver.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestAttributeMethodArgumentResolver.java new file mode 100644 index 00000000..def67fde --- /dev/null +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestAttributeMethodArgumentResolver.java @@ -0,0 +1,59 @@ +/* + * 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.mvc.method.annotation; + +import javax.servlet.ServletException; + +import org.springframework.core.MethodParameter; +import org.springframework.web.bind.ServletRequestBindingException; +import org.springframework.web.bind.annotation.RequestAttribute; +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; + +/** + * Resolves method arguments annotated with an @{@link RequestAttribute}. + * + * @author Rossen Stoyanchev + * @since 4.3 + */ +public class RequestAttributeMethodArgumentResolver extends AbstractNamedValueMethodArgumentResolver { + + @Override + public boolean supportsParameter(MethodParameter parameter) { + return parameter.hasParameterAnnotation(RequestAttribute.class); + } + + @Override + protected NamedValueInfo createNamedValueInfo(MethodParameter parameter) { + RequestAttribute ann = parameter.getParameterAnnotation(RequestAttribute.class); + return new NamedValueInfo(ann.name(), ann.required(), ValueConstants.DEFAULT_NONE); + } + + @Override + protected Object resolveName(String name, MethodParameter parameter, NativeWebRequest request){ + return request.getAttribute(name, RequestAttributes.SCOPE_REQUEST); + } + + @Override + protected void handleMissingValue(String name, MethodParameter parameter) throws ServletException { + throw new ServletRequestBindingException("Missing request attribute '" + name + + "' of type " + parameter.getNestedParameterType().getSimpleName()); + } + +} 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 ce031bd5..a48efb1b 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 @@ -48,7 +48,6 @@ 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; @@ -117,10 +116,6 @@ 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; @@ -599,6 +594,8 @@ public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter resolvers.add(new RequestHeaderMapMethodArgumentResolver()); resolvers.add(new ServletCookieValueMethodArgumentResolver(getBeanFactory())); resolvers.add(new ExpressionValueMethodArgumentResolver(getBeanFactory())); + resolvers.add(new SessionAttributeMethodArgumentResolver()); + resolvers.add(new RequestAttributeMethodArgumentResolver()); // Type-based argument resolution resolvers.add(new ServletRequestMethodArgumentResolver()); @@ -638,6 +635,8 @@ public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter resolvers.add(new MatrixVariableMethodArgumentResolver()); resolvers.add(new MatrixVariableMapMethodArgumentResolver()); resolvers.add(new ExpressionValueMethodArgumentResolver(getBeanFactory())); + resolvers.add(new SessionAttributeMethodArgumentResolver()); + resolvers.add(new RequestAttributeMethodArgumentResolver()); // Type-based argument resolution resolvers.add(new ServletRequestMethodArgumentResolver()); @@ -673,10 +672,6 @@ public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter 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)); @@ -795,46 +790,50 @@ public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter HttpServletResponse response, HandlerMethod handlerMethod) throws Exception { ServletWebRequest webRequest = new ServletWebRequest(request, response); + try { + WebDataBinderFactory binderFactory = getDataBinderFactory(handlerMethod); + ModelFactory modelFactory = getModelFactory(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, invocableMethod); + mavContainer.setIgnoreDefaultModelOnRedirect(this.ignoreDefaultModelOnRedirect); + + AsyncWebRequest asyncWebRequest = WebAsyncUtils.createAsyncWebRequest(request, response); + asyncWebRequest.setTimeout(this.asyncRequestTimeout); + + WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request); + asyncManager.setTaskExecutor(this.taskExecutor); + asyncManager.setAsyncWebRequest(asyncWebRequest); + asyncManager.registerCallableInterceptors(this.callableInterceptors); + asyncManager.registerDeferredResultInterceptors(this.deferredResultInterceptors); + + if (asyncManager.hasConcurrentResult()) { + Object result = asyncManager.getConcurrentResult(); + mavContainer = (ModelAndViewContainer) asyncManager.getConcurrentResultContext()[0]; + asyncManager.clearConcurrentResult(); + if (logger.isDebugEnabled()) { + logger.debug("Found concurrent result value [" + result + "]"); + } + invocableMethod = invocableMethod.wrapConcurrentResult(result); + } - WebDataBinderFactory binderFactory = getDataBinderFactory(handlerMethod); - ModelFactory modelFactory = getModelFactory(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, invocableMethod); - mavContainer.setIgnoreDefaultModelOnRedirect(this.ignoreDefaultModelOnRedirect); - - AsyncWebRequest asyncWebRequest = WebAsyncUtils.createAsyncWebRequest(request, response); - asyncWebRequest.setTimeout(this.asyncRequestTimeout); - - WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request); - asyncManager.setTaskExecutor(this.taskExecutor); - asyncManager.setAsyncWebRequest(asyncWebRequest); - asyncManager.registerCallableInterceptors(this.callableInterceptors); - asyncManager.registerDeferredResultInterceptors(this.deferredResultInterceptors); - - if (asyncManager.hasConcurrentResult()) { - Object result = asyncManager.getConcurrentResult(); - mavContainer = (ModelAndViewContainer) asyncManager.getConcurrentResultContext()[0]; - asyncManager.clearConcurrentResult(); - if (logger.isDebugEnabled()) { - logger.debug("Found concurrent result value [" + result + "]"); + invocableMethod.invokeAndHandle(webRequest, mavContainer); + if (asyncManager.isConcurrentHandlingStarted()) { + return null; } - invocableMethod = invocableMethod.wrapConcurrentResult(result); - } - invocableMethod.invokeAndHandle(webRequest, mavContainer); - if (asyncManager.isConcurrentHandlingStarted()) { - return null; + return getModelAndView(mavContainer, modelFactory, webRequest); + } + finally { + webRequest.requestCompleted(); } - - return getModelAndView(mavContainer, modelFactory, webRequest); } /** @@ -934,7 +933,7 @@ public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter return null; } ModelMap model = mavContainer.getModel(); - ModelAndView mav = new ModelAndView(mavContainer.getViewName(), model); + ModelAndView mav = new ModelAndView(mavContainer.getViewName(), model, mavContainer.getStatus()); if (!mavContainer.isViewReference()) { mav.setView((View) mavContainer.getView()); } 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 5ffb6aaf..b4abdc16 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 @@ -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. @@ -20,10 +20,11 @@ import java.lang.reflect.AnnotatedElement; import java.lang.reflect.Method; import java.util.Arrays; import java.util.List; +import java.util.Set; +import javax.servlet.http.HttpServletRequest; 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; @@ -34,6 +35,8 @@ 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.handler.MatchableHandlerMapping; +import org.springframework.web.servlet.handler.RequestMatchResult; import org.springframework.web.servlet.mvc.condition.AbstractRequestCondition; import org.springframework.web.servlet.mvc.condition.CompositeRequestCondition; import org.springframework.web.servlet.mvc.condition.RequestCondition; @@ -51,7 +54,7 @@ import org.springframework.web.servlet.mvc.method.RequestMappingInfoHandlerMappi * @since 3.1 */ public class RequestMappingHandlerMapping extends RequestMappingInfoHandlerMapping - implements EmbeddedValueResolverAware { + implements MatchableHandlerMapping, EmbeddedValueResolverAware { private boolean useSuffixPatternMatch = true; @@ -109,13 +112,13 @@ public class RequestMappingHandlerMapping extends RequestMappingInfoHandlerMappi @Override public void setEmbeddedValueResolver(StringValueResolver resolver) { - this.embeddedValueResolver = resolver; + this.embeddedValueResolver = resolver; } @Override public void afterPropertiesSet() { this.config = new RequestMappingInfo.BuilderConfiguration(); - this.config.setPathHelper(getUrlPathHelper()); + this.config.setUrlPathHelper(getUrlPathHelper()); this.config.setPathMatcher(getPathMatcher()); this.config.setSuffixPatternMatch(this.useSuffixPatternMatch); this.config.setTrailingSlashMatch(this.useTrailingSlashMatch); @@ -168,8 +171,8 @@ public class RequestMappingHandlerMapping extends RequestMappingInfoHandlerMappi */ @Override protected boolean isHandler(Class<?> beanType) { - return ((AnnotationUtils.findAnnotation(beanType, Controller.class) != null) || - (AnnotationUtils.findAnnotation(beanType, RequestMapping.class) != null)); + return (AnnotatedElementUtils.hasAnnotation(beanType, Controller.class) || + AnnotatedElementUtils.hasAnnotation(beanType, RequestMapping.class)); } /** @@ -276,6 +279,18 @@ public class RequestMappingHandlerMapping extends RequestMappingInfoHandlerMappi } @Override + public RequestMatchResult match(HttpServletRequest request, String pattern) { + RequestMappingInfo info = RequestMappingInfo.paths(pattern).options(this.config).build(); + RequestMappingInfo matchingInfo = info.getMatchingCondition(request); + if (matchingInfo == null) { + return null; + } + Set<String> patterns = matchingInfo.getPatternsCondition().getPatterns(); + String lookupPath = getUrlPathHelper().getLookupPathForRequest(request); + return new RequestMatchResult(patterns.iterator().next(), lookupPath, getPathMatcher()); + } + + @Override protected CorsConfiguration initCorsConfiguration(Object handler, Method method, RequestMappingInfo mappingInfo) { HandlerMethod handlerMethod = createHandlerMethod(handler, method); CrossOrigin typeAnnotation = AnnotatedElementUtils.findMergedAnnotation(handlerMethod.getBeanType(), CrossOrigin.class); @@ -314,19 +329,19 @@ public class RequestMappingHandlerMapping extends RequestMappingInfoHandlerMappi return; } for (String origin : annotation.origins()) { - config.addAllowedOrigin(origin); + config.addAllowedOrigin(resolveCorsAnnotationValue(origin)); } for (RequestMethod method : annotation.methods()) { config.addAllowedMethod(method.name()); } for (String header : annotation.allowedHeaders()) { - config.addAllowedHeader(header); + config.addAllowedHeader(resolveCorsAnnotationValue(header)); } for (String header : annotation.exposedHeaders()) { - config.addExposedHeader(header); + config.addExposedHeader(resolveCorsAnnotationValue(header)); } - String allowCredentials = annotation.allowCredentials(); + String allowCredentials = resolveCorsAnnotationValue(annotation.allowCredentials()); if ("true".equalsIgnoreCase(allowCredentials)) { config.setAllowCredentials(true); } @@ -334,8 +349,8 @@ public class RequestMappingHandlerMapping extends RequestMappingInfoHandlerMappi 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 + "]."); + 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) { @@ -343,4 +358,8 @@ public class RequestMappingHandlerMapping extends RequestMappingInfoHandlerMappi } } + private String resolveCorsAnnotationValue(String value) { + return (this.embeddedValueResolver != null ? this.embeddedValueResolver.resolveStringValue(value) : value); + } + } 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 ba34c2e4..d78d77de 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 @@ -16,19 +16,15 @@ 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; -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; import org.springframework.web.bind.WebDataBinder; @@ -40,12 +36,10 @@ import org.springframework.web.context.request.NativeWebRequest; import org.springframework.web.method.support.ModelAndViewContainer; import org.springframework.web.multipart.MultipartException; import org.springframework.web.multipart.MultipartFile; -import org.springframework.web.multipart.MultipartHttpServletRequest; import org.springframework.web.multipart.MultipartResolver; import org.springframework.web.multipart.support.MissingServletRequestPartException; +import org.springframework.web.multipart.support.MultipartResolutionDelegate; import org.springframework.web.multipart.support.RequestPartServletServerHttpRequest; -import org.springframework.web.servlet.mvc.support.DefaultHandlerExceptionResolver; -import org.springframework.web.util.WebUtils; /** * Resolves the following method arguments: @@ -64,12 +58,13 @@ import org.springframework.web.util.WebUtils; * it is derived from the name of the method argument. * * <p>Automatic validation may be applied if the argument is annotated with - * {@code @javax.validation.Valid}. In case of validation failure, a - * {@link MethodArgumentNotValidException} is raised and a 400 response status - * code returned if {@link DefaultHandlerExceptionResolver} is configured. + * {@code @javax.validation.Valid}. In case of validation failure, a {@link MethodArgumentNotValidException} + * is raised and a 400 response status code returned if + * {@link org.springframework.web.servlet.mvc.support.DefaultHandlerExceptionResolver} is configured. * * @author Rossen Stoyanchev * @author Brian Clozel + * @author Juergen Hoeller * @since 3.1 */ public class RequestPartMethodArgumentResolver extends AbstractMessageConverterMethodArgumentResolver { @@ -106,18 +101,10 @@ public class RequestPartMethodArgumentResolver extends AbstractMessageConverterM return true; } else { - if (parameter.hasParameterAnnotation(RequestParam.class)){ - return false; - } - else if (MultipartFile.class == parameter.getParameterType()) { - return true; - } - else if ("javax.servlet.http.Part".equals(parameter.getParameterType().getName())) { - return true; - } - else { + if (parameter.hasParameterAnnotation(RequestParam.class)) { return false; } + return MultipartResolutionDelegate.isMultipartArgument(parameter.nestedIfOptional()); } } @@ -126,87 +113,58 @@ public class RequestPartMethodArgumentResolver extends AbstractMessageConverterM NativeWebRequest request, WebDataBinderFactory binderFactory) throws Exception { 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(); - } + RequestPart requestPart = parameter.getParameterAnnotation(RequestPart.class); + boolean isRequired = ((requestPart == null || requestPart.required()) && !parameter.isOptional()); - Object arg; + String name = getPartName(parameter, requestPart); + parameter = parameter.nestedIfOptional(); + Object arg = null; - if (MultipartFile.class == paramType) { - Assert.notNull(multipartRequest, "Expected MultipartHttpServletRequest: is a MultipartResolver configured?"); - arg = multipartRequest.getFile(partName); - } - else if (isMultipartFileCollection(parameter)) { - Assert.notNull(multipartRequest, "Expected MultipartHttpServletRequest: is a MultipartResolver configured?"); - arg = multipartRequest.getFiles(partName); - } - else if (isMultipartFileArray(parameter)) { - Assert.notNull(multipartRequest, "Expected MultipartHttpServletRequest: is a MultipartResolver configured?"); - List<MultipartFile> files = multipartRequest.getFiles(partName); - arg = files.toArray(new MultipartFile[files.size()]); - } - else if ("javax.servlet.http.Part".equals(paramType.getName())) { - assertIsMultipartRequest(servletRequest); - arg = servletRequest.getPart(partName); - } - else if (isPartCollection(parameter)) { - assertIsMultipartRequest(servletRequest); - arg = new ArrayList<Object>(servletRequest.getParts()); - } - else if (isPartArray(parameter)) { - assertIsMultipartRequest(servletRequest); - arg = RequestPartResolver.resolvePart(servletRequest); + Object mpArg = MultipartResolutionDelegate.resolveMultipartArgument(name, parameter, servletRequest); + if (mpArg != MultipartResolutionDelegate.UNRESOLVABLE) { + arg = mpArg; } else { try { - HttpInputMessage inputMessage = new RequestPartServletServerHttpRequest(servletRequest, partName); + HttpInputMessage inputMessage = new RequestPartServletServerHttpRequest(servletRequest, name); arg = readWithMessageConverters(inputMessage, parameter, parameter.getNestedGenericParameterType()); - WebDataBinder binder = binderFactory.createBinder(request, arg, partName); + WebDataBinder binder = binderFactory.createBinder(request, arg, name); if (arg != null) { validateIfApplicable(binder, parameter); if (binder.getBindingResult().hasErrors() && isBindExceptionRequired(binder, parameter)) { throw new MethodArgumentNotValidException(parameter, binder.getBindingResult()); } } - mavContainer.addAttribute(BindingResult.MODEL_KEY_PREFIX + partName, binder.getBindingResult()); + mavContainer.addAttribute(BindingResult.MODEL_KEY_PREFIX + name, binder.getBindingResult()); } catch (MissingServletRequestPartException ex) { - // handled below - arg = null; + if (isRequired) { + throw ex; + } + } + catch (MultipartException ex) { + if (isRequired) { + throw ex; + } } } - RequestPart requestPart = parameter.getParameterAnnotation(RequestPart.class); - boolean isRequired = ((requestPart == null || requestPart.required()) && !optional); - if (arg == null && isRequired) { - throw new MissingServletRequestPartException(partName); + if (!MultipartResolutionDelegate.isMultipartRequest(servletRequest)) { + throw new MultipartException("Current request is not a multipart request"); + } + else { + throw new MissingServletRequestPartException(name); + } } - if (optional) { + if (parameter.isOptional()) { arg = OptionalResolver.resolveValue(arg); } return arg; } - private static void assertIsMultipartRequest(HttpServletRequest request) { - String contentType = request.getContentType(); - if (contentType == null || !contentType.toLowerCase().startsWith("multipart/")) { - throw new MultipartException("The current request is not a multipart request"); - } - } - - private String getPartName(MethodParameter methodParam) { - RequestPart requestPart = methodParam.getParameterAnnotation(RequestPart.class); + private String getPartName(MethodParameter methodParam, RequestPart requestPart) { String partName = (requestPart != null ? requestPart.name() : ""); if (partName.length() == 0) { partName = methodParam.getParameterName(); @@ -219,49 +177,6 @@ public class RequestPartMethodArgumentResolver extends AbstractMessageConverterM return partName; } - private boolean isMultipartFileCollection(MethodParameter methodParam) { - Class<?> collectionType = getCollectionParameterType(methodParam); - return MultipartFile.class == collectionType; - } - - private boolean isMultipartFileArray(MethodParameter methodParam) { - Class<?> paramType = methodParam.getNestedParameterType().getComponentType(); - return MultipartFile.class == paramType; - } - - private boolean isPartCollection(MethodParameter methodParam) { - Class<?> collectionType = getCollectionParameterType(methodParam); - return (collectionType != null && "javax.servlet.http.Part".equals(collectionType.getName())); - } - - private boolean isPartArray(MethodParameter methodParam) { - Class<?> paramType = methodParam.getNestedParameterType().getComponentType(); - return (paramType != null && "javax.servlet.http.Part".equals(paramType.getName())); - } - - private Class<?> getCollectionParameterType(MethodParameter methodParam) { - Class<?> paramType = methodParam.getNestedParameterType(); - if (Collection.class == paramType || List.class.isAssignableFrom(paramType)){ - Class<?> valueType = GenericCollectionTypeResolver.getCollectionParameterType(methodParam); - if (valueType != null) { - return valueType; - } - } - return null; - } - - - /** - * Inner class to avoid hard-coded dependency on Servlet 3.0 Part type... - */ - private static class RequestPartResolver { - - public static Object resolvePart(HttpServletRequest servletRequest) throws Exception { - Collection<Part> parts = servletRequest.getParts(); - return parts.toArray(new Part[parts.size()]); - } - } - /** * Inner class to avoid hard-coded dependency on Java 8 Optional type... @@ -270,7 +185,11 @@ public class RequestPartMethodArgumentResolver extends AbstractMessageConverterM private static class OptionalResolver { public static Object resolveValue(Object value) { - return Optional.ofNullable(value); + if (value == null || (value instanceof Collection && ((Collection) value).isEmpty()) || + (value instanceof Object[] && ((Object[]) value).length == 0)) { + return Optional.empty(); + } + return Optional.of(value); } } 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 bc187962..e8de9e85 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 @@ -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,15 +19,17 @@ package org.springframework.web.servlet.mvc.method.annotation; import java.io.IOException; import java.lang.reflect.Type; import java.util.List; + import javax.servlet.http.HttpServletRequest; import org.springframework.core.Conventions; import org.springframework.core.MethodParameter; -import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.core.annotation.AnnotatedElementUtils; 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.http.server.ServletServerHttpResponse; import org.springframework.validation.BindingResult; import org.springframework.web.HttpMediaTypeNotAcceptableException; import org.springframework.web.HttpMediaTypeNotSupportedException; @@ -108,8 +110,8 @@ public class RequestResponseBodyMethodProcessor extends AbstractMessageConverter @Override public boolean supportsReturnType(MethodParameter returnType) { - return (AnnotationUtils.findAnnotation(returnType.getContainingClass(), ResponseBody.class) != null || - returnType.getMethodAnnotation(ResponseBody.class) != null); + return (AnnotatedElementUtils.hasAnnotation(returnType.getContainingClass(), ResponseBody.class) || + returnType.hasMethodAnnotation(ResponseBody.class)); } /** @@ -146,7 +148,7 @@ public class RequestResponseBodyMethodProcessor extends AbstractMessageConverter Object arg = readWithMessageConverters(inputMessage, methodParam, paramType); if (arg == null) { - if (methodParam.getParameterAnnotation(RequestBody.class).required()) { + if (checkRequired(methodParam)) { throw new HttpMessageNotReadableException("Required request body is missing: " + methodParam.getMethod().toGenericString()); } @@ -154,15 +156,21 @@ public class RequestResponseBodyMethodProcessor extends AbstractMessageConverter return arg; } + protected boolean checkRequired(MethodParameter methodParam) { + return methodParam.getParameterAnnotation(RequestBody.class).required(); + } + @Override public void handleReturnValue(Object returnValue, MethodParameter returnType, ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws IOException, HttpMediaTypeNotAcceptableException, HttpMessageNotWritableException { mavContainer.setRequestHandled(true); + ServletServerHttpRequest inputMessage = createInputMessage(webRequest); + ServletServerHttpResponse outputMessage = createOutputMessage(webRequest); // Try even with null return value. ResponseBodyAdvice could get involved. - writeWithMessageConverters(returnValue, returnType, webRequest); + writeWithMessageConverters(returnValue, returnType, inputMessage, outputMessage); } } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ResponseBodyEmitterAdapter.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ResponseBodyEmitterAdapter.java new file mode 100644 index 00000000..fc827cc2 --- /dev/null +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ResponseBodyEmitterAdapter.java @@ -0,0 +1,39 @@ +/* + * 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.mvc.method.annotation; + +import org.springframework.http.server.ServerHttpResponse; + +/** + * Contract to adapt streaming async types to {@code ResponseBodyEmitter}. + * + * @author Rossen Stoyanchev + * @since 4.3 + */ +public interface ResponseBodyEmitterAdapter { + + /** + * Obtain a {@code ResponseBodyEmitter} for the given return value. + * If the return is the body {@code ResponseEntity} then the given + * {@code ServerHttpResponse} contains its status and headers. + * @param returnValue the return value (never {@code null}) + * @param response the response + * @return the return value adapted to a {@code ResponseBodyEmitter} + */ + ResponseBodyEmitter adaptToEmitter(Object returnValue, ServerHttpResponse response); + +} 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 index 9a216058..264ca73c 100644 --- 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 @@ -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. @@ -18,8 +18,9 @@ package org.springframework.web.servlet.mvc.method.annotation; import java.io.IOException; import java.io.OutputStream; +import java.util.HashMap; import java.util.List; - +import java.util.Map; import javax.servlet.ServletRequest; import javax.servlet.http.HttpServletResponse; @@ -44,8 +45,9 @@ import org.springframework.web.method.support.AsyncHandlerMethodReturnValueHandl import org.springframework.web.method.support.ModelAndViewContainer; /** - * Supports return values of type {@link ResponseBodyEmitter} and also - * {@code ResponseEntity<ResponseBodyEmitter>}. + * Handler for return values of type {@link ResponseBodyEmitter} (and the + * {@code ResponseEntity<ResponseBodyEmitter>} sub-class) as well as any other + * async type with a {@link #getAdapterMap() registered adapter}. * * @author Rossen Stoyanchev * @since 4.2 @@ -54,36 +56,63 @@ public class ResponseBodyEmitterReturnValueHandler implements AsyncHandlerMethod private static final Log logger = LogFactory.getLog(ResponseBodyEmitterReturnValueHandler.class); + private final List<HttpMessageConverter<?>> messageConverters; + private final Map<Class<?>, ResponseBodyEmitterAdapter> adapterMap; + public ResponseBodyEmitterReturnValueHandler(List<HttpMessageConverter<?>> messageConverters) { Assert.notEmpty(messageConverters, "'messageConverters' must not be empty"); this.messageConverters = messageConverters; + this.adapterMap = new HashMap<Class<?>, ResponseBodyEmitterAdapter>(3); + this.adapterMap.put(ResponseBodyEmitter.class, new SimpleResponseBodyEmitterAdapter()); + } + + + /** + * Return the map with {@code ResponseBodyEmitter} adapters. + * By default the map contains a single adapter {@code ResponseBodyEmitter} + * that simply downcasts the return value. + * @return the map of adapters + */ + public Map<Class<?>, ResponseBodyEmitterAdapter> getAdapterMap() { + return this.adapterMap; + } + + private ResponseBodyEmitterAdapter getAdapterFor(Class<?> type) { + if (type != null) { + for (Class<?> adapteeType : getAdapterMap().keySet()) { + if (adapteeType.isAssignableFrom(type)) { + return getAdapterMap().get(adapteeType); + } + } + } + return null; } @Override public boolean supportsReturnType(MethodParameter returnType) { - if (ResponseBodyEmitter.class.isAssignableFrom(returnType.getParameterType())) { - return true; + Class<?> bodyType; + if (ResponseEntity.class.isAssignableFrom(returnType.getParameterType())) { + bodyType = ResolvableType.forMethodParameter(returnType).getGeneric(0).resolve(); } - else if (ResponseEntity.class.isAssignableFrom(returnType.getParameterType())) { - Class<?> bodyType = ResolvableType.forMethodParameter(returnType).getGeneric(0).resolve(); - return (bodyType != null && ResponseBodyEmitter.class.isAssignableFrom(bodyType)); + else { + bodyType = returnType.getParameterType(); } - return false; + return (getAdapterFor(bodyType) != null); } @Override public boolean isAsyncReturnValue(Object returnValue, MethodParameter returnType) { if (returnValue != null) { - if (returnValue instanceof ResponseBodyEmitter) { - return true; + Object adaptFrom = returnValue; + if (returnValue instanceof ResponseEntity) { + adaptFrom = ((ResponseEntity) returnValue).getBody(); } - else if (returnValue instanceof ResponseEntity) { - Object body = ((ResponseEntity) returnValue).getBody(); - return (body != null && body instanceof ResponseBodyEmitter); + if (adaptFrom != null) { + return (getAdapterFor(adaptFrom.getClass()) != null); } } return false; @@ -101,13 +130,14 @@ public class ResponseBodyEmitterReturnValueHandler implements AsyncHandlerMethod HttpServletResponse response = webRequest.getNativeResponse(HttpServletResponse.class); ServerHttpResponse outputMessage = new ServletServerHttpResponse(response); - if (ResponseEntity.class.isAssignableFrom(returnValue.getClass())) { + if (returnValue instanceof ResponseEntity) { ResponseEntity<?> responseEntity = (ResponseEntity<?>) returnValue; - outputMessage.setStatusCode(responseEntity.getStatusCode()); + response.setStatus(responseEntity.getStatusCodeValue()); outputMessage.getHeaders().putAll(responseEntity.getHeaders()); returnValue = responseEntity.getBody(); if (returnValue == null) { mavContainer.setRequestHandled(true); + outputMessage.flush(); return; } } @@ -115,8 +145,9 @@ public class ResponseBodyEmitterReturnValueHandler implements AsyncHandlerMethod ServletRequest request = webRequest.getNativeRequest(ServletRequest.class); ShallowEtagHeaderFilter.disableContentCaching(request); - Assert.isInstanceOf(ResponseBodyEmitter.class, returnValue); - ResponseBodyEmitter emitter = (ResponseBodyEmitter) returnValue; + ResponseBodyEmitterAdapter adapter = getAdapterFor(returnValue.getClass()); + Assert.notNull(adapter); + ResponseBodyEmitter emitter = adapter.adaptToEmitter(returnValue, outputMessage); emitter.extendResponse(outputMessage); // Commit the response and wrap to ignore further header changes @@ -133,6 +164,18 @@ public class ResponseBodyEmitterReturnValueHandler implements AsyncHandlerMethod /** + * Adapter for {@code ResponseBodyEmitter} return values. + */ + private static class SimpleResponseBodyEmitterAdapter implements ResponseBodyEmitterAdapter { + + @Override + public ResponseBodyEmitter adaptToEmitter(Object returnValue, ServerHttpResponse response) { + Assert.isInstanceOf(ResponseBodyEmitter.class, returnValue); + return (ResponseBodyEmitter) returnValue; + } + } + + /** * ResponseBodyEmitter.Handler that writes with HttpMessageConverter's. */ private class HttpMessageConvertingHandler implements ResponseBodyEmitter.Handler { 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 0be4cb9b..267f1959 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 @@ -46,7 +46,6 @@ import org.springframework.web.bind.annotation.ExceptionHandler; 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; /** @@ -99,8 +98,9 @@ public abstract class ResponseEntityExceptionHandler { * @param ex the target exception * @param request the current request */ + @SuppressWarnings("deprecation") @ExceptionHandler({ - NoSuchRequestHandlingMethodException.class, + org.springframework.web.servlet.mvc.multiaction.NoSuchRequestHandlingMethodException.class, HttpRequestMethodNotSupportedException.class, HttpMediaTypeNotSupportedException.class, HttpMediaTypeNotAcceptableException.class, @@ -118,9 +118,9 @@ public abstract class ResponseEntityExceptionHandler { }) public final ResponseEntity<Object> handleException(Exception ex, WebRequest request) { HttpHeaders headers = new HttpHeaders(); - if (ex instanceof NoSuchRequestHandlingMethodException) { + if (ex instanceof org.springframework.web.servlet.mvc.multiaction.NoSuchRequestHandlingMethodException) { HttpStatus status = HttpStatus.NOT_FOUND; - return handleNoSuchRequestHandlingMethod((NoSuchRequestHandlingMethodException) ex, headers, status, request); + return handleNoSuchRequestHandlingMethod((org.springframework.web.servlet.mvc.multiaction.NoSuchRequestHandlingMethodException) ex, headers, status, request); } else if (ex instanceof HttpRequestMethodNotSupportedException) { HttpStatus status = HttpStatus.METHOD_NOT_ALLOWED; @@ -213,8 +213,10 @@ public abstract class ResponseEntityExceptionHandler { * @param status the selected response status * @param request the current request * @return a {@code ResponseEntity} instance + * @deprecated as of 4.3, along with {@link org.springframework.web.servlet.mvc.multiaction.NoSuchRequestHandlingMethodException} */ - protected ResponseEntity<Object> handleNoSuchRequestHandlingMethod(NoSuchRequestHandlingMethodException ex, + @Deprecated + protected ResponseEntity<Object> handleNoSuchRequestHandlingMethod(org.springframework.web.servlet.mvc.multiaction.NoSuchRequestHandlingMethodException ex, HttpHeaders headers, HttpStatus status, WebRequest request) { pageNotFoundLogger.warn(ex.getMessage()); diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ServletCookieValueMethodArgumentResolver.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ServletCookieValueMethodArgumentResolver.java index 8c51d5d8..cf174ea1 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ServletCookieValueMethodArgumentResolver.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ServletCookieValueMethodArgumentResolver.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. @@ -52,7 +52,7 @@ public class ServletCookieValueMethodArgumentResolver extends AbstractCookieValu protected Object resolveName(String cookieName, MethodParameter parameter, NativeWebRequest webRequest) throws Exception { HttpServletRequest servletRequest = webRequest.getNativeRequest(HttpServletRequest.class); Cookie cookieValue = WebUtils.getCookie(servletRequest, cookieName); - if (Cookie.class.isAssignableFrom(parameter.getParameterType())) { + if (Cookie.class.isAssignableFrom(parameter.getNestedParameterType())) { return cookieValue; } else if (cookieValue != null) { 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 f843a581..d8c19445 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 @@ -24,6 +24,7 @@ import java.util.concurrent.Callable; import org.springframework.core.MethodParameter; import org.springframework.core.ResolvableType; +import org.springframework.core.annotation.AnnotatedElementUtils; import org.springframework.http.HttpStatus; import org.springframework.util.ClassUtils; import org.springframework.util.StringUtils; @@ -82,6 +83,9 @@ public class ServletInvocableHandlerMethod extends InvocableHandlerMethod { private void initResponseStatus() { ResponseStatus annotation = getMethodAnnotation(ResponseStatus.class); + if (annotation == null) { + annotation = AnnotatedElementUtils.findMergedAnnotation(getBeanType(), ResponseStatus.class); + } if (annotation != null) { this.responseStatus = annotation.code(); this.responseReason = annotation.reason(); @@ -238,6 +242,14 @@ public class ServletInvocableHandlerMethod extends InvocableHandlerMethod { public <A extends Annotation> A getMethodAnnotation(Class<A> annotationType) { return ServletInvocableHandlerMethod.this.getMethodAnnotation(annotationType); } + + /** + * Bridge to controller method-level annotations. + */ + @Override + public <A extends Annotation> boolean hasMethodAnnotation(Class<A> annotationType) { + return ServletInvocableHandlerMethod.this.hasMethodAnnotation(annotationType); + } } @@ -258,6 +270,12 @@ public class ServletInvocableHandlerMethod extends InvocableHandlerMethod { this.returnType = ResolvableType.forType(super.getGenericParameterType()).getGeneric(0); } + public ConcurrentResultMethodParameter(ConcurrentResultMethodParameter original) { + super(original); + this.returnValue = original.returnValue; + this.returnType = original.returnType; + } + @Override public Class<?> getParameterType() { if (this.returnValue != null) { @@ -273,6 +291,11 @@ public class ServletInvocableHandlerMethod extends InvocableHandlerMethod { public Type getGenericParameterType() { return this.returnType.getType(); } + + @Override + public ConcurrentResultMethodParameter clone() { + return new ConcurrentResultMethodParameter(this); + } } } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/SessionAttributeMethodArgumentResolver.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/SessionAttributeMethodArgumentResolver.java new file mode 100644 index 00000000..7c7d0811 --- /dev/null +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/SessionAttributeMethodArgumentResolver.java @@ -0,0 +1,59 @@ +/* + * 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.mvc.method.annotation; + +import javax.servlet.ServletException; + +import org.springframework.core.MethodParameter; +import org.springframework.web.bind.ServletRequestBindingException; +import org.springframework.web.bind.annotation.SessionAttribute; +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; + +/** + * Resolves method arguments annotated with an @{@link SessionAttribute}. + * + * @author Rossen Stoyanchev + * @since 4.3 + */ +public class SessionAttributeMethodArgumentResolver extends AbstractNamedValueMethodArgumentResolver { + + @Override + public boolean supportsParameter(MethodParameter parameter) { + return parameter.hasParameterAnnotation(SessionAttribute.class); + } + + @Override + protected NamedValueInfo createNamedValueInfo(MethodParameter parameter) { + SessionAttribute ann = parameter.getParameterAnnotation(SessionAttribute.class); + return new NamedValueInfo(ann.name(), ann.required(), ValueConstants.DEFAULT_NONE); + } + + @Override + protected Object resolveName(String name, MethodParameter parameter, NativeWebRequest request){ + return request.getAttribute(name, RequestAttributes.SCOPE_SESSION); + } + + @Override + protected void handleMissingValue(String name, MethodParameter parameter) throws ServletException { + throw new ServletRequestBindingException("Missing session attribute '" + name + + "' of type " + parameter.getNestedParameterType().getSimpleName()); + } + +} 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 index 4bc3267e..72b283f0 100644 --- 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 @@ -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. @@ -38,6 +38,8 @@ public class SseEmitter extends ResponseBodyEmitter { static final MediaType TEXT_PLAIN = new MediaType("text", "plain", Charset.forName("UTF-8")); + static final MediaType UTF8_TEXT_EVENTSTREAM = new MediaType("text", "event-stream", Charset.forName("UTF-8")); + /** * Create a new SseEmitter instance. @@ -65,7 +67,7 @@ public class SseEmitter extends ResponseBodyEmitter { HttpHeaders headers = outputMessage.getHeaders(); if (headers.getContentType() == null) { - headers.setContentType(new MediaType("text", "event-stream")); + headers.setContentType(UTF8_TEXT_EVENTSTREAM); } } 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 index 40852899..a3b64612 100644 --- 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 @@ -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. @@ -13,11 +13,11 @@ * 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; @@ -33,7 +33,6 @@ 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} @@ -68,14 +67,14 @@ public class StreamingResponseBodyReturnValueHandler implements HandlerMethodRet HttpServletResponse response = webRequest.getNativeResponse(HttpServletResponse.class); ServerHttpResponse outputMessage = new ServletServerHttpResponse(response); - if (ResponseEntity.class.isAssignableFrom(returnValue.getClass())) { + if (returnValue instanceof ResponseEntity) { ResponseEntity<?> responseEntity = (ResponseEntity<?>) returnValue; - outputMessage.setStatusCode(responseEntity.getStatusCode()); + response.setStatus(responseEntity.getStatusCodeValue()); outputMessage.getHeaders().putAll(responseEntity.getHeaders()); - returnValue = responseEntity.getBody(); if (returnValue == null) { mavContainer.setRequestHandled(true); + outputMessage.flush(); return; } } @@ -97,7 +96,6 @@ public class StreamingResponseBodyReturnValueHandler implements HandlerMethodRet private final StreamingResponseBody streamingBody; - public StreamingResponseBodyTask(OutputStream outputStream, StreamingResponseBody streamingBody) { this.outputStream = outputStream; this.streamingBody = streamingBody; diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/multiaction/AbstractUrlMethodNameResolver.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/multiaction/AbstractUrlMethodNameResolver.java index 3ee748d4..0cc4fd1d 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/multiaction/AbstractUrlMethodNameResolver.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/multiaction/AbstractUrlMethodNameResolver.java @@ -34,7 +34,9 @@ import org.springframework.web.util.UrlPathHelper; * * @author Juergen Hoeller * @since 14.01.2004 + * @deprecated as of 4.3, in favor of annotation-driven handler methods */ +@Deprecated public abstract class AbstractUrlMethodNameResolver implements MethodNameResolver { /** Logger available to subclasses */ diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/multiaction/InternalPathMethodNameResolver.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/multiaction/InternalPathMethodNameResolver.java index f81e806b..4f0845e2 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/multiaction/InternalPathMethodNameResolver.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/multiaction/InternalPathMethodNameResolver.java @@ -35,7 +35,9 @@ import org.springframework.web.util.WebUtils; * * @author Rod Johnson * @author Juergen Hoeller -*/ + * @deprecated as of 4.3, in favor of annotation-driven handler methods + */ +@Deprecated public class InternalPathMethodNameResolver extends AbstractUrlMethodNameResolver { private String prefix = ""; @@ -97,7 +99,7 @@ public class InternalPathMethodNameResolver extends AbstractUrlMethodNameResolve /** * Extract the handler method name from the given request URI. - * Delegates to {@code WebUtils.extractViewNameFromUrlPath(String)}. + * Delegates to {@code WebUtils.extractFilenameFromUrlPath(String)}. * @param uri the request URI (e.g. "/index.html") * @return the extracted URI filename (e.g. "index") * @see org.springframework.web.util.WebUtils#extractFilenameFromUrlPath diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/multiaction/MethodNameResolver.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/multiaction/MethodNameResolver.java index d67f3019..8d4751f8 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/multiaction/MethodNameResolver.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/multiaction/MethodNameResolver.java @@ -28,7 +28,9 @@ import javax.servlet.http.HttpServletRequest; * * @author Rod Johnson * @see MultiActionController#setMethodNameResolver + * @deprecated as of 4.3, in favor of annotation-driven handler methods */ +@Deprecated public interface MethodNameResolver { /** 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 188c9667..6ef9b7bf 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 @@ -127,7 +127,9 @@ import org.springframework.web.servlet.mvc.LastModified; * @see ParameterMethodNameResolver * @see org.springframework.web.servlet.mvc.LastModified#getLastModified * @see org.springframework.web.bind.ServletRequestDataBinder + * @deprecated as of 4.3, in favor of annotation-driven handler methods */ +@Deprecated public class MultiActionController extends AbstractController implements LastModified { /** Suffix for last-modified methods */ diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/multiaction/NoSuchRequestHandlingMethodException.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/multiaction/NoSuchRequestHandlingMethodException.java index 1038c800..7d7d547f 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/multiaction/NoSuchRequestHandlingMethodException.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/multiaction/NoSuchRequestHandlingMethodException.java @@ -30,7 +30,9 @@ import org.springframework.web.util.UrlPathHelper; * @author Rod Johnson * @author Juergen Hoeller * @see MethodNameResolver#getHandlerMethodName(javax.servlet.http.HttpServletRequest) + * @deprecated as of 4.3, in favor of annotation-driven handler methods */ +@Deprecated @SuppressWarnings("serial") public class NoSuchRequestHandlingMethodException extends ServletException { diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/multiaction/ParameterMethodNameResolver.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/multiaction/ParameterMethodNameResolver.java index ff1b4547..55971724 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/multiaction/ParameterMethodNameResolver.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/multiaction/ParameterMethodNameResolver.java @@ -79,7 +79,9 @@ import org.springframework.web.util.WebUtils; * @see #setMethodParamNames * @see #setLogicalMappings * @see #setDefaultMethodName + * @deprecated as of 4.3, in favor of annotation-driven handler methods */ +@Deprecated public class ParameterMethodNameResolver implements MethodNameResolver { /** diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/multiaction/PropertiesMethodNameResolver.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/multiaction/PropertiesMethodNameResolver.java index 0f458a9e..8bc14b06 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/multiaction/PropertiesMethodNameResolver.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/multiaction/PropertiesMethodNameResolver.java @@ -45,7 +45,9 @@ import org.springframework.util.PathMatcher; * @author Juergen Hoeller * @see java.util.Properties * @see org.springframework.util.AntPathMatcher + * @deprecated as of 4.3, in favor of annotation-driven handler methods */ +@Deprecated public class PropertiesMethodNameResolver extends AbstractUrlMethodNameResolver implements InitializingBean { diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/support/AbstractControllerUrlHandlerMapping.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/support/AbstractControllerUrlHandlerMapping.java index 7893bc59..5604ed32 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/support/AbstractControllerUrlHandlerMapping.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/support/AbstractControllerUrlHandlerMapping.java @@ -31,7 +31,9 @@ import org.springframework.web.servlet.handler.AbstractDetectingUrlHandlerMappin * @since 2.5.3 * @see ControllerClassNameHandlerMapping * @see ControllerBeanNameHandlerMapping + * @deprecated as of 4.3, in favor of annotation-driven handler methods */ +@Deprecated public abstract class AbstractControllerUrlHandlerMapping extends AbstractDetectingUrlHandlerMapping { private ControllerTypePredicate predicate = new AnnotationControllerTypePredicate(); diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/support/AnnotationControllerTypePredicate.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/support/AnnotationControllerTypePredicate.java index ef0d574d..d9c3cfae 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/support/AnnotationControllerTypePredicate.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/support/AnnotationControllerTypePredicate.java @@ -25,7 +25,9 @@ import org.springframework.stereotype.Controller; * * @author Juergen Hoeller * @since 2.5.3 + * @deprecated as of 4.3, in favor of annotation-driven handler methods */ +@Deprecated class AnnotationControllerTypePredicate extends ControllerTypePredicate { @Override diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/support/ControllerBeanNameHandlerMapping.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/support/ControllerBeanNameHandlerMapping.java index dc88ef32..90fa2159 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/support/ControllerBeanNameHandlerMapping.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/support/ControllerBeanNameHandlerMapping.java @@ -37,7 +37,9 @@ import org.springframework.util.StringUtils; * @since 2.5.3 * @see ControllerClassNameHandlerMapping * @see org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping + * @deprecated as of 4.3, in favor of annotation-driven handler methods */ +@Deprecated public class ControllerBeanNameHandlerMapping extends AbstractControllerUrlHandlerMapping { private String urlPrefix = ""; diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/support/ControllerClassNameHandlerMapping.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/support/ControllerClassNameHandlerMapping.java index 752554cc..b47545dd 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/support/ControllerClassNameHandlerMapping.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/support/ControllerClassNameHandlerMapping.java @@ -18,7 +18,6 @@ package org.springframework.web.servlet.mvc.support; import org.springframework.util.ClassUtils; import org.springframework.util.StringUtils; -import org.springframework.web.servlet.mvc.multiaction.MultiActionController; /** * Implementation of {@link org.springframework.web.servlet.HandlerMapping} that @@ -36,7 +35,7 @@ import org.springframework.web.servlet.mvc.multiaction.MultiActionController; * <li>{@code HomeController} -> {@code /home*}</li> * </ul> * - * <p>For {@link MultiActionController MultiActionControllers} and {@code @Controller} + * <p>For {@code MultiActionController MultiActionControllers} and {@code @Controller} * beans, a similar mapping is registered, except that all sub-paths are registered * using the trailing wildcard pattern {@code /*}. For example: * <ul> @@ -44,7 +43,7 @@ import org.springframework.web.servlet.mvc.multiaction.MultiActionController; * <li>{@code CatalogController} -> {@code /catalog}, {@code /catalog/*}</li> * </ul> * - * <p>For {@link MultiActionController} it is often useful to use + * <p>For {@code MultiActionController} it is often useful to use * this mapping strategy in conjunction with the * {@link org.springframework.web.servlet.mvc.multiaction.InternalPathMethodNameResolver}. * @@ -56,7 +55,9 @@ import org.springframework.web.servlet.mvc.multiaction.MultiActionController; * @since 2.0 * @see org.springframework.web.servlet.mvc.Controller * @see org.springframework.web.servlet.mvc.multiaction.MultiActionController + * @deprecated as of 4.3, in favor of annotation-driven handler methods */ +@Deprecated public class ControllerClassNameHandlerMapping extends AbstractControllerUrlHandlerMapping { /** diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/support/ControllerTypePredicate.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/support/ControllerTypePredicate.java index 33571f46..20f690d4 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/support/ControllerTypePredicate.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/support/ControllerTypePredicate.java @@ -17,22 +17,24 @@ package org.springframework.web.servlet.mvc.support; import org.springframework.web.servlet.mvc.Controller; -import org.springframework.web.servlet.mvc.multiaction.MultiActionController; /** * Internal helper class that identifies controller types. * * @author Juergen Hoeller * @since 2.5.3 + * @deprecated as of 4.3, in favor of annotation-driven handler methods */ +@Deprecated class ControllerTypePredicate { public boolean isControllerType(Class<?> beanClass) { return Controller.class.isAssignableFrom(beanClass); } + @SuppressWarnings("deprecation") public boolean isMultiActionControllerType(Class<?> beanClass) { - return MultiActionController.class.isAssignableFrom(beanClass); + return org.springframework.web.servlet.mvc.multiaction.MultiActionController.class.isAssignableFrom(beanClass); } } 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 77290e56..6dea3daa 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 @@ -49,7 +49,6 @@ import org.springframework.web.multipart.support.MissingServletRequestPartExcept import org.springframework.web.servlet.ModelAndView; import org.springframework.web.servlet.NoHandlerFoundException; import org.springframework.web.servlet.handler.AbstractHandlerExceptionResolver; -import org.springframework.web.servlet.mvc.multiaction.NoSuchRequestHandlingMethodException; /** * Default implementation of the {@link org.springframework.web.servlet.HandlerExceptionResolver @@ -101,13 +100,14 @@ public class DefaultHandlerExceptionResolver extends AbstractHandlerExceptionRes @Override + @SuppressWarnings("deprecation") protected ModelAndView doResolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { try { - if (ex instanceof NoSuchRequestHandlingMethodException) { - return handleNoSuchRequestHandlingMethod((NoSuchRequestHandlingMethodException) ex, request, response, - handler); + if (ex instanceof org.springframework.web.servlet.mvc.multiaction.NoSuchRequestHandlingMethodException) { + return handleNoSuchRequestHandlingMethod((org.springframework.web.servlet.mvc.multiaction.NoSuchRequestHandlingMethodException) ex, + request, response, handler); } else if (ex instanceof HttpRequestMethodNotSupportedException) { return handleHttpRequestMethodNotSupported((HttpRequestMethodNotSupportedException) ex, request, @@ -180,8 +180,10 @@ public class DefaultHandlerExceptionResolver extends AbstractHandlerExceptionRes * at the time of the exception (for example, if multipart resolution failed) * @return an empty ModelAndView indicating the exception was handled * @throws IOException potentially thrown from response.sendError() + * @deprecated as of 4.3, along with {@link org.springframework.web.servlet.mvc.multiaction.NoSuchRequestHandlingMethodException} */ - protected ModelAndView handleNoSuchRequestHandlingMethod(NoSuchRequestHandlingMethodException ex, + @Deprecated + protected ModelAndView handleNoSuchRequestHandlingMethod(org.springframework.web.servlet.mvc.multiaction.NoSuchRequestHandlingMethodException ex, HttpServletRequest request, HttpServletResponse response, Object handler) throws IOException { pageNotFoundLogger.warn(ex.getMessage()); diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/CssLinkResourceTransformer.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/CssLinkResourceTransformer.java index 4380246e..fa884f7a 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/CssLinkResourceTransformer.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/CssLinkResourceTransformer.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. @@ -49,12 +49,11 @@ import org.springframework.util.StringUtils; */ public class CssLinkResourceTransformer extends ResourceTransformerSupport { - private static final Log logger = LogFactory.getLog(CssLinkResourceTransformer.class); - private static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8"); + private static final Log logger = LogFactory.getLog(CssLinkResourceTransformer.class); - private final List<CssLinkParser> linkParsers = new ArrayList<CssLinkParser>(); + private final List<CssLinkParser> linkParsers = new ArrayList<CssLinkParser>(2); public CssLinkResourceTransformer() { @@ -81,7 +80,7 @@ public class CssLinkResourceTransformer extends ResourceTransformerSupport { byte[] bytes = FileCopyUtils.copyToByteArray(resource.getInputStream()); String content = new String(bytes, DEFAULT_CHARSET); - Set<CssLinkInfo> infos = new HashSet<CssLinkInfo>(5); + Set<CssLinkInfo> infos = new HashSet<CssLinkInfo>(8); for (CssLinkParser parser : this.linkParsers) { parser.parseLink(content, infos); } @@ -123,17 +122,16 @@ public class CssLinkResourceTransformer extends ResourceTransformerSupport { private boolean hasScheme(String link) { int schemeIndex = link.indexOf(":"); - return (schemeIndex > 0 && !link.substring(0, schemeIndex).contains("/")) - || link.indexOf("//") == 0; + return (schemeIndex > 0 && !link.substring(0, schemeIndex).contains("/")) || link.indexOf("//") == 0; } - protected static interface CssLinkParser { + protected interface CssLinkParser { void parseLink(String content, Set<CssLinkInfo> linkInfos); - } + protected static abstract class AbstractCssLinkParser implements CssLinkParser { /** @@ -189,6 +187,7 @@ public class CssLinkResourceTransformer extends ResourceTransformerSupport { } + private static class ImportStatementCssLinkParser extends AbstractCssLinkParser { @Override @@ -208,6 +207,7 @@ public class CssLinkResourceTransformer extends ResourceTransformerSupport { } } + private static class UrlFunctionCssLinkParser extends AbstractCssLinkParser { @Override @@ -229,8 +229,7 @@ public class CssLinkResourceTransformer extends ResourceTransformerSupport { private final int end; - - private CssLinkInfo(int start, int end) { + public CssLinkInfo(int start, int end) { this.start = start; this.end = end; } 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 fd25c306..cf9c020e 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 @@ -16,17 +16,13 @@ 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.ServletContext; import javax.servlet.ServletException; -import javax.servlet.ServletOutputStream; +import javax.servlet.ServletResponse; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; @@ -34,21 +30,27 @@ import org.apache.commons.logging.Log; 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.core.io.support.ResourceRegion; import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; import org.springframework.http.HttpRange; import org.springframework.http.MediaType; +import org.springframework.http.converter.ResourceHttpMessageConverter; +import org.springframework.http.converter.ResourceRegionHttpMessageConverter; import org.springframework.http.server.ServletServerHttpRequest; +import org.springframework.http.server.ServletServerHttpResponse; 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.HttpMediaTypeNotAcceptableException; import org.springframework.web.HttpRequestHandler; +import org.springframework.web.accept.ContentNegotiationManager; +import org.springframework.web.accept.ContentNegotiationManagerFactoryBean; +import org.springframework.web.accept.PathExtensionContentNegotiationStrategy; import org.springframework.web.context.request.ServletWebRequest; import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.cors.CorsConfigurationSource; @@ -56,49 +58,45 @@ import org.springframework.web.servlet.HandlerMapping; import org.springframework.web.servlet.support.WebContentGenerator; /** - * {@link HttpRequestHandler} that serves static resources optimized for superior browser performance - * (according to the guidelines of Page Speed, YSlow, etc.) by allowing for flexible cache settings - * ({@linkplain #setCacheSeconds "cacheSeconds" property}, last-modified support). + * {@code HttpRequestHandler} that serves static resources in an optimized way + * according to the guidelines of Page Speed, YSlow, etc. * - * <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 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. - * For example, resources could be served from a classpath location such as - * "classpath:/META-INF/public-web-resources/", allowing convenient packaging and serving of resources - * such as a JavaScript library from within jar files. + * <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. Resources could be served from a classpath location, + * e.g. "classpath:/META-INF/public-web-resources/", allowing convenient packaging + * and serving of resources such as .js, .css, and others in jar files. * - * <p>To ensure that users with a primed browser cache get the latest changes to application-specific - * resources upon deployment of new versions of the application, it is recommended that a version - * string is used in the URL mapping pattern that selects this handler. Such patterns can be easily - * parameterized using Spring EL. See the reference manual for further examples of this approach. + * <p>This request handler may also be configured with a + * {@link #setResourceResolvers(List) resourcesResolver} and + * {@link #setResourceTransformers(List) resourceTransformer} chains to support + * arbitrary resolution and transformation of resources being served. By default a + * {@link PathResourceResolver} simply finds resources based on the configured + * "locations". An application can configure additional resolvers and + * transformers such as the {@link VersionResourceResolver} which can resolve + * and prepare URLs for resources with a version in the URL. * - * <p>For various front-end needs — such as ensuring that users with a primed browser cache - * get the latest changes, or serving variations of resources (e.g., minified versions) — - * {@link org.springframework.web.servlet.resource.ResourceResolver}s can be configured. - * - * <p>This handler can be configured through use of a - * {@link org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry} - * or the {@code <mvc:resources/>} XML configuration element. + * <p>This 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. * * @author Keith Donald * @author Jeremy Grelle * @author Juergen Hoeller * @author Arjen Poutsma * @author Brian Clozel + * @author Rossen Stoyanchev * @since 3.0.4 */ public class ResourceHttpRequestHandler extends WebContentGenerator implements HttpRequestHandler, InitializingBean, CorsConfigurationSource { - private static final Log logger = LogFactory.getLog(ResourceHttpRequestHandler.class); + // Servlet 3.1 setContentLengthLong(long) available? + private static final boolean contentLengthLongAvailable = + ClassUtils.hasMethod(ServletResponse.class, "setContentLengthLong", long.class); - private static final boolean jafPresent = ClassUtils.isPresent( - "javax.activation.FileTypeMap", ResourceHttpRequestHandler.class.getClassLoader()); + private static final Log logger = LogFactory.getLog(ResourceHttpRequestHandler.class); private final List<Resource> locations = new ArrayList<Resource>(4); @@ -107,17 +105,24 @@ public class ResourceHttpRequestHandler extends WebContentGenerator private final List<ResourceTransformer> resourceTransformers = new ArrayList<ResourceTransformer>(4); + private ResourceHttpMessageConverter resourceHttpMessageConverter; + + private ResourceRegionHttpMessageConverter resourceRegionHttpMessageConverter; + + private ContentNegotiationManager contentNegotiationManager; + + private final ContentNegotiationManagerFactoryBean cnmFactoryBean = new ContentNegotiationManagerFactoryBean(); + private CorsConfiguration corsConfiguration; public ResourceHttpRequestHandler() { - super(METHOD_GET, METHOD_HEAD); - this.resourceResolvers.add(new PathResourceResolver()); + super(HttpMethod.GET.name(), HttpMethod.HEAD.name()); } /** - * Set a {@code List} of {@code Resource} paths to use as sources + * Set the {@code List} of {@code Resource} paths to use as sources * for serving static resources. */ public void setLocations(List<Resource> locations) { @@ -126,6 +131,10 @@ public class ResourceHttpRequestHandler extends WebContentGenerator this.locations.addAll(locations); } + /** + * Return the {@code List} of {@code Resource} paths to use as sources + * for serving static resources. + */ public List<Resource> getLocations() { return this.locations; } @@ -167,15 +176,87 @@ public class ResourceHttpRequestHandler extends WebContentGenerator return this.resourceTransformers; } + /** + * Configure the {@link ResourceHttpMessageConverter} to use. + * <p>By default a {@link ResourceHttpMessageConverter} will be configured. + * @since 4.3 + */ + public void setResourceHttpMessageConverter(ResourceHttpMessageConverter resourceHttpMessageConverter) { + this.resourceHttpMessageConverter = resourceHttpMessageConverter; + } + + /** + * Return the list of configured resource converters. + * @since 4.3 + */ + public ResourceHttpMessageConverter getResourceHttpMessageConverter() { + return this.resourceHttpMessageConverter; + } + + /** + * Configure the {@link ResourceRegionHttpMessageConverter} to use. + * <p>By default a {@link ResourceRegionHttpMessageConverter} will be configured. + * @since 4.3 + */ + public void setResourceRegionHttpMessageConverter(ResourceRegionHttpMessageConverter resourceRegionHttpMessageConverter) { + this.resourceRegionHttpMessageConverter = resourceRegionHttpMessageConverter; + } + + /** + * Return the list of configured resource region converters. + * @since 4.3 + */ + public ResourceRegionHttpMessageConverter getResourceRegionHttpMessageConverter() { + return this.resourceRegionHttpMessageConverter; + } + + /** + * Configure a {@code ContentNegotiationManager} to determine the media types + * for resources being served. If the manager contains a path + * extension strategy it will be used to look up the file extension + * of resources being served via + * {@link PathExtensionContentNegotiationStrategy#getMediaTypeForResource + * getMediaTypeForResource}. If that fails the check is then expanded + * to use any configured content negotiation strategy against the request. + * <p>By default a {@link ContentNegotiationManagerFactoryBean} with default + * settings is used to create the manager. See the Javadoc of + * {@code ContentNegotiationManagerFactoryBean} for details + * @param contentNegotiationManager the manager to use + * @since 4.3 + */ + public void setContentNegotiationManager(ContentNegotiationManager contentNegotiationManager) { + this.contentNegotiationManager = contentNegotiationManager; + } + + /** + * Return the specified content negotiation manager. + * @since 4.3 + */ + public ContentNegotiationManager getContentNegotiationManager() { + return this.contentNegotiationManager; + } + + /** + * Specify the CORS configuration for resources served by this handler. + * <p>By default this is not set in which allows cross-origin requests. + */ public void setCorsConfiguration(CorsConfiguration corsConfiguration) { this.corsConfiguration = corsConfiguration; } + /** + * Return the specified CORS configuration. + */ @Override public CorsConfiguration getCorsConfiguration(HttpServletRequest request) { return this.corsConfiguration; } + @Override + protected void initServletContext(ServletContext servletContext) { + this.cnmFactoryBean.setServletContext(servletContext); + } + @Override public void afterPropertiesSet() throws Exception { @@ -183,7 +264,20 @@ public class ResourceHttpRequestHandler extends WebContentGenerator logger.warn("Locations list is empty. No resources will be served unless a " + "custom ResourceResolver is configured as an alternative to PathResourceResolver."); } + if (this.resourceResolvers.isEmpty()) { + this.resourceResolvers.add(new PathResourceResolver()); + } initAllowedLocations(); + if (this.contentNegotiationManager == null) { + this.cnmFactoryBean.afterPropertiesSet(); + this.contentNegotiationManager = this.cnmFactoryBean.getObject(); + } + if (this.resourceHttpMessageConverter == null) { + this.resourceHttpMessageConverter = new ResourceHttpMessageConverter(); + } + if(this.resourceRegionHttpMessageConverter == null) { + this.resourceRegionHttpMessageConverter = new ResourceRegionHttpMessageConverter(); + } } /** @@ -206,7 +300,6 @@ public class ResourceHttpRequestHandler extends WebContentGenerator } } - /** * Processes a resource request. * <p>Checks for the existence of the requested resource in the configured list of locations. @@ -223,10 +316,7 @@ public class ResourceHttpRequestHandler extends WebContentGenerator public void handleRequest(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { - // Supported methods and required session - checkRequest(request); - - // Check whether a matching resource exists + // For very general mappings (e.g. "/") we need to check 404 first Resource resource = getResource(request); if (resource == null) { logger.trace("No matching resource found - returning 404"); @@ -234,6 +324,14 @@ public class ResourceHttpRequestHandler extends WebContentGenerator return; } + if (HttpMethod.OPTIONS.matches(request.getMethod())) { + response.setHeader("Allow", getAllowHeader()); + return; + } + + // Supported methods and required session + checkRequest(request); + // Header phase if (new ServletWebRequest(request, response).checkNotModified(resource.lastModified())) { logger.trace("Resource not modified - returning 304"); @@ -244,7 +342,7 @@ public class ResourceHttpRequestHandler extends WebContentGenerator prepareResponse(response); // Check the media type for the resource - MediaType mediaType = getMediaType(resource); + MediaType mediaType = getMediaType(request, resource); if (mediaType != null) { if (logger.isTraceEnabled()) { logger.trace("Determined media type '" + mediaType + "' for " + resource); @@ -263,12 +361,30 @@ public class ResourceHttpRequestHandler extends WebContentGenerator return; } + ServletServerHttpResponse outputMessage = new ServletServerHttpResponse(response); if (request.getHeader(HttpHeaders.RANGE) == null) { setHeaders(response, resource, mediaType); - writeContent(response, resource); + this.resourceHttpMessageConverter.write(resource, mediaType, outputMessage); } else { - writePartialContent(request, response, resource, mediaType); + response.setHeader(HttpHeaders.ACCEPT_RANGES, "bytes"); + ServletServerHttpRequest inputMessage = new ServletServerHttpRequest(request); + try { + List<HttpRange> httpRanges = inputMessage.getHeaders().getRange(); + response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT); + if(httpRanges.size() == 1) { + ResourceRegion resourceRegion = httpRanges.get(0).toResourceRegion(resource); + this.resourceRegionHttpMessageConverter.write(resourceRegion, mediaType, outputMessage); + } + else { + this.resourceRegionHttpMessageConverter.write( + HttpRange.toResourceRegions(httpRanges, resource), mediaType, outputMessage); + } + } + catch (IllegalArgumentException ex) { + response.setHeader("Content-Range", "bytes */" + resource.contentLength()); + response.sendError(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE); + } } } @@ -384,26 +500,59 @@ public class ResourceHttpRequestHandler extends WebContentGenerator } /** - * Determine an appropriate media type for the given resource. + * Determine the media type for the given request and the resource matched + * to it. This implementation first tries to determine the MediaType based + * strictly on the file extension of the Resource via + * {@link PathExtensionContentNegotiationStrategy#getMediaTypeForResource} + * and then expands to check against the request via + * {@link ContentNegotiationManager#resolveMediaTypes}. + * @param request the current request * @param resource the resource to check * @return the corresponding media type, or {@code null} if none found */ - protected MediaType getMediaType(Resource resource) { - MediaType mediaType = null; - String mimeType = getServletContext().getMimeType(resource.getFilename()); - if (StringUtils.hasText(mimeType)) { - mediaType = MediaType.parseMediaType(mimeType); - } - if (jafPresent && (mediaType == null || MediaType.APPLICATION_OCTET_STREAM.equals(mediaType))) { - MediaType jafMediaType = ActivationMediaTypeFactory.getMediaType(resource.getFilename()); - if (jafMediaType != null && !MediaType.APPLICATION_OCTET_STREAM.equals(jafMediaType)) { - mediaType = jafMediaType; + @SuppressWarnings("deprecation") + protected MediaType getMediaType(HttpServletRequest request, Resource resource) { + // For backwards compatibility + MediaType mediaType = getMediaType(resource); + if (mediaType != null) { + return mediaType; + } + + Class<PathExtensionContentNegotiationStrategy> clazz = PathExtensionContentNegotiationStrategy.class; + PathExtensionContentNegotiationStrategy strategy = this.contentNegotiationManager.getStrategy(clazz); + if (strategy != null) { + mediaType = strategy.getMediaTypeForResource(resource); + } + + if (mediaType == null) { + ServletWebRequest webRequest = new ServletWebRequest(request); + try { + List<MediaType> mediaTypes = getContentNegotiationManager().resolveMediaTypes(webRequest); + if (!mediaTypes.isEmpty()) { + mediaType = mediaTypes.get(0); + } + } + catch (HttpMediaTypeNotAcceptableException ex) { + // Ignore } } + return mediaType; } /** + * Determine an appropriate media type for the given resource. + * @param resource the resource to check + * @return the corresponding media type, or {@code null} if none found + * @deprecated as of 4.3 this method is deprecated; please override + * {@link #getMediaType(HttpServletRequest, Resource)} instead. + */ + @Deprecated + protected MediaType getMediaType(Resource resource) { + return null; + } + + /** * Set headers on the given servlet response. * Called for GET requests as well as HEAD requests. * @param response current servlet response @@ -414,9 +563,17 @@ public class ResourceHttpRequestHandler extends WebContentGenerator protected void setHeaders(HttpServletResponse response, Resource resource, MediaType mediaType) throws IOException { long length = resource.contentLength(); if (length > Integer.MAX_VALUE) { - throw new IOException("Resource content too long (beyond Integer.MAX_VALUE): " + resource); + if (contentLengthLongAvailable) { + response.setContentLengthLong(length); + } + else { + response.setHeader(HttpHeaders.CONTENT_LENGTH, Long.toString(length)); + } } - response.setContentLength((int) length); + else { + response.setContentLength((int) length); + } + if (mediaType != null) { response.setContentType(mediaType.toString()); } @@ -429,187 +586,10 @@ public class ResourceHttpRequestHandler extends WebContentGenerator response.setHeader(HttpHeaders.ACCEPT_RANGES, "bytes"); } - /** - * Write the actual content out to the given servlet response, - * streaming the resource's content. - * @param response current servlet response - * @param resource the identified resource (never {@code null}) - * @throws IOException in case of errors while writing the content - */ - protected void writeContent(HttpServletResponse response, Resource resource) throws IOException { - try { - InputStream in = resource.getInputStream(); - try { - StreamUtils.copy(in, response.getOutputStream()); - } - catch (NullPointerException ex) { - // ignore, see SPR-13620 - } - finally { - try { - in.close(); - } - catch (Throwable ex) { - // ignore, see SPR-12999 - } - } - } - catch (FileNotFoundException ex) { - // ignore, see SPR-12999 - } - } - - /** - * 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() + "]"; } - - /** - * Inner class to avoid a hard-coded JAF dependency. - */ - private static class ActivationMediaTypeFactory { - - private static final FileTypeMap fileTypeMap; - - static { - fileTypeMap = loadFileTypeMapFromContextSupportModule(); - } - - private static FileTypeMap loadFileTypeMapFromContextSupportModule() { - // See if we can find the extended mime.types from the context-support module... - Resource mappingLocation = new ClassPathResource("org/springframework/mail/javamail/mime.types"); - if (mappingLocation.exists()) { - InputStream inputStream = null; - try { - inputStream = mappingLocation.getInputStream(); - return new MimetypesFileTypeMap(inputStream); - } - catch (IOException ex) { - // ignore - } - finally { - if (inputStream != null) { - try { - inputStream.close(); - } - catch (IOException ex) { - // ignore - } - } - } - } - return FileTypeMap.getDefaultFileTypeMap(); - } - - public static MediaType getMediaType(String filename) { - String mediaType = fileTypeMap.getContentType(filename); - return (StringUtils.hasText(mediaType) ? MediaType.parseMediaType(mediaType) : null); - } - } - } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/ResourceTransformer.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/ResourceTransformer.java index fe941d83..de17faf1 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/ResourceTransformer.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/ResourceTransformer.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,10 +35,10 @@ public interface ResourceTransformer { * @param request the current request * @param resource the resource to transform * @param transformerChain the chain of remaining transformers to delegate to - * @return the transformed resource, never {@code null} + * @return the transformed resource (never {@code null}) * @throws IOException if the transformation fails */ Resource transform(HttpServletRequest request, Resource resource, ResourceTransformerChain transformerChain) throws IOException; -}
\ No newline at end of file +} 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 471ae154..901d0c5e 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 @@ -27,6 +27,7 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.web.filter.OncePerRequestFilter; +import org.springframework.web.util.UrlPathHelper; /** * A filter that wraps the {@link HttpServletResponse} and overrides its @@ -96,13 +97,14 @@ public class ResourceUrlEncodingFilter extends OncePerRequestFilter { private void initLookupPath(ResourceUrlProvider urlProvider) { if (this.indexLookupPath == null) { - String requestUri = urlProvider.getPathHelper().getRequestUri(this.request); - String lookupPath = urlProvider.getPathHelper().getLookupPathForRequest(this.request); + UrlPathHelper pathHelper = urlProvider.getUrlPathHelper(); + String requestUri = pathHelper.getRequestUri(this.request); + String lookupPath = pathHelper.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); + String contextPath = pathHelper.getContextPath(this.request); if (requestUri.equals(contextPath)) { this.indexLookupPath = requestUri.length(); this.prefixLookupPath = requestUri; 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 9ee0c5a8..f39e02ff 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 @@ -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. @@ -51,7 +51,7 @@ public class ResourceUrlProvider implements ApplicationListener<ContextRefreshed protected final Log logger = LogFactory.getLog(getClass()); - private UrlPathHelper pathHelper = new UrlPathHelper(); + private UrlPathHelper urlPathHelper = new UrlPathHelper(); private PathMatcher pathMatcher = new AntPathMatcher(); @@ -65,15 +65,24 @@ public class ResourceUrlProvider implements ApplicationListener<ContextRefreshed * {@link #getForRequestUrl(javax.servlet.http.HttpServletRequest, String)} * in order to derive the lookup path for a target request URL path. */ - public void setUrlPathHelper(UrlPathHelper pathHelper) { - this.pathHelper = pathHelper; + public void setUrlPathHelper(UrlPathHelper urlPathHelper) { + this.urlPathHelper = urlPathHelper; } /** * Return the configured {@code UrlPathHelper}. + * @since 4.2.8 */ + public UrlPathHelper getUrlPathHelper() { + return this.urlPathHelper; + } + + /** + * @deprecated as of Spring 4.2.8, in favor of {@link #getUrlPathHelper} + */ + @Deprecated public UrlPathHelper getPathHelper() { - return this.pathHelper; + return this.urlPathHelper; } /** @@ -135,6 +144,7 @@ public class ResourceUrlProvider implements ApplicationListener<ContextRefreshed } } + protected void detectResourceHandlers(ApplicationContext appContext) { logger.debug("Looking for resource handler mappings"); @@ -158,7 +168,6 @@ public class ResourceUrlProvider implements ApplicationListener<ContextRefreshed } } - /** * A variation on {@link #getForLookupPath(String)} that accepts a full request * URL path (i.e. including context and servlet path) and returns the full request @@ -181,8 +190,9 @@ public class ResourceUrlProvider implements ApplicationListener<ContextRefreshed } private int getLookupPathIndex(HttpServletRequest request) { - String requestUri = getPathHelper().getRequestUri(request); - String lookupPath = getPathHelper().getLookupPathForRequest(request); + UrlPathHelper pathHelper = getUrlPathHelper(); + String requestUri = pathHelper.getRequestUri(request); + String lookupPath = pathHelper.getLookupPathForRequest(request); return requestUri.indexOf(lookupPath); } 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 index 8d08bbd7..a76214c3 100644 --- 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 @@ -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. @@ -45,11 +45,29 @@ import org.springframework.core.io.Resource; */ public class WebJarsResourceResolver extends AbstractResourceResolver { - private final static String WEBJARS_LOCATION = "META-INF/resources/webjars"; + 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(); + + private final WebJarAssetLocator webJarAssetLocator; + + + /** + * Create a {@code WebJarsResourceResolver} with a default {@code WebJarAssetLocator} instance. + */ + public WebJarsResourceResolver() { + this(new WebJarAssetLocator()); + } + + /** + * Create a {@code WebJarsResourceResolver} with a custom {@code WebJarAssetLocator} instance, + * e.g. with a custom index. + * @since 4.3 + */ + public WebJarsResourceResolver(WebJarAssetLocator webJarAssetLocator) { + this.webJarAssetLocator = webJarAssetLocator; + } @Override diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/support/AbstractAnnotationConfigDispatcherServletInitializer.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/support/AbstractAnnotationConfigDispatcherServletInitializer.java index fc897a10..20b2b9e4 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/support/AbstractAnnotationConfigDispatcherServletInitializer.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/support/AbstractAnnotationConfigDispatcherServletInitializer.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. @@ -32,6 +32,9 @@ import org.springframework.web.context.support.AnnotationConfigWebApplicationCon * Further template and customization methods are provided by * {@link AbstractDispatcherServletInitializer}. * + * <p>This is the preferred approach for applications that use Java-based + * Spring configuration. + * * @author Arjen Poutsma * @author Chris Beams * @since 3.2 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 9fae8679..df008b77 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-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. @@ -53,17 +53,18 @@ import org.springframework.web.util.UrlPathHelper; import org.springframework.web.util.WebUtils; /** - * Context holder for request-specific state, like current web application context, current locale, current theme, - * and potential binding errors. Provides easy access to localized messages and Errors instances. + * Context holder for request-specific state, like current web application context, current locale, + * current theme, and potential binding errors. Provides easy access to localized messages and + * Errors instances. * - * <p>Suitable for exposition to views, and usage within JSP's "useBean" tag, JSP scriptlets, JSTL EL, Velocity - * templates, etc. Necessary for views that do not have access to the servlet request, like Velocity templates. + * <p>Suitable for exposition to views, and usage within JSP's "useBean" tag, JSP scriptlets, JSTL EL, + * etc. Necessary for views that do not have access to the servlet request, like FreeMarker templates. * * <p>Can be instantiated manually, or automatically exposed to views as model attribute via AbstractView's * "requestContextAttribute" property. * - * <p>Will also work outside of DispatcherServlet requests, accessing the root WebApplicationContext and using - * an appropriate fallback for the locale (the HttpServletRequest's primary locale). + * <p>Will also work outside of DispatcherServlet requests, accessing the root WebApplicationContext + * and using an appropriate fallback for the locale (the HttpServletRequest's primary locale). * * @author Juergen Hoeller * @author Rossen Stoyanchev @@ -467,7 +468,7 @@ public class RequestContext { /** * (De)activate default HTML escaping for messages and errors, for the scope of this RequestContext. * <p>The default is the application-wide setting (the "defaultHtmlEscape" context-param in web.xml). - * @see org.springframework.web.util.WebUtils#isDefaultHtmlEscape + * @see org.springframework.web.util.WebUtils#getDefaultHtmlEscape */ public void setDefaultHtmlEscape(boolean defaultHtmlEscape) { this.defaultHtmlEscape = defaultHtmlEscape; 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 ac6e0769..8c854917 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 @@ -198,7 +198,7 @@ public abstract class RequestContextUtils { * <p>Consider using {@link org.springframework.context.i18n.LocaleContextHolder#getTimeZone()} * which will normally be populated with the same TimeZone: That method only * differs in terms of its fallback to the system time zone if the LocaleResolver - * hasn't provided provided a specific time zone (instead of this method's {@code null}). + * hasn't provided a specific time zone (instead of this method's {@code null}). * @param request current HTTP request * @return the current time zone for the given request, either from the * TimeZoneAwareLocaleResolver or {@code null} if none associated 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 07abc64b..367aed85 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 @@ -16,6 +16,7 @@ package org.springframework.web.servlet.support; +import java.util.Enumeration; import javax.servlet.http.HttpServletRequest; import org.springframework.http.HttpRequest; @@ -27,8 +28,8 @@ import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletRequestAttributes; import org.springframework.web.util.UriComponents; import org.springframework.web.util.UriComponentsBuilder; +import org.springframework.web.util.UriUtils; import org.springframework.web.util.UrlPathHelper; -import org.springframework.web.util.WebUtils; /** * A UriComponentsBuilder that extracts information from the HttpServletRequest. @@ -43,7 +44,6 @@ public class ServletUriComponentsBuilder extends UriComponentsBuilder { /** * Default constructor. Protected to prevent direct instantiation. - * * @see #fromContextPath(HttpServletRequest) * @see #fromServletMapping(HttpServletRequest) * @see #fromRequest(HttpServletRequest) @@ -133,8 +133,15 @@ public class ServletUriComponentsBuilder extends UriComponentsBuilder { } private static String prependForwardedPrefix(HttpServletRequest request, String path) { - String prefix = request.getHeader("X-Forwarded-Prefix"); - if (StringUtils.hasText(prefix)) { + String prefix = null; + Enumeration<String> names = request.getHeaderNames(); + while (names.hasMoreElements()) { + String name = names.nextElement(); + if ("X-Forwarded-Prefix".equalsIgnoreCase(name)) { + prefix = request.getHeader(name); + } + } + if (prefix != null) { path = prefix + path; } return path; @@ -211,8 +218,7 @@ public class ServletUriComponentsBuilder extends UriComponentsBuilder { public String removePathExtension() { String extension = null; if (this.originalPath != null) { - String filename = WebUtils.extractFullFilenameFromUrlPath(this.originalPath); - extension = StringUtils.getFilenameExtension(filename); + extension = UriUtils.extractFileExtension(this.originalPath); if (!StringUtils.isEmpty(extension)) { int end = this.originalPath.length() - (extension.length() + 1); replacePath(this.originalPath.substring(0, end)); 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 20cf89d2..6cf54cf2 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 @@ -16,8 +16,10 @@ package org.springframework.web.servlet.support; +import java.util.ArrayList; import java.util.Arrays; -import java.util.HashSet; +import java.util.Collection; +import java.util.Collections; import java.util.LinkedHashSet; import java.util.Set; import java.util.concurrent.TimeUnit; @@ -26,6 +28,9 @@ import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.springframework.http.CacheControl; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.util.ClassUtils; import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; import org.springframework.web.HttpRequestMethodNotSupportedException; @@ -53,6 +58,7 @@ import org.springframework.web.context.support.WebApplicationObjectSupport; * @author Rod Johnson * @author Juergen Hoeller * @author Brian Clozel + * @author Rossen Stoyanchev * @see #setCacheSeconds * @see #setCacheControl * @see #setRequireSession @@ -74,16 +80,27 @@ public abstract class WebContentGenerator extends WebApplicationObjectSupport { protected static final String HEADER_CACHE_CONTROL = "Cache-Control"; + /** Checking for Servlet 3.0+ HttpServletResponse.getHeaders(String) */ + private static final boolean servlet3Present = + ClassUtils.hasMethod(HttpServletResponse.class, "getHeaders", String.class); + /** Set of supported HTTP methods */ private Set<String> supportedMethods; + private String allowHeader; + private boolean requireSession = false; private CacheControl cacheControl; private int cacheSeconds = -1; + private String[] varyByRequestHeaders; + + + // deprecated fields + /** Use HTTP 1.0 expires header? */ private boolean useExpiresHeader = false; @@ -112,11 +129,12 @@ public abstract class WebContentGenerator extends WebApplicationObjectSupport { */ public WebContentGenerator(boolean restrictDefaultSupportedMethods) { if (restrictDefaultSupportedMethods) { - this.supportedMethods = new HashSet<String>(4); + this.supportedMethods = new LinkedHashSet<String>(4); this.supportedMethods.add(METHOD_GET); this.supportedMethods.add(METHOD_HEAD); this.supportedMethods.add(METHOD_POST); } + initAllowHeader(); } /** @@ -124,7 +142,7 @@ public abstract class WebContentGenerator extends WebApplicationObjectSupport { * @param supportedMethods the supported HTTP methods for this content generator */ public WebContentGenerator(String... supportedMethods) { - this.supportedMethods = new HashSet<String>(Arrays.asList(supportedMethods)); + setSupportedMethods(supportedMethods); } @@ -140,6 +158,7 @@ public abstract class WebContentGenerator extends WebApplicationObjectSupport { else { this.supportedMethods = null; } + initAllowHeader(); } /** @@ -149,6 +168,40 @@ public abstract class WebContentGenerator extends WebApplicationObjectSupport { return StringUtils.toStringArray(this.supportedMethods); } + private void initAllowHeader() { + Collection<String> allowedMethods; + if (this.supportedMethods == null) { + allowedMethods = new ArrayList<String>(HttpMethod.values().length - 1); + for (HttpMethod method : HttpMethod.values()) { + if (!HttpMethod.TRACE.equals(method)) { + allowedMethods.add(method.name()); + } + } + } + else if (this.supportedMethods.contains(HttpMethod.OPTIONS.name())) { + allowedMethods = this.supportedMethods; + } + else { + allowedMethods = new ArrayList<String>(this.supportedMethods); + allowedMethods.add(HttpMethod.OPTIONS.name()); + + } + this.allowHeader = StringUtils.collectionToCommaDelimitedString(allowedMethods); + } + + /** + * Return the "Allow" header value to use in response to an HTTP OPTIONS + * request based on the configured {@link #setSupportedMethods supported + * methods} also automatically adding "OPTIONS" to the list even if not + * present as a supported method. This means sub-classes don't have to + * explicitly list "OPTIONS" as a supported method as long as HTTP OPTIONS + * requests are handled before making a call to + * {@link #checkRequest(HttpServletRequest)}. + */ + protected String getAllowHeader() { + return this.allowHeader; + } + /** * Set whether a session should be required to handle requests. */ @@ -205,6 +258,29 @@ public abstract class WebContentGenerator extends WebApplicationObjectSupport { } /** + * Configure one or more request header names (e.g. "Accept-Language") to + * add to the "Vary" response header to inform clients that the response is + * subject to content negotiation and variances based on the value of the + * given request headers. The configured request header names are added only + * if not already present in the response "Vary" header. + * <p><strong>Note:</strong> This property is only supported on Servlet 3.0+ + * which allows checking existing response header values. + * @param varyByRequestHeaders one or more request header names + * @since 4.3 + */ + public final void setVaryByRequestHeaders(String... varyByRequestHeaders) { + this.varyByRequestHeaders = varyByRequestHeaders; + } + + /** + * Return the configured request header names for the "Vary" response header. + * @since 4.3 + */ + public final String[] getVaryByRequestHeaders() { + return this.varyByRequestHeaders; + } + + /** * 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 @@ -322,6 +398,11 @@ public abstract class WebContentGenerator extends WebApplicationObjectSupport { else { applyCacheSeconds(response, this.cacheSeconds); } + if (servlet3Present && this.varyByRequestHeaders != null) { + for (String value : getVaryRequestHeadersToAdd(response)) { + response.addHeader("Vary", value); + } + } } /** @@ -513,4 +594,26 @@ public abstract class WebContentGenerator extends WebApplicationObjectSupport { } } + + private Collection<String> getVaryRequestHeadersToAdd(HttpServletResponse response) { + if (!response.containsHeader(HttpHeaders.VARY)) { + return Arrays.asList(getVaryByRequestHeaders()); + } + Collection<String> result = new ArrayList<String>(getVaryByRequestHeaders().length); + Collections.addAll(result, getVaryByRequestHeaders()); + for (String header : response.getHeaders(HttpHeaders.VARY)) { + for (String existing : StringUtils.tokenizeToStringArray(header, ",")) { + if ("*".equals(existing)) { + return Collections.emptyList(); + } + for (String value : getVaryByRequestHeaders()) { + if (value.equalsIgnoreCase(existing)) { + result.remove(value); + } + } + } + } + return result; + } + } 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 ebde41ea..f0e619e9 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 @@ -38,7 +38,7 @@ import org.springframework.web.bind.WebDataBinder; * the bound {@link Collection}. * <h3>Approach Three</h3> * For any other bound value type, the '{@code input(checkbox)}' is marked as 'checked' - * if the the configured {@link #setValue(Object) value} is equal to the bound value. + * if the configured {@link #setValue(Object) value} is equal to the bound value. * * @author Rob Harrop * @author Juergen Hoeller 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 5aa9d232..d0b76ff9 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 @@ -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. @@ -147,7 +147,9 @@ public class FormTag extends AbstractHtmlElementTag { * Set the name of the form attribute in the model. * <p>May be a runtime expression. * @see #setModelAttribute + * @deprecated as of Spring 4.3, in favor of {@link #setModelAttribute} */ + @Deprecated public void setCommandName(String commandName) { this.modelAttribute = commandName; } @@ -155,7 +157,9 @@ public class FormTag extends AbstractHtmlElementTag { /** * Get the name of the form attribute in the model. * @see #getModelAttribute + * @deprecated as of Spring 4.3, in favor of {@link #getModelAttribute} */ + @Deprecated protected String getCommandName() { return this.modelAttribute; } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/view/DefaultRequestToViewNameTranslator.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/view/DefaultRequestToViewNameTranslator.java index dd8b1d39..1e42bb3b 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/view/DefaultRequestToViewNameTranslator.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/view/DefaultRequestToViewNameTranslator.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. @@ -24,28 +24,28 @@ import org.springframework.web.servlet.RequestToViewNameTranslator; import org.springframework.web.util.UrlPathHelper; /** - * {@link org.springframework.web.servlet.RequestToViewNameTranslator} - * that simply transforms the URI of the incoming request into a view name. + * {@link RequestToViewNameTranslator} that simply transforms the URI of + * the incoming request into a view name. * - * <p>Can be explicitly defined as the "viewNameTranslator" bean in a + * <p>Can be explicitly defined as the {@code viewNameTranslator} bean in a * {@link org.springframework.web.servlet.DispatcherServlet} context. * Otherwise, a plain default instance will be used. * * <p>The default transformation simply strips leading and trailing slashes * as well as the file extension of the URI, and returns the result as the - * view name with the configured {@link #setPrefix "prefix"} and a - * {@link #setSuffix "suffix"} added as appropriate. + * view name with the configured {@link #setPrefix prefix} and a + * {@link #setSuffix suffix} added as appropriate. * * <p>The stripping of the leading slash and file extension can be disabled - * using the {@link #setStripLeadingSlash "stripLeadingSlash"} and - * {@link #setStripExtension "stripExtension"} properties, respectively. + * using the {@link #setStripLeadingSlash stripLeadingSlash} and + * {@link #setStripExtension stripExtension} properties, respectively. * * <p>Find below some examples of request to view name translation. - * - * <pre class="code">http://localhost:8080/gamecast/display.html -> display - * http://localhost:8080/gamecast/displayShoppingCart.html -> displayShoppingCart - * http://localhost:8080/gamecast/admin/index.html -> admin/index - * </pre> + * <ul> + * <li>{@code http://localhost:8080/gamecast/display.html} » {@code display}</li> + * <li>{@code http://localhost:8080/gamecast/displayShoppingCart.html} » {@code displayShoppingCart}</li> + * <li>{@code http://localhost:8080/gamecast/admin/index.html} » {@code admin/index}</li> + * </ul> * * @author Rob Harrop * @author Juergen Hoeller 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 679e8a95..13a70850 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 @@ -66,6 +66,19 @@ public class InternalResourceViewResolver extends UrlBasedViewResolver { setViewClass(viewClass); } + /** + * A convenience constructor that allows for specifying {@link #setPrefix prefix} + * and {@link #setSuffix suffix} as constructor arguments. + * @param prefix the prefix that gets prepended to view names when building a URL + * @param suffix the suffix that gets appended to view names when building a URL + * @since 4.3 + */ + public InternalResourceViewResolver(String prefix, String suffix) { + this(); + setPrefix(prefix); + setSuffix(suffix); + } + /** * This resolver requires {@link InternalResourceView}. 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 0a188696..928e5ef9 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-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. @@ -104,6 +104,8 @@ public class RedirectView extends AbstractUrlBasedView implements SmartView { private boolean propagateQueryParams = false; + private String[] hosts; + /** * Constructor for use as a bean. @@ -253,6 +255,28 @@ public class RedirectView extends AbstractUrlBasedView implements SmartView { } /** + * Configure one or more hosts associated with the application. + * All other hosts will be considered external hosts. + * <p>In effect, this property provides a way turn off encoding via + * {@link HttpServletResponse#encodeRedirectURL} for URLs that have a + * host and that host is not listed as a known host. + * <p>If not set (the default) all URLs are encoded through the response. + * @param hosts one or more application hosts + * @since 4.3 + */ + public void setHosts(String... hosts) { + this.hosts = hosts; + } + + /** + * Return the configured application hosts. + * @since 4.3 + */ + public String[] getHosts() { + return this.hosts; + } + + /** * Returns "true" indicating this view performs a redirect. */ @Override @@ -583,30 +607,56 @@ public class RedirectView extends AbstractUrlBasedView implements SmartView { protected void sendRedirect(HttpServletRequest request, HttpServletResponse response, String targetUrl, boolean http10Compatible) throws IOException { - String encodedRedirectURL = response.encodeRedirectURL(targetUrl); + String encodedURL = (isRemoteHost(targetUrl) ? targetUrl : 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); + response.setHeader("Location", encodedURL); } else if (attributeStatusCode != null) { response.setStatus(attributeStatusCode.value()); - response.setHeader("Location", encodedRedirectURL); + response.setHeader("Location", encodedURL); } else { // Send status code 302 by default. - response.sendRedirect(encodedRedirectURL); + response.sendRedirect(encodedURL); } } else { HttpStatus statusCode = getHttp11StatusCode(request, response, targetUrl); response.setStatus(statusCode.value()); - response.setHeader("Location", encodedRedirectURL); + response.setHeader("Location", encodedURL); } } /** + * Whether the given targetUrl has a host that is a "foreign" system in which + * case {@link HttpServletResponse#encodeRedirectURL} will not be applied. + * This method returns {@code true} if the {@link #setHosts(String[])} + * property is configured and the target URL has a host that does not match. + * @param targetUrl the target redirect URL + * @return {@code true} the target URL has a remote host, {@code false} if it + * the URL does not have a host or the "host" property is not configured. + * @since 4.3 + */ + protected boolean isRemoteHost(String targetUrl) { + if (ObjectUtils.isEmpty(getHosts())) { + return false; + } + String targetHost = UriComponentsBuilder.fromUriString(targetUrl).build().getHost(); + if (StringUtils.isEmpty(targetHost)) { + return false; + } + for (String host : getHosts()) { + if (targetHost.equals(host)) { + return false; + } + } + return true; + } + + /** * Determines the status code to use for HTTP 1.1 compatible requests. * <p>The default implementation returns the {@link #setStatusCode(HttpStatus) statusCode} * property if set, or the value of the {@link #RESPONSE_STATUS_ATTRIBUTE} attribute. diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/view/UrlBasedViewResolver.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/view/UrlBasedViewResolver.java index 8bd441ea..9b09f8a9 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/view/UrlBasedViewResolver.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/view/UrlBasedViewResolver.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. @@ -20,6 +20,7 @@ import java.util.HashMap; import java.util.Locale; import java.util.Map; import java.util.Properties; +import javax.servlet.http.HttpServletResponse; import org.springframework.beans.BeanUtils; import org.springframework.core.Ordered; @@ -112,6 +113,8 @@ public class UrlBasedViewResolver extends AbstractCachingViewResolver implements private boolean redirectHttp10Compatible = true; + private String[] redirectHosts; + private String requestContextAttribute; /** Map of static attributes, keyed by attribute name (String) */ @@ -254,6 +257,28 @@ public class UrlBasedViewResolver extends AbstractCachingViewResolver implements } /** + * Configure one or more hosts associated with the application. + * All other hosts will be considered external hosts. + * <p>In effect, this property provides a way turn off encoding on redirect + * via {@link HttpServletResponse#encodeRedirectURL} for URLs that have a + * host and that host is not listed as a known host. + * <p>If not set (the default) all URLs are encoded through the response. + * @param redirectHosts one or more application hosts + * @since 4.3 + */ + public void setRedirectHosts(String... redirectHosts) { + this.redirectHosts = redirectHosts; + } + + /** + * Return the configured application hosts for redirect purposes. + * @since 4.3 + */ + public String[] getRedirectHosts() { + return this.redirectHosts; + } + + /** * Set the name of the RequestContext attribute for all views. * @param requestContextAttribute name of the RequestContext attribute * @see AbstractView#setRequestContextAttribute @@ -435,6 +460,7 @@ public class UrlBasedViewResolver extends AbstractCachingViewResolver implements if (viewName.startsWith(REDIRECT_URL_PREFIX)) { String redirectUrl = viewName.substring(REDIRECT_URL_PREFIX.length()); RedirectView view = new RedirectView(redirectUrl, isRedirectContextRelative(), isRedirectHttp10Compatible()); + view.setHosts(getRedirectHosts()); return applyLifecycleMethods(viewName, view); } // Check for special "forward:" prefix. diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/view/freemarker/FreeMarkerView.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/view/freemarker/FreeMarkerView.java index f5e260eb..00e678f1 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/view/freemarker/FreeMarkerView.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/view/freemarker/FreeMarkerView.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. @@ -20,7 +20,6 @@ import java.io.FileNotFoundException; import java.io.IOException; import java.util.Collections; import java.util.Enumeration; -import java.util.HashSet; import java.util.Locale; import java.util.Map; import javax.servlet.GenericServlet; @@ -42,6 +41,7 @@ import freemarker.ext.servlet.HttpRequestParametersHashModel; import freemarker.ext.servlet.HttpSessionHashModel; import freemarker.ext.servlet.ServletContextHashModel; import freemarker.template.Configuration; +import freemarker.template.DefaultObjectWrapperBuilder; import freemarker.template.ObjectWrapper; import freemarker.template.SimpleHash; import freemarker.template.Template; @@ -186,10 +186,10 @@ public class FreeMarkerView extends AbstractTemplateView { * {@link ObjectWrapper#DEFAULT_WRAPPER default wrapper} if none specified. * @see freemarker.template.Configuration#getObjectWrapper() */ - @SuppressWarnings("deprecation") protected ObjectWrapper getObjectWrapper() { ObjectWrapper ow = getConfiguration().getObjectWrapper(); - return (ow != null ? ow : ObjectWrapper.DEFAULT_WRAPPER); + return (ow != null ? ow : + new DefaultObjectWrapperBuilder(Configuration.DEFAULT_INCOMPATIBLE_IMPROVEMENTS).build()); } /** @@ -405,7 +405,7 @@ public class FreeMarkerView extends AbstractTemplateView { @Override public Enumeration<String> getInitParameterNames() { - return Collections.enumeration(new HashSet<String>()); + return Collections.enumeration(Collections.<String>emptySet()); } } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/view/freemarker/FreeMarkerViewResolver.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/view/freemarker/FreeMarkerViewResolver.java index c774ccf0..3768eb8a 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/view/freemarker/FreeMarkerViewResolver.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/view/freemarker/FreeMarkerViewResolver.java @@ -40,11 +40,29 @@ import org.springframework.web.servlet.view.AbstractTemplateViewResolver; */ public class FreeMarkerViewResolver extends AbstractTemplateViewResolver { + /** + * Sets the default {@link #setViewClass view class} to {@link #requiredViewClass}: + * by default {@link FreeMarkerView}. + */ public FreeMarkerViewResolver() { setViewClass(requiredViewClass()); } /** + * A convenience constructor that allows for specifying {@link #setPrefix prefix} + * and {@link #setSuffix suffix} as constructor arguments. + * @param prefix the prefix that gets prepended to view names when building a URL + * @param suffix the suffix that gets appended to view names when building a URL + * @since 4.3 + */ + public FreeMarkerViewResolver(String prefix, String suffix) { + this(); + setPrefix(prefix); + setSuffix(suffix); + } + + + /** * Requires {@link FreeMarkerView}. */ @Override diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/view/groovy/GroovyMarkupView.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/view/groovy/GroovyMarkupView.java index 290d96c7..38eda981 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/view/groovy/GroovyMarkupView.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/view/groovy/GroovyMarkupView.java @@ -54,7 +54,7 @@ public class GroovyMarkupView extends AbstractTemplateView { /** * Set the MarkupTemplateEngine to use in this view. - * <p>If not set, the engine is auto-detected by looking up up a single + * <p>If not set, the engine is auto-detected by looking up a single * {@link GroovyMarkupConfig} bean in the web application context and using * it to obtain the configured {@code MarkupTemplateEngine} instance. * @see GroovyMarkupConfig diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/view/groovy/GroovyMarkupViewResolver.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/view/groovy/GroovyMarkupViewResolver.java index 79c5f87a..197abdf2 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/view/groovy/GroovyMarkupViewResolver.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/view/groovy/GroovyMarkupViewResolver.java @@ -38,10 +38,28 @@ import org.springframework.web.servlet.view.AbstractTemplateViewResolver; */ public class GroovyMarkupViewResolver extends AbstractTemplateViewResolver { + /** + * Sets the default {@link #setViewClass view class} to {@link #requiredViewClass}: + * by default {@link GroovyMarkupView}. + */ public GroovyMarkupViewResolver() { setViewClass(requiredViewClass()); } + /** + * A convenience constructor that allows for specifying {@link #setPrefix prefix} + * and {@link #setSuffix suffix} as constructor arguments. + * @param prefix the prefix that gets prepended to view names when building a URL + * @param suffix the suffix that gets appended to view names when building a URL + * @since 4.3 + */ + public GroovyMarkupViewResolver(String prefix, String suffix) { + this(); + setPrefix(prefix); + setSuffix(suffix); + } + + @Override protected Class<?> requiredViewClass() { return GroovyMarkupView.class; 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 7e9af07d..7821494d 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 @@ -170,7 +170,7 @@ public class JasperReportsMultiFormatView extends AbstractJasperReportsView { String format = (String) model.get(this.formatKey); if (format == null) { - throw new IllegalArgumentException("No format format found in model"); + throw new IllegalArgumentException("No format found in model"); } if (logger.isDebugEnabled()) { 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 34b75fa2..cf67ecc5 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 @@ -38,7 +38,7 @@ import org.springframework.web.servlet.view.AbstractView; * Abstract base class for Jackson based and content type independent * {@link AbstractView} implementations. * - * <p>Compatible with Jackson 2.1 and higher. + * <p>Compatible with Jackson 2.6 and higher, as of Spring 4.3. * * @author Jeremy Grelle * @author Arjen Poutsma 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 2039e8c0..12a5baa0 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 @@ -49,7 +49,7 @@ import org.springframework.web.servlet.View; * * <p>The default constructor uses the default configuration provided by {@link Jackson2ObjectMapperBuilder}. * - * <p>Compatible with Jackson 2.1 and higher. + * <p>Compatible with Jackson 2.6 and higher, as of Spring 4.3. * * @author Jeremy Grelle * @author Arjen Poutsma 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 index 908b273e..ac8298a8 100644 --- 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 @@ -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. @@ -18,13 +18,9 @@ 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; @@ -40,7 +36,6 @@ 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; @@ -55,7 +50,7 @@ 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 + * <p>If not set, each property is auto-detected by looking up a single * {@link ScriptTemplateConfig} bean in the web application context and using * it to obtain the configured properties. * @@ -96,7 +91,7 @@ public class ScriptTemplateView extends AbstractUrlBasedView { private Charset charset; - private String resourceLoaderPath; + private String[] resourceLoaderPaths; private ResourceLoader resourceLoader; @@ -184,7 +179,16 @@ public class ScriptTemplateView extends AbstractUrlBasedView { * See {@link ScriptTemplateConfigurer#setResourceLoaderPath(String)} documentation. */ public void setResourceLoaderPath(String resourceLoaderPath) { - this.resourceLoaderPath = resourceLoaderPath; + String[] paths = StringUtils.commaDelimitedListToStringArray(resourceLoaderPath); + this.resourceLoaderPaths = new String[paths.length + 1]; + this.resourceLoaderPaths[0] = ""; + for (int i = 0; i < paths.length; i++) { + String path = paths[i]; + if (!path.endsWith("/") && !path.endsWith(":")) { + path = path + "/"; + } + this.resourceLoaderPaths[i + 1] = path; + } } @@ -214,12 +218,12 @@ public class ScriptTemplateView extends AbstractUrlBasedView { 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.resourceLoaderPaths == null) { + String resourceLoaderPath = viewConfig.getResourceLoaderPath(); + setResourceLoaderPath(resourceLoaderPath == null ? DEFAULT_RESOURCE_LOADER_PATH : resourceLoaderPath); } if (this.resourceLoader == null) { - this.resourceLoader = new DefaultResourceLoader(createClassLoader()); + this.resourceLoader = getApplicationContext(); } if (this.sharedEngine == null && viewConfig.isSharedEngine() != null) { this.sharedEngine = viewConfig.isSharedEngine(); @@ -282,10 +286,7 @@ public class ScriptTemplateView extends AbstractUrlBasedView { 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"); - } + Resource resource = getResource(script); engine.eval(new InputStreamReader(resource.getInputStream())); } } @@ -295,26 +296,14 @@ public class ScriptTemplateView extends AbstractUrlBasedView { } } - 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()); - } - } - } + protected Resource getResource(String location) { + for (String path : this.resourceLoaderPaths) { + Resource resource = this.resourceLoader.getResource(path + location); + if (resource.exists()) { + return resource; } } - 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); + throw new IllegalStateException("Resource [" + location + "] not found"); } protected ScriptTemplateConfig autodetectViewConfig() throws BeansException { @@ -329,7 +318,6 @@ public class ScriptTemplateView extends AbstractUrlBasedView { } } - @Override protected void prepareResponse(HttpServletRequest request, HttpServletResponse response) { super.prepareResponse(request, response); @@ -365,7 +353,7 @@ public class ScriptTemplateView extends AbstractUrlBasedView { } protected String getTemplate(String path) throws IOException { - Resource resource = this.resourceLoader.getResource(path); + Resource resource = getResource(path); InputStreamReader reader = new InputStreamReader(resource.getInputStream(), this.charset); return FileCopyUtils.copyToString(reader); } 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 index 54f61470..168ce304 100644 --- 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 @@ -35,10 +35,28 @@ import org.springframework.web.servlet.view.UrlBasedViewResolver; */ public class ScriptTemplateViewResolver extends UrlBasedViewResolver { + /** + * Sets the default {@link #setViewClass view class} to {@link #requiredViewClass}: + * by default {@link ScriptTemplateView}. + */ public ScriptTemplateViewResolver() { setViewClass(requiredViewClass()); } + /** + * A convenience constructor that allows for specifying {@link #setPrefix prefix} + * and {@link #setSuffix suffix} as constructor arguments. + * @param prefix the prefix that gets prepended to view names when building a URL + * @param suffix the suffix that gets appended to view names when building a URL + * @since 4.3 + */ + public ScriptTemplateViewResolver(String prefix, String suffix) { + this(); + setPrefix(prefix); + setSuffix(suffix); + } + + @Override protected Class<?> requiredViewClass() { return ScriptTemplateView.class; diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/view/tiles3/AbstractSpringPreparerFactory.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/view/tiles3/AbstractSpringPreparerFactory.java index 93103388..f58dbed0 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/view/tiles3/AbstractSpringPreparerFactory.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/view/tiles3/AbstractSpringPreparerFactory.java @@ -25,7 +25,7 @@ import org.springframework.web.context.WebApplicationContext; import org.springframework.web.servlet.DispatcherServlet; /** - * Abstract implementation of the Tiles3 {@link org.apache.tiles.preparer.PreparerFactory} + * Abstract implementation of the Tiles {@link org.apache.tiles.preparer.PreparerFactory} * interface, obtaining the current Spring WebApplicationContext and delegating to * {@link #getPreparer(String, org.springframework.web.context.WebApplicationContext)}. * diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/view/tiles3/SimpleSpringPreparerFactory.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/view/tiles3/SimpleSpringPreparerFactory.java index 457ace34..6efe19a8 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/view/tiles3/SimpleSpringPreparerFactory.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/view/tiles3/SimpleSpringPreparerFactory.java @@ -27,7 +27,7 @@ import org.apache.tiles.preparer.factory.NoSuchPreparerException; import org.springframework.web.context.WebApplicationContext; /** - * Tiles3 {@link org.apache.tiles.preparer.PreparerFactory} implementation + * Tiles {@link org.apache.tiles.preparer.PreparerFactory} implementation * that expects preparer class names and builds preparer instances for those, * creating them through the Spring ApplicationContext in order to apply * Spring container callbacks and configured Spring BeanPostProcessors. diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/view/tiles3/SpringBeanPreparerFactory.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/view/tiles3/SpringBeanPreparerFactory.java index 0e252dc2..e04afdd2 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/view/tiles3/SpringBeanPreparerFactory.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/view/tiles3/SpringBeanPreparerFactory.java @@ -22,7 +22,7 @@ import org.apache.tiles.preparer.ViewPreparer; import org.springframework.web.context.WebApplicationContext; /** - * Tiles3 {@link org.apache.tiles.preparer.PreparerFactory} implementation + * Tiles {@link org.apache.tiles.preparer.PreparerFactory} implementation * that expects preparer bean names and obtains preparer beans from the * Spring ApplicationContext. The full bean creation process will be in * the control of the Spring application context in this case, allowing diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/view/tiles3/package-info.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/view/tiles3/package-info.java index 845ac016..8934e376 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/view/tiles3/package-info.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/view/tiles3/package-info.java @@ -1,6 +1,6 @@ /** * Support classes for the integration of - * <a href="http://tiles.apache.org">Tiles3</a> + * <a href="http://tiles.apache.org">Tiles 3</a> * (the standalone version of Tiles) as Spring web view technology. * Contains a View implementation for Tiles definitions. */ diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/view/velocity/VelocityConfig.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/view/velocity/VelocityConfig.java index 9bffdadf..934883a3 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/view/velocity/VelocityConfig.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/view/velocity/VelocityConfig.java @@ -26,7 +26,9 @@ import org.apache.velocity.app.VelocityEngine; * @author Rod Johnson * @see VelocityConfigurer * @see VelocityView + * @deprecated as of Spring 4.3, in favor of FreeMarker */ +@Deprecated public interface VelocityConfig { /** diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/view/velocity/VelocityConfigurer.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/view/velocity/VelocityConfigurer.java index 1dbd8a20..5f10a43a 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/view/velocity/VelocityConfigurer.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/view/velocity/VelocityConfigurer.java @@ -25,7 +25,6 @@ import org.apache.velocity.runtime.resource.loader.ClasspathResourceLoader; import org.springframework.beans.factory.InitializingBean; import org.springframework.context.ResourceLoaderAware; -import org.springframework.ui.velocity.VelocityEngineFactory; import org.springframework.web.context.ServletContextAware; /** @@ -69,8 +68,10 @@ import org.springframework.web.context.ServletContextAware; * @see #setResourceLoaderPath * @see #setVelocityEngine * @see VelocityView + * @deprecated as of Spring 4.3, in favor of FreeMarker */ -public class VelocityConfigurer extends VelocityEngineFactory +@Deprecated +public class VelocityConfigurer extends org.springframework.ui.velocity.VelocityEngineFactory implements VelocityConfig, InitializingBean, ResourceLoaderAware, ServletContextAware { /** the name of the resource loader for Spring's bind macros */ diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/view/velocity/VelocityLayoutView.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/view/velocity/VelocityLayoutView.java index 65493af1..b5e3ae8a 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/view/velocity/VelocityLayoutView.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/view/velocity/VelocityLayoutView.java @@ -50,7 +50,9 @@ import org.springframework.core.NestedIOException; * @see #setLayoutUrl * @see #setLayoutKey * @see #setScreenContentKey + * @deprecated as of Spring 4.3, in favor of FreeMarker */ +@Deprecated public class VelocityLayoutView extends VelocityToolboxView { /** diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/view/velocity/VelocityLayoutViewResolver.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/view/velocity/VelocityLayoutViewResolver.java index 02124e0d..630dcc3e 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/view/velocity/VelocityLayoutViewResolver.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/view/velocity/VelocityLayoutViewResolver.java @@ -31,7 +31,9 @@ import org.springframework.web.servlet.view.AbstractUrlBasedView; * @see #setLayoutUrl * @see #setLayoutKey * @see #setScreenContentKey + * @deprecated as of Spring 4.3, in favor of FreeMarker */ +@Deprecated public class VelocityLayoutViewResolver extends VelocityViewResolver { private String layoutUrl; diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/view/velocity/VelocityToolboxView.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/view/velocity/VelocityToolboxView.java index ad54ffda..8513f9d5 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/view/velocity/VelocityToolboxView.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/view/velocity/VelocityToolboxView.java @@ -62,7 +62,9 @@ import org.springframework.util.ReflectionUtils; * @see #initTool * @see org.apache.velocity.tools.view.context.ViewContext * @see org.apache.velocity.tools.view.context.ChainedContext + * @deprecated as of Spring 4.3, in favor of FreeMarker */ +@Deprecated public class VelocityToolboxView extends VelocityView { private String toolboxConfigLocation; diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/view/velocity/VelocityView.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/view/velocity/VelocityView.java index 23751a5d..936ee0d8 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/view/velocity/VelocityView.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/view/velocity/VelocityView.java @@ -83,7 +83,9 @@ import org.springframework.web.util.NestedServletException; * @see #setVelocityEngine * @see VelocityConfig * @see VelocityConfigurer + * @deprecated as of Spring 4.3, in favor of FreeMarker */ +@Deprecated public class VelocityView extends AbstractTemplateView { private Map<String, Class<?>> toolAttributes; 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 e812ec92..95191a4a 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 @@ -40,7 +40,9 @@ import org.springframework.web.servlet.view.AbstractUrlBasedView; * @see #setDateToolAttribute * @see #setNumberToolAttribute * @see VelocityView + * @deprecated as of Spring 4.3, in favor of FreeMarker */ +@Deprecated public class VelocityViewResolver extends AbstractTemplateViewResolver { private String dateToolAttribute; 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 dfcf59e5..ef40ba89 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 @@ -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,7 +36,7 @@ import org.springframework.web.servlet.view.json.AbstractJackson2View; * * <p>The default constructor uses the default configuration provided by {@link Jackson2ObjectMapperBuilder}. * - * <p>Compatible with Jackson 2.1 and higher. + * <p>Compatible with Jackson 2.6 and higher, as of Spring 4.3. * * @author Sebastien Deleuze * @since 4.1 diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/view/xslt/XsltView.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/view/xslt/XsltView.java index 35587b78..8b1e1f82 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/view/xslt/XsltView.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/view/xslt/XsltView.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. @@ -73,7 +73,7 @@ import org.springframework.web.util.WebUtils; */ public class XsltView extends AbstractUrlBasedView { - private Class<?> transformerFactoryClass; + private Class<? extends TransformerFactory> transformerFactoryClass; private String sourceKey; @@ -97,8 +97,7 @@ public class XsltView extends AbstractUrlBasedView { * <p>The default constructor of the specified class will be called * to build the TransformerFactory for this view. */ - public void setTransformerFactoryClass(Class<?> transformerFactoryClass) { - Assert.isAssignable(TransformerFactory.class, transformerFactoryClass); + public void setTransformerFactoryClass(Class<? extends TransformerFactory> transformerFactoryClass) { this.transformerFactoryClass = transformerFactoryClass; } @@ -195,10 +194,10 @@ public class XsltView extends AbstractUrlBasedView { * @see #setTransformerFactoryClass * @see #getTransformerFactory() */ - protected TransformerFactory newTransformerFactory(Class<?> transformerFactoryClass) { + protected TransformerFactory newTransformerFactory(Class<? extends TransformerFactory> transformerFactoryClass) { if (transformerFactoryClass != null) { try { - return (TransformerFactory) transformerFactoryClass.newInstance(); + return transformerFactoryClass.newInstance(); } catch (Exception ex) { throw new TransformerFactoryConfigurationError(ex, "Could not instantiate TransformerFactory"); |