Add RequestCondition hierarchy

A pretty complete equivalent to the same in spring-webmvc except for
CORS checks, and custom HTTP methods. Another notable difference is
that the "params" condition works on query params strictly.
This commit is contained in:
Rossen Stoyanchev
2016-04-15 16:09:59 -04:00
parent 641c6428e8
commit b2ca216498
25 changed files with 4608 additions and 0 deletions

View File

@@ -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<AbstractMediaTypeExpression>, 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();
}
}

View File

@@ -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<T> implements NameValueExpression<T> {
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();
}
}

View File

@@ -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<T extends AbstractRequestCondition<T>>
implements RequestCondition<T> {
@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.
* <p>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.
* <p>For example {@code " || "} for URL patterns or {@code " && "}
* for param expressions.
*/
protected abstract String getToStringInfix();
}

View File

@@ -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.
*
* <p>When {@code CompositeRequestCondition} instances are combined or compared
* they are expected to (a) contain the same number of conditions and (b) that
* conditions in the respective index are of the same type. It is acceptable to
* provide {@code null} conditions or no conditions at all to the constructor.
*
* @author Rossen Stoyanchev
*/
public class CompositeRequestCondition extends AbstractRequestCondition<CompositeRequestCondition> {
private final RequestConditionHolder[] requestConditions;
/**
* Create an instance with 0 or more {@code RequestCondition} types. It is
* important to create {@code CompositeRequestCondition} instances with the
* same number of conditions so they may be compared and combined.
* It is acceptable to provide {@code null} conditions.
*/
public CompositeRequestCondition(RequestCondition<?>... requestConditions) {
this.requestConditions = wrap(requestConditions);
}
private 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<RequestCondition<?>> getConditions() {
return unwrap();
}
private List<RequestCondition<?>> unwrap() {
List<RequestCondition<?>> 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 <em>all</em> contained conditions to match the request and return the
* resulting "matching" condition instances.
* <p>An empty {@code CompositeRequestCondition} matches to all requests.
*/
@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;
}
}
}

View File

