View Javadoc
1   /*
2    * Copyright 2002-2014 the original author or authors.
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    *      http://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  
17  package org.springframework.web.servlet.mvc.method.annotation;
18  
19  import java.lang.reflect.Method;
20  import java.util.ArrayList;
21  import java.util.List;
22  
23  import org.springframework.context.EmbeddedValueResolverAware;
24  import org.springframework.core.annotation.AnnotationUtils;
25  import org.springframework.stereotype.Controller;
26  import org.springframework.util.Assert;
27  import org.springframework.util.StringValueResolver;
28  import org.springframework.web.accept.ContentNegotiationManager;
29  import org.springframework.web.bind.annotation.RequestMapping;
30  import org.springframework.web.servlet.mvc.condition.AbstractRequestCondition;
31  import org.springframework.web.servlet.mvc.condition.CompositeRequestCondition;
32  import org.springframework.web.servlet.mvc.condition.ConsumesRequestCondition;
33  import org.springframework.web.servlet.mvc.condition.HeadersRequestCondition;
34  import org.springframework.web.servlet.mvc.condition.ParamsRequestCondition;
35  import org.springframework.web.servlet.mvc.condition.PatternsRequestCondition;
36  import org.springframework.web.servlet.mvc.condition.ProducesRequestCondition;
37  import org.springframework.web.servlet.mvc.condition.RequestCondition;
38  import org.springframework.web.servlet.mvc.condition.RequestMethodsRequestCondition;
39  import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
40  import org.springframework.web.servlet.mvc.method.RequestMappingInfoHandlerMapping;
41  
42  /**
43   * Creates {@link RequestMappingInfo} instances from type and method-level
44   * {@link RequestMapping @RequestMapping} annotations in
45   * {@link Controller @Controller} classes.
46   *
47   * @author Arjen Poutsma
48   * @author Rossen Stoyanchev
49   * @since 3.1
50   */
51  public class RequestMappingHandlerMapping extends RequestMappingInfoHandlerMapping
52  		implements EmbeddedValueResolverAware {
53  
54  	private boolean useSuffixPatternMatch = true;
55  
56  	private boolean useRegisteredSuffixPatternMatch = false;
57  
58  	private boolean useTrailingSlashMatch = true;
59  
60  	private ContentNegotiationManager contentNegotiationManager = new ContentNegotiationManager();
61  
62  	private final List<String> fileExtensions = new ArrayList<String>();
63  
64  	private StringValueResolver embeddedValueResolver;
65  
66  
67  	/**
68  	 * Whether to use suffix pattern match (".*") when matching patterns to
69  	 * requests. If enabled a method mapped to "/users" also matches to "/users.*".
70  	 * <p>The default value is {@code true}.
71  	 * <p>Also see {@link #setUseRegisteredSuffixPatternMatch(boolean)} for
72  	 * more fine-grained control over specific suffixes to allow.
73  	 */
74  	public void setUseSuffixPatternMatch(boolean useSuffixPatternMatch) {
75  		this.useSuffixPatternMatch = useSuffixPatternMatch;
76  	}
77  
78  	/**
79  	 * Whether to use suffix pattern match for registered file extensions only
80  	 * when matching patterns to requests.
81  	 * <p>If enabled, a controller method mapped to "/users" also matches to
82  	 * "/users.json" assuming ".json" is a file extension registered with the
83  	 * provided {@link #setContentNegotiationManager(ContentNegotiationManager)
84  	 * contentNegotiationManager}. This can be useful for allowing only specific
85  	 * URL extensions to be used as well as in cases where a "." in the URL path
86  	 * can lead to ambiguous interpretation of path variable content, (e.g. given
87  	 * "/users/{user}" and incoming URLs such as "/users/john.j.joe" and
88  	 * "/users/john.j.joe.json").
89  	 * <p>If enabled, this flag also enables
90  	 * {@link #setUseSuffixPatternMatch(boolean) useSuffixPatternMatch}. The
91  	 * default value is {@code false}.
92  	 */
93  	public void setUseRegisteredSuffixPatternMatch(boolean useRegisteredSuffixPatternMatch) {
94  		this.useRegisteredSuffixPatternMatch = useRegisteredSuffixPatternMatch;
95  		this.useSuffixPatternMatch = (useRegisteredSuffixPatternMatch || this.useSuffixPatternMatch);
96  	}
97  
98  	/**
99  	 * Whether to match to URLs irrespective of the presence of a trailing slash.
100 	 * If enabled a method mapped to "/users" also matches to "/users/".
101 	 * <p>The default value is {@code true}.
102 	 */
103 	public void setUseTrailingSlashMatch(boolean useTrailingSlashMatch) {
104 		this.useTrailingSlashMatch = useTrailingSlashMatch;
105 	}
106 
107 	/**
108 	 * Set the {@link ContentNegotiationManager} to use to determine requested media types.
109 	 * If not set, the default constructor is used.
110 	 */
111 	public void setContentNegotiationManager(ContentNegotiationManager contentNegotiationManager) {
112 		Assert.notNull(contentNegotiationManager, "ContentNegotiationManager must not be null");
113 		this.contentNegotiationManager = contentNegotiationManager;
114 	}
115 
116 	@Override
117 	public void setEmbeddedValueResolver(StringValueResolver resolver) {
118 		this.embeddedValueResolver  = resolver;
119 	}
120 
121 	@Override
122 	public void afterPropertiesSet() {
123 		if (this.useRegisteredSuffixPatternMatch) {
124 			this.fileExtensions.addAll(this.contentNegotiationManager.getAllFileExtensions());
125 		}
126 		super.afterPropertiesSet();
127 	}
128 
129 
130 
131 	/**
132 	 * Whether to use suffix pattern matching.
133 	 */
134 	public boolean useSuffixPatternMatch() {
135 		return this.useSuffixPatternMatch;
136 	}
137 
138 	/**
139 	 * Whether to use registered suffixes for pattern matching.
140 	 */
141 	public boolean useRegisteredSuffixPatternMatch() {
142 		return this.useRegisteredSuffixPatternMatch;
143 	}
144 
145 	/**
146 	 * Whether to match to URLs irrespective of the presence of a trailing slash.
147 	 */
148 	public boolean useTrailingSlashMatch() {
149 		return this.useTrailingSlashMatch;
150 	}
151 
152 	/**
153 	 * Return the configured {@link ContentNegotiationManager}.
154 	 */
155 	public ContentNegotiationManager getContentNegotiationManager() {
156 		return this.contentNegotiationManager;
157 	}
158 
159 	/**
160 	 * Return the file extensions to use for suffix pattern matching.
161 	 */
162 	public List<String> getFileExtensions() {
163 		return this.fileExtensions;
164 	}
165 
166 
167 	/**
168 	 * {@inheritDoc}
169 	 * Expects a handler to have a type-level @{@link Controller} annotation.
170 	 */
171 	@Override
172 	protected boolean isHandler(Class<?> beanType) {
173 		return ((AnnotationUtils.findAnnotation(beanType, Controller.class) != null) ||
174 				(AnnotationUtils.findAnnotation(beanType, RequestMapping.class) != null));
175 	}
176 
177 	/**
178 	 * Uses method and type-level @{@link RequestMapping} annotations to create
179 	 * the RequestMappingInfo.
180 	 * @return the created RequestMappingInfo, or {@code null} if the method
181 	 * does not have a {@code @RequestMapping} annotation.
182 	 * @see #getCustomMethodCondition(Method)
183 	 * @see #getCustomTypeCondition(Class)
184 	 */
185 	@Override
186 	protected RequestMappingInfo getMappingForMethod(Method method, Class<?> handlerType) {
187 		RequestMappingInfo info = null;
188 		RequestMapping methodAnnotation = AnnotationUtils.findAnnotation(method, RequestMapping.class);
189 		if (methodAnnotation != null) {
190 			RequestCondition<?> methodCondition = getCustomMethodCondition(method);
191 			info = createRequestMappingInfo(methodAnnotation, methodCondition);
192 			RequestMapping typeAnnotation = AnnotationUtils.findAnnotation(handlerType, RequestMapping.class);
193 			if (typeAnnotation != null) {
194 				RequestCondition<?> typeCondition = getCustomTypeCondition(handlerType);
195 				info = createRequestMappingInfo(typeAnnotation, typeCondition).combine(info);
196 			}
197 		}
198 		return info;
199 	}
200 
201 	/**
202 	 * Provide a custom type-level request condition.
203 	 * The custom {@link RequestCondition} can be of any type so long as the
204 	 * same condition type is returned from all calls to this method in order
205 	 * to ensure custom request conditions can be combined and compared.
206 	 * <p>Consider extending {@link AbstractRequestCondition} for custom
207 	 * condition types and using {@link CompositeRequestCondition} to provide
208 	 * multiple custom conditions.
209 	 * @param handlerType the handler type for which to create the condition
210 	 * @return the condition, or {@code null}
211 	 */
212 	protected RequestCondition<?> getCustomTypeCondition(Class<?> handlerType) {
213 		return null;
214 	}
215 
216 	/**
217 	 * Provide a custom method-level request condition.
218 	 * The custom {@link RequestCondition} can be of any type so long as the
219 	 * same condition type is returned from all calls to this method in order
220 	 * to ensure custom request conditions can be combined and compared.
221 	 * <p>Consider extending {@link AbstractRequestCondition} for custom
222 	 * condition types and using {@link CompositeRequestCondition} to provide
223 	 * multiple custom conditions.
224 	 * @param method the handler method for which to create the condition
225 	 * @return the condition, or {@code null}
226 	 */
227 	protected RequestCondition<?> getCustomMethodCondition(Method method) {
228 		return null;
229 	}
230 
231 	/**
232 	 * Created a RequestMappingInfo from a RequestMapping annotation.
233 	 */
234 	protected RequestMappingInfo createRequestMappingInfo(RequestMapping annotation, RequestCondition<?> customCondition) {
235 		String[] patterns = resolveEmbeddedValuesInPatterns(annotation.value());
236 		return new RequestMappingInfo(
237 				annotation.name(),
238 				new PatternsRequestCondition(patterns, getUrlPathHelper(), getPathMatcher(),
239 						this.useSuffixPatternMatch, this.useTrailingSlashMatch, this.fileExtensions),
240 				new RequestMethodsRequestCondition(annotation.method()),
241 				new ParamsRequestCondition(annotation.params()),
242 				new HeadersRequestCondition(annotation.headers()),
243 				new ConsumesRequestCondition(annotation.consumes(), annotation.headers()),
244 				new ProducesRequestCondition(annotation.produces(), annotation.headers(), this.contentNegotiationManager),
245 				customCondition);
246 	}
247 
248 	/**
249 	 * Resolve placeholder values in the given array of patterns.
250 	 * @return a new array with updated patterns
251 	 */
252 	protected String[] resolveEmbeddedValuesInPatterns(String[] patterns) {
253 		if (this.embeddedValueResolver == null) {
254 			return patterns;
255 		}
256 		else {
257 			String[] resolvedPatterns = new String[patterns.length];
258 			for (int i = 0; i < patterns.length; i++) {
259 				resolvedPatterns[i] = this.embeddedValueResolver.resolveStringValue(patterns[i]);
260 			}
261 			return resolvedPatterns;
262 		}
263 	}
264 
265 }