From 33a7b91e57b98bbb9a8afc669d07213d8e7fd418 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Thu, 14 Apr 2016 12:56:57 -0400 Subject: [PATCH] Add abstract case class HandlerMappings This commit adds AbstractHandlerMethodMapping, a starting point for AbstractHandlerMapping, and HttpRequestPathHelper with a similar purpose to UrlPathHelper but based with ServerWebExchange as input. --- .../handler/AbstractHandlerMapping.java | 104 ++++ .../method/AbstractHandlerMethodMapping.java | 562 ++++++++++++++++++ .../web/util/HttpRequestPathHelper.java | 66 ++ .../method/HandlerMethodMappingTests.java | 204 +++++++ 4 files changed, 936 insertions(+) create mode 100644 spring-web-reactive/src/main/java/org/springframework/web/reactive/handler/AbstractHandlerMapping.java create mode 100644 spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/AbstractHandlerMethodMapping.java create mode 100644 spring-web-reactive/src/main/java/org/springframework/web/util/HttpRequestPathHelper.java create mode 100644 spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/HandlerMethodMappingTests.java diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/handler/AbstractHandlerMapping.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/handler/AbstractHandlerMapping.java new file mode 100644 index 00000000000..8cb52673373 --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/handler/AbstractHandlerMapping.java @@ -0,0 +1,104 @@ +/* + * 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 org.springframework.context.support.ApplicationObjectSupport; +import org.springframework.core.Ordered; +import org.springframework.util.AntPathMatcher; +import org.springframework.util.Assert; +import org.springframework.util.PathMatcher; +import org.springframework.web.reactive.HandlerMapping; +import org.springframework.web.util.HttpRequestPathHelper; + +/** + * Abstract base class for {@link org.springframework.web.reactive.HandlerMapping} + * implementations. + * + * @author Rossen Stoyanchev + */ +public abstract class AbstractHandlerMapping extends ApplicationObjectSupport + implements HandlerMapping, Ordered { + + private int order = Integer.MAX_VALUE; // default: same as non-Ordered + + private HttpRequestPathHelper pathHelper = new HttpRequestPathHelper(); + + private PathMatcher pathMatcher = new AntPathMatcher(); + + + // TODO: CORS + + /** + * Specify the order value for this HandlerMapping bean. + *

Default value is {@code Integer.MAX_VALUE}, meaning that it's non-ordered. + * @see org.springframework.core.Ordered#getOrder() + */ + public final void setOrder(int order) { + this.order = order; + } + + @Override + public final int getOrder() { + return this.order; + } + + /** + * Set if the path should be URL-decoded. This sets the same property on the + * underlying path helper. + * @see HttpRequestPathHelper#setUrlDecode(boolean) + */ + public void setUrlDecode(boolean urlDecode) { + this.pathHelper.setUrlDecode(urlDecode); + } + + /** + * Set the {@link HttpRequestPathHelper} to use for resolution of lookup + * paths. Use this to override the default implementation with a custom + * subclass or to share common path helper settings across multiple + * HandlerMappings. + */ + public void setPathHelper(HttpRequestPathHelper pathHelper) { + this.pathHelper = pathHelper; + } + + /** + * Return the {@link HttpRequestPathHelper} implementation to use for + * resolution of lookup paths. + */ + public HttpRequestPathHelper getPathHelper() { + return this.pathHelper; + } + + /** + * Set the PathMatcher implementation to use for matching URL paths + * against registered URL patterns. Default is AntPathMatcher. + * @see org.springframework.util.AntPathMatcher + */ + public void setPathMatcher(PathMatcher pathMatcher) { + Assert.notNull(pathMatcher, "PathMatcher must not be null"); + this.pathMatcher = pathMatcher; + // this.corsConfigSource.setPathMatcher(pathMatcher); + } + + /** + * Return the PathMatcher implementation to use for matching URL paths + * against registered URL patterns. + */ + public PathMatcher getPathMatcher() { + return this.pathMatcher; + } + +} diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/AbstractHandlerMethodMapping.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/AbstractHandlerMethodMapping.java new file mode 100644 index 00000000000..6b23f30ee93 --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/AbstractHandlerMethodMapping.java @@ -0,0 +1,562 @@ +/* + * 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.result.method; + +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.locks.ReentrantReadWriteLock; + +import reactor.core.publisher.Mono; + +import org.springframework.aop.support.AopUtils; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.core.MethodIntrospector; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.web.method.HandlerMethod; +import org.springframework.web.reactive.HandlerMapping; +import org.springframework.web.reactive.handler.AbstractHandlerMapping; +import org.springframework.web.server.ServerWebExchange; + +/** + * Abstract base class for {@link HandlerMapping} implementations that define + * a mapping between a request and a {@link HandlerMethod}. + * + *

For each registered handler method, a unique mapping is maintained with + * subclasses defining the details of the mapping type {@code }. + * + * @author Rossen Stoyanchev + * @param The mapping for a {@link HandlerMethod} containing the conditions + * needed to match the handler method to incoming request. + */ +public abstract class AbstractHandlerMethodMapping extends AbstractHandlerMapping implements InitializingBean { + + /** + * Bean name prefix for target beans behind scoped proxies. Used to exclude those + * targets from handler method detection, in favor of the corresponding proxies. + *

We're not checking the autowire-candidate status here, which is how the + * proxy target filtering problem is being handled at the autowiring level, + * since autowire-candidate may have been turned to {@code false} for other + * reasons, while still expecting the bean to be eligible for handler methods. + *

Originally defined in {@link org.springframework.aop.scope.ScopedProxyUtils} + * but duplicated here to avoid a hard dependency on the spring-aop module. + */ + private static final String SCOPED_TARGET_NAME_PREFIX = "scopedTarget."; + + + private final MappingRegistry mappingRegistry = new MappingRegistry(); + + + // TODO: handlerMethodMappingNamingStrategy + + /** + * Return a (read-only) map with all mappings and HandlerMethod's. + */ + public Map getHandlerMethods() { + this.mappingRegistry.acquireReadLock(); + try { + return Collections.unmodifiableMap(this.mappingRegistry.getMappings()); + } + finally { + this.mappingRegistry.releaseReadLock(); + } + } + + /** + * Return the internal mapping registry. Provided for testing purposes. + */ + MappingRegistry getMappingRegistry() { + return this.mappingRegistry; + } + + /** + * Register the given mapping. + *

This method may be invoked at runtime after initialization has completed. + * @param mapping the mapping for the handler method + * @param handler the handler + * @param method the method + */ + public void registerMapping(T mapping, Object handler, Method method) { + this.mappingRegistry.register(mapping, handler, method); + } + + /** + * Un-register the given mapping. + *

This method may be invoked at runtime after initialization has completed. + * @param mapping the mapping to unregister + */ + public void unregisterMapping(T mapping) { + this.mappingRegistry.unregister(mapping); + } + + + // Handler method detection + + /** + * Detects handler methods at initialization. + */ + @Override + public void afterPropertiesSet() { + initHandlerMethods(); + } + + /** + * Scan beans in the ApplicationContext, detect and register handler methods. + * @see #isHandler(Class) + * @see #getMappingForMethod(Method, Class) + * @see #handlerMethodsInitialized(Map) + */ + protected void initHandlerMethods() { + if (logger.isDebugEnabled()) { + logger.debug("Looking for request mappings in application context: " + getApplicationContext()); + } + String[] beanNames = getApplicationContext().getBeanNamesForType(Object.class); + + for (String beanName : beanNames) { + if (!beanName.startsWith(SCOPED_TARGET_NAME_PREFIX)) { + Class beanType = null; + try { + beanType = getApplicationContext().getType(beanName); + } + catch (Throwable ex) { + // An unresolvable bean type, probably from a lazy bean - let's ignore it. + if (logger.isDebugEnabled()) { + logger.debug("Could not resolve target class for bean with name '" + beanName + "'", ex); + } + } + if (beanType != null && isHandler(beanType)) { + detectHandlerMethods(beanName); + } + } + } + handlerMethodsInitialized(getHandlerMethods()); + } + + /** + * Look for handler methods in a handler. + * @param handler the bean name of a handler or a handler instance + */ + protected void detectHandlerMethods(final Object handler) { + Class handlerType = (handler instanceof String ? + getApplicationContext().getType((String) handler) : handler.getClass()); + final Class userType = ClassUtils.getUserClass(handlerType); + + Map methods = MethodIntrospector.selectMethods(userType, + (MethodIntrospector.MetadataLookup) method -> getMappingForMethod(method, userType)); + + if (logger.isDebugEnabled()) { + logger.debug(methods.size() + " request handler methods found on " + userType + ": " + methods); + } + for (Map.Entry entry : methods.entrySet()) { + Method invocableMethod = AopUtils.selectInvocableMethod(entry.getKey(), userType); + T mapping = entry.getValue(); + registerHandlerMethod(handler, invocableMethod, mapping); + } + } + + /** + * Register a handler method and its unique mapping. Invoked at startup for + * each detected handler method. + * @param handler the bean name of the handler or the handler instance + * @param method the method to register + * @param mapping the mapping conditions associated with the handler method + * @throws IllegalStateException if another method was already registered + * under the same mapping + */ + protected void registerHandlerMethod(Object handler, Method method, T mapping) { + this.mappingRegistry.register(mapping, handler, method); + } + + /** + * Create the HandlerMethod instance. + * @param handler either a bean name or an actual handler instance + * @param method the target method + * @return the created HandlerMethod + */ + protected HandlerMethod createHandlerMethod(Object handler, Method method) { + HandlerMethod handlerMethod; + if (handler instanceof String) { + String beanName = (String) handler; + handlerMethod = new HandlerMethod(beanName, + getApplicationContext().getAutowireCapableBeanFactory(), method); + } + else { + handlerMethod = new HandlerMethod(handler, method); + } + return handlerMethod; + } + + /** + * Invoked after all handler methods have been detected. + * @param handlerMethods a read-only map with handler methods and mappings. + */ + protected void handlerMethodsInitialized(Map handlerMethods) { + } + + + // Handler method lookup + + /** + * Look up a handler method for the given request. + * @param exchange the current exchange + */ + @Override + public Mono getHandler(ServerWebExchange exchange) { + String lookupPath = getPathHelper().getLookupPathForRequest(exchange); + if (logger.isDebugEnabled()) { + logger.debug("Looking up handler method for path " + lookupPath); + } + this.mappingRegistry.acquireReadLock(); + try { + HandlerMethod handlerMethod = null; + try { + handlerMethod = lookupHandlerMethod(lookupPath, exchange); + } + catch (Exception ex) { + return Mono.error(ex); + } + if (logger.isDebugEnabled()) { + if (handlerMethod != null) { + logger.debug("Returning handler method [" + handlerMethod + "]"); + } + else { + logger.debug("Did not find handler method for [" + lookupPath + "]"); + } + } + return (handlerMethod != null ? Mono.just(handlerMethod.createWithResolvedBean()) : Mono.empty()); + } + finally { + this.mappingRegistry.releaseReadLock(); + } + } + + /** + * Look up the best-matching handler method for the current request. + * If multiple matches are found, the best match is selected. + * @param lookupPath mapping lookup path within the current servlet mapping + * @param exchange the current exchange + * @return the best-matching handler method, or {@code null} if no match + * @see #handleMatch(Object, String, ServerWebExchange) + * @see #handleNoMatch(Set, String, ServerWebExchange) + */ + protected HandlerMethod lookupHandlerMethod(String lookupPath, ServerWebExchange exchange) + throws Exception { + + List matches = new ArrayList(); + List directPathMatches = this.mappingRegistry.getMappingsByUrl(lookupPath); + if (directPathMatches != null) { + addMatchingMappings(directPathMatches, matches, exchange); + } + if (matches.isEmpty()) { + // No choice but to go through all mappings... + addMatchingMappings(this.mappingRegistry.getMappings().keySet(), matches, exchange); + } + + if (!matches.isEmpty()) { + Comparator comparator = new MatchComparator(getMappingComparator(exchange)); + Collections.sort(matches, comparator); + if (logger.isTraceEnabled()) { + logger.trace("Found " + matches.size() + " matching mapping(s) for [" + + lookupPath + "] : " + matches); + } + Match bestMatch = matches.get(0); + if (matches.size() > 1) { + Match secondBestMatch = matches.get(1); + if (comparator.compare(bestMatch, secondBestMatch) == 0) { + Method m1 = bestMatch.handlerMethod.getMethod(); + Method m2 = secondBestMatch.handlerMethod.getMethod(); + throw new IllegalStateException("Ambiguous handler methods mapped for HTTP path '" + + lookupPath + "': {" + m1 + ", " + m2 + "}"); + } + } + handleMatch(bestMatch.mapping, lookupPath, exchange); + return bestMatch.handlerMethod; + } + else { + return handleNoMatch(this.mappingRegistry.getMappings().keySet(), lookupPath, exchange); + } + } + + private void addMatchingMappings(Collection mappings, List matches, ServerWebExchange exchange) { + for (T mapping : mappings) { + T match = getMatchingMapping(mapping, exchange); + if (match != null) { + matches.add(new Match(match, this.mappingRegistry.getMappings().get(mapping))); + } + } + } + + /** + * Invoked when a matching mapping is found. + * @param mapping the matching mapping + * @param lookupPath mapping lookup path within the current servlet mapping + * @param exchange the current exchange + */ + protected void handleMatch(T mapping, String lookupPath, ServerWebExchange exchange) { + } + + /** + * Invoked when no matching mapping is not found. + * @param mappings all registered mappings + * @param lookupPath mapping lookup path within the current servlet mapping + * @param exchange the current exchange + * @return an alternative HandlerMethod or {@code null} + * @throws Exception provides details that can be translated into an error status code + */ + protected HandlerMethod handleNoMatch(Set mappings, String lookupPath, ServerWebExchange exchange) + throws Exception { + + return null; + } + + + // Abstract template methods + + /** + * Whether the given type is a handler with handler methods. + * @param beanType the type of the bean being checked + * @return "true" if this a handler type, "false" otherwise. + */ + protected abstract boolean isHandler(Class beanType); + + /** + * Provide the mapping for a handler method. A method for which no + * mapping can be provided is not a handler method. + * @param method the method to provide a mapping for + * @param handlerType the handler type, possibly a sub-type of the method's + * declaring class + * @return the mapping, or {@code null} if the method is not mapped + */ + protected abstract T getMappingForMethod(Method method, Class handlerType); + + /** + * Extract and return the URL paths contained in a mapping. + */ + protected abstract Set getMappingPathPatterns(T mapping); + + /** + * Check if a mapping matches the current request and return a (potentially + * new) mapping with conditions relevant to the current request. + * @param mapping the mapping to get a match for + * @param exchange the current exchange + * @return the match, or {@code null} if the mapping doesn't match + */ + protected abstract T getMatchingMapping(T mapping, ServerWebExchange exchange); + + /** + * Return a comparator for sorting matching mappings. + * The returned comparator should sort 'better' matches higher. + * @param exchange the current exchange + * @return the comparator (never {@code null}) + */ + protected abstract Comparator getMappingComparator(ServerWebExchange exchange); + + + /** + * A registry that maintains all mappings to handler methods, exposing methods + * to perform lookups and providing concurrent access. + * + *

Package-private for testing purposes. + */ + class MappingRegistry { + + private final Map> registry = new HashMap<>(); + + private final Map mappingLookup = new LinkedHashMap<>(); + + private final MultiValueMap urlLookup = new LinkedMultiValueMap<>(); + + private final ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock(); + + /** + * Return all mappings and handler methods. Not thread-safe. + * @see #acquireReadLock() + */ + public Map getMappings() { + return this.mappingLookup; + } + + /** + * Return matches for the given URL path. Not thread-safe. + * @see #acquireReadLock() + */ + public List getMappingsByUrl(String urlPath) { + return this.urlLookup.get(urlPath); + } + + /** + * Acquire the read lock when using getMappings and getMappingsByUrl. + */ + public void acquireReadLock() { + this.readWriteLock.readLock().lock(); + } + + /** + * Release the read lock after using getMappings and getMappingsByUrl. + */ + public void releaseReadLock() { + this.readWriteLock.readLock().unlock(); + } + + public void register(T mapping, Object handler, Method method) { + this.readWriteLock.writeLock().lock(); + try { + HandlerMethod handlerMethod = createHandlerMethod(handler, method); + assertUniqueMethodMapping(handlerMethod, mapping); + + if (logger.isInfoEnabled()) { + logger.info("Mapped \"" + mapping + "\" onto " + handlerMethod); + } + this.mappingLookup.put(mapping, handlerMethod); + + List directUrls = getDirectUrls(mapping); + for (String url : directUrls) { + this.urlLookup.add(url, mapping); + } + + this.registry.put(mapping, new MappingRegistration<>(mapping, handlerMethod, directUrls)); + } + finally { + this.readWriteLock.writeLock().unlock(); + } + } + + private void assertUniqueMethodMapping(HandlerMethod newHandlerMethod, T mapping) { + HandlerMethod handlerMethod = this.mappingLookup.get(mapping); + if (handlerMethod != null && !handlerMethod.equals(newHandlerMethod)) { + throw new IllegalStateException( + "Ambiguous mapping. Cannot map '" + newHandlerMethod.getBean() + "' method \n" + + newHandlerMethod + "\nto " + mapping + ": There is already '" + + handlerMethod.getBean() + "' bean method\n" + handlerMethod + " mapped."); + } + } + + private List getDirectUrls(T mapping) { + List urls = new ArrayList<>(1); + for (String path : getMappingPathPatterns(mapping)) { + if (!getPathMatcher().isPattern(path)) { + urls.add(path); + } + } + return urls; + } + + public void unregister(T mapping) { + this.readWriteLock.writeLock().lock(); + try { + MappingRegistration definition = this.registry.remove(mapping); + if (definition == null) { + return; + } + + this.mappingLookup.remove(definition.getMapping()); + + for (String url : definition.getDirectUrls()) { + List list = this.urlLookup.get(url); + if (list != null) { + list.remove(definition.getMapping()); + if (list.isEmpty()) { + this.urlLookup.remove(url); + } + } + } + } + finally { + this.readWriteLock.writeLock().unlock(); + } + } + } + + + private static class MappingRegistration { + + private final T mapping; + + private final HandlerMethod handlerMethod; + + private final List directUrls; + + + public MappingRegistration(T mapping, HandlerMethod handlerMethod, List directUrls) { + Assert.notNull(mapping); + Assert.notNull(handlerMethod); + this.mapping = mapping; + this.handlerMethod = handlerMethod; + this.directUrls = (directUrls != null ? directUrls : Collections.emptyList()); + } + + public T getMapping() { + return this.mapping; + } + + public HandlerMethod getHandlerMethod() { + return this.handlerMethod; + } + + public List getDirectUrls() { + return this.directUrls; + } + } + + + /** + * A thin wrapper around a matched HandlerMethod and its mapping, for the purpose of + * comparing the best match with a comparator in the context of the current request. + */ + private class Match { + + private final T mapping; + + private final HandlerMethod handlerMethod; + + public Match(T mapping, HandlerMethod handlerMethod) { + this.mapping = mapping; + this.handlerMethod = handlerMethod; + } + + @Override + public String toString() { + return this.mapping.toString(); + } + } + + + private class MatchComparator implements Comparator { + + private final Comparator comparator; + + public MatchComparator(Comparator comparator) { + this.comparator = comparator; + } + + @Override + public int compare(Match match1, Match match2) { + return this.comparator.compare(match1.mapping, match2.mapping); + } + } + +} diff --git a/spring-web-reactive/src/main/java/org/springframework/web/util/HttpRequestPathHelper.java b/spring-web-reactive/src/main/java/org/springframework/web/util/HttpRequestPathHelper.java new file mode 100644 index 00000000000..45c893d3e8c --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/web/util/HttpRequestPathHelper.java @@ -0,0 +1,66 @@ +/* + * 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.util; + +import java.io.UnsupportedEncodingException; + +import org.springframework.web.server.ServerWebExchange; + +/** + * A helper class to obtain the lookup path for path matching purposes. + * + * @author Rossen Stoyanchev + */ +public class HttpRequestPathHelper { + + private boolean urlDecode = true; + + + // TODO: sanitize path, default/request encoding?, remove path params? + + /** + * Set if the request path should be URL-decoded. + *

Default is "true". + * @see UriUtils#decode(String, String) + */ + public void setUrlDecode(boolean urlDecode) { + this.urlDecode = urlDecode; + } + + /** + * Whether the request path should be URL decoded. + */ + public boolean shouldUrlDecode() { + return this.urlDecode; + } + + + public String getLookupPathForRequest(ServerWebExchange exchange) { + String path = exchange.getRequest().getURI().getPath(); + return (this.shouldUrlDecode() ? decode(path) : path); + } + + private String decode(String path) { + try { + return UriUtils.decode(path, "UTF-8"); + } + catch (UnsupportedEncodingException ex) { + // Should not happen + throw new IllegalStateException("Could not decode request string [" + path + "]"); + } + } + +} \ No newline at end of file diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/HandlerMethodMappingTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/HandlerMethodMappingTests.java new file mode 100644 index 00000000000..0bcda250d9a --- /dev/null +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/method/HandlerMethodMappingTests.java @@ -0,0 +1,204 @@ +/* + * 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.result.method; + +import java.lang.reflect.Method; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.Set; + +import org.junit.Before; +import org.junit.Test; +import reactor.core.publisher.Mono; +import reactor.core.test.TestSubscriber; + +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.stereotype.Controller; +import org.springframework.util.AntPathMatcher; +import org.springframework.util.PathMatcher; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.method.HandlerMethod; +import org.springframework.web.reactive.result.method.AbstractHandlerMethodMapping; +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.mockito.Mockito.mock; + +/** + * Unit tests for {@link AbstractHandlerMethodMapping}. + * + * @author Rossen Stoyanchev + */ +public class HandlerMethodMappingTests { + + private AbstractHandlerMethodMapping mapping; + + private MyHandler handler; + + private Method method1; + + private Method method2; + + + @Before + public void setUp() throws Exception { + this.mapping = new MyHandlerMethodMapping(); + this.handler = new MyHandler(); + this.method1 = handler.getClass().getMethod("handlerMethod1"); + this.method2 = handler.getClass().getMethod("handlerMethod2"); + } + + + @Test(expected = IllegalStateException.class) + public void registerDuplicates() { + this.mapping.registerMapping("foo", this.handler, this.method1); + this.mapping.registerMapping("foo", this.handler, this.method2); + } + + @Test + public void directMatch() throws Exception { + String key = "foo"; + this.mapping.registerMapping(key, this.handler, this.method1); + + Mono result = this.mapping.getHandler(createExchange(HttpMethod.GET, key)); + assertEquals(this.method1, ((HandlerMethod) result.get()).getMethod()); + } + + @Test + public void patternMatch() throws Exception { + this.mapping.registerMapping("/fo*", this.handler, this.method1); + this.mapping.registerMapping("/f*", this.handler, this.method2); + + Mono result = this.mapping.getHandler(createExchange(HttpMethod.GET, "/foo")); + assertEquals(this.method1, ((HandlerMethod) result.get()).getMethod()); + } + + @Test + public void ambiguousMatch() throws Exception { + this.mapping.registerMapping("/f?o", this.handler, this.method1); + this.mapping.registerMapping("/fo?", this.handler, this.method2); + Mono result = this.mapping.getHandler(createExchange(HttpMethod.GET, "/foo")); + + TestSubscriber subscriber = new TestSubscriber<>(); + result.subscribeWith(subscriber); + subscriber.assertError(IllegalStateException.class); + } + + @Test + public void registerMapping() throws Exception { + String key1 = "/foo"; + String key2 = "/foo*"; + this.mapping.registerMapping(key1, this.handler, this.method1); + this.mapping.registerMapping(key2, this.handler, this.method2); + + List directUrlMatches = this.mapping.getMappingRegistry().getMappingsByUrl(key1); + assertNotNull(directUrlMatches); + assertEquals(1, directUrlMatches.size()); + assertEquals(key1, directUrlMatches.get(0)); + } + + @Test + public void registerMappingWithSameMethodAndTwoHandlerInstances() throws Exception { + String key1 = "foo"; + String key2 = "bar"; + MyHandler handler1 = new MyHandler(); + MyHandler handler2 = new MyHandler(); + this.mapping.registerMapping(key1, handler1, this.method1); + this.mapping.registerMapping(key2, handler2, this.method1); + + List directUrlMatches = this.mapping.getMappingRegistry().getMappingsByUrl(key1); + assertNotNull(directUrlMatches); + assertEquals(1, directUrlMatches.size()); + assertEquals(key1, directUrlMatches.get(0)); + } + + @Test + public void unregisterMapping() throws Exception { + String key = "foo"; + this.mapping.registerMapping(key, this.handler, this.method1); + Mono result = this.mapping.getHandler(createExchange(HttpMethod.GET, key)); + assertNotNull(result.get()); + + this.mapping.unregisterMapping(key); + result = this.mapping.getHandler(createExchange(HttpMethod.GET, key)); + assertNull(result.get()); + assertNull(this.mapping.getMappingRegistry().getMappingsByUrl(key)); + } + + private ServerWebExchange createExchange(HttpMethod httpMethod, String path) throws URISyntaxException { + ServerHttpRequest request = new MockServerHttpRequest(httpMethod, new URI(path)); + WebSessionManager sessionManager = mock(WebSessionManager.class); + return new DefaultServerWebExchange(request, new MockServerHttpResponse(), sessionManager); + } + + + private static class MyHandlerMethodMapping extends AbstractHandlerMethodMapping { + + private PathMatcher pathMatcher = new AntPathMatcher(); + + @Override + protected boolean isHandler(Class beanType) { + return true; + } + + @Override + protected String getMappingForMethod(Method method, Class handlerType) { + String methodName = method.getName(); + return methodName.startsWith("handler") ? methodName : null; + } + + @Override + protected Set getMappingPathPatterns(String key) { + return (this.pathMatcher.isPattern(key) ? Collections.emptySet() : Collections.singleton(key)); + } + + @Override + protected String getMatchingMapping(String pattern, ServerWebExchange exchange) { + String lookupPath = exchange.getRequest().getURI().getPath(); + return (this.pathMatcher.match(pattern, lookupPath) ? pattern : null); + } + + @Override + protected Comparator getMappingComparator(ServerWebExchange exchange) { + String lookupPath = exchange.getRequest().getURI().getPath(); + return this.pathMatcher.getPatternComparator(lookupPath); + } + + } + + @Controller + private static class MyHandler { + + @RequestMapping @SuppressWarnings("unused") + public void handlerMethod1() { + } + + @RequestMapping @SuppressWarnings("unused") + public void handlerMethod2() { + } + } +} \ No newline at end of file