RequestMappingHandlerMapping.java 12.5 KB
Newer Older
1
/*
S
Sebastien Deleuze 已提交
2
 * Copyright 2002-2015 the original author or authors.
3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.springframework.web.servlet.mvc.method.annotation;

import java.lang.reflect.Method;
20 21
import java.util.ArrayList;
import java.util.List;
22

23
import org.springframework.context.EmbeddedValueResolverAware;
24 25
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.stereotype.Controller;
26
import org.springframework.util.Assert;
S
Sebastien Deleuze 已提交
27
import org.springframework.util.CollectionUtils;
28
import org.springframework.util.StringValueResolver;
29
import org.springframework.web.accept.ContentNegotiationManager;
S
Sebastien Deleuze 已提交
30
import org.springframework.web.bind.annotation.CrossOrigin;
31
import org.springframework.web.bind.annotation.RequestMapping;
S
Sebastien Deleuze 已提交
32 33 34
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.method.HandlerMethod;
35 36
import org.springframework.web.servlet.mvc.condition.AbstractRequestCondition;
import org.springframework.web.servlet.mvc.condition.CompositeRequestCondition;
37 38
import org.springframework.web.servlet.mvc.condition.ConsumesRequestCondition;
import org.springframework.web.servlet.mvc.condition.HeadersRequestCondition;
S
Sebastien Deleuze 已提交
39
import org.springframework.web.servlet.mvc.condition.NameValueExpression;
40 41 42
import org.springframework.web.servlet.mvc.condition.ParamsRequestCondition;
import org.springframework.web.servlet.mvc.condition.PatternsRequestCondition;
import org.springframework.web.servlet.mvc.condition.ProducesRequestCondition;
43
import org.springframework.web.servlet.mvc.condition.RequestCondition;
44
import org.springframework.web.servlet.mvc.condition.RequestMethodsRequestCondition;
45 46
import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
import org.springframework.web.servlet.mvc.method.RequestMappingInfoHandlerMapping;
47 48

/**
S
Stevo Slavic 已提交
49 50
 * Creates {@link RequestMappingInfo} instances from type and method-level
 * {@link RequestMapping @RequestMapping} annotations in
51
 * {@link Controller @Controller} classes.
52
 *
53 54
 * @author Arjen Poutsma
 * @author Rossen Stoyanchev
55
 * @since 3.1
56
 */
