Browse Source

Add CompositeRequestCondition

The new type makes it easier providing multiple custom request mapping
conditions via setters on RequestMappingHandlerMapping.

Issue: SPR-9350
pull/64/merge
Rossen Stoyanchev 14 years ago
parent
commit
bdc3599d3d
  1. 181
      spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/condition/CompositeRequestCondition.java
  2. 52
      spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/condition/RequestConditionHolder.java
  3. 24
      spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerMapping.java
  4. 141
      spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/condition/CompositeRequestConditionTests.java
  5. 25
      spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/condition/RequestConditionHolderTests.java
  6. 13
      src/dist/changelog.txt

181
spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/condition/CompositeRequestCondition.java

@ -0,0 +1,181 @@ @@ -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.
*
* <p>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<CompositeRequestCondition> {
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<RequestCondition<?>> getConditions() {
return unwrap();
}
private List<RequestCondition<?>> unwrap() {
List<RequestCondition<?>> result = new ArrayList<RequestCondition<?>>();
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 <em>all</em> contained conditions to match the request and return the
* resulting "matching" condition instances.
* <p>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;
}
}
}

52
spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/condition/RequestConditionHolder.java

@ -1,5 +1,5 @@ @@ -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; @@ -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.
*
* <p>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.
* <p>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<RequestConditionHolder> {
@SuppressWarnings("rawtypes")
private final RequestCondition condition;
private final RequestCondition<Object> 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<Object>) 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<Reque @@ -69,20 +71,19 @@ public final class RequestConditionHolder extends AbstractRequestCondition<Reque
* instances after making sure the conditions are of the same type.
* Or if one holder is empty, the other holder is returned.
*/
@SuppressWarnings("unchecked")
public RequestConditionHolder combine(RequestConditionHolder other) {
if (condition == null && other.condition == null) {
if (this.condition == null && other.condition == null) {
return this;
}
else if (condition == null) {
else if (this.condition == null) {
return other;
}
else if (other.condition == null) {
return this;
}
else {
assertIsCompatible(other);
RequestCondition<?> 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<Reque @@ -90,8 +91,8 @@ public final class RequestConditionHolder extends AbstractRequestCondition<Reque
/**
* Ensure the held request conditions are of the same type.
*/
private void assertIsCompatible(RequestConditionHolder other) {
Class<?> 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<Reque @@ -104,10 +105,10 @@ public final class RequestConditionHolder extends AbstractRequestCondition<Reque
* holder, return the same holder instance.
*/
public RequestConditionHolder getMatchingCondition(HttpServletRequest request) {
if (condition == null) {
if (this.condition == null) {
return this;
}
RequestCondition<?> 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 AbstractRequestCondition<Reque @@ -116,20 +117,19 @@ public final class RequestConditionHolder extends AbstractRequestCondition<Reque
* instances after making sure the conditions are of the same type.
* Or if one holder is empty, the other holder is preferred.
*/
@SuppressWarnings("unchecked")
public int compareTo(RequestConditionHolder other, HttpServletRequest request) {
if (condition == null && other.condition == null) {
if (this.condition == null && other.condition == null) {
return 0;
}
else if (condition == null) {
else if (this.condition == null) {
return 1;
}
else if (other.condition == null) {
return -1;
}
else {
assertIsCompatible(other);
return condition.compareTo(other.condition, request);
assertEqualConditionTypes(other);
return this.condition.compareTo(other.condition, request);
}
}

24
spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerMapping.java

@ -21,6 +21,8 @@ import java.lang.reflect.Method; @@ -21,6 +21,8 @@ import java.lang.reflect.Method;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.mvc.condition.AbstractRequestCondition;
import org.springframework.web.servlet.mvc.condition.CompositeRequestCondition;
import org.springframework.web.servlet.mvc.condition.ConsumesRequestCondition;
import org.springframework.web.servlet.mvc.condition.HeadersRequestCondition;
import org.springframework.web.servlet.mvc.condition.ParamsRequestCondition;
@ -114,26 +116,36 @@ public class RequestMappingHandlerMapping extends RequestMappingInfoHandlerMappi @@ -114,26 +116,36 @@ public class RequestMappingHandlerMapping extends RequestMappingInfoHandlerMappi
}
/**
* Provide a custom method-level request condition.
* Provide a custom type-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 method the handler method for which to create the condition
*
* <p>Consider 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
*
* <p>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;
}

141
spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/condition/CompositeRequestConditionTests.java

@ -0,0 +1,141 @@ @@ -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());
}
}

25
spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/condition/RequestConditionHolderTests.java

@ -1,5 +1,5 @@ @@ -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; @@ -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 { @@ -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"));

13
src/dist/changelog.txt vendored

@ -6,24 +6,25 @@ http://www.springsource.org @@ -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<String, String>
* add "excludedExceptions" property to SimpleUrlHandlerMapping
* add CompositeRequestCondition for use with multiple custom request mapping conditions
Changes in version 3.1.1 (2012-02-16)
-------------------------------------

Loading…
Cancel
Save