Skip to content

Commit 89396ff

Browse files
committed
Refactor handleNoMatch for @RequestMapping
Originally handleNoMatch looked for partial matches based on URL pattern, HTTP method, consumes, produces, and params in that order but without narrowing down the set of partial matches resulting in potentially inaccruate response status codes Commit 473de0 added an improvement to narrow the set with partial matches for URL pattern and HTTP method matches. This commit overhauls handleNoMatch so that the narrowing down of matches happens at each stage resulting in more accurate error reporting for request mappings with fine-grained conditions. Issue: SPR-14397
1 parent eba8730 commit 89396ff

File tree

1 file changed

+207
-81
lines changed

1 file changed

+207
-81
lines changed

spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/RequestMappingInfoHandlerMapping.java

Lines changed: 207 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@
2020
import java.util.ArrayList;
2121
import java.util.Collections;
2222
import java.util.Comparator;
23-
import java.util.HashSet;
2423
import java.util.LinkedHashMap;
2524
import java.util.LinkedHashSet;
2625
import java.util.List;
@@ -46,7 +45,6 @@
4645
import org.springframework.web.servlet.HandlerMapping;
4746
import org.springframework.web.servlet.handler.AbstractHandlerMethodMapping;
4847
import org.springframework.web.servlet.mvc.condition.NameValueExpression;
49-
import org.springframework.web.servlet.mvc.condition.ParamsRequestCondition;
5048
import org.springframework.web.util.WebUtils;
5149