@@ -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<ConsumesRequestCondition> {
// private final static ConsumesRequestCondition PRE_FLIGHT_MATCH = new ConsumesRequestCondition();
private final List<ConsumeMediaTypeExpression> 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<ConsumeMediaTypeExpression> expressions) {
this.expressions = new ArrayList<>(expressions);
Collections.sort(this.expressions);
}
private static Set<ConsumeMediaTypeExpression> parseExpressions(String[] consumes, String[] headers) {
Set<ConsumeMediaTypeExpression> 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<MediaTypeExpression> getExpressions() {
return new LinkedHashSet<>(this.expressions);
}
/**
* Returns the media types for this condition excluding negated expressions.
*/
public Set<MediaType> getConsumableMediaTypes() {
Set<MediaType> 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<ConsumeMediaTypeExpression> 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<ConsumeMediaTypeExpression> result = new LinkedHashSet<>(expressions);
for (Iterator<ConsumeMediaTypeExpression> iterator = result.iterator(); iterator.hasNext();) {
ConsumeMediaTypeExpression expression = iterator.next();
if (!expression.match(exchange)) {
iterator.remove();
}
}
return (result.isEmpty()) ? null : new ConsumesRequestCondition(result);
}
/**
* Returns:
* <ul>
* <li>0 if the two conditions have the same number of expressions
* <li>Less than 0 if "this" has more or more specific media type expressions
* <li>Greater than 0 if "other" has more or more specific media type expressions
* </ul>
* <p>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());
}
}
}
}

View File

@@ -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()}.
*
* <p>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<HeadersRequestCondition> {
// private final static HeadersRequestCondition PRE_FLIGHT_MATCH = new HeadersRequestCondition();
private final Set<HeaderExpression> 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<HeaderExpression> conditions) {
this.expressions = Collections.unmodifiableSet(new LinkedHashSet<>(conditions));
}
private static Collection<HeaderExpression> parseExpressions(String... headers) {
Set<HeaderExpression> expressions = new LinkedHashSet<HeaderExpression>();
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<NameValueExpression<String>> getExpressions() {
return new LinkedHashSet<>(this.expressions);
}
@Override
protected Collection<HeaderExpression> 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<HeaderExpression> 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:
* <ul>
* <li>0 if the two conditions have the same number of header expressions
* <li>Less than 0 if "this" instance has more header expressions
* <li>Greater than 0 if the "other" instance has more header expressions
* </ul>
* <p>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<String> {
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));
}
}
}

View File

@@ -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();
}

View File

@@ -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<T> {
String getName();
T getValue();
boolean isNegated();
}

View File

@@ -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<ParamsRequestCondition> {
private final Set<ParamExpression> 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<ParamExpression> conditions) {
this.expressions = Collections.unmodifiableSet(new LinkedHashSet<>(conditions));
}
private static Collection<ParamExpression> parseExpressions(String... params) {
Set<ParamExpression> 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<NameValueExpression<String>> getExpressions() {
return new LinkedHashSet<>(this.expressions);
}
@Override
protected Collection<ParamExpression> 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<ParamExpression> 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:
* <ul>
* <li>0 if the two conditions have the same number of parameter expressions
* <li>Less than 0 if "this" instance has more parameter expressions
* <li>Greater than 0 if the "other" instance has more parameter expressions
* </ul>
* <p>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<String> {
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));
}
}
}

View File

@@ -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<PatternsRequestCondition> {
private final Set<String> patterns;
private final HttpRequestPathHelper pathHelper;
private final PathMatcher pathMatcher;
private final boolean useSuffixPatternMatch;
private final boolean useTrailingSlashMatch;
private final List<String> fileExtensions = new ArrayList<String>();
/**
* 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<String> extensions) {
this(asList(patterns), pathHelper, pathMatcher, useSuffixPatternMatch, useTrailingSlashMatch, extensions);
}
/**
* Private constructor accepting a collection of patterns.
*/
private PatternsRequestCondition(Collection<String> patterns, HttpRequestPathHelper pathHelper,
PathMatcher pathMatcher, boolean useSuffixPatternMatch, boolean useTrailingSlashMatch,
List<String> 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<String> asList(String... patterns) {
return (patterns != null ? Arrays.asList(patterns) : Collections.emptyList());
}
private static Set<String> prependLeadingSlash(Collection<String> patterns) {
if (patterns == null) {
return Collections.emptySet();
}
Set<String> result = new LinkedHashSet<>(patterns.size());
for (String pattern : patterns) {
if (StringUtils.hasLength(pattern) && !pattern.startsWith("/")) {
pattern = "/" + pattern;
}
result.add(pattern);
}
return result;
}
public Set<String> getPatterns() {
return this.patterns;
}
@Override
protected Collection<String> 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:
* <ul>
* <li>If there are patterns in both instances, combine the patterns in "this" with
* the patterns in "other" using {@link PathMatcher#combine(String, String)}.
* <li>If only one instance has patterns, use them.
* <li>If neither instance has patterns, use an empty String (i.e. "").
* </ul>
*/
@Override
public PatternsRequestCondition combine(PatternsRequestCondition other) {
Set<String> 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)}.
* <p>A matching pattern is obtained by making checks in the following order:
* <ul>
* <li>Direct match
* <li>Pattern match with ".*" appended if the pattern doesn't already contain a "."
* <li>Pattern match
* <li>Pattern match with "/" appended if the pattern doesn't already end in "/"
* </ul>
* @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<String> 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<String> getMatchingPatterns(String lookupPath) {
List<String> 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.
* <p>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<String> patternComparator = this.pathMatcher.getPatternComparator(lookupPath);
Iterator<String> iterator = this.patterns.iterator();
Iterator<String> 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;
}
}
}

View File

