diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/condition/CompositeRequestCondition.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/condition/CompositeRequestCondition.java new file mode 100644 index 00000000000..5166ebee09d --- /dev/null +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/condition/CompositeRequestCondition.java @@ -0,0 +1,181 @@ +/* + * Copyright 2002-2012 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.condition; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import javax.servlet.http.HttpServletRequest; + +import org.springframework.util.Assert; +import org.springframework.util.ObjectUtils; + +import edu.emory.mathcs.backport.java.util.Collections; + +/** + * Implements the {@link RequestCondition} contract by delegating to multiple + * {@code RequestCondition} types and using a logical conjunction (' && ') to + * ensure all conditions match a given request. + * + *

When {@code CompositeRequestCondition} instances are combined or compared + * they are expected to (a) contain the same number of conditions and (b) that + * conditions in the respective index are of the same type. It is acceptable to + * provide {@code null} conditions or no conditions at all to the constructor. + * + * @author Rossen Stoyanchev + * @since 3.2 + */ +public class CompositeRequestCondition extends AbstractRequestCondition { + + private final RequestConditionHolder[] requestConditions; + + /** + * Create an instance with 0 or more {@code RequestCondition} types. It is + * important to create {@code CompositeRequestCondition} instances with the + * same number of conditions so they may be compared and combined. + * It is acceptable to provide {@code null} conditions. + */ + public CompositeRequestCondition(RequestCondition... requestConditions) { + this.requestConditions = wrap(requestConditions); + } + + private RequestConditionHolder[] wrap(RequestCondition... rawConditions) { + RequestConditionHolder[] wrappedConditions = new RequestConditionHolder[rawConditions.length]; + for (int i = 0; i < rawConditions.length; i++) { + wrappedConditions[i] = new RequestConditionHolder(rawConditions[i]); + } + return wrappedConditions; + } + + private CompositeRequestCondition(RequestConditionHolder[] requestConditions) { + this.requestConditions = requestConditions; + } + + /** + * Whether this instance contains 0 conditions or not. + */ + public boolean isEmpty() { + return ObjectUtils.isEmpty(this.requestConditions); + } + + /** + * Return the underlying conditions, possibly empty but never {@code null}. + */ + public List> getConditions() { + return unwrap(); + } + + private List> unwrap() { + List> result = new ArrayList>(); + for (RequestConditionHolder holder : this.requestConditions) { + result.add(holder.getCondition()); + } + return result; + } + + @Override + protected Collection getContent() { + return (isEmpty()) ? Collections.emptyList() : getConditions(); + } + + @Override + protected String getToStringInfix() { + return " && "; + } + + private int getLength() { + return this.requestConditions.length; + } + + /** + * If one instance is empty, return the other. + * If both instances have conditions, combine the individual conditions + * after ensuring they are of the same type and number. + */ + public CompositeRequestCondition combine(CompositeRequestCondition other) { + if (isEmpty() && other.isEmpty()) { + return this; + } + else if (other.isEmpty()) { + return this; + } + else if (isEmpty()) { + return other; + } + else { + assertNumberOfConditions(other); + RequestConditionHolder[] combinedConditions = new RequestConditionHolder[getLength()]; + for (int i = 0; i < getLength(); i++) { + combinedConditions[i] = this.requestConditions[i].combine(other.requestConditions[i]); + } + return new CompositeRequestCondition(combinedConditions); + } + } + + private void assertNumberOfConditions(CompositeRequestCondition other) { + Assert.isTrue(getLength() == other.getLength(), + "Cannot combine CompositeRequestConditions with a different number of conditions. " + + this.requestConditions + " and " + other.requestConditions); + } + + /** + * Delegate to all contained conditions to match the request and return the + * resulting "matching" condition instances. + *

An empty {@code CompositeRequestCondition} matches to all requests. + */ + public CompositeRequestCondition getMatchingCondition(HttpServletRequest request) { + if (isEmpty()) { + return this; + } + RequestConditionHolder[] matchingConditions = new RequestConditionHolder[getLength()]; + for (int i = 0; i < getLength(); i++) { + matchingConditions[i] = this.requestConditions[i].getMatchingCondition(request); + if (matchingConditions[i] == null) { + return null; + } + } + return new CompositeRequestCondition(matchingConditions); + } + + /** + * If one instance is empty, the other "wins". If both instances have + * conditions, compare them in the order in which they were provided. + */ + public int compareTo(CompositeRequestCondition other, HttpServletRequest request) { + if (isEmpty() && other.isEmpty()) { + return 0; + } + else if (isEmpty()) { + return 1; + } + else if (other.isEmpty()) { + return -1; + } + else { + assertNumberOfConditions(other); + for (int i = 0; i < getLength(); i++) { + int result = this.requestConditions[i].compareTo(other.requestConditions[i], request); + if (result != 0) { + return result; + } + } + return 0; + } + } + +} diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/condition/RequestConditionHolder.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/condition/RequestConditionHolder.java index b28fef5f175..8779839fbbd 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/condition/RequestConditionHolder.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/condition/RequestConditionHolder.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2011 the original author or authors. + * Copyright 2002-2012 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. @@ -23,40 +23,42 @@ import javax.servlet.http.HttpServletRequest; /** - * A holder for a {@link RequestCondition} useful when the type of the held - * request condition is not known ahead of time - e.g. custom condition. + * A holder for a {@link RequestCondition} useful when the type of the request + * condition is not known ahead of time, e.g. custom condition. Since this + * class is also an implementation of {@code RequestCondition}, effectively it + * decorates the held request condition and allows it to be combined and compared + * with other request conditions in a type and null safe way. * - *

An implementation of {@code RequestCondition} itself, a - * {@code RequestConditionHolder} decorates the held request condition allowing - * it to be combined and compared with other custom request conditions while - * ensuring type and null safety. + *

When two {@code RequestConditionHolder} instances are combined or compared + * with each other, it is expected the conditions they hold are of the same type. + * If they are not, a {@link ClassCastException} is raised. * * @author Rossen Stoyanchev * @since 3.1 */ public final class RequestConditionHolder extends AbstractRequestCondition { - @SuppressWarnings("rawtypes") - private final RequestCondition condition; + private final RequestCondition condition; /** * Create a new holder to wrap the given request condition. * @param requestCondition the condition to hold, may be {@code null} */ + @SuppressWarnings("unchecked") public RequestConditionHolder(RequestCondition requestCondition) { - this.condition = requestCondition; + this.condition = (RequestCondition) requestCondition; } /** * Return the held request condition, or {@code null} if not holding one. */ public RequestCondition getCondition() { - return condition; + return this.condition; } @Override protected Collection getContent() { - return condition != null ? Collections.singleton(condition) : Collections.emptyList(); + return this.condition != null ? Collections.singleton(this.condition) : Collections.emptyList(); } @Override @@ -69,20 +71,19 @@ public final class RequestConditionHolder extends AbstractRequestCondition combined = (RequestCondition) condition.combine(other.condition); + assertEqualConditionTypes(other); + RequestCondition combined = (RequestCondition) this.condition.combine(other.condition); return new RequestConditionHolder(combined); } } @@ -90,8 +91,8 @@ public final class RequestConditionHolder extends AbstractRequestCondition clazz = condition.getClass(); + private void assertEqualConditionTypes(RequestConditionHolder other) { + Class clazz = this.condition.getClass(); Class otherClazz = other.condition.getClass(); if (!clazz.equals(otherClazz)) { throw new ClassCastException("Incompatible request conditions: " + clazz + " and " + otherClazz); @@ -104,10 +105,10 @@ public final class RequestConditionHolder extends AbstractRequestCondition match = (RequestCondition) condition.getMatchingCondition(request); + RequestCondition match = (RequestCondition) this.condition.getMatchingCondition(request); return (match != null) ? new RequestConditionHolder(match) : null; } @@ -116,20 +117,19 @@ public final class RequestConditionHolder extends AbstractRequestConditionConsider extending {@link AbstractRequestCondition} for custom + * condition types and using {@link CompositeRequestCondition} to provide + * multiple custom conditions. + * + * @param handlerType the handler type for which to create the condition * @return the condition, or {@code null} */ - protected RequestCondition getCustomMethodCondition(Method method) { + protected RequestCondition getCustomTypeCondition(Class handlerType) { return null; } /** - * Provide a custom type-level request condition. + * Provide a custom method-level request condition. * The custom {@link RequestCondition} can be of any type so long as the * same condition type is returned from all calls to this method in order * to ensure custom request conditions can be combined and compared. - * @param handlerType the handler type for which to create the condition + * + *

Consider extending {@link AbstractRequestCondition} for custom + * condition types and using {@link CompositeRequestCondition} to provide + * multiple custom conditions. + * + * @param method the handler method for which to create the condition * @return the condition, or {@code null} */ - protected RequestCondition getCustomTypeCondition(Class handlerType) { + protected RequestCondition getCustomMethodCondition(Method method) { return null; } diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/condition/CompositeRequestConditionTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/condition/CompositeRequestConditionTests.java new file mode 100644 index 00000000000..d374a40b6d6 --- /dev/null +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/condition/CompositeRequestConditionTests.java @@ -0,0 +1,141 @@ +/* + * Copyright 2002-2011 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.condition; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; + +import javax.servlet.http.HttpServletRequest; + +import org.junit.Before; +import org.junit.Test; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.web.bind.annotation.RequestMethod; + +/** + * A test fixture for {@link CompositeRequestCondition} tests. + * + * @author Rossen Stoyanchev + */ +public class CompositeRequestConditionTests { + + private ParamsRequestCondition param1; + private ParamsRequestCondition param2; + private ParamsRequestCondition param3; + + private HeadersRequestCondition header1; + private HeadersRequestCondition header2; + private HeadersRequestCondition header3; + + @Before + public void setup() { + this.param1 = new ParamsRequestCondition("param1"); + this.param2 = new ParamsRequestCondition("param2"); + this.param3 = this.param1.combine(this.param2); + + this.header1 = new HeadersRequestCondition("header1"); + this.header2 = new HeadersRequestCondition("header2"); + this.header3 = this.header1.combine(this.header2); + } + + @Test + public void combine() { + CompositeRequestCondition cond1 = new CompositeRequestCondition(this.param1, this.header1); + CompositeRequestCondition cond2 = new CompositeRequestCondition(this.param2, this.header2); + CompositeRequestCondition cond3 = new CompositeRequestCondition(this.param3, this.header3); + + assertEquals(cond3, cond1.combine(cond2)); + } + + @Test + public void combineEmpty() { + CompositeRequestCondition empty = new CompositeRequestCondition(); + CompositeRequestCondition notEmpty = new CompositeRequestCondition(this.param1); + + assertSame(empty, empty.combine(empty)); + assertSame(notEmpty, notEmpty.combine(empty)); + assertSame(notEmpty, empty.combine(notEmpty)); + } + + @Test(expected=IllegalArgumentException.class) + public void combineDifferentLength() { + CompositeRequestCondition cond1 = new CompositeRequestCondition(this.param1); + CompositeRequestCondition cond2 = new CompositeRequestCondition(this.param1, this.header1); + cond1.combine(cond2); + } + + @Test + public void match() { + MockHttpServletRequest request = new MockHttpServletRequest("GET", "/"); + request.setParameter("param1", "paramValue1"); + request.addHeader("header1", "headerValue1"); + + RequestCondition getPostCond = new RequestMethodsRequestCondition(RequestMethod.GET, RequestMethod.POST); + RequestCondition getCond = new RequestMethodsRequestCondition(RequestMethod.GET); + + CompositeRequestCondition condition = new CompositeRequestCondition(this.param1, getPostCond); + CompositeRequestCondition matchingCondition = new CompositeRequestCondition(this.param1, getCond); + + assertEquals(matchingCondition, condition.getMatchingCondition(request)); + } + + @Test + public void noMatch() { + MockHttpServletRequest request = new MockHttpServletRequest("GET", "/"); + CompositeRequestCondition cond = new CompositeRequestCondition(this.param1); + + assertNull(cond.getMatchingCondition(request)); + } + + @Test + public void matchEmpty() { + CompositeRequestCondition empty = new CompositeRequestCondition(); + assertSame(empty, empty.getMatchingCondition(new MockHttpServletRequest())); + } + + @Test + public void compare() { + HttpServletRequest request = new MockHttpServletRequest(); + + CompositeRequestCondition cond1 = new CompositeRequestCondition(this.param1); + CompositeRequestCondition cond3 = new CompositeRequestCondition(this.param3); + + assertEquals(1, cond1.compareTo(cond3, request)); + assertEquals(-1, cond3.compareTo(cond1, request)); + } + + @Test + public void compareEmpty() { + HttpServletRequest request = new MockHttpServletRequest(); + + CompositeRequestCondition empty = new CompositeRequestCondition(); + CompositeRequestCondition notEmpty = new CompositeRequestCondition(this.param1); + + assertEquals(0, empty.compareTo(empty, request)); + assertEquals(-1, notEmpty.compareTo(empty, request)); + assertEquals(1, empty.compareTo(notEmpty, request)); + } + + @Test(expected=IllegalArgumentException.class) + public void compareDifferentLength() { + CompositeRequestCondition cond1 = new CompositeRequestCondition(this.param1); + CompositeRequestCondition cond2 = new CompositeRequestCondition(this.param1, this.header1); + cond1.compareTo(cond2, new MockHttpServletRequest()); + } + +} diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/condition/RequestConditionHolderTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/condition/RequestConditionHolderTests.java index 9236bd9ea15..45dd2dcf1be 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/condition/RequestConditionHolderTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/condition/RequestConditionHolderTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2011 the original author or authors. + * Copyright 2002-2012 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. @@ -27,23 +27,12 @@ import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.web.bind.annotation.RequestMethod; /** - * A test fixture for - * {code org.springframework.web.servlet.mvc.method.RequestConditionHolder} tests. + * A test fixture for {@link RequestConditionHolder} tests. * * @author Rossen Stoyanchev */ public class RequestConditionHolderTests { - @Test - public void combineEmpty() { - RequestConditionHolder empty = new RequestConditionHolder(null); - RequestConditionHolder notEmpty = new RequestConditionHolder(new ParamsRequestCondition("name")); - - assertSame(empty, empty.combine(new RequestConditionHolder(null))); - assertSame(notEmpty, notEmpty.combine(empty)); - assertSame(notEmpty, empty.combine(notEmpty)); - } - @Test public void combine() { RequestConditionHolder params1 = new RequestConditionHolder(new ParamsRequestCondition("name1")); @@ -53,6 +42,16 @@ public class RequestConditionHolderTests { assertEquals(expected, params1.combine(params2)); } + @Test + public void combineEmpty() { + RequestConditionHolder empty = new RequestConditionHolder(null); + RequestConditionHolder notEmpty = new RequestConditionHolder(new ParamsRequestCondition("name")); + + assertSame(empty, empty.combine(empty)); + assertSame(notEmpty, notEmpty.combine(empty)); + assertSame(notEmpty, empty.combine(notEmpty)); + } + @Test(expected=ClassCastException.class) public void combineIncompatible() { RequestConditionHolder params = new RequestConditionHolder(new ParamsRequestCondition("name")); diff --git a/src/dist/changelog.txt b/src/dist/changelog.txt index 857563a5763..2c089eddf22 100644 --- a/src/dist/changelog.txt +++ b/src/dist/changelog.txt @@ -6,24 +6,25 @@ http://www.springsource.org Changes in version 3.2 M1 ------------------------------------- +* add Servlet 3.0 based async support * upgraded to AspectJ 1.6.12, JUnit 4.10, TestNG 6.5.2 +* add HttpMessageConverter and View types compatible with Jackson 2.0 * better handling on failure to parse invalid 'Content-Type' or 'Accept' headers * handle a controller method's return value based on the actual returned value (vs declared type) * fix issue with combining identical controller and method level request mapping paths * fix concurrency issue in AnnotationMethodHandlerExceptionResolver * fix case-sensitivity issue with some containers on access to 'Content-Disposition' header -* add Servlet 3.0 based async support * fix issue with encoded params in UriComponentsBuilder -* add HttpMessageConverter and View types compatible with Jackson 2.0 * add pretty print option to Jackson HttpMessageConverter and View types -* fix issue with resolving Errors controller method argument -* detect controller methods via InitializingBean in RequestMappingHandlerMapping * translate IOException from Jackson to HttpMessageNotReadableException +* fix issue with resolving Errors controller method argument +* implement InitializingBean in RequestMappingHandlerMapping to detect controller methods * fix content negotiation issue when sorting selected media types by quality value -* Prevent further writing to the response when @ResponseStatus contains a reason -* Deprecate HttpStatus codes 419, 420, 421 +* prevent further writing to the response when @ResponseStatus contains a reason +* deprecate HttpStatus codes 419, 420, 421 * support access to all URI vars via @PathVariable Map * add "excludedExceptions" property to SimpleUrlHandlerMapping +* add CompositeRequestCondition for use with multiple custom request mapping conditions Changes in version 3.1.1 (2012-02-16) -------------------------------------