API versioning support for Spring MVC
See gh-34566
This commit is contained in:
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
* 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.accept;
|
||||
|
||||
/**
|
||||
* Contract to parse a version into an Object representation.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 7.0
|
||||
* @param <V> the parsed object type
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface ApiVersionParser<V extends Comparable<V>> {
|
||||
|
||||
/**
|
||||
* Parse the version into an Object.
|
||||
* @param version the value to parse
|
||||
* @return an Object that represents the version
|
||||
*/
|
||||
V parseVersion(String version);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* 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.accept;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
/**
|
||||
* Contract to extract the version from a request.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 7.0
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public
|
||||
interface ApiVersionResolver {
|
||||
|
||||
/**
|
||||
* Resolve the version for the given request.
|
||||
* @param request the current request
|
||||
* @return the version value, or {@code null} if not found
|
||||
*/
|
||||
@Nullable String resolveVersion(HttpServletRequest request);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
/*
|
||||
* 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.accept;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
/**
|
||||
* 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 request the current request
|
||||
* @return the version, if present or {@code null}
|
||||
*/
|
||||
@Nullable
|
||||
String resolveVersion(HttpServletRequest request);
|
||||
|
||||
/**
|
||||
* 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 request the request
|
||||
* @throws MissingApiVersionException if the version is required, but not specified
|
||||
* @throws InvalidApiVersionException if the version is not supported
|
||||
*/
|
||||
void validateVersion(@Nullable Comparable<?> requestVersion, HttpServletRequest request)
|
||||
throws MissingApiVersionException, InvalidApiVersionException;
|
||||
|
||||
/**
|
||||
* Return a default version to use for requests that don't specify one.
|
||||
*/
|
||||
@Nullable Comparable<?> getDefaultVersion();
|
||||
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
/*
|
||||
* 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.accept;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.TreeSet;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* 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(HttpServletRequest request) {
|
||||
for (ApiVersionResolver resolver : this.versionResolvers) {
|
||||
String version = resolver.resolveVersion(request);
|
||||
if (version != null) {
|
||||
return version;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Comparable<?> parseVersion(String version) {
|
||||
return this.versionParser.parseVersion(version);
|
||||
}
|
||||
|
||||
public void validateVersion(@Nullable Comparable<?> requestVersion, HttpServletRequest request)
|
||||
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,54 @@
|
||||
/*
|
||||
* 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.accept;
|
||||
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
|
||||
/**
|
||||
* Exception raised when an API version cannot be parsed, or is not in the
|
||||
* supported version set.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 7.0
|
||||
*/
|
||||
@SuppressWarnings("serial")
|
||||
public class InvalidApiVersionException extends ResponseStatusException {
|
||||
|
||||
private final String version;
|
||||
|
||||
|
||||
public InvalidApiVersionException(String version) {
|
||||
this(version, null, null);
|
||||
}
|
||||
|
||||
public InvalidApiVersionException(String version, @Nullable String msg, @Nullable Exception cause) {
|
||||
super(HttpStatus.BAD_REQUEST, (msg != null ? msg : "Invalid API version: '" + version + "'."), cause);
|
||||
this.version = version;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Return the requested version.
|
||||
*/
|
||||
public String getVersion() {
|
||||
return this.version;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* 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.accept;
|
||||
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
|
||||
/**
|
||||
* Exception raised when an API version is required, but is not present.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 7.0
|
||||
*/
|
||||
@SuppressWarnings("serial")
|
||||
public class MissingApiVersionException extends ResponseStatusException {
|
||||
|
||||
public MissingApiVersionException() {
|
||||
super(HttpStatus.BAD_REQUEST, "API version is required.");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
* 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.accept;
|
||||
|
||||
/**
|
||||
* Exception raised when an API version is valid, but did not match the versions
|
||||
* declared in request mappings for the endpoint. This can happen when a
|
||||
* controller method is mapped with a fixed version, e.g. "2", but the request
|
||||
* is for a higher version.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 7.0
|
||||
*/
|
||||
@SuppressWarnings("serial")
|
||||
public class NotAcceptableApiVersionException extends InvalidApiVersionException {
|
||||
|
||||
|
||||
public NotAcceptableApiVersionException(String version) {
|
||||
super(version);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
/*
|
||||
* 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.accept;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
import org.springframework.http.server.PathContainer;
|
||||
import org.springframework.http.server.RequestPath;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.web.util.ServletRequestPathUtils;
|
||||
|
||||
/**
|
||||
* {@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(HttpServletRequest request) {
|
||||
if (ServletRequestPathUtils.hasParsedRequestPath(request)) {
|
||||
RequestPath path = ServletRequestPathUtils.getParsedRequestPath(request);
|
||||
int i = 0;
|
||||
for (PathContainer.Element e : path.pathWithinApplication().elements()) {
|
||||
if (e instanceof PathContainer.PathSegment && i++ == this.pathSegmentIndex) {
|
||||
return e.value();
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
/*
|
||||
* 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.accept;
|
||||
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* Parser for semantic API versioning with a major, minor, and patch values.
|
||||
* For example "1", "1.0", "1.2", "1.2.0", "1.2.3". Leading, non-integer
|
||||
* characters, as in "v1.0", are skipped.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 7.0
|
||||
*/
|
||||
public class SemanticApiVersionParser implements ApiVersionParser<SemanticApiVersionParser.Version> {
|
||||
|
||||
private static final Pattern semantinVersionPattern = Pattern.compile("^(\\d+)(\\.(\\d+))?(\\.(\\d+))?$");
|
||||
|
||||
|
||||
@Override
|
||||
public Version parseVersion(String version) {
|
||||
Assert.notNull(version, "'version' is required");
|
||||
|
||||
version = skipNonDigits(version);
|
||||
|
||||
Matcher matcher = semantinVersionPattern.matcher(version);
|
||||
Assert.state(matcher.matches(), "Invalid API version format");
|
||||
|
||||
String major = matcher.group(1);
|
||||
String minor = matcher.group(3);
|
||||
String patch = matcher.group(5);
|
||||
|
||||
return new Version(
|
||||
Integer.parseInt(major),
|
||||
(minor != null ? Integer.parseInt(minor) : 0),
|
||||
(patch != null ? Integer.parseInt(patch) : 0));
|
||||
}
|
||||
|
||||
private static String skipNonDigits(String value) {
|
||||
for (int i = 0; i < value.length(); i++) {
|
||||
if (Character.isDigit(value.charAt(i))) {
|
||||
return value.substring(i);
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Representation of a semantic version.
|
||||
*/
|
||||
public static final class Version implements Comparable<Version> {
|
||||
|
||||
private final int major;
|
||||
|
||||
private final int minor;
|
||||
|
||||
private final int patch;
|
||||
|
||||
Version(int major, int minor, int patch) {
|
||||
this.major = major;
|
||||
this.minor = minor;
|
||||
this.patch = patch;
|
||||
}
|
||||
|
||||
public int getMajor() {
|
||||
return this.major;
|
||||
}
|
||||
|
||||
public int getMinor() {
|
||||
return this.minor;
|
||||
}
|
||||
|
||||
public int getPatch() {
|
||||
return this.patch;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compareTo(SemanticApiVersionParser.Version other) {
|
||||
int result = Integer.compare(this.major, other.major);
|
||||
if (result != 0) {
|
||||
return result;
|
||||
}
|
||||
result = Integer.compare(this.minor, other.minor);
|
||||
if (result != 0) {
|
||||
return result;
|
||||
}
|
||||
return Integer.compare(this.patch, other.patch);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object other) {
|
||||
return (this == other || (other instanceof Version otherVersion &&
|
||||
this.major == otherVersion.major &&
|
||||
this.minor == otherVersion.minor &&
|
||||
this.patch == otherVersion.patch));
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int result = this.major;
|
||||
result = 31 * result + this.minor;
|
||||
result = 31 * result + this.patch;
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return this.major + "." + this.minor + "." + this.patch;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
* 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.
|
||||
@@ -94,4 +94,10 @@ public @interface DeleteMapping {
|
||||
@AliasFor(annotation = RequestMapping.class)
|
||||
String[] produces() default {};
|
||||
|
||||
/**
|
||||
* Alias for {@link RequestMapping#version()}.
|
||||
*/
|
||||
@AliasFor(annotation = RequestMapping.class)
|
||||
String version() default "";
|
||||
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
* 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.
|
||||
@@ -95,4 +95,10 @@ public @interface GetMapping {
|
||||
@AliasFor(annotation = RequestMapping.class)
|
||||
String[] produces() default {};
|
||||
|
||||
/**
|
||||
* Alias for {@link RequestMapping#version()}.
|
||||
*/
|
||||
@AliasFor(annotation = RequestMapping.class)
|
||||
String version() default "";
|
||||
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
* 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.
|
||||
@@ -94,4 +94,10 @@ public @interface PatchMapping {
|
||||
@AliasFor(annotation = RequestMapping.class)
|
||||
String[] produces() default {};
|
||||
|
||||
/**
|
||||
* Alias for {@link RequestMapping#version()}.
|
||||
*/
|
||||
@AliasFor(annotation = RequestMapping.class)
|
||||
String version() default "";
|
||||
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
* 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.
|
||||
@@ -94,4 +94,10 @@ public @interface PostMapping {
|
||||
@AliasFor(annotation = RequestMapping.class)
|
||||
String[] produces() default {};
|
||||
|
||||
/**
|
||||
* Alias for {@link RequestMapping#version()}.
|
||||
*/
|
||||
@AliasFor(annotation = RequestMapping.class)
|
||||
String version() default "";
|
||||
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
* 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.
|
||||
@@ -94,4 +94,10 @@ public @interface PutMapping {
|
||||
@AliasFor(annotation = RequestMapping.class)
|
||||
String[] produces() default {};
|
||||
|
||||
/**
|
||||
* Alias for {@link RequestMapping#version()}.
|
||||
*/
|
||||
@AliasFor(annotation = RequestMapping.class)
|
||||
String version() default "";
|
||||
|
||||
}
|
||||
|
||||
@@ -216,4 +216,19 @@ public @interface RequestMapping {
|
||||
*/
|
||||
String[] produces() default {};
|
||||
|
||||
/**
|
||||
* Narrows the primary mapping by an API version. The version may be one
|
||||
* of the following:
|
||||
* <ul>
|
||||
* <li>Fixed version ("1.2") -- match this version only.
|
||||
* <li>Baseline version ("1.2+") -- match this and subsequent versions.
|
||||
* </ul>
|
||||
* <p>A baseline version allows an endpoint to continue to work in
|
||||
* subsequent versions if it remains compatible. When an incompatible change
|
||||
* is made eventually, a new controller method for the same endpoint but
|
||||
* with a higher version takes precedence.
|
||||
* @since 7.0
|
||||
*/
|
||||
String version() default "";
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
/*
|
||||
* 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.accept;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.jspecify.annotations.Nullable;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.web.testfixture.servlet.MockHttpServletRequest;
|
||||
|
||||
import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
|
||||
import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy;
|
||||
|
||||
/**
|
||||
* Unit tests for {@link 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());
|
||||
|
||||
MockHttpServletRequest request = new MockHttpServletRequest();
|
||||
strategy.validateVersion(v1, request);
|
||||
strategy.validateVersion(v2, request);
|
||||
|
||||
assertThatThrownBy(() -> strategy.validateVersion(v9, request))
|
||||
.isInstanceOf(InvalidApiVersionException.class);
|
||||
}
|
||||
|
||||
private static DefaultApiVersionStrategy initVersionStrategy(@Nullable String defaultValue) {
|
||||
return new DefaultApiVersionStrategy(
|
||||
List.of(request -> request.getParameter("api-version")),
|
||||
new SemanticApiVersionParser(), true, defaultValue);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
/*
|
||||
* 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.accept;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.web.testfixture.servlet.MockHttpServletRequest;
|
||||
import org.springframework.web.util.ServletRequestPathUtils;
|
||||
|
||||
import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
|
||||
|
||||
/**
|
||||
* Unit tests for {@link 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) {
|
||||
MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri);
|
||||
try {
|
||||
ServletRequestPathUtils.parseAndCache(request);
|
||||
String actual = new PathApiVersionResolver(index).resolveVersion(request);
|
||||
assertThat(actual).isEqualTo(expected);
|
||||
}
|
||||
finally {
|
||||
ServletRequestPathUtils.clearParsedRequestPath(request);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
/*
|
||||
* 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.accept;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.ValueSource;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
|
||||
import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
|
||||
|
||||
/**
|
||||
* Unit tests for {@link SemanticApiVersionParser}.
|
||||
* @author Rossen Stoyanchev
|
||||
*/
|
||||
public class SemanticApiVersionParserTests {
|
||||
|
||||
private final SemanticApiVersionParser parser = new SemanticApiVersionParser();
|
||||
|
||||
|
||||
@Test
|
||||
void parse() {
|
||||
testParse("0", 0, 0, 0);
|
||||
testParse("0.3", 0, 3, 0);
|
||||
testParse("4.5", 4, 5, 0);
|
||||
testParse("6.7.8", 6, 7, 8);
|
||||
testParse("v01", 1, 0, 0);
|
||||
testParse("version-1.2", 1, 2, 0);
|
||||
}
|
||||
|
||||
private void testParse(String input, int major, int minor, int patch) {
|
||||
SemanticApiVersionParser.Version actual = this.parser.parseVersion(input);
|
||||
assertThat(actual.getMajor()).isEqualTo(major);
|
||||
assertThat(actual.getMinor()).isEqualTo(minor);
|
||||
assertThat(actual.getPatch()).isEqualTo(patch);
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(strings = {"", "v", "1a", "1.0a", "1.0.0a", "1.0.0.", "1.0.0-"})
|
||||
void parseInvalid(String input) {
|
||||
testParseInvalid(input);
|
||||
}
|
||||
|
||||
private void testParseInvalid(String input) {
|
||||
assertThatIllegalStateException().isThrownBy(() -> this.parser.parseVersion(input))
|
||||
.withMessage("Invalid API version format");
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user