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.
This commit is contained in:
parent
341f23e0e6
commit
33a7b91e57
|
@ -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 @@
|
|||
/*
|
||||
* 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 @@
|
|||
/*
|
||||
* 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 @@
|
|||
/*
|
||||
* 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