57 58
public class RequestMappingHandlerMapping extends RequestMappingInfoHandlerMapping
		implements EmbeddedValueResolverAware {
59

60 61
	private boolean useSuffixPatternMatch = true;

62 63
	private boolean useRegisteredSuffixPatternMatch = false;

64
	private boolean useTrailingSlashMatch = true;
65

66 67
	private ContentNegotiationManager contentNegotiationManager = new ContentNegotiationManager();

68
	private final List<String> fileExtensions = new ArrayList<String>();
69

70 71
	private StringValueResolver embeddedValueResolver;

72

73
	/**
74
	 * Whether to use suffix pattern match (".*") when matching patterns to
75
	 * requests. If enabled a method mapped to "/users" also matches to "/users.*".
S
Stevo Slavic 已提交
76
	 * <p>The default value is {@code true}.
77
	 * <p>Also see {@link #setUseRegisteredSuffixPatternMatch(boolean)} for
S
Sam Brannen 已提交
78
	 * more fine-grained control over specific suffixes to allow.
79 80 81 82
	 */
	public void setUseSuffixPatternMatch(boolean useSuffixPatternMatch) {
		this.useSuffixPatternMatch = useSuffixPatternMatch;
	}
83

84 85 86 87 88 89 90 91 92 93 94 95 96 97 98
	/**
	 * Whether to use suffix pattern match for registered file extensions only
	 * when matching patterns to requests.
	 * <p>If enabled, a controller method mapped to "/users" also matches to
	 * "/users.json" assuming ".json" is a file extension registered with the
	 * provided {@link #setContentNegotiationManager(ContentNegotiationManager)
	 * contentNegotiationManager}. This can be useful for allowing only specific
	 * URL extensions to be used as well as in cases where a "." in the URL path
	 * can lead to ambiguous interpretation of path variable content, (e.g. given
	 * "/users/{user}" and incoming URLs such as "/users/john.j.joe" and
	 * "/users/john.j.joe.json").
	 * <p>If enabled, this flag also enables
	 * {@link #setUseSuffixPatternMatch(boolean) useSuffixPatternMatch}. The
	 * default value is {@code false}.
	 */
99 100
	public void setUseRegisteredSuffixPatternMatch(boolean useRegisteredSuffixPatternMatch) {
		this.useRegisteredSuffixPatternMatch = useRegisteredSuffixPatternMatch;
J
Juergen Hoeller 已提交
101
		this.useSuffixPatternMatch = (useRegisteredSuffixPatternMatch || this.useSuffixPatternMatch);
102 103
	}

104 105 106 107 108 109 110 111
	/**
	 * Whether to match to URLs irrespective of the presence of a trailing slash.
	 * If enabled a method mapped to "/users" also matches to "/users/".
	 * <p>The default value is {@code true}.
	 */
	public void setUseTrailingSlashMatch(boolean useTrailingSlashMatch) {
		this.useTrailingSlashMatch = useTrailingSlashMatch;
	}
112

113 114 115 116 117
	/**
	 * Set the {@link ContentNegotiationManager} to use to determine requested media types.
	 * If not set, the default constructor is used.
	 */
	public void setContentNegotiationManager(ContentNegotiationManager contentNegotiationManager) {
J
Juergen Hoeller 已提交
118
		Assert.notNull(contentNegotiationManager, "ContentNegotiationManager must not be null");
119 120 121
		this.contentNegotiationManager = contentNegotiationManager;
	}

J
Juergen Hoeller 已提交
122 123 124 125 126 127 128 129 130 131 132 133 134 135 136
	@Override
	public void setEmbeddedValueResolver(StringValueResolver resolver) {
		this.embeddedValueResolver  = resolver;
	}

	@Override
	public void afterPropertiesSet() {
		if (this.useRegisteredSuffixPatternMatch) {
			this.fileExtensions.addAll(this.contentNegotiationManager.getAllFileExtensions());
		}
		super.afterPropertiesSet();
	}



137
	/**
138
	 * Whether to use suffix pattern matching.
139
	 */
140 141
	public boolean useSuffixPatternMatch() {
		return this.useSuffixPatternMatch;
142
	}
143 144 145 146 147

	/**
	 * Whether to use registered suffixes for pattern matching.
	 */
	public boolean useRegisteredSuffixPatternMatch() {
J
Juergen Hoeller 已提交
148
		return this.useRegisteredSuffixPatternMatch;
149 150
	}

151
	/**
J
Juergen Hoeller 已提交
152
	 * Whether to match to URLs irrespective of the presence of a trailing slash.
153 154 155 156
	 */
	public boolean useTrailingSlashMatch() {
		return this.useTrailingSlashMatch;
	}
157

158 159 160 161
	/**
	 * Return the configured {@link ContentNegotiationManager}.
	 */
	public ContentNegotiationManager getContentNegotiationManager() {
162 163 164 165
		return this.contentNegotiationManager;
	}

	/**
166
	 * Return the file extensions to use for suffix pattern matching.
167
	 */
168
	public List<String> getFileExtensions() {
169
		return this.fileExtensions;
170 171
	}

172

173
	/**
S
Stevo Slavic 已提交
174
	 * {@inheritDoc}
175
	 * Expects a handler to have a type-level @{@link Controller} annotation.
176 177
	 */
	@Override
178
	protected boolean isHandler(Class<?> beanType) {
179 180
		return ((AnnotationUtils.findAnnotation(beanType, Controller.class) != null) ||
				(AnnotationUtils.findAnnotation(beanType, RequestMapping.class) != null));
181 182 183
	}

	/**
184 185 186 187 188 189
	 * Uses method and type-level @{@link RequestMapping} annotations to create
	 * the RequestMappingInfo.
	 * @return the created RequestMappingInfo, or {@code null} if the method
	 * does not have a {@code @RequestMapping} annotation.
	 * @see #getCustomMethodCondition(Method)
	 * @see #getCustomTypeCondition(Class)
190 191
	 */
	@Override
192
	protected RequestMappingInfo getMappingForMethod(Method method, Class<?> handlerType) {
193 194 195 196 197 198 199 200 201
		RequestMappingInfo info = null;
		RequestMapping methodAnnotation = AnnotationUtils.findAnnotation(method, RequestMapping.class);
		if (methodAnnotation != null) {
			RequestCondition<?> methodCondition = getCustomMethodCondition(method);
			info = createRequestMappingInfo(methodAnnotation, methodCondition);
			RequestMapping typeAnnotation = AnnotationUtils.findAnnotation(handlerType, RequestMapping.class);
			if (typeAnnotation != null) {
				RequestCondition<?> typeCondition = getCustomTypeCondition(handlerType);
				info = createRequestMappingInfo(typeAnnotation, typeCondition).combine(info);
202
			}
203
		}
204 205 206 207
		return info;
	}

	/**
208
	 * Provide a custom type-level request condition.
S
Stevo Slavic 已提交
209
	 * The custom {@link RequestCondition} can be of any type so long as the
210
	 * same condition type is returned from all calls to this method in order
S
Stevo Slavic 已提交
211
	 * to ensure custom request conditions can be combined and compared.
212 213 214 215
	 * <p>Consider extending {@link AbstractRequestCondition} for custom
	 * condition types and using {@link CompositeRequestCondition} to provide
	 * multiple custom conditions.
	 * @param handlerType the handler type for which to create the condition
216 217
	 * @return the condition, or {@code null}
	 */
218
	protected RequestCondition<?> getCustomTypeCondition(Class<?> handlerType) {
219 220
		return null;
	}
221

222
	/**
223
	 * Provide a custom method-level request condition.
S
Stevo Slavic 已提交
224
	 * The custom {@link RequestCondition} can be of any type so long as the
225
	 * same condition type is returned from all calls to this method in order
S
Stevo Slavic 已提交
226
	 * to ensure custom request conditions can be combined and compared.
227 228 229 230
	 * <p>Consider extending {@link AbstractRequestCondition} for custom
	 * condition types and using {@link CompositeRequestCondition} to provide
	 * multiple custom conditions.
	 * @param method the handler method for which to create the condition
231 232
	 * @return the condition, or {@code null}
	 */
233
	protected RequestCondition<?> getCustomMethodCondition(Method method) {
234
		return null;
235 236
	}

237
	/**
238
	 * Created a RequestMappingInfo from a RequestMapping annotation.
239
	 */
J
Juergen Hoeller 已提交
240
	protected RequestMappingInfo createRequestMappingInfo(RequestMapping annotation, RequestCondition<?> customCondition) {
241
		String[] patterns = resolveEmbeddedValuesInPatterns(annotation.value());
242
		return new RequestMappingInfo(
243
				annotation.name(),
244
				new PatternsRequestCondition(patterns, getUrlPathHelper(), getPathMatcher(),
245
						this.useSuffixPatternMatch, this.useTrailingSlashMatch, this.fileExtensions),
246 247 248 249
				new RequestMethodsRequestCondition(annotation.method()),
				new ParamsRequestCondition(annotation.params()),
				new HeadersRequestCondition(annotation.headers()),
				new ConsumesRequestCondition(annotation.consumes(), annotation.headers()),
J
Juergen Hoeller 已提交
250
				new ProducesRequestCondition(annotation.produces(), annotation.headers(), this.contentNegotiationManager),
251
				customCondition);
R
Rossen Stoyanchev 已提交
252
	}
253

254 255 256 257 258 259 260 261 262 263
	/**
	 * Resolve placeholder values in the given array of patterns.
	 * @return a new array with updated patterns
	 */
	protected String[] resolveEmbeddedValuesInPatterns(String[] patterns) {
		if (this.embeddedValueResolver == null) {
			return patterns;
		}
		else {
			String[] resolvedPatterns = new String[patterns.length];
J
Juergen Hoeller 已提交
264
			for (int i = 0; i < patterns.length; i++) {
265 266 267 268 269 270
				resolvedPatterns[i] = this.embeddedValueResolver.resolveStringValue(patterns[i]);
			}
			return resolvedPatterns;
		}
	}

S
Sebastien Deleuze 已提交
271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327
	@Override
	protected CorsConfiguration initCorsConfiguration(Object handler, Method method, RequestMappingInfo mappingInfo) {
		HandlerMethod handlerMethod = createHandlerMethod(handler, method);

		CorsConfiguration config = new CorsConfiguration();

		CrossOrigin typeAnnotation = AnnotationUtils.findAnnotation(handlerMethod.getBeanType(), CrossOrigin.class);
		applyAnnotation(config, typeAnnotation);

		CrossOrigin methodAnnotation = AnnotationUtils.findAnnotation(method, CrossOrigin.class);
		applyAnnotation(config, methodAnnotation);

		if (CollectionUtils.isEmpty(config.getAllowedMethods())) {
			for (RequestMethod allowedMethod : mappingInfo.getMethodsCondition().getMethods()) {
				config.addAllowedMethod(allowedMethod.name());
			}
		}
		if (CollectionUtils.isEmpty(config.getAllowedHeaders())) {
			for (NameValueExpression<String> headerExpression : mappingInfo.getHeadersCondition().getExpressions()) {
				if (!headerExpression.isNegated()) {
					config.addAllowedHeader(headerExpression.getName());
				}
			}
		}
		return config;
	}

	private void applyAnnotation(CorsConfiguration config, CrossOrigin annotation) {
		if (annotation == null) {
			return;
		}
		for (String origin : annotation.origin()) {
			config.addAllowedOrigin(origin);
		}
		for (RequestMethod method : annotation.method()) {
			config.addAllowedMethod(method.name());
		}
		for (String header : annotation.allowedHeaders()) {
			config.addAllowedHeader(header);
		}
		for (String header : annotation.exposedHeaders()) {
			config.addExposedHeader(header);
		}
		if (annotation.allowCredentials().equalsIgnoreCase("true")) {
			config.setAllowCredentials(true);
		}
		else if (annotation.allowCredentials().equalsIgnoreCase("false")) {
			config.setAllowCredentials(false);
		}
		else if (!annotation.allowCredentials().isEmpty()) {
			throw new IllegalStateException("AllowCredentials value must be \"true\", \"false\" or \"\" (empty string), current value is " + annotation.allowCredentials());
		}
		if (annotation.maxAge() != -1 && config.getMaxAge() == null) {
			config.setMaxAge(annotation.maxAge());
		}
	}

328
}