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.reactive.result.method;
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 reactor.core.publisher.Mono;
036
037import org.springframework.aop.support.AopUtils;
038import org.springframework.beans.factory.InitializingBean;
039import org.springframework.core.MethodIntrospector;
040import org.springframework.http.server.RequestPath;
041import org.springframework.lang.Nullable;
042import org.springframework.util.Assert;
043import org.springframework.util.ClassUtils;
044import org.springframework.web.cors.CorsConfiguration;
045import org.springframework.web.cors.reactive.CorsUtils;
046import org.springframework.web.method.HandlerMethod;
047import org.springframework.web.reactive.HandlerMapping;
048import org.springframework.web.reactive.handler.AbstractHandlerMapping;
049import org.springframework.web.server.ServerWebExchange;
050
051/**
052 * Abstract base class for {@link HandlerMapping} implementations that define
053 * a mapping between a request and a {@link HandlerMethod}.
054 *
055 * <p>For each registered handler method, a unique mapping is maintained with
056 * subclasses defining the details of the mapping type {@code <T>}.
057 *
058 * @author Rossen Stoyanchev
059 * @author Brian Clozel
060 * @author Sam Brannen
061 * @since 5.0
062 * @param <T> the mapping for a {@link HandlerMethod} containing the conditions
063 * needed to match the handler method to an incoming request.
064 */
065public abstract class AbstractHandlerMethodMapping<T> extends AbstractHandlerMapping implements InitializingBean {
066
067        /**
068         * Bean name prefix for target beans behind scoped proxies. Used to exclude those
069         * targets from handler method detection, in favor of the corresponding proxies.
070         * <p>We're not checking the autowire-candidate status here, which is how the
071         * proxy target filtering problem is being handled at the autowiring level,
072         * since autowire-candidate may have been turned to {@code false} for other
073         * reasons, while still expecting the bean to be eligible for handler methods.
074         * <p>Originally defined in {@link org.springframework.aop.scope.ScopedProxyUtils}
075         * but duplicated here to avoid a hard dependency on the spring-aop module.
076         */
077        private static final String SCOPED_TARGET_NAME_PREFIX = "scopedTarget.";
078
079        /**
080         * HandlerMethod to return on a pre-flight request match when the request
081         * mappings are more nuanced than the access control headers.
082         */
083        private static final HandlerMethod PREFLIGHT_AMBIGUOUS_MATCH =
084                        new HandlerMethod(new PreFlightAmbiguousMatchHandler(),
085                                        ClassUtils.getMethod(PreFlightAmbiguousMatchHandler.class, "handle"));
086
087        private static final CorsConfiguration ALLOW_CORS_CONFIG = new CorsConfiguration();
088
089        static {
090                ALLOW_CORS_CONFIG.addAllowedOrigin("*");
091                ALLOW_CORS_CONFIG.addAllowedMethod("*");
092                ALLOW_CORS_CONFIG.addAllowedHeader("*");
093                ALLOW_CORS_CONFIG.setAllowCredentials(true);
094        }
095
096
097        private final MappingRegistry mappingRegistry = new MappingRegistry();
098
099
100        // TODO: handlerMethodMappingNamingStrategy
101
102        /**
103         * Return a (read-only) map with all mappings and HandlerMethod's.
104         */
105        public Map<T, HandlerMethod> getHandlerMethods() {
106                this.mappingRegistry.acquireReadLock();
107                try {
108                        return Collections.unmodifiableMap(this.mappingRegistry.getMappings());
109                }
110                finally {
111                        this.mappingRegistry.releaseReadLock();
112                }
113        }
114
115        /**
116         * Return the internal mapping registry. Provided for testing purposes.
117         */
118        MappingRegistry getMappingRegistry() {
119                return this.mappingRegistry;
120        }
121
122        /**
123         * Register the given mapping.
124         * <p>This method may be invoked at runtime after initialization has completed.
125         * @param mapping the mapping for the handler method
126         * @param handler the handler
127         * @param method the method
128         */
129        public void registerMapping(T mapping, Object handler, Method method) {
130                if (logger.isTraceEnabled()) {
131                        logger.trace("Register \"" + mapping + "\" to " + method.toGenericString());
132                }
133                this.mappingRegistry.register(mapping, handler, method);
134        }
135
136        /**
137         * Un-register the given mapping.
138         * <p>This method may be invoked at runtime after initialization has completed.
139         * @param mapping the mapping to unregister
140         */
141        public void unregisterMapping(T mapping) {
142                if (logger.isTraceEnabled()) {
143                        logger.trace("Unregister mapping \"" + mapping);
144                }
145                this.mappingRegistry.unregister(mapping);
146        }
147
148
149        // Handler method detection
150
151        /**
152         * Detects handler methods at initialization.
153         */
154        @Override
155        public void afterPropertiesSet() {
156
157                initHandlerMethods();
158
159                // Total includes detected mappings + explicit registrations via registerMapping..
160                int total = this.getHandlerMethods().size();
161
162                if ((logger.isTraceEnabled() && total == 0) || (logger.isDebugEnabled() && total > 0) ) {
163                        logger.debug(total + " mappings in " + formatMappingName());
164                }
165        }
166
167        /**
168         * Scan beans in the ApplicationContext, detect and register handler methods.
169         * @see #isHandler(Class)
170         * @see #getMappingForMethod(Method, Class)
171         * @see #handlerMethodsInitialized(Map)
172         */
173        protected void initHandlerMethods() {
174                String[] beanNames = obtainApplicationContext().getBeanNamesForType(Object.class);
175
176                for (String beanName : beanNames) {
177                        if (!beanName.startsWith(SCOPED_TARGET_NAME_PREFIX)) {
178                                Class<?> beanType = null;
179                                try {
180                                        beanType = obtainApplicationContext().getType(beanName);
181                                }
182                                catch (Throwable ex) {
183                                        // An unresolvable bean type, probably from a lazy bean - let's ignore it.
184                                        if (logger.isTraceEnabled()) {
185                                                logger.trace("Could not resolve type for bean '" + beanName + "'", ex);
186                                        }
187                                }
188                                if (beanType != null && isHandler(beanType)) {
189                                        detectHandlerMethods(beanName);
190                                }
191                        }
192                }
193                handlerMethodsInitialized(getHandlerMethods());
194        }
195
196        /**
197         * Look for handler methods in a handler.
198         * @param handler the bean name of a handler or a handler instance
199         */
200        protected void detectHandlerMethods(final Object handler) {
201                Class<?> handlerType = (handler instanceof String ?
202                                obtainApplicationContext().getType((String) handler) : handler.getClass());
203
204                if (handlerType != null) {
205                        final Class<?> userType = ClassUtils.getUserClass(handlerType);
206                        Map<Method, T> methods = MethodIntrospector.selectMethods(userType,
207                                        (MethodIntrospector.MetadataLookup<T>) method -> getMappingForMethod(method, userType));
208                        if (logger.isTraceEnabled()) {
209                                logger.trace(formatMappings(userType, methods));
210                        }
211                        methods.forEach((method, mapping) -> {
212                                Method invocableMethod = AopUtils.selectInvocableMethod(method, userType);
213                                registerHandlerMethod(handler, invocableMethod, mapping);
214                        });
215                }
216        }
217
218        private String formatMappings(Class<?> userType, Map<Method, T> methods) {
219                String formattedType = Arrays.stream(ClassUtils.getPackageName(userType).split("\\."))
220                                .map(p -> p.substring(0, 1))
221                                .collect(Collectors.joining(".", "", "." + userType.getSimpleName()));
222                Function<Method, String> methodFormatter = method -> Arrays.stream(method.getParameterTypes())
223                                .map(Class::getSimpleName)
224                                .collect(Collectors.joining(",", "(", ")"));
225                return methods.entrySet().stream()
226                                .map(e -> {
227                                        Method method = e.getKey();
228                                        return e.getValue() + ": " + method.getName() + methodFormatter.apply(method);
229                                })
230                                .collect(Collectors.joining("\n\t", "\n\t" + formattedType + ":" + "\n\t", ""));
231        }
232
233        /**
234         * Register a handler method and its unique mapping. Invoked at startup for
235         * each detected handler method.
236         * @param handler the bean name of the handler or the handler instance
237         * @param method the method to register
238         * @param mapping the mapping conditions associated with the handler method
239         * @throws IllegalStateException if another method was already registered
240         * under the same mapping
241         */
242        protected void registerHandlerMethod(Object handler, Method method, T mapping) {
243                this.mappingRegistry.register(mapping, handler, method);
244        }
245
246        /**
247         * Create the HandlerMethod instance.
248         * @param handler either a bean name or an actual handler instance
249         * @param method the target method
250         * @return the created HandlerMethod
251         */
252        protected HandlerMethod createHandlerMethod(Object handler, Method method) {
253                if (handler instanceof String) {
254                        return new HandlerMethod((String) handler,
255                                        obtainApplicationContext().getAutowireCapableBeanFactory(), method);
256                }
257                return new HandlerMethod(handler, method);
258        }
259
260        /**
261         * Extract and return the CORS configuration for the mapping.
262         */
263        @Nullable
264        protected CorsConfiguration initCorsConfiguration(Object handler, Method method, T mapping) {
265                return null;
266        }
267
268        /**
269         * Invoked after all handler methods have been detected.
270         * @param handlerMethods a read-only map with handler methods and mappings.
271         */
272        protected void handlerMethodsInitialized(Map<T, HandlerMethod> handlerMethods) {
273        }
274
275
276        // Handler method lookup
277
278        /**
279         * Look up a handler method for the given request.
280         * @param exchange the current exchange
281         */
282        @Override
283        public Mono<HandlerMethod> getHandlerInternal(ServerWebExchange exchange) {
284                this.mappingRegistry.acquireReadLock();
285                try {
286                        HandlerMethod handlerMethod;
287                        try {
288                                handlerMethod = lookupHandlerMethod(exchange);
289                        }
290                        catch (Exception ex) {
291                                return Mono.error(ex);
292                        }
293                        if (handlerMethod != null) {
294                                handlerMethod = handlerMethod.createWithResolvedBean();
295                        }
296                        return Mono.justOrEmpty(handlerMethod);
297                }
298                finally {
299                        this.mappingRegistry.releaseReadLock();
300                }
301        }
302
303        /**
304         * Look up the best-matching handler method for the current request.
305         * If multiple matches are found, the best match is selected.
306         * @param exchange the current exchange
307         * @return the best-matching handler method, or {@code null} if no match
308         * @see #handleMatch
309         * @see #handleNoMatch
310         */
311        @Nullable
312        protected HandlerMethod lookupHandlerMethod(ServerWebExchange exchange) throws Exception {
313                List<Match> matches = new ArrayList<>();
314                addMatchingMappings(this.mappingRegistry.getMappings().keySet(), matches, exchange);
315
316                if (!matches.isEmpty()) {
317                        Comparator<Match> comparator = new MatchComparator(getMappingComparator(exchange));
318                        matches.sort(comparator);
319                        Match bestMatch = matches.get(0);
320                        if (matches.size() > 1) {
321                                if (logger.isTraceEnabled()) {
322                                        logger.trace(exchange.getLogPrefix() + matches.size() + " matching mappings: " + matches);
323                                }
324                                if (CorsUtils.isPreFlightRequest(exchange.getRequest())) {
325                                        return PREFLIGHT_AMBIGUOUS_MATCH;
326                                }
327                                Match secondBestMatch = matches.get(1);
328                                if (comparator.compare(bestMatch, secondBestMatch) == 0) {
329                                        Method m1 = bestMatch.handlerMethod.getMethod();
330                                        Method m2 = secondBestMatch.handlerMethod.getMethod();
331                                        RequestPath path = exchange.getRequest().getPath();
332                                        throw new IllegalStateException(
333                                                        "Ambiguous handler methods mapped for '" + path + "': {" + m1 + ", " + m2 + "}");
334                                }
335                        }
336                        handleMatch(bestMatch.mapping, bestMatch.handlerMethod, exchange);
337                        return bestMatch.handlerMethod;
338                }
339                else {
340                        return handleNoMatch(this.mappingRegistry.getMappings().keySet(), exchange);
341                }
342        }
343
344        private void addMatchingMappings(Collection<T> mappings, List<Match> matches, ServerWebExchange exchange) {
345                for (T mapping : mappings) {
346                        T match = getMatchingMapping(mapping, exchange);
347                        if (match != null) {
348                                matches.add(new Match(match, this.mappingRegistry.getMappings().get(mapping)));
349                        }
350                }
351        }
352
353        /**
354         * Invoked when a matching mapping is found.
355         * @param mapping the matching mapping
356         * @param handlerMethod the matching method
357         * @param exchange the current exchange
358         */
359        protected void handleMatch(T mapping, HandlerMethod handlerMethod, ServerWebExchange exchange) {
360        }
361
362        /**
363         * Invoked when no matching mapping is not found.
364         * @param mappings all registered mappings
365         * @param exchange the current exchange
366         * @return an alternative HandlerMethod or {@code null}
367         * @throws Exception provides details that can be translated into an error status code
368         */
369        @Nullable
370        protected HandlerMethod handleNoMatch(Set<T> mappings, ServerWebExchange exchange) throws Exception {
371                return null;
372        }
373
374        @Override
375        protected boolean hasCorsConfigurationSource(Object handler) {
376                return super.hasCorsConfigurationSource(handler) ||
377                                (handler instanceof HandlerMethod && this.mappingRegistry.getCorsConfiguration((HandlerMethod) handler) != null);
378        }
379
380        @Override
381        protected CorsConfiguration getCorsConfiguration(Object handler, ServerWebExchange exchange) {
382                CorsConfiguration corsConfig = super.getCorsConfiguration(handler, exchange);
383                if (handler instanceof HandlerMethod) {
384                        HandlerMethod handlerMethod = (HandlerMethod) handler;
385                        if (handlerMethod.equals(PREFLIGHT_AMBIGUOUS_MATCH)) {
386                                return ALLOW_CORS_CONFIG;
387                        }
388                        CorsConfiguration methodConfig = this.mappingRegistry.getCorsConfiguration(handlerMethod);
389                        corsConfig = (corsConfig != null ? corsConfig.combine(methodConfig) : methodConfig);
390                }
391                return corsConfig;
392        }
393
394
395        // Abstract template methods
396
397        /**
398         * Whether the given type is a handler with handler methods.
399         * @param beanType the type of the bean being checked
400         * @return "true" if this a handler type, "false" otherwise.
401         */
402        protected abstract boolean isHandler(Class<?> beanType);
403
404        /**
405         * Provide the mapping for a handler method. A method for which no
406         * mapping can be provided is not a handler method.
407         * @param method the method to provide a mapping for
408         * @param handlerType the handler type, possibly a sub-type of the method's
409         * declaring class
410         * @return the mapping, or {@code null} if the method is not mapped
411         */
412        @Nullable
413        protected abstract T getMappingForMethod(Method method, Class<?> handlerType);
414
415        /**
416         * Check if a mapping matches the current request and return a (potentially
417         * new) mapping with conditions relevant to the current request.
418         * @param mapping the mapping to get a match for
419         * @param exchange the current exchange
420         * @return the match, or {@code null} if the mapping doesn't match
421         */
422        @Nullable
423        protected abstract T getMatchingMapping(T mapping, ServerWebExchange exchange);
424
425        /**
426         * Return a comparator for sorting matching mappings.
427         * The returned comparator should sort 'better' matches higher.
428         * @param exchange the current exchange
429         * @return the comparator (never {@code null})
430         */
431        protected abstract Comparator<T> getMappingComparator(ServerWebExchange exchange);
432
433
434        /**
435         * A registry that maintains all mappings to handler methods, exposing methods
436         * to perform lookups and providing concurrent access.
437         *
438         * <p>Package-private for testing purposes.
439         */
440        class MappingRegistry {
441
442                private final Map<T, MappingRegistration<T>> registry = new HashMap<>();
443
444                private final Map<T, HandlerMethod> mappingLookup = new LinkedHashMap<>();
445
446                private final Map<HandlerMethod, CorsConfiguration> corsLookup = new ConcurrentHashMap<>();
447
448                private final ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
449
450                /**
451                 * Return all mappings and handler methods. Not thread-safe.
452                 * @see #acquireReadLock()
453                 */
454                public Map<T, HandlerMethod> getMappings() {
455                        return this.mappingLookup;
456                }
457
458                /**
459                 * Return CORS configuration. Thread-safe for concurrent use.
460                 */
461                @Nullable
462                public CorsConfiguration getCorsConfiguration(HandlerMethod handlerMethod) {
463                        HandlerMethod original = handlerMethod.getResolvedFromHandlerMethod();
464                        return this.corsLookup.get(original != null ? original : handlerMethod);
465                }
466
467                /**
468                 * Acquire the read lock when using getMappings and getMappingsByUrl.
469                 */
470                public void acquireReadLock() {
471                        this.readWriteLock.readLock().lock();
472                }
473
474                /**
475                 * Release the read lock after using getMappings and getMappingsByUrl.
476                 */
477                public void releaseReadLock() {
478                        this.readWriteLock.readLock().unlock();
479                }
480
481                public void register(T mapping, Object handler, Method method) {
482                        this.readWriteLock.writeLock().lock();
483                        try {
484                                HandlerMethod handlerMethod = createHandlerMethod(handler, method);
485                                validateMethodMapping(handlerMethod, mapping);
486                                this.mappingLookup.put(mapping, handlerMethod);
487
488                                CorsConfiguration corsConfig = initCorsConfiguration(handler, method, mapping);
489                                if (corsConfig != null) {
490                                        this.corsLookup.put(handlerMethod, corsConfig);
491                                }
492
493                                this.registry.put(mapping, new MappingRegistration<>(mapping, handlerMethod));
494                        }
495                        finally {
496                                this.readWriteLock.writeLock().unlock();
497                        }
498                }
499
500                private void validateMethodMapping(HandlerMethod handlerMethod, T mapping) {
501                        // Assert that the supplied mapping is unique.
502                        HandlerMethod existingHandlerMethod = this.mappingLookup.get(mapping);
503                        if (existingHandlerMethod != null && !existingHandlerMethod.equals(handlerMethod)) {
504                                throw new IllegalStateException(
505                                                "Ambiguous mapping. Cannot map '" + handlerMethod.getBean() + "' method \n" +
506                                                handlerMethod + "\nto " + mapping + ": There is already '" +
507                                                existingHandlerMethod.getBean() + "' bean method\n" + existingHandlerMethod + " mapped.");
508                        }
509                }
510
511                public void unregister(T mapping) {
512                        this.readWriteLock.writeLock().lock();
513                        try {
514                                MappingRegistration<T> definition = this.registry.remove(mapping);
515                                if (definition == null) {
516                                        return;
517                                }
518
519                                this.mappingLookup.remove(definition.getMapping());
520                                this.corsLookup.remove(definition.getHandlerMethod());
521                        }
522                        finally {
523                                this.readWriteLock.writeLock().unlock();
524                        }
525                }
526        }
527
528
529        private static class MappingRegistration<T> {
530
531                private final T mapping;
532
533                private final HandlerMethod handlerMethod;
534
535                public MappingRegistration(T mapping, HandlerMethod handlerMethod) {
536                        Assert.notNull(mapping, "Mapping must not be null");
537                        Assert.notNull(handlerMethod, "HandlerMethod must not be null");
538                        this.mapping = mapping;
539                        this.handlerMethod = handlerMethod;
540                }
541
542                public T getMapping() {
543                        return this.mapping;
544                }
545
546                public HandlerMethod getHandlerMethod() {
547                        return this.handlerMethod;
548                }
549
550        }
551
552
553        /**
554         * A thin wrapper around a matched HandlerMethod and its mapping, for the purpose of
555         * comparing the best match with a comparator in the context of the current request.
556         */
557        private class Match {
558
559                private final T mapping;
560
561                private final HandlerMethod handlerMethod;
562
563                public Match(T mapping, HandlerMethod handlerMethod) {
564                        this.mapping = mapping;
565                        this.handlerMethod = handlerMethod;
566                }
567
568                @Override
569                public String toString() {
570                        return this.mapping.toString();
571                }
572        }
573
574
575        private class MatchComparator implements Comparator<Match> {
576
577                private final Comparator<T> comparator;
578
579                public MatchComparator(Comparator<T> comparator) {
580                        this.comparator = comparator;
581                }
582
583                @Override
584                public int compare(Match match1, Match match2) {
585                        return this.comparator.compare(match1.mapping, match2.mapping);
586                }
587        }
588
589
590        private static class PreFlightAmbiguousMatchHandler {
591
592                @SuppressWarnings("unused")
593                public void handle() {
594                        throw new UnsupportedOperationException("Not implemented");
595                }
596        }
597
598}