001/* 002 * Copyright 2002-2018 the original author or authors. 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * https://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016 017package org.springframework.web.reactive.handler; 018 019import java.util.Collections; 020import java.util.LinkedHashMap; 021import java.util.List; 022import java.util.Map; 023import java.util.stream.Collectors; 024 025import reactor.core.publisher.Mono; 026 027import org.springframework.beans.BeansException; 028import org.springframework.http.server.PathContainer; 029import org.springframework.lang.Nullable; 030import org.springframework.util.Assert; 031import org.springframework.util.StringUtils; 032import org.springframework.web.server.ServerWebExchange; 033import org.springframework.web.util.pattern.PathPattern; 034 035/** 036 * Abstract base class for URL-mapped 037 * {@link org.springframework.web.reactive.HandlerMapping} implementations. 038 * 039 * <p>Supports direct matches, e.g. a registered "/test" matches "/test", and 040 * various path pattern matches, e.g. a registered "/t*" pattern matches 041 * both "/test" and "/team", "/test/*" matches all paths under "/test", 042 * "/test/**" matches all paths below "/test". For details, see the 043 * {@link org.springframework.web.util.pattern.PathPattern} javadoc. 044 * 045 * <p>Will search all path patterns to find the most specific match for the 046 * current request path. The most specific pattern is defined as the longest 047 * path pattern with the fewest captured variables and wildcards. 048 * 049 * @author Rossen Stoyanchev 050 * @author Juergen Hoeller 051 * @author Brian Clozel 052 * @since 5.0 053 */ 054public abstract class AbstractUrlHandlerMapping extends AbstractHandlerMapping { 055 056 private boolean lazyInitHandlers = false; 057 058 private final Map<PathPattern, Object> handlerMap = new LinkedHashMap<>(); 059 060 061 /** 062 * Set whether to lazily initialize handlers. Only applicable to 063 * singleton handlers, as prototypes are always lazily initialized. 064 * Default is "false", as eager initialization allows for more efficiency 065 * through referencing the controller objects directly. 066 * <p>If you want to allow your controllers to be lazily initialized, 067 * make them "lazy-init" and set this flag to true. Just making them 068 * "lazy-init" will not work, as they are initialized through the 069 * references from the handler mapping in this case. 070 */ 071 public void setLazyInitHandlers(boolean lazyInitHandlers) { 072 this.lazyInitHandlers = lazyInitHandlers; 073 } 074 075 /** 076 * Return a read-only view of registered path patterns and handlers which may 077 * may be an actual handler instance or the bean name of lazily initialized 078 * handler. 079 */ 080 public final Map<PathPattern, Object> getHandlerMap() { 081 return Collections.unmodifiableMap(this.handlerMap); 082 } 083 084 085 @Override 086 public Mono<Object> getHandlerInternal(ServerWebExchange exchange) { 087 PathContainer lookupPath = exchange.getRequest().getPath().pathWithinApplication(); 088 Object handler; 089 try { 090 handler = lookupHandler(lookupPath, exchange); 091 } 092 catch (Exception ex) { 093 return Mono.error(ex); 094 } 095 return Mono.justOrEmpty(handler); 096 } 097 098 /** 099 * Look up a handler instance for the given URL lookup path. 100 * <p>Supports direct matches, e.g. a registered "/test" matches "/test", 101 * and various path pattern matches, e.g. a registered "/t*" matches 102 * both "/test" and "/team". For details, see the PathPattern class. 103 * @param lookupPath the URL the handler is mapped to 104 * @param exchange the current exchange 105 * @return the associated handler instance, or {@code null} if not found 106 * @see org.springframework.web.util.pattern.PathPattern 107 */ 108 @Nullable 109 protected Object lookupHandler(PathContainer lookupPath, ServerWebExchange exchange) throws Exception { 110 111 List<PathPattern> matches = this.handlerMap.keySet().stream() 112 .filter(key -> key.matches(lookupPath)) 113 .collect(Collectors.toList()); 114 115 if (matches.isEmpty()) { 116 return null; 117 } 118 119 if (matches.size() > 1) { 120 matches.sort(PathPattern.SPECIFICITY_COMPARATOR); 121 if (logger.isTraceEnabled()) { 122 logger.debug(exchange.getLogPrefix() + "Matching patterns " + matches); 123 } 124 } 125 126 PathPattern pattern = matches.get(0); 127 PathContainer pathWithinMapping = pattern.extractPathWithinPattern(lookupPath); 128 return handleMatch(this.handlerMap.get(pattern), pattern, pathWithinMapping, exchange); 129 } 130 131 private Object handleMatch(Object handler, PathPattern bestMatch, PathContainer pathWithinMapping, 132 ServerWebExchange exchange) { 133 134 // Bean name or resolved handler? 135 if (handler instanceof String) { 136 String handlerName = (String) handler; 137 handler = obtainApplicationContext().getBean(handlerName); 138 } 139 140 validateHandler(handler, exchange); 141 142 exchange.getAttributes().put(BEST_MATCHING_HANDLER_ATTRIBUTE, handler); 143 exchange.getAttributes().put(BEST_MATCHING_PATTERN_ATTRIBUTE, bestMatch); 144 exchange.getAttributes().put(PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE, pathWithinMapping); 145 146 return handler; 147 } 148 149 /** 150 * Validate the given handler against the current request. 151 * <p>The default implementation is empty. Can be overridden in subclasses, 152 * for example to enforce specific preconditions expressed in URL mappings. 153 * @param handler the handler object to validate 154 * @param exchange current exchange 155 */ 156 @SuppressWarnings("UnusedParameters") 157 protected void validateHandler(Object handler, ServerWebExchange exchange) { 158 } 159 160 /** 161 * Register the specified handler for the given URL paths. 162 * @param urlPaths the URLs that the bean should be mapped to 163 * @param beanName the name of the handler bean 164 * @throws BeansException if the handler couldn't be registered 165 * @throws IllegalStateException if there is a conflicting handler registered 166 */ 167 protected void registerHandler(String[] urlPaths, String beanName) throws BeansException, IllegalStateException { 168 Assert.notNull(urlPaths, "URL path array must not be null"); 169 for (String urlPath : urlPaths) { 170 registerHandler(urlPath, beanName); 171 } 172 } 173 174 /** 175 * Register the specified handler for the given URL path. 176 * @param urlPath the URL the bean should be mapped to 177 * @param handler the handler instance or handler bean name String 178 * (a bean name will automatically be resolved into the corresponding handler bean) 179 * @throws BeansException if the handler couldn't be registered 180 * @throws IllegalStateException if there is a conflicting handler registered 181 */ 182 protected void registerHandler(String urlPath, Object handler) throws BeansException, IllegalStateException { 183 Assert.notNull(urlPath, "URL path must not be null"); 184 Assert.notNull(handler, "Handler object must not be null"); 185 Object resolvedHandler = handler; 186 187 // Parse path pattern 188 urlPath = prependLeadingSlash(urlPath); 189 PathPattern pattern = getPathPatternParser().parse(urlPath); 190 if (this.handlerMap.containsKey(pattern)) { 191 Object existingHandler = this.handlerMap.get(pattern); 192 if (existingHandler != null && existingHandler != resolvedHandler) { 193 throw new IllegalStateException( 194 "Cannot map " + getHandlerDescription(handler) + " to [" + urlPath + "]: " + 195 "there is already " + getHandlerDescription(existingHandler) + " mapped."); 196 } 197 } 198 199 // Eagerly resolve handler if referencing singleton via name. 200 if (!this.lazyInitHandlers && handler instanceof String) { 201 String handlerName = (String) handler; 202 if (obtainApplicationContext().isSingleton(handlerName)) { 203 resolvedHandler = obtainApplicationContext().getBean(handlerName); 204 } 205 } 206 207 // Register resolved handler 208 this.handlerMap.put(pattern, resolvedHandler); 209 if (logger.isTraceEnabled()) { 210 logger.trace("Mapped [" + urlPath + "] onto " + getHandlerDescription(handler)); 211 } 212 } 213 214 private String getHandlerDescription(Object handler) { 215 return (handler instanceof String ? "'" + handler + "'" : handler.toString()); 216 } 217 218 219 private static String prependLeadingSlash(String pattern) { 220 if (StringUtils.hasLength(pattern) && !pattern.startsWith("/")) { 221 return "/" + pattern; 222 } 223 else { 224 return pattern; 225 } 226 } 227 228}