001/*
002 * Copyright 2002-2018 the original author or authors.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *      https://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016
017package org.springframework.web.servlet.mvc.method.annotation;
018
019import java.lang.reflect.AnnotatedElement;
020import java.lang.reflect.Method;
021import java.util.List;
022import java.util.Set;
023import javax.servlet.http.HttpServletRequest;
024
025import org.springframework.context.EmbeddedValueResolverAware;
026import org.springframework.core.annotation.AnnotatedElementUtils;
027import org.springframework.stereotype.Controller;
028import org.springframework.util.Assert;
029import org.springframework.util.CollectionUtils;
030import org.springframework.util.StringValueResolver;
031import org.springframework.web.accept.ContentNegotiationManager;
032import org.springframework.web.bind.annotation.CrossOrigin;
033import org.springframework.web.bind.annotation.RequestMapping;
034import org.springframework.web.bind.annotation.RequestMethod;
035import org.springframework.web.cors.CorsConfiguration;
036import org.springframework.web.method.HandlerMethod;
037import org.springframework.web.servlet.handler.MatchableHandlerMapping;
038import org.springframework.web.servlet.handler.RequestMatchResult;
039import org.springframework.web.servlet.mvc.condition.AbstractRequestCondition;
040import org.springframework.web.servlet.mvc.condition.CompositeRequestCondition;
041import org.springframework.web.servlet.mvc.condition.RequestCondition;
042import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
043import org.springframework.web.servlet.mvc.method.RequestMappingInfoHandlerMapping;
044
045/**
046 * Creates {@link RequestMappingInfo} instances from type and method-level
047 * {@link RequestMapping @RequestMapping} annotations in
048 * {@link Controller @Controller} classes.
049 *
050 * @author Arjen Poutsma
051 * @author Rossen Stoyanchev
052 * @author Sam Brannen
053 * @since 3.1
054 */
055public class RequestMappingHandlerMapping extends RequestMappingInfoHandlerMapping
056                implements MatchableHandlerMapping, EmbeddedValueResolverAware {
057
058        private boolean useSuffixPatternMatch = true;
059
060        private boolean useRegisteredSuffixPatternMatch = false;
061
062        private boolean useTrailingSlashMatch = true;
063
064        private ContentNegotiationManager contentNegotiationManager = new ContentNegotiationManager();
065
066        private StringValueResolver embeddedValueResolver;
067
068        private RequestMappingInfo.BuilderConfiguration config = new RequestMappingInfo.BuilderConfiguration();
069
070
071        /**
072         * Whether to use suffix pattern match (".*") when matching patterns to
073         * requests. If enabled a method mapped to "/users" also matches to "/users.*".
074         * <p>The default value is {@code true}.
075         * <p>Also see {@link #setUseRegisteredSuffixPatternMatch(boolean)} for
076         * more fine-grained control over specific suffixes to allow.
077         */
078        public void setUseSuffixPatternMatch(boolean useSuffixPatternMatch) {
079                this.useSuffixPatternMatch = useSuffixPatternMatch;
080        }
081
082        /**
083         * Whether suffix pattern matching should work only against path extensions
084         * explicitly registered with the {@link ContentNegotiationManager}. This
085         * is generally recommended to reduce ambiguity and to avoid issues such as
086         * when a "." appears in the path for other reasons.
087         * <p>By default this is set to "false".
088         */
089        public void setUseRegisteredSuffixPatternMatch(boolean useRegisteredSuffixPatternMatch) {
090                this.useRegisteredSuffixPatternMatch = useRegisteredSuffixPatternMatch;
091                this.useSuffixPatternMatch = (useRegisteredSuffixPatternMatch || this.useSuffixPatternMatch);
092        }
093
094        /**
095         * Whether to match to URLs irrespective of the presence of a trailing slash.
096         * If enabled a method mapped to "/users" also matches to "/users/".
097         * <p>The default value is {@code true}.
098         */
099        public void setUseTrailingSlashMatch(boolean useTrailingSlashMatch) {
100                this.useTrailingSlashMatch = useTrailingSlashMatch;
101        }
102
103        /**
104         * Set the {@link ContentNegotiationManager} to use to determine requested media types.
105         * If not set, the default constructor is used.
106         */
107        public void setContentNegotiationManager(ContentNegotiationManager contentNegotiationManager) {
108                Assert.notNull(contentNegotiationManager, "ContentNegotiationManager must not be null");
109                this.contentNegotiationManager = contentNegotiationManager;
110        }
111
112        /**
113         * Return the configured {@link ContentNegotiationManager}.
114         */
115        public ContentNegotiationManager getContentNegotiationManager() {
116                return this.contentNegotiationManager;
117        }
118
119        @Override
120        public void setEmbeddedValueResolver(StringValueResolver resolver) {
121                this.embeddedValueResolver = resolver;
122        }
123
124        @Override
125        public void afterPropertiesSet() {
126                this.config = new RequestMappingInfo.BuilderConfiguration();
127                this.config.setUrlPathHelper(getUrlPathHelper());
128                this.config.setPathMatcher(getPathMatcher());
129                this.config.setSuffixPatternMatch(this.useSuffixPatternMatch);
130                this.config.setTrailingSlashMatch(this.useTrailingSlashMatch);
131                this.config.setRegisteredSuffixPatternMatch(this.useRegisteredSuffixPatternMatch);
132                this.config.setContentNegotiationManager(getContentNegotiationManager());
133
134                super.afterPropertiesSet();
135        }
136
137
138        /**
139         * Whether to use suffix pattern matching.
140         */
141        public boolean useSuffixPatternMatch() {
142                return this.useSuffixPatternMatch;
143        }
144
145        /**
146         * Whether to use registered suffixes for pattern matching.
147         */
148        public boolean useRegisteredSuffixPatternMatch() {
149                return this.useRegisteredSuffixPatternMatch;
150        }
151
152        /**
153         * Whether to match to URLs irrespective of the presence of a trailing slash.
154         */
155        public boolean useTrailingSlashMatch() {
156                return this.useTrailingSlashMatch;
157        }
158
159        /**
160         * Return the file extensions to use for suffix pattern matching.
161         */
162        public List<String> getFileExtensions() {
163                return this.config.getFileExtensions();
164        }
165
166
167        /**
168         * {@inheritDoc}
169         * <p>Expects a handler to have either a type-level @{@link Controller}
170         * annotation or a type-level @{@link RequestMapping} annotation.
171         */
172        @Override
173        protected boolean isHandler(Class<?> beanType) {
174                return (AnnotatedElementUtils.hasAnnotation(beanType, Controller.class) ||
175                                AnnotatedElementUtils.hasAnnotation(beanType, RequestMapping.class));
176        }
177
178        /**
179         * Uses method and type-level @{@link RequestMapping} annotations to create
180         * the RequestMappingInfo.
181         * @return the created RequestMappingInfo, or {@code null} if the method
182         * does not have a {@code @RequestMapping} annotation.
183         * @see #getCustomMethodCondition(Method)
184         * @see #getCustomTypeCondition(Class)
185         */
186        @Override
187        protected RequestMappingInfo getMappingForMethod(Method method, Class<?> handlerType) {
188                RequestMappingInfo info = createRequestMappingInfo(method);
189                if (info != null) {
190                        RequestMappingInfo typeInfo = createRequestMappingInfo(handlerType);
191                        if (typeInfo != null) {
192                                info = typeInfo.combine(info);
193                        }
194                }
195                return info;
196        }
197
198        /**
199         * Delegates to {@link #createRequestMappingInfo(RequestMapping, RequestCondition)},
200         * supplying the appropriate custom {@link RequestCondition} depending on whether
201         * the supplied {@code annotatedElement} is a class or method.
202         * @see #getCustomTypeCondition(Class)
203         * @see #getCustomMethodCondition(Method)
204         */
205        private RequestMappingInfo createRequestMappingInfo(AnnotatedElement element) {
206                RequestMapping requestMapping = AnnotatedElementUtils.findMergedAnnotation(element, RequestMapping.class);
207                RequestCondition<?> condition = (element instanceof Class ?
208                                getCustomTypeCondition((Class<?>) element) : getCustomMethodCondition((Method) element));
209                return (requestMapping != null ? createRequestMappingInfo(requestMapping, condition) : null);
210        }
211
212        /**
213         * Provide a custom type-level request condition.
214         * The custom {@link RequestCondition} can be of any type so long as the
215         * same condition type is returned from all calls to this method in order
216         * to ensure custom request conditions can be combined and compared.
217         * <p>Consider extending {@link AbstractRequestCondition} for custom
218         * condition types and using {@link CompositeRequestCondition} to provide
219         * multiple custom conditions.
220         * @param handlerType the handler type for which to create the condition
221         * @return the condition, or {@code null}
222         */
223        protected RequestCondition<?> getCustomTypeCondition(Class<?> handlerType) {
224                return null;
225        }
226
227        /**
228         * Provide a custom method-level request condition.
229         * The custom {@link RequestCondition} can be of any type so long as the
230         * same condition type is returned from all calls to this method in order
231         * to ensure custom request conditions can be combined and compared.
232         * <p>Consider extending {@link AbstractRequestCondition} for custom
233         * condition types and using {@link CompositeRequestCondition} to provide
234         * multiple custom conditions.
235         * @param method the handler method for which to create the condition
236         * @return the condition, or {@code null}
237         */
238        protected RequestCondition<?> getCustomMethodCondition(Method method) {
239                return null;
240        }
241
242        /**
243         * Create a {@link RequestMappingInfo} from the supplied
244         * {@link RequestMapping @RequestMapping} annotation, which is either
245         * a directly declared annotation, a meta-annotation, or the synthesized
246         * result of merging annotation attributes within an annotation hierarchy.
247         */
248        protected RequestMappingInfo createRequestMappingInfo(
249                        RequestMapping requestMapping, RequestCondition<?> customCondition) {
250
251                return RequestMappingInfo
252                                .paths(resolveEmbeddedValuesInPatterns(requestMapping.path()))
253                                .methods(requestMapping.method())
254                                .params(requestMapping.params())
255                                .headers(requestMapping.headers())
256                                .consumes(requestMapping.consumes())
257                                .produces(requestMapping.produces())
258                                .mappingName(requestMapping.name())
259                                .customCondition(customCondition)
260                                .options(this.config)
261                                .build();
262        }
263
264        /**
265         * Resolve placeholder values in the given array of patterns.
266         * @return a new array with updated patterns
267         */
268        protected String[] resolveEmbeddedValuesInPatterns(String[] patterns) {
269                if (this.embeddedValueResolver == null) {
270                        return patterns;
271                }
272                else {
273                        String[] resolvedPatterns = new String[patterns.length];
274                        for (int i = 0; i < patterns.length; i++) {
275                                resolvedPatterns[i] = this.embeddedValueResolver.resolveStringValue(patterns[i]);
276                        }
277                        return resolvedPatterns;
278                }
279        }
280
281        @Override
282        public RequestMatchResult match(HttpServletRequest request, String pattern) {
283                RequestMappingInfo info = RequestMappingInfo.paths(pattern).options(this.config).build();
284                RequestMappingInfo matchingInfo = info.getMatchingCondition(request);
285                if (matchingInfo == null) {
286                        return null;
287                }
288                Set<String> patterns = matchingInfo.getPatternsCondition().getPatterns();
289                String lookupPath = getUrlPathHelper().getLookupPathForRequest(request);
290                return new RequestMatchResult(patterns.iterator().next(), lookupPath, getPathMatcher());
291        }
292
293        @Override
294        protected CorsConfiguration initCorsConfiguration(Object handler, Method method, RequestMappingInfo mappingInfo) {
295                HandlerMethod handlerMethod = createHandlerMethod(handler, method);
296                CrossOrigin typeAnnotation = AnnotatedElementUtils.findMergedAnnotation(handlerMethod.getBeanType(), CrossOrigin.class);
297                CrossOrigin methodAnnotation = AnnotatedElementUtils.findMergedAnnotation(method, CrossOrigin.class);
298
299                if (typeAnnotation == null && methodAnnotation == null) {
300                        return null;
301                }302
303                CorsConfiguration config = new CorsConfiguration();
304                updateCorsConfig(config, typeAnnotation);
305                updateCorsConfig(config, methodAnnotation);
306
307                if (CollectionUtils.isEmpty(config.getAllowedMethods())) {
308                        for (RequestMethod allowedMethod : mappingInfo.getMethodsCondition().getMethods()) {
309                                config.addAllowedMethod(allowedMethod.name());
310                        }
311                }
312                return config.applyPermitDefaultValues();
313        }
314
315        private void updateCorsConfig(CorsConfiguration config, CrossOrigin annotation) {
316                if (annotation == null) {
317                        return;
318                }
319                for (String origin : annotation.origins()) {
320                        config.addAllowedOrigin(resolveCorsAnnotationValue(origin));
321                }
322                for (RequestMethod method : annotation.methods()) {
323                        config.addAllowedMethod(method.name());
324                }
325                for (String header : annotation.allowedHeaders()) {
326                        config.addAllowedHeader(resolveCorsAnnotationValue(header));
327                }
328                for (String header : annotation.exposedHeaders()) {
329                        config.addExposedHeader(resolveCorsAnnotationValue(header));
330                }
331
332                String allowCredentials = resolveCorsAnnotationValue(annotation.allowCredentials());
333                if ("true".equalsIgnoreCase(allowCredentials)) {
334                        config.setAllowCredentials(true);
335                }
336                else if ("false".equalsIgnoreCase(allowCredentials)) {
337                        config.setAllowCredentials(false);
338                }
339                else if (!allowCredentials.isEmpty()) {
340                        throw new IllegalStateException("@CrossOrigin's allowCredentials value must be \"true\", \"false\", " +
341                                        "or an empty string (\"\"): current value is [" + allowCredentials + "]");
342                }
343
344                if (annotation.maxAge() >= 0 && config.getMaxAge() == null) {
345                        config.setMaxAge(annotation.maxAge());
346                }
347        }
348
349        private String resolveCorsAnnotationValue(String value) {
350                return (this.embeddedValueResolver != null ? this.embeddedValueResolver.resolveStringValue(value) : value);
351        }
352
353}