/* * Copyright 2002-2017 the original author or authors. * * 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; import static org.hamcrest.Matchers.*; import static org.junit.Assert.*; import java.lang.reflect.Method; import java.util.Arrays; import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import javax.servlet.http.HttpServletRequest; import org.junit.Before; import org.junit.Test; import org.springframework.core.annotation.AnnotationUtils; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.mock.web.test.MockHttpServletRequest; import org.springframework.stereotype.Controller; import org.springframework.util.MultiValueMap; import org.springframework.web.HttpMediaTypeNotAcceptableException; import org.springframework.web.HttpMediaTypeNotSupportedException; import org.springframework.web.HttpRequestMethodNotSupportedException; import org.springframework.web.bind.UnsatisfiedServletRequestParameterException; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.context.request.ServletWebRequest; import org.springframework.web.context.support.StaticWebApplicationContext; import org.springframework.web.method.HandlerMethod; import org.springframework.web.method.support.InvocableHandlerMethod; import org.springframework.web.method.support.ModelAndViewContainer; import org.springframework.web.servlet.HandlerExecutionChain; import org.springframework.web.servlet.HandlerInterceptor; import org.springframework.web.servlet.HandlerMapping; import org.springframework.web.servlet.handler.HandlerInterceptorAdapter; import org.springframework.web.servlet.handler.MappedInterceptor; import org.springframework.web.servlet.mvc.condition.ConsumesRequestCondition; import org.springframework.web.servlet.mvc.condition.HeadersRequestCondition; import org.springframework.web.servlet.mvc.condition.ParamsRequestCondition; import org.springframework.web.servlet.mvc.condition.PatternsRequestCondition; import org.springframework.web.servlet.mvc.condition.ProducesRequestCondition; import org.springframework.web.servlet.mvc.condition.RequestMethodsRequestCondition; import org.springframework.web.util.UrlPathHelper; /** * Test fixture with {@link RequestMappingInfoHandlerMapping}. * * @author Arjen Poutsma * @author Rossen Stoyanchev */ public class RequestMappingInfoHandlerMappingTests { private TestRequestMappingInfoHandlerMapping handlerMapping; private HandlerMethod fooMethod; private HandlerMethod fooParamMethod; private HandlerMethod barMethod; private HandlerMethod emptyMethod; @Before public void setup() throws Exception { TestController testController = new TestController(); this.fooMethod = new HandlerMethod(testController, "foo"); this.fooParamMethod = new HandlerMethod(testController, "fooParam"); this.barMethod = new HandlerMethod(testController, "bar"); this.emptyMethod = new HandlerMethod(testController, "empty"); this.handlerMapping = new TestRequestMappingInfoHandlerMapping(); this.handlerMapping.registerHandler(testController); this.handlerMapping.setRemoveSemicolonContent(false); } @Test public void getMappingPathPatterns() throws Exception { String[] patterns = {"/foo/*", "/foo", "/bar/*", "/bar"}; RequestMappingInfo info = RequestMappingInfo.paths(patterns).build(); Set<String> actual = this.handlerMapping.getMappingPathPatterns(info); assertEquals(new HashSet<>(Arrays.asList(patterns)), actual); } @Test public void getHandlerDirectMatch() throws Exception { MockHttpServletRequest request = new MockHttpServletRequest("GET", "/foo"); HandlerMethod handlerMethod = getHandler(request); assertEquals(this.fooMethod.getMethod(), handlerMethod.getMethod()); } @Test public void getHandlerGlobMatch() throws Exception { MockHttpServletRequest request = new MockHttpServletRequest("GET", "/bar"); HandlerMethod handlerMethod = getHandler(request); assertEquals(this.barMethod.getMethod(), handlerMethod.getMethod()); } @Test public void getHandlerEmptyPathMatch() throws Exception { MockHttpServletRequest request = new MockHttpServletRequest("GET", ""); HandlerMethod handlerMethod = getHandler(request); assertEquals(this.emptyMethod.getMethod(), handlerMethod.getMethod()); request = new MockHttpServletRequest("GET", "/"); handlerMethod = getHandler(request); assertEquals(this.emptyMethod.getMethod(), handlerMethod.getMethod()); } @Test public void getHandlerBestMatch() throws Exception { MockHttpServletRequest request = new MockHttpServletRequest("GET", "/foo"); request.setParameter("p", "anything"); HandlerMethod handlerMethod = getHandler(request); assertEquals(this.fooParamMethod.getMethod(), handlerMethod.getMethod()); } @Test public void getHandlerRequestMethodNotAllowed() throws Exception { try { MockHttpServletRequest request = new MockHttpServletRequest("POST", "/bar"); this.handlerMapping.getHandler(request); fail("HttpRequestMethodNotSupportedException expected"); } catch (HttpRequestMethodNotSupportedException ex) { assertArrayEquals("Invalid supported methods", new String[]{"GET", "HEAD"}, ex.getSupportedMethods()); } } // SPR-9603 @Test(expected = HttpMediaTypeNotAcceptableException.class) public void getHandlerRequestMethodMatchFalsePositive() throws Exception { MockHttpServletRequest request = new MockHttpServletRequest("GET", "/users"); request.addHeader("Accept", "application/xml"); this.handlerMapping.registerHandler(new UserController()); this.handlerMapping.getHandler(request); } // SPR-8462 @Test public void getHandlerMediaTypeNotSupported() throws Exception { testHttpMediaTypeNotSupportedException("/person/1"); testHttpMediaTypeNotSupportedException("/person/1/"); testHttpMediaTypeNotSupportedException("/person/1.json"); } @Test public void getHandlerHttpOptions() throws Exception { testHttpOptions("/foo", "GET,HEAD"); testHttpOptions("/person/1", "PUT"); testHttpOptions("/persons", "GET,HEAD,POST,PUT,PATCH,DELETE,OPTIONS"); testHttpOptions("/something", "PUT,POST"); } @Test public void getHandlerTestInvalidContentType() throws Exception { try { MockHttpServletRequest request = new MockHttpServletRequest("PUT", "/person/1"); request.setContentType("bogus"); this.handlerMapping.getHandler(request); fail("HttpMediaTypeNotSupportedException expected"); } catch (HttpMediaTypeNotSupportedException ex) { assertEquals("Invalid mime type \"bogus\": does not contain '/'", ex.getMessage()); } } // SPR-8462 @Test public void getHandlerMediaTypeNotAccepted() throws Exception { testHttpMediaTypeNotAcceptableException("/persons"); testHttpMediaTypeNotAcceptableException("/persons/"); testHttpMediaTypeNotAcceptableException("/persons.json"); } // SPR-12854 @Test public void getHandlerUnsatisfiedServletRequestParameterException() throws Exception { try { MockHttpServletRequest request = new MockHttpServletRequest("GET", "/params"); this.handlerMapping.getHandler(request); fail("UnsatisfiedServletRequestParameterException expected"); } catch (UnsatisfiedServletRequestParameterException ex) { List<String[]> groups = ex.getParamConditionGroups(); assertEquals(2, groups.size()); assertThat(Arrays.asList("foo=bar", "bar=baz"), containsInAnyOrder(groups.get(0)[0], groups.get(1)[0])); } } @Test public void getHandlerProducibleMediaTypesAttribute() throws Exception { MockHttpServletRequest request = new MockHttpServletRequest("GET", "/content"); request.addHeader("Accept", "application/xml"); this.handlerMapping.getHandler(request); String name = HandlerMapping.PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE; assertEquals(Collections.singleton(MediaType.APPLICATION_XML), request.getAttribute(name)); request = new MockHttpServletRequest("GET", "/content"); request.addHeader("Accept", "application/json"); this.handlerMapping.getHandler(request); assertNull("Negated expression shouldn't be listed as producible type", request.getAttribute(name)); } @Test public void getHandlerMappedInterceptors() throws Exception { String path = "/foo"; HandlerInterceptor interceptor = new HandlerInterceptorAdapter() {}; MappedInterceptor mappedInterceptor = new MappedInterceptor(new String[] {path}, interceptor); TestRequestMappingInfoHandlerMapping mapping = new TestRequestMappingInfoHandlerMapping(); mapping.registerHandler(new TestController()); mapping.setInterceptors(new Object[] { mappedInterceptor }); mapping.setApplicationContext(new StaticWebApplicationContext()); HandlerExecutionChain chain = mapping.getHandler(new MockHttpServletRequest("GET", path)); assertNotNull(chain); assertNotNull(chain.getInterceptors()); assertSame(interceptor, chain.getInterceptors()[0]); chain = mapping.getHandler(new MockHttpServletRequest("GET", "/invalid")); assertNull(chain); } @SuppressWarnings("unchecked") @Test public void handleMatchUriTemplateVariables() { RequestMappingInfo key = RequestMappingInfo.paths("/{path1}/{path2}").build(); MockHttpServletRequest request = new MockHttpServletRequest("GET", "/1/2"); String lookupPath = new UrlPathHelper().getLookupPathForRequest(request); this.handlerMapping.handleMatch(key, lookupPath, request); String name = HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE; Map<String, String> uriVariables = (Map<String, String>) request.getAttribute(name); assertNotNull(uriVariables); assertEquals("1", uriVariables.get("path1")); assertEquals("2", uriVariables.get("path2")); } // SPR-9098 @SuppressWarnings("unchecked") @Test public void handleMatchUriTemplateVariablesDecode() { RequestMappingInfo key = RequestMappingInfo.paths("/{group}/{identifier}").build(); MockHttpServletRequest request = new MockHttpServletRequest("GET", "/group/a%2Fb"); UrlPathHelper pathHelper = new UrlPathHelper(); pathHelper.setUrlDecode(false); String lookupPath = pathHelper.getLookupPathForRequest(request); this.handlerMapping.setUrlPathHelper(pathHelper); this.handlerMapping.handleMatch(key, lookupPath, request); String name = HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE; Map<String, String> uriVariables = (Map<String, String>) request.getAttribute(name); assertNotNull(uriVariables); assertEquals("group", uriVariables.get("group")); assertEquals("a/b", uriVariables.get("identifier")); } @Test public void handleMatchBestMatchingPatternAttribute() { RequestMappingInfo key = RequestMappingInfo.paths("/{path1}/2", "/**").build(); MockHttpServletRequest request = new MockHttpServletRequest("GET", "/1/2"); this.handlerMapping.handleMatch(key, "/1/2", request); assertEquals("/{path1}/2", request.getAttribute(HandlerMapping.BEST_MATCHING_PATTERN_ATTRIBUTE)); } @Test public void handleMatchBestMatchingPatternAttributeNoPatternsDefined() { RequestMappingInfo key = RequestMappingInfo.paths().build(); MockHttpServletRequest request = new MockHttpServletRequest("GET", "/1/2"); this.handlerMapping.handleMatch(key, "/1/2", request); assertEquals("/1/2", request.getAttribute(HandlerMapping.BEST_MATCHING_PATTERN_ATTRIBUTE)); } @Test public void handleMatchMatrixVariables() { MockHttpServletRequest request; MultiValueMap<String, String> matrixVariables; Map<String, String> uriVariables; request = new MockHttpServletRequest(); handleMatch(request, "/{cars}", "/cars;colors=red,blue,green;year=2012"); matrixVariables = getMatrixVariables(request, "cars"); uriVariables = getUriTemplateVariables(request); assertNotNull(matrixVariables); assertEquals(Arrays.asList("red", "blue", "green"), matrixVariables.get("colors")); assertEquals("2012", matrixVariables.getFirst("year")); assertEquals("cars", uriVariables.get("cars")); request = new MockHttpServletRequest(); handleMatch(request, "/{cars:[^;]+}{params}", "/cars;colors=red,blue,green;year=2012"); matrixVariables = getMatrixVariables(request, "params"); uriVariables = getUriTemplateVariables(request); assertNotNull(matrixVariables); assertEquals(Arrays.asList("red", "blue", "green"), matrixVariables.get("colors")); assertEquals("2012", matrixVariables.getFirst("year")); assertEquals("cars", uriVariables.get("cars")); assertEquals(";colors=red,blue,green;year=2012", uriVariables.get("params")); request = new MockHttpServletRequest(); handleMatch(request, "/{cars:[^;]+}{params}", "/cars"); matrixVariables = getMatrixVariables(request, "params"); uriVariables = getUriTemplateVariables(request); assertNull(matrixVariables); assertEquals("cars", uriVariables.get("cars")); assertEquals("", uriVariables.get("params")); } @Test public void handleMatchMatrixVariablesDecoding() { MockHttpServletRequest request; UrlPathHelper urlPathHelper = new UrlPathHelper(); urlPathHelper.setUrlDecode(false); urlPathHelper.setRemoveSemicolonContent(false); this.handlerMapping.setUrlPathHelper(urlPathHelper ); request = new MockHttpServletRequest(); handleMatch(request, "/path{filter}", "/path;mvar=a%2fb"); MultiValueMap<String, String> matrixVariables = getMatrixVariables(request, "filter"); Map<String, String> uriVariables = getUriTemplateVariables(request); assertNotNull(matrixVariables); assertEquals(Collections.singletonList("a/b"), matrixVariables.get("mvar")); assertEquals(";mvar=a/b", uriVariables.get("filter")); } private HandlerMethod getHandler(MockHttpServletRequest request) throws Exception { HandlerExecutionChain chain = this.handlerMapping.getHandler(request); assertNotNull(chain); return (HandlerMethod) chain.getHandler(); } private void testHttpMediaTypeNotSupportedException(String url) throws Exception { try { MockHttpServletRequest request = new MockHttpServletRequest("PUT", url); request.setContentType("application/json"); this.handlerMapping.getHandler(request); fail("HttpMediaTypeNotSupportedException expected"); } catch (HttpMediaTypeNotSupportedException ex) { assertEquals("Invalid supported consumable media types", Collections.singletonList(new MediaType("application", "xml")), ex.getSupportedMediaTypes()); } } private void testHttpOptions(String requestURI, String allowHeader) throws Exception { MockHttpServletRequest request = new MockHttpServletRequest("OPTIONS", requestURI); HandlerMethod handlerMethod = getHandler(request); ServletWebRequest webRequest = new ServletWebRequest(request); ModelAndViewContainer mavContainer = new ModelAndViewContainer(); Object result = new InvocableHandlerMethod(handlerMethod).invokeForRequest(webRequest, mavContainer); assertNotNull(result); assertEquals(HttpHeaders.class, result.getClass()); assertEquals(allowHeader, ((HttpHeaders) result).getFirst("Allow")); } private void testHttpMediaTypeNotAcceptableException(String url) throws Exception { try { MockHttpServletRequest request = new MockHttpServletRequest("GET", url); request.addHeader("Accept", "application/json"); this.handlerMapping.getHandler(request); fail("HttpMediaTypeNotAcceptableException expected"); } catch (HttpMediaTypeNotAcceptableException ex) { assertEquals("Invalid supported producible media types", Collections.singletonList(new MediaType("application", "xml")), ex.getSupportedMediaTypes()); } } private void handleMatch(MockHttpServletRequest request, String pattern, String lookupPath) { RequestMappingInfo info = RequestMappingInfo.paths(pattern).build(); this.handlerMapping.handleMatch(info, lookupPath, request); } @SuppressWarnings("unchecked") private MultiValueMap<String, String> getMatrixVariables(HttpServletRequest request, String uriVarName) { String attrName = HandlerMapping.MATRIX_VARIABLES_ATTRIBUTE; return ((Map<String, MultiValueMap<String, String>>) request.getAttribute(attrName)).get(uriVarName); } @SuppressWarnings("unchecked") private Map<String, String> getUriTemplateVariables(HttpServletRequest request) { String attrName = HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE; return (Map<String, String>) request.getAttribute(attrName); } @SuppressWarnings("unused") @Controller private static class TestController { @RequestMapping(value = "/foo", method = RequestMethod.GET) public void foo() { } @RequestMapping(value = "/foo", method = RequestMethod.GET, params="p") public void fooParam() { } @RequestMapping(value = "/ba*", method = { RequestMethod.GET, RequestMethod.HEAD }) public void bar() { } @RequestMapping(value = "") public void empty() { } @RequestMapping(value = "/person/{id}", method = RequestMethod.PUT, consumes="application/xml") public void consumes(@RequestBody String text) { } @RequestMapping(value = "/persons", produces="application/xml") public String produces() { return ""; } @RequestMapping(value = "/params", params="foo=bar") public String param() { return ""; } @RequestMapping(value = "/params", params="bar=baz") public String param2() { return ""; } @RequestMapping(value = "/content", produces="application/xml") public String xmlContent() { return ""; } @RequestMapping(value = "/content", produces="!application/xml") public String nonXmlContent() { return ""; } @RequestMapping(value = "/something", method = RequestMethod.OPTIONS) public HttpHeaders fooOptions() { HttpHeaders headers = new HttpHeaders(); headers.add("Allow", "PUT,POST"); return headers; } } @SuppressWarnings("unused") @Controller private static class UserController { @RequestMapping(value = "/users", method = RequestMethod.GET, produces = "application/json") public void getUser() { } @RequestMapping(value = "/users", method = RequestMethod.PUT) public void saveUser() { } } private static class TestRequestMappingInfoHandlerMapping extends RequestMappingInfoHandlerMapping { public void registerHandler(Object handler) { super.detectHandlerMethods(handler); } @Override protected boolean isHandler(Class<?> beanType) { return AnnotationUtils.findAnnotation(beanType, RequestMapping.class) != null; } @Override protected RequestMappingInfo getMappingForMethod(Method method, Class<?> handlerType) { RequestMapping annot = AnnotationUtils.findAnnotation(method, RequestMapping.class); if (annot != null) { return new RequestMappingInfo( new PatternsRequestCondition(annot.value(), getUrlPathHelper(), getPathMatcher(), true, true), new RequestMethodsRequestCondition(annot.method()), new ParamsRequestCondition(annot.params()), new HeadersRequestCondition(annot.headers()), new ConsumesRequestCondition(annot.consumes(), annot.headers()), new ProducesRequestCondition(annot.produces(), annot.headers()), null); } else { return null; } } } }