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;
18  
19  import java.util.ArrayList;
20  import java.util.Collections;
21  import java.util.Comparator;
22  import java.util.HashSet;
23  import java.util.LinkedHashMap;
24  import java.util.LinkedHashSet;
25  import java.util.Map;
26  import java.util.Map.Entry;
27  import java.util.Set;
28  import javax.servlet.ServletException;
29  import javax.servlet.http.HttpServletRequest;
30  
31  import org.springframework.http.InvalidMediaTypeException;
32  import org.springframework.http.MediaType;
33  import org.springframework.util.CollectionUtils;
34  import org.springframework.util.MultiValueMap;
35  import org.springframework.util.StringUtils;
36  import org.springframework.web.HttpMediaTypeNotAcceptableException;
37  import org.springframework.web.HttpMediaTypeNotSupportedException;
38  import org.springframework.web.HttpRequestMethodNotSupportedException;
39  import org.springframework.web.bind.UnsatisfiedServletRequestParameterException;
40  import org.springframework.web.bind.annotation.RequestMethod;
41  import org.springframework.web.method.HandlerMethod;
42  import org.springframework.web.servlet.HandlerMapping;
43  import org.springframework.web.servlet.handler.AbstractHandlerMethodMapping;
44  import org.springframework.web.servlet.mvc.condition.NameValueExpression;
45  import org.springframework.web.servlet.mvc.condition.ParamsRequestCondition;
46  import org.springframework.web.util.WebUtils;
47  
48  /**
49   * Abstract base class for classes for which {@link RequestMappingInfo} defines
50   * the mapping between a request and a handler method.
51   *
52   * @author Arjen Poutsma
53   * @author Rossen Stoyanchev
54   * @since 3.1
55   */
56  public abstract class RequestMappingInfoHandlerMapping extends AbstractHandlerMethodMapping<RequestMappingInfo> {
57  
58  	protected RequestMappingInfoHandlerMapping() {
59  		setHandlerMethodMappingNamingStrategy(new RequestMappingInfoHandlerMethodMappingNamingStrategy());
60  	}
61  
62  
63  	/**
64  	 * Get the URL path patterns associated with this {@link RequestMappingInfo}.
65  	 */
66  	@Override
67  	protected Set<String> getMappingPathPatterns(RequestMappingInfo info) {
68  		return info.getPatternsCondition().getPatterns();
69  	}
70  
71  	/**
72  	 * Check if the given RequestMappingInfo matches the current request and
73  	 * return a (potentially new) instance with conditions that match the
74  	 * current request -- for example with a subset of URL patterns.
75  	 * @return an info in case of a match; or {@code null} otherwise.
76  	 */
77  	@Override
78  	protected RequestMappingInfo getMatchingMapping(RequestMappingInfo info, HttpServletRequest request) {
79  		return info.getMatchingCondition(request);
80  	}
81  
82  	/**
83  	 * Provide a Comparator to sort RequestMappingInfos matched to a request.
84  	 */
85  	@Override
86  	protected Comparator<RequestMappingInfo> getMappingComparator(final HttpServletRequest request) {
87  		return new Comparator<RequestMappingInfo>() {
88  			@Override
89  			public int compare(RequestMappingInfo info1, RequestMappingInfo info2) {
90  				return info1.compareTo(info2, request);
91  			}
92  		};
93  	}
94  
95  	/**
96  	 * Expose URI template variables, matrix variables, and producible media types in the request.
97  	 * @see HandlerMapping#URI_TEMPLATE_VARIABLES_ATTRIBUTE
98  	 * @see HandlerMapping#MATRIX_VARIABLES_ATTRIBUTE
99  	 * @see HandlerMapping#PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE
100 	 */
101 	@Override
102 	protected void handleMatch(RequestMappingInfo info, String lookupPath, HttpServletRequest request) {
103 		super.handleMatch(info, lookupPath, request);
104 
105 		String bestPattern;
106 		Map<String, String> uriVariables;
107 		Map<String, String> decodedUriVariables;
108 
109 		Set<String> patterns = info.getPatternsCondition().getPatterns();
110 		if (patterns.isEmpty()) {
111 			bestPattern = lookupPath;
112 			uriVariables = Collections.emptyMap();
113 			decodedUriVariables = Collections.emptyMap();
114 		}
115 		else {
116 			bestPattern = patterns.iterator().next();
117 			uriVariables = getPathMatcher().extractUriTemplateVariables(bestPattern, lookupPath);
118 			decodedUriVariables = getUrlPathHelper().decodePathVariables(request, uriVariables);
119 		}
120 
121 		request.setAttribute(BEST_MATCHING_PATTERN_ATTRIBUTE, bestPattern);
122 		request.setAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE, decodedUriVariables);
123 
124 		if (isMatrixVariableContentAvailable()) {
125 			Map<String, MultiValueMap<String, String>> matrixVars = extractMatrixVariables(request, uriVariables);
126 			request.setAttribute(HandlerMapping.MATRIX_VARIABLES_ATTRIBUTE, matrixVars);
127 		}
128 
129 		if (!info.getProducesCondition().getProducibleMediaTypes().isEmpty()) {
130 			Set<MediaType> mediaTypes = info.getProducesCondition().getProducibleMediaTypes();
131 			request.setAttribute(PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE, mediaTypes);
132 		}
133 	}
134 
135 	private boolean isMatrixVariableContentAvailable() {
136 		return !getUrlPathHelper().shouldRemoveSemicolonContent();
137 	}
138 
139 	private Map<String, MultiValueMap<String, String>> extractMatrixVariables(
140 			HttpServletRequest request, Map<String, String> uriVariables) {
141 
142 		Map<String, MultiValueMap<String, String>> result = new LinkedHashMap<String, MultiValueMap<String, String>>();
143 		for (Entry<String, String> uriVar : uriVariables.entrySet()) {
144 			String uriVarValue = uriVar.getValue();
145 
146 			int equalsIndex = uriVarValue.indexOf('=');
147 			if (equalsIndex == -1) {
148 				continue;
149 			}
150 
151 			String matrixVariables;
152 
153 			int semicolonIndex = uriVarValue.indexOf(';');
154 			if ((semicolonIndex == -1) || (semicolonIndex == 0) || (equalsIndex < semicolonIndex)) {
155 				matrixVariables = uriVarValue;
156 			}
157 			else {
158 				matrixVariables = uriVarValue.substring(semicolonIndex + 1);
159 				uriVariables.put(uriVar.getKey(), uriVarValue.substring(0, semicolonIndex));
160 			}
161 
162 			MultiValueMap<String, String> vars = WebUtils.parseMatrixVariables(matrixVariables);
163 			result.put(uriVar.getKey(), getUrlPathHelper().decodeMatrixVariables(request, vars));
164 		}
165 		return result;
166 	}
167 
168 	/**
169 	 * Iterate all RequestMappingInfos once again, look if any match by URL at
170 	 * least and raise exceptions accordingly.
171 	 * @throws HttpRequestMethodNotSupportedException if there are matches by URL
172 	 * but not by HTTP method
173 	 * @throws HttpMediaTypeNotAcceptableException if there are matches by URL
174 	 * but not by consumable/producible media types
175 	 */
176 	@Override
177 	protected HandlerMethod handleNoMatch(Set<RequestMappingInfo> requestMappingInfos,
178 			String lookupPath, HttpServletRequest request) throws ServletException {
179 
180 		Set<String> allowedMethods = new LinkedHashSet<String>(4);
181 
182 		Set<RequestMappingInfo> patternMatches = new HashSet<RequestMappingInfo>();
183 		Set<RequestMappingInfo> patternAndMethodMatches = new HashSet<RequestMappingInfo>();
184 
185 		for (RequestMappingInfo info : requestMappingInfos) {
186 			if (info.getPatternsCondition().getMatchingCondition(request) != null) {
187 				patternMatches.add(info);
188 				if (info.getMethodsCondition().getMatchingCondition(request) != null) {
189 					patternAndMethodMatches.add(info);
190 				}
191 				else {
192 					for (RequestMethod method : info.getMethodsCondition().getMethods()) {
193 						allowedMethods.add(method.name());
194 					}
195 				}
196 			}
197 		}
198 
199 		if (patternMatches.isEmpty()) {
200 			return null;
201 		}
202 		else if (patternAndMethodMatches.isEmpty() && !allowedMethods.isEmpty()) {
203 			throw new HttpRequestMethodNotSupportedException(request.getMethod(), allowedMethods);
204 		}
205 
206 		Set<MediaType> consumableMediaTypes;
207 		Set<MediaType> producibleMediaTypes;
208 		Set<String> paramConditions;
209 
210 		if (patternAndMethodMatches.isEmpty()) {
211 			consumableMediaTypes = getConsumableMediaTypes(request, patternMatches);
212 			producibleMediaTypes = getProducibleMediaTypes(request, patternMatches);
213 			paramConditions = getRequestParams(request, patternMatches);
214 		}
215 		else {
216 			consumableMediaTypes = getConsumableMediaTypes(request, patternAndMethodMatches);
217 			producibleMediaTypes = getProducibleMediaTypes(request, patternAndMethodMatches);
218 			paramConditions = getRequestParams(request, patternAndMethodMatches);
219 		}
220 
221 		if (!consumableMediaTypes.isEmpty()) {
222 			MediaType contentType = null;
223 			if (StringUtils.hasLength(request.getContentType())) {
224 				try {
225 					contentType = MediaType.parseMediaType(request.getContentType());
226 				}
227 				catch (InvalidMediaTypeException ex) {
228 					throw new HttpMediaTypeNotSupportedException(ex.getMessage());
229 				}
230 			}
231 			throw new HttpMediaTypeNotSupportedException(contentType, new ArrayList<MediaType>(consumableMediaTypes));
232 		}
233 		else if (!producibleMediaTypes.isEmpty()) {
234 			throw new HttpMediaTypeNotAcceptableException(new ArrayList<MediaType>(producibleMediaTypes));
235 		}
236 		else if (!CollectionUtils.isEmpty(paramConditions)) {
237 			String[] params = paramConditions.toArray(new String[paramConditions.size()]);
238 			throw new UnsatisfiedServletRequestParameterException(params, request.getParameterMap());
239 		}
240 		else {
241 			return null;
242 		}
243 	}
244 
245 	private Set<MediaType> getConsumableMediaTypes(HttpServletRequest request, Set<RequestMappingInfo> partialMatches) {
246 		Set<MediaType> result = new HashSet<MediaType>();
247 		for (RequestMappingInfo partialMatch : partialMatches) {
248 			if (partialMatch.getConsumesCondition().getMatchingCondition(request) == null) {
249 				result.addAll(partialMatch.getConsumesCondition().getConsumableMediaTypes());
250 			}
251 		}
252 		return result;
253 	}
254 
255 	private Set<MediaType> getProducibleMediaTypes(HttpServletRequest request, Set<RequestMappingInfo> partialMatches) {
256 		Set<MediaType> result = new HashSet<MediaType>();
257 		for (RequestMappingInfo partialMatch : partialMatches) {
258 			if (partialMatch.getProducesCondition().getMatchingCondition(request) == null) {
259 				result.addAll(partialMatch.getProducesCondition().getProducibleMediaTypes());
260 			}
261 		}
262 		return result;
263 	}
264 
265 	private Set<String> getRequestParams(HttpServletRequest request, Set<RequestMappingInfo> partialMatches) {
266 		for (RequestMappingInfo partialMatch : partialMatches) {
267 			ParamsRequestCondition condition = partialMatch.getParamsCondition();
268 			if (!CollectionUtils.isEmpty(condition.getExpressions()) && (condition.getMatchingCondition(request) == null)) {
269 				Set<String> expressions = new HashSet<String>();
270 				for (NameValueExpression<String> expr : condition.getExpressions()) {
271 					expressions.add(expr.toString());
272 				}
273 				return expressions;
274 			}
275 		}
276 		return null;
277 	}
278 
279 }