Browse Source
This commit adds AbstractHandlerMethodMapping, a starting point for AbstractHandlerMapping, and HttpRequestPathHelper with a similar purpose to UrlPathHelper but based with ServerWebExchange as input.pull/1111/head
4 changed files with 936 additions and 0 deletions
@ -0,0 +1,104 @@
@@ -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. |
||||
* <p>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; |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,562 @@
@@ -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}. |
||||
* |
||||
* <p>For each registered handler method, a unique mapping is maintained with |
||||
* subclasses defining the details of the mapping type {@code <T>}. |
||||
* |
||||
* @author Rossen Stoyanchev |
||||
* @param <T> The mapping for a {@link HandlerMethod} containing the conditions |
||||
* needed to match the handler method to incoming request. |
||||
*/ |
||||
public abstract class AbstractHandlerMethodMapping<T> 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. |
||||
* <p>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. |
||||
* <p>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<T, HandlerMethod> 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. |
||||
* <p>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. |
||||
* <p>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<Method, T> methods = MethodIntrospector.selectMethods(userType, |
||||
(MethodIntrospector.MetadataLookup<T>) method -> getMappingForMethod(method, userType)); |
||||
|
||||
if (logger.isDebugEnabled()) { |
||||
logger.debug(methods.size() + " request handler methods found on " + userType + ": " + methods); |
||||
} |
||||
for (Map.Entry<Method, T> 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<T, HandlerMethod> handlerMethods) { |
||||
} |
||||
|
||||
|
||||
// Handler method lookup
|
||||
|
||||
/** |
||||
* Look up a handler method for the given request. |
||||
* @param exchange the current exchange |
||||
*/ |
||||
@Override |
||||
public Mono<Object> 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<Match> matches = new ArrayList<Match>(); |
||||
List<T> 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<Match> 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<T> mappings, List<Match> 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<T> 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<String> 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<T> getMappingComparator(ServerWebExchange exchange); |
||||
|
||||
|
||||
/** |
||||
* A registry that maintains all mappings to handler methods, exposing methods |
||||
* to perform lookups and providing concurrent access. |
||||
* |
||||
* <p>Package-private for testing purposes. |
||||
*/ |
||||
class MappingRegistry { |
||||
|
||||
private final Map<T, MappingRegistration<T>> registry = new HashMap<>(); |
||||
|
||||
private final Map<T, HandlerMethod> mappingLookup = new LinkedHashMap<>(); |
||||
|
||||
private final MultiValueMap<String, T> urlLookup = new LinkedMultiValueMap<>(); |
||||
|
||||
private final ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock(); |
||||
|
||||
/** |
||||
* Return all mappings and handler methods. Not thread-safe. |
||||
* @see #acquireReadLock() |
||||
*/ |
||||
public Map<T, HandlerMethod> getMappings() { |
||||
return this.mappingLookup; |
||||
} |
||||
|
||||
/** |
||||
* Return matches for the given URL path. Not thread-safe. |
||||
* @see #acquireReadLock() |
||||
*/ |
||||
public List<T> 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<String> 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<String> getDirectUrls(T mapping) { |
||||
List<String> 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<T> definition = this.registry.remove(mapping); |
||||
if (definition == null) { |
||||
return; |
||||
} |
||||
|
||||
this.mappingLookup.remove(definition.getMapping()); |
||||
|
||||
for (String url : definition.getDirectUrls()) { |
||||
List<T> 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<T> { |
||||
|
||||
private final T mapping; |
||||
|
||||
private final HandlerMethod handlerMethod; |
||||
|
||||
private final List<String> directUrls; |
||||
|
||||
|
||||
public MappingRegistration(T mapping, HandlerMethod handlerMethod, List<String> 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<String> 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<Match> { |
||||
|
||||
private final Comparator<T> comparator; |
||||
|
||||
public MatchComparator(Comparator<T> comparator) { |
||||
this.comparator = comparator; |
||||
} |
||||
|
||||
@Override |
||||
public int compare(Match match1, Match match2) { |
||||
return this.comparator.compare(match1.mapping, match2.mapping); |
||||
} |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,66 @@
@@ -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. |
||||
* <p>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 + "]"); |
||||
} |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,204 @@
@@ -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<String> 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<Object> 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<Object> 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<Object> result = this.mapping.getHandler(createExchange(HttpMethod.GET, "/foo")); |
||||
|
||||
TestSubscriber<Object> 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<Object> 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<String> { |
||||
|
||||
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<String> 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<String> 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() { |
||||
} |
||||
} |
||||
} |
||||
Loading…
Reference in new issue