org.springframework.web.reactive.result.method.RequestMappingInfo Maven / Gradle / Ivy
/*
* Copyright 2002-2017 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.Arrays;
import java.util.List;
import java.util.stream.Collectors;
import org.springframework.lang.Nullable;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.reactive.accept.RequestedContentTypeResolver;
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.pattern.PathPattern;
import org.springframework.web.util.pattern.PathPatternParser;
/**
* Encapsulates the following request mapping conditions:
*
* - {@link PatternsRequestCondition}
*
- {@link RequestMethodsRequestCondition}
*
- {@link ParamsRequestCondition}
*
- {@link HeadersRequestCondition}
*
- {@link ConsumesRequestCondition}
*
- {@link ProducesRequestCondition}
*
- {@code RequestCondition} (optional, custom request condition)
*
*
* @author Rossen Stoyanchev
* @since 5.0
*/
public final class RequestMappingInfo implements RequestCondition {
@Nullable
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(@Nullable String name, @Nullable PatternsRequestCondition patterns,
@Nullable RequestMethodsRequestCondition methods, @Nullable ParamsRequestCondition params,
@Nullable HeadersRequestCondition headers, @Nullable ConsumesRequestCondition consumes,
@Nullable ProducesRequestCondition produces, @Nullable 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(@Nullable PatternsRequestCondition patterns,
@Nullable RequestMethodsRequestCondition methods, @Nullable ParamsRequestCondition params,
@Nullable HeadersRequestCondition headers, @Nullable ConsumesRequestCondition consumes,
@Nullable ProducesRequestCondition produces, @Nullable 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, @Nullable 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}.
*/
@Nullable
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}.
*/
@Nullable
public RequestCondition getCustomCondition() {
return this.customConditionHolder.getCondition();
}
/**
* Combines "this" request mapping info (i.e. the current instance) with another request mapping info instance.
* Example: combine type- and method-level request mappings.
* @return a new request mapping info instance; never {@code null}
*/
@Override
public RequestMappingInfo combine(RequestMappingInfo other) {
String name = combineNames(other);
PatternsRequestCondition patterns = this.patternsCondition.combine(other.patternsCondition);
RequestMethodsRequestCondition methods = this.methodsCondition.combine(other.methodsCondition);
ParamsRequestCondition params = this.paramsCondition.combine(other.paramsCondition);
HeadersRequestCondition headers = this.headersCondition.combine(other.headersCondition);
ConsumesRequestCondition consumes = this.consumesCondition.combine(other.consumesCondition);
ProducesRequestCondition produces = this.producesCondition.combine(other.producesCondition);
RequestConditionHolder custom = this.customConditionHolder.combine(other.customConditionHolder);
return new RequestMappingInfo(name, patterns,
methods, params, headers, consumes, produces, custom.getCondition());
}
@Nullable
private String combineNames(RequestMappingInfo other) {
if (this.name != null && other.name != null) {
String separator = "#";
return this.name + separator + other.name;
}
else if (this.name != null) {
return this.name;
}
else {
return (other.name != null ? other.name : null);
}
}
/**
* Checks if all conditions in this request mapping info match the provided request and returns
* a potentially new request mapping info with conditions tailored to the current request.
*
For example the returned instance may contain the subset of URL patterns that match to
* the current request, sorted with best matching patterns on top.
* @return a new instance in case all conditions match; or {@code null} otherwise
*/
@Override
@Nullable
public RequestMappingInfo getMatchingCondition(ServerWebExchange exchange) {
RequestMethodsRequestCondition methods = this.methodsCondition.getMatchingCondition(exchange);
ParamsRequestCondition params = this.paramsCondition.getMatchingCondition(exchange);
HeadersRequestCondition headers = this.headersCondition.getMatchingCondition(exchange);
ConsumesRequestCondition consumes = this.consumesCondition.getMatchingCondition(exchange);
ProducesRequestCondition produces = this.producesCondition.getMatchingCondition(exchange);
if (methods == null || params == null || headers == null || consumes == null || produces == null) {
return null;
}
PatternsRequestCondition patterns = this.patternsCondition.getMatchingCondition(exchange);
if (patterns == null) {
return null;
}
RequestConditionHolder custom = this.customConditionHolder.getMatchingCondition(exchange);
if (custom == null) {
return null;
}
return new RequestMappingInfo(this.name, patterns,
methods, params, headers, consumes, produces, custom.getCondition());
}
/**
* Compares "this" info (i.e. the current instance) with another info in the context of a request.
*
Note: It is assumed both instances have been obtained via
* {@link #getMatchingCondition(ServerWebExchange)} to ensure they have conditions with
* content relevant to current request.
*/
@Override
public int compareTo(RequestMappingInfo other, ServerWebExchange exchange) {
int result = this.patternsCondition.compareTo(other.getPatternsCondition(), exchange);
if (result != 0) {
return result;
}
result = this.paramsCondition.compareTo(other.getParamsCondition(), exchange);
if (result != 0) {
return result;
}
result = this.headersCondition.compareTo(other.getHeadersCondition(), exchange);
if (result != 0) {
return result;
}
result = this.consumesCondition.compareTo(other.getConsumesCondition(), exchange);
if (result != 0) {
return result;
}
result = this.producesCondition.compareTo(other.getProducesCondition(), exchange);
if (result != 0) {
return result;
}
result = this.methodsCondition.compareTo(other.getMethodsCondition(), exchange);
if (result != 0) {
return result;
}
result = this.customConditionHolder.compareTo(other.customConditionHolder, exchange);
if (result != 0) {
return result;
}
return 0;
}
@Override
public boolean equals(Object other) {
if (this == other) {
return true;
}
if (!(other instanceof RequestMappingInfo)) {
return false;
}
RequestMappingInfo otherInfo = (RequestMappingInfo) other;
return (this.patternsCondition.equals(otherInfo.patternsCondition) &&
this.methodsCondition.equals(otherInfo.methodsCondition) &&
this.paramsCondition.equals(otherInfo.paramsCondition) &&
this.headersCondition.equals(otherInfo.headersCondition) &&
this.consumesCondition.equals(otherInfo.consumesCondition) &&
this.producesCondition.equals(otherInfo.producesCondition) &&
this.customConditionHolder.equals(otherInfo.customConditionHolder));
}
@Override
public int hashCode() {
return (this.patternsCondition.hashCode() * 31 + // primary differentiation
this.methodsCondition.hashCode() + this.paramsCondition.hashCode() +
this.headersCondition.hashCode() + this.consumesCondition.hashCode() +
this.producesCondition.hashCode() + this.customConditionHolder.hashCode());
}
@Override
public String toString() {
StringBuilder builder = new StringBuilder("{");
builder.append(this.patternsCondition);
if (!this.methodsCondition.isEmpty()) {
builder.append(",methods=").append(this.methodsCondition);
}
if (!this.paramsCondition.isEmpty()) {
builder.append(",params=").append(this.paramsCondition);
}
if (!this.headersCondition.isEmpty()) {
builder.append(",headers=").append(this.headersCondition);
}
if (!this.consumesCondition.isEmpty()) {
builder.append(",consumes=").append(this.consumesCondition);
}
if (!this.producesCondition.isEmpty()) {
builder.append(",produces=").append(this.producesCondition);
}
if (!this.customConditionHolder.isEmpty()) {
builder.append(",custom=").append(this.customConditionHolder);
}
builder.append('}');
return builder.toString();
}
/**
* Create a new {@code RequestMappingInfo.Builder} with the given paths.
* @param paths the paths to use
*/
public static Builder paths(String... paths) {
return new DefaultBuilder(paths);
}
/**
* Defines a builder for creating a RequestMappingInfo.
*/
public interface Builder {
/**
* Set the path patterns.
*/
Builder paths(String... paths);
/**
* Set the request method conditions.
*/
Builder methods(RequestMethod... methods);
/**
* Set the request param conditions.
*/
Builder params(String... params);
/**
* Set the header conditions.
*
By default this is not set.
*/
Builder headers(String... headers);
/**
* Set the consumes conditions.
*/
Builder consumes(String... consumes);
/**
* Set the produces conditions.
*/
Builder produces(String... produces);
/**
* Set the mapping name.
*/
Builder mappingName(String name);
/**
* Set a custom condition to use.
*/
Builder customCondition(RequestCondition condition);
/**
* Provide additional configuration needed for request mapping purposes.
*/
Builder options(BuilderConfiguration options);
/**
* Build the RequestMappingInfo.
*/
RequestMappingInfo build();
}
private static class DefaultBuilder implements Builder {
private String[] paths;
@Nullable
private RequestMethod[] methods;
@Nullable
private String[] params;
@Nullable
private String[] headers;
@Nullable
private String[] consumes;
@Nullable
private String[] produces;
@Nullable
private String mappingName;
@Nullable
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() {
RequestedContentTypeResolver contentTypeResolver = this.options.getContentTypeResolver();
PathPatternParser parser = this.options.getPatternParser() != null ?
this.options.getPatternParser() : new PathPatternParser();
PatternsRequestCondition patternsCondition = new PatternsRequestCondition(parse(this.paths, parser));
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);
}
private static List parse(String[] paths, PathPatternParser parser) {
return Arrays
.stream(paths)
.map(path -> {
if (StringUtils.hasText(path) && !path.startsWith("/")) {
path = "/" + path;
}
return parser.parse(path);
})
.collect(Collectors.toList());
}
}
/**
* 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 {
@Nullable
private PathPatternParser patternParser;
@Nullable
private RequestedContentTypeResolver contentTypeResolver;
public void setPatternParser(PathPatternParser patternParser) {
this.patternParser = patternParser;
}
@Nullable
public PathPatternParser getPatternParser() {
return this.patternParser;
}
/**
* Set the ContentNegotiationManager to use for the ProducesRequestCondition.
* By default this is not set.
*/
public void setContentTypeResolver(RequestedContentTypeResolver resolver) {
this.contentTypeResolver = resolver;
}
@Nullable
public RequestedContentTypeResolver getContentTypeResolver() {
return this.contentTypeResolver;
}
}
}