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