diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/bind/annotation/RequestMapping.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/bind/annotation/RequestMapping.java index 6adc9606d41..c5d0e0c4c35 100644 --- a/org.springframework.web.servlet/src/main/java/org/springframework/web/bind/annotation/RequestMapping.java +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/bind/annotation/RequestMapping.java @@ -230,4 +230,25 @@ public @interface RequestMapping { */ String[] params() default {}; + /** + * The headers of the mapped request, narrowing the primary mapping. + *

Same format for any environment: a sequence of "My-Header=myValue" style + * expressions, with a request only mapped if each such header is found + * to have the given value. "My-Header" style expressions are also supported, + * with such headers having to be present in the request (allowed to have + * any value). Finally, "!My-Header" style expressions indicate that the + * specified header is not supposed to be present in the request. + *

Also supports media type wildcards (*), for headers such as Accept + * and Content-Type. For instance, + *

+	 * @RequestMapping(value = "/something", headers = "content-type=text/*")
+	 * 
+ * will match requests with a Content-Type of "text/html", "text/plain", etc. + *

Supported at the type level as well as at the method level! + * When used at the type level, all method-level mappings inherit + * this header restriction (i.e. the type-level restriction + * gets checked before the handler method is even resolved). + * @see org.springframework.http.MediaType + */ + String[] headers() default {}; } diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/annotation/AnnotationMethodHandlerAdapter.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/annotation/AnnotationMethodHandlerAdapter.java index 2b936b5de37..e712a60ff4f 100644 --- a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/annotation/AnnotationMethodHandlerAdapter.java +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/annotation/AnnotationMethodHandlerAdapter.java @@ -396,12 +396,15 @@ public class AnnotationMethodHandlerAdapter extends WebContentGenerator implemen if (!hasTypeLevelMapping() || !Arrays.equals(mapping.params(), getTypeLevelMapping().params())) { mappingInfo.params = mapping.params(); } + if (!hasTypeLevelMapping() || !Arrays.equals(mapping.headers(), getTypeLevelMapping().headers())) { + mappingInfo.headers = mapping.headers(); + } boolean match = false; if (mappingInfo.paths.length > 0) { List matchedPaths = new ArrayList(mappingInfo.paths.length); for (String methodLevelPattern : mappingInfo.paths) { if (isPathMatch(methodLevelPattern, lookupPath)) { - if (checkParameters(mappingInfo, request)) { + if (mappingInfo.matches(request)) { match = true; matchedPaths.add(methodLevelPattern); } @@ -418,7 +421,7 @@ public class AnnotationMethodHandlerAdapter extends WebContentGenerator implemen } else { // No paths specified: parameter match sufficient. - match = checkParameters(mappingInfo, request); + match = mappingInfo.matches(request); if (match && mappingInfo.methods.length == 0 && mappingInfo.params.length == 0 && resolvedMethodName != null && !resolvedMethodName.equals(handlerMethod.getName())) { match = false; @@ -514,11 +517,6 @@ public class AnnotationMethodHandlerAdapter extends WebContentGenerator implemen return false; } - private boolean checkParameters(RequestMappingInfo mapping, HttpServletRequest request) { - return ServletAnnotationMappingUtils.checkRequestMethod(mapping.methods, request) && - ServletAnnotationMappingUtils.checkParameters(mapping.params, request); - } - @SuppressWarnings("unchecked") private void extractHandlerMethodUriTemplates(String mappedPath, String lookupPath, @@ -726,21 +724,29 @@ public class AnnotationMethodHandlerAdapter extends WebContentGenerator implemen String[] params = new String[0]; + String[] headers = new String[0]; + String bestMatchedPath() { return matchedPaths.length > 0 ? matchedPaths[0] : null; } + public boolean matches(HttpServletRequest request) { + return ServletAnnotationMappingUtils.checkRequestMethod(this.methods, request) && + ServletAnnotationMappingUtils.checkParameters(this.params, request) && + ServletAnnotationMappingUtils.checkHeaders(this.headers, request); + } + @Override public boolean equals(Object obj) { RequestMappingInfo other = (RequestMappingInfo) obj; return (Arrays.equals(this.paths, other.paths) && Arrays.equals(this.methods, other.methods) && - Arrays.equals(this.params, other.params)); + Arrays.equals(this.params, other.params) && Arrays.equals(this.headers, other.headers)); } @Override public int hashCode() { - return (Arrays.hashCode(this.paths) * 29 + Arrays.hashCode(this.methods) * 31 + - Arrays.hashCode(this.params)); + return (Arrays.hashCode(this.paths) * 23 + Arrays.hashCode(this.methods) * 29 + + Arrays.hashCode(this.params) * 31 + Arrays.hashCode(this.headers)); } } @@ -777,14 +783,21 @@ public class AnnotationMethodHandlerAdapter extends WebContentGenerator implemen } else if (info1MethodCount == 1 & info2MethodCount > 1) { return -1; - } else if (info2MethodCount == 1 & info1MethodCount > 1) { return 1; } int info1ParamCount = info1.params.length; int info2ParamCount = info2.params.length; - return (info1ParamCount < info2ParamCount ? 1 : (info1ParamCount == info2ParamCount ? 0 : -1)); + if (info1ParamCount != info2ParamCount) { + return info2ParamCount - info1ParamCount; + } + int info1HeaderCount = info1.headers.length; + int info2HeaderCount = info2.headers.length; + if (info1HeaderCount != info2HeaderCount) { + return info2HeaderCount - info1HeaderCount; + } + return 0; } } diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/annotation/ServletAnnotationMappingUtils.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/annotation/ServletAnnotationMappingUtils.java index 70cf9526a68..9fa4ab88f17 100644 --- a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/annotation/ServletAnnotationMappingUtils.java +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/annotation/ServletAnnotationMappingUtils.java @@ -16,8 +16,11 @@ package org.springframework.web.servlet.mvc.annotation; +import java.util.Iterator; +import java.util.List; import javax.servlet.http.HttpServletRequest; +import org.springframework.http.MediaType; import org.springframework.util.ObjectUtils; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.util.WebUtils; @@ -32,6 +35,7 @@ abstract class ServletAnnotationMappingUtils { /** * Check whether the given request matches the specified request methods. + * * @param methods the HTTP request methods to check against * @param request the current HTTP request to check */ @@ -49,8 +53,8 @@ abstract class ServletAnnotationMappingUtils { /** * Check whether the given request matches the specified parameter conditions. - * @param params the parameter conditions, following - * {@link org.springframework.web.bind.annotation.RequestMapping#params()} + * + * @param params the parameter conditions, following {@link org.springframework.web.bind.annotation.RequestMapping#params()} * @param request the current HTTP request to check */ public static boolean checkParameters(String[] params, HttpServletRequest request) { @@ -79,4 +83,62 @@ abstract class ServletAnnotationMappingUtils { return true; } + /** + * Check whether the given request matches the specified header conditions. + * + * @param headers the header conditions, following {@link org.springframework.web.bind.annotation.RequestMapping#headers()} + * @param request the current HTTP request to check + */ + public static boolean checkHeaders(String[] headers, HttpServletRequest request) { + if (!ObjectUtils.isEmpty(headers)) { + for (String header : headers) { + int separator = header.indexOf('='); + if (separator == -1) { + if (header.startsWith("!")) { + if (hasHeader(request, header.substring(1))) { + return false; + } + } + else if (!hasHeader(request, header)) { + return false; + } + } + else { + String key = header.substring(0, separator); + String value = header.substring(separator + 1); + if (isMediaTypeHeader(key)) { + List requestMediaTypes = MediaType.parseMediaTypes(request.getHeader(key)); + List valueMediaTypes = MediaType.parseMediaTypes(value); + boolean found = false; + for (Iterator valIter = valueMediaTypes.iterator(); valIter.hasNext() && !found;) { + MediaType valueMediaType = valIter.next(); + for (Iterator reqIter = requestMediaTypes.iterator(); reqIter.hasNext() && !found;) { + MediaType requestMediaType = reqIter.next(); + if (valueMediaType.includes(requestMediaType)) { + found = true; + } + } + + } + if (!found) { + return false; + } + } + else if (!value.equals(request.getHeader(key))) { + return false; + } + } + } + } + return true; + } + + private static boolean hasHeader(HttpServletRequest request, String headerName) { + return request.getHeader(headerName) != null; + } + + private static boolean isMediaTypeHeader(String headerName) { + return "Accept".equalsIgnoreCase(headerName) || "Content-Type".equalsIgnoreCase(headerName); + } + } diff --git a/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/mvc/annotation/ServletAnnotationControllerTests.java b/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/mvc/annotation/ServletAnnotationControllerTests.java index 62dd293a9ca..5ee4c427c6f 100644 --- a/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/mvc/annotation/ServletAnnotationControllerTests.java +++ b/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/mvc/annotation/ServletAnnotationControllerTests.java @@ -894,6 +894,23 @@ public class ServletAnnotationControllerTests { assertEquals("Invalid response status code", HttpServletResponse.SC_BAD_REQUEST, response.getStatus()); } + @Test + public void headers() throws ServletException, IOException { + initServlet(HeadersController.class); + + MockHttpServletRequest request = new MockHttpServletRequest("GET", "/something"); + request.addHeader("Content-Type", "application/pdf"); + MockHttpServletResponse response = new MockHttpServletResponse(); + servlet.service(request, response); + assertEquals("pdf", response.getContentAsString()); + + request = new MockHttpServletRequest("GET", "/something"); + request.addHeader("Content-Type", "text/html"); + response = new MockHttpServletResponse(); + servlet.service(request, response); + assertEquals("text", response.getContentAsString()); + } + /* * Controllers */ @@ -1472,6 +1489,20 @@ public class ServletAnnotationControllerTests { } } + @Controller + public static class HeadersController { + + @RequestMapping(value = "/something", headers = "content-type=application/pdf") + public void handlePdf(Writer writer) throws IOException { + writer.write("pdf"); + } + + @RequestMapping(value = "/something", headers = "content-type=text/*") + public void handleHtml(Writer writer) throws IOException { + writer.write("text"); + } + } + public static class MyMessageConverter implements HttpMessageConverter { diff --git a/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/mvc/annotation/ServletAnnotationMappingUtilsTests.java b/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/mvc/annotation/ServletAnnotationMappingUtilsTests.java index 294006af299..f11dc01bfb2 100644 --- a/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/mvc/annotation/ServletAnnotationMappingUtilsTests.java +++ b/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/mvc/annotation/ServletAnnotationMappingUtilsTests.java @@ -1,7 +1,22 @@ +/* + * Copyright 2002-2009 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.annotation; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; +import static org.junit.Assert.*; import org.junit.Test; import org.springframework.mock.web.MockHttpServletRequest; @@ -26,4 +41,96 @@ public class ServletAnnotationMappingUtilsTests { assertFalse("Invalid request method result", result); } + @Test + public void checkParametersSimpleMatch() { + MockHttpServletRequest request = new MockHttpServletRequest("GET", "/"); + request.addParameter("param1", "value1"); + String[] params = new String[]{"param1", "!param2"}; + boolean result = ServletAnnotationMappingUtils.checkParameters(params, request); + assertTrue("Invalid request method result", result); + } + + @Test + public void checkParametersSimpleNoMatch() { + MockHttpServletRequest request = new MockHttpServletRequest("GET", "/"); + request.addParameter("param1", "value1"); + request.addParameter("param2", "value2"); + String[] params = new String[]{"param1", "!param2"}; + boolean result = ServletAnnotationMappingUtils.checkParameters(params, request); + assertFalse("Invalid request method result", result); + } + + @Test + public void checkParametersKeyValueMatch() { + MockHttpServletRequest request = new MockHttpServletRequest("GET", "/"); + request.addParameter("param1", "value1"); + String[] params = new String[]{"param1=value1"}; + boolean result = ServletAnnotationMappingUtils.checkParameters(params, request); + assertTrue("Invalid request method result", result); + } + + @Test + public void checkParametersKeyValueNoMatch() { + MockHttpServletRequest request = new MockHttpServletRequest("GET", "/"); + request.addParameter("param1", "value1"); + String[] params = new String[]{"param1=foo"}; + boolean result = ServletAnnotationMappingUtils.checkParameters(params, request); + assertFalse("Invalid request method result", result); + } + + @Test + public void checkHeadersSimpleMatch() { + MockHttpServletRequest request = new MockHttpServletRequest("GET", "/"); + request.addHeader("header1", "value1"); + String[] headers = new String[]{"header1", "!header2"}; + boolean result = ServletAnnotationMappingUtils.checkHeaders(headers, request); + assertTrue("Invalid request method result", result); + } + + @Test + public void checkHeadersSimpleNoMatch() { + MockHttpServletRequest request = new MockHttpServletRequest("GET", "/"); + request.addHeader("header1", "value1"); + request.addHeader("header2", "value2"); + String[] headers = new String[]{"header1", "!header2"}; + boolean result = ServletAnnotationMappingUtils.checkHeaders(headers, request); + assertFalse("Invalid request method result", result); + } + + @Test + public void checkHeadersKeyValueMatch() { + MockHttpServletRequest request = new MockHttpServletRequest("GET", "/"); + request.addHeader("header1", "value1"); + String[] headers = new String[]{"header1=value1"}; + boolean result = ServletAnnotationMappingUtils.checkHeaders(headers, request); + assertTrue("Invalid request method result", result); + } + + @Test + public void checkHeadersKeyValueNoMatch() { + MockHttpServletRequest request = new MockHttpServletRequest("GET", "/"); + request.addHeader("header1", "value1"); + String[] headers = new String[]{"header1=foo"}; + boolean result = ServletAnnotationMappingUtils.checkHeaders(headers, request); + assertFalse("Invalid request method result", result); + } + + @Test + public void checkHeadersAcceptMatch() { + MockHttpServletRequest request = new MockHttpServletRequest("GET", "/"); + request.addHeader("Accept", "application/pdf, text/html"); + String[] headers = new String[]{"accept=text/html, application/*"}; + boolean result = ServletAnnotationMappingUtils.checkHeaders(headers, request); + assertTrue("Invalid request method result", result); + } + + @Test + public void checkHeadersAcceptNoMatch() { + MockHttpServletRequest request = new MockHttpServletRequest("GET", "/"); + request.addHeader("Accept", "application/pdf, text/html"); + String[] headers = new String[]{"accept=audio/basic, application/xml"}; + boolean result = ServletAnnotationMappingUtils.checkHeaders(headers, request); + assertFalse("Invalid request method result", result); + } + }