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