From da361699a46bbe1d00e3fd9dd207f9ff2fe4148b Mon Sep 17 00:00:00 2001 From: rstoyanchev Date: Thu, 31 Jul 2025 14:58:26 +0100 Subject: [PATCH] Add MediaType parameter to ApiVersionInserter Closes gh-35259 --- .../web/client/ApiVersionInserter.java | 22 +++++++++--- .../web/client/DefaultApiVersionInserter.java | 26 +++++++++++--- .../DefaultApiVersionInserterBuilder.java | 35 +++++++++---------- .../web/client/RestClientVersionTests.java | 10 +++++- 4 files changed, 65 insertions(+), 28 deletions(-) diff --git a/spring-web/src/main/java/org/springframework/web/client/ApiVersionInserter.java b/spring-web/src/main/java/org/springframework/web/client/ApiVersionInserter.java index 8ff0fbe5a99..2e61e54bec8 100644 --- a/spring-web/src/main/java/org/springframework/web/client/ApiVersionInserter.java +++ b/spring-web/src/main/java/org/springframework/web/client/ApiVersionInserter.java @@ -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 diff --git a/spring-web/src/main/java/org/springframework/web/client/DefaultApiVersionInserter.java b/spring-web/src/main/java/org/springframework/web/client/DefaultApiVersionInserter.java index 903568b098f..a625063943f 100644 --- a/spring-web/src/main/java/org/springframework/web/client/DefaultApiVersionInserter.java +++ b/spring-web/src/main/java/org/springframework/web/client/DefaultApiVersionInserter.java @@ -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 params = new LinkedHashMap<>(contentType.getParameters()); + params.put(this.mediaTypeParam, this.versionFormatter.formatVersion(version)); + contentType = new MediaType(contentType, params); + headers.setContentType(contentType); + } } } diff --git a/spring-web/src/main/java/org/springframework/web/client/DefaultApiVersionInserterBuilder.java b/spring-web/src/main/java/org/springframework/web/client/DefaultApiVersionInserterBuilder.java index d67493088cf..5b8e6017620 100644 --- a/spring-web/src/main/java/org/springframework/web/client/DefaultApiVersionInserterBuilder.java +++ b/spring-web/src/main/java/org/springframework/web/client/DefaultApiVersionInserterBuilder.java @@ -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}. - *

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); } } diff --git a/spring-web/src/test/java/org/springframework/web/client/RestClientVersionTests.java b/spring-web/src/test/java/org/springframework/web/client/RestClientVersionTests.java index adddf6e2601..7ea43c0ab95 100644 --- a/spring-web/src/test/java/org/springframework/web/client/RestClientVersionTests.java +++ b/spring-web/src/test/java/org/springframework/web/client/RestClientVersionTests.java @@ -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);