Add SimpleUrlHandlerMapping

This commit replaces the temporary SimpleUrlHandlerMapping with
a full-featured one.
This commit is contained in:
Rossen Stoyanchev
2016-04-21 17:02:15 -04:00
parent 53f8b84bcc
commit a23f792052
6 changed files with 566 additions and 30 deletions

View File

@@ -39,6 +39,17 @@ public interface HandlerMapping {
*/
String BEST_MATCHING_PATTERN_ATTRIBUTE = HandlerMapping.class.getName() + ".bestMatchingPattern";
/**
* Name of the {@link ServerWebExchange} attribute that contains the path
* within the handler mapping, in case of a pattern match, or the full
* relevant URI (typically within the DispatcherServlet's mapping) else.
* <p>Note: This attribute is not required to be supported by all
* HandlerMapping implementations. URL-based HandlerMappings will
* typically support it, but handlers should not necessarily expect
* this request attribute to be present in all scenarios.
*/
String PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE = HandlerMapping.class.getName() + ".pathWithinHandlerMapping";
/**
* Name of the {@link ServerWebExchange} attribute that contains the URI
* templates map, mapping variable names to values.

View File

@@ -0,0 +1,257 @@
/*
* 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.reactive.handler;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import reactor.core.publisher.Mono;
import org.springframework.beans.BeansException;
import org.springframework.util.Assert;
import org.springframework.web.server.ServerWebExchange;
/**
* Abstract base class for URL-mapped
* {@link org.springframework.web.reactive.HandlerMapping} implementations.
*
* <p>Supports direct matches, e.g. a registered "/test" matches "/test", and
* various Ant-style pattern matches, e.g. a registered "/t*" pattern matches
* both "/test" and "/team", "/test/*" matches all paths under "/test",
* "/test/**" matches all paths below "/test". For details, see the
* {@link org.springframework.util.AntPathMatcher AntPathMatcher} javadoc.
*
* <p>Will search all path patterns to find the most exact match for the
* current request path. The most exact match is defined as the longest
* path pattern that matches the current request path.
*
* @author Rossen Stoyanchev
*/
public abstract class AbstractUrlHandlerMapping extends AbstractHandlerMapping {
private boolean useTrailingSlashMatch = false;
private boolean lazyInitHandlers = false;
private final Map<String, Object> handlerMap = new LinkedHashMap<>();
/**
* Whether to match to URLs irrespective of the presence of a trailing slash.
* If enabled a URL pattern such as "/users" also matches to "/users/".
* <p>The default value is {@code false}.
*/
public void setUseTrailingSlashMatch(boolean useTrailingSlashMatch) {
this.useTrailingSlashMatch = useTrailingSlashMatch;
}
/**
* Whether to match to URLs irrespective of the presence of a trailing slash.
*/
public boolean useTrailingSlashMatch() {
return this.useTrailingSlashMatch;
}
/**
* Set whether to lazily initialize handlers. Only applicable to
* singleton handlers, as prototypes are always lazily initialized.
* Default is "false", as eager initialization allows for more efficiency
* through referencing the controller objects directly.
* <p>If you want to allow your controllers to be lazily initialized,
* make them "lazy-init" and set this flag to true. Just making them
* "lazy-init" will not work, as they are initialized through the
* references from the handler mapping in this case.
*/
public void setLazyInitHandlers(boolean lazyInitHandlers) {
this.lazyInitHandlers = lazyInitHandlers;
}
/**
* Return the registered handlers as an unmodifiable Map, with the registered path
* as key and the handler object (or handler bean name in case of a lazy-init handler)
* as value.
*/
public final Map<String, Object> getHandlerMap() {
return Collections.unmodifiableMap(this.handlerMap);
}
@Override
public Mono<Object> getHandler(ServerWebExchange exchange) {
String lookupPath = getPathHelper().getLookupPathForRequest(exchange);
Object handler = null;
try {
handler = lookupHandler(lookupPath, exchange);
}
catch (Exception ex) {
return Mono.error(ex);
}
if (handler != null && logger.isDebugEnabled()) {
logger.debug("Mapping [" + lookupPath + "] to " + handler);
}
else if (handler == null && logger.isTraceEnabled()) {
logger.trace("No handler mapping found for [" + lookupPath + "]");
}
return Mono.justOrEmpty(handler);
}
/**
* Look up a handler instance for the given URL path.
*
* <p>Supports direct matches, e.g. a registered "/test" matches "/test",
* and various Ant-style pattern matches, e.g. a registered "/t*" matches
* both "/test" and "/team". For details, see the AntPathMatcher class.
*
* <p>Looks for the most exact pattern, where most exact is defined as
* the longest path pattern.
*
* @param urlPath URL the bean is mapped to
* @param exchange the current exchange
* @return the associated handler instance, or {@code null} if not found
* @see org.springframework.util.AntPathMatcher
*/
protected Object lookupHandler(String urlPath, ServerWebExchange exchange) throws Exception {
// Direct match?
Object handler = this.handlerMap.get(urlPath);
if (handler != null) {
return handleMatch(handler, urlPath, urlPath, exchange);
}
// Pattern match?
List<String> matches = new ArrayList<>();
for (String pattern : this.handlerMap.keySet()) {
if (getPathMatcher().match(pattern, urlPath)) {
matches.add(pattern);
}
else if (useTrailingSlashMatch()) {
if (!pattern.endsWith("/") && getPathMatcher().match(pattern + "/", urlPath)) {
matches.add(pattern +"/");
}
}
}
String bestMatch = null;
Comparator<String> comparator = getPathMatcher().getPatternComparator(urlPath);
if (!matches.isEmpty()) {
Collections.sort(matches, comparator);
if (logger.isDebugEnabled()) {
logger.debug("Matching patterns for request [" + urlPath + "] are " + matches);
}
bestMatch = matches.get(0);
}
if (bestMatch != null) {
handler = this.handlerMap.get(bestMatch);
if (handler == null) {
Assert.isTrue(bestMatch.endsWith("/"));
handler = this.handlerMap.get(bestMatch.substring(0, bestMatch.length() - 1));
}
String pathWithinMapping = getPathMatcher().extractPathWithinPattern(bestMatch, urlPath);
return handleMatch(handler, bestMatch, pathWithinMapping, exchange);
}
// No handler found...
return null;
}
private Object handleMatch(Object handler, String bestMatch, String pathWithinMapping,
ServerWebExchange exchange) throws Exception {
// Bean name or resolved handler?
if (handler instanceof String) {
String handlerName = (String) handler;
handler = getApplicationContext().getBean(handlerName);
}
validateHandler(handler, exchange);
exchange.getAttributes().put(PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE, pathWithinMapping);
exchange.getAttributes().put(BEST_MATCHING_PATTERN_ATTRIBUTE, bestMatch);
return handler;
}
/**
* Validate the given handler against the current request.
* <p>The default implementation is empty. Can be overridden in subclasses,
* for example to enforce specific preconditions expressed in URL mappings.
* @param handler the handler object to validate
* @param exchange current exchange
* @throws Exception if validation failed
*/
@SuppressWarnings("UnusedParameters")
protected void validateHandler(Object handler, ServerWebExchange exchange) throws Exception {
}
/**
* Register the specified handler for the given URL paths.
* @param urlPaths the URLs that the bean should be mapped to
* @param beanName the name of the handler bean
* @throws BeansException if the handler couldn't be registered
* @throws IllegalStateException if there is a conflicting handler registered
*/
protected void registerHandler(String[] urlPaths, String beanName) throws BeansException, IllegalStateException {
Assert.notNull(urlPaths, "URL path array must not be null");
for (String urlPath : urlPaths) {
registerHandler(urlPath, beanName);
}
}
/**
* Register the specified handler for the given URL path.
* @param urlPath the URL the bean should be mapped to
* @param handler the handler instance or handler bean name String
* (a bean name will automatically be resolved into the corresponding handler bean)
* @throws BeansException if the handler couldn't be registered
* @throws IllegalStateException if there is a conflicting handler registered
*/
protected void registerHandler(String urlPath, Object handler) throws BeansException, IllegalStateException {
Assert.notNull(urlPath, "URL path must not be null");
Assert.notNull(handler, "Handler object must not be null");
Object resolvedHandler = handler;
// Eagerly resolve handler if referencing singleton via name.
if (!this.lazyInitHandlers && handler instanceof String) {
String handlerName = (String) handler;
if (getApplicationContext().isSingleton(handlerName)) {
resolvedHandler = getApplicationContext().getBean(handlerName);
}
}
Object mappedHandler = this.handlerMap.get(urlPath);
if (mappedHandler != null) {
if (mappedHandler != resolvedHandler) {
throw new IllegalStateException(
"Cannot map " + getHandlerDescription(handler) + " to URL path [" + urlPath +
"]: There is already " + getHandlerDescription(mappedHandler) + " mapped.");
}
}
else {
this.handlerMap.put(urlPath, resolvedHandler);
if (logger.isInfoEnabled()) {
logger.info("Mapped URL path [" + urlPath + "] onto " + getHandlerDescription(handler));
}
}
}
private String getHandlerDescription(Object handler) {
return "handler " + (handler instanceof String ? "'" + handler + "'" : "of type [" + handler.getClass() + "]");
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2015 the original author or authors.
* Copyright 2002-2016 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -16,41 +16,116 @@
package org.springframework.web.reactive.handler;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Properties;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import org.springframework.web.reactive.HandlerMapping;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.beans.BeansException;
import org.springframework.util.CollectionUtils;
/**
* Implementation of the {@link org.springframework.web.reactive.HandlerMapping}
* interface to map from URLs to request handler beans. Supports both mapping
* to bean instances and mapping to bean names; the latter is required for
* non-singleton handlers.
*
* <p>The "urlMap" property is suitable for populating the handler map with
* bean instances. Mappings to bean names can be set via the "mappings"
* property, in a form accepted by the {@code java.util.Properties} class,
* like as follows:
*
* <pre>
* /welcome.html=ticketController
* /show.html=ticketController
* </pre>
*
* <p>The syntax is {@code PATH=HANDLER_BEAN_NAME}. If the path doesn't begin
* with a slash, one is prepended.
*
* <p>Supports direct matches, e.g. a registered "/test" matches "/test", and
* various Ant-style pattern matches, e.g. a registered "/t*" pattern matches
* both "/test" and "/team", "/test/*" matches all paths under "/test",
* "/test/**" matches all paths below "/test". For details, see the
* {@link org.springframework.util.AntPathMatcher AntPathMatcher} javadoc.
*
* @author Rossen Stoyanchev
*/
public class SimpleUrlHandlerMapping implements HandlerMapping {
public class SimpleUrlHandlerMapping extends AbstractUrlHandlerMapping {
private final Map<String, Object> handlerMap = new HashMap<>();
private final Map<String, Object> urlMap = new LinkedHashMap<>();
public void setHandlers(Map<String, Object> handlers) {
this.handlerMap.clear();
if (handlers != null) {
this.handlerMap.putAll(handlers);
/**
* Map URL paths to handler bean names.
* This is the typical way of configuring this HandlerMapping.
* <p>Supports direct URL matches and Ant-style pattern matches. For syntax
* details, see the {@link org.springframework.util.AntPathMatcher} javadoc.
* @param mappings properties with URLs as keys and bean names as values
* @see #setUrlMap
*/
public void setMappings(Properties mappings) {
CollectionUtils.mergePropertiesIntoMap(mappings, this.urlMap);
}
/**
* Set a Map with URL paths as keys and handler beans (or handler bean names)
* as values. Convenient for population with bean references.
* <p>Supports direct URL matches and Ant-style pattern matches. For syntax
* details, see the {@link org.springframework.util.AntPathMatcher} javadoc.
* @param urlMap map with URLs as keys and beans as values
* @see #setMappings
*/
public void setUrlMap(Map<String, ?> urlMap) {
this.urlMap.putAll(urlMap);
}
/**
* Allow Map access to the URL path mappings, with the option to add or
* override specific entries.
* <p>Useful for specifying entries directly, for example via "urlMap[myKey]".
* This is particularly useful for adding or overriding entries in child
* bean definitions.
*/
public Map<String, ?> getUrlMap() {
return this.urlMap;
}
/**
* Calls the {@link #registerHandlers} method in addition to the
* superclass's initialization.
*/
@Override
public void initApplicationContext() throws BeansException {
super.initApplicationContext();
registerHandlers(this.urlMap);
}
/**
* Register all handlers specified in the URL map for the corresponding paths.
* @param urlMap Map with URL paths as keys and handler beans or bean names as values
* @throws BeansException if a handler couldn't be registered
* @throws IllegalStateException if there is a conflicting handler registered
*/
protected void registerHandlers(Map<String, Object> urlMap) throws BeansException {
if (urlMap.isEmpty()) {
logger.warn("Neither 'urlMap' nor 'mappings' set on SimpleUrlHandlerMapping");
}
else {
for (Map.Entry<String, Object> entry : urlMap.entrySet()) {
String url = entry.getKey();
Object handler = entry.getValue();
// Prepend with slash if not already present.
if (!url.startsWith("/")) {
url = "/" + url;
}
// Remove whitespace from handler bean name.
if (handler instanceof String) {
handler = ((String) handler).trim();
}
registerHandler(url, handler);
}
}
}
@Override
public Mono<Object> getHandler(ServerWebExchange exchange) {
return Flux.create(subscriber -> {
String path = exchange.getRequest().getURI().getPath();
Object handler = this.handlerMap.get(path);
if (handler != null) {
subscriber.onNext(handler);
}
subscriber.onComplete();
}).next();
}
}

View File

@@ -0,0 +1,155 @@
/*
* 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.reactive.handler;
import java.net.URI;
import java.net.URISyntaxException;
import org.junit.Test;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.http.HttpMethod;
import org.springframework.http.server.reactive.MockServerHttpRequest;
import org.springframework.http.server.reactive.MockServerHttpResponse;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.web.reactive.HandlerMapping;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.adapter.DefaultServerWebExchange;
import org.springframework.web.server.session.WebSessionManager;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;
import static org.mockito.Mockito.mock;
import static org.springframework.web.reactive.HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE;
/**
* Unit tests for {@link SimpleUrlHandlerMapping}.
*
* @author Rossen Stoyanchev
*/
public class SimpleUrlHandlerMappingTests {
@Test
public void handlerMappingJavaConfig() throws Exception {
AnnotationConfigApplicationContext wac = new AnnotationConfigApplicationContext();
wac.register(WebConfig.class);
wac.refresh();
HandlerMapping handlerMapping = (HandlerMapping) wac.getBean("handlerMapping");
Object mainController = wac.getBean("mainController");
Object otherController = wac.getBean("otherController");
testUrl("/welcome.html", mainController, handlerMapping, "/welcome.html");
testUrl("/welcome.x", otherController, handlerMapping, "welcome.x");
testUrl("/welcome/", otherController, handlerMapping, "welcome");
testUrl("/show.html", mainController, handlerMapping, "/show.html");
testUrl("/bookseats.html", mainController, handlerMapping, "/bookseats.html");
}
@Test
public void handlerMappingXmlConfig() throws Exception {
ClassPathXmlApplicationContext wac = new ClassPathXmlApplicationContext("map.xml", getClass());
wac.refresh();
HandlerMapping handlerMapping = wac.getBean("mapping", HandlerMapping.class);
Object mainController = wac.getBean("mainController");
testUrl("/pathmatchingTest.html", mainController, handlerMapping, "pathmatchingTest.html");
testUrl("welcome.html", null, handlerMapping, null);
testUrl("/pathmatchingAA.html", mainController, handlerMapping, "pathmatchingAA.html");
testUrl("/pathmatchingA.html", null, handlerMapping, null);
testUrl("/administrator/pathmatching.html", mainController, handlerMapping, "pathmatching.html");
testUrl("/administrator/test/pathmatching.html", mainController, handlerMapping, "test/pathmatching.html");
testUrl("/administratort/pathmatching.html", null, handlerMapping, null);
testUrl("/administrator/another/bla.xml", mainController, handlerMapping, "/administrator/another/bla.xml");
testUrl("/administrator/another/bla.gif", null, handlerMapping, null);
testUrl("/administrator/test/testlastbit", mainController, handlerMapping, "test/testlastbit");
testUrl("/administrator/test/testla", null, handlerMapping, null);
testUrl("/administrator/testing/longer/bla", mainController, handlerMapping, "bla");
testUrl("/administrator/testing/longer2/notmatching/notmatching", null, handlerMapping, null);
testUrl("/shortpattern/testing/toolong", null, handlerMapping, null);
testUrl("/XXpathXXmatching.html", mainController, handlerMapping, "XXpathXXmatching.html");
testUrl("/pathXXmatching.html", mainController, handlerMapping, "pathXXmatching.html");
testUrl("/XpathXXmatching.html", null, handlerMapping, null);
testUrl("/XXpathmatching.html", null, handlerMapping, null);
testUrl("/show12.html", mainController, handlerMapping, "show12.html");
testUrl("/show123.html", mainController, handlerMapping, "/show123.html");
testUrl("/show1.html", mainController, handlerMapping, "show1.html");
testUrl("/reallyGood-test-is-this.jpeg", mainController, handlerMapping, "reallyGood-test-is-this.jpeg");
testUrl("/reallyGood-tst-is-this.jpeg", null, handlerMapping, null);
testUrl("/testing/test.jpeg", mainController, handlerMapping, "testing/test.jpeg");
testUrl("/testing/test.jpg", null, handlerMapping, null);
testUrl("/anotherTest", mainController, handlerMapping, "anotherTest");
testUrl("/stillAnotherTest", null, handlerMapping, null);
testUrl("outofpattern*ye", null, handlerMapping, null);
testUrl("/test%26t%20est/path%26m%20atching.html", null, handlerMapping, null);
}
private void testUrl(String url, Object bean, HandlerMapping handlerMapping, String pathWithinMapping)
throws URISyntaxException {
ServerWebExchange exchange = createExchange(url);
Object actual = handlerMapping.getHandler(exchange).get();
if (bean != null) {
assertNotNull(actual);
assertSame(bean, actual);
//noinspection OptionalGetWithoutIsPresent
assertEquals(pathWithinMapping, exchange.getAttribute(PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE).get());
}
else {
assertNull(actual);
}
}
private ServerWebExchange createExchange(String path) throws URISyntaxException {
ServerHttpRequest request = new MockServerHttpRequest(HttpMethod.GET, new URI(path));
WebSessionManager sessionManager = mock(WebSessionManager.class);
return new DefaultServerWebExchange(request, new MockServerHttpResponse(), sessionManager);
}
@Configuration
static class WebConfig {
@Bean @SuppressWarnings("unused")
public SimpleUrlHandlerMapping handlerMapping() {
SimpleUrlHandlerMapping hm = new SimpleUrlHandlerMapping();
hm.setUseTrailingSlashMatch(true);
hm.registerHandler("/welcome*", otherController());
hm.registerHandler("/welcome.html", mainController());
hm.registerHandler("/show.html", mainController());
hm.registerHandler("/bookseats.html", mainController());
return hm;
}
@Bean
public Object mainController() {
return new Object();
}
@Bean
public Object otherController() {
return new Object();
}
}
}

View File

@@ -39,10 +39,8 @@ import org.springframework.web.client.RestTemplate;
import org.springframework.web.reactive.DispatcherHandler;
import org.springframework.web.reactive.ResponseStatusExceptionHandler;
import org.springframework.web.reactive.handler.SimpleUrlHandlerMapping;
import org.springframework.web.reactive.result.SimpleResultHandler;
import org.springframework.web.reactive.result.WebHandlerHandlerAdapter;
import org.springframework.web.server.WebHandler;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebHandler;
import org.springframework.web.server.adapter.WebHttpHandlerBuilder;
import static org.junit.Assert.assertArrayEquals;
@@ -138,7 +136,7 @@ public class WebHandlerIntegrationTests extends AbstractHttpHandlerIntegrationTe
map.put("/foo", new FooHandler());
map.put("/bar", new BarHandler());
map.put("/header", new HeaderSettingHandler());
setHandlers(map);
setUrlMap(map);
}
}

View File

@@ -0,0 +1,40 @@
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="mapping" class="org.springframework.web.reactive.handler.SimpleUrlHandlerMapping">
<property name="urlDecode" value="true" />
<property name="mappings">
<value>
welcome.html=mainController
/**/pathmatchingTest.html=mainController
/**/pathmatching??.html=mainController
/**/path??matching.html=mainController
/**/??path??matching.html=mainController
/**/*.jsp=mainController
/administrator/**/pathmatching.html=mainController
/administrator/**/testlast*=mainController
/administrator/another/bla.xml=mainController
/administrator/testing/longer/**/**/**/**/**=mainController
/administrator/testing/longer2/**/**/bla/**=mainController
/*test*.jpeg=mainController
/*/test.jpeg=mainController
/outofpattern*yeah=mainController
/anotherTest*=mainController
/stillAnotherTestYeah=mainController
/shortpattern/testing=mainController
/show123.html=mainController
/sho*=mainController
/bookseats.html=mainController
/reservation.html=mainController
/payment.html=mainController
/confirmation.html=mainController
/test%26t%20est/path%26m%20atching.html=mainController
</value>
</property>
</bean>
<bean id="mainController" class="java.lang.Object"/>
</beans>