diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/condition/AbstractMediaTypeExpression.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/condition/AbstractMediaTypeExpression.java new file mode 100644 index 0000000000..8ae3974257 --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/condition/AbstractMediaTypeExpression.java @@ -0,0 +1,115 @@ +/* + * Copyright 2002-2016 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.reactive.result.condition; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.http.MediaType; +import org.springframework.web.HttpMediaTypeException; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.server.ServerWebExchange; + +/** + * Supports media type expressions as described in: + * {@link RequestMapping#consumes()} and {@link RequestMapping#produces()}. + * + * @author Rossen Stoyanchev + */ +abstract class AbstractMediaTypeExpression implements Comparable, MediaTypeExpression { + + protected final Log logger = LogFactory.getLog(getClass()); + + private final MediaType mediaType; + + private final boolean isNegated; + + + AbstractMediaTypeExpression(String expression) { + if (expression.startsWith("!")) { + this.isNegated = true; + expression = expression.substring(1); + } + else { + this.isNegated = false; + } + this.mediaType = MediaType.parseMediaType(expression); + } + + AbstractMediaTypeExpression(MediaType mediaType, boolean negated) { + this.mediaType = mediaType; + this.isNegated = negated; + } + + + @Override + public MediaType getMediaType() { + return this.mediaType; + } + + @Override + public boolean isNegated() { + return this.isNegated; + } + + + public final boolean match(ServerWebExchange exchange) { + try { + boolean match = matchMediaType(exchange); + return (!this.isNegated == match); + } + catch (HttpMediaTypeException ex) { + return false; + } + } + + protected abstract boolean matchMediaType(ServerWebExchange exchange) throws HttpMediaTypeException; + + + @Override + public int compareTo(AbstractMediaTypeExpression other) { + return MediaType.SPECIFICITY_COMPARATOR.compare(this.getMediaType(), other.getMediaType()); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj != null && getClass() == obj.getClass()) { + AbstractMediaTypeExpression other = (AbstractMediaTypeExpression) obj; + return (this.mediaType.equals(other.mediaType) && this.isNegated == other.isNegated); + } + return false; + } + + @Override + public int hashCode() { + return this.mediaType.hashCode(); + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + if (this.isNegated) { + builder.append('!'); + } + builder.append(this.mediaType.toString()); + return builder.toString(); + } + +} diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/condition/AbstractNameValueExpression.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/condition/AbstractNameValueExpression.java new file mode 100644 index 0000000000..9fbbdb2ef7 --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/condition/AbstractNameValueExpression.java @@ -0,0 +1,127 @@ +/* + * Copyright 2002-2016 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.reactive.result.condition; + +import org.springframework.web.server.ServerWebExchange; + +/** + * Supports "name=value" style expressions as described in: + * {@link org.springframework.web.bind.annotation.RequestMapping#params()} and + * {@link org.springframework.web.bind.annotation.RequestMapping#headers()}. + * + * @author Rossen Stoyanchev + */ +abstract class AbstractNameValueExpression implements NameValueExpression { + + protected final String name; + + protected final T value; + + protected final boolean isNegated; + + AbstractNameValueExpression(String expression) { + int separator = expression.indexOf('='); + if (separator == -1) { + this.isNegated = expression.startsWith("!"); + this.name = isNegated ? expression.substring(1) : expression; + this.value = null; + } + else { + this.isNegated = (separator > 0) && (expression.charAt(separator - 1) == '!'); + this.name = isNegated ? expression.substring(0, separator - 1) : expression.substring(0, separator); + this.value = parseValue(expression.substring(separator + 1)); + } + } + + @Override + public String getName() { + return this.name; + } + + @Override + public T getValue() { + return this.value; + } + + @Override + public boolean isNegated() { + return this.isNegated; + } + + protected abstract boolean isCaseSensitiveName(); + + protected abstract T parseValue(String valueExpression); + + public final boolean match(ServerWebExchange exchange) { + boolean isMatch; + if (this.value != null) { + isMatch = matchValue(exchange); + } + else { + isMatch = matchName(exchange); + } + return this.isNegated != isMatch; + } + + protected abstract boolean matchName(ServerWebExchange exchange); + + protected abstract boolean matchValue(ServerWebExchange exchange); + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj != null && obj instanceof AbstractNameValueExpression) { + AbstractNameValueExpression other = (AbstractNameValueExpression) obj; + String thisName = isCaseSensitiveName() ? this.name : this.name.toLowerCase(); + String otherName = isCaseSensitiveName() ? other.name : other.name.toLowerCase(); + return ((thisName.equalsIgnoreCase(otherName)) && + (this.value != null ? this.value.equals(other.value) : other.value == null) && + this.isNegated == other.isNegated); + } + return false; + } + + @Override + public int hashCode() { + int result = isCaseSensitiveName() ? name.hashCode() : name.toLowerCase().hashCode(); + result = 31 * result + (value != null ? value.hashCode() : 0); + result = 31 * result + (isNegated ? 1 : 0); + return result; + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + if (value != null) { + builder.append(name); + if (isNegated) { + builder.append('!'); + } + builder.append('='); + builder.append(value); + } + else { + if (isNegated) { + builder.append('!'); + } + builder.append(name); + } + return builder.toString(); + } +} diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/condition/AbstractRequestCondition.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/condition/AbstractRequestCondition.java new file mode 100644 index 0000000000..8b6edb5b68 --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/condition/AbstractRequestCondition.java @@ -0,0 +1,86 @@ +/* + * Copyright 2002-2016 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.reactive.result.condition; + +import java.util.Collection; +import java.util.Iterator; + +/** + * A base class for {@link RequestCondition} types providing implementations of + * {@link #equals(Object)}, {@link #hashCode()}, and {@link #toString()}. + * + * @author Rossen Stoyanchev + */ +public abstract class AbstractRequestCondition> + implements RequestCondition { + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj != null && getClass() == obj.getClass()) { + AbstractRequestCondition other = (AbstractRequestCondition) obj; + return getContent().equals(other.getContent()); + } + return false; + } + + @Override + public int hashCode() { + return getContent().hashCode(); + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder("["); + for (Iterator iterator = getContent().iterator(); iterator.hasNext();) { + Object expression = iterator.next(); + builder.append(expression.toString()); + if (iterator.hasNext()) { + builder.append(getToStringInfix()); + } + } + builder.append("]"); + return builder.toString(); + } + + /** + * Indicates whether this condition is empty, i.e. whether or not it + * contains any discrete items. + * @return {@code true} if empty; {@code false} otherwise + */ + public boolean isEmpty() { + return getContent().isEmpty(); + } + + + /** + * Return the discrete items a request condition is composed of. + *

For example URL patterns, HTTP request methods, param expressions, etc. + * @return a collection of objects, never {@code null} + */ + protected abstract Collection getContent(); + + /** + * The notation to use when printing discrete items of content. + *

For example {@code " || "} for URL patterns or {@code " && "} + * for param expressions. + */ + protected abstract String getToStringInfix(); + +} diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/condition/CompositeRequestCondition.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/condition/CompositeRequestCondition.java new file mode 100644 index 0000000000..8d70918a55 --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/condition/CompositeRequestCondition.java @@ -0,0 +1,184 @@ +/* + * Copyright 2002-2016 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.reactive.result.condition; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; + +import org.springframework.util.Assert; +import org.springframework.util.ObjectUtils; +import org.springframework.web.server.ServerWebExchange; + +/** + * 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 + */ +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 CompositeRequestCondition(RequestConditionHolder[] requestConditions) { + this.requestConditions = 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; + } + + /** + * 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. + */ + @Override + 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. " + + ObjectUtils.nullSafeToString(this.requestConditions) + " and " + + ObjectUtils.nullSafeToString(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. + */ + @Override + public CompositeRequestCondition getMatchingCondition(ServerWebExchange exchange) { + if (isEmpty()) { + return this; + } + RequestConditionHolder[] matchingConditions = new RequestConditionHolder[getLength()]; + for (int i = 0; i < getLength(); i++) { + matchingConditions[i] = this.requestConditions[i].getMatchingCondition(exchange); + 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. + */ + @Override + public int compareTo(CompositeRequestCondition other, ServerWebExchange exchange) { + 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], exchange); + if (result != 0) { + return result; + } + } + return 0; + } + } + +} diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/condition/ConsumesRequestCondition.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/condition/ConsumesRequestCondition.java new file mode 100644 index 0000000000..02c5907e2f --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/condition/ConsumesRequestCondition.java @@ -0,0 +1,234 @@ +/* + * Copyright 2002-2016 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.reactive.result.condition; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Iterator; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; + +import org.springframework.http.InvalidMediaTypeException; +import org.springframework.http.MediaType; +import org.springframework.web.HttpMediaTypeNotSupportedException; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.server.ServerWebExchange; + +/** + * A logical disjunction (' || ') request condition to match a request's + * 'Content-Type' header to a list of media type expressions. Two kinds of + * media type expressions are supported, which are described in + * {@link RequestMapping#consumes()} and {@link RequestMapping#headers()} + * where the header name is 'Content-Type'. Regardless of which syntax is + * used, the semantics are the same. + * + * @author Rossen Stoyanchev + */ +public final class ConsumesRequestCondition extends AbstractRequestCondition { + +// private final static ConsumesRequestCondition PRE_FLIGHT_MATCH = new ConsumesRequestCondition(); + + + private final List expressions; + + + /** + * Creates a new instance from 0 or more "consumes" expressions. + * @param consumes expressions with the syntax described in + * {@link RequestMapping#consumes()}; if 0 expressions are provided, + * the condition will match to every request + */ + public ConsumesRequestCondition(String... consumes) { + this(consumes, null); + } + + /** + * Creates a new instance with "consumes" and "header" expressions. + * "Header" expressions where the header name is not 'Content-Type' or have + * no header value defined are ignored. If 0 expressions are provided in + * total, the condition will match to every request + * @param consumes as described in {@link RequestMapping#consumes()} + * @param headers as described in {@link RequestMapping#headers()} + */ + public ConsumesRequestCondition(String[] consumes, String[] headers) { + this(parseExpressions(consumes, headers)); + } + + /** + * Private constructor accepting parsed media type expressions. + */ + private ConsumesRequestCondition(Collection expressions) { + this.expressions = new ArrayList<>(expressions); + Collections.sort(this.expressions); + } + + + private static Set parseExpressions(String[] consumes, String[] headers) { + Set result = new LinkedHashSet<>(); + if (headers != null) { + for (String header : headers) { + HeadersRequestCondition.HeaderExpression expr = new HeadersRequestCondition.HeaderExpression(header); + if ("Content-Type".equalsIgnoreCase(expr.name)) { + for (MediaType mediaType : MediaType.parseMediaTypes(expr.value)) { + result.add(new ConsumeMediaTypeExpression(mediaType, expr.isNegated)); + } + } + } + } + if (consumes != null) { + for (String consume : consumes) { + result.add(new ConsumeMediaTypeExpression(consume)); + } + } + return result; + } + + + /** + * Return the contained MediaType expressions. + */ + public Set getExpressions() { + return new LinkedHashSet<>(this.expressions); + } + + /** + * Returns the media types for this condition excluding negated expressions. + */ + public Set getConsumableMediaTypes() { + Set result = new LinkedHashSet<>(); + for (ConsumeMediaTypeExpression expression : this.expressions) { + if (!expression.isNegated()) { + result.add(expression.getMediaType()); + } + } + return result; + } + + /** + * Whether the condition has any media type expressions. + */ + public boolean isEmpty() { + return this.expressions.isEmpty(); + } + + @Override + protected Collection getContent() { + return this.expressions; + } + + @Override + protected String getToStringInfix() { + return " || "; + } + + /** + * Returns the "other" instance if it has any expressions; returns "this" + * instance otherwise. Practically that means a method-level "consumes" + * overrides a type-level "consumes" condition. + */ + @Override + public ConsumesRequestCondition combine(ConsumesRequestCondition other) { + return !other.expressions.isEmpty() ? other : this; + } + + /** + * Checks if any of the contained media type expressions match the given + * request 'Content-Type' header and returns an instance that is guaranteed + * to contain matching expressions only. The match is performed via + * {@link MediaType#includes(MediaType)}. + * @param exchange the current exchange + * @return the same instance if the condition contains no expressions; + * or a new condition with matching expressions only; + * or {@code null} if no expressions match. + */ + @Override + public ConsumesRequestCondition getMatchingCondition(ServerWebExchange exchange) { +// if (CorsUtils.isPreFlightRequest(request)) { +// return PRE_FLIGHT_MATCH; +// } + if (isEmpty()) { + return this; + } + Set result = new LinkedHashSet<>(expressions); + for (Iterator iterator = result.iterator(); iterator.hasNext();) { + ConsumeMediaTypeExpression expression = iterator.next(); + if (!expression.match(exchange)) { + iterator.remove(); + } + } + return (result.isEmpty()) ? null : new ConsumesRequestCondition(result); + } + + /** + * Returns: + *

    + *
  • 0 if the two conditions have the same number of expressions + *
  • Less than 0 if "this" has more or more specific media type expressions + *
  • Greater than 0 if "other" has more or more specific media type expressions + *
+ *

