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 faafdfd8326f..7bccd3781bb1 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 @@ -35,7 +35,6 @@ import org.springframework.web.servlet.DispatcherServlet; import org.springframework.web.servlet.handler.AbstractHandlerMapping; import org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping; -import org.springframework.web.servlet.handler.HandlerMappingIntrospector; import org.springframework.web.servlet.i18n.AcceptHeaderLocaleResolver; import org.springframework.web.servlet.mvc.HttpRequestHandlerAdapter; import org.springframework.web.servlet.mvc.SimpleControllerHandlerAdapter; @@ -72,14 +71,11 @@ public abstract class MvcNamespaceUtils { private static final String CORS_CONFIGURATION_BEAN_NAME = "mvcCorsConfigurations"; - private static final String HANDLER_MAPPING_INTROSPECTOR_BEAN_NAME = "mvcHandlerMappingIntrospector"; - public static void registerDefaultComponents(ParserContext context, @Nullable Object source) { registerBeanNameUrlHandlerMapping(context, source); registerHttpRequestHandlerAdapter(context, source); registerSimpleControllerHandlerAdapter(context, source); - registerHandlerMappingIntrospector(context, source); registerLocaleResolver(context, source); registerViewNameTranslator(context, source); registerFlashMapManager(context, source); @@ -275,22 +271,6 @@ else if (corsConfigurations != null) { return new RuntimeBeanReference(CORS_CONFIGURATION_BEAN_NAME); } - /** - * Registers an {@link HandlerMappingIntrospector} under a well-known name - * unless already registered. - */ - @SuppressWarnings("removal") - private static void registerHandlerMappingIntrospector(ParserContext context, @Nullable Object source) { - if (!context.getRegistry().containsBeanDefinition(HANDLER_MAPPING_INTROSPECTOR_BEAN_NAME)) { - RootBeanDefinition beanDef = new RootBeanDefinition(HandlerMappingIntrospector.class); - beanDef.setSource(source); - beanDef.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); - beanDef.setLazyInit(true); - context.getRegistry().registerBeanDefinition(HANDLER_MAPPING_INTROSPECTOR_BEAN_NAME, beanDef); - context.registerComponent(new BeanComponentDefinition(beanDef, HANDLER_MAPPING_INTROSPECTOR_BEAN_NAME)); - } - } - /** * Registers an {@link AcceptHeaderLocaleResolver} under a well-known name * unless already registered. 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 242e416cdedd..ad77070d00c5 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 @@ -35,7 +35,6 @@ import org.springframework.context.ApplicationContextAware; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.Lazy; import org.springframework.core.KotlinDetector; import org.springframework.core.convert.converter.Converter; import org.springframework.format.Formatter; @@ -61,6 +60,7 @@ import org.springframework.web.bind.support.ConfigurableWebBindingInitializer; import org.springframework.web.context.ServletContextAware; import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.PreFlightRequestHandler; import org.springframework.web.method.support.CompositeUriComponentsContributor; import org.springframework.web.method.support.HandlerMethodArgumentResolver; import org.springframework.web.method.support.HandlerMethodReturnValueHandler; @@ -76,8 +76,8 @@ import org.springframework.web.servlet.handler.AbstractHandlerMapping; import org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping; import org.springframework.web.servlet.handler.ConversionServiceExposingInterceptor; +import org.springframework.web.servlet.handler.DefaultPreFlightRequestHandler; import org.springframework.web.servlet.handler.HandlerExceptionResolverComposite; -import org.springframework.web.servlet.handler.HandlerMappingIntrospector; import org.springframework.web.servlet.i18n.AcceptHeaderLocaleResolver; import org.springframework.web.servlet.mvc.Controller; import org.springframework.web.servlet.mvc.HttpRequestHandlerAdapter; @@ -1161,11 +1161,9 @@ protected final Map getCorsConfigurations() { protected void addCorsMappings(CorsRegistry registry) { } - @SuppressWarnings("removal") @Bean - @Lazy - public HandlerMappingIntrospector mvcHandlerMappingIntrospector() { - return new HandlerMappingIntrospector(); + public PreFlightRequestHandler preFlightRequestHandler() { + return new DefaultPreFlightRequestHandler(); } @Bean 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 a42566b390c3..55b14bc10e56 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 @@ -762,7 +762,7 @@ private class CorsInterceptor implements HandlerInterceptor, CorsConfigurationSo private final @Nullable CorsConfiguration config; - public CorsInterceptor(@Nullable CorsConfiguration config) { + CorsInterceptor(@Nullable CorsConfiguration config) { this.config = config; } @@ -795,7 +795,7 @@ protected boolean invokeCorsProcessor( private final class PreFlightHttpRequestHandler extends CorsInterceptor implements HttpRequestHandler, PreFlightRequestHandler { - public PreFlightHttpRequestHandler(@Nullable CorsConfiguration config) { + PreFlightHttpRequestHandler(@Nullable CorsConfiguration config) { super(config); } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/DefaultPreFlightRequestHandler.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/DefaultPreFlightRequestHandler.java new file mode 100644 index 000000000000..9d07d6aa8608 --- /dev/null +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/DefaultPreFlightRequestHandler.java @@ -0,0 +1,157 @@ +/* + * Copyright 2002-present 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 + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.web.servlet.handler; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Properties; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.jspecify.annotations.Nullable; + +import org.springframework.beans.factory.BeanFactoryUtils; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.core.annotation.AnnotationAwareOrderComparator; +import org.springframework.core.io.ClassPathResource; +import org.springframework.core.io.Resource; +import org.springframework.core.io.support.PropertiesLoaderUtils; +import org.springframework.http.server.RequestPath; +import org.springframework.http.server.ServletServerHttpRequest; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; +import org.springframework.util.StringUtils; +import org.springframework.web.cors.CorsUtils; +import org.springframework.web.cors.PreFlightRequestHandler; +import org.springframework.web.servlet.DispatcherServlet; +import org.springframework.web.servlet.HandlerExecutionChain; +import org.springframework.web.servlet.HandlerMapping; +import org.springframework.web.servlet.NoHandlerFoundException; +import org.springframework.web.util.ServletRequestPathUtils; + +/** + * Default implementation of {@link PreFlightRequestHandler} that discovers all + * {@link HandlerMapping} beans in the {@link ApplicationContext} and uses them to + * find a handler for a pre-flight CORS request, then delegates to the + * {@link PreFlightRequestHandler} returned by that mapping. + * + *

Handler mappings are detected and sorted in the same way as in + * {@link DispatcherServlet}, with fallback to the default mappings configured in + * {@code DispatcherServlet.properties} if no mappings are found. + * + * @author Rossen Stoyanchev + * @since 7.1 + * @see PreFlightRequestHandler + * @see HandlerMapping + * @see DispatcherServlet + */ +public class DefaultPreFlightRequestHandler + implements PreFlightRequestHandler, ApplicationContextAware, InitializingBean { + + private @Nullable ApplicationContext applicationContext; + + private @Nullable List handlerMappings; + + @Override + public void setApplicationContext(ApplicationContext applicationContext) { + this.applicationContext = applicationContext; + } + + @Override + public void afterPropertiesSet() { + if (this.handlerMappings == null) { + Assert.notNull(this.applicationContext, "No ApplicationContext"); + this.handlerMappings = initHandlerMappings(this.applicationContext); + } + } + + private static List initHandlerMappings(ApplicationContext context) { + Map beans = + BeanFactoryUtils.beansOfTypeIncludingAncestors(context, HandlerMapping.class, true, false); + + if (!beans.isEmpty()) { + List mappings = new ArrayList<>(beans.values()); + AnnotationAwareOrderComparator.sort(mappings); + return Collections.unmodifiableList(mappings); + } + + return Collections.unmodifiableList(initFallback(context)); + } + + private static List initFallback(ApplicationContext applicationContext) { + Properties properties; + try { + Resource resource = new ClassPathResource("DispatcherServlet.properties", DispatcherServlet.class); + properties = PropertiesLoaderUtils.loadProperties(resource); + } + catch (IOException ex) { + throw new IllegalStateException("Could not load DispatcherServlet.properties: " + ex.getMessage()); + } + + String value = properties.getProperty(HandlerMapping.class.getName()); + String[] names = StringUtils.commaDelimitedListToStringArray(value); + List result = new ArrayList<>(names.length); + for (String name : names) { + try { + Class clazz = ClassUtils.forName(name, DispatcherServlet.class.getClassLoader()); + Object mapping = applicationContext.getAutowireCapableBeanFactory().createBean(clazz); + result.add((HandlerMapping) mapping); + } + catch (ClassNotFoundException ex) { + throw new IllegalStateException("Could not find default HandlerMapping [" + name + "]"); + } + } + return result; + } + + /** + * Find the matching {@link HandlerMapping} for the request, and invoke the + * handler it returns as a {@link PreFlightRequestHandler}. + * @throws NoHandlerFoundException if no handler matches the request + * @since 7.1 + */ + @Override + public void handlePreFlight(HttpServletRequest request, HttpServletResponse response) throws Exception { + Assert.state(this.handlerMappings != null, "Not yet initialized via afterPropertiesSet."); + Assert.state(CorsUtils.isPreFlightRequest(request), "Not a pre-flight request."); + RequestPath previousPath = (RequestPath) request.getAttribute(ServletRequestPathUtils.PATH_ATTRIBUTE); + try { + ServletRequestPathUtils.parseAndCache(request); + for (HandlerMapping mapping : this.handlerMappings) { + HandlerExecutionChain chain = mapping.getHandler(request); + if (chain != null) { + Object handler = chain.getHandler(); + if (handler instanceof PreFlightRequestHandler preFlightHandler) { + preFlightHandler.handlePreFlight(request, response); + return; + } + throw new IllegalStateException("Expected PreFlightRequestHandler: " + handler.getClass()); + } + } + throw new NoHandlerFoundException( + request.getMethod(), request.getRequestURI(), new ServletServerHttpRequest(request).getHeaders()); + } + finally { + ServletRequestPathUtils.setParsedRequestPath(previousPath, request); + } + } +} diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/HandlerMappingIntrospector.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/HandlerMappingIntrospector.java deleted file mode 100644 index 0fbdb2fe2854..000000000000 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/HandlerMappingIntrospector.java +++ /dev/null @@ -1,631 +0,0 @@ -/* - * Copyright 2002-present 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 - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.web.servlet.handler; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.Collections; -import java.util.Enumeration; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Properties; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.function.BiFunction; -import java.util.stream.Collectors; - -import jakarta.servlet.DispatcherType; -import jakarta.servlet.Filter; -import jakarta.servlet.ServletRequest; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletRequestWrapper; -import jakarta.servlet.http.HttpServletResponse; -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; -import org.jspecify.annotations.Nullable; - -import org.springframework.beans.factory.BeanFactoryUtils; -import org.springframework.beans.factory.InitializingBean; -import org.springframework.context.ApplicationContext; -import org.springframework.context.ApplicationContextAware; -import org.springframework.core.annotation.AnnotationAwareOrderComparator; -import org.springframework.core.io.ClassPathResource; -import org.springframework.core.io.Resource; -import org.springframework.core.io.support.PropertiesLoaderUtils; -import org.springframework.http.server.RequestPath; -import org.springframework.http.server.ServletServerHttpRequest; -import org.springframework.util.Assert; -import org.springframework.util.ClassUtils; -import org.springframework.util.StringUtils; -import org.springframework.web.cors.CorsConfiguration; -import org.springframework.web.cors.CorsConfigurationSource; -import org.springframework.web.cors.CorsUtils; -import org.springframework.web.cors.PreFlightRequestHandler; -import org.springframework.web.servlet.DispatcherServlet; -import org.springframework.web.servlet.HandlerExecutionChain; -import org.springframework.web.servlet.HandlerInterceptor; -import org.springframework.web.servlet.HandlerMapping; -import org.springframework.web.servlet.NoHandlerFoundException; -import org.springframework.web.util.ServletRequestPathUtils; -import org.springframework.web.util.UrlPathHelper; -import org.springframework.web.util.pattern.PathPatternParser; - -/** - * Helper class to get information from the {@code HandlerMapping} that would - * serve a specific request. - * - *

Provides the following methods: - *

- * - *

Note that this is primarily an SPI to allow Spring Security - * to align its pattern matching with the same pattern matching that would be - * used in Spring MVC for a given request, in order to avoid security issues. - * - *

Use of this component incurs the performance overhead of mapping the - * request, and should not be repeated multiple times per request. - * {@link #createCacheFilter()} exposes a Filter to cache the results. - * Applications that rely on Spring Security don't need to deploy this Filter - * since Spring Security doe that. However, other custom security layers, used - * in place of Spring Security that use this component should deploy the cache - * Filter with requirements described in the Javadoc for the method. - * - * @author Rossen Stoyanchev - * @since 4.3.1 - * @deprecated in favor of using just {@link PathPatternParser}; when - * {@link #allHandlerMappingsUsePathPatternParser} returns true, it is sufficient - * to use that to align with handler mappings. - */ -@SuppressWarnings("removal") -@Deprecated(since = "7.0", forRemoval = true) -public class HandlerMappingIntrospector - implements CorsConfigurationSource, PreFlightRequestHandler, ApplicationContextAware, InitializingBean { - - private static final Log logger = LogFactory.getLog(HandlerMappingIntrospector.class.getName()); - - private static final String CACHED_RESULT_ATTRIBUTE = - HandlerMappingIntrospector.class.getName() + ".CachedResult"; - - private static final int DEFAULT_CACHE_LIMIT = 2048; - - - private @Nullable ApplicationContext applicationContext; - - private @Nullable List handlerMappings; - - private Map pathPatternMappings = Collections.emptyMap(); - - private int patternCacheLimit = DEFAULT_CACHE_LIMIT; - - private final CacheResultLogHelper cacheLogHelper = new CacheResultLogHelper(); - - - /** - * Set a limit on the maximum number of security patterns passed into - * {@link MatchableHandlerMapping#match} at runtime to cache. - *

By default, this is set to 2048. - * @param patternCacheLimit the limit to use - * @since 6.2.8 - */ - public void setPatternCacheLimit(int patternCacheLimit) { - this.patternCacheLimit = patternCacheLimit; - } - - /** - * Return the configured limit on security patterns to cache. - * @since 6.2.8 - */ - public int getPatternCacheLimit() { - return this.patternCacheLimit; - } - - @Override - public void setApplicationContext(ApplicationContext applicationContext) { - this.applicationContext = applicationContext; - } - - @Override - public void afterPropertiesSet() { - if (this.handlerMappings == null) { - Assert.notNull(this.applicationContext, "No ApplicationContext"); - this.handlerMappings = initHandlerMappings(this.applicationContext); - - this.pathPatternMappings = this.handlerMappings.stream() - .filter(m -> m instanceof MatchableHandlerMapping hm && hm.getPatternParser() != null) - .map(hm -> (MatchableHandlerMapping) hm) - .collect(Collectors.toMap(hm -> hm, (MatchableHandlerMapping delegate) -> - new PathPatternMatchableHandlerMapping(delegate, getPatternCacheLimit()))); - } - } - - private static List initHandlerMappings(ApplicationContext context) { - - Map beans = - BeanFactoryUtils.beansOfTypeIncludingAncestors(context, HandlerMapping.class, true, false); - - if (!beans.isEmpty()) { - List mappings = new ArrayList<>(beans.values()); - AnnotationAwareOrderComparator.sort(mappings); - return Collections.unmodifiableList(mappings); - } - - return Collections.unmodifiableList(initFallback(context)); - } - - private static List initFallback(ApplicationContext applicationContext) { - Properties properties; - try { - Resource resource = new ClassPathResource("DispatcherServlet.properties", DispatcherServlet.class); - properties = PropertiesLoaderUtils.loadProperties(resource); - } - catch (IOException ex) { - throw new IllegalStateException("Could not load DispatcherServlet.properties: " + ex.getMessage()); - } - - String value = properties.getProperty(HandlerMapping.class.getName()); - String[] names = StringUtils.commaDelimitedListToStringArray(value); - List result = new ArrayList<>(names.length); - for (String name : names) { - try { - Class clazz = ClassUtils.forName(name, DispatcherServlet.class.getClassLoader()); - Object mapping = applicationContext.getAutowireCapableBeanFactory().createBean(clazz); - result.add((HandlerMapping) mapping); - } - catch (ClassNotFoundException ex) { - throw new IllegalStateException("Could not find default HandlerMapping [" + name + "]"); - } - } - return result; - } - - - /** - * Return the configured or detected {@code HandlerMapping}s. - */ - public List getHandlerMappings() { - return (this.handlerMappings != null ? this.handlerMappings : Collections.emptyList()); - } - - /** - * Return {@code true} if all {@link HandlerMapping} beans - * {@link HandlerMapping#usesPathPatterns() use parsed PathPatterns}, - * and {@code false} if any don't. - * @since 6.2 - */ - public boolean allHandlerMappingsUsePathPatternParser() { - Assert.state(this.handlerMappings != null, "Not yet initialized via afterPropertiesSet."); - return getHandlerMappings().stream().allMatch(HandlerMapping::usesPathPatterns); - } - - - /** - * Find the matching {@link HandlerMapping} for the request, and invoke the - * handler it returns as a {@link PreFlightRequestHandler}. - * @throws NoHandlerFoundException if no handler matches the request - * @since 6.2 - */ - @Override - public void handlePreFlight(HttpServletRequest request, HttpServletResponse response) throws Exception { - Assert.state(this.handlerMappings != null, "Not yet initialized via afterPropertiesSet."); - Assert.state(CorsUtils.isPreFlightRequest(request), "Not a pre-flight request."); - RequestPath previousPath = (RequestPath) request.getAttribute(ServletRequestPathUtils.PATH_ATTRIBUTE); - try { - ServletRequestPathUtils.parseAndCache(request); - for (HandlerMapping mapping : this.handlerMappings) { - HandlerExecutionChain chain = mapping.getHandler(request); - if (chain != null) { - Object handler = chain.getHandler(); - if (handler instanceof PreFlightRequestHandler preFlightHandler) { - preFlightHandler.handlePreFlight(request, response); - return; - } - throw new IllegalStateException("Expected PreFlightRequestHandler: " + handler.getClass()); - } - } - throw new NoHandlerFoundException( - request.getMethod(), request.getRequestURI(), new ServletServerHttpRequest(request).getHeaders()); - } - finally { - ServletRequestPathUtils.setParsedRequestPath(previousPath, request); - } - } - - - /** - * {@link Filter} that looks up the {@code MatchableHandlerMapping} and - * {@link CorsConfiguration} for the request proactively before delegating - * to the rest of the chain, caching the result in a request attribute, and - * restoring it after the chain returns. - *

Note: Applications that rely on Spring Security do - * not use this component directly and should not deploy the filter instead - * allowing Spring Security to do it. Other custom security layers used in - * place of Spring Security that also rely on {@code HandlerMappingIntrospector} - * should deploy this filter ahead of other filters where lookups are - * performed, and should also make sure the filter is configured to handle - * all dispatcher types. - * @return the Filter instance to use - * @since 6.0.14 - */ - public Filter createCacheFilter() { - return (request, response, chain) -> { - CachedResult previous = setCache((HttpServletRequest) request); - try { - chain.doFilter(request, response); - } - finally { - resetCache(request, previous); - } - }; - } - - /** - * Perform a lookup and save the {@link CachedResult} as a request attribute. - * This method can be invoked from a filter before subsequent calls to - * {@link #getMatchableHandlerMapping(HttpServletRequest)} and - * {@link #getCorsConfiguration(HttpServletRequest)} to avoid repeated lookups. - * @param request the current request - * @return the previous {@link CachedResult}, if there is one from a parent dispatch - * @since 6.0.14 - */ - public @Nullable CachedResult setCache(HttpServletRequest request) { - CachedResult previous = (CachedResult) request.getAttribute(CACHED_RESULT_ATTRIBUTE); - if (previous == null || !previous.matches(request)) { - HttpServletRequest wrapped = new AttributesPreservingRequest(request); - CachedResult result; - try { - // Try to get both in one lookup (with ignoringException=false) - result = doWithHandlerMapping(wrapped, false, (mapping, executionChain) -> { - MatchableHandlerMapping matchableMapping = createMatchableHandlerMapping(mapping, wrapped); - CorsConfiguration corsConfig = getCorsConfiguration(executionChain, wrapped); - return new CachedResult(request, matchableMapping, corsConfig, null, null); - }); - } - catch (Exception ex) { - try { - // Try CorsConfiguration at least with ignoreException=true - AttributesPreservingRequest requestToUse = new AttributesPreservingRequest(request); - result = doWithHandlerMapping(requestToUse, true, (mapping, executionChain) -> { - CorsConfiguration corsConfig = getCorsConfiguration(executionChain, wrapped); - return new CachedResult(request, null, corsConfig, ex, null); - }); - } - catch (Exception ex2) { - result = new CachedResult(request, null, null, ex, new IllegalStateException(ex2)); - } - } - if (result == null) { - result = new CachedResult(request, null, null, null, null); - } - request.setAttribute(CACHED_RESULT_ATTRIBUTE, result); - } - return previous; - } - - /** - * Restore a previous {@link CachedResult}. This method can be invoked from - * a filter after delegating to the rest of the chain. - * @since 6.0.14 - */ - public void resetCache(ServletRequest request, @Nullable CachedResult cachedResult) { - request.setAttribute(CACHED_RESULT_ATTRIBUTE, cachedResult); - } - - /** - * Find the {@link HandlerMapping} that would handle the given request and - * return a {@link MatchableHandlerMapping} to use for path matching. - * @param request the current request - * @return the resolved {@code MatchableHandlerMapping}, or {@code null} - * @throws IllegalStateException if the matching HandlerMapping is not an - * instance of {@link MatchableHandlerMapping} - * @throws Exception if any of the HandlerMapping's raise an exception - */ - public @Nullable MatchableHandlerMapping getMatchableHandlerMapping(HttpServletRequest request) throws Exception { - CachedResult result = CachedResult.getResultFor(request); - if (result != null) { - return result.getHandlerMapping(); - } - this.cacheLogHelper.logHandlerMappingCacheMiss(request); - HttpServletRequest requestToUse = new AttributesPreservingRequest(request); - return doWithHandlerMapping(requestToUse, false, - (mapping, executionChain) -> createMatchableHandlerMapping(mapping, requestToUse)); - } - - private MatchableHandlerMapping createMatchableHandlerMapping(HandlerMapping mapping, HttpServletRequest request) { - if (mapping instanceof MatchableHandlerMapping) { - PathPatternMatchableHandlerMapping pathPatternMapping = this.pathPatternMappings.get(mapping); - if (pathPatternMapping != null) { - RequestPath requestPath = ServletRequestPathUtils.getParsedRequestPath(request); - return new LookupPathMatchableHandlerMapping(pathPatternMapping, requestPath); - } - else { - String lookupPath = (String) request.getAttribute(UrlPathHelper.PATH_ATTRIBUTE); - return new LookupPathMatchableHandlerMapping((MatchableHandlerMapping) mapping, lookupPath); - } - } - throw new IllegalStateException("HandlerMapping is not a MatchableHandlerMapping"); - } - - @Override - public @Nullable CorsConfiguration getCorsConfiguration(HttpServletRequest request) { - CachedResult result = CachedResult.getResultFor(request); - if (result != null) { - return result.getCorsConfig(); - } - this.cacheLogHelper.logCorsConfigCacheMiss(request); - try { - boolean ignoreException = true; - AttributesPreservingRequest requestToUse = new AttributesPreservingRequest(request); - return doWithHandlerMapping(requestToUse, ignoreException, - (handlerMapping, executionChain) -> getCorsConfiguration(executionChain, requestToUse)); - } - catch (Exception ex) { - // HandlerMapping exceptions are ignored. More basic error like parsing the request path. - throw new IllegalStateException(ex); - } - } - - private static @Nullable CorsConfiguration getCorsConfiguration(HandlerExecutionChain chain, HttpServletRequest request) { - for (HandlerInterceptor interceptor : chain.getInterceptorList()) { - if (interceptor instanceof CorsConfigurationSource source) { - return source.getCorsConfiguration(request); - } - } - if (chain.getHandler() instanceof CorsConfigurationSource source) { - return source.getCorsConfiguration(request); - } - return null; - } - - private @Nullable T doWithHandlerMapping( - HttpServletRequest request, boolean ignoreException, - BiFunction extractor) throws Exception { - - Assert.state(this.handlerMappings != null, "HandlerMapping's not initialized"); - - boolean parsePath = !this.pathPatternMappings.isEmpty(); - RequestPath previousPath = null; - if (parsePath) { - previousPath = (RequestPath) request.getAttribute(ServletRequestPathUtils.PATH_ATTRIBUTE); - ServletRequestPathUtils.parseAndCache(request); - } - try { - for (HandlerMapping handlerMapping : this.handlerMappings) { - HandlerExecutionChain chain = null; - try { - chain = handlerMapping.getHandler(request); - } - catch (Exception ex) { - if (!ignoreException) { - throw ex; - } - } - if (chain == null) { - continue; - } - return extractor.apply(handlerMapping, chain); - } - } - finally { - if (parsePath) { - ServletRequestPathUtils.setParsedRequestPath(previousPath, request); - } - } - return null; - } - - - /** - * Container for a {@link MatchableHandlerMapping} and {@link CorsConfiguration} - * for a given request matched by dispatcher type and requestURI. - * @since 6.0.14 - */ - @SuppressWarnings("serial") - public static final class CachedResult { - - private final DispatcherType dispatcherType; - - private final String requestURI; - - private final @Nullable MatchableHandlerMapping handlerMapping; - - private final @Nullable CorsConfiguration corsConfig; - - private final @Nullable Exception failure; - - private final @Nullable IllegalStateException corsConfigFailure; - - private CachedResult(HttpServletRequest request, - @Nullable MatchableHandlerMapping mapping, @Nullable CorsConfiguration config, - @Nullable Exception failure, @Nullable IllegalStateException corsConfigFailure) { - - this.dispatcherType = request.getDispatcherType(); - this.requestURI = request.getRequestURI(); - this.handlerMapping = mapping; - this.corsConfig = config; - this.failure = failure; - this.corsConfigFailure = corsConfigFailure; - } - - public boolean matches(HttpServletRequest request) { - return (this.dispatcherType.equals(request.getDispatcherType()) && - this.requestURI.equals(request.getRequestURI())); - } - - public @Nullable MatchableHandlerMapping getHandlerMapping() throws Exception { - if (this.failure != null) { - throw this.failure; - } - return this.handlerMapping; - } - - public @Nullable CorsConfiguration getCorsConfig() { - if (this.corsConfigFailure != null) { - throw this.corsConfigFailure; - } - return this.corsConfig; - } - - @Override - public String toString() { - return "CachedResult for " + this.dispatcherType + " dispatch to '" + this.requestURI + "'"; - } - - - /** - * Return a {@link CachedResult} that matches the given request. - */ - public static @Nullable CachedResult getResultFor(HttpServletRequest request) { - CachedResult result = (CachedResult) request.getAttribute(CACHED_RESULT_ATTRIBUTE); - return (result != null && result.matches(request) ? result : null); - } - } - - - private static class CacheResultLogHelper { - - private final Map counters = - Map.of("MatchableHandlerMapping", new AtomicInteger(), "CorsConfiguration", new AtomicInteger()); - - public void logHandlerMappingCacheMiss(HttpServletRequest request) { - logCacheMiss("MatchableHandlerMapping", request); - } - - public void logCorsConfigCacheMiss(HttpServletRequest request) { - logCacheMiss("CorsConfiguration", request); - } - - private void logCacheMiss(String label, HttpServletRequest request) { - AtomicInteger counter = this.counters.get(label); - Assert.notNull(counter, "Expected '" + label + "' counter."); - - String message = getLogMessage(label, request); - - if (logger.isDebugEnabled() && counter.getAndIncrement() == 0) { - logger.debug(message + " This is logged once only at DEBUG level, and every time at TRACE."); - } - else if (logger.isTraceEnabled()) { - logger.trace("No CachedResult, performing " + label + " lookup instead."); - } - } - - private static String getLogMessage(String label, HttpServletRequest request) { - return "Cache miss for " + request.getDispatcherType() + " dispatch to '" + request.getRequestURI() + "' " + - "(previous " + request.getAttribute(CACHED_RESULT_ATTRIBUTE) + "). " + - "Performing " + label + " lookup. If there are repeated lookups per request, " + - "consider using HandlerMappingIntrospector#createCacheFilter()" + - "to create a Servlet Filter to set the cache for the request."; - } - } - - - /** - * Request wrapper that buffers request attributes in order protect the - * underlying request from attribute changes. - */ - private static class AttributesPreservingRequest extends HttpServletRequestWrapper { - - private final Map attributes; - - AttributesPreservingRequest(HttpServletRequest request) { - super(request); - this.attributes = initAttributes(request); - this.attributes.put(AbstractHandlerMapping.SUPPRESS_LOGGING_ATTRIBUTE, Boolean.TRUE); - } - - private Map initAttributes(HttpServletRequest request) { - Map map = new HashMap<>(); - Enumeration names = request.getAttributeNames(); - while (names.hasMoreElements()) { - String name = names.nextElement(); - map.put(name, request.getAttribute(name)); - } - return map; - } - - @Override - public void setAttribute(String name, Object value) { - this.attributes.put(name, value); - } - - @Override - public @Nullable Object getAttribute(String name) { - return this.attributes.get(name); - } - - @Override - public Enumeration getAttributeNames() { - return Collections.enumeration(this.attributes.keySet()); - } - - @Override - public void removeAttribute(String name) { - this.attributes.remove(name); - } - } - - - private static class LookupPathMatchableHandlerMapping implements MatchableHandlerMapping { - - private final MatchableHandlerMapping delegate; - - private final Object lookupPath; - - private final String pathAttributeName; - - LookupPathMatchableHandlerMapping(MatchableHandlerMapping delegate, Object lookupPath) { - this.delegate = delegate; - this.lookupPath = lookupPath; - this.pathAttributeName = (lookupPath instanceof RequestPath ? - ServletRequestPathUtils.PATH_ATTRIBUTE : UrlPathHelper.PATH_ATTRIBUTE); - } - - @Override - public @Nullable PathPatternParser getPatternParser() { - return this.delegate.getPatternParser(); - } - - @SuppressWarnings("removal") - @Deprecated(since = "7.0", forRemoval = true) - @Override - public @Nullable RequestMatchResult match(HttpServletRequest request, String pattern) { - pattern = initFullPathPattern(pattern); - Object previousPath = request.getAttribute(this.pathAttributeName); - request.setAttribute(this.pathAttributeName, this.lookupPath); - try { - return this.delegate.match(request, pattern); - } - finally { - request.setAttribute(this.pathAttributeName, previousPath); - } - } - - private String initFullPathPattern(String pattern) { - PathPatternParser parser = (getPatternParser() != null ? getPatternParser() : PathPatternParser.defaultInstance); - return parser.initFullPathPattern(pattern); - } - - @Override - public @Nullable HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception { - return this.delegate.getHandler(request); - } - } - -} diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/MatchableHandlerMapping.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/MatchableHandlerMapping.java index 5466683f35ab..2ae3c4a4e588 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/MatchableHandlerMapping.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/MatchableHandlerMapping.java @@ -31,7 +31,7 @@ * * @author Rossen Stoyanchev * @since 4.3.1 - * @deprecated together with {@link HandlerMappingIntrospector} without a replacement. + * @deprecated without a replacement. */ @Deprecated(since = "7.0", forRemoval = true) public interface MatchableHandlerMapping extends HandlerMapping { diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/PathPatternMatchableHandlerMapping.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/PathPatternMatchableHandlerMapping.java deleted file mode 100644 index e44a565b33cb..000000000000 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/PathPatternMatchableHandlerMapping.java +++ /dev/null @@ -1,79 +0,0 @@ -/* - * Copyright 2002-present 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 - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.web.servlet.handler; - -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; - -import jakarta.servlet.http.HttpServletRequest; -import org.jspecify.annotations.Nullable; - -import org.springframework.http.server.PathContainer; -import org.springframework.util.Assert; -import org.springframework.web.servlet.HandlerExecutionChain; -import org.springframework.web.util.ServletRequestPathUtils; -import org.springframework.web.util.pattern.PathPattern; -import org.springframework.web.util.pattern.PathPatternParser; - -/** - * Decorate another {@link MatchableHandlerMapping} that's configured with a - * {@link PathPatternParser} in order to parse and cache String patterns passed - * into the {@code match} method. - * - * @author Rossen Stoyanchev - * @since 5.3 - * @deprecated together with {@link HandlerMappingIntrospector} without a replacement. - */ -@SuppressWarnings("removal") -@Deprecated(since = "7.0", forRemoval = true) -class PathPatternMatchableHandlerMapping implements MatchableHandlerMapping { - - private final MatchableHandlerMapping delegate; - - private final PathPatternParser parser; - - private final Map pathPatternCache = new ConcurrentHashMap<>(); - - private final int cacheLimit; - - - public PathPatternMatchableHandlerMapping(MatchableHandlerMapping delegate, int cacheLimit) { - Assert.notNull(delegate, "HandlerMapping to delegate to is required."); - Assert.notNull(delegate.getPatternParser(), "Expected HandlerMapping configured to use PatternParser."); - this.delegate = delegate; - this.parser = delegate.getPatternParser(); - this.cacheLimit = cacheLimit; - } - - @SuppressWarnings("removal") - @Deprecated(since = "7.0", forRemoval = true) - @Override - public @Nullable RequestMatchResult match(HttpServletRequest request, String pattern) { - PathPattern pathPattern = this.pathPatternCache.computeIfAbsent(pattern, value -> { - Assert.state(this.pathPatternCache.size() < this.cacheLimit, "Max size for pattern cache exceeded."); - return this.parser.parse(pattern); - }); - PathContainer path = ServletRequestPathUtils.getParsedRequestPath(request).pathWithinApplication(); - return (pathPattern.matches(path) ? new RequestMatchResult(pathPattern, path) : null); - } - - @Override - public @Nullable HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception { - return this.delegate.getHandler(request); - } - -} diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/RequestMatchResult.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/RequestMatchResult.java index 9aa1f24a9233..fe993bda8d12 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/RequestMatchResult.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/RequestMatchResult.java @@ -34,7 +34,7 @@ * * @author Rossen Stoyanchev * @since 4.3.1 - * @deprecated together with {@link HandlerMappingIntrospector} without a replacement. + * @deprecated without a replacement. */ @SuppressWarnings("removal") @Deprecated(since = "7.0", forRemoval = true) diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/config/MvcNamespaceTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/config/MvcNamespaceTests.java index 3de64e17c6b7..5c495f2c2ed6 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/config/MvcNamespaceTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/config/MvcNamespaceTests.java @@ -95,7 +95,6 @@ import org.springframework.web.servlet.handler.AbstractHandlerMapping; import org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping; import org.springframework.web.servlet.handler.ConversionServiceExposingInterceptor; -import org.springframework.web.servlet.handler.HandlerMappingIntrospector; import org.springframework.web.servlet.handler.MappedInterceptor; import org.springframework.web.servlet.handler.SimpleUrlHandlerMapping; import org.springframework.web.servlet.handler.UserRoleAuthorizationInterceptor; @@ -260,13 +259,6 @@ void defaultConfig() throws Exception { CompositeUriComponentsContributor.class); assertThat(uriComponentsContributor).isNotNull(); - - String name = "mvcHandlerMappingIntrospector"; - HandlerMappingIntrospector introspector = this.appContext.getBean(name, HandlerMappingIntrospector.class); - assertThat(introspector).isNotNull(); - assertThat(introspector.getHandlerMappings()).hasSize(2); - assertThat(introspector.getHandlerMappings()).element(0).isSameAs(mapping); - assertThat(introspector.getHandlerMappings().get(1).getClass()).isEqualTo(BeanNameUrlHandlerMapping.class); } @Test // gh-25290 diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/handler/DefaultPreFlightRequestHandlerTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/handler/DefaultPreFlightRequestHandlerTests.java new file mode 100644 index 000000000000..e0794ea4f069 --- /dev/null +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/handler/DefaultPreFlightRequestHandlerTests.java @@ -0,0 +1,129 @@ +/* + * Copyright 2002-present 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 + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.web.servlet.handler; + +import org.junit.jupiter.api.Test; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpHeaders; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.CrossOrigin; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.context.WebApplicationContext; +import org.springframework.web.context.support.AnnotationConfigWebApplicationContext; +import org.springframework.web.cors.PreFlightRequestHandler; +import org.springframework.web.servlet.NoHandlerFoundException; +import org.springframework.web.servlet.function.RouterFunction; +import org.springframework.web.servlet.function.RouterFunctions; +import org.springframework.web.servlet.function.ServerResponse; +import org.springframework.web.servlet.function.support.RouterFunctionMapping; +import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping; +import org.springframework.web.testfixture.servlet.MockHttpServletRequest; +import org.springframework.web.testfixture.servlet.MockHttpServletResponse; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +/** + * Tests for {@link DefaultPreFlightRequestHandler}. + * + * @since 7.1 + */ +class DefaultPreFlightRequestHandlerTests { + + @Test + void handlePreFlight() throws Exception { + AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext(); + context.register(TestConfig.class); + context.refresh(); + + MockHttpServletRequest request = new MockHttpServletRequest("OPTIONS", "/path"); + request.addHeader("Origin", "http://localhost:9000"); + request.addHeader(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, "POST"); + MockHttpServletResponse response = new MockHttpServletResponse(); + + initHandler(context).handlePreFlight(request, response); + + assertThat(response.getHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN)).isEqualTo("http://localhost:9000"); + assertThat(response.getHeaders(HttpHeaders.ACCESS_CONTROL_ALLOW_METHODS)).containsExactly("POST"); + } + + @Test + void handlePreFlightWithNoHandlerFoundException() { + AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext(); + context.register(TestConfig.class); + context.refresh(); + + MockHttpServletRequest request = new MockHttpServletRequest("OPTIONS", "/unknownPath"); + request.addHeader("Origin", "http://localhost:9000"); + request.addHeader(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, "POST"); + MockHttpServletResponse response = new MockHttpServletResponse(); + + assertThatThrownBy(() -> initHandler(context).handlePreFlight(request, response)) + .isInstanceOf(NoHandlerFoundException.class); + + assertThat(response.getHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN)).isNull(); + assertThat(response.getHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_METHODS)).isNull(); + } + + private static PreFlightRequestHandler initHandler(WebApplicationContext context) { + DefaultPreFlightRequestHandler preFlightRequestHandler = new DefaultPreFlightRequestHandler(); + preFlightRequestHandler.setApplicationContext(context); + preFlightRequestHandler.afterPropertiesSet(); + return preFlightRequestHandler; + } + + + @Configuration + static class TestConfig { + + @Bean + public RouterFunctionMapping routerFunctionMapping() { + RouterFunctionMapping mapping = new RouterFunctionMapping(); + mapping.setOrder(1); + return mapping; + } + + @Bean + public RequestMappingHandlerMapping handlerMapping() { + RequestMappingHandlerMapping mapping = new RequestMappingHandlerMapping(); + mapping.setOrder(2); + return mapping; + } + + @Bean + TestController testController() { + return new TestController(); + } + + @Bean + public RouterFunction routerFunction() { + return RouterFunctions.route().GET("/fn-path", request -> ServerResponse.ok().build()).build(); + } + } + + + @CrossOrigin("http://localhost:9000") + @Controller + private static class TestController { + + @PostMapping("/path") + void handle() { + } + } +} diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/handler/HandlerMappingIntrospectorTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/handler/HandlerMappingIntrospectorTests.java deleted file mode 100644 index c3792e777324..000000000000 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/handler/HandlerMappingIntrospectorTests.java +++ /dev/null @@ -1,479 +0,0 @@ -/* - * Copyright 2002-present 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 - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.web.servlet.handler; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; - -import jakarta.servlet.Filter; -import jakarta.servlet.FilterChain; -import jakarta.servlet.ServletException; -import jakarta.servlet.ServletRequest; -import jakarta.servlet.ServletResponse; -import jakarta.servlet.http.HttpServlet; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.ValueSource; - -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.http.HttpHeaders; -import org.springframework.stereotype.Controller; -import org.springframework.web.bind.annotation.CrossOrigin; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.context.WebApplicationContext; -import org.springframework.web.context.support.AnnotationConfigWebApplicationContext; -import org.springframework.web.context.support.GenericWebApplicationContext; -import org.springframework.web.context.support.StaticWebApplicationContext; -import org.springframework.web.cors.CorsConfiguration; -import org.springframework.web.cors.CorsConfigurationSource; -import org.springframework.web.servlet.HandlerExecutionChain; -import org.springframework.web.servlet.HandlerMapping; -import org.springframework.web.servlet.NoHandlerFoundException; -import org.springframework.web.servlet.function.RouterFunction; -import org.springframework.web.servlet.function.RouterFunctions; -import org.springframework.web.servlet.function.ServerResponse; -import org.springframework.web.servlet.function.support.RouterFunctionMapping; -import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping; -import org.springframework.web.testfixture.servlet.MockFilterChain; -import org.springframework.web.testfixture.servlet.MockHttpServletRequest; -import org.springframework.web.testfixture.servlet.MockHttpServletResponse; -import org.springframework.web.util.ServletRequestPathUtils; -import org.springframework.web.util.pattern.PathPattern; -import org.springframework.web.util.pattern.PathPatternParser; -import org.springframework.web.util.pattern.PatternParseException; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatIllegalStateException; -import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.springframework.web.servlet.HandlerMapping.BEST_MATCHING_PATTERN_ATTRIBUTE; - -/** - * Tests for {@link HandlerMappingIntrospector}. - * - * @author Rossen Stoyanchev - * @since 4.3.1 - */ -@SuppressWarnings("removal") -class HandlerMappingIntrospectorTests { - - @Test - void detectHandlerMappings() { - StaticWebApplicationContext context = new StaticWebApplicationContext(); - context.registerSingleton("A", SimpleUrlHandlerMapping.class); - context.registerSingleton("B", SimpleUrlHandlerMapping.class); - context.registerSingleton("C", SimpleUrlHandlerMapping.class); - context.refresh(); - - List expected = Arrays.asList(context.getBean("A"), context.getBean("B"), context.getBean("C")); - List actual = initIntrospector(context).getHandlerMappings(); - - assertThat(actual).isEqualTo(expected); - } - - @Test - void detectHandlerMappingsOrdered() { - GenericWebApplicationContext context = new GenericWebApplicationContext(); - context.registerBean("B", SimpleUrlHandlerMapping.class, () -> { - SimpleUrlHandlerMapping mapping = new SimpleUrlHandlerMapping(); - mapping.setOrder(2); - return mapping; - }); - context.registerBean("C", SimpleUrlHandlerMapping.class, () -> { - SimpleUrlHandlerMapping mapping = new SimpleUrlHandlerMapping(); - mapping.setOrder(3); - return mapping; - }); - context.registerBean("A", SimpleUrlHandlerMapping.class, () -> { - SimpleUrlHandlerMapping mapping = new SimpleUrlHandlerMapping(); - mapping.setOrder(1); - return mapping; - }); - context.refresh(); - - List expected = Arrays.asList(context.getBean("A"), context.getBean("B"), context.getBean("C")); - List actual = initIntrospector(context).getHandlerMappings(); - - assertThat(actual).isEqualTo(expected); - } - - @Test - void useParsedPatternsOnly() { - GenericWebApplicationContext context = new GenericWebApplicationContext(); - context.registerBean("A", SimpleUrlHandlerMapping.class); - context.registerBean("B", SimpleUrlHandlerMapping.class); - context.registerBean("C", SimpleUrlHandlerMapping.class); - context.refresh(); - - assertThat(initIntrospector(context).allHandlerMappingsUsePathPatternParser()).isTrue(); - - context = new GenericWebApplicationContext(); - context.registerBean("A", SimpleUrlHandlerMapping.class); - context.registerBean("B", SimpleUrlHandlerMapping.class); - context.registerBean("C", SimpleUrlHandlerMapping.class, () -> { - SimpleUrlHandlerMapping mapping = new SimpleUrlHandlerMapping(); - mapping.setPatternParser(null); - return mapping; - }); - context.refresh(); - - assertThat(initIntrospector(context).allHandlerMappingsUsePathPatternParser()).isFalse(); - } - - @SuppressWarnings("removal") - @ParameterizedTest - @ValueSource(booleans = {true, false}) - void getMatchable(boolean usePathPatterns) throws Exception { - - TestPathPatternParser parser = new TestPathPatternParser(); - - GenericWebApplicationContext context = new GenericWebApplicationContext(); - context.registerBean("mapping", SimpleUrlHandlerMapping.class, () -> { - SimpleUrlHandlerMapping mapping = new SimpleUrlHandlerMapping(); - if (usePathPatterns) { - mapping.setPatternParser(parser); - } - mapping.setUrlMap(Collections.singletonMap("/path/*", new Object())); - return mapping; - }); - context.refresh(); - - MockHttpServletRequest request = new MockHttpServletRequest("GET", "/path/123"); - MatchableHandlerMapping mapping = initIntrospector(context).getMatchableHandlerMapping(request); - - assertThat(mapping).isNotNull(); - assertThat(request.getAttribute(BEST_MATCHING_PATTERN_ATTRIBUTE)).as("Attribute changes not ignored").isNull(); - assertThat(request.getAttribute(ServletRequestPathUtils.PATH_ATTRIBUTE)).as("Parsed path not cleaned").isNull(); - - assertThat(mapping.match(request, "/p*/*")).isNotNull(); - assertThat(mapping.match(request, "/b*/*")).isNull(); - - if (usePathPatterns) { - assertThat(parser.getParsedPatterns()).containsExactly("/path/*", "/p*/*", "/b*/*"); - } - } - - @Test - void getMatchableWhereHandlerMappingDoesNotImplementMatchableInterface() { - StaticWebApplicationContext cxt = new StaticWebApplicationContext(); - cxt.registerBean("mapping", HandlerMapping.class, () -> request -> new HandlerExecutionChain(new Object())); - cxt.refresh(); - - MockHttpServletRequest request = new MockHttpServletRequest(); - assertThatIllegalStateException().isThrownBy(() -> initIntrospector(cxt).getMatchableHandlerMapping(request)); - } - - @Test // gh-26833 - void getMatchablePreservesRequestAttributes() throws Exception { - AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext(); - context.register(TestConfig.class); - context.refresh(); - - MockHttpServletRequest request = new MockHttpServletRequest("POST", "/path"); - request.setAttribute("name", "value"); - - MatchableHandlerMapping matchable = initIntrospector(context).getMatchableHandlerMapping(request); - assertThat(matchable).isNotNull(); - - // RequestPredicates.restoreAttributes clears and re-adds attributes - assertThat(request.getAttribute("name")).isEqualTo("value"); - } - - @Test - void getCorsConfigurationPreFlight() { - AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext(); - context.register(TestConfig.class); - context.refresh(); - - // PRE-FLIGHT - - MockHttpServletRequest request = new MockHttpServletRequest("OPTIONS", "/path"); - request.addHeader("Origin", "http://localhost:9000"); - request.addHeader(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, "POST"); - CorsConfiguration corsConfig = initIntrospector(context).getCorsConfiguration(request); - - assertThat(corsConfig).isNotNull(); - assertThat(corsConfig.getAllowedOrigins()).isEqualTo(Collections.singletonList("http://localhost:9000")); - assertThat(corsConfig.getAllowedMethods()).isEqualTo(Collections.singletonList("POST")); - } - - @Test - void getCorsConfigurationActual() { - AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext(); - context.register(TestConfig.class); - context.refresh(); - - MockHttpServletRequest request = new MockHttpServletRequest("POST", "/path"); - request.addHeader("Origin", "http://localhost:9000"); - CorsConfiguration corsConfig = initIntrospector(context).getCorsConfiguration(request); - - assertThat(corsConfig).isNotNull(); - assertThat(corsConfig.getAllowedOrigins()).isEqualTo(Collections.singletonList("http://localhost:9000")); - assertThat(corsConfig.getAllowedMethods()).isEqualTo(Collections.singletonList("POST")); - } - - @Test - void handlePreFlight() throws Exception { - AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext(); - context.register(TestConfig.class); - context.refresh(); - - MockHttpServletRequest request = new MockHttpServletRequest("OPTIONS", "/path"); - request.addHeader("Origin", "http://localhost:9000"); - request.addHeader(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, "POST"); - MockHttpServletResponse response = new MockHttpServletResponse(); - - initIntrospector(context).handlePreFlight(request, response); - - assertThat(response.getHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN)).isEqualTo("http://localhost:9000"); - assertThat(response.getHeaders(HttpHeaders.ACCESS_CONTROL_ALLOW_METHODS)).containsExactly("POST"); - } - - @Test - void handlePreFlightWithNoHandlerFoundException() { - AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext(); - context.register(TestConfig.class); - context.refresh(); - - MockHttpServletRequest request = new MockHttpServletRequest("OPTIONS", "/unknownPath"); - request.addHeader("Origin", "http://localhost:9000"); - request.addHeader(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, "POST"); - MockHttpServletResponse response = new MockHttpServletResponse(); - - assertThatThrownBy(() -> initIntrospector(context).handlePreFlight(request, response)) - .isInstanceOf(NoHandlerFoundException.class); - - assertThat(response.getHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN)).isNull(); - assertThat(response.getHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_METHODS)).isNull(); - } - - @ParameterizedTest - @ValueSource(strings = {"/test", "/resource/1234****"}) // gh-31937 - void cacheFilter(String uri) throws Exception { - CorsConfiguration corsConfig = new CorsConfiguration(); - TestMatchableHandlerMapping mapping = new TestMatchableHandlerMapping(); - mapping.registerHandler("/*", new TestHandler(corsConfig)); - - HandlerMappingIntrospector introspector = initIntrospector(mapping); - - MockHttpServletRequest request = new MockHttpServletRequest("GET", uri); - MockHttpServletResponse response = new MockHttpServletResponse(); - - MockFilterChain filterChain = new MockFilterChain( - new TestServlet(), introspector.createCacheFilter(), new AuthFilter(introspector, corsConfig)); - - filterChain.doFilter(request, response); - - assertThat(response.getContentAsString()).isEqualTo("Success"); - assertThat(mapping.getInvocationCount()).isEqualTo(1); - assertThat(mapping.getMatchCount()).isEqualTo(1); - } - - @Test - void cacheFilterWithNestedDispatch() throws Exception { - CorsConfiguration corsConfig1 = new CorsConfiguration(); - CorsConfiguration corsConfig2 = new CorsConfiguration(); - - TestMatchableHandlerMapping mapping1 = new TestMatchableHandlerMapping(); - TestMatchableHandlerMapping mapping2 = new TestMatchableHandlerMapping(); - - mapping1.registerHandler("/1", new TestHandler(corsConfig1)); - mapping2.registerHandler("/2", new TestHandler(corsConfig2)); - - HandlerMappingIntrospector introspector = initIntrospector(mapping1, mapping2); - - MockFilterChain filterChain = new MockFilterChain( - new TestServlet(), - introspector.createCacheFilter(), - new AuthFilter(introspector, corsConfig1), - (req, res, chain) -> chain.doFilter(new MockHttpServletRequest("GET", "/2"), res), - introspector.createCacheFilter(), - new AuthFilter(introspector, corsConfig2)); - - MockHttpServletResponse response = new MockHttpServletResponse(); - filterChain.doFilter(new MockHttpServletRequest("GET", "/1"), response); - - assertThat(response.getContentAsString()).isEqualTo("Success"); - assertThat(mapping1.getInvocationCount()).isEqualTo(2); - assertThat(mapping2.getInvocationCount()).isEqualTo(1); - assertThat(mapping1.getMatchCount()).isEqualTo(1); - assertThat(mapping2.getMatchCount()).isEqualTo(1); - } - - private HandlerMappingIntrospector initIntrospector(TestMatchableHandlerMapping... mappings) { - StaticWebApplicationContext context = new StaticWebApplicationContext(); - int index = 0; - for (TestMatchableHandlerMapping mapping : mappings) { - context.registerBean("mapping" + index++, TestMatchableHandlerMapping.class, () -> mapping); - } - context.refresh(); - return initIntrospector(context); - } - - private static HandlerMappingIntrospector initIntrospector(WebApplicationContext context) { - HandlerMappingIntrospector introspector = new HandlerMappingIntrospector(); - introspector.setApplicationContext(context); - introspector.afterPropertiesSet(); - return introspector; - } - - - @Configuration - static class TestConfig { - - @Bean - public RouterFunctionMapping routerFunctionMapping() { - RouterFunctionMapping mapping = new RouterFunctionMapping(); - mapping.setOrder(1); - return mapping; - } - - @Bean - public RequestMappingHandlerMapping handlerMapping() { - RequestMappingHandlerMapping mapping = new RequestMappingHandlerMapping(); - mapping.setOrder(2); - return mapping; - } - - @Bean - public TestController testController() { - return new TestController(); - } - - @Bean - public RouterFunction routerFunction() { - return RouterFunctions.route().GET("/fn-path", request -> ServerResponse.ok().build()).build(); - } - } - - - @CrossOrigin("http://localhost:9000") - @Controller - private static class TestController { - - @PostMapping("/path") - void handle() { - } - } - - - private static class TestPathPatternParser extends PathPatternParser { - - private final List parsedPatterns = new ArrayList<>(); - - - public List getParsedPatterns() { - return this.parsedPatterns; - } - - @Override - public PathPattern parse(String pathPattern) throws PatternParseException { - this.parsedPatterns.add(pathPattern); - return super.parse(pathPattern); - } - } - - - private static class TestMatchableHandlerMapping extends SimpleUrlHandlerMapping { - - private int invocationCount; - - private int matchCount; - - public int getInvocationCount() { - return this.invocationCount; - } - - public int getMatchCount() { - return this.matchCount; - } - - @Override - protected Object getHandlerInternal(HttpServletRequest request) throws Exception { - this.invocationCount++; - Object handler = super.getHandlerInternal(request); - if (handler != null) { - this.matchCount++; - } - return handler; - } - } - - - private static class TestHandler implements CorsConfigurationSource { - - private final CorsConfiguration corsConfig; - - private TestHandler(CorsConfiguration corsConfig) { - this.corsConfig = corsConfig; - } - - @Override - public CorsConfiguration getCorsConfiguration(HttpServletRequest request) { - return this.corsConfig; - } - } - - - private static class AuthFilter implements Filter { - - private final HandlerMappingIntrospector introspector; - - private final CorsConfiguration corsConfig; - - private AuthFilter(HandlerMappingIntrospector introspector, CorsConfiguration corsConfig) { - this.introspector = introspector; - this.corsConfig = corsConfig; - } - - @Override - public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { - try { - for (int i = 0; i < 10; i++) { - HttpServletRequest httpRequest = (HttpServletRequest) req; - assertThat(introspector.getMatchableHandlerMapping(httpRequest)).isNotNull(); - assertThat(introspector.getCorsConfiguration(httpRequest)).isSameAs(corsConfig); - } - } - catch (Exception ex) { - throw new IllegalStateException(ex); - } - chain.doFilter(req, res); - } - } - - - @SuppressWarnings("serial") - private static class TestServlet extends HttpServlet { - - @Override - protected void service(HttpServletRequest req, HttpServletResponse res) { - try { - res.getWriter().print("Success"); - } - catch (Exception ex) { - throw new IllegalStateException(ex); - } - } - } - -}