@@ -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<ProducesRequestCondition> {
// private final static ProducesRequestCondition PRE_FLIGHT_MATCH = new ProducesRequestCondition();
private final List<ProduceMediaTypeExpression> MEDIA_TYPE_ALL_LIST =
Collections.singletonList(new ProduceMediaTypeExpression("*/*"));
private final List<ProduceMediaTypeExpression> 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<ProduceMediaTypeExpression> expressions,
ContentTypeResolver resolver) {
this.expressions = new ArrayList<>(expressions);
Collections.sort(this.expressions);
this.contentTypeResolver = (resolver != null ? resolver : new HeaderContentTypeResolver());
}
private Set<ProduceMediaTypeExpression> parseExpressions(String[] produces, String[] headers) {
Set<ProduceMediaTypeExpression> 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<MediaTypeExpression> getExpressions() {
return new LinkedHashSet<>(this.expressions);
}
/**
* Return the contained producible media types excluding negated expressions.
*/
public Set<MediaType> getProducibleMediaTypes() {
Set<MediaType> 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<ProduceMediaTypeExpression> 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<ProduceMediaTypeExpression> result = new LinkedHashSet<>(expressions);
for (Iterator<ProduceMediaTypeExpression> 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:
* <ol>
* <li>Sort 'Accept' header media types by quality value via
* {@link MediaType#sortByQualityValue(List)} and iterate the list.
* <li>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)}.
* <li>If a lower index is found, the condition at that index wins.
* <li>If both indexes are equal, the media types at the index are
* compared further with {@link MediaType#SPECIFICITY_COMPARATOR}.
* </ol>
* <p>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<MediaType> 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<MediaType> getAcceptedMediaTypes(ServerWebExchange exchange)
throws HttpMediaTypeNotAcceptableException {
List<MediaType> 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<ProduceMediaTypeExpression> 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<MediaType> acceptedMediaTypes = getAcceptedMediaTypes(exchange);
for (MediaType acceptedMediaType : acceptedMediaTypes) {
if (getMediaType().isCompatibleWith(acceptedMediaType)) {
return true;
}
}
return false;
}
}
}

View File

@@ -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.
*
* <p>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 <T> the type of objects that this RequestCondition can be combined
* with and compared to
*/
public interface RequestCondition<T> {
/**
* 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.
* <p>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);
}

View File

@@ -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.
*
* <p>When two {@code RequestConditionHolder} instances are combined or compared
* with each other, it is expected the conditions they hold are of the same type.
* If they are not, a {@link ClassCastException} is raised.
*
* @author Rossen Stoyanchev
*/
public final class RequestConditionHolder extends AbstractRequestCondition<RequestConditionHolder> {
private final RequestCondition<Object> condition;
/**
* Create a new holder to wrap the given request condition.
* @param requestCondition the condition to hold, may be {@code null}
*/
@SuppressWarnings("unchecked")
public RequestConditionHolder(RequestCondition<?> requestCondition) {
this.condition = (RequestCondition<Object>) 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);
}
}
}

View File

@@ -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<RequestMethodsRequestCondition> {
private static final RequestMethodsRequestCondition HEAD_CONDITION =
new RequestMethodsRequestCondition(RequestMethod.HEAD);
private final Set<RequestMethod> 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<RequestMethod> requestMethods) {
this.methods = Collections.unmodifiableSet(new LinkedHashSet<>(requestMethods));
}
private static List<RequestMethod> asList(RequestMethod... requestMethods) {
return (requestMethods != null ? Arrays.asList(requestMethods) : Collections.emptyList());
}
/**
* Returns all {@link RequestMethod}s contained in this condition.
*/
public Set<RequestMethod> getMethods() {
return this.methods;
}
@Override
protected Collection<RequestMethod> 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<RequestMethod> 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:
* <ul>
* <li>0 if the two conditions contain the same number of HTTP request methods
* <li>Less than 0 if "this" instance has an HTTP request method but "other" doesn't
* <li>Greater than 0 "other" has an HTTP request method but "this" doesn't
* </ul>
* <p>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());
}
}

View File

@@ -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;

View File

@@ -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:
* <ol>
* <li>{@link PatternsRequestCondition}
* <li>{@link RequestMethodsRequestCondition}
* <li>{@link ParamsRequestCondition}
* <li>{@link HeadersRequestCondition}
* <li>{@link ConsumesRequestCondition}
* <li>{@link ProducesRequestCondition}
* <li>{@code RequestCondition} (optional, custom request condition)
* </ol>
*
* @author Rossen Stoyanchev
*/
public final class RequestMappingInfo implements RequestCondition<RequestMappingInfo> {
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.
* <p>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.
* <p>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.
* <p>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.
* <p>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.
* <p>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.
* <p>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.
* <p>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.
* <p>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<String> 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.
* <p>By default this is not set.
*/
public void setContentTypeResolver(ContentTypeResolver resolver) {
this.contentTypeResolver = resolver;
}
public ContentTypeResolver getContentTypeResolver() {
return this.contentTypeResolver;
}
}
}

View File

@@ -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);
}
}

View File

@@ -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<ConsumeMediaTypeExpression> 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);
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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<String> 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);
}
}

View File

@@ -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<ProducesRequestCondition.ProduceMediaTypeExpression> 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);
}
}

View File

@@ -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);
}
}

View File

@@ -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<RequestMappingInfo> comparator = (info, otherInfo) -> info.compareTo(otherInfo, exchange);
List<RequestMappingInfo> 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);
}
}

View File

@@ -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);
}
}