It is assumed that both instances have been obtained via + * {@link #getMatchingCondition(ServerWebExchange)} and each instance contains + * the matching consumable media type expression only or is otherwise empty. + */ + @Override + public int compareTo(ConsumesRequestCondition other, ServerWebExchange exchange) { + if (this.expressions.isEmpty() && other.expressions.isEmpty()) { + return 0; + } + else if (this.expressions.isEmpty()) { + return 1; + } + else if (other.expressions.isEmpty()) { + return -1; + } + else { + return this.expressions.get(0).compareTo(other.expressions.get(0)); + } + } + + + /** + * Parses and matches a single media type expression to a request's 'Content-Type' header. + */ + static class ConsumeMediaTypeExpression extends AbstractMediaTypeExpression { + + ConsumeMediaTypeExpression(String expression) { + super(expression); + } + + ConsumeMediaTypeExpression(MediaType mediaType, boolean negated) { + super(mediaType, negated); + } + + @Override + protected boolean matchMediaType(ServerWebExchange exchange) throws HttpMediaTypeNotSupportedException { + try { + MediaType contentType = exchange.getRequest().getHeaders().getContentType(); + contentType = (contentType != null ? contentType : MediaType.APPLICATION_OCTET_STREAM); + return getMediaType().includes(contentType); + } + catch (InvalidMediaTypeException ex) { + throw new HttpMediaTypeNotSupportedException("Can't parse Content-Type [" + + exchange.getRequest().getHeaders().getFirst("Content-Type") + + "]: " + ex.getMessage()); + } + } + } + +} diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/condition/HeadersRequestCondition.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/condition/HeadersRequestCondition.java new file mode 100644 index 0000000000..14c97a8b41 --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/condition/HeadersRequestCondition.java @@ -0,0 +1,167 @@ +/* + * Copyright 2002-2016 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.reactive.result.condition; + +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.Set; + +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.server.ServerWebExchange; + +/** + * A logical conjunction (' && ') request condition that matches a request against + * a set of header expressions with syntax defined in {@link RequestMapping#headers()}. + * + *

Expressions passed to the constructor with header names 'Accept' or + * 'Content-Type' are ignored. See {@link ConsumesRequestCondition} and + * {@link ProducesRequestCondition} for those. + * + * @author Rossen Stoyanchev + */ +public final class HeadersRequestCondition extends AbstractRequestCondition { + +// private final static HeadersRequestCondition PRE_FLIGHT_MATCH = new HeadersRequestCondition(); + + + private final Set expressions; + + + /** + * Create a new instance from the given header expressions. Expressions with + * header names 'Accept' or 'Content-Type' are ignored. See {@link ConsumesRequestCondition} + * and {@link ProducesRequestCondition} for those. + * @param headers media type expressions with syntax defined in {@link RequestMapping#headers()}; + * if 0, the condition will match to every request + */ + public HeadersRequestCondition(String... headers) { + this(parseExpressions(headers)); + } + + private HeadersRequestCondition(Collection conditions) { + this.expressions = Collections.unmodifiableSet(new LinkedHashSet<>(conditions)); + } + + + private static Collection parseExpressions(String... headers) { + Set expressions = new LinkedHashSet(); + if (headers != null) { + for (String header : headers) { + HeaderExpression expr = new HeaderExpression(header); + if ("Accept".equalsIgnoreCase(expr.name) || "Content-Type".equalsIgnoreCase(expr.name)) { + continue; + } + expressions.add(expr); + } + } + return expressions; + } + + /** + * Return the contained request header expressions. + */ + public Set> getExpressions() { + return new LinkedHashSet<>(this.expressions); + } + + @Override + protected Collection getContent() { + return this.expressions; + } + + @Override + protected String getToStringInfix() { + return " && "; + } + + /** + * Returns a new instance with the union of the header expressions + * from "this" and the "other" instance. + */ + @Override + public HeadersRequestCondition combine(HeadersRequestCondition other) { + Set set = new LinkedHashSet<>(this.expressions); + set.addAll(other.expressions); + return new HeadersRequestCondition(set); + } + + /** + * Returns "this" instance if the request matches all expressions; + * or {@code null} otherwise. + */ + @Override + public HeadersRequestCondition getMatchingCondition(ServerWebExchange exchange) { +// if (CorsUtils.isPreFlightRequest(request)) { +// return PRE_FLIGHT_MATCH; +// } + for (HeaderExpression expression : expressions) { + if (!expression.match(exchange)) { + return null; + } + } + return this; + } + + /** + * Returns: + *

    + *
  • 0 if the two conditions have the same number of header expressions + *
  • Less than 0 if "this" instance has more header expressions + *
  • Greater than 0 if the "other" instance has more header expressions + *
+ *

It is assumed that both instances have been obtained via + * {@link #getMatchingCondition(ServerWebExchange)} and each instance + * contains the matching header expression only or is otherwise empty. + */ + @Override + public int compareTo(HeadersRequestCondition other, ServerWebExchange exchange) { + return other.expressions.size() - this.expressions.size(); + } + + + /** + * Parses and matches a single header expression to a request. + */ + static class HeaderExpression extends AbstractNameValueExpression { + + public HeaderExpression(String expression) { + super(expression); + } + + @Override + protected boolean isCaseSensitiveName() { + return false; + } + + @Override + protected String parseValue(String valueExpression) { + return valueExpression; + } + + @Override + protected boolean matchName(ServerWebExchange exchange) { + return exchange.getRequest().getHeaders().get(name) != null; + } + + @Override + protected boolean matchValue(ServerWebExchange exchange) { + return value.equals(exchange.getRequest().getHeaders().getFirst(name)); + } + } + +} diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/condition/MediaTypeExpression.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/condition/MediaTypeExpression.java new file mode 100644 index 0000000000..6e9a3265d7 --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/condition/MediaTypeExpression.java @@ -0,0 +1,35 @@ +/* + * Copyright 2002-2016 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.reactive.result.condition; + +import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.RequestMapping; + +/** + * A contract for media type expressions (e.g. "text/plain", "!text/plain") as + * defined in the {@code @RequestMapping} annotation for "consumes" and + * "produces" conditions. + * + * @author Rossen Stoyanchev + */ +public interface MediaTypeExpression { + + MediaType getMediaType(); + + boolean isNegated(); + +} diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/condition/NameValueExpression.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/condition/NameValueExpression.java new file mode 100644 index 0000000000..8818330c02 --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/condition/NameValueExpression.java @@ -0,0 +1,35 @@ +/* + * Copyright 2002-2016 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.reactive.result.condition; + +import org.springframework.web.bind.annotation.RequestMapping; + +/** + * A contract for {@code "name!=value"} style expression used to specify request + * parameters and request header conditions in {@code @RequestMapping}. + * + * @author Rossen Stoyanchev + */ +public interface NameValueExpression { + + String getName(); + + T getValue(); + + boolean isNegated(); + +} diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/condition/ParamsRequestCondition.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/condition/ParamsRequestCondition.java new file mode 100644 index 0000000000..b3829d1a6d --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/condition/ParamsRequestCondition.java @@ -0,0 +1,152 @@ +/* + * Copyright 2002-2016 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.reactive.result.condition; + +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.Set; + +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.server.ServerWebExchange; + +/** + * A logical conjunction (' && ') request condition that matches a request against + * a set parameter expressions with syntax defined in {@link RequestMapping#params()}. + * + * @author Rossen Stoyanchev + */ +public final class ParamsRequestCondition extends AbstractRequestCondition { + + private final Set expressions; + + + /** + * Create a new instance from the given param expressions. + * @param params expressions with syntax defined in {@link RequestMapping#params()}; + * if 0, the condition will match to every request. + */ + public ParamsRequestCondition(String... params) { + this(parseExpressions(params)); + } + + private ParamsRequestCondition(Collection conditions) { + this.expressions = Collections.unmodifiableSet(new LinkedHashSet<>(conditions)); + } + + + private static Collection parseExpressions(String... params) { + Set expressions = new LinkedHashSet<>(); + if (params != null) { + for (String param : params) { + expressions.add(new ParamExpression(param)); + } + } + return expressions; + } + + + /** + * Return the contained request parameter expressions. + */ + public Set> getExpressions() { + return new LinkedHashSet<>(this.expressions); + } + + @Override + protected Collection getContent() { + return this.expressions; + } + + @Override + protected String getToStringInfix() { + return " && "; + } + + /** + * Returns a new instance with the union of the param expressions + * from "this" and the "other" instance. + */ + @Override + public ParamsRequestCondition combine(ParamsRequestCondition other) { + Set set = new LinkedHashSet<>(this.expressions); + set.addAll(other.expressions); + return new ParamsRequestCondition(set); + } + + /** + * Returns "this" instance if the request matches all param expressions; + * or {@code null} otherwise. + */ + @Override + public ParamsRequestCondition getMatchingCondition(ServerWebExchange exchange) { + for (ParamExpression expression : expressions) { + if (!expression.match(exchange)) { + return null; + } + } + return this; + } + + /** + * Returns: + *

    + *
  • 0 if the two conditions have the same number of parameter expressions + *
  • Less than 0 if "this" instance has more parameter expressions + *
  • Greater than 0 if the "other" instance has more parameter expressions + *
+ *

It is assumed that both instances have been obtained via + * {@link #getMatchingCondition(ServerWebExchange)} and each instance + * contains the matching parameter expressions only or is otherwise empty. + */ + @Override + public int compareTo(ParamsRequestCondition other, ServerWebExchange exchange) { + return (other.expressions.size() - this.expressions.size()); + } + + + /** + * Parses and matches a single param expression to a request. + */ + static class ParamExpression extends AbstractNameValueExpression { + + ParamExpression(String expression) { + super(expression); + } + + @Override + protected boolean isCaseSensitiveName() { + return true; + } + + @Override + protected String parseValue(String valueExpression) { + return valueExpression; + } + + @Override + protected boolean matchName(ServerWebExchange exchange) { + return exchange.getRequest().getQueryParams().containsKey(this.name); + } + + @Override + protected boolean matchValue(ServerWebExchange exchange) { + return this.value.equals(exchange.getRequest().getQueryParams().getFirst(this.name)); + } + } + +} diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/condition/PatternsRequestCondition.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/condition/PatternsRequestCondition.java new file mode 100644 index 0000000000..cba2fe75ef --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/condition/PatternsRequestCondition.java @@ -0,0 +1,285 @@ +/* + * Copyright 2002-2016 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.reactive.result.condition; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.Iterator; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; + +import org.springframework.util.AntPathMatcher; +import org.springframework.util.PathMatcher; +import org.springframework.util.StringUtils; +import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.util.HttpRequestPathHelper; + +/** + * A logical disjunction (' || ') request condition that matches a request + * against a set of URL path patterns. + * + * @author Rossen Stoyanchev + */ +public final class PatternsRequestCondition extends AbstractRequestCondition { + + private final Set patterns; + + private final HttpRequestPathHelper pathHelper; + + private final PathMatcher pathMatcher; + + private final boolean useSuffixPatternMatch; + + private final boolean useTrailingSlashMatch; + + private final List fileExtensions = new ArrayList(); + + + /** + * Creates a new instance with the given URL patterns. + * Each pattern that is not empty and does not start with "/" is prepended with "/". + * @param patterns 0 or more URL patterns; if 0 the condition will match to every request. + */ + public PatternsRequestCondition(String... patterns) { + this(asList(patterns), null, null, true, true, null); + } + + /** + * Creates a new instance with the given URL patterns. + * Each pattern that is not empty and does not start with "/" is pre-pended with "/". + * @param patterns the URL patterns to use; if 0, the condition will match to every request. + * @param pathHelper to determine the lookup path for a request + * @param pathMatcher for pattern path matching + * @param useSuffixPatternMatch whether to enable matching by suffix (".*") + * @param useTrailingSlashMatch whether to match irrespective of a trailing slash + * @param extensions file extensions to consider for path matching + */ + public PatternsRequestCondition(String[] patterns, HttpRequestPathHelper pathHelper, + PathMatcher pathMatcher, boolean useSuffixPatternMatch, boolean useTrailingSlashMatch, + List extensions) { + + this(asList(patterns), pathHelper, pathMatcher, useSuffixPatternMatch, useTrailingSlashMatch, extensions); + } + + /** + * Private constructor accepting a collection of patterns. + */ + private PatternsRequestCondition(Collection patterns, HttpRequestPathHelper pathHelper, + PathMatcher pathMatcher, boolean useSuffixPatternMatch, boolean useTrailingSlashMatch, + List fileExtensions) { + + this.patterns = Collections.unmodifiableSet(prependLeadingSlash(patterns)); + this.pathHelper = (pathHelper != null ? pathHelper : new HttpRequestPathHelper()); + this.pathMatcher = (pathMatcher != null ? pathMatcher : new AntPathMatcher()); + this.useSuffixPatternMatch = useSuffixPatternMatch; + this.useTrailingSlashMatch = useTrailingSlashMatch; + if (fileExtensions != null) { + for (String fileExtension : fileExtensions) { + if (fileExtension.charAt(0) != '.') { + fileExtension = "." + fileExtension; + } + this.fileExtensions.add(fileExtension); + } + } + } + + + private static List asList(String... patterns) { + return (patterns != null ? Arrays.asList(patterns) : Collections.emptyList()); + } + + private static Set prependLeadingSlash(Collection patterns) { + if (patterns == null) { + return Collections.emptySet(); + } + Set result = new LinkedHashSet<>(patterns.size()); + for (String pattern : patterns) { + if (StringUtils.hasLength(pattern) && !pattern.startsWith("/")) { + pattern = "/" + pattern; + } + result.add(pattern); + } + return result; + } + + public Set getPatterns() { + return this.patterns; + } + + @Override + protected Collection getContent() { + return this.patterns; + } + + @Override + protected String getToStringInfix() { + return " || "; + } + + /** + * Returns a new instance with URL patterns from the current instance ("this") and + * the "other" instance as follows: + *

    + *
  • If there are patterns in both instances, combine the patterns in "this" with + * the patterns in "other" using {@link PathMatcher#combine(String, String)}. + *
  • If only one instance has patterns, use them. + *
  • If neither instance has patterns, use an empty String (i.e. ""). + *
+ */ + @Override + public PatternsRequestCondition combine(PatternsRequestCondition other) { + Set result = new LinkedHashSet<>(); + if (!this.patterns.isEmpty() && !other.patterns.isEmpty()) { + for (String pattern1 : this.patterns) { + for (String pattern2 : other.patterns) { + result.add(this.pathMatcher.combine(pattern1, pattern2)); + } + } + } + else if (!this.patterns.isEmpty()) { + result.addAll(this.patterns); + } + else if (!other.patterns.isEmpty()) { + result.addAll(other.patterns); + } + else { + result.add(""); + } + return new PatternsRequestCondition(result, this.pathHelper, this.pathMatcher, this.useSuffixPatternMatch, + this.useTrailingSlashMatch, this.fileExtensions); + } + + /** + * Checks if any of the patterns match the given request and returns an instance + * that is guaranteed to contain matching patterns, sorted via + * {@link PathMatcher#getPatternComparator(String)}. + *

A matching pattern is obtained by making checks in the following order: + *

    + *
  • Direct match + *
  • Pattern match with ".*" appended if the pattern doesn't already contain a "." + *
  • Pattern match + *
  • Pattern match with "/" appended if the pattern doesn't already end in "/" + *
+ * @param exchange the current exchange + * @return the same instance if the condition contains no patterns; + * or a new condition with sorted matching patterns; + * or {@code null} if no patterns match. + */ + @Override + public PatternsRequestCondition getMatchingCondition(ServerWebExchange exchange) { + if (this.patterns.isEmpty()) { + return this; + } + + String lookupPath = this.pathHelper.getLookupPathForRequest(exchange); + List matches = getMatchingPatterns(lookupPath); + + return matches.isEmpty() ? null : + new PatternsRequestCondition(matches, this.pathHelper, this.pathMatcher, this.useSuffixPatternMatch, + this.useTrailingSlashMatch, this.fileExtensions); + } + + /** + * Find the patterns matching the given lookup path. Invoking this method should + * yield results equivalent to those of calling + * {@link #getMatchingCondition(ServerWebExchange)}. + * This method is provided as an alternative to be used if no request is available + * (e.g. introspection, tooling, etc). + * @param lookupPath the lookup path to match to existing patterns + * @return a collection of matching patterns sorted with the closest match at the top + */ + public List getMatchingPatterns(String lookupPath) { + List matches = new ArrayList<>(); + for (String pattern : this.patterns) { + String match = getMatchingPattern(pattern, lookupPath); + if (match != null) { + matches.add(match); + } + } + Collections.sort(matches, this.pathMatcher.getPatternComparator(lookupPath)); + return matches; + } + + private String getMatchingPattern(String pattern, String lookupPath) { + if (pattern.equals(lookupPath)) { + return pattern; + } + if (this.useSuffixPatternMatch) { + if (!this.fileExtensions.isEmpty() && lookupPath.indexOf('.') != -1) { + for (String extension : this.fileExtensions) { + if (this.pathMatcher.match(pattern + extension, lookupPath)) { + return pattern + extension; + } + } + } + else { + boolean hasSuffix = pattern.indexOf('.') != -1; + if (!hasSuffix && this.pathMatcher.match(pattern + ".*", lookupPath)) { + return pattern + ".*"; + } + } + } + if (this.pathMatcher.match(pattern, lookupPath)) { + return pattern; + } + if (this.useTrailingSlashMatch) { + if (!pattern.endsWith("/") && this.pathMatcher.match(pattern + "/", lookupPath)) { + return pattern +"/"; + } + } + return null; + } + + /** + * Compare the two conditions based on the URL patterns they contain. + * Patterns are compared one at a time, from top to bottom via + * {@link PathMatcher#getPatternComparator(String)}. If all compared + * patterns match equally, but one instance has more patterns, it is + * considered a closer match. + *

It is assumed that both instances have been obtained via + * {@link #getMatchingCondition(ServerWebExchange)} to ensure they + * contain only patterns that match the request and are sorted with + * the best matches on top. + */ + @Override + public int compareTo(PatternsRequestCondition other, ServerWebExchange exchange) { + String lookupPath = this.pathHelper.getLookupPathForRequest(exchange); + Comparator patternComparator = this.pathMatcher.getPatternComparator(lookupPath); + Iterator iterator = this.patterns.iterator(); + Iterator iteratorOther = other.patterns.iterator(); + while (iterator.hasNext() && iteratorOther.hasNext()) { + int result = patternComparator.compare(iterator.next(), iteratorOther.next()); + if (result != 0) { + return result; + } + } + if (iterator.hasNext()) { + return -1; + } + else if (iteratorOther.hasNext()) { + return 1; + } + else { + return 0; + } + } + +} diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/condition/ProducesRequestCondition.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/condition/ProducesRequestCondition.java new file mode 100644 index 0000000000..d9943b009a --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/condition/ProducesRequestCondition.java @@ -0,0 +1,318 @@ +/* + * Copyright 2002-2016 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.reactive.result.condition; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Iterator; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; + +import org.springframework.http.MediaType; +import org.springframework.web.HttpMediaTypeNotAcceptableException; +import org.springframework.web.accept.ContentNegotiationManager; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.reactive.accept.ContentTypeResolver; +import org.springframework.web.reactive.accept.HeaderContentTypeResolver; +import org.springframework.web.server.ServerWebExchange; + +/** + * A logical disjunction (' || ') request condition to match a request's 'Accept' header + * to a list of media type expressions. Two kinds of media type expressions are + * supported, which are described in {@link RequestMapping#produces()} and + * {@link RequestMapping#headers()} where the header name is 'Accept'. + * Regardless of which syntax is used, the semantics are the same. + * + * @author Rossen Stoyanchev + */ +public final class ProducesRequestCondition extends AbstractRequestCondition { + +// private final static ProducesRequestCondition PRE_FLIGHT_MATCH = new ProducesRequestCondition(); + + + private final List MEDIA_TYPE_ALL_LIST = + Collections.singletonList(new ProduceMediaTypeExpression("*/*")); + + private final List expressions; + + private final ContentTypeResolver contentTypeResolver; + + + /** + * Creates a new instance from "produces" expressions. If 0 expressions + * are provided in total, this condition will match to any request. + * @param produces expressions with syntax defined by {@link RequestMapping#produces()} + */ + public ProducesRequestCondition(String... produces) { + this(produces, null); + } + + /** + * Creates a new instance with "produces" and "header" expressions. "Header" + * expressions where the header name is not 'Accept' or have no header value + * defined are ignored. If 0 expressions are provided in total, this condition + * will match to any request. + * @param produces expressions with syntax defined by {@link RequestMapping#produces()} + * @param headers expressions with syntax defined by {@link RequestMapping#headers()} + */ + public ProducesRequestCondition(String[] produces, String[] headers) { + this(produces, headers, null); + } + + /** + * Same as {@link #ProducesRequestCondition(String[], String[])} but also + * accepting a {@link ContentNegotiationManager}. + * @param produces expressions with syntax defined by {@link RequestMapping#produces()} + * @param headers expressions with syntax defined by {@link RequestMapping#headers()} + * @param resolver used to determine requested content type + */ + public ProducesRequestCondition(String[] produces, String[] headers, ContentTypeResolver resolver) { + this.expressions = new ArrayList<>(parseExpressions(produces, headers)); + Collections.sort(this.expressions); + this.contentTypeResolver = (resolver != null ? resolver : new HeaderContentTypeResolver()); + } + + /** + * Private constructor with already parsed media type expressions. + */ + private ProducesRequestCondition(Collection expressions, + ContentTypeResolver resolver) { + + this.expressions = new ArrayList<>(expressions); + Collections.sort(this.expressions); + this.contentTypeResolver = (resolver != null ? resolver : new HeaderContentTypeResolver()); + } + + + private Set parseExpressions(String[] produces, String[] headers) { + Set result = new LinkedHashSet<>(); + if (headers != null) { + for (String header : headers) { + HeadersRequestCondition.HeaderExpression expr = new HeadersRequestCondition.HeaderExpression(header); + if ("Accept".equalsIgnoreCase(expr.name)) { + for (MediaType mediaType : MediaType.parseMediaTypes(expr.value)) { + result.add(new ProduceMediaTypeExpression(mediaType, expr.isNegated)); + } + } + } + } + if (produces != null) { + for (String produce : produces) { + result.add(new ProduceMediaTypeExpression(produce)); + } + } + return result; + } + + /** + * Return the contained "produces" expressions. + */ + public Set getExpressions() { + return new LinkedHashSet<>(this.expressions); + } + + /** + * Return the contained producible media types excluding negated expressions. + */ + public Set getProducibleMediaTypes() { + Set result = new LinkedHashSet<>(); + for (ProduceMediaTypeExpression expression : this.expressions) { + if (!expression.isNegated()) { + result.add(expression.getMediaType()); + } + } + return result; + } + + /** + * Whether the condition has any media type expressions. + */ + public boolean isEmpty() { + return this.expressions.isEmpty(); + } + + @Override + protected List getContent() { + return this.expressions; + } + + @Override + protected String getToStringInfix() { + return " || "; + } + + /** + * Returns the "other" instance if it has any expressions; returns "this" + * instance otherwise. Practically that means a method-level "produces" + * overrides a type-level "produces" condition. + */ + @Override + public ProducesRequestCondition combine(ProducesRequestCondition other) { + return (!other.expressions.isEmpty() ? other : this); + } + + /** + * Checks if any of the contained media type expressions match the given + * request 'Content-Type' header and returns an instance that is guaranteed + * to contain matching expressions only. The match is performed via + * {@link MediaType#isCompatibleWith(MediaType)}. + * @param exchange the current exchange + * @return the same instance if there are no expressions; + * or a new condition with matching expressions; + * or {@code null} if no expressions match. + */ + @Override + public ProducesRequestCondition getMatchingCondition(ServerWebExchange exchange) { +// if (CorsUtils.isPreFlightRequest(request)) { +// return PRE_FLIGHT_MATCH; +// } + if (isEmpty()) { + return this; + } + Set result = new LinkedHashSet<>(expressions); + for (Iterator iterator = result.iterator(); iterator.hasNext();) { + ProduceMediaTypeExpression expression = iterator.next(); + if (!expression.match(exchange)) { + iterator.remove(); + } + } + return (result.isEmpty()) ? null : new ProducesRequestCondition(result, this.contentTypeResolver); + } + + /** + * Compares this and another "produces" condition as follows: + *

    + *
  1. Sort 'Accept' header media types by quality value via + * {@link MediaType#sortByQualityValue(List)} and iterate the list. + *
  2. Get the first index of matching media types in each "produces" + * condition first matching with {@link MediaType#equals(Object)} and + * then with {@link MediaType#includes(MediaType)}. + *
  3. If a lower index is found, the condition at that index wins. + *
  4. If both indexes are equal, the media types at the index are + * compared further with {@link MediaType#SPECIFICITY_COMPARATOR}. + *
+ *

It is assumed that both instances have been obtained via + * {@link #getMatchingCondition(ServerWebExchange)} and each instance + * contains the matching producible media type expression only or + * is otherwise empty. + */ + @Override + public int compareTo(ProducesRequestCondition other, ServerWebExchange exchange) { + try { + List acceptedMediaTypes = getAcceptedMediaTypes(exchange); + for (MediaType acceptedMediaType : acceptedMediaTypes) { + int thisIndex = this.indexOfEqualMediaType(acceptedMediaType); + int otherIndex = other.indexOfEqualMediaType(acceptedMediaType); + int result = compareMatchingMediaTypes(this, thisIndex, other, otherIndex); + if (result != 0) { + return result; + } + thisIndex = this.indexOfIncludedMediaType(acceptedMediaType); + otherIndex = other.indexOfIncludedMediaType(acceptedMediaType); + result = compareMatchingMediaTypes(this, thisIndex, other, otherIndex); + if (result != 0) { + return result; + } + } + return 0; + } + catch (HttpMediaTypeNotAcceptableException ex) { + // should never happen + throw new IllegalStateException("Cannot compare without having any requested media types", ex); + } + } + + private List getAcceptedMediaTypes(ServerWebExchange exchange) + throws HttpMediaTypeNotAcceptableException { + + List mediaTypes = this.contentTypeResolver.resolveMediaTypes(exchange); + return mediaTypes.isEmpty() ? Collections.singletonList(MediaType.ALL) : mediaTypes; + } + + private int indexOfEqualMediaType(MediaType mediaType) { + for (int i = 0; i < getExpressionsToCompare().size(); i++) { + MediaType currentMediaType = getExpressionsToCompare().get(i).getMediaType(); + if (mediaType.getType().equalsIgnoreCase(currentMediaType.getType()) && + mediaType.getSubtype().equalsIgnoreCase(currentMediaType.getSubtype())) { + return i; + } + } + return -1; + } + + private int indexOfIncludedMediaType(MediaType mediaType) { + for (int i = 0; i < getExpressionsToCompare().size(); i++) { + if (mediaType.includes(getExpressionsToCompare().get(i).getMediaType())) { + return i; + } + } + return -1; + } + + private int compareMatchingMediaTypes(ProducesRequestCondition condition1, int index1, + ProducesRequestCondition condition2, int index2) { + + int result = 0; + if (index1 != index2) { + result = index2 - index1; + } + else if (index1 != -1) { + ProduceMediaTypeExpression expr1 = condition1.getExpressionsToCompare().get(index1); + ProduceMediaTypeExpression expr2 = condition2.getExpressionsToCompare().get(index2); + result = expr1.compareTo(expr2); + result = (result != 0) ? result : expr1.getMediaType().compareTo(expr2.getMediaType()); + } + return result; + } + + /** + * Return the contained "produces" expressions or if that's empty, a list + * with a {@code MediaType_ALL} expression. + */ + private List getExpressionsToCompare() { + return (this.expressions.isEmpty() ? MEDIA_TYPE_ALL_LIST : this.expressions); + } + + + /** + * Parses and matches a single media type expression to a request's 'Accept' header. + */ + class ProduceMediaTypeExpression extends AbstractMediaTypeExpression { + + ProduceMediaTypeExpression(MediaType mediaType, boolean negated) { + super(mediaType, negated); + } + + ProduceMediaTypeExpression(String expression) { + super(expression); + } + + @Override + protected boolean matchMediaType(ServerWebExchange exchange) throws HttpMediaTypeNotAcceptableException { + List acceptedMediaTypes = getAcceptedMediaTypes(exchange); + for (MediaType acceptedMediaType : acceptedMediaTypes) { + if (getMediaType().isCompatibleWith(acceptedMediaType)) { + return true; + } + } + return false; + } + } + +} diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/condition/RequestCondition.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/condition/RequestCondition.java new file mode 100644 index 0000000000..99bc2f2aa8 --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/condition/RequestCondition.java @@ -0,0 +1,68 @@ +/* + * Copyright 2002-2016 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.reactive.result.condition; + +import javax.servlet.http.HttpServletRequest; + +import org.springframework.web.server.ServerWebExchange; + +/** + * Contract for request mapping conditions. + * + *

Request conditions can be combined via {@link #combine(Object)}, matched to + * a request via {@link #getMatchingCondition(ServerWebExchange)}, and compared + * to each other via {@link #compareTo(Object, ServerWebExchange)} to determine + * which is a closer match for a given request. + * + * @author Rossen Stoyanchev + * @param the type of objects that this RequestCondition can be combined + * with and compared to + */ +public interface RequestCondition { + + /** + * Combine this condition with another such as conditions from a + * type-level and method-level {@code @RequestMapping} annotation. + * @param other the condition to combine with. + * @return a request condition instance that is the result of combining + * the two condition instances. + */ + T combine(T other); + + /** + * Check if the condition matches the request returning a potentially new + * instance created for the current request. For example a condition with + * multiple URL patterns may return a new instance only with those patterns + * that match the request. + *

For CORS pre-flight requests, conditions should match to the would-be, + * actual request (e.g. URL pattern, query parameters, and the HTTP method + * from the "Access-Control-Request-Method" header). If a condition cannot + * be matched to a pre-flight request it should return an instance with + * empty content thus not causing a failure to match. + * @return a condition instance in case of a match or {@code null} otherwise. + */ + T getMatchingCondition(ServerWebExchange exchange); + + /** + * Compare this condition to another condition in the context of + * a specific request. This method assumes both instances have + * been obtained via {@link #getMatchingCondition(ServerWebExchange)} + * to ensure they have content relevant to current request only. + */ + int compareTo(T other, ServerWebExchange exchange); + +} diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/condition/RequestConditionHolder.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/condition/RequestConditionHolder.java new file mode 100644 index 0000000000..5e035f46a7 --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/condition/RequestConditionHolder.java @@ -0,0 +1,139 @@ +/* + * Copyright 2002-2016 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.reactive.result.condition; + +import java.util.Collection; +import java.util.Collections; + +import org.springframework.web.server.ServerWebExchange; + +/** + * 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. + * + *

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 + */ +public final class RequestConditionHolder extends AbstractRequestCondition { + + 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) requestCondition; + } + + + /** + * Return the held request condition, or {@code null} if not holding one. + */ + public RequestCondition getCondition() { + return this.condition; + } + + @Override + protected Collection getContent() { + return (this.condition != null ? Collections.singleton(this.condition) : Collections.emptyList()); + } + + @Override + protected String getToStringInfix() { + return " "; + } + + /** + * Combine the request conditions held by the two RequestConditionHolder + * instances after making sure the conditions are of the same type. + * Or if one holder is empty, the other holder is returned. + */ + @Override + public RequestConditionHolder combine(RequestConditionHolder other) { + if (this.condition == null && other.condition == null) { + return this; + } + else if (this.condition == null) { + return other; + } + else if (other.condition == null) { + return this; + } + else { + assertEqualConditionTypes(other); + RequestCondition combined = (RequestCondition) this.condition.combine(other.condition); + return new RequestConditionHolder(combined); + } + } + + /** + * Ensure the held request conditions are of the same type. + */ + 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); + } + } + + /** + * Get the matching condition for the held request condition wrap it in a + * new RequestConditionHolder instance. Or otherwise if this is an empty + * holder, return the same holder instance. + */ + @Override + public RequestConditionHolder getMatchingCondition(ServerWebExchange exchange) { + if (this.condition == null) { + return this; + } + RequestCondition match = (RequestCondition) this.condition.getMatchingCondition(exchange); + return (match != null ? new RequestConditionHolder(match) : null); + } + + /** + * Compare the request conditions held by the two RequestConditionHolder + * instances after making sure the conditions are of the same type. + * Or if one holder is empty, the other holder is preferred. + */ + @Override + public int compareTo(RequestConditionHolder other, ServerWebExchange exchange) { + if (this.condition == null && other.condition == null) { + return 0; + } + else if (this.condition == null) { + return 1; + } + else if (other.condition == null) { + return -1; + } + else { + assertEqualConditionTypes(other); + return this.condition.compareTo(other.condition, exchange); + } + } + +} diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/condition/RequestMethodsRequestCondition.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/condition/RequestMethodsRequestCondition.java new file mode 100644 index 0000000000..f8d8814da2 --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/condition/RequestMethodsRequestCondition.java @@ -0,0 +1,162 @@ +/* + * Copyright 2002-2016 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.reactive.result.condition; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; +import javax.servlet.http.HttpServletRequest; + +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.server.ServerWebExchange; + +/** + * A logical disjunction (' || ') request condition that matches a request + * against a set of {@link RequestMethod}s. + * + * @author Rossen Stoyanchev + */ +public final class RequestMethodsRequestCondition extends AbstractRequestCondition { + + private static final RequestMethodsRequestCondition HEAD_CONDITION = + new RequestMethodsRequestCondition(RequestMethod.HEAD); + + + private final Set methods; + + + /** + * Create a new instance with the given request methods. + * @param requestMethods 0 or more HTTP request methods; + * if, 0 the condition will match to every request + */ + public RequestMethodsRequestCondition(RequestMethod... requestMethods) { + this(asList(requestMethods)); + } + + private RequestMethodsRequestCondition(Collection requestMethods) { + this.methods = Collections.unmodifiableSet(new LinkedHashSet<>(requestMethods)); + } + + + private static List asList(RequestMethod... requestMethods) { + return (requestMethods != null ? Arrays.asList(requestMethods) : Collections.emptyList()); + } + + + /** + * Returns all {@link RequestMethod}s contained in this condition. + */ + public Set getMethods() { + return this.methods; + } + + @Override + protected Collection getContent() { + return this.methods; + } + + @Override + protected String getToStringInfix() { + return " || "; + } + + /** + * Returns a new instance with a union of the HTTP request methods + * from "this" and the "other" instance. + */ + @Override + public RequestMethodsRequestCondition combine(RequestMethodsRequestCondition other) { + Set set = new LinkedHashSet<>(this.methods); + set.addAll(other.methods); + return new RequestMethodsRequestCondition(set); + } + + /** + * Check if any of the HTTP request methods match the given request and + * return an instance that contains the matching HTTP request method only. + * @param exchange the current exchange + * @return the same instance if the condition is empty (unless the request + * method is HTTP OPTIONS), a new condition with the matched request method, + * or {@code null} if there is no match or the condition is empty and the + * request method is OPTIONS. + */ + @Override + public RequestMethodsRequestCondition getMatchingCondition(ServerWebExchange exchange) { +// if (CorsUtils.isPreFlightRequest(request)) { +// return matchPreFlight(request); +// } + if (getMethods().isEmpty()) { + if (RequestMethod.OPTIONS.name().equals(exchange.getRequest().getMethod().name())) { + return null; // No implicit match for OPTIONS (we handle it) + } + return this; + } + return matchRequestMethod(exchange.getRequest().getMethod().name()); + } + + /** + * On a pre-flight request match to the would-be, actual request. + * Hence empty conditions is a match, otherwise try to match to the HTTP + * method in the "Access-Control-Request-Method" header. + */ + @SuppressWarnings("unused") + private RequestMethodsRequestCondition matchPreFlight(HttpServletRequest request) { + if (getMethods().isEmpty()) { + return this; + } + String expectedMethod = request.getHeader(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD); + return matchRequestMethod(expectedMethod); + } + + private RequestMethodsRequestCondition matchRequestMethod(String httpMethodValue) { + HttpMethod httpMethod = HttpMethod.resolve(httpMethodValue); + if (httpMethod != null) { + for (RequestMethod method : getMethods()) { + if (httpMethod.matches(method.name())) { + return new RequestMethodsRequestCondition(method); + } + } + if (httpMethod == HttpMethod.HEAD && getMethods().contains(RequestMethod.GET)) { + return HEAD_CONDITION; + } + } + return null; + } + + /** + * Returns: + *
    + *
  • 0 if the two conditions contain the same number of HTTP request methods + *
  • Less than 0 if "this" instance has an HTTP request method but "other" doesn't + *
  • Greater than 0 "other" has an HTTP request method but "this" doesn't + *
+ *

It is assumed that both instances have been obtained via + * {@link #getMatchingCondition(ServerWebExchange)} and therefore each instance + * contains the matching HTTP request method only or is otherwise empty. + */ + @Override + public int compareTo(RequestMethodsRequestCondition other, ServerWebExchange exchange) { + return (other.methods.size() - this.methods.size()); + } + +} diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/condition/package-info.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/condition/package-info.java new file mode 100644 index 0000000000..1163fbdafd --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/condition/package-info.java @@ -0,0 +1,6 @@ +/** + * Support for mapping requests based on a + * {@link org.springframework.web.reactive.result.condition.RequestCondition + * RequestCondition} type hierarchy. + */ +package org.springframework.web.reactive.result.condition; diff --git a/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/RequestMappingInfo.java b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/RequestMappingInfo.java new file mode 100644 index 0000000000..15f91ea04d --- /dev/null +++ b/spring-web-reactive/src/main/java/org/springframework/web/reactive/result/method/RequestMappingInfo.java @@ -0,0 +1,602 @@ +/* + * Copyright 2002-2016 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.reactive.result.method; + +import java.util.List; + +import org.springframework.util.PathMatcher; +import org.springframework.util.StringUtils; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.reactive.accept.ContentTypeResolver; +import org.springframework.web.reactive.accept.FileExtensionContentTypeResolver; +import org.springframework.web.reactive.result.condition.ConsumesRequestCondition; +import org.springframework.web.reactive.result.condition.HeadersRequestCondition; +import org.springframework.web.reactive.result.condition.ParamsRequestCondition; +import org.springframework.web.reactive.result.condition.PatternsRequestCondition; +import org.springframework.web.reactive.result.condition.ProducesRequestCondition; +import org.springframework.web.reactive.result.condition.RequestCondition; +import org.springframework.web.reactive.result.condition.RequestConditionHolder; +import org.springframework.web.reactive.result.condition.RequestMethodsRequestCondition; +import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.util.HttpRequestPathHelper; + +/** + * Encapsulates the following request mapping conditions: + *

    + *
  1. {@link PatternsRequestCondition} + *
  2. {@link RequestMethodsRequestCondition} + *
  3. {@link ParamsRequestCondition} + *
  4. {@link HeadersRequestCondition} + *
  5. {@link ConsumesRequestCondition} + *
  6. {@link ProducesRequestCondition} + *
  7. {@code RequestCondition} (optional, custom request condition) + *
+ * + * @author Rossen Stoyanchev + */ +public final class RequestMappingInfo implements RequestCondition { + + private final String name; + + private final PatternsRequestCondition patternsCondition; + + private final RequestMethodsRequestCondition methodsCondition; + + private final ParamsRequestCondition paramsCondition; + + private final HeadersRequestCondition headersCondition; + + private final ConsumesRequestCondition consumesCondition; + + private final ProducesRequestCondition producesCondition; + + private final RequestConditionHolder customConditionHolder; + + + public RequestMappingInfo(String name, PatternsRequestCondition patterns, RequestMethodsRequestCondition methods, + ParamsRequestCondition params, HeadersRequestCondition headers, ConsumesRequestCondition consumes, + ProducesRequestCondition produces, RequestCondition custom) { + + this.name = (StringUtils.hasText(name) ? name : null); + this.patternsCondition = (patterns != null ? patterns : new PatternsRequestCondition()); + this.methodsCondition = (methods != null ? methods : new RequestMethodsRequestCondition()); + this.paramsCondition = (params != null ? params : new ParamsRequestCondition()); + this.headersCondition = (headers != null ? headers : new HeadersRequestCondition()); + this.consumesCondition = (consumes != null ? consumes : new ConsumesRequestCondition()); + this.producesCondition = (produces != null ? produces : new ProducesRequestCondition()); + this.customConditionHolder = new RequestConditionHolder(custom); + } + + /** + * Creates a new instance with the given request conditions. + */ + public RequestMappingInfo(PatternsRequestCondition patterns, RequestMethodsRequestCondition methods, + ParamsRequestCondition params, HeadersRequestCondition headers, ConsumesRequestCondition consumes, + ProducesRequestCondition produces, RequestCondition custom) { + + this(null, patterns, methods, params, headers, consumes, produces, custom); + } + + /** + * Re-create a RequestMappingInfo with the given custom request condition. + */ + public RequestMappingInfo(RequestMappingInfo info, RequestCondition customRequestCondition) { + this(info.name, info.patternsCondition, info.methodsCondition, info.paramsCondition, info.headersCondition, + info.consumesCondition, info.producesCondition, customRequestCondition); + } + + + /** + * Return the name for this mapping, or {@code null}. + */ + public String getName() { + return this.name; + } + + /** + * Returns the URL patterns of this {@link RequestMappingInfo}; + * or instance with 0 patterns, never {@code null}. + */ + public PatternsRequestCondition getPatternsCondition() { + return this.patternsCondition; + } + + /** + * Returns the HTTP request methods of this {@link RequestMappingInfo}; + * or instance with 0 request methods, never {@code null}. + */ + public RequestMethodsRequestCondition getMethodsCondition() { + return this.methodsCondition; + } + + /** + * Returns the "parameters" condition of this {@link RequestMappingInfo}; + * or instance with 0 parameter expressions, never {@code null}. + */ + public ParamsRequestCondition getParamsCondition() { + return this.paramsCondition; + } + + /** + * Returns the "headers" condition of this {@link RequestMappingInfo}; + * or instance with 0 header expressions, never {@code null}. + */ + public HeadersRequestCondition getHeadersCondition() { + return this.headersCondition; + } + + /** + * Returns the "consumes" condition of this {@link RequestMappingInfo}; + * or instance with 0 consumes expressions, never {@code null}. + */ + public ConsumesRequestCondition getConsumesCondition() { + return this.consumesCondition; + } + + /** + * Returns the "produces" condition of this {@link RequestMappingInfo}; + * or instance with 0 produces expressions, never {@code null}. + */ + public ProducesRequestCondition getProducesCondition() { + return this.producesCondition; + } + + /** + * Returns the "custom" condition of this {@link RequestMappingInfo}; or {@code null}. + */ + public RequestCondition getCustomCondition() { + return this.customConditionHolder.getCondition(); + } + + + /** + * Combines "this" request mapping info (i.e. the current instance) with another request mapping info instance. + *

Example: combine type- and method-level request mappings. + * @return a new request mapping info instance; never {@code null} + */ + @Override + public RequestMappingInfo combine(RequestMappingInfo other) { + String name = combineNames(other); + PatternsRequestCondition patterns = this.patternsCondition.combine(other.patternsCondition); + RequestMethodsRequestCondition methods = this.methodsCondition.combine(other.methodsCondition); + ParamsRequestCondition params = this.paramsCondition.combine(other.paramsCondition); + HeadersRequestCondition headers = this.headersCondition.combine(other.headersCondition); + ConsumesRequestCondition consumes = this.consumesCondition.combine(other.consumesCondition); + ProducesRequestCondition produces = this.producesCondition.combine(other.producesCondition); + RequestConditionHolder custom = this.customConditionHolder.combine(other.customConditionHolder); + + return new RequestMappingInfo(name, patterns, + methods, params, headers, consumes, produces, custom.getCondition()); + } + + private String combineNames(RequestMappingInfo other) { + if (this.name != null && other.name != null) { + String separator = "#"; + return this.name + separator + other.name; + } + else if (this.name != null) { + return this.name; + } + else { + return (other.name != null ? other.name : null); + } + } + + /** + * Checks if all conditions in this request mapping info match the provided request and returns + * a potentially new request mapping info with conditions tailored to the current request. + *

For example the returned instance may contain the subset of URL patterns that match to + * the current request, sorted with best matching patterns on top. + * @return a new instance in case all conditions match; or {@code null} otherwise + */ + @Override + public RequestMappingInfo getMatchingCondition(ServerWebExchange exchange) { + RequestMethodsRequestCondition methods = this.methodsCondition.getMatchingCondition(exchange); + ParamsRequestCondition params = this.paramsCondition.getMatchingCondition(exchange); + HeadersRequestCondition headers = this.headersCondition.getMatchingCondition(exchange); + ConsumesRequestCondition consumes = this.consumesCondition.getMatchingCondition(exchange); + ProducesRequestCondition produces = this.producesCondition.getMatchingCondition(exchange); + + if (methods == null || params == null || headers == null || consumes == null || produces == null) { + return null; + } + + PatternsRequestCondition patterns = this.patternsCondition.getMatchingCondition(exchange); + if (patterns == null) { + return null; + } + + RequestConditionHolder custom = this.customConditionHolder.getMatchingCondition(exchange); + if (custom == null) { + return null; + } + + return new RequestMappingInfo(this.name, patterns, + methods, params, headers, consumes, produces, custom.getCondition()); + } + + /** + * Compares "this" info (i.e. the current instance) with another info in the context of a request. + *

Note: It is assumed both instances have been obtained via + * {@link #getMatchingCondition(ServerWebExchange)} to ensure they have conditions with + * content relevant to current request. + */ + @Override + public int compareTo(RequestMappingInfo other, ServerWebExchange exchange) { + int result = this.patternsCondition.compareTo(other.getPatternsCondition(), exchange); + if (result != 0) { + return result; + } + result = this.paramsCondition.compareTo(other.getParamsCondition(), exchange); + if (result != 0) { + return result; + } + result = this.headersCondition.compareTo(other.getHeadersCondition(), exchange); + if (result != 0) { + return result; + } + result = this.consumesCondition.compareTo(other.getConsumesCondition(), exchange); + if (result != 0) { + return result; + } + result = this.producesCondition.compareTo(other.getProducesCondition(), exchange); + if (result != 0) { + return result; + } + result = this.methodsCondition.compareTo(other.getMethodsCondition(), exchange); + if (result != 0) { + return result; + } + result = this.customConditionHolder.compareTo(other.customConditionHolder, exchange); + if (result != 0) { + return result; + } + return 0; + } + + @Override + public boolean equals(Object other) { + if (this == other) { + return true; + } + if (!(other instanceof RequestMappingInfo)) { + return false; + } + RequestMappingInfo otherInfo = (RequestMappingInfo) other; + return (this.patternsCondition.equals(otherInfo.patternsCondition) && + this.methodsCondition.equals(otherInfo.methodsCondition) && + this.paramsCondition.equals(otherInfo.paramsCondition) && + this.headersCondition.equals(otherInfo.headersCondition) && + this.consumesCondition.equals(otherInfo.consumesCondition) && + this.producesCondition.equals(otherInfo.producesCondition) && + this.customConditionHolder.equals(otherInfo.customConditionHolder)); + } + + @Override + public int hashCode() { + return (this.patternsCondition.hashCode() * 31 + // primary differentiation + this.methodsCondition.hashCode() + this.paramsCondition.hashCode() + + this.headersCondition.hashCode() + this.consumesCondition.hashCode() + + this.producesCondition.hashCode() + this.customConditionHolder.hashCode()); + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder("{"); + builder.append(this.patternsCondition); + if (!this.methodsCondition.isEmpty()) { + builder.append(",methods=").append(this.methodsCondition); + } + if (!this.paramsCondition.isEmpty()) { + builder.append(",params=").append(this.paramsCondition); + } + if (!this.headersCondition.isEmpty()) { + builder.append(",headers=").append(this.headersCondition); + } + if (!this.consumesCondition.isEmpty()) { + builder.append(",consumes=").append(this.consumesCondition); + } + if (!this.producesCondition.isEmpty()) { + builder.append(",produces=").append(this.producesCondition); + } + if (!this.customConditionHolder.isEmpty()) { + builder.append(",custom=").append(this.customConditionHolder); + } + builder.append('}'); + return builder.toString(); + } + + + /** + * Create a new {@code RequestMappingInfo.Builder} with the given paths. + * @param paths the paths to use + */ + public static Builder paths(String... paths) { + return new DefaultBuilder(paths); + } + + + /** + * Defines a builder for creating a RequestMappingInfo. + */ + public interface Builder { + + /** + * Set the path patterns. + */ + Builder paths(String... paths); + + /** + * Set the request method conditions. + */ + Builder methods(RequestMethod... methods); + + /** + * Set the request param conditions. + */ + Builder params(String... params); + + /** + * Set the header conditions. + *

By default this is not set. + */ + Builder headers(String... headers); + + /** + * Set the consumes conditions. + */ + Builder consumes(String... consumes); + + /** + * Set the produces conditions. + */ + Builder produces(String... produces); + + /** + * Set the mapping name. + */ + Builder mappingName(String name); + + /** + * Set a custom condition to use. + */ + Builder customCondition(RequestCondition condition); + + /** + * Provide additional configuration needed for request mapping purposes. + */ + Builder options(BuilderConfiguration options); + + /** + * Build the RequestMappingInfo. + */ + RequestMappingInfo build(); + } + + + private static class DefaultBuilder implements Builder { + + private String[] paths; + + private RequestMethod[] methods; + + private String[] params; + + private String[] headers; + + private String[] consumes; + + private String[] produces; + + private String mappingName; + + private RequestCondition customCondition; + + private BuilderConfiguration options = new BuilderConfiguration(); + + public DefaultBuilder(String... paths) { + this.paths = paths; + } + + @Override + public Builder paths(String... paths) { + this.paths = paths; + return this; + } + + @Override + public DefaultBuilder methods(RequestMethod... methods) { + this.methods = methods; + return this; + } + + @Override + public DefaultBuilder params(String... params) { + this.params = params; + return this; + } + + @Override + public DefaultBuilder headers(String... headers) { + this.headers = headers; + return this; + } + + @Override + public DefaultBuilder consumes(String... consumes) { + this.consumes = consumes; + return this; + } + + @Override + public DefaultBuilder produces(String... produces) { + this.produces = produces; + return this; + } + + @Override + public DefaultBuilder mappingName(String name) { + this.mappingName = name; + return this; + } + + @Override + public DefaultBuilder customCondition(RequestCondition condition) { + this.customCondition = condition; + return this; + } + + @Override + public Builder options(BuilderConfiguration options) { + this.options = options; + return this; + } + + @Override + public RequestMappingInfo build() { + ContentTypeResolver contentTypeResolver = this.options.getContentTypeResolver(); + + PatternsRequestCondition patternsCondition = new PatternsRequestCondition( + this.paths, this.options.getPathHelper(), this.options.getPathMatcher(), + this.options.useSuffixPatternMatch(), this.options.useTrailingSlashMatch(), + this.options.getFileExtensions()); + + return new RequestMappingInfo(this.mappingName, patternsCondition, + new RequestMethodsRequestCondition(methods), + new ParamsRequestCondition(this.params), + new HeadersRequestCondition(this.headers), + new ConsumesRequestCondition(this.consumes, this.headers), + new ProducesRequestCondition(this.produces, this.headers, contentTypeResolver), + this.customCondition); + } + } + + + /** + * Container for configuration options used for request mapping purposes. + * Such configuration is required to create RequestMappingInfo instances but + * is typically used across all RequestMappingInfo instances. + * @see Builder#options + */ + public static class BuilderConfiguration { + + private HttpRequestPathHelper pathHelper; + + private PathMatcher pathMatcher; + + private boolean trailingSlashMatch = true; + + private boolean suffixPatternMatch = true; + + private boolean registeredSuffixPatternMatch = false; + + private ContentTypeResolver contentTypeResolver; + + /** + * Set a custom UrlPathHelper to use for the PatternsRequestCondition. + *

By default this is not set. + */ + public void setPathHelper(HttpRequestPathHelper pathHelper) { + this.pathHelper = pathHelper; + } + + public HttpRequestPathHelper getPathHelper() { + return this.pathHelper; + } + + /** + * Set a custom PathMatcher to use for the PatternsRequestCondition. + *

By default this is not set. + */ + public void setPathMatcher(PathMatcher pathMatcher) { + this.pathMatcher = pathMatcher; + } + + public PathMatcher getPathMatcher() { + return this.pathMatcher; + } + + /** + * Whether to apply trailing slash matching in PatternsRequestCondition. + *

By default this is set to 'true'. + */ + public void setTrailingSlashMatch(boolean trailingSlashMatch) { + this.trailingSlashMatch = trailingSlashMatch; + } + + public boolean useTrailingSlashMatch() { + return this.trailingSlashMatch; + } + + /** + * Whether to apply suffix pattern matching in PatternsRequestCondition. + *

By default this is set to 'true'. + * @see #setRegisteredSuffixPatternMatch(boolean) + */ + public void setSuffixPatternMatch(boolean suffixPatternMatch) { + this.suffixPatternMatch = suffixPatternMatch; + } + + public boolean useSuffixPatternMatch() { + return this.suffixPatternMatch; + } + + /** + * Whether suffix pattern matching should be restricted to registered + * file extensions only. Setting this property also sets + * suffixPatternMatch=true and requires that a + * {@link #setContentTypeResolver} is also configured in order to + * obtain the registered file extensions. + */ + public void setRegisteredSuffixPatternMatch(boolean registeredSuffixPatternMatch) { + this.registeredSuffixPatternMatch = registeredSuffixPatternMatch; + this.suffixPatternMatch = (registeredSuffixPatternMatch || this.suffixPatternMatch); + } + + public boolean useRegisteredSuffixPatternMatch() { + return this.registeredSuffixPatternMatch; + } + + /** + * Return the file extensions to use for suffix pattern matching. If + * {@code registeredSuffixPatternMatch=true}, the extensions are obtained + * from the configured {@code contentTypeResolver}. + */ + public List getFileExtensions() { + ContentTypeResolver resolver = getContentTypeResolver(); + if (useRegisteredSuffixPatternMatch() && resolver != null) { + if (resolver instanceof FileExtensionContentTypeResolver) { + return ((FileExtensionContentTypeResolver) resolver).getAllFileExtensions(); + } + } + return null; + } + + /** + * Set the ContentNegotiationManager to use for the ProducesRequestCondition. + *

By default this is not set. + */ + public void setContentTypeResolver(ContentTypeResolver resolver) { + this.contentTypeResolver = resolver; + } + + public ContentTypeResolver getContentTypeResolver() { + return this.contentTypeResolver; + } + } + +} diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/condition/CompositeRequestConditionTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/condition/CompositeRequestConditionTests.java new file mode 100644 index 0000000000..88b92ed960 --- /dev/null +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/condition/CompositeRequestConditionTests.java @@ -0,0 +1,151 @@ +/* + * Copyright 2002-2016 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.reactive.result.condition; + +import java.net.URI; + +import org.junit.Before; +import org.junit.Test; + +import org.springframework.http.HttpMethod; +import org.springframework.http.server.reactive.MockServerHttpRequest; +import org.springframework.http.server.reactive.MockServerHttpResponse; +import org.springframework.http.server.reactive.ServerHttpRequest; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.server.adapter.DefaultServerWebExchange; +import org.springframework.web.server.session.WebSessionManager; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; +import static org.mockito.Mockito.mock; + +/** + * Unit tests for {@link CompositeRequestCondition}. + * + * @author Rossen Stoyanchev + */ +public class CompositeRequestConditionTests { + + private ServerWebExchange exchange; + + private ServerHttpRequest request; + + private ParamsRequestCondition param1; + private ParamsRequestCondition param2; + private ParamsRequestCondition param3; + + private HeadersRequestCondition header1; + private HeadersRequestCondition header2; + private HeadersRequestCondition header3; + + + @Before + public void setup() throws Exception { + WebSessionManager sessionManager = mock(WebSessionManager.class); + this.request = new MockServerHttpRequest(HttpMethod.GET, new URI("/")); + this.exchange = new DefaultServerWebExchange(request, new MockServerHttpResponse(), sessionManager); + + 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() { + this.request.getQueryParams().add("param1", "paramValue1"); + + RequestCondition condition1 = new RequestMethodsRequestCondition(RequestMethod.GET, RequestMethod.POST); + RequestCondition condition2 = new RequestMethodsRequestCondition(RequestMethod.GET); + + CompositeRequestCondition composite1 = new CompositeRequestCondition(this.param1, condition1); + CompositeRequestCondition composite2 = new CompositeRequestCondition(this.param1, condition2); + + assertEquals(composite2, composite1.getMatchingCondition(this.exchange)); + } + + @Test + public void noMatch() { + CompositeRequestCondition cond = new CompositeRequestCondition(this.param1); + assertNull(cond.getMatchingCondition(this.exchange)); + } + + @Test + public void matchEmpty() { + CompositeRequestCondition empty = new CompositeRequestCondition(); + assertSame(empty, empty.getMatchingCondition(this.exchange)); + } + + @Test + public void compare() { + CompositeRequestCondition cond1 = new CompositeRequestCondition(this.param1); + CompositeRequestCondition cond3 = new CompositeRequestCondition(this.param3); + + assertEquals(1, cond1.compareTo(cond3, this.exchange)); + assertEquals(-1, cond3.compareTo(cond1, this.exchange)); + } + + @Test + public void compareEmpty() { + CompositeRequestCondition empty = new CompositeRequestCondition(); + CompositeRequestCondition notEmpty = new CompositeRequestCondition(this.param1); + + assertEquals(0, empty.compareTo(empty, this.exchange)); + assertEquals(-1, notEmpty.compareTo(empty, this.exchange)); + assertEquals(1, empty.compareTo(notEmpty, this.exchange)); + } + + @Test(expected=IllegalArgumentException.class) + public void compareDifferentLength() { + CompositeRequestCondition cond1 = new CompositeRequestCondition(this.param1); + CompositeRequestCondition cond2 = new CompositeRequestCondition(this.param1, this.header1); + cond1.compareTo(cond2, this.exchange); + } + +} diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/condition/ConsumesRequestConditionTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/condition/ConsumesRequestConditionTests.java new file mode 100644 index 0000000000..03c4acf57c --- /dev/null +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/condition/ConsumesRequestConditionTests.java @@ -0,0 +1,210 @@ +/* + * 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.reactive.result.condition; + +import java.net.URI; +import java.net.URISyntaxException; +import java.util.Collection; +import java.util.Collections; + +import org.junit.Test; + +import org.springframework.http.HttpMethod; +import org.springframework.http.server.reactive.MockServerHttpRequest; +import org.springframework.http.server.reactive.MockServerHttpResponse; +import org.springframework.http.server.reactive.ServerHttpRequest; +import org.springframework.web.reactive.result.condition.ConsumesRequestCondition.ConsumeMediaTypeExpression; +import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.server.adapter.DefaultServerWebExchange; +import org.springframework.web.server.session.WebSessionManager; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import static org.mockito.Mockito.mock; + +/** + * @author Arjen Poutsma + */ +public class ConsumesRequestConditionTests { + + @Test + public void consumesMatch() throws Exception { + ServerWebExchange exchange = createExchange("text/plain"); + ConsumesRequestCondition condition = new ConsumesRequestCondition("text/plain"); + + assertNotNull(condition.getMatchingCondition(exchange)); + } + + @Test + public void negatedConsumesMatch() throws Exception { + ServerWebExchange exchange = createExchange("text/plain"); + ConsumesRequestCondition condition = new ConsumesRequestCondition("!text/plain"); + + assertNull(condition.getMatchingCondition(exchange)); + } + + @Test + public void getConsumableMediaTypesNegatedExpression() throws Exception { + ConsumesRequestCondition condition = new ConsumesRequestCondition("!application/xml"); + assertEquals(Collections.emptySet(), condition.getConsumableMediaTypes()); + } + + @Test + public void consumesWildcardMatch() throws Exception { + ServerWebExchange exchange = createExchange("text/plain"); + ConsumesRequestCondition condition = new ConsumesRequestCondition("text/*"); + + assertNotNull(condition.getMatchingCondition(exchange)); + } + + @Test + public void consumesMultipleMatch() throws Exception { + ServerWebExchange exchange = createExchange("text/plain"); + ConsumesRequestCondition condition = new ConsumesRequestCondition("text/plain", "application/xml"); + + assertNotNull(condition.getMatchingCondition(exchange)); + } + + @Test + public void consumesSingleNoMatch() throws Exception { + ServerWebExchange exchange = createExchange("application/xml"); + ConsumesRequestCondition condition = new ConsumesRequestCondition("text/plain"); + + assertNull(condition.getMatchingCondition(exchange)); + } + + @Test + public void consumesParseError() throws Exception { + ServerWebExchange exchange = createExchange("01"); + ConsumesRequestCondition condition = new ConsumesRequestCondition("text/plain"); + + assertNull(condition.getMatchingCondition(exchange)); + } + + @Test + public void consumesParseErrorWithNegation() throws Exception { + ServerWebExchange exchange = createExchange("01"); + ConsumesRequestCondition condition = new ConsumesRequestCondition("!text/plain"); + + assertNull(condition.getMatchingCondition(exchange)); + } + + @Test + public void compareToSingle() throws Exception { + ServerWebExchange exchange = createExchange(); + + ConsumesRequestCondition condition1 = new ConsumesRequestCondition("text/plain"); + ConsumesRequestCondition condition2 = new ConsumesRequestCondition("text/*"); + + int result = condition1.compareTo(condition2, exchange); + assertTrue("Invalid comparison result: " + result, result < 0); + + result = condition2.compareTo(condition1, exchange); + assertTrue("Invalid comparison result: " + result, result > 0); + } + + @Test + public void compareToMultiple() throws Exception { + ServerWebExchange exchange = createExchange(); + + ConsumesRequestCondition condition1 = new ConsumesRequestCondition("*/*", "text/plain"); + ConsumesRequestCondition condition2 = new ConsumesRequestCondition("text/*", "text/plain;q=0.7"); + + int result = condition1.compareTo(condition2, exchange); + assertTrue("Invalid comparison result: " + result, result < 0); + + result = condition2.compareTo(condition1, exchange); + assertTrue("Invalid comparison result: " + result, result > 0); + } + + + @Test + public void combine() throws Exception { + ConsumesRequestCondition condition1 = new ConsumesRequestCondition("text/plain"); + ConsumesRequestCondition condition2 = new ConsumesRequestCondition("application/xml"); + + ConsumesRequestCondition result = condition1.combine(condition2); + assertEquals(condition2, result); + } + + @Test + public void combineWithDefault() throws Exception { + ConsumesRequestCondition condition1 = new ConsumesRequestCondition("text/plain"); + ConsumesRequestCondition condition2 = new ConsumesRequestCondition(); + + ConsumesRequestCondition result = condition1.combine(condition2); + assertEquals(condition1, result); + } + + @Test + public void parseConsumesAndHeaders() throws Exception { + String[] consumes = new String[] {"text/plain"}; + String[] headers = new String[]{"foo=bar", "content-type=application/xml,application/pdf"}; + ConsumesRequestCondition condition = new ConsumesRequestCondition(consumes, headers); + + assertConditions(condition, "text/plain", "application/xml", "application/pdf"); + } + + @Test + public void getMatchingCondition() throws Exception { + ServerWebExchange exchange = createExchange("text/plain"); + ConsumesRequestCondition condition = new ConsumesRequestCondition("text/plain", "application/xml"); + + ConsumesRequestCondition result = condition.getMatchingCondition(exchange); + assertConditions(result, "text/plain"); + + condition = new ConsumesRequestCondition("application/xml"); + result = condition.getMatchingCondition(exchange); + assertNull(result); + } + + private void assertConditions(ConsumesRequestCondition condition, String... expected) { + Collection expressions = condition.getContent(); + assertEquals("Invalid amount of conditions", expressions.size(), expected.length); + for (String s : expected) { + boolean found = false; + for (ConsumeMediaTypeExpression expr : expressions) { + String conditionMediaType = expr.getMediaType().toString(); + if (conditionMediaType.equals(s)) { + found = true; + break; + + } + } + if (!found) { + fail("Condition [" + s + "] not found"); + } + } + } + + private ServerWebExchange createExchange() throws URISyntaxException { + return createExchange(null); + } + + private ServerWebExchange createExchange(String contentType) throws URISyntaxException { + ServerHttpRequest request = new MockServerHttpRequest(HttpMethod.GET, new URI("/")); + if (contentType != null) { + request.getHeaders().add("Content-Type", contentType); + } + WebSessionManager sessionManager = mock(WebSessionManager.class); + return new DefaultServerWebExchange(request, new MockServerHttpResponse(), sessionManager); + } + +} diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/condition/HeadersRequestConditionTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/condition/HeadersRequestConditionTests.java new file mode 100644 index 0000000000..b5dab36223 --- /dev/null +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/condition/HeadersRequestConditionTests.java @@ -0,0 +1,173 @@ +/* + * 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.reactive.result.condition; + +import java.net.URI; +import java.net.URISyntaxException; +import java.util.Collection; + +import org.junit.Test; + +import org.springframework.http.HttpMethod; +import org.springframework.http.server.reactive.MockServerHttpRequest; +import org.springframework.http.server.reactive.MockServerHttpResponse; +import org.springframework.http.server.reactive.ServerHttpRequest; +import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.server.adapter.DefaultServerWebExchange; +import org.springframework.web.server.session.WebSessionManager; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; + +/** + * Unit tests for {@link HeadersRequestCondition}. + * + * @author Rossen Stoyanchev + */ +public class HeadersRequestConditionTests { + + @Test + public void headerEquals() { + assertEquals(new HeadersRequestCondition("foo"), new HeadersRequestCondition("foo")); + assertEquals(new HeadersRequestCondition("foo"), new HeadersRequestCondition("FOO")); + assertFalse(new HeadersRequestCondition("foo").equals(new HeadersRequestCondition("bar"))); + assertEquals(new HeadersRequestCondition("foo=bar"), new HeadersRequestCondition("foo=bar")); + assertEquals(new HeadersRequestCondition("foo=bar"), new HeadersRequestCondition("FOO=bar")); + } + + @Test + public void headerPresent() throws Exception { + ServerWebExchange exchange = createExchange("Accept", ""); + HeadersRequestCondition condition = new HeadersRequestCondition("accept"); + + assertNotNull(condition.getMatchingCondition(exchange)); + } + + @Test + public void headerPresentNoMatch() throws Exception { + ServerWebExchange exchange = createExchange("bar", ""); + HeadersRequestCondition condition = new HeadersRequestCondition("foo"); + + assertNull(condition.getMatchingCondition(exchange)); + } + + @Test + public void headerNotPresent() throws Exception { + ServerWebExchange exchange = createExchange(); + HeadersRequestCondition condition = new HeadersRequestCondition("!accept"); + + assertNotNull(condition.getMatchingCondition(exchange)); + } + + @Test + public void headerValueMatch() throws Exception { + ServerWebExchange exchange = createExchange("foo", "bar"); + HeadersRequestCondition condition = new HeadersRequestCondition("foo=bar"); + + assertNotNull(condition.getMatchingCondition(exchange)); + } + + @Test + public void headerValueNoMatch() throws Exception { + ServerWebExchange exchange = createExchange("foo", "bazz"); + HeadersRequestCondition condition = new HeadersRequestCondition("foo=bar"); + + assertNull(condition.getMatchingCondition(exchange)); + } + + @Test + public void headerCaseSensitiveValueMatch() throws Exception { + ServerWebExchange exchange = createExchange("foo", "bar"); + HeadersRequestCondition condition = new HeadersRequestCondition("foo=Bar"); + + assertNull(condition.getMatchingCondition(exchange)); + } + + @Test + public void headerValueMatchNegated() throws Exception { + ServerWebExchange exchange = createExchange("foo", "baz"); + HeadersRequestCondition condition = new HeadersRequestCondition("foo!=bar"); + + assertNotNull(condition.getMatchingCondition(exchange)); + } + + @Test + public void headerValueNoMatchNegated() throws Exception { + ServerWebExchange exchange = createExchange("foo", "bar"); + HeadersRequestCondition condition = new HeadersRequestCondition("foo!=bar"); + + assertNull(condition.getMatchingCondition(exchange)); + } + + @Test + public void compareTo() throws Exception { + ServerWebExchange exchange = createExchange(); + + HeadersRequestCondition condition1 = new HeadersRequestCondition("foo", "bar", "baz"); + HeadersRequestCondition condition2 = new HeadersRequestCondition("foo", "bar"); + + int result = condition1.compareTo(condition2, exchange); + assertTrue("Invalid comparison result: " + result, result < 0); + + result = condition2.compareTo(condition1, exchange); + assertTrue("Invalid comparison result: " + result, result > 0); + } + + + @Test + public void combine() { + HeadersRequestCondition condition1 = new HeadersRequestCondition("foo=bar"); + HeadersRequestCondition condition2 = new HeadersRequestCondition("foo=baz"); + + HeadersRequestCondition result = condition1.combine(condition2); + Collection conditions = result.getContent(); + assertEquals(2, conditions.size()); + } + + @Test + public void getMatchingCondition() throws Exception { + ServerWebExchange exchange = createExchange("foo", "bar"); + HeadersRequestCondition condition = new HeadersRequestCondition("foo"); + + HeadersRequestCondition result = condition.getMatchingCondition(exchange); + assertEquals(condition, result); + + condition = new HeadersRequestCondition("bar"); + + result = condition.getMatchingCondition(exchange); + assertNull(result); + } + + + private ServerWebExchange createExchange() throws URISyntaxException { + return createExchange(null, null); + } + + private ServerWebExchange createExchange(String headerName, String headerValue) throws URISyntaxException { + ServerHttpRequest request = new MockServerHttpRequest(HttpMethod.GET, new URI("/")); + if (headerName != null) { + request.getHeaders().add(headerName, headerValue); + } + WebSessionManager sessionManager = mock(WebSessionManager.class); + return new DefaultServerWebExchange(request, new MockServerHttpResponse(), sessionManager); + } + +} diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/condition/ParamsRequestConditionTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/condition/ParamsRequestConditionTests.java new file mode 100644 index 0000000000..f28ce748fb --- /dev/null +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/condition/ParamsRequestConditionTests.java @@ -0,0 +1,145 @@ +/* + * 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.reactive.result.condition; + +import java.net.URI; +import java.net.URISyntaxException; +import java.util.Collection; + +import org.junit.Test; + +import org.springframework.http.HttpMethod; +import org.springframework.http.server.reactive.MockServerHttpRequest; +import org.springframework.http.server.reactive.MockServerHttpResponse; +import org.springframework.http.server.reactive.ServerHttpRequest; +import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.server.adapter.DefaultServerWebExchange; +import org.springframework.web.server.session.WebSessionManager; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; + +/** + * @author Arjen Poutsma + */ +public class ParamsRequestConditionTests { + + @Test + public void paramEquals() { + assertEquals(new ParamsRequestCondition("foo"), new ParamsRequestCondition("foo")); + assertFalse(new ParamsRequestCondition("foo").equals(new ParamsRequestCondition("bar"))); + assertFalse(new ParamsRequestCondition("foo").equals(new ParamsRequestCondition("FOO"))); + assertEquals(new ParamsRequestCondition("foo=bar"), new ParamsRequestCondition("foo=bar")); + assertFalse(new ParamsRequestCondition("foo=bar").equals(new ParamsRequestCondition("FOO=bar"))); + } + + @Test + public void paramPresent() throws Exception { + ServerWebExchange exchange = createExchange("foo", ""); + ParamsRequestCondition condition = new ParamsRequestCondition("foo"); + + assertNotNull(condition.getMatchingCondition(exchange)); + } + + @Test + public void paramPresentNoMatch() throws Exception { + ServerWebExchange exchange = createExchange("bar", ""); + ParamsRequestCondition condition = new ParamsRequestCondition("foo"); + + assertNull(condition.getMatchingCondition(exchange)); + } + + @Test + public void paramNotPresent() throws Exception { + ServerWebExchange exchange = createExchange(); + ParamsRequestCondition condition = new ParamsRequestCondition("!foo"); + + assertNotNull(condition.getMatchingCondition(exchange)); + } + + @Test + public void paramValueMatch() throws Exception { + ServerWebExchange exchange = createExchange("foo", "bar"); + ParamsRequestCondition condition = new ParamsRequestCondition("foo=bar"); + + assertNotNull(condition.getMatchingCondition(exchange)); + } + + @Test + public void paramValueNoMatch() throws Exception { + ServerWebExchange exchange = createExchange("foo", "bazz"); + ParamsRequestCondition condition = new ParamsRequestCondition("foo=bar"); + + assertNull(condition.getMatchingCondition(exchange)); + } + + @Test + public void compareTo() throws Exception { + ServerWebExchange exchange = createExchange(); + + ParamsRequestCondition condition1 = new ParamsRequestCondition("foo", "bar", "baz"); + ParamsRequestCondition condition2 = new ParamsRequestCondition("foo", "bar"); + + int result = condition1.compareTo(condition2, exchange); + assertTrue("Invalid comparison result: " + result, result < 0); + + result = condition2.compareTo(condition1, exchange); + assertTrue("Invalid comparison result: " + result, result > 0); + } + + @Test + public void combine() { + ParamsRequestCondition condition1 = new ParamsRequestCondition("foo=bar"); + ParamsRequestCondition condition2 = new ParamsRequestCondition("foo=baz"); + + ParamsRequestCondition result = condition1.combine(condition2); + Collection conditions = result.getContent(); + assertEquals(2, conditions.size()); + } + + @Test + public void getMatchingCondition() throws Exception { + ServerWebExchange exchange = createExchange("foo", "bar"); + ParamsRequestCondition condition = new ParamsRequestCondition("foo"); + + ParamsRequestCondition result = condition.getMatchingCondition(exchange); + assertEquals(condition, result); + + condition = new ParamsRequestCondition("bar"); + + result = condition.getMatchingCondition(exchange); + assertNull(result); + } + + private ServerWebExchange createExchange() throws URISyntaxException { + return createExchange(null, null); + } + + private ServerWebExchange createExchange(String paramName, String paramValue) throws URISyntaxException { + ServerHttpRequest request = new MockServerHttpRequest(HttpMethod.GET, new URI("/")); + if (paramName != null) { + request.getQueryParams().add(paramName, paramValue); + } + WebSessionManager sessionManager = mock(WebSessionManager.class); + return new DefaultServerWebExchange(request, new MockServerHttpResponse(), sessionManager); + } + +} diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/condition/PatternsRequestConditionTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/condition/PatternsRequestConditionTests.java new file mode 100644 index 0000000000..715b247425 --- /dev/null +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/condition/PatternsRequestConditionTests.java @@ -0,0 +1,234 @@ +/* + * Copyright 2002-2016 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.reactive.result.condition; + +import java.net.URI; +import java.net.URISyntaxException; +import java.util.Collections; +import java.util.List; + +import org.junit.Test; + +import org.springframework.http.HttpMethod; +import org.springframework.http.server.reactive.MockServerHttpRequest; +import org.springframework.http.server.reactive.MockServerHttpResponse; +import org.springframework.http.server.reactive.ServerHttpRequest; +import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.server.adapter.DefaultServerWebExchange; +import org.springframework.web.server.session.WebSessionManager; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.mockito.Mockito.mock; + +/** + * Unit tests for {@link PatternsRequestCondition}. + * + * @author Rossen Stoyanchev + */ +public class PatternsRequestConditionTests { + + @Test + public void prependSlash() { + PatternsRequestCondition c = new PatternsRequestCondition("foo"); + assertEquals("/foo", c.getPatterns().iterator().next()); + } + + @Test + public void prependNonEmptyPatternsOnly() { + PatternsRequestCondition c = new PatternsRequestCondition(""); + assertEquals("Do not prepend empty patterns (SPR-8255)", "", c.getPatterns().iterator().next()); + } + + @Test + public void combineEmptySets() { + PatternsRequestCondition c1 = new PatternsRequestCondition(); + PatternsRequestCondition c2 = new PatternsRequestCondition(); + + assertEquals(new PatternsRequestCondition(""), c1.combine(c2)); + } + + @Test + public void combineOnePatternWithEmptySet() { + PatternsRequestCondition c1 = new PatternsRequestCondition("/type1", "/type2"); + PatternsRequestCondition c2 = new PatternsRequestCondition(); + + assertEquals(new PatternsRequestCondition("/type1", "/type2"), c1.combine(c2)); + + c1 = new PatternsRequestCondition(); + c2 = new PatternsRequestCondition("/method1", "/method2"); + + assertEquals(new PatternsRequestCondition("/method1", "/method2"), c1.combine(c2)); + } + + @Test + public void combineMultiplePatterns() { + PatternsRequestCondition c1 = new PatternsRequestCondition("/t1", "/t2"); + PatternsRequestCondition c2 = new PatternsRequestCondition("/m1", "/m2"); + + assertEquals(new PatternsRequestCondition("/t1/m1", "/t1/m2", "/t2/m1", "/t2/m2"), c1.combine(c2)); + } + + @Test + public void matchDirectPath() throws Exception { + PatternsRequestCondition condition = new PatternsRequestCondition("/foo"); + PatternsRequestCondition match = condition.getMatchingCondition(createExchange("/foo")); + + assertNotNull(match); + } + + @Test + public void matchPattern() throws Exception { + PatternsRequestCondition condition = new PatternsRequestCondition("/foo/*"); + PatternsRequestCondition match = condition.getMatchingCondition(createExchange("/foo/bar")); + + assertNotNull(match); + } + + @Test + public void matchSortPatterns() throws Exception { + PatternsRequestCondition condition = new PatternsRequestCondition("/**", "/foo/bar", "/foo/*"); + PatternsRequestCondition match = condition.getMatchingCondition(createExchange("/foo/bar")); + PatternsRequestCondition expected = new PatternsRequestCondition("/foo/bar", "/foo/*", "/**"); + + assertEquals(expected, match); + } + + @Test + public void matchSuffixPattern() throws Exception { + ServerWebExchange exchange = createExchange("/foo.html"); + + PatternsRequestCondition condition = new PatternsRequestCondition("/{foo}"); + PatternsRequestCondition match = condition.getMatchingCondition(exchange); + + assertNotNull(match); + assertEquals("/{foo}.*", match.getPatterns().iterator().next()); + + condition = new PatternsRequestCondition(new String[] {"/{foo}"}, null, null, false, false, null); + match = condition.getMatchingCondition(exchange); + + assertNotNull(match); + assertEquals("/{foo}", match.getPatterns().iterator().next()); + } + + // SPR-8410 + + @Test + public void matchSuffixPatternUsingFileExtensions() throws Exception { + String[] patterns = new String[] {"/jobs/{jobName}"}; + List extensions = Collections.singletonList("json"); + PatternsRequestCondition condition = new PatternsRequestCondition(patterns, null, null, true, false, extensions); + + ServerWebExchange exchange = createExchange("/jobs/my.job"); + PatternsRequestCondition match = condition.getMatchingCondition(exchange); + + assertNotNull(match); + assertEquals("/jobs/{jobName}", match.getPatterns().iterator().next()); + + exchange = createExchange("/jobs/my.job.json"); + match = condition.getMatchingCondition(exchange); + + assertNotNull(match); + assertEquals("/jobs/{jobName}.json", match.getPatterns().iterator().next()); + } + + @Test + public void matchSuffixPatternUsingFileExtensions2() throws Exception { + PatternsRequestCondition condition1 = new PatternsRequestCondition( + new String[] {"/prefix"}, null, null, true, false, Collections.singletonList("json")); + + PatternsRequestCondition condition2 = new PatternsRequestCondition( + new String[] {"/suffix"}, null, null, true, false, null); + + PatternsRequestCondition combined = condition1.combine(condition2); + + ServerWebExchange exchange = createExchange("/prefix/suffix.json"); + PatternsRequestCondition match = combined.getMatchingCondition(exchange); + + assertNotNull(match); + } + + @Test + public void matchTrailingSlash() throws Exception { + ServerWebExchange exchange = createExchange("/foo/"); + + PatternsRequestCondition condition = new PatternsRequestCondition("/foo"); + PatternsRequestCondition match = condition.getMatchingCondition(exchange); + + assertNotNull(match); + assertEquals("Should match by default", "/foo/", match.getPatterns().iterator().next()); + + condition = new PatternsRequestCondition(new String[] {"/foo"}, null, null, false, true, null); + match = condition.getMatchingCondition(exchange); + + assertNotNull(match); + assertEquals("Trailing slash should be insensitive to useSuffixPatternMatch settings (SPR-6164, SPR-5636)", + "/foo/", match.getPatterns().iterator().next()); + + condition = new PatternsRequestCondition(new String[] {"/foo"}, null, null, false, false, null); + match = condition.getMatchingCondition(exchange); + + assertNull(match); + } + + @Test + public void matchPatternContainsExtension() throws Exception { + PatternsRequestCondition condition = new PatternsRequestCondition("/foo.jpg"); + PatternsRequestCondition match = condition.getMatchingCondition(createExchange("/foo.html")); + + assertNull(match); + } + + @Test + public void compareEqualPatterns() throws Exception { + PatternsRequestCondition c1 = new PatternsRequestCondition("/foo*"); + PatternsRequestCondition c2 = new PatternsRequestCondition("/foo*"); + + assertEquals(0, c1.compareTo(c2, createExchange("/foo"))); + } + + @Test + public void comparePatternSpecificity() throws Exception { + PatternsRequestCondition c1 = new PatternsRequestCondition("/fo*"); + PatternsRequestCondition c2 = new PatternsRequestCondition("/foo"); + + assertEquals(1, c1.compareTo(c2, createExchange("/foo"))); + } + + @Test + public void compareNumberOfMatchingPatterns() throws Exception { + ServerWebExchange exchange = createExchange("/foo.html"); + + PatternsRequestCondition c1 = new PatternsRequestCondition("/foo", "*.jpeg"); + PatternsRequestCondition c2 = new PatternsRequestCondition("/foo", "*.html"); + + PatternsRequestCondition match1 = c1.getMatchingCondition(exchange); + PatternsRequestCondition match2 = c2.getMatchingCondition(exchange); + + assertNotNull(match1); + assertEquals(1, match1.compareTo(match2, exchange)); + } + + + private ServerWebExchange createExchange(String path) throws URISyntaxException { + ServerHttpRequest request = new MockServerHttpRequest(HttpMethod.GET, new URI(path)); + WebSessionManager sessionManager = mock(WebSessionManager.class); + return new DefaultServerWebExchange(request, new MockServerHttpResponse(), sessionManager); + } + +} diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/condition/ProducesRequestConditionTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/condition/ProducesRequestConditionTests.java new file mode 100644 index 0000000000..4ae8d19363 --- /dev/null +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/condition/ProducesRequestConditionTests.java @@ -0,0 +1,327 @@ +/* + * Copyright 2002-2016 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.reactive.result.condition; + +import java.net.URI; +import java.net.URISyntaxException; +import java.util.Collection; +import java.util.Collections; + +import org.junit.Test; + +import org.springframework.http.HttpMethod; +import org.springframework.http.server.reactive.MockServerHttpRequest; +import org.springframework.http.server.reactive.MockServerHttpResponse; +import org.springframework.http.server.reactive.ServerHttpRequest; +import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.server.adapter.DefaultServerWebExchange; +import org.springframework.web.server.session.WebSessionManager; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import static org.mockito.Mockito.mock; + +/** + * Unit tests for {@link ProducesRequestCondition}. + * + * @author Rossen Stoyanchev + */ +public class ProducesRequestConditionTests { + + @Test + public void match() throws Exception { + ServerWebExchange exchange = createExchange("text/plain"); + ProducesRequestCondition condition = new ProducesRequestCondition("text/plain"); + + assertNotNull(condition.getMatchingCondition(exchange)); + } + + @Test + public void matchNegated() throws Exception { + ServerWebExchange exchange = createExchange("text/plain"); + ProducesRequestCondition condition = new ProducesRequestCondition("!text/plain"); + + assertNull(condition.getMatchingCondition(exchange)); + } + + @Test + public void getProducibleMediaTypes() throws Exception { + ProducesRequestCondition condition = new ProducesRequestCondition("!application/xml"); + assertEquals(Collections.emptySet(), condition.getProducibleMediaTypes()); + } + + @Test + public void matchWildcard() throws Exception { + ServerWebExchange exchange = createExchange("text/plain"); + ProducesRequestCondition condition = new ProducesRequestCondition("text/*"); + + assertNotNull(condition.getMatchingCondition(exchange)); + } + + @Test + public void matchMultiple() throws Exception { + ServerWebExchange exchange = createExchange("text/plain"); + ProducesRequestCondition condition = new ProducesRequestCondition("text/plain", "application/xml"); + + assertNotNull(condition.getMatchingCondition(exchange)); + } + + @Test + public void matchSingle() throws Exception { + ServerWebExchange exchange = createExchange("application/xml"); + ProducesRequestCondition condition = new ProducesRequestCondition("text/plain"); + + assertNull(condition.getMatchingCondition(exchange)); + } + + @Test + public void matchParseError() throws Exception { + ServerWebExchange exchange = createExchange("bogus"); + ProducesRequestCondition condition = new ProducesRequestCondition("text/plain"); + + assertNull(condition.getMatchingCondition(exchange)); + } + + @Test + public void matchParseErrorWithNegation() throws Exception { + ServerWebExchange exchange = createExchange("bogus"); + ProducesRequestCondition condition = new ProducesRequestCondition("!text/plain"); + + assertNull(condition.getMatchingCondition(exchange)); + } + + @Test + public void compareTo() throws Exception { + ProducesRequestCondition html = new ProducesRequestCondition("text/html"); + ProducesRequestCondition xml = new ProducesRequestCondition("application/xml"); + ProducesRequestCondition none = new ProducesRequestCondition(); + + ServerWebExchange exchange = createExchange("application/xml, text/html"); + + assertTrue(html.compareTo(xml, exchange) > 0); + assertTrue(xml.compareTo(html, exchange) < 0); + assertTrue(xml.compareTo(none, exchange) < 0); + assertTrue(none.compareTo(xml, exchange) > 0); + assertTrue(html.compareTo(none, exchange) < 0); + assertTrue(none.compareTo(html, exchange) > 0); + + exchange = createExchange("application/xml, text/*"); + + assertTrue(html.compareTo(xml, exchange) > 0); + assertTrue(xml.compareTo(html, exchange) < 0); + + exchange = createExchange("application/pdf"); + + assertTrue(html.compareTo(xml, exchange) == 0); + assertTrue(xml.compareTo(html, exchange) == 0); + + // See SPR-7000 + exchange = createExchange("text/html;q=0.9,application/xml"); + + assertTrue(html.compareTo(xml, exchange) > 0); + assertTrue(xml.compareTo(html, exchange) < 0); + } + + @Test + public void compareToWithSingleExpression() throws Exception { + ServerWebExchange exchange = createExchange("text/plain"); + + ProducesRequestCondition condition1 = new ProducesRequestCondition("text/plain"); + ProducesRequestCondition condition2 = new ProducesRequestCondition("text/*"); + + int result = condition1.compareTo(condition2, exchange); + assertTrue("Invalid comparison result: " + result, result < 0); + + result = condition2.compareTo(condition1, exchange); + assertTrue("Invalid comparison result: " + result, result > 0); + } + + @Test + public void compareToMultipleExpressions() throws Exception { + ProducesRequestCondition condition1 = new ProducesRequestCondition("*/*", "text/plain"); + ProducesRequestCondition condition2 = new ProducesRequestCondition("text/*", "text/plain;q=0.7"); + + ServerWebExchange exchange = createExchange("text/plain"); + + int result = condition1.compareTo(condition2, exchange); + assertTrue("Invalid comparison result: " + result, result < 0); + + result = condition2.compareTo(condition1, exchange); + assertTrue("Invalid comparison result: " + result, result > 0); + } + + @Test + public void compareToMultipleExpressionsAndMultipeAcceptHeaderValues() throws Exception { + ProducesRequestCondition condition1 = new ProducesRequestCondition("text/*", "text/plain"); + ProducesRequestCondition condition2 = new ProducesRequestCondition("application/*", "application/xml"); + + ServerWebExchange exchange = createExchange("text/plain", "application/xml"); + + int result = condition1.compareTo(condition2, exchange); + assertTrue("Invalid comparison result: " + result, result < 0); + + result = condition2.compareTo(condition1, exchange); + assertTrue("Invalid comparison result: " + result, result > 0); + + exchange = createExchange("application/xml", "text/plain"); + + result = condition1.compareTo(condition2, exchange); + assertTrue("Invalid comparison result: " + result, result > 0); + + result = condition2.compareTo(condition1, exchange); + assertTrue("Invalid comparison result: " + result, result < 0); + } + + // SPR-8536 + + @Test + public void compareToMediaTypeAll() throws Exception { + ServerWebExchange exchange = createExchange(); + + ProducesRequestCondition condition1 = new ProducesRequestCondition(); + ProducesRequestCondition condition2 = new ProducesRequestCondition("application/json"); + + assertTrue("Should have picked '*/*' condition as an exact match", + condition1.compareTo(condition2, exchange) < 0); + assertTrue("Should have picked '*/*' condition as an exact match", + condition2.compareTo(condition1, exchange) > 0); + + condition1 = new ProducesRequestCondition("*/*"); + condition2 = new ProducesRequestCondition("application/json"); + + assertTrue(condition1.compareTo(condition2, exchange) < 0); + assertTrue(condition2.compareTo(condition1, exchange) > 0); + + exchange = createExchange("*/*"); + + condition1 = new ProducesRequestCondition(); + condition2 = new ProducesRequestCondition("application/json"); + + assertTrue(condition1.compareTo(condition2, exchange) < 0); + assertTrue(condition2.compareTo(condition1, exchange) > 0); + + condition1 = new ProducesRequestCondition("*/*"); + condition2 = new ProducesRequestCondition("application/json"); + + assertTrue(condition1.compareTo(condition2, exchange) < 0); + assertTrue(condition2.compareTo(condition1, exchange) > 0); + } + + // SPR-9021 + + @Test + public void compareToMediaTypeAllWithParameter() throws Exception { + ServerWebExchange exchange = createExchange("*/*;q=0.9"); + + ProducesRequestCondition condition1 = new ProducesRequestCondition(); + ProducesRequestCondition condition2 = new ProducesRequestCondition("application/json"); + + assertTrue(condition1.compareTo(condition2, exchange) < 0); + assertTrue(condition2.compareTo(condition1, exchange) > 0); + } + + @Test + public void compareToEqualMatch() throws Exception { + ServerWebExchange exchange = createExchange("text/*"); + + ProducesRequestCondition condition1 = new ProducesRequestCondition("text/plain"); + ProducesRequestCondition condition2 = new ProducesRequestCondition("text/xhtml"); + + int result = condition1.compareTo(condition2, exchange); + assertTrue("Should have used MediaType.equals(Object) to break the match", result < 0); + + result = condition2.compareTo(condition1, exchange); + assertTrue("Should have used MediaType.equals(Object) to break the match", result > 0); + } + + @Test + public void combine() throws Exception { + ProducesRequestCondition condition1 = new ProducesRequestCondition("text/plain"); + ProducesRequestCondition condition2 = new ProducesRequestCondition("application/xml"); + + ProducesRequestCondition result = condition1.combine(condition2); + assertEquals(condition2, result); + } + + @Test + public void combineWithDefault() throws Exception { + ProducesRequestCondition condition1 = new ProducesRequestCondition("text/plain"); + ProducesRequestCondition condition2 = new ProducesRequestCondition(); + + ProducesRequestCondition result = condition1.combine(condition2); + assertEquals(condition1, result); + } + + @Test + public void instantiateWithProducesAndHeaderConditions() throws Exception { + String[] produces = new String[] {"text/plain"}; + String[] headers = new String[]{"foo=bar", "accept=application/xml,application/pdf"}; + ProducesRequestCondition condition = new ProducesRequestCondition(produces, headers); + + assertConditions(condition, "text/plain", "application/xml", "application/pdf"); + } + + @Test + public void getMatchingCondition() throws Exception { + ServerWebExchange exchange = createExchange("text/plain"); + + ProducesRequestCondition condition = new ProducesRequestCondition("text/plain", "application/xml"); + + ProducesRequestCondition result = condition.getMatchingCondition(exchange); + assertConditions(result, "text/plain"); + + condition = new ProducesRequestCondition("application/xml"); + + result = condition.getMatchingCondition(exchange); + assertNull(result); + } + + private void assertConditions(ProducesRequestCondition condition, String... expected) { + Collection expressions = condition.getContent(); + assertEquals("Invalid number of conditions", expressions.size(), expected.length); + for (String s : expected) { + boolean found = false; + for (ProducesRequestCondition.ProduceMediaTypeExpression expr : expressions) { + String conditionMediaType = expr.getMediaType().toString(); + if (conditionMediaType.equals(s)) { + found = true; + break; + + } + } + if (!found) { + fail("Condition [" + s + "] not found"); + } + } + } + + + private ServerWebExchange createExchange(String... accept) throws URISyntaxException { + ServerHttpRequest request = new MockServerHttpRequest(HttpMethod.GET, new URI("/")); + if (accept != null) { + for (String value : accept) { + request.getHeaders().add("Accept", value); + } + } + WebSessionManager sessionManager = mock(WebSessionManager.class); + return new DefaultServerWebExchange(request, new MockServerHttpResponse(), sessionManager); + } + +} diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/condition/RequestConditionHolderTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/condition/RequestConditionHolderTests.java new file mode 100644 index 0000000000..d5686381b7 --- /dev/null +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/condition/RequestConditionHolderTests.java @@ -0,0 +1,141 @@ +/* + * Copyright 2002-2016 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.reactive.result.condition; + +import java.net.URI; +import java.net.URISyntaxException; + +import org.junit.Before; +import org.junit.Test; + +import org.springframework.http.HttpMethod; +import org.springframework.http.server.reactive.MockServerHttpRequest; +import org.springframework.http.server.reactive.MockServerHttpResponse; +import org.springframework.http.server.reactive.ServerHttpRequest; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.server.adapter.DefaultServerWebExchange; +import org.springframework.web.server.session.WebSessionManager; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; +import static org.mockito.Mockito.mock; + +/** + * Unit tests for {@link RequestConditionHolder}. + * + * @author Rossen Stoyanchev + */ +public class RequestConditionHolderTests { + + private ServerWebExchange exchange; + + + @Before + public void setUp() throws Exception { + this.exchange = createExchange(); + } + + private ServerWebExchange createExchange() throws URISyntaxException { + ServerHttpRequest request = new MockServerHttpRequest(HttpMethod.GET, new URI("/")); + WebSessionManager sessionManager = mock(WebSessionManager.class); + return new DefaultServerWebExchange(request, new MockServerHttpResponse(), sessionManager); + } + + + @Test + public void combine() { + RequestConditionHolder params1 = new RequestConditionHolder(new ParamsRequestCondition("name1")); + RequestConditionHolder params2 = new RequestConditionHolder(new ParamsRequestCondition("name2")); + RequestConditionHolder expected = new RequestConditionHolder(new ParamsRequestCondition("name1", "name2")); + + 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")); + RequestConditionHolder headers = new RequestConditionHolder(new HeadersRequestCondition("name")); + params.combine(headers); + } + + @Test + public void match() { + RequestMethodsRequestCondition rm = new RequestMethodsRequestCondition(RequestMethod.GET, RequestMethod.POST); + RequestConditionHolder custom = new RequestConditionHolder(rm); + RequestMethodsRequestCondition expected = new RequestMethodsRequestCondition(RequestMethod.GET); + + RequestConditionHolder holder = custom.getMatchingCondition(this.exchange); + assertNotNull(holder); + assertEquals(expected, holder.getCondition()); + } + + @Test + public void noMatch() { + RequestMethodsRequestCondition rm = new RequestMethodsRequestCondition(RequestMethod.POST); + RequestConditionHolder custom = new RequestConditionHolder(rm); + + assertNull(custom.getMatchingCondition(this.exchange)); + } + + @Test + public void matchEmpty() { + RequestConditionHolder empty = new RequestConditionHolder(null); + assertSame(empty, empty.getMatchingCondition(this.exchange)); + } + + @Test + public void compare() { + RequestConditionHolder params11 = new RequestConditionHolder(new ParamsRequestCondition("1")); + RequestConditionHolder params12 = new RequestConditionHolder(new ParamsRequestCondition("1", "2")); + + assertEquals(1, params11.compareTo(params12, this.exchange)); + assertEquals(-1, params12.compareTo(params11, this.exchange)); + } + + @Test + public void compareEmpty() { + RequestConditionHolder empty = new RequestConditionHolder(null); + RequestConditionHolder empty2 = new RequestConditionHolder(null); + RequestConditionHolder notEmpty = new RequestConditionHolder(new ParamsRequestCondition("name")); + + assertEquals(0, empty.compareTo(empty2, this.exchange)); + assertEquals(-1, notEmpty.compareTo(empty, this.exchange)); + assertEquals(1, empty.compareTo(notEmpty, this.exchange)); + } + + @Test(expected=ClassCastException.class) + public void compareIncompatible() { + RequestConditionHolder params = new RequestConditionHolder(new ParamsRequestCondition("name")); + RequestConditionHolder headers = new RequestConditionHolder(new HeadersRequestCondition("name")); + params.compareTo(headers, this.exchange); + } + + +} diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/condition/RequestMappingInfoTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/condition/RequestMappingInfoTests.java new file mode 100644 index 0000000000..7ebc75be9e --- /dev/null +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/condition/RequestMappingInfoTests.java @@ -0,0 +1,355 @@ +/* + * Copyright 2002-2016 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.reactive.result.condition; + +import java.net.URI; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; + +import org.junit.Before; +import org.junit.Ignore; +import org.junit.Test; + +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.MediaType; +import org.springframework.http.server.reactive.MockServerHttpRequest; +import org.springframework.http.server.reactive.MockServerHttpResponse; +import org.springframework.http.server.reactive.ServerHttpRequest; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.reactive.result.method.RequestMappingInfo; +import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.server.adapter.DefaultServerWebExchange; +import org.springframework.web.server.session.WebSessionManager; + +import static java.util.Arrays.asList; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.mockito.Mockito.mock; + +/** + * Unit tests for {@link RequestMappingInfo}. + * + * @author Rossen Stoyanchev + */ +public class RequestMappingInfoTests { + + private ServerWebExchange exchange; + + private ServerHttpRequest request; + + + // TODO: CORS pre-flight (see @Ignored) + + @Before + public void setUp() throws Exception { + WebSessionManager sessionManager = mock(WebSessionManager.class); + this.request = new MockServerHttpRequest(HttpMethod.GET, new URI("/foo")); + this.exchange = new DefaultServerWebExchange(request, new MockServerHttpResponse(), sessionManager); + } + + + @Test + public void createEmpty() { + RequestMappingInfo info = new RequestMappingInfo(null, null, null, null, null, null, null); + + assertEquals(0, info.getPatternsCondition().getPatterns().size()); + assertEquals(0, info.getMethodsCondition().getMethods().size()); + assertEquals(true, info.getConsumesCondition().isEmpty()); + assertEquals(true, info.getProducesCondition().isEmpty()); + assertNotNull(info.getParamsCondition()); + assertNotNull(info.getHeadersCondition()); + assertNull(info.getCustomCondition()); + } + + @Test + public void matchPatternsCondition() { + RequestMappingInfo info = new RequestMappingInfo( + new PatternsRequestCondition("/foo*", "/bar"), null, null, null, null, null, null); + RequestMappingInfo expected = new RequestMappingInfo( + new PatternsRequestCondition("/foo*"), null, null, null, null, null, null); + + assertEquals(expected, info.getMatchingCondition(this.exchange)); + + info = new RequestMappingInfo( + new PatternsRequestCondition("/**", "/foo*", "/foo"), null, null, null, null, null, null); + expected = new RequestMappingInfo( + new PatternsRequestCondition("/foo", "/foo*", "/**"), null, null, null, null, null, null); + + assertEquals(expected, info.getMatchingCondition(this.exchange)); + } + + @Test + public void matchParamsCondition() { + this.request.getQueryParams().add("foo", "bar"); + + RequestMappingInfo info = + new RequestMappingInfo( + new PatternsRequestCondition("/foo"), null, + new ParamsRequestCondition("foo=bar"), null, null, null, null); + RequestMappingInfo match = info.getMatchingCondition(this.exchange); + + assertNotNull(match); + + info = new RequestMappingInfo( + new PatternsRequestCondition("/foo"), null, + new ParamsRequestCondition("foo!=bar"), null, null, null, null); + match = info.getMatchingCondition(this.exchange); + + assertNull(match); + } + + @Test + public void matchHeadersCondition() { + this.request.getHeaders().add("foo", "bar"); + + RequestMappingInfo info = + new RequestMappingInfo( + new PatternsRequestCondition("/foo"), null, null, + new HeadersRequestCondition("foo=bar"), null, null, null); + RequestMappingInfo match = info.getMatchingCondition(this.exchange); + + assertNotNull(match); + + info = new RequestMappingInfo( + new PatternsRequestCondition("/foo"), null, null, + new HeadersRequestCondition("foo!=bar"), null, null, null); + match = info.getMatchingCondition(this.exchange); + + assertNull(match); + } + + @Test + public void matchConsumesCondition() { + this.request.getHeaders().setContentType(MediaType.TEXT_PLAIN); + + RequestMappingInfo info = + new RequestMappingInfo( + new PatternsRequestCondition("/foo"), null, null, null, + new ConsumesRequestCondition("text/plain"), null, null); + RequestMappingInfo match = info.getMatchingCondition(this.exchange); + + assertNotNull(match); + + info = new RequestMappingInfo( + new PatternsRequestCondition("/foo"), null, null, null, + new ConsumesRequestCondition("application/xml"), null, null); + match = info.getMatchingCondition(this.exchange); + + assertNull(match); + } + + @Test + public void matchProducesCondition() { + this.request.getHeaders().setAccept(Collections.singletonList(MediaType.TEXT_PLAIN)); + + RequestMappingInfo info = + new RequestMappingInfo( + new PatternsRequestCondition("/foo"), null, null, null, null, + new ProducesRequestCondition("text/plain"), null); + RequestMappingInfo match = info.getMatchingCondition(this.exchange); + + assertNotNull(match); + + info = new RequestMappingInfo( + new PatternsRequestCondition("/foo"), null, null, null, null, + new ProducesRequestCondition("application/xml"), null); + match = info.getMatchingCondition(this.exchange); + + assertNull(match); + } + + @Test + public void matchCustomCondition() { + this.request.getQueryParams().add("foo", "bar"); + + RequestMappingInfo info = + new RequestMappingInfo( + new PatternsRequestCondition("/foo"), null, null, null, null, null, + new ParamsRequestCondition("foo=bar")); + RequestMappingInfo match = info.getMatchingCondition(this.exchange); + + assertNotNull(match); + + info = new RequestMappingInfo( + new PatternsRequestCondition("/foo"), null, + new ParamsRequestCondition("foo!=bar"), null, null, null, + new ParamsRequestCondition("foo!=bar")); + match = info.getMatchingCondition(this.exchange); + + assertNull(match); + } + + @Test + public void compareTwoHttpMethodsOneParam() { + RequestMappingInfo none = new RequestMappingInfo(null, null, null, null, null, null, null); + RequestMappingInfo oneMethod = + new RequestMappingInfo(null, + new RequestMethodsRequestCondition(RequestMethod.GET), null, null, null, null, null); + RequestMappingInfo oneMethodOneParam = + new RequestMappingInfo(null, + new RequestMethodsRequestCondition(RequestMethod.GET), + new ParamsRequestCondition("foo"), null, null, null, null); + + Comparator comparator = (info, otherInfo) -> info.compareTo(otherInfo, exchange); + + List list = asList(none, oneMethod, oneMethodOneParam); + Collections.shuffle(list); + Collections.sort(list, comparator); + + assertEquals(oneMethodOneParam, list.get(0)); + assertEquals(oneMethod, list.get(1)); + assertEquals(none, list.get(2)); + } + + @Test + public void equals() { + RequestMappingInfo info1 = new RequestMappingInfo( + new PatternsRequestCondition("/foo"), + new RequestMethodsRequestCondition(RequestMethod.GET), + new ParamsRequestCondition("foo=bar"), + new HeadersRequestCondition("foo=bar"), + new ConsumesRequestCondition("text/plain"), + new ProducesRequestCondition("text/plain"), + new ParamsRequestCondition("customFoo=customBar")); + + RequestMappingInfo info2 = new RequestMappingInfo( + new PatternsRequestCondition("/foo"), + new RequestMethodsRequestCondition(RequestMethod.GET), + new ParamsRequestCondition("foo=bar"), + new HeadersRequestCondition("foo=bar"), + new ConsumesRequestCondition("text/plain"), + new ProducesRequestCondition("text/plain"), + new ParamsRequestCondition("customFoo=customBar")); + + assertEquals(info1, info2); + assertEquals(info1.hashCode(), info2.hashCode()); + + info2 = new RequestMappingInfo( + new PatternsRequestCondition("/foo", "/NOOOOOO"), + new RequestMethodsRequestCondition(RequestMethod.GET), + new ParamsRequestCondition("foo=bar"), + new HeadersRequestCondition("foo=bar"), + new ConsumesRequestCondition("text/plain"), + new ProducesRequestCondition("text/plain"), + new ParamsRequestCondition("customFoo=customBar")); + + assertFalse(info1.equals(info2)); + assertNotEquals(info1.hashCode(), info2.hashCode()); + + info2 = new RequestMappingInfo( + new PatternsRequestCondition("/foo"), + new RequestMethodsRequestCondition(RequestMethod.GET, RequestMethod.POST), + new ParamsRequestCondition("foo=bar"), + new HeadersRequestCondition("foo=bar"), + new ConsumesRequestCondition("text/plain"), + new ProducesRequestCondition("text/plain"), + new ParamsRequestCondition("customFoo=customBar")); + + assertFalse(info1.equals(info2)); + assertNotEquals(info1.hashCode(), info2.hashCode()); + + info2 = new RequestMappingInfo( + new PatternsRequestCondition("/foo"), + new RequestMethodsRequestCondition(RequestMethod.GET), + new ParamsRequestCondition("/NOOOOOO"), + new HeadersRequestCondition("foo=bar"), + new ConsumesRequestCondition("text/plain"), + new ProducesRequestCondition("text/plain"), + new ParamsRequestCondition("customFoo=customBar")); + + assertFalse(info1.equals(info2)); + assertNotEquals(info1.hashCode(), info2.hashCode()); + + info2 = new RequestMappingInfo( + new PatternsRequestCondition("/foo"), + new RequestMethodsRequestCondition(RequestMethod.GET), + new ParamsRequestCondition("foo=bar"), + new HeadersRequestCondition("/NOOOOOO"), + new ConsumesRequestCondition("text/plain"), + new ProducesRequestCondition("text/plain"), + new ParamsRequestCondition("customFoo=customBar")); + + assertFalse(info1.equals(info2)); + assertNotEquals(info1.hashCode(), info2.hashCode()); + + info2 = new RequestMappingInfo( + new PatternsRequestCondition("/foo"), + new RequestMethodsRequestCondition(RequestMethod.GET), + new ParamsRequestCondition("foo=bar"), + new HeadersRequestCondition("foo=bar"), + new ConsumesRequestCondition("text/NOOOOOO"), + new ProducesRequestCondition("text/plain"), + new ParamsRequestCondition("customFoo=customBar")); + + assertFalse(info1.equals(info2)); + assertNotEquals(info1.hashCode(), info2.hashCode()); + + info2 = new RequestMappingInfo( + new PatternsRequestCondition("/foo"), + new RequestMethodsRequestCondition(RequestMethod.GET), + new ParamsRequestCondition("foo=bar"), + new HeadersRequestCondition("foo=bar"), + new ConsumesRequestCondition("text/plain"), + new ProducesRequestCondition("text/NOOOOOO"), + new ParamsRequestCondition("customFoo=customBar")); + + assertFalse(info1.equals(info2)); + assertNotEquals(info1.hashCode(), info2.hashCode()); + + info2 = new RequestMappingInfo( + new PatternsRequestCondition("/foo"), + new RequestMethodsRequestCondition(RequestMethod.GET), + new ParamsRequestCondition("foo=bar"), + new HeadersRequestCondition("foo=bar"), + new ConsumesRequestCondition("text/plain"), + new ProducesRequestCondition("text/plain"), + new ParamsRequestCondition("customFoo=NOOOOOO")); + + assertFalse(info1.equals(info2)); + assertNotEquals(info1.hashCode(), info2.hashCode()); + } + + @Test + @Ignore + public void preFlightRequest() throws Exception { + ServerHttpRequest request = new MockServerHttpRequest(HttpMethod.OPTIONS, new URI("/foo")); + request.getHeaders().add(HttpHeaders.ORIGIN, "http://domain.com"); + request.getHeaders().add(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, "POST"); + + WebSessionManager manager = mock(WebSessionManager.class); + MockServerHttpResponse response = new MockServerHttpResponse(); + ServerWebExchange exchange = new DefaultServerWebExchange(request, response, manager); + + RequestMappingInfo info = new RequestMappingInfo( + new PatternsRequestCondition("/foo"), new RequestMethodsRequestCondition(RequestMethod.POST), null, + null, null, null, null); + RequestMappingInfo match = info.getMatchingCondition(exchange); + assertNotNull(match); + + info = new RequestMappingInfo( + new PatternsRequestCondition("/foo"), new RequestMethodsRequestCondition(RequestMethod.OPTIONS), null, + null, null, null, null); + match = info.getMatchingCondition(exchange); + assertNull("Pre-flight should match the ACCESS_CONTROL_REQUEST_METHOD", match); + } + +} diff --git a/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/condition/RequestMethodsRequestConditionTests.java b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/condition/RequestMethodsRequestConditionTests.java new file mode 100644 index 0000000000..cce2b92ee0 --- /dev/null +++ b/spring-web-reactive/src/test/java/org/springframework/web/reactive/result/condition/RequestMethodsRequestConditionTests.java @@ -0,0 +1,157 @@ +/* + * Copyright 2002-2016 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.reactive.result.condition; + +import java.net.URI; +import java.net.URISyntaxException; +import java.util.Collections; + +import org.junit.Ignore; +import org.junit.Test; + +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.server.reactive.MockServerHttpRequest; +import org.springframework.http.server.reactive.MockServerHttpResponse; +import org.springframework.http.server.reactive.ServerHttpRequest; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.server.adapter.DefaultServerWebExchange; +import org.springframework.web.server.session.WebSessionManager; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.springframework.web.bind.annotation.RequestMethod.DELETE; +import static org.springframework.web.bind.annotation.RequestMethod.GET; +import static org.springframework.web.bind.annotation.RequestMethod.HEAD; +import static org.springframework.web.bind.annotation.RequestMethod.OPTIONS; +import static org.springframework.web.bind.annotation.RequestMethod.POST; +import static org.springframework.web.bind.annotation.RequestMethod.PUT; + +/** + * Unit tests for {@link RequestMethodsRequestCondition}. + * + * @author Rossen Stoyanchev + */ +public class RequestMethodsRequestConditionTests { + + // TODO: custom method, CORS pre-flight (see @Ignored) + + @Test + public void getMatchingCondition() throws Exception { + testMatch(new RequestMethodsRequestCondition(GET), GET); + testMatch(new RequestMethodsRequestCondition(GET, POST), GET); + testNoMatch(new RequestMethodsRequestCondition(GET), POST); + } + + @Test + public void getMatchingConditionWithHttpHead() throws Exception { + testMatch(new RequestMethodsRequestCondition(HEAD), HEAD); + testMatch(new RequestMethodsRequestCondition(GET), HEAD); + testNoMatch(new RequestMethodsRequestCondition(POST), HEAD); + } + + @Test + public void getMatchingConditionWithEmptyConditions() throws Exception { + RequestMethodsRequestCondition condition = new RequestMethodsRequestCondition(); + for (RequestMethod method : RequestMethod.values()) { + if (!OPTIONS.equals(method)) { + ServerWebExchange exchange = createExchange(method.name()); + assertNotNull(condition.getMatchingCondition(exchange)); + } + } + testNoMatch(condition, OPTIONS); + } + + @Test + @Ignore + public void getMatchingConditionWithCustomMethod() throws Exception { + ServerWebExchange exchange = createExchange("PROPFIND"); + assertNotNull(new RequestMethodsRequestCondition().getMatchingCondition(exchange)); + assertNull(new RequestMethodsRequestCondition(GET, POST).getMatchingCondition(exchange)); + } + + @Test + @Ignore + public void getMatchingConditionWithCorsPreFlight() throws Exception { + ServerWebExchange exchange = createExchange("OPTIONS"); + exchange.getRequest().getHeaders().add("Origin", "http://example.com"); + exchange.getRequest().getHeaders().add(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, "PUT"); + + assertNotNull(new RequestMethodsRequestCondition().getMatchingCondition(exchange)); + assertNotNull(new RequestMethodsRequestCondition(PUT).getMatchingCondition(exchange)); + assertNull(new RequestMethodsRequestCondition(DELETE).getMatchingCondition(exchange)); + } + + @Test + public void compareTo() throws Exception { + RequestMethodsRequestCondition c1 = new RequestMethodsRequestCondition(GET, HEAD); + RequestMethodsRequestCondition c2 = new RequestMethodsRequestCondition(POST); + RequestMethodsRequestCondition c3 = new RequestMethodsRequestCondition(); + + ServerWebExchange exchange = createExchange(); + + int result = c1.compareTo(c2, exchange); + assertTrue("Invalid comparison result: " + result, result < 0); + + result = c2.compareTo(c1, exchange); + assertTrue("Invalid comparison result: " + result, result > 0); + + result = c2.compareTo(c3, exchange); + assertTrue("Invalid comparison result: " + result, result < 0); + + result = c1.compareTo(c1, exchange); + assertEquals("Invalid comparison result ", 0, result); + } + + @Test + public void combine() { + RequestMethodsRequestCondition condition1 = new RequestMethodsRequestCondition(GET); + RequestMethodsRequestCondition condition2 = new RequestMethodsRequestCondition(POST); + + RequestMethodsRequestCondition result = condition1.combine(condition2); + assertEquals(2, result.getContent().size()); + } + + + private void testMatch(RequestMethodsRequestCondition condition, RequestMethod method) throws Exception { + ServerWebExchange exchange = createExchange(method.name()); + RequestMethodsRequestCondition actual = condition.getMatchingCondition(exchange); + assertNotNull(actual); + assertEquals(Collections.singleton(method), actual.getContent()); + } + + private void testNoMatch(RequestMethodsRequestCondition condition, RequestMethod method) throws Exception { + ServerWebExchange exchange = createExchange(method.name()); + assertNull(condition.getMatchingCondition(exchange)); + } + + + private ServerWebExchange createExchange() throws URISyntaxException { + return createExchange(null); + } + + private ServerWebExchange createExchange(String method) throws URISyntaxException { + ServerHttpRequest request = new MockServerHttpRequest(HttpMethod.resolve(method), new URI("/")); + WebSessionManager sessionManager = mock(WebSessionManager.class); + return new DefaultServerWebExchange(request, new MockServerHttpResponse(), sessionManager); + } + +}