5250
/**
@@ -183,65 +181,35 @@ private Map<String, MultiValueMap<String, String>> extractMatrixVariables(
183181
}
184182

185183
/**
186-
* Iterate all RequestMappingInfos once again, look if any match by URL at
187-
* least and raise exceptions accordingly.
184+
* Iterate all RequestMappingInfo's once again, look if any match by URL at
185+
* least and raise exceptions according to what doesn't match.
186+
*
188187
* @throws HttpRequestMethodNotSupportedException if there are matches by URL
189188
* but not by HTTP method
190189
* @throws HttpMediaTypeNotAcceptableException if there are matches by URL
191190
* but not by consumable/producible media types
192191
*/
193192
@Override
194-
protected HandlerMethod handleNoMatch(Set<RequestMappingInfo> requestMappingInfos,
195-
String lookupPath, HttpServletRequest request) throws ServletException {
193+
protected HandlerMethod handleNoMatch(Set<RequestMappingInfo> infos, String lookupPath,
194+
HttpServletRequest request) throws ServletException {
196195

197-
Set<String> allowedMethods = new LinkedHashSet<String>(4);
196+
PartialMatchHelper helper = new PartialMatchHelper(infos, request);
198197

199-
Set<RequestMappingInfo> patternMatches = new HashSet<RequestMappingInfo>();
200-
Set<RequestMappingInfo> patternAndMethodMatches = new HashSet<RequestMappingInfo>();
201-
202-
for (RequestMappingInfo info : requestMappingInfos) {
203-
if (info.getPatternsCondition().getMatchingCondition(request) != null) {
204-
patternMatches.add(info);
205-
if (info.getMethodsCondition().getMatchingCondition(request) != null) {
206-
patternAndMethodMatches.add(info);
207-
}
208-
else {
209-
for (RequestMethod method : info.getMethodsCondition().getMethods()) {
210-
allowedMethods.add(method.name());
211-
}
212-
}
213-
}
214-
}
215-
216-
if (patternMatches.isEmpty()) {
198+
if (helper.isEmpty()) {
217199
return null;
218200
}
219-
else if (patternAndMethodMatches.isEmpty()) {
201+
202+
if (helper.hasMethodsMismatch()) {
203+
Set<String> methods = helper.getAllowedMethods();
220204
if (HttpMethod.OPTIONS.matches(request.getMethod())) {
221-
HttpOptionsHandler handler = new HttpOptionsHandler(allowedMethods);
205+
HttpOptionsHandler handler = new HttpOptionsHandler(methods);
222206
return new HandlerMethod(handler, HTTP_OPTIONS_HANDLE_METHOD);
223207
}
224-
else if (!allowedMethods.isEmpty()) {
225-
throw new HttpRequestMethodNotSupportedException(request.getMethod(), allowedMethods);
226-
}
208+
throw new HttpRequestMethodNotSupportedException(request.getMethod(), methods);
227209
}
228210

229-
Set<MediaType> consumableMediaTypes;
230-
Set<MediaType> producibleMediaTypes;
231-
List<String[]> paramConditions;
232-
233-
if (patternAndMethodMatches.isEmpty()) {
234-
consumableMediaTypes = getConsumableMediaTypes(request, patternMatches);
235-
producibleMediaTypes = getProducibleMediaTypes(request, patternMatches);
236-
paramConditions = getRequestParams(request, patternMatches);
237-
}
238-
else {
239-
consumableMediaTypes = getConsumableMediaTypes(request, patternAndMethodMatches);
240-
producibleMediaTypes = getProducibleMediaTypes(request, patternAndMethodMatches);
241-
paramConditions = getRequestParams(request, patternAndMethodMatches);
242-
}
243-
244-
if (!consumableMediaTypes.isEmpty()) {
211+
if (helper.hasConsumesMismatch()) {
212+
Set<MediaType> mediaTypes = helper.getConsumableMediaTypes();
245213
MediaType contentType = null;
246214
if (StringUtils.hasLength(request.getContentType())) {
247215
try {
@@ -251,56 +219,214 @@ else if (!allowedMethods.isEmpty()) {
251219
throw new HttpMediaTypeNotSupportedException(ex.getMessage());
252220
}
253221
}
254-
throw new HttpMediaTypeNotSupportedException(contentType, new ArrayList<MediaType>(consumableMediaTypes));
255-
}
256-
else if (!producibleMediaTypes.isEmpty()) {
257-
throw new HttpMediaTypeNotAcceptableException(new ArrayList<MediaType>(producibleMediaTypes));
222+
throw new HttpMediaTypeNotSupportedException(contentType, new ArrayList<MediaType>(mediaTypes));
258223
}
259-
else if (!CollectionUtils.isEmpty(paramConditions)) {
260-
throw new UnsatisfiedServletRequestParameterException(paramConditions, request.getParameterMap());
224+
225+
if (helper.hasProducesMismatch()) {
226+
Set<MediaType> mediaTypes = helper.getProducibleMediaTypes();
227+
throw new HttpMediaTypeNotAcceptableException(new ArrayList<MediaType>(mediaTypes));
261228
}
262-
else {
263-
return null;
229+
230+
if (helper.hasParamsMismatch()) {
231+
List<String[]> conditions = helper.getParamConditions();
232+
throw new UnsatisfiedServletRequestParameterException(conditions, request.getParameterMap());
264233
}
234+
235+
return null;
265236
}
266237

267-
private Set<MediaType> getConsumableMediaTypes(HttpServletRequest request, Set<RequestMappingInfo> partialMatches) {
268-
Set<MediaType> result = new HashSet<MediaType>();
269-
for (RequestMappingInfo partialMatch : partialMatches) {
270-
if (partialMatch.getConsumesCondition().getMatchingCondition(request) == null) {
271-
result.addAll(partialMatch.getConsumesCondition().getConsumableMediaTypes());
238+
239+
/**
240+
* Aggregate all partial matches and expose methods checking across them.
241+
*/
242+
private static class PartialMatchHelper {
243+
244+
private final List<PartialMatch> partialMatches = new ArrayList<PartialMatch>();
245+
246+
247+
public PartialMatchHelper(Set<RequestMappingInfo> infos, HttpServletRequest request) {
248+
for (RequestMappingInfo info : infos) {
249+
if (info.getPatternsCondition().getMatchingCondition(request) != null) {
250+
this.partialMatches.add(new PartialMatch(info, request));
251+
}
272252
}
273253
}
274-
return result;
275-
}
276254

277-
private Set<MediaType> getProducibleMediaTypes(HttpServletRequest request, Set<RequestMappingInfo> partialMatches) {
278-
Set<MediaType> result = new HashSet<MediaType>();
279-
for (RequestMappingInfo partialMatch : partialMatches) {
280-
if (partialMatch.getProducesCondition().getMatchingCondition(request) == null) {
281-
result.addAll(partialMatch.getProducesCondition().getProducibleMediaTypes());
255+
256+
/**
257+
* Whether there any partial matches.
258+
*/
259+
public boolean isEmpty() {
260+
return this.partialMatches.isEmpty();
261+
}
262+
263+
/**
264+
* Any partial matches for "methods"?
265+
*/
266+
public boolean hasMethodsMismatch() {
267+
for (PartialMatch match : this.partialMatches) {
268+
if (match.hasMethodsMatch()) {
269+
return false;
270+
}
282271
}
272+
return true;
283273
}
284-
return result;
285-
}
286274

287-
private List<String[]> getRequestParams(HttpServletRequest request, Set<RequestMappingInfo> partialMatches) {
288-
List<String[]> result = new ArrayList<String[]>();
289-
for (RequestMappingInfo partialMatch : partialMatches) {
290-
ParamsRequestCondition condition = partialMatch.getParamsCondition();
291-
Set<NameValueExpression<String>> expressions = condition.getExpressions();
292-
if (!CollectionUtils.isEmpty(expressions) && condition.getMatchingCondition(request) == null) {
293-
int i = 0;
294-
String[] array = new String[expressions.size()];
295-
for (NameValueExpression<String> expression : expressions) {
296-
array[i++] = expression.toString();
275+
/**
276+
* Any partial matches for "methods" and "consumes"?
277+
*/
278+
public boolean hasConsumesMismatch() {
279+
for (PartialMatch match : this.partialMatches) {
280+
if (match.hasConsumesMatch()) {
281+
return false;
297282
}
298-
result.add(array);
299283
}
284+
return true;
285+
}
286+
287+
/**
288+
* Any partial matches for "methods", "consumes", and "produces"?
289+
*/
290+
public boolean hasProducesMismatch() {
291+
for (PartialMatch match : this.partialMatches) {
292+
if (match.hasProducesMatch()) {
293+
return false;
294+
}
295+
}
296+
return true;
300297
}
301-
return result;
302-
}
303298

299+
/**
300+
* Any partial matches for "methods", "consumes", "produces", and "params"?
301+
*/
302+
public boolean hasParamsMismatch() {
303+
for (PartialMatch match : this.partialMatches) {
304+
if (match.hasParamsMatch()) {
305+
return false;
306+
}
307+
}
308+
return true;
309+
}
310+
311+
/**
312+
* Return declared HTTP methods.
313+
*/
314+
public Set<String> getAllowedMethods() {
315+
Set<String> result = new LinkedHashSet<String>();
316+
for (PartialMatch match : this.partialMatches) {
317+
for (RequestMethod method : match.getInfo().getMethodsCondition().getMethods()) {
318+
result.add(method.name());
319+
}
320+
}
321+
return result;
322+
}
323+
324+
/**
325+
* Return declared "consumable" types but only among those that also
326+
* match the "methods" condition.
327+
*/
328+
public Set<MediaType> getConsumableMediaTypes() {
329+
Set<MediaType> result = new LinkedHashSet<MediaType>();
330+
for (PartialMatch match : this.partialMatches) {
331+
if (match.hasMethodsMatch()) {
332+
result.addAll(match.getInfo().getConsumesCondition().getConsumableMediaTypes());
333+
}
334+
}
335+
return result;
336+
}
337+
338+
/**
339+
* Return declared "producible" types but only among those that also
340+
* match the "methods" and "consumes" conditions.
341+
*/
342+
public Set<MediaType> getProducibleMediaTypes() {
343+
Set<MediaType> result = new LinkedHashSet<MediaType>();
344+
for (PartialMatch match : this.partialMatches) {
345+
if (match.hasConsumesMatch()) {
346+
result.addAll(match.getInfo().getProducesCondition().getProducibleMediaTypes());
347+
}
348+
}
349+
return result;
350+
}
351+
352+
/**
353+
* Return declared "params" conditions but only among those that also
354+
* match the "methods", "consumes", and "params" conditions.
355+
*/
356+
public List<String[]> getParamConditions() {
357+
List<String[]> result = new ArrayList<String[]>();
358+
for (PartialMatch match : this.partialMatches) {
359+
if (match.hasProducesMatch()) {
360+
Set<NameValueExpression<String>> set = match.getInfo().getParamsCondition().getExpressions();
361+
if (!CollectionUtils.isEmpty(set)) {
362+
int i = 0;
363+
String[] array = new String[set.size()];
364+
for (NameValueExpression<String> expression : set) {
365+
array[i++] = expression.toString();
366+
}
367+
result.add(array);
368+
}
369+
}
370+
}
371+
return result;
372+
}
373+
374+
375+
/**
376+
* Container for a RequestMappingInfo that matches the URL path at least.
377+
*/
378+
private static class PartialMatch {
379+
380+
private final RequestMappingInfo info;
381+
382+
private final boolean methodsMatch;
383+
384+
private final boolean consumesMatch;
385+
386+
private final boolean producesMatch;
387+
388+
private final boolean paramsMatch;
389+
390+
391+
/**
392+
* @param info RequestMappingInfo that matches the URL path.
393+
* @param request the current request
394+
*/
395+
public PartialMatch(RequestMappingInfo info, HttpServletRequest request) {
396+
this.info = info;
397+
this.methodsMatch = info.getMethodsCondition().getMatchingCondition(request) != null;
398+
this.consumesMatch = info.getConsumesCondition().getMatchingCondition(request) != null;
399+
this.producesMatch = info.getProducesCondition().getMatchingCondition(request) != null;
400+
this.paramsMatch = info.getParamsCondition().getMatchingCondition(request) != null;
401+
}
402+
403+
404+
public RequestMappingInfo getInfo() {
405+
return this.info;
406+
}
407+
408+
public boolean hasMethodsMatch() {
409+
return this.methodsMatch;
410+
}
411+
412+
public boolean hasConsumesMatch() {
413+
return hasMethodsMatch() && this.consumesMatch;
414+
}
415+
416+
public boolean hasProducesMatch() {
417+
return hasConsumesMatch() && this.producesMatch;
418+
}
419+
420+
public boolean hasParamsMatch() {
421+
return hasProducesMatch() && this.paramsMatch;
422+
}
423+
424+
@Override
425+
public String toString() {
426+
return this.info.toString();
427+
}
428+
}
429+
}
304430

305431
/**
306432
* Default handler for HTTP OPTIONS.

0 commit comments

Comments
 (0)