Skip to content

Commit 2b1a815

Browse files
committed
Add supportedVersionPredicate to ApiVersionConfigurer
Closes gh-35267
1 parent 87838aa commit 2b1a815

File tree

12 files changed

+117
-48
lines changed

12 files changed

+117
-48
lines changed

spring-test/src/test/java/org/springframework/test/web/servlet/client/samples/ApiVersionTests.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ private Map<String, String> performRequest(
7676

7777
DefaultApiVersionStrategy versionStrategy = new DefaultApiVersionStrategy(
7878
List.of(versionResolver), new SemanticApiVersionParser(),
79-
true, null, true, null);
79+
true, null, true, null, null);
8080

8181
RestTestClient client = RestTestClient.bindToController(new TestController())
8282
.configureServer(mockMvcBuilder -> mockMvcBuilder.setApiVersionStrategy(versionStrategy))

spring-test/src/test/java/org/springframework/test/web/servlet/samples/standalone/ApiVersionTests.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ public void queryParameter() throws Exception {
5050

5151
DefaultApiVersionStrategy versionStrategy = new DefaultApiVersionStrategy(
5252
List.of(request -> request.getHeader(header)), new SemanticApiVersionParser(),
53-
true, null, true, null);
53+
true, null, true, null, null);
5454

5555
MockMvc mockMvc = standaloneSetup(new PersonController())
5656
.setApiVersionStrategy(versionStrategy)

spring-web/src/main/java/org/springframework/web/accept/DefaultApiVersionStrategy.java

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import java.util.List;
2121
import java.util.Set;
2222
import java.util.TreeSet;
23+
import java.util.function.Predicate;
2324

2425
import jakarta.servlet.http.HttpServletRequest;
2526
import jakarta.servlet.http.HttpServletResponse;
@@ -50,6 +51,8 @@ public class DefaultApiVersionStrategy implements ApiVersionStrategy {
5051

5152
private final Set<Comparable<?>> detectedVersions = new TreeSet<>();
5253

54+
private final Predicate<Comparable<?>> supportedVersionPredicate;
55+
5356
private final @Nullable ApiVersionDeprecationHandler deprecationHandler;
5457

5558

@@ -71,7 +74,8 @@ public class DefaultApiVersionStrategy implements ApiVersionStrategy {
7174
*/
7275
public DefaultApiVersionStrategy(
7376
List<ApiVersionResolver> versionResolvers, ApiVersionParser<?> versionParser,
74-
boolean versionRequired, @Nullable String defaultVersion, boolean detectSupportedVersions,
77+
boolean versionRequired, @Nullable String defaultVersion,
78+
boolean detectSupportedVersions, @Nullable Predicate<Comparable<?>> supportedVersionPredicate,
7579
@Nullable ApiVersionDeprecationHandler deprecationHandler) {
7680

7781
Assert.notEmpty(versionResolvers, "At least one ApiVersionResolver is required");
@@ -82,9 +86,16 @@ public DefaultApiVersionStrategy(
8286
this.versionRequired = (versionRequired && defaultVersion == null);
8387
this.defaultVersion = (defaultVersion != null ? versionParser.parseVersion(defaultVersion) : null);
8488
this.detectSupportedVersions = detectSupportedVersions;
89+
this.supportedVersionPredicate = initSupportedVersionPredicate(supportedVersionPredicate);
8590
this.deprecationHandler = deprecationHandler;
8691
}
8792

93+
private Predicate<Comparable<?>> initSupportedVersionPredicate(@Nullable Predicate<Comparable<?>> predicate) {
94+
return (predicate != null ? predicate :
95+
(version -> (this.supportedVersions.contains(version) ||
96+
this.detectSupportedVersions && this.detectedVersions.contains(version))));
97+
}
98+
8899

89100
@Override
90101
public @Nullable Comparable<?> getDefaultVersion() {
@@ -160,16 +171,11 @@ public void validateVersion(@Nullable Comparable<?> requestVersion, HttpServletR
160171
return;
161172
}
162173

163-
if (!isSupportedVersion(requestVersion)) {
174+
if (!this.supportedVersionPredicate.test(requestVersion)) {
164175
throw new InvalidApiVersionException(requestVersion.toString());
165176
}
166177
}
167178

168-
private boolean isSupportedVersion(Comparable<?> requestVersion) {
169-
return (this.supportedVersions.contains(requestVersion) ||
170-
this.detectSupportedVersions && this.detectedVersions.contains(requestVersion));
171-
}
172-
173179
@Override
174180
public void handleDeprecations(Comparable<?> version, HttpServletRequest request, HttpServletResponse response) {
175181
if (this.deprecationHandler != null) {

spring-web/src/test/java/org/springframework/web/accept/DefaultApiVersionStrategiesTests.java

Lines changed: 27 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
package org.springframework.web.accept;
1818

1919
import java.util.List;
20+
import java.util.function.Predicate;
2021

2122
import org.jspecify.annotations.Nullable;
2223
import org.junit.jupiter.api.Test;
@@ -44,6 +45,13 @@ void defaultVersionIsParsed() {
4445
assertThat(strategy.getDefaultVersion()).isEqualTo(parser.parseVersion(version));
4546
}
4647

48+
@Test
49+
void missingRequiredVersion() {
50+
assertThatThrownBy(() -> validateVersion(null, apiVersionStrategy()))
51+
.isInstanceOf(MissingApiVersionException.class)
52+
.hasMessage("400 BAD_REQUEST \"API version is required.\"");
53+
}
54+
4755
@Test
4856
void validateSupportedVersion() {
4957
String version = "1.2";
@@ -53,7 +61,7 @@ void validateSupportedVersion() {
5361
}
5462

5563
@Test
56-
void rejectUnsupportedVersion() {
64+
void validateUnsupportedVersion() {
5765
assertThatThrownBy(() -> validateVersion("1.2", apiVersionStrategy()))
5866
.isInstanceOf(InvalidApiVersionException.class)
5967
.hasMessage("400 BAD_REQUEST \"Invalid API version: '1.2.0'.\"");
@@ -62,7 +70,7 @@ void rejectUnsupportedVersion() {
6270
@Test
6371
void validateDetectedVersion() {
6472
String version = "1.2";
65-
DefaultApiVersionStrategy strategy = apiVersionStrategy(null, true);
73+
DefaultApiVersionStrategy strategy = apiVersionStrategy(null, true, null);
6674
strategy.addMappedVersion(version);
6775
validateVersion(version, strategy);
6876
}
@@ -76,30 +84,37 @@ void validateWhenDetectedVersionOff() {
7684
}
7785

7886
@Test
79-
void missingRequiredVersion() {
80-
assertThatThrownBy(() -> validateVersion(null, apiVersionStrategy()))
81-
.isInstanceOf(MissingApiVersionException.class)
82-
.hasMessage("400 BAD_REQUEST \"API version is required.\"");
87+
void validateSupportedWithPredicate() {
88+
SemanticApiVersionParser.Version parsedVersion = parser.parseVersion("1.2");
89+
validateVersion("1.2", apiVersionStrategy(null, false, version -> version.equals(parsedVersion)));
90+
}
91+
92+
@Test
93+
void validateUnsupportedWithPredicate() {
94+
DefaultApiVersionStrategy strategy = apiVersionStrategy(null, false, version -> version.equals("1.2"));
95+
assertThatThrownBy(() -> validateVersion("1.2", strategy)).isInstanceOf(InvalidApiVersionException.class);
8396
}
8497

8598
private static DefaultApiVersionStrategy apiVersionStrategy() {
86-
return apiVersionStrategy(null, false);
99+
return apiVersionStrategy(null, false, null);
87100
}
88101

89102
private static DefaultApiVersionStrategy apiVersionStrategy(@Nullable String defaultVersion) {
90-
return apiVersionStrategy(defaultVersion, false);
103+
return apiVersionStrategy(defaultVersion, false, null);
91104
}
92105

93106
private static DefaultApiVersionStrategy apiVersionStrategy(
94-
@Nullable String defaultVersion, boolean detectSupportedVersions) {
107+
@Nullable String defaultVersion, boolean detectSupportedVersions,
108+
@Nullable Predicate<Comparable<?>> supportedVersionPredicate) {
95109

96110
return new DefaultApiVersionStrategy(
97-
List.of(request -> request.getParameter("api-version")),
98-
new SemanticApiVersionParser(), true, defaultVersion, detectSupportedVersions, null);
111+
List.of(request -> request.getParameter("api-version")), new SemanticApiVersionParser(),
112+
true, defaultVersion, detectSupportedVersions, supportedVersionPredicate, null);
99113
}
100114

101115
private void validateVersion(@Nullable String version, DefaultApiVersionStrategy strategy) {
102-
strategy.validateVersion(version != null ? parser.parseVersion(version) : null, request);
116+
Comparable<?> parsedVersion = (version != null ? parser.parseVersion(version) : null);
117+
strategy.validateVersion(parsedVersion, request);
103118
}
104119

105120
}

spring-webflux/src/main/java/org/springframework/web/reactive/accept/DefaultApiVersionStrategy.java

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import java.util.List;
2121
import java.util.Set;
2222
import java.util.TreeSet;
23+
import java.util.function.Predicate;
2324

2425
import org.jspecify.annotations.Nullable;
2526

@@ -52,6 +53,8 @@ public class DefaultApiVersionStrategy implements ApiVersionStrategy {
5253

5354
private final Set<Comparable<?>> detectedVersions = new TreeSet<>();
5455

56+
private final Predicate<Comparable<?>> supportedVersionPredicate;
57+
5558
private final @Nullable ApiVersionDeprecationHandler deprecationHandler;
5659

5760

@@ -73,7 +76,8 @@ public class DefaultApiVersionStrategy implements ApiVersionStrategy {
7376
*/
7477
public DefaultApiVersionStrategy(
7578
List<ApiVersionResolver> versionResolvers, ApiVersionParser<?> versionParser,
76-
boolean versionRequired, @Nullable String defaultVersion, boolean detectSupportedVersions,
79+
boolean versionRequired, @Nullable String defaultVersion,
80+
boolean detectSupportedVersions, @Nullable Predicate<Comparable<?>> supportedVersionPredicate,
7781
@Nullable ApiVersionDeprecationHandler deprecationHandler) {
7882

7983
Assert.notEmpty(versionResolvers, "At least one ApiVersionResolver is required");
@@ -84,9 +88,16 @@ public DefaultApiVersionStrategy(
8488
this.versionRequired = (versionRequired && defaultVersion == null);
8589
this.defaultVersion = (defaultVersion != null ? versionParser.parseVersion(defaultVersion) : null);
8690
this.detectSupportedVersions = detectSupportedVersions;
91+
this.supportedVersionPredicate = initSupportedVersionPredicate(supportedVersionPredicate);
8792
this.deprecationHandler = deprecationHandler;
8893
}
8994

95+
private Predicate<Comparable<?>> initSupportedVersionPredicate(@Nullable Predicate<Comparable<?>> predicate) {
96+
return (predicate != null ? predicate :
97+
(version -> (this.supportedVersions.contains(version) ||
98+
this.detectSupportedVersions && this.detectedVersions.contains(version))));
99+
}
100+
90101

91102
@Override
92103
public @Nullable Comparable<?> getDefaultVersion() {
@@ -111,7 +122,7 @@ public boolean detectSupportedVersions() {
111122
* considered supported, and use of this method is optional. However, if you
112123
* prefer to use only explicitly configured, supported versions, then set
113124
* {@code detectSupportedVersions} flag to {@code false}.
114-
* @param versions the supported versions to add
125+
* @param versions the supported versions to add
115126
* @see #addMappedVersion(String...)
116127
*/
117128
public void addSupportedVersion(String... versions) {
@@ -161,16 +172,11 @@ public void validateVersion(@Nullable Comparable<?> requestVersion, ServerWebExc
161172
return;
162173
}
163174

164-
if (!isSupportedVersion(requestVersion)) {
175+
if (!this.supportedVersionPredicate.test(requestVersion)) {
165176
throw new InvalidApiVersionException(requestVersion.toString());
166177
}
167178
}
168179

169-
private boolean isSupportedVersion(Comparable<?> requestVersion) {
170-
return (this.supportedVersions.contains(requestVersion) ||
171-
this.detectSupportedVersions && this.detectedVersions.contains(requestVersion));
172-
}
173-
174180
@Override
175181
public void handleDeprecations(Comparable<?> version, ServerWebExchange exchange) {
176182
if (this.deprecationHandler != null) {

spring-webflux/src/main/java/org/springframework/web/reactive/config/ApiVersionConfigurer.java

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import java.util.LinkedHashSet;
2323
import java.util.List;
2424
import java.util.Set;
25+
import java.util.function.Predicate;
2526

2627
import org.jspecify.annotations.Nullable;
2728

@@ -58,6 +59,8 @@ public class ApiVersionConfigurer {
5859

5960
private boolean detectSupportedVersions = true;
6061

62+
private @Nullable Predicate<Comparable<?>> supportedVersionPredicate;
63+
6164
private @Nullable ApiVersionDeprecationHandler deprecationHandler;
6265

6366

@@ -178,6 +181,16 @@ public ApiVersionConfigurer detectSupportedVersions(boolean detect) {
178181
return this;
179182
}
180183

184+
/**
185+
* Provide a {@link Predicate} to perform supported version checks with, in
186+
* effect taking over the supported version check and superseding the
187+
* {@link #addSupportedVersions} and {@link #detectSupportedVersions}.
188+
* @param predicate the predicate to use
189+
*/
190+
public void setSupportedVersionPredicate(@Nullable Predicate<Comparable<?>> predicate) {
191+
this.supportedVersionPredicate = predicate;
192+
}
193+
181194
/**
182195
* Configure a handler to add handling for requests with a deprecated API
183196
* version. Typically, this involves sending hints and information about
@@ -199,8 +212,9 @@ public ApiVersionConfigurer setDeprecationHandler(ApiVersionDeprecationHandler h
199212

200213
DefaultApiVersionStrategy strategy = new DefaultApiVersionStrategy(this.versionResolvers,
201214
(this.versionParser != null ? this.versionParser : new SemanticApiVersionParser()),
202-
(this.versionRequired != null ? this.versionRequired : true),
203-
this.defaultVersion, this.detectSupportedVersions, this.deprecationHandler);
215+
(this.versionRequired != null ? this.versionRequired : true), this.defaultVersion,
216+
this.detectSupportedVersions, this.supportedVersionPredicate,
217+
this.deprecationHandler);
204218

205219
this.supportedVersions.forEach(strategy::addSupportedVersion);
206220

spring-webflux/src/test/java/org/springframework/web/reactive/accept/DefaultApiVersionStrategiesTests.java

Lines changed: 26 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
package org.springframework.web.reactive.accept;
1818

1919
import java.util.List;
20+
import java.util.function.Predicate;
2021

2122
import org.jspecify.annotations.Nullable;
2223
import org.junit.jupiter.api.Test;
@@ -45,10 +46,17 @@ public class DefaultApiVersionStrategiesTests {
4546
@Test
4647
void defaultVersionIsParsed() {
4748
String version = "1.2.3";
48-
ApiVersionStrategy strategy = apiVersionStrategy(version, false);
49+
ApiVersionStrategy strategy = apiVersionStrategy(version, false, null);
4950
assertThat(strategy.getDefaultVersion()).isEqualTo(parser.parseVersion(version));
5051
}
5152

53+
@Test
54+
void missingRequiredVersion() {
55+
assertThatThrownBy(() -> validateVersion(null, apiVersionStrategy()))
56+
.isInstanceOf(MissingApiVersionException.class)
57+
.hasMessage("400 BAD_REQUEST \"API version is required.\"");
58+
}
59+
5260
@Test
5361
void validateSupportedVersion() {
5462
String version = "1.2";
@@ -58,7 +66,7 @@ void validateSupportedVersion() {
5866
}
5967

6068
@Test
61-
void rejectUnsupportedVersion() {
69+
void validateUnsupportedVersion() {
6270
assertThatThrownBy(() -> validateVersion("1.2", apiVersionStrategy()))
6371
.isInstanceOf(InvalidApiVersionException.class)
6472
.hasMessage("400 BAD_REQUEST \"Invalid API version: '1.2.0'.\"");
@@ -67,7 +75,7 @@ void rejectUnsupportedVersion() {
6775
@Test
6876
void validateDetectedVersion() {
6977
String version = "1.2";
70-
DefaultApiVersionStrategy strategy = apiVersionStrategy(null, true);
78+
DefaultApiVersionStrategy strategy = apiVersionStrategy(null, true, null);
7179
strategy.addMappedVersion(version);
7280
validateVersion(version, strategy);
7381
}
@@ -81,26 +89,33 @@ void validateWhenDetectedVersionOff() {
8189
}
8290

8391
@Test
84-
void missingRequiredVersion() {
85-
assertThatThrownBy(() -> validateVersion(null, apiVersionStrategy()))
86-
.isInstanceOf(MissingApiVersionException.class)
87-
.hasMessage("400 BAD_REQUEST \"API version is required.\"");
92+
void validateSupportedWithPredicate() {
93+
SemanticApiVersionParser.Version parsedVersion = parser.parseVersion("1.2");
94+
validateVersion("1.2", apiVersionStrategy(null, false, version -> version.equals(parsedVersion)));
95+
}
96+
97+
@Test
98+
void validateUnsupportedWithPredicate() {
99+
DefaultApiVersionStrategy strategy = apiVersionStrategy(null, false, version -> version.equals("1.2"));
100+
assertThatThrownBy(() -> validateVersion("1.2", strategy)).isInstanceOf(InvalidApiVersionException.class);
88101
}
89102

90103
private static DefaultApiVersionStrategy apiVersionStrategy() {
91-
return apiVersionStrategy(null, false);
104+
return apiVersionStrategy(null, false, null);
92105
}
93106

94107
private static DefaultApiVersionStrategy apiVersionStrategy(
95-
@Nullable String defaultVersion, boolean detectSupportedVersions) {
108+
@Nullable String defaultVersion, boolean detectSupportedVersions,
109+
@Nullable Predicate<Comparable<?>> supportedVersionPredicate) {
96110

97111
return new DefaultApiVersionStrategy(
98112
List.of(exchange -> exchange.getRequest().getQueryParams().getFirst("api-version")),
99-
parser, true, defaultVersion, detectSupportedVersions, null);
113+
parser, true, defaultVersion, detectSupportedVersions, supportedVersionPredicate, null);
100114
}
101115

102116
private void validateVersion(@Nullable String version, DefaultApiVersionStrategy strategy) {
103-
strategy.validateVersion(version != null ? parser.parseVersion(version) : null, exchange);
117+
Comparable<?> parsedVersion = (version != null ? parser.parseVersion(version) : null);
118+
strategy.validateVersion(parsedVersion, exchange);
104119
}
105120

106121
}

spring-webflux/src/test/java/org/springframework/web/reactive/function/server/RequestPredicatesTests.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -380,7 +380,7 @@ private static DefaultServerRequest serverRequest(String version) {
380380

381381
private static DefaultApiVersionStrategy apiVersionStrategy() {
382382
return new DefaultApiVersionStrategy(
383-
List.of(exchange -> null), new SemanticApiVersionParser(), true, null, false, null);
383+
List.of(exchange -> null), new SemanticApiVersionParser(), true, null, false, null, null);
384384
}
385385

386386
}

spring-webflux/src/test/java/org/springframework/web/reactive/result/condition/VersionRequestConditionTests.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ void setUp() {
5252
private static DefaultApiVersionStrategy initVersionStrategy(@Nullable String defaultVersion) {
5353
return new DefaultApiVersionStrategy(
5454
List.of(exchange -> exchange.getRequest().getQueryParams().getFirst("api-version")),
55-
new SemanticApiVersionParser(), true, defaultVersion, false, null);
55+
new SemanticApiVersionParser(), true, defaultVersion, false, null, null);
5656
}
5757

5858
@Test

0 commit comments

Comments
 (0)