001/*
002 * Copyright 2002-2019 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.servlet.handler;
018
019import java.util.ArrayList;
020import java.util.Collections;
021import java.util.Comparator;
022import java.util.LinkedHashMap;
023import java.util.List;
024import java.util.Map;
025
026import javax.servlet.http.HttpServletRequest;
027import javax.servlet.http.HttpServletResponse;
028
029import org.springframework.beans.BeansException;
030import org.springframework.context.ApplicationContext;
031import org.springframework.lang.Nullable;
032import org.springframework.util.Assert;
033import org.springframework.util.CollectionUtils;
034import org.springframework.util.StringUtils;
035import org.springframework.web.servlet.HandlerExecutionChain;
036
037/**
038 * Abstract base class for URL-mapped {@link org.springframework.web.servlet.HandlerMapping}
039 * implementations. Provides infrastructure for mapping handlers to URLs and configurable
040 * URL lookup. For information on the latter, see "alwaysUseFullPath" property.
041 *
042 * <p>Supports direct matches, e.g. a registered "/test" matches "/test", and
043 * various Ant-style pattern matches, e.g. a registered "/t*" pattern matches
044 * both "/test" and "/team", "/test/*" matches all paths in the "/test" directory,
045 * "/test/**" matches all paths below "/test". For details, see the
046 * {@link org.springframework.util.AntPathMatcher AntPathMatcher} javadoc.
047 *
048 * <p>Will search all path patterns to find the most exact match for the
049 * current request path. The most exact match is defined as the longest
050 * path pattern that matches the current request path.
051 *
052 * @author Juergen Hoeller
053 * @author Arjen Poutsma
054 * @since 16.04.2003
055 */
056public abstract class AbstractUrlHandlerMapping extends AbstractHandlerMapping implements MatchableHandlerMapping {
057
058        @Nullable
059        private Object rootHandler;
060
061        private boolean useTrailingSlashMatch = false;
062
063        private boolean lazyInitHandlers = false;
064
065        private final Map<String, Object> handlerMap = new LinkedHashMap<>();
066
067
068        /**
069         * Set the root handler for this handler mapping, that is,
070         * the handler to be registered for the root path ("/").
071         * <p>Default is {@code null}, indicating no root handler.
072         */
073        public void setRootHandler(@Nullable Object rootHandler) {
074                this.rootHandler = rootHandler;
075        }
076
077        /**
078         * Return the root handler for this handler mapping (registered for "/"),
079         * or {@code null} if none.
080         */
081        @Nullable
082        public Object getRootHandler() {
083                return this.rootHandler;
084        }
085
086        /**
087         * Whether to match to URLs irrespective of the presence of a trailing slash.
088         * If enabled a URL pattern such as "/users" also matches to "/users/".
089         * <p>The default value is {@code false}.
090         */
091        public void setUseTrailingSlashMatch(boolean useTrailingSlashMatch) {
092                this.useTrailingSlashMatch = useTrailingSlashMatch;
093        }
094
095        /**
096         * Whether to match to URLs irrespective of the presence of a trailing slash.
097         */
098        public boolean useTrailingSlashMatch() {
099                return this.useTrailingSlashMatch;
100        }
101
102        /**
103         * Set whether to lazily initialize handlers. Only applicable to
104         * singleton handlers, as prototypes are always lazily initialized.
105         * Default is "false", as eager initialization allows for more efficiency
106         * through referencing the controller objects directly.
107         * <p>If you want to allow your controllers to be lazily initialized,
108         * make them "lazy-init" and set this flag to true. Just making them
109         * "lazy-init" will not work, as they are initialized through the
110         * references from the handler mapping in this case.
111         */
112        public void setLazyInitHandlers(boolean lazyInitHandlers) {
113                this.lazyInitHandlers = lazyInitHandlers;
114        }
115
116        /**
117         * Look up a handler for the URL path of the given request.
118         * @param request current HTTP request
119         * @return the handler instance, or {@code null} if none found
120         */
121        @Override
122        @Nullable
123        protected Object getHandlerInternal(HttpServletRequest request) throws Exception {
124                String lookupPath = getUrlPathHelper().getLookupPathForRequest(request);
125                request.setAttribute(LOOKUP_PATH, lookupPath);
126                Object handler = lookupHandler(lookupPath, request);
127                if (handler == null) {
128                        // We need to care for the default handler directly, since we need to
129                        // expose the PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE for it as well.
130                        Object rawHandler = null;
131                        if (StringUtils.matchesCharacter(lookupPath, '/')) {
132                                rawHandler = getRootHandler();
133                        }
134                        if (rawHandler == null) {
135                                rawHandler = getDefaultHandler();
136                        }
137                        if (rawHandler != null) {
138                                // Bean name or resolved handler?
139                                if (rawHandler instanceof String) {
140                                        String handlerName = (String) rawHandler;
141                                        rawHandler = obtainApplicationContext().getBean(handlerName);
142                                }
143                                validateHandler(rawHandler, request);
144                                handler = buildPathExposingHandler(rawHandler, lookupPath, lookupPath, null);
145                        }
146                }
147                return handler;
148        }
149
150        /**
151         * Look up a handler instance for the given URL path.
152         * <p>Supports direct matches, e.g. a registered "/test" matches "/test",
153         * and various Ant-style pattern matches, e.g. a registered "/t*" matches
154         * both "/test" and "/team". For details, see the AntPathMatcher class.
155         * <p>Looks for the most exact pattern, where most exact is defined as
156         * the longest path pattern.
157         * @param urlPath the URL the bean is mapped to
158         * @param request current HTTP request (to expose the path within the mapping to)
159         * @return the associated handler instance, or {@code null} if not found
160         * @see #exposePathWithinMapping
161         * @see org.springframework.util.AntPathMatcher
162         */
163        @Nullable
164        protected Object lookupHandler(String urlPath, HttpServletRequest request) throws Exception {
165                // Direct match?
166                Object handler = this.handlerMap.get(urlPath);
167                if (handler != null) {
168                        // Bean name or resolved handler?
169                        if (handler instanceof String) {
170                                String handlerName = (String) handler;
171                                handler = obtainApplicationContext().getBean(handlerName);
172                        }
173                        validateHandler(handler, request);
174                        return buildPathExposingHandler(handler, urlPath, urlPath, null);
175                }
176
177                // Pattern match?
178                List<String> matchingPatterns = new ArrayList<>();
179                for (String registeredPattern : this.handlerMap.keySet()) {
180                        if (getPathMatcher().match(registeredPattern, urlPath)) {
181                                matchingPatterns.add(registeredPattern);
182                        }
183                        else if (useTrailingSlashMatch()) {
184                                if (!registeredPattern.endsWith("/") && getPathMatcher().match(registeredPattern + "/", urlPath)) {
185                                        matchingPatterns.add(registeredPattern + "/");
186                                }
187                        }
188                }
189
190                String bestMatch = null;
191                Comparator<String> patternComparator = getPathMatcher().getPatternComparator(urlPath);
192                if (!matchingPatterns.isEmpty()) {
193                        matchingPatterns.sort(patternComparator);
194                        if (logger.isTraceEnabled() && matchingPatterns.size() > 1) {
195                                logger.trace("Matching patterns " + matchingPatterns);
196                        }
197                        bestMatch = matchingPatterns.get(0);
198                }
199                if (bestMatch != null) {
200                        handler = this.handlerMap.get(bestMatch);
201                        if (handler == null) {
202                                if (bestMatch.endsWith("/")) {
203                                        handler = this.handlerMap.get(bestMatch.substring(0, bestMatch.length() - 1));
204                                }
205                                if (handler == null) {
206                                        throw new IllegalStateException(
207                                                        "Could not find handler for best pattern match [" + bestMatch + "]");
208                                }
209                        }
210                        // Bean name or resolved handler?
211                        if (handler instanceof String) {
212                                String handlerName = (String) handler;
213                                handler = obtainApplicationContext().getBean(handlerName);
214                        }
215                        validateHandler(handler, request);
216                        String pathWithinMapping = getPathMatcher().extractPathWithinPattern(bestMatch, urlPath);
217
218                        // There might be multiple 'best patterns', let's make sure we have the correct URI template variables
219                        // for all of them
220                        Map<String, String> uriTemplateVariables = new LinkedHashMap<>();
221                        for (String matchingPattern : matchingPatterns) {
222                                if (patternComparator.compare(bestMatch, matchingPattern) == 0) {
223                                        Map<String, String> vars = getPathMatcher().extractUriTemplateVariables(matchingPattern, urlPath);
224                                        Map<String, String> decodedVars = getUrlPathHelper().decodePathVariables(request, vars);
225                                        uriTemplateVariables.putAll(decodedVars);
226                                }
227                        }
228                        if (logger.isTraceEnabled() && uriTemplateVariables.size() > 0) {
229                                logger.trace("URI variables " + uriTemplateVariables);
230                        }
231                        return buildPathExposingHandler(handler, bestMatch, pathWithinMapping, uriTemplateVariables);
232                }
233
234                // No handler found...
235                return null;
236        }
237
238        /**
239         * Validate the given handler against the current request.
240         * <p>The default implementation is empty. Can be overridden in subclasses,
241         * for example to enforce specific preconditions expressed in URL mappings.
242         * @param handler the handler object to validate
243         * @param request current HTTP request
244         * @throws Exception if validation failed
245         */
246        protected void validateHandler(Object handler, HttpServletRequest request) throws Exception {
247        }
248
249        /**
250         * Build a handler object for the given raw handler, exposing the actual
251         * handler, the {@link #PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE}, as well as
252         * the {@link #URI_TEMPLATE_VARIABLES_ATTRIBUTE} before executing the handler.
253         * <p>The default implementation builds a {@link HandlerExecutionChain}
254         * with a special interceptor that exposes the path attribute and uri template variables
255         * @param rawHandler the raw handler to expose
256         * @param pathWithinMapping the path to expose before executing the handler
257         * @param uriTemplateVariables the URI template variables, can be {@code null} if no variables found
258         * @return the final handler object
259         */
260        protected Object buildPathExposingHandler(Object rawHandler, String bestMatchingPattern,
261                        String pathWithinMapping, @Nullable Map<String, String> uriTemplateVariables) {
262
263                HandlerExecutionChain chain = new HandlerExecutionChain(rawHandler);
264                chain.addInterceptor(new PathExposingHandlerInterceptor(bestMatchingPattern, pathWithinMapping));
265                if (!CollectionUtils.isEmpty(uriTemplateVariables)) {
266                        chain.addInterceptor(new UriTemplateVariablesHandlerInterceptor(uriTemplateVariables));
267                }
268                return chain;
269        }
270
271        /**
272         * Expose the path within the current mapping as request attribute.
273         * @param pathWithinMapping the path within the current mapping
274         * @param request the request to expose the path to
275         * @see #PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE
276         */
277        protected void exposePathWithinMapping(String bestMatchingPattern, String pathWithinMapping,
278                        HttpServletRequest request) {
279
280                request.setAttribute(BEST_MATCHING_PATTERN_ATTRIBUTE, bestMatchingPattern);
281                request.setAttribute(PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE, pathWithinMapping);
282        }
283
284        /**
285         * Expose the URI templates variables as request attribute.
286         * @param uriTemplateVariables the URI template variables
287         * @param request the request to expose the path to
288         * @see #PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE
289         */
290        protected void exposeUriTemplateVariables(Map<String, String> uriTemplateVariables, HttpServletRequest request) {
291                request.setAttribute(URI_TEMPLATE_VARIABLES_ATTRIBUTE, uriTemplateVariables);
292        }
293
294        @Override
295        @Nullable
296        public RequestMatchResult match(HttpServletRequest request, String pattern) {
297                String lookupPath = getUrlPathHelper().getLookupPathForRequest(request, LOOKUP_PATH);
298                if (getPathMatcher().match(pattern, lookupPath)) {
299                        return new RequestMatchResult(pattern, lookupPath, getPathMatcher());
300                }
301                else if (useTrailingSlashMatch()) {
302                        if (!pattern.endsWith("/") && getPathMatcher().match(pattern + "/", lookupPath)) {
303                                return new RequestMatchResult(pattern + "/", lookupPath, getPathMatcher());
304                        }
305                }
306                return null;
307        }
308
309        /**
310         * Register the specified handler for the given URL paths.
311         * @param urlPaths the URLs that the bean should be mapped to
312         * @param beanName the name of the handler bean
313         * @throws BeansException if the handler couldn't be registered
314         * @throws IllegalStateException if there is a conflicting handler registered
315         */
316        protected void registerHandler(String[] urlPaths, String beanName) throws BeansException, IllegalStateException {
317                Assert.notNull(urlPaths, "URL path array must not be null");
318                for (String urlPath : urlPaths) {
319                        registerHandler(urlPath, beanName);
320                }
321        }
322
323        /**
324         * Register the specified handler for the given URL path.
325         * @param urlPath the URL the bean should be mapped to
326         * @param handler the handler instance or handler bean name String
327         * (a bean name will automatically be resolved into the corresponding handler bean)
328         * @throws BeansException if the handler couldn't be registered
329         * @throws IllegalStateException if there is a conflicting handler registered
330         */
331        protected void registerHandler(String urlPath, Object handler) throws BeansException, IllegalStateException {
332                Assert.notNull(urlPath, "URL path must not be null");
333                Assert.notNull(handler, "Handler object must not be null");
334                Object resolvedHandler = handler;
335
336                // Eagerly resolve handler if referencing singleton via name.
337                if (!this.lazyInitHandlers && handler instanceof String) {
338                        String handlerName = (String) handler;
339                        ApplicationContext applicationContext = obtainApplicationContext();
340                        if (applicationContext.isSingleton(handlerName)) {
341                                resolvedHandler = applicationContext.getBean(handlerName);
342                        }
343                }
344
345                Object mappedHandler = this.handlerMap.get(urlPath);
346                if (mappedHandler != null) {
347                        if (mappedHandler != resolvedHandler) {
348                                throw new IllegalStateException(
349                                                "Cannot map " + getHandlerDescription(handler) + " to URL path [" + urlPath +
350                                                "]: There is already " + getHandlerDescription(mappedHandler) + " mapped.");
351                        }
352                }
353                else {
354                        if (urlPath.equals("/")) {
355                                if (logger.isTraceEnabled()) {
356                                        logger.trace("Root mapping to " + getHandlerDescription(handler));
357                                }
358                                setRootHandler(resolvedHandler);
359                        }
360                        else if (urlPath.equals("/*")) {
361                                if (logger.isTraceEnabled()) {
362                                        logger.trace("Default mapping to " + getHandlerDescription(handler));
363                                }
364                                setDefaultHandler(resolvedHandler);
365                        }
366                        else {
367                                this.handlerMap.put(urlPath, resolvedHandler);
368                                if (logger.isTraceEnabled()) {
369                                        logger.trace("Mapped [" + urlPath + "] onto " + getHandlerDescription(handler));
370                                }
371                        }
372                }
373        }
374
375        private String getHandlerDescription(Object handler) {
376                return (handler instanceof String ? "'" + handler + "'" : handler.toString());
377        }
378
379
380        /**
381         * Return the registered handlers as an unmodifiable Map, with the registered path
382         * as key and the handler object (or handler bean name in case of a lazy-init handler)
383         * as value.
384         * @see #getDefaultHandler()
385         */
386        public final Map<String, Object> getHandlerMap() {
387                return Collections.unmodifiableMap(this.handlerMap);
388        }
389
390        /**
391         * Indicates whether this handler mapping support type-level mappings. Default to {@code false}.
392         */
393        protected boolean supportsTypeLevelMappings() {
394                return false;
395        }
396
397
398        /**
399         * Special interceptor for exposing the
400         * {@link AbstractUrlHandlerMapping#PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE} attribute.
401         * @see AbstractUrlHandlerMapping#exposePathWithinMapping
402         */
403        private class PathExposingHandlerInterceptor extends HandlerInterceptorAdapter {
404
405                private final String bestMatchingPattern;
406
407                private final String pathWithinMapping;
408
409                public PathExposingHandlerInterceptor(String bestMatchingPattern, String pathWithinMapping) {
410                        this.bestMatchingPattern = bestMatchingPattern;
411                        this.pathWithinMapping = pathWithinMapping;
412                }
413
414                @Override
415                public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
416                        exposePathWithinMapping(this.bestMatchingPattern, this.pathWithinMapping, request);
417                        request.setAttribute(BEST_MATCHING_HANDLER_ATTRIBUTE, handler);
418                        request.setAttribute(INTROSPECT_TYPE_LEVEL_MAPPING, supportsTypeLevelMappings());
419                        return true;
420                }
421
422        }
423
424        /**
425         * Special interceptor for exposing the
426         * {@link AbstractUrlHandlerMapping#URI_TEMPLATE_VARIABLES_ATTRIBUTE} attribute.
427         * @see AbstractUrlHandlerMapping#exposePathWithinMapping
428         */
429        private class UriTemplateVariablesHandlerInterceptor extends HandlerInterceptorAdapter {
430
431                private final Map<String, String> uriTemplateVariables;
432
433                public UriTemplateVariablesHandlerInterceptor(Map<String, String> uriTemplateVariables) {
434                        this.uriTemplateVariables = uriTemplateVariables;
435                }
436
437                @Override
438                public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
439                        exposeUriTemplateVariables(this.uriTemplateVariables, request);
440                        return true;
441                }
442        }
443
444}