diff options
author | Emmanuel Bourg <ebourg@apache.org> | 2016-08-02 11:13:32 +0200 |
---|---|---|
committer | Emmanuel Bourg <ebourg@apache.org> | 2016-08-02 11:13:32 +0200 |
commit | f69f2a4b8ea697b3a631c0dc7a470e3c9793fee3 (patch) | |
tree | db2f25b29aa3e59c463ab41d3f2856f6265bb1a5 /spring-webmvc/src/main/java/org | |
parent | 5575b60c30c5a0c308c4ba3a2db93956d8c1746c (diff) |
Imported Upstream version 4.2.6
Diffstat (limited to 'spring-webmvc/src/main/java/org')
132 files changed, 6905 insertions, 2213 deletions
diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/AsyncHandlerInterceptor.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/AsyncHandlerInterceptor.java index 82c016d7..5284a2d1 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/AsyncHandlerInterceptor.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/AsyncHandlerInterceptor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 the original author or authors. + * Copyright 2002-2015 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,20 +22,34 @@ import javax.servlet.http.HttpServletResponse; import org.springframework.web.method.HandlerMethod; /** - * Extends {@code HandlerInterceptor} with a callback method invoked during - * asynchronous request handling. + * Extends {@code HandlerInterceptor} with a callback method invoked after the + * start of asynchronous request handling. * - * <p>When a handler starts asynchronous request handling, the DispatcherServlet - * exits without invoking {@code postHandle} and {@code afterCompletion}, as it - * normally does, since the results of request handling (e.g. ModelAndView) - * will. be produced concurrently in another thread. In such scenarios, - * {@link #afterConcurrentHandlingStarted(HttpServletRequest, HttpServletResponse, Object)} - * is invoked instead allowing implementations to perform tasks such as cleaning - * up thread bound attributes. + * <p>When a handler starts an asynchronous request, the {@link DispatcherServlet} + * exits without invoking {@code postHandle} and {@code afterCompletion} as it + * normally does for a synchronous request, since the result of request handling + * (e.g. ModelAndView) is likely not yet ready and will be produced concurrently + * from another thread. In such scenarios, {@link #afterConcurrentHandlingStarted} + * is invoked instead, allowing implementations to perform tasks such as cleaning + * up thread-bound attributes before releasing the thread to the Servlet container. * * <p>When asynchronous handling completes, the request is dispatched to the - * container for further processing. At this stage the DispatcherServlet invokes - * {@code preHandle}, {@code postHandle} and {@code afterCompletion} as usual. + * container for further processing. At this stage the {@code DispatcherServlet} + * invokes {@code preHandle}, {@code postHandle}, and {@code afterCompletion}. + * To distinguish between the initial request and the subsequent dispatch + * after asynchronous handling completes, interceptors can check whether the + * {@code javax.servlet.DispatcherType} of {@link javax.servlet.ServletRequest} + * is {@code "REQUEST"} or {@code "ASYNC"}. + * + * <p>Note that {@code HandlerInterceptor} implementations may need to do work + * when an async request times out or completes with a network error. For such + * cases the Servlet container does not dispatch and therefore the + * {@code postHandle} and {@code afterCompletion} methods will not be invoked. + * Instead, interceptors can register to track an asynchronous request through + * the {@code registerCallbackInterceptor} and {@code registerDeferredResultInterceptor} + * methods on {@link org.springframework.web.context.request.async.WebAsyncManager + * WebAsyncManager}. This can be done proactively on every request from + * {@code preHandle} regardless of whether async request processing will start. * * @author Rossen Stoyanchev * @since 3.2 @@ -48,14 +62,15 @@ public interface AsyncHandlerInterceptor extends HandlerInterceptor { /** * Called instead of {@code postHandle} and {@code afterCompletion}, when - * the a handler is being executed concurrently. Implementations may use the - * provided request and response but should avoid modifying them in ways - * that would conflict with the concurrent execution of the handler. A - * typical use of this method would be to clean thread local variables. + * the a handler is being executed concurrently. + * <p>Implementations may use the provided request and response but should + * avoid modifying them in ways that would conflict with the concurrent + * execution of the handler. A typical use of this method would be to + * clean up thread-local variables. * * @param request the current request * @param response the current response - * @param handler handler (or {@link HandlerMethod}) that started async + * @param handler the handler (or {@link HandlerMethod}) that started async * execution, for type and/or instance examination * @throws Exception in case of errors */ diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/DispatcherServlet.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/DispatcherServlet.java index 97c1f4f4..7fd94248 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/DispatcherServlet.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/DispatcherServlet.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * Copyright 2002-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -42,7 +42,7 @@ import org.springframework.beans.factory.NoSuchBeanDefinitionException; import org.springframework.context.ApplicationContext; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.i18n.LocaleContext; -import org.springframework.core.OrderComparator; +import org.springframework.core.annotation.AnnotationAwareOrderComparator; import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.support.PropertiesLoaderUtils; import org.springframework.http.server.ServletServerHttpRequest; @@ -206,7 +206,7 @@ public class DispatcherServlet extends FrameworkServlet { /** * Request attribute to hold the current web application context. * Otherwise only the global web app context is obtainable by tags etc. - * @see org.springframework.web.servlet.support.RequestContextUtils#getWebApplicationContext + * @see org.springframework.web.servlet.support.RequestContextUtils#findWebApplicationContext */ public static final String WEB_APPLICATION_CONTEXT_ATTRIBUTE = DispatcherServlet.class.getName() + ".CONTEXT"; @@ -328,6 +328,7 @@ public class DispatcherServlet extends FrameworkServlet { /** List of ViewResolvers used by this servlet */ private List<ViewResolver> viewResolvers; + /** * Create a new {@code DispatcherServlet} that will create its own internal web * application context based on defaults and values provided through servlet @@ -392,6 +393,7 @@ public class DispatcherServlet extends FrameworkServlet { super(webApplicationContext); } + /** * Set whether to detect all HandlerMapping beans in this servlet's context. Otherwise, * just a single bean with name "handlerMapping" will be expected. @@ -463,6 +465,7 @@ public class DispatcherServlet extends FrameworkServlet { this.cleanupAfterInclude = cleanupAfterInclude; } + /** * This implementation calls {@link #initStrategies}. */ @@ -547,9 +550,8 @@ public class DispatcherServlet extends FrameworkServlet { // We need to use the default. this.themeResolver = getDefaultStrategy(context, ThemeResolver.class); if (logger.isDebugEnabled()) { - logger.debug( - "Unable to locate ThemeResolver with name '" + THEME_RESOLVER_BEAN_NAME + "': using default [" + - this.themeResolver + "]"); + logger.debug("Unable to locate ThemeResolver with name '" + THEME_RESOLVER_BEAN_NAME + + "': using default [" + this.themeResolver + "]"); } } } @@ -569,7 +571,7 @@ public class DispatcherServlet extends FrameworkServlet { if (!matchingBeans.isEmpty()) { this.handlerMappings = new ArrayList<HandlerMapping>(matchingBeans.values()); // We keep HandlerMappings in sorted order. - OrderComparator.sort(this.handlerMappings); + AnnotationAwareOrderComparator.sort(this.handlerMappings); } } else { @@ -607,7 +609,7 @@ public class DispatcherServlet extends FrameworkServlet { if (!matchingBeans.isEmpty()) { this.handlerAdapters = new ArrayList<HandlerAdapter>(matchingBeans.values()); // We keep HandlerAdapters in sorted order. - OrderComparator.sort(this.handlerAdapters); + AnnotationAwareOrderComparator.sort(this.handlerAdapters); } } else { @@ -645,7 +647,7 @@ public class DispatcherServlet extends FrameworkServlet { if (!matchingBeans.isEmpty()) { this.handlerExceptionResolvers = new ArrayList<HandlerExceptionResolver>(matchingBeans.values()); // We keep HandlerExceptionResolvers in sorted order. - OrderComparator.sort(this.handlerExceptionResolvers); + AnnotationAwareOrderComparator.sort(this.handlerExceptionResolvers); } } else { @@ -707,7 +709,7 @@ public class DispatcherServlet extends FrameworkServlet { if (!matchingBeans.isEmpty()) { this.viewResolvers = new ArrayList<ViewResolver>(matchingBeans.values()); // We keep ViewResolvers in sorted order. - OrderComparator.sort(this.viewResolvers); + AnnotationAwareOrderComparator.sort(this.viewResolvers); } } else { @@ -737,8 +739,7 @@ public class DispatcherServlet extends FrameworkServlet { */ private void initFlashMapManager(ApplicationContext context) { try { - this.flashMapManager = - context.getBean(FLASH_MAP_MANAGER_BEAN_NAME, FlashMapManager.class); + this.flashMapManager = context.getBean(FLASH_MAP_MANAGER_BEAN_NAME, FlashMapManager.class); if (logger.isDebugEnabled()) { logger.debug("Using FlashMapManager [" + this.flashMapManager + "]"); } @@ -838,7 +839,8 @@ public class DispatcherServlet extends FrameworkServlet { /** * Create a default strategy. - * <p>The default implementation uses {@link org.springframework.beans.factory.config.AutowireCapableBeanFactory#createBean}. + * <p>The default implementation uses + * {@link org.springframework.beans.factory.config.AutowireCapableBeanFactory#createBean}. * @param context the current WebApplicationContext * @param clazz the strategy implementation class to instantiate * @return the fully configured strategy instance @@ -962,7 +964,7 @@ public class DispatcherServlet extends FrameworkServlet { return; } - applyDefaultViewName(request, mv); + applyDefaultViewName(processedRequest, mv); mappedHandler.applyPostHandle(processedRequest, response, mv); } catch (Exception ex) { @@ -1099,7 +1101,8 @@ public class DispatcherServlet extends FrameworkServlet { * @see MultipartResolver#cleanupMultipart */ protected void cleanupMultipart(HttpServletRequest request) { - MultipartHttpServletRequest multipartRequest = WebUtils.getNativeRequest(request, MultipartHttpServletRequest.class); + MultipartHttpServletRequest multipartRequest = + WebUtils.getNativeRequest(request, MultipartHttpServletRequest.class); if (multipartRequest != null) { this.multipartResolver.cleanupMultipart(multipartRequest); } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/DispatcherServlet.properties b/spring-webmvc/src/main/java/org/springframework/web/servlet/DispatcherServlet.properties deleted file mode 100644 index c8e5ab29..00000000 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/DispatcherServlet.properties +++ /dev/null @@ -1,24 +0,0 @@ -# Default implementation classes for DispatcherServlet's strategy interfaces. -# Used as fallback when no matching beans are found in the DispatcherServlet context. -# Not meant to be customized by application developers. - -org.springframework.web.servlet.LocaleResolver=org.springframework.web.servlet.i18n.AcceptHeaderLocaleResolver - -org.springframework.web.servlet.ThemeResolver=org.springframework.web.servlet.theme.FixedThemeResolver - -org.springframework.web.servlet.HandlerMapping=org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping,\ - org.springframework.web.servlet.mvc.annotation.DefaultAnnotationHandlerMapping - -org.springframework.web.servlet.HandlerAdapter=org.springframework.web.servlet.mvc.HttpRequestHandlerAdapter,\ - org.springframework.web.servlet.mvc.SimpleControllerHandlerAdapter,\ - org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter - -org.springframework.web.servlet.HandlerExceptionResolver=org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerExceptionResolver,\ - org.springframework.web.servlet.mvc.annotation.ResponseStatusExceptionResolver,\ - org.springframework.web.servlet.mvc.support.DefaultHandlerExceptionResolver - -org.springframework.web.servlet.RequestToViewNameTranslator=org.springframework.web.servlet.view.DefaultRequestToViewNameTranslator - -org.springframework.web.servlet.ViewResolver=org.springframework.web.servlet.view.InternalResourceViewResolver - -org.springframework.web.servlet.FlashMapManager=org.springframework.web.servlet.support.SessionFlashMapManager
\ No newline at end of file diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/FlashMap.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/FlashMap.java index 03a2ecfd..cc752af7 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/FlashMap.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/FlashMap.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2014 the original author or authors. + * Copyright 2002-2015 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -50,11 +50,9 @@ public final class FlashMap extends HashMap<String, Object> implements Comparabl private String targetRequestPath; - private final MultiValueMap<String, String> targetRequestParams = new LinkedMultiValueMap<String, String>(); + private final MultiValueMap<String, String> targetRequestParams = new LinkedMultiValueMap<String, String>(4); - private long expirationStartTime; - - private int timeToLive; + private long expirationTime = -1; /** @@ -112,8 +110,25 @@ public final class FlashMap extends HashMap<String, Object> implements Comparabl * @param timeToLive the number of seconds before expiration */ public void startExpirationPeriod(int timeToLive) { - this.expirationStartTime = System.currentTimeMillis(); - this.timeToLive = timeToLive; + this.expirationTime = System.currentTimeMillis() + timeToLive * 1000; + } + + /** + * Set the expiration time for the FlashMap. This is provided for serialization + * purposes but can also be used instead {@link #startExpirationPeriod(int)}. + * @since 4.2 + */ + public void setExpirationTime(long expirationTime) { + this.expirationTime = expirationTime; + } + + /** + * Return the expiration time for the FlashMap or -1 if the expiration + * period has not started. + * @since 4.2 + */ + public long getExpirationTime() { + return this.expirationTime; } /** @@ -121,8 +136,7 @@ public final class FlashMap extends HashMap<String, Object> implements Comparabl * elapsed time since the call to {@link #startExpirationPeriod}. */ public boolean isExpired() { - return (this.expirationStartTime != 0 && - (System.currentTimeMillis() - this.expirationStartTime > this.timeToLive * 1000)); + return (this.expirationTime != -1 && System.currentTimeMillis() > this.expirationTime); } @@ -167,11 +181,8 @@ public final class FlashMap extends HashMap<String, Object> implements Comparabl @Override public String toString() { - StringBuilder sb = new StringBuilder(); - sb.append("FlashMap [attributes=").append(super.toString()); - sb.append(", targetRequestPath=").append(this.targetRequestPath); - sb.append(", targetRequestParams=").append(this.targetRequestParams).append("]"); - return sb.toString(); + return "FlashMap [attributes=" + super.toString() + ", targetRequestPath=" + + this.targetRequestPath + ", targetRequestParams=" + this.targetRequestParams + "]"; } } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/FrameworkServlet.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/FrameworkServlet.java index c68d06b9..0cf705a9 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/FrameworkServlet.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/FrameworkServlet.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * Copyright 2002-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -42,11 +42,10 @@ import org.springframework.context.i18n.SimpleLocaleContext; import org.springframework.core.GenericTypeResolver; import org.springframework.core.annotation.AnnotationAwareOrderComparator; import org.springframework.core.env.ConfigurableEnvironment; -import org.springframework.util.Assert; +import org.springframework.http.HttpMethod; import org.springframework.util.ClassUtils; import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; -import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.context.ConfigurableWebApplicationContext; import org.springframework.web.context.ConfigurableWebEnvironment; import org.springframework.web.context.ContextLoader; @@ -61,6 +60,7 @@ import org.springframework.web.context.request.async.WebAsyncUtils; import org.springframework.web.context.support.ServletRequestHandledEvent; import org.springframework.web.context.support.WebApplicationContextUtils; import org.springframework.web.context.support.XmlWebApplicationContext; +import org.springframework.web.cors.CorsUtils; import org.springframework.web.util.NestedServletException; import org.springframework.web.util.WebUtils; @@ -369,9 +369,11 @@ public abstract class FrameworkServlet extends HttpServletBean implements Applic * @see #applyInitializers */ @SuppressWarnings("unchecked") - public void setContextInitializers(ApplicationContextInitializer<? extends ConfigurableApplicationContext>... initializers) { - for (ApplicationContextInitializer<? extends ConfigurableApplicationContext> initializer : initializers) { - this.contextInitializers.add((ApplicationContextInitializer<ConfigurableApplicationContext>) initializer); + public void setContextInitializers(ApplicationContextInitializer<?>... initializers) { + if (initializers != null) { + for (ApplicationContextInitializer<?> initializer : initializers) { + this.contextInitializers.add((ApplicationContextInitializer<ConfigurableApplicationContext>) initializer); + } } } @@ -734,17 +736,17 @@ public abstract class FrameworkServlet extends HttpServletBean implements Applic Class<?> initializerClass = ClassUtils.forName(className, wac.getClassLoader()); Class<?> initializerContextClass = GenericTypeResolver.resolveTypeArgument(initializerClass, ApplicationContextInitializer.class); - if (initializerContextClass != null) { - Assert.isAssignable(initializerContextClass, wac.getClass(), String.format( - "Could not add context initializer [%s] since its generic parameter [%s] " + + if (initializerContextClass != null && !initializerContextClass.isInstance(wac)) { + throw new ApplicationContextException(String.format( + "Could not apply context initializer [%s] since its generic parameter [%s] " + "is not assignable from the type of application context used by this " + - "framework servlet [%s]: ", initializerClass.getName(), initializerContextClass.getName(), + "framework servlet: [%s]", initializerClass.getName(), initializerContextClass.getName(), wac.getClass().getName())); } return BeanUtils.instantiateClass(initializerClass, ApplicationContextInitializer.class); } - catch (Exception ex) { - throw new IllegalArgumentException(String.format("Could not instantiate class [%s] specified " + + catch (ClassNotFoundException ex) { + throw new ApplicationContextException(String.format("Could not load class [%s] specified " + "via 'contextInitializerClasses' init-param", className), ex); } } @@ -834,7 +836,7 @@ public abstract class FrameworkServlet extends HttpServletBean implements Applic protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { - if (RequestMethod.PATCH.name().equalsIgnoreCase(request.getMethod())) { + if (HttpMethod.PATCH.matches(request.getMethod())) { processRequest(request, response); } else { @@ -899,7 +901,7 @@ public abstract class FrameworkServlet extends HttpServletBean implements Applic protected void doOptions(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { - if (this.dispatchOptionsRequest) { + if (this.dispatchOptionsRequest || CorsUtils.isPreFlightRequest(request)) { processRequest(request, response); if (response.containsHeader("Allow")) { // Proper OPTIONS response coming from a handler - we're done. @@ -913,7 +915,7 @@ public abstract class FrameworkServlet extends HttpServletBean implements Applic @Override public void setHeader(String name, String value) { if ("Allow".equals(name)) { - value = (StringUtils.hasLength(value) ? value + ", " : "") + RequestMethod.PATCH.name(); + value = (StringUtils.hasLength(value) ? value + ", " : "") + HttpMethod.PATCH.name(); } super.setHeader(name, value); } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/HandlerExceptionResolver.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/HandlerExceptionResolver.java index 13407758..d7d1b6d9 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/HandlerExceptionResolver.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/HandlerExceptionResolver.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 the original author or authors. + * Copyright 2002-2015 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,13 +20,13 @@ import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; /** - * Interface to be implemented by objects than can resolve exceptions thrown - * during handler mapping or execution, in the typical case to error views. - * Implementors are typically registered as beans in the application context. + * Interface to be implemented by objects that can resolve exceptions thrown during + * handler mapping or execution, in the typical case to error views. Implementors are + * typically registered as beans in the application context. * - * <p>Error views are analogous to the error page JSPs, but can be used with - * any kind of exception including any checked exception, with potentially - * fine-granular mappings for specific handlers. + * <p>Error views are analogous to JSP error pages but can be used with any kind of + * exception including any checked exception, with potentially fine-grained mappings for + * specific handlers. * * @author Juergen Hoeller * @since 22.11.2003 @@ -34,9 +34,9 @@ import javax.servlet.http.HttpServletResponse; public interface HandlerExceptionResolver { /** - * Try to resolve the given exception that got thrown during on handler execution, - * returning a ModelAndView that represents a specific error page if appropriate. - * <p>The returned ModelAndView may be {@linkplain ModelAndView#isEmpty() empty} + * Try to resolve the given exception that got thrown during handler execution, + * returning a {@link ModelAndView} that represents a specific error page if appropriate. + * <p>The returned {@code ModelAndView} may be {@linkplain ModelAndView#isEmpty() empty} * to indicate that the exception has been resolved successfully but that no view * should be rendered, for instance by setting a status code. * @param request current HTTP request @@ -44,8 +44,8 @@ public interface HandlerExceptionResolver { * @param handler the executed handler, or {@code null} if none chosen at the * time of the exception (for example, if multipart resolution failed) * @param ex the exception that got thrown during handler execution - * @return a corresponding ModelAndView to forward to, - * or {@code null} for default processing + * @return a corresponding {@code ModelAndView} to forward to, or {@code null} + * for default processing */ ModelAndView resolveException( HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex); diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/HandlerInterceptor.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/HandlerInterceptor.java index a03e5152..2aa28d5d 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/HandlerInterceptor.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/HandlerInterceptor.java @@ -77,10 +77,16 @@ public interface HandlerInterceptor { /** * Intercept the execution of a handler. Called after HandlerMapping determined * an appropriate handler object, but before HandlerAdapter invokes the handler. + * * <p>DispatcherServlet processes a handler in an execution chain, consisting * of any number of interceptors, with the handler itself at the end. * With this method, each interceptor can decide to abort the execution chain, * typically sending a HTTP error or writing a custom response. + * + * <p><strong>Note:</strong> special considerations apply for asynchronous + * request processing. For more details see + * {@link org.springframework.web.servlet.AsyncHandlerInterceptor}. + * * @param request current HTTP request * @param response current HTTP response * @param handler chosen handler to execute, for type and/or instance evaluation @@ -96,10 +102,16 @@ public interface HandlerInterceptor { * Intercept the execution of a handler. Called after HandlerAdapter actually * invoked the handler, but before the DispatcherServlet renders the view. * Can expose additional model objects to the view via the given ModelAndView. + * * <p>DispatcherServlet processes a handler in an execution chain, consisting * of any number of interceptors, with the handler itself at the end. * With this method, each interceptor can post-process an execution, * getting applied in inverse order of the execution chain. + * + * <p><strong>Note:</strong> special considerations apply for asynchronous + * request processing. For more details see + * {@link org.springframework.web.servlet.AsyncHandlerInterceptor}. + * * @param request current HTTP request * @param response current HTTP response * @param handler handler (or {@link HandlerMethod}) that started async @@ -115,11 +127,18 @@ public interface HandlerInterceptor { * Callback after completion of request processing, that is, after rendering * the view. Will be called on any outcome of handler execution, thus allows * for proper resource cleanup. + * * <p>Note: Will only be called if this interceptor's {@code preHandle} * method has successfully completed and returned {@code true}! + * * <p>As with the {@code postHandle} method, the method will be invoked on each * interceptor in the chain in reverse order, so the first interceptor will be * the last to be invoked. + * + * <p><strong>Note:</strong> special considerations apply for asynchronous + * request processing. For more details see + * {@link org.springframework.web.servlet.AsyncHandlerInterceptor}. + * * @param request current HTTP request * @param response current HTTP response * @param handler handler (or {@link HandlerMethod}) that started async diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/HandlerMapping.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/HandlerMapping.java index 791e169a..117bff35 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/HandlerMapping.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/HandlerMapping.java @@ -24,7 +24,7 @@ import javax.servlet.http.HttpServletRequest; * * <p>This class can be implemented by application developers, although this is not * necessary, as {@link org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping} - * and {@link org.springframework.web.servlet.handler.SimpleUrlHandlerMapping} + * and {@link org.springframework.web.servlet.mvc.annotation.DefaultAnnotationHandlerMapping} * are included in the framework. The former is the default if no * HandlerMapping bean is registered in the application context. * @@ -49,7 +49,7 @@ import javax.servlet.http.HttpServletRequest; * @see org.springframework.core.Ordered * @see org.springframework.web.servlet.handler.AbstractHandlerMapping * @see org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping - * @see org.springframework.web.servlet.handler.SimpleUrlHandlerMapping + * @see org.springframework.web.servlet.mvc.annotation.DefaultAnnotationHandlerMapping */ public interface HandlerMapping { diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/NoHandlerFoundException.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/NoHandlerFoundException.java index 959bba67..63f83c1c 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/NoHandlerFoundException.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/NoHandlerFoundException.java @@ -23,9 +23,10 @@ import javax.servlet.http.HttpServletResponse; import org.springframework.http.HttpHeaders; /** - * Exception to be thrown if DispatcherServlet is unable to determine a corresponding - * handler for an incoming HTTP request. The DispatcherServlet throws this exception - * only if its "throwExceptionIfNoHandlerFound" property is set to "true". + * By default when the DispatcherServlet can't find a handler for a request it + * sends a 404 response. However if its property "throwExceptionIfNoHandlerFound" + * is set to {@code true} this exception is raised and may be handled with + * a configured HandlerExceptionResolver. * * @author Brian Clozel * @since 4.0 @@ -49,12 +50,13 @@ public class NoHandlerFoundException extends ServletException { * @param headers the HTTP request headers */ public NoHandlerFoundException(String httpMethod, String requestURL, HttpHeaders headers) { - super("No handler found for " + httpMethod + " " + requestURL + ", headers=" + headers); + super("No handler found for " + httpMethod + " " + requestURL); this.httpMethod = httpMethod; this.requestURL = requestURL; this.headers = headers; } + public String getHttpMethod() { return this.httpMethod; } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/ViewRendererServlet.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/ViewRendererServlet.java index e7574d23..f634ac80 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/ViewRendererServlet.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/ViewRendererServlet.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 the original author or authors. + * Copyright 2002-2015 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -48,7 +48,7 @@ public class ViewRendererServlet extends HttpServlet { /** * Request attribute to hold current web application context. * Otherwise only the global web app context is obtainable by tags etc. - * @see org.springframework.web.servlet.support.RequestContextUtils#getWebApplicationContext + * @see org.springframework.web.servlet.support.RequestContextUtils#findWebApplicationContext */ public static final String WEB_APPLICATION_CONTEXT_ATTRIBUTE = DispatcherServlet.WEB_APPLICATION_CONTEXT_ATTRIBUTE; diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/AnnotationDrivenBeanDefinitionParser.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/AnnotationDrivenBeanDefinitionParser.java index 243a7561..7c8cbcb4 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/AnnotationDrivenBeanDefinitionParser.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/AnnotationDrivenBeanDefinitionParser.java @@ -74,6 +74,7 @@ import org.springframework.web.servlet.mvc.HttpRequestHandlerAdapter; import org.springframework.web.servlet.mvc.SimpleControllerHandlerAdapter; import org.springframework.web.servlet.mvc.annotation.ResponseStatusExceptionResolver; import org.springframework.web.servlet.mvc.method.annotation.ExceptionHandlerExceptionResolver; +import org.springframework.web.servlet.mvc.method.annotation.JsonViewRequestBodyAdvice; import org.springframework.web.servlet.mvc.method.annotation.JsonViewResponseBodyAdvice; import org.springframework.web.servlet.mvc.method.annotation.MvcUriComponentsBuilder; import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter; @@ -202,6 +203,9 @@ class AnnotationDrivenBeanDefinitionParser implements BeanDefinitionParser { configurePathMatchingProperties(handlerMappingDef, element, parserContext); readerContext.getRegistry().registerBeanDefinition(HANDLER_MAPPING_BEAN_NAME , handlerMappingDef); + RuntimeBeanReference corsConfigurationsRef = MvcNamespaceUtils.registerCorsConfigurations(null, parserContext, source); + handlerMappingDef.getPropertyValues().add("corsConfigurations", corsConfigurationsRef); + RuntimeBeanReference conversionService = getConversionService(element, source, parserContext); RuntimeBeanReference validator = getValidator(element, source, parserContext); RuntimeBeanReference messageCodesResolver = getMessageCodesResolver(element); @@ -227,6 +231,7 @@ class AnnotationDrivenBeanDefinitionParser implements BeanDefinitionParser { handlerAdapterDef.getPropertyValues().add("contentNegotiationManager", contentNegotiationManager); handlerAdapterDef.getPropertyValues().add("webBindingInitializer", bindingDef); handlerAdapterDef.getPropertyValues().add("messageConverters", messageConverters); + addRequestBodyAdvice(handlerAdapterDef); addResponseBodyAdvice(handlerAdapterDef); if (element.hasAttribute("ignore-default-model-on-redirect")) { @@ -313,6 +318,13 @@ class AnnotationDrivenBeanDefinitionParser implements BeanDefinitionParser { return null; } + protected void addRequestBodyAdvice(RootBeanDefinition beanDef) { + if (jackson2Present) { + beanDef.getPropertyValues().add("requestBodyAdvice", + new RootBeanDefinition(JsonViewRequestBodyAdvice.class)); + } + } + protected void addResponseBodyAdvice(RootBeanDefinition beanDef) { if (jackson2Present) { beanDef.getPropertyValues().add("responseBodyAdvice", diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/CorsBeanDefinitionParser.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/CorsBeanDefinitionParser.java new file mode 100644 index 00000000..c62f70dc --- /dev/null +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/CorsBeanDefinitionParser.java @@ -0,0 +1,120 @@ +/* + * Copyright 2002-2015 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.web.servlet.config; + +import java.util.Arrays; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import org.w3c.dom.Element; + +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.xml.BeanDefinitionParser; +import org.springframework.beans.factory.xml.ParserContext; +import org.springframework.http.HttpMethod; +import org.springframework.util.StringUtils; +import org.springframework.util.xml.DomUtils; +import org.springframework.web.cors.CorsConfiguration; + +/** + * {@link org.springframework.beans.factory.xml.BeanDefinitionParser} that parses a + * {@code cors} element in order to set the CORS configuration in the various + * {AbstractHandlerMapping} beans created by {@link AnnotationDrivenBeanDefinitionParser}, + * {@link ResourcesBeanDefinitionParser} and {@link ViewControllerBeanDefinitionParser}. + * + * @author Sebastien Deleuze + * @since 4.2 + */ +public class CorsBeanDefinitionParser implements BeanDefinitionParser { + + private static final List<String> DEFAULT_ALLOWED_ORIGINS = Arrays.asList("*"); + + private static final List<String> DEFAULT_ALLOWED_METHODS = + Arrays.asList(HttpMethod.GET.name(), HttpMethod.HEAD.name(), HttpMethod.POST.name()); + + private static final List<String> DEFAULT_ALLOWED_HEADERS = Arrays.asList("*"); + + private static final boolean DEFAULT_ALLOW_CREDENTIALS = true; + + private static final long DEFAULT_MAX_AGE = 1600; + + + @Override + public BeanDefinition parse(Element element, ParserContext parserContext) { + + Map<String, CorsConfiguration> corsConfigurations = new LinkedHashMap<String, CorsConfiguration>(); + List<Element> mappings = DomUtils.getChildElementsByTagName(element, "mapping"); + + if (mappings.isEmpty()) { + CorsConfiguration config = new CorsConfiguration(); + config.setAllowedOrigins(DEFAULT_ALLOWED_ORIGINS); + config.setAllowedMethods(DEFAULT_ALLOWED_METHODS); + config.setAllowedHeaders(DEFAULT_ALLOWED_HEADERS); + config.setAllowCredentials(DEFAULT_ALLOW_CREDENTIALS); + config.setMaxAge(DEFAULT_MAX_AGE); + corsConfigurations.put("/**", config); + } + else { + for (Element mapping : mappings) { + CorsConfiguration config = new CorsConfiguration(); + if (mapping.hasAttribute("allowed-origins")) { + String[] allowedOrigins = StringUtils.tokenizeToStringArray(mapping.getAttribute("allowed-origins"), ","); + config.setAllowedOrigins(Arrays.asList(allowedOrigins)); + } + else { + config.setAllowedOrigins(DEFAULT_ALLOWED_ORIGINS); + } + if (mapping.hasAttribute("allowed-methods")) { + String[] allowedMethods = StringUtils.tokenizeToStringArray(mapping.getAttribute("allowed-methods"), ","); + config.setAllowedMethods(Arrays.asList(allowedMethods)); + } + else { + config.setAllowedMethods(DEFAULT_ALLOWED_METHODS); + } + if (mapping.hasAttribute("allowed-headers")) { + String[] allowedHeaders = StringUtils.tokenizeToStringArray(mapping.getAttribute("allowed-headers"), ","); + config.setAllowedHeaders(Arrays.asList(allowedHeaders)); + } + else { + config.setAllowedHeaders(DEFAULT_ALLOWED_HEADERS); + } + if (mapping.hasAttribute("exposed-headers")) { + String[] exposedHeaders = StringUtils.tokenizeToStringArray(mapping.getAttribute("exposed-headers"), ","); + config.setExposedHeaders(Arrays.asList(exposedHeaders)); + } + if (mapping.hasAttribute("allow-credentials")) { + config.setAllowCredentials(Boolean.parseBoolean(mapping.getAttribute("allow-credentials"))); + } + else { + config.setAllowCredentials(DEFAULT_ALLOW_CREDENTIALS); + } + if (mapping.hasAttribute("max-age")) { + config.setMaxAge(Long.parseLong(mapping.getAttribute("max-age"))); + } + else { + config.setMaxAge(DEFAULT_MAX_AGE); + } + corsConfigurations.put(mapping.getAttribute("path"), config); + } + } + + MvcNamespaceUtils.registerCorsConfigurations(corsConfigurations, parserContext, parserContext.extractSource(element)); + return null; + } + +} diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/MvcNamespaceHandler.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/MvcNamespaceHandler.java index 03edc267..1cfe9108 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/MvcNamespaceHandler.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/MvcNamespaceHandler.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2014 the original author or authors. + * Copyright 2002-2015 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -24,6 +24,7 @@ import org.springframework.beans.factory.xml.NamespaceHandlerSupport; * * @author Keith Donald * @author Jeremy Grelle + * @author Sebastien Deleuze * @since 3.0 */ public class MvcNamespaceHandler extends NamespaceHandlerSupport { @@ -42,6 +43,8 @@ public class MvcNamespaceHandler extends NamespaceHandlerSupport { registerBeanDefinitionParser("freemarker-configurer", new FreeMarkerConfigurerBeanDefinitionParser()); registerBeanDefinitionParser("velocity-configurer", new VelocityConfigurerBeanDefinitionParser()); registerBeanDefinitionParser("groovy-configurer", new GroovyMarkupConfigurerBeanDefinitionParser()); + registerBeanDefinitionParser("script-template-configurer", new ScriptTemplateConfigurerBeanDefinitionParser()); + registerBeanDefinitionParser("cors", new CorsBeanDefinitionParser()); } } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/MvcNamespaceUtils.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/MvcNamespaceUtils.java index 30e1688d..e6684136 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/MvcNamespaceUtils.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/MvcNamespaceUtils.java @@ -16,6 +16,9 @@ package org.springframework.web.servlet.config; +import java.util.LinkedHashMap; +import java.util.Map; + import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.config.RuntimeBeanReference; import org.springframework.beans.factory.parsing.BeanComponentDefinition; @@ -23,6 +26,7 @@ import org.springframework.beans.factory.support.RootBeanDefinition; import org.springframework.beans.factory.xml.ParserContext; import org.springframework.util.AntPathMatcher; import org.springframework.util.PathMatcher; +import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping; import org.springframework.web.servlet.mvc.HttpRequestHandlerAdapter; import org.springframework.web.servlet.mvc.SimpleControllerHandlerAdapter; @@ -50,6 +54,8 @@ abstract class MvcNamespaceUtils { private static final String PATH_MATCHER_BEAN_NAME = "mvcPathMatcher"; + private static final String CORS_CONFIGURATION_BEAN_NAME = "mvcCorsConfigurations"; + public static void registerDefaultComponents(ParserContext parserContext, Object source) { registerBeanNameUrlHandlerMapping(parserContext, source); @@ -113,6 +119,8 @@ abstract class MvcNamespaceUtils { beanNameMappingDef.setSource(source); beanNameMappingDef.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); beanNameMappingDef.getPropertyValues().add("order", 2); // consistent with WebMvcConfigurationSupport + RuntimeBeanReference corsConfigurationsRef = MvcNamespaceUtils.registerCorsConfigurations(null, parserContext, source); + beanNameMappingDef.getPropertyValues().add("corsConfigurations", corsConfigurationsRef); parserContext.getRegistry().registerBeanDefinition(BEAN_NAME_URL_HANDLER_MAPPING_BEAN_NAME, beanNameMappingDef); parserContext.registerComponent(new BeanComponentDefinition(beanNameMappingDef, BEAN_NAME_URL_HANDLER_MAPPING_BEAN_NAME)); } @@ -146,4 +154,28 @@ abstract class MvcNamespaceUtils { } } + /** + * Registers a {@code Map<String, CorsConfiguration>} (mapped {@code CorsConfiguration}s) + * under a well-known name unless already registered. The bean definition may be updated + * if a non-null CORS configuration is provided. + * @return a RuntimeBeanReference to this {@code Map<String, CorsConfiguration>} instance + */ + public static RuntimeBeanReference registerCorsConfigurations(Map<String, CorsConfiguration> corsConfigurations, ParserContext parserContext, Object source) { + if (!parserContext.getRegistry().containsBeanDefinition(CORS_CONFIGURATION_BEAN_NAME)) { + RootBeanDefinition corsConfigurationsDef = new RootBeanDefinition(LinkedHashMap.class); + corsConfigurationsDef.setSource(source); + corsConfigurationsDef.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); + if (corsConfigurations != null) { + corsConfigurationsDef.getConstructorArgumentValues().addIndexedArgumentValue(0, corsConfigurations); + } + parserContext.getReaderContext().getRegistry().registerBeanDefinition(CORS_CONFIGURATION_BEAN_NAME, corsConfigurationsDef); + parserContext.registerComponent(new BeanComponentDefinition(corsConfigurationsDef, CORS_CONFIGURATION_BEAN_NAME)); + } + else if (corsConfigurations != null) { + BeanDefinition corsConfigurationsDef = parserContext.getRegistry().getBeanDefinition(CORS_CONFIGURATION_BEAN_NAME); + corsConfigurationsDef.getConstructorArgumentValues().addIndexedArgumentValue(0, corsConfigurations); + } + return new RuntimeBeanReference(CORS_CONFIGURATION_BEAN_NAME); + } + } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/ResourcesBeanDefinitionParser.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/ResourcesBeanDefinitionParser.java index 26322bf7..f55e287a 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/ResourcesBeanDefinitionParser.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/ResourcesBeanDefinitionParser.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2014 the original author or authors. + * Copyright 2002-2015 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,6 +18,7 @@ package org.springframework.web.servlet.config; import java.util.Arrays; import java.util.Map; +import java.util.concurrent.TimeUnit; import org.w3c.dom.Element; @@ -32,8 +33,10 @@ import org.springframework.beans.factory.xml.BeanDefinitionParser; import org.springframework.beans.factory.xml.ParserContext; import org.springframework.cache.concurrent.ConcurrentMapCache; import org.springframework.core.Ordered; +import org.springframework.util.ClassUtils; import org.springframework.util.StringUtils; import org.springframework.util.xml.DomUtils; +import org.springframework.http.CacheControl; import org.springframework.web.servlet.handler.MappedInterceptor; import org.springframework.web.servlet.handler.SimpleUrlHandlerMapping; import org.springframework.web.servlet.mvc.HttpRequestHandlerAdapter; @@ -49,6 +52,7 @@ import org.springframework.web.servlet.resource.ResourceTransformer; import org.springframework.web.servlet.resource.ResourceUrlProvider; import org.springframework.web.servlet.resource.ResourceUrlProviderExposingInterceptor; import org.springframework.web.servlet.resource.VersionResourceResolver; +import org.springframework.web.servlet.resource.WebJarsResourceResolver; /** * {@link org.springframework.beans.factory.xml.BeanDefinitionParser} that parses a @@ -76,6 +80,9 @@ class ResourcesBeanDefinitionParser implements BeanDefinitionParser { private static final String RESOURCE_URL_PROVIDER = "mvcResourceUrlProvider"; + private static final boolean isWebJarsAssetLocatorPresent = ClassUtils.isPresent( + "org.webjars.WebJarAssetLocator", ResourcesBeanDefinitionParser.class.getClassLoader()); + @Override public BeanDefinition parse(Element element, ParserContext parserContext) { @@ -109,6 +116,9 @@ class ResourcesBeanDefinitionParser implements BeanDefinitionParser { // Use a default of near-lowest precedence, still allowing for even lower precedence in other mappings handlerMappingDef.getPropertyValues().add("order", StringUtils.hasText(order) ? order : Ordered.LOWEST_PRECEDENCE - 1); + RuntimeBeanReference corsConfigurationsRef = MvcNamespaceUtils.registerCorsConfigurations(null, parserContext, source); + handlerMappingDef.getPropertyValues().add("corsConfigurations", corsConfigurationsRef); + String beanName = parserContext.getReaderContext().generateBeanName(handlerMappingDef); parserContext.getRegistry().registerBeanDefinition(beanName, handlerMappingDef); parserContext.registerComponent(new BeanComponentDefinition(handlerMappingDef, beanName)); @@ -162,6 +172,12 @@ class ResourcesBeanDefinitionParser implements BeanDefinitionParser { resourceHandlerDef.getPropertyValues().add("cacheSeconds", cacheSeconds); } + Element cacheControlElement = DomUtils.getChildElementByTagName(element, "cache-control"); + if (cacheControlElement != null) { + CacheControl cacheControl = parseCacheControl(cacheControlElement); + resourceHandlerDef.getPropertyValues().add("cacheControl", cacheControl); + } + Element resourceChainElement = DomUtils.getChildElementByTagName(element, "resource-chain"); if (resourceChainElement != null) { parseResourceChain(resourceHandlerDef, parserContext, resourceChainElement, source); @@ -197,6 +213,38 @@ class ResourcesBeanDefinitionParser implements BeanDefinitionParser { } } + private CacheControl parseCacheControl(Element element) { + CacheControl cacheControl = CacheControl.empty(); + if ("true".equals(element.getAttribute("no-cache"))) { + cacheControl = CacheControl.noCache(); + } + else if ("true".equals(element.getAttribute("no-store"))) { + cacheControl = CacheControl.noStore(); + } + else if (element.hasAttribute("max-age")) { + cacheControl = CacheControl.maxAge(Long.parseLong(element.getAttribute("max-age")), TimeUnit.SECONDS); + } + if ("true".equals(element.getAttribute("must-revalidate"))) { + cacheControl = cacheControl.mustRevalidate(); + } + if ("true".equals(element.getAttribute("no-transform"))) { + cacheControl = cacheControl.noTransform(); + } + if ("true".equals(element.getAttribute("cache-public"))) { + cacheControl = cacheControl.cachePublic(); + } + if ("true".equals(element.getAttribute("cache-private"))) { + cacheControl = cacheControl.cachePrivate(); + } + if ("true".equals(element.getAttribute("proxy-revalidate"))) { + cacheControl = cacheControl.proxyRevalidate(); + } + if (element.hasAttribute("s-maxage")) { + cacheControl = cacheControl.sMaxAge(Long.parseLong(element.getAttribute("s-maxage")), TimeUnit.SECONDS); + } + return cacheControl; + } + private void parseResourceCache(ManagedList<? super Object> resourceResolvers, ManagedList<? super Object> resourceTransformers, Element element, Object source) { @@ -262,6 +310,12 @@ class ResourcesBeanDefinitionParser implements BeanDefinitionParser { } if (isAutoRegistration) { + if (isWebJarsAssetLocatorPresent) { + RootBeanDefinition webJarsResolverDef = new RootBeanDefinition(WebJarsResourceResolver.class); + webJarsResolverDef.setSource(source); + webJarsResolverDef.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); + resourceResolvers.add(webJarsResolverDef); + } RootBeanDefinition pathResolverDef = new RootBeanDefinition(PathResourceResolver.class); pathResolverDef.setSource(source); pathResolverDef.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/ScriptTemplateConfigurerBeanDefinitionParser.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/ScriptTemplateConfigurerBeanDefinitionParser.java new file mode 100644 index 00000000..13a71ac1 --- /dev/null +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/ScriptTemplateConfigurerBeanDefinitionParser.java @@ -0,0 +1,91 @@ +/* + * Copyright 2002-2015 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.web.servlet.config; + +import java.nio.charset.Charset; +import java.util.ArrayList; +import java.util.List; + +import org.w3c.dom.Element; + +import org.springframework.beans.factory.support.AbstractBeanDefinition; +import org.springframework.beans.factory.support.BeanDefinitionBuilder; +import org.springframework.beans.factory.xml.AbstractSimpleBeanDefinitionParser; +import org.springframework.beans.factory.xml.ParserContext; +import org.springframework.util.xml.DomUtils; + +/** + * Parse the <mvc:script-template-configurer> MVC namespace element and register a + * {@code ScriptTemplateConfigurer} bean. + * + * @author Sebastien Deleuze + * @since 4.2 + */ +public class ScriptTemplateConfigurerBeanDefinitionParser extends AbstractSimpleBeanDefinitionParser { + + public static final String BEAN_NAME = "mvcScriptTemplateConfigurer"; + + + @Override + protected String resolveId(Element element, AbstractBeanDefinition definition, ParserContext parserContext) { + return BEAN_NAME; + } + + @Override + protected String getBeanClassName(Element element) { + return "org.springframework.web.servlet.view.script.ScriptTemplateConfigurer"; + } + + @Override + protected void doParse(Element element, ParserContext parserContext, BeanDefinitionBuilder builder) { + List<Element> childElements = DomUtils.getChildElementsByTagName(element, "script"); + if (!childElements.isEmpty()) { + List<String> locations = new ArrayList<String>(childElements.size()); + for (Element childElement : childElements) { + locations.add(childElement.getAttribute("location")); + } + builder.addPropertyValue("scripts", locations.toArray(new String[locations.size()])); + } + builder.addPropertyValue("engineName", element.getAttribute("engine-name")); + if (element.hasAttribute("render-object")) { + builder.addPropertyValue("renderObject", element.getAttribute("render-object")); + } + if (element.hasAttribute("render-function")) { + builder.addPropertyValue("renderFunction", element.getAttribute("render-function")); + } + if (element.hasAttribute("content-type")) { + builder.addPropertyValue("contentType", element.getAttribute("content-type")); + } + if (element.hasAttribute("charset")) { + builder.addPropertyValue("charset", Charset.forName(element.getAttribute("charset"))); + } + if (element.hasAttribute("resource-loader-path")) { + builder.addPropertyValue("resourceLoaderPath", element.getAttribute("resource-loader-path")); + } + if (element.hasAttribute("shared-engine")) { + builder.addPropertyValue("sharedEngine", element.getAttribute("shared-engine")); + } + } + + @Override + protected boolean isEligibleAttribute(String name) { + return (name.equals("engine-name") || name.equals("scripts") || name.equals("render-object") || + name.equals("render-function") || name.equals("content-type") || + name.equals("charset") || name.equals("resource-loader-path")); + } + +} diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/TilesConfigurerBeanDefinitionParser.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/TilesConfigurerBeanDefinitionParser.java index 890e5333..3b607993 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/TilesConfigurerBeanDefinitionParser.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/TilesConfigurerBeanDefinitionParser.java @@ -32,6 +32,7 @@ import org.springframework.util.xml.DomUtils; * a corresponding TilesConfigurer bean. * @author Rossen Stoyanchev + * @author Juergen Hoeller * @since 4.1 */ public class TilesConfigurerBeanDefinitionParser extends AbstractSingleBeanDefinitionParser { @@ -65,6 +66,12 @@ public class TilesConfigurerBeanDefinitionParser extends AbstractSingleBeanDefin if (element.hasAttribute("validate-definitions")) { builder.addPropertyValue("validateDefinitions", element.getAttribute("validate-definitions")); } + if (element.hasAttribute("definitions-factory")) { + builder.addPropertyValue("definitionsFactoryClass", element.getAttribute("definitions-factory")); + } + if (element.hasAttribute("preparer-factory")) { + builder.addPropertyValue("preparerFactoryClass", element.getAttribute("preparer-factory")); + } } } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/ViewControllerBeanDefinitionParser.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/ViewControllerBeanDefinitionParser.java index 9025ff1f..06d61dc1 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/ViewControllerBeanDefinitionParser.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/ViewControllerBeanDefinitionParser.java @@ -21,6 +21,7 @@ import java.util.Map; import org.w3c.dom.Element; import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.config.RuntimeBeanReference; import org.springframework.beans.factory.parsing.BeanComponentDefinition; import org.springframework.beans.factory.support.ManagedMap; import org.springframework.beans.factory.support.RootBeanDefinition; @@ -125,6 +126,8 @@ class ViewControllerBeanDefinitionParser implements BeanDefinitionParser { beanDef.getPropertyValues().add("order", "1"); beanDef.getPropertyValues().add("pathMatcher", MvcNamespaceUtils.registerPathMatcher(null, context, source)); beanDef.getPropertyValues().add("urlPathHelper", MvcNamespaceUtils.registerUrlPathHelper(null, context, source)); + RuntimeBeanReference corsConfigurationsRef = MvcNamespaceUtils.registerCorsConfigurations(null, context, source); + beanDef.getPropertyValues().add("corsConfigurations", corsConfigurationsRef); return beanDef; } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/ViewResolversBeanDefinitionParser.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/ViewResolversBeanDefinitionParser.java index 7f07ed9e..06e645f5 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/ViewResolversBeanDefinitionParser.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/ViewResolversBeanDefinitionParser.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2014 the original author or authors. + * Copyright 2002-2015 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -37,6 +37,7 @@ import org.springframework.web.servlet.view.InternalResourceViewResolver; import org.springframework.web.servlet.view.ViewResolverComposite; import org.springframework.web.servlet.view.freemarker.FreeMarkerViewResolver; import org.springframework.web.servlet.view.groovy.GroovyMarkupViewResolver; +import org.springframework.web.servlet.view.script.ScriptTemplateViewResolver; import org.springframework.web.servlet.view.tiles3.TilesViewResolver; import org.springframework.web.servlet.view.velocity.VelocityViewResolver; @@ -60,6 +61,8 @@ import org.springframework.web.servlet.view.velocity.VelocityViewResolver; * @see TilesConfigurerBeanDefinitionParser * @see FreeMarkerConfigurerBeanDefinitionParser * @see VelocityConfigurerBeanDefinitionParser + * @see GroovyMarkupConfigurerBeanDefinitionParser + * @see ScriptTemplateConfigurerBeanDefinitionParser */ public class ViewResolversBeanDefinitionParser implements BeanDefinitionParser { @@ -72,7 +75,7 @@ public class ViewResolversBeanDefinitionParser implements BeanDefinitionParser { ManagedList<Object> resolvers = new ManagedList<Object>(4); resolvers.setSource(context.extractSource(element)); - String[] names = new String[] {"jsp", "tiles", "bean-name", "freemarker", "velocity", "groovy", "bean", "ref"}; + String[] names = new String[] {"jsp", "tiles", "bean-name", "freemarker", "velocity", "groovy", "script-template", "bean", "ref"}; for (Element resolverElement : DomUtils.getChildElementsByTagName(element, names)) { String name = resolverElement.getLocalName(); @@ -106,6 +109,10 @@ public class ViewResolversBeanDefinitionParser implements BeanDefinitionParser { resolverBeanDef.getPropertyValues().add("suffix", ".tpl"); addUrlBasedViewResolverProperties(resolverElement, resolverBeanDef); } + else if ("script-template".equals(name)) { + resolverBeanDef = new RootBeanDefinition(ScriptTemplateViewResolver.class); + addUrlBasedViewResolverProperties(resolverElement, resolverBeanDef); + } else if ("bean-name".equals(name)) { resolverBeanDef = new RootBeanDefinition(BeanNameViewResolver.class); } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/ContentNegotiationConfigurer.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/ContentNegotiationConfigurer.java index 946fda52..c22dda73 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/ContentNegotiationConfigurer.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/ContentNegotiationConfigurer.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2014 the original author or authors. + * Copyright 2002-2015 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -23,21 +23,70 @@ import org.springframework.http.MediaType; import org.springframework.web.accept.ContentNegotiationManager; import org.springframework.web.accept.ContentNegotiationManagerFactoryBean; import org.springframework.web.accept.ContentNegotiationStrategy; +import org.springframework.web.accept.FixedContentNegotiationStrategy; +import org.springframework.web.accept.HeaderContentNegotiationStrategy; +import org.springframework.web.accept.ParameterContentNegotiationStrategy; +import org.springframework.web.accept.PathExtensionContentNegotiationStrategy; /** - * Helps with configuring a {@link ContentNegotiationManager}. + * Creates a {@code ContentNegotiationManager} and configures it with + * one or more {@link ContentNegotiationStrategy} instances. The following shows + * the resulting strategy instances, the methods used to configured them, and + * whether enabled by default: * - * <p>By default strategies for checking the extension of the request path and - * the {@code Accept} header are registered. The path extension check will perform - * lookups through the {@link ServletContext} and the Java Activation Framework - * (if present) unless {@linkplain #mediaTypes(Map) media types} are configured. + * <table> + * <tr> + * <th>Configurer Property</th> + * <th>Underlying Strategy</th> + * <th>Default Setting</th> + * </tr> + * <tr> + * <td>{@link #favorPathExtension}</td> + * <td>{@link PathExtensionContentNegotiationStrategy Path Extension strategy}</td> + * <td>On</td> + * </tr> + * <tr> + * <td>{@link #favorParameter}</td> + * <td>{@link ParameterContentNegotiationStrategy Parameter strategy}</td> + * <td>Off</td> + * </tr> + * <tr> + * <td>{@link #ignoreAcceptHeader}</td> + * <td>{@link HeaderContentNegotiationStrategy Header strategy}</td> + * <td>On</td> + * </tr> + * <tr> + * <td>{@link #defaultContentType}</td> + * <td>{@link FixedContentNegotiationStrategy Fixed content strategy}</td> + * <td>Not set</td> + * </tr> + * <tr> + * <td>{@link #defaultContentTypeStrategy}</td> + * <td>{@link ContentNegotiationStrategy}</td> + * <td>Not set</td> + * </tr> + * </table> + * + * <p>The order in which strategies are configured is fixed. You can only turn + * them on or off. + * + * <p>For the path extension and parameter strategies you may explicitly add + * {@link #mediaType MediaType mappings}. Those will be used to resolve path + * extensions and/or a query parameter value such as "json" to a concrete media + * type such as "application/json". + * + * <p>The path extension strategy will also use {@link ServletContext#getMimeType} + * and the Java Activation framework (JAF), if available, to resolve a path + * extension to a MediaType. You may however {@link #useJaf suppress} the use + * of JAF. * * @author Rossen Stoyanchev * @since 3.2 */ public class ContentNegotiationConfigurer { - private final ContentNegotiationManagerFactoryBean factoryBean = new ContentNegotiationManagerFactoryBean(); + private final ContentNegotiationManagerFactoryBean factory = + new ContentNegotiationManagerFactoryBean(); private final Map<String, MediaType> mediaTypes = new HashMap<String, MediaType>(); @@ -46,18 +95,19 @@ public class ContentNegotiationConfigurer { * Class constructor with {@link javax.servlet.ServletContext}. */ public ContentNegotiationConfigurer(ServletContext servletContext) { - this.factoryBean.setServletContext(servletContext); + this.factory.setServletContext(servletContext); } + /** - * Indicate whether the extension of the request path should be used to determine - * the requested media type with the <em>highest priority</em>. - * <p>By default this value is set to {@code true} in which case a request + * Whether the path extension in the URL path should be used to determine + * the requested media type. + * <p>By default this is set to {@code true} in which case a request * for {@code /hotels.pdf} will be interpreted as a request for - * {@code "application/pdf"} regardless of the {@code Accept} header. + * {@code "application/pdf"} regardless of the 'Accept' header. */ public ContentNegotiationConfigurer favorPathExtension(boolean favorPathExtension) { - this.factoryBean.setFavorPathExtension(favorPathExtension); + this.factory.setFavorPathExtension(favorPathExtension); return this; } @@ -82,7 +132,9 @@ public class ContentNegotiationConfigurer { } /** - * An alternative to {@link #mediaType} with a Map of registrations to add. + * An alternative to {@link #mediaType}. + * @see #mediaType(String, MediaType) + * @see #replaceMediaTypes(Map) */ public ContentNegotiationConfigurer mediaTypes(Map<String, MediaType> mediaTypes) { if (mediaTypes != null) { @@ -92,9 +144,9 @@ public class ContentNegotiationConfigurer { } /** - * Add mappings from file extensions to media types replacing any previous mappings. - * <p>If this property is not set, the Java Action Framework, if available, may - * still be used in conjunction with {@link #favorPathExtension(boolean)}. + * Similar to {@link #mediaType} but for replacing existing mappings. + * @see #mediaType(String, MediaType) + * @see #mediaTypes(Map) */ public ContentNegotiationConfigurer replaceMediaTypes(Map<String, MediaType> mediaTypes) { this.mediaTypes.clear(); @@ -103,101 +155,84 @@ public class ContentNegotiationConfigurer { } /** - * Whether to ignore requests that have a file extension that does not match - * any mapped media types. Setting this to {@code false} will result in a - * {@code HttpMediaTypeNotAcceptableException} when there is no match. - * + * Whether to ignore requests with path extension that cannot be resolved + * to any media type. Setting this to {@code false} will result in an + * {@code HttpMediaTypeNotAcceptableException} if there is no match. * <p>By default this is set to {@code true}. */ public ContentNegotiationConfigurer ignoreUnknownPathExtensions(boolean ignore) { - this.factoryBean.setIgnoreUnknownPathExtensions(ignore); + this.factory.setIgnoreUnknownPathExtensions(ignore); return this; } /** - * Indicate whether to use the Java Activation Framework as a fallback option - * to map from file extensions to media types. This is used only when - * {@link #favorPathExtension(boolean)} is set to {@code true}. - * <p>The default value is {@code true}. - * @see #parameterName - * @see #mediaTypes(Map) + * When {@link #favorPathExtension} is set, this property determines whether + * to allow use of JAF (Java Activation Framework) to resolve a path + * extension to a specific MediaType. + * <p>By default this is not set in which case + * {@code PathExtensionContentNegotiationStrategy} will use JAF if available. */ public ContentNegotiationConfigurer useJaf(boolean useJaf) { - this.factoryBean.setUseJaf(useJaf); + this.factory.setUseJaf(useJaf); return this; } /** - * Indicate whether a request parameter should be used to determine the - * requested media type with the <em>2nd highest priority</em>, i.e. - * after path extensions but before the {@code Accept} header. - * <p>The default value is {@code false}. If set to to {@code true}, a request - * for {@code /hotels?format=pdf} will be interpreted as a request for - * {@code "application/pdf"} regardless of the {@code Accept} header. - * <p>To use this option effectively you must also configure the MediaType - * type mappings via {@link #mediaTypes(Map)}. + * Whether a request parameter ("format" by default) should be used to + * determine the requested media type. For this option to work you must + * register {@link #mediaType(String, MediaType) media type mappings}. + * <p>By default this is set to {@code false}. * @see #parameterName(String) */ public ContentNegotiationConfigurer favorParameter(boolean favorParameter) { - this.factoryBean.setFavorParameter(favorParameter); + this.factory.setFavorParameter(favorParameter); return this; } /** - * Set the parameter name that can be used to determine the requested media type - * if the {@link #favorParameter(boolean)} property is {@code true}. + * Set the query parameter name to use when {@link #favorParameter} is on. * <p>The default parameter name is {@code "format"}. */ public ContentNegotiationConfigurer parameterName(String parameterName) { - this.factoryBean.setParameterName(parameterName); + this.factory.setParameterName(parameterName); return this; } /** - * Indicate whether the HTTP {@code Accept} header should be ignored altogether. - * If set the {@code Accept} header is checked at the - * <em>3rd highest priority</em>, i.e. after the request path extension and - * possibly a request parameter if configured. + * Whether to disable checking the 'Accept' request header. * <p>By default this value is set to {@code false}. */ public ContentNegotiationConfigurer ignoreAcceptHeader(boolean ignoreAcceptHeader) { - this.factoryBean.setIgnoreAcceptHeader(ignoreAcceptHeader); + this.factory.setIgnoreAcceptHeader(ignoreAcceptHeader); return this; } /** - * Set the default content type to use when no content type was requested. - * <p>Note that internally this method creates and adds a - * {@link org.springframework.web.accept.FixedContentNegotiationStrategy - * FixedContentNegotiationStrategy}. Alternatively you can also provide a - * custom strategy via {@link #defaultContentTypeStrategy}. + * Set the default content type to use when no content type is requested. + * <p>By default this is not set. + * @see #defaultContentTypeStrategy */ public ContentNegotiationConfigurer defaultContentType(MediaType defaultContentType) { - this.factoryBean.setDefaultContentType(defaultContentType); + this.factory.setDefaultContentType(defaultContentType); return this; } /** - * Configure a custom {@link ContentNegotiationStrategy} to use to determine - * the default content type to use when no content type was requested. - * <p>However also consider using {@link #defaultContentType} which provides - * a simpler alternative to doing the same. + * Set a custom {@link ContentNegotiationStrategy} to use to determine + * the content type to use when no content type is requested. + * <p>By default this is not set. + * @see #defaultContentType * @since 4.1.2 */ public ContentNegotiationConfigurer defaultContentTypeStrategy(ContentNegotiationStrategy defaultStrategy) { - this.factoryBean.setDefaultContentTypeStrategy(defaultStrategy); + this.factory.setDefaultContentTypeStrategy(defaultStrategy); return this; } - /** - * Return the configured {@link ContentNegotiationManager} instance - */ protected ContentNegotiationManager getContentNegotiationManager() throws Exception { - if (!this.mediaTypes.isEmpty()) { - this.factoryBean.addMediaTypes(mediaTypes); - } - this.factoryBean.afterPropertiesSet(); - return this.factoryBean.getObject(); + this.factory.addMediaTypes(this.mediaTypes); + this.factory.afterPropertiesSet(); + return this.factory.getObject(); } } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/CorsRegistration.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/CorsRegistration.java new file mode 100644 index 00000000..75d315b5 --- /dev/null +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/CorsRegistration.java @@ -0,0 +1,99 @@ +/* + * Copyright 2002-2015 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.web.servlet.config.annotation; + +import java.util.ArrayList; +import java.util.Arrays; + +import org.springframework.http.HttpMethod; +import org.springframework.web.bind.annotation.CrossOrigin; +import org.springframework.web.cors.CorsConfiguration; + +/** + * {@code CorsRegistration} assists with the creation of a + * {@link CorsConfiguration} instance mapped to a path pattern. + * + * <p>If no path pattern is specified, cross-origin request handling is + * mapped to {@code "/**"}. + * + * <p>By default, all origins, all headers, credentials and {@code GET}, + * {@code HEAD}, and {@code POST} methods are allowed, and the max age is + * set to 30 minutes. + * + * @author Sebastien Deleuze + * @author Sam Brannen + * @since 4.2 + * @see CorsConfiguration + * @see CorsRegistry + */ +public class CorsRegistration { + + private final String pathPattern; + + private final CorsConfiguration config; + + public CorsRegistration(String pathPattern) { + this.pathPattern = pathPattern; + // Same implicit default values as the @CrossOrigin annotation + allows simple methods + this.config = new CorsConfiguration(); + this.config.setAllowedOrigins(Arrays.asList(CrossOrigin.DEFAULT_ORIGINS)); + this.config.setAllowedMethods(Arrays.asList(HttpMethod.GET.name(), + HttpMethod.HEAD.name(), HttpMethod.POST.name())); + this.config.setAllowedHeaders(Arrays.asList(CrossOrigin.DEFAULT_ALLOWED_HEADERS)); + this.config.setAllowCredentials(CrossOrigin.DEFAULT_ALLOW_CREDENTIALS); + this.config.setMaxAge(CrossOrigin.DEFAULT_MAX_AGE); + } + + public CorsRegistration allowedOrigins(String... origins) { + this.config.setAllowedOrigins(new ArrayList<String>(Arrays.asList(origins))); + return this; + } + + public CorsRegistration allowedMethods(String... methods) { + this.config.setAllowedMethods(new ArrayList<String>(Arrays.asList(methods))); + return this; + } + + public CorsRegistration allowedHeaders(String... headers) { + this.config.setAllowedHeaders(new ArrayList<String>(Arrays.asList(headers))); + return this; + } + + public CorsRegistration exposedHeaders(String... headers) { + this.config.setExposedHeaders(new ArrayList<String>(Arrays.asList(headers))); + return this; + } + + public CorsRegistration maxAge(long maxAge) { + this.config.setMaxAge(maxAge); + return this; + } + + public CorsRegistration allowCredentials(boolean allowCredentials) { + this.config.setAllowCredentials(allowCredentials); + return this; + } + + protected String getPathPattern() { + return this.pathPattern; + } + + protected CorsConfiguration getCorsConfiguration() { + return this.config; + } + +} diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/CorsRegistry.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/CorsRegistry.java new file mode 100644 index 00000000..3f4e334e --- /dev/null +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/CorsRegistry.java @@ -0,0 +1,63 @@ +/* + * Copyright 2002-2015 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.web.servlet.config.annotation; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import org.springframework.web.cors.CorsConfiguration; + +/** + * {@code CorsRegistry} assists with the registration of {@link CorsConfiguration} + * mapped to a path pattern. + * + * @author Sebastien Deleuze + * @since 4.2 + * @see CorsRegistration + */ +public class CorsRegistry { + + private final List<CorsRegistration> registrations = new ArrayList<CorsRegistration>(); + + + /** + * Enable cross origin request handling for the specified path pattern. + * + * <p>Exact path mapping URIs (such as {@code "/admin"}) are supported as + * well as Ant-style path patterns (such as {@code "/admin/**"}). + * + * <p>By default, all origins, all headers, credentials and {@code GET}, + * {@code HEAD}, and {@code POST} methods are allowed, and the max age + * is set to 30 minutes. + */ + public CorsRegistration addMapping(String pathPattern) { + CorsRegistration registration = new CorsRegistration(pathPattern); + this.registrations.add(registration); + return registration; + } + + protected Map<String, CorsConfiguration> getCorsConfigurations() { + Map<String, CorsConfiguration> configs = new LinkedHashMap<String, CorsConfiguration>(this.registrations.size()); + for (CorsRegistration registration : this.registrations) { + configs.put(registration.getPathPattern(), registration.getCorsConfiguration()); + } + return configs; + } + +} diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/DelegatingWebMvcConfiguration.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/DelegatingWebMvcConfiguration.java index bf384ce8..084acd90 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/DelegatingWebMvcConfiguration.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/DelegatingWebMvcConfiguration.java @@ -132,4 +132,9 @@ public class DelegatingWebMvcConfiguration extends WebMvcConfigurationSupport { this.configurers.configureHandlerExceptionResolvers(exceptionResolvers); } + @Override + protected void addCorsMappings(CorsRegistry registry) { + this.configurers.addCorsMappings(registry); + } + } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/EnableWebMvc.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/EnableWebMvc.java index 17dd890f..8de751cc 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/EnableWebMvc.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/EnableWebMvc.java @@ -1,15 +1,19 @@ /* - * Copyright 2002-2014 the original author or authors. + * Copyright 2002-2016 the original author or authors. * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * - * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on - * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the - * specific language governing permissions and limitations under the License. + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. */ + package org.springframework.web.servlet.config.annotation; import java.lang.annotation.Documented; @@ -43,17 +47,17 @@ import org.springframework.context.annotation.Import; * @ComponentScan(basePackageClasses = { MyConfiguration.class }) * public class MyConfiguration extends WebMvcConfigurerAdapter { * - * @Override - * public void addFormatters(FormatterRegistry formatterRegistry) { - * formatterRegistry.addConverter(new MyConverter()); - * } + * @Override + * public void addFormatters(FormatterRegistry formatterRegistry) { + * formatterRegistry.addConverter(new MyConverter()); + * } * - * @Override - * public void configureMessageConverters(List<HttpMessageConverter<?>> converters) { - * converters.add(new MyHttpMessageConverter()); - * } + * @Override + * public void configureMessageConverters(List<HttpMessageConverter<?>> converters) { + * converters.add(new MyHttpMessageConverter()); + * } * - * // More overridden methods ... + * // More overridden methods ... * } * </pre> * @@ -67,16 +71,16 @@ import org.springframework.context.annotation.Import; * @ComponentScan(basePackageClasses = { MyConfiguration.class }) * public class MyConfiguration extends WebMvcConfigurationSupport { * - * @Override - * public void addFormatters(FormatterRegistry formatterRegistry) { - * formatterRegistry.addConverter(new MyConverter()); - * } + * @Override + * public void addFormatters(FormatterRegistry formatterRegistry) { + * formatterRegistry.addConverter(new MyConverter()); + * } * - * @Bean - * public RequestMappingHandlerAdapter requestMappingHandlerAdapter() { - * // Create or delegate to "super" to create and - * // customize properties of RequestMapingHandlerAdapter - * } + * @Bean + * public RequestMappingHandlerAdapter requestMappingHandlerAdapter() { + * // Create or delegate to "super" to create and + * // customize properties of RequestMappingHandlerAdapter + * } * } * </pre> * diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/PathMatchConfigurer.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/PathMatchConfigurer.java index 8203ea20..35fa13e3 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/PathMatchConfigurer.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/PathMatchConfigurer.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2014 the original author or authors. + * Copyright 2002-2015 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,8 +20,8 @@ import org.springframework.util.PathMatcher; import org.springframework.web.util.UrlPathHelper; /** - * Helps with configuring HandlerMappings path matching options such as trailing slash match, - * suffix registration, path matcher and path helper. + * Helps with configuring HandlerMappings path matching options such as trailing + * slash match, suffix registration, path matcher and path helper. * * <p>Configured path matcher and path helper instances are shared for: * <ul> @@ -37,11 +37,11 @@ import org.springframework.web.util.UrlPathHelper; */ public class PathMatchConfigurer { - private Boolean useSuffixPatternMatch; + private Boolean suffixPatternMatch; - private Boolean useTrailingSlashMatch; + private Boolean trailingSlashMatch; - private Boolean useRegisteredSuffixPatternMatch; + private Boolean registeredSuffixPatternMatch; private UrlPathHelper urlPathHelper; @@ -51,10 +51,11 @@ public class PathMatchConfigurer { /** * Whether to use suffix pattern match (".*") when matching patterns to * requests. If enabled a method mapped to "/users" also matches to "/users.*". - * <p>The default value is {@code true}. + * <p>By default this is set to {@code true}. + * @see #registeredSuffixPatternMatch */ - public PathMatchConfigurer setUseSuffixPatternMatch(Boolean useSuffixPatternMatch) { - this.useSuffixPatternMatch = useSuffixPatternMatch; + public PathMatchConfigurer setUseSuffixPatternMatch(Boolean suffixPatternMatch) { + this.suffixPatternMatch = suffixPatternMatch; return this; } @@ -63,28 +64,24 @@ public class PathMatchConfigurer { * If enabled a method mapped to "/users" also matches to "/users/". * <p>The default value is {@code true}. */ - public PathMatchConfigurer setUseTrailingSlashMatch(Boolean useTrailingSlashMatch) { - this.useTrailingSlashMatch = useTrailingSlashMatch; + public PathMatchConfigurer setUseTrailingSlashMatch(Boolean trailingSlashMatch) { + this.trailingSlashMatch = trailingSlashMatch; return this; } /** - * Whether to use suffix pattern match for registered file extensions only - * when matching patterns to requests. - * <p>If enabled, a controller method mapped to "/users" also matches to - * "/users.json" assuming ".json" is a file extension registered with the - * provided {@link org.springframework.web.accept.ContentNegotiationManager}.</p> - * <p>The {@link org.springframework.web.accept.ContentNegotiationManager} can be customized - * using a {@link ContentNegotiationConfigurer}.</p> - * <p>If enabled, this flag also enables - * {@link #setUseSuffixPatternMatch(Boolean) useSuffixPatternMatch}. The - * default value is {@code false}.</p> - * @see org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping - * @see ContentNegotiationConfigurer - * + * Whether suffix pattern matching should work only against path extensions + * explicitly registered when you + * {@link WebMvcConfigurer#configureContentNegotiation configure content + * negotiation}. This is generally recommended to reduce ambiguity and to + * avoid issues such as when a "." appears in the path for other reasons. + * <p>By default this is set to "false". + * @see WebMvcConfigurer#configureContentNegotiation */ - public PathMatchConfigurer setUseRegisteredSuffixPatternMatch(Boolean useRegisteredSuffixPatternMatch) { - this.useRegisteredSuffixPatternMatch = useRegisteredSuffixPatternMatch; + public PathMatchConfigurer setUseRegisteredSuffixPatternMatch( + Boolean registeredSuffixPatternMatch) { + + this.registeredSuffixPatternMatch = registeredSuffixPatternMatch; return this; } @@ -110,15 +107,15 @@ public class PathMatchConfigurer { } public Boolean isUseSuffixPatternMatch() { - return this.useSuffixPatternMatch; + return this.suffixPatternMatch; } public Boolean isUseTrailingSlashMatch() { - return this.useTrailingSlashMatch; + return this.trailingSlashMatch; } public Boolean isUseRegisteredSuffixPatternMatch() { - return this.useRegisteredSuffixPatternMatch; + return this.registeredSuffixPatternMatch; } public UrlPathHelper getUrlPathHelper() { diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/ResourceChainRegistration.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/ResourceChainRegistration.java index 7749491f..92a75bed 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/ResourceChainRegistration.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/ResourceChainRegistration.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2014 the original author or authors. + * Copyright 2002-2015 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,6 +22,7 @@ import java.util.List; import org.springframework.cache.Cache; import org.springframework.cache.concurrent.ConcurrentMapCache; import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; import org.springframework.web.servlet.resource.CachingResourceResolver; import org.springframework.web.servlet.resource.CachingResourceTransformer; import org.springframework.web.servlet.resource.CssLinkResourceTransformer; @@ -29,6 +30,7 @@ import org.springframework.web.servlet.resource.PathResourceResolver; import org.springframework.web.servlet.resource.ResourceResolver; import org.springframework.web.servlet.resource.ResourceTransformer; import org.springframework.web.servlet.resource.VersionResourceResolver; +import org.springframework.web.servlet.resource.WebJarsResourceResolver; /** * Assists with the registration of resource resolvers and transformers. @@ -40,6 +42,10 @@ public class ResourceChainRegistration { private static final String DEFAULT_CACHE_NAME = "spring-resource-chain-cache"; + private static final boolean isWebJarsAssetLocatorPresent = ClassUtils.isPresent( + "org.webjars.WebJarAssetLocator", ResourceChainRegistration.class.getClassLoader()); + + private final List<ResourceResolver> resolvers = new ArrayList<ResourceResolver>(4); private final List<ResourceTransformer> transformers = new ArrayList<ResourceTransformer>(4); @@ -98,6 +104,9 @@ public class ResourceChainRegistration { protected List<ResourceResolver> getResourceResolvers() { if (!this.hasPathResolver) { List<ResourceResolver> result = new ArrayList<ResourceResolver>(this.resolvers); + if (isWebJarsAssetLocatorPresent) { + result.add(new WebJarsResourceResolver()); + } result.add(new PathResourceResolver()); return result; } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/ResourceHandlerRegistration.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/ResourceHandlerRegistration.java index 152617cd..54c15642 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/ResourceHandlerRegistration.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/ResourceHandlerRegistration.java @@ -23,6 +23,7 @@ import org.springframework.cache.Cache; import org.springframework.core.io.Resource; import org.springframework.core.io.ResourceLoader; import org.springframework.util.Assert; +import org.springframework.http.CacheControl; import org.springframework.web.servlet.resource.PathResourceResolver; import org.springframework.web.servlet.resource.ResourceHttpRequestHandler; @@ -44,6 +45,8 @@ public class ResourceHandlerRegistration { private Integer cachePeriod; + private CacheControl cacheControl; + private ResourceChainRegistration resourceChainRegistration; @@ -68,7 +71,7 @@ public class ResourceHandlerRegistration { * {@code /META-INF/public-web-resources/} directory, with resources in the web application root taking precedence. * @return the same {@link ResourceHandlerRegistration} instance, for chained method invocation */ - public ResourceHandlerRegistration addResourceLocations(String...resourceLocations) { + public ResourceHandlerRegistration addResourceLocations(String... resourceLocations) { for (String location : resourceLocations) { this.locations.add(resourceLoader.getResource(location)); } @@ -88,6 +91,21 @@ public class ResourceHandlerRegistration { } /** + * Specify the {@link org.springframework.http.CacheControl} which should be used + * by the resource handler. + * + * <p>Setting a custom value here will override the configuration set with {@link #setCachePeriod}. + * + * @param cacheControl the CacheControl configuration to use + * @return the same {@link ResourceHandlerRegistration} instance, for chained method invocation + * @since 4.2 + */ + public ResourceHandlerRegistration setCacheControl(CacheControl cacheControl) { + this.cacheControl = cacheControl; + return this; + } + + /** * Configure a chain of resource resolvers and transformers to use. This * can be useful, for example, to apply a version strategy to resource URLs. * @@ -147,7 +165,10 @@ public class ResourceHandlerRegistration { handler.setResourceTransformers(this.resourceChainRegistration.getResourceTransformers()); } handler.setLocations(this.locations); - if (this.cachePeriod != null) { + if (this.cacheControl != null) { + handler.setCacheControl(this.cacheControl); + } + else if (this.cachePeriod != null) { handler.setCacheSeconds(this.cachePeriod); } return handler; diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/ViewResolverRegistry.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/ViewResolverRegistry.java index 6af073c5..c4fedda8 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/ViewResolverRegistry.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/ViewResolverRegistry.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2014 the original author or authors. + * Copyright 2002-2015 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -37,6 +37,8 @@ import org.springframework.web.servlet.view.freemarker.FreeMarkerConfigurer; import org.springframework.web.servlet.view.freemarker.FreeMarkerViewResolver; import org.springframework.web.servlet.view.groovy.GroovyMarkupConfigurer; import org.springframework.web.servlet.view.groovy.GroovyMarkupViewResolver; +import org.springframework.web.servlet.view.script.ScriptTemplateConfigurer; +import org.springframework.web.servlet.view.script.ScriptTemplateViewResolver; import org.springframework.web.servlet.view.tiles3.TilesConfigurer; import org.springframework.web.servlet.view.tiles3.TilesViewResolver; import org.springframework.web.servlet.view.velocity.VelocityConfigurer; @@ -234,6 +236,22 @@ public class ViewResolverRegistry { } /** + * Register a script template view resolver with an empty default view name prefix and suffix. + * @since 4.2 + */ + public UrlBasedViewResolverRegistration scriptTemplate() { + if (this.applicationContext != null && !hasBeanOfType(ScriptTemplateConfigurer.class)) { + throw new BeanInitializationException("In addition to a script template view resolver " + + "there must also be a single ScriptTemplateConfig bean in this web application context " + + "(or its parent): ScriptTemplateConfigurer is the usual implementation. " + + "This bean may be given any name."); + } + ScriptRegistration registration = new ScriptRegistration(); + this.viewResolvers.add(registration.getViewResolver()); + return registration; + } + + /** * Register a bean name view resolver that interprets view names as the names * of {@link org.springframework.web.servlet.View} beans. */ @@ -324,4 +342,12 @@ public class ViewResolverRegistry { } } + private static class ScriptRegistration extends UrlBasedViewResolverRegistration { + + private ScriptRegistration() { + super(new ScriptTemplateViewResolver()); + getViewResolver(); + } + } + } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurationSupport.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurationSupport.java index 46112ed3..37976905 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurationSupport.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurationSupport.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2014 the original author or authors. + * Copyright 2002-2015 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -66,6 +66,7 @@ import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.support.ConfigurableWebBindingInitializer; import org.springframework.web.context.ServletContextAware; +import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.method.support.CompositeUriComponentsContributor; import org.springframework.web.method.support.HandlerMethodArgumentResolver; import org.springframework.web.method.support.HandlerMethodReturnValueHandler; @@ -83,7 +84,9 @@ import org.springframework.web.servlet.mvc.HttpRequestHandlerAdapter; import org.springframework.web.servlet.mvc.SimpleControllerHandlerAdapter; import org.springframework.web.servlet.mvc.annotation.ResponseStatusExceptionResolver; import org.springframework.web.servlet.mvc.method.annotation.ExceptionHandlerExceptionResolver; +import org.springframework.web.servlet.mvc.method.annotation.JsonViewRequestBodyAdvice; import org.springframework.web.servlet.mvc.method.annotation.JsonViewResponseBodyAdvice; +import org.springframework.web.servlet.mvc.method.annotation.RequestBodyAdvice; import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter; import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping; import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice; @@ -198,6 +201,8 @@ public class WebMvcConfigurationSupport implements ApplicationContextAware, Serv private List<HttpMessageConverter<?>> messageConverters; + private Map<String, CorsConfiguration> corsConfigurations; + /** * Set the Spring {@link ApplicationContext}, e.g. for resource loading. @@ -207,6 +212,10 @@ public class WebMvcConfigurationSupport implements ApplicationContextAware, Serv this.applicationContext = applicationContext; } + public ApplicationContext getApplicationContext() { + return this.applicationContext; + } + /** * Set the {@link javax.servlet.ServletContext}, e.g. for resource handling, * looking up file extensions, etc. @@ -216,6 +225,9 @@ public class WebMvcConfigurationSupport implements ApplicationContextAware, Serv this.servletContext = servletContext; } + public ServletContext getServletContext() { + return this.servletContext; + } /** * Return a {@link RequestMappingHandlerMapping} ordered at 0 for mapping @@ -223,10 +235,11 @@ public class WebMvcConfigurationSupport implements ApplicationContextAware, Serv */ @Bean public RequestMappingHandlerMapping requestMappingHandlerMapping() { - RequestMappingHandlerMapping handlerMapping = new RequestMappingHandlerMapping(); + RequestMappingHandlerMapping handlerMapping = createRequestMappingHandlerMapping(); handlerMapping.setOrder(0); handlerMapping.setInterceptors(getInterceptors()); handlerMapping.setContentNegotiationManager(mvcContentNegotiationManager()); + handlerMapping.setCorsConfigurations(getCorsConfigurations()); PathMatchConfigurer configurer = getPathMatchConfigurer(); if (configurer.isUseSuffixPatternMatch() != null) { @@ -249,6 +262,14 @@ public class WebMvcConfigurationSupport implements ApplicationContextAware, Serv } /** + * Protected method for plugging in a custom sub-class of + * {@link RequestMappingHandlerMapping}. + */ + protected RequestMappingHandlerMapping createRequestMappingHandlerMapping() { + return new RequestMappingHandlerMapping(); + } + + /** * Provide access to the shared handler interceptors used to configure * {@link HandlerMapping} instances with. This method cannot be overridden, * use {@link #addInterceptors(InterceptorRegistry)} instead. @@ -351,6 +372,7 @@ public class WebMvcConfigurationSupport implements ApplicationContextAware, Serv handlerMapping.setPathMatcher(mvcPathMatcher()); handlerMapping.setUrlPathHelper(mvcUrlPathHelper()); handlerMapping.setInterceptors(getInterceptors()); + handlerMapping.setCorsConfigurations(getCorsConfigurations()); return handlerMapping; } @@ -370,6 +392,7 @@ public class WebMvcConfigurationSupport implements ApplicationContextAware, Serv BeanNameUrlHandlerMapping mapping = new BeanNameUrlHandlerMapping(); mapping.setOrder(2); mapping.setInterceptors(getInterceptors()); + mapping.setCorsConfigurations(getCorsConfigurations()); return mapping; } @@ -389,6 +412,7 @@ public class WebMvcConfigurationSupport implements ApplicationContextAware, Serv handlerMapping.setUrlPathHelper(mvcUrlPathHelper()); handlerMapping.setInterceptors(new HandlerInterceptor[] { new ResourceUrlProviderExposingInterceptor(mvcResourceUrlProvider())}); + handlerMapping.setCorsConfigurations(getCorsConfigurations()); } else { handlerMapping = new EmptyHandlerMapping(); @@ -464,9 +488,13 @@ public class WebMvcConfigurationSupport implements ApplicationContextAware, Serv adapter.setCustomReturnValueHandlers(returnValueHandlers); if (jackson2Present) { - List<ResponseBodyAdvice<?>> interceptors = new ArrayList<ResponseBodyAdvice<?>>(); - interceptors.add(new JsonViewResponseBodyAdvice()); - adapter.setResponseBodyAdvice(interceptors); + List<RequestBodyAdvice> requestBodyAdvices = new ArrayList<RequestBodyAdvice>(); + requestBodyAdvices.add(new JsonViewRequestBodyAdvice()); + adapter.setRequestBodyAdvice(requestBodyAdvices); + + List<ResponseBodyAdvice<?>> responseBodyAdvices = new ArrayList<ResponseBodyAdvice<?>>(); + responseBodyAdvices.add(new JsonViewResponseBodyAdvice()); + adapter.setResponseBodyAdvice(responseBodyAdvices); } AsyncSupportConfigurer configurer = new AsyncSupportConfigurer(); @@ -843,6 +871,26 @@ public class WebMvcConfigurationSupport implements ApplicationContextAware, Serv protected void configureViewResolvers(ViewResolverRegistry registry) { } + /** + * @since 4.2 + */ + protected final Map<String, CorsConfiguration> getCorsConfigurations() { + if (this.corsConfigurations == null) { + CorsRegistry registry = new CorsRegistry(); + addCorsMappings(registry); + this.corsConfigurations = registry.getCorsConfigurations(); + } + return this.corsConfigurations; + } + + /** + * Override this method to configure cross origin requests processing. + * @since 4.2 + * @see CorsRegistry + */ + protected void addCorsMappings(CorsRegistry registry) { + } + private static final class EmptyHandlerMapping extends AbstractHandlerMapping { diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurer.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurer.java index 85c09d38..3e9c509f 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurer.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurer.java @@ -182,4 +182,10 @@ public interface WebMvcConfigurer { */ void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer); + /** + * Configure cross origin requests processing. + * @since 4.2 + */ + void addCorsMappings(CorsRegistry registry); + } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurerAdapter.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurerAdapter.java index 7f4c7c34..90d4b6ae 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurerAdapter.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurerAdapter.java @@ -165,4 +165,12 @@ public abstract class WebMvcConfigurerAdapter implements WebMvcConfigurer { public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) { } + /** + * {@inheritDoc} + * <p>This implementation is empty. + */ + @Override + public void addCorsMappings(CorsRegistry registry) { + } + } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurerComposite.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurerComposite.java index af08decd..c88ba0ff 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurerComposite.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurerComposite.java @@ -153,6 +153,13 @@ class WebMvcConfigurerComposite implements WebMvcConfigurer { return selectSingleInstance(candidates, Validator.class); } + @Override + public void addCorsMappings(CorsRegistry registry) { + for (WebMvcConfigurer delegate : this.delegates) { + delegate.addCorsMappings(registry); + } + } + private <T> T selectSingleInstance(List<T> instances, Class<T> instanceType) { if (instances.size() > 1) { throw new IllegalStateException( diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/AbstractHandlerExceptionResolver.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/AbstractHandlerExceptionResolver.java index 764bec2c..9ae1cb42 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/AbstractHandlerExceptionResolver.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/AbstractHandlerExceptionResolver.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2013 the original author or authors. + * Copyright 2002-2015 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -30,19 +30,17 @@ import org.springframework.web.servlet.ModelAndView; /** * Abstract base class for {@link HandlerExceptionResolver} implementations. * - * <p>Provides a set of mapped handlers that the resolver should map to, - * and the {@link Ordered} implementation. + * <p>Supports mapped {@linkplain #setMappedHandlers handlers} and + * {@linkplain #setMappedHandlerClasses handler classes} that the resolver + * should be applied to and implements the {@link Ordered} interface. * * @author Arjen Poutsma * @author Juergen Hoeller + * @author Sam Brannen * @since 3.0 */ public abstract class AbstractHandlerExceptionResolver implements HandlerExceptionResolver, Ordered { - private static final String HEADER_PRAGMA = "Pragma"; - - private static final String HEADER_EXPIRES = "Expires"; - private static final String HEADER_CACHE_CONTROL = "Cache-Control"; @@ -71,10 +69,10 @@ public abstract class AbstractHandlerExceptionResolver implements HandlerExcepti /** * Specify the set of handlers that this exception resolver should apply to. - * The exception mappings and the default error view will only apply to the specified handlers. - * <p>If no handlers and handler classes are set, the exception mappings and the default error + * <p>The exception mappings and the default error view will only apply to the specified handlers. + * <p>If no handlers or handler classes are set, the exception mappings and the default error * view will apply to all handlers. This means that a specified default error view will be used - * as fallback for all exceptions; any further HandlerExceptionResolvers in the chain will be + * as a fallback for all exceptions; any further HandlerExceptionResolvers in the chain will be * ignored in this case. */ public void setMappedHandlers(Set<?> mappedHandlers) { @@ -83,20 +81,20 @@ public abstract class AbstractHandlerExceptionResolver implements HandlerExcepti /** * Specify the set of classes that this exception resolver should apply to. - * The exception mappings and the default error view will only apply to handlers of the - * specified type; the specified types may be interfaces and superclasses of handlers as well. - * <p>If no handlers and handler classes are set, the exception mappings and the default error + * <p>The exception mappings and the default error view will only apply to handlers of the + * specified types; the specified types may be interfaces or superclasses of handlers as well. + * <p>If no handlers or handler classes are set, the exception mappings and the default error * view will apply to all handlers. This means that a specified default error view will be used - * as fallback for all exceptions; any further HandlerExceptionResolvers in the chain will be + * as a fallback for all exceptions; any further HandlerExceptionResolvers in the chain will be * ignored in this case. */ - public void setMappedHandlerClasses(Class<?>[] mappedHandlerClasses) { + public void setMappedHandlerClasses(Class<?>... mappedHandlerClasses) { this.mappedHandlerClasses = mappedHandlerClasses; } /** * Set the log category for warn logging. The name will be passed to the underlying logger - * implementation through Commons Logging, getting interpreted as log category according + * implementation through Commons Logging, getting interpreted as a log category according * to the logger's configuration. * <p>Default is no warn logging. Specify this setting to activate warn logging into a specific * category. Alternatively, override the {@link #logException} method for custom logging. @@ -110,9 +108,9 @@ public abstract class AbstractHandlerExceptionResolver implements HandlerExcepti /** * Specify whether to prevent HTTP response caching for any view resolved - * by this HandlerExceptionResolver. - * <p>Default is "false". Switch this to "true" in order to automatically - * generate HTTP response headers that suppress response caching. + * by this exception resolver. + * <p>Default is {@code false}. Switch this to {@code true} in order to + * automatically generate HTTP response headers that suppress response caching. */ public void setPreventResponseCaching(boolean preventResponseCaching) { this.preventResponseCaching = preventResponseCaching; @@ -120,9 +118,10 @@ public abstract class AbstractHandlerExceptionResolver implements HandlerExcepti /** - * Checks whether this resolver is supposed to apply (i.e. the handler matches - * in case of "mappedHandlers" having been specified), then delegates to the - * {@link #doResolveException} template method. + * Check whether this resolver is supposed to apply (i.e. if the supplied handler + * matches any of the configured {@linkplain #setMappedHandlers handlers} or + * {@linkplain #setMappedHandlerClasses handler classes}), and then delegate + * to the {@link #doResolveException} template method. */ @Override public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, @@ -130,8 +129,8 @@ public abstract class AbstractHandlerExceptionResolver implements HandlerExcepti if (shouldApplyTo(request, handler)) { // Log exception, both at debug log level and at warn level, if desired. - if (logger.isDebugEnabled()) { - logger.debug("Resolving exception from handler [" + handler + "]: " + ex); + if (this.logger.isDebugEnabled()) { + this.logger.debug("Resolving exception from handler [" + handler + "]: " + ex); } logException(ex, request); prepareResponse(ex, response); @@ -144,8 +143,9 @@ public abstract class AbstractHandlerExceptionResolver implements HandlerExcepti /** * Check whether this resolver is supposed to apply to the given handler. - * <p>The default implementation checks against the specified mapped handlers - * and handler classes, if any. + * <p>The default implementation checks against the configured + * {@linkplain #setMappedHandlers handlers} and + * {@linkplain #setMappedHandlerClasses handler classes}, if any. * @param request current HTTP request * @param handler the executed handler, or {@code null} if none chosen * at the time of the exception (for example, if multipart resolution failed) @@ -175,7 +175,6 @@ public abstract class AbstractHandlerExceptionResolver implements HandlerExcepti * Log the given exception at warn level, provided that warn logging has been * activated through the {@link #setWarnLogCategory "warnLogCategory"} property. * <p>Calls {@link #buildLogMessage} in order to determine the concrete message to log. - * Always passes the full exception to the logger. * @param ex the exception that got thrown during handler execution * @param request current HTTP request (useful for obtaining metadata) * @see #setWarnLogCategory @@ -184,7 +183,7 @@ public abstract class AbstractHandlerExceptionResolver implements HandlerExcepti */ protected void logException(Exception ex, HttpServletRequest request) { if (this.warnLogger != null && this.warnLogger.isWarnEnabled()) { - this.warnLogger.warn(buildLogMessage(ex, request), ex); + this.warnLogger.warn(buildLogMessage(ex, request)); } } @@ -195,7 +194,7 @@ public abstract class AbstractHandlerExceptionResolver implements HandlerExcepti * @return the log message to use */ protected String buildLogMessage(Exception ex, HttpServletRequest request) { - return "Handler execution resulted in exception"; + return "Handler execution resulted in exception: " + ex; } /** @@ -215,20 +214,17 @@ public abstract class AbstractHandlerExceptionResolver implements HandlerExcepti /** * Prevents the response from being cached, through setting corresponding - * HTTP headers. See {@code http://www.mnot.net/cache_docs}. + * HTTP {@code Cache-Control: no-store} header. * @param response current HTTP response */ protected void preventCaching(HttpServletResponse response) { - response.setHeader(HEADER_PRAGMA, "no-cache"); - response.setDateHeader(HEADER_EXPIRES, 1L); - response.setHeader(HEADER_CACHE_CONTROL, "no-cache"); response.addHeader(HEADER_CACHE_CONTROL, "no-store"); } /** - * Actually resolve the given exception that got thrown during on handler execution, - * returning a ModelAndView that represents a specific error page if appropriate. + * Actually resolve the given exception that got thrown during handler execution, + * returning a {@link ModelAndView} that represents a specific error page if appropriate. * <p>May be overridden in subclasses, in order to apply specific exception checks. * Note that this template method will be invoked <i>after</i> checking whether this * resolved applies ("mappedHandlers" etc), so an implementation may simply proceed @@ -238,7 +234,7 @@ public abstract class AbstractHandlerExceptionResolver implements HandlerExcepti * @param handler the executed handler, or {@code null} if none chosen at the time * of the exception (for example, if multipart resolution failed) * @param ex the exception that got thrown during handler execution - * @return a corresponding ModelAndView to forward to, or {@code null} for default processing + * @return a corresponding {@code ModelAndView} to forward to, or {@code null} for default processing */ protected abstract ModelAndView doResolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex); diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/AbstractHandlerMapping.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/AbstractHandlerMapping.java index f0ac1307..1877f194 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/AbstractHandlerMapping.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/AbstractHandlerMapping.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2014 the original author or authors. + * Copyright 2002-2015 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,14 +16,22 @@ package org.springframework.web.servlet.handler; +import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import java.util.Map; import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; import org.springframework.beans.BeansException; import org.springframework.beans.factory.BeanFactoryUtils; import org.springframework.core.Ordered; +import org.springframework.web.HttpRequestHandler; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; +import org.springframework.web.cors.CorsProcessor; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.CorsConfigurationSource; import org.springframework.util.AntPathMatcher; import org.springframework.util.Assert; import org.springframework.util.PathMatcher; @@ -32,6 +40,8 @@ import org.springframework.web.context.support.WebApplicationObjectSupport; import org.springframework.web.servlet.HandlerExecutionChain; import org.springframework.web.servlet.HandlerInterceptor; import org.springframework.web.servlet.HandlerMapping; +import org.springframework.web.cors.DefaultCorsProcessor; +import org.springframework.web.cors.CorsUtils; import org.springframework.web.util.UrlPathHelper; /** @@ -69,7 +79,9 @@ public abstract class AbstractHandlerMapping extends WebApplicationObjectSupport private final List<HandlerInterceptor> adaptedInterceptors = new ArrayList<HandlerInterceptor>(); - private final List<MappedInterceptor> mappedInterceptors = new ArrayList<MappedInterceptor>(); + private CorsProcessor corsProcessor = new DefaultCorsProcessor(); + + private final UrlBasedCorsConfigurationSource corsConfigSource = new UrlBasedCorsConfigurationSource(); /** @@ -112,6 +124,7 @@ public abstract class AbstractHandlerMapping extends WebApplicationObjectSupport */ public void setAlwaysUseFullPath(boolean alwaysUseFullPath) { this.urlPathHelper.setAlwaysUseFullPath(alwaysUseFullPath); + this.corsConfigSource.setAlwaysUseFullPath(alwaysUseFullPath); } /** @@ -123,6 +136,7 @@ public abstract class AbstractHandlerMapping extends WebApplicationObjectSupport */ public void setUrlDecode(boolean urlDecode) { this.urlPathHelper.setUrlDecode(urlDecode); + this.corsConfigSource.setUrlDecode(urlDecode); } /** @@ -132,6 +146,7 @@ public abstract class AbstractHandlerMapping extends WebApplicationObjectSupport */ public void setRemoveSemicolonContent(boolean removeSemicolonContent) { this.urlPathHelper.setRemoveSemicolonContent(removeSemicolonContent); + this.corsConfigSource.setRemoveSemicolonContent(removeSemicolonContent); } /** @@ -143,6 +158,7 @@ public abstract class AbstractHandlerMapping extends WebApplicationObjectSupport public void setUrlPathHelper(UrlPathHelper urlPathHelper) { Assert.notNull(urlPathHelper, "UrlPathHelper must not be null"); this.urlPathHelper = urlPathHelper; + this.corsConfigSource.setUrlPathHelper(urlPathHelper); } /** @@ -160,6 +176,7 @@ public abstract class AbstractHandlerMapping extends WebApplicationObjectSupport public void setPathMatcher(PathMatcher pathMatcher) { Assert.notNull(pathMatcher, "PathMatcher must not be null"); this.pathMatcher = pathMatcher; + this.corsConfigSource.setPathMatcher(pathMatcher); } /** @@ -184,6 +201,40 @@ public abstract class AbstractHandlerMapping extends WebApplicationObjectSupport this.interceptors.addAll(Arrays.asList(interceptors)); } + /** + * Configure a custom {@link CorsProcessor} to use to apply the matched + * {@link CorsConfiguration} for a request. + * <p>By default {@link DefaultCorsProcessor} is used. + * @since 4.2 + */ + public void setCorsProcessor(CorsProcessor corsProcessor) { + Assert.notNull(corsProcessor, "CorsProcessor must not be null"); + this.corsProcessor = corsProcessor; + } + + /** + * Return the configured {@link CorsProcessor}. + */ + public CorsProcessor getCorsProcessor() { + return this.corsProcessor; + } + + /** + * Set "global" CORS configuration based on URL patterns. By default the first + * matching URL pattern is combined with the CORS configuration for the + * handler, if any. + * @since 4.2 + */ + public void setCorsConfigurations(Map<String, CorsConfiguration> corsConfigurations) { + this.corsConfigSource.setCorsConfigurations(corsConfigurations); + } + + /** + * Get the CORS configuration. + */ + public Map<String, CorsConfiguration> getCorsConfigurations() { + return this.corsConfigSource.getCorsConfigurations(); + } /** * Initializes the interceptors. @@ -193,7 +244,7 @@ public abstract class AbstractHandlerMapping extends WebApplicationObjectSupport @Override protected void initApplicationContext() throws BeansException { extendInterceptors(this.interceptors); - detectMappedInterceptors(this.mappedInterceptors); + detectMappedInterceptors(this.adaptedInterceptors); initInterceptors(); } @@ -216,7 +267,7 @@ public abstract class AbstractHandlerMapping extends WebApplicationObjectSupport * from the current context and its ancestors. Subclasses can override and refine this policy. * @param mappedInterceptors an empty list to add {@link MappedInterceptor} instances to */ - protected void detectMappedInterceptors(List<MappedInterceptor> mappedInterceptors) { + protected void detectMappedInterceptors(List<HandlerInterceptor> mappedInterceptors) { mappedInterceptors.addAll( BeanFactoryUtils.beansOfTypeIncludingAncestors( getApplicationContext(), MappedInterceptor.class, true, false).values()); @@ -235,12 +286,7 @@ public abstract class AbstractHandlerMapping extends WebApplicationObjectSupport if (interceptor == null) { throw new IllegalArgumentException("Entry number " + i + " in interceptors array is null"); } - if (interceptor instanceof MappedInterceptor) { - this.mappedInterceptors.add((MappedInterceptor) interceptor); - } - else { - this.adaptedInterceptors.add(adaptInterceptor(interceptor)); - } + this.adaptedInterceptors.add(adaptInterceptor(interceptor)); } } } @@ -283,8 +329,14 @@ public abstract class AbstractHandlerMapping extends WebApplicationObjectSupport * @return the array of {@link MappedInterceptor}s, or {@code null} if none */ protected final MappedInterceptor[] getMappedInterceptors() { - int count = this.mappedInterceptors.size(); - return (count > 0 ? this.mappedInterceptors.toArray(new MappedInterceptor[count]) : null); + List<MappedInterceptor> mappedInterceptors = new ArrayList<MappedInterceptor>(); + for (HandlerInterceptor interceptor : this.adaptedInterceptors) { + if (interceptor instanceof MappedInterceptor) { + mappedInterceptors.add((MappedInterceptor) interceptor); + } + } + int count = mappedInterceptors.size(); + return (count > 0 ? mappedInterceptors.toArray(new MappedInterceptor[count]) : null); } /** @@ -308,13 +360,26 @@ public abstract class AbstractHandlerMapping extends WebApplicationObjectSupport String handlerName = (String) handler; handler = getApplicationContext().getBean(handlerName); } - return getHandlerExecutionChain(handler, request); + + HandlerExecutionChain executionChain = getHandlerExecutionChain(handler, request); + if (CorsUtils.isCorsRequest(request)) { + CorsConfiguration globalConfig = this.corsConfigSource.getCorsConfiguration(request); + CorsConfiguration handlerConfig = getCorsConfiguration(handler, request); + CorsConfiguration config = (globalConfig != null ? globalConfig.combine(handlerConfig) : handlerConfig); + executionChain = getCorsHandlerExecutionChain(request, executionChain, config); + } + return executionChain; } /** * Look up a handler for the given request, returning {@code null} if no * specific one is found. This method is called by {@link #getHandler}; * a {@code null} return value will lead to the default handler, if one is set. + * <p>On CORS pre-flight requests this method should return a match not for + * the pre-flight request but for the expected actual request based on the URL + * path, the HTTP methods from the "Access-Control-Request-Method" header, and + * the headers from the "Access-Control-Request-Headers" header thus allowing + * the CORS configuration to be obtained via {@link #getCorsConfigurations}, * <p>Note: This method may also return a pre-built {@link HandlerExecutionChain}, * combining a handler object with dynamically determined interceptors. * Statically specified interceptors will get merged into such an existing chain. @@ -329,8 +394,9 @@ public abstract class AbstractHandlerMapping extends WebApplicationObjectSupport * applicable interceptors. * <p>The default implementation builds a standard {@link HandlerExecutionChain} * with the given handler, the handler mapping's common interceptors, and any - * {@link MappedInterceptor}s matching to the current request URL. Subclasses - * may override this in order to extend/rearrange the list of interceptors. + * {@link MappedInterceptor}s matching to the current request URL. Interceptors + * are added in the order they were registered. Subclasses may override this + * in order to extend/rearrange the list of interceptors. * <p><b>NOTE:</b> The passed-in handler object may be a raw handler or a * pre-built {@link HandlerExecutionChain}. This method should handle those * two cases explicitly, either building a new {@link HandlerExecutionChain} @@ -346,16 +412,96 @@ public abstract class AbstractHandlerMapping extends WebApplicationObjectSupport protected HandlerExecutionChain getHandlerExecutionChain(Object handler, HttpServletRequest request) { HandlerExecutionChain chain = (handler instanceof HandlerExecutionChain ? (HandlerExecutionChain) handler : new HandlerExecutionChain(handler)); - chain.addInterceptors(getAdaptedInterceptors()); String lookupPath = this.urlPathHelper.getLookupPathForRequest(request); - for (MappedInterceptor mappedInterceptor : this.mappedInterceptors) { - if (mappedInterceptor.matches(lookupPath, this.pathMatcher)) { - chain.addInterceptor(mappedInterceptor.getInterceptor()); + for (HandlerInterceptor interceptor : this.adaptedInterceptors) { + if (interceptor instanceof MappedInterceptor) { + MappedInterceptor mappedInterceptor = (MappedInterceptor) interceptor; + if (mappedInterceptor.matches(lookupPath, this.pathMatcher)) { + chain.addInterceptor(mappedInterceptor.getInterceptor()); + } } + else { + chain.addInterceptor(interceptor); + } + } + return chain; + } + + /** + * Retrieve the CORS configuration for the given handler. + * @param handler the handler to check (never {@code null}). + * @param request the current request. + * @return the CORS configuration for the handler or {@code null}. + * @since 4.2 + */ + protected CorsConfiguration getCorsConfiguration(Object handler, HttpServletRequest request) { + if (handler instanceof HandlerExecutionChain) { + handler = ((HandlerExecutionChain) handler).getHandler(); } + if (handler instanceof CorsConfigurationSource) { + return ((CorsConfigurationSource) handler).getCorsConfiguration(request); + } + return null; + } + + /** + * Update the HandlerExecutionChain for CORS-related handling. + * <p>For pre-flight requests, the default implementation replaces the selected + * handler with a simple HttpRequestHandler that invokes the configured + * {@link #setCorsProcessor}. + * <p>For actual requests, the default implementation inserts a + * HandlerInterceptor that makes CORS-related checks and adds CORS headers. + * @param request the current request + * @param chain the handler chain + * @param config the applicable CORS configuration, possibly {@code null} + * @since 4.2 + */ + protected HandlerExecutionChain getCorsHandlerExecutionChain(HttpServletRequest request, + HandlerExecutionChain chain, CorsConfiguration config) { + if (CorsUtils.isPreFlightRequest(request)) { + HandlerInterceptor[] interceptors = chain.getInterceptors(); + chain = new HandlerExecutionChain(new PreFlightHandler(config), interceptors); + } + else { + chain.addInterceptor(new CorsInterceptor(config)); + } return chain; } + + private class PreFlightHandler implements HttpRequestHandler { + + private final CorsConfiguration config; + + public PreFlightHandler(CorsConfiguration config) { + this.config = config; + } + + @Override + public void handleRequest(HttpServletRequest request, HttpServletResponse response) + throws IOException { + + corsProcessor.processRequest(this.config, request, response); + } + } + + + private class CorsInterceptor extends HandlerInterceptorAdapter { + + private final CorsConfiguration config; + + public CorsInterceptor(CorsConfiguration config) { + this.config = config; + } + + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, + Object handler) throws Exception { + + return corsProcessor.processRequest(this.config, request, response); + } + } + } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/AbstractHandlerMethodMapping.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/AbstractHandlerMethodMapping.java index 6acaccfd..d84d1601 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/AbstractHandlerMethodMapping.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/AbstractHandlerMethodMapping.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * Copyright 2002-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,22 +21,26 @@ import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Comparator; -import java.util.IdentityHashMap; +import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.locks.ReentrantReadWriteLock; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import org.springframework.beans.factory.BeanFactoryUtils; import org.springframework.beans.factory.InitializingBean; +import org.springframework.core.MethodIntrospector; +import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; -import org.springframework.util.ReflectionUtils.MethodFilter; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.CorsUtils; import org.springframework.web.method.HandlerMethod; -import org.springframework.web.method.HandlerMethodSelector; import org.springframework.web.servlet.HandlerMapping; /** @@ -67,16 +71,24 @@ public abstract class AbstractHandlerMethodMapping<T> extends AbstractHandlerMap */ private static final String SCOPED_TARGET_NAME_PREFIX = "scopedTarget."; + private static final HandlerMethod PREFLIGHT_AMBIGUOUS_MATCH = + new HandlerMethod(new EmptyHandler(), ClassUtils.getMethod(EmptyHandler.class, "handle")); - private boolean detectHandlerMethodsInAncestorContexts = false; + private static final CorsConfiguration ALLOW_CORS_CONFIG = new CorsConfiguration(); + + static { + ALLOW_CORS_CONFIG.addAllowedOrigin("*"); + ALLOW_CORS_CONFIG.addAllowedMethod("*"); + ALLOW_CORS_CONFIG.addAllowedHeader("*"); + ALLOW_CORS_CONFIG.setAllowCredentials(true); + } - private HandlerMethodMappingNamingStrategy<T> namingStrategy; - private final Map<T, HandlerMethod> handlerMethods = new LinkedHashMap<T, HandlerMethod>(); + private boolean detectHandlerMethodsInAncestorContexts = false; - private final MultiValueMap<String, T> urlMap = new LinkedMultiValueMap<String, T>(); + private HandlerMethodMappingNamingStrategy<T> namingStrategy; - private final MultiValueMap<String, HandlerMethod> nameMap = new LinkedMultiValueMap<String, HandlerMethod>(); + private final MappingRegistry mappingRegistry = new MappingRegistry(); /** @@ -94,27 +106,75 @@ public abstract class AbstractHandlerMethodMapping<T> extends AbstractHandlerMap /** * Configure the naming strategy to use for assigning a default name to every * mapped handler method. + * <p>The default naming strategy is based on the capital letters of the + * class name followed by "#" and then the method name, e.g. "TC#getFoo" + * for a class named TestController with method getFoo. */ public void setHandlerMethodMappingNamingStrategy(HandlerMethodMappingNamingStrategy<T> namingStrategy) { this.namingStrategy = namingStrategy; } /** - * Return a map with all handler methods and their mappings. + * Return the configured naming strategy or {@code null}. + */ + public HandlerMethodMappingNamingStrategy<T> getNamingStrategy() { + return this.namingStrategy; + } + + /** + * Return a (read-only) map with all mappings and HandlerMethod's. */ public Map<T, HandlerMethod> getHandlerMethods() { - return Collections.unmodifiableMap(this.handlerMethods); + this.mappingRegistry.acquireReadLock(); + try { + return Collections.unmodifiableMap(this.mappingRegistry.getMappings()); + } + finally { + this.mappingRegistry.releaseReadLock(); + } } /** - * Return the handler methods mapped to the mapping with the given name. + * Return the handler methods for the given mapping name. * @param mappingName the mapping name + * @return a list of matching HandlerMethod's or {@code null}; the returned + * list will never be modified and is safe to iterate. + * @see #setHandlerMethodMappingNamingStrategy */ public List<HandlerMethod> getHandlerMethodsForMappingName(String mappingName) { - return this.nameMap.get(mappingName); + return this.mappingRegistry.getHandlerMethodsByMappingName(mappingName); + } + + /** + * Return the internal mapping registry. Provided for testing purposes. + */ + MappingRegistry getMappingRegistry() { + return this.mappingRegistry; + } + + /** + * Register the given mapping. + * <p>This method may be invoked at runtime after initialization has completed. + * @param mapping the mapping for the handler method + * @param handler the handler + * @param method the method + */ + public void registerMapping(T mapping, Object handler, Method method) { + this.mappingRegistry.register(mapping, handler, method); + } + + /** + * Un-register the given mapping. + * <p>This method may be invoked at runtime after initialization has completed. + * @param mapping the mapping to unregister + */ + public void unregisterMapping(T mapping) { + this.mappingRegistry.unregister(mapping); } + // Handler method detection + /** * Detects handler methods at initialization. */ @@ -133,69 +193,56 @@ public abstract class AbstractHandlerMethodMapping<T> extends AbstractHandlerMap if (logger.isDebugEnabled()) { logger.debug("Looking for request mappings in application context: " + getApplicationContext()); } - String[] beanNames = (this.detectHandlerMethodsInAncestorContexts ? BeanFactoryUtils.beanNamesForTypeIncludingAncestors(getApplicationContext(), Object.class) : getApplicationContext().getBeanNamesForType(Object.class)); for (String beanName : beanNames) { - if (!beanName.startsWith(SCOPED_TARGET_NAME_PREFIX) && - isHandler(getApplicationContext().getType(beanName))){ - detectHandlerMethods(beanName); + if (!beanName.startsWith(SCOPED_TARGET_NAME_PREFIX)) { + Class<?> beanType = null; + try { + beanType = getApplicationContext().getType(beanName); + } + catch (Throwable ex) { + // An unresolvable bean type, probably from a lazy bean - let's ignore it. + if (logger.isDebugEnabled()) { + logger.debug("Could not resolve target class for bean with name '" + beanName + "'", ex); + } + } + if (beanType != null && isHandler(beanType)) { + detectHandlerMethods(beanName); + } } } handlerMethodsInitialized(getHandlerMethods()); } /** - * Whether the given type is a handler with handler methods. - * @param beanType the type of the bean being checked - * @return "true" if this a handler type, "false" otherwise. - */ - protected abstract boolean isHandler(Class<?> beanType); - - /** * Look for handler methods in a handler. * @param handler the bean name of a handler or a handler instance */ protected void detectHandlerMethods(final Object handler) { - Class<?> handlerType = - (handler instanceof String ? getApplicationContext().getType((String) handler) : handler.getClass()); - - // Avoid repeated calls to getMappingForMethod which would rebuild RequestMappingInfo instances - final Map<Method, T> mappings = new IdentityHashMap<Method, T>(); + Class<?> handlerType = (handler instanceof String ? + getApplicationContext().getType((String) handler) : handler.getClass()); final Class<?> userType = ClassUtils.getUserClass(handlerType); - Set<Method> methods = HandlerMethodSelector.selectMethods(userType, new MethodFilter() { - @Override - public boolean matches(Method method) { - T mapping = getMappingForMethod(method, userType); - if (mapping != null) { - mappings.put(method, mapping); - return true; - } - else { - return false; - } - } - }); + Map<Method, T> methods = MethodIntrospector.selectMethods(userType, + new MethodIntrospector.MetadataLookup<T>() { + @Override + public T inspect(Method method) { + return getMappingForMethod(method, userType); + } + }); - for (Method method : methods) { - registerHandlerMethod(handler, method, mappings.get(method)); + if (logger.isDebugEnabled()) { + logger.debug(methods.size() + " request handler methods found on " + userType + ": " + methods); + } + for (Map.Entry<Method, T> entry : methods.entrySet()) { + registerHandlerMethod(handler, entry.getKey(), entry.getValue()); } } /** - * Provide the mapping for a handler method. A method for which no - * mapping can be provided is not a handler method. - * @param method the method to provide a mapping for - * @param handlerType the handler type, possibly a sub-type of the method's - * declaring class - * @return the mapping, or {@code null} if the method is not mapped - */ - protected abstract T getMappingForMethod(Method method, Class<?> handlerType); - - /** * Register a handler method and its unique mapping. Invoked at startup for * each detected handler method. * @param handler the bean name of the handler or the handler instance @@ -205,52 +252,7 @@ public abstract class AbstractHandlerMethodMapping<T> extends AbstractHandlerMap * under the same mapping */ protected void registerHandlerMethod(Object handler, Method method, T mapping) { - HandlerMethod newHandlerMethod = createHandlerMethod(handler, method); - HandlerMethod oldHandlerMethod = this.handlerMethods.get(mapping); - if (oldHandlerMethod != null && !oldHandlerMethod.equals(newHandlerMethod)) { - throw new IllegalStateException("Ambiguous mapping found. Cannot map '" + newHandlerMethod.getBean() + - "' bean method \n" + newHandlerMethod + "\nto " + mapping + ": There is already '" + - oldHandlerMethod.getBean() + "' bean method\n" + oldHandlerMethod + " mapped."); - } - - this.handlerMethods.put(mapping, newHandlerMethod); - if (logger.isInfoEnabled()) { - logger.info("Mapped \"" + mapping + "\" onto " + newHandlerMethod); - } - - Set<String> patterns = getMappingPathPatterns(mapping); - for (String pattern : patterns) { - if (!getPathMatcher().isPattern(pattern)) { - this.urlMap.add(pattern, mapping); - } - } - - if (this.namingStrategy != null) { - String name = this.namingStrategy.getName(newHandlerMethod, mapping); - updateNameMap(name, newHandlerMethod); - } - } - - private void updateNameMap(String name, HandlerMethod newHandlerMethod) { - List<HandlerMethod> handlerMethods = this.nameMap.get(name); - if (handlerMethods != null) { - for (HandlerMethod handlerMethod : handlerMethods) { - if (handlerMethod.getMethod().equals(newHandlerMethod.getMethod())) { - logger.trace("Mapping name already registered. Multiple controller instances perhaps?"); - return; - } - } - } - - logger.trace("Mapping name=" + name); - this.nameMap.add(name, newHandlerMethod); - - if (this.nameMap.get(name).size() > 1) { - if (logger.isDebugEnabled()) { - logger.debug("Mapping name clash for handlerMethods=" + this.nameMap.get(name) + - ". Consider assigning explicit names."); - } - } + this.mappingRegistry.register(mapping, handler, method); } /** @@ -273,9 +275,11 @@ public abstract class AbstractHandlerMethodMapping<T> extends AbstractHandlerMap } /** - * Extract and return the URL paths contained in a mapping. + * Extract and return the CORS configuration for the mapping. */ - protected abstract Set<String> getMappingPathPatterns(T mapping); + protected CorsConfiguration initCorsConfiguration(Object handler, Method method, T mapping) { + return null; + } /** * Invoked after all handler methods have been detected. @@ -285,6 +289,8 @@ public abstract class AbstractHandlerMethodMapping<T> extends AbstractHandlerMap } + // Handler method lookup + /** * Look up a handler method for the given request. */ @@ -294,16 +300,22 @@ public abstract class AbstractHandlerMethodMapping<T> extends AbstractHandlerMap if (logger.isDebugEnabled()) { logger.debug("Looking up handler method for path " + lookupPath); } - HandlerMethod handlerMethod = lookupHandlerMethod(lookupPath, request); - if (logger.isDebugEnabled()) { - if (handlerMethod != null) { - logger.debug("Returning handler method [" + handlerMethod + "]"); - } - else { - logger.debug("Did not find handler method for [" + lookupPath + "]"); + this.mappingRegistry.acquireReadLock(); + try { + HandlerMethod handlerMethod = lookupHandlerMethod(lookupPath, request); + if (logger.isDebugEnabled()) { + if (handlerMethod != null) { + logger.debug("Returning handler method [" + handlerMethod + "]"); + } + else { + logger.debug("Did not find handler method for [" + lookupPath + "]"); + } } + return (handlerMethod != null ? handlerMethod.createWithResolvedBean() : null); + } + finally { + this.mappingRegistry.releaseReadLock(); } - return (handlerMethod != null ? handlerMethod.createWithResolvedBean() : null); } /** @@ -317,37 +329,40 @@ public abstract class AbstractHandlerMethodMapping<T> extends AbstractHandlerMap */ protected HandlerMethod lookupHandlerMethod(String lookupPath, HttpServletRequest request) throws Exception { List<Match> matches = new ArrayList<Match>(); - List<T> directPathMatches = this.urlMap.get(lookupPath); + List<T> directPathMatches = this.mappingRegistry.getMappingsByUrl(lookupPath); if (directPathMatches != null) { addMatchingMappings(directPathMatches, matches, request); } if (matches.isEmpty()) { // No choice but to go through all mappings... - addMatchingMappings(this.handlerMethods.keySet(), matches, request); + addMatchingMappings(this.mappingRegistry.getMappings().keySet(), matches, request); } if (!matches.isEmpty()) { Comparator<Match> comparator = new MatchComparator(getMappingComparator(request)); Collections.sort(matches, comparator); if (logger.isTraceEnabled()) { - logger.trace("Found " + matches.size() + " matching mapping(s) for [" + lookupPath + "] : " + matches); + logger.trace("Found " + matches.size() + " matching mapping(s) for [" + + lookupPath + "] : " + matches); } Match bestMatch = matches.get(0); if (matches.size() > 1) { + if (CorsUtils.isPreFlightRequest(request)) { + return PREFLIGHT_AMBIGUOUS_MATCH; + } Match secondBestMatch = matches.get(1); if (comparator.compare(bestMatch, secondBestMatch) == 0) { Method m1 = bestMatch.handlerMethod.getMethod(); Method m2 = secondBestMatch.handlerMethod.getMethod(); - throw new IllegalStateException( - "Ambiguous handler methods mapped for HTTP path '" + request.getRequestURL() + "': {" + - m1 + ", " + m2 + "}"); + throw new IllegalStateException("Ambiguous handler methods mapped for HTTP path '" + + request.getRequestURL() + "': {" + m1 + ", " + m2 + "}"); } } handleMatch(bestMatch.mapping, lookupPath, request); return bestMatch.handlerMethod; } else { - return handleNoMatch(this.handlerMethods.keySet(), lookupPath, request); + return handleNoMatch(this.mappingRegistry.getMappings().keySet(), lookupPath, request); } } @@ -355,11 +370,75 @@ public abstract class AbstractHandlerMethodMapping<T> extends AbstractHandlerMap for (T mapping : mappings) { T match = getMatchingMapping(mapping, request); if (match != null) { - matches.add(new Match(match, this.handlerMethods.get(mapping))); + matches.add(new Match(match, this.mappingRegistry.getMappings().get(mapping))); + } + } + } + + /** + * Invoked when a matching mapping is found. + * @param mapping the matching mapping + * @param lookupPath mapping lookup path within the current servlet mapping + * @param request the current request + */ + protected void handleMatch(T mapping, String lookupPath, HttpServletRequest request) { + request.setAttribute(HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE, lookupPath); + } + + /** + * Invoked when no matching mapping is not found. + * @param mappings all registered mappings + * @param lookupPath mapping lookup path within the current servlet mapping + * @param request the current request + * @throws ServletException in case of errors + */ + protected HandlerMethod handleNoMatch(Set<T> mappings, String lookupPath, HttpServletRequest request) + throws Exception { + + return null; + } + + @Override + protected CorsConfiguration getCorsConfiguration(Object handler, HttpServletRequest request) { + CorsConfiguration corsConfig = super.getCorsConfiguration(handler, request); + if (handler instanceof HandlerMethod) { + HandlerMethod handlerMethod = (HandlerMethod) handler; + if (handlerMethod.equals(PREFLIGHT_AMBIGUOUS_MATCH)) { + return AbstractHandlerMethodMapping.ALLOW_CORS_CONFIG; + } + else { + CorsConfiguration corsConfigFromMethod = this.mappingRegistry.getCorsConfiguration(handlerMethod); + corsConfig = (corsConfig != null ? corsConfig.combine(corsConfigFromMethod) : corsConfigFromMethod); } } + return corsConfig; } + + // Abstract template methods + + /** + * Whether the given type is a handler with handler methods. + * @param beanType the type of the bean being checked + * @return "true" if this a handler type, "false" otherwise. + */ + protected abstract boolean isHandler(Class<?> beanType); + + /** + * Provide the mapping for a handler method. A method for which no + * mapping can be provided is not a handler method. + * @param method the method to provide a mapping for + * @param handlerType the handler type, possibly a sub-type of the method's + * declaring class + * @return the mapping, or {@code null} if the method is not mapped + */ + protected abstract T getMappingForMethod(Method method, Class<?> handlerType); + + /** + * Extract and return the URL paths contained in a mapping. + */ + protected abstract Set<String> getMappingPathPatterns(T mapping); + /** * Check if a mapping matches the current request and return a (potentially * new) mapping with conditions relevant to the current request. @@ -377,27 +456,245 @@ public abstract class AbstractHandlerMethodMapping<T> extends AbstractHandlerMap */ protected abstract Comparator<T> getMappingComparator(HttpServletRequest request); + /** - * Invoked when a matching mapping is found. - * @param mapping the matching mapping - * @param lookupPath mapping lookup path within the current servlet mapping - * @param request the current request + * A registry that maintains all mappings to handler methods, exposing methods + * to perform lookups and providing concurrent access. + * + * <p>Package-private for testing purposes. */ - protected void handleMatch(T mapping, String lookupPath, HttpServletRequest request) { - request.setAttribute(HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE, lookupPath); + class MappingRegistry { + + private final Map<T, MappingRegistration<T>> registry = new HashMap<T, MappingRegistration<T>>(); + + private final Map<T, HandlerMethod> mappingLookup = new LinkedHashMap<T, HandlerMethod>(); + + private final MultiValueMap<String, T> urlLookup = new LinkedMultiValueMap<String, T>(); + + private final Map<String, List<HandlerMethod>> nameLookup = + new ConcurrentHashMap<String, List<HandlerMethod>>(); + + private final Map<HandlerMethod, CorsConfiguration> corsLookup = + new ConcurrentHashMap<HandlerMethod, CorsConfiguration>(); + + private final ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock(); + + /** + * Return all mappings and handler methods. Not thread-safe. + * @see #acquireReadLock() + */ + public Map<T, HandlerMethod> getMappings() { + return this.mappingLookup; + } + + /** + * Return matches for the given URL path. Not thread-safe. + * @see #acquireReadLock() + */ + public List<T> getMappingsByUrl(String urlPath) { + return this.urlLookup.get(urlPath); + } + + /** + * Return handler methods by mapping name. Thread-safe for concurrent use. + */ + public List<HandlerMethod> getHandlerMethodsByMappingName(String mappingName) { + return this.nameLookup.get(mappingName); + } + + /** + * Return CORS configuration. Thread-safe for concurrent use. + */ + public CorsConfiguration getCorsConfiguration(HandlerMethod handlerMethod) { + HandlerMethod original = handlerMethod.getResolvedFromHandlerMethod(); + return this.corsLookup.get(original != null ? original : handlerMethod); + } + + /** + * Acquire the read lock when using getMappings and getMappingsByUrl. + */ + public void acquireReadLock() { + this.readWriteLock.readLock().lock(); + } + + /** + * Release the read lock after using getMappings and getMappingsByUrl. + */ + public void releaseReadLock() { + this.readWriteLock.readLock().unlock(); + } + + public void register(T mapping, Object handler, Method method) { + this.readWriteLock.writeLock().lock(); + try { + HandlerMethod handlerMethod = createHandlerMethod(handler, method); + assertUniqueMethodMapping(handlerMethod, mapping); + + if (logger.isInfoEnabled()) { + logger.info("Mapped \"" + mapping + "\" onto " + handlerMethod); + } + this.mappingLookup.put(mapping, handlerMethod); + + List<String> directUrls = getDirectUrls(mapping); + for (String url : directUrls) { + this.urlLookup.add(url, mapping); + } + + String name = null; + if (getNamingStrategy() != null) { + name = getNamingStrategy().getName(handlerMethod, mapping); + addMappingName(name, handlerMethod); + } + + CorsConfiguration corsConfig = initCorsConfiguration(handler, method, mapping); + if (corsConfig != null) { + this.corsLookup.put(handlerMethod, corsConfig); + } + + this.registry.put(mapping, new MappingRegistration<T>(mapping, handlerMethod, directUrls, name)); + } + finally { + this.readWriteLock.writeLock().unlock(); + } + } + + private void assertUniqueMethodMapping(HandlerMethod newHandlerMethod, T mapping) { + HandlerMethod handlerMethod = this.mappingLookup.get(mapping); + if (handlerMethod != null && !handlerMethod.equals(newHandlerMethod)) { + throw new IllegalStateException( + "Ambiguous mapping. Cannot map '" + newHandlerMethod.getBean() + "' method \n" + + newHandlerMethod + "\nto " + mapping + ": There is already '" + + handlerMethod.getBean() + "' bean method\n" + handlerMethod + " mapped."); + } + } + + private List<String> getDirectUrls(T mapping) { + List<String> urls = new ArrayList<String>(1); + for (String path : getMappingPathPatterns(mapping)) { + if (!getPathMatcher().isPattern(path)) { + urls.add(path); + } + } + return urls; + } + + private void addMappingName(String name, HandlerMethod handlerMethod) { + List<HandlerMethod> oldList = this.nameLookup.get(name); + if (oldList == null) { + oldList = Collections.<HandlerMethod>emptyList(); + } + + for (HandlerMethod current : oldList) { + if (handlerMethod.equals(current)) { + return; + } + } + + if (logger.isTraceEnabled()) { + logger.trace("Mapping name '" + name + "'"); + } + + List<HandlerMethod> newList = new ArrayList<HandlerMethod>(oldList.size() + 1); + newList.addAll(oldList); + newList.add(handlerMethod); + this.nameLookup.put(name, newList); + + if (newList.size() > 1) { + if (logger.isTraceEnabled()) { + logger.trace("Mapping name clash for handlerMethods " + newList + + ". Consider assigning explicit names."); + } + } + } + + public void unregister(T mapping) { + this.readWriteLock.writeLock().lock(); + try { + MappingRegistration<T> definition = this.registry.remove(mapping); + if (definition == null) { + return; + } + + this.mappingLookup.remove(definition.getMapping()); + + for (String url : definition.getDirectUrls()) { + List<T> list = this.urlLookup.get(url); + if (list != null) { + list.remove(definition.getMapping()); + if (list.isEmpty()) { + this.urlLookup.remove(url); + } + } + } + + removeMappingName(definition); + + this.corsLookup.remove(definition.getHandlerMethod()); + } + finally { + this.readWriteLock.writeLock().unlock(); + } + } + + private void removeMappingName(MappingRegistration<T> definition) { + String name = definition.getMappingName(); + if (name == null) { + return; + } + HandlerMethod handlerMethod = definition.getHandlerMethod(); + List<HandlerMethod> oldList = this.nameLookup.get(name); + if (oldList == null) { + return; + } + if (oldList.size() <= 1) { + this.nameLookup.remove(name); + return; + } + List<HandlerMethod> newList = new ArrayList<HandlerMethod>(oldList.size() - 1); + for (HandlerMethod current : oldList) { + if (!current.equals(handlerMethod)) { + newList.add(current); + } + } + this.nameLookup.put(name, newList); + } } - /** - * Invoked when no matching mapping is not found. - * @param mappings all registered mappings - * @param lookupPath mapping lookup path within the current servlet mapping - * @param request the current request - * @throws ServletException in case of errors - */ - protected HandlerMethod handleNoMatch(Set<T> mappings, String lookupPath, HttpServletRequest request) - throws Exception { - return null; + private static class MappingRegistration<T> { + + private final T mapping; + + private final HandlerMethod handlerMethod; + + private final List<String> directUrls; + + private final String mappingName; + + public MappingRegistration(T mapping, HandlerMethod handlerMethod, List<String> directUrls, String mappingName) { + Assert.notNull(mapping); + Assert.notNull(handlerMethod); + this.mapping = mapping; + this.handlerMethod = handlerMethod; + this.directUrls = (directUrls != null ? directUrls : Collections.<String>emptyList()); + this.mappingName = mappingName; + } + + public T getMapping() { + return this.mapping; + } + + public HandlerMethod getHandlerMethod() { + return this.handlerMethod; + } + + public List<String> getDirectUrls() { + return this.directUrls; + } + + public String getMappingName() { + return this.mappingName; + } } @@ -437,4 +734,12 @@ public abstract class AbstractHandlerMethodMapping<T> extends AbstractHandlerMap } } + + private static class EmptyHandler { + + public void handle() { + throw new UnsupportedOperationException("not implemented"); + } + } + } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/AbstractUrlHandlerMapping.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/AbstractUrlHandlerMapping.java index 1817dae3..5d6406e5 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/AbstractUrlHandlerMapping.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/AbstractUrlHandlerMapping.java @@ -54,6 +54,8 @@ public abstract class AbstractUrlHandlerMapping extends AbstractHandlerMapping { private Object rootHandler; + private boolean useTrailingSlashMatch = false; + private boolean lazyInitHandlers = false; private final Map<String, Object> handlerMap = new LinkedHashMap<String, Object>(); @@ -77,6 +79,22 @@ public abstract class AbstractUrlHandlerMapping extends AbstractHandlerMapping { } /** + * Whether to match to URLs irrespective of the presence of a trailing slash. + * If enabled a URL pattern such as "/users" also matches to "/users/". + * <p>The default value is {@code false}. + */ + public void setUseTrailingSlashMatch(boolean useTrailingSlashMatch) { + this.useTrailingSlashMatch = useTrailingSlashMatch; + } + + /** + * Whether to match to URLs irrespective of the presence of a trailing slash. + */ + public boolean useTrailingSlashMatch() { + return this.useTrailingSlashMatch; + } + + /** * Set whether to lazily initialize handlers. Only applicable to * singleton handlers, as prototypes are always lazily initialized. * Default is "false", as eager initialization allows for more efficiency @@ -159,6 +177,11 @@ public abstract class AbstractUrlHandlerMapping extends AbstractHandlerMapping { if (getPathMatcher().match(registeredPattern, urlPath)) { matchingPatterns.add(registeredPattern); } + else if (useTrailingSlashMatch()) { + if (!registeredPattern.endsWith("/") && getPathMatcher().match(registeredPattern + "/", urlPath)) { + matchingPatterns.add(registeredPattern +"/"); + } + } } String bestPatternMatch = null; Comparator<String> patternComparator = getPathMatcher().getPatternComparator(urlPath); @@ -171,6 +194,10 @@ public abstract class AbstractUrlHandlerMapping extends AbstractHandlerMapping { } if (bestPatternMatch != null) { handler = this.handlerMap.get(bestPatternMatch); + if (handler == null) { + Assert.isTrue(bestPatternMatch.endsWith("/")); + handler = this.handlerMap.get(bestPatternMatch.substring(0, bestPatternMatch.length() - 1)); + } // Bean name or resolved handler? if (handler instanceof String) { String handlerName = (String) handler; diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/MappedInterceptor.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/MappedInterceptor.java index 88b1b86d..a238d537 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/MappedInterceptor.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/MappedInterceptor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 the original author or authors. + * Copyright 2002-2015 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,14 +16,18 @@ package org.springframework.web.servlet.handler; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + import org.springframework.util.PathMatcher; import org.springframework.web.context.request.WebRequestInterceptor; import org.springframework.web.servlet.HandlerInterceptor; +import org.springframework.web.servlet.ModelAndView; /** - * Contains a {@link HandlerInterceptor} along with include (and optionally - * exclude) path patterns to which the interceptor should apply. Also provides - * matching logic to test if the interceptor applies to a given request path. + * Contains and delegates calls to a {@link HandlerInterceptor} along with + * include (and optionally exclude) path patterns to which the interceptor should apply. + * Also provides matching logic to test if the interceptor applies to a given request path. * * <p>A MappedInterceptor can be registered directly with any * {@link org.springframework.web.servlet.handler.AbstractHandlerMethodMapping @@ -34,9 +38,10 @@ import org.springframework.web.servlet.HandlerInterceptor; * * @author Keith Donald * @author Rossen Stoyanchev + * @author Brian Clozel * @since 3.0 */ -public final class MappedInterceptor { +public final class MappedInterceptor implements HandlerInterceptor { private final String[] includePatterns; @@ -122,6 +127,21 @@ public final class MappedInterceptor { return this.interceptor; } + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { + return this.interceptor.preHandle(request, response, handler); + } + + @Override + public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { + this.interceptor.postHandle(request, response, handler, modelAndView); + } + + @Override + public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { + this.interceptor.afterCompletion(request, response, handler, ex); + } + /** * Returns {@code true} if the interceptor applies to the given request path. * @param lookupPath the current request path diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/SimpleMappingExceptionResolver.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/SimpleMappingExceptionResolver.java index 8ca6708d..b24363f1 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/SimpleMappingExceptionResolver.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/SimpleMappingExceptionResolver.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2013 the original author or authors. + * Copyright 2002-2015 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -267,7 +267,7 @@ public class SimpleMappingExceptionResolver extends AbstractHandlerExceptionReso return depth; } // If we've gone as far as we can go and haven't found it... - if (exceptionClass.equals(Throwable.class)) { + if (exceptionClass == Throwable.class) { return -1; } return getDepth(exceptionMapping, exceptionClass.getSuperclass(), depth + 1); diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/i18n/CookieLocaleResolver.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/i18n/CookieLocaleResolver.java index 4bf43e28..547a91fc 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/i18n/CookieLocaleResolver.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/i18n/CookieLocaleResolver.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2013 the original author or authors. + * Copyright 2002-2015 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -111,6 +111,7 @@ public class CookieLocaleResolver extends CookieGenerator implements LocaleConte /** * Set a fixed TimeZone that this resolver will return if no cookie found. + * @since 4.0 */ public void setDefaultTimeZone(TimeZone defaultTimeZone) { this.defaultTimeZone = defaultTimeZone; @@ -119,6 +120,7 @@ public class CookieLocaleResolver extends CookieGenerator implements LocaleConte /** * Return the fixed TimeZone that this resolver will return if no cookie found, * if any. + * @since 4.0 */ protected TimeZone getDefaultTimeZone() { return this.defaultTimeZone; @@ -171,7 +173,7 @@ public class CookieLocaleResolver extends CookieGenerator implements LocaleConte } } request.setAttribute(LOCALE_REQUEST_ATTRIBUTE_NAME, - (locale != null ? locale: determineDefaultLocale(request))); + (locale != null ? locale : determineDefaultLocale(request))); request.setAttribute(TIME_ZONE_REQUEST_ATTRIBUTE_NAME, (timeZone != null ? timeZone : determineDefaultTimeZone(request))); } @@ -197,7 +199,7 @@ public class CookieLocaleResolver extends CookieGenerator implements LocaleConte removeCookie(response); } request.setAttribute(LOCALE_REQUEST_ATTRIBUTE_NAME, - (locale != null ? locale: determineDefaultLocale(request))); + (locale != null ? locale : determineDefaultLocale(request))); request.setAttribute(TIME_ZONE_REQUEST_ATTRIBUTE_NAME, (timeZone != null ? timeZone : determineDefaultTimeZone(request))); } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/i18n/LocaleChangeInterceptor.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/i18n/LocaleChangeInterceptor.java index 833c2962..099e4b46 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/i18n/LocaleChangeInterceptor.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/i18n/LocaleChangeInterceptor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2013 the original author or authors. + * Copyright 2002-2015 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,6 +20,10 @@ import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; import org.springframework.web.servlet.LocaleResolver; import org.springframework.web.servlet.handler.HandlerInterceptorAdapter; @@ -30,6 +34,7 @@ import org.springframework.web.servlet.support.RequestContextUtils; * via a configurable request parameter (default parameter name: "locale"). * * @author Juergen Hoeller + * @author Rossen Stoyanchev * @since 20.06.2003 * @see org.springframework.web.servlet.LocaleResolver */ @@ -40,8 +45,15 @@ public class LocaleChangeInterceptor extends HandlerInterceptorAdapter { */ public static final String DEFAULT_PARAM_NAME = "locale"; + + protected final Log logger = LogFactory.getLog(getClass()); + private String paramName = DEFAULT_PARAM_NAME; + private String[] httpMethods; + + private boolean ignoreInvalidLocale = false; + /** * Set the name of the parameter that contains a locale specification @@ -59,21 +71,80 @@ public class LocaleChangeInterceptor extends HandlerInterceptorAdapter { return this.paramName; } + /** + * Configure the HTTP method(s) over which the locale can be changed. + * @param httpMethods the methods + * @since 4.2 + */ + public void setHttpMethods(String... httpMethods) { + this.httpMethods = httpMethods; + } + + /** + * Return the configured HTTP methods. + * @since 4.2 + */ + public String[] getHttpMethods() { + return this.httpMethods; + } + + /** + * Set whether to ignore an invalid value for the locale parameter. + * @since 4.2.2 + */ + public void setIgnoreInvalidLocale(boolean ignoreInvalidLocale) { + this.ignoreInvalidLocale = ignoreInvalidLocale; + } + + /** + * Return whether to ignore an invalid value for the locale parameter. + * @since 4.2.2 + */ + public boolean isIgnoreInvalidLocale() { + return this.ignoreInvalidLocale; + } + @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws ServletException { - String newLocale = request.getParameter(this.paramName); + String newLocale = request.getParameter(getParamName()); if (newLocale != null) { - LocaleResolver localeResolver = RequestContextUtils.getLocaleResolver(request); - if (localeResolver == null) { - throw new IllegalStateException("No LocaleResolver found: not in a DispatcherServlet request?"); + if (checkHttpMethod(request.getMethod())) { + LocaleResolver localeResolver = RequestContextUtils.getLocaleResolver(request); + if (localeResolver == null) { + throw new IllegalStateException( + "No LocaleResolver found: not in a DispatcherServlet request?"); + } + try { + localeResolver.setLocale(request, response, StringUtils.parseLocaleString(newLocale)); + } + catch (IllegalArgumentException ex) { + if (isIgnoreInvalidLocale()) { + logger.debug("Ignoring invalid locale value [" + newLocale + "]: " + ex.getMessage()); + } + else { + throw ex; + } + } } - localeResolver.setLocale(request, response, StringUtils.parseLocaleString(newLocale)); } // Proceed in any case. return true; } + private boolean checkHttpMethod(String currentMethod) { + String[] configuredMethods = getHttpMethods(); + if (ObjectUtils.isEmpty(configuredMethods)) { + return true; + } + for (String configuredMethod : configuredMethods) { + if (configuredMethod.equalsIgnoreCase(currentMethod)) { + return true; + } + } + return false; + } + } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/i18n/SessionLocaleResolver.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/i18n/SessionLocaleResolver.java index 1aa94df6..6437f87b 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/i18n/SessionLocaleResolver.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/i18n/SessionLocaleResolver.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2013 the original author or authors. + * Copyright 2002-2015 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -32,15 +32,25 @@ import org.springframework.web.util.WebUtils; * accept-header locale. * * <p>This is most appropriate if the application needs user sessions anyway, - * that is, when the HttpSession does not have to be created for the locale. - * The session may optionally contain an associated time zone attribute as well; - * alternatively, you may specify a default time zone. + * i.e. when the {@code HttpSession} does not have to be created just for storing + * the user's locale. The session may optionally contain an associated time zone + * attribute as well; alternatively, you may specify a default time zone. * * <p>Custom controllers can override the user's locale and time zone by calling * {@code #setLocale(Context)} on the resolver, e.g. responding to a locale change * request. As a more convenient alternative, consider using * {@link org.springframework.web.servlet.support.RequestContext#changeLocale}. * + * <p>In contrast to {@link CookieLocaleResolver}, this strategy stores locally + * chosen locale settings in the Servlet container's {@code HttpSession}. As a + * consequence, those settings are just temporary for each session and therefore + * lost when each session terminates. + * + * <p>Note that there is no direct relationship with external session management + * mechanisms such as the "Spring Session" project. This {@code LocaleResolver} + * will simply evaluate and modify corresponding {@code HttpSession} attributes + * against the current {@code HttpServletRequest}. + * * @author Juergen Hoeller * @since 27.02.2003 * @see #setDefaultLocale diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/AbstractController.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/AbstractController.java index 772953ff..46fab982 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/AbstractController.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/AbstractController.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2014 the original author or authors. + * Copyright 2002-2015 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -25,7 +25,7 @@ import org.springframework.web.servlet.support.WebContentGenerator; import org.springframework.web.util.WebUtils; /** - * <p>Convenient superclass for controller implementations, using the Template Method + * Convenient superclass for controller implementations, using the Template Method * design pattern. * * <p><b><a name="workflow">Workflow @@ -130,7 +130,8 @@ public abstract class AbstractController extends WebContentGenerator implements throws Exception { // Delegate to WebContentGenerator for checking and preparing. - checkAndPrepare(request, response, this instanceof LastModified); + checkRequest(request); + prepareResponse(response); // Execute handleRequestInternal in synchronized block if required. if (this.synchronizeOnSession) { @@ -152,6 +153,6 @@ public abstract class AbstractController extends WebContentGenerator implements * @see #handleRequest */ protected abstract ModelAndView handleRequestInternal(HttpServletRequest request, HttpServletResponse response) - throws Exception; + throws Exception; } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/WebContentInterceptor.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/WebContentInterceptor.java index f9c23f84..038667f4 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/WebContentInterceptor.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/WebContentInterceptor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 the original author or authors. + * Copyright 2002-2015 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,6 +20,7 @@ import java.util.Enumeration; import java.util.HashMap; import java.util.Map; import java.util.Properties; + import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; @@ -27,21 +28,24 @@ import javax.servlet.http.HttpServletResponse; import org.springframework.util.AntPathMatcher; import org.springframework.util.Assert; import org.springframework.util.PathMatcher; +import org.springframework.http.CacheControl; import org.springframework.web.servlet.HandlerInterceptor; import org.springframework.web.servlet.ModelAndView; import org.springframework.web.servlet.support.WebContentGenerator; import org.springframework.web.util.UrlPathHelper; /** - * Interceptor that checks and prepares request and response. Checks for supported - * methods and a required session, and applies the specified number of cache seconds. + * Handler interceptor that checks the request and prepares the response. + * Checks for supported methods and a required session, and applies the + * specified {@link org.springframework.http.CacheControl} builder. * See superclass bean properties for configuration options. * - * <p>All the settings supported by this interceptor can also be set on AbstractController. - * This interceptor is mainly intended for applying checks and preparations to a set of - * controllers mapped by a HandlerMapping. + * <p>All the settings supported by this interceptor can also be set on + * {@link AbstractController}. This interceptor is mainly intended for applying + * checks and preparations to a set of controllers mapped by a HandlerMapping. * * @author Juergen Hoeller + * @author Brian Clozel * @since 27.11.2003 * @see AbstractController */ @@ -49,14 +53,16 @@ public class WebContentInterceptor extends WebContentGenerator implements Handle private UrlPathHelper urlPathHelper = new UrlPathHelper(); + private PathMatcher pathMatcher = new AntPathMatcher(); + private Map<String, Integer> cacheMappings = new HashMap<String, Integer>(); - private PathMatcher pathMatcher = new AntPathMatcher(); + private Map<String, CacheControl> cacheControlMappings = new HashMap<String, CacheControl>(); public WebContentInterceptor() { - // no restriction of HTTP methods by default, - // in particular for use with annotated controllers + // No restriction of HTTP methods by default, + // in particular for use with annotated controllers... super(false); } @@ -120,7 +126,28 @@ public class WebContentInterceptor extends WebContentGenerator implements Handle Enumeration<?> propNames = cacheMappings.propertyNames(); while (propNames.hasMoreElements()) { String path = (String) propNames.nextElement(); - this.cacheMappings.put(path, Integer.valueOf(cacheMappings.getProperty(path))); + int cacheSeconds = Integer.valueOf(cacheMappings.getProperty(path)); + this.cacheMappings.put(path, cacheSeconds); + } + } + + /** + * Map specific URL paths to a specific {@link org.springframework.http.CacheControl}. + * <p>Overrides the default cache seconds setting of this interceptor. + * Can specify a empty {@link org.springframework.http.CacheControl} instance + * to exclude a URL path from default caching. + * <p>Supports direct matches, e.g. a registered "/test" matches "/test", + * and a various Ant-style pattern matches, e.g. a registered "/t*" matches + * both "/test" and "/team". For details, see the AntPathMatcher javadoc. + * @param cacheControl the {@code CacheControl} to use + * @param paths URL paths that will map to the given {@code CacheControl} + * @see #setCacheSeconds + * @see org.springframework.util.AntPathMatcher + * @since 4.2 + */ + public void addCacheMapping(CacheControl cacheControl, String... paths) { + for (String path : paths) { + this.cacheControlMappings.put(path, cacheControl); } } @@ -128,6 +155,7 @@ public class WebContentInterceptor extends WebContentGenerator implements Handle * Set the PathMatcher implementation to use for matching URL paths * against registered URL patterns, for determining cache mappings. * Default is AntPathMatcher. + * @see #addCacheMapping * @see #setCacheMappings * @see org.springframework.util.AntPathMatcher */ @@ -139,44 +167,76 @@ public class WebContentInterceptor extends WebContentGenerator implements Handle @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) - throws ServletException { + throws ServletException { + + checkRequest(request); String lookupPath = this.urlPathHelper.getLookupPathForRequest(request); if (logger.isDebugEnabled()) { logger.debug("Looking up cache seconds for [" + lookupPath + "]"); } + CacheControl cacheControl = lookupCacheControl(lookupPath); Integer cacheSeconds = lookupCacheSeconds(lookupPath); - if (cacheSeconds != null) { + if (cacheControl != null) { if (logger.isDebugEnabled()) { - logger.debug("Applying " + cacheSeconds + " cache seconds to [" + lookupPath + "]"); + logger.debug("Applying CacheControl to [" + lookupPath + "]"); } - checkAndPrepare(request, response, cacheSeconds, handler instanceof LastModified); + applyCacheControl(response, cacheControl); + } + else if (cacheSeconds != null) { + if (logger.isDebugEnabled()) { + logger.debug("Applying CacheControl to [" + lookupPath + "]"); + } + applyCacheSeconds(response, cacheSeconds); } else { if (logger.isDebugEnabled()) { logger.debug("Applying default cache seconds to [" + lookupPath + "]"); } - checkAndPrepare(request, response, handler instanceof LastModified); + prepareResponse(response); } return true; } /** - * Look up a cache seconds value for the given URL path. + * Look up a {@link org.springframework.http.CacheControl} instance for the given URL path. + * <p>Supports direct matches, e.g. a registered "/test" matches "/test", + * and various Ant-style pattern matches, e.g. a registered "/t*" matches + * both "/test" and "/team". For details, see the AntPathMatcher class. + * @param urlPath URL the bean is mapped to + * @return the associated {@code CacheControl}, or {@code null} if not found + * @see org.springframework.util.AntPathMatcher + */ + protected CacheControl lookupCacheControl(String urlPath) { + // Direct match? + CacheControl cacheControl = this.cacheControlMappings.get(urlPath); + if (cacheControl == null) { + // Pattern match? + for (String registeredPath : this.cacheControlMappings.keySet()) { + if (this.pathMatcher.match(registeredPath, urlPath)) { + cacheControl = this.cacheControlMappings.get(registeredPath); + } + } + } + return cacheControl; + } + + /** + * Look up a cacheSeconds integer value for the given URL path. * <p>Supports direct matches, e.g. a registered "/test" matches "/test", * and various Ant-style pattern matches, e.g. a registered "/t*" matches * both "/test" and "/team". For details, see the AntPathMatcher class. * @param urlPath URL the bean is mapped to - * @return the associated cache seconds, or {@code null} if not found + * @return the cacheSeconds integer value, or {@code null} if not found * @see org.springframework.util.AntPathMatcher */ protected Integer lookupCacheSeconds(String urlPath) { - // direct match? + // Direct match? Integer cacheSeconds = this.cacheMappings.get(urlPath); if (cacheSeconds == null) { - // pattern match? + // Pattern match? for (String registeredPath : this.cacheMappings.keySet()) { if (this.pathMatcher.match(registeredPath, urlPath)) { cacheSeconds = this.cacheMappings.get(registeredPath); diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/annotation/AnnotationMethodHandlerAdapter.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/annotation/AnnotationMethodHandlerAdapter.java index 28cf8ce0..f3cdd873 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/annotation/AnnotationMethodHandlerAdapter.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/annotation/AnnotationMethodHandlerAdapter.java @@ -56,6 +56,7 @@ import org.springframework.beans.factory.config.ConfigurableBeanFactory; import org.springframework.core.DefaultParameterNameDiscoverer; import org.springframework.core.Ordered; import org.springframework.core.ParameterNameDiscoverer; +import org.springframework.core.annotation.AnnotatedElementUtils; import org.springframework.core.annotation.AnnotationUtils; import org.springframework.http.HttpEntity; import org.springframework.http.HttpHeaders; @@ -119,7 +120,7 @@ import org.springframework.web.util.WebUtils; /** * Implementation of the {@link org.springframework.web.servlet.HandlerAdapter} interface - * that maps handler methods based on HTTP paths, HTTP methods and request parameters + * that maps handler methods based on HTTP paths, HTTP methods, and request parameters * expressed through the {@link RequestMapping} annotation. * * <p>Supports request parameter binding through the {@link RequestParam} annotation. @@ -133,13 +134,13 @@ import org.springframework.web.util.WebUtils; * * @author Juergen Hoeller * @author Arjen Poutsma + * @author Sam Brannen * @since 2.5 * @see #setPathMatcher * @see #setMethodNameResolver * @see #setWebBindingInitializer * @see #setSessionAttributeStore - * - * @deprecated in Spring 3.2 in favor of + * @deprecated as of Spring 3.2, in favor of * {@link org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter RequestMappingHandlerAdapter} */ @Deprecated @@ -411,12 +412,9 @@ public class AnnotationMethodHandlerAdapter extends WebContentGenerator } if (annotatedWithSessionAttributes) { - // Always prevent caching in case of session attribute management. checkAndPrepare(request, response, this.cacheSecondsForSessionAttributeHandlers, true); - // Prepare cached set of session attributes names. } else { - // Uses configured default cacheSeconds setting. checkAndPrepare(request, response, true); } @@ -891,7 +889,7 @@ public class AnnotationMethodHandlerAdapter extends WebContentGenerator else if (Principal.class.isAssignableFrom(parameterType)) { return request.getUserPrincipal(); } - else if (Locale.class.equals(parameterType)) { + else if (Locale.class == parameterType) { return RequestContextUtils.getLocale(request); } else if (InputStream.class.isAssignableFrom(parameterType)) { @@ -915,19 +913,19 @@ public class AnnotationMethodHandlerAdapter extends WebContentGenerator public ModelAndView getModelAndView(Method handlerMethod, Class<?> handlerType, Object returnValue, ExtendedModelMap implicitModel, ServletWebRequest webRequest) throws Exception { - ResponseStatus responseStatusAnn = AnnotationUtils.findAnnotation(handlerMethod, ResponseStatus.class); - if (responseStatusAnn != null) { - HttpStatus responseStatus = responseStatusAnn.value(); - String reason = responseStatusAnn.reason(); + ResponseStatus responseStatus = AnnotatedElementUtils.findMergedAnnotation(handlerMethod, ResponseStatus.class); + if (responseStatus != null) { + HttpStatus statusCode = responseStatus.code(); + String reason = responseStatus.reason(); if (!StringUtils.hasText(reason)) { - webRequest.getResponse().setStatus(responseStatus.value()); + webRequest.getResponse().setStatus(statusCode.value()); } else { - webRequest.getResponse().sendError(responseStatus.value(), reason); + webRequest.getResponse().sendError(statusCode.value(), reason); } // to be picked up by the RedirectView - webRequest.getRequest().setAttribute(View.RESPONSE_STATUS_ATTRIBUTE, responseStatus); + webRequest.getRequest().setAttribute(View.RESPONSE_STATUS_ATTRIBUTE, statusCode); this.responseArgumentUsed = true; } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/annotation/AnnotationMethodHandlerExceptionResolver.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/annotation/AnnotationMethodHandlerExceptionResolver.java index 49c6ea90..70a59702 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/annotation/AnnotationMethodHandlerExceptionResolver.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/annotation/AnnotationMethodHandlerExceptionResolver.java @@ -43,7 +43,9 @@ import javax.xml.transform.Source; import org.springframework.core.ExceptionDepthComparator; import org.springframework.core.GenericTypeResolver; import org.springframework.core.MethodParameter; +import org.springframework.core.annotation.AnnotatedElementUtils; import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.core.annotation.SynthesizingMethodParameter; import org.springframework.http.HttpInputMessage; import org.springframework.http.HttpOutputMessage; import org.springframework.http.HttpStatus; @@ -260,7 +262,7 @@ public class AnnotationMethodHandlerExceptionResolver extends AbstractHandlerExc Object[] args = new Object[paramTypes.length]; Class<?> handlerType = handler.getClass(); for (int i = 0; i < args.length; i++) { - MethodParameter methodParam = new MethodParameter(handlerMethod, i); + MethodParameter methodParam = new SynthesizingMethodParameter(handlerMethod, i); GenericTypeResolver.resolveParameterType(methodParam, handlerType); Class<?> paramType = methodParam.getParameterType(); Object argValue = resolveCommonArgument(methodParam, webRequest, thrownException); @@ -343,7 +345,7 @@ public class AnnotationMethodHandlerExceptionResolver extends AbstractHandlerExc else if (Principal.class.isAssignableFrom(parameterType)) { return request.getUserPrincipal(); } - else if (Locale.class.equals(parameterType)) { + else if (Locale.class == parameterType) { return RequestContextUtils.getLocale(request); } else if (InputStream.class.isAssignableFrom(parameterType)) { @@ -379,15 +381,15 @@ public class AnnotationMethodHandlerExceptionResolver extends AbstractHandlerExc private ModelAndView getModelAndView(Method handlerMethod, Object returnValue, ServletWebRequest webRequest) throws Exception { - ResponseStatus responseStatusAnn = AnnotationUtils.findAnnotation(handlerMethod, ResponseStatus.class); - if (responseStatusAnn != null) { - HttpStatus responseStatus = responseStatusAnn.value(); - String reason = responseStatusAnn.reason(); + ResponseStatus responseStatus = AnnotatedElementUtils.findMergedAnnotation(handlerMethod, ResponseStatus.class); + if (responseStatus != null) { + HttpStatus statusCode = responseStatus.code(); + String reason = responseStatus.reason(); if (!StringUtils.hasText(reason)) { - webRequest.getResponse().setStatus(responseStatus.value()); + webRequest.getResponse().setStatus(statusCode.value()); } else { - webRequest.getResponse().sendError(responseStatus.value(), reason); + webRequest.getResponse().sendError(statusCode.value(), reason); } } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/annotation/DefaultAnnotationHandlerMapping.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/annotation/DefaultAnnotationHandlerMapping.java index 77c99cde..a1275c9c 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/annotation/DefaultAnnotationHandlerMapping.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/annotation/DefaultAnnotationHandlerMapping.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 the original author or authors. + * Copyright 2002-2015 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -78,8 +78,7 @@ import org.springframework.web.servlet.handler.AbstractDetectingUrlHandlerMappin * @since 2.5 * @see RequestMapping * @see AnnotationMethodHandlerAdapter - * - * @deprecated in Spring 3.2 in favor of + * @deprecated as of Spring 3.2, in favor of * {@link org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping RequestMappingHandlerMapping} */ @Deprecated diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/annotation/ResponseStatusExceptionResolver.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/annotation/ResponseStatusExceptionResolver.java index 28fcb63d..b9b9f788 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/annotation/ResponseStatusExceptionResolver.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/annotation/ResponseStatusExceptionResolver.java @@ -22,7 +22,7 @@ import javax.servlet.http.HttpServletResponse; import org.springframework.context.MessageSource; import org.springframework.context.MessageSourceAware; import org.springframework.context.i18n.LocaleContextHolder; -import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.core.annotation.AnnotatedElementUtils; import org.springframework.util.StringUtils; import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.servlet.ModelAndView; @@ -37,9 +37,15 @@ import org.springframework.web.servlet.handler.AbstractHandlerExceptionResolver; * {@link org.springframework.web.servlet.DispatcherServlet DispatcherServlet} * and the MVC Java config and the MVC namespace. * + * <p>As of 4.2 this resolver also looks recursively for {@code @ResponseStatus} + * present on cause exceptions, and as of 4.2.2 this resolver supports + * attribute overrides for {@code @ResponseStatus} in custom composed annotations. + * * @author Arjen Poutsma * @author Rossen Stoyanchev + * @author Sam Brannen * @since 3.0 + * @see AnnotatedElementUtils#findMergedAnnotation */ public class ResponseStatusExceptionResolver extends AbstractHandlerExceptionResolver implements MessageSourceAware { @@ -56,7 +62,7 @@ public class ResponseStatusExceptionResolver extends AbstractHandlerExceptionRes protected ModelAndView doResolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { - ResponseStatus responseStatus = AnnotationUtils.findAnnotation(ex.getClass(), ResponseStatus.class); + ResponseStatus responseStatus = AnnotatedElementUtils.findMergedAnnotation(ex.getClass(), ResponseStatus.class); if (responseStatus != null) { try { return resolveResponseStatus(responseStatus, request, response, handler, ex); @@ -65,6 +71,10 @@ public class ResponseStatusExceptionResolver extends AbstractHandlerExceptionRes logger.warn("Handling of @ResponseStatus resulted in Exception", resolveEx); } } + else if (ex.getCause() instanceof Exception) { + ex = (Exception) ex.getCause(); + return doResolveException(request, response, handler, ex); + } return null; } @@ -87,7 +97,7 @@ public class ResponseStatusExceptionResolver extends AbstractHandlerExceptionRes protected ModelAndView resolveResponseStatus(ResponseStatus responseStatus, HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { - int statusCode = responseStatus.value().value(); + int statusCode = responseStatus.code().value(); String reason = responseStatus.reason(); if (this.messageSource != null) { reason = this.messageSource.getMessage(reason, null, reason, LocaleContextHolder.getLocale()); diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/annotation/ServletAnnotationMappingUtils.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/annotation/ServletAnnotationMappingUtils.java index 5c0282f5..6c1288fc 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/annotation/ServletAnnotationMappingUtils.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/annotation/ServletAnnotationMappingUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 the original author or authors. + * Copyright 2002-2015 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -31,8 +31,7 @@ import org.springframework.web.util.WebUtils; * @author Juergen Hoeller * @author Arjen Poutsma * @since 2.5.2 - * - * @deprecated in 3.2 together with {@link DefaultAnnotationHandlerMapping}, + * @deprecated as of Spring 3.2, together with {@link DefaultAnnotationHandlerMapping}, * {@link AnnotationMethodHandlerAdapter}, and {@link AnnotationMethodHandlerExceptionResolver}. */ @Deprecated @@ -154,7 +153,7 @@ abstract class ServletAnnotationMappingUtils { } private static boolean isMediaTypeHeader(String headerName) { - return "Accept".equalsIgnoreCase(headerName) || "Content-Type".equalsIgnoreCase(headerName); + return ("Accept".equalsIgnoreCase(headerName) || "Content-Type".equalsIgnoreCase(headerName)); } } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/condition/AbstractMediaTypeExpression.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/condition/AbstractMediaTypeExpression.java index 532f6fbc..e6708f27 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/condition/AbstractMediaTypeExpression.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/condition/AbstractMediaTypeExpression.java @@ -93,7 +93,7 @@ abstract class AbstractMediaTypeExpression implements Comparable<AbstractMediaTy if (this == obj) { return true; } - if (obj != null && getClass().equals(obj.getClass())) { + if (obj != null && getClass() == obj.getClass()) { AbstractMediaTypeExpression other = (AbstractMediaTypeExpression) obj; return (this.mediaType.equals(other.mediaType) && this.isNegated == other.isNegated); } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/condition/AbstractRequestCondition.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/condition/AbstractRequestCondition.java index aa86fe50..92dd4e87 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/condition/AbstractRequestCondition.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/condition/AbstractRequestCondition.java @@ -33,7 +33,7 @@ public abstract class AbstractRequestCondition<T extends AbstractRequestConditio if (this == obj) { return true; } - if (obj != null && getClass().equals(obj.getClass())) { + if (obj != null && getClass() == obj.getClass()) { AbstractRequestCondition<?> other = (AbstractRequestCondition<?>) obj; return getContent().equals(other.getContent()); } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/RequestMappingInfo.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/RequestMappingInfo.java index bf73f151..395ecb9c 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/RequestMappingInfo.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/RequestMappingInfo.java @@ -16,9 +16,15 @@ package org.springframework.web.servlet.mvc.method; +import java.util.List; import javax.servlet.http.HttpServletRequest; +import org.springframework.http.HttpHeaders; +import org.springframework.util.PathMatcher; import org.springframework.util.StringUtils; +import org.springframework.web.accept.ContentNegotiationManager; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.cors.CorsUtils; import org.springframework.web.servlet.mvc.condition.ConsumesRequestCondition; import org.springframework.web.servlet.mvc.condition.HeadersRequestCondition; import org.springframework.web.servlet.mvc.condition.ParamsRequestCondition; @@ -27,6 +33,7 @@ import org.springframework.web.servlet.mvc.condition.ProducesRequestCondition; import org.springframework.web.servlet.mvc.condition.RequestCondition; import org.springframework.web.servlet.mvc.condition.RequestConditionHolder; import org.springframework.web.servlet.mvc.condition.RequestMethodsRequestCondition; +import org.springframework.web.util.UrlPathHelper; /** * Encapsulates the following request mapping conditions: @@ -208,7 +215,15 @@ public final class RequestMappingInfo implements RequestCondition<RequestMapping ProducesRequestCondition produces = this.producesCondition.getMatchingCondition(request); if (methods == null || params == null || headers == null || consumes == null || produces == null) { - return null; + if (CorsUtils.isPreFlightRequest(request)) { + methods = getAccessControlRequestMethodCondition(request); + if (methods == null || params == null) { + return null; + } + } + else { + return null; + } } PatternsRequestCondition patterns = this.patternsCondition.getMatchingCondition(request); @@ -225,6 +240,21 @@ public final class RequestMappingInfo implements RequestCondition<RequestMapping methods, params, headers, consumes, produces, custom.getCondition()); } + /** + * Return a matching RequestMethodsRequestCondition based on the expected + * HTTP method specified in a CORS pre-flight request. + */ + private RequestMethodsRequestCondition getAccessControlRequestMethodCondition(HttpServletRequest request) { + String expectedMethod = request.getHeader(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD); + if (StringUtils.hasText(expectedMethod)) { + for (RequestMethod method : getMethodsCondition().getMethods()) { + if (expectedMethod.equalsIgnoreCase(method.name())) { + return new RequestMethodsRequestCondition(method); + } + } + } + return null; + } /** * Compares "this" info (i.e. the current instance) with another info in the context of a request. @@ -317,4 +347,283 @@ public final class RequestMappingInfo implements RequestCondition<RequestMapping return builder.toString(); } + + /** + * Create a new {@code RequestMappingInfo.Builder} with the given paths. + * @param paths the paths to use + * @since 4.2 + */ + public static Builder paths(String... paths) { + return new DefaultBuilder(paths); + } + + + /** + * Defines a builder for creating a RequestMappingInfo. + * @since 4.2 + */ + public interface Builder { + + /** + * Set the path patterns. + */ + Builder paths(String... paths); + + /** + * Set the request method conditions. + */ + Builder methods(RequestMethod... methods); + + /** + * Set the request param conditions. + */ + Builder params(String... params); + + /** + * Set the header conditions. + * <p>By default this is not set. + */ + Builder headers(String... headers); + + /** + * Set the consumes conditions. + */ + Builder consumes(String... consumes); + + /** + * Set the produces conditions. + */ + Builder produces(String... produces); + + /** + * Set the mapping name. + */ + Builder mappingName(String name); + + /** + * Set a custom condition to use. + */ + Builder customCondition(RequestCondition<?> condition); + + /** + * Provide additional configuration needed for request mapping purposes. + */ + Builder options(BuilderConfiguration options); + + /** + * Build the RequestMappingInfo. + */ + RequestMappingInfo build(); + } + + + private static class DefaultBuilder implements Builder { + + private String[] paths; + + private RequestMethod[] methods; + + private String[] params; + + private String[] headers; + + private String[] consumes; + + private String[] produces; + + private String mappingName; + + private RequestCondition<?> customCondition; + + private BuilderConfiguration options = new BuilderConfiguration(); + + public DefaultBuilder(String... paths) { + this.paths = paths; + } + + @Override + public Builder paths(String... paths) { + this.paths = paths; + return this; + } + + @Override + public DefaultBuilder methods(RequestMethod... methods) { + this.methods = methods; + return this; + } + + @Override + public DefaultBuilder params(String... params) { + this.params = params; + return this; + } + + @Override + public DefaultBuilder headers(String... headers) { + this.headers = headers; + return this; + } + + @Override + public DefaultBuilder consumes(String... consumes) { + this.consumes = consumes; + return this; + } + + @Override + public DefaultBuilder produces(String... produces) { + this.produces = produces; + return this; + } + + @Override + public DefaultBuilder mappingName(String name) { + this.mappingName = name; + return this; + } + + @Override + public DefaultBuilder customCondition(RequestCondition<?> condition) { + this.customCondition = condition; + return this; + } + + @Override + public Builder options(BuilderConfiguration options) { + this.options = options; + return this; + } + + @Override + public RequestMappingInfo build() { + ContentNegotiationManager manager = this.options.getContentNegotiationManager(); + + PatternsRequestCondition patternsCondition = new PatternsRequestCondition( + this.paths, this.options.getUrlPathHelper(), this.options.getPathMatcher(), + this.options.useSuffixPatternMatch(), this.options.useTrailingSlashMatch(), + this.options.getFileExtensions()); + + return new RequestMappingInfo(this.mappingName, patternsCondition, + new RequestMethodsRequestCondition(methods), + new ParamsRequestCondition(this.params), + new HeadersRequestCondition(this.headers), + new ConsumesRequestCondition(this.consumes, this.headers), + new ProducesRequestCondition(this.produces, this.headers, manager), + this.customCondition); + } + } + + + /** + * Container for configuration options used for request mapping purposes. + * Such configuration is required to create RequestMappingInfo instances but + * is typically used across all RequestMappingInfo instances. + * @since 4.2 + * @see Builder#options + */ + public static class BuilderConfiguration { + + private UrlPathHelper urlPathHelper; + + private PathMatcher pathMatcher; + + private boolean trailingSlashMatch = true; + + private boolean suffixPatternMatch = true; + + private boolean registeredSuffixPatternMatch = false; + + private ContentNegotiationManager contentNegotiationManager; + + /** + * Set a custom UrlPathHelper to use for the PatternsRequestCondition. + * <p>By default this is not set. + */ + public void setPathHelper(UrlPathHelper pathHelper) { + this.urlPathHelper = pathHelper; + } + + public UrlPathHelper getUrlPathHelper() { + return this.urlPathHelper; + } + + /** + * Set a custom PathMatcher to use for the PatternsRequestCondition. + * <p>By default this is not set. + */ + public void setPathMatcher(PathMatcher pathMatcher) { + this.pathMatcher = pathMatcher; + } + + public PathMatcher getPathMatcher() { + return this.pathMatcher; + } + + /** + * Whether to apply trailing slash matching in PatternsRequestCondition. + * <p>By default this is set to 'true'. + */ + public void setTrailingSlashMatch(boolean trailingSlashMatch) { + this.trailingSlashMatch = trailingSlashMatch; + } + + public boolean useTrailingSlashMatch() { + return this.trailingSlashMatch; + } + + /** + * Whether to apply suffix pattern matching in PatternsRequestCondition. + * <p>By default this is set to 'true'. + * @see #setRegisteredSuffixPatternMatch(boolean) + */ + public void setSuffixPatternMatch(boolean suffixPatternMatch) { + this.suffixPatternMatch = suffixPatternMatch; + } + + public boolean useSuffixPatternMatch() { + return this.suffixPatternMatch; + } + + /** + * Whether suffix pattern matching should be restricted to registered + * file extensions only. Setting this property also sets + * suffixPatternMatch=true and requires that a + * {@link #setContentNegotiationManager} is also configured in order to + * obtain the registered file extensions. + */ + public void setRegisteredSuffixPatternMatch(boolean registeredSuffixPatternMatch) { + this.registeredSuffixPatternMatch = registeredSuffixPatternMatch; + this.suffixPatternMatch = (registeredSuffixPatternMatch || this.suffixPatternMatch); + } + + public boolean useRegisteredSuffixPatternMatch() { + return this.registeredSuffixPatternMatch; + } + + /** + * Return the file extensions to use for suffix pattern matching. If + * {@code registeredSuffixPatternMatch=true}, the extensions are obtained + * from the configured {@code contentNegotiationManager}. + */ + public List<String> getFileExtensions() { + if (useRegisteredSuffixPatternMatch() && getContentNegotiationManager() != null) { + return this.contentNegotiationManager.getAllFileExtensions(); + } + return null; + } + + /** + * Set the ContentNegotiationManager to use for the ProducesRequestCondition. + * <p>By default this is not set. + */ + public void setContentNegotiationManager(ContentNegotiationManager manager) { + this.contentNegotiationManager = manager; + } + + public ContentNegotiationManager getContentNegotiationManager() { + return this.contentNegotiationManager; + } + } + } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/RequestMappingInfoHandlerMapping.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/RequestMappingInfoHandlerMapping.java index 5574ccfe..5c18540a 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/RequestMappingInfoHandlerMapping.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/RequestMappingInfoHandlerMapping.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2014 the original author or authors. + * Copyright 2002-2015 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,6 +22,7 @@ import java.util.Comparator; import java.util.HashSet; import java.util.LinkedHashMap; import java.util.LinkedHashSet; +import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Set; @@ -205,7 +206,7 @@ public abstract class RequestMappingInfoHandlerMapping extends AbstractHandlerMe Set<MediaType> consumableMediaTypes; Set<MediaType> producibleMediaTypes; - Set<String> paramConditions; + List<String[]> paramConditions; if (patternAndMethodMatches.isEmpty()) { consumableMediaTypes = getConsumableMediaTypes(request, patternMatches); @@ -234,8 +235,7 @@ public abstract class RequestMappingInfoHandlerMapping extends AbstractHandlerMe throw new HttpMediaTypeNotAcceptableException(new ArrayList<MediaType>(producibleMediaTypes)); } else if (!CollectionUtils.isEmpty(paramConditions)) { - String[] params = paramConditions.toArray(new String[paramConditions.size()]); - throw new UnsatisfiedServletRequestParameterException(params, request.getParameterMap()); + throw new UnsatisfiedServletRequestParameterException(paramConditions, request.getParameterMap()); } else { return null; @@ -262,18 +262,21 @@ public abstract class RequestMappingInfoHandlerMapping extends AbstractHandlerMe return result; } - private Set<String> getRequestParams(HttpServletRequest request, Set<RequestMappingInfo> partialMatches) { + private List<String[]> getRequestParams(HttpServletRequest request, Set<RequestMappingInfo> partialMatches) { + List<String[]> result = new ArrayList<String[]>(); for (RequestMappingInfo partialMatch : partialMatches) { ParamsRequestCondition condition = partialMatch.getParamsCondition(); - if (!CollectionUtils.isEmpty(condition.getExpressions()) && (condition.getMatchingCondition(request) == null)) { - Set<String> expressions = new HashSet<String>(); - for (NameValueExpression<String> expr : condition.getExpressions()) { - expressions.add(expr.toString()); + Set<NameValueExpression<String>> expressions = condition.getExpressions(); + if (!CollectionUtils.isEmpty(expressions) && condition.getMatchingCondition(request) == null) { + int i = 0; + String[] array = new String[expressions.size()]; + for (NameValueExpression<String> expression : expressions) { + array[i++] = expression.toString(); } - return expressions; + result.add(array); } } - return null; + return result; } } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/AbstractJsonpResponseBodyAdvice.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/AbstractJsonpResponseBodyAdvice.java index 85f1e699..0a383c6c 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/AbstractJsonpResponseBodyAdvice.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/AbstractJsonpResponseBodyAdvice.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2014 the original author or authors. + * Copyright 2002-2015 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/AbstractMessageConverterMethodArgumentResolver.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/AbstractMessageConverterMethodArgumentResolver.java index 6357551b..75c78a91 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/AbstractMessageConverterMethodArgumentResolver.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/AbstractMessageConverterMethodArgumentResolver.java @@ -17,10 +17,13 @@ package org.springframework.web.servlet.mvc.method.annotation; import java.io.IOException; +import java.io.InputStream; +import java.io.PushbackInputStream; import java.lang.annotation.Annotation; import java.lang.reflect.Type; import java.util.ArrayList; import java.util.Collections; +import java.util.EnumSet; import java.util.LinkedHashSet; import java.util.List; import java.util.Set; @@ -32,11 +35,15 @@ import org.apache.commons.logging.LogFactory; import org.springframework.core.MethodParameter; import org.springframework.core.ResolvableType; import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.http.HttpHeaders; import org.springframework.http.HttpInputMessage; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpRequest; import org.springframework.http.InvalidMediaTypeException; import org.springframework.http.MediaType; import org.springframework.http.converter.GenericHttpMessageConverter; import org.springframework.http.converter.HttpMessageConverter; +import org.springframework.http.converter.HttpMessageNotReadableException; import org.springframework.http.server.ServletServerHttpRequest; import org.springframework.util.Assert; import org.springframework.validation.Errors; @@ -56,17 +63,39 @@ import org.springframework.web.method.support.HandlerMethodArgumentResolver; */ public abstract class AbstractMessageConverterMethodArgumentResolver implements HandlerMethodArgumentResolver { + private static final Set<HttpMethod> SUPPORTED_METHODS = + EnumSet.of(HttpMethod.POST, HttpMethod.PUT, HttpMethod.PATCH); + + private static final Object NO_VALUE = new Object(); + + protected final Log logger = LogFactory.getLog(getClass()); protected final List<HttpMessageConverter<?>> messageConverters; protected final List<MediaType> allSupportedMediaTypes; + private final RequestResponseBodyAdviceChain advice; + + + /** + * Basic constructor with converters only. + */ + public AbstractMessageConverterMethodArgumentResolver(List<HttpMessageConverter<?>> converters) { + this(converters, null); + } + + /** + * Constructor with converters and {@code Request~} and {@code ResponseBodyAdvice}. + * @since 4.2 + */ + public AbstractMessageConverterMethodArgumentResolver(List<HttpMessageConverter<?>> converters, + List<Object> requestResponseBodyAdvice) { - public AbstractMessageConverterMethodArgumentResolver(List<HttpMessageConverter<?>> messageConverters) { - Assert.notEmpty(messageConverters, "'messageConverters' must not be empty"); - this.messageConverters = messageConverters; - this.allSupportedMediaTypes = getAllSupportedMediaTypes(messageConverters); + Assert.notEmpty(converters, "'messageConverters' must not be empty"); + this.messageConverters = converters; + this.allSupportedMediaTypes = getAllSupportedMediaTypes(converters); + this.advice = new RequestResponseBodyAdviceChain(requestResponseBodyAdvice); } @@ -86,6 +115,15 @@ public abstract class AbstractMessageConverterMethodArgumentResolver implements /** + * Return the configured {@link RequestBodyAdvice} and + * {@link RequestBodyAdvice} where each instance may be wrapped as a + * {@link org.springframework.web.method.ControllerAdviceBean ControllerAdviceBean}. + */ + protected RequestResponseBodyAdviceChain getAdvice() { + return this.advice; + } + + /** * Create the method argument value of the expected parameter type by * reading from the given request. * @param <T> the expected type of the argument value to be created @@ -96,8 +134,8 @@ public abstract class AbstractMessageConverterMethodArgumentResolver implements * @throws IOException if the reading from the request fails * @throws HttpMediaTypeNotSupportedException if no suitable message converter is found */ - protected <T> Object readWithMessageConverters(NativeWebRequest webRequest, - MethodParameter methodParam, Type paramType) throws IOException, HttpMediaTypeNotSupportedException { + protected <T> Object readWithMessageConverters(NativeWebRequest webRequest, MethodParameter methodParam, + Type paramType) throws IOException, HttpMediaTypeNotSupportedException, HttpMessageNotReadableException { HttpInputMessage inputMessage = createInputMessage(webRequest); return readWithMessageConverters(inputMessage, methodParam, paramType); @@ -108,19 +146,19 @@ public abstract class AbstractMessageConverterMethodArgumentResolver implements * from the given HttpInputMessage. * @param <T> the expected type of the argument value to be created * @param inputMessage the HTTP input message representing the current request - * @param methodParam the method parameter descriptor - * @param targetType the type of object to create, not necessarily the same as - * the method parameter type (e.g. for {@code HttpEntity<String>} method - * parameter the target type is String) + * @param param the method parameter descriptor (may be {@code null}) + * @param targetType the target type, not necessarily the same as the method + * parameter type, e.g. for {@code HttpEntity<String>}. * @return the created method argument value * @throws IOException if the reading from the request fails * @throws HttpMediaTypeNotSupportedException if no suitable message converter is found */ @SuppressWarnings("unchecked") - protected <T> Object readWithMessageConverters(HttpInputMessage inputMessage, - MethodParameter methodParam, Type targetType) throws IOException, HttpMediaTypeNotSupportedException { + protected <T> Object readWithMessageConverters(HttpInputMessage inputMessage, MethodParameter param, + Type targetType) throws IOException, HttpMediaTypeNotSupportedException, HttpMessageNotReadableException { MediaType contentType; + boolean noContentType = false; try { contentType = inputMessage.getHeaders().getContentType(); } @@ -128,34 +166,76 @@ public abstract class AbstractMessageConverterMethodArgumentResolver implements throw new HttpMediaTypeNotSupportedException(ex.getMessage()); } if (contentType == null) { + noContentType = true; contentType = MediaType.APPLICATION_OCTET_STREAM; } - Class<?> contextClass = methodParam.getContainingClass(); - Class<T> targetClass = (Class<T>) - ResolvableType.forMethodParameter(methodParam, targetType).resolve(Object.class); - - for (HttpMessageConverter<?> converter : this.messageConverters) { - if (converter instanceof GenericHttpMessageConverter) { - GenericHttpMessageConverter<?> genericConverter = (GenericHttpMessageConverter<?>) converter; - if (genericConverter.canRead(targetType, contextClass, contentType)) { - if (logger.isDebugEnabled()) { - logger.debug("Reading [" + targetType + "] as \"" + - contentType + "\" using [" + converter + "]"); + Class<?> contextClass = (param != null ? param.getContainingClass() : null); + Class<T> targetClass = (targetType instanceof Class<?> ? (Class<T>) targetType : null); + if (targetClass == null) { + ResolvableType resolvableType = (param != null ? + ResolvableType.forMethodParameter(param) : ResolvableType.forType(targetType)); + targetClass = (Class<T>) resolvableType.resolve(); + } + + HttpMethod httpMethod = ((HttpRequest) inputMessage).getMethod(); + Object body = NO_VALUE; + + try { + inputMessage = new EmptyBodyCheckingHttpInputMessage(inputMessage); + + for (HttpMessageConverter<?> converter : this.messageConverters) { + Class<HttpMessageConverter<?>> converterType = (Class<HttpMessageConverter<?>>) converter.getClass(); + if (converter instanceof GenericHttpMessageConverter) { + GenericHttpMessageConverter<?> genericConverter = (GenericHttpMessageConverter<?>) converter; + if (genericConverter.canRead(targetType, contextClass, contentType)) { + if (logger.isDebugEnabled()) { + logger.debug("Read [" + targetType + "] as \"" + contentType + "\" with [" + converter + "]"); + } + if (inputMessage.getBody() != null) { + inputMessage = getAdvice().beforeBodyRead(inputMessage, param, targetType, converterType); + body = genericConverter.read(targetType, contextClass, inputMessage); + body = getAdvice().afterBodyRead(body, inputMessage, param, targetType, converterType); + } + else { + body = null; + body = getAdvice().handleEmptyBody(body, inputMessage, param, targetType, converterType); + } + break; } - return genericConverter.read(targetType, contextClass, inputMessage); } - } - if (converter.canRead(targetClass, contentType)) { - if (logger.isDebugEnabled()) { - logger.debug("Reading [" + targetClass.getName() + "] as \"" + - contentType + "\" using [" + converter + "]"); + else if (targetClass != null) { + if (converter.canRead(targetClass, contentType)) { + if (logger.isDebugEnabled()) { + logger.debug("Read [" + targetType + "] as \"" + contentType + "\" with [" + converter + "]"); + } + if (inputMessage.getBody() != null) { + inputMessage = getAdvice().beforeBodyRead(inputMessage, param, targetType, converterType); + body = ((HttpMessageConverter<T>) converter).read(targetClass, inputMessage); + body = getAdvice().afterBodyRead(body, inputMessage, param, targetType, converterType); + } + else { + body = null; + body = getAdvice().handleEmptyBody(body, inputMessage, param, targetType, converterType); + } + break; + } } - return ((HttpMessageConverter<T>) converter).read(targetClass, inputMessage); } } + catch (IOException ex) { + throw new HttpMessageNotReadableException("Could not read document: " + ex.getMessage(), ex); + } + + if (body == NO_VALUE) { + if (httpMethod == null || !SUPPORTED_METHODS.contains(httpMethod) || + (noContentType && inputMessage.getBody() == null)) { + return null; + } + throw new HttpMediaTypeNotSupportedException(contentType, this.allSupportedMediaTypes); + } - throw new HttpMediaTypeNotSupportedException(contentType, this.allSupportedMediaTypes); + return body; } /** @@ -205,4 +285,54 @@ public abstract class AbstractMessageConverterMethodArgumentResolver implements return !hasBindingResult; } + + private static class EmptyBodyCheckingHttpInputMessage implements HttpInputMessage { + + private final HttpHeaders headers; + + private final InputStream body; + + private final HttpMethod method; + + + public EmptyBodyCheckingHttpInputMessage(HttpInputMessage inputMessage) throws IOException { + this.headers = inputMessage.getHeaders(); + InputStream inputStream = inputMessage.getBody(); + if (inputStream == null) { + this.body = null; + } + else if (inputStream.markSupported()) { + inputStream.mark(1); + this.body = (inputStream.read() != -1 ? inputStream : null); + inputStream.reset(); + } + else { + PushbackInputStream pushbackInputStream = new PushbackInputStream(inputStream); + int b = pushbackInputStream.read(); + if (b == -1) { + this.body = null; + } + else { + this.body = pushbackInputStream; + pushbackInputStream.unread(b); + } + } + this.method = ((HttpRequest) inputMessage).getMethod(); + } + + @Override + public HttpHeaders getHeaders() { + return this.headers; + } + + @Override + public InputStream getBody() throws IOException { + return this.body; + } + + public HttpMethod getMethod() { + return this.method; + } + } + } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/AbstractMessageConverterMethodProcessor.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/AbstractMessageConverterMethodProcessor.java index 0982a873..f4034995 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/AbstractMessageConverterMethodProcessor.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/AbstractMessageConverterMethodProcessor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2014 the original author or authors. + * Copyright 2002-2015 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,6 +17,7 @@ package org.springframework.web.servlet.mvc.method.annotation; import java.io.IOException; +import java.lang.reflect.Type; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; @@ -29,10 +30,13 @@ import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.springframework.core.MethodParameter; +import org.springframework.http.HttpEntity; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpOutputMessage; import org.springframework.http.MediaType; +import org.springframework.http.converter.GenericHttpMessageConverter; import org.springframework.http.converter.HttpMessageConverter; +import org.springframework.http.converter.HttpMessageNotWritableException; import org.springframework.http.server.ServletServerHttpRequest; import org.springframework.http.server.ServletServerHttpResponse; import org.springframework.util.CollectionUtils; @@ -83,27 +87,36 @@ public abstract class AbstractMessageConverterMethodProcessor extends AbstractMe private final PathExtensionContentNegotiationStrategy pathStrategy; - private final ResponseBodyAdviceChain adviceChain; - private final Set<String> safeExtensions = new HashSet<String>(); - protected AbstractMessageConverterMethodProcessor(List<HttpMessageConverter<?>> messageConverters) { - this(messageConverters, null); + + /** + * Constructor with list of converters only. + */ + protected AbstractMessageConverterMethodProcessor(List<HttpMessageConverter<?>> converters) { + this(converters, null); } - protected AbstractMessageConverterMethodProcessor(List<HttpMessageConverter<?>> messageConverters, - ContentNegotiationManager manager) { - this(messageConverters, manager, null); + /** + * Constructor with list of converters and ContentNegotiationManager. + */ + protected AbstractMessageConverterMethodProcessor(List<HttpMessageConverter<?>> converters, + ContentNegotiationManager contentNegotiationManager) { + + this(converters, contentNegotiationManager, null); } - protected AbstractMessageConverterMethodProcessor(List<HttpMessageConverter<?>> messageConverters, - ContentNegotiationManager manager, List<Object> responseBodyAdvice) { + /** + * Constructor with list of converters and ContentNegotiationManager as well + * as request/response body advice instances. + */ + protected AbstractMessageConverterMethodProcessor(List<HttpMessageConverter<?>> converters, + ContentNegotiationManager manager, List<Object> requestResponseBodyAdvice) { - super(messageConverters); + super(converters, requestResponseBodyAdvice); this.contentNegotiationManager = (manager != null ? manager : new ContentNegotiationManager()); this.pathStrategy = initPathStrategy(this.contentNegotiationManager); - this.adviceChain = new ResponseBodyAdviceChain(responseBodyAdvice); this.safeExtensions.addAll(this.contentNegotiationManager.getAllFileExtensions()); this.safeExtensions.addAll(WHITELISTED_EXTENSIONS); } @@ -118,10 +131,6 @@ public abstract class AbstractMessageConverterMethodProcessor extends AbstractMe } - protected ResponseBodyAdviceChain getAdviceChain() { - return this.adviceChain; - } - /** * Creates a new {@link HttpOutputMessage} from the given {@link NativeWebRequest}. * @param webRequest the web request to create an output message from @@ -137,7 +146,7 @@ public abstract class AbstractMessageConverterMethodProcessor extends AbstractMe * {@link #writeWithMessageConverters(Object, MethodParameter, ServletServerHttpRequest, ServletServerHttpResponse)} */ protected <T> void writeWithMessageConverters(T returnValue, MethodParameter returnType, NativeWebRequest webRequest) - throws IOException, HttpMediaTypeNotAcceptableException { + throws IOException, HttpMediaTypeNotAcceptableException, HttpMessageNotWritableException { ServletServerHttpRequest inputMessage = createInputMessage(webRequest); ServletServerHttpResponse outputMessage = createOutputMessage(webRequest); @@ -157,12 +166,17 @@ public abstract class AbstractMessageConverterMethodProcessor extends AbstractMe @SuppressWarnings("unchecked") protected <T> void writeWithMessageConverters(T returnValue, MethodParameter returnType, ServletServerHttpRequest inputMessage, ServletServerHttpResponse outputMessage) - throws IOException, HttpMediaTypeNotAcceptableException { + throws IOException, HttpMediaTypeNotAcceptableException, HttpMessageNotWritableException { Class<?> returnValueClass = getReturnValueType(returnValue, returnType); + Type returnValueType = getGenericType(returnType); HttpServletRequest servletRequest = inputMessage.getServletRequest(); List<MediaType> requestedMediaTypes = getAcceptableMediaTypes(servletRequest); - List<MediaType> producibleMediaTypes = getProducibleMediaTypes(servletRequest, returnValueClass); + List<MediaType> producibleMediaTypes = getProducibleMediaTypes(servletRequest, returnValueClass, returnValueType); + + if (returnValue != null && producibleMediaTypes.isEmpty()) { + throw new IllegalArgumentException("No converter found for return value of type: " + returnValueClass); + } Set<MediaType> compatibleMediaTypes = new LinkedHashSet<MediaType>(); for (MediaType requestedType : requestedMediaTypes) { @@ -197,15 +211,35 @@ public abstract class AbstractMessageConverterMethodProcessor extends AbstractMe if (selectedMediaType != null) { selectedMediaType = selectedMediaType.removeQualityValue(); for (HttpMessageConverter<?> messageConverter : this.messageConverters) { - if (messageConverter.canWrite(returnValueClass, selectedMediaType)) { - returnValue = this.adviceChain.invoke(returnValue, returnType, selectedMediaType, - (Class<HttpMessageConverter<?>>) messageConverter.getClass(), inputMessage, outputMessage); + if (messageConverter instanceof GenericHttpMessageConverter) { + if (((GenericHttpMessageConverter<T>) messageConverter).canWrite(returnValueType, + returnValueClass, selectedMediaType)) { + returnValue = (T) getAdvice().beforeBodyWrite(returnValue, returnType, selectedMediaType, + (Class<? extends HttpMessageConverter<?>>) messageConverter.getClass(), + inputMessage, outputMessage); + if (returnValue != null) { + addContentDispositionHeader(inputMessage, outputMessage); + ((GenericHttpMessageConverter<T>) messageConverter).write(returnValue, + returnValueType, selectedMediaType, outputMessage); + if (logger.isDebugEnabled()) { + logger.debug("Written [" + returnValue + "] as \"" + + selectedMediaType + "\" using [" + messageConverter + "]"); + } + } + return; + } + } + else if (messageConverter.canWrite(returnValueClass, selectedMediaType)) { + returnValue = (T) getAdvice().beforeBodyWrite(returnValue, returnType, selectedMediaType, + (Class<? extends HttpMessageConverter<?>>) messageConverter.getClass(), + inputMessage, outputMessage); if (returnValue != null) { addContentDispositionHeader(inputMessage, outputMessage); - ((HttpMessageConverter<T>) messageConverter).write(returnValue, selectedMediaType, outputMessage); + ((HttpMessageConverter<T>) messageConverter).write(returnValue, + selectedMediaType, outputMessage); if (logger.isDebugEnabled()) { - logger.debug("Written [" + returnValue + "] as \"" + selectedMediaType + "\" using [" + - messageConverter + "]"); + logger.debug("Written [" + returnValue + "] as \"" + + selectedMediaType + "\" using [" + messageConverter + "]"); } } return; @@ -229,15 +263,40 @@ public abstract class AbstractMessageConverterMethodProcessor extends AbstractMe } /** + * Return the generic type of the {@code returnType} (or of the nested type if it is + * a {@link HttpEntity}). + */ + private Type getGenericType(MethodParameter returnType) { + Type type; + if (HttpEntity.class.isAssignableFrom(returnType.getParameterType())) { + returnType.increaseNestingLevel(); + type = returnType.getNestedGenericParameterType(); + } + else { + type = returnType.getGenericParameterType(); + } + return type; + } + + /** + * @see #getProducibleMediaTypes(HttpServletRequest, Class, Type) + */ + @SuppressWarnings({"unchecked", "unused"}) + protected List<MediaType> getProducibleMediaTypes(HttpServletRequest request, Class<?> returnValueClass) { + return getProducibleMediaTypes(request, returnValueClass, null); + } + + /** * Returns the media types that can be produced: * <ul> * <li>The producible media types specified in the request mappings, or * <li>Media types of configured converters that can write the specific return value, or * <li>{@link MediaType#ALL} * </ul> + * @since 4.2 */ @SuppressWarnings("unchecked") - protected List<MediaType> getProducibleMediaTypes(HttpServletRequest request, Class<?> returnValueClass) { + protected List<MediaType> getProducibleMediaTypes(HttpServletRequest request, Class<?> returnValueClass, Type returnValueType) { Set<MediaType> mediaTypes = (Set<MediaType>) request.getAttribute(HandlerMapping.PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE); if (!CollectionUtils.isEmpty(mediaTypes)) { return new ArrayList<MediaType>(mediaTypes); @@ -245,7 +304,12 @@ public abstract class AbstractMessageConverterMethodProcessor extends AbstractMe else if (!this.allSupportedMediaTypes.isEmpty()) { List<MediaType> result = new ArrayList<MediaType>(); for (HttpMessageConverter<?> converter : this.messageConverters) { - if (converter.canWrite(returnValueClass, null)) { + if (converter instanceof GenericHttpMessageConverter && returnValueType != null) { + if (((GenericHttpMessageConverter<?>) converter).canWrite(returnValueType, returnValueClass, null)) { + result.addAll(converter.getSupportedMediaTypes()); + } + } + else if (converter.canWrite(returnValueClass, null)) { result.addAll(converter.getSupportedMediaTypes()); } } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/AsyncTaskMethodReturnValueHandler.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/AsyncTaskMethodReturnValueHandler.java index 88c7c04c..6691d65c 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/AsyncTaskMethodReturnValueHandler.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/AsyncTaskMethodReturnValueHandler.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2014 the original author or authors. + * Copyright 2002-2015 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,7 +21,7 @@ import org.springframework.core.MethodParameter; import org.springframework.web.context.request.NativeWebRequest; import org.springframework.web.context.request.async.WebAsyncTask; import org.springframework.web.context.request.async.WebAsyncUtils; -import org.springframework.web.method.support.HandlerMethodReturnValueHandler; +import org.springframework.web.method.support.AsyncHandlerMethodReturnValueHandler; import org.springframework.web.method.support.ModelAndViewContainer; /** @@ -30,7 +30,7 @@ import org.springframework.web.method.support.ModelAndViewContainer; * @author Rossen Stoyanchev * @since 3.2 */ -public class AsyncTaskMethodReturnValueHandler implements HandlerMethodReturnValueHandler { +public class AsyncTaskMethodReturnValueHandler implements AsyncHandlerMethodReturnValueHandler { private final BeanFactory beanFactory; @@ -46,6 +46,11 @@ public class AsyncTaskMethodReturnValueHandler implements HandlerMethodReturnVal } @Override + public boolean isAsyncReturnValue(Object returnValue, MethodParameter returnType) { + return (returnValue != null && returnValue instanceof WebAsyncTask); + } + + @Override public void handleReturnValue(Object returnValue, MethodParameter returnType, ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception { diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/CallableMethodReturnValueHandler.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/CallableMethodReturnValueHandler.java index a38c05bd..cfd5dade 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/CallableMethodReturnValueHandler.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/CallableMethodReturnValueHandler.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2014 the original author or authors. + * Copyright 2002-2015 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,7 +21,7 @@ import java.util.concurrent.Callable; import org.springframework.core.MethodParameter; import org.springframework.web.context.request.NativeWebRequest; import org.springframework.web.context.request.async.WebAsyncUtils; -import org.springframework.web.method.support.HandlerMethodReturnValueHandler; +import org.springframework.web.method.support.AsyncHandlerMethodReturnValueHandler; import org.springframework.web.method.support.ModelAndViewContainer; /** @@ -30,7 +30,7 @@ import org.springframework.web.method.support.ModelAndViewContainer; * @author Rossen Stoyanchev * @since 3.2 */ -public class CallableMethodReturnValueHandler implements HandlerMethodReturnValueHandler { +public class CallableMethodReturnValueHandler implements AsyncHandlerMethodReturnValueHandler { @Override public boolean supportsReturnType(MethodParameter returnType) { @@ -38,6 +38,11 @@ public class CallableMethodReturnValueHandler implements HandlerMethodReturnValu } @Override + public boolean isAsyncReturnValue(Object returnValue, MethodParameter returnType) { + return (returnValue != null && returnValue instanceof Callable); + } + + @Override public void handleReturnValue(Object returnValue, MethodParameter returnType, ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception { diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/CompletionStageReturnValueHandler.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/CompletionStageReturnValueHandler.java new file mode 100644 index 00000000..6543b76c --- /dev/null +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/CompletionStageReturnValueHandler.java @@ -0,0 +1,80 @@ +/* + * Copyright 2002-2015 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.web.servlet.mvc.method.annotation; + +import java.util.concurrent.CompletionStage; +import java.util.function.Consumer; +import java.util.function.Function; + +import org.springframework.core.MethodParameter; +import org.springframework.lang.UsesJava8; +import org.springframework.web.context.request.NativeWebRequest; +import org.springframework.web.context.request.async.DeferredResult; +import org.springframework.web.context.request.async.WebAsyncUtils; +import org.springframework.web.method.support.AsyncHandlerMethodReturnValueHandler; +import org.springframework.web.method.support.ModelAndViewContainer; + +/** + * Handles return values of type {@link CompletionStage} (implemented by + * {@link java.util.concurrent.CompletableFuture} for example). + * + * @author Sebastien Deleuze + * @since 4.2 + */ +@UsesJava8 +public class CompletionStageReturnValueHandler implements AsyncHandlerMethodReturnValueHandler { + + @Override + public boolean supportsReturnType(MethodParameter returnType) { + return CompletionStage.class.isAssignableFrom(returnType.getParameterType()); + } + + @Override + public boolean isAsyncReturnValue(Object returnValue, MethodParameter returnType) { + return (returnValue != null && returnValue instanceof CompletionStage); + } + + @Override + public void handleReturnValue(Object returnValue, MethodParameter returnType, + ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception { + + if (returnValue == null) { + mavContainer.setRequestHandled(true); + return; + } + + final DeferredResult<Object> deferredResult = new DeferredResult<Object>(); + WebAsyncUtils.getAsyncManager(webRequest).startDeferredResultProcessing(deferredResult, mavContainer); + + @SuppressWarnings("unchecked") + CompletionStage<Object> future = (CompletionStage<Object>) returnValue; + future.thenAccept(new Consumer<Object>() { + @Override + public void accept(Object result) { + deferredResult.setResult(result); + } + }); + future.exceptionally(new Function<Throwable, Object>() { + @Override + public Object apply(Throwable ex) { + deferredResult.setErrorResult(ex); + return null; + } + }); + } + +} diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/DeferredResultMethodReturnValueHandler.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/DeferredResultMethodReturnValueHandler.java index 3b66aec9..147e6aff 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/DeferredResultMethodReturnValueHandler.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/DeferredResultMethodReturnValueHandler.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2014 the original author or authors. + * Copyright 2002-2015 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,7 +20,7 @@ import org.springframework.core.MethodParameter; import org.springframework.web.context.request.NativeWebRequest; import org.springframework.web.context.request.async.DeferredResult; import org.springframework.web.context.request.async.WebAsyncUtils; -import org.springframework.web.method.support.HandlerMethodReturnValueHandler; +import org.springframework.web.method.support.AsyncHandlerMethodReturnValueHandler; import org.springframework.web.method.support.ModelAndViewContainer; /** @@ -29,7 +29,7 @@ import org.springframework.web.method.support.ModelAndViewContainer; * @author Rossen Stoyanchev * @since 3.2 */ -public class DeferredResultMethodReturnValueHandler implements HandlerMethodReturnValueHandler { +public class DeferredResultMethodReturnValueHandler implements AsyncHandlerMethodReturnValueHandler { @Override public boolean supportsReturnType(MethodParameter returnType) { @@ -37,6 +37,11 @@ public class DeferredResultMethodReturnValueHandler implements HandlerMethodRetu } @Override + public boolean isAsyncReturnValue(Object returnValue, MethodParameter returnType) { + return (returnValue != null && returnValue instanceof DeferredResult); + } + + @Override public void handleReturnValue(Object returnValue, MethodParameter returnType, ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception { diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ExceptionHandlerExceptionResolver.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ExceptionHandlerExceptionResolver.java index a708cc2c..c897a419 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ExceptionHandlerExceptionResolver.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ExceptionHandlerExceptionResolver.java @@ -31,7 +31,7 @@ import javax.xml.transform.Source; import org.springframework.beans.factory.InitializingBean; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; -import org.springframework.core.OrderComparator; +import org.springframework.core.annotation.AnnotationAwareOrderComparator; import org.springframework.http.converter.ByteArrayHttpMessageConverter; import org.springframework.http.converter.HttpMessageConverter; import org.springframework.http.converter.StringHttpMessageConverter; @@ -258,7 +258,7 @@ public class ExceptionHandlerExceptionResolver extends AbstractHandlerMethodExce } List<ControllerAdviceBean> adviceBeans = ControllerAdviceBean.findAnnotatedBeans(getApplicationContext()); - OrderComparator.sort(adviceBeans); + AnnotationAwareOrderComparator.sort(adviceBeans); for (ControllerAdviceBean adviceBean : adviceBeans) { ExceptionHandlerMethodResolver resolver = new ExceptionHandlerMethodResolver(adviceBean.getBeanType()); @@ -293,6 +293,7 @@ public class ExceptionHandlerExceptionResolver extends AbstractHandlerMethodExce // Type-based argument resolution resolvers.add(new ServletRequestMethodArgumentResolver()); resolvers.add(new ServletResponseMethodArgumentResolver()); + resolvers.add(new ModelMethodProcessor()); // Custom arguments if (getCustomArgumentResolvers() != null) { @@ -359,11 +360,11 @@ public class ExceptionHandlerExceptionResolver extends AbstractHandlerMethodExce if (logger.isDebugEnabled()) { logger.debug("Invoking @ExceptionHandler method: " + exceptionHandlerMethod); } - exceptionHandlerMethod.invokeAndHandle(webRequest, mavContainer, exception); + exceptionHandlerMethod.invokeAndHandle(webRequest, mavContainer, exception, handlerMethod); } catch (Exception invocationEx) { - if (logger.isErrorEnabled()) { - logger.error("Failed to invoke @ExceptionHandler method: " + exceptionHandlerMethod, invocationEx); + if (logger.isDebugEnabled()) { + logger.debug("Failed to invoke @ExceptionHandler method: " + exceptionHandlerMethod, invocationEx); } return null; } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/HttpEntityMethodProcessor.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/HttpEntityMethodProcessor.java index 26d06eff..7741b1c4 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/HttpEntityMethodProcessor.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/HttpEntityMethodProcessor.java @@ -25,12 +25,15 @@ import org.springframework.core.MethodParameter; import org.springframework.core.ResolvableType; import org.springframework.http.HttpEntity; import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; import org.springframework.http.RequestEntity; import org.springframework.http.ResponseEntity; import org.springframework.http.converter.HttpMessageConverter; import org.springframework.http.server.ServletServerHttpRequest; import org.springframework.http.server.ServletServerHttpResponse; import org.springframework.util.Assert; +import org.springframework.util.StringUtils; import org.springframework.web.HttpMediaTypeNotSupportedException; import org.springframework.web.accept.ContentNegotiationManager; import org.springframework.web.bind.support.WebDataBinderFactory; @@ -48,31 +51,58 @@ import org.springframework.web.method.support.ModelAndViewContainer; * * @author Arjen Poutsma * @author Rossen Stoyanchev + * @author Brian Clozel * @since 3.1 */ public class HttpEntityMethodProcessor extends AbstractMessageConverterMethodProcessor { - public HttpEntityMethodProcessor(List<HttpMessageConverter<?>> messageConverters) { - super(messageConverters); + /** + * Basic constructor with converters only. Suitable for resolving + * {@code HttpEntity}. For handling {@code ResponseEntity} consider also + * providing a {@code ContentNegotiationManager}. + */ + public HttpEntityMethodProcessor(List<HttpMessageConverter<?>> converters) { + super(converters); } - public HttpEntityMethodProcessor(List<HttpMessageConverter<?>> messageConverters, - ContentNegotiationManager contentNegotiationManager) { + /** + * Basic constructor with converters and {@code ContentNegotiationManager}. + * Suitable for resolving {@code HttpEntity} and handling {@code ResponseEntity} + * without {@code Request~} or {@code ResponseBodyAdvice}. + */ + public HttpEntityMethodProcessor(List<HttpMessageConverter<?>> converters, + ContentNegotiationManager manager) { - super(messageConverters, contentNegotiationManager); + super(converters, manager); } - public HttpEntityMethodProcessor(List<HttpMessageConverter<?>> messageConverters, - ContentNegotiationManager contentNegotiationManager, List<Object> responseBodyAdvice) { + /** + * Complete constructor for resolving {@code HttpEntity} method arguments. + * For handling {@code ResponseEntity} consider also providing a + * {@code ContentNegotiationManager}. + * @since 4.2 + */ + public HttpEntityMethodProcessor(List<HttpMessageConverter<?>> converters, + List<Object> requestResponseBodyAdvice) { - super(messageConverters, contentNegotiationManager, responseBodyAdvice); + super(converters, null, requestResponseBodyAdvice); + } + + /** + * Complete constructor for resolving {@code HttpEntity} and handling + * {@code ResponseEntity}. + */ + public HttpEntityMethodProcessor(List<HttpMessageConverter<?>> converters, + ContentNegotiationManager manager, List<Object> requestResponseBodyAdvice) { + + super(converters, manager, requestResponseBodyAdvice); } @Override public boolean supportsParameter(MethodParameter parameter) { - return (HttpEntity.class.equals(parameter.getParameterType()) || - RequestEntity.class.equals(parameter.getParameterType())); + return (HttpEntity.class == parameter.getParameterType() || + RequestEntity.class == parameter.getParameterType()); } @Override @@ -90,7 +120,7 @@ public class HttpEntityMethodProcessor extends AbstractMessageConverterMethodPro Type paramType = getHttpEntityType(parameter); Object body = readWithMessageConverters(webRequest, parameter, paramType); - if (RequestEntity.class.equals(parameter.getParameterType())) { + if (RequestEntity.class == parameter.getParameterType()) { return new RequestEntity<Object>(body, inputMessage.getHeaders(), inputMessage.getMethod(), inputMessage.getURI()); } @@ -131,9 +161,6 @@ public class HttpEntityMethodProcessor extends AbstractMessageConverterMethodPro Assert.isInstanceOf(HttpEntity.class, returnValue); HttpEntity<?> responseEntity = (HttpEntity<?>) returnValue; - if (responseEntity instanceof ResponseEntity) { - outputMessage.setStatusCode(((ResponseEntity<?>) responseEntity).getStatusCode()); - } HttpHeaders entityHeaders = responseEntity.getHeaders(); if (!entityHeaders.isEmpty()) { @@ -141,12 +168,70 @@ public class HttpEntityMethodProcessor extends AbstractMessageConverterMethodPro } Object body = responseEntity.getBody(); + if (responseEntity instanceof ResponseEntity) { + outputMessage.setStatusCode(((ResponseEntity<?>) responseEntity).getStatusCode()); + if (HttpMethod.GET == inputMessage.getMethod() && isResourceNotModified(inputMessage, outputMessage)) { + outputMessage.setStatusCode(HttpStatus.NOT_MODIFIED); + // Ensure headers are flushed, no body should be written. + outputMessage.flush(); + // Skip call to converters, as they may update the body. + return; + } + } // Try even with null body. ResponseBodyAdvice could get involved. writeWithMessageConverters(body, returnType, inputMessage, outputMessage); // Ensure headers are flushed even if no body was written. - outputMessage.getBody(); + outputMessage.flush(); + } + + private boolean isResourceNotModified(ServletServerHttpRequest inputMessage, ServletServerHttpResponse outputMessage) { + List<String> ifNoneMatch = inputMessage.getHeaders().getIfNoneMatch(); + long ifModifiedSince = inputMessage.getHeaders().getIfModifiedSince(); + String eTag = addEtagPadding(outputMessage.getHeaders().getETag()); + long lastModified = outputMessage.getHeaders().getLastModified(); + boolean notModified = false; + + if (!ifNoneMatch.isEmpty() && (inputMessage.getHeaders().containsKey(HttpHeaders.IF_UNMODIFIED_SINCE) + || inputMessage.getHeaders().containsKey(HttpHeaders.IF_MATCH))) { + // invalid conditional request, do not process + } + else if (lastModified != -1 && StringUtils.hasLength(eTag)) { + notModified = isETagNotModified(ifNoneMatch, eTag) && isTimeStampNotModified(ifModifiedSince, lastModified); + } + else if (lastModified != -1) { + notModified = isTimeStampNotModified(ifModifiedSince, lastModified); + } + else if (StringUtils.hasLength(eTag)) { + notModified = isETagNotModified(ifNoneMatch, eTag); + } + return notModified; + } + + private boolean isETagNotModified(List<String> ifNoneMatch, String etag) { + if (StringUtils.hasLength(etag)) { + for (String clientETag : ifNoneMatch) { + // Compare weak/strong ETags as per https://tools.ietf.org/html/rfc7232#section-2.3 + if (StringUtils.hasLength(clientETag) && + (clientETag.replaceFirst("^W/", "").equals(etag.replaceFirst("^W/", "")))) { + return true; + } + } + } + return false; + } + + private boolean isTimeStampNotModified(long ifModifiedSince, long lastModifiedTimestamp) { + return (ifModifiedSince >= (lastModifiedTimestamp / 1000 * 1000)); + } + + private String addEtagPadding(String etag) { + if (StringUtils.hasLength(etag) && + (!(etag.startsWith("\"") || etag.startsWith("W/\"")) || !etag.endsWith("\"")) ) { + etag = "\"" + etag + "\""; + } + return etag; } @Override diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/JsonViewRequestBodyAdvice.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/JsonViewRequestBodyAdvice.java new file mode 100644 index 00000000..255e969a --- /dev/null +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/JsonViewRequestBodyAdvice.java @@ -0,0 +1,73 @@ +/* + * Copyright 2002-2015 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.web.servlet.mvc.method.annotation; + +import java.io.IOException; +import java.lang.reflect.Type; + +import com.fasterxml.jackson.annotation.JsonView; + +import org.springframework.core.MethodParameter; +import org.springframework.http.HttpInputMessage; +import org.springframework.http.converter.HttpMessageConverter; +import org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter; +import org.springframework.http.converter.json.MappingJacksonInputMessage; + +/** + * A {@link RequestBodyAdvice} implementation that adds support for Jackson's + * {@code @JsonView} annotation declared on a Spring MVC {@code @HttpEntity} + * or {@code @RequestBody} method parameter. + * + * <p>The deserialization view specified in the annotation will be passed in to the + * {@link org.springframework.http.converter.json.MappingJackson2HttpMessageConverter} + * which will then use it to deserialize the request body with. + * + * <p>Note that despite {@code @JsonView} allowing for more than one class to + * be specified, the use for a request body advice is only supported with + * exactly one class argument. Consider the use of a composite interface. + * + * <p>Jackson 2.5 or later is required for parameter-level use of {@code @JsonView}. + * + * @author Sebastien Deleuze + * @since 4.2 + * @see com.fasterxml.jackson.annotation.JsonView + * @see com.fasterxml.jackson.databind.ObjectMapper#readerWithView(Class) + */ +public class JsonViewRequestBodyAdvice extends RequestBodyAdviceAdapter { + + @Override + public boolean supports(MethodParameter methodParameter, Type targetType, + Class<? extends HttpMessageConverter<?>> converterType) { + + return (AbstractJackson2HttpMessageConverter.class.isAssignableFrom(converterType) && + methodParameter.getParameterAnnotation(JsonView.class) != null); + } + + @Override + public HttpInputMessage beforeBodyRead(HttpInputMessage inputMessage, MethodParameter methodParameter, + Type targetType, Class<? extends HttpMessageConverter<?>> selectedConverterType) throws IOException { + + JsonView annotation = methodParameter.getParameterAnnotation(JsonView.class); + Class<?>[] classes = annotation.value(); + if (classes.length != 1) { + throw new IllegalArgumentException( + "@JsonView only supported for request body advice with exactly 1 class argument: " + methodParameter); + } + return new MappingJacksonInputMessage(inputMessage.getBody(), inputMessage.getHeaders(), classes[0]); + } + +} diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/JsonViewResponseBodyAdvice.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/JsonViewResponseBodyAdvice.java index 944706db..3ed6bb8f 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/JsonViewResponseBodyAdvice.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/JsonViewResponseBodyAdvice.java @@ -32,7 +32,7 @@ import org.springframework.http.server.ServerHttpResponse; * * <p>The serialization view specified in the annotation will be passed in to the * {@link org.springframework.http.converter.json.MappingJackson2HttpMessageConverter} - * which will then use it to serialize the response body with. + * which will then use it to serialize the response body. * * <p>Note that despite {@code @JsonView} allowing for more than one class to * be specified, the use for a response body advice is only supported with diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ListenableFutureReturnValueHandler.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ListenableFutureReturnValueHandler.java index af9fb1a3..18d25a4f 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ListenableFutureReturnValueHandler.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ListenableFutureReturnValueHandler.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2014 the original author or authors. + * Copyright 2002-2015 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,7 +22,7 @@ import org.springframework.util.concurrent.ListenableFutureCallback; import org.springframework.web.context.request.NativeWebRequest; import org.springframework.web.context.request.async.DeferredResult; import org.springframework.web.context.request.async.WebAsyncUtils; -import org.springframework.web.method.support.HandlerMethodReturnValueHandler; +import org.springframework.web.method.support.AsyncHandlerMethodReturnValueHandler; import org.springframework.web.method.support.ModelAndViewContainer; /** @@ -32,7 +32,7 @@ import org.springframework.web.method.support.ModelAndViewContainer; * @author Rossen Stoyanchev * @since 4.1 */ -public class ListenableFutureReturnValueHandler implements HandlerMethodReturnValueHandler { +public class ListenableFutureReturnValueHandler implements AsyncHandlerMethodReturnValueHandler { @Override public boolean supportsReturnType(MethodParameter returnType) { @@ -40,6 +40,11 @@ public class ListenableFutureReturnValueHandler implements HandlerMethodReturnVa } @Override + public boolean isAsyncReturnValue(Object returnValue, MethodParameter returnType) { + return (returnValue != null && returnValue instanceof ListenableFuture); + } + + @Override public void handleReturnValue(Object returnValue, MethodParameter returnType, ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception { diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/MatrixVariableMapMethodArgumentResolver.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/MatrixVariableMapMethodArgumentResolver.java index ffefd917..59a8e987 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/MatrixVariableMapMethodArgumentResolver.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/MatrixVariableMapMethodArgumentResolver.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 the original author or authors. + * Copyright 2002-2015 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -35,8 +35,8 @@ import org.springframework.web.servlet.HandlerMapping; /** * Resolves method arguments of type Map annotated with - * {@link MatrixVariable @MatrixVariable} where the annotation the does not - * specify a name. If a name specified then the argument will by resolved by the + * {@link MatrixVariable @MatrixVariable} where the annotation does not + * specify a name. If a name is specified then the argument will by resolved by the * {@link MatrixVariableMethodArgumentResolver} instead. * * @author Rossen Stoyanchev @@ -46,10 +46,10 @@ public class MatrixVariableMapMethodArgumentResolver implements HandlerMethodArg @Override public boolean supportsParameter(MethodParameter parameter) { - MatrixVariable paramAnnot = parameter.getParameterAnnotation(MatrixVariable.class); - if (paramAnnot != null) { + MatrixVariable matrixVariable = parameter.getParameterAnnotation(MatrixVariable.class); + if (matrixVariable != null) { if (Map.class.isAssignableFrom(parameter.getParameterType())) { - return !StringUtils.hasText(paramAnnot.value()); + return !StringUtils.hasText(matrixVariable.name()); } } return false; diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/MatrixVariableMethodArgumentResolver.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/MatrixVariableMethodArgumentResolver.java index 8e563d22..6e971176 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/MatrixVariableMethodArgumentResolver.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/MatrixVariableMethodArgumentResolver.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2014 the original author or authors. + * Copyright 2002-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -33,12 +33,13 @@ import org.springframework.web.method.annotation.AbstractNamedValueMethodArgumen import org.springframework.web.servlet.HandlerMapping; /** - * Resolves method arguments annotated with an {@link MatrixVariable @PathParam}. + * Resolves method arguments annotated with {@link MatrixVariable @MatrixVariable}. * * <p>If the method parameter is of type Map and no name is specified, then it will * by resolved by the {@link MatrixVariableMapMethodArgumentResolver} instead. * * @author Rossen Stoyanchev + * @author Sam Brannen * @since 3.2 */ public class MatrixVariableMethodArgumentResolver extends AbstractNamedValueMethodArgumentResolver { @@ -47,14 +48,15 @@ public class MatrixVariableMethodArgumentResolver extends AbstractNamedValueMeth super(null); } + @Override public boolean supportsParameter(MethodParameter parameter) { if (!parameter.hasParameterAnnotation(MatrixVariable.class)) { return false; } if (Map.class.isAssignableFrom(parameter.getParameterType())) { - String paramName = parameter.getParameterAnnotation(MatrixVariable.class).value(); - return StringUtils.hasText(paramName); + String variableName = parameter.getParameterAnnotation(MatrixVariable.class).name(); + return StringUtils.hasText(variableName); } return true; } @@ -62,17 +64,14 @@ public class MatrixVariableMethodArgumentResolver extends AbstractNamedValueMeth @Override protected NamedValueInfo createNamedValueInfo(MethodParameter parameter) { MatrixVariable annotation = parameter.getParameterAnnotation(MatrixVariable.class); - return new PathParamNamedValueInfo(annotation); + return new MatrixVariableNamedValueInfo(annotation); } @Override + @SuppressWarnings("unchecked") protected Object resolveName(String name, MethodParameter parameter, NativeWebRequest request) throws Exception { - - @SuppressWarnings("unchecked") - Map<String, MultiValueMap<String, String>> pathParameters = - (Map<String, MultiValueMap<String, String>>) request.getAttribute( - HandlerMapping.MATRIX_VARIABLES_ATTRIBUTE, RequestAttributes.SCOPE_REQUEST); - + Map<String, MultiValueMap<String, String>> pathParameters = (Map<String, MultiValueMap<String, String>>) + request.getAttribute(HandlerMapping.MATRIX_VARIABLES_ATTRIBUTE, RequestAttributes.SCOPE_REQUEST); if (CollectionUtils.isEmpty(pathParameters)) { return null; } @@ -94,7 +93,7 @@ public class MatrixVariableMethodArgumentResolver extends AbstractNamedValueMeth String paramType = parameter.getParameterType().getName(); throw new ServletRequestBindingException( "Found more than one match for URI path parameter '" + name + - "' for parameter type [" + paramType + "]. Use pathVar attribute to disambiguate."); + "' for parameter type [" + paramType + "]. Use 'pathVar' attribute to disambiguate."); } paramValues.addAll(params.get(name)); found = true; @@ -120,10 +119,11 @@ public class MatrixVariableMethodArgumentResolver extends AbstractNamedValueMeth } - private static class PathParamNamedValueInfo extends NamedValueInfo { + private static class MatrixVariableNamedValueInfo extends NamedValueInfo { - private PathParamNamedValueInfo(MatrixVariable annotation) { - super(annotation.value(), annotation.required(), annotation.defaultValue()); + private MatrixVariableNamedValueInfo(MatrixVariable annotation) { + super(annotation.name(), annotation.required(), annotation.defaultValue()); } } -}
\ No newline at end of file + +} diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/MvcUriComponentsBuilder.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/MvcUriComponentsBuilder.java index abdc8049..e8ff7ee9 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/MvcUriComponentsBuilder.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/MvcUriComponentsBuilder.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2014 the original author or authors. + * Copyright 2002-2015 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,6 +21,7 @@ import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Set; import javax.servlet.http.HttpServletRequest; import org.aopalliance.intercept.MethodInterceptor; @@ -38,14 +39,18 @@ import org.springframework.cglib.proxy.Factory; import org.springframework.cglib.proxy.MethodProxy; import org.springframework.core.DefaultParameterNameDiscoverer; import org.springframework.core.MethodParameter; +import org.springframework.core.MethodIntrospector; import org.springframework.core.ParameterNameDiscoverer; -import org.springframework.core.annotation.AnnotationUtils; -import org.springframework.objenesis.ObjenesisStd; +import org.springframework.core.annotation.AnnotatedElementUtils; +import org.springframework.core.annotation.SynthesizingMethodParameter; +import org.springframework.objenesis.ObjenesisException; +import org.springframework.objenesis.SpringObjenesis; import org.springframework.util.AntPathMatcher; import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; import org.springframework.util.PathMatcher; import org.springframework.util.ReflectionUtils; +import org.springframework.util.ReflectionUtils.MethodFilter; import org.springframework.util.StringUtils; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.context.WebApplicationContext; @@ -62,14 +67,26 @@ import org.springframework.web.util.UriComponents; import org.springframework.web.util.UriComponentsBuilder; /** - * A UriComponentsBuilder that helps to build URIs to Spring MVC controllers - * and methods from their request mappings. + * Creates instances of {@link org.springframework.web.util.UriComponentsBuilder} + * by pointing to Spring MVC controllers and {@code @RequestMapping} methods. + * + * <p>The static {@code fromXxx(...)} methods prepare links relative to the + * current request as determined by a call to + * {@link org.springframework.web.servlet.support.ServletUriComponentsBuilder#fromCurrentServletMapping()}. + * + * <p>The static {@code fromXxx(UriComponentsBuilder,...)} methods can be given + * the baseUrl when operating outside the context of a request. + * + * <p>You can also create an MvcUriComponentsBuilder instance with a baseUrl + * via {@link #relativeTo(org.springframework.web.util.UriComponentsBuilder)} + * and then use the non-static {@code withXxx(...)} method variants. * * @author Oliver Gierke * @author Rossen Stoyanchev + * @author Sam Brannen * @since 4.0 */ -public class MvcUriComponentsBuilder extends UriComponentsBuilder { +public class MvcUriComponentsBuilder { /** * Well-known name for the {@link CompositeUriComponentsContributor} object in the bean factory. @@ -79,7 +96,7 @@ public class MvcUriComponentsBuilder extends UriComponentsBuilder { private static final Log logger = LogFactory.getLog(MvcUriComponentsBuilder.class); - private static final ObjenesisStd objenesis = new ObjenesisStd(true); + private static final SpringObjenesis objenesis = new SpringObjenesis(); private static final PathMatcher pathMatcher = new AntPathMatcher(); @@ -92,27 +109,33 @@ public class MvcUriComponentsBuilder extends UriComponentsBuilder { new PathVariableMethodArgumentResolver(), new RequestParamMethodArgumentResolver(false)); } + private final UriComponentsBuilder baseUrl; + /** * Default constructor. Protected to prevent direct instantiation. - * * @see #fromController(Class) * @see #fromMethodName(Class, String, Object...) * @see #fromMethodCall(Object) * @see #fromMappingName(String) * @see #fromMethod(java.lang.reflect.Method, Object...) */ - protected MvcUriComponentsBuilder() { + protected MvcUriComponentsBuilder(UriComponentsBuilder baseUrl) { + Assert.notNull(baseUrl, "'baseUrl' is required"); + this.baseUrl = baseUrl; } + /** - * Create a deep copy of the given MvcUriComponentsBuilder. - * @param other the other builder to copy from + * Create an instance of this class with a base URL. After that calls to one + * of the instance based {@code withXxx(...}} methods will create URLs relative + * to the given base URL. */ - protected MvcUriComponentsBuilder(MvcUriComponentsBuilder other) { - super(other); + public static MvcUriComponentsBuilder relativeTo(UriComponentsBuilder baseUrl) { + return new MvcUriComponentsBuilder(baseUrl); } + /** * Create a {@link UriComponentsBuilder} from the mapping of a controller class * and current request information including Servlet mapping. If the controller @@ -121,20 +144,25 @@ public class MvcUriComponentsBuilder extends UriComponentsBuilder { * @return a UriComponentsBuilder instance (never {@code null}) */ public static UriComponentsBuilder fromController(Class<?> controllerType) { - String mapping = getTypeRequestMapping(controllerType); - return ServletUriComponentsBuilder.fromCurrentServletMapping().path(mapping); + return fromController(null, controllerType); } - private static String getTypeRequestMapping(Class<?> controllerType) { - Assert.notNull(controllerType, "'controllerType' must not be null"); - RequestMapping annot = AnnotationUtils.findAnnotation(controllerType, RequestMapping.class); - if (annot == null || ObjectUtils.isEmpty(annot.value()) || StringUtils.isEmpty(annot.value()[0])) { - return "/"; - } - if (annot.value().length > 1 && logger.isWarnEnabled()) { - logger.warn("Multiple paths on controller " + controllerType.getName() + ", using first one"); - } - return annot.value()[0]; + /** + * An alternative to {@link #fromController(Class)} that accepts a + * {@code UriComponentsBuilder} representing the base URL. This is useful + * when using MvcUriComponentsBuilder outside the context of processing a + * request or to apply a custom baseUrl not matching the current request. + * @param builder the builder for the base URL; the builder will be cloned + * and therefore not modified and may be re-used for further calls. + * @param controllerType the controller to build a URI for + * @return a UriComponentsBuilder instance (never {@code null}) + */ + public static UriComponentsBuilder fromController(UriComponentsBuilder builder, + Class<?> controllerType) { + + builder = getBaseUrlToUse(builder); + String mapping = getTypeRequestMapping(controllerType); + return builder.path(mapping); } /** @@ -143,32 +171,37 @@ public class MvcUriComponentsBuilder extends UriComponentsBuilder { * to {@link #fromMethod(java.lang.reflect.Method, Object...)}. * @param controllerType the controller * @param methodName the method name - * @param argumentValues the argument values + * @param args the argument values * @return a UriComponentsBuilder instance, never {@code null} * @throws IllegalArgumentException if there is no matching or * if there is more than one matching method */ - public static UriComponentsBuilder fromMethodName(Class<?> controllerType, String methodName, Object... argumentValues) { - Method method = getMethod(controllerType, methodName, argumentValues); - return fromMethod(method, argumentValues); - } - - private static Method getMethod(Class<?> controllerType, String methodName, Object... argumentValues) { - Method match = null; - for (Method method : controllerType.getDeclaredMethods()) { - if (method.getName().equals(methodName) && method.getParameterTypes().length == argumentValues.length) { - if (match != null) { - throw new IllegalArgumentException("Found two methods named '" + methodName + "' having " + - Arrays.asList(argumentValues) + " arguments, controller " + controllerType.getName()); - } - match = method; - } - } - if (match == null) { - throw new IllegalArgumentException("No method '" + methodName + "' with " + argumentValues.length + - " parameters found in " + controllerType.getName()); - } - return match; + public static UriComponentsBuilder fromMethodName(Class<?> controllerType, + String methodName, Object... args) { + + Method method = getMethod(controllerType, methodName, args); + return fromMethodInternal(null, controllerType, method, args); + } + + /** + * An alternative to {@link #fromMethodName(Class, String, Object...)} that + * accepts a {@code UriComponentsBuilder} representing the base URL. This is + * useful when using MvcUriComponentsBuilder outside the context of processing + * a request or to apply a custom baseUrl not matching the current request. + * @param builder the builder for the base URL; the builder will be cloned + * and therefore not modified and may be re-used for further calls. + * @param controllerType the controller + * @param methodName the method name + * @param args the argument values + * @return a UriComponentsBuilder instance, never {@code null} + * @throws IllegalArgumentException if there is no matching or + * if there is more than one matching method + */ + public static UriComponentsBuilder fromMethodName(UriComponentsBuilder builder, + Class<?> controllerType, String methodName, Object... args) { + + Method method = getMethod(controllerType, methodName, args); + return fromMethodInternal(builder, controllerType, method, args); } /** @@ -192,29 +225,51 @@ public class MvcUriComponentsBuilder extends UriComponentsBuilder { * // Inline style with static import of "MvcUriComponentsBuilder.on" * * MvcUriComponentsBuilder.fromMethodCall( - * on(CustomerController.class).showAddresses("US")).buildAndExpand(1); + * on(AddressController.class).getAddressesForCountry("US")).buildAndExpand(1); * * // Longer form useful for repeated invocation (and void controller methods) * - * CustomerController controller = MvcUriComponentsBuilder.on(CustomController.class); + * AddressController controller = MvcUriComponentsBuilder.on(AddressController.class); * controller.addAddress(null); * builder = MvcUriComponentsBuilder.fromMethodCall(controller); * controller.getAddressesForCountry("US") * builder = MvcUriComponentsBuilder.fromMethodCall(controller); * </pre> - * @param invocationInfo either the value returned from a "mock" controller + * @param info either the value returned from a "mock" controller * invocation or the "mock" controller itself after an invocation * @return a UriComponents instance */ - public static UriComponentsBuilder fromMethodCall(Object invocationInfo) { - Assert.isInstanceOf(MethodInvocationInfo.class, invocationInfo); - MethodInvocationInfo info = (MethodInvocationInfo) invocationInfo; - return fromMethod(info.getControllerMethod(), info.getArgumentValues()); + public static UriComponentsBuilder fromMethodCall(Object info) { + Assert.isInstanceOf(MethodInvocationInfo.class, info); + MethodInvocationInfo invocationInfo = (MethodInvocationInfo) info; + Class<?> controllerType = invocationInfo.getControllerType(); + Method method = invocationInfo.getControllerMethod(); + Object[] arguments = invocationInfo.getArgumentValues(); + return fromMethodInternal(null, controllerType, method, arguments); + } + + /** + * An alternative to {@link #fromMethodCall(Object)} that accepts a + * {@code UriComponentsBuilder} representing the base URL. This is useful + * when using MvcUriComponentsBuilder outside the context of processing a + * request or to apply a custom baseUrl not matching the current request. + * @param builder the builder for the base URL; the builder will be cloned + * and therefore not modified and may be re-used for further calls. + * @param info either the value returned from a "mock" controller + * invocation or the "mock" controller itself after an invocation + * @return a UriComponents instance + */ + public static UriComponentsBuilder fromMethodCall(UriComponentsBuilder builder, Object info) { + Assert.isInstanceOf(MethodInvocationInfo.class, info); + MethodInvocationInfo invocationInfo = (MethodInvocationInfo) info; + Class<?> controllerType = invocationInfo.getControllerType(); + Method method = invocationInfo.getControllerMethod(); + Object[] arguments = invocationInfo.getArgumentValues(); + return fromMethodInternal(builder, controllerType, method, arguments); } /** * Create a URL from the name of a Spring MVC controller method's request mapping. - * * <p>The configured * {@link org.springframework.web.servlet.handler.HandlerMethodMappingNamingStrategy * HandlerMethodMappingNamingStrategy} determines the names of controller @@ -225,11 +280,9 @@ public class MvcUriComponentsBuilder extends UriComponentsBuilder { * naming convention does not produce unique results, an explicit name may * be assigned through the name attribute of the {@code @RequestMapping} * annotation. - * * <p>This is aimed primarily for use in view rendering technologies and EL * expressions. The Spring URL tag library registers this method as a function * called "mvcUrl". - * * <p>For example, given this controller: * <pre class="code"> * @RequestMapping("/people") @@ -248,10 +301,8 @@ public class MvcUriComponentsBuilder extends UriComponentsBuilder { * * <a href="${s:mvcUrl('PC#getPerson').arg(0,"123").build()}">Get Person</a> * </pre> - * * <p>Note that it's not necessary to specify all arguments. Only the ones * required to prepare the URL, mainly {@code @RequestParam} and {@code @PathVariable}). - * * @param mappingName the mapping name * @return a builder to to prepare the URI String * @throws IllegalArgumentException if the mapping name is not found or @@ -259,16 +310,36 @@ public class MvcUriComponentsBuilder extends UriComponentsBuilder { * @since 4.1 */ public static MethodArgumentBuilder fromMappingName(String mappingName) { + return fromMappingName(null, mappingName); + } + + /** + * An alternative to {@link #fromMappingName(String)} that accepts a + * {@code UriComponentsBuilder} representing the base URL. This is useful + * when using MvcUriComponentsBuilder outside the context of processing a + * request or to apply a custom baseUrl not matching the current request. + * @param builder the builder for the base URL; the builder will be cloned + * and therefore not modified and may be re-used for further calls. + * @param name the mapping name + * @return a builder to to prepare the URI String + * @throws IllegalArgumentException if the mapping name is not found or + * if there is no unique match + * @since 4.2 + */ + public static MethodArgumentBuilder fromMappingName(UriComponentsBuilder builder, String name) { RequestMappingInfoHandlerMapping handlerMapping = getRequestMappingInfoHandlerMapping(); - List<HandlerMethod> handlerMethods = handlerMapping.getHandlerMethodsForMappingName(mappingName); + List<HandlerMethod> handlerMethods = handlerMapping.getHandlerMethodsForMappingName(name); if (handlerMethods == null) { - throw new IllegalArgumentException("Mapping mappingName not found: " + mappingName); + throw new IllegalArgumentException("Mapping mappingName not found: " + name); } if (handlerMethods.size() != 1) { - throw new IllegalArgumentException( - "No unique match for mapping mappingName " + mappingName + ": " + handlerMethods); + throw new IllegalArgumentException("No unique match for mapping mappingName " + + name + ": " + handlerMethods); } - return new MethodArgumentBuilder(handlerMethods.get(0).getMethod()); + HandlerMethod handlerMethod = handlerMethods.get(0); + Class<?> controllerType = handlerMethod.getBeanType(); + Method method = handlerMethod.getMethod(); + return new MethodArgumentBuilder(builder, controllerType, method); } /** @@ -276,34 +347,126 @@ public class MvcUriComponentsBuilder extends UriComponentsBuilder { * and an array of method argument values. The array of values must match the * signature of the controller method. Values for {@code @RequestParam} and * {@code @PathVariable} are used for building the URI (via implementations of - * {@link org.springframework.web.method.support.UriComponentsContributor}) - * while remaining argument values are ignored and can be {@code null}. + * {@link org.springframework.web.method.support.UriComponentsContributor + * UriComponentsContributor}) while remaining argument values are ignored and + * can be {@code null}. + * @param controllerType the controller type * @param method the controller method - * @param argumentValues argument values for the controller method + * @param args argument values for the controller method * @return a UriComponentsBuilder instance, never {@code null} + * @since 4.2 + */ + public static UriComponentsBuilder fromMethod(Class<?> controllerType, Method method, Object... args) { + return fromMethodInternal(null, controllerType, method, args); + } + + /** + * An alternative to {@link #fromMethod(java.lang.reflect.Method, Object...)} + * that accepts a {@code UriComponentsBuilder} representing the base URL. + * This is useful when using MvcUriComponentsBuilder outside the context of + * processing a request or to apply a custom baseUrl not matching the + * current request. + * @param baseUrl the builder for the base URL; the builder will be cloned + * and therefore not modified and may be re-used for further calls. + * @param controllerType the controller type + * @param method the controller method + * @param args argument values for the controller method + * @return a UriComponentsBuilder instance (never {@code null}) + * @since 4.2 + */ + public static UriComponentsBuilder fromMethod(UriComponentsBuilder baseUrl, + Class<?> controllerType, Method method, Object... args) { + + return fromMethodInternal(baseUrl, + (controllerType != null ? controllerType : method.getDeclaringClass()), method, args); + } + + /** + * @see #fromMethod(Class, Method, Object...) + * @see #fromMethod(UriComponentsBuilder, Class, Method, Object...) + * @deprecated as of 4.2, this is deprecated in favor of the overloaded + * method that also accepts a controllerType argument */ - public static UriComponentsBuilder fromMethod(Method method, Object... argumentValues) { - String typePath = getTypeRequestMapping(method.getDeclaringClass()); + @Deprecated + public static UriComponentsBuilder fromMethod(Method method, Object... args) { + return fromMethodInternal(null, method.getDeclaringClass(), method, args); + } + + private static UriComponentsBuilder fromMethodInternal(UriComponentsBuilder baseUrl, + Class<?> controllerType, Method method, Object... args) { + + baseUrl = getBaseUrlToUse(baseUrl); + String typePath = getTypeRequestMapping(controllerType); String methodPath = getMethodRequestMapping(method); String path = pathMatcher.combine(typePath, methodPath); + baseUrl.path(path); + UriComponents uriComponents = applyContributors(baseUrl, method, args); + return UriComponentsBuilder.newInstance().uriComponents(uriComponents); + } - UriComponentsBuilder builder = ServletUriComponentsBuilder.fromCurrentServletMapping().path(path); - UriComponents uriComponents = applyContributors(builder, method, argumentValues); - return ServletUriComponentsBuilder.newInstance().uriComponents(uriComponents); + private static UriComponentsBuilder getBaseUrlToUse(UriComponentsBuilder baseUrl) { + if (baseUrl != null) { + return (UriComponentsBuilder) baseUrl.clone(); + } + else { + return ServletUriComponentsBuilder.fromCurrentServletMapping(); + } + } + + private static String getTypeRequestMapping(Class<?> controllerType) { + Assert.notNull(controllerType, "'controllerType' must not be null"); + RequestMapping requestMapping = AnnotatedElementUtils.findMergedAnnotation(controllerType, RequestMapping.class); + if (requestMapping == null) { + return "/"; + } + String[] paths = requestMapping.path(); + if (ObjectUtils.isEmpty(paths) || StringUtils.isEmpty(paths[0])) { + return "/"; + } + if (paths.length > 1 && logger.isWarnEnabled()) { + logger.warn("Multiple paths on controller " + controllerType.getName() + ", using first one"); + } + return paths[0]; } private static String getMethodRequestMapping(Method method) { - RequestMapping annot = AnnotationUtils.findAnnotation(method, RequestMapping.class); - if (annot == null) { + Assert.notNull(method, "'method' must not be null"); + RequestMapping requestMapping = AnnotatedElementUtils.findMergedAnnotation(method, RequestMapping.class); + if (requestMapping == null) { throw new IllegalArgumentException("No @RequestMapping on: " + method.toGenericString()); } - if (ObjectUtils.isEmpty(annot.value()) || StringUtils.isEmpty(annot.value()[0])) { + String[] paths = requestMapping.path(); + if (ObjectUtils.isEmpty(paths) || StringUtils.isEmpty(paths[0])) { return "/"; } - if (annot.value().length > 1 && logger.isWarnEnabled()) { + if (paths.length > 1 && logger.isWarnEnabled()) { logger.warn("Multiple paths on method " + method.toGenericString() + ", using first one"); } - return annot.value()[0]; + return paths[0]; + } + + private static Method getMethod(Class<?> controllerType, final String methodName, final Object... args) { + MethodFilter selector = new MethodFilter() { + @Override + public boolean matches(Method method) { + String name = method.getName(); + int argLength = method.getParameterTypes().length; + return (name.equals(methodName) && argLength == args.length); + } + }; + Set<Method> methods = MethodIntrospector.selectMethods(controllerType, selector); + if (methods.size() == 1) { + return methods.iterator().next(); + } + else if (methods.size() > 1) { + throw new IllegalArgumentException(String.format( + "Found two methods named '%s' accepting arguments %s in controller %s: [%s]", + methodName, Arrays.asList(args), controllerType.getName(), methods)); + } + else { + throw new IllegalArgumentException("No method named '" + methodName + "' with " + args.length + + " arguments found in controller " + controllerType.getName()); + } } private static UriComponents applyContributors(UriComponentsBuilder builder, Method method, Object... args) { @@ -322,7 +485,7 @@ public class MvcUriComponentsBuilder extends UriComponentsBuilder { final Map<String, Object> uriVars = new HashMap<String, Object>(); for (int i = 0; i < paramCount; i++) { - MethodParameter param = new MethodParameter(method, i); + MethodParameter param = new SynthesizingMethodParameter(method, i); param.initParameterNameDiscovery(parameterNameDiscoverer); contributor.contributeMethodArgument(param, args[i], builder, uriVars); } @@ -336,7 +499,7 @@ public class MvcUriComponentsBuilder extends UriComponentsBuilder { }); } - protected static CompositeUriComponentsContributor getConfiguredUriComponentsContributor() { + private static CompositeUriComponentsContributor getConfiguredUriComponentsContributor() { WebApplicationContext wac = getWebApplicationContext(); if (wac == null) { return null; @@ -353,7 +516,7 @@ public class MvcUriComponentsBuilder extends UriComponentsBuilder { } } - protected static RequestMappingInfoHandlerMapping getRequestMappingInfoHandlerMapping() { + private static RequestMappingInfoHandlerMapping getRequestMappingInfoHandlerMapping() { WebApplicationContext wac = getWebApplicationContext(); Assert.notNull(wac, "Cannot lookup handler method mappings without WebApplicationContext"); try { @@ -427,7 +590,7 @@ public class MvcUriComponentsBuilder extends UriComponentsBuilder { */ public static <T> T controller(Class<T> controllerType) { Assert.notNull(controllerType, "'controllerType' must not be null"); - return initProxy(controllerType, new ControllerMethodInvocationInterceptor()); + return initProxy(controllerType, new ControllerMethodInvocationInterceptor(controllerType)); } @SuppressWarnings("unchecked") @@ -439,21 +602,85 @@ public class MvcUriComponentsBuilder extends UriComponentsBuilder { factory.addAdvice(interceptor); return (T) factory.getProxy(); } + else { Enhancer enhancer = new Enhancer(); enhancer.setSuperclass(type); enhancer.setInterfaces(new Class<?>[] {MethodInvocationInfo.class}); enhancer.setNamingPolicy(SpringNamingPolicy.INSTANCE); enhancer.setCallbackType(org.springframework.cglib.proxy.MethodInterceptor.class); - Factory factory = (Factory) objenesis.newInstance(enhancer.createClass()); - factory.setCallbacks(new Callback[] {interceptor}); - return (T) factory; + + Class<?> proxyClass = enhancer.createClass(); + Object proxy = null; + + if (objenesis.isWorthTrying()) { + try { + proxy = objenesis.newInstance(proxyClass, enhancer.getUseCache()); + } + catch (ObjenesisException ex) { + logger.debug("Unable to instantiate controller proxy using Objenesis, " + + "falling back to regular construction", ex); + } + } + + if (proxy == null) { + try { + proxy = proxyClass.newInstance(); + } + catch (Exception ex) { + throw new IllegalStateException("Unable to instantiate controller proxy using Objenesis, " + + "and regular controller instantiation via default constructor fails as well", ex); + } + } + + ((Factory) proxy).setCallbacks(new Callback[] {interceptor}); + return (T) proxy; } } - @Override - protected Object clone() { - return new MvcUriComponentsBuilder(this); + /** + * An alternative to {@link #fromController(Class)} for use with an instance + * of this class created via a call to {@link #relativeTo}. + * @since 4.2 + */ + public UriComponentsBuilder withController(Class<?> controllerType) { + return fromController(this.baseUrl, controllerType); + } + + /** + * An alternative to {@link #fromMethodName(Class, String, Object...)}} for + * use with an instance of this class created via {@link #relativeTo}. + * @since 4.2 + */ + public UriComponentsBuilder withMethodName(Class<?> controllerType, String methodName, Object... args) { + return fromMethodName(this.baseUrl, controllerType, methodName, args); + } + + /** + * An alternative to {@link #fromMethodCall(Object)} for use with an instance + * of this class created via {@link #relativeTo}. + * @since 4.2 + */ + public UriComponentsBuilder withMethodCall(Object invocationInfo) { + return fromMethodCall(this.baseUrl, invocationInfo); + } + + /** + * An alternative to {@link #fromMappingName(String)} for use with an instance + * of this class created via {@link #relativeTo}. + * @since 4.2 + */ + public MethodArgumentBuilder withMappingName(String mappingName) { + return fromMappingName(this.baseUrl, mappingName); + } + + /** + * An alternative to {@link #fromMethod(Class, Method, Object...)} + * for use with an instance of this class created via {@link #relativeTo}. + * @since 4.2 + */ + public UriComponentsBuilder withMethod(Class<?> controllerType, Method method, Object... args) { + return fromMethod(this.baseUrl, controllerType, method, args); } @@ -466,10 +693,18 @@ public class MvcUriComponentsBuilder extends UriComponentsBuilder { private static final Method getArgumentValues = ReflectionUtils.findMethod(MethodInvocationInfo.class, "getArgumentValues"); + private static final Method getControllerType = + ReflectionUtils.findMethod(MethodInvocationInfo.class, "getControllerType"); + private Method controllerMethod; private Object[] argumentValues; + private Class<?> controllerType; + + ControllerMethodInvocationInterceptor(Class<?> controllerType) { + this.controllerType = controllerType; + } @Override public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) { @@ -479,6 +714,9 @@ public class MvcUriComponentsBuilder extends UriComponentsBuilder { else if (getArgumentValues.equals(method)) { return this.argumentValues; } + else if (getControllerType.equals(method)) { + return this.controllerType; + } else if (ReflectionUtils.isObjectMethod(method)) { return ReflectionUtils.invokeMethod(method, obj, args); } @@ -486,7 +724,7 @@ public class MvcUriComponentsBuilder extends UriComponentsBuilder { this.controllerMethod = method; this.argumentValues = args; Class<?> returnType = method.getReturnType(); - return (void.class.equals(returnType) ? null : returnType.cast(initProxy(returnType, this))); + return (void.class == returnType ? null : returnType.cast(initProxy(returnType, this))); } } @@ -502,18 +740,36 @@ public class MvcUriComponentsBuilder extends UriComponentsBuilder { Method getControllerMethod(); Object[] getArgumentValues(); + + Class<?> getControllerType(); } public static class MethodArgumentBuilder { + private final Class<?> controllerType; + private final Method method; private final Object[] argumentValues; + private final UriComponentsBuilder baseUrl; - public MethodArgumentBuilder(Method method) { + /** + * @since 4.2 + */ + public MethodArgumentBuilder(Class<?> controllerType, Method method) { + this(null, controllerType, method); + } + + /** + * @since 4.2 + */ + public MethodArgumentBuilder(UriComponentsBuilder baseUrl, Class<?> controllerType, Method method) { + Assert.notNull(controllerType, "'controllerType' is required"); Assert.notNull(method, "'method' is required"); + this.baseUrl = (baseUrl != null ? baseUrl : initBaseUrl()); + this.controllerType = controllerType; this.method = method; this.argumentValues = new Object[method.getParameterTypes().length]; for (int i = 0; i < this.argumentValues.length; i++) { @@ -521,19 +777,34 @@ public class MvcUriComponentsBuilder extends UriComponentsBuilder { } } + /** + * @see #MethodArgumentBuilder(Class, Method) + * @deprecated as of 4.2, this is deprecated in favor of alternative constructors + * that accept a controllerType argument + */ + @Deprecated + public MethodArgumentBuilder(Method method) { + this(method.getDeclaringClass(), method); + } + + private static UriComponentsBuilder initBaseUrl() { + UriComponentsBuilder builder = ServletUriComponentsBuilder.fromCurrentServletMapping(); + return UriComponentsBuilder.fromPath(builder.build().getPath()); + } + public MethodArgumentBuilder arg(int index, Object value) { this.argumentValues[index] = value; return this; } public String build() { - return MvcUriComponentsBuilder.fromMethod(this.method, this.argumentValues) - .build(false).encode().toUriString(); + return fromMethodInternal(this.baseUrl, this.controllerType, this.method, + this.argumentValues).build(false).encode().toUriString(); } - public String buildAndExpand(Object... uriVariables) { - return MvcUriComponentsBuilder.fromMethod(this.method, this.argumentValues) - .build(false).expand(uriVariables).encode().toString(); + public String buildAndExpand(Object... uriVars) { + return fromMethodInternal(this.baseUrl, this.controllerType, this.method, + this.argumentValues).build(false).expand(uriVars).encode().toString(); } } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/PathVariableMethodArgumentResolver.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/PathVariableMethodArgumentResolver.java index 3aa5143c..0475e03b 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/PathVariableMethodArgumentResolver.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/PathVariableMethodArgumentResolver.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2014 the original author or authors. + * Copyright 2002-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -25,6 +25,7 @@ import org.springframework.core.convert.ConversionService; import org.springframework.core.convert.TypeDescriptor; import org.springframework.core.convert.converter.Converter; import org.springframework.util.StringUtils; +import org.springframework.web.bind.MissingPathVariableException; import org.springframework.web.bind.ServletRequestBindingException; import org.springframework.web.bind.WebDataBinder; import org.springframework.web.bind.annotation.PathVariable; @@ -32,7 +33,6 @@ import org.springframework.web.bind.annotation.ValueConstants; import org.springframework.web.context.request.NativeWebRequest; import org.springframework.web.context.request.RequestAttributes; import org.springframework.web.method.annotation.AbstractNamedValueMethodArgumentResolver; -import org.springframework.web.method.annotation.RequestParamMapMethodArgumentResolver; import org.springframework.web.method.support.ModelAndViewContainer; import org.springframework.web.method.support.UriComponentsContributor; import org.springframework.web.servlet.HandlerMapping; @@ -42,22 +42,18 @@ import org.springframework.web.util.UriComponentsBuilder; /** * Resolves method arguments annotated with an @{@link PathVariable}. * - * <p>An @{@link PathVariable} is a named value that gets resolved from a URI - * template variable. It is always required and does not have a default value - * to fall back on. See the base class + * <p>An @{@link PathVariable} is a named value that gets resolved from a URI template variable. + * It is always required and does not have a default value to fall back on. See the base class * {@link org.springframework.web.method.annotation.AbstractNamedValueMethodArgumentResolver} * for more information on how named values are processed. * - * <p>If the method parameter type is {@link Map}, the name specified in the - * annotation is used to resolve the URI variable String value. The value is - * then converted to a {@link Map} via type conversion assuming a suitable - * {@link Converter} or {@link PropertyEditor} has been registered. - * Or if the annotation does not specify name the - * {@link RequestParamMapMethodArgumentResolver} is used instead to provide - * access to all URI variables in a map. + * <p>If the method parameter type is {@link Map}, the name specified in the annotation is used + * to resolve the URI variable String value. The value is then converted to a {@link Map} via + * type conversion, assuming a suitable {@link Converter} or {@link PropertyEditor} has been + * registered. * - * <p>A {@link WebDataBinder} is invoked to apply type conversion to resolved - * path variable values that don't yet match the method parameter type. + * <p>A {@link WebDataBinder} is invoked to apply type conversion to resolved path variable + * values that don't yet match the method parameter type. * * @author Rossen Stoyanchev * @author Arjen Poutsma @@ -94,16 +90,16 @@ public class PathVariableMethodArgumentResolver extends AbstractNamedValueMethod @Override @SuppressWarnings("unchecked") protected Object resolveName(String name, MethodParameter parameter, NativeWebRequest request) throws Exception { - Map<String, String> uriTemplateVars = - (Map<String, String>) request.getAttribute( - HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE, RequestAttributes.SCOPE_REQUEST); - return (uriTemplateVars != null) ? uriTemplateVars.get(name) : null; + Map<String, String> uriTemplateVars = (Map<String, String>) request.getAttribute( + HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE, RequestAttributes.SCOPE_REQUEST); + return (uriTemplateVars != null ? uriTemplateVars.get(name) : null); } @Override - protected void handleMissingValue(String name, MethodParameter parameter) throws ServletRequestBindingException { - throw new ServletRequestBindingException("Missing URI template variable '" + name + - "' for method parameter of type " + parameter.getParameterType().getSimpleName()); + protected void handleMissingValue(String name, MethodParameter parameter) + throws ServletRequestBindingException { + + throw new MissingPathVariableException(name, parameter); } @Override diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestBodyAdvice.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestBodyAdvice.java new file mode 100644 index 00000000..ee0be650 --- /dev/null +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestBodyAdvice.java @@ -0,0 +1,91 @@ +/* + * Copyright 2002-2015 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.web.servlet.mvc.method.annotation; + +import java.io.IOException; +import java.lang.reflect.Type; + +import org.springframework.core.MethodParameter; +import org.springframework.http.HttpInputMessage; +import org.springframework.http.converter.HttpMessageConverter; + +/** + * Allows customizing the request before its body is read and converted into an + * Object and also allows for processing of the resulting Object before it is + * passed into a controller method as an {@code @RequestBody} or an + * {@code HttpEntity} method argument. + * + * <p>Implementations of this contract may be registered directly with the + * {@code RequestMappingHandlerAdapter} or more likely annotated with + * {@code @ControllerAdvice} in which case they are auto-detected. + * + * @author Rossen Stoyanchev + * @since 4.2 + */ +public interface RequestBodyAdvice { + + /** + * Invoked first to determine if this interceptor applies. + * @param methodParameter the method parameter + * @param targetType the target type, not necessarily the same as the method + * parameter type, e.g. for {@code HttpEntity<String>}. + * @param converterType the selected converter type + * @return whether this interceptor should be invoked or not + */ + boolean supports(MethodParameter methodParameter, Type targetType, + Class<? extends HttpMessageConverter<?>> converterType); + + /** + * Invoked second (and last) if the body is empty. + * @param body set to {@code null} before the first advice is called + * @param inputMessage the request + * @param parameter the method parameter + * @param targetType the target type, not necessarily the same as the method + * parameter type, e.g. for {@code HttpEntity<String>}. + * @param converterType the selected converter type + * @return the value to use or {@code null} which may then raise an + * {@code HttpMessageNotReadableException} if the argument is required. + */ + Object handleEmptyBody(Object body, HttpInputMessage inputMessage, MethodParameter parameter, + Type targetType, Class<? extends HttpMessageConverter<?>> converterType); + + /** + * Invoked second before the request body is read and converted. + * @param inputMessage the request + * @param parameter the target method parameter + * @param targetType the target type, not necessarily the same as the method + * parameter type, e.g. for {@code HttpEntity<String>}. + * @param converterType the converter used to deserialize the body + * @return the input request or a new instance, never {@code null} + */ + HttpInputMessage beforeBodyRead(HttpInputMessage inputMessage, MethodParameter parameter, + Type targetType, Class<? extends HttpMessageConverter<?>> converterType) throws IOException; + + /** + * Invoked third (and last) after the request body is converted to an Object. + * @param body set to the converter Object before the 1st advice is called + * @param inputMessage the request + * @param parameter the target method parameter + * @param targetType the target type, not necessarily the same as the method + * parameter type, e.g. for {@code HttpEntity<String>}. + * @param converterType the converter used to deserialize the body + * @return the same body or a new instance + */ + Object afterBodyRead(Object body, HttpInputMessage inputMessage, MethodParameter parameter, + Type targetType, Class<? extends HttpMessageConverter<?>> converterType); + +} diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestBodyAdviceAdapter.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestBodyAdviceAdapter.java new file mode 100644 index 00000000..ee110ee9 --- /dev/null +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestBodyAdviceAdapter.java @@ -0,0 +1,70 @@ +/* + * Copyright 2002-2015 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.web.servlet.mvc.method.annotation; + +import java.io.IOException; +import java.lang.reflect.Type; + +import org.springframework.core.MethodParameter; +import org.springframework.http.HttpInputMessage; +import org.springframework.http.converter.HttpMessageConverter; + +/** + * A convenient starting point for implementing + * {@link org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice + * ResponseBodyAdvice} with default method implementations. + * + * <p>Sub-classes are required to implement {@link #supports} to return true + * depending on when the advice applies. + * + * @author Rossen Stoyanchev + * @since 4.2 + */ +public abstract class RequestBodyAdviceAdapter implements RequestBodyAdvice { + + /** + * The default implementation returns the body that was passed in. + */ + @Override + public Object handleEmptyBody(Object body, HttpInputMessage inputMessage, + MethodParameter parameter, Type targetType, + Class<? extends HttpMessageConverter<?>> converterType) { + + return body; + } + + /** + * The default implementation returns the InputMessage that was passed in. + */ + @Override + public HttpInputMessage beforeBodyRead(HttpInputMessage inputMessage, MethodParameter parameter, + Type targetType, Class<? extends HttpMessageConverter<?>> converterType) + throws IOException { + + return inputMessage; + } + + /** + * The default implementation returns the body that was passed in. + */ + @Override + public Object afterBodyRead(Object body, HttpInputMessage inputMessage, MethodParameter parameter, + Type targetType, Class<? extends HttpMessageConverter<?>> converterType) { + + return body; + } + +} diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerAdapter.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerAdapter.java index 2c87b61d..ce031bd5 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerAdapter.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerAdapter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * Copyright 2002-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -35,8 +35,9 @@ import org.springframework.beans.factory.BeanFactoryAware; import org.springframework.beans.factory.InitializingBean; import org.springframework.beans.factory.config.ConfigurableBeanFactory; import org.springframework.core.DefaultParameterNameDiscoverer; -import org.springframework.core.OrderComparator; +import org.springframework.core.MethodIntrospector; import org.springframework.core.ParameterNameDiscoverer; +import org.springframework.core.annotation.AnnotationAwareOrderComparator; import org.springframework.core.annotation.AnnotationUtils; import org.springframework.core.task.AsyncTaskExecutor; import org.springframework.core.task.SimpleAsyncTaskExecutor; @@ -47,6 +48,7 @@ import org.springframework.http.converter.support.AllEncompassingFormHttpMessage import org.springframework.http.converter.xml.SourceHttpMessageConverter; import org.springframework.ui.ModelMap; import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; import org.springframework.util.CollectionUtils; import org.springframework.util.ReflectionUtils.MethodFilter; import org.springframework.web.accept.ContentNegotiationManager; @@ -69,7 +71,6 @@ import org.springframework.web.context.request.async.WebAsyncTask; import org.springframework.web.context.request.async.WebAsyncUtils; import org.springframework.web.method.ControllerAdviceBean; import org.springframework.web.method.HandlerMethod; -import org.springframework.web.method.HandlerMethodSelector; import org.springframework.web.method.annotation.ErrorsMethodArgumentResolver; import org.springframework.web.method.annotation.ExpressionValueMethodArgumentResolver; import org.springframework.web.method.annotation.InitBinderDataBinderFactory; @@ -108,6 +109,7 @@ import org.springframework.web.util.WebUtils; * use {@link #setArgumentResolvers} and {@link #setReturnValueHandlers}. * * @author Rossen Stoyanchev + * @author Juergen Hoeller * @since 3.1 * @see HandlerMethodArgumentResolver * @see HandlerMethodReturnValueHandler @@ -115,6 +117,10 @@ import org.springframework.web.util.WebUtils; public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter implements BeanFactoryAware, InitializingBean { + private static final boolean completionStagePresent = ClassUtils.isPresent( + "java.util.concurrent.CompletionStage", RequestMappingHandlerAdapter.class.getClassLoader()); + + private List<HandlerMethodArgumentResolver> customArgumentResolvers; private HandlerMethodArgumentResolverComposite argumentResolvers; @@ -131,7 +137,7 @@ public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter private List<HttpMessageConverter<?>> messageConverters; - private List<Object> responseBodyAdvice = new ArrayList<Object>(); + private List<Object> requestResponseBodyAdvice = new ArrayList<Object>(); private WebBindingInitializer webBindingInitializer; @@ -329,15 +335,24 @@ public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter } /** - * Add one or more components to modify the response after the execution of a - * controller method annotated with {@code @ResponseBody}, or a method returning - * {@code ResponseEntity} and before the body is written to the response with - * the selected {@code HttpMessageConverter}. + * Add one or more {@code RequestBodyAdvice} instances to intercept the + * request before it is read and converted for {@code @RequestBody} and + * {@code HttpEntity} method arguments. + */ + public void setRequestBodyAdvice(List<RequestBodyAdvice> requestBodyAdvice) { + if (requestBodyAdvice != null) { + this.requestResponseBodyAdvice.addAll(requestBodyAdvice); + } + } + + /** + * Add one or more {@code ResponseBodyAdvice} instances to intercept the + * response before {@code @ResponseBody} or {@code ResponseEntity} return + * values are written to the response body. */ public void setResponseBodyAdvice(List<ResponseBodyAdvice<?>> responseBodyAdvice) { - this.responseBodyAdvice.clear(); if (responseBodyAdvice != null) { - this.responseBodyAdvice.addAll(responseBodyAdvice); + this.requestResponseBodyAdvice.addAll(responseBodyAdvice); } } @@ -429,7 +444,14 @@ public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter /** * Cache content produced by {@code @SessionAttributes} annotated handlers - * for the given number of seconds. Default is 0, preventing caching completely. + * for the given number of seconds. + * <p>Possible values are: + * <ul> + * <li>-1: no generation of cache-related headers</li> + * <li>0 (default value): "Cache-Control: no-store" will prevent caching</li> + * <li>1 or higher: "Cache-Control: max-age=seconds" will ask to cache content; + * not advised when dealing with session attributes</li> + * </ul> * <p>In contrast to the "cacheSeconds" property which will apply to all general * handlers (but not to {@code @SessionAttributes} annotated handlers), * this setting will apply to {@code @SessionAttributes} handlers only. @@ -518,29 +540,41 @@ public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter } List<ControllerAdviceBean> beans = ControllerAdviceBean.findAnnotatedBeans(getApplicationContext()); - OrderComparator.sort(beans); + AnnotationAwareOrderComparator.sort(beans); - List<Object> responseBodyAdviceBeans = new ArrayList<Object>(); + List<Object> requestResponseBodyAdviceBeans = new ArrayList<Object>(); for (ControllerAdviceBean bean : beans) { - Set<Method> attrMethods = HandlerMethodSelector.selectMethods(bean.getBeanType(), MODEL_ATTRIBUTE_METHODS); + Set<Method> attrMethods = MethodIntrospector.selectMethods(bean.getBeanType(), MODEL_ATTRIBUTE_METHODS); if (!attrMethods.isEmpty()) { this.modelAttributeAdviceCache.put(bean, attrMethods); - logger.info("Detected @ModelAttribute methods in " + bean); + if (logger.isInfoEnabled()) { + logger.info("Detected @ModelAttribute methods in " + bean); + } } - Set<Method> binderMethods = HandlerMethodSelector.selectMethods(bean.getBeanType(), INIT_BINDER_METHODS); + Set<Method> binderMethods = MethodIntrospector.selectMethods(bean.getBeanType(), INIT_BINDER_METHODS); if (!binderMethods.isEmpty()) { this.initBinderAdviceCache.put(bean, binderMethods); - logger.info("Detected @InitBinder methods in " + bean); + if (logger.isInfoEnabled()) { + logger.info("Detected @InitBinder methods in " + bean); + } + } + if (RequestBodyAdvice.class.isAssignableFrom(bean.getBeanType())) { + requestResponseBodyAdviceBeans.add(bean); + if (logger.isInfoEnabled()) { + logger.info("Detected RequestBodyAdvice bean in " + bean); + } } if (ResponseBodyAdvice.class.isAssignableFrom(bean.getBeanType())) { - responseBodyAdviceBeans.add(bean); - logger.info("Detected ResponseBodyAdvice bean in " + bean); + requestResponseBodyAdviceBeans.add(bean); + if (logger.isInfoEnabled()) { + logger.info("Detected ResponseBodyAdvice bean in " + bean); + } } } - if (!responseBodyAdviceBeans.isEmpty()) { - this.responseBodyAdvice.addAll(0, responseBodyAdviceBeans); + if (!requestResponseBodyAdviceBeans.isEmpty()) { + this.requestResponseBodyAdvice.addAll(0, requestResponseBodyAdviceBeans); } } @@ -559,8 +593,8 @@ public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter resolvers.add(new MatrixVariableMethodArgumentResolver()); resolvers.add(new MatrixVariableMapMethodArgumentResolver()); resolvers.add(new ServletModelAttributeMethodProcessor(false)); - resolvers.add(new RequestResponseBodyMethodProcessor(getMessageConverters())); - resolvers.add(new RequestPartMethodArgumentResolver(getMessageConverters())); + resolvers.add(new RequestResponseBodyMethodProcessor(getMessageConverters(), this.requestResponseBodyAdvice)); + resolvers.add(new RequestPartMethodArgumentResolver(getMessageConverters(), this.requestResponseBodyAdvice)); resolvers.add(new RequestHeaderMethodArgumentResolver(getBeanFactory())); resolvers.add(new RequestHeaderMapMethodArgumentResolver()); resolvers.add(new ServletCookieValueMethodArgumentResolver(getBeanFactory())); @@ -569,7 +603,7 @@ public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter // Type-based argument resolution resolvers.add(new ServletRequestMethodArgumentResolver()); resolvers.add(new ServletResponseMethodArgumentResolver()); - resolvers.add(new HttpEntityMethodProcessor(getMessageConverters())); + resolvers.add(new HttpEntityMethodProcessor(getMessageConverters(), this.requestResponseBodyAdvice)); resolvers.add(new RedirectAttributesMethodArgumentResolver()); resolvers.add(new ModelMethodProcessor()); resolvers.add(new MapMethodProcessor()); @@ -631,18 +665,23 @@ public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter handlers.add(new ModelAndViewMethodReturnValueHandler()); handlers.add(new ModelMethodProcessor()); handlers.add(new ViewMethodReturnValueHandler()); - handlers.add(new HttpEntityMethodProcessor( - getMessageConverters(), this.contentNegotiationManager, this.responseBodyAdvice)); + handlers.add(new ResponseBodyEmitterReturnValueHandler(getMessageConverters())); + handlers.add(new StreamingResponseBodyReturnValueHandler()); + handlers.add(new HttpEntityMethodProcessor(getMessageConverters(), + this.contentNegotiationManager, this.requestResponseBodyAdvice)); handlers.add(new HttpHeadersReturnValueHandler()); handlers.add(new CallableMethodReturnValueHandler()); handlers.add(new DeferredResultMethodReturnValueHandler()); handlers.add(new AsyncTaskMethodReturnValueHandler(this.beanFactory)); handlers.add(new ListenableFutureReturnValueHandler()); + if (completionStagePresent) { + handlers.add(new CompletionStageReturnValueHandler()); + } // Annotation-based return value types handlers.add(new ModelAttributeMethodProcessor(false)); - handlers.add(new RequestResponseBodyMethodProcessor( - getMessageConverters(), this.contentNegotiationManager, this.responseBodyAdvice)); + handlers.add(new RequestResponseBodyMethodProcessor(getMessageConverters(), + this.contentNegotiationManager, this.requestResponseBodyAdvice)); // Multi-purpose return value types handlers.add(new ViewNameMethodReturnValueHandler()); @@ -682,14 +721,8 @@ public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter protected ModelAndView handleInternal(HttpServletRequest request, HttpServletResponse response, HandlerMethod handlerMethod) throws Exception { - if (getSessionAttributesHandler(handlerMethod).hasSessionAttributes()) { - // Always prevent caching in case of session attribute management. - checkAndPrepare(request, response, this.cacheSecondsForSessionAttributeHandlers, true); - } - else { - // Uses configured default cacheSeconds setting. - checkAndPrepare(request, response, true); - } + ModelAndView mav; + checkRequest(request); // Execute invokeHandlerMethod in synchronized block if required. if (this.synchronizeOnSession) { @@ -697,12 +730,29 @@ public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter if (session != null) { Object mutex = WebUtils.getSessionMutex(session); synchronized (mutex) { - return invokeHandlerMethod(request, response, handlerMethod); + mav = invokeHandlerMethod(request, response, handlerMethod); } } + else { + // No HttpSession available -> no mutex necessary + mav = invokeHandlerMethod(request, response, handlerMethod); + } + } + else { + // No synchronization on session demanded at all... + mav = invokeHandlerMethod(request, response, handlerMethod); + } + + if (!response.containsHeader(HEADER_CACHE_CONTROL)) { + if (getSessionAttributesHandler(handlerMethod).hasSessionAttributes()) { + applyCacheSeconds(response, this.cacheSecondsForSessionAttributeHandlers); + } + else { + prepareResponse(response); + } } - return invokeHandlerMethod(request, response, handlerMethod); + return mav; } /** @@ -738,19 +788,26 @@ public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter /** * Invoke the {@link RequestMapping} handler method preparing a {@link ModelAndView} * if view resolution is required. + * @since 4.2 + * @see #createInvocableHandlerMethod(HandlerMethod) */ - private ModelAndView invokeHandlerMethod(HttpServletRequest request, HttpServletResponse response, - HandlerMethod handlerMethod) throws Exception { + protected ModelAndView invokeHandlerMethod(HttpServletRequest request, + HttpServletResponse response, HandlerMethod handlerMethod) throws Exception { ServletWebRequest webRequest = new ServletWebRequest(request, response); WebDataBinderFactory binderFactory = getDataBinderFactory(handlerMethod); ModelFactory modelFactory = getModelFactory(handlerMethod, binderFactory); - ServletInvocableHandlerMethod requestMappingMethod = createRequestMappingMethod(handlerMethod, binderFactory); + + ServletInvocableHandlerMethod invocableMethod = createInvocableHandlerMethod(handlerMethod); + invocableMethod.setHandlerMethodArgumentResolvers(this.argumentResolvers); + invocableMethod.setHandlerMethodReturnValueHandlers(this.returnValueHandlers); + invocableMethod.setDataBinderFactory(binderFactory); + invocableMethod.setParameterNameDiscoverer(this.parameterNameDiscoverer); ModelAndViewContainer mavContainer = new ModelAndViewContainer(); mavContainer.addAllAttributes(RequestContextUtils.getInputFlashMap(request)); - modelFactory.initModel(webRequest, mavContainer, requestMappingMethod); + modelFactory.initModel(webRequest, mavContainer, invocableMethod); mavContainer.setIgnoreDefaultModelOnRedirect(this.ignoreDefaultModelOnRedirect); AsyncWebRequest asyncWebRequest = WebAsyncUtils.createAsyncWebRequest(request, response); @@ -769,10 +826,10 @@ public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter if (logger.isDebugEnabled()) { logger.debug("Found concurrent result value [" + result + "]"); } - requestMappingMethod = requestMappingMethod.wrapConcurrentResult(result); + invocableMethod = invocableMethod.wrapConcurrentResult(result); } - requestMappingMethod.invokeAndHandle(webRequest, mavContainer); + invocableMethod.invokeAndHandle(webRequest, mavContainer); if (asyncManager.isConcurrentHandlingStarted()) { return null; } @@ -780,16 +837,14 @@ public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter return getModelAndView(mavContainer, modelFactory, webRequest); } - private ServletInvocableHandlerMethod createRequestMappingMethod( - HandlerMethod handlerMethod, WebDataBinderFactory binderFactory) { - - ServletInvocableHandlerMethod requestMethod; - requestMethod = new ServletInvocableHandlerMethod(handlerMethod); - requestMethod.setHandlerMethodArgumentResolvers(this.argumentResolvers); - requestMethod.setHandlerMethodReturnValueHandlers(this.returnValueHandlers); - requestMethod.setDataBinderFactory(binderFactory); - requestMethod.setParameterNameDiscoverer(this.parameterNameDiscoverer); - return requestMethod; + /** + * Create a {@link ServletInvocableHandlerMethod} from the given {@link HandlerMethod} definition. + * @param handlerMethod the {@link HandlerMethod} definition + * @return the corresponding {@link ServletInvocableHandlerMethod} (or custom subclass thereof) + * @since 4.2 + */ + protected ServletInvocableHandlerMethod createInvocableHandlerMethod(HandlerMethod handlerMethod) { + return new ServletInvocableHandlerMethod(handlerMethod); } private ModelFactory getModelFactory(HandlerMethod handlerMethod, WebDataBinderFactory binderFactory) { @@ -797,7 +852,7 @@ public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter Class<?> handlerType = handlerMethod.getBeanType(); Set<Method> methods = this.modelAttributeCache.get(handlerType); if (methods == null) { - methods = HandlerMethodSelector.selectMethods(handlerType, MODEL_ATTRIBUTE_METHODS); + methods = MethodIntrospector.selectMethods(handlerType, MODEL_ATTRIBUTE_METHODS); this.modelAttributeCache.put(handlerType, methods); } List<InvocableHandlerMethod> attrMethods = new ArrayList<InvocableHandlerMethod>(); @@ -829,12 +884,12 @@ public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter Class<?> handlerType = handlerMethod.getBeanType(); Set<Method> methods = this.initBinderCache.get(handlerType); if (methods == null) { - methods = HandlerMethodSelector.selectMethods(handlerType, INIT_BINDER_METHODS); + methods = MethodIntrospector.selectMethods(handlerType, INIT_BINDER_METHODS); this.initBinderCache.put(handlerType, methods); } List<InvocableHandlerMethod> initBinderMethods = new ArrayList<InvocableHandlerMethod>(); // Global methods first - for (Entry<ControllerAdviceBean, Set<Method>> entry : this.initBinderAdviceCache .entrySet()) { + for (Entry<ControllerAdviceBean, Set<Method>> entry : this.initBinderAdviceCache.entrySet()) { if (entry.getKey().isApplicableToBeanType(handlerType)) { Object bean = entry.getKey().resolveBean(); for (Method method : entry.getValue()) { @@ -896,7 +951,6 @@ public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter * MethodFilter that matches {@link InitBinder @InitBinder} methods. */ public static final MethodFilter INIT_BINDER_METHODS = new MethodFilter() { - @Override public boolean matches(Method method) { return AnnotationUtils.findAnnotation(method, InitBinder.class) != null; @@ -907,7 +961,6 @@ public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter * MethodFilter that matches {@link ModelAttribute @ModelAttribute} methods. */ public static final MethodFilter MODEL_ATTRIBUTE_METHODS = new MethodFilter() { - @Override public boolean matches(Method method) { return ((AnnotationUtils.findAnnotation(method, RequestMapping.class) == null) && diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerMapping.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerMapping.java index dd28d139..5ffb6aaf 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerMapping.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerMapping.java @@ -16,26 +16,27 @@ package org.springframework.web.servlet.mvc.method.annotation; +import java.lang.reflect.AnnotatedElement; import java.lang.reflect.Method; -import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import org.springframework.context.EmbeddedValueResolverAware; +import org.springframework.core.annotation.AnnotatedElementUtils; import org.springframework.core.annotation.AnnotationUtils; import org.springframework.stereotype.Controller; import org.springframework.util.Assert; +import org.springframework.util.CollectionUtils; import org.springframework.util.StringValueResolver; import org.springframework.web.accept.ContentNegotiationManager; +import org.springframework.web.bind.annotation.CrossOrigin; import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.method.HandlerMethod; import org.springframework.web.servlet.mvc.condition.AbstractRequestCondition; import org.springframework.web.servlet.mvc.condition.CompositeRequestCondition; -import org.springframework.web.servlet.mvc.condition.ConsumesRequestCondition; -import org.springframework.web.servlet.mvc.condition.HeadersRequestCondition; -import org.springframework.web.servlet.mvc.condition.ParamsRequestCondition; -import org.springframework.web.servlet.mvc.condition.PatternsRequestCondition; -import org.springframework.web.servlet.mvc.condition.ProducesRequestCondition; import org.springframework.web.servlet.mvc.condition.RequestCondition; -import org.springframework.web.servlet.mvc.condition.RequestMethodsRequestCondition; import org.springframework.web.servlet.mvc.method.RequestMappingInfo; import org.springframework.web.servlet.mvc.method.RequestMappingInfoHandlerMapping; @@ -46,6 +47,7 @@ import org.springframework.web.servlet.mvc.method.RequestMappingInfoHandlerMappi * * @author Arjen Poutsma * @author Rossen Stoyanchev + * @author Sam Brannen * @since 3.1 */ public class RequestMappingHandlerMapping extends RequestMappingInfoHandlerMapping @@ -59,10 +61,10 @@ public class RequestMappingHandlerMapping extends RequestMappingInfoHandlerMappi private ContentNegotiationManager contentNegotiationManager = new ContentNegotiationManager(); - private final List<String> fileExtensions = new ArrayList<String>(); - private StringValueResolver embeddedValueResolver; + private RequestMappingInfo.BuilderConfiguration config = new RequestMappingInfo.BuilderConfiguration(); + /** * Whether to use suffix pattern match (".*") when matching patterns to @@ -76,19 +78,11 @@ public class RequestMappingHandlerMapping extends RequestMappingInfoHandlerMappi } /** - * Whether to use suffix pattern match for registered file extensions only - * when matching patterns to requests. - * <p>If enabled, a controller method mapped to "/users" also matches to - * "/users.json" assuming ".json" is a file extension registered with the - * provided {@link #setContentNegotiationManager(ContentNegotiationManager) - * contentNegotiationManager}. This can be useful for allowing only specific - * URL extensions to be used as well as in cases where a "." in the URL path - * can lead to ambiguous interpretation of path variable content, (e.g. given - * "/users/{user}" and incoming URLs such as "/users/john.j.joe" and - * "/users/john.j.joe.json"). - * <p>If enabled, this flag also enables - * {@link #setUseSuffixPatternMatch(boolean) useSuffixPatternMatch}. The - * default value is {@code false}. + * Whether suffix pattern matching should work only against path extensions + * explicitly registered with the {@link ContentNegotiationManager}. This + * is generally recommended to reduce ambiguity and to avoid issues such as + * when a "." appears in the path for other reasons. + * <p>By default this is set to "false". */ public void setUseRegisteredSuffixPatternMatch(boolean useRegisteredSuffixPatternMatch) { this.useRegisteredSuffixPatternMatch = useRegisteredSuffixPatternMatch; @@ -120,9 +114,14 @@ public class RequestMappingHandlerMapping extends RequestMappingInfoHandlerMappi @Override public void afterPropertiesSet() { - if (this.useRegisteredSuffixPatternMatch) { - this.fileExtensions.addAll(this.contentNegotiationManager.getAllFileExtensions()); - } + this.config = new RequestMappingInfo.BuilderConfiguration(); + this.config.setPathHelper(getUrlPathHelper()); + this.config.setPathMatcher(getPathMatcher()); + this.config.setSuffixPatternMatch(this.useSuffixPatternMatch); + this.config.setTrailingSlashMatch(this.useTrailingSlashMatch); + this.config.setRegisteredSuffixPatternMatch(this.useRegisteredSuffixPatternMatch); + this.config.setContentNegotiationManager(getContentNegotiationManager()); + super.afterPropertiesSet(); } @@ -159,7 +158,7 @@ public class RequestMappingHandlerMapping extends RequestMappingInfoHandlerMappi * Return the file extensions to use for suffix pattern matching. */ public List<String> getFileExtensions() { - return this.fileExtensions; + return this.config.getFileExtensions(); } @@ -183,21 +182,31 @@ public class RequestMappingHandlerMapping extends RequestMappingInfoHandlerMappi */ @Override protected RequestMappingInfo getMappingForMethod(Method method, Class<?> handlerType) { - RequestMappingInfo info = null; - RequestMapping methodAnnotation = AnnotationUtils.findAnnotation(method, RequestMapping.class); - if (methodAnnotation != null) { - RequestCondition<?> methodCondition = getCustomMethodCondition(method); - info = createRequestMappingInfo(methodAnnotation, methodCondition); - RequestMapping typeAnnotation = AnnotationUtils.findAnnotation(handlerType, RequestMapping.class); - if (typeAnnotation != null) { - RequestCondition<?> typeCondition = getCustomTypeCondition(handlerType); - info = createRequestMappingInfo(typeAnnotation, typeCondition).combine(info); + RequestMappingInfo info = createRequestMappingInfo(method); + if (info != null) { + RequestMappingInfo typeInfo = createRequestMappingInfo(handlerType); + if (typeInfo != null) { + info = typeInfo.combine(info); } } return info; } /** + * Delegates to {@link #createRequestMappingInfo(RequestMapping, RequestCondition)}, + * supplying the appropriate custom {@link RequestCondition} depending on whether + * the supplied {@code annotatedElement} is a class or method. + * @see #getCustomTypeCondition(Class) + * @see #getCustomMethodCondition(Method) + */ + private RequestMappingInfo createRequestMappingInfo(AnnotatedElement element) { + RequestMapping requestMapping = AnnotatedElementUtils.findMergedAnnotation(element, RequestMapping.class); + RequestCondition<?> condition = (element instanceof Class<?> ? + getCustomTypeCondition((Class<?>) element) : getCustomMethodCondition((Method) element)); + return (requestMapping != null ? createRequestMappingInfo(requestMapping, condition) : null); + } + + /** * Provide a custom type-level request condition. * The custom {@link RequestCondition} can be of any type so long as the * same condition type is returned from all calls to this method in order @@ -229,20 +238,24 @@ public class RequestMappingHandlerMapping extends RequestMappingInfoHandlerMappi /** * Create a {@link RequestMappingInfo} from the supplied - * {@link RequestMapping @RequestMapping} annotation. + * {@link RequestMapping @RequestMapping} annotation, which is either + * a directly declared annotation, a meta-annotation, or the synthesized + * result of merging annotation attributes within an annotation hierarchy. */ - protected RequestMappingInfo createRequestMappingInfo(RequestMapping annotation, RequestCondition<?> customCondition) { - String[] patterns = resolveEmbeddedValuesInPatterns(annotation.value()); - return new RequestMappingInfo( - annotation.name(), - new PatternsRequestCondition(patterns, getUrlPathHelper(), getPathMatcher(), - this.useSuffixPatternMatch, this.useTrailingSlashMatch, this.fileExtensions), - new RequestMethodsRequestCondition(annotation.method()), - new ParamsRequestCondition(annotation.params()), - new HeadersRequestCondition(annotation.headers()), - new ConsumesRequestCondition(annotation.consumes(), annotation.headers()), - new ProducesRequestCondition(annotation.produces(), annotation.headers(), this.contentNegotiationManager), - customCondition); + protected RequestMappingInfo createRequestMappingInfo( + RequestMapping requestMapping, RequestCondition<?> customCondition) { + + return RequestMappingInfo + .paths(resolveEmbeddedValuesInPatterns(requestMapping.path())) + .methods(requestMapping.method()) + .params(requestMapping.params()) + .headers(requestMapping.headers()) + .consumes(requestMapping.consumes()) + .produces(requestMapping.produces()) + .mappingName(requestMapping.name()) + .customCondition(customCondition) + .options(this.config) + .build(); } /** @@ -262,4 +275,72 @@ public class RequestMappingHandlerMapping extends RequestMappingInfoHandlerMappi } } + @Override + protected CorsConfiguration initCorsConfiguration(Object handler, Method method, RequestMappingInfo mappingInfo) { + HandlerMethod handlerMethod = createHandlerMethod(handler, method); + CrossOrigin typeAnnotation = AnnotatedElementUtils.findMergedAnnotation(handlerMethod.getBeanType(), CrossOrigin.class); + CrossOrigin methodAnnotation = AnnotatedElementUtils.findMergedAnnotation(method, CrossOrigin.class); + + if (typeAnnotation == null && methodAnnotation == null) { + return null; + } + + CorsConfiguration config = new CorsConfiguration(); + updateCorsConfig(config, typeAnnotation); + updateCorsConfig(config, methodAnnotation); + + if (CollectionUtils.isEmpty(config.getAllowedOrigins())) { + config.setAllowedOrigins(Arrays.asList(CrossOrigin.DEFAULT_ORIGINS)); + } + if (CollectionUtils.isEmpty(config.getAllowedMethods())) { + for (RequestMethod allowedMethod : mappingInfo.getMethodsCondition().getMethods()) { + config.addAllowedMethod(allowedMethod.name()); + } + } + if (CollectionUtils.isEmpty(config.getAllowedHeaders())) { + config.setAllowedHeaders(Arrays.asList(CrossOrigin.DEFAULT_ALLOWED_HEADERS)); + } + if (config.getAllowCredentials() == null) { + config.setAllowCredentials(CrossOrigin.DEFAULT_ALLOW_CREDENTIALS); + } + if (config.getMaxAge() == null) { + config.setMaxAge(CrossOrigin.DEFAULT_MAX_AGE); + } + return config; + } + + private void updateCorsConfig(CorsConfiguration config, CrossOrigin annotation) { + if (annotation == null) { + return; + } + for (String origin : annotation.origins()) { + config.addAllowedOrigin(origin); + } + for (RequestMethod method : annotation.methods()) { + config.addAllowedMethod(method.name()); + } + for (String header : annotation.allowedHeaders()) { + config.addAllowedHeader(header); + } + for (String header : annotation.exposedHeaders()) { + config.addExposedHeader(header); + } + + String allowCredentials = annotation.allowCredentials(); + if ("true".equalsIgnoreCase(allowCredentials)) { + config.setAllowCredentials(true); + } + else if ("false".equalsIgnoreCase(allowCredentials)) { + config.setAllowCredentials(false); + } + else if (!allowCredentials.isEmpty()) { + throw new IllegalStateException("@CrossOrigin's allowCredentials value must be \"true\", \"false\", " + + "or an empty string (\"\"); current value is [" + allowCredentials + "]."); + } + + if (annotation.maxAge() >= 0 && config.getMaxAge() == null) { + config.setMaxAge(annotation.maxAge()); + } + } + } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestPartMethodArgumentResolver.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestPartMethodArgumentResolver.java index 6bd1c5a4..ba34c2e4 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestPartMethodArgumentResolver.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestPartMethodArgumentResolver.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * Copyright 2002-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,6 +19,7 @@ package org.springframework.web.servlet.mvc.method.annotation; import java.util.ArrayList; import java.util.Collection; import java.util.List; +import java.util.Optional; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.Part; @@ -26,6 +27,7 @@ import org.springframework.core.GenericCollectionTypeResolver; import org.springframework.core.MethodParameter; import org.springframework.http.HttpInputMessage; import org.springframework.http.converter.HttpMessageConverter; +import org.springframework.lang.UsesJava8; import org.springframework.util.Assert; import org.springframework.validation.BindingResult; import org.springframework.web.bind.MethodArgumentNotValidException; @@ -72,10 +74,23 @@ import org.springframework.web.util.WebUtils; */ public class RequestPartMethodArgumentResolver extends AbstractMessageConverterMethodArgumentResolver { + /** + * Basic constructor with converters only. + */ public RequestPartMethodArgumentResolver(List<HttpMessageConverter<?>> messageConverters) { super(messageConverters); } + /** + * Constructor with converters and {@code Request~} and + * {@code ResponseBodyAdvice}. + */ + public RequestPartMethodArgumentResolver(List<HttpMessageConverter<?>> messageConverters, + List<Object> requestResponseBodyAdvice) { + + super(messageConverters, requestResponseBodyAdvice); + } + /** * Supports the following: @@ -94,7 +109,7 @@ public class RequestPartMethodArgumentResolver extends AbstractMessageConverterM if (parameter.hasParameterAnnotation(RequestParam.class)){ return false; } - else if (MultipartFile.class.equals(parameter.getParameterType())) { + else if (MultipartFile.class == parameter.getParameterType()) { return true; } else if ("javax.servlet.http.Part".equals(parameter.getParameterType().getName())) { @@ -112,14 +127,21 @@ public class RequestPartMethodArgumentResolver extends AbstractMessageConverterM HttpServletRequest servletRequest = request.getNativeRequest(HttpServletRequest.class); assertIsMultipartRequest(servletRequest); - MultipartHttpServletRequest multipartRequest = WebUtils.getNativeRequest(servletRequest, MultipartHttpServletRequest.class); String partName = getPartName(parameter); + Class<?> paramType = parameter.getParameterType(); + boolean optional = paramType.getName().equals("java.util.Optional"); + if (optional) { + parameter = new MethodParameter(parameter); + parameter.increaseNestingLevel(); + paramType = parameter.getNestedParameterType(); + } + Object arg; - if (MultipartFile.class.equals(parameter.getParameterType())) { + if (MultipartFile.class == paramType) { Assert.notNull(multipartRequest, "Expected MultipartHttpServletRequest: is a MultipartResolver configured?"); arg = multipartRequest.getFile(partName); } @@ -132,7 +154,7 @@ public class RequestPartMethodArgumentResolver extends AbstractMessageConverterM List<MultipartFile> files = multipartRequest.getFiles(partName); arg = files.toArray(new MultipartFile[files.size()]); } - else if ("javax.servlet.http.Part".equals(parameter.getParameterType().getName())) { + else if ("javax.servlet.http.Part".equals(paramType.getName())) { assertIsMultipartRequest(servletRequest); arg = servletRequest.getPart(partName); } @@ -147,7 +169,7 @@ public class RequestPartMethodArgumentResolver extends AbstractMessageConverterM else { try { HttpInputMessage inputMessage = new RequestPartServletServerHttpRequest(servletRequest, partName); - arg = readWithMessageConverters(inputMessage, parameter, parameter.getParameterType()); + arg = readWithMessageConverters(inputMessage, parameter, parameter.getNestedGenericParameterType()); WebDataBinder binder = binderFactory.createBinder(request, arg, partName); if (arg != null) { validateIfApplicable(binder, parameter); @@ -163,12 +185,15 @@ public class RequestPartMethodArgumentResolver extends AbstractMessageConverterM } } - RequestPart ann = parameter.getParameterAnnotation(RequestPart.class); - boolean isRequired = (ann == null || ann.required()); + RequestPart requestPart = parameter.getParameterAnnotation(RequestPart.class); + boolean isRequired = ((requestPart == null || requestPart.required()) && !optional); if (arg == null && isRequired) { throw new MissingServletRequestPartException(partName); } + if (optional) { + arg = OptionalResolver.resolveValue(arg); + } return arg; } @@ -181,8 +206,8 @@ public class RequestPartMethodArgumentResolver extends AbstractMessageConverterM } private String getPartName(MethodParameter methodParam) { - RequestPart ann = methodParam.getParameterAnnotation(RequestPart.class); - String partName = (ann != null ? ann.value() : ""); + RequestPart requestPart = methodParam.getParameterAnnotation(RequestPart.class); + String partName = (requestPart != null ? requestPart.name() : ""); if (partName.length() == 0) { partName = methodParam.getParameterName(); if (partName == null) { @@ -196,12 +221,12 @@ public class RequestPartMethodArgumentResolver extends AbstractMessageConverterM private boolean isMultipartFileCollection(MethodParameter methodParam) { Class<?> collectionType = getCollectionParameterType(methodParam); - return MultipartFile.class.equals(collectionType); + return MultipartFile.class == collectionType; } private boolean isMultipartFileArray(MethodParameter methodParam) { Class<?> paramType = methodParam.getNestedParameterType().getComponentType(); - return MultipartFile.class.equals(paramType); + return MultipartFile.class == paramType; } private boolean isPartCollection(MethodParameter methodParam) { @@ -216,7 +241,7 @@ public class RequestPartMethodArgumentResolver extends AbstractMessageConverterM private Class<?> getCollectionParameterType(MethodParameter methodParam) { Class<?> paramType = methodParam.getNestedParameterType(); - if (Collection.class.equals(paramType) || List.class.isAssignableFrom(paramType)){ + if (Collection.class == paramType || List.class.isAssignableFrom(paramType)){ Class<?> valueType = GenericCollectionTypeResolver.getCollectionParameterType(methodParam); if (valueType != null) { return valueType; @@ -237,4 +262,16 @@ public class RequestPartMethodArgumentResolver extends AbstractMessageConverterM } } + + /** + * Inner class to avoid hard-coded dependency on Java 8 Optional type... + */ + @UsesJava8 + private static class OptionalResolver { + + public static Object resolveValue(Object value) { + return Optional.ofNullable(value); + } + } + } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestResponseBodyAdviceChain.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestResponseBodyAdviceChain.java new file mode 100644 index 00000000..d41295cf --- /dev/null +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestResponseBodyAdviceChain.java @@ -0,0 +1,177 @@ +/* + * Copyright 2002-2015 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.web.servlet.mvc.method.annotation; + +import java.io.IOException; +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import org.springframework.core.MethodParameter; +import org.springframework.http.HttpInputMessage; +import org.springframework.http.MediaType; +import org.springframework.http.converter.HttpMessageConverter; +import org.springframework.http.server.ServerHttpRequest; +import org.springframework.http.server.ServerHttpResponse; +import org.springframework.util.CollectionUtils; +import org.springframework.web.method.ControllerAdviceBean; + + +/** + * Invokes {@link RequestBodyAdvice} and {@link ResponseBodyAdvice} where each + * instance may be (and is most likely) wrapped with + * {@link org.springframework.web.method.ControllerAdviceBean ControllerAdviceBean}. + * + * @author Rossen Stoyanchev + * @since 4.2 + */ +class RequestResponseBodyAdviceChain implements RequestBodyAdvice, ResponseBodyAdvice<Object> { + + private final List<Object> requestBodyAdvice = new ArrayList<Object>(4); + + private final List<Object> responseBodyAdvice = new ArrayList<Object>(4); + + + /** + * Create an instance from a list of objects that are either of type + * {@code ControllerAdviceBean} or {@code RequestBodyAdvice}. + */ + public RequestResponseBodyAdviceChain(List<Object> requestResponseBodyAdvice) { + initAdvice(requestResponseBodyAdvice); + } + + private void initAdvice(List<Object> requestResponseBodyAdvice) { + if (requestResponseBodyAdvice == null) { + return; + } + for (Object advice : requestResponseBodyAdvice) { + Class<?> beanType = (advice instanceof ControllerAdviceBean ? + ((ControllerAdviceBean) advice).getBeanType() : advice.getClass()); + if (RequestBodyAdvice.class.isAssignableFrom(beanType)) { + this.requestBodyAdvice.add(advice); + } + else if (ResponseBodyAdvice.class.isAssignableFrom(beanType)) { + this.responseBodyAdvice.add(advice); + } + } + } + + private List<Object> getAdvice(Class<?> adviceType) { + if (RequestBodyAdvice.class == adviceType) { + return this.requestBodyAdvice; + } + else if (ResponseBodyAdvice.class == adviceType) { + return this.responseBodyAdvice; + } + else { + throw new IllegalArgumentException("Unexpected adviceType: " + adviceType); + } + } + + + @Override + public boolean supports(MethodParameter param, Type type, Class<? extends HttpMessageConverter<?>> converterType) { + throw new UnsupportedOperationException("Not implemented"); + } + + @Override + public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) { + throw new UnsupportedOperationException("Not implemented"); + } + + @Override + public Object handleEmptyBody(Object body, HttpInputMessage inputMessage, MethodParameter parameter, + Type targetType, Class<? extends HttpMessageConverter<?>> converterType) { + + for (RequestBodyAdvice advice : getMatchingAdvice(parameter, RequestBodyAdvice.class)) { + if (advice.supports(parameter, targetType, converterType)) { + body = advice.handleEmptyBody(body, inputMessage, parameter, targetType, converterType); + } + } + return body; + } + + @Override + public HttpInputMessage beforeBodyRead(HttpInputMessage request, MethodParameter parameter, + Type targetType, Class<? extends HttpMessageConverter<?>> converterType) throws IOException { + + for (RequestBodyAdvice advice : getMatchingAdvice(parameter, RequestBodyAdvice.class)) { + if (advice.supports(parameter, targetType, converterType)) { + request = advice.beforeBodyRead(request, parameter, targetType, converterType); + } + } + return request; + } + + @Override + public Object afterBodyRead(Object body, HttpInputMessage inputMessage, MethodParameter parameter, + Type targetType, Class<? extends HttpMessageConverter<?>> converterType) { + + for (RequestBodyAdvice advice : getMatchingAdvice(parameter, RequestBodyAdvice.class)) { + if (advice.supports(parameter, targetType, converterType)) { + body = advice.afterBodyRead(body, inputMessage, parameter, targetType, converterType); + } + } + return body; + } + + @Override + public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType contentType, + Class<? extends HttpMessageConverter<?>> converterType, + ServerHttpRequest request, ServerHttpResponse response) { + + return processBody(body, returnType, contentType, converterType, request, response); + } + + @SuppressWarnings("unchecked") + private <T> Object processBody(Object body, MethodParameter returnType, MediaType contentType, + Class<? extends HttpMessageConverter<?>> converterType, + ServerHttpRequest request, ServerHttpResponse response) { + + for (ResponseBodyAdvice<?> advice : getMatchingAdvice(returnType, ResponseBodyAdvice.class)) { + if (advice.supports(returnType, converterType)) { + body = ((ResponseBodyAdvice<T>) advice).beforeBodyWrite((T) body, returnType, + contentType, converterType, request, response); + } + } + return body; + } + + @SuppressWarnings("unchecked") + private <A> List<A> getMatchingAdvice(MethodParameter parameter, Class<? extends A> adviceType) { + List<Object> availableAdvice = getAdvice(adviceType); + if (CollectionUtils.isEmpty(availableAdvice)) { + return Collections.emptyList(); + } + List<A> result = new ArrayList<A>(availableAdvice.size()); + for (Object advice : availableAdvice) { + if (advice instanceof ControllerAdviceBean) { + ControllerAdviceBean adviceBean = (ControllerAdviceBean) advice; + if (!adviceBean.isApplicableToBeanType(parameter.getContainingClass())) { + continue; + } + advice = adviceBean.resolveBean(); + } + if (adviceType.isAssignableFrom(advice.getClass())) { + result.add((A) advice); + } + } + return result; + } + +} diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestResponseBodyMethodProcessor.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestResponseBodyMethodProcessor.java index fccf6ee6..bc187962 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestResponseBodyMethodProcessor.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestResponseBodyMethodProcessor.java @@ -17,20 +17,16 @@ package org.springframework.web.servlet.mvc.method.annotation; import java.io.IOException; -import java.io.InputStream; -import java.io.PushbackInputStream; import java.lang.reflect.Type; import java.util.List; -import javax.servlet.ServletInputStream; import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletRequestWrapper; import org.springframework.core.Conventions; import org.springframework.core.MethodParameter; import org.springframework.core.annotation.AnnotationUtils; -import org.springframework.http.HttpInputMessage; import org.springframework.http.converter.HttpMessageConverter; import org.springframework.http.converter.HttpMessageNotReadableException; +import org.springframework.http.converter.HttpMessageNotWritableException; import org.springframework.http.server.ServletServerHttpRequest; import org.springframework.validation.BindingResult; import org.springframework.web.HttpMediaTypeNotAcceptableException; @@ -61,20 +57,47 @@ import org.springframework.web.servlet.mvc.support.DefaultHandlerExceptionResolv */ public class RequestResponseBodyMethodProcessor extends AbstractMessageConverterMethodProcessor { - public RequestResponseBodyMethodProcessor(List<HttpMessageConverter<?>> messageConverters) { - super(messageConverters); + /** + * Basic constructor with converters only. Suitable for resolving + * {@code @RequestBody}. For handling {@code @ResponseBody} consider also + * providing a {@code ContentNegotiationManager}. + */ + public RequestResponseBodyMethodProcessor(List<HttpMessageConverter<?>> converters) { + super(converters); } - public RequestResponseBodyMethodProcessor(List<HttpMessageConverter<?>> messageConverters, - ContentNegotiationManager contentNegotiationManager) { + /** + * Basic constructor with converters and {@code ContentNegotiationManager}. + * Suitable for resolving {@code @RequestBody} and handling + * {@code @ResponseBody} without {@code Request~} or + * {@code ResponseBodyAdvice}. + */ + public RequestResponseBodyMethodProcessor(List<HttpMessageConverter<?>> converters, + ContentNegotiationManager manager) { - super(messageConverters, contentNegotiationManager); + super(converters, manager); } - public RequestResponseBodyMethodProcessor(List<HttpMessageConverter<?>> messageConverters, - ContentNegotiationManager contentNegotiationManager, List<Object> responseBodyAdvice) { + /** + * Complete constructor for resolving {@code @RequestBody} method arguments. + * For handling {@code @ResponseBody} consider also providing a + * {@code ContentNegotiationManager}. + * @since 4.2 + */ + public RequestResponseBodyMethodProcessor(List<HttpMessageConverter<?>> converters, + List<Object> requestResponseBodyAdvice) { + + super(converters, null, requestResponseBodyAdvice); + } + + /** + * Complete constructor for resolving {@code @RequestBody} and handling + * {@code @ResponseBody}. + */ + public RequestResponseBodyMethodProcessor(List<HttpMessageConverter<?>> converters, + ContentNegotiationManager manager, List<Object> requestResponseBodyAdvice) { - super(messageConverters, contentNegotiationManager, responseBodyAdvice); + super(converters, manager, requestResponseBodyAdvice); } @@ -101,6 +124,7 @@ public class RequestResponseBodyMethodProcessor extends AbstractMessageConverter Object arg = readWithMessageConverters(webRequest, parameter, parameter.getGenericParameterType()); String name = Conventions.getVariableNameForParameter(parameter); + WebDataBinder binder = binderFactory.createBinder(webRequest, arg, name); if (arg != null) { validateIfApplicable(binder, parameter); @@ -109,75 +133,31 @@ public class RequestResponseBodyMethodProcessor extends AbstractMessageConverter } } mavContainer.addAttribute(BindingResult.MODEL_KEY_PREFIX + name, binder.getBindingResult()); + return arg; } @Override protected <T> Object readWithMessageConverters(NativeWebRequest webRequest, MethodParameter methodParam, - Type paramType) throws IOException, HttpMediaTypeNotSupportedException { + Type paramType) throws IOException, HttpMediaTypeNotSupportedException, HttpMessageNotReadableException { HttpServletRequest servletRequest = webRequest.getNativeRequest(HttpServletRequest.class); - HttpInputMessage inputMessage = new ServletServerHttpRequest(servletRequest); + ServletServerHttpRequest inputMessage = new ServletServerHttpRequest(servletRequest); - InputStream inputStream = inputMessage.getBody(); - if (inputStream == null) { - return handleEmptyBody(methodParam); - } - else if (inputStream.markSupported()) { - inputStream.mark(1); - if (inputStream.read() == -1) { - return handleEmptyBody(methodParam); - } - inputStream.reset(); - } - else { - final PushbackInputStream pushbackInputStream = new PushbackInputStream(inputStream); - int b = pushbackInputStream.read(); - if (b == -1) { - return handleEmptyBody(methodParam); - } - else { - pushbackInputStream.unread(b); + Object arg = readWithMessageConverters(inputMessage, methodParam, paramType); + if (arg == null) { + if (methodParam.getParameterAnnotation(RequestBody.class).required()) { + throw new HttpMessageNotReadableException("Required request body is missing: " + + methodParam.getMethod().toGenericString()); } - HttpServletRequest wrappedRequest = new HttpServletRequestWrapper(servletRequest) { - @Override - public ServletInputStream getInputStream() throws IOException { - return new ServletInputStream() { - @Override - public int read() throws IOException { - return pushbackInputStream.read(); - } - @Override - public void close() throws IOException { - super.close(); - pushbackInputStream.close(); - } - }; - } - }; - inputMessage = new ServletServerHttpRequest(wrappedRequest) { - @Override - public InputStream getBody() { - // Form POST should not get here - return pushbackInputStream; - } - }; } - - return super.readWithMessageConverters(inputMessage, methodParam, paramType); - } - - private Object handleEmptyBody(MethodParameter param) { - if (param.getParameterAnnotation(RequestBody.class).required()) { - throw new HttpMessageNotReadableException("Required request body content is missing: " + param); - } - return null; + return arg; } @Override public void handleReturnValue(Object returnValue, MethodParameter returnType, ModelAndViewContainer mavContainer, NativeWebRequest webRequest) - throws IOException, HttpMediaTypeNotAcceptableException { + throws IOException, HttpMediaTypeNotAcceptableException, HttpMessageNotWritableException { mavContainer.setRequestHandled(true); diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ResponseBodyAdvice.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ResponseBodyAdvice.java index 01ae9cd0..b9c8c83d 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ResponseBodyAdvice.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ResponseBodyAdvice.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2014 the original author or authors. + * Copyright 2002-2015 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -24,7 +24,7 @@ import org.springframework.http.server.ServerHttpResponse; /** * Allows customizing the response after the execution of an {@code @ResponseBody} - * or an {@code ResponseEntity} controller method but before the body is written + * or a {@code ResponseEntity} controller method but before the body is written * with an {@code HttpMessageConverter}. * * <p>Implementations may be may be registered directly with diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ResponseBodyAdviceChain.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ResponseBodyAdviceChain.java deleted file mode 100644 index 4b17fce7..00000000 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ResponseBodyAdviceChain.java +++ /dev/null @@ -1,78 +0,0 @@ -/* - * Copyright 2002-2015 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.web.servlet.mvc.method.annotation; - -import java.util.List; - -import org.springframework.core.MethodParameter; -import org.springframework.http.MediaType; -import org.springframework.http.converter.HttpMessageConverter; -import org.springframework.http.server.ServerHttpRequest; -import org.springframework.http.server.ServerHttpResponse; -import org.springframework.util.CollectionUtils; -import org.springframework.web.method.ControllerAdviceBean; - -/** - * Invokes a a list of {@link ResponseBodyAdvice} beans. - * - * @author Rossen Stoyanchev - * @since 4.1 - */ -class ResponseBodyAdviceChain { - - private final List<Object> advice; - - - public ResponseBodyAdviceChain(List<Object> advice) { - this.advice = advice; - } - - - public boolean hasAdvice() { - return !CollectionUtils.isEmpty(this.advice); - } - - @SuppressWarnings("unchecked") - public <T> T invoke(T body, MethodParameter returnType, - MediaType selectedContentType, Class<? extends HttpMessageConverter<?>> selectedConverterType, - ServerHttpRequest request, ServerHttpResponse response) { - - if (this.advice != null) { - for (Object advice : this.advice) { - if (advice instanceof ControllerAdviceBean) { - ControllerAdviceBean adviceBean = (ControllerAdviceBean) advice; - if (!adviceBean.isApplicableToBeanType(returnType.getContainingClass())) { - continue; - } - advice = adviceBean.resolveBean(); - } - if (advice instanceof ResponseBodyAdvice) { - ResponseBodyAdvice<T> typedAdvice = (ResponseBodyAdvice<T>) advice; - if (typedAdvice.supports(returnType, selectedConverterType)) { - body = typedAdvice.beforeBodyWrite(body, returnType, - selectedContentType, selectedConverterType, request, response); - } - } - else { - throw new IllegalStateException("Expected ResponseBodyAdvice: " + advice); - } - } - } - return body; - } - -} diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ResponseBodyEmitter.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ResponseBodyEmitter.java new file mode 100644 index 00000000..18d96b07 --- /dev/null +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ResponseBodyEmitter.java @@ -0,0 +1,286 @@ +/* + * Copyright 2002-2015 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.web.servlet.mvc.method.annotation; + +import java.io.IOException; +import java.util.LinkedHashSet; +import java.util.Set; + +import org.springframework.http.MediaType; +import org.springframework.http.server.ServerHttpResponse; +import org.springframework.util.Assert; + +/** + * A controller method return value type for asynchronous request processing + * where one or more objects are written to the response. + * + * <p>While {@link org.springframework.web.context.request.async.DeferredResult} + * is used to produce a single result, a {@code ResponseBodyEmitter} can be used + * to send multiple objects where each object is written with a compatible + * {@link org.springframework.http.converter.HttpMessageConverter}. + * + * <p>Supported as a return type on its own as well as within a + * {@link org.springframework.http.ResponseEntity}. + * + * <pre> + * @RequestMapping(value="/stream", method=RequestMethod.GET) + * public ResponseBodyEmitter handle() { + * ResponseBodyEmitter emitter = new ResponseBodyEmitter(); + * // Pass the emitter to another component... + * return emitter; + * } + * + * // in another thread + * emitter.send(foo1); + * + * // and again + * emitter.send(foo2); + * + * // and done + * emitter.complete(); + * </pre> + * + * @author Rossen Stoyanchev + * @author Juergen Hoeller + * @since 4.2 + */ +public class ResponseBodyEmitter { + + private final Long timeout; + + private final Set<DataWithMediaType> earlySendAttempts = new LinkedHashSet<DataWithMediaType>(8); + + private Handler handler; + + private boolean complete; + + private Throwable failure; + + private final DefaultCallback timeoutCallback = new DefaultCallback(); + + private final DefaultCallback completionCallback = new DefaultCallback(); + + + /** + * Create a new ResponseBodyEmitter instance. + */ + public ResponseBodyEmitter() { + this.timeout = null; + } + + /** + * Create a ResponseBodyEmitter with a custom timeout value. + * <p>By default not set in which case the default configured in the MVC + * Java Config or the MVC namespace is used, or if that's not set, then the + * timeout depends on the default of the underlying server. + * @param timeout timeout value in milliseconds + */ + public ResponseBodyEmitter(Long timeout) { + this.timeout = timeout; + } + + + /** + * Return the configured timeout value, if any. + */ + public Long getTimeout() { + return this.timeout; + } + + + synchronized void initialize(Handler handler) throws IOException { + this.handler = handler; + + for (DataWithMediaType sendAttempt : this.earlySendAttempts) { + sendInternal(sendAttempt.getData(), sendAttempt.getMediaType()); + } + this.earlySendAttempts.clear(); + + if (this.complete) { + if (this.failure != null) { + this.handler.completeWithError(this.failure); + } + else { + this.handler.complete(); + } + } + else { + this.handler.onTimeout(this.timeoutCallback); + this.handler.onCompletion(this.completionCallback); + } + } + + /** + * Invoked after the response is updated with the status code and headers, + * if the ResponseBodyEmitter is wrapped in a ResponseEntity, but before the + * response is committed, i.e. before the response body has been written to. + * <p>The default implementation is empty. + */ + protected void extendResponse(ServerHttpResponse outputMessage) { + } + + /** + * Write the given object to the response. + * <p>If any exception occurs a dispatch is made back to the app server where + * Spring MVC will pass the exception through its exception handling mechanism. + * @param object the object to write + * @throws IOException raised when an I/O error occurs + * @throws java.lang.IllegalStateException wraps any other errors + */ + public void send(Object object) throws IOException { + send(object, null); + } + + /** + * Write the given object to the response also using a MediaType hint. + * <p>If any exception occurs a dispatch is made back to the app server where + * Spring MVC will pass the exception through its exception handling mechanism. + * @param object the object to write + * @param mediaType a MediaType hint for selecting an HttpMessageConverter + * @throws IOException raised when an I/O error occurs + * @throws java.lang.IllegalStateException wraps any other errors + */ + public synchronized void send(Object object, MediaType mediaType) throws IOException { + Assert.state(!this.complete, "ResponseBodyEmitter is already set complete"); + sendInternal(object, mediaType); + } + + private void sendInternal(Object object, MediaType mediaType) throws IOException { + if (object != null) { + if (this.handler != null) { + try { + this.handler.send(object, mediaType); + } + catch (IOException ex) { + completeWithError(ex); + throw ex; + } + catch (Throwable ex) { + completeWithError(ex); + throw new IllegalStateException("Failed to send " + object, ex); + } + } + else { + this.earlySendAttempts.add(new DataWithMediaType(object, mediaType)); + } + } + } + + /** + * Complete request processing. + * <p>A dispatch is made into the app server where Spring MVC completes + * asynchronous request processing. + */ + public synchronized void complete() { + this.complete = true; + if (this.handler != null) { + this.handler.complete(); + } + } + + /** + * Complete request processing with an error. + * <p>A dispatch is made into the app server where Spring MVC will pass the + * exception through its exception handling mechanism. + */ + public synchronized void completeWithError(Throwable ex) { + this.complete = true; + this.failure = ex; + if (this.handler != null) { + this.handler.completeWithError(ex); + } + } + + /** + * Register code to invoke when the async request times out. This method is + * called from a container thread when an async request times out. + */ + public synchronized void onTimeout(Runnable callback) { + this.timeoutCallback.setDelegate(callback); + } + + /** + * Register code to invoke when the async request completes. This method is + * called from a container thread when an async request completed for any + * reason including timeout and network error. This method is useful for + * detecting that a {@code ResponseBodyEmitter} instance is no longer usable. + */ + public synchronized void onCompletion(Runnable callback) { + this.completionCallback.setDelegate(callback); + } + + + /** + * Handle sent objects and complete request processing. + */ + interface Handler { + + void send(Object data, MediaType mediaType) throws IOException; + + void complete(); + + void completeWithError(Throwable failure); + + void onTimeout(Runnable callback); + + void onCompletion(Runnable callback); + } + + + /** + * A simple holder of data to be written along with a MediaType hint for + * selecting a message converter to write with. + */ + public static class DataWithMediaType { + + private final Object data; + + private final MediaType mediaType; + + public DataWithMediaType(Object data, MediaType mediaType) { + this.data = data; + this.mediaType = mediaType; + } + + public Object getData() { + return this.data; + } + + public MediaType getMediaType() { + return this.mediaType; + } + } + + + private class DefaultCallback implements Runnable { + + private Runnable delegate; + + public void setDelegate(Runnable delegate) { + this.delegate = delegate; + } + + @Override + public void run() { + ResponseBodyEmitter.this.complete = true; + if (this.delegate != null) { + this.delegate.run(); + } + } + } + +} diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ResponseBodyEmitterReturnValueHandler.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ResponseBodyEmitterReturnValueHandler.java new file mode 100644 index 00000000..9a216058 --- /dev/null +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ResponseBodyEmitterReturnValueHandler.java @@ -0,0 +1,232 @@ +/* + * Copyright 2002-2015 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.web.servlet.mvc.method.annotation; + +import java.io.IOException; +import java.io.OutputStream; +import java.util.List; + +import javax.servlet.ServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.core.MethodParameter; +import org.springframework.core.ResolvableType; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.http.converter.HttpMessageConverter; +import org.springframework.http.server.ServerHttpResponse; +import org.springframework.http.server.ServletServerHttpResponse; +import org.springframework.util.Assert; +import org.springframework.web.context.request.NativeWebRequest; +import org.springframework.web.context.request.async.DeferredResult; +import org.springframework.web.context.request.async.WebAsyncUtils; +import org.springframework.web.filter.ShallowEtagHeaderFilter; +import org.springframework.web.method.support.AsyncHandlerMethodReturnValueHandler; +import org.springframework.web.method.support.ModelAndViewContainer; + +/** + * Supports return values of type {@link ResponseBodyEmitter} and also + * {@code ResponseEntity<ResponseBodyEmitter>}. + * + * @author Rossen Stoyanchev + * @since 4.2 + */ +public class ResponseBodyEmitterReturnValueHandler implements AsyncHandlerMethodReturnValueHandler { + + private static final Log logger = LogFactory.getLog(ResponseBodyEmitterReturnValueHandler.class); + + private final List<HttpMessageConverter<?>> messageConverters; + + + public ResponseBodyEmitterReturnValueHandler(List<HttpMessageConverter<?>> messageConverters) { + Assert.notEmpty(messageConverters, "'messageConverters' must not be empty"); + this.messageConverters = messageConverters; + } + + + @Override + public boolean supportsReturnType(MethodParameter returnType) { + if (ResponseBodyEmitter.class.isAssignableFrom(returnType.getParameterType())) { + return true; + } + else if (ResponseEntity.class.isAssignableFrom(returnType.getParameterType())) { + Class<?> bodyType = ResolvableType.forMethodParameter(returnType).getGeneric(0).resolve(); + return (bodyType != null && ResponseBodyEmitter.class.isAssignableFrom(bodyType)); + } + return false; + } + + @Override + public boolean isAsyncReturnValue(Object returnValue, MethodParameter returnType) { + if (returnValue != null) { + if (returnValue instanceof ResponseBodyEmitter) { + return true; + } + else if (returnValue instanceof ResponseEntity) { + Object body = ((ResponseEntity) returnValue).getBody(); + return (body != null && body instanceof ResponseBodyEmitter); + } + } + return false; + } + + @Override + public void handleReturnValue(Object returnValue, MethodParameter returnType, + ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception { + + if (returnValue == null) { + mavContainer.setRequestHandled(true); + return; + } + + HttpServletResponse response = webRequest.getNativeResponse(HttpServletResponse.class); + ServerHttpResponse outputMessage = new ServletServerHttpResponse(response); + + if (ResponseEntity.class.isAssignableFrom(returnValue.getClass())) { + ResponseEntity<?> responseEntity = (ResponseEntity<?>) returnValue; + outputMessage.setStatusCode(responseEntity.getStatusCode()); + outputMessage.getHeaders().putAll(responseEntity.getHeaders()); + returnValue = responseEntity.getBody(); + if (returnValue == null) { + mavContainer.setRequestHandled(true); + return; + } + } + + ServletRequest request = webRequest.getNativeRequest(ServletRequest.class); + ShallowEtagHeaderFilter.disableContentCaching(request); + + Assert.isInstanceOf(ResponseBodyEmitter.class, returnValue); + ResponseBodyEmitter emitter = (ResponseBodyEmitter) returnValue; + emitter.extendResponse(outputMessage); + + // Commit the response and wrap to ignore further header changes + outputMessage.getBody(); + outputMessage.flush(); + outputMessage = new StreamingServletServerHttpResponse(outputMessage); + + DeferredResult<?> deferredResult = new DeferredResult<Object>(emitter.getTimeout()); + WebAsyncUtils.getAsyncManager(webRequest).startDeferredResultProcessing(deferredResult, mavContainer); + + HttpMessageConvertingHandler handler = new HttpMessageConvertingHandler(outputMessage, deferredResult); + emitter.initialize(handler); + } + + + /** + * ResponseBodyEmitter.Handler that writes with HttpMessageConverter's. + */ + private class HttpMessageConvertingHandler implements ResponseBodyEmitter.Handler { + + private final ServerHttpResponse outputMessage; + + private final DeferredResult<?> deferredResult; + + public HttpMessageConvertingHandler(ServerHttpResponse outputMessage, DeferredResult<?> deferredResult) { + this.outputMessage = outputMessage; + this.deferredResult = deferredResult; + } + + @Override + public void send(Object data, MediaType mediaType) throws IOException { + sendInternal(data, mediaType); + } + + @SuppressWarnings("unchecked") + private <T> void sendInternal(T data, MediaType mediaType) throws IOException { + for (HttpMessageConverter<?> converter : ResponseBodyEmitterReturnValueHandler.this.messageConverters) { + if (converter.canWrite(data.getClass(), mediaType)) { + ((HttpMessageConverter<T>) converter).write(data, mediaType, this.outputMessage); + this.outputMessage.flush(); + if (logger.isDebugEnabled()) { + logger.debug("Written [" + data + "] using [" + converter + "]"); + } + return; + } + } + throw new IllegalArgumentException("No suitable converter for " + data.getClass()); + } + + @Override + public void complete() { + this.deferredResult.setResult(null); + } + + @Override + public void completeWithError(Throwable failure) { + this.deferredResult.setErrorResult(failure); + } + + @Override + public void onTimeout(Runnable callback) { + this.deferredResult.onTimeout(callback); + } + + @Override + public void onCompletion(Runnable callback) { + this.deferredResult.onCompletion(callback); + } + } + + + /** + * Wrap to silently ignore header changes HttpMessageConverter's that would + * otherwise cause HttpHeaders to raise exceptions. + */ + private static class StreamingServletServerHttpResponse implements ServerHttpResponse { + + private final ServerHttpResponse delegate; + + private final HttpHeaders mutableHeaders = new HttpHeaders(); + + public StreamingServletServerHttpResponse(ServerHttpResponse delegate) { + this.delegate = delegate; + this.mutableHeaders.putAll(delegate.getHeaders()); + } + + @Override + public void setStatusCode(HttpStatus status) { + this.delegate.setStatusCode(status); + } + + @Override + public HttpHeaders getHeaders() { + return this.mutableHeaders; + } + + @Override + public OutputStream getBody() throws IOException { + return this.delegate.getBody(); + } + + @Override + public void flush() throws IOException { + this.delegate.flush(); + } + + @Override + public void close() { + this.delegate.close(); + } + } + +} diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ResponseEntityExceptionHandler.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ResponseEntityExceptionHandler.java index 7ac762cc..0be4cb9b 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ResponseEntityExceptionHandler.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ResponseEntityExceptionHandler.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2013 the original author or authors. + * Copyright 2002-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package org.springframework.web.servlet.mvc.method.annotation; import java.util.List; @@ -37,6 +38,7 @@ import org.springframework.web.HttpMediaTypeNotAcceptableException; import org.springframework.web.HttpMediaTypeNotSupportedException; import org.springframework.web.HttpRequestMethodNotSupportedException; import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.MissingPathVariableException; import org.springframework.web.bind.MissingServletRequestParameterException; import org.springframework.web.bind.ServletRequestBindingException; import org.springframework.web.bind.annotation.ControllerAdvice; @@ -45,34 +47,35 @@ import org.springframework.web.context.request.WebRequest; import org.springframework.web.multipart.support.MissingServletRequestPartException; import org.springframework.web.servlet.NoHandlerFoundException; import org.springframework.web.servlet.mvc.multiaction.NoSuchRequestHandlingMethodException; +import org.springframework.web.util.WebUtils; /** * A convenient base class for {@link ControllerAdvice @ControllerAdvice} classes * that wish to provide centralized exception handling across all * {@code @RequestMapping} methods through {@code @ExceptionHandler} methods. * - * <p>This base class provides an {@code @ExceptionHandler} for handling standard - * Spring MVC exceptions that returns a {@code ResponseEntity} to be written with - * {@link HttpMessageConverter message converters}. This is in contrast to + * <p>This base class provides an {@code @ExceptionHandler} method for handling + * internal Spring MVC exceptions. This method returns a {@code ResponseEntity} + * for writing to the response with a {@link HttpMessageConverter message converter}. + * in contrast to * {@link org.springframework.web.servlet.mvc.support.DefaultHandlerExceptionResolver - * DefaultHandlerExceptionResolver} which returns a {@code ModelAndView} instead. + * DefaultHandlerExceptionResolver} which returns a + * {@link org.springframework.web.servlet.ModelAndView ModelAndView}. * - * <p>If there is no need to write error content to the response body, or if using - * view resolution, e.g. {@code ContentNegotiatingViewResolver}, then use - * {@code DefaultHandlerExceptionResolver} instead. + * <p>If there is no need to write error content to the response body, or when + * using view resolution (e.g., via {@code ContentNegotiatingViewResolver}), + * then {@code DefaultHandlerExceptionResolver} is good enough. * * <p>Note that in order for an {@code @ControllerAdvice} sub-class to be * detected, {@link ExceptionHandlerExceptionResolver} must be configured. * * @author Rossen Stoyanchev * @since 3.2 - * + * @see #handleException(Exception, WebRequest) * @see org.springframework.web.servlet.mvc.support.DefaultHandlerExceptionResolver */ public abstract class ResponseEntityExceptionHandler { - protected final Log logger = LogFactory.getLog(getClass()); - /** * Log category to use when no mapped handler is found for a request. * @see #pageNotFoundLogger @@ -80,22 +83,28 @@ public abstract class ResponseEntityExceptionHandler { public static final String PAGE_NOT_FOUND_LOG_CATEGORY = "org.springframework.web.servlet.PageNotFound"; /** - * Additional logger to use when no mapped handler is found for a request. + * Specific logger to use when no mapped handler is found for a request. * @see #PAGE_NOT_FOUND_LOG_CATEGORY */ protected static final Log pageNotFoundLogger = LogFactory.getLog(PAGE_NOT_FOUND_LOG_CATEGORY); + /** + * Common logger for use in subclasses. + */ + protected final Log logger = LogFactory.getLog(getClass()); + /** * Provides handling for standard Spring MVC exceptions. * @param ex the target exception * @param request the current request */ - @ExceptionHandler(value={ + @ExceptionHandler({ NoSuchRequestHandlingMethodException.class, HttpRequestMethodNotSupportedException.class, HttpMediaTypeNotSupportedException.class, HttpMediaTypeNotAcceptableException.class, + MissingPathVariableException.class, MissingServletRequestParameterException.class, ServletRequestBindingException.class, ConversionNotSupportedException.class, @@ -108,9 +117,7 @@ public abstract class ResponseEntityExceptionHandler { NoHandlerFoundException.class }) public final ResponseEntity<Object> handleException(Exception ex, WebRequest request) { - HttpHeaders headers = new HttpHeaders(); - if (ex instanceof NoSuchRequestHandlingMethodException) { HttpStatus status = HttpStatus.NOT_FOUND; return handleNoSuchRequestHandlingMethod((NoSuchRequestHandlingMethodException) ex, headers, status, request); @@ -127,6 +134,10 @@ public abstract class ResponseEntityExceptionHandler { HttpStatus status = HttpStatus.NOT_ACCEPTABLE; return handleHttpMediaTypeNotAcceptable((HttpMediaTypeNotAcceptableException) ex, headers, status, request); } + else if (ex instanceof MissingPathVariableException) { + HttpStatus status = HttpStatus.INTERNAL_SERVER_ERROR; + return handleMissingPathVariable((MissingPathVariableException) ex, headers, status, request); + } else if (ex instanceof MissingServletRequestParameterException) { HttpStatus status = HttpStatus.BAD_REQUEST; return handleMissingServletRequestParameter((MissingServletRequestParameterException) ex, headers, status, request); @@ -176,27 +187,27 @@ public abstract class ResponseEntityExceptionHandler { /** * A single place to customize the response body of all Exception types. - * This method returns {@code null} by default. + * <p>The default implementation sets the {@link WebUtils#ERROR_EXCEPTION_ATTRIBUTE} + * request attribute and creates a {@link ResponseEntity} from the given + * body, headers, and status. * @param ex the exception - * @param body the body to use for the response - * @param headers the headers to be written to the response - * @param status the selected response status + * @param body the body for the response + * @param headers the headers for the response + * @param status the response status * @param request the current request */ protected ResponseEntity<Object> handleExceptionInternal(Exception ex, Object body, HttpHeaders headers, HttpStatus status, WebRequest request) { if (HttpStatus.INTERNAL_SERVER_ERROR.equals(status)) { - request.setAttribute("javax.servlet.error.exception", ex, WebRequest.SCOPE_REQUEST); + request.setAttribute(WebUtils.ERROR_EXCEPTION_ATTRIBUTE, ex, WebRequest.SCOPE_REQUEST); } - return new ResponseEntity<Object>(body, headers, status); } /** * Customize the response for NoSuchRequestHandlingMethodException. - * This method logs a warning and delegates to - * {@link #handleExceptionInternal(Exception, Object, HttpHeaders, HttpStatus, WebRequest)}. + * <p>This method logs a warning and delegates to {@link #handleExceptionInternal}. * @param ex the exception * @param headers the headers to be written to the response * @param status the selected response status @@ -213,8 +224,8 @@ public abstract class ResponseEntityExceptionHandler { /** * Customize the response for HttpRequestMethodNotSupportedException. - * This method logs a warning, sets the "Allow" header, and delegates to - * {@link #handleExceptionInternal(Exception, Object, HttpHeaders, HttpStatus, WebRequest)}. + * <p>This method logs a warning, sets the "Allow" header, and delegates to + * {@link #handleExceptionInternal}. * @param ex the exception * @param headers the headers to be written to the response * @param status the selected response status @@ -230,14 +241,13 @@ public abstract class ResponseEntityExceptionHandler { if (!supportedMethods.isEmpty()) { headers.setAllow(supportedMethods); } - return handleExceptionInternal(ex, null, headers, status, request); } /** * Customize the response for HttpMediaTypeNotSupportedException. - * This method sets the "Accept" header and delegates to - * {@link #handleExceptionInternal(Exception, Object, HttpHeaders, HttpStatus, WebRequest)}. + * <p>This method sets the "Accept" header and delegates to + * {@link #handleExceptionInternal}. * @param ex the exception * @param headers the headers to be written to the response * @param status the selected response status @@ -257,7 +267,7 @@ public abstract class ResponseEntityExceptionHandler { /** * Customize the response for HttpMediaTypeNotAcceptableException. - * This method delegates to {@link #handleExceptionInternal(Exception, Object, HttpHeaders, HttpStatus, WebRequest)}. + * <p>This method delegates to {@link #handleExceptionInternal}. * @param ex the exception * @param headers the headers to be written to the response * @param status the selected response status @@ -271,8 +281,24 @@ public abstract class ResponseEntityExceptionHandler { } /** + * Customize the response for MissingPathVariableException. + * <p>This method delegates to {@link #handleExceptionInternal}. + * @param ex the exception + * @param headers the headers to be written to the response + * @param status the selected response status + * @param request the current request + * @return a {@code ResponseEntity} instance + * @since 4.2 + */ + protected ResponseEntity<Object> handleMissingPathVariable(MissingPathVariableException ex, + HttpHeaders headers, HttpStatus status, WebRequest request) { + + return handleExceptionInternal(ex, null, headers, status, request); + } + + /** * Customize the response for MissingServletRequestParameterException. - * This method delegates to {@link #handleExceptionInternal(Exception, Object, HttpHeaders, HttpStatus, WebRequest)}. + * <p>This method delegates to {@link #handleExceptionInternal}. * @param ex the exception * @param headers the headers to be written to the response * @param status the selected response status @@ -287,7 +313,7 @@ public abstract class ResponseEntityExceptionHandler { /** * Customize the response for ServletRequestBindingException. - * This method delegates to {@link #handleExceptionInternal(Exception, Object, HttpHeaders, HttpStatus, WebRequest)}. + * <p>This method delegates to {@link #handleExceptionInternal}. * @param ex the exception * @param headers the headers to be written to the response * @param status the selected response status @@ -302,7 +328,7 @@ public abstract class ResponseEntityExceptionHandler { /** * Customize the response for ConversionNotSupportedException. - * This method delegates to {@link #handleExceptionInternal(Exception, Object, HttpHeaders, HttpStatus, WebRequest)}. + * <p>This method delegates to {@link #handleExceptionInternal}. * @param ex the exception * @param headers the headers to be written to the response * @param status the selected response status @@ -317,7 +343,7 @@ public abstract class ResponseEntityExceptionHandler { /** * Customize the response for TypeMismatchException. - * This method delegates to {@link #handleExceptionInternal(Exception, Object, HttpHeaders, HttpStatus, WebRequest)}. + * <p>This method delegates to {@link #handleExceptionInternal}. * @param ex the exception * @param headers the headers to be written to the response * @param status the selected response status @@ -332,7 +358,7 @@ public abstract class ResponseEntityExceptionHandler { /** * Customize the response for HttpMessageNotReadableException. - * This method delegates to {@link #handleExceptionInternal(Exception, Object, HttpHeaders, HttpStatus, WebRequest)}. + * <p>This method delegates to {@link #handleExceptionInternal}. * @param ex the exception * @param headers the headers to be written to the response * @param status the selected response status @@ -347,7 +373,7 @@ public abstract class ResponseEntityExceptionHandler { /** * Customize the response for HttpMessageNotWritableException. - * This method delegates to {@link #handleExceptionInternal(Exception, Object, HttpHeaders, HttpStatus, WebRequest)}. + * <p>This method delegates to {@link #handleExceptionInternal}. * @param ex the exception * @param headers the headers to be written to the response * @param status the selected response status @@ -362,7 +388,7 @@ public abstract class ResponseEntityExceptionHandler { /** * Customize the response for MethodArgumentNotValidException. - * This method delegates to {@link #handleExceptionInternal(Exception, Object, HttpHeaders, HttpStatus, WebRequest)}. + * <p>This method delegates to {@link #handleExceptionInternal}. * @param ex the exception * @param headers the headers to be written to the response * @param status the selected response status @@ -377,7 +403,7 @@ public abstract class ResponseEntityExceptionHandler { /** * Customize the response for MissingServletRequestPartException. - * This method delegates to {@link #handleExceptionInternal(Exception, Object, HttpHeaders, HttpStatus, WebRequest)}. + * <p>This method delegates to {@link #handleExceptionInternal}. * @param ex the exception * @param headers the headers to be written to the response * @param status the selected response status @@ -392,7 +418,7 @@ public abstract class ResponseEntityExceptionHandler { /** * Customize the response for BindException. - * This method delegates to {@link #handleExceptionInternal(Exception, Object, HttpHeaders, HttpStatus, WebRequest)}. + * <p>This method delegates to {@link #handleExceptionInternal}. * @param ex the exception * @param headers the headers to be written to the response * @param status the selected response status @@ -407,7 +433,7 @@ public abstract class ResponseEntityExceptionHandler { /** * Customize the response for NoHandlerFoundException. - * This method delegates to {@link #handleExceptionInternal(Exception, Object, HttpHeaders, HttpStatus, WebRequest)}. + * <p>This method delegates to {@link #handleExceptionInternal}. * @param ex the exception * @param headers the headers to be written to the response * @param status the selected response status @@ -415,8 +441,8 @@ public abstract class ResponseEntityExceptionHandler { * @return a {@code ResponseEntity} instance * @since 4.0 */ - protected ResponseEntity<Object> handleNoHandlerFoundException(NoHandlerFoundException ex, HttpHeaders headers, - HttpStatus status, WebRequest request) { + protected ResponseEntity<Object> handleNoHandlerFoundException( + NoHandlerFoundException ex, HttpHeaders headers, HttpStatus status, WebRequest request) { return handleExceptionInternal(ex, null, headers, status, request); } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ServletInvocableHandlerMethod.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ServletInvocableHandlerMethod.java index d48b6502..05cbba3c 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ServletInvocableHandlerMethod.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ServletInvocableHandlerMethod.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2014 the original author or authors. + * Copyright 2002-2015 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -83,7 +83,7 @@ public class ServletInvocableHandlerMethod extends InvocableHandlerMethod { private void initResponseStatus() { ResponseStatus annotation = getMethodAnnotation(ResponseStatus.class); if (annotation != null) { - this.responseStatus = annotation.value(); + this.responseStatus = annotation.code(); this.responseReason = annotation.reason(); } } @@ -260,7 +260,19 @@ public class ServletInvocableHandlerMethod extends InvocableHandlerMethod { @Override public Class<?> getParameterType() { - return (this.returnValue != null ? this.returnValue.getClass() : this.returnType.getRawClass()); + if (this.returnValue != null) { + return this.returnValue.getClass(); + } + Class<?> parameterType = super.getParameterType(); + if (ResponseBodyEmitter.class.isAssignableFrom(parameterType) || + StreamingResponseBody.class.isAssignableFrom(parameterType)) { + return parameterType; + } + if (ResolvableType.NONE.equals(this.returnType)) { + throw new IllegalArgumentException("Expected one of Callable, DeferredResult, or ListenableFuture: " + + super.getParameterType()); + } + return this.returnType.getRawClass(); } @Override diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ServletRequestMethodArgumentResolver.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ServletRequestMethodArgumentResolver.java index 6c0db89c..1ec50e65 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ServletRequestMethodArgumentResolver.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ServletRequestMethodArgumentResolver.java @@ -69,12 +69,12 @@ public class ServletRequestMethodArgumentResolver implements HandlerMethodArgume MultipartRequest.class.isAssignableFrom(paramType) || HttpSession.class.isAssignableFrom(paramType) || Principal.class.isAssignableFrom(paramType) || - Locale.class.equals(paramType) || - TimeZone.class.equals(paramType) || + Locale.class == paramType || + TimeZone.class == paramType || "java.time.ZoneId".equals(paramType.getName()) || InputStream.class.isAssignableFrom(paramType) || Reader.class.isAssignableFrom(paramType) || - HttpMethod.class.equals(paramType)); + HttpMethod.class == paramType); } @Override @@ -98,16 +98,16 @@ public class ServletRequestMethodArgumentResolver implements HandlerMethodArgume else if (HttpSession.class.isAssignableFrom(paramType)) { return request.getSession(); } - else if (HttpMethod.class.equals(paramType)) { + else if (HttpMethod.class == paramType) { return ((ServletWebRequest) webRequest).getHttpMethod(); } else if (Principal.class.isAssignableFrom(paramType)) { return request.getUserPrincipal(); } - else if (Locale.class.equals(paramType)) { + else if (Locale.class == paramType) { return RequestContextUtils.getLocale(request); } - else if (TimeZone.class.equals(paramType)) { + else if (TimeZone.class == paramType) { TimeZone timeZone = RequestContextUtils.getTimeZone(request); return (timeZone != null ? timeZone : TimeZone.getDefault()); } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/SseEmitter.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/SseEmitter.java new file mode 100644 index 00000000..4bc3267e --- /dev/null +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/SseEmitter.java @@ -0,0 +1,251 @@ +/* + * Copyright 2002-2015 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.web.servlet.mvc.method.annotation; + +import java.io.IOException; +import java.nio.charset.Charset; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.Set; + +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.http.server.ServerHttpResponse; + +/** + * A specialization of {@link ResponseBodyEmitter} for sending + * <a href="http://www.w3.org/TR/eventsource/">Server-Sent Events</a>. + * + * @author Rossen Stoyanchev + * @author Juergen Hoeller + * @since 4.2 + */ +public class SseEmitter extends ResponseBodyEmitter { + + static final MediaType TEXT_PLAIN = new MediaType("text", "plain", Charset.forName("UTF-8")); + + + /** + * Create a new SseEmitter instance. + */ + public SseEmitter() { + super(); + } + + /** + * Create a SseEmitter with a custom timeout value. + * <p>By default not set in which case the default configured in the MVC + * Java Config or the MVC namespace is used, or if that's not set, then the + * timeout depends on the default of the underlying server. + * @param timeout timeout value in milliseconds + * @since 4.2.2 + */ + public SseEmitter(Long timeout) { + super(timeout); + } + + + @Override + protected void extendResponse(ServerHttpResponse outputMessage) { + super.extendResponse(outputMessage); + + HttpHeaders headers = outputMessage.getHeaders(); + if (headers.getContentType() == null) { + headers.setContentType(new MediaType("text", "event-stream")); + } + } + + /** + * Send the object formatted as a single SSE "data" line. It's equivalent to: + * <pre> + * // static import of SseEmitter.* + * + * SseEmitter emitter = new SseEmitter(); + * emitter.send(event().data(myObject)); + * </pre> + * @param object the object to write + * @throws IOException raised when an I/O error occurs + * @throws java.lang.IllegalStateException wraps any other errors + */ + @Override + public void send(Object object) throws IOException { + send(object, null); + } + + /** + * Send the object formatted as a single SSE "data" line. It's equivalent to: + * <pre> + * // static import of SseEmitter.* + * + * SseEmitter emitter = new SseEmitter(); + * emitter.send(event().data(myObject, MediaType.APPLICATION_JSON)); + * </pre> + * @param object the object to write + * @param mediaType a MediaType hint for selecting an HttpMessageConverter + * @throws IOException raised when an I/O error occurs + */ + @Override + public void send(Object object, MediaType mediaType) throws IOException { + if (object != null) { + send(event().data(object, mediaType)); + } + } + + /** + * Send an SSE event prepared with the given builder. For example: + * <pre> + * // static import of SseEmitter + * + * SseEmitter emitter = new SseEmitter(); + * emitter.send(event().name("update").id("1").data(myObject)); + * </pre> + * @param builder a builder for an SSE formatted event. + * @throws IOException raised when an I/O error occurs + */ + public void send(SseEventBuilder builder) throws IOException { + Set<DataWithMediaType> dataToSend = builder.build(); + synchronized (this) { + for (DataWithMediaType entry : dataToSend) { + super.send(entry.getData(), entry.getMediaType()); + } + } + } + + + public static SseEventBuilder event() { + return new SseEventBuilderImpl(); + } + + + /** + * A builder for an SSE event. + */ + public interface SseEventBuilder { + + /** + * Add an SSE "comment" line. + */ + SseEventBuilder comment(String comment); + + /** + * Add an SSE "event" line. + */ + SseEventBuilder name(String eventName); + + /** + * Add an SSE "id" line. + */ + SseEventBuilder id(String id); + + /** + * Add an SSE "event" line. + */ + SseEventBuilder reconnectTime(long reconnectTimeMillis); + + /** + * Add an SSE "data" line. + */ + SseEventBuilder data(Object object); + + /** + * Add an SSE "data" line. + */ + SseEventBuilder data(Object object, MediaType mediaType); + + /** + * Return one or more Object-MediaType pairs to write via + * {@link #send(Object, MediaType)}. + * @since 4.2.3 + */ + Set<DataWithMediaType> build(); + } + + + /** + * Default implementation of SseEventBuilder. + */ + private static class SseEventBuilderImpl implements SseEventBuilder { + + private final Set<DataWithMediaType> dataToSend = new LinkedHashSet<DataWithMediaType>(4); + + private StringBuilder sb; + + @Override + public SseEventBuilder comment(String comment) { + append(":").append(comment != null ? comment : "").append("\n"); + return this; + } + + @Override + public SseEventBuilder name(String name) { + append("event:").append(name != null ? name : "").append("\n"); + return this; + } + + @Override + public SseEventBuilder id(String id) { + append("id:").append(id != null ? id : "").append("\n"); + return this; + } + + @Override + public SseEventBuilder reconnectTime(long reconnectTimeMillis) { + append("retry:").append(String.valueOf(reconnectTimeMillis)).append("\n"); + return this; + } + + @Override + public SseEventBuilder data(Object object) { + return data(object, null); + } + + @Override + public SseEventBuilder data(Object object, MediaType mediaType) { + append("data:"); + saveAppendedText(); + this.dataToSend.add(new DataWithMediaType(object, mediaType)); + append("\n"); + return this; + } + + SseEventBuilderImpl append(String text) { + if (this.sb == null) { + this.sb = new StringBuilder(); + } + this.sb.append(text); + return this; + } + + @Override + public Set<DataWithMediaType> build() { + if ((this.sb == null || this.sb.length() == 0) && this.dataToSend.isEmpty()) { + return Collections.<DataWithMediaType>emptySet(); + } + append("\n"); + saveAppendedText(); + return this.dataToSend; + } + + private void saveAppendedText() { + if (this.sb != null) { + this.dataToSend.add(new DataWithMediaType(this.sb.toString(), TEXT_PLAIN)); + this.sb = null; + } + } + } + +} diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/StreamingResponseBody.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/StreamingResponseBody.java new file mode 100644 index 00000000..a3a4240c --- /dev/null +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/StreamingResponseBody.java @@ -0,0 +1,47 @@ +/* + * Copyright 2002-2015 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.web.servlet.mvc.method.annotation; + + +import java.io.IOException; +import java.io.OutputStream; + +/** + * A controller method return value type for asynchronous request processing + * where the application can write directly to the response {@code OutputStream} + * without holding up the Servlet container thread. + * + * <p><strong>Note:</strong> when using this option it is highly recommended to + * configure explicitly the TaskExecutor used in Spring MVC for executing + * asynchronous requests. Both the MVC Java config and the MVC namespaces provide + * options to configure asynchronous handling. If not using those, an application + * can set the {@code taskExecutor} property of + * {@link org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter + * RequestMappingHandlerAdapter}. + * + * @author Rossen Stoyanchev + * @since 4.2 + */ +public interface StreamingResponseBody { + + /** + * A callback for writing to the response body. + * @param outputStream the stream for the response body + * @throws IOException an exception while writing + */ + void writeTo(OutputStream outputStream) throws IOException; + +} diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/StreamingResponseBodyReturnValueHandler.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/StreamingResponseBodyReturnValueHandler.java new file mode 100644 index 00000000..40852899 --- /dev/null +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/StreamingResponseBodyReturnValueHandler.java @@ -0,0 +1,113 @@ +/* + * Copyright 2002-2015 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.web.servlet.mvc.method.annotation; + +import java.io.OutputStream; +import java.util.concurrent.Callable; + +import javax.servlet.ServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.springframework.core.MethodParameter; +import org.springframework.core.ResolvableType; +import org.springframework.http.ResponseEntity; +import org.springframework.http.server.ServerHttpResponse; +import org.springframework.http.server.ServletServerHttpResponse; +import org.springframework.util.Assert; +import org.springframework.web.context.request.NativeWebRequest; +import org.springframework.web.context.request.async.WebAsyncUtils; +import org.springframework.web.filter.ShallowEtagHeaderFilter; +import org.springframework.web.method.support.HandlerMethodReturnValueHandler; +import org.springframework.web.method.support.ModelAndViewContainer; + + +/** + * Supports return values of type + * {@link org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody} + * and also {@code ResponseEntity<StreamingResponseBody>}. + * + * @author Rossen Stoyanchev + * @since 4.2 + */ +public class StreamingResponseBodyReturnValueHandler implements HandlerMethodReturnValueHandler { + + @Override + public boolean supportsReturnType(MethodParameter returnType) { + if (StreamingResponseBody.class.isAssignableFrom(returnType.getParameterType())) { + return true; + } + else if (ResponseEntity.class.isAssignableFrom(returnType.getParameterType())) { + Class<?> bodyType = ResolvableType.forMethodParameter(returnType).getGeneric(0).resolve(); + return (bodyType != null && StreamingResponseBody.class.isAssignableFrom(bodyType)); + } + return false; + } + + @Override + public void handleReturnValue(Object returnValue, MethodParameter returnType, + ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception { + + if (returnValue == null) { + mavContainer.setRequestHandled(true); + return; + } + + HttpServletResponse response = webRequest.getNativeResponse(HttpServletResponse.class); + ServerHttpResponse outputMessage = new ServletServerHttpResponse(response); + + if (ResponseEntity.class.isAssignableFrom(returnValue.getClass())) { + ResponseEntity<?> responseEntity = (ResponseEntity<?>) returnValue; + outputMessage.setStatusCode(responseEntity.getStatusCode()); + outputMessage.getHeaders().putAll(responseEntity.getHeaders()); + + returnValue = responseEntity.getBody(); + if (returnValue == null) { + mavContainer.setRequestHandled(true); + return; + } + } + + ServletRequest request = webRequest.getNativeRequest(ServletRequest.class); + ShallowEtagHeaderFilter.disableContentCaching(request); + + Assert.isInstanceOf(StreamingResponseBody.class, returnValue); + StreamingResponseBody streamingBody = (StreamingResponseBody) returnValue; + + Callable<Void> callable = new StreamingResponseBodyTask(outputMessage.getBody(), streamingBody); + WebAsyncUtils.getAsyncManager(webRequest).startCallableProcessing(callable, mavContainer); + } + + + private static class StreamingResponseBodyTask implements Callable<Void> { + + private final OutputStream outputStream; + + private final StreamingResponseBody streamingBody; + + + public StreamingResponseBodyTask(OutputStream outputStream, StreamingResponseBody streamingBody) { + this.outputStream = outputStream; + this.streamingBody = streamingBody; + } + + @Override + public Void call() throws Exception { + this.streamingBody.writeTo(this.outputStream); + return null; + } + } + +} diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/UriComponentsBuilderMethodArgumentResolver.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/UriComponentsBuilderMethodArgumentResolver.java index 74e632f2..d8d32084 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/UriComponentsBuilderMethodArgumentResolver.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/UriComponentsBuilderMethodArgumentResolver.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2014 the original author or authors. + * Copyright 2002-2015 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -26,6 +26,7 @@ import org.springframework.web.method.support.ModelAndViewContainer; import org.springframework.web.servlet.support.ServletUriComponentsBuilder; import org.springframework.web.util.UriComponentsBuilder; + /** * Resolvers argument values of type {@link UriComponentsBuilder}. * @@ -37,9 +38,11 @@ import org.springframework.web.util.UriComponentsBuilder; */ public class UriComponentsBuilderMethodArgumentResolver implements HandlerMethodArgumentResolver { + @Override public boolean supportsParameter(MethodParameter parameter) { - return UriComponentsBuilder.class.isAssignableFrom(parameter.getParameterType()); + Class<?> type = parameter.getParameterType(); + return (UriComponentsBuilder.class == type || ServletUriComponentsBuilder.class == type); } @Override diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ViewNameMethodReturnValueHandler.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ViewNameMethodReturnValueHandler.java index 65919d22..9601ce60 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ViewNameMethodReturnValueHandler.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ViewNameMethodReturnValueHandler.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2014 the original author or authors. + * Copyright 2002-2015 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -24,8 +24,9 @@ import org.springframework.web.method.support.ModelAndViewContainer; import org.springframework.web.servlet.RequestToViewNameTranslator; /** - * Handles return values of types {@code void} and {@code String} interpreting - * them as view name reference. + * Handles return values of types {@code void} and {@code String} interpreting them + * as view name reference. As of 4.2, it also handles general {@code CharSequence} + * types, e.g. {@code StringBuilder} or Groovy's {@code GString}, as view names. * * <p>A {@code null} return value, either due to a {@code void} return type or * as the actual return value is left as-is allowing the configured @@ -37,6 +38,7 @@ import org.springframework.web.servlet.RequestToViewNameTranslator; * the handlers that support these annotations. * * @author Rossen Stoyanchev + * @author Juergen Hoeller * @since 3.1 */ public class ViewNameMethodReturnValueHandler implements HandlerMethodReturnValueHandler { @@ -68,24 +70,21 @@ public class ViewNameMethodReturnValueHandler implements HandlerMethodReturnValu @Override public boolean supportsReturnType(MethodParameter returnType) { Class<?> paramType = returnType.getParameterType(); - return (void.class.equals(paramType) || String.class.equals(paramType)); + return (void.class == paramType || CharSequence.class.isAssignableFrom(paramType)); } @Override public void handleReturnValue(Object returnValue, MethodParameter returnType, ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception { - if (returnValue == null) { - return; - } - else if (returnValue instanceof String) { - String viewName = (String) returnValue; + if (returnValue instanceof CharSequence) { + String viewName = returnValue.toString(); mavContainer.setViewName(viewName); if (isRedirectViewName(viewName)) { mavContainer.setRedirectModelScenario(true); } } - else { + else if (returnValue != null){ // should not happen throw new UnsupportedOperationException("Unexpected return type: " + returnType.getParameterType().getName() + " in method: " + returnType.getMethod()); @@ -101,10 +100,7 @@ public class ViewNameMethodReturnValueHandler implements HandlerMethodReturnValu * reference; "false" otherwise. */ protected boolean isRedirectViewName(String viewName) { - if (PatternMatchUtils.simpleMatch(this.redirectPatterns, viewName)) { - return true; - } - return viewName.startsWith("redirect:"); + return (PatternMatchUtils.simpleMatch(this.redirectPatterns, viewName) || viewName.startsWith("redirect:")); } } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/multiaction/MultiActionController.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/multiaction/MultiActionController.java index a47920ae..188c9667 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/multiaction/MultiActionController.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/multiaction/MultiActionController.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 the original author or authors. + * Copyright 2002-2015 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.springframework.web.servlet.mvc.multiaction; +package org.springframework.web.servlet. mvc.multiaction; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; @@ -288,12 +288,12 @@ public class MultiActionController extends AbstractController implements LastMod */ private boolean isHandlerMethod(Method method) { Class<?> returnType = method.getReturnType(); - if (ModelAndView.class.equals(returnType) || Map.class.equals(returnType) || String.class.equals(returnType) || - void.class.equals(returnType)) { + if (ModelAndView.class == returnType || Map.class == returnType || String.class == returnType || + void.class == returnType) { Class<?>[] parameterTypes = method.getParameterTypes(); return (parameterTypes.length >= 2 && - HttpServletRequest.class.equals(parameterTypes[0]) && - HttpServletResponse.class.equals(parameterTypes[1]) && + HttpServletRequest.class == parameterTypes[0] && + HttpServletResponse.class == parameterTypes[1] && !("handleRequest".equals(method.getName()) && parameterTypes.length == 2)); } return false; @@ -329,7 +329,7 @@ public class MultiActionController extends AbstractController implements LastMod method.getName() + LAST_MODIFIED_METHOD_SUFFIX, new Class<?>[] {HttpServletRequest.class}); Class<?> returnType = lastModifiedMethod.getReturnType(); - if (!(long.class.equals(returnType) || Long.class.equals(returnType))) { + if (!(long.class == returnType || Long.class == returnType)) { throw new IllegalStateException("last-modified method [" + lastModifiedMethod + "] declares an invalid return type - needs to be 'long' or 'Long'"); } @@ -452,7 +452,7 @@ public class MultiActionController extends AbstractController implements LastMod params.add(request); params.add(response); - if (paramTypes.length >= 3 && paramTypes[2].equals(HttpSession.class)) { + if (paramTypes.length >= 3 && HttpSession.class == paramTypes[2]) { HttpSession session = request.getSession(false); if (session == null) { throw new HttpSessionRequiredException( @@ -462,8 +462,7 @@ public class MultiActionController extends AbstractController implements LastMod } // If last parameter isn't of HttpSession type, it's a command. - if (paramTypes.length >= 3 && - !paramTypes[paramTypes.length - 1].equals(HttpSession.class)) { + if (paramTypes.length >= 3 && HttpSession.class != paramTypes[paramTypes.length - 1]) { Object command = newCommandObject(paramTypes[paramTypes.length - 1]); params.add(command); bind(request, command); @@ -608,7 +607,7 @@ public class MultiActionController extends AbstractController implements LastMod logger.debug("Trying to find handler for exception class [" + exceptionClass.getName() + "]"); } Method handler = this.exceptionHandlerMap.get(exceptionClass); - while (handler == null && !exceptionClass.equals(Throwable.class)) { + while (handler == null && exceptionClass != Throwable.class) { if (logger.isDebugEnabled()) { logger.debug("Trying to find handler for exception superclass [" + exceptionClass.getName() + "]"); } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/support/DefaultHandlerExceptionResolver.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/support/DefaultHandlerExceptionResolver.java index 26a69b16..77290e56 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/support/DefaultHandlerExceptionResolver.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/support/DefaultHandlerExceptionResolver.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2013 the original author or authors. + * Copyright 2002-2015 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -38,6 +38,7 @@ import org.springframework.web.HttpMediaTypeNotAcceptableException; import org.springframework.web.HttpMediaTypeNotSupportedException; import org.springframework.web.HttpRequestMethodNotSupportedException; import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.MissingPathVariableException; import org.springframework.web.bind.MissingServletRequestParameterException; import org.springframework.web.bind.ServletRequestBindingException; import org.springframework.web.bind.annotation.ModelAttribute; @@ -55,12 +56,13 @@ import org.springframework.web.servlet.mvc.multiaction.NoSuchRequestHandlingMeth * HandlerExceptionResolver} interface that resolves standard Spring exceptions and translates * them to corresponding HTTP status codes. * - * <p>This exception resolver is enabled by default in the {@link org.springframework.web.servlet.DispatcherServlet}. + * <p>This exception resolver is enabled by default in the common Spring + * {@link org.springframework.web.servlet.DispatcherServlet}. * * @author Arjen Poutsma * @author Rossen Stoyanchev + * @author Juergen Hoeller * @since 3.0 - * * @see org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler * @see #handleNoSuchRequestHandlingMethod * @see #handleHttpRequestMethodNotSupported @@ -119,6 +121,10 @@ public class DefaultHandlerExceptionResolver extends AbstractHandlerExceptionRes return handleHttpMediaTypeNotAcceptable((HttpMediaTypeNotAcceptableException) ex, request, response, handler); } + else if (ex instanceof MissingPathVariableException) { + return handleMissingPathVariable((MissingPathVariableException) ex, request, + response, handler); + } else if (ex instanceof MissingServletRequestParameterException) { return handleMissingServletRequestParameter((MissingServletRequestParameterException) ex, request, response, handler); @@ -140,10 +146,12 @@ public class DefaultHandlerExceptionResolver extends AbstractHandlerExceptionRes return handleHttpMessageNotWritable((HttpMessageNotWritableException) ex, request, response, handler); } else if (ex instanceof MethodArgumentNotValidException) { - return handleMethodArgumentNotValidException((MethodArgumentNotValidException) ex, request, response, handler); + return handleMethodArgumentNotValidException((MethodArgumentNotValidException) ex, request, response, + handler); } else if (ex instanceof MissingServletRequestPartException) { - return handleMissingServletRequestPartException((MissingServletRequestPartException) ex, request, response, handler); + return handleMissingServletRequestPartException((MissingServletRequestPartException) ex, request, + response, handler); } else if (ex instanceof BindException) { return handleBindException((BindException) ex, request, response, handler); @@ -153,7 +161,9 @@ public class DefaultHandlerExceptionResolver extends AbstractHandlerExceptionRes } } catch (Exception handlerException) { - logger.warn("Handling of [" + ex.getClass().getName() + "] resulted in Exception", handlerException); + if (logger.isWarnEnabled()) { + logger.warn("Handling of [" + ex.getClass().getName() + "] resulted in Exception", handlerException); + } } return null; } @@ -206,9 +216,10 @@ public class DefaultHandlerExceptionResolver extends AbstractHandlerExceptionRes /** * Handle the case where no {@linkplain org.springframework.http.converter.HttpMessageConverter message converters} - * were found for the PUT or POSTed content. <p>The default implementation sends an HTTP 415 error, - * sets the "Accept" header, and returns an empty {@code ModelAndView}. Alternatively, a fallback - * view could be chosen, or the HttpMediaTypeNotSupportedException could be rethrown as-is. + * were found for the PUT or POSTed content. + * <p>The default implementation sends an HTTP 415 error, sets the "Accept" header, + * and returns an empty {@code ModelAndView}. Alternatively, a fallback view could + * be chosen, or the HttpMediaTypeNotSupportedException could be rethrown as-is. * @param ex the HttpMediaTypeNotSupportedException to be handled * @param request current HTTP request * @param response current HTTP response @@ -248,6 +259,26 @@ public class DefaultHandlerExceptionResolver extends AbstractHandlerExceptionRes } /** + * Handle the case when a declared path variable does not match any extracted URI variable. + * <p>The default implementation sends an HTTP 500 error, and returns an empty {@code ModelAndView}. + * Alternatively, a fallback view could be chosen, or the MissingPathVariableException + * could be rethrown as-is. + * @param ex the MissingPathVariableException to be handled + * @param request current HTTP request + * @param response current HTTP response + * @param handler the executed handler + * @return an empty ModelAndView indicating the exception was handled + * @throws IOException potentially thrown from response.sendError() + * @since 4.2 + */ + protected ModelAndView handleMissingPathVariable(MissingPathVariableException ex, + HttpServletRequest request, HttpServletResponse response, Object handler) throws IOException { + + response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, ex.getMessage()); + return new ModelAndView(); + } + + /** * Handle the case when a required parameter is missing. * <p>The default implementation sends an HTTP 400 error, and returns an empty {@code ModelAndView}. * Alternatively, a fallback view could be chosen, or the MissingServletRequestParameterException @@ -280,7 +311,7 @@ public class DefaultHandlerExceptionResolver extends AbstractHandlerExceptionRes protected ModelAndView handleServletRequestBindingException(ServletRequestBindingException ex, HttpServletRequest request, HttpServletResponse response, Object handler) throws IOException { - response.sendError(HttpServletResponse.SC_BAD_REQUEST); + response.sendError(HttpServletResponse.SC_BAD_REQUEST, ex.getMessage()); return new ModelAndView(); } @@ -298,22 +329,14 @@ public class DefaultHandlerExceptionResolver extends AbstractHandlerExceptionRes protected ModelAndView handleConversionNotSupported(ConversionNotSupportedException ex, HttpServletRequest request, HttpServletResponse response, Object handler) throws IOException { + if (logger.isWarnEnabled()) { + logger.warn("Failed to convert request element: " + ex); + } sendServerError(ex, request, response); return new ModelAndView(); } /** - * Invoked to send a server error. Sets the status to 500 and also sets the - * request attribute "javax.servlet.error.exception" to the Exception. - */ - protected void sendServerError(Exception ex, - HttpServletRequest request, HttpServletResponse response) throws IOException { - - request.setAttribute("javax.servlet.error.exception", ex); - response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); - } - - /** * Handle the case when a {@link org.springframework.web.bind.WebDataBinder} conversion error occurs. * <p>The default implementation sends an HTTP 400 error, and returns an empty {@code ModelAndView}. * Alternatively, a fallback view could be chosen, or the TypeMismatchException could be rethrown as-is. @@ -327,6 +350,9 @@ public class DefaultHandlerExceptionResolver extends AbstractHandlerExceptionRes protected ModelAndView handleTypeMismatch(TypeMismatchException ex, HttpServletRequest request, HttpServletResponse response, Object handler) throws IOException { + if (logger.isWarnEnabled()) { + logger.warn("Failed to bind request element: " + ex); + } response.sendError(HttpServletResponse.SC_BAD_REQUEST); return new ModelAndView(); } @@ -347,6 +373,9 @@ public class DefaultHandlerExceptionResolver extends AbstractHandlerExceptionRes protected ModelAndView handleHttpMessageNotReadable(HttpMessageNotReadableException ex, HttpServletRequest request, HttpServletResponse response, Object handler) throws IOException { + if (logger.isWarnEnabled()) { + logger.warn("Failed to read HTTP message: " + ex); + } response.sendError(HttpServletResponse.SC_BAD_REQUEST); return new ModelAndView(); } @@ -367,6 +396,9 @@ public class DefaultHandlerExceptionResolver extends AbstractHandlerExceptionRes protected ModelAndView handleHttpMessageNotWritable(HttpMessageNotWritableException ex, HttpServletRequest request, HttpServletResponse response, Object handler) throws IOException { + if (logger.isWarnEnabled()) { + logger.warn("Failed to write HTTP message: " + ex); + } sendServerError(ex, request, response); return new ModelAndView(); } @@ -383,6 +415,7 @@ public class DefaultHandlerExceptionResolver extends AbstractHandlerExceptionRes */ protected ModelAndView handleMethodArgumentNotValidException(MethodArgumentNotValidException ex, HttpServletRequest request, HttpServletResponse response, Object handler) throws IOException { + response.sendError(HttpServletResponse.SC_BAD_REQUEST); return new ModelAndView(); } @@ -399,6 +432,7 @@ public class DefaultHandlerExceptionResolver extends AbstractHandlerExceptionRes */ protected ModelAndView handleMissingServletRequestPartException(MissingServletRequestPartException ex, HttpServletRequest request, HttpServletResponse response, Object handler) throws IOException { + response.sendError(HttpServletResponse.SC_BAD_REQUEST, ex.getMessage()); return new ModelAndView(); } @@ -407,7 +441,7 @@ public class DefaultHandlerExceptionResolver extends AbstractHandlerExceptionRes * Handle the case where an {@linkplain ModelAttribute @ModelAttribute} method * argument has binding or validation errors and is not followed by another * method argument of type {@link BindingResult}. - * By default an HTTP 400 error is sent back to the client. + * By default, an HTTP 400 error is sent back to the client. * @param request current HTTP request * @param response current HTTP response * @param handler the executed handler @@ -416,14 +450,15 @@ public class DefaultHandlerExceptionResolver extends AbstractHandlerExceptionRes */ protected ModelAndView handleBindException(BindException ex, HttpServletRequest request, HttpServletResponse response, Object handler) throws IOException { + response.sendError(HttpServletResponse.SC_BAD_REQUEST); return new ModelAndView(); } /** * Handle the case where no handler was found during the dispatch. - * <p>The default sends an HTTP 404 error, and returns - * an empty {@code ModelAndView}. Alternatively, a fallback view could be chosen, + * <p>The default implementation sends an HTTP 404 error and returns an empty + * {@code ModelAndView}. Alternatively, a fallback view could be chosen, * or the NoHandlerFoundException could be rethrown as-is. * @param ex the NoHandlerFoundException to be handled * @param request current HTTP request @@ -434,10 +469,23 @@ public class DefaultHandlerExceptionResolver extends AbstractHandlerExceptionRes * @throws IOException potentially thrown from response.sendError() * @since 4.0 */ - protected ModelAndView handleNoHandlerFoundException(NoHandlerFoundException ex, HttpServletRequest request, - HttpServletResponse response, Object handler) throws IOException { + protected ModelAndView handleNoHandlerFoundException(NoHandlerFoundException ex, + HttpServletRequest request, HttpServletResponse response, Object handler) throws IOException { + response.sendError(HttpServletResponse.SC_NOT_FOUND); return new ModelAndView(); } + + /** + * Invoked to send a server error. Sets the status to 500 and also sets the + * request attribute "javax.servlet.error.exception" to the Exception. + */ + protected void sendServerError(Exception ex, HttpServletRequest request, HttpServletResponse response) + throws IOException { + + request.setAttribute("javax.servlet.error.exception", ex); + response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); + } + } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/DefaultResourceResolverChain.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/DefaultResourceResolverChain.java index 7bb638f2..501f2371 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/DefaultResourceResolverChain.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/DefaultResourceResolverChain.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * Copyright 2002-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -78,12 +78,11 @@ class DefaultResourceResolverChain implements ResourceResolverChain { private ResourceResolver getNext() { Assert.state(this.index <= this.resolvers.size(), - "Current index exceeds the number of configured ResourceResolver's"); + "Current index exceeds the number of configured ResourceResolvers"); if (this.index == (this.resolvers.size() - 1)) { return null; } - this.index++; return this.resolvers.get(this.index); } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/PathResourceResolver.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/PathResourceResolver.java index fc964907..2119b7a7 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/PathResourceResolver.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/PathResourceResolver.java @@ -158,7 +158,7 @@ public class PathResourceResolver extends AbstractResourceResolver { } private boolean isResourceUnderLocation(Resource resource, Resource location) throws IOException { - if (!resource.getClass().equals(location.getClass())) { + if (resource.getClass() != location.getClass()) { return false; } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/ResourceHttpRequestHandler.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/ResourceHttpRequestHandler.java index d36c6d3f..f5319893 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/ResourceHttpRequestHandler.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/ResourceHttpRequestHandler.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * Copyright 2002-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,12 +19,14 @@ package org.springframework.web.servlet.resource; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; +import java.io.OutputStream; import java.net.URLDecoder; import java.util.ArrayList; import java.util.List; import javax.activation.FileTypeMap; import javax.activation.MimetypesFileTypeMap; import javax.servlet.ServletException; +import javax.servlet.ServletOutputStream; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; @@ -34,16 +36,22 @@ import org.apache.commons.logging.LogFactory; import org.springframework.beans.factory.InitializingBean; import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.Resource; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpRange; import org.springframework.http.MediaType; +import org.springframework.http.server.ServletServerHttpRequest; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.CollectionUtils; +import org.springframework.util.MimeTypeUtils; import org.springframework.util.ObjectUtils; import org.springframework.util.ResourceUtils; import org.springframework.util.StreamUtils; import org.springframework.util.StringUtils; import org.springframework.web.HttpRequestHandler; import org.springframework.web.context.request.ServletWebRequest; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.CorsConfigurationSource; import org.springframework.web.servlet.HandlerMapping; import org.springframework.web.servlet.support.WebContentGenerator; @@ -55,8 +63,8 @@ import org.springframework.web.servlet.support.WebContentGenerator; * <p>The {@linkplain #setLocations "locations" property} takes a list of Spring {@link Resource} * locations from which static resources are allowed to be served by this handler. For a given request, * the list of locations will be consulted in order for the presence of the requested resource, and the - * first found match will be written to the response, with {@code Expires} and {@code Cache-Control} - * headers set as configured. The handler also properly evaluates the {@code Last-Modified} header + * first found match will be written to the response, with a HTTP Caching headers + * set as configured. The handler also properly evaluates the {@code Last-Modified} header * (if present) so that a {@code 304} status code will be returned as appropriate, avoiding unnecessary * overhead for resources that are already cached by the client. The use of {@code Resource} locations * allows resource requests to easily be mapped to locations other than the web application root. @@ -80,11 +88,12 @@ import org.springframework.web.servlet.support.WebContentGenerator; * @author Keith Donald * @author Jeremy Grelle * @author Juergen Hoeller + * @author Arjen Poutsma + * @author Brian Clozel * @since 3.0.4 */ -public class ResourceHttpRequestHandler extends WebContentGenerator implements HttpRequestHandler, InitializingBean { - - private static final String CONTENT_ENCODING = "Content-Encoding"; +public class ResourceHttpRequestHandler extends WebContentGenerator + implements HttpRequestHandler, InitializingBean, CorsConfigurationSource { private static final Log logger = LogFactory.getLog(ResourceHttpRequestHandler.class); @@ -98,6 +107,8 @@ public class ResourceHttpRequestHandler extends WebContentGenerator implements H private final List<ResourceTransformer> resourceTransformers = new ArrayList<ResourceTransformer>(4); + private CorsConfiguration corsConfiguration; + public ResourceHttpRequestHandler() { super(METHOD_GET, METHOD_HEAD); @@ -156,6 +167,15 @@ public class ResourceHttpRequestHandler extends WebContentGenerator implements H return this.resourceTransformers; } + public void setCorsConfiguration(CorsConfiguration corsConfiguration) { + this.corsConfiguration = corsConfiguration; + } + + @Override + public CorsConfiguration getCorsConfiguration(HttpServletRequest request) { + return this.corsConfiguration; + } + @Override public void afterPropertiesSet() throws Exception { @@ -167,17 +187,15 @@ public class ResourceHttpRequestHandler extends WebContentGenerator implements H } /** - * Look for a {@link org.springframework.web.servlet.resource.PathResourceResolver} - * among the {@link #getResourceResolvers() resource resolvers} and configure - * its {@code "allowedLocations"} to match the value of the - * {@link #setLocations(java.util.List) locations} property unless the "allowed - * locations" of the {@code PathResourceResolver} is non-empty. + * Look for a {@code PathResourceResolver} among the configured resource + * resolvers and set its {@code allowedLocations} property (if empty) to + * match the {@link #setLocations locations} configured on this class. */ protected void initAllowedLocations() { if (CollectionUtils.isEmpty(this.locations)) { return; } - for (int i = getResourceResolvers().size()-1; i >= 0; i--) { + for (int i = getResourceResolvers().size() - 1; i >= 0; i--) { if (getResourceResolvers().get(i) instanceof PathResourceResolver) { PathResourceResolver pathResolver = (PathResourceResolver) getResourceResolvers().get(i); if (ObjectUtils.isEmpty(pathResolver.getAllowedLocations())) { @@ -188,6 +206,7 @@ public class ResourceHttpRequestHandler extends WebContentGenerator implements H } } + /** * Processes a resource request. * <p>Checks for the existence of the requested resource in the configured list of locations. @@ -204,9 +223,10 @@ public class ResourceHttpRequestHandler extends WebContentGenerator implements H public void handleRequest(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { - checkAndPrepare(request, response, true); + // Supported methods and required session + checkRequest(request); - // check whether a matching resource exists + // Check whether a matching resource exists Resource resource = getResource(request); if (resource == null) { logger.trace("No matching resource found - returning 404"); @@ -214,7 +234,16 @@ public class ResourceHttpRequestHandler extends WebContentGenerator implements H return; } - // check the resource's media type + // Header phase + if (new ServletWebRequest(request, response).checkNotModified(resource.lastModified())) { + logger.trace("Resource not modified - returning 304"); + return; + } + + // Apply cache settings, if any + prepareResponse(response); + + // Check the media type for the resource MediaType mediaType = getMediaType(resource); if (mediaType != null) { if (logger.isTraceEnabled()) { @@ -227,19 +256,20 @@ public class ResourceHttpRequestHandler extends WebContentGenerator implements H } } - // header phase - if (new ServletWebRequest(request, response).checkNotModified(resource.lastModified())) { - logger.trace("Resource not modified - returning 304"); - return; - } - setHeaders(response, resource, mediaType); - - // content phase + // Content phase if (METHOD_HEAD.equals(request.getMethod())) { + setHeaders(response, resource, mediaType); logger.trace("HEAD request - skipping content"); return; } - writeContent(response, resource); + + if (request.getHeader(HttpHeaders.RANGE) == null) { + setHeaders(response, resource, mediaType); + writeContent(response, resource); + } + else { + writePartialContent(request, response, resource, mediaType); + } } protected Resource getResource(HttpServletRequest request) throws IOException { @@ -387,14 +417,16 @@ public class ResourceHttpRequestHandler extends WebContentGenerator implements H throw new IOException("Resource content too long (beyond Integer.MAX_VALUE): " + resource); } response.setContentLength((int) length); - if (mediaType != null) { response.setContentType(mediaType.toString()); } - if (resource instanceof EncodedResource) { - response.setHeader(CONTENT_ENCODING, ((EncodedResource) resource).getContentEncoding()); + response.setHeader(HttpHeaders.CONTENT_ENCODING, ((EncodedResource) resource).getContentEncoding()); + } + if (resource instanceof VersionedResource) { + response.setHeader(HttpHeaders.ETAG, "\"" + ((VersionedResource) resource).getVersion() + "\""); } + response.setHeader(HttpHeaders.ACCEPT_RANGES, "bytes"); } /** @@ -427,10 +459,113 @@ public class ResourceHttpRequestHandler extends WebContentGenerator implements H } } + /** + * Write parts of the resource as indicated by the request {@code Range} header. + * @param request current servlet request + * @param response current servlet response + * @param resource the identified resource (never {@code null}) + * @param contentType the content type + * @throws IOException in case of errors while writing the content + */ + protected void writePartialContent(HttpServletRequest request, HttpServletResponse response, + Resource resource, MediaType contentType) throws IOException { + + long length = resource.contentLength(); + + List<HttpRange> ranges; + try { + HttpHeaders headers = new ServletServerHttpRequest(request).getHeaders(); + ranges = headers.getRange(); + } + catch (IllegalArgumentException ex) { + response.addHeader("Content-Range", "bytes */" + length); + response.sendError(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE); + return; + } + + response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT); + + if (ranges.size() == 1) { + HttpRange range = ranges.get(0); + + long start = range.getRangeStart(length); + long end = range.getRangeEnd(length); + long rangeLength = end - start + 1; + + setHeaders(response, resource, contentType); + response.addHeader("Content-Range", "bytes " + start + "-" + end + "/" + length); + response.setContentLength((int) rangeLength); + + InputStream in = resource.getInputStream(); + try { + copyRange(in, response.getOutputStream(), start, end); + } + finally { + try { + in.close(); + } + catch (IOException ex) { + // ignore + } + } + } + else { + String boundaryString = MimeTypeUtils.generateMultipartBoundaryString(); + response.setContentType("multipart/byteranges; boundary=" + boundaryString); + + ServletOutputStream out = response.getOutputStream(); + + for (HttpRange range : ranges) { + long start = range.getRangeStart(length); + long end = range.getRangeEnd(length); + + InputStream in = resource.getInputStream(); + + // Writing MIME header. + out.println(); + out.println("--" + boundaryString); + if (contentType != null) { + out.println("Content-Type: " + contentType); + } + out.println("Content-Range: bytes " + start + "-" + end + "/" + length); + out.println(); + + // Printing content + copyRange(in, out, start, end); + } + out.println(); + out.print("--" + boundaryString + "--"); + } + } + + private void copyRange(InputStream in, OutputStream out, long start, long end) throws IOException { + long skipped = in.skip(start); + if (skipped < start) { + throw new IOException("Skipped only " + skipped + " bytes out of " + start + " required."); + } + + long bytesToCopy = end - start + 1; + byte buffer[] = new byte[StreamUtils.BUFFER_SIZE]; + while (bytesToCopy > 0) { + int bytesRead = in.read(buffer); + if (bytesRead <= bytesToCopy) { + out.write(buffer, 0, bytesRead); + bytesToCopy -= bytesRead; + } + else { + out.write(buffer, 0, (int) bytesToCopy); + bytesToCopy = 0; + } + if (bytesRead == -1) { + break; + } + } + } + + @Override public String toString() { - return "ResourceHttpRequestHandler [locations=" + - getLocations() + ", resolvers=" + getResourceResolvers() + "]"; + return "ResourceHttpRequestHandler [locations=" + getLocations() + ", resolvers=" + getResourceResolvers() + "]"; } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/ResourceUrlEncodingFilter.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/ResourceUrlEncodingFilter.java index 9a053450..471ae154 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/ResourceUrlEncodingFilter.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/ResourceUrlEncodingFilter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * Copyright 2002-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -37,6 +37,7 @@ import org.springframework.web.filter.OncePerRequestFilter; * @author Jeremy Grelle * @author Rossen Stoyanchev * @author Sam Brannen + * @author Brian Clozel * @since 4.1 */ public class ResourceUrlEncodingFilter extends OncePerRequestFilter { @@ -54,11 +55,13 @@ public class ResourceUrlEncodingFilter extends OncePerRequestFilter { private static class ResourceUrlEncodingResponseWrapper extends HttpServletResponseWrapper { - private HttpServletRequest request; + private final HttpServletRequest request; - /* Cache the index of the path within the DispatcherServlet mapping. */ + /* Cache the index and prefix of the path within the DispatcherServlet mapping */ private Integer indexLookupPath; + private String prefixLookupPath; + public ResourceUrlEncodingResponseWrapper(HttpServletRequest request, HttpServletResponse wrapped) { super(wrapped); this.request = request; @@ -71,30 +74,47 @@ public class ResourceUrlEncodingFilter extends OncePerRequestFilter { logger.debug("Request attribute exposing ResourceUrlProvider not found"); return super.encodeURL(url); } - initIndexLookupPath(resourceUrlProvider); - if (url.length() >= this.indexLookupPath) { - String prefix = url.substring(0, this.indexLookupPath); - String lookupPath = url.substring(this.indexLookupPath); + + initLookupPath(resourceUrlProvider); + if (url.startsWith(this.prefixLookupPath)) { + int suffixIndex = getQueryParamsIndex(url); + String suffix = url.substring(suffixIndex); + String lookupPath = url.substring(this.indexLookupPath, suffixIndex); lookupPath = resourceUrlProvider.getForLookupPath(lookupPath); if (lookupPath != null) { - return super.encodeURL(prefix + lookupPath); + return super.encodeURL(this.prefixLookupPath + lookupPath + suffix); } } + return super.encodeURL(url); } private ResourceUrlProvider getResourceUrlProvider() { - String name = ResourceUrlProviderExposingInterceptor.RESOURCE_URL_PROVIDER_ATTR; - return (ResourceUrlProvider) this.request.getAttribute(name); + return (ResourceUrlProvider) this.request.getAttribute( + ResourceUrlProviderExposingInterceptor.RESOURCE_URL_PROVIDER_ATTR); } - private void initIndexLookupPath(ResourceUrlProvider urlProvider) { + private void initLookupPath(ResourceUrlProvider urlProvider) { if (this.indexLookupPath == null) { String requestUri = urlProvider.getPathHelper().getRequestUri(this.request); String lookupPath = urlProvider.getPathHelper().getLookupPathForRequest(this.request); this.indexLookupPath = requestUri.lastIndexOf(lookupPath); + this.prefixLookupPath = requestUri.substring(0, this.indexLookupPath); + + if ("/".equals(lookupPath) && !"/".equals(requestUri)) { + String contextPath = urlProvider.getPathHelper().getContextPath(this.request); + if (requestUri.equals(contextPath)) { + this.indexLookupPath = requestUri.length(); + this.prefixLookupPath = requestUri; + } + } } } + + private int getQueryParamsIndex(String url) { + int index = url.indexOf("?"); + return (index > 0 ? index : url.length()); + } } } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/ResourceUrlProvider.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/ResourceUrlProvider.java index a79000d8..9ee0c5a8 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/ResourceUrlProvider.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/ResourceUrlProvider.java @@ -30,7 +30,7 @@ import org.apache.commons.logging.LogFactory; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationListener; import org.springframework.context.event.ContextRefreshedEvent; -import org.springframework.core.OrderComparator; +import org.springframework.core.annotation.AnnotationAwareOrderComparator; import org.springframework.util.AntPathMatcher; import org.springframework.util.PathMatcher; import org.springframework.web.servlet.handler.SimpleUrlHandlerMapping; @@ -140,7 +140,7 @@ public class ResourceUrlProvider implements ApplicationListener<ContextRefreshed Map<String, SimpleUrlHandlerMapping> map = appContext.getBeansOfType(SimpleUrlHandlerMapping.class); List<SimpleUrlHandlerMapping> handlerMappings = new ArrayList<SimpleUrlHandlerMapping>(map.values()); - OrderComparator.sort(handlerMappings); + AnnotationAwareOrderComparator.sort(handlerMappings); for (SimpleUrlHandlerMapping hm : handlerMappings) { for (String pattern : hm.getHandlerMap().keySet()) { @@ -171,11 +171,13 @@ public class ResourceUrlProvider implements ApplicationListener<ContextRefreshed if (logger.isTraceEnabled()) { logger.trace("Getting resource URL for request URL \"" + requestUrl + "\""); } - int index = getLookupPathIndex(request); - String prefix = requestUrl.substring(0, index); - String lookupPath = requestUrl.substring(index); + int prefixIndex = getLookupPathIndex(request); + int suffixIndex = getQueryParamsIndex(requestUrl); + String prefix = requestUrl.substring(0, prefixIndex); + String suffix = requestUrl.substring(suffixIndex); + String lookupPath = requestUrl.substring(prefixIndex, suffixIndex); String resolvedLookupPath = getForLookupPath(lookupPath); - return (resolvedLookupPath != null ? prefix + resolvedLookupPath : null); + return (resolvedLookupPath != null ? prefix + resolvedLookupPath + suffix : null); } private int getLookupPathIndex(HttpServletRequest request) { @@ -184,6 +186,11 @@ public class ResourceUrlProvider implements ApplicationListener<ContextRefreshed return requestUri.indexOf(lookupPath); } + private int getQueryParamsIndex(String lookupPath) { + int index = lookupPath.indexOf("?"); + return index > 0 ? index : lookupPath.length(); + } + /** * Compare the given path against configured resource handler mappings and * if a match is found use the {@code ResourceResolver} chain of the matched @@ -211,7 +218,7 @@ public class ResourceUrlProvider implements ApplicationListener<ContextRefreshed if (!matchingPatterns.isEmpty()) { Comparator<String> patternComparator = getPathMatcher().getPatternComparator(lookupPath); Collections.sort(matchingPatterns, patternComparator); - for(String pattern : matchingPatterns) { + for (String pattern : matchingPatterns) { String pathWithinMapping = getPathMatcher().extractPathWithinPattern(pattern, lookupPath); String pathMapping = lookupPath.substring(0, lookupPath.indexOf(pathWithinMapping)); if (logger.isTraceEnabled()) { diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/ResourceUrlProviderExposingInterceptor.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/ResourceUrlProviderExposingInterceptor.java index 0c69345e..3c55ec21 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/ResourceUrlProviderExposingInterceptor.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/ResourceUrlProviderExposingInterceptor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2014 the original author or authors. + * Copyright 2002-2015 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -34,20 +34,19 @@ public class ResourceUrlProviderExposingInterceptor extends HandlerInterceptorAd /** * Name of the request attribute that holds the {@link ResourceUrlProvider}. */ - public static final String RESOURCE_URL_PROVIDER_ATTR = ResourceUrlProvider.class.getName().toString(); - + public static final String RESOURCE_URL_PROVIDER_ATTR = ResourceUrlProvider.class.getName(); private final ResourceUrlProvider resourceUrlProvider; public ResourceUrlProviderExposingInterceptor(ResourceUrlProvider resourceUrlProvider) { - Assert.notNull(resourceUrlProvider, "'resourceUrlProvider' is required"); + Assert.notNull(resourceUrlProvider, "ResourceUrlProvider is required"); this.resourceUrlProvider = resourceUrlProvider; } @Override - public boolean preHandle(HttpServletRequest request, HttpServletResponse response, - Object handler) throws Exception { + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) + throws Exception { request.setAttribute(RESOURCE_URL_PROVIDER_ATTR, this.resourceUrlProvider); return true; diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/VersionResourceResolver.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/VersionResourceResolver.java index 45f831bf..f7eb7cb0 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/VersionResourceResolver.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/VersionResourceResolver.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * Copyright 2002-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,7 +16,13 @@ package org.springframework.web.servlet.resource; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.net.URI; +import java.net.URL; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.Comparator; import java.util.LinkedHashMap; @@ -24,6 +30,7 @@ import java.util.List; import java.util.Map; import javax.servlet.http.HttpServletRequest; +import org.springframework.core.io.AbstractResource; import org.springframework.core.io.Resource; import org.springframework.util.AntPathMatcher; import org.springframework.util.StringUtils; @@ -103,14 +110,26 @@ public class VersionResourceResolver extends AbstractResourceResolver { * fetched from a git commit sha, a property file, or environment variable * and set with SpEL expressions in the configuration (e.g. see {@code @Value} * in Java config). + * <p>If not done already, variants of the given {@code pathPatterns}, prefixed with + * the {@code version} will be also configured. For example, adding a {@code "/js/**"} path pattern + * will also cofigure automatically a {@code "/v1.0.0/js/**"} with {@code "v1.0.0"} the + * {@code version} String given as an argument. * @param version a version string * @param pathPatterns one or more resource URL path patterns * @return the current instance for chained method invocation * @see FixedVersionStrategy */ public VersionResourceResolver addFixedVersionStrategy(String version, String... pathPatterns) { - addVersionStrategy(new FixedVersionStrategy(version), pathPatterns); - return this; + List<String> patternsList = Arrays.asList(pathPatterns); + List<String> prefixedPatterns = new ArrayList<String>(pathPatterns.length); + String versionPrefix = "/" + version; + for (String pattern : patternsList) { + prefixedPatterns.add(pattern); + if (!pattern.startsWith(versionPrefix) && !patternsList.contains(versionPrefix + pattern)) { + prefixedPatterns.add(versionPrefix + pattern); + } + } + return addVersionStrategy(new FixedVersionStrategy(version), prefixedPatterns.toArray(new String[0])); } /** @@ -164,9 +183,9 @@ public class VersionResourceResolver extends AbstractResourceResolver { String actualVersion = versionStrategy.getResourceVersion(baseResource); if (candidateVersion.equals(actualVersion)) { if (logger.isTraceEnabled()) { - logger.trace("Resource matches extracted version ["+ candidateVersion + "]"); + logger.trace("Resource matches extracted version [" + candidateVersion + "]"); } - return baseResource; + return new FileNameVersionedResource(baseResource, candidateVersion); } else { if (logger.isTraceEnabled()) { @@ -218,4 +237,82 @@ public class VersionResourceResolver extends AbstractResourceResolver { return null; } + + private class FileNameVersionedResource extends AbstractResource implements VersionedResource { + + private final Resource original; + + private final String version; + + public FileNameVersionedResource(Resource original, String version) { + this.original = original; + this.version = version; + } + + @Override + public boolean exists() { + return this.original.exists(); + } + + @Override + public boolean isReadable() { + return this.original.isReadable(); + } + + @Override + public boolean isOpen() { + return this.original.isOpen(); + } + + @Override + public URL getURL() throws IOException { + return this.original.getURL(); + } + + @Override + public URI getURI() throws IOException { + return this.original.getURI(); + } + + @Override + public File getFile() throws IOException { + return this.original.getFile(); + } + + @Override + public String getFilename() { + return this.original.getFilename(); + } + + @Override + public long contentLength() throws IOException { + return this.original.contentLength(); + } + + @Override + public long lastModified() throws IOException { + return this.original.lastModified(); + } + + @Override + public Resource createRelative(String relativePath) throws IOException { + return this.original.createRelative(relativePath); + } + + @Override + public String getDescription() { + return original.getDescription(); + } + + @Override + public InputStream getInputStream() throws IOException { + return original.getInputStream(); + } + + @Override + public String getVersion() { + return this.version; + } + } + } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/VersionedResource.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/VersionedResource.java new file mode 100644 index 00000000..c780df8c --- /dev/null +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/VersionedResource.java @@ -0,0 +1,33 @@ +/* + * Copyright 2002-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.web.servlet.resource; + +import org.springframework.core.io.Resource; + +/** + * Interface for a resource descriptor that describes its version with a + * version string that can be derived from its content and/or metadata. + * + * @author Brian Clozel + * @since 4.2.5 + * @see VersionResourceResolver + */ +public interface VersionedResource extends Resource { + + String getVersion(); + +} diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/WebJarsResourceResolver.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/WebJarsResourceResolver.java new file mode 100644 index 00000000..8d08bbd7 --- /dev/null +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/resource/WebJarsResourceResolver.java @@ -0,0 +1,107 @@ +/* + * Copyright 2002-2015 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.web.servlet.resource; + +import java.util.List; +import javax.servlet.http.HttpServletRequest; + +import org.webjars.MultipleMatchesException; +import org.webjars.WebJarAssetLocator; + +import org.springframework.core.io.Resource; + +/** + * A {@code ResourceResolver} that delegates to the chain to locate a resource and then + * attempts to find a matching versioned resource contained in a WebJar JAR file. + * + * <p>This allows WebJars.org users to write version agnostic paths in their templates, + * like {@code <script src="/jquery/jquery.min.js"/>}. + * This path will be resolved to the unique version {@code <script src="/jquery/1.2.0/jquery.min.js"/>}, + * which is a better fit for HTTP caching and version management in applications. + * + * <p>This also resolves resources for version agnostic HTTP requests {@code "GET /jquery/jquery.min.js"}. + * + * <p>This resolver requires the "org.webjars:webjars-locator" library on classpath, + * and is automatically registered if that library is present. + * + * @author Brian Clozel + * @since 4.2 + * @see org.springframework.web.servlet.config.annotation.ResourceChainRegistration + * @see <a href="http://www.webjars.org">webjars.org</a> + */ +public class WebJarsResourceResolver extends AbstractResourceResolver { + + private final static String WEBJARS_LOCATION = "META-INF/resources/webjars"; + + private final static int WEBJARS_LOCATION_LENGTH = WEBJARS_LOCATION.length(); + + private final WebJarAssetLocator webJarAssetLocator = new WebJarAssetLocator(); + + + @Override + protected Resource resolveResourceInternal(HttpServletRequest request, String requestPath, + List<? extends Resource> locations, ResourceResolverChain chain) { + + Resource resolved = chain.resolveResource(request, requestPath, locations); + if (resolved == null) { + String webJarResourcePath = findWebJarResourcePath(requestPath); + if (webJarResourcePath != null) { + return chain.resolveResource(request, webJarResourcePath, locations); + } + } + return resolved; + } + + @Override + protected String resolveUrlPathInternal(String resourceUrlPath, + List<? extends Resource> locations, ResourceResolverChain chain) { + + String path = chain.resolveUrlPath(resourceUrlPath, locations); + if (path == null) { + String webJarResourcePath = findWebJarResourcePath(resourceUrlPath); + if (webJarResourcePath != null) { + return chain.resolveUrlPath(webJarResourcePath, locations); + } + } + return path; + } + + protected String findWebJarResourcePath(String path) { + try { + int startOffset = (path.startsWith("/") ? 1 : 0); + int endOffset = path.indexOf("/", 1); + if (endOffset != -1) { + String webjar = path.substring(startOffset, endOffset); + String partialPath = path.substring(endOffset); + String webJarPath = webJarAssetLocator.getFullPath(webjar, partialPath); + return webJarPath.substring(WEBJARS_LOCATION_LENGTH); + } + } + catch (MultipleMatchesException ex) { + if (logger.isWarnEnabled()) { + logger.warn("WebJar version conflict for \"" + path + "\"", ex); + } + } + catch (IllegalArgumentException ex) { + if (logger.isTraceEnabled()) { + logger.trace("No WebJar resource found for \"" + path + "\""); + } + } + return null; + } + +} diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/support/AbstractDispatcherServletInitializer.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/support/AbstractDispatcherServletInitializer.java index e7a969f9..3fe22004 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/support/AbstractDispatcherServletInitializer.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/support/AbstractDispatcherServletInitializer.java @@ -5,7 +5,7 @@ * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, @@ -25,12 +25,14 @@ import javax.servlet.ServletContext; import javax.servlet.ServletException; import javax.servlet.ServletRegistration; +import org.springframework.context.ApplicationContextInitializer; import org.springframework.core.Conventions; import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; import org.springframework.web.context.AbstractContextLoaderInitializer; import org.springframework.web.context.WebApplicationContext; import org.springframework.web.servlet.DispatcherServlet; +import org.springframework.web.servlet.FrameworkServlet; /** * Base class for {@link org.springframework.web.WebApplicationInitializer} @@ -51,6 +53,8 @@ import org.springframework.web.servlet.DispatcherServlet; * @author Arjen Poutsma * @author Chris Beams * @author Rossen Stoyanchev + * @author Juergen Hoeller + * @author Stephane Nicoll * @since 3.2 */ public abstract class AbstractDispatcherServletInitializer extends AbstractContextLoaderInitializer { @@ -74,7 +78,8 @@ public abstract class AbstractDispatcherServletInitializer extends AbstractConte * from {@link #createServletApplicationContext()}, and mapping it to the patterns * returned from {@link #getServletMappings()}. * <p>Further customization can be achieved by overriding {@link - * #customizeRegistration(ServletRegistration.Dynamic)}. + * #customizeRegistration(ServletRegistration.Dynamic)} or + * {@link #createDispatcherServlet(WebApplicationContext)}. * @param servletContext the context to register the servlet against */ protected void registerDispatcherServlet(ServletContext servletContext) { @@ -86,7 +91,9 @@ public abstract class AbstractDispatcherServletInitializer extends AbstractConte "createServletApplicationContext() did not return an application " + "context for servlet [" + servletName + "]"); - DispatcherServlet dispatcherServlet = new DispatcherServlet(servletAppContext); + FrameworkServlet dispatcherServlet = createDispatcherServlet(servletAppContext); + dispatcherServlet.setContextInitializers(getServletApplicationContextInitializers()); + ServletRegistration.Dynamic registration = servletContext.addServlet(servletName, dispatcherServlet); Assert.notNull(registration, "Failed to register servlet with name '" + servletName + "'." + @@ -126,6 +133,28 @@ public abstract class AbstractDispatcherServletInitializer extends AbstractConte protected abstract WebApplicationContext createServletApplicationContext(); /** + * Create a {@link DispatcherServlet} (or other kind of {@link FrameworkServlet}-derived + * dispatcher) with the specified {@link WebApplicationContext}. + * <p>Note: This allows for any {@link FrameworkServlet} subclass as of 4.2.3. + * Previously, it insisted on returning a {@link DispatcherServlet} or subclass thereof. + */ + protected FrameworkServlet createDispatcherServlet(WebApplicationContext servletAppContext) { + return new DispatcherServlet(servletAppContext); + } + + /** + * Specify application context initializers to be applied to the servlet-specific + * application context that the {@code DispatcherServlet} is being created with. + * @since 4.2 + * @see #createServletApplicationContext() + * @see DispatcherServlet#setContextInitializers + * @see #getRootApplicationContextInitializers() + */ + protected ApplicationContextInitializer<?>[] getServletApplicationContextInitializers() { + return null; + } + + /** * Specify the servlet mapping(s) for the {@code DispatcherServlet} — * for example {@code "/"}, {@code "/app"}, etc. * @see #registerDispatcherServlet(ServletContext) diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/support/AbstractFlashMapManager.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/support/AbstractFlashMapManager.java index 50366287..c859a230 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/support/AbstractFlashMapManager.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/support/AbstractFlashMapManager.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2014 the original author or authors. + * Copyright 2002-2015 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,11 +16,11 @@ package org.springframework.web.servlet.support; -import java.util.ArrayList; import java.util.Collections; import java.util.LinkedList; import java.util.List; import java.util.concurrent.CopyOnWriteArrayList; + import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; @@ -30,12 +30,13 @@ import org.apache.commons.logging.LogFactory; import org.springframework.util.Assert; import org.springframework.util.CollectionUtils; import org.springframework.util.MultiValueMap; -import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; import org.springframework.web.servlet.FlashMap; import org.springframework.web.servlet.FlashMapManager; +import org.springframework.web.util.UriComponents; import org.springframework.web.util.UrlPathHelper; + /** * A base class for {@link FlashMapManager} implementations. * @@ -172,10 +173,16 @@ public abstract class AbstractFlashMapManager implements FlashMapManager { return false; } } - MultiValueMap<String, String> targetParams = flashMap.getTargetRequestParams(); - for (String expectedName : targetParams.keySet()) { - for (String expectedValue : targetParams.get(expectedName)) { - if (!ObjectUtils.containsElement(request.getParameterValues(expectedName), expectedValue)) { + UriComponents uriComponents = ServletUriComponentsBuilder.fromRequest(request).build(); + MultiValueMap<String, String> actualParams = uriComponents.getQueryParams(); + MultiValueMap<String, String> expectedParams = flashMap.getTargetRequestParams(); + for (String expectedName : expectedParams.keySet()) { + List<String> actualValues = actualParams.get(expectedName); + if (actualValues == null) { + return false; + } + for (String expectedValue : expectedParams.get(expectedName)) { + if (!actualValues.contains(expectedValue)) { return false; } } @@ -191,7 +198,6 @@ public abstract class AbstractFlashMapManager implements FlashMapManager { String path = decodeAndNormalizePath(flashMap.getTargetRequestPath(), request); flashMap.setTargetRequestPath(path); - decodeParameters(flashMap.getTargetRequestParams(), request); if (logger.isDebugEnabled()) { logger.debug("Saving FlashMap=" + flashMap); @@ -227,17 +233,6 @@ public abstract class AbstractFlashMapManager implements FlashMapManager { return path; } - private void decodeParameters(MultiValueMap<String, String> params, HttpServletRequest request) { - for (String name : new ArrayList<String>(params.keySet())) { - for (String value : new ArrayList<String>(params.remove(name))) { - name = getUrlPathHelper().decodeRequestString(request, name); - value = getUrlPathHelper().decodeRequestString(request, value); - params.add(name, value); - } - } - } - - /** * Retrieve saved FlashMap instances from the underlying storage. * @param request the current request diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/support/RequestContext.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/support/RequestContext.java index ec328fb4..9fae8679 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/support/RequestContext.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/support/RequestContext.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2014 the original author or authors. + * Copyright 2002-2015 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -89,12 +89,6 @@ public class RequestContext { */ public static final String WEB_APPLICATION_CONTEXT_ATTRIBUTE = RequestContext.class.getName() + ".CONTEXT"; - /** - * The name of the bean to use to look up in an implementation of - * {@link RequestDataValueProcessor} has been configured. - */ - private static final String REQUEST_DATA_VALUE_PROCESSOR_BEAN_NAME = "requestDataValueProcessor"; - protected static final boolean jstlPresent = ClassUtils.isPresent("javax.servlet.jsp.jstl.core.Config", RequestContext.class.getClassLoader()); @@ -236,7 +230,11 @@ public class RequestContext { // ServletContext needs to be specified to be able to fall back to the root context! this.webApplicationContext = (WebApplicationContext) request.getAttribute(WEB_APPLICATION_CONTEXT_ATTRIBUTE); if (this.webApplicationContext == null) { - this.webApplicationContext = RequestContextUtils.getWebApplicationContext(request, servletContext); + this.webApplicationContext = RequestContextUtils.findWebApplicationContext(request, servletContext); + if (this.webApplicationContext == null) { + throw new IllegalStateException("No WebApplicationContext found: not in a DispatcherServlet " + + "request and no ContextLoaderListener registered?"); + } } // Determine locale to use for this RequestContext. @@ -271,9 +269,9 @@ public class RequestContext { this.urlPathHelper = new UrlPathHelper(); - if (this.webApplicationContext.containsBean(REQUEST_DATA_VALUE_PROCESSOR_BEAN_NAME)) { + if (this.webApplicationContext.containsBean(RequestContextUtils.REQUEST_DATA_VALUE_PROCESSOR_BEAN_NAME)) { this.requestDataValueProcessor = this.webApplicationContext.getBean( - REQUEST_DATA_VALUE_PROCESSOR_BEAN_NAME, RequestDataValueProcessor.class); + RequestContextUtils.REQUEST_DATA_VALUE_PROCESSOR_BEAN_NAME, RequestDataValueProcessor.class); } } @@ -493,11 +491,11 @@ public class RequestContext { /** * Is HTML escaping using the response encoding by default? * If enabled, only XML markup significant characters will be escaped with UTF-* encodings. - * <p>Falls back to {@code false} in case of no explicit default given. + * <p>Falls back to {@code true} in case of no explicit default given, as of Spring 4.2. * @since 4.1.2 */ public boolean isResponseEncodedHtmlEscape() { - return (this.responseEncodedHtmlEscape != null && this.responseEncodedHtmlEscape.booleanValue()); + return (this.responseEncodedHtmlEscape == null || this.responseEncodedHtmlEscape.booleanValue()); } /** diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/support/RequestContextUtils.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/support/RequestContextUtils.java index e9c25b93..ac6e0769 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/support/RequestContextUtils.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/support/RequestContextUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2013 the original author or authors. + * Copyright 2002-2015 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -27,6 +27,7 @@ import org.springframework.context.i18n.LocaleContext; import org.springframework.context.i18n.TimeZoneAwareLocaleContext; import org.springframework.ui.context.Theme; import org.springframework.ui.context.ThemeSource; +import org.springframework.web.context.ContextLoader; import org.springframework.web.context.WebApplicationContext; import org.springframework.web.context.support.WebApplicationContextUtils; import org.springframework.web.servlet.DispatcherServlet; @@ -52,15 +53,25 @@ import org.springframework.web.servlet.ThemeResolver; public abstract class RequestContextUtils { /** + * The name of the bean to use to look up in an implementation of + * {@link RequestDataValueProcessor} has been configured. + * @since 4.2.1 + */ + public static final String REQUEST_DATA_VALUE_PROCESSOR_BEAN_NAME = "requestDataValueProcessor"; + + + /** * Look for the WebApplicationContext associated with the DispatcherServlet * that has initiated request processing. * @param request current HTTP request * @return the request-specific web application context * @throws IllegalStateException if no servlet-specific context has been found + * @see #getWebApplicationContext(ServletRequest, ServletContext) + * @deprecated as of Spring 4.2.1, in favor of + * {@link #findWebApplicationContext(HttpServletRequest)} */ - public static WebApplicationContext getWebApplicationContext(ServletRequest request) - throws IllegalStateException { - + @Deprecated + public static WebApplicationContext getWebApplicationContext(ServletRequest request) throws IllegalStateException { return getWebApplicationContext(request, null); } @@ -76,7 +87,12 @@ public abstract class RequestContextUtils { * if no request-specific context has been found * @throws IllegalStateException if neither a servlet-specific nor a * global context has been found + * @see DispatcherServlet#WEB_APPLICATION_CONTEXT_ATTRIBUTE + * @see WebApplicationContextUtils#getRequiredWebApplicationContext(ServletContext) + * @deprecated as of Spring 4.2.1, in favor of + * {@link #findWebApplicationContext(HttpServletRequest, ServletContext)} */ + @Deprecated public static WebApplicationContext getWebApplicationContext( ServletRequest request, ServletContext servletContext) throws IllegalStateException { @@ -92,6 +108,57 @@ public abstract class RequestContextUtils { } /** + * Look for the WebApplicationContext associated with the DispatcherServlet + * that has initiated request processing, and for the global context if none + * was found associated with the current request. The global context will + * be found via the ServletContext or via ContextLoader's current context. + * <p>NOTE: This variant remains compatible with Servlet 2.5, explicitly + * checking a given ServletContext instead of deriving it from the request. + * @param request current HTTP request + * @param servletContext current servlet context + * @return the request-specific WebApplicationContext, or the global one + * if no request-specific context has been found, or {@code null} if none + * @since 4.2.1 + * @see DispatcherServlet#WEB_APPLICATION_CONTEXT_ATTRIBUTE + * @see WebApplicationContextUtils#getWebApplicationContext(ServletContext) + * @see ContextLoader#getCurrentWebApplicationContext() + */ + public static WebApplicationContext findWebApplicationContext( + HttpServletRequest request, ServletContext servletContext) { + + WebApplicationContext webApplicationContext = (WebApplicationContext) request.getAttribute( + DispatcherServlet.WEB_APPLICATION_CONTEXT_ATTRIBUTE); + if (webApplicationContext == null) { + if (servletContext != null) { + webApplicationContext = WebApplicationContextUtils.getWebApplicationContext(servletContext); + } + if (webApplicationContext == null) { + webApplicationContext = ContextLoader.getCurrentWebApplicationContext(); + } + } + return webApplicationContext; + } + + /** + * Look for the WebApplicationContext associated with the DispatcherServlet + * that has initiated request processing, and for the global context if none + * was found associated with the current request. The global context will + * be found via the ServletContext or via ContextLoader's current context. + * <p>NOTE: This variant requires Servlet 3.0+ and is generally recommended + * for forward-looking custom user code. + * @param request current HTTP request + * @return the request-specific WebApplicationContext, or the global one + * if no request-specific context has been found, or {@code null} if none + * @since 4.2.1 + * @see #findWebApplicationContext(HttpServletRequest, ServletContext) + * @see ServletRequest#getServletContext() + * @see ContextLoader#getCurrentWebApplicationContext() + */ + public static WebApplicationContext findWebApplicationContext(HttpServletRequest request) { + return findWebApplicationContext(request, request.getServletContext()); + } + + /** * Return the LocaleResolver that has been bound to the request by the * DispatcherServlet. * @param request current HTTP request diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/support/ServletUriComponentsBuilder.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/support/ServletUriComponentsBuilder.java index d0591ce2..74c804f4 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/support/ServletUriComponentsBuilder.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/support/ServletUriComponentsBuilder.java @@ -223,7 +223,7 @@ public class ServletUriComponentsBuilder extends UriComponentsBuilder { } @Override - protected Object clone() { + public Object clone() { return new ServletUriComponentsBuilder(this); } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/support/WebContentGenerator.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/support/WebContentGenerator.java index 087bcc0f..20cf89d2 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/support/WebContentGenerator.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/support/WebContentGenerator.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2014 the original author or authors. + * Copyright 2002-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,15 +18,18 @@ package org.springframework.web.servlet.support; import java.util.Arrays; import java.util.HashSet; +import java.util.LinkedHashSet; import java.util.Set; +import java.util.concurrent.TimeUnit; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; +import org.springframework.http.CacheControl; +import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; import org.springframework.web.HttpRequestMethodNotSupportedException; import org.springframework.web.HttpSessionRequiredException; -import org.springframework.web.context.request.WebRequest; import org.springframework.web.context.support.WebApplicationObjectSupport; /** @@ -36,13 +39,22 @@ import org.springframework.web.context.support.WebApplicationObjectSupport; * Can also be used for custom handlers that have their own * {@link org.springframework.web.servlet.HandlerAdapter}. * - * <p>Supports HTTP cache control options. The usage of corresponding - * HTTP headers can be controlled via the "useExpiresHeader", - * "useCacheControlHeader" and "useCacheControlNoStore" properties. + * <p>Supports HTTP cache control options. The usage of corresponding HTTP + * headers can be controlled via the {@link #setCacheSeconds "cacheSeconds"} + * and {@link #setCacheControl "cacheControl"} properties. + * + * <p><b>NOTE:</b> As of Spring 4.2, this generator's default behavior changed when + * using only {@link #setCacheSeconds}, sending HTTP response headers that are in line + * with current browsers and proxies implementations (i.e. no HTTP 1.0 headers anymore) + * Reverting to the previous behavior can be easily done by using one of the newly + * deprecated methods {@link #setUseExpiresHeader}, {@link #setUseCacheControlHeader}, + * {@link #setUseCacheControlNoStore} or {@link #setAlwaysMustRevalidate}. * * @author Rod Johnson * @author Juergen Hoeller + * @author Brian Clozel * @see #setCacheSeconds + * @see #setCacheControl * @see #setRequireSession */ public abstract class WebContentGenerator extends WebApplicationObjectSupport { @@ -56,21 +68,24 @@ public abstract class WebContentGenerator extends WebApplicationObjectSupport { /** HTTP method "POST" */ public static final String METHOD_POST = "POST"; - private static final String HEADER_PRAGMA = "Pragma"; private static final String HEADER_EXPIRES = "Expires"; - private static final String HEADER_CACHE_CONTROL = "Cache-Control"; + protected static final String HEADER_CACHE_CONTROL = "Cache-Control"; /** Set of supported HTTP methods */ - private Set<String> supportedMethods; + private Set<String> supportedMethods; private boolean requireSession = false; + private CacheControl cacheControl; + + private int cacheSeconds = -1; + /** Use HTTP 1.0 expires header? */ - private boolean useExpiresHeader = true; + private boolean useExpiresHeader = false; /** Use HTTP 1.1 cache-control header? */ private boolean useCacheControlHeader = true; @@ -78,8 +93,6 @@ public abstract class WebContentGenerator extends WebApplicationObjectSupport { /** Use HTTP 1.1 cache-control header value "no-store"? */ private boolean useCacheControlNoStore = true; - private int cacheSeconds = -1; - private boolean alwaysMustRevalidate = false; @@ -121,8 +134,8 @@ public abstract class WebContentGenerator extends WebApplicationObjectSupport { * unrestricted for general controllers and interceptors. */ public final void setSupportedMethods(String... methods) { - if (methods != null) { - this.supportedMethods = new HashSet<String>(Arrays.asList(methods)); + if (!ObjectUtils.isEmpty(methods)) { + this.supportedMethods = new LinkedHashSet<String>(Arrays.asList(methods)); } else { this.supportedMethods = null; @@ -151,17 +164,64 @@ public abstract class WebContentGenerator extends WebApplicationObjectSupport { } /** - * Set whether to use the HTTP 1.0 expires header. Default is "true". + * Set the {@link org.springframework.http.CacheControl} instance to build + * the Cache-Control HTTP response header. + * @since 4.2 + */ + public final void setCacheControl(CacheControl cacheControl) { + this.cacheControl = cacheControl; + } + + /** + * Get the {@link org.springframework.http.CacheControl} instance + * that builds the Cache-Control HTTP response header. + * @since 4.2 + */ + public final CacheControl getCacheControl() { + return this.cacheControl; + } + + /** + * Cache content for the given number of seconds, by writing + * cache-related HTTP headers to the response: + * <ul> + * <li>seconds == -1 (default value): no generation cache-related headers</li> + * <li>seconds == 0: "Cache-Control: no-store" will prevent caching</li> + * <li>seconds > 0: "Cache-Control: max-age=seconds" will ask to cache content</li> + * </ul> + * <p>For more specific needs, a custom {@link org.springframework.http.CacheControl} + * should be used. + * @see #setCacheControl + */ + public final void setCacheSeconds(int seconds) { + this.cacheSeconds = seconds; + } + + /** + * Return the number of seconds that content is cached. + */ + public final int getCacheSeconds() { + return this.cacheSeconds; + } + + /** + * Set whether to use the HTTP 1.0 expires header. Default is "false", + * as of 4.2. * <p>Note: Cache headers will only get applied if caching is enabled * (or explicitly prevented) for the current request. + * @deprecated as of 4.2, since going forward, the HTTP 1.1 cache-control + * header will be required, with the HTTP 1.0 headers disappearing */ + @Deprecated public final void setUseExpiresHeader(boolean useExpiresHeader) { this.useExpiresHeader = useExpiresHeader; } /** * Return whether the HTTP 1.0 expires header is used. + * @deprecated as of 4.2, in favor of {@link #getCacheControl()} */ + @Deprecated public final boolean isUseExpiresHeader() { return this.useExpiresHeader; } @@ -170,14 +230,19 @@ public abstract class WebContentGenerator extends WebApplicationObjectSupport { * Set whether to use the HTTP 1.1 cache-control header. Default is "true". * <p>Note: Cache headers will only get applied if caching is enabled * (or explicitly prevented) for the current request. + * @deprecated as of 4.2, since going forward, the HTTP 1.1 cache-control + * header will be required, with the HTTP 1.0 headers disappearing */ + @Deprecated public final void setUseCacheControlHeader(boolean useCacheControlHeader) { this.useCacheControlHeader = useCacheControlHeader; } /** * Return whether the HTTP 1.1 cache-control header is used. + * @deprecated as of 4.2, in favor of {@link #getCacheControl()} */ + @Deprecated public final boolean isUseCacheControlHeader() { return this.useCacheControlHeader; } @@ -185,123 +250,191 @@ public abstract class WebContentGenerator extends WebApplicationObjectSupport { /** * Set whether to use the HTTP 1.1 cache-control header value "no-store" * when preventing caching. Default is "true". + * @deprecated as of 4.2, in favor of {@link #setCacheControl} */ + @Deprecated public final void setUseCacheControlNoStore(boolean useCacheControlNoStore) { this.useCacheControlNoStore = useCacheControlNoStore; } /** * Return whether the HTTP 1.1 cache-control header value "no-store" is used. + * @deprecated as of 4.2, in favor of {@link #getCacheControl()} */ + @Deprecated public final boolean isUseCacheControlNoStore() { return this.useCacheControlNoStore; } /** - * An option to add 'must-revalidate' to every Cache-Control header. This - * may be useful with annotated controller methods, which can - * programmatically do a lastModified calculation as described in - * {@link WebRequest#checkNotModified(long)}. Default is "false", - * effectively relying on whether the handler implements - * {@link org.springframework.web.servlet.mvc.LastModified} or not. + * An option to add 'must-revalidate' to every Cache-Control header. + * This may be useful with annotated controller methods, which can + * programmatically do a last-modified calculation as described in + * {@link org.springframework.web.context.request.WebRequest#checkNotModified(long)}. + * <p>Default is "false". + * @deprecated as of 4.2, in favor of {@link #setCacheControl} */ - public void setAlwaysMustRevalidate(boolean mustRevalidate) { + @Deprecated + public final void setAlwaysMustRevalidate(boolean mustRevalidate) { this.alwaysMustRevalidate = mustRevalidate; } /** * Return whether 'must-revalidate' is added to every Cache-Control header. + * @deprecated as of 4.2, in favor of {@link #getCacheControl()} */ - public boolean isAlwaysMustRevalidate() { - return alwaysMustRevalidate; + @Deprecated + public final boolean isAlwaysMustRevalidate() { + return this.alwaysMustRevalidate; } + /** - * Cache content for the given number of seconds. Default is -1, - * indicating no generation of cache-related headers. - * <p>Only if this is set to 0 (no cache) or a positive value (cache for - * this many seconds) will this class generate cache headers. - * <p>The headers can be overwritten by subclasses, before content is generated. + * Check the given request for supported methods and a required session, if any. + * @param request current HTTP request + * @throws ServletException if the request cannot be handled because a check failed + * @since 4.2 */ - public final void setCacheSeconds(int seconds) { - this.cacheSeconds = seconds; + protected final void checkRequest(HttpServletRequest request) throws ServletException { + // Check whether we should support the request method. + String method = request.getMethod(); + if (this.supportedMethods != null && !this.supportedMethods.contains(method)) { + throw new HttpRequestMethodNotSupportedException( + method, StringUtils.toStringArray(this.supportedMethods)); + } + + // Check whether a session is required. + if (this.requireSession && request.getSession(false) == null) { + throw new HttpSessionRequiredException("Pre-existing session required but none found"); + } } /** - * Return the number of seconds that content is cached. + * Prepare the given response according to the settings of this generator. + * Applies the number of cache seconds specified for this generator. + * @param response current HTTP response + * @since 4.2 */ - public final int getCacheSeconds() { - return this.cacheSeconds; + protected final void prepareResponse(HttpServletResponse response) { + if (this.cacheControl != null) { + applyCacheControl(response, this.cacheControl); + } + else { + applyCacheSeconds(response, this.cacheSeconds); + } } - /** - * Check and prepare the given request and response according to the settings - * of this generator. Checks for supported methods and a required session, - * and applies the number of cache seconds specified for this generator. - * @param request current HTTP request + * Set the HTTP Cache-Control header according to the given settings. * @param response current HTTP response - * @param lastModified if the mapped handler provides Last-Modified support - * @throws ServletException if the request cannot be handled because a check failed + * @param cacheControl the pre-configured cache control settings + * @since 4.2 */ - protected final void checkAndPrepare( - HttpServletRequest request, HttpServletResponse response, boolean lastModified) - throws ServletException { - - checkAndPrepare(request, response, this.cacheSeconds, lastModified); + protected final void applyCacheControl(HttpServletResponse response, CacheControl cacheControl) { + String ccValue = cacheControl.getHeaderValue(); + if (ccValue != null) { + // Set computed HTTP 1.1 Cache-Control header + response.setHeader(HEADER_CACHE_CONTROL, ccValue); + + if (response.containsHeader(HEADER_PRAGMA)) { + // Reset HTTP 1.0 Pragma header if present + response.setHeader(HEADER_PRAGMA, ""); + } + if (response.containsHeader(HEADER_EXPIRES)) { + // Reset HTTP 1.0 Expires header if present + response.setHeader(HEADER_EXPIRES, ""); + } + } } /** - * Check and prepare the given request and response according to the settings - * of this generator. Checks for supported methods and a required session, - * and applies the given number of cache seconds. - * @param request current HTTP request + * Apply the given cache seconds and generate corresponding HTTP headers, + * i.e. allow caching for the given number of seconds in case of a positive + * value, prevent caching if given a 0 value, do nothing else. + * Does not tell the browser to revalidate the resource. * @param response current HTTP response * @param cacheSeconds positive number of seconds into the future that the * response should be cacheable for, 0 to prevent caching - * @param lastModified if the mapped handler provides Last-Modified support - * @throws ServletException if the request cannot be handled because a check failed */ - protected final void checkAndPrepare( - HttpServletRequest request, HttpServletResponse response, int cacheSeconds, boolean lastModified) - throws ServletException { - - // Check whether we should support the request method. - String method = request.getMethod(); - if (this.supportedMethods != null && !this.supportedMethods.contains(method)) { - throw new HttpRequestMethodNotSupportedException( - method, StringUtils.toStringArray(this.supportedMethods)); + @SuppressWarnings("deprecation") + protected final void applyCacheSeconds(HttpServletResponse response, int cacheSeconds) { + if (this.useExpiresHeader || !this.useCacheControlHeader) { + // Deprecated HTTP 1.0 cache behavior, as in previous Spring versions + if (cacheSeconds > 0) { + cacheForSeconds(response, cacheSeconds); + } + else if (cacheSeconds == 0) { + preventCaching(response); + } } - - // Check whether a session is required. - if (this.requireSession) { - if (request.getSession(false) == null) { - throw new HttpSessionRequiredException("Pre-existing session required but none found"); + else { + CacheControl cControl; + if (cacheSeconds > 0) { + cControl = CacheControl.maxAge(cacheSeconds, TimeUnit.SECONDS); + if (this.alwaysMustRevalidate) { + cControl = cControl.mustRevalidate(); + } + } + else if (cacheSeconds == 0) { + cControl = (this.useCacheControlNoStore ? CacheControl.noStore() : CacheControl.noCache()); } + else { + cControl = CacheControl.empty(); + } + applyCacheControl(response, cControl); } + } + - // Do declarative cache control. - // Revalidate if the controller supports last-modified. - applyCacheSeconds(response, cacheSeconds, lastModified); + /** + * @see #checkRequest(HttpServletRequest) + * @see #prepareResponse(HttpServletResponse) + * @deprecated as of 4.2, since the {@code lastModified} flag is effectively ignored, + * with a must-revalidate header only generated if explicitly configured + */ + @Deprecated + protected final void checkAndPrepare( + HttpServletRequest request, HttpServletResponse response, boolean lastModified) throws ServletException { + + checkRequest(request); + prepareResponse(response); } /** - * Prevent the response from being cached. - * See {@code http://www.mnot.net/cache_docs}. + * @see #checkRequest(HttpServletRequest) + * @see #applyCacheSeconds(HttpServletResponse, int) + * @deprecated as of 4.2, since the {@code lastModified} flag is effectively ignored, + * with a must-revalidate header only generated if explicitly configured */ - protected final void preventCaching(HttpServletResponse response) { - response.setHeader(HEADER_PRAGMA, "no-cache"); - if (this.useExpiresHeader) { - // HTTP 1.0 header - response.setDateHeader(HEADER_EXPIRES, 1L); + @Deprecated + protected final void checkAndPrepare( + HttpServletRequest request, HttpServletResponse response, int cacheSeconds, boolean lastModified) + throws ServletException { + + checkRequest(request); + applyCacheSeconds(response, cacheSeconds); + } + + /** + * Apply the given cache seconds and generate respective HTTP headers. + * <p>That is, allow caching for the given number of seconds in the + * case of a positive value, prevent caching if given a 0 value, else + * do nothing (i.e. leave caching to the client). + * @param response the current HTTP response + * @param cacheSeconds the (positive) number of seconds into the future + * that the response should be cacheable for; 0 to prevent caching; and + * a negative value to leave caching to the client. + * @param mustRevalidate whether the client should revalidate the resource + * (typically only necessary for controllers with last-modified support) + * @deprecated as of 4.2, in favor of {@link #applyCacheControl} + */ + @Deprecated + protected final void applyCacheSeconds(HttpServletResponse response, int cacheSeconds, boolean mustRevalidate) { + if (cacheSeconds > 0) { + cacheForSeconds(response, cacheSeconds, mustRevalidate); } - if (this.useCacheControlHeader) { - // HTTP 1.1 header: "no-cache" is the standard value, - // "no-store" is necessary to prevent caching on FireFox. - response.setHeader(HEADER_CACHE_CONTROL, "no-cache"); - if (this.useCacheControlNoStore) { - response.addHeader(HEADER_CACHE_CONTROL, "no-store"); - } + else if (cacheSeconds == 0) { + preventCaching(response); } } @@ -311,8 +444,9 @@ public abstract class WebContentGenerator extends WebApplicationObjectSupport { * @param response current HTTP response * @param seconds number of seconds into the future that the response * should be cacheable for - * @see #cacheForSeconds(javax.servlet.http.HttpServletResponse, int, boolean) + * @deprecated as of 4.2, in favor of {@link #applyCacheControl} */ + @Deprecated protected final void cacheForSeconds(HttpServletResponse response, int seconds) { cacheForSeconds(response, seconds, false); } @@ -326,12 +460,19 @@ public abstract class WebContentGenerator extends WebApplicationObjectSupport { * should be cacheable for * @param mustRevalidate whether the client should revalidate the resource * (typically only necessary for controllers with last-modified support) + * @deprecated as of 4.2, in favor of {@link #applyCacheControl} */ + @Deprecated protected final void cacheForSeconds(HttpServletResponse response, int seconds, boolean mustRevalidate) { if (this.useExpiresHeader) { // HTTP 1.0 header response.setDateHeader(HEADER_EXPIRES, System.currentTimeMillis() + seconds * 1000L); } + else if (response.containsHeader(HEADER_EXPIRES)) { + // Reset HTTP 1.0 Expires header if present + response.setHeader(HEADER_EXPIRES, ""); + } + if (this.useCacheControlHeader) { // HTTP 1.1 header String headerValue = "max-age=" + seconds; @@ -340,42 +481,36 @@ public abstract class WebContentGenerator extends WebApplicationObjectSupport { } response.setHeader(HEADER_CACHE_CONTROL, headerValue); } - } - /** - * Apply the given cache seconds and generate corresponding HTTP headers, - * i.e. allow caching for the given number of seconds in case of a positive - * value, prevent caching if given a 0 value, do nothing else. - * Does not tell the browser to revalidate the resource. - * @param response current HTTP response - * @param seconds positive number of seconds into the future that the - * response should be cacheable for, 0 to prevent caching - * @see #cacheForSeconds(javax.servlet.http.HttpServletResponse, int, boolean) - */ - protected final void applyCacheSeconds(HttpServletResponse response, int seconds) { - applyCacheSeconds(response, seconds, false); + if (response.containsHeader(HEADER_PRAGMA)) { + // Reset HTTP 1.0 Pragma header if present + response.setHeader(HEADER_PRAGMA, ""); + } } /** - * Apply the given cache seconds and generate respective HTTP headers. - * <p>That is, allow caching for the given number of seconds in the - * case of a positive value, prevent caching if given a 0 value, else - * do nothing (i.e. leave caching to the client). - * @param response the current HTTP response - * @param seconds the (positive) number of seconds into the future that - * the response should be cacheable for; 0 to prevent caching; and - * a negative value to leave caching to the client. - * @param mustRevalidate whether the client should revalidate the resource - * (typically only necessary for controllers with last-modified support) + * Prevent the response from being cached. + * Only called in HTTP 1.0 compatibility mode. + * <p>See {@code http://www.mnot.net/cache_docs}. + * @deprecated as of 4.2, in favor of {@link #applyCacheControl} */ - protected final void applyCacheSeconds(HttpServletResponse response, int seconds, boolean mustRevalidate) { - if (seconds > 0) { - cacheForSeconds(response, seconds, mustRevalidate); + @Deprecated + protected final void preventCaching(HttpServletResponse response) { + response.setHeader(HEADER_PRAGMA, "no-cache"); + + if (this.useExpiresHeader) { + // HTTP 1.0 Expires header + response.setDateHeader(HEADER_EXPIRES, 1L); } - else if (seconds == 0) { - preventCaching(response); + + if (this.useCacheControlHeader) { + // HTTP 1.1 Cache-Control header: "no-cache" is the standard value, + // "no-store" is necessary to prevent caching on Firefox. + response.setHeader(HEADER_CACHE_CONTROL, "no-cache"); + if (this.useCacheControlNoStore) { + response.addHeader(HEADER_CACHE_CONTROL, "no-store"); + } } - // Leave caching to the client otherwise. } } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/tags/TransformTag.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/tags/TransformTag.java index 05d662b4..6cb54379 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/tags/TransformTag.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/tags/TransformTag.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2013 the original author or authors. + * Copyright 2002-2015 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,7 +21,6 @@ import java.io.IOException; import javax.servlet.jsp.JspException; import javax.servlet.jsp.tagext.TagSupport; -import org.springframework.web.util.HtmlUtils; import org.springframework.web.util.TagUtils; /** diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/tags/UrlTag.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/tags/UrlTag.java index 622846af..201a5fb4 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/tags/UrlTag.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/tags/UrlTag.java @@ -208,7 +208,12 @@ public class UrlTag extends HtmlEscapingAwareTag implements ParamAware { url.append(request.getContextPath()); } else { - url.append(this.context); + if (this.context.endsWith("/")) { + url.append(this.context.substring(0, this.context.length() - 1)); + } + else { + url.append(this.context); + } } } if (this.type != UrlType.RELATIVE && this.type != UrlType.ABSOLUTE && !this.value.startsWith("/")) { diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/tags/form/AbstractMultiCheckedElementTag.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/tags/form/AbstractMultiCheckedElementTag.java index cb9c6963..58d4cc79 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/tags/form/AbstractMultiCheckedElementTag.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/tags/form/AbstractMultiCheckedElementTag.java @@ -149,7 +149,7 @@ public abstract class AbstractMultiCheckedElementTag extends AbstractCheckedElem /** * Set the HTML element used to enclose the * '{@code input type="checkbox/radio"}' tag. - * <p>Defaults to an HTML '{@code <span/>}' tag. + * <p>Defaults to an HTML '{@code <span/>}' tag. */ public void setElement(String element) { Assert.hasText(element, "'element' cannot be null or blank"); diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/tags/form/CheckboxTag.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/tags/form/CheckboxTag.java index c7abcaa4..ebde41ea 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/tags/form/CheckboxTag.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/tags/form/CheckboxTag.java @@ -71,7 +71,7 @@ public class CheckboxTag extends AbstractSingleCheckedElementTag { Object boundValue = getBoundValue(); Class<?> valueType = getBindStatus().getValueType(); - if (Boolean.class.equals(valueType) || boolean.class.equals(valueType)) { + if (Boolean.class == valueType || boolean.class == valueType) { // the concrete type may not be a Boolean - can be String if (boundValue instanceof String) { boundValue = Boolean.valueOf((String) boundValue); diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/tags/form/ErrorsTag.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/tags/form/ErrorsTag.java index c05a4468..1673f18f 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/tags/form/ErrorsTag.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/tags/form/ErrorsTag.java @@ -72,7 +72,7 @@ public class ErrorsTag extends AbstractHtmlElementBodyTag implements BodyTag { /** * Set the HTML element must be used to render the error messages. - * <p>Defaults to an HTML '{@code <span/>}' tag. + * <p>Defaults to an HTML '{@code <span/>}' tag. */ public void setElement(String element) { Assert.hasText(element, "'element' cannot be null or blank"); @@ -88,7 +88,7 @@ public class ErrorsTag extends AbstractHtmlElementBodyTag implements BodyTag { /** * Set the delimiter to be used between error messages. - * <p>Defaults to an HTML '{@code <br/>}' tag. + * <p>Defaults to an HTML '{@code <br/>}' tag. */ public void setDelimiter(String delimiter) { this.delimiter = delimiter; @@ -105,7 +105,7 @@ public class ErrorsTag extends AbstractHtmlElementBodyTag implements BodyTag { /** * Get the value for the HTML '{@code id}' attribute. * <p>Appends '{@code .errors}' to the value returned by {@link #getPropertyPath()} - * or to the model attribute name if the {@code <form:errors/>} tag's + * or to the model attribute name if the {@code <form:errors/>} tag's * '{@code path}' attribute has been omitted. * @return the value for the HTML '{@code id}' attribute * @see #getPropertyPath() diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/tags/form/FormTag.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/tags/form/FormTag.java index 5e3a5404..5aa9d232 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/tags/form/FormTag.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/tags/form/FormTag.java @@ -325,7 +325,19 @@ public class FormTag extends AbstractHtmlElementTag { /** * Get the name of the request param for non-browser supported HTTP methods. + * @since 4.2.3 */ + @SuppressWarnings("deprecation") + protected String getMethodParam() { + return getMethodParameter(); + } + + /** + * Get the name of the request param for non-browser supported HTTP methods. + * @deprecated as of 4.2.3, in favor of {@link #getMethodParam()} which is + * a proper pairing for {@link #setMethodParam(String)} + */ + @Deprecated protected String getMethodParameter() { return this.methodParam; } @@ -363,7 +375,7 @@ public class FormTag extends AbstractHtmlElementTag { if (!isMethodBrowserSupported(getMethod())) { assertHttpMethod(getMethod()); - String inputName = getMethodParameter(); + String inputName = getMethodParam(); String inputType = "hidden"; tagWriter.startTag(INPUT_TAG); writeOptionalAttribute(tagWriter, TYPE_ATTRIBUTE, inputType); diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/tags/form/OptionTag.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/tags/form/OptionTag.java index 2e97f9e0..36e381d0 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/tags/form/OptionTag.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/tags/form/OptionTag.java @@ -76,12 +76,12 @@ public class OptionTag extends AbstractHtmlElementBodyTag implements BodyTag { /** - * The 'value' attribute of the rendered HTML {@code <option>} tag. + * The 'value' attribute of the rendered HTML {@code <option>} tag. */ private Object value; /** - * The text body of the rendered HTML {@code <option>} tag. + * The text body of the rendered HTML {@code <option>} tag. */ private String label; @@ -93,14 +93,14 @@ public class OptionTag extends AbstractHtmlElementBodyTag implements BodyTag { /** - * Set the 'value' attribute of the rendered HTML {@code <option>} tag. + * Set the 'value' attribute of the rendered HTML {@code <option>} tag. */ public void setValue(Object value) { this.value = value; } /** - * Get the 'value' attribute of the rendered HTML {@code <option>} tag. + * Get the 'value' attribute of the rendered HTML {@code <option>} tag. */ protected Object getValue() { return this.value; @@ -121,7 +121,7 @@ public class OptionTag extends AbstractHtmlElementBodyTag implements BodyTag { } /** - * Set the text body of the rendered HTML {@code <option>} tag. + * Set the text body of the rendered HTML {@code <option>} tag. * <p>May be a runtime expression. */ public void setLabel(String label) { @@ -130,7 +130,7 @@ public class OptionTag extends AbstractHtmlElementBodyTag implements BodyTag { } /** - * Get the text body of the rendered HTML {@code <option>} tag. + * Get the text body of the rendered HTML {@code <option>} tag. */ protected String getLabel() { return this.label; diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/tags/form/TextareaTag.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/tags/form/TextareaTag.java index 09539fbb..16987862 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/tags/form/TextareaTag.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/tags/form/TextareaTag.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 the original author or authors. + * Copyright 2002-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -98,7 +98,7 @@ public class TextareaTag extends AbstractHtmlInputElementTag { writeOptionalAttribute(tagWriter, COLS_ATTRIBUTE, getCols()); writeOptionalAttribute(tagWriter, ONSELECT_ATTRIBUTE, getOnselect()); String value = getDisplayString(getBoundValue(), getPropertyEditor()); - tagWriter.appendValue(processFieldValue(getName(), value, "textarea")); + tagWriter.appendValue("\r\n" + processFieldValue(getName(), value, "textarea")); tagWriter.endTag(); return SKIP_BODY; } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/view/ContentNegotiatingViewResolver.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/view/ContentNegotiatingViewResolver.java index 2b902c36..511cd4c4 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/view/ContentNegotiatingViewResolver.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/view/ContentNegotiatingViewResolver.java @@ -23,17 +23,15 @@ import java.util.LinkedHashSet; import java.util.List; import java.util.Locale; import java.util.Map; -import java.util.Properties; import java.util.Set; -import javax.activation.FileTypeMap; import javax.servlet.ServletContext; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.springframework.beans.factory.BeanFactoryUtils; import org.springframework.beans.factory.InitializingBean; -import org.springframework.core.OrderComparator; import org.springframework.core.Ordered; +import org.springframework.core.annotation.AnnotationAwareOrderComparator; import org.springframework.http.MediaType; import org.springframework.util.Assert; import org.springframework.util.CollectionUtils; @@ -93,7 +91,7 @@ public class ContentNegotiatingViewResolver extends WebApplicationObjectSupport private ContentNegotiationManager contentNegotiationManager; - private final ContentNegotiationManagerFactoryBean cnManagerFactoryBean = new ContentNegotiationManagerFactoryBean(); + private final ContentNegotiationManagerFactoryBean cnmFactoryBean = new ContentNegotiationManagerFactoryBean(); private boolean useNotAcceptableStatusCode = false; @@ -130,90 +128,6 @@ public class ContentNegotiatingViewResolver extends WebApplicationObjectSupport } /** - * Indicate whether the extension of the request path should be used to determine the requested media type, - * in favor of looking at the {@code Accept} header. The default value is {@code true}. - * <p>For instance, when this flag is {@code true} (the default), a request for {@code /hotels.pdf} - * will result in an {@code AbstractPdfView} being resolved, while the {@code Accept} header can be the - * browser-defined {@code text/html,application/xhtml+xml}. - * @deprecated use {@link #setContentNegotiationManager(ContentNegotiationManager)} - */ - @Deprecated - public void setFavorPathExtension(boolean favorPathExtension) { - this.cnManagerFactoryBean.setFavorPathExtension(favorPathExtension); - } - - /** - * Indicate whether to use the Java Activation Framework to map from file extensions to media types. - * <p>Default is {@code true}, i.e. the Java Activation Framework is used (if available). - * @deprecated use {@link #setContentNegotiationManager(ContentNegotiationManager)} - */ - @Deprecated - public void setUseJaf(boolean useJaf) { - this.cnManagerFactoryBean.setUseJaf(useJaf); - } - - /** - * Indicate whether a request parameter should be used to determine the requested media type, - * in favor of looking at the {@code Accept} header. The default value is {@code false}. - * <p>For instance, when this flag is {@code true}, a request for {@code /hotels?format=pdf} will result - * in an {@code AbstractPdfView} being resolved, while the {@code Accept} header can be the browser-defined - * {@code text/html,application/xhtml+xml}. - * @deprecated use {@link #setContentNegotiationManager(ContentNegotiationManager)} - */ - @Deprecated - public void setFavorParameter(boolean favorParameter) { - this.cnManagerFactoryBean.setFavorParameter(favorParameter); - } - - /** - * Set the parameter name that can be used to determine the requested media type if the {@link - * #setFavorParameter} property is {@code true}. The default parameter name is {@code format}. - * @deprecated use {@link #setContentNegotiationManager(ContentNegotiationManager)} - */ - @Deprecated - public void setParameterName(String parameterName) { - this.cnManagerFactoryBean.setParameterName(parameterName); - } - - /** - * Indicate whether the HTTP {@code Accept} header should be ignored. Default is {@code false}. - * <p>If set to {@code true}, this view resolver will only refer to the file extension and/or - * parameter, as indicated by the {@link #setFavorPathExtension favorPathExtension} and - * {@link #setFavorParameter favorParameter} properties. - * @deprecated use {@link #setContentNegotiationManager(ContentNegotiationManager)} - */ - @Deprecated - public void setIgnoreAcceptHeader(boolean ignoreAcceptHeader) { - this.cnManagerFactoryBean.setIgnoreAcceptHeader(ignoreAcceptHeader); - } - - /** - * Set the mapping from file extensions to media types. - * <p>When this mapping is not set or when an extension is not present, this view resolver - * will fall back to using a {@link FileTypeMap} when the Java Action Framework is available. - * @deprecated use {@link #setContentNegotiationManager(ContentNegotiationManager)} - */ - @Deprecated - public void setMediaTypes(Map<String, String> mediaTypes) { - if (mediaTypes != null) { - Properties props = new Properties(); - props.putAll(mediaTypes); - this.cnManagerFactoryBean.setMediaTypes(props); - } - } - - /** - * Set the default content type. - * <p>This content type will be used when file extension, parameter, nor {@code Accept} - * header define a content-type, either through being disabled or empty. - * @deprecated use {@link #setContentNegotiationManager(ContentNegotiationManager)} - */ - @Deprecated - public void setDefaultContentType(MediaType defaultContentType) { - this.cnManagerFactoryBean.setDefaultContentType(defaultContentType); - } - - /** * Indicate whether a {@link HttpServletResponse#SC_NOT_ACCEPTABLE 406 Not Acceptable} * status code should be returned if no suitable view can be found. * <p>Default is {@code false}, meaning that this view resolver returns {@code null} for @@ -284,15 +198,15 @@ public class ContentNegotiatingViewResolver extends WebApplicationObjectSupport logger.warn("Did not find any ViewResolvers to delegate to; please configure them using the " + "'viewResolvers' property on the ContentNegotiatingViewResolver"); } - OrderComparator.sort(this.viewResolvers); - this.cnManagerFactoryBean.setServletContext(servletContext); + AnnotationAwareOrderComparator.sort(this.viewResolvers); + this.cnmFactoryBean.setServletContext(servletContext); } @Override public void afterPropertiesSet() { if (this.contentNegotiationManager == null) { - this.cnManagerFactoryBean.afterPropertiesSet(); - this.contentNegotiationManager = this.cnManagerFactoryBean.getObject(); + this.cnmFactoryBean.afterPropertiesSet(); + this.contentNegotiationManager = this.cnmFactoryBean.getObject(); } } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/view/InternalResourceViewResolver.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/view/InternalResourceViewResolver.java index eb9a1bf5..679e8a95 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/view/InternalResourceViewResolver.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/view/InternalResourceViewResolver.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2014 the original author or authors. + * Copyright 2002-2015 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -60,7 +60,7 @@ public class InternalResourceViewResolver extends UrlBasedViewResolver { */ public InternalResourceViewResolver() { Class<?> viewClass = requiredViewClass(); - if (viewClass.equals(InternalResourceView.class) && jstlPresent) { + if (InternalResourceView.class == viewClass && jstlPresent) { viewClass = JstlView.class; } setViewClass(viewClass); diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/view/RedirectView.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/view/RedirectView.java index c3a4e380..0a188696 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/view/RedirectView.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/view/RedirectView.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2014 the original author or authors. + * Copyright 2002-2015 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -36,14 +36,12 @@ import org.springframework.http.HttpStatus; import org.springframework.util.CollectionUtils; import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; -import org.springframework.web.context.ContextLoader; import org.springframework.web.context.WebApplicationContext; import org.springframework.web.servlet.FlashMap; import org.springframework.web.servlet.FlashMapManager; import org.springframework.web.servlet.HandlerMapping; import org.springframework.web.servlet.SmartView; import org.springframework.web.servlet.View; -import org.springframework.web.servlet.support.RequestContext; import org.springframework.web.servlet.support.RequestContextUtils; import org.springframework.web.servlet.support.RequestDataValueProcessor; import org.springframework.web.util.UriComponents; @@ -299,7 +297,7 @@ public class RedirectView extends AbstractUrlBasedView implements SmartView { } /** - * Creates the target URL by checking if the redirect string is a URI template first, + * Create the target URL by checking if the redirect string is a URI template first, * expanding it with the given model, and then optionally appending simple type model * attributes as query String parameters. */ @@ -554,27 +552,23 @@ public class RedirectView extends AbstractUrlBasedView implements SmartView { /** * Find the registered {@link RequestDataValueProcessor}, if any, and allow * it to update the redirect target URL. + * @param targetUrl the given redirect URL * @return the updated URL or the same as URL as the one passed in */ protected String updateTargetUrl(String targetUrl, Map<String, Object> model, HttpServletRequest request, HttpServletResponse response) { - RequestContext requestContext = null; - if (getWebApplicationContext() != null) { - requestContext = createRequestContext(request, response, model); + WebApplicationContext wac = getWebApplicationContext(); + if (wac == null) { + wac = RequestContextUtils.findWebApplicationContext(request, getServletContext()); } - else { - WebApplicationContext wac = ContextLoader.getCurrentWebApplicationContext(); - if (wac != null && wac.getServletContext() != null) { - requestContext = new RequestContext(request, response, wac.getServletContext(), model); - } - } - if (requestContext != null) { - RequestDataValueProcessor processor = requestContext.getRequestDataValueProcessor(); - if (processor != null) { - targetUrl = processor.processUrl(request, targetUrl); - } + + if (wac != null && wac.containsBean(RequestContextUtils.REQUEST_DATA_VALUE_PROCESSOR_BEAN_NAME)) { + RequestDataValueProcessor processor = wac.getBean( + RequestContextUtils.REQUEST_DATA_VALUE_PROCESSOR_BEAN_NAME, RequestDataValueProcessor.class); + return processor.processUrl(request, targetUrl); } + return targetUrl; } @@ -591,10 +585,15 @@ public class RedirectView extends AbstractUrlBasedView implements SmartView { String encodedRedirectURL = response.encodeRedirectURL(targetUrl); if (http10Compatible) { + HttpStatus attributeStatusCode = (HttpStatus) request.getAttribute(View.RESPONSE_STATUS_ATTRIBUTE); if (this.statusCode != null) { response.setStatus(this.statusCode.value()); response.setHeader("Location", encodedRedirectURL); } + else if (attributeStatusCode != null) { + response.setStatus(attributeStatusCode.value()); + response.setHeader("Location", encodedRedirectURL); + } else { // Send status code 302 by default. response.sendRedirect(encodedRedirectURL); diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/view/document/AbstractExcelView.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/view/document/AbstractExcelView.java index 425ac888..c5a1d619 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/view/document/AbstractExcelView.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/view/document/AbstractExcelView.java @@ -91,7 +91,10 @@ import org.springframework.web.servlet.view.AbstractView; * @author Jean-Pierre Pawlak * @author Juergen Hoeller * @see AbstractPdfView + * @deprecated as of Spring 4.2, in favor of {@link AbstractXlsView} and its + * {@link AbstractXlsxView} and {@link AbstractXlsxStreamingView} variants */ +@Deprecated public abstract class AbstractExcelView extends AbstractView { /** The content type for an Excel response */ diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/view/document/AbstractXlsView.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/view/document/AbstractXlsView.java new file mode 100644 index 00000000..4279bb3e --- /dev/null +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/view/document/AbstractXlsView.java @@ -0,0 +1,120 @@ +/* + * Copyright 2002-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.web.servlet.view.document; + +import java.io.Closeable; +import java.io.IOException; +import java.util.Map; +import javax.servlet.ServletOutputStream; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.apache.poi.hssf.usermodel.HSSFWorkbook; +import org.apache.poi.ss.usermodel.Workbook; + +import org.springframework.web.servlet.view.AbstractView; + +/** + * Convenient superclass for Excel document views in traditional XLS format. + * Compatible with Apache POI 3.5 and higher. + * + * <p>For working with the workbook in the subclass, see + * <a href="http://poi.apache.org">Apache's POI site</a> + * + * @author Juergen Hoeller + * @since 4.2 + */ +public abstract class AbstractXlsView extends AbstractView { + + /** + * Default Constructor. + * Sets the content type of the view to "application/vnd.ms-excel". + */ + public AbstractXlsView() { + setContentType("application/vnd.ms-excel"); + } + + + @Override + protected boolean generatesDownloadContent() { + return true; + } + + /** + * Renders the Excel view, given the specified model. + */ + @Override + protected final void renderMergedOutputModel( + Map<String, Object> model, HttpServletRequest request, HttpServletResponse response) throws Exception { + + // Create a fresh workbook instance for this render step. + Workbook workbook = createWorkbook(model, request); + + // Delegate to application-provided document code. + buildExcelDocument(model, workbook, request, response); + + // Set the content type. + response.setContentType(getContentType()); + + // Flush byte array to servlet output stream. + renderWorkbook(workbook, response);; + } + + + /** + * Template method for creating the POI {@link Workbook} instance. + * <p>The default implementation creates a traditional {@link HSSFWorkbook}. + * Spring-provided subclasses are overriding this for the OOXML-based variants; + * custom subclasses may override this for reading a workbook from a file. + * @param model the model Map + * @param request current HTTP request (for taking the URL or headers into account) + * @return the new {@link Workbook} instance + */ + protected Workbook createWorkbook(Map<String, Object> model, HttpServletRequest request) { + return new HSSFWorkbook(); + } + + /** + * The actual render step: taking the POI {@link Workbook} and rendering + * it to the given response. + * @param workbook the POI Workbook to render + * @param response current HTTP response + * @throws IOException when thrown by I/O methods that we're delegating to + */ + protected void renderWorkbook(Workbook workbook, HttpServletResponse response) throws IOException { + ServletOutputStream out = response.getOutputStream(); + workbook.write(out); + + // Closeable only implemented as of POI 3.10 + if (workbook instanceof Closeable) { + ((Closeable) workbook).close(); + } + } + + /** + * Application-provided subclasses must implement this method to populate + * the Excel workbook document, given the model. + * @param model the model Map + * @param workbook the Excel workbook to populate + * @param request in case we need locale etc. Shouldn't look at attributes. + * @param response in case we need to set cookies. Shouldn't write to it. + */ + protected abstract void buildExcelDocument( + Map<String, Object> model, Workbook workbook, HttpServletRequest request, HttpServletResponse response) + throws Exception; + +} diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/view/document/AbstractXlsxStreamingView.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/view/document/AbstractXlsxStreamingView.java new file mode 100644 index 00000000..2db6b646 --- /dev/null +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/view/document/AbstractXlsxStreamingView.java @@ -0,0 +1,59 @@ +/* + * Copyright 2002-2015 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.web.servlet.view.document; + +import java.io.IOException; +import java.util.Map; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.apache.poi.ss.usermodel.Workbook; +import org.apache.poi.xssf.streaming.SXSSFWorkbook; + +/** + * Convenient superclass for Excel document views in the Office 2007 XLSX format, + * using POI's streaming variant. Compatible with Apache POI 3.9 and higher. + * + * <p>For working with the workbook in subclasses, see + * <a href="http://poi.apache.org">Apache's POI site</a>. + * + * @author Juergen Hoeller + * @since 4.2 + */ +public abstract class AbstractXlsxStreamingView extends AbstractXlsxView { + + /** + * This implementation creates a {@link SXSSFWorkbook} for streaming the XLSX format. + */ + @Override + protected SXSSFWorkbook createWorkbook(Map<String, Object> model, HttpServletRequest request) { + return new SXSSFWorkbook(); + } + + /** + * This implementation disposes of the {@link SXSSFWorkbook} when done with rendering. + * @see org.apache.poi.xssf.streaming.SXSSFWorkbook#dispose() + */ + @Override + protected void renderWorkbook(Workbook workbook, HttpServletResponse response) throws IOException { + super.renderWorkbook(workbook, response); + + // Dispose of temporary files in case of streaming variant... + ((SXSSFWorkbook) workbook).dispose(); + } + +} diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/view/document/AbstractXlsxView.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/view/document/AbstractXlsxView.java new file mode 100644 index 00000000..4c26ffa7 --- /dev/null +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/view/document/AbstractXlsxView.java @@ -0,0 +1,54 @@ +/* + * Copyright 2002-2015 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.web.servlet.view.document; + +import java.util.Map; +import javax.servlet.http.HttpServletRequest; + +import org.apache.poi.ss.usermodel.Workbook; +import org.apache.poi.xssf.usermodel.XSSFWorkbook; + +/** + * Convenient superclass for Excel document views in the Office 2007 XLSX format + * (as supported by POI-OOXML). Compatible with Apache POI 3.5 and higher. + * + * <p>For working with the workbook in subclasses, see + * <a href="http://poi.apache.org">Apache's POI site</a>. + * + * @author Juergen Hoeller + * @since 4.2 + */ +public abstract class AbstractXlsxView extends AbstractXlsView { + + /** + * Default Constructor. + * <p>Sets the content type of the view to + * {@code "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"}. + */ + public AbstractXlsxView() { + setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"); + } + + /** + * This implementation creates an {@link XSSFWorkbook} for the XLSX format. + */ + @Override + protected Workbook createWorkbook(Map<String, Object> model, HttpServletRequest request) { + return new XSSFWorkbook(); + } + +} diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/view/freemarker/spring.ftl b/spring-webmvc/src/main/java/org/springframework/web/servlet/view/freemarker/spring.ftl deleted file mode 100644 index bfb41221..00000000 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/view/freemarker/spring.ftl +++ /dev/null @@ -1,383 +0,0 @@ -<#ftl strip_whitespace=true> -<#-- - * spring.ftl - * - * This file consists of a collection of FreeMarker macros aimed at easing - * some of the common requirements of web applications - in particular - * handling of forms. - * - * Spring's FreeMarker support will automatically make this file and therefore - * all macros within it available to any application using Spring's - * FreeMarkerConfigurer. - * - * To take advantage of these macros, the "exposeSpringMacroHelpers" property - * of the FreeMarker class needs to be set to "true". This will expose a - * RequestContext under the name "springMacroRequestContext", as needed by - * the macros in this library. - * - * @author Darren Davison - * @author Juergen Hoeller - * @since 1.1 - --> - -<#-- - * message - * - * Macro to translate a message code into a message. - --> -<#macro message code>${springMacroRequestContext.getMessage(code)}</#macro> - -<#-- - * messageText - * - * Macro to translate a message code into a message, - * using the given default text if no message found. - --> -<#macro messageText code, text>${springMacroRequestContext.getMessage(code, text)}</#macro> - -<#-- - * messageArgs - * - * Macro to translate a message code with arguments into a message. - --> -<#macro messageArgs code, args>${springMacroRequestContext.getMessage(code, args)}</#macro> - -<#-- - * messageArgsText - * - * Macro to translate a message code with arguments into a message, - * using the given default text if no message found. - --> -<#macro messageArgsText code, args, text>${springMacroRequestContext.getMessage(code, args, text)}</#macro> - -<#-- - * theme - * - * Macro to translate a theme message code into a message. - --> -<#macro theme code>${springMacroRequestContext.getThemeMessage(code)}</#macro> - -<#-- - * themeText - * - * Macro to translate a theme message code into a message, - * using the given default text if no message found. - --> -<#macro themeText code, text>${springMacroRequestContext.getThemeMessage(code, text)}</#macro> - -<#-- - * themeArgs - * - * Macro to translate a theme message code with arguments into a message. - --> -<#macro themeArgs code, args>${springMacroRequestContext.getThemeMessage(code, args)}</#macro> - -<#-- - * themeArgsText - * - * Macro to translate a theme message code with arguments into a message, - * using the given default text if no message found. - --> -<#macro themeArgsText code, args, text>${springMacroRequestContext.getThemeMessage(code, args, text)}</#macro> - -<#-- - * url - * - * Takes a relative URL and makes it absolute from the server root by - * adding the context root for the web application. - --> -<#macro url relativeUrl extra...><#if extra?? && extra?size!=0>${springMacroRequestContext.getContextUrl(relativeUrl,extra)}<#else>${springMacroRequestContext.getContextUrl(relativeUrl)}</#if></#macro> - -<#-- - * bind - * - * Exposes a BindStatus object for the given bind path, which can be - * a bean (e.g. "person") to get global errors, or a bean property - * (e.g. "person.name") to get field errors. Can be called multiple times - * within a form to bind to multiple command objects and/or field names. - * - * This macro will participate in the default HTML escape setting for the given - * RequestContext. This can be customized by calling "setDefaultHtmlEscape" - * on the "springMacroRequestContext" context variable, or via the - * "defaultHtmlEscape" context-param in web.xml (same as for the JSP bind tag). - * Also regards a "htmlEscape" variable in the namespace of this library. - * - * Producing no output, the following context variable will be available - * each time this macro is referenced (assuming you import this library in - * your templates with the namespace 'spring'): - * - * spring.status : a BindStatus instance holding the command object name, - * expression, value, and error messages and codes for the path supplied - * - * @param path : the path (string value) of the value required to bind to. - * Spring defaults to a command name of "command" but this can be overridden - * by user config. - --> -<#macro bind path> - <#if htmlEscape?exists> - <#assign status = springMacroRequestContext.getBindStatus(path, htmlEscape)> - <#else> - <#assign status = springMacroRequestContext.getBindStatus(path)> - </#if> - <#-- assign a temporary value, forcing a string representation for any - kind of variable. This temp value is only used in this macro lib --> - <#if status.value?exists && status.value?is_boolean> - <#assign stringStatusValue=status.value?string> - <#else> - <#assign stringStatusValue=status.value?default("")> - </#if> -</#macro> - -<#-- - * bindEscaped - * - * Similar to spring:bind, but takes an explicit HTML escape flag rather - * than relying on the default HTML escape setting. - --> -<#macro bindEscaped path, htmlEscape> - <#assign status = springMacroRequestContext.getBindStatus(path, htmlEscape)> - <#-- assign a temporary value, forcing a string representation for any - kind of variable. This temp value is only used in this macro lib --> - <#if status.value?exists && status.value?is_boolean> - <#assign stringStatusValue=status.value?string> - <#else> - <#assign stringStatusValue=status.value?default("")> - </#if> -</#macro> - -<#-- - * formInput - * - * Display a form input field of type 'text' and bind it to an attribute - * of a command or bean. - * - * @param path the name of the field to bind to - * @param attributes any additional attributes for the element (such as class - * or CSS styles or size - --> -<#macro formInput path attributes="" fieldType="text"> - <@bind path/> - <input type="${fieldType}" id="${status.expression?replace('[','')?replace(']','')}" name="${status.expression}" value="<#if fieldType!="password">${stringStatusValue}</#if>" ${attributes}<@closeTag/> -</#macro> - -<#-- - * formPasswordInput - * - * Display a form input field of type 'password' and bind it to an attribute - * of a command or bean. No value will ever be displayed. This functionality - * can also be obtained by calling the formInput macro with a 'type' parameter - * of 'password'. - * - * @param path the name of the field to bind to - * @param attributes any additional attributes for the element (such as class - * or CSS styles or size - --> -<#macro formPasswordInput path attributes=""> - <@formInput path, attributes, "password"/> -</#macro> - -<#-- - * formHiddenInput - * - * Generate a form input field of type 'hidden' and bind it to an attribute - * of a command or bean. This functionality can also be obtained by calling - * the formInput macro with a 'type' parameter of 'hidden'. - * - * @param path the name of the field to bind to - * @param attributes any additional attributes for the element (such as class - * or CSS styles or size - --> -<#macro formHiddenInput path attributes=""> - <@formInput path, attributes, "hidden"/> -</#macro> - -<#-- - * formTextarea - * - * Display a text area and bind it to an attribute of a command or bean. - * - * @param path the name of the field to bind to - * @param attributes any additional attributes for the element (such as class - * or CSS styles or size - --> -<#macro formTextarea path attributes=""> - <@bind path/> - <textarea id="${status.expression?replace('[','')?replace(']','')}" name="${status.expression}" ${attributes}>${stringStatusValue}</textarea> -</#macro> - -<#-- - * formSingleSelect - * - * Show a selectbox (dropdown) input element allowing a single value to be chosen - * from a list of options. - * - * @param path the name of the field to bind to - * @param options a map (value=label) of all the available options - * @param attributes any additional attributes for the element (such as class - * or CSS styles or size ---> -<#macro formSingleSelect path options attributes=""> - <@bind path/> - <select id="${status.expression?replace('[','')?replace(']','')}" name="${status.expression}" ${attributes}> - <#if options?is_hash> - <#list options?keys as value> - <option value="${value?html}"<@checkSelected value/>>${options[value]?html}</option> - </#list> - <#else> - <#list options as value> - <option value="${value?html}"<@checkSelected value/>>${value?html}</option> - </#list> - </#if> - </select> -</#macro> - -<#-- - * formMultiSelect - * - * Show a listbox of options allowing the user to make 0 or more choices from - * the list of options. - * - * @param path the name of the field to bind to - * @param options a map (value=label) of all the available options - * @param attributes any additional attributes for the element (such as class - * or CSS styles or size ---> -<#macro formMultiSelect path options attributes=""> - <@bind path/> - <select multiple="multiple" id="${status.expression?replace('[','')?replace(']','')}" name="${status.expression}" ${attributes}> - <#list options?keys as value> - <#assign isSelected = contains(status.actualValue?default([""]), value)> - <option value="${value?html}"<#if isSelected> selected="selected"</#if>>${options[value]?html}</option> - </#list> - </select> -</#macro> - -<#-- - * formRadioButtons - * - * Show radio buttons. - * - * @param path the name of the field to bind to - * @param options a map (value=label) of all the available options - * @param separator the html tag or other character list that should be used to - * separate each option. Typically ' ' or '<br>' - * @param attributes any additional attributes for the element (such as class - * or CSS styles or size ---> -<#macro formRadioButtons path options separator attributes=""> - <@bind path/> - <#list options?keys as value> - <#assign id="${status.expression?replace('[','')?replace(']','')}${value_index}"> - <input type="radio" id="${id}" name="${status.expression}" value="${value?html}"<#if stringStatusValue == value> checked="checked"</#if> ${attributes}<@closeTag/> - <label for="${id}">${options[value]?html}</label>${separator} - </#list> -</#macro> - -<#-- - * formCheckboxes - * - * Show checkboxes. - * - * @param path the name of the field to bind to - * @param options a map (value=label) of all the available options - * @param separator the html tag or other character list that should be used to - * separate each option. Typically ' ' or '<br>' - * @param attributes any additional attributes for the element (such as class - * or CSS styles or size ---> -<#macro formCheckboxes path options separator attributes=""> - <@bind path/> - <#list options?keys as value> - <#assign id="${status.expression?replace('[','')?replace(']','')}${value_index}"> - <#assign isSelected = contains(status.actualValue?default([""]), value)> - <input type="checkbox" id="${id}" name="${status.expression}" value="${value?html}"<#if isSelected> checked="checked"</#if> ${attributes}<@closeTag/> - <label for="${id}">${options[value]?html}</label>${separator} - </#list> - <input type="hidden" name="_${status.expression}" value="on"/> -</#macro> - -<#-- - * formCheckbox - * - * Show a single checkbox. - * - * @param path the name of the field to bind to - * @param attributes any additional attributes for the element (such as class - * or CSS styles or size ---> -<#macro formCheckbox path attributes=""> - <@bind path /> - <#assign id="${status.expression?replace('[','')?replace(']','')}"> - <#assign isSelected = status.value?? && status.value?string=="true"> - <input type="hidden" name="_${status.expression}" value="on"/> - <input type="checkbox" id="${id}" name="${status.expression}"<#if isSelected> checked="checked"</#if> ${attributes}/> -</#macro> - -<#-- - * showErrors - * - * Show validation errors for the currently bound field, with - * optional style attributes. - * - * @param separator the html tag or other character list that should be used to - * separate each option. Typically '<br>'. - * @param classOrStyle either the name of a CSS class element (which is defined in - * the template or an external CSS file) or an inline style. If the value passed in here - * contains a colon (:) then a 'style=' attribute will be used, else a 'class=' attribute - * will be used. ---> -<#macro showErrors separator classOrStyle=""> - <#list status.errorMessages as error> - <#if classOrStyle == ""> - <b>${error}</b> - <#else> - <#if classOrStyle?index_of(":") == -1><#assign attr="class"><#else><#assign attr="style"></#if> - <span ${attr}="${classOrStyle}">${error}</span> - </#if> - <#if error_has_next>${separator}</#if> - </#list> -</#macro> - -<#-- - * checkSelected - * - * Check a value in a list to see if it is the currently selected value. - * If so, add the 'selected="selected"' text to the output. - * Handles values of numeric and string types. - * This function is used internally but can be accessed by user code if required. - * - * @param value the current value in a list iteration ---> -<#macro checkSelected value> - <#if stringStatusValue?is_number && stringStatusValue == value?number>selected="selected"</#if> - <#if stringStatusValue?is_string && stringStatusValue == value>selected="selected"</#if> -</#macro> - -<#-- - * contains - * - * Macro to return true if the list contains the scalar, false if not. - * Surprisingly not a FreeMarker builtin. - * This function is used internally but can be accessed by user code if required. - * - * @param list the list to search for the item - * @param item the item to search for in the list - * @return true if item is found in the list, false otherwise ---> -<#function contains list item> - <#list list as nextInList> - <#if nextInList == item><#return true></#if> - </#list> - <#return false> -</#function> - -<#-- - * closeTag - * - * Simple macro to close an HTML tag that has no body with '>' or '/>', - * depending on the value of a 'xhtmlCompliant' variable in the namespace - * of this library. ---> -<#macro closeTag> - <#if xhtmlCompliant?exists && xhtmlCompliant>/><#else>></#if> -</#macro> diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/view/jasperreports/JasperReportsMultiFormatView.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/view/jasperreports/JasperReportsMultiFormatView.java index 2ae6bf8a..7e9af07d 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/view/jasperreports/JasperReportsMultiFormatView.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/view/jasperreports/JasperReportsMultiFormatView.java @@ -53,6 +53,7 @@ import org.springframework.util.CollectionUtils; * <li>{@code html} - {@code JasperReportsHtmlView}</li> * <li>{@code pdf} - {@code JasperReportsPdfView}</li> * <li>{@code xls} - {@code JasperReportsXlsView}</li> + * <li>{@code xlsx} - {@code JasperReportsXlsxView}</li> (as of Spring 4.2) * </ul> * * <p>The format key can be changed using the {@code formatKey} property. @@ -100,6 +101,7 @@ public class JasperReportsMultiFormatView extends AbstractJasperReportsView { this.formatMappings.put("html", JasperReportsHtmlView.class); this.formatMappings.put("pdf", JasperReportsPdfView.class); this.formatMappings.put("xls", JasperReportsXlsView.class); + this.formatMappings.put("xlsx", JasperReportsXlsxView.class); } @@ -119,6 +121,7 @@ public class JasperReportsMultiFormatView extends AbstractJasperReportsView { * <li>{@code html} - {@code JasperReportsHtmlView}</li> * <li>{@code pdf} - {@code JasperReportsPdfView}</li> * <li>{@code xls} - {@code JasperReportsXlsView}</li> + * <li>{@code xlsx} - {@code JasperReportsXlsxView}</li> (as of Spring 4.2) * </ul> */ public void setFormatMappings(Map<String, Class<? extends AbstractJasperReportsView>> formatMappings) { diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/view/jasperreports/JasperReportsXlsxView.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/view/jasperreports/JasperReportsXlsxView.java new file mode 100644 index 00000000..f86f3ce2 --- /dev/null +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/view/jasperreports/JasperReportsXlsxView.java @@ -0,0 +1,50 @@ +/* + * Copyright 2002-2015 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.web.servlet.view.jasperreports; + +import net.sf.jasperreports.engine.export.ooxml.JRXlsxExporter; + +/** + * Implementation of {@code AbstractJasperReportsSingleFormatView} + * that renders report results in XLSX format. + * + * <p><b>This class is compatible with classic JasperReports releases back until 2.x.</b> + * As a consequence, it keeps using the {@link net.sf.jasperreports.engine.JRExporter} + * API which got deprecated as of JasperReports 5.5.2 (early 2014). + * + * @author Rob Harrop + * @author Juergen Hoeller + * @since 4.2 + */ +@SuppressWarnings({"deprecation", "rawtypes"}) +public class JasperReportsXlsxView extends AbstractJasperReportsSingleFormatView { + + public JasperReportsXlsxView() { + setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"); + } + + @Override + protected net.sf.jasperreports.engine.JRExporter createExporter() { + return new JRXlsxExporter(); + } + + @Override + protected boolean useWriter() { + return false; + } + +} diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/view/json/AbstractJackson2View.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/view/json/AbstractJackson2View.java index 3ce9ecdf..34b75fa2 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/view/json/AbstractJackson2View.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/view/json/AbstractJackson2View.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2014 the original author or authors. + * Copyright 2002-2015 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -28,6 +28,7 @@ import com.fasterxml.jackson.core.JsonEncoding; import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.databind.ser.FilterProvider; import org.springframework.http.converter.json.MappingJacksonValue; import org.springframework.util.Assert; @@ -60,7 +61,7 @@ public abstract class AbstractJackson2View extends AbstractView { protected AbstractJackson2View(ObjectMapper objectMapper, String contentType) { - this.objectMapper = objectMapper; + setObjectMapper(objectMapper); setContentType(contentType); setExposePathVariables(false); } @@ -122,12 +123,6 @@ public abstract class AbstractJackson2View extends AbstractView { } /** - * Set the attribute in the model that should be rendered by this view. - * When set, all other model attributes will be ignored. - */ - public abstract void setModelKey(String modelKey); - - /** * Disables caching of the generated JSON. * <p>Default is {@code true}, which will prevent the client from caching the generated JSON. */ @@ -150,9 +145,7 @@ public abstract class AbstractJackson2View extends AbstractView { setResponseContentType(request, response); response.setCharacterEncoding(this.encoding.getJavaName()); if (this.disableCaching) { - response.addHeader("Pragma", "no-cache"); - response.addHeader("Cache-Control", "no-cache, no-store, max-age=0"); - response.addDateHeader("Expires", 1L); + response.addHeader("Cache-Control", "no-store"); } } @@ -178,45 +171,42 @@ public abstract class AbstractJackson2View extends AbstractView { protected Object filterAndWrapModel(Map<String, Object> model, HttpServletRequest request) { Object value = filterModel(model); Class<?> serializationView = (Class<?>) model.get(JsonView.class.getName()); - if (serializationView != null) { + FilterProvider filters = (FilterProvider) model.get(FilterProvider.class.getName()); + if (serializationView != null || filters != null) { MappingJacksonValue container = new MappingJacksonValue(value); container.setSerializationView(serializationView); + container.setFilters(filters); value = container; } return value; } /** - * Filter out undesired attributes from the given model. - * The return value can be either another {@link Map} or a single value object. - * @param model the model, as passed on to {@link #renderMergedOutputModel} - * @return the value to be rendered - */ - protected abstract Object filterModel(Map<String, Object> model); - - /** * Write the actual JSON content to the stream. * @param stream the output stream to use * @param object the value to be rendered, as returned from {@link #filterModel} * @throws IOException if writing failed */ - protected void writeContent(OutputStream stream, Object object) - throws IOException { - + protected void writeContent(OutputStream stream, Object object) throws IOException { JsonGenerator generator = this.objectMapper.getFactory().createGenerator(stream, this.encoding); writePrefix(generator, object); Class<?> serializationView = null; + FilterProvider filters = null; Object value = object; if (value instanceof MappingJacksonValue) { MappingJacksonValue container = (MappingJacksonValue) value; value = container.getValue(); serializationView = container.getSerializationView(); + filters = container.getFilters(); } if (serializationView != null) { this.objectMapper.writerWithView(serializationView).writeValue(generator, value); } + else if (filters != null) { + this.objectMapper.writer(filters).writeValue(generator, value); + } else { this.objectMapper.writeValue(generator, value); } @@ -224,13 +214,27 @@ public abstract class AbstractJackson2View extends AbstractView { generator.flush(); } + + /** + * Set the attribute in the model that should be rendered by this view. + * When set, all other model attributes will be ignored. + */ + public abstract void setModelKey(String modelKey); + + /** + * Filter out undesired attributes from the given model. + * The return value can be either another {@link Map} or a single value object. + * @param model the model, as passed on to {@link #renderMergedOutputModel} + * @return the value to be rendered + */ + protected abstract Object filterModel(Map<String, Object> model); + /** * Write a prefix before the main content. * @param generator the generator to use for writing content. * @param object the object to write to the output message. */ protected void writePrefix(JsonGenerator generator, Object object) throws IOException { - } /** @@ -239,7 +243,6 @@ public abstract class AbstractJackson2View extends AbstractView { * @param object the object to write to the output message. */ protected void writeSuffix(JsonGenerator generator, Object object) throws IOException { - } } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/view/json/MappingJackson2JsonView.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/view/json/MappingJackson2JsonView.java index 31c76c14..2039e8c0 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/view/json/MappingJackson2JsonView.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/view/json/MappingJackson2JsonView.java @@ -30,6 +30,7 @@ import javax.servlet.http.HttpServletResponse; import com.fasterxml.jackson.annotation.JsonView; import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.ser.FilterProvider; import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder; import org.springframework.http.converter.json.MappingJacksonValue; @@ -40,7 +41,7 @@ import org.springframework.web.servlet.View; /** * Spring MVC {@link View} that renders JSON content by serializing the model for the current request - * using <a href="http://jackson.codehaus.org/">Jackson 2's</a> {@link ObjectMapper}. + * using <a href="http://wiki.fasterxml.com/JacksonHome">Jackson 2's</a> {@link ObjectMapper}. * * <p>By default, the entire contents of the model map (with the exception of framework-specific classes) * will be encoded as JSON. If the model contains only one key, you can have it extracted encoded as JSON @@ -94,6 +95,15 @@ public class MappingJackson2JsonView extends AbstractJackson2View { super(Jackson2ObjectMapperBuilder.json().build(), DEFAULT_CONTENT_TYPE); } + /** + * Construct a new {@code MappingJackson2JsonView} using the provided + * {@link ObjectMapper} and setting the content type to {@code application/json}. + * @since 4.2.1 + */ + public MappingJackson2JsonView(ObjectMapper objectMapper) { + super(objectMapper, DEFAULT_CONTENT_TYPE); + } + /** * Specify a custom prefix to use for this view's JSON output. @@ -105,16 +115,15 @@ public class MappingJackson2JsonView extends AbstractJackson2View { } /** - * Indicates whether the JSON output by this view should be prefixed with <tt>"{} && "</tt>. + * Indicates whether the JSON output by this view should be prefixed with <tt>")]}', "</tt>. * Default is {@code false}. * <p>Prefixing the JSON string in this manner is used to help prevent JSON Hijacking. * The prefix renders the string syntactically invalid as a script so that it cannot be hijacked. - * This prefix does not affect the evaluation of JSON, but if JSON validation is performed - * on the string, the prefix would need to be ignored. + * This prefix should be stripped before parsing the string as JSON. * @see #setJsonPrefix */ public void setPrefixJson(boolean prefixJson) { - this.jsonPrefix = (prefixJson ? "{} && " : null); + this.jsonPrefix = (prefixJson ? ")]}', " : null); } /** @@ -141,29 +150,11 @@ public class MappingJackson2JsonView extends AbstractJackson2View { } /** - * Set the attributes in the model that should be rendered by this view. - * When set, all other model attributes will be ignored. - * @deprecated use {@link #setModelKeys(Set)} instead - */ - @Deprecated - public void setRenderedAttributes(Set<String> renderedAttributes) { - this.modelKeys = renderedAttributes; - } - - /** - * Return the attributes in the model that should be rendered by this view. - * @deprecated use {@link #getModelKeys()} instead - */ - @Deprecated - public final Set<String> getRenderedAttributes() { - return this.modelKeys; - } - - /** - * Set whether to serialize models containing a single attribute as a map or whether to - * extract the single value from the model and serialize it directly. - * <p>The effect of setting this flag is similar to using {@code MappingJackson2HttpMessageConverter} - * with an {@code @ResponseBody} request-handling method. + * Set whether to serialize models containing a single attribute as a map or + * whether to extract the single value from the model and serialize it directly. + * <p>The effect of setting this flag is similar to using + * {@code MappingJackson2HttpMessageConverter} with an {@code @ResponseBody} + * request-handling method. * <p>Default is {@code false}. */ public void setExtractValueFromSingleKeyModel(boolean extractValueFromSingleKeyModel) { @@ -226,7 +217,8 @@ public class MappingJackson2JsonView extends AbstractJackson2View { Set<String> modelKeys = (!CollectionUtils.isEmpty(this.modelKeys) ? this.modelKeys : model.keySet()); for (Map.Entry<String, Object> entry : model.entrySet()) { if (!(entry.getValue() instanceof BindingResult) && modelKeys.contains(entry.getKey()) && - !entry.getKey().equals(JsonView.class.getName())) { + !entry.getKey().equals(JsonView.class.getName()) && + !entry.getKey().equals(FilterProvider.class.getName())) { result.put(entry.getKey(), entry.getValue()); } } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/view/script/ScriptTemplateConfig.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/view/script/ScriptTemplateConfig.java new file mode 100644 index 00000000..a11968bd --- /dev/null +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/view/script/ScriptTemplateConfig.java @@ -0,0 +1,79 @@ +/* + * Copyright 2002-2015 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.web.servlet.view.script; + +import java.nio.charset.Charset; +import javax.script.ScriptEngine; + +/** + * Interface to be implemented by objects that configure and manage a + * JSR-223 {@link ScriptEngine} for automatic lookup in a web environment. + * Detected and used by {@link ScriptTemplateView}. + * + * @author Sebastien Deleuze + * @since 4.2 + */ +public interface ScriptTemplateConfig { + + /** + * Return the {@link ScriptEngine} to use by the views. + */ + ScriptEngine getEngine(); + + /** + * Return the engine name that will be used to instantiate the {@link ScriptEngine}. + */ + String getEngineName(); + + /** + * Return whether to use a shared engine for all threads or whether to create + * thread-local engine instances for each thread. + */ + Boolean isSharedEngine(); + + /** + * Return the scripts to be loaded by the script engine (library or user provided). + */ + String[] getScripts(); + + /** + * Return the object where the render function belongs (optional). + */ + String getRenderObject(); + + /** + * Return the render function name (mandatory). + */ + String getRenderFunction(); + + /** + * Return the content type to use for the response. + * @since 4.2.1 + */ + String getContentType(); + + /** + * Return the charset used to read script and template files. + */ + Charset getCharset(); + + /** + * Return the resource loader path(s) via a Spring resource location. + */ + String getResourceLoaderPath(); + +} diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/view/script/ScriptTemplateConfigurer.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/view/script/ScriptTemplateConfigurer.java new file mode 100644 index 00000000..33c4d654 --- /dev/null +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/view/script/ScriptTemplateConfigurer.java @@ -0,0 +1,225 @@ +/* + * Copyright 2002-2015 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.web.servlet.view.script; + +import java.nio.charset.Charset; +import javax.script.ScriptEngine; + +/** + * An implementation of Spring MVC's {@link ScriptTemplateConfig} for creating + * a {@code ScriptEngine} for use in a web application. + * + * <pre class="code"> + * + * // Add the following to an @Configuration class + * @Bean + * public ScriptTemplateConfigurer mustacheConfigurer() { + * ScriptTemplateConfigurer configurer = new ScriptTemplateConfigurer(); + * configurer.setEngineName("nashorn"); + * configurer.setScripts("mustache.js"); + * configurer.setRenderObject("Mustache"); + * configurer.setRenderFunction("render"); + * return configurer; + * } + * </pre> + * + * <p><b>NOTE:</b> It is possible to use non thread-safe script engines with + * templating libraries not designed for concurrency, like Handlebars or React running on + * Nashorn, by setting the {@link #setSharedEngine sharedEngine} property to {@code false}. + * + * @author Sebastien Deleuze + * @since 4.2 + * @see ScriptTemplateView + */ +public class ScriptTemplateConfigurer implements ScriptTemplateConfig { + + private ScriptEngine engine; + + private String engineName; + + private Boolean sharedEngine; + + private String[] scripts; + + private String renderObject; + + private String renderFunction; + + private String contentType; + + private Charset charset; + + private String resourceLoaderPath; + + + /** + * Set the {@link ScriptEngine} to use by the view. + * The script engine must implement {@code Invocable}. + * You must define {@code engine} or {@code engineName}, not both. + * <p>When the {@code sharedEngine} flag is set to {@code false}, you should not specify + * the script engine with this setter, but with the {@link #setEngineName(String)} + * one (since it implies multiple lazy instantiations of the script engine). + * @see #setEngineName(String) + */ + public void setEngine(ScriptEngine engine) { + this.engine = engine; + } + + @Override + public ScriptEngine getEngine() { + return this.engine; + } + + /** + * Set the engine name that will be used to instantiate the {@link ScriptEngine}. + * The script engine must implement {@code Invocable}. + * You must define {@code engine} or {@code engineName}, not both. + * @see #setEngine(ScriptEngine) + */ + public void setEngineName(String engineName) { + this.engineName = engineName; + } + + @Override + public String getEngineName() { + return this.engineName; + } + + /** + * When set to {@code false}, use thread-local {@link ScriptEngine} instances instead + * of one single shared instance. This flag should be set to {@code false} for those + * using non thread-safe script engines with templating libraries not designed for + * concurrency, like Handlebars or React running on Nashorn for example. + * In this case, Java 8u60 or greater is required due to + * <a href="https://bugs.openjdk.java.net/browse/JDK-8076099">this bug</a>. + * <p>When this flag is set to {@code false}, the script engine must be specified using + * {@link #setEngineName(String)}. Using {@link #setEngine(ScriptEngine)} is not + * possible because multiple instances of the script engine need to be created lazily + * (one per thread). + * @see <a href="http://docs.oracle.com/javase/8/docs/api/javax/script/ScriptEngineFactory.html#getParameter-java.lang.String-">THREADING ScriptEngine parameter<a/> + */ + public void setSharedEngine(Boolean sharedEngine) { + this.sharedEngine = sharedEngine; + } + + @Override + public Boolean isSharedEngine() { + return this.sharedEngine; + } + + /** + * Set the scripts to be loaded by the script engine (library or user provided). + * Since {@code resourceLoaderPath} default value is "classpath:", you can load easily + * any script available on the classpath. + * <p>For example, in order to use a JavaScript library available as a WebJars dependency + * and a custom "render.js" file, you should call + * {@code configurer.setScripts("/META-INF/resources/webjars/library/version/library.js", + * "com/myproject/script/render.js");}. + * @see #setResourceLoaderPath + * @see <a href="http://www.webjars.org">WebJars</a> + */ + public void setScripts(String... scriptNames) { + this.scripts = scriptNames; + } + + @Override + public String[] getScripts() { + return this.scripts; + } + + /** + * Set the object where the render function belongs (optional). + * For example, in order to call {@code Mustache.render()}, {@code renderObject} + * should be set to {@code "Mustache"} and {@code renderFunction} to {@code "render"}. + */ + public void setRenderObject(String renderObject) { + this.renderObject = renderObject; + } + + @Override + public String getRenderObject() { + return this.renderObject; + } + + /** + * Set the render function name (mandatory). + * + * <p>This function will be called with the following parameters: + * <ol> + * <li>{@code String template}: the template content</li> + * <li>{@code Map model}: the view model</li> + * <li>{@code String url}: the template url (since 4.2.2)</li> + * </ol> + */ + public void setRenderFunction(String renderFunction) { + this.renderFunction = renderFunction; + } + + @Override + public String getRenderFunction() { + return this.renderFunction; + } + + /** + * Set the content type to use for the response. + * ({@code text/html} by default). + * @since 4.2.1 + */ + public void setContentType(String contentType) { + this.contentType = contentType; + } + + /** + * Return the content type to use for the response. + * @since 4.2.1 + */ + @Override + public String getContentType() { + return this.contentType; + } + + /** + * Set the charset used to read script and template files. + * ({@code UTF-8} by default). + */ + public void setCharset(Charset charset) { + this.charset = charset; + } + + @Override + public Charset getCharset() { + return this.charset; + } + + /** + * Set the resource loader path(s) via a Spring resource location. + * Accepts multiple locations as a comma-separated list of paths. + * Standard URLs like "file:" and "classpath:" and pseudo URLs are supported + * as understood by Spring's {@link org.springframework.core.io.ResourceLoader}. + * Relative paths are allowed when running in an ApplicationContext. + * <p>Default is "classpath:". + */ + public void setResourceLoaderPath(String resourceLoaderPath) { + this.resourceLoaderPath = resourceLoaderPath; + } + + @Override + public String getResourceLoaderPath() { + return this.resourceLoaderPath; + } + +} diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/view/script/ScriptTemplateView.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/view/script/ScriptTemplateView.java new file mode 100644 index 00000000..908b273e --- /dev/null +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/view/script/ScriptTemplateView.java @@ -0,0 +1,408 @@ +/* + * Copyright 2002-2015 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.web.servlet.view.script; + +import java.io.IOException; +import java.io.InputStreamReader; +import java.net.URL; +import java.net.URLClassLoader; +import java.nio.charset.Charset; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import javax.script.Invocable; +import javax.script.ScriptEngine; +import javax.script.ScriptEngineManager; +import javax.script.ScriptException; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.BeanFactoryUtils; +import org.springframework.beans.factory.NoSuchBeanDefinitionException; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextException; +import org.springframework.core.NamedThreadLocal; +import org.springframework.core.io.DefaultResourceLoader; +import org.springframework.core.io.Resource; +import org.springframework.core.io.ResourceLoader; +import org.springframework.scripting.support.StandardScriptEvalException; +import org.springframework.scripting.support.StandardScriptUtils; +import org.springframework.util.Assert; +import org.springframework.util.FileCopyUtils; +import org.springframework.util.ObjectUtils; +import org.springframework.util.StringUtils; +import org.springframework.web.servlet.view.AbstractUrlBasedView; + +/** + * An {@link AbstractUrlBasedView} subclass designed to run any template library + * based on a JSR-223 script engine. + * + * <p>If not set, each property is auto-detected by looking up up a single + * {@link ScriptTemplateConfig} bean in the web application context and using + * it to obtain the configured properties. + * + * <p>Nashorn Javascript engine requires Java 8+, and may require setting the + * {@code sharedEngine} property to {@code false} in order to run properly. See + * {@link ScriptTemplateConfigurer#setSharedEngine(Boolean)} for more details. + * + * @author Sebastien Deleuze + * @author Juergen Hoeller + * @since 4.2 + * @see ScriptTemplateConfigurer + * @see ScriptTemplateViewResolver + */ +public class ScriptTemplateView extends AbstractUrlBasedView { + + public static final String DEFAULT_CONTENT_TYPE = "text/html"; + + private static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8"); + + private static final String DEFAULT_RESOURCE_LOADER_PATH = "classpath:"; + + + private static final ThreadLocal<Map<Object, ScriptEngine>> enginesHolder = + new NamedThreadLocal<Map<Object, ScriptEngine>>("ScriptTemplateView engines"); + + + private ScriptEngine engine; + + private String engineName; + + private Boolean sharedEngine; + + private String[] scripts; + + private String renderObject; + + private String renderFunction; + + private Charset charset; + + private String resourceLoaderPath; + + private ResourceLoader resourceLoader; + + private volatile ScriptEngineManager scriptEngineManager; + + + /** + * Constructor for use as a bean. + * @see #setUrl + */ + public ScriptTemplateView() { + setContentType(null); + } + + /** + * Create a new ScriptTemplateView with the given URL. + * @since 4.2.1 + */ + public ScriptTemplateView(String url) { + super(url); + setContentType(null); + } + + + /** + * See {@link ScriptTemplateConfigurer#setEngine(ScriptEngine)} documentation. + */ + public void setEngine(ScriptEngine engine) { + Assert.isInstanceOf(Invocable.class, engine); + this.engine = engine; + } + + /** + * See {@link ScriptTemplateConfigurer#setEngineName(String)} documentation. + */ + public void setEngineName(String engineName) { + this.engineName = engineName; + } + + /** + * See {@link ScriptTemplateConfigurer#setSharedEngine(Boolean)} documentation. + */ + public void setSharedEngine(Boolean sharedEngine) { + this.sharedEngine = sharedEngine; + } + + /** + * See {@link ScriptTemplateConfigurer#setScripts(String...)} documentation. + */ + public void setScripts(String... scripts) { + this.scripts = scripts; + } + + /** + * See {@link ScriptTemplateConfigurer#setRenderObject(String)} documentation. + */ + public void setRenderObject(String renderObject) { + this.renderObject = renderObject; + } + + /** + * See {@link ScriptTemplateConfigurer#setRenderFunction(String)} documentation. + */ + public void setRenderFunction(String functionName) { + this.renderFunction = functionName; + } + + /** + * See {@link ScriptTemplateConfigurer#setContentType(String)}} documentation. + * @since 4.2.1 + */ + @Override + public void setContentType(String contentType) { + super.setContentType(contentType); + } + + /** + * See {@link ScriptTemplateConfigurer#setCharset(Charset)} documentation. + */ + public void setCharset(Charset charset) { + this.charset = charset; + } + + /** + * See {@link ScriptTemplateConfigurer#setResourceLoaderPath(String)} documentation. + */ + public void setResourceLoaderPath(String resourceLoaderPath) { + this.resourceLoaderPath = resourceLoaderPath; + } + + + @Override + protected void initApplicationContext(ApplicationContext context) { + super.initApplicationContext(context); + + ScriptTemplateConfig viewConfig = autodetectViewConfig(); + if (this.engine == null && viewConfig.getEngine() != null) { + setEngine(viewConfig.getEngine()); + } + if (this.engineName == null && viewConfig.getEngineName() != null) { + this.engineName = viewConfig.getEngineName(); + } + if (this.scripts == null && viewConfig.getScripts() != null) { + this.scripts = viewConfig.getScripts(); + } + if (this.renderObject == null && viewConfig.getRenderObject() != null) { + this.renderObject = viewConfig.getRenderObject(); + } + if (this.renderFunction == null && viewConfig.getRenderFunction() != null) { + this.renderFunction = viewConfig.getRenderFunction(); + } + if (this.getContentType() == null) { + setContentType(viewConfig.getContentType() != null ? viewConfig.getContentType() : DEFAULT_CONTENT_TYPE); + } + if (this.charset == null) { + this.charset = (viewConfig.getCharset() != null ? viewConfig.getCharset() : DEFAULT_CHARSET); + } + if (this.resourceLoaderPath == null) { + this.resourceLoaderPath = (viewConfig.getResourceLoaderPath() != null ? + viewConfig.getResourceLoaderPath() : DEFAULT_RESOURCE_LOADER_PATH); + } + if (this.resourceLoader == null) { + this.resourceLoader = new DefaultResourceLoader(createClassLoader()); + } + if (this.sharedEngine == null && viewConfig.isSharedEngine() != null) { + this.sharedEngine = viewConfig.isSharedEngine(); + } + + Assert.isTrue(!(this.engine != null && this.engineName != null), + "You should define either 'engine' or 'engineName', not both."); + Assert.isTrue(!(this.engine == null && this.engineName == null), + "No script engine found, please specify either 'engine' or 'engineName'."); + + if (Boolean.FALSE.equals(this.sharedEngine)) { + Assert.isTrue(this.engineName != null, + "When 'sharedEngine' is set to false, you should specify the " + + "script engine using the 'engineName' property, not the 'engine' one."); + } + else if (this.engine != null) { + loadScripts(this.engine); + } + else { + setEngine(createEngineFromName()); + } + + Assert.isTrue(this.renderFunction != null, "The 'renderFunction' property must be defined."); + } + + + protected ScriptEngine getEngine() { + if (Boolean.FALSE.equals(this.sharedEngine)) { + Map<Object, ScriptEngine> engines = enginesHolder.get(); + if (engines == null) { + engines = new HashMap<Object, ScriptEngine>(4); + enginesHolder.set(engines); + } + Object engineKey = (!ObjectUtils.isEmpty(this.scripts) ? + new EngineKey(this.engineName, this.scripts) : this.engineName); + ScriptEngine engine = engines.get(engineKey); + if (engine == null) { + engine = createEngineFromName(); + engines.put(engineKey, engine); + } + return engine; + } + else { + // Simply return the configured ScriptEngine... + return this.engine; + } + } + + protected ScriptEngine createEngineFromName() { + if (this.scriptEngineManager == null) { + this.scriptEngineManager = new ScriptEngineManager(getApplicationContext().getClassLoader()); + } + + ScriptEngine engine = StandardScriptUtils.retrieveEngineByName(this.scriptEngineManager, this.engineName); + loadScripts(engine); + return engine; + } + + protected void loadScripts(ScriptEngine engine) { + if (!ObjectUtils.isEmpty(this.scripts)) { + try { + for (String script : this.scripts) { + Resource resource = this.resourceLoader.getResource(script); + if (!resource.exists()) { + throw new IllegalStateException("Script resource [" + script + "] not found"); + } + engine.eval(new InputStreamReader(resource.getInputStream())); + } + } + catch (Exception ex) { + throw new IllegalStateException("Failed to load script", ex); + } + } + } + + protected ClassLoader createClassLoader() { + String[] paths = StringUtils.commaDelimitedListToStringArray(this.resourceLoaderPath); + List<URL> urls = new ArrayList<URL>(); + try { + for (String path : paths) { + Resource[] resources = getApplicationContext().getResources(path); + if (resources.length > 0) { + for (Resource resource : resources) { + if (resource.exists()) { + urls.add(resource.getURL()); + } + } + } + } + } + catch (IOException ex) { + throw new IllegalStateException("Cannot create class loader: " + ex.getMessage()); + } + ClassLoader classLoader = getApplicationContext().getClassLoader(); + return (urls.size() > 0 ? new URLClassLoader(urls.toArray(new URL[urls.size()]), classLoader) : classLoader); + } + + protected ScriptTemplateConfig autodetectViewConfig() throws BeansException { + try { + return BeanFactoryUtils.beanOfTypeIncludingAncestors( + getApplicationContext(), ScriptTemplateConfig.class, true, false); + } + catch (NoSuchBeanDefinitionException ex) { + throw new ApplicationContextException("Expected a single ScriptTemplateConfig bean in the current " + + "Servlet web application context or the parent root context: ScriptTemplateConfigurer is " + + "the usual implementation. This bean may have any name.", ex); + } + } + + + @Override + protected void prepareResponse(HttpServletRequest request, HttpServletResponse response) { + super.prepareResponse(request, response); + + setResponseContentType(request, response); + response.setCharacterEncoding(this.charset.name()); + } + + @Override + protected void renderMergedOutputModel(Map<String, Object> model, HttpServletRequest request, + HttpServletResponse response) throws Exception { + + try { + ScriptEngine engine = getEngine(); + Invocable invocable = (Invocable) engine; + String url = getUrl(); + String template = getTemplate(url); + + Object html; + if (this.renderObject != null) { + Object thiz = engine.eval(this.renderObject); + html = invocable.invokeMethod(thiz, this.renderFunction, template, model, url); + } + else { + html = invocable.invokeFunction(this.renderFunction, template, model, url); + } + + response.getWriter().write(String.valueOf(html)); + } + catch (ScriptException ex) { + throw new ServletException("Failed to render script template", new StandardScriptEvalException(ex)); + } + } + + protected String getTemplate(String path) throws IOException { + Resource resource = this.resourceLoader.getResource(path); + InputStreamReader reader = new InputStreamReader(resource.getInputStream(), this.charset); + return FileCopyUtils.copyToString(reader); + } + + + /** + * Key class for the {@code enginesHolder ThreadLocal}. + * Only used if scripts have been specified; otherwise, the + * {@code engineName String} will be used as cache key directly. + */ + private static class EngineKey { + + private final String engineName; + + private final String[] scripts; + + public EngineKey(String engineName, String[] scripts) { + this.engineName = engineName; + this.scripts = scripts; + } + + @Override + public boolean equals(Object other) { + if (this == other) { + return true; + } + if (!(other instanceof EngineKey)) { + return false; + } + EngineKey otherKey = (EngineKey) other; + return (this.engineName.equals(otherKey.engineName) && Arrays.equals(this.scripts, otherKey.scripts)); + } + + @Override + public int hashCode() { + return (this.engineName.hashCode() * 29 + Arrays.hashCode(this.scripts)); + } + } + +} diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/view/script/ScriptTemplateViewResolver.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/view/script/ScriptTemplateViewResolver.java new file mode 100644 index 00000000..54f61470 --- /dev/null +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/view/script/ScriptTemplateViewResolver.java @@ -0,0 +1,47 @@ +/* + * Copyright 2002-2015 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.web.servlet.view.script; + +import org.springframework.web.servlet.view.UrlBasedViewResolver; + +/** + * Convenience subclass of {@link UrlBasedViewResolver} that supports + * {@link ScriptTemplateView} and custom subclasses of it. + * + * <p>The view class for all views created by this resolver can be specified + * via the {@link #setViewClass(Class)} property. + * + * <p><b>Note:</b> When chaining ViewResolvers this resolver will check for the + * existence of the specified template resources and only return a non-null + * View object if a template is actually found. + * + * @author Sebastien Deleuze + * @since 4.2 + * @see ScriptTemplateConfigurer + */ +public class ScriptTemplateViewResolver extends UrlBasedViewResolver { + + public ScriptTemplateViewResolver() { + setViewClass(requiredViewClass()); + } + + @Override + protected Class<?> requiredViewClass() { + return ScriptTemplateView.class; + } + +} diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/view/script/package-info.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/view/script/package-info.java new file mode 100644 index 00000000..141d3537 --- /dev/null +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/view/script/package-info.java @@ -0,0 +1,6 @@ +/** + * Support classes for views based on the JSR-223 script engine abstraction + * (as included in Java 6+), e.g. using JavaScript via Nashorn on JDK 8. + * Contains a View implementation for scripted templates. + */ +package org.springframework.web.servlet.view.script; diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/view/velocity/VelocityViewResolver.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/view/velocity/VelocityViewResolver.java index d1c903cc..e812ec92 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/view/velocity/VelocityViewResolver.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/view/velocity/VelocityViewResolver.java @@ -106,7 +106,7 @@ public class VelocityViewResolver extends AbstractTemplateViewResolver { super.initApplicationContext(); if (this.toolboxConfigLocation != null) { - if (VelocityView.class.equals(getViewClass())) { + if (VelocityView.class == getViewClass()) { logger.info("Using VelocityToolboxView instead of default VelocityView " + "due to specified toolboxConfigLocation"); setViewClass(VelocityToolboxView.class); diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/view/velocity/spring.vm b/spring-webmvc/src/main/java/org/springframework/web/servlet/view/velocity/spring.vm deleted file mode 100644 index 9064115e..00000000 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/view/velocity/spring.vm +++ /dev/null @@ -1,319 +0,0 @@ -#** - * spring.vm - * - * This file consists of a collection of Velocity macros aimed at easing - * some of the common requirements of web applications - in particular - * handling of forms. - * - * Spring's Velocity support will automatically make this file and therefore - * all macros within it available to any application using Spring's - * VelocityConfigurer. - * - * To take advantage of these macros, the "exposeSpringMacroHelpers" property - * of the VelocityView class needs to be set to "true". This will expose a - * RequestContext under the name "springMacroRequestContext", as needed by - * the macros in this library. - * - * @author Darren Davison - * @author Juergen Hoeller - * @since 1.1 - *# - -#** - * springMessage - * - * Macro to translate a message code into a message. - *# -#macro( springMessage $code )$springMacroRequestContext.getMessage($code)#end - -#** - * springMessageText - * - * Macro to translate a message code into a message, - * using the given default text if no message found. - *# -#macro( springMessageText $code $text )$springMacroRequestContext.getMessage($code, $text)#end - -#** - * springTheme - * - * Macro to translate a theme message code into a string. - *# -#macro( springTheme $code )$springMacroRequestContext.getThemeMessage($code)#end - -#** - * springThemeText - * - * Macro to translate a theme message code into a string, - * using the given default text if no message found. - *# -#macro( springThemeText $code $text )$springMacroRequestContext.getThemeMessage($code, $text)#end - -#** - * springUrl - * - * Takes a relative URL and makes it absolute from the server root by - * adding the context root for the web application. - *# -#macro( springUrl $relativeUrl )$springMacroRequestContext.getContextPath()${relativeUrl}#end - -#** - * springBind - * - * Exposes a BindStatus object for the given bind path, which can be - * a bean (e.g. "person") to get global errors, or a bean property - * (e.g. "person.name") to get field errors. Can be called multiple times - * within a form to bind to multiple command objects and/or field names. - * - * This macro will participate in the default HTML escape setting for the given - * RequestContext. This can be customized by calling "setDefaultHtmlEscape" - * on the "springMacroRequestContext" context variable, or via the - * "defaultHtmlEscape" context-param in web.xml (same as for the JSP bind tag). - * Also regards a "springHtmlEscape" variable in the template context. - * - * Producing no output, the following context variable will be available - * each time this macro is referenced: - * - * $status : a BindStatus instance holding the command object name, - * expression, value, and error codes and messages for the path supplied - * - * @param $path : the path (string value) of the value required to bind to. - * Spring defaults to a command name of "command" but this can be overridden - * by user config. - *# -#macro( springBind $path ) - #if("$!springHtmlEscape"!="") - #set( $status = $springMacroRequestContext.getBindStatus($path, $springHtmlEscape) ) - #else - #set( $status = $springMacroRequestContext.getBindStatus($path) ) - #end -#end - -#** - * springBindEscaped - * - * Similar to springBind, but takes an explicit HTML escape flag rather - * than relying on the default HTML escape setting. - *# -#macro( springBindEscaped $path $htmlEscape ) - #set( $status = $springMacroRequestContext.getBindStatus($path, $htmlEscape) ) -#end - -#** - * springFormInput - * - * Display a form input field of type 'text' and bind it to an attribute - * of a command or bean. - * - * @param path the name of the field to bind to - * @param attributes any additional attributes for the element (such as class - * or CSS styles or size) - * - *# -#macro( springFormInput $path $attributes ) - #springBind($path) - <input type="text" id="#springXmlId(${status.expression})" name="${status.expression}" value="$!status.value" ${attributes}#springCloseTag() -#end - -#** - * springFormPasswordInput - * - * Display a form input field of type 'password' and bind it to an attribute - * of a command or bean. No value will ever be specified for this field regardless - * of whether one exists or not. For hopefully obvious reasons! - * - * @param path the name of the field to bind to - * @param attributes any additional attributes for the element (such as class - * or CSS styles or size) - * - *# -#macro( springFormPasswordInput $path $attributes ) - #springBind($path) - <input type="password" id="#springXmlId(${status.expression})" name="${status.expression}" value="" ${attributes}#springCloseTag() -#end - -#** - * springFormHiddenInput - * - * Generate a form input field of type 'hidden' and bind it to an attribute - * of a command or bean. - * - * @param path the name of the field to bind to - * @param attributes any additional attributes for the element (such as class - * or CSS styles or size) - * - *# -#macro( springFormHiddenInput $path $attributes ) - #springBind($path) - <input type="hidden" id="#springXmlId(${status.expression})" name="${status.expression}" value="$!status.value" ${attributes}#springCloseTag() -#end - -#** - * formTextArea - * - * display a text area and bind it to an attribute - * of a command or bean - * - * @param path the name of the field to bind to - * @param attributes any additional attributes for the element (such as class - * or CSS styles or size) - * - *# -#macro( springFormTextarea $path $attributes ) - #springBind($path) - <textarea id="#springXmlId(${status.expression})" name="${status.expression}" ${attributes}>$!status.value</textarea> -#end - -#** - * springFormSingleSelect - * - * Show a selectbox (dropdown) input element allowing a single value to be chosen - * from a list of options. - * - * The null check for $status.value leverages Velocity's 'quiet' notation rather - * than the more common #if($status.value) since this method evaluates to the - * boolean 'false' if the content of $status.value is the String "false" - not - * what we want. - * - * @param path the name of the field to bind to - * @param options a map (value=label) of all the available options - * @param attributes any additional attributes for the element (such as class - * or CSS styles or size) -*# -#macro( springFormSingleSelect $path $options $attributes ) - #springBind($path) - <select id="#springXmlId(${status.expression})" name="${status.expression}" ${attributes}> - #foreach($option in $options.keySet()) - <option value="${option}" - #if("$!status.value"=="$option") selected="selected" #end> - ${options.get($option)}</option> - #end - </select> -#end - -#** - * springFormMultiSelect - * - * Show a listbox of options allowing the user to make 0 or more choices from - * the list of options. - * - * @param path the name of the field to bind to - * @param options a map (value=label) of all the available options - * @param attributes any additional attributes for the element (such as class - * or CSS styles or size) -*# -#macro( springFormMultiSelect $path $options $attributes ) - #springBind($path) - <select multiple="multiple" id="#springXmlId(${status.expression})" name="${status.expression}" ${attributes}> - #foreach($option in $options.keySet()) - <option value="${option}" - #foreach($item in $status.actualValue) - #if($item==$option) selected="selected" #end - #end - >${options.get($option)}</option> - #end - </select> -#end - -#** - * springFormRadioButtons - * - * Show radio buttons. - * - * @param path the name of the field to bind to - * @param options a map (value=label) of all the available options - * @param separator the html tag or other character list that should be used to - * separate each option. Typically ' ' or '<br>' - * @param attributes any additional attributes for the element (such as class - * or CSS styles or size) -*# -#macro( springFormRadioButtons $path $options $separator $attributes ) - #springBind($path) - #foreach($option in $options.keySet()) - <input type="radio" name="${status.expression}" value="${option}" - #if("$!status.value"=="$option") checked="checked" #end - ${attributes} - #springCloseTag() - ${options.get($option)} ${separator} - #end -#end - -#** - * springFormCheckboxes - * - * Show checkboxes. - * - * @param path the name of the field to bind to - * @param options a map (value=label) of all the available options - * @param separator the html tag or other character list that should be used to - * separate each option. Typically ' ' or '<br>'. - * @param attributes any additional attributes for the element (such as class - * or CSS styles or size) -*# -#macro( springFormCheckboxes $path $options $separator $attributes ) - #springBind($path) - #foreach($option in $options.keySet()) - <input type="checkbox" name="${status.expression}" value="${option}" - #foreach($item in $status.actualValue) - #if($item==$option) checked="checked" #end - #end - ${attributes} #springCloseTag() - ${options.get($option)} ${separator} - #end - <input type="hidden" name="_${status.expression}" value="on"/> -#end - -#** - * springFormCheckbox - * - * Show a single checkbox. - * - * @param path the name of the field to bind to - * @param attributes any additional attributes for the element (such as class - * or CSS styles or size) -*# -#macro( springFormCheckbox $path $attributes ) - #springBind($path) - <input type="hidden" name="_#springXmlId(${status.expression})" value="on"/> - <input type="checkbox" id="#springXmlId(${status.expression})" name="${status.expression}"#if("$!{status.value}"=="true") checked="checked"#end ${attributes}/> -#end - -#** - * springShowErrors - * - * Show validation errors for the currently bound field, with - * optional style attributes. - * - * @param separator the html tag or other character list that should be used to - * separate each option. Typically '<br>'. - * @param classOrStyle either the name of a CSS class element (which is defined in - * the template or an external CSS file) or an inline style. If the value passed in here - * contains a colon (:) then a 'style=' attribute will be used, else a 'class=' attribute - * will be used. -*# -#macro( springShowErrors $separator $classOrStyle ) - #foreach($error in $status.errorMessages) - #if($classOrStyle=="") - <b>${error}</b> - #else - #if($classOrStyle.indexOf(":")==-1) - #set($attr="class") - #else - #set($attr="style") - #end - <span ${attr}="${classOrStyle}">${error}</span> - #end - ${separator} - #end -#end - -#** - * springCloseTag - * - * Simple macro to close an HTML tag that has no body with '>' or '/>', - * depending on the value of a 'springXhtmlCompliant' variable in the - * template context. - *# -#macro( springCloseTag )#if($springXhtmlCompliant)/>#else>#end#end - -#macro( springXmlId $id)#if($id)$id.replaceAll("\[","").replaceAll("\]","")#else$id#end#end diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/view/xml/MappingJackson2XmlView.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/view/xml/MappingJackson2XmlView.java index 66b9d4c4..dfcf59e5 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/view/xml/MappingJackson2XmlView.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/view/xml/MappingJackson2XmlView.java @@ -28,7 +28,7 @@ import org.springframework.web.servlet.view.json.AbstractJackson2View; /** * Spring MVC {@link View} that renders XML content by serializing the model for the current request - * using <a href="http://jackson.codehaus.org/">Jackson 2's</a> {@link XmlMapper}. + * using <a href="http://wiki.fasterxml.com/JacksonHome">Jackson 2's</a> {@link XmlMapper}. * * <p>The Object to be serialized is supplied as a parameter in the model. The first serializable * entry is used. Users can either specify a specific entry in the model via the @@ -58,6 +58,14 @@ public class MappingJackson2XmlView extends AbstractJackson2View { super(Jackson2ObjectMapperBuilder.xml().build(), DEFAULT_CONTENT_TYPE); } + /** + * Construct a new {@code MappingJackson2XmlView} using the provided {@link XmlMapper} + * and setting the content type to {@code application/xml}. + * @since 4.2.1 + */ + public MappingJackson2XmlView(XmlMapper xmlMapper) { + super(xmlMapper, DEFAULT_CONTENT_TYPE); + } /** * {@inheritDoc} |