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}