Add MediaType parameter to ApiVersionInserter

Closes gh-35259
This commit is contained in:
rstoyanchev
2025-07-31 15:32:12 +01:00
parent 08ccf46399
commit da361699a4
4 changed files with 65 additions and 28 deletions
@@ -57,7 +57,7 @@ public interface ApiVersionInserter {
* @param header the name of a header to hold the version
*/
static ApiVersionInserter useHeader(@Nullable String header) {
return new DefaultApiVersionInserterBuilder(header, null, null).build();
return new DefaultApiVersionInserterBuilder(header, null, null, null).build();
}
/**
@@ -65,7 +65,15 @@ public interface ApiVersionInserter {
* @param queryParam the name of a query parameter to hold the version
*/
static ApiVersionInserter useQueryParam(@Nullable String queryParam) {
return new DefaultApiVersionInserterBuilder(null, queryParam, null).build();
return new DefaultApiVersionInserterBuilder(null, queryParam, null, null).build();
}
/**
* Create an inserter to set a MediaType parameter on the "Content-Type" header.
* @param mediaTypeParam the name of the media type parameter to hold the version
*/
static ApiVersionInserter useMediaTypeParam(@Nullable String mediaTypeParam) {
return new DefaultApiVersionInserterBuilder(null, null, mediaTypeParam, null).build();
}
/**
@@ -73,14 +81,14 @@ public interface ApiVersionInserter {
* @param pathSegmentIndex the index of the path segment to hold the version
*/
static ApiVersionInserter usePathSegment(@Nullable Integer pathSegmentIndex) {
return new DefaultApiVersionInserterBuilder(null, null, pathSegmentIndex).build();
return new DefaultApiVersionInserterBuilder(null, null, null, pathSegmentIndex).build();
}
/**
* Create a builder for an {@link ApiVersionInserter}.
*/
static Builder builder() {
return new DefaultApiVersionInserterBuilder(null, null, null);
return new DefaultApiVersionInserterBuilder(null, null, null, null);
}
@@ -101,6 +109,12 @@ public interface ApiVersionInserter {
*/
Builder useQueryParam(@Nullable String queryParam);
/**
* Create an inserter to set a MediaType parameter on the "Content-Type" header.
* @param param the name of the media type parameter to hold the version
*/
Builder useMediaTypeParam(@Nullable String param);
/**
* Configure the inserter to insert a path segment.
* @param pathSegmentIndex the index of the path segment to hold the version
@@ -18,11 +18,14 @@ package org.springframework.web.client;
import java.net.URI;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import org.jspecify.annotations.Nullable;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.util.Assert;
import org.springframework.web.util.UriComponentsBuilder;
@@ -39,20 +42,23 @@ final class DefaultApiVersionInserter implements ApiVersionInserter {
private final @Nullable String queryParam;
private final @Nullable String mediaTypeParam;
private final @Nullable Integer pathSegmentIndex;
private final ApiVersionFormatter versionFormatter;
DefaultApiVersionInserter(
@Nullable String header, @Nullable String queryParam, @Nullable Integer pathSegmentIndex,
@Nullable ApiVersionFormatter formatter) {
@Nullable String header, @Nullable String queryParam, @Nullable String mediaTypeParam,
@Nullable Integer pathSegmentIndex, @Nullable ApiVersionFormatter formatter) {
Assert.isTrue(header != null || queryParam != null || pathSegmentIndex != null,
"Expected 'header', 'queryParam', or 'pathSegmentIndex' to be configured");
Assert.isTrue(header != null || queryParam != null || mediaTypeParam != null || pathSegmentIndex != null,
"Expected 'header', 'queryParam', 'mediaTypeParam', or 'pathSegmentIndex' to be configured");
this.header = header;
this.queryParam = queryParam;
this.mediaTypeParam = mediaTypeParam;
this.pathSegmentIndex = pathSegmentIndex;
this.versionFormatter = (formatter != null ? formatter : Object::toString);
}
@@ -86,7 +92,17 @@ final class DefaultApiVersionInserter implements ApiVersionInserter {
@Override
public void insertVersion(Object version, HttpHeaders headers) {
if (this.header != null) {
headers.set(this.header, this.versionFormatter.formatVersion(version));
String formattedVersion = this.versionFormatter.formatVersion(version);
headers.set(this.header, formattedVersion);
}
if (this.mediaTypeParam != null) {
MediaType contentType = headers.getContentType();
if (contentType != null) {
Map<String, String> params = new LinkedHashMap<>(contentType.getParameters());
params.put(this.mediaTypeParam, this.versionFormatter.formatVersion(version));
contentType = new MediaType(contentType, params);
headers.setContentType(contentType);
}
}
}
@@ -33,16 +33,20 @@ final class DefaultApiVersionInserterBuilder implements ApiVersionInserter.Build
private @Nullable String queryParam;
private @Nullable String mediaTypeParam;
private @Nullable Integer pathSegmentIndex;
private @Nullable ApiVersionFormatter versionFormatter;
DefaultApiVersionInserterBuilder(
@Nullable String header, @Nullable String queryParam, @Nullable Integer pathSegmentIndex) {
@Nullable String header, @Nullable String queryParam, @Nullable String mediaTypeParam,
@Nullable Integer pathSegmentIndex) {
this.header = header;
this.queryParam = queryParam;
this.mediaTypeParam = mediaTypeParam;
this.pathSegmentIndex = pathSegmentIndex;
}
@@ -50,45 +54,40 @@ final class DefaultApiVersionInserterBuilder implements ApiVersionInserter.Build
* Configure the inserter to set a header.
* @param header the name of the header to hold the version
*/
@Override
public ApiVersionInserter.Builder useHeader(@Nullable String header) {
this.header = header;
return this;
}
/**
* Configure the inserter to set a query parameter.
* @param queryParam the name of the query parameter to hold the version
*/
@Override
public ApiVersionInserter.Builder useQueryParam(@Nullable String queryParam) {
this.queryParam = queryParam;
return this;
}
/**
* Configure the inserter to insert a path segment.
* @param pathSegmentIndex the index of the path segment to hold the version
*/
@Override
public ApiVersionInserter.Builder useMediaTypeParam(@Nullable String param) {
this.mediaTypeParam = param;
return this;
}
@Override
public ApiVersionInserter.Builder usePathSegment(@Nullable Integer pathSegmentIndex) {
this.pathSegmentIndex = pathSegmentIndex;
return this;
}
/**
* Format the version Object into a String using the given {@link ApiVersionFormatter}.
* <p>By default, the version is formatted with {@link Object#toString()}.
* @param versionFormatter the formatter to use
*/
@Override
public ApiVersionInserter.Builder withVersionFormatter(ApiVersionFormatter versionFormatter) {
this.versionFormatter = versionFormatter;
return this;
}
/**
* Build the inserter.
*/
public ApiVersionInserter build() {
return new DefaultApiVersionInserter(
this.header, this.queryParam, this.pathSegmentIndex, this.versionFormatter);
this.header, this.queryParam, this.mediaTypeParam, this.pathSegmentIndex,
this.versionFormatter);
}
}
@@ -26,6 +26,7 @@ import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.http.MediaType;
import org.springframework.http.client.JdkClientHttpRequestFactory;
import static org.assertj.core.api.Assertions.assertThat;
@@ -73,6 +74,12 @@ public class RestClientVersionTests {
expectRequest(request -> assertThat(request.getTarget()).isEqualTo("/path?api-version=1.2"));
}
@Test
void mediaTypeParam() {
performRequest(ApiVersionInserter.useMediaTypeParam("v"));
expectRequest(request -> assertThat(request.getHeaders().get("Content-Type")).isEqualTo("application/json;v=1.2"));
}
@Test
void pathSegmentIndexLessThanSize() {
performRequest(ApiVersionInserter.builder().usePathSegment(0).withVersionFormatter(v -> "v" + v).build());
@@ -103,7 +110,8 @@ public class RestClientVersionTests {
private void performRequest(ApiVersionInserter versionInserter) {
restClientBuilder.apiVersionInserter(versionInserter).build()
.get().uri("/path")
.post().uri("/path")
.contentType(MediaType.APPLICATION_JSON)
.apiVersion(1.2)
.retrieve()
.body(String.class);