Migrate MediaType.sortByQualityValue() to local implementation#3856
Migrate MediaType.sortByQualityValue() to local implementation#3856gdgenchev wants to merge 2 commits intocloudfoundry:developfrom
Conversation
…ng 7 Spring 7 removed MediaType.sortByQualityValue() and QUALITY_VALUE_COMPARATOR that were deprecated in Spring 6. Copy the sorting logic into a new MediaTypeComparators utility class to preserve content negotiation behavior that respects client quality value preferences from Accept headers. https://github.com/spring-projects/spring-framework/blob/9f431e2eac1b6d8d5ca385d0cc367bac94dd37e7/spring-web/src/main/java/org/springframework/http/MediaType.java#L927-L965
6b1a076 to
13ed684
Compare
There was a problem hiding this comment.
Pull request overview
This PR preserves Spring content-negotiation behavior after MediaType.sortByQualityValue() was removed (Spring 7) by introducing a local MediaType quality-value comparator and updating call sites to use it.
Changes:
- Added
MediaTypeComparators.BY_QUALITY_VALUE, mirroring Spring 6’s quality-value comparator logic. - Replaced
MediaType.sortByQualityValue(...)withacceptedMediaTypes.sort(MediaTypeComparators.BY_QUALITY_VALUE)in two renderers/views.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
server/src/main/java/org/cloudfoundry/identity/uaa/web/ConvertingExceptionView.java |
Switches Accept media type sorting to the new local comparator. |
server/src/main/java/org/cloudfoundry/identity/uaa/util/MediaTypeComparators.java |
Introduces a Spring-6-compatible MediaType quality-value comparator. |
server/src/main/java/org/cloudfoundry/identity/uaa/oauth/provider/error/DefaultOAuth2ExceptionRenderer.java |
Switches Accept media type sorting to the new local comparator. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
- Rename MediaTypeComparators to MediaTypeUtils for better semantics - Add sortByQualityValue() utility method to handle immutable lists - Fix usages to create mutable copies before sorting - Remove unnecessary if-else in ConvertingExceptionView - Add comprehensive unit tests including parameterized tests
e775cb2 to
cf69219
Compare
There was a problem hiding this comment.
Pull request overview
This PR preserves UAA’s existing content-negotiation behavior by replacing the removed MediaType.sortByQualityValue(...) API with an equivalent local implementation, ensuring Accept header q (quality) preferences are still respected when selecting response converters.
Changes:
- Added
MediaTypeUtilswith aBY_QUALITY_VALUEcomparator andsortByQualityValue(...)method matching Spring 6 behavior. - Updated server-side rendering paths to use
MediaTypeUtils.sortByQualityValue(...)instead of the removed Spring API. - Added unit tests covering sorting behavior and comparator edge cases (including stable ordering expectations).
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated no comments.
| File | Description |
|---|---|
server/src/main/java/org/cloudfoundry/identity/uaa/util/MediaTypeUtils.java |
Introduces the local comparator + sort helper to replace removed Spring API. |
server/src/main/java/org/cloudfoundry/identity/uaa/web/ConvertingExceptionView.java |
Switches Accept sorting to the new local utility before choosing a message converter. |
server/src/main/java/org/cloudfoundry/identity/uaa/oauth/provider/error/DefaultOAuth2ExceptionRenderer.java |
Switches Accept sorting to the new local utility before choosing a message converter. |
server/src/test/java/org/cloudfoundry/identity/uaa/util/MediaTypeUtilsTest.java |
Adds unit coverage to validate ordering, stability, and comparator semantics. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
There was a problem hiding this comment.
Pull request overview
This PR preserves UAA’s Accept-header content negotiation behavior after Spring Framework removed MediaType.sortByQualityValue() by introducing a local equivalent and updating call sites to use it.
Changes:
- Added
MediaTypeUtilswith a Spring 6–compatible quality-value comparator and in-place sort helper. - Replaced usages of
MediaType.sortByQualityValue(...)withMediaTypeUtils.sortByQualityValue(...). - Added unit tests covering null handling and comparator behavior.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
| server/src/main/java/org/cloudfoundry/identity/uaa/util/MediaTypeUtils.java | New local Spring-6-equivalent comparator + sorting helper for MediaType. |
| server/src/test/java/org/cloudfoundry/identity/uaa/util/MediaTypeUtilsTest.java | New unit tests for the utility comparator/sort behavior. |
| server/src/main/java/org/cloudfoundry/identity/uaa/web/ConvertingExceptionView.java | Switched Accept sorting to use the local utility. |
| server/src/main/java/org/cloudfoundry/identity/uaa/oauth/provider/error/DefaultOAuth2ExceptionRenderer.java | Switched Accept sorting to use the local utility. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| private MediaTypeUtils() { | ||
| } | ||
|
|
||
| /** | ||
| * Comparator for sorting media types by quality value. | ||
| * This implementation preserves the exact sorting logic from Spring Framework 6's | ||
| * MediaType.QUALITY_VALUE_COMPARATOR. | ||
| */ | ||
| public static final Comparator<MediaType> BY_QUALITY_VALUE = (mediaType1, mediaType2) -> { | ||
| double quality1 = mediaType1.getQualityValue(); | ||
| double quality2 = mediaType2.getQualityValue(); | ||
| int qualityComparison = Double.compare(quality2, quality1); | ||
| if (qualityComparison != 0) { | ||
| return qualityComparison; // audio/*;q=0.7 < audio/*;q=0.3 | ||
| } else if (mediaType1.isWildcardType() && !mediaType2.isWildcardType()) { // */* < audio/* | ||
| return 1; | ||
| } else if (mediaType2.isWildcardType() && !mediaType1.isWildcardType()) { // audio/* > */* | ||
| return -1; | ||
| } else if (!mediaType1.getType().equals(mediaType2.getType())) { // audio/basic == text/html | ||
| return 0; | ||
| } else { // mediaType1.getType().equals(mediaType2.getType()) | ||
| if (mediaType1.isWildcardSubtype() && !mediaType2.isWildcardSubtype()) { // audio/* < audio/basic | ||
| return 1; | ||
| } else if (mediaType2.isWildcardSubtype() && !mediaType1.isWildcardSubtype()) { // audio/basic > audio/* | ||
| return -1; | ||
| } else if (!mediaType1.getSubtype().equals(mediaType2.getSubtype())) { // audio/basic == audio/wave | ||
| return 0; | ||
| } else { | ||
| int paramsSize1 = mediaType1.getParameters().size(); | ||
| int paramsSize2 = mediaType2.getParameters().size(); | ||
| return Integer.compare(paramsSize2, paramsSize1); // audio/basic;level=1 < audio/basic | ||
| } | ||
| } | ||
| }; | ||
|
|
||
| /** | ||
| * Sorts the given list of {@code MediaType} objects by quality value. | ||
| * This method replicates Spring Framework 6's deprecated MediaType.sortByQualityValue() | ||
| * behavior. | ||
| * | ||
| * @param mediaTypes the list to sort | ||
| * @throws IllegalArgumentException if the list is {@code null} | ||
| */ | ||
| public static void sortByQualityValue(List<MediaType> mediaTypes) { | ||
| Assert.notNull(mediaTypes, "'mediaTypes' must not be null"); | ||
| if (mediaTypes.size() > 1) { | ||
| mediaTypes.sort(BY_QUALITY_VALUE); | ||
| } | ||
| } |
| @Test | ||
| void sortByQualityValueWithNullThrowsException() { | ||
| assertThatThrownBy(() -> MediaTypeUtils.sortByQualityValue(null)) | ||
| .isInstanceOf(IllegalArgumentException.class) | ||
| .hasMessageContaining("'mediaTypes' must not be null"); | ||
| } | ||
|
|
||
| @ParameterizedTest | ||
| @MethodSource("listsThatShouldNotBeSorted") | ||
| void sortByQualityValueWithListsThatDoNotNeedSorting(List<MediaType> mediaTypes, List<MediaType> expected) { | ||
| MediaTypeUtils.sortByQualityValue(mediaTypes); | ||
| assertThat(mediaTypes).isEqualTo(expected); | ||
| } | ||
|
|
||
| static Stream<Arguments> listsThatShouldNotBeSorted() { | ||
| return Stream.of( | ||
| Arguments.of(Collections.emptyList(), Collections.emptyList()), | ||
| Arguments.of(Collections.singletonList(MediaType.APPLICATION_JSON), Collections.singletonList(MediaType.APPLICATION_JSON)) | ||
| ); | ||
| } | ||
|
|
||
| @Test | ||
| void sortByQualityValueMaintainsStableSortOrder() { | ||
| MediaType audioBasic = new MediaType("audio", "basic"); | ||
| MediaType audio = new MediaType("audio"); | ||
| MediaType audio03 = new MediaType("audio", "*", 0.3); | ||
| MediaType audio07 = new MediaType("audio", "*", 0.7); | ||
| MediaType audioBasicLevel = new MediaType("audio", "basic", Collections.singletonMap("level", "1")); | ||
| MediaType all = MediaType.ALL; | ||
|
|
||
| List<MediaType> expected = new ArrayList<>(); | ||
| expected.add(audioBasicLevel); | ||
| expected.add(audioBasic); | ||
| expected.add(audio); | ||
| expected.add(all); | ||
| expected.add(audio07); | ||
| expected.add(audio03); | ||
|
|
||
| List<MediaType> result = new ArrayList<>(expected); | ||
| Random rnd = new Random(); | ||
| // shuffle & sort 10 times | ||
| for (int i = 0; i < 10; i++) { | ||
| Collections.shuffle(result, rnd); | ||
| MediaTypeUtils.sortByQualityValue(result); | ||
|
|
||
| for (int j = 0; j < result.size(); j++) { | ||
| assertThat(result.get(j)).as("Invalid media type at " + j).isSameAs(expected.get(j)); | ||
| } | ||
| } | ||
| } |
| @Test | ||
| void sortByQualityValueMaintainsStableSortOrder() { | ||
| MediaType audioBasic = new MediaType("audio", "basic"); | ||
| MediaType audio = new MediaType("audio"); | ||
| MediaType audio03 = new MediaType("audio", "*", 0.3); | ||
| MediaType audio07 = new MediaType("audio", "*", 0.7); | ||
| MediaType audioBasicLevel = new MediaType("audio", "basic", Collections.singletonMap("level", "1")); | ||
| MediaType all = MediaType.ALL; | ||
|
|
||
| List<MediaType> expected = new ArrayList<>(); | ||
| expected.add(audioBasicLevel); | ||
| expected.add(audioBasic); | ||
| expected.add(audio); | ||
| expected.add(all); | ||
| expected.add(audio07); | ||
| expected.add(audio03); | ||
|
|
||
| List<MediaType> result = new ArrayList<>(expected); | ||
| Random rnd = new Random(); | ||
| // shuffle & sort 10 times | ||
| for (int i = 0; i < 10; i++) { | ||
| Collections.shuffle(result, rnd); | ||
| MediaTypeUtils.sortByQualityValue(result); | ||
|
|
||
| for (int j = 0; j < result.size(); j++) { | ||
| assertThat(result.get(j)).as("Invalid media type at " + j).isSameAs(expected.get(j)); | ||
| } | ||
| } | ||
| } |
MediaType.sortByQualityValue()was deprecated in Spring 6 and later removed in Spring 7. Copy the sorting logic into a utility class to preserve content negotiation behavior that respects client quality value preferences from Accept headers.Exact copy of Spring 6:
https://github.com/spring-projects/spring-framework/blob/9f431e2eac1b6d8d5ca385d0cc367bac94dd37e7/spring-web/src/main/java/org/springframework/http/MediaType.java#L906-L911
https://github.com/spring-projects/spring-framework/blob/9f431e2eac1b6d8d5ca385d0cc367bac94dd37e7/spring-web/src/main/java/org/springframework/http/MediaType.java#L927-L965
https://github.com/spring-projects/spring-framework/blob/9f431e2eac1b6d8d5ca385d0cc367bac94dd37e7/spring-web/src/test/java/org/springframework/http/MediaTypeTests.java#L390-L444 (+ some additional tests for the utility method)
This maintains backward compatibility.
The other approach would be to switch to https://github.com/spring-projects/spring-framework/blob/c6096ff6e5e7de92cf6a3466e211d72b401a0178/spring-core/src/main/java/org/springframework/util/MimeTypeUtils.java#L351-L358
but it will not be backward compatible and we will have to change some ITs that cover this.