Browse Source

Support API versioning via MediaType parameter

Closes gh-35050
pull/35129/head
rstoyanchev 6 months ago
parent
commit
5d34f9c87e
  1. 73
      spring-web/src/main/java/org/springframework/web/accept/MediaTypeParamApiVersionResolver.java
  2. 81
      spring-web/src/test/java/org/springframework/web/accept/MediaTypeParamApiVersionResolverTests.java
  3. 66
      spring-webflux/src/main/java/org/springframework/web/reactive/accept/MediaTypeParamApiVersionResolver.java
  4. 14
      spring-webflux/src/main/java/org/springframework/web/reactive/config/ApiVersionConfigurer.java
  5. 80
      spring-webflux/src/test/java/org/springframework/web/reactive/accept/MediaTypeParamApiVersionResolverTests.java
  6. 20
      spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/ApiVersionConfigurer.java

73
spring-web/src/main/java/org/springframework/web/accept/MediaTypeParamApiVersionResolver.java

@ -0,0 +1,73 @@ @@ -0,0 +1,73 @@
/*
* 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.util.Enumeration;
import jakarta.servlet.http.HttpServletRequest;
import org.jspecify.annotations.Nullable;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
/**
* {@link ApiVersionResolver} that extracts the version from a media type
* parameter found in the Accept or Content-Type headers.
*
* @author Rossen Stoyanchev
* @since 7.0
*/
public class MediaTypeParamApiVersionResolver implements ApiVersionResolver {
private final MediaType compatibleMediaType;
private final String parameterName;
/**
* Create an instance.
* @param compatibleMediaType the media type to extract the parameter from with
* the match established via {@link MediaType#isCompatibleWith(MediaType)}
* @param paramName the name of the parameter
*/
public MediaTypeParamApiVersionResolver(MediaType compatibleMediaType, String paramName) {
this.compatibleMediaType = compatibleMediaType;
this.parameterName = paramName;
}
@Override
public @Nullable String resolveVersion(HttpServletRequest request) {
Enumeration<String> headers = request.getHeaders(HttpHeaders.ACCEPT);
while (headers.hasMoreElements()) {
String header = headers.nextElement();
for (MediaType mediaType : MediaType.parseMediaTypes(header)) {
if (this.compatibleMediaType.isCompatibleWith(mediaType)) {
return mediaType.getParameter(this.parameterName);
}
}
}
String header = request.getHeader(HttpHeaders.CONTENT_TYPE);
for (MediaType mediaType : MediaType.parseMediaTypes(header)) {
if (this.compatibleMediaType.isCompatibleWith(mediaType)) {
return mediaType.getParameter(this.parameterName);
}
}
return null;
}
}

81
spring-web/src/test/java/org/springframework/web/accept/MediaTypeParamApiVersionResolverTests.java

@ -0,0 +1,81 @@ @@ -0,0 +1,81 @@
/*
* 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.util.Map;
import org.junit.jupiter.api.Test;
import org.springframework.http.MediaType;
import org.springframework.web.testfixture.servlet.MockHttpServletRequest;
import org.springframework.web.util.ServletRequestPathUtils;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Unit tests for {@link MediaTypeParamApiVersionResolver}.
* @author Rossen Stoyanchev
*/
public class MediaTypeParamApiVersionResolverTests {
private final MediaType mediaType = MediaType.parseMediaType("application/x.abc+json");
private final ApiVersionResolver resolver = new MediaTypeParamApiVersionResolver(mediaType, "version");
private final MockHttpServletRequest request = new MockHttpServletRequest("GET", "/path");
@Test
void resolveFromAccept() {
String version = "3";
this.request.addHeader("Accept", getMediaType(version));
testResolve(this.resolver, this.request, version);
}
@Test
void resolveFromContentType() {
String version = "3";
this.request.setContentType(getMediaType(version).toString());
testResolve(this.resolver, this.request, version);
}
@Test
void wildcard() {
MediaType compatibleMediaType = MediaType.parseMediaType("application/*+json");
ApiVersionResolver resolver = new MediaTypeParamApiVersionResolver(compatibleMediaType, "version");
String version = "3";
this.request.addHeader("Accept", getMediaType(version));
testResolve(resolver, this.request, version);
}
private MediaType getMediaType(String version) {
return new MediaType(this.mediaType, Map.of("version", version));
}
private void testResolve(ApiVersionResolver resolver, MockHttpServletRequest request, String expected) {
try {
ServletRequestPathUtils.parseAndCache(request);
String actual = resolver.resolveVersion(request);
assertThat(actual).isEqualTo(expected);
}
finally {
ServletRequestPathUtils.clearParsedRequestPath(request);
}
}
}

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

@ -0,0 +1,66 @@ @@ -0,0 +1,66 @@
/*
* 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.reactive.accept;
import org.jspecify.annotations.Nullable;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.web.server.ServerWebExchange;
/**
* {@link org.springframework.web.accept.ApiVersionResolver} that extracts the version from a media type
* parameter found in the Accept or Content-Type headers.
*
* @author Rossen Stoyanchev
* @since 7.0
*/
public class MediaTypeParamApiVersionResolver implements ApiVersionResolver {
private final MediaType compatibleMediaType;
private final String parameterName;
/**
* Create an instance.
* @param compatibleMediaType the media type to extract the parameter from with
* the match established via {@link MediaType#isCompatibleWith(MediaType)}
* @param paramName the name of the parameter
*/
public MediaTypeParamApiVersionResolver(MediaType compatibleMediaType, String paramName) {
this.compatibleMediaType = compatibleMediaType;
this.parameterName = paramName;
}
@Override
public @Nullable String resolveVersion(ServerWebExchange exchange) {
HttpHeaders headers = exchange.getRequest().getHeaders();
for (MediaType mediaType : headers.getAccept()) {
if (this.compatibleMediaType.isCompatibleWith(mediaType)) {
return mediaType.getParameter(this.parameterName);
}
}
MediaType mediaType = headers.getContentType();
if (mediaType != null && this.compatibleMediaType.isCompatibleWith(mediaType)) {
return mediaType.getParameter(this.parameterName);
}
return null;
}
}

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

@ -25,12 +25,14 @@ import java.util.Set; @@ -25,12 +25,14 @@ import java.util.Set;
import org.jspecify.annotations.Nullable;
import org.springframework.http.MediaType;
import org.springframework.web.accept.ApiVersionParser;
import org.springframework.web.accept.InvalidApiVersionException;
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.MediaTypeParamApiVersionResolver;
import org.springframework.web.reactive.accept.PathApiVersionResolver;
/**
@ -82,6 +84,18 @@ public class ApiVersionConfigurer { @@ -82,6 +84,18 @@ public class ApiVersionConfigurer {
return this;
}
/**
* Add resolver to extract the version from a media type parameter found in
* the Accept or Content-Type headers.
* @param compatibleMediaType the media type to extract the parameter from with
* the match established via {@link MediaType#isCompatibleWith(MediaType)}
* @param paramName the name of the parameter
*/
public ApiVersionConfigurer useMediaTypeParameter(MediaType compatibleMediaType, String paramName) {
this.versionResolvers.add(new MediaTypeParamApiVersionResolver(compatibleMediaType, paramName));
return this;
}
/**
* Add custom resolvers to resolve the API version.
* @param resolvers the resolvers to use

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

@ -0,0 +1,80 @@ @@ -0,0 +1,80 @@
/*
* 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.reactive.accept;
import java.util.Map;
import org.junit.jupiter.api.Test;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
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.Assertions.assertThat;
/**
* Unit tests for {@link MediaTypeParamApiVersionResolver}.
* @author Rossen Stoyanchev
*/
public class MediaTypeParamApiVersionResolverTests {
private final MediaType mediaType = MediaType.parseMediaType("application/x.abc+json");
private final ApiVersionResolver resolver = new MediaTypeParamApiVersionResolver(mediaType, "version");
private final MockServerHttpRequest.BaseBuilder<?> request = MockServerHttpRequest.get("/path");
@Test
void resolveFromAccept() {
String version = "3";
this.request.accept(getMediaType(version));
testResolve(this.resolver, this.request, version);
}
@Test
void resolveFromContentType() {
String version = "3";
this.request.header(HttpHeaders.CONTENT_TYPE, getMediaType(version).toString());
testResolve(this.resolver, this.request, version);
}
@Test
void wildcard() {
MediaType compatibleMediaType = MediaType.parseMediaType("application/*+json");
ApiVersionResolver resolver = new MediaTypeParamApiVersionResolver(compatibleMediaType, "version");
String version = "3";
this.request.accept(getMediaType(version));
testResolve(resolver, this.request, version);
}
private MediaType getMediaType(String version) {
return new MediaType(this.mediaType, Map.of("version", version));
}
private static void testResolve(
ApiVersionResolver resolver, MockServerHttpRequest.BaseBuilder<?> request, String expected) {
ServerWebExchange exchange = MockServerWebExchange.from(request);
String actual = resolver.resolveVersion(exchange);
assertThat(actual).isEqualTo(expected);
}
}

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

@ -25,10 +25,12 @@ import java.util.Set; @@ -25,10 +25,12 @@ import java.util.Set;
import org.jspecify.annotations.Nullable;
import org.springframework.http.MediaType;
import org.springframework.web.accept.ApiVersionParser;
import org.springframework.web.accept.ApiVersionResolver;
import org.springframework.web.accept.ApiVersionStrategy;
import org.springframework.web.accept.DefaultApiVersionStrategy;
import org.springframework.web.accept.MediaTypeParamApiVersionResolver;
import org.springframework.web.accept.PathApiVersionResolver;
import org.springframework.web.accept.SemanticApiVersionParser;
@ -52,7 +54,7 @@ public class ApiVersionConfigurer { @@ -52,7 +54,7 @@ public class ApiVersionConfigurer {
/**
* Add a resolver that extracts the API version from a request header.
* Add resolver to extract the version from a request header.
* @param headerName the header name to check
*/
public ApiVersionConfigurer useRequestHeader(String headerName) {
@ -61,7 +63,7 @@ public class ApiVersionConfigurer { @@ -61,7 +63,7 @@ public class ApiVersionConfigurer {
}
/**
* Add a resolver that extracts the API version from a request parameter.
* Add resolver to extract the version from a request parameter.
* @param paramName the parameter name to check
*/
public ApiVersionConfigurer useRequestParam(String paramName) {
@ -70,7 +72,7 @@ public class ApiVersionConfigurer { @@ -70,7 +72,7 @@ public class ApiVersionConfigurer {
}
/**
* Add a resolver that extracts the API version from a path segment.
* Add resolver to extract the 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.
*/
@ -79,6 +81,18 @@ public class ApiVersionConfigurer { @@ -79,6 +81,18 @@ public class ApiVersionConfigurer {
return this;
}
/**
* Add resolver to extract the version from a media type parameter found in
* the Accept or Content-Type headers.
* @param compatibleMediaType the media type to extract the parameter from with
* the match established via {@link MediaType#isCompatibleWith(MediaType)}
* @param paramName the name of the parameter
*/
public ApiVersionConfigurer useMediaTypeParameter(MediaType compatibleMediaType, String paramName) {
this.versionResolvers.add(new MediaTypeParamApiVersionResolver(compatibleMediaType, paramName));
return this;
}
/**
* Add custom resolvers to resolve the API version.
* @param resolvers the resolvers to use

Loading…
Cancel
Save