mirror of
https://github.com/spring-projects/spring-framework.git
synced 2026-09-23 22:59:03 +00:00
Add handling of deprecated API versions
See gh-35049
This commit is contained in:
@@ -0,0 +1,43 @@
|
||||
/*
|
||||
* Copyright 2002-present 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 jakarta.servlet.http.HttpServletResponse;
|
||||
|
||||
/**
|
||||
* Contract to add handling of requests with a deprecated API version. Typically,
|
||||
* this involves use of response headers to send hints and information about
|
||||
* the deprecation to clients.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 7.0
|
||||
* @see StandardApiDeprecationHandler
|
||||
*/
|
||||
public interface ApiDeprecationHandler {
|
||||
|
||||
/**
|
||||
* Check if the requested API version is deprecated, and if so handle it
|
||||
* accordingly, e.g. by setting response headers to signal the deprecation,
|
||||
* to specify relevant dates and provide links to further details.
|
||||
* @param version the resolved and parsed request version
|
||||
* @param request the current request
|
||||
* @param response the current response
|
||||
*/
|
||||
void handleVersion(Comparable<?> version, HttpServletRequest request, HttpServletResponse response);
|
||||
|
||||
}
|
||||
@@ -17,6 +17,7 @@
|
||||
package org.springframework.web.accept;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
/**
|
||||
@@ -25,6 +26,7 @@ import org.jspecify.annotations.Nullable;
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 7.0
|
||||
* @see DefaultApiVersionStrategy
|
||||
*/
|
||||
public interface ApiVersionStrategy {
|
||||
|
||||
@@ -32,6 +34,7 @@ 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}
|
||||
* @see ApiVersionResolver
|
||||
*/
|
||||
@Nullable
|
||||
String resolveVersion(HttpServletRequest request);
|
||||
@@ -40,6 +43,7 @@ public interface ApiVersionStrategy {
|
||||
* Parse the version of a request into an Object.
|
||||
* @param version the value to parse
|
||||
* @return an Object that represents the version
|
||||
* @see ApiVersionParser
|
||||
*/
|
||||
Comparable<?> parseVersion(String version);
|
||||
|
||||
@@ -58,4 +62,15 @@ public interface ApiVersionStrategy {
|
||||
*/
|
||||
@Nullable Comparable<?> getDefaultVersion();
|
||||
|
||||
/**
|
||||
* Check if the requested API version is deprecated, and if so handle it
|
||||
* accordingly, e.g. by setting response headers to signal the deprecation,
|
||||
* to specify relevant dates and provide links to further details.
|
||||
* @param version the resolved and parsed request version
|
||||
* @param request the current request
|
||||
* @param response the current response
|
||||
* @see ApiDeprecationHandler
|
||||
*/
|
||||
void handleDeprecations(Comparable<?> version, HttpServletRequest request, HttpServletResponse response);
|
||||
|
||||
}
|
||||
|
||||
+16
-2
@@ -22,13 +22,14 @@ import java.util.Set;
|
||||
import java.util.TreeSet;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
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.
|
||||
* configured version resolvers, version parser, and deprecation handler.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 7.0
|
||||
@@ -43,6 +44,8 @@ public class DefaultApiVersionStrategy implements ApiVersionStrategy {
|
||||
|
||||
private final @Nullable Comparable<?> defaultVersion;
|
||||
|
||||
private final @Nullable ApiDeprecationHandler deprecationHandler;
|
||||
|
||||
private final Set<Comparable<?>> supportedVersions = new TreeSet<>();
|
||||
|
||||
|
||||
@@ -56,10 +59,13 @@ public class DefaultApiVersionStrategy implements ApiVersionStrategy {
|
||||
* validation fails with {@link MissingApiVersionException}
|
||||
* @param defaultVersion a default version to assign to requests that
|
||||
* don't specify one
|
||||
* @param deprecationHandler handler to send hints and information about
|
||||
* deprecated API versions to clients
|
||||
*/
|
||||
public DefaultApiVersionStrategy(
|
||||
List<ApiVersionResolver> versionResolvers, ApiVersionParser<?> versionParser,
|
||||
boolean versionRequired, @Nullable String defaultVersion) {
|
||||
boolean versionRequired, @Nullable String defaultVersion,
|
||||
@Nullable ApiDeprecationHandler deprecationHandler) {
|
||||
|
||||
Assert.notEmpty(versionResolvers, "At least one ApiVersionResolver is required");
|
||||
Assert.notNull(versionParser, "ApiVersionParser is required");
|
||||
@@ -68,6 +74,7 @@ public class DefaultApiVersionStrategy implements ApiVersionStrategy {
|
||||
this.versionParser = versionParser;
|
||||
this.versionRequired = (versionRequired && defaultVersion == null);
|
||||
this.defaultVersion = (defaultVersion != null ? versionParser.parseVersion(defaultVersion) : null);
|
||||
this.deprecationHandler = deprecationHandler;
|
||||
}
|
||||
|
||||
|
||||
@@ -120,6 +127,13 @@ public class DefaultApiVersionStrategy implements ApiVersionStrategy {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleDeprecations(Comparable<?> version, HttpServletRequest request, HttpServletResponse response) {
|
||||
if (this.deprecationHandler != null) {
|
||||
this.deprecationHandler.handleVersion(version, request, response);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "DefaultApiVersionStrategy[supportedVersions=" + this.supportedVersions +
|
||||
|
||||
+255
@@ -0,0 +1,255 @@
|
||||
/*
|
||||
* Copyright 2002-present 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.net.URI;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* {@code ApiDeprecationHandler} based on
|
||||
* <a href="https://datatracker.ietf.org/doc/html/rfc9745">RFC 9745</a> and
|
||||
* <a href="https://datatracker.ietf.org/doc/html/rfc8594">RFC 8594</a> that
|
||||
* provides the option to set the "Deprecation" and "Sunset" response headers,
|
||||
* as well as to add "Link" headers with further details about both.
|
||||
* <p>To use this handler, create an instance, call {@link #configureVersion}
|
||||
* for each deprecated version, and use the returned {@link VersionSpec} to
|
||||
* provide the deprecation details to send to clients.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 7.0
|
||||
*/
|
||||
public class StandardApiDeprecationHandler implements ApiDeprecationHandler {
|
||||
|
||||
private final ApiVersionParser<?> versionParser;
|
||||
|
||||
private final Map<Comparable<?>, VersionInfo> infos = new HashMap<>();
|
||||
|
||||
|
||||
/**
|
||||
* Create an instance.
|
||||
* <p>By default, {@link SemanticApiVersionParser} is used to parse configured
|
||||
* API versions, so those can be compared to request versions parsed at runtime.
|
||||
* If you have a custom parser, then please use the
|
||||
* {@link #StandardApiDeprecationHandler(ApiVersionParser)} constructor.
|
||||
*/
|
||||
public StandardApiDeprecationHandler() {
|
||||
this(new SemanticApiVersionParser());
|
||||
}
|
||||
|
||||
/**
|
||||
* Variant of the default constructor with a custom {@link ApiVersionParser}.
|
||||
* This needs to be the same as the parser type used at runtime to parse
|
||||
* request versions.
|
||||
*/
|
||||
public StandardApiDeprecationHandler(ApiVersionParser<?> parser) {
|
||||
this.versionParser = parser;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Mark the given API version as deprecated, and use the returned
|
||||
* {@link VersionSpec} to configure the deprecation details to send to clients.
|
||||
* @param version the version to mark as deprecated
|
||||
* @return a spec to configure deprecation details
|
||||
*/
|
||||
public VersionSpec configureVersion(String version) {
|
||||
Comparable<?> parsedVersion = this.versionParser.parseVersion(version);
|
||||
return new VersionSpec(parsedVersion);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void handleVersion(
|
||||
Comparable<?> requestVersion, HttpServletRequest request, HttpServletResponse response) {
|
||||
|
||||
for (VersionInfo info : this.infos.values()) {
|
||||
if (info.match(requestVersion, request)) {
|
||||
if (info.deprecationDate() != null) {
|
||||
response.setHeader("Deprecation", info.deprecationDate());
|
||||
}
|
||||
if (info.deprecationLink() != null) {
|
||||
response.addHeader(HttpHeaders.LINK, info.deprecationLink());
|
||||
}
|
||||
if (info.sunsetDate() != null) {
|
||||
response.setHeader("Sunset", info.sunsetDate());
|
||||
}
|
||||
if (info.sunsetLink() != null) {
|
||||
response.addHeader(HttpHeaders.LINK, info.sunsetLink());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "StandardApiDeprecationHandler " + this.infos.values();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* A spec to configure deprecation details for an API version.
|
||||
*/
|
||||
public final class VersionSpec {
|
||||
|
||||
private final Comparable<?> version;
|
||||
|
||||
private VersionSpec(Comparable<?> version) {
|
||||
this.version = version;
|
||||
StandardApiDeprecationHandler.this.infos.put(version, new VersionInfo(version));
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a predicate to filter which requests to send deprecation info to.
|
||||
* <p>By default, all requests with the deprecated version are handled.
|
||||
* This predicate to narrow the requests that should expose deprecation
|
||||
* information.
|
||||
* @param predicate a predicate to check the request with
|
||||
* @return the same spec instance
|
||||
*/
|
||||
public VersionSpec setRequestPredicate(Predicate<HttpServletRequest> predicate) {
|
||||
return map(info -> info.withRequestPredicate(predicate));
|
||||
}
|
||||
|
||||
/**
|
||||
* Specify a deprecation date for the "Deprecation" response header.
|
||||
* @param date the deprecation date
|
||||
* @return the same spec instance
|
||||
*/
|
||||
public VersionSpec setDeprecationDate(ZonedDateTime date) {
|
||||
return map(info -> info.withDeprecationDate(date));
|
||||
}
|
||||
|
||||
/**
|
||||
* Specify a URL for the "Link" response header with
|
||||
* {@code rel="deprecation"} and {@code type="text/html"}.
|
||||
* @param uri the link value
|
||||
* @return the same spec instance
|
||||
*/
|
||||
public VersionSpec setDeprecationLink(URI uri) {
|
||||
return setDeprecationLink(uri, MediaType.TEXT_HTML);
|
||||
}
|
||||
|
||||
/**
|
||||
* Variation of {@link #setDeprecationLink(URI)} for use with a media type
|
||||
* other than "text/html".
|
||||
* @param uri the link value
|
||||
* @param mediaType the media type to use
|
||||
* @return the same spec instance
|
||||
*/
|
||||
public VersionSpec setDeprecationLink(URI uri, MediaType mediaType) {
|
||||
return map(info -> info.withDeprecationLink(uri, mediaType));
|
||||
}
|
||||
|
||||
/**
|
||||
* Specify a deprecation date for the "Sunset" response header.
|
||||
* @param date the sunset date
|
||||
* @return the same spec instance
|
||||
*/
|
||||
public VersionSpec setSunsetDate(ZonedDateTime date) {
|
||||
return map(info -> info.withSunsetDate(date));
|
||||
}
|
||||
|
||||
/**
|
||||
* Specify a URL for the "Link" response header with
|
||||
* {@code rel="sunset"} and {@code type="text/html"}.
|
||||
* @param uri the link value
|
||||
* @return the same spec instance
|
||||
*/
|
||||
public VersionSpec setSunsetLink(URI uri) {
|
||||
return setSunsetLink(uri, MediaType.TEXT_HTML);
|
||||
}
|
||||
|
||||
/**
|
||||
* Variation of {@link #setSunsetLink(URI)} for use with a media type
|
||||
* other than "text/html".
|
||||
* @param uri the link value
|
||||
* @param mediaType the media type to use
|
||||
* @return the same spec instance
|
||||
*/
|
||||
public VersionSpec setSunsetLink(URI uri, MediaType mediaType) {
|
||||
return map(info -> info.withSunsetLink(uri, mediaType));
|
||||
}
|
||||
|
||||
private VersionSpec map(Function<VersionInfo, VersionInfo> function) {
|
||||
StandardApiDeprecationHandler.this.infos.compute(this.version, (version, versionInfo) -> {
|
||||
Assert.state(versionInfo != null, "No VersionInfo");
|
||||
return function.apply(versionInfo);
|
||||
});
|
||||
return this;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
private record VersionInfo(
|
||||
Comparable<?> version,
|
||||
Predicate<HttpServletRequest> requestPredicate,
|
||||
@Nullable String deprecationDate, @Nullable String deprecationLink,
|
||||
@Nullable String sunsetDate, @Nullable String sunsetLink) {
|
||||
|
||||
VersionInfo(Comparable<?> version) {
|
||||
this(version, request -> true, null, null, null, null);
|
||||
}
|
||||
|
||||
public VersionInfo withRequestPredicate(Predicate<HttpServletRequest> predicate) {
|
||||
return new VersionInfo(version(), predicate,
|
||||
deprecationDate(), deprecationLink(), sunsetDate(), sunsetLink());
|
||||
}
|
||||
|
||||
public VersionInfo withDeprecationDate(ZonedDateTime deprecationDate) {
|
||||
return new VersionInfo(version(), requestPredicate(),
|
||||
"@" + deprecationDate.toInstant().getEpochSecond(), deprecationLink(),
|
||||
sunsetDate(), sunsetLink());
|
||||
}
|
||||
|
||||
public VersionInfo withDeprecationLink(URI uri, MediaType mediaType) {
|
||||
return new VersionInfo(version(), requestPredicate(),
|
||||
deprecationDate(), String.format("<%s>; rel=\"deprecation\"; type=\"%s\"", uri, mediaType),
|
||||
sunsetDate(), sunsetLink());
|
||||
}
|
||||
|
||||
public VersionInfo withSunsetDate(ZonedDateTime sunsetDate) {
|
||||
return new VersionInfo(version(), requestPredicate(),
|
||||
deprecationDate(), deprecationLink(),
|
||||
sunsetDate.format(DateTimeFormatter.RFC_1123_DATE_TIME), sunsetLink());
|
||||
}
|
||||
|
||||
public VersionInfo withSunsetLink(URI uri, MediaType mediaType) {
|
||||
return new VersionInfo(version(), requestPredicate(),
|
||||
deprecationDate(), deprecationLink(),
|
||||
sunsetDate(), String.format("<%s>; rel=\"sunset\"; type=\"%s\"", uri, mediaType));
|
||||
}
|
||||
|
||||
boolean match(Comparable<?> requestVersion, HttpServletRequest request) {
|
||||
return (version().equals(requestVersion) && requestPredicate().test(request));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user