Skip to content

Commit a30ab30

Browse files
committed
Introduce HandlerMapping introspection API
Issue: SPR-14321
1 parent a25c43f commit a30ab30

File tree

7 files changed

+554
-11
lines changed

7 files changed

+554
-11
lines changed

spring-webmvc/src/main/java/org/springframework/web/servlet/handler/AbstractHandlerMapping.java

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -27,21 +27,21 @@
2727
import org.springframework.beans.BeansException;
2828
import org.springframework.beans.factory.BeanFactoryUtils;
2929
import org.springframework.core.Ordered;
30-
import org.springframework.web.HttpRequestHandler;
31-
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
32-
import org.springframework.web.cors.CorsProcessor;
33-
import org.springframework.web.cors.CorsConfiguration;
34-
import org.springframework.web.cors.CorsConfigurationSource;
3530
import org.springframework.util.AntPathMatcher;
3631
import org.springframework.util.Assert;
3732
import org.springframework.util.PathMatcher;
33+
import org.springframework.web.HttpRequestHandler;
3834
import org.springframework.web.context.request.WebRequestInterceptor;
3935
import org.springframework.web.context.support.WebApplicationObjectSupport;
36+
import org.springframework.web.cors.CorsConfiguration;
37+
import org.springframework.web.cors.CorsConfigurationSource;
38+
import org.springframework.web.cors.CorsProcessor;
39+
import org.springframework.web.cors.CorsUtils;
40+
import org.springframework.web.cors.DefaultCorsProcessor;
41+
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
4042
import org.springframework.web.servlet.HandlerExecutionChain;
4143
import org.springframework.web.servlet.HandlerInterceptor;
4244
import org.springframework.web.servlet.HandlerMapping;
43-
import org.springframework.web.cors.DefaultCorsProcessor;
44-
import org.springframework.web.cors.CorsUtils;
4545
import org.springframework.web.util.UrlPathHelper;
4646

4747
/**
@@ -471,7 +471,7 @@ protected HandlerExecutionChain getCorsHandlerExecutionChain(HttpServletRequest
471471
}
472472

473473

474-
private class PreFlightHandler implements HttpRequestHandler {
474+
private class PreFlightHandler implements HttpRequestHandler, CorsConfigurationSource {
475475

476476
private final CorsConfiguration config;
477477

@@ -485,10 +485,15 @@ public void handleRequest(HttpServletRequest request, HttpServletResponse respon
485485

486486
corsProcessor.processRequest(this.config, request, response);
487487
}
488+
489+
@Override
490+
public CorsConfiguration getCorsConfiguration(HttpServletRequest request) {
491+
return this.config;
492+
}
488493
}
489494

490495

491-
private class CorsInterceptor extends HandlerInterceptorAdapter {
496+
private class CorsInterceptor extends HandlerInterceptorAdapter implements CorsConfigurationSource {
492497

493498
private final CorsConfiguration config;
494499

@@ -502,6 +507,11 @@ public boolean preHandle(HttpServletRequest request, HttpServletResponse respons
502507

503508
return corsProcessor.processRequest(this.config, request, response);
504509
}
510+
511+
@Override
512+
public CorsConfiguration getCorsConfiguration(HttpServletRequest request) {
513+
return this.config;
514+
}
505515
}
506516

507517
}

spring-webmvc/src/main/java/org/springframework/web/servlet/handler/AbstractUrlHandlerMapping.java

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@
3030
import org.springframework.util.CollectionUtils;
3131
import org.springframework.web.servlet.HandlerExecutionChain;
3232
import org.springframework.web.servlet.HandlerMapping;
33+
import org.springframework.web.servlet.support.MatchableHandlerMapping;
34+
import org.springframework.web.servlet.support.RequestMatchResult;
3335

3436
/**
3537
* Abstract base class for URL-mapped {@link org.springframework.web.servlet.HandlerMapping}
@@ -50,7 +52,8 @@
5052
* @author Arjen Poutsma
5153
* @since 16.04.2003
5254
*/
53-
public abstract class AbstractUrlHandlerMapping extends AbstractHandlerMapping {
55+
public abstract class AbstractUrlHandlerMapping extends AbstractHandlerMapping
56+
implements MatchableHandlerMapping {
5457

5558
private Object rootHandler;
5659

@@ -279,6 +282,20 @@ protected void exposeUriTemplateVariables(Map<String, String> uriTemplateVariabl
279282
request.setAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE, uriTemplateVariables);
280283
}
281284

285+
@Override
286+
public RequestMatchResult match(HttpServletRequest request, String pattern) {
287+
String lookupPath = getUrlPathHelper().getLookupPathForRequest(request);
288+
if (getPathMatcher().match(pattern, lookupPath)) {
289+
return new RequestMatchResult(pattern, lookupPath, getPathMatcher());
290+
}
291+
else if (useTrailingSlashMatch()) {
292+
if (!pattern.endsWith("/") && getPathMatcher().match(pattern + "/", lookupPath)) {
293+
return new RequestMatchResult(pattern + "/", lookupPath, getPathMatcher());
294+
}
295+
}
296+
return null;
297+
}
298+
282299
/**
283300
* Register the specified handler for the given URL paths.
284301
* @param urlPaths the URLs that the bean should be mapped to

spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerMapping.java

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@
2020
import java.lang.reflect.Method;
2121
import java.util.Arrays;
2222
import java.util.List;
23+
import java.util.Set;
24+
import javax.servlet.http.HttpServletRequest;
2325

2426
import org.springframework.context.EmbeddedValueResolverAware;
2527
import org.springframework.core.annotation.AnnotatedElementUtils;
@@ -38,6 +40,8 @@
3840
import org.springframework.web.servlet.mvc.condition.RequestCondition;
3941
import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
4042
import org.springframework.web.servlet.mvc.method.RequestMappingInfoHandlerMapping;
43+
import org.springframework.web.servlet.support.MatchableHandlerMapping;
44+
import org.springframework.web.servlet.support.RequestMatchResult;
4145

4246
/**
4347
* Creates {@link RequestMappingInfo} instances from type and method-level
@@ -50,7 +54,7 @@
5054
* @since 3.1
5155
*/
5256
public class RequestMappingHandlerMapping extends RequestMappingInfoHandlerMapping
53-
implements EmbeddedValueResolverAware {
57+
implements EmbeddedValueResolverAware, MatchableHandlerMapping {
5458

5559
private boolean useSuffixPatternMatch = true;
5660

@@ -274,6 +278,18 @@ protected String[] resolveEmbeddedValuesInPatterns(String[] patterns) {
274278
}
275279
}
276280

281+
@Override
282+
public RequestMatchResult match(HttpServletRequest request, String pattern) {
283+
RequestMappingInfo info = RequestMappingInfo.paths(pattern).options(this.config).build();
284+
RequestMappingInfo matchingInfo = info.getMatchingCondition(request);
285+
if (matchingInfo == null) {
286+
return null;
287+
}
288+
Set<String> patterns = matchingInfo.getPatternsCondition().getPatterns();
289+
String lookupPath = getUrlPathHelper().getLookupPathForRequest(request);
290+
return new RequestMatchResult(patterns.iterator().next(), lookupPath, getPathMatcher());
291+
}
292+
277293
@Override
278294
protected CorsConfiguration initCorsConfiguration(Object handler, Method method, RequestMappingInfo mappingInfo) {
279295
HandlerMethod handlerMethod = createHandlerMethod(handler, method);
Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
/*
2+
* Copyright 2002-2016 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.web.servlet.support;
17+
18+
import java.io.IOException;
19+
import java.util.ArrayList;
20+
import java.util.List;
21+
import java.util.Map;
22+
import java.util.Properties;
23+
import javax.servlet.http.HttpServletRequest;
24+
import javax.servlet.http.HttpServletRequestWrapper;
25+
26+
import org.springframework.beans.factory.BeanFactoryUtils;
27+
import org.springframework.context.ApplicationContext;
28+
import org.springframework.core.annotation.AnnotationAwareOrderComparator;
29+
import org.springframework.core.io.ClassPathResource;
30+
import org.springframework.core.io.Resource;
31+
import org.springframework.core.io.support.PropertiesLoaderUtils;
32+
import org.springframework.util.ClassUtils;
33+
import org.springframework.util.StringUtils;
34+
import org.springframework.web.cors.CorsConfiguration;
35+
import org.springframework.web.cors.CorsConfigurationSource;
36+
import org.springframework.web.servlet.DispatcherServlet;
37+
import org.springframework.web.servlet.HandlerExecutionChain;
38+
import org.springframework.web.servlet.HandlerInterceptor;
39+
import org.springframework.web.servlet.HandlerMapping;
40+
41+
/**
42+
* Helper class to get information from the {@code HandlerMapping} that would
43+
* serve a specific request.
44+
*
45+
* <p>Provides the following methods:
46+
* <ul>
47+
* <li>{@link #getMatchableHandlerMapping} -- obtain a {@code HandlerMapping}
48+
* to check request-matching criteria against.
49+
* <li>{@link #getCorsConfiguration} -- obtain the CORS configuration for the
50+
* request.
51+
* </ul>
52+
*
53+
* @author Rossen Stoyanchev
54+
* @since 4.3
55+
*/
56+
public class HandlerMappingIntrospector implements CorsConfigurationSource {
57+
58+
private final List<HandlerMapping> handlerMappings;
59+
60+
61+
/**
62+
* Constructor that detects the configured {@code HandlerMapping}s in the
63+
* given {@code ApplicationContext} or falling back on
64+
* "DispatcherServlet.properties" like the {@code DispatcherServlet}.
65+
*/
66+
public HandlerMappingIntrospector(ApplicationContext context) {
67+
this.handlerMappings = initHandlerMappings(context);
68+
}
69+
70+
71+
private static List<HandlerMapping> initHandlerMappings(ApplicationContext context) {
72+
73+
Map<String, HandlerMapping> beans = BeanFactoryUtils.beansOfTypeIncludingAncestors(
74+
context, HandlerMapping.class, true, false);
75+
76+
if (!beans.isEmpty()) {
77+
List<HandlerMapping> mappings = new ArrayList<HandlerMapping>(beans.values());
78+
AnnotationAwareOrderComparator.sort(mappings);
79+
return mappings;
80+
}
81+
82+
return initDefaultHandlerMappings(context);
83+
}
84+
85+
private static List<HandlerMapping> initDefaultHandlerMappings(ApplicationContext context) {
86+
Properties props;
87+
String path = "DispatcherServlet.properties";
88+
try {
89+
Resource resource = new ClassPathResource(path, DispatcherServlet.class);
90+
props = PropertiesLoaderUtils.loadProperties(resource);
91+
}
92+
catch (IOException ex) {
93+
throw new IllegalStateException("Could not load '" + path + "': " + ex.getMessage());
94+
}
95+
96+
String value = props.getProperty(HandlerMapping.class.getName());
97+
String[] names = StringUtils.commaDelimitedListToStringArray(value);
98+
List<HandlerMapping> result = new ArrayList<HandlerMapping>(names.length);
99+
for (String name : names) {
100+
try {
101+
Class<?> clazz = ClassUtils.forName(name, DispatcherServlet.class.getClassLoader());
102+
Object mapping = context.getAutowireCapableBeanFactory().createBean(clazz);
103+
result.add((HandlerMapping) mapping);
104+
}
105+
catch (ClassNotFoundException ex) {
106+
throw new IllegalStateException("Could not find default HandlerMapping [" + name + "]");
107+
}
108+
}
109+
return result;
110+
}
111+
112+
113+
/**
114+
* Return the configured HandlerMapping's.
115+
*/
116+
public List<HandlerMapping> getHandlerMappings() {
117+
return this.handlerMappings;
118+
}
119+
120+
121+
/**
122+
* Find the {@link HandlerMapping} that would handle the given request and
123+
* return it as a {@link MatchableHandlerMapping} that can be used to
124+
* test request-matching criteria. If the matching HandlerMapping is not an
125+
* instance of {@link MatchableHandlerMapping}, an IllegalStateException is
126+
* raised.
127+
*
128+
* @param request the current request
129+
* @return the resolved matcher, or {@code null}
130+
* @throws Exception if any of the HandlerMapping's raise an exception
131+
*/
132+
public MatchableHandlerMapping getMatchableHandlerMapping(HttpServletRequest request) throws Exception {
133+
HttpServletRequest wrapper = new RequestAttributeChangeIgnoringWrapper(request);
134+
for (HandlerMapping handlerMapping : this.handlerMappings) {
135+
Object handler = handlerMapping.getHandler(wrapper);
136+
if (handler == null) {
137+
continue;
138+
}
139+
if (handlerMapping instanceof MatchableHandlerMapping) {
140+
return ((MatchableHandlerMapping) handlerMapping);
141+
}
142+
throw new IllegalStateException("HandlerMapping is not a MatchableHandlerMapping");
143+
}
144+
return null;
145+
}
146+
147+
@Override
148+
public CorsConfiguration getCorsConfiguration(HttpServletRequest request) {
149+
HttpServletRequest wrapper = new RequestAttributeChangeIgnoringWrapper(request);
150+
for (HandlerMapping handlerMapping : this.handlerMappings) {
151+
HandlerExecutionChain handler = null;
152+
try {
153+
handler = handlerMapping.getHandler(wrapper);
154+
}
155+
catch (Exception ex) {
156+
// Ignore
157+
}
158+
if (handler == null) {
159+
continue;
160+
}
161+
if (handler.getInterceptors() != null) {
162+
for (HandlerInterceptor interceptor : handler.getInterceptors()) {
163+
if (interceptor instanceof CorsConfigurationSource) {
164+
return ((CorsConfigurationSource) interceptor).getCorsConfiguration(wrapper);
165+
}
166+
}
167+
}
168+
if (handler.getHandler() instanceof CorsConfigurationSource) {
169+
return ((CorsConfigurationSource) handler.getHandler()).getCorsConfiguration(wrapper);
170+
}
171+
}
172+
return null;
173+
}
174+
175+
176+
/**
177+
* Request wrapper that ignores request attribute changes.
178+
*/
179+
private static class RequestAttributeChangeIgnoringWrapper extends HttpServletRequestWrapper {
180+
181+
182+
private RequestAttributeChangeIgnoringWrapper(HttpServletRequest request) {
183+
super(request);
184+
}
185+
186+
@Override
187+
public void setAttribute(String name, Object value) {
188+
// Ignore attribute change
189+
}
190+
}
191+
192+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
/*
2+
* Copyright 2002-2016 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.web.servlet.support;
17+
18+
import javax.servlet.http.HttpServletRequest;
19+
20+
import org.springframework.web.servlet.HandlerMapping;
21+
22+
/**
23+
* Additional interface that a {@link HandlerMapping} can implement to expose
24+
* a request matching API aligned with its internal request matching
25+
* configuration and implementation.
26+
*
27+
* @author Rossen Stoyanchev
28+
* @since 4.3
29+
* @see HandlerMappingIntrospector
30+
*/
31+
public interface MatchableHandlerMapping {
32+
33+
/**
34+
* Whether the given request matches the request criteria.
35+
* @param request the current request
36+
* @param pattern the pattern to match
37+
* @return the result from request matching or {@code null}
38+
*/
39+
RequestMatchResult match(HttpServletRequest request, String pattern);
40+
41+
}

0 commit comments

Comments
 (0)