2020import java .util .ArrayList ;
2121import java .util .Collections ;
2222import java .util .Comparator ;
23- import java .util .HashSet ;
2423import java .util .LinkedHashMap ;
2524import java .util .LinkedHashSet ;
2625import java .util .List ;
4645import org .springframework .web .servlet .HandlerMapping ;
4746import org .springframework .web .servlet .handler .AbstractHandlerMethodMapping ;
4847import org .springframework .web .servlet .mvc .condition .NameValueExpression ;
49- import org .springframework .web .servlet .mvc .condition .ParamsRequestCondition ;
5048import 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