7 changed files with 554 additions and 11 deletions
@ -0,0 +1,192 @@
@@ -0,0 +1,192 @@
|
||||
/* |
||||
* 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.support; |
||||
|
||||
import java.io.IOException; |
||||
import java.util.ArrayList; |
||||
import java.util.List; |
||||
import java.util.Map; |
||||
import java.util.Properties; |
||||
import javax.servlet.http.HttpServletRequest; |
||||
import javax.servlet.http.HttpServletRequestWrapper; |
||||
|
||||
import org.springframework.beans.factory.BeanFactoryUtils; |
||||
import org.springframework.context.ApplicationContext; |
||||
import org.springframework.core.annotation.AnnotationAwareOrderComparator; |
||||
import org.springframework.core.io.ClassPathResource; |
||||
import org.springframework.core.io.Resource; |
||||
import org.springframework.core.io.support.PropertiesLoaderUtils; |
||||
import org.springframework.util.ClassUtils; |
||||
import org.springframework.util.StringUtils; |
||||
import org.springframework.web.cors.CorsConfiguration; |
||||
import org.springframework.web.cors.CorsConfigurationSource; |
||||
import org.springframework.web.servlet.DispatcherServlet; |
||||
import org.springframework.web.servlet.HandlerExecutionChain; |
||||
import org.springframework.web.servlet.HandlerInterceptor; |
||||
import org.springframework.web.servlet.HandlerMapping; |
||||
|
||||
/** |
||||
* Helper class to get information from the {@code HandlerMapping} that would |
||||
* serve a specific request. |
||||
* |
||||
* <p>Provides the following methods: |
||||
* <ul> |
||||
* <li>{@link #getMatchableHandlerMapping} -- obtain a {@code HandlerMapping} |
||||
* to check request-matching criteria against. |
||||
* <li>{@link #getCorsConfiguration} -- obtain the CORS configuration for the |
||||
* request. |
||||
* </ul> |
||||
* |
||||
* @author Rossen Stoyanchev |
||||
* @since 4.3 |
||||
*/ |
||||
public class HandlerMappingIntrospector implements CorsConfigurationSource { |
||||
|
||||
private final List<HandlerMapping> handlerMappings; |
||||
|
||||
|
||||
/** |
||||
* Constructor that detects the configured {@code HandlerMapping}s in the |
||||
* given {@code ApplicationContext} or falling back on |
||||
* "DispatcherServlet.properties" like the {@code DispatcherServlet}. |
||||
*/ |
||||
public HandlerMappingIntrospector(ApplicationContext context) { |
||||
this.handlerMappings = initHandlerMappings(context); |
||||
} |
||||
|
||||
|
||||
private static List<HandlerMapping> initHandlerMappings(ApplicationContext context) { |
||||
|
||||
Map<String, HandlerMapping> beans = BeanFactoryUtils.beansOfTypeIncludingAncestors( |
||||
context, HandlerMapping.class, true, false); |
||||
|
||||
if (!beans.isEmpty()) { |
||||
List<HandlerMapping> mappings = new ArrayList<HandlerMapping>(beans.values()); |
||||
AnnotationAwareOrderComparator.sort(mappings); |
||||
return mappings; |
||||
} |
||||
|
||||
return initDefaultHandlerMappings(context); |
||||
} |
||||
|
||||
private static List<HandlerMapping> initDefaultHandlerMappings(ApplicationContext context) { |
||||
Properties props; |
||||
String path = "DispatcherServlet.properties"; |
||||
try { |
||||
Resource resource = new ClassPathResource(path, DispatcherServlet.class); |
||||
props = PropertiesLoaderUtils.loadProperties(resource); |
||||
} |
||||
catch (IOException ex) { |
||||
throw new IllegalStateException("Could not load '" + path + "': " + ex.getMessage()); |
||||
} |
||||
|
||||
String value = props.getProperty(HandlerMapping.class.getName()); |
||||
String[] names = StringUtils.commaDelimitedListToStringArray(value); |
||||
List<HandlerMapping> result = new ArrayList<HandlerMapping>(names.length); |
||||
for (String name : names) { |
||||
try { |
||||
Class<?> clazz = ClassUtils.forName(name, DispatcherServlet.class.getClassLoader()); |
||||
Object mapping = context.getAutowireCapableBeanFactory().createBean(clazz); |
||||
result.add((HandlerMapping) mapping); |
||||
} |
||||
catch (ClassNotFoundException ex) { |
||||
throw new IllegalStateException("Could not find default HandlerMapping [" + name + "]"); |
||||
} |
||||
} |
||||
return result; |
||||
} |
||||
|
||||
|
||||
/** |
||||
* Return the configured HandlerMapping's. |
||||
*/ |
||||
public List<HandlerMapping> getHandlerMappings() { |
||||
return this.handlerMappings; |
||||
} |
||||
|
||||
|
||||
/** |
||||
* Find the {@link HandlerMapping} that would handle the given request and |
||||
* return it as a {@link MatchableHandlerMapping} that can be used to |
||||
* test request-matching criteria. If the matching HandlerMapping is not an |
||||
* instance of {@link MatchableHandlerMapping}, an IllegalStateException is |
||||
* raised. |
||||
* |
||||
* @param request the current request |
||||
* @return the resolved matcher, or {@code null} |
||||
* @throws Exception if any of the HandlerMapping's raise an exception |
||||
*/ |
||||
public MatchableHandlerMapping getMatchableHandlerMapping(HttpServletRequest request) throws Exception { |
||||
HttpServletRequest wrapper = new RequestAttributeChangeIgnoringWrapper(request); |
||||
for (HandlerMapping handlerMapping : this.handlerMappings) { |
||||
Object handler = handlerMapping.getHandler(wrapper); |
||||
if (handler == null) { |
||||
continue; |
||||
} |
||||
if (handlerMapping instanceof MatchableHandlerMapping) { |
||||
return ((MatchableHandlerMapping) handlerMapping); |
||||
} |
||||
throw new IllegalStateException("HandlerMapping is not a MatchableHandlerMapping"); |
||||
} |
||||
return null; |
||||
} |
||||
|
||||
@Override |
||||
public CorsConfiguration getCorsConfiguration(HttpServletRequest request) { |
||||
HttpServletRequest wrapper = new RequestAttributeChangeIgnoringWrapper(request); |
||||
for (HandlerMapping handlerMapping : this.handlerMappings) { |
||||
HandlerExecutionChain handler = null; |
||||
try { |
||||
handler = handlerMapping.getHandler(wrapper); |
||||
} |
||||
catch (Exception ex) { |
||||
// Ignore
|
||||
} |
||||
if (handler == null) { |
||||
continue; |
||||
} |
||||
if (handler.getInterceptors() != null) { |
||||
for (HandlerInterceptor interceptor : handler.getInterceptors()) { |
||||
if (interceptor instanceof CorsConfigurationSource) { |
||||
return ((CorsConfigurationSource) interceptor).getCorsConfiguration(wrapper); |
||||
} |
||||
} |
||||
} |
||||
if (handler.getHandler() instanceof CorsConfigurationSource) { |
||||
return ((CorsConfigurationSource) handler.getHandler()).getCorsConfiguration(wrapper); |
||||
} |
||||
} |
||||
return null; |
||||
} |
||||
|
||||
|
||||
/** |
||||
* Request wrapper that ignores request attribute changes. |
||||
*/ |
||||
private static class RequestAttributeChangeIgnoringWrapper extends HttpServletRequestWrapper { |
||||
|
||||
|
||||
private RequestAttributeChangeIgnoringWrapper(HttpServletRequest request) { |
||||
super(request); |
||||
} |
||||
|
||||
@Override |
||||
public void setAttribute(String name, Object value) { |
||||
// Ignore attribute change
|
||||
} |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,41 @@
@@ -0,0 +1,41 @@
|
||||
/* |
||||
* 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.support; |
||||
|
||||
import javax.servlet.http.HttpServletRequest; |
||||
|
||||
import org.springframework.web.servlet.HandlerMapping; |
||||
|
||||
/** |
||||
* Additional interface that a {@link HandlerMapping} can implement to expose |
||||
* a request matching API aligned with its internal request matching |
||||
* configuration and implementation. |
||||
* |
||||
* @author Rossen Stoyanchev |
||||
* @since 4.3 |
||||
* @see HandlerMappingIntrospector |
||||
*/ |
||||
public interface MatchableHandlerMapping { |
||||
|
||||
/** |
||||
* Whether the given request matches the request criteria. |
||||
* @param request the current request |
||||
* @param pattern the pattern to match |
||||
* @return the result from request matching or {@code null} |
||||
*/ |
||||
RequestMatchResult match(HttpServletRequest request, String pattern); |
||||
|
||||
} |
||||
@ -0,0 +1,77 @@
@@ -0,0 +1,77 @@
|
||||
/* |
||||
* 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.support; |
||||
|
||||
import java.util.Collections; |
||||
import java.util.Map; |
||||
|
||||
import org.springframework.util.Assert; |
||||
import org.springframework.util.PathMatcher; |
||||
|
||||
/** |
||||
* Container for the result from request pattern matching via |
||||
* {@link MatchableHandlerMapping} with a method to further extract URI template |
||||
* variables from the pattern. |
||||
* |
||||
* @author Rossen Stoyanchev |
||||
* @since 4.3 |
||||
*/ |
||||
public class RequestMatchResult { |
||||
|
||||
private final String matchingPattern; |
||||
|
||||
private final String lookupPath; |
||||
|
||||
private final PathMatcher pathMatcher; |
||||
|
||||
|
||||
/** |
||||
* Create an instance with a matching pattern. |
||||
* @param matchingPattern the matching pattern, possibly not the same as the |
||||
* input pattern, e.g. inputPattern="/foo" and matchingPattern="/foo/". |
||||
* @param lookupPath the lookup path extracted from the request |
||||
* @param pathMatcher the PathMatcher used |
||||
*/ |
||||
public RequestMatchResult(String matchingPattern, String lookupPath, PathMatcher pathMatcher) { |
||||
Assert.hasText(matchingPattern, "'matchingPattern' is required"); |
||||
Assert.hasText(lookupPath, "'lookupPath' is required"); |
||||
Assert.notNull(pathMatcher, "'pathMatcher' is required"); |
||||
this.matchingPattern = matchingPattern; |
||||
this.lookupPath = lookupPath; |
||||
this.pathMatcher = pathMatcher; |
||||
} |
||||
|
||||
|
||||
/** |
||||
* Whether the pattern was matched to the request. |
||||
*/ |
||||
public boolean isMatch() { |
||||
return (this.matchingPattern != null); |
||||
} |
||||
|
||||
/** |
||||
* Extract URI template variables from the matching pattern as defined in |
||||
* {@link PathMatcher#extractUriTemplateVariables}. |
||||
* @return a map with URI template variables |
||||
*/ |
||||
public Map<String, String> extractUriTemplateVariables() { |
||||
if (!isMatch()) { |
||||
return Collections.<String, String>emptyMap(); |
||||
} |
||||
return this.pathMatcher.extractUriTemplateVariables(this.matchingPattern, this.lookupPath); |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,190 @@
@@ -0,0 +1,190 @@
|
||||
/* |
||||
* 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.support; |
||||
|
||||
import java.util.Arrays; |
||||
import java.util.Collections; |
||||
import java.util.List; |
||||
import javax.servlet.http.HttpServletRequest; |
||||
|
||||
import org.junit.Test; |
||||
|
||||
import org.springframework.beans.MutablePropertyValues; |
||||
import org.springframework.context.annotation.Bean; |
||||
import org.springframework.context.annotation.Configuration; |
||||
import org.springframework.http.HttpHeaders; |
||||
import org.springframework.mock.web.test.MockHttpServletRequest; |
||||
import org.springframework.stereotype.Controller; |
||||
import org.springframework.web.bind.annotation.CrossOrigin; |
||||
import org.springframework.web.bind.annotation.PostMapping; |
||||
import org.springframework.web.context.support.AnnotationConfigWebApplicationContext; |
||||
import org.springframework.web.context.support.StaticWebApplicationContext; |
||||
import org.springframework.web.cors.CorsConfiguration; |
||||
import org.springframework.web.servlet.HandlerExecutionChain; |
||||
import org.springframework.web.servlet.HandlerMapping; |
||||
import org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping; |
||||
import org.springframework.web.servlet.handler.SimpleUrlHandlerMapping; |
||||
import org.springframework.web.servlet.mvc.annotation.DefaultAnnotationHandlerMapping; |
||||
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping; |
||||
|
||||
import static org.junit.Assert.assertEquals; |
||||
import static org.junit.Assert.assertNotNull; |
||||
import static org.junit.Assert.assertNull; |
||||
import static org.springframework.web.servlet.HandlerMapping.BEST_MATCHING_PATTERN_ATTRIBUTE; |
||||
|
||||
/** |
||||
* Unit tests for {@link HandlerMappingIntrospector}. |
||||
* @author Rossen Stoyanchev |
||||
*/ |
||||
public class HandlerMappingIntrospectorTests { |
||||
|
||||
@Test |
||||
public void detectHandlerMappings() throws Exception { |
||||
StaticWebApplicationContext cxt = new StaticWebApplicationContext(); |
||||
cxt.registerSingleton("hmA", SimpleUrlHandlerMapping.class); |
||||
cxt.registerSingleton("hmB", SimpleUrlHandlerMapping.class); |
||||
cxt.registerSingleton("hmC", SimpleUrlHandlerMapping.class); |
||||
cxt.refresh(); |
||||
|
||||
List<?> expected = Arrays.asList(cxt.getBean("hmA"), cxt.getBean("hmB"), cxt.getBean("hmC")); |
||||
List<HandlerMapping> actual = new HandlerMappingIntrospector(cxt).getHandlerMappings(); |
||||
|
||||
assertEquals(expected, actual); |
||||
} |
||||
|
||||
@Test |
||||
public void detectHandlerMappingsOrdered() throws Exception { |
||||
StaticWebApplicationContext cxt = new StaticWebApplicationContext(); |
||||
MutablePropertyValues pvs = new MutablePropertyValues(Collections.singletonMap("order", "3")); |
||||
cxt.registerSingleton("hmA", SimpleUrlHandlerMapping.class, pvs); |
||||
pvs = new MutablePropertyValues(Collections.singletonMap("order", "2")); |
||||
cxt.registerSingleton("hmB", SimpleUrlHandlerMapping.class, pvs); |
||||
pvs = new MutablePropertyValues(Collections.singletonMap("order", "1")); |
||||
cxt.registerSingleton("hmC", SimpleUrlHandlerMapping.class, pvs); |
||||
cxt.refresh(); |
||||
|
||||
List<?> expected = Arrays.asList(cxt.getBean("hmC"), cxt.getBean("hmB"), cxt.getBean("hmA")); |
||||
List<HandlerMapping> actual = new HandlerMappingIntrospector(cxt).getHandlerMappings(); |
||||
|
||||
assertEquals(expected, actual); |
||||
} |
||||
|
||||
@Test @SuppressWarnings("deprecation") |
||||
public void defaultHandlerMappings() throws Exception { |
||||
StaticWebApplicationContext cxt = new StaticWebApplicationContext(); |
||||
cxt.refresh(); |
||||
|
||||
List<HandlerMapping> actual = new HandlerMappingIntrospector(cxt).getHandlerMappings(); |
||||
assertEquals(2, actual.size()); |
||||
assertEquals(BeanNameUrlHandlerMapping.class, actual.get(0).getClass()); |
||||
assertEquals(DefaultAnnotationHandlerMapping.class, actual.get(1).getClass()); |
||||
} |
||||
|
||||
@Test |
||||
public void getMatchable() throws Exception { |
||||
|
||||
MutablePropertyValues pvs = new MutablePropertyValues( |
||||
Collections.singletonMap("urlMap", |
||||
Collections.singletonMap("/path", new Object()))); |
||||
|
||||
StaticWebApplicationContext cxt = new StaticWebApplicationContext(); |
||||
cxt.registerSingleton("hm", SimpleUrlHandlerMapping.class, pvs); |
||||
cxt.refresh(); |
||||
|
||||
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/path"); |
||||
MatchableHandlerMapping hm = new HandlerMappingIntrospector(cxt).getMatchableHandlerMapping(request); |
||||
|
||||
assertEquals(cxt.getBean("hm"), hm); |
||||
assertNull("Attributes changes not ignored", request.getAttribute(BEST_MATCHING_PATTERN_ATTRIBUTE)); |
||||
} |
||||
|
||||
@Test(expected = IllegalStateException.class) |
||||
public void getMatchableWhereHandlerMappingDoesNotImplementMatchableInterface() throws Exception { |
||||
StaticWebApplicationContext cxt = new StaticWebApplicationContext(); |
||||
cxt.registerSingleton("hm1", TestHandlerMapping.class); |
||||
cxt.refresh(); |
||||
|
||||
MockHttpServletRequest request = new MockHttpServletRequest(); |
||||
new HandlerMappingIntrospector(cxt).getMatchableHandlerMapping(request); |
||||
} |
||||
|
||||
@Test |
||||
public void getCorsConfigurationPreFlight() throws Exception { |
||||
AnnotationConfigWebApplicationContext cxt = new AnnotationConfigWebApplicationContext(); |
||||
cxt.register(TestConfig.class); |
||||
cxt.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 = new HandlerMappingIntrospector(cxt).getCorsConfiguration(request); |
||||
|
||||
assertNotNull(corsConfig); |
||||
assertEquals(Collections.singletonList("http://localhost:9000"), corsConfig.getAllowedOrigins()); |
||||
assertEquals(Collections.singletonList("POST"), corsConfig.getAllowedMethods()); |
||||
} |
||||
|
||||
@Test |
||||
public void getCorsConfigurationActual() throws Exception { |
||||
AnnotationConfigWebApplicationContext cxt = new AnnotationConfigWebApplicationContext(); |
||||
cxt.register(TestConfig.class); |
||||
cxt.refresh(); |
||||
|
||||
MockHttpServletRequest request = new MockHttpServletRequest("POST", "/path"); |
||||
request.addHeader("Origin", "http://localhost:9000"); |
||||
CorsConfiguration corsConfig = new HandlerMappingIntrospector(cxt).getCorsConfiguration(request); |
||||
|
||||
assertNotNull(corsConfig); |
||||
assertEquals(Collections.singletonList("http://localhost:9000"), corsConfig.getAllowedOrigins()); |
||||
assertEquals(Collections.singletonList("POST"), corsConfig.getAllowedMethods()); |
||||
} |
||||
|
||||
|
||||
private static class TestHandlerMapping implements HandlerMapping { |
||||
|
||||
@Override |
||||
public HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception { |
||||
return new HandlerExecutionChain(new Object()); |
||||
} |
||||
} |
||||
|
||||
@Configuration @SuppressWarnings({"WeakerAccess", "unused"}) |
||||
static class TestConfig { |
||||
|
||||
@Bean |
||||
public RequestMappingHandlerMapping handlerMapping() { |
||||
return new RequestMappingHandlerMapping(); |
||||
} |
||||
|
||||
@Bean |
||||
public TestController testController() { |
||||
return new TestController(); |
||||
} |
||||
|
||||
} |
||||
|
||||
@CrossOrigin("http://localhost:9000") |
||||
@Controller |
||||
private static class TestController { |
||||
|
||||
@PostMapping("/path") |
||||
public void handle() { |
||||
} |
||||
} |
||||
|
||||
} |
||||
Loading…
Reference in new issue