18 changed files with 1244 additions and 14 deletions
@ -0,0 +1,40 @@ |
|||||||
|
/* |
||||||
|
* Copyright 2002-2025 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 |
||||||
|
* |
||||||
|
* https://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.accept; |
||||||
|
|
||||||
|
import org.jspecify.annotations.Nullable; |
||||||
|
|
||||||
|
import org.springframework.web.server.ServerWebExchange; |
||||||
|
|
||||||
|
/** |
||||||
|
* Contract to extract the version from a request. |
||||||
|
* |
||||||
|
* @author Rossen Stoyanchev |
||||||
|
* @since 7.0 |
||||||
|
*/ |
||||||
|
@FunctionalInterface |
||||||
|
public |
||||||
|
interface ApiVersionResolver { |
||||||
|
|
||||||
|
/** |
||||||
|
* Resolve the version for the given exchange. |
||||||
|
* @param exchange the current exchange |
||||||
|
* @return the version value, or {@code null} if not found |
||||||
|
*/ |
||||||
|
@Nullable String resolveVersion(ServerWebExchange exchange); |
||||||
|
|
||||||
|
} |
||||||
@ -0,0 +1,64 @@ |
|||||||
|
/* |
||||||
|
* Copyright 2002-2025 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 |
||||||
|
* |
||||||
|
* https://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.accept; |
||||||
|
|
||||||
|
import org.jspecify.annotations.Nullable; |
||||||
|
|
||||||
|
import org.springframework.web.accept.InvalidApiVersionException; |
||||||
|
import org.springframework.web.accept.MissingApiVersionException; |
||||||
|
import org.springframework.web.server.ServerWebExchange; |
||||||
|
|
||||||
|
/** |
||||||
|
* The main component that encapsulates configuration preferences and strategies |
||||||
|
* to manage API versioning for an application. |
||||||
|
* |
||||||
|
* @author Rossen Stoyanchev |
||||||
|
* @since 7.0 |
||||||
|
*/ |
||||||
|
public interface ApiVersionStrategy { |
||||||
|
|
||||||
|
/** |
||||||
|
* Resolve the version value from a request, e.g. from a request header. |
||||||
|
* @param exchange the current exchange |
||||||
|
* @return the version, if present or {@code null} |
||||||
|
*/ |
||||||
|
@Nullable |
||||||
|
String resolveVersion(ServerWebExchange exchange); |
||||||
|
|
||||||
|
/** |
||||||
|
* Parse the version of a request into an Object. |
||||||
|
* @param version the value to parse |
||||||
|
* @return an Object that represents the version |
||||||
|
*/ |
||||||
|
Comparable<?> parseVersion(String version); |
||||||
|
|
||||||
|
/** |
||||||
|
* Validate a request version, including required and supported version checks. |
||||||
|
* @param requestVersion the version to validate |
||||||
|
* @param exchange the exchange |
||||||
|
* @throws MissingApiVersionException if the version is required, but not specified |
||||||
|
* @throws InvalidApiVersionException if the version is not supported |
||||||
|
*/ |
||||||
|
void validateVersion(@Nullable Comparable<?> requestVersion, ServerWebExchange exchange) |
||||||
|
throws MissingApiVersionException, InvalidApiVersionException; |
||||||
|
|
||||||
|
/** |
||||||
|
* Return a default version to use for requests that don't specify one. |
||||||
|
*/ |
||||||
|
@Nullable Comparable<?> getDefaultVersion(); |
||||||
|
|
||||||
|
} |
||||||
@ -0,0 +1,132 @@ |
|||||||
|
/* |
||||||
|
* Copyright 2002-2025 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 |
||||||
|
* |
||||||
|
* https://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.accept; |
||||||
|
|
||||||
|
import java.util.ArrayList; |
||||||
|
import java.util.List; |
||||||
|
import java.util.Set; |
||||||
|
import java.util.TreeSet; |
||||||
|
|
||||||
|
import org.jspecify.annotations.Nullable; |
||||||
|
|
||||||
|
import org.springframework.util.Assert; |
||||||
|
import org.springframework.web.accept.ApiVersionParser; |
||||||
|
import org.springframework.web.accept.InvalidApiVersionException; |
||||||
|
import org.springframework.web.accept.MissingApiVersionException; |
||||||
|
import org.springframework.web.server.ServerWebExchange; |
||||||
|
|
||||||
|
/** |
||||||
|
* Default implementation of {@link ApiVersionStrategy} that delegates to the |
||||||
|
* configured version resolvers and version parser. |
||||||
|
* |
||||||
|
* @author Rossen Stoyanchev |
||||||
|
* @since 7.0 |
||||||
|
*/ |
||||||
|
public class DefaultApiVersionStrategy implements ApiVersionStrategy { |
||||||
|
|
||||||
|
private final List<ApiVersionResolver> versionResolvers; |
||||||
|
|
||||||
|
private final ApiVersionParser<?> versionParser; |
||||||
|
|
||||||
|
private final boolean versionRequired; |
||||||
|
|
||||||
|
private final @Nullable Comparable<?> defaultVersion; |
||||||
|
|
||||||
|
private final Set<Comparable<?>> supportedVersions = new TreeSet<>(); |
||||||
|
|
||||||
|
|
||||||
|
/** |
||||||
|
* Create an instance. |
||||||
|
* @param versionResolvers one or more resolvers to try; the first non-null |
||||||
|
* value returned by any resolver becomes the resolved used |
||||||
|
* @param versionParser parser for to raw version values |
||||||
|
* @param versionRequired whether a version is required; if a request |
||||||
|
* does not have a version, and a {@code defaultVersion} is not specified, |
||||||
|
* validation fails with {@link MissingApiVersionException} |
||||||
|
* @param defaultVersion a default version to assign to requests that |
||||||
|
* don't specify one |
||||||
|
*/ |
||||||
|
public DefaultApiVersionStrategy( |
||||||
|
List<ApiVersionResolver> versionResolvers, ApiVersionParser<?> versionParser, |
||||||
|
boolean versionRequired, @Nullable String defaultVersion) { |
||||||
|
|
||||||
|
Assert.notEmpty(versionResolvers, "At least one ApiVersionResolver is required"); |
||||||
|
Assert.notNull(versionParser, "ApiVersionParser is required"); |
||||||
|
|
||||||
|
this.versionResolvers = new ArrayList<>(versionResolvers); |
||||||
|
this.versionParser = versionParser; |
||||||
|
this.versionRequired = (versionRequired && defaultVersion == null); |
||||||
|
this.defaultVersion = (defaultVersion != null ? versionParser.parseVersion(defaultVersion) : null); |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
@Override |
||||||
|
public @Nullable Comparable<?> getDefaultVersion() { |
||||||
|
return this.defaultVersion; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Add to the list of known, supported versions to check against in |
||||||
|
* {@link ApiVersionStrategy#validateVersion}. Request versions that are not |
||||||
|
* in the supported result in {@link InvalidApiVersionException} |
||||||
|
* in {@link ApiVersionStrategy#validateVersion}. |
||||||
|
* @param versions the versions to add |
||||||
|
*/ |
||||||
|
public void addSupportedVersion(String... versions) { |
||||||
|
for (String version : versions) { |
||||||
|
this.supportedVersions.add(parseVersion(version)); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public @Nullable String resolveVersion(ServerWebExchange exchange) { |
||||||
|
for (ApiVersionResolver resolver : this.versionResolvers) { |
||||||
|
String version = resolver.resolveVersion(exchange); |
||||||
|
if (version != null) { |
||||||
|
return version; |
||||||
|
} |
||||||
|
} |
||||||
|
return null; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public Comparable<?> parseVersion(String version) { |
||||||
|
return this.versionParser.parseVersion(version); |
||||||
|
} |
||||||
|
|
||||||
|
public void validateVersion(@Nullable Comparable<?> requestVersion, ServerWebExchange exchange) |
||||||
|
throws MissingApiVersionException, InvalidApiVersionException { |
||||||
|
|
||||||
|
if (requestVersion == null) { |
||||||
|
if (this.versionRequired) { |
||||||
|
throw new MissingApiVersionException(); |
||||||
|
} |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
if (!this.supportedVersions.contains(requestVersion)) { |
||||||
|
throw new InvalidApiVersionException(requestVersion.toString()); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public String toString() { |
||||||
|
return "DefaultApiVersionStrategy[supportedVersions=" + this.supportedVersions + |
||||||
|
", versionRequired=" + this.versionRequired + ", defaultVersion=" + this.defaultVersion + "]"; |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
@ -0,0 +1,58 @@ |
|||||||
|
/* |
||||||
|
* Copyright 2002-2025 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 |
||||||
|
* |
||||||
|
* https://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.accept; |
||||||
|
|
||||||
|
import org.jspecify.annotations.Nullable; |
||||||
|
|
||||||
|
import org.springframework.http.server.PathContainer; |
||||||
|
import org.springframework.util.Assert; |
||||||
|
import org.springframework.web.server.ServerWebExchange; |
||||||
|
|
||||||
|
/** |
||||||
|
* {@link ApiVersionResolver} that extract the version from a path segment. |
||||||
|
* |
||||||
|
* @author Rossen Stoyanchev |
||||||
|
* @since 7.0 |
||||||
|
*/ |
||||||
|
public class PathApiVersionResolver implements ApiVersionResolver { |
||||||
|
|
||||||
|
private final int pathSegmentIndex; |
||||||
|
|
||||||
|
|
||||||
|
/** |
||||||
|
* Create a resolver instance. |
||||||
|
* @param pathSegmentIndex the index of the path segment that contains |
||||||
|
* the API version |
||||||
|
*/ |
||||||
|
public PathApiVersionResolver(int pathSegmentIndex) { |
||||||
|
Assert.isTrue(pathSegmentIndex >= 0, "'pathSegmentIndex' must be >= 0"); |
||||||
|
this.pathSegmentIndex = pathSegmentIndex; |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
@Override |
||||||
|
public @Nullable String resolveVersion(ServerWebExchange exchange) { |
||||||
|
int i = 0; |
||||||
|
for (PathContainer.Element e : exchange.getRequest().getPath().pathWithinApplication().elements()) { |
||||||
|
if (e instanceof PathContainer.PathSegment && i++ == this.pathSegmentIndex) { |
||||||
|
return e.value(); |
||||||
|
} |
||||||
|
} |
||||||
|
return null; |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
@ -0,0 +1,156 @@ |
|||||||
|
/* |
||||||
|
* Copyright 2002-2025 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 |
||||||
|
* |
||||||
|
* https://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.config; |
||||||
|
|
||||||
|
import java.util.ArrayList; |
||||||
|
import java.util.Arrays; |
||||||
|
import java.util.Collections; |
||||||
|
import java.util.LinkedHashSet; |
||||||
|
import java.util.List; |
||||||
|
import java.util.Set; |
||||||
|
|
||||||
|
import org.jspecify.annotations.Nullable; |
||||||
|
|
||||||
|
import org.springframework.web.accept.ApiVersionParser; |
||||||
|
import org.springframework.web.accept.SemanticApiVersionParser; |
||||||
|
import org.springframework.web.reactive.accept.ApiVersionResolver; |
||||||
|
import org.springframework.web.reactive.accept.ApiVersionStrategy; |
||||||
|
import org.springframework.web.reactive.accept.DefaultApiVersionStrategy; |
||||||
|
import org.springframework.web.reactive.accept.PathApiVersionResolver; |
||||||
|
|
||||||
|
/** |
||||||
|
* Configure API versioning. |
||||||
|
* |
||||||
|
* @author Rossen Stoyanchev |
||||||
|
* @since 7.0 |
||||||
|
*/ |
||||||
|
public class ApiVersionConfigurer { |
||||||
|
|
||||||
|
private final List<ApiVersionResolver> versionResolvers = new ArrayList<>(); |
||||||
|
|
||||||
|
private @Nullable ApiVersionParser<?> versionParser; |
||||||
|
|
||||||
|
private boolean versionRequired = true; |
||||||
|
|
||||||
|
private @Nullable String defaultVersion; |
||||||
|
|
||||||
|
private final Set<String> supportedVersions = new LinkedHashSet<>(); |
||||||
|
|
||||||
|
|
||||||
|
/** |
||||||
|
* Add a resolver that extracts the API version from a request header. |
||||||
|
* @param headerName the header name to check |
||||||
|
*/ |
||||||
|
public ApiVersionConfigurer useRequestHeader(String headerName) { |
||||||
|
this.versionResolvers.add(exchange -> exchange.getRequest().getHeaders().getFirst(headerName)); |
||||||
|
return this; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Add a resolver that extracts the API version from a request parameter. |
||||||
|
* @param paramName the parameter name to check |
||||||
|
*/ |
||||||
|
public ApiVersionConfigurer useRequestParam(String paramName) { |
||||||
|
this.versionResolvers.add(exchange -> exchange.getRequest().getQueryParams().getFirst(paramName)); |
||||||
|
return this; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Add a resolver that extracts the API version from a path segment. |
||||||
|
* @param index the index of the path segment to check; e.g. for URL's like |
||||||
|
* "/{version}/..." use index 0, for "/api/{version}/..." index 1. |
||||||
|
*/ |
||||||
|
public ApiVersionConfigurer usePathSegment(int index) { |
||||||
|
this.versionResolvers.add(new PathApiVersionResolver(index)); |
||||||
|
return this; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Add custom resolvers to resolve the API version. |
||||||
|
* @param resolvers the resolvers to use |
||||||
|
*/ |
||||||
|
public ApiVersionConfigurer useVersionResolver(ApiVersionResolver... resolvers) { |
||||||
|
this.versionResolvers.addAll(Arrays.asList(resolvers)); |
||||||
|
return this; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Configure a parser to parse API versions with. |
||||||
|
* <p>By default, {@link SemanticApiVersionParser} is used. |
||||||
|
* @param versionParser the parser to user |
||||||
|
*/ |
||||||
|
public ApiVersionConfigurer setVersionParser(@Nullable ApiVersionParser<?> versionParser) { |
||||||
|
this.versionParser = versionParser; |
||||||
|
return this; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Whether requests are required to have an API version. When set to |
||||||
|
* {@code true}, {@link org.springframework.web.accept.MissingApiVersionException} |
||||||
|
* is raised, resulting in a 400 response if the request doesn't have an API |
||||||
|
* version. When set to false, a request without a version is considered to |
||||||
|
* accept any version. |
||||||
|
* <p>By default, this is set to {@code true} when API versioning is enabled |
||||||
|
* by adding at least one {@link ApiVersionResolver}). When a |
||||||
|
* {@link #setDefaultVersion defaultVersion} is also set, this is |
||||||
|
* automatically set to {@code false}. |
||||||
|
* @param required whether an API version is required. |
||||||
|
*/ |
||||||
|
public ApiVersionConfigurer setVersionRequired(boolean required) { |
||||||
|
this.versionRequired = required; |
||||||
|
return this; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Configure a default version to assign to requests that don't specify one. |
||||||
|
* @param defaultVersion the default version to use |
||||||
|
*/ |
||||||
|
public ApiVersionConfigurer setDefaultVersion(@Nullable String defaultVersion) { |
||||||
|
this.defaultVersion = defaultVersion; |
||||||
|
return this; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Add to the list of supported versions to validate request versions against. |
||||||
|
* Request versions that are not supported result in |
||||||
|
* {@link org.springframework.web.accept.InvalidApiVersionException}. |
||||||
|
* <p>Note that the set of supported versions is populated from versions |
||||||
|
* listed in controller mappings. Therefore, typically you do not have to |
||||||
|
* manage this list except for the initial API version, when controller |
||||||
|
* don't have to have a version to start. |
||||||
|
* @param versions supported versions to add |
||||||
|
*/ |
||||||
|
public ApiVersionConfigurer addSupportedVersions(String... versions) { |
||||||
|
Collections.addAll(this.supportedVersions, versions); |
||||||
|
return this; |
||||||
|
} |
||||||
|
|
||||||
|
protected @Nullable ApiVersionStrategy getApiVersionStrategy() { |
||||||
|
if (this.versionResolvers.isEmpty()) { |
||||||
|
return null; |
||||||
|
} |
||||||
|
|
||||||
|
DefaultApiVersionStrategy strategy = new DefaultApiVersionStrategy(this.versionResolvers, |
||||||
|
(this.versionParser != null ? this.versionParser : new SemanticApiVersionParser()), |
||||||
|
this.versionRequired, this.defaultVersion); |
||||||
|
|
||||||
|
this.supportedVersions.forEach(strategy::addSupportedVersion); |
||||||
|
|
||||||
|
return strategy; |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
@ -0,0 +1,203 @@ |
|||||||
|
/* |
||||||
|
* Copyright 2002-2025 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 |
||||||
|
* |
||||||
|
* https://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.Set; |
||||||
|
|
||||||
|
import org.jspecify.annotations.Nullable; |
||||||
|
|
||||||
|
import org.springframework.util.Assert; |
||||||
|
import org.springframework.web.accept.InvalidApiVersionException; |
||||||
|
import org.springframework.web.accept.NotAcceptableApiVersionException; |
||||||
|
import org.springframework.web.bind.annotation.RequestMapping; |
||||||
|
import org.springframework.web.reactive.accept.ApiVersionStrategy; |
||||||
|
import org.springframework.web.server.ServerWebExchange; |
||||||
|
|
||||||
|
/** |
||||||
|
* Request condition to map based on the API version of the request. |
||||||
|
* Versions can be fixed (e.g. "1.2") or baseline (e.g. "1.2+") as described |
||||||
|
* in {@link RequestMapping#version()}. |
||||||
|
* |
||||||
|
* |
||||||
|
* @author Rossen Stoyanchev |
||||||
|
* @since 7.0 |
||||||
|
*/ |
||||||
|
public final class VersionRequestCondition extends AbstractRequestCondition<VersionRequestCondition> { |
||||||
|
|
||||||
|
private static final String VERSION_ATTRIBUTE_NAME = VersionRequestCondition.class.getName() + ".VERSION"; |
||||||
|
|
||||||
|
private static final String NO_VERSION_ATTRIBUTE = "NO_VERSION"; |
||||||
|
|
||||||
|
private static final ApiVersionStrategy NO_OP_VERSION_STRATEGY = new NoOpApiVersionStrategy(); |
||||||
|
|
||||||
|
|
||||||
|
private final @Nullable String versionValue; |
||||||
|
|
||||||
|
private final @Nullable Object version; |
||||||
|
|
||||||
|
private final boolean baselineVersion; |
||||||
|
|
||||||
|
private final ApiVersionStrategy versionStrategy; |
||||||
|
|
||||||
|
private final Set<String> content; |
||||||
|
|
||||||
|
|
||||||
|
public VersionRequestCondition() { |
||||||
|
this.versionValue = null; |
||||||
|
this.version = null; |
||||||
|
this.baselineVersion = false; |
||||||
|
this.versionStrategy = NO_OP_VERSION_STRATEGY; |
||||||
|
this.content = Collections.emptySet(); |
||||||
|
} |
||||||
|
|
||||||
|
public VersionRequestCondition(String configuredVersion, ApiVersionStrategy versionStrategy) { |
||||||
|
this.baselineVersion = configuredVersion.endsWith("+"); |
||||||
|
this.versionValue = updateVersion(configuredVersion, this.baselineVersion); |
||||||
|
this.version = versionStrategy.parseVersion(this.versionValue); |
||||||
|
this.versionStrategy = versionStrategy; |
||||||
|
this.content = Set.of(configuredVersion); |
||||||
|
} |
||||||
|
|
||||||
|
private static String updateVersion(String version, boolean baselineVersion) { |
||||||
|
return (baselineVersion ? version.substring(0, version.length() - 1) : version); |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
@Override |
||||||
|
protected Collection<String> getContent() { |
||||||
|
return this.content; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
protected String getToStringInfix() { |
||||||
|
return " && "; |
||||||
|
} |
||||||
|
|
||||||
|
public @Nullable String getVersion() { |
||||||
|
return this.versionValue; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public VersionRequestCondition combine(VersionRequestCondition other) { |
||||||
|
return (other.version != null ? other : this); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public @Nullable VersionRequestCondition getMatchingCondition(ServerWebExchange exchange) { |
||||||
|
if (this.version == null) { |
||||||
|
return this; |
||||||
|
} |
||||||
|
|
||||||
|
Comparable<?> version = exchange.getAttribute(VERSION_ATTRIBUTE_NAME); |
||||||
|
if (version == null) { |
||||||
|
String value = this.versionStrategy.resolveVersion(exchange); |
||||||
|
version = (value != null ? parseVersion(value) : this.versionStrategy.getDefaultVersion()); |
||||||
|
this.versionStrategy.validateVersion(version, exchange); |
||||||
|
version = (version != null ? version : NO_VERSION_ATTRIBUTE); |
||||||
|
exchange.getAttributes().put(VERSION_ATTRIBUTE_NAME, (version)); |
||||||
|
} |
||||||
|
|
||||||
|
if (version == NO_VERSION_ATTRIBUTE) { |
||||||
|
return this; |
||||||
|
} |
||||||
|
|
||||||
|
// At this stage, match all versions as baseline versions.
|
||||||
|
// Strict matching for fixed versions is enforced at the end in handleMatch.
|
||||||
|
|
||||||
|
int result = compareVersions(this.version, version); |
||||||
|
return (result <= 0 ? this : null); |
||||||
|
} |
||||||
|
|
||||||
|
private Comparable<?> parseVersion(String value) { |
||||||
|
try { |
||||||
|
return this.versionStrategy.parseVersion(value); |
||||||
|
} |
||||||
|
catch (Exception ex) { |
||||||
|
throw new InvalidApiVersionException(value, null, ex); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
@SuppressWarnings("unchecked") |
||||||
|
private <V extends Comparable<V>> int compareVersions(Object v1, Object v2) { |
||||||
|
return ((V) v1).compareTo((V) v2); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public int compareTo(VersionRequestCondition other, ServerWebExchange exchange) { |
||||||
|
Object otherVersion = other.version; |
||||||
|
if (this.version == null && otherVersion == null) { |
||||||
|
return 0; |
||||||
|
} |
||||||
|
else if (this.version != null && otherVersion != null) { |
||||||
|
// make higher version bubble up
|
||||||
|
return (-1 * compareVersions(this.version, otherVersion)); |
||||||
|
} |
||||||
|
else { |
||||||
|
return (this.version != null ? -1 : 1); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Perform a final check on the matched request mapping version. |
||||||
|
* <p>In order to ensure baseline versions are properly capped by higher |
||||||
|
* fixed versions, initially we match all versions as baseline versions in |
||||||
|
* {@link #getMatchingCondition(ServerWebExchange)}. Once the highest of |
||||||
|
* potentially multiple matches is selected, we enforce the strict match |
||||||
|
* for fixed versions. |
||||||
|
* <p>For example, given controller methods for "1.2+" and "1.5", and |
||||||
|
* a request for "1.6", both are matched, allowing "1.5" to be selected, but |
||||||
|
* that is then rejected as not acceptable since it is not an exact match. |
||||||
|
* @param exchange the current exchange |
||||||
|
* @throws NotAcceptableApiVersionException if the matched condition has a |
||||||
|
* fixed version that is not equal to the request version |
||||||
|
*/ |
||||||
|
public void handleMatch(ServerWebExchange exchange) { |
||||||
|
if (this.version != null && !this.baselineVersion) { |
||||||
|
Comparable<?> version = exchange.getAttribute(VERSION_ATTRIBUTE_NAME); |
||||||
|
Assert.state(version != null, "No API version attribute"); |
||||||
|
if (!this.version.equals(version)) { |
||||||
|
throw new NotAcceptableApiVersionException(version.toString()); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
private static final class NoOpApiVersionStrategy implements ApiVersionStrategy { |
||||||
|
|
||||||
|
@Override |
||||||
|
public @Nullable String resolveVersion(ServerWebExchange exchange) { |
||||||
|
return null; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public String parseVersion(String version) { |
||||||
|
return version; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public void validateVersion(@Nullable Comparable<?> requestVersion, ServerWebExchange exchange) { |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public @Nullable Comparable<?> getDefaultVersion() { |
||||||
|
return null; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
@ -0,0 +1,74 @@ |
|||||||
|
/* |
||||||
|
* Copyright 2002-2025 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 |
||||||
|
* |
||||||
|
* https://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.accept; |
||||||
|
|
||||||
|
import java.util.List; |
||||||
|
|
||||||
|
import org.jspecify.annotations.Nullable; |
||||||
|
import org.junit.jupiter.api.Test; |
||||||
|
|
||||||
|
import org.springframework.web.accept.InvalidApiVersionException; |
||||||
|
import org.springframework.web.accept.SemanticApiVersionParser; |
||||||
|
import org.springframework.web.server.ServerWebExchange; |
||||||
|
import org.springframework.web.testfixture.http.server.reactive.MockServerHttpRequest; |
||||||
|
import org.springframework.web.testfixture.server.MockServerWebExchange; |
||||||
|
|
||||||
|
import static org.assertj.core.api.AssertionsForClassTypes.assertThat; |
||||||
|
import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; |
||||||
|
|
||||||
|
/** |
||||||
|
* Unit tests for {@link org.springframework.web.accept.DefaultApiVersionStrategy}. |
||||||
|
* @author Rossen Stoyanchev |
||||||
|
*/ |
||||||
|
public class DefaultApiVersionStrategiesTests { |
||||||
|
|
||||||
|
private final SemanticApiVersionParser parser = new SemanticApiVersionParser(); |
||||||
|
|
||||||
|
|
||||||
|
@Test |
||||||
|
void defaultVersion() { |
||||||
|
SemanticApiVersionParser.Version version = this.parser.parseVersion("1.2.3"); |
||||||
|
ApiVersionStrategy strategy = initVersionStrategy(version.toString()); |
||||||
|
|
||||||
|
assertThat(strategy.getDefaultVersion()).isEqualTo(version); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
void supportedVersions() { |
||||||
|
SemanticApiVersionParser.Version v1 = this.parser.parseVersion("1"); |
||||||
|
SemanticApiVersionParser.Version v2 = this.parser.parseVersion("2"); |
||||||
|
SemanticApiVersionParser.Version v9 = this.parser.parseVersion("9"); |
||||||
|
|
||||||
|
DefaultApiVersionStrategy strategy = initVersionStrategy(null); |
||||||
|
strategy.addSupportedVersion(v1.toString()); |
||||||
|
strategy.addSupportedVersion(v2.toString()); |
||||||
|
|
||||||
|
ServerWebExchange exchange = MockServerWebExchange.from(MockServerHttpRequest.get("/")); |
||||||
|
strategy.validateVersion(v1, exchange); |
||||||
|
strategy.validateVersion(v2, exchange); |
||||||
|
|
||||||
|
assertThatThrownBy(() -> strategy.validateVersion(v9, exchange)) |
||||||
|
.isInstanceOf(InvalidApiVersionException.class); |
||||||
|
} |
||||||
|
|
||||||
|
private static DefaultApiVersionStrategy initVersionStrategy(@Nullable String defaultValue) { |
||||||
|
return new DefaultApiVersionStrategy( |
||||||
|
List.of(exchange -> exchange.getRequest().getQueryParams().getFirst("api-version")), |
||||||
|
new SemanticApiVersionParser(), true, defaultValue); |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
@ -0,0 +1,45 @@ |
|||||||
|
/* |
||||||
|
* Copyright 2002-2025 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 |
||||||
|
* |
||||||
|
* https://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.accept; |
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test; |
||||||
|
|
||||||
|
import org.springframework.web.server.ServerWebExchange; |
||||||
|
import org.springframework.web.testfixture.http.server.reactive.MockServerHttpRequest; |
||||||
|
import org.springframework.web.testfixture.server.MockServerWebExchange; |
||||||
|
|
||||||
|
import static org.assertj.core.api.AssertionsForClassTypes.assertThat; |
||||||
|
|
||||||
|
/** |
||||||
|
* Unit tests for {@link org.springframework.web.accept.PathApiVersionResolver}. |
||||||
|
* @author Rossen Stoyanchev |
||||||
|
*/ |
||||||
|
public class PathApiVersionResolverTests { |
||||||
|
|
||||||
|
@Test |
||||||
|
void resolve() { |
||||||
|
testResolve(0, "/1.0/path", "1.0"); |
||||||
|
testResolve(1, "/app/1.1/path", "1.1"); |
||||||
|
} |
||||||
|
|
||||||
|
private static void testResolve(int index, String requestUri, String expected) { |
||||||
|
ServerWebExchange exchange = MockServerWebExchange.from(MockServerHttpRequest.get(requestUri)); |
||||||
|
String actual = new PathApiVersionResolver(index).resolveVersion(exchange); |
||||||
|
assertThat(actual).isEqualTo(expected); |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
@ -0,0 +1,172 @@ |
|||||||
|
/* |
||||||
|
* Copyright 2002-2025 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 |
||||||
|
* |
||||||
|
* https://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.List; |
||||||
|
|
||||||
|
import org.jspecify.annotations.Nullable; |
||||||
|
import org.junit.jupiter.api.BeforeEach; |
||||||
|
import org.junit.jupiter.api.Test; |
||||||
|
|
||||||
|
import org.springframework.web.accept.NotAcceptableApiVersionException; |
||||||
|
import org.springframework.web.accept.SemanticApiVersionParser; |
||||||
|
import org.springframework.web.reactive.accept.DefaultApiVersionStrategy; |
||||||
|
import org.springframework.web.server.ServerWebExchange; |
||||||
|
import org.springframework.web.testfixture.http.server.reactive.MockServerHttpRequest; |
||||||
|
import org.springframework.web.testfixture.server.MockServerWebExchange; |
||||||
|
|
||||||
|
import static org.assertj.core.api.AssertionsForClassTypes.assertThat; |
||||||
|
import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; |
||||||
|
|
||||||
|
/** |
||||||
|
* Unit tests for {@link VersionRequestCondition}. |
||||||
|
* @author Rossen Stoyanchev |
||||||
|
*/ |
||||||
|
public class VersionRequestConditionTests { |
||||||
|
|
||||||
|
private DefaultApiVersionStrategy strategy; |
||||||
|
|
||||||
|
|
||||||
|
@BeforeEach |
||||||
|
void setUp() { |
||||||
|
this.strategy = initVersionStrategy(null); |
||||||
|
} |
||||||
|
|
||||||
|
private static DefaultApiVersionStrategy initVersionStrategy(@Nullable String defaultValue) { |
||||||
|
return new DefaultApiVersionStrategy( |
||||||
|
List.of(exchange -> exchange.getRequest().getQueryParams().getFirst("api-version")), |
||||||
|
new SemanticApiVersionParser(), true, defaultValue); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
void combineMethodLevelOnly() { |
||||||
|
VersionRequestCondition condition = emptyCondition().combine(condition("1.1")); |
||||||
|
assertThat(condition.getVersion()).isEqualTo("1.1"); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
void combineTypeLevelOnly() { |
||||||
|
VersionRequestCondition condition = condition("1.1").combine(emptyCondition()); |
||||||
|
assertThat(condition.getVersion()).isEqualTo("1.1"); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
void combineTypeAndMethodLevel() { |
||||||
|
assertThat(condition("1.1").combine(condition("1.2")).getVersion()).isEqualTo("1.2"); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
void fixedVersionMatch() { |
||||||
|
String conditionVersion = "1.2"; |
||||||
|
this.strategy.addSupportedVersion("1.1", "1.3"); |
||||||
|
|
||||||
|
testMatch("v1.1", conditionVersion, true, false); |
||||||
|
testMatch("v1.2", conditionVersion, false, false); |
||||||
|
testMatch("v1.3", conditionVersion, false, true); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
void baselineVersionMatch() { |
||||||
|
String conditionVersion = "1.2+"; |
||||||
|
this.strategy.addSupportedVersion("1.1", "1.3"); |
||||||
|
|
||||||
|
testMatch("v1.1", conditionVersion, true, false); |
||||||
|
testMatch("v1.2", conditionVersion, false, false); |
||||||
|
testMatch("v1.3", conditionVersion, false, false); |
||||||
|
} |
||||||
|
|
||||||
|
private void testMatch( |
||||||
|
String requestVersion, String conditionVersion, boolean notCompatible, boolean notAcceptable) { |
||||||
|
|
||||||
|
ServerWebExchange exchange = exchangeWithVersion(requestVersion); |
||||||
|
VersionRequestCondition condition = condition(conditionVersion); |
||||||
|
VersionRequestCondition match = condition.getMatchingCondition(exchange); |
||||||
|
|
||||||
|
if (notCompatible) { |
||||||
|
assertThat(match).isNull(); |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
assertThat(match).isSameAs(condition); |
||||||
|
|
||||||
|
if (notAcceptable) { |
||||||
|
assertThatThrownBy(() -> condition.handleMatch(exchange)).isInstanceOf(NotAcceptableApiVersionException.class); |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
condition.handleMatch(exchange); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
void missingRequiredVersion() { |
||||||
|
assertThatThrownBy(() -> condition("1.2").getMatchingCondition(exchange())) |
||||||
|
.hasMessage("400 BAD_REQUEST \"API version is required.\""); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
void defaultVersion() { |
||||||
|
String version = "1.2"; |
||||||
|
this.strategy = initVersionStrategy(version); |
||||||
|
VersionRequestCondition condition = condition(version); |
||||||
|
VersionRequestCondition match = condition.getMatchingCondition(exchange()); |
||||||
|
|
||||||
|
assertThat(match).isSameAs(condition); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
void unsupportedVersion() { |
||||||
|
assertThatThrownBy(() -> condition("1.2").getMatchingCondition(exchangeWithVersion("1.3"))) |
||||||
|
.hasMessage("400 BAD_REQUEST \"Invalid API version: '1.3.0'.\""); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
void compare() { |
||||||
|
testCompare("1.1", "1", "1.1"); |
||||||
|
testCompare("1.1.1", "1", "1.1", "1.1.1"); |
||||||
|
testCompare("10", "1.1", "10"); |
||||||
|
testCompare("10", "2", "10"); |
||||||
|
} |
||||||
|
|
||||||
|
private void testCompare(String expected, String... versions) { |
||||||
|
List<VersionRequestCondition> list = Arrays.stream(versions) |
||||||
|
.map(this::condition) |
||||||
|
.sorted((c1, c2) -> c1.compareTo(c2, exchange())) |
||||||
|
.toList(); |
||||||
|
|
||||||
|
assertThat(list.get(0)).isEqualTo(condition(expected)); |
||||||
|
} |
||||||
|
|
||||||
|
private VersionRequestCondition condition(String v) { |
||||||
|
this.strategy.addSupportedVersion(v.endsWith("+") ? v.substring(0, v.length() - 1) : v); |
||||||
|
return new VersionRequestCondition(v, this.strategy); |
||||||
|
} |
||||||
|
|
||||||
|
private VersionRequestCondition emptyCondition() { |
||||||
|
return new VersionRequestCondition(); |
||||||
|
} |
||||||
|
|
||||||
|
private static MockServerWebExchange exchange() { |
||||||
|
return MockServerWebExchange.from(MockServerHttpRequest.get("/path")); |
||||||
|
} |
||||||
|
|
||||||
|
private ServerWebExchange exchangeWithVersion(String v) { |
||||||
|
return MockServerWebExchange.from( |
||||||
|
MockServerHttpRequest.get("/path").queryParam("api-version", v)); |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
@ -0,0 +1,110 @@ |
|||||||
|
/* |
||||||
|
* Copyright 2002-2025 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 |
||||||
|
* |
||||||
|
* https://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.annotation; |
||||||
|
|
||||||
|
import org.springframework.context.ApplicationContext; |
||||||
|
import org.springframework.context.annotation.AnnotationConfigApplicationContext; |
||||||
|
import org.springframework.http.RequestEntity; |
||||||
|
import org.springframework.http.ResponseEntity; |
||||||
|
import org.springframework.web.bind.annotation.GetMapping; |
||||||
|
import org.springframework.web.bind.annotation.RestController; |
||||||
|
import org.springframework.web.client.HttpClientErrorException; |
||||||
|
import org.springframework.web.reactive.config.ApiVersionConfigurer; |
||||||
|
import org.springframework.web.reactive.config.EnableWebFlux; |
||||||
|
import org.springframework.web.reactive.config.WebFluxConfigurer; |
||||||
|
import org.springframework.web.testfixture.http.server.reactive.bootstrap.HttpServer; |
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat; |
||||||
|
import static org.assertj.core.api.Assertions.assertThatThrownBy; |
||||||
|
|
||||||
|
/** |
||||||
|
* {@code @RequestMapping} integration focusing on API versioning. |
||||||
|
* @author Rossen Stoyanchev |
||||||
|
*/ |
||||||
|
public class RequestMappingVersionIntegrationTests extends AbstractRequestMappingIntegrationTests { |
||||||
|
|
||||||
|
@Override |
||||||
|
protected ApplicationContext initApplicationContext() { |
||||||
|
AnnotationConfigApplicationContext wac = new AnnotationConfigApplicationContext(); |
||||||
|
wac.register(WebConfig.class, TestController.class); |
||||||
|
wac.refresh(); |
||||||
|
return wac; |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
@ParameterizedHttpServerTest |
||||||
|
void initialVersion(HttpServer httpServer) throws Exception { |
||||||
|
startServer(httpServer); |
||||||
|
assertThat(exchangeWithVersion("1.0").getBody()).isEqualTo("none"); |
||||||
|
assertThat(exchangeWithVersion("1.1").getBody()).isEqualTo("none"); |
||||||
|
} |
||||||
|
|
||||||
|
@ParameterizedHttpServerTest |
||||||
|
void baselineVersion(HttpServer httpServer) throws Exception { |
||||||
|
startServer(httpServer); |
||||||
|
assertThat(exchangeWithVersion("1.2").getBody()).isEqualTo("1.2"); |
||||||
|
assertThat(exchangeWithVersion("1.3").getBody()).isEqualTo("1.2"); |
||||||
|
} |
||||||
|
|
||||||
|
@ParameterizedHttpServerTest |
||||||
|
void fixedVersion(HttpServer httpServer) throws Exception { |
||||||
|
startServer(httpServer); |
||||||
|
assertThat(exchangeWithVersion("1.5").getBody()).isEqualTo("1.5"); |
||||||
|
assertThatThrownBy(() -> exchangeWithVersion("1.6")).isInstanceOf(HttpClientErrorException.BadRequest.class); |
||||||
|
} |
||||||
|
|
||||||
|
private ResponseEntity<String> exchangeWithVersion(String version) { |
||||||
|
String url = "http://localhost:" + this.port; |
||||||
|
RequestEntity<Void> requestEntity = RequestEntity.get(url).header("X-API-Version", version).build(); |
||||||
|
return getRestTemplate().exchange(requestEntity, String.class); |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
@EnableWebFlux |
||||||
|
private static class WebConfig implements WebFluxConfigurer { |
||||||
|
|
||||||
|
@Override |
||||||
|
public void configureApiVersioning(ApiVersionConfigurer configurer) { |
||||||
|
configurer.useRequestHeader("X-API-Version").addSupportedVersions("1", "1.1", "1.3", "1.6"); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
@RestController |
||||||
|
private static class TestController { |
||||||
|
|
||||||
|
@GetMapping |
||||||
|
String noVersion() { |
||||||
|
return getBody("none"); |
||||||
|
} |
||||||
|
|
||||||
|
@GetMapping(version = "1.2+") |
||||||
|
String version1_2() { |
||||||
|
return getBody("1.2"); |
||||||
|
} |
||||||
|
|
||||||
|
@GetMapping(version = "1.5") |
||||||
|
String version1_5() { |
||||||
|
return getBody("1.5"); |
||||||
|
} |
||||||
|
|
||||||
|
private static String getBody(String version) { |
||||||
|
return version; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
Loading…
Reference in new issue