Skip to content

Commit 482cfb0

Browse files
committed
Add detectSupportedVersions in spring-webmvc
Closes gh-35105
1 parent 3cb8a83 commit 482cfb0

File tree

8 files changed

+137
-58
lines changed

8 files changed

+137
-58
lines changed

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

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -49,9 +49,8 @@ public void queryParameter() throws Exception {
4949
String header = "API-Version";
5050

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

5655
MockMvc mockMvc = standaloneSetup(new PersonController())
5756
.setApiVersionStrategy(versionStrategy)

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

Lines changed: 48 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -44,10 +44,14 @@ public class DefaultApiVersionStrategy implements ApiVersionStrategy {
4444

4545
private final @Nullable Comparable<?> defaultVersion;
4646

47-
private final @Nullable ApiVersionDeprecationHandler deprecationHandler;
48-
4947
private final Set<Comparable<?>> supportedVersions = new TreeSet<>();
5048

49+
private final boolean detectSupportedVersions;
50+
51+
private final Set<Comparable<?>> detectedVersions = new TreeSet<>();
52+
53+
private final @Nullable ApiVersionDeprecationHandler deprecationHandler;
54+
5155

5256
/**
5357
* Create an instance.
@@ -59,12 +63,15 @@ public class DefaultApiVersionStrategy implements ApiVersionStrategy {
5963
* validation fails with {@link MissingApiVersionException}
6064
* @param defaultVersion a default version to assign to requests that
6165
* don't specify one
66+
* @param detectSupportedVersions whether to use API versions that appear in
67+
* mappings for supported version validation (true), or use only explicitly
68+
* configured versions (false).
6269
* @param deprecationHandler handler to send hints and information about
6370
* deprecated API versions to clients
6471
*/
6572
public DefaultApiVersionStrategy(
6673
List<ApiVersionResolver> versionResolvers, ApiVersionParser<?> versionParser,
67-
boolean versionRequired, @Nullable String defaultVersion,
74+
boolean versionRequired, @Nullable String defaultVersion, boolean detectSupportedVersions,
6875
@Nullable ApiVersionDeprecationHandler deprecationHandler) {
6976

7077
Assert.notEmpty(versionResolvers, "At least one ApiVersionResolver is required");
@@ -74,6 +81,7 @@ public DefaultApiVersionStrategy(
7481
this.versionParser = versionParser;
7582
this.versionRequired = (versionRequired && defaultVersion == null);
7683
this.defaultVersion = (defaultVersion != null ? versionParser.parseVersion(defaultVersion) : null);
84+
this.detectSupportedVersions = detectSupportedVersions;
7785
this.deprecationHandler = deprecationHandler;
7886
}
7987

@@ -84,18 +92,38 @@ public DefaultApiVersionStrategy(
8492
}
8593

8694
/**
87-
* Add to the list of known, supported versions to check against in
88-
* {@link ApiVersionStrategy#validateVersion}. Request versions that are not
89-
* in the supported result in {@link InvalidApiVersionException}
90-
* in {@link ApiVersionStrategy#validateVersion}.
91-
* @param versions the versions to add
95+
* Add to the list of supported versions to check against in
96+
* {@link ApiVersionStrategy#validateVersion} before raising
97+
* {@link InvalidApiVersionException} for unknown versions.
98+
* <p>By default, actual version values that appear in request mappings are
99+
* considered supported, and use of this method is optional. However, if you
100+
* prefer to use only explicitly configured, supported versions, then set
101+
* {@code detectSupportedVersions} flag to {@code false}.
102+
* @param versions the supported versions to add
103+
* @see #addMappedVersion(String...)
92104
*/
93105
public void addSupportedVersion(String... versions) {
94106
for (String version : versions) {
95107
this.supportedVersions.add(parseVersion(version));
96108
}
97109
}
98110

111+
/**
112+
* Internal method to add to the list of actual version values that appear in
113+
* request mappings, which allows supported versions to be discovered rather
114+
* than {@link #addSupportedVersion(String...) configured}.
115+
* <p>If you prefer to use explicitly configured, supported versions only,
116+
* set the {@code detectSupportedVersions} flag to {@code false}.
117+
* @param versions the versions to add
118+
* @see #addSupportedVersion(String...)
119+
*/
120+
public void addMappedVersion(String... versions) {
121+
for (String version : versions) {
122+
this.detectedVersions.add(parseVersion(version));
123+
}
124+
}
125+
126+
99127
@Override
100128
public @Nullable String resolveVersion(HttpServletRequest request) {
101129
for (ApiVersionResolver resolver : this.versionResolvers) {
@@ -122,11 +150,16 @@ public void validateVersion(@Nullable Comparable<?> requestVersion, HttpServletR
122150
return;
123151
}
124152

125-
if (!this.supportedVersions.contains(requestVersion)) {
153+
if (!isSupportedVersion(requestVersion)) {
126154
throw new InvalidApiVersionException(requestVersion.toString());
127155
}
128156
}
129157

158+
private boolean isSupportedVersion(Comparable<?> requestVersion) {
159+
return (this.supportedVersions.contains(requestVersion) ||
160+
this.detectSupportedVersions && this.detectedVersions.contains(requestVersion));
161+
}
162+
130163
@Override
131164
public void handleDeprecations(Comparable<?> version, HttpServletRequest request, HttpServletResponse response) {
132165
if (this.deprecationHandler != null) {
@@ -136,8 +169,12 @@ public void handleDeprecations(Comparable<?> version, HttpServletRequest request
136169

137170
@Override
138171
public String toString() {
139-
return "DefaultApiVersionStrategy[supportedVersions=" + this.supportedVersions +
140-
", versionRequired=" + this.versionRequired + ", defaultVersion=" + this.defaultVersion + "]";
172+
return "DefaultApiVersionStrategy[" +
173+
"supportedVersions=" + this.supportedVersions + ", " +
174+
"mappedVersions=" + this.detectedVersions + ", " +
175+
"detectSupportedVersions=" + this.detectSupportedVersions + ", " +
176+
"versionRequired=" + this.versionRequired + ", " +
177+
"defaultVersion=" + this.defaultVersion + "]";
141178
}
142179

143180
}

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

Lines changed: 45 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -32,47 +32,74 @@
3232
*/
3333
public class DefaultApiVersionStrategiesTests {
3434

35-
private final SemanticApiVersionParser parser = new SemanticApiVersionParser();
35+
private static final SemanticApiVersionParser parser = new SemanticApiVersionParser();
36+
37+
private final MockHttpServletRequest request = new MockHttpServletRequest();
3638

3739

3840
@Test
3941
void defaultVersionIsParsed() {
40-
SemanticApiVersionParser.Version version = this.parser.parseVersion("1.2.3");
41-
ApiVersionStrategy strategy = initVersionStrategy(version.toString());
42-
43-
assertThat(strategy.getDefaultVersion()).isEqualTo(version);
42+
String version = "1.2.3";
43+
ApiVersionStrategy strategy = apiVersionStrategy(version);
44+
assertThat(strategy.getDefaultVersion()).isEqualTo(parser.parseVersion(version));
4445
}
4546

4647
@Test
4748
void validateSupportedVersion() {
48-
SemanticApiVersionParser.Version v12 = this.parser.parseVersion("1.2");
49+
String version = "1.2";
50+
DefaultApiVersionStrategy strategy = apiVersionStrategy();
51+
strategy.addSupportedVersion(version);
52+
validateVersion(version, strategy);
53+
}
4954

50-
DefaultApiVersionStrategy strategy = initVersionStrategy(null);
51-
strategy.addSupportedVersion(v12.toString());
55+
@Test
56+
void rejectUnsupportedVersion() {
57+
assertThatThrownBy(() -> validateVersion("1.2", apiVersionStrategy()))
58+
.isInstanceOf(InvalidApiVersionException.class)
59+
.hasMessage("400 BAD_REQUEST \"Invalid API version: '1.2.0'.\"");
60+
}
5261

53-
MockHttpServletRequest request = new MockHttpServletRequest();
54-
strategy.validateVersion(v12, request);
62+
@Test
63+
void validateDetectedVersion() {
64+
String version = "1.2";
65+
DefaultApiVersionStrategy strategy = apiVersionStrategy(null, true);
66+
strategy.addMappedVersion(version);
67+
validateVersion(version, strategy);
5568
}
5669

5770
@Test
58-
void validateUnsupportedVersion() {
59-
assertThatThrownBy(() -> initVersionStrategy(null).validateVersion("1.2", new MockHttpServletRequest()))
60-
.isInstanceOf(InvalidApiVersionException.class)
61-
.hasMessage("400 BAD_REQUEST \"Invalid API version: '1.2'.\"");
71+
void validateWhenDetectedVersionOff() {
72+
String version = "1.2";
73+
DefaultApiVersionStrategy strategy = apiVersionStrategy();
74+
strategy.addMappedVersion(version);
75+
assertThatThrownBy(() -> validateVersion(version, strategy)).isInstanceOf(InvalidApiVersionException.class);
6276
}
6377

6478
@Test
6579
void missingRequiredVersion() {
66-
DefaultApiVersionStrategy strategy = initVersionStrategy(null);
67-
assertThatThrownBy(() -> strategy.validateVersion(null, new MockHttpServletRequest()))
80+
assertThatThrownBy(() -> validateVersion(null, apiVersionStrategy()))
6881
.isInstanceOf(MissingApiVersionException.class)
6982
.hasMessage("400 BAD_REQUEST \"API version is required.\"");
7083
}
7184

72-
private static DefaultApiVersionStrategy initVersionStrategy(@Nullable String defaultValue) {
85+
private static DefaultApiVersionStrategy apiVersionStrategy() {
86+
return apiVersionStrategy(null, false);
87+
}
88+
89+
private static DefaultApiVersionStrategy apiVersionStrategy(@Nullable String defaultVersion) {
90+
return apiVersionStrategy(defaultVersion, false);
91+
}
92+
93+
private static DefaultApiVersionStrategy apiVersionStrategy(
94+
@Nullable String defaultVersion, boolean detectSupportedVersions) {
95+
7396
return new DefaultApiVersionStrategy(
7497
List.of(request -> request.getParameter("api-version")),
75-
new SemanticApiVersionParser(), true, defaultValue, null);
98+
new SemanticApiVersionParser(), true, defaultVersion, detectSupportedVersions, null);
99+
}
100+
101+
private void validateVersion(@Nullable String version, DefaultApiVersionStrategy strategy) {
102+
strategy.validateVersion(version != null ? parser.parseVersion(version) : null, request);
76103
}
77104

78105
}

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

Lines changed: 10 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ public class DefaultApiVersionStrategiesTests {
3939

4040
private static final SemanticApiVersionParser parser = new SemanticApiVersionParser();
4141

42-
private static final ServerWebExchange exchange = MockServerWebExchange.from(MockServerHttpRequest.get("/"));
42+
private final ServerWebExchange exchange = MockServerWebExchange.from(MockServerHttpRequest.get("/"));
4343

4444

4545
@Test
@@ -58,34 +58,31 @@ void validateSupportedVersion() {
5858
}
5959

6060
@Test
61-
void validateUnsupportedVersion() {
61+
void rejectUnsupportedVersion() {
6262
assertThatThrownBy(() -> validateVersion("1.2", apiVersionStrategy()))
6363
.isInstanceOf(InvalidApiVersionException.class)
6464
.hasMessage("400 BAD_REQUEST \"Invalid API version: '1.2.0'.\"");
6565
}
6666

6767
@Test
68-
void validateDetectedSupportedVersion() {
68+
void validateDetectedVersion() {
6969
String version = "1.2";
7070
DefaultApiVersionStrategy strategy = apiVersionStrategy(null, true);
7171
strategy.addMappedVersion(version);
7272
validateVersion(version, strategy);
7373
}
7474

7575
@Test
76-
void validateWhenDetectSupportedVersionsIsOff() {
76+
void validateWhenDetectedVersionOff() {
7777
String version = "1.2";
7878
DefaultApiVersionStrategy strategy = apiVersionStrategy();
7979
strategy.addMappedVersion(version);
80-
81-
assertThatThrownBy(() -> strategy.validateVersion(version, exchange))
82-
.isInstanceOf(InvalidApiVersionException.class);
80+
assertThatThrownBy(() -> validateVersion(version, strategy)).isInstanceOf(InvalidApiVersionException.class);
8381
}
8482

8583
@Test
8684
void missingRequiredVersion() {
87-
DefaultApiVersionStrategy strategy = apiVersionStrategy();
88-
assertThatThrownBy(() -> strategy.validateVersion(null, exchange))
85+
assertThatThrownBy(() -> validateVersion(null, apiVersionStrategy()))
8986
.isInstanceOf(MissingApiVersionException.class)
9087
.hasMessage("400 BAD_REQUEST \"API version is required.\"");
9188
}
@@ -95,15 +92,15 @@ private static DefaultApiVersionStrategy apiVersionStrategy() {
9592
}
9693

9794
private static DefaultApiVersionStrategy apiVersionStrategy(
98-
@Nullable String defaultValue, boolean detectSupportedVersions) {
95+
@Nullable String defaultVersion, boolean detectSupportedVersions) {
9996

10097
return new DefaultApiVersionStrategy(
10198
List.of(exchange -> exchange.getRequest().getQueryParams().getFirst("api-version")),
102-
parser, true, defaultValue, detectSupportedVersions, null);
99+
parser, true, defaultVersion, detectSupportedVersions, null);
103100
}
104101

105-
private static void validateVersion(String version, DefaultApiVersionStrategy strategy) {
106-
strategy.validateVersion(parser.parseVersion(version), exchange);
102+
private void validateVersion(@Nullable String version, DefaultApiVersionStrategy strategy) {
103+
strategy.validateVersion(version != null ? parser.parseVersion(version) : null, exchange);
107104
}
108105

109106
}

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,10 +48,10 @@ void setUp() {
4848
this.strategy = initVersionStrategy(null);
4949
}
5050

51-
private static DefaultApiVersionStrategy initVersionStrategy(@Nullable String defaultValue) {
51+
private static DefaultApiVersionStrategy initVersionStrategy(@Nullable String defaultVersion) {
5252
return new DefaultApiVersionStrategy(
5353
List.of(exchange -> exchange.getRequest().getQueryParams().getFirst("api-version")),
54-
new SemanticApiVersionParser(), true, defaultValue, false, null);
54+
new SemanticApiVersionParser(), true, defaultVersion, false, null);
5555
}
5656

5757
@Test

spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/ApiVersionConfigurer.java

Lines changed: 27 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
import org.springframework.web.accept.ApiVersionResolver;
3232
import org.springframework.web.accept.ApiVersionStrategy;
3333
import org.springframework.web.accept.DefaultApiVersionStrategy;
34+
import org.springframework.web.accept.InvalidApiVersionException;
3435
import org.springframework.web.accept.MediaTypeParamApiVersionResolver;
3536
import org.springframework.web.accept.PathApiVersionResolver;
3637
import org.springframework.web.accept.SemanticApiVersionParser;
@@ -56,6 +57,8 @@ public class ApiVersionConfigurer {
5657

5758
private final Set<String> supportedVersions = new LinkedHashSet<>();
5859

60+
private boolean detectSupportedVersions = true;
61+
5962

6063
/**
6164
* Add resolver to extract the version from a request header.
@@ -155,28 +158,44 @@ public ApiVersionConfigurer setDeprecationHandler(ApiVersionDeprecationHandler h
155158
}
156159

157160
/**
158-
* Add to the list of supported versions to validate request versions against.
159-
* Request versions that are not supported result in
160-
* {@link org.springframework.web.accept.InvalidApiVersionException}.
161-
* <p>Note that the set of supported versions is populated from versions
162-
* listed in controller mappings. Therefore, typically you do not have to
163-
* manage this list except for the initial API version, when controller
164-
* don't have to have a version to start.
161+
* Add to the list of supported versions to check against before raising
162+
* {@link InvalidApiVersionException} for unknown versions.
163+
* <p>By default, actual version values that appear in request mappings are
164+
* used for validation. Therefore, use of this method is optional. However,
165+
* if you prefer to use explicitly configured, supported versions only, then
166+
* set {@link #detectSupportedVersions} to {@code false}.
167+
* <p>Note that the initial API version, if not explicitly declared in any
168+
* request mappings, may need to be declared here instead as a supported
169+
* version.
165170
* @param versions supported versions to add
166171
*/
167172
public ApiVersionConfigurer addSupportedVersions(String... versions) {
168173
Collections.addAll(this.supportedVersions, versions);
169174
return this;
170175
}
171176

177+
/**
178+
* Whether to use versions from mappings for supported version validation.
179+
* <p>By default, this is {@code true} in which case mapped versions are
180+
* considered supported versions. Set this to {@code false} if you want to
181+
* use only explicitly configured {@link #addSupportedVersions(String...)
182+
* supported versions}.
183+
* @param detect whether to use detected versions for validation
184+
*/
185+
public ApiVersionConfigurer detectSupportedVersions(boolean detect) {
186+
this.detectSupportedVersions = detect;
187+
return this;
188+
}
189+
172190
protected @Nullable ApiVersionStrategy getApiVersionStrategy() {
173191
if (this.versionResolvers.isEmpty()) {
174192
return null;
175193
}
176194

177195
DefaultApiVersionStrategy strategy = new DefaultApiVersionStrategy(this.versionResolvers,
178196
(this.versionParser != null ? this.versionParser : new SemanticApiVersionParser()),
179-
this.versionRequired, this.defaultVersion, this.deprecationHandler);
197+
this.versionRequired, this.defaultVersion, this.detectSupportedVersions,
198+
this.deprecationHandler);
180199

181200
this.supportedVersions.forEach(strategy::addSupportedVersion);
182201

spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerMapping.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -306,7 +306,7 @@ protected boolean isHandler(Class<?> beanType) {
306306
if (requestMappingInfo != null && this.apiVersionStrategy instanceof DefaultApiVersionStrategy davs) {
307307
String version = requestMappingInfo.getVersionCondition().getVersion();
308308
if (version != null) {
309-
davs.addSupportedVersion(version);
309+
davs.addMappedVersion(version);
310310
}
311311
}
312312

spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/condition/VersionRequestConditionTests.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,10 +46,10 @@ void setUp() {
4646
this.strategy = initVersionStrategy(null);
4747
}
4848

49-
private static DefaultApiVersionStrategy initVersionStrategy(@Nullable String defaultValue) {
49+
private static DefaultApiVersionStrategy initVersionStrategy(@Nullable String defaultVersion) {
5050
return new DefaultApiVersionStrategy(
5151
List.of(request -> request.getParameter("api-version")),
52-
new SemanticApiVersionParser(), true, defaultValue, null);
52+
new SemanticApiVersionParser(), true, defaultVersion, false, null);
5353
}
5454

5555
@Test

0 commit comments

Comments
 (0)