001/*
002 * Copyright 2002-2020 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.lang.reflect.Method;
020import java.util.ArrayList;
021import java.util.Arrays;
022import java.util.Collection;
023import java.util.Collections;
024import java.util.Comparator;
025import java.util.HashMap;
026import java.util.LinkedHashMap;
027import java.util.List;
028import java.util.Map;
029import java.util.Set;
030import java.util.concurrent.ConcurrentHashMap;
031import java.util.concurrent.locks.ReentrantReadWriteLock;
032import java.util.function.Function;
033import java.util.stream.Collectors;
034
035import javax.servlet.ServletException;
036import javax.servlet.http.HttpServletRequest;
037
038import kotlin.reflect.KFunction;
039import kotlin.reflect.jvm.ReflectJvmMapping;
040
041import org.springframework.aop.support.AopUtils;
042import org.springframework.beans.factory.BeanFactoryUtils;
043import org.springframework.beans.factory.InitializingBean;
044import org.springframework.core.KotlinDetector;
045import org.springframework.core.MethodIntrospector;
046import org.springframework.lang.Nullable;
047import org.springframework.util.Assert;
048import org.springframework.util.ClassUtils;
049import org.springframework.util.LinkedMultiValueMap;
050import org.springframework.util.MultiValueMap;
051import org.springframework.web.cors.CorsConfiguration;
052import org.springframework.web.cors.CorsUtils;
053import org.springframework.web.method.HandlerMethod;
054import org.springframework.web.servlet.HandlerMapping;
055
056/**
057 * Abstract base class for {@link HandlerMapping} implementations that define
058 * a mapping between a request and a {@link HandlerMethod}.
059 *
060 * <p>For each registered handler method, a unique mapping is maintained with
061 * subclasses defining the details of the mapping type {@code <T>}.
062 *
063 * @author Arjen Poutsma
064 * @author Rossen Stoyanchev
065 * @author Juergen Hoeller
066 * @author Sam Brannen
067 * @since 3.1
068 * @param <T> the mapping for a {@link HandlerMethod} containing the conditions
069 * needed to match the handler method to an incoming request.
070 */
071public abstract class AbstractHandlerMethodMapping<T> extends AbstractHandlerMapping implements InitializingBean {
072
073        /**
074         * Bean name prefix for target beans behind scoped proxies. Used to exclude those
075         * targets from handler method detection, in favor of the corresponding proxies.
076         * <p>We're not checking the autowire-candidate status here, which is how the
077         * proxy target filtering problem is being handled at the autowiring level,
078         * since autowire-candidate may have been turned to {@code false} for other
079         * reasons, while still expecting the bean to be eligible for handler methods.
080         * <p>Originally defined in {@link org.springframework.aop.scope.ScopedProxyUtils}
081         * but duplicated here to avoid a hard dependency on the spring-aop module.
082         */
083        private static final String SCOPED_TARGET_NAME_PREFIX = "scopedTarget.";
084
085        private static final HandlerMethod PREFLIGHT_AMBIGUOUS_MATCH =
086                        new HandlerMethod(new EmptyHandler(), ClassUtils.getMethod(EmptyHandler.class, "handle"));
087
088        private static final CorsConfiguration ALLOW_CORS_CONFIG = new CorsConfiguration();
089
090        static {
091                ALLOW_CORS_CONFIG.addAllowedOrigin("*");
092                ALLOW_CORS_CONFIG.addAllowedMethod("*");
093                ALLOW_CORS_CONFIG.addAllowedHeader("*");
094                ALLOW_CORS_CONFIG.setAllowCredentials(true);
095        }
096
097
098        private boolean detectHandlerMethodsInAncestorContexts = false;
099
100        @Nullable
101        private HandlerMethodMappingNamingStrategy<T> namingStrategy;
102
103        private final MappingRegistry mappingRegistry = new MappingRegistry();
104
105
106        /**
107         * Whether to detect handler methods in beans in ancestor ApplicationContexts.
108         * <p>Default is "false": Only beans in the current ApplicationContext are
109         * considered, i.e. only in the context that this HandlerMapping itself
110         * is defined in (typically the current DispatcherServlet's context).
111         * <p>Switch this flag on to detect handler beans in ancestor contexts
112         * (typically the Spring root WebApplicationContext) as well.
113         * @see #getCandidateBeanNames()
114         */
115        public void setDetectHandlerMethodsInAncestorContexts(boolean detectHandlerMethodsInAncestorContexts) {
116                this.detectHandlerMethodsInAncestorContexts = detectHandlerMethodsInAncestorContexts;
117        }
118
119        /**
120         * Configure the naming strategy to use for assigning a default name to every
121         * mapped handler method.
122         * <p>The default naming strategy is based on the capital letters of the
123         * class name followed by "#" and then the method name, e.g. "TC#getFoo"
124         * for a class named TestController with method getFoo.
125         */
126        public void setHandlerMethodMappingNamingStrategy(HandlerMethodMappingNamingStrategy<T> namingStrategy) {
127                this.namingStrategy = namingStrategy;
128        }
129
130        /**
131         * Return the configured naming strategy or {@code null}.
132         */
133        @Nullable
134        public HandlerMethodMappingNamingStrategy<T> getNamingStrategy() {
135                return this.namingStrategy;
136        }
137
138        /**
139         * Return a (read-only) map with all mappings and HandlerMethod's.
140         */
141        public Map<T, HandlerMethod> getHandlerMethods() {
142                this.mappingRegistry.acquireReadLock();
143                try {
144                        return Collections.unmodifiableMap(this.mappingRegistry.getMappings());
145                }
146                finally {
147                        this.mappingRegistry.releaseReadLock();
148                }
149        }
150
151        /**
152         * Return the handler methods for the given mapping name.
153         * @param mappingName the mapping name
154         * @return a list of matching HandlerMethod's or {@code null}; the returned
155         * list will never be modified and is safe to iterate.
156         * @see #setHandlerMethodMappingNamingStrategy
157         */
158        @Nullable
159        public List<HandlerMethod> getHandlerMethodsForMappingName(String mappingName) {
160                return this.mappingRegistry.getHandlerMethodsByMappingName(mappingName);
161        }
162
163        /**
164         * Return the internal mapping registry. Provided for testing purposes.
165         */
166        MappingRegistry getMappingRegistry() {
167                return this.mappingRegistry;
168        }
169
170        /**
171         * Register the given mapping.
172         * <p>This method may be invoked at runtime after initialization has completed.
173         * @param mapping the mapping for the handler method
174         * @param handler the handler
175         * @param method the method
176         */
177        public void registerMapping(T mapping, Object handler, Method method) {
178                if (logger.isTraceEnabled()) {
179                        logger.trace("Register \"" + mapping + "\" to " + method.toGenericString());
180                }
181                this.mappingRegistry.register(mapping, handler, method);
182        }
183
184        /**
185         * Un-register the given mapping.
186         * <p>This method may be invoked at runtime after initialization has completed.
187         * @param mapping the mapping to unregister
188         */
189        public void unregisterMapping(T mapping) {
190                if (logger.isTraceEnabled()) {
191                        logger.trace("Unregister mapping \"" + mapping + "\"");
192                }
193                this.mappingRegistry.unregister(mapping);
194        }
195
196
197        // Handler method detection
198
199        /**
200         * Detects handler methods at initialization.
201         * @see #initHandlerMethods
202         */
203        @Override
204        public void afterPropertiesSet() {
205                initHandlerMethods();
206        }
207
208        /**
209         * Scan beans in the ApplicationContext, detect and register handler methods.
210         * @see #getCandidateBeanNames()
211         * @see #processCandidateBean
212         * @see #handlerMethodsInitialized
213         */
214        protected void initHandlerMethods() {
215                for (String beanName : getCandidateBeanNames()) {
216                        if (!beanName.startsWith(SCOPED_TARGET_NAME_PREFIX)) {
217                                processCandidateBean(beanName);
218                        }
219                }
220                handlerMethodsInitialized(getHandlerMethods());
221        }
222
223        /**
224         * Determine the names of candidate beans in the application context.
225         * @since 5.1
226         * @see #setDetectHandlerMethodsInAncestorContexts
227         * @see BeanFactoryUtils#beanNamesForTypeIncludingAncestors
228         */
229        protected String[] getCandidateBeanNames() {
230                return (this.detectHandlerMethodsInAncestorContexts ?
231                                BeanFactoryUtils.beanNamesForTypeIncludingAncestors(obtainApplicationContext(), Object.class) :
232                                obtainApplicationContext().getBeanNamesForType(Object.class));
233        }
234
235        /**
236         * Determine the type of the specified candidate bean and call
237         * {@link #detectHandlerMethods} if identified as a handler type.
238         * <p>This implementation avoids bean creation through checking
239         * {@link org.springframework.beans.factory.BeanFactory#getType}
240         * and calling {@link #detectHandlerMethods} with the bean name.
241         * @param beanName the name of the candidate bean
242         * @since 5.1
243         * @see #isHandler
244         * @see #detectHandlerMethods
245         */
246        protected void processCandidateBean(String beanName) {
247                Class<?> beanType = null;
248                try {
249                        beanType = obtainApplicationContext().getType(beanName);
250                }
251                catch (Throwable ex) {
252                        // An unresolvable bean type, probably from a lazy bean - let's ignore it.
253                        if (logger.isTraceEnabled()) {
254                                logger.trace("Could not resolve type for bean '" + beanName + "'", ex);
255                        }
256                }
257                if (beanType != null && isHandler(beanType)) {
258                        detectHandlerMethods(beanName);
259                }
260        }
261
262        /**
263         * Look for handler methods in the specified handler bean.
264         * @param handler either a bean name or an actual handler instance
265         * @see #getMappingForMethod
266         */
267        protected void detectHandlerMethods(Object handler) {
268                Class<?> handlerType = (handler instanceof String ?
269                                obtainApplicationContext().getType((String) handler) : handler.getClass());
270
271                if (handlerType != null) {
272                        Class<?> userType = ClassUtils.getUserClass(handlerType);
273                        Map<Method, T> methods = MethodIntrospector.selectMethods(userType,
274                                        (MethodIntrospector.MetadataLookup<T>) method -> {
275                                                try {
276                                                        return getMappingForMethod(method, userType);
277                                                }
278                                                catch (Throwable ex) {
279                                                        throw new IllegalStateException("Invalid mapping on handler class [" +
280                                                                        userType.getName() + "]: " + method, ex);
281                                                }
282                                        });
283                        if (logger.isTraceEnabled()) {
284                                logger.trace(formatMappings(userType, methods));
285                        }
286                        methods.forEach((method, mapping) -> {
287                                Method invocableMethod = AopUtils.selectInvocableMethod(method, userType);
288                                registerHandlerMethod(handler, invocableMethod, mapping);
289                        });
290                }
291        }
292
293        private String formatMappings(Class<?> userType, Map<Method, T> methods) {
294                String formattedType = Arrays.stream(ClassUtils.getPackageName(userType).split("\\."))
295                                .map(p -> p.substring(0, 1))
296                                .collect(Collectors.joining(".", "", "." + userType.getSimpleName()));
297                Function<Method, String> methodFormatter = method -> Arrays.stream(method.getParameterTypes())
298                                .map(Class::getSimpleName)
299                                .collect(Collectors.joining(",", "(", ")"));
300                return methods.entrySet().stream()
301                                .map(e -> {
302                                        Method method = e.getKey();
303                                        return e.getValue() + ": " + method.getName() + methodFormatter.apply(method);
304                                })
305                                .collect(Collectors.joining("\n\t", "\n\t" + formattedType + ":" + "\n\t", ""));
306        }
307
308        /**
309         * Register a handler method and its unique mapping. Invoked at startup for
310         * each detected handler method.
311         * @param handler the bean name of the handler or the handler instance
312         * @param method the method to register
313         * @param mapping the mapping conditions associated with the handler method
314         * @throws IllegalStateException if another method was already registered
315         * under the same mapping
316         */
317        protected void registerHandlerMethod(Object handler, Method method, T mapping) {
318                this.mappingRegistry.register(mapping, handler, method);
319        }
320
321        /**
322         * Create the HandlerMethod instance.
323         * @param handler either a bean name or an actual handler instance
324         * @param method the target method
325         * @return the created HandlerMethod
326         */
327        protected HandlerMethod createHandlerMethod(Object handler, Method method) {
328                if (handler instanceof String) {
329                        return new HandlerMethod((String) handler,
330                                        obtainApplicationContext().getAutowireCapableBeanFactory(), method);
331                }
332                return new HandlerMethod(handler, method);
333        }
334
335        /**
336         * Extract and return the CORS configuration for the mapping.
337         */
338        @Nullable
339        protected CorsConfiguration initCorsConfiguration(Object handler, Method method, T mapping) {
340                return null;
341        }
342
343        /**
344         * Invoked after all handler methods have been detected.
345         * @param handlerMethods a read-only map with handler methods and mappings.
346         */
347        protected void handlerMethodsInitialized(Map<T, HandlerMethod> handlerMethods) {
348                // Total includes detected mappings + explicit registrations via registerMapping
349                int total = handlerMethods.size();
350                if ((logger.isTraceEnabled() && total == 0) || (logger.isDebugEnabled() && total > 0) ) {
351                        logger.debug(total + " mappings in " + formatMappingName());
352                }
353        }
354
355
356        // Handler method lookup
357
358        /**
359         * Look up a handler method for the given request.
360         */
361        @Override
362        protected HandlerMethod getHandlerInternal(HttpServletRequest request) throws Exception {
363                String lookupPath = getUrlPathHelper().getLookupPathForRequest(request);
364                request.setAttribute(LOOKUP_PATH, lookupPath);
365                this.mappingRegistry.acquireReadLock();
366                try {
367                        HandlerMethod handlerMethod = lookupHandlerMethod(lookupPath, request);
368                        return (handlerMethod != null ? handlerMethod.createWithResolvedBean() : null);
369                }
370                finally {
371                        this.mappingRegistry.releaseReadLock();
372                }
373        }
374
375        /**
376         * Look up the best-matching handler method for the current request.
377         * If multiple matches are found, the best match is selected.
378         * @param lookupPath mapping lookup path within the current servlet mapping
379         * @param request the current request
380         * @return the best-matching handler method, or {@code null} if no match
381         * @see #handleMatch(Object, String, HttpServletRequest)
382         * @see #handleNoMatch(Set, String, HttpServletRequest)
383         */
384        @Nullable
385        protected HandlerMethod lookupHandlerMethod(String lookupPath, HttpServletRequest request) throws Exception {
386                List<Match> matches = new ArrayList<>();
387                List<T> directPathMatches = this.mappingRegistry.getMappingsByUrl(lookupPath);
388                if (directPathMatches != null) {
389                        addMatchingMappings(directPathMatches, matches, request);
390                }
391                if (matches.isEmpty()) {
392                        // No choice but to go through all mappings...
393                        addMatchingMappings(this.mappingRegistry.getMappings().keySet(), matches, request);
394                }
395
396                if (!matches.isEmpty()) {
397                        Match bestMatch = matches.get(0);
398                        if (matches.size() > 1) {
399                                Comparator<Match> comparator = new MatchComparator(getMappingComparator(request));
400                                matches.sort(comparator);
401                                bestMatch = matches.get(0);
402                                if (logger.isTraceEnabled()) {
403                                        logger.trace(matches.size() + " matching mappings: " + matches);
404                                }
405                                if (CorsUtils.isPreFlightRequest(request)) {
406                                        return PREFLIGHT_AMBIGUOUS_MATCH;
407                                }
408                                Match secondBestMatch = matches.get(1);
409                                if (comparator.compare(bestMatch, secondBestMatch) == 0) {
410                                        Method m1 = bestMatch.handlerMethod.getMethod();
411                                        Method m2 = secondBestMatch.handlerMethod.getMethod();
412                                        String uri = request.getRequestURI();
413                                        throw new IllegalStateException(
414                                                        "Ambiguous handler methods mapped for '" + uri + "': {" + m1 + ", " + m2 + "}");
415                                }
416                        }
417                        request.setAttribute(BEST_MATCHING_HANDLER_ATTRIBUTE, bestMatch.handlerMethod);
418                        handleMatch(bestMatch.mapping, lookupPath, request);
419                        return bestMatch.handlerMethod;
420                }
421                else {
422                        return handleNoMatch(this.mappingRegistry.getMappings().keySet(), lookupPath, request);
423                }
424        }
425
426        private void addMatchingMappings(Collection<T> mappings, List<Match> matches, HttpServletRequest request) {
427                for (T mapping : mappings) {
428                        T match = getMatchingMapping(mapping, request);
429                        if (match != null) {
430                                matches.add(new Match(match, this.mappingRegistry.getMappings().get(mapping)));
431                        }
432                }
433        }
434
435        /**
436         * Invoked when a matching mapping is found.
437         * @param mapping the matching mapping
438         * @param lookupPath mapping lookup path within the current servlet mapping
439         * @param request the current request
440         */
441        protected void handleMatch(T mapping, String lookupPath, HttpServletRequest request) {
442                request.setAttribute(HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE, lookupPath);
443        }
444
445        /**
446         * Invoked when no matching mapping is not found.
447         * @param mappings all registered mappings
448         * @param lookupPath mapping lookup path within the current servlet mapping
449         * @param request the current request
450         * @throws ServletException in case of errors
451         */
452        @Nullable
453        protected HandlerMethod handleNoMatch(Set<T> mappings, String lookupPath, HttpServletRequest request)
454                        throws Exception {
455
456                return null;
457        }
458
459        @Override
460        protected boolean hasCorsConfigurationSource(Object handler) {
461                return super.hasCorsConfigurationSource(handler) ||
462                                (handler instanceof HandlerMethod && this.mappingRegistry.getCorsConfiguration((HandlerMethod) handler) != null);
463        }
464
465        @Override
466        protected CorsConfiguration getCorsConfiguration(Object handler, HttpServletRequest request) {
467                CorsConfiguration corsConfig = super.getCorsConfiguration(handler, request);
468                if (handler instanceof HandlerMethod) {
469                        HandlerMethod handlerMethod = (HandlerMethod) handler;
470                        if (handlerMethod.equals(PREFLIGHT_AMBIGUOUS_MATCH)) {
471                                return AbstractHandlerMethodMapping.ALLOW_CORS_CONFIG;
472                        }
473                        else {
474                                CorsConfiguration corsConfigFromMethod = this.mappingRegistry.getCorsConfiguration(handlerMethod);
475                                corsConfig = (corsConfig != null ? corsConfig.combine(corsConfigFromMethod) : corsConfigFromMethod);
476                        }
477                }
478                return corsConfig;
479        }
480
481
482        // Abstract template methods
483
484        /**
485         * Whether the given type is a handler with handler methods.
486         * @param beanType the type of the bean being checked
487         * @return "true" if this a handler type, "false" otherwise.
488         */
489        protected abstract boolean isHandler(Class<?> beanType);
490
491        /**
492         * Provide the mapping for a handler method. A method for which no
493         * mapping can be provided is not a handler method.
494         * @param method the method to provide a mapping for
495         * @param handlerType the handler type, possibly a sub-type of the method's
496         * declaring class
497         * @return the mapping, or {@code null} if the method is not mapped
498         */
499        @Nullable
500        protected abstract T getMappingForMethod(Method method, Class<?> handlerType);
501
502        /**
503         * Extract and return the URL paths contained in the supplied mapping.
504         */
505        protected abstract Set<String> getMappingPathPatterns(T mapping);
506
507        /**
508         * Check if a mapping matches the current request and return a (potentially
509         * new) mapping with conditions relevant to the current request.
510         * @param mapping the mapping to get a match for
511         * @param request the current HTTP servlet request
512         * @return the match, or {@code null} if the mapping doesn't match
513         */
514        @Nullable
515        protected abstract T getMatchingMapping(T mapping, HttpServletRequest request);
516
517        /**
518         * Return a comparator for sorting matching mappings.
519         * The returned comparator should sort 'better' matches higher.
520         * @param request the current request
521         * @return the comparator (never {@code null})
522         */
523        protected abstract Comparator<T> getMappingComparator(HttpServletRequest request);
524
525
526        /**
527         * A registry that maintains all mappings to handler methods, exposing methods
528         * to perform lookups and providing concurrent access.
529         * <p>Package-private for testing purposes.
530         */
531        class MappingRegistry {
532
533                private final Map<T, MappingRegistration<T>> registry = new HashMap<>();
534
535                private final Map<T, HandlerMethod> mappingLookup = new LinkedHashMap<>();
536
537                private final MultiValueMap<String, T> urlLookup = new LinkedMultiValueMap<>();
538
539                private final Map<String, List<HandlerMethod>> nameLookup = new ConcurrentHashMap<>();
540
541                private final Map<HandlerMethod, CorsConfiguration> corsLookup = new ConcurrentHashMap<>();
542
543                private final ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
544
545                /**
546                 * Return all mappings and handler methods. Not thread-safe.
547                 * @see #acquireReadLock()
548                 */
549                public Map<T, HandlerMethod> getMappings() {
550                        return this.mappingLookup;
551                }
552
553                /**
554                 * Return matches for the given URL path. Not thread-safe.
555                 * @see #acquireReadLock()
556                 */
557                @Nullable
558                public List<T> getMappingsByUrl(String urlPath) {
559                        return this.urlLookup.get(urlPath);
560                }
561
562                /**
563                 * Return handler methods by mapping name. Thread-safe for concurrent use.
564                 */
565                public List<HandlerMethod> getHandlerMethodsByMappingName(String mappingName) {
566                        return this.nameLookup.get(mappingName);
567                }
568
569                /**
570                 * Return CORS configuration. Thread-safe for concurrent use.
571                 */
572                @Nullable
573                public CorsConfiguration getCorsConfiguration(HandlerMethod handlerMethod) {
574                        HandlerMethod original = handlerMethod.getResolvedFromHandlerMethod();
575                        return this.corsLookup.get(original != null ? original : handlerMethod);
576                }
577
578                /**
579                 * Acquire the read lock when using getMappings and getMappingsByUrl.
580                 */
581                public void acquireReadLock() {
582                        this.readWriteLock.readLock().lock();
583                }
584
585                /**
586                 * Release the read lock after using getMappings and getMappingsByUrl.
587                 */
588                public void releaseReadLock() {
589                        this.readWriteLock.readLock().unlock();
590                }
591
592                public void register(T mapping, Object handler, Method method) {
593                        // Assert that the handler method is not a suspending one.
594                        if (KotlinDetector.isKotlinType(method.getDeclaringClass())) {
595                                Class<?>[] parameterTypes = method.getParameterTypes();
596                                if ((parameterTypes.length > 0) && "kotlin.coroutines.Continuation".equals(parameterTypes[parameterTypes.length - 1].getName())) {
597                                        throw new IllegalStateException("Unsupported suspending handler method detected: " + method);
598                                }
599                        }
600                        this.readWriteLock.writeLock().lock();
601                        try {
602                                HandlerMethod handlerMethod = createHandlerMethod(handler, method);
603                                validateMethodMapping(handlerMethod, mapping);
604                                this.mappingLookup.put(mapping, handlerMethod);
605
606                                List<String> directUrls = getDirectUrls(mapping);
607                                for (String url : directUrls) {
608                                        this.urlLookup.add(url, mapping);
609                                }
610
611                                String name = null;
612                                if (getNamingStrategy() != null) {
613                                        name = getNamingStrategy().getName(handlerMethod, mapping);
614                                        addMappingName(name, handlerMethod);
615                                }
616
617                                CorsConfiguration corsConfig = initCorsConfiguration(handler, method, mapping);
618                                if (corsConfig != null) {
619                                        this.corsLookup.put(handlerMethod, corsConfig);
620                                }
621
622                                this.registry.put(mapping, new MappingRegistration<>(mapping, handlerMethod, directUrls, name));
623                        }
624                        finally {
625                                this.readWriteLock.writeLock().unlock();
626                        }
627                }
628
629                private void validateMethodMapping(HandlerMethod handlerMethod, T mapping) {
630                        // Assert that the supplied mapping is unique.
631                        HandlerMethod existingHandlerMethod = this.mappingLookup.get(mapping);
632                        if (existingHandlerMethod != null && !existingHandlerMethod.equals(handlerMethod)) {
633                                throw new IllegalStateException(
634                                                "Ambiguous mapping. Cannot map '" + handlerMethod.getBean() + "' method \n" +
635                                                handlerMethod + "\nto " + mapping + ": There is already '" +
636                                                existingHandlerMethod.getBean() + "' bean method\n" + existingHandlerMethod + " mapped.");
637                        }
638                }
639
640                private List<String> getDirectUrls(T mapping) {
641                        List<String> urls = new ArrayList<>(1);
642                        for (String path : getMappingPathPatterns(mapping)) {
643                                if (!getPathMatcher().isPattern(path)) {
644                                        urls.add(path);
645                                }
646                        }
647                        return urls;
648                }
649
650                private void addMappingName(String name, HandlerMethod handlerMethod) {
651                        List<HandlerMethod> oldList = this.nameLookup.get(name);
652                        if (oldList == null) {
653                                oldList = Collections.emptyList();
654                        }
655
656                        for (HandlerMethod current : oldList) {
657                                if (handlerMethod.equals(current)) {
658                                        return;
659                                }
660                        }
661
662                        List<HandlerMethod> newList = new ArrayList<>(oldList.size() + 1);
663                        newList.addAll(oldList);
664                        newList.add(handlerMethod);
665                        this.nameLookup.put(name, newList);
666                }
667
668                public void unregister(T mapping) {
669                        this.readWriteLock.writeLock().lock();
670                        try {
671                                MappingRegistration<T> definition = this.registry.remove(mapping);
672                                if (definition == null) {
673                                        return;
674                                }
675
676                                this.mappingLookup.remove(definition.getMapping());
677
678                                for (String url : definition.getDirectUrls()) {
679                                        List<T> list = this.urlLookup.get(url);
680                                        if (list != null) {
681                                                list.remove(definition.getMapping());
682                                                if (list.isEmpty()) {
683                                                        this.urlLookup.remove(url);
684                                                }
685                                        }
686                                }
687
688                                removeMappingName(definition);
689
690                                this.corsLookup.remove(definition.getHandlerMethod());
691                        }
692                        finally {
693                                this.readWriteLock.writeLock().unlock();
694                        }
695                }
696
697                private void removeMappingName(MappingRegistration<T> definition) {
698                        String name = definition.getMappingName();
699                        if (name == null) {
700                                return;
701                        }
702                        HandlerMethod handlerMethod = definition.getHandlerMethod();
703                        List<HandlerMethod> oldList = this.nameLookup.get(name);
704                        if (oldList == null) {
705                                return;
706                        }
707                        if (oldList.size() <= 1) {
708                                this.nameLookup.remove(name);
709                                return;
710                        }
711                        List<HandlerMethod> newList = new ArrayList<>(oldList.size() - 1);
712                        for (HandlerMethod current : oldList) {
713                                if (!current.equals(handlerMethod)) {
714                                        newList.add(current);
715                                }
716                        }
717                        this.nameLookup.put(name, newList);
718                }
719        }
720
721
722        private static class MappingRegistration<T> {
723
724                private final T mapping;
725
726                private final HandlerMethod handlerMethod;
727
728                private final List<String> directUrls;
729
730                @Nullable
731                private final String mappingName;
732
733                public MappingRegistration(T mapping, HandlerMethod handlerMethod,
734                                @Nullable List<String> directUrls, @Nullable String mappingName) {
735
736                        Assert.notNull(mapping, "Mapping must not be null");
737                        Assert.notNull(handlerMethod, "HandlerMethod must not be null");
738                        this.mapping = mapping;
739                        this.handlerMethod = handlerMethod;
740                        this.directUrls = (directUrls != null ? directUrls : Collections.emptyList());
741                        this.mappingName = mappingName;
742                }
743
744                public T getMapping() {
745                        return this.mapping;
746                }
747
748                public HandlerMethod getHandlerMethod() {
749                        return this.handlerMethod;
750                }
751
752                public List<String> getDirectUrls() {
753                        return this.directUrls;
754                }
755
756                @Nullable
757                public String getMappingName() {
758                        return this.mappingName;
759                }
760        }
761
762
763        /**
764         * A thin wrapper around a matched HandlerMethod and its mapping, for the purpose of
765         * comparing the best match with a comparator in the context of the current request.
766         */
767        private class Match {
768
769                private final T mapping;
770
771                private final HandlerMethod handlerMethod;
772
773                public Match(T mapping, HandlerMethod handlerMethod) {
774                        this.mapping = mapping;
775                        this.handlerMethod = handlerMethod;
776                }
777
778                @Override
779                public String toString() {
780                        return this.mapping.toString();
781                }
782        }
783
784
785        private class MatchComparator implements Comparator<Match> {
786
787                private final Comparator<T> comparator;
788
789                public MatchComparator(Comparator<T> comparator) {
790                        this.comparator = comparator;
791                }
792
793                @Override
794                public int compare(Match match1, Match match2) {
795                        return this.comparator.compare(match1.mapping, match2.mapping);
796                }
797        }
798
799
800        private static class EmptyHandler {
801
802                @SuppressWarnings("unused")
803                public void handle() {
804                        throw new UnsupportedOperationException("Not implemented");
805                }
806        }
807
808        /**
809         * Inner class to avoid a hard dependency on Kotlin at runtime.
810         */
811        private static class KotlinDelegate {
812
813                static private boolean isSuspend(Method method) {
814                        KFunction<?> function = ReflectJvmMapping.getKotlinFunction(method);
815                        return function != null && function.isSuspend();
816                }
817        }
818
819}