diff --git a/spring-core/src/main/java/org/springframework/util/CollectionUtils.java b/spring-core/src/main/java/org/springframework/util/CollectionUtils.java index d1e9354abda9..12da49383bd6 100644 --- a/spring-core/src/main/java/org/springframework/util/CollectionUtils.java +++ b/spring-core/src/main/java/org/springframework/util/CollectionUtils.java @@ -29,6 +29,7 @@ import java.util.Properties; import java.util.Set; import java.util.SortedSet; +import java.util.function.BiPredicate; import org.springframework.lang.Nullable; @@ -480,6 +481,38 @@ public static MultiValueMap unmodifiableMultiValueMap( return toMultiValueMap(unmodifiableMap); } + /** + * Sort the specified list with the (inefficient) bubble sort algorithm, + * using the specified swapping function. + * + *

Note: for general purpose sorting, + * {@link Collections#sort(List, java.util.Comparator)} is far more efficient. + * However, bubble sort does not require a transitive comparison operation, + * whereas {@link java.util.Comparator#compare(Object, Object)} does. + * @param the type of the objects in the list + * @param list the list to be sorted + * @param swap the function that determines whether two elements should be + * swapped + * @since 6.0 + * @see Collections#sort(List, java.util.Comparator) + */ + public static void bubbleSort(List list, BiPredicate swap) { + Assert.notNull(list, "List must not be null"); + Assert.notNull(swap, "Swap must not be null"); + + int len = list.size(); + for (int i = 0; i < len; i++) { + for (int j = 1; j < len - i ; j++) { + T prev = list.get(j - 1); + T cur = list.get(j); + if (swap.test(prev, cur)) { + list.set(j, prev); + list.set(j - 1, cur); + } + } + } + } + /** * Iterator wrapping an Enumeration. diff --git a/spring-core/src/main/java/org/springframework/util/MimeType.java b/spring-core/src/main/java/org/springframework/util/MimeType.java index b427527267d0..cf6010a4e570 100644 --- a/spring-core/src/main/java/org/springframework/util/MimeType.java +++ b/spring-core/src/main/java/org/springframework/util/MimeType.java @@ -26,7 +26,6 @@ import java.util.Comparator; import java.util.Iterator; import java.util.LinkedHashMap; -import java.util.List; import java.util.Locale; import java.util.Map; import java.util.TreeSet; @@ -529,7 +528,6 @@ private void appendTo(Map map, StringBuilder builder) { /** * Compares this MIME Type to another alphabetically. * @param other the MIME Type to compare to - * @see MimeTypeUtils#sortBySpecificity(List) */ @Override public int compareTo(MimeType other) { @@ -592,6 +590,88 @@ public int compareTo(MimeType other) { return 0; } + /** + * Indicates whether this {@code MimeType} is more specific than the given + * type. + *

    + *
  1. if this mime type has a {@linkplain #isWildcardType() wildcard type}, + * and the other does not, then this method returns {@code false}.
  2. + *
  3. if this mime type does not have a {@linkplain #isWildcardType() wildcard type}, + * and the other does, then this method returns {@code true}.
  4. + *
  5. if this mime type has a {@linkplain #isWildcardType() wildcard type}, + * and the other does not, then this method returns {@code false}.
  6. + *
  7. if this mime type does not have a {@linkplain #isWildcardType() wildcard type}, + * and the other does, then this method returns {@code true}.
  8. + *
  9. if the two mime types have identical {@linkplain #getType() type} and + * {@linkplain #getSubtype() subtype}, then the mime type with the most + * parameters is more specific than the other.
  10. + *
  11. Otherwise, this method returns {@code false}.
  12. + *
+ * @param other the {@code MimeType} to be compared + * @return the result of the comparison + * @since 6.0 + * @see #isLessSpecific(MimeType) + * @see HTTP 1.1: Semantics + * and Content, section 5.3.2 + */ + public boolean isMoreSpecific(MimeType other) { + Assert.notNull(other, "Other must not be null"); + boolean thisWildcard = isWildcardType(); + boolean otherWildcard = other.isWildcardType(); + if (thisWildcard && !otherWildcard) { // */* > audio/* + return false; + } + else if (!thisWildcard && otherWildcard) { // audio/* < */* + return true; + } + else { + boolean thisWildcardSubtype = isWildcardSubtype(); + boolean otherWildcardSubtype = other.isWildcardSubtype(); + if (thisWildcardSubtype && !otherWildcardSubtype) { // audio/* > audio/basic + return false; + } + else if (!thisWildcardSubtype && otherWildcardSubtype) { // audio/basic < audio/* + return true; + } + else if (getType().equals(other.getType()) && getSubtype().equals(other.getSubtype())) { + int paramsSize1 = getParameters().size(); + int paramsSize2 = other.getParameters().size(); + return paramsSize1 > paramsSize2; + } + else { + return false; + } + } + } + + /** + * Indicates whether this {@code MimeType} is more less than the given type. + *
    + *
  1. if this mime type has a {@linkplain #isWildcardType() wildcard type}, + * and the other does not, then this method returns {@code true}.
  2. + *
  3. if this mime type does not have a {@linkplain #isWildcardType() wildcard type}, + * and the other does, then this method returns {@code false}.
  4. + *
  5. if this mime type has a {@linkplain #isWildcardType() wildcard type}, + * and the other does not, then this method returns {@code true}.
  6. + *
  7. if this mime type does not have a {@linkplain #isWildcardType() wildcard type}, + * and the other does, then this method returns {@code false}.
  8. + *
  9. if the two mime types have identical {@linkplain #getType() type} and + * {@linkplain #getSubtype() subtype}, then the mime type with the least + * parameters is less specific than the other.
  10. + *
  11. Otherwise, this method returns {@code false}.
  12. + *
+ * @param other the {@code MimeType} to be compared + * @return the result of the comparison + * @since 6.0 + * @see #isMoreSpecific(MimeType) + * @see HTTP 1.1: Semantics + * and Content, section 5.3.2 + */ + public boolean isLessSpecific(MimeType other) { + Assert.notNull(other, "Other must not be null"); + return other.isMoreSpecific(this); + } + private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException { // Rely on default serialization, just initialize state after deserialization. ois.defaultReadObject(); @@ -625,7 +705,9 @@ private static Map addCharsetParameter(Charset charset, Map the type of mime types that may be compared by this comparator + * @deprecated As of 6.0, with no direct replacement */ + @Deprecated public static class SpecificityComparator implements Comparator { @Override diff --git a/spring-core/src/main/java/org/springframework/util/MimeTypeUtils.java b/spring-core/src/main/java/org/springframework/util/MimeTypeUtils.java index 05809bc5ad7b..f3650a5fe2e2 100644 --- a/spring-core/src/main/java/org/springframework/util/MimeTypeUtils.java +++ b/spring-core/src/main/java/org/springframework/util/MimeTypeUtils.java @@ -51,8 +51,10 @@ public abstract class MimeTypeUtils { 'V', 'W', 'X', 'Y', 'Z'}; /** - * Comparator used by {@link #sortBySpecificity(List)}. + * Comparator formally used by {@link #sortBySpecificity(List)}. + * @deprecated As of 6.0, with no direct replacement */ + @Deprecated public static final Comparator SPECIFICITY_COMPARATOR = new MimeType.SpecificityComparator<>(); /** @@ -334,34 +336,23 @@ public static String toString(Collection mimeTypes) { } /** - * Sorts the given list of {@code MimeType} objects by specificity. - *

Given two mime types: - *

    - *
  1. if either mime type has a {@linkplain MimeType#isWildcardType() wildcard type}, - * then the mime type without the wildcard is ordered before the other.
  2. - *
  3. if the two mime types have different {@linkplain MimeType#getType() types}, - * then they are considered equal and remain their current order.
  4. - *
  5. if either mime type has a {@linkplain MimeType#isWildcardSubtype() wildcard subtype} - * , then the mime type without the wildcard is sorted before the other.
  6. - *
  7. if the two mime types have different {@linkplain MimeType#getSubtype() subtypes}, - * then they are considered equal and remain their current order.
  8. - *
  9. if the two mime types have a different amount of - * {@linkplain MimeType#getParameter(String) parameters}, then the mime type with the most - * parameters is ordered before the other.
  10. - *
- *

For example:

audio/basic < audio/* < */*
- *
audio/basic;level=1 < audio/basic
- *
audio/basic == text/html
audio/basic == - * audio/wave
+ * Sorts the given list of {@code MimeType} objects by + * {@linkplain MimeType#isMoreSpecific(MimeType) specificity}. + * + *

Because of the computational cost, this method throws an exception + * when the given list contains too many elements. * @param mimeTypes the list of mime types to be sorted + * @throws IllegalArgumentException if {@code mimeTypes} contains more + * than 50 elements * @see HTTP 1.1: Semantics * and Content, section 5.3.2 + * @see MimeType#isMoreSpecific(MimeType) */ - public static void sortBySpecificity(List mimeTypes) { + public static void sortBySpecificity(List mimeTypes) { Assert.notNull(mimeTypes, "'mimeTypes' must not be null"); - if (mimeTypes.size() > 1) { - mimeTypes.sort(SPECIFICITY_COMPARATOR); - } + Assert.isTrue(mimeTypes.size() <= 50, "Too many elements"); + + CollectionUtils.bubbleSort(mimeTypes, MimeType::isLessSpecific); } diff --git a/spring-core/src/test/java/org/springframework/util/CollectionUtilsTests.java b/spring-core/src/test/java/org/springframework/util/CollectionUtilsTests.java index 451a6895ccff..5cfb9adc35bd 100644 --- a/spring-core/src/test/java/org/springframework/util/CollectionUtilsTests.java +++ b/spring-core/src/test/java/org/springframework/util/CollectionUtilsTests.java @@ -209,6 +209,13 @@ void hasUniqueObject() { assertThat(CollectionUtils.hasUniqueObject(list)).isFalse(); } + @Test + void bubbleSort() { + List list = new ArrayList<>(List.of(10, 9, 8, 7, 6, 5, 4, 3, 2, 1)); + CollectionUtils.bubbleSort(list, (i1, i2) -> i1 > i2); + assertThat(list).containsExactly(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); + } + private static final class Instance { diff --git a/spring-core/src/test/java/org/springframework/util/MimeTypeTests.java b/spring-core/src/test/java/org/springframework/util/MimeTypeTests.java index 59bfad035805..8422e24457ac 100644 --- a/spring-core/src/test/java/org/springframework/util/MimeTypeTests.java +++ b/spring-core/src/test/java/org/springframework/util/MimeTypeTests.java @@ -402,6 +402,62 @@ void compareToCaseSensitivity() { assertThat(m2.compareTo(m1) != 0).as("Invalid comparison result").isTrue(); } + @Test + void isMoreSpecific() { + MimeType audioBasic = new MimeType("audio", "basic"); + MimeType audio = new MimeType("audio"); + MimeType audioWave = new MimeType("audio", "wave"); + MimeType audioBasicLevel = new MimeType("audio", "basic", singletonMap("level", "1")); + + assertThat(audioBasic.isMoreSpecific(audioBasicLevel)).isFalse(); + assertThat(audioBasicLevel.isMoreSpecific(audioBasic)).isTrue(); + + assertThat(audio.isMoreSpecific(MimeTypeUtils.ALL)).isTrue(); + assertThat(MimeTypeUtils.ALL.isMoreSpecific(audio)).isFalse(); + + assertThat(audioBasicLevel.isMoreSpecific(audioBasic)).isTrue(); + assertThat(audioBasic.isMoreSpecific(audioBasicLevel)).isFalse(); + + assertThat(audioBasic.isMoreSpecific(MimeTypeUtils.TEXT_HTML)).isFalse(); + assertThat(audioBasic.isMoreSpecific(audioWave)).isFalse(); + assertThat(audioBasicLevel.isMoreSpecific(MimeTypeUtils.TEXT_HTML)).isFalse(); + } + + @Test + void isLessSpecific() { + MimeType audioBasic = new MimeType("audio", "basic"); + MimeType audio = new MimeType("audio"); + MimeType audioWave = new MimeType("audio", "wave"); + MimeType audioBasicLevel = new MimeType("audio", "basic", singletonMap("level", "1")); + + assertThat(audioBasic.isLessSpecific(audioBasicLevel)).isTrue(); + assertThat(audioBasicLevel.isLessSpecific(audioBasic)).isFalse(); + + assertThat(audio.isLessSpecific(MimeTypeUtils.ALL)).isFalse(); + assertThat(MimeTypeUtils.ALL.isLessSpecific(audio)).isTrue(); + + assertThat(audioBasicLevel.isLessSpecific(audioBasic)).isFalse(); + assertThat(audioBasic.isLessSpecific(audioBasicLevel)).isTrue(); + + assertThat(audioBasic.isLessSpecific(MimeTypeUtils.TEXT_HTML)).isFalse(); + assertThat(audioBasic.isLessSpecific(audioWave)).isFalse(); + assertThat(audioBasicLevel.isLessSpecific(MimeTypeUtils.TEXT_HTML)).isFalse(); + } + + @Test + void sortBySpecificity() { + MimeType audioBasic = new MimeType("audio", "basic"); + MimeType audio = new MimeType("audio"); + MimeType audioWave = new MimeType("audio", "wave"); + MimeType audioBasicLevel = new MimeType("audio", "basic", singletonMap("level", "1")); + + List mimeTypes = new ArrayList<>(List.of(MimeTypeUtils.ALL, audio, audioWave, audioBasic, + audioBasicLevel)); + MimeTypeUtils.sortBySpecificity(mimeTypes); + + assertThat(mimeTypes).containsExactly(audioWave, audioBasicLevel, audioBasic, audio, MimeTypeUtils.ALL); + } + @Test // SPR-13157 void equalsIsCaseInsensitiveForCharsets() { MimeType m1 = new MimeType("text", "plain", singletonMap("charset", "UTF-8")); diff --git a/spring-web/src/main/java/org/springframework/http/MediaType.java b/spring-web/src/main/java/org/springframework/http/MediaType.java index 17744012ac13..ae279fc0c6d0 100644 --- a/spring-web/src/main/java/org/springframework/http/MediaType.java +++ b/spring-web/src/main/java/org/springframework/http/MediaType.java @@ -534,6 +534,82 @@ public double getQualityValue() { return (qualityFactor != null ? Double.parseDouble(unquote(qualityFactor)) : 1D); } + /** + * Indicates whether this {@code MediaType} more specific than the given type. + *

    + *
  1. if this media type has a {@linkplain #getQualityValue() quality factor} higher than the other, + * then this method returns {@code true}.
  2. + *
  3. if this media type has a {@linkplain #getQualityValue() quality factor} lower than the other, + * then this method returns {@code false}.
  4. + *
  5. if this mime type has a {@linkplain #isWildcardType() wildcard type}, + * and the other does not, then this method returns {@code false}.
  6. + *
  7. if this mime type does not have a {@linkplain #isWildcardType() wildcard type}, + * and the other does, then this method returns {@code true}.
  8. + *
  9. if this mime type has a {@linkplain #isWildcardType() wildcard type}, + * and the other does not, then this method returns {@code false}.
  10. + *
  11. if this mime type does not have a {@linkplain #isWildcardType() wildcard type}, + * and the other does, then this method returns {@code true}.
  12. + *
  13. if the two mime types have identical {@linkplain #getType() type} and + * {@linkplain #getSubtype() subtype}, then the mime type with the most + * parameters is more specific than the other.
  14. + *
  15. Otherwise, this method returns {@code false}.
  16. + *
+ * @param other the {@code MimeType} to be compared + * @return the result of the comparison + * @since 6.0 + * @see #isLessSpecific(MimeType) + * @see HTTP 1.1: Semantics + * and Content, section 5.3.2 + */ + @Override + public boolean isMoreSpecific(MimeType other) { + Assert.notNull(other, "Other must not be null"); + if (other instanceof MediaType otherMediaType) { + double quality1 = getQualityValue(); + double quality2 = otherMediaType.getQualityValue(); + if (quality1 > quality2) { + return true; + } + else if (quality1 < quality2) { + return false; + } + } + return super.isMoreSpecific(other); + } + + /** + * Indicates whether this {@code MediaType} more specific than the given type. + *
    + *
  1. if this media type has a {@linkplain #getQualityValue() quality factor} higher than the other, + * then this method returns {@code false}.
  2. + *
  3. if this media type has a {@linkplain #getQualityValue() quality factor} lower than the other, + * then this method returns {@code true}.
  4. + *
  5. if this mime type has a {@linkplain #isWildcardType() wildcard type}, + * and the other does not, then this method returns {@code true}.
  6. + *
  7. if this mime type does not have a {@linkplain #isWildcardType() wildcard type}, + * and the other does, then this method returns {@code false}.
  8. + *
  9. if this mime type has a {@linkplain #isWildcardType() wildcard type}, + * and the other does not, then this method returns {@code true}.
  10. + *
  11. if this mime type does not have a {@linkplain #isWildcardType() wildcard type}, + * and the other does, then this method returns {@code false}.
  12. + *
  13. if the two mime types have identical {@linkplain #getType() type} and + * {@linkplain #getSubtype() subtype}, then the mime type with the least + * parameters is less specific than the other.
  14. + *
  15. Otherwise, this method returns {@code false}.
  16. + *
+ * @param other the {@code MimeType} to be compared + * @return the result of the comparison + * @since 6.0 + * @see #isMoreSpecific(MimeType) + * @see HTTP 1.1: Semantics + * and Content, section 5.3.2 + */ + @Override + public boolean isLessSpecific(MimeType other) { + Assert.notNull(other, "Other must not be null"); + return other.isMoreSpecific(this); + } + /** * Indicate whether this {@code MediaType} includes the given media type. *

For instance, {@code text/*} includes {@code text/plain} and {@code text/html}, @@ -731,9 +807,9 @@ public static String toString(Collection mediaTypes) { *

audio/basic == text/html
*
audio/basic == audio/wave
* @param mediaTypes the list of media types to be sorted - * @see HTTP 1.1: Semantics - * and Content, section 5.3.2 + * @deprecated As of 6.0, in favor of {@link MimeTypeUtils#sortBySpecificity(List)} */ + @Deprecated public static void sortBySpecificity(List mediaTypes) { Assert.notNull(mediaTypes, "'mediaTypes' must not be null"); if (mediaTypes.size() > 1) { @@ -760,7 +836,9 @@ public static void sortBySpecificity(List mediaTypes) { * * @param mediaTypes the list of media types to be sorted * @see #getQualityValue() + * @deprecated As of 6.0, with no direct replacement */ + @Deprecated public static void sortByQualityValue(List mediaTypes) { Assert.notNull(mediaTypes, "'mediaTypes' must not be null"); if (mediaTypes.size() > 1) { @@ -771,9 +849,9 @@ public static void sortByQualityValue(List mediaTypes) { /** * Sorts the given list of {@code MediaType} objects by specificity as the * primary criteria and quality value the secondary. - * @see MediaType#sortBySpecificity(List) - * @see MediaType#sortByQualityValue(List) + * @deprecated As of 6.0, in favor of {@link MimeTypeUtils#sortBySpecificity(List)} */ + @Deprecated public static void sortBySpecificityAndQuality(List mediaTypes) { Assert.notNull(mediaTypes, "'mediaTypes' must not be null"); if (mediaTypes.size() > 1) { @@ -784,7 +862,9 @@ public static void sortBySpecificityAndQuality(List mediaTypes) { /** * Comparator used by {@link #sortByQualityValue(List)}. + * @deprecated As of 6.0, with no direct replacement */ + @Deprecated public static final Comparator QUALITY_VALUE_COMPARATOR = (mediaType1, mediaType2) -> { double quality1 = mediaType1.getQualityValue(); double quality2 = mediaType2.getQualityValue(); @@ -822,7 +902,9 @@ else if (!mediaType1.getSubtype().equals(mediaType2.getSubtype())) { // audio/b /** * Comparator used by {@link #sortBySpecificity(List)}. + * @deprecated As of 6.0, with no direct replacement */ + @Deprecated public static final Comparator SPECIFICITY_COMPARATOR = new SpecificityComparator<>() { @Override diff --git a/spring-web/src/test/java/org/springframework/http/MediaTypeTests.java b/spring-web/src/test/java/org/springframework/http/MediaTypeTests.java index 74e73bfa7126..9c145058452c 100644 --- a/spring-web/src/test/java/org/springframework/http/MediaTypeTests.java +++ b/spring-web/src/test/java/org/springframework/http/MediaTypeTests.java @@ -28,6 +28,7 @@ import org.springframework.core.convert.ConversionService; import org.springframework.core.convert.support.DefaultConversionService; import org.springframework.core.testfixture.io.SerializationTestUtils; +import org.springframework.util.MimeTypeUtils; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; @@ -240,6 +241,44 @@ public void compareToCaseSensitivity() { } + @Test + void isMoreSpecific() { + MediaType audio = new MediaType("audio"); + MediaType audioBasic = new MediaType("audio", "basic"); + MediaType audioBasic07 = new MediaType("audio", "basic", 0.7); + MediaType audioBasic03 = new MediaType("audio", "basic", 0.3); + + assertThat(audioBasic.isMoreSpecific(audio)).isTrue(); + assertThat(audio.isMoreSpecific(audioBasic)).isFalse(); + + assertThat(audio.isMoreSpecific(audioBasic07)).isTrue(); + assertThat(audioBasic07.isMoreSpecific(audio)).isFalse(); + + assertThat(audioBasic07.isMoreSpecific(audioBasic03)).isTrue(); + assertThat(audioBasic03.isMoreSpecific(audioBasic07)).isFalse(); + + assertThat(audioBasic.isMoreSpecific(MediaType.TEXT_HTML)).isFalse(); + } + + @Test + void isLessSpecific() { + MediaType audio = new MediaType("audio"); + MediaType audioBasic = new MediaType("audio", "basic"); + MediaType audioBasic07 = new MediaType("audio", "basic", 0.7); + MediaType audioBasic03 = new MediaType("audio", "basic", 0.3); + + assertThat(audioBasic.isLessSpecific(audio)).isFalse(); + assertThat(audio.isLessSpecific(audioBasic)).isTrue(); + + assertThat(audio.isLessSpecific(audioBasic07)).isFalse(); + assertThat(audioBasic07.isLessSpecific(audio)).isTrue(); + + assertThat(audioBasic07.isLessSpecific(audioBasic03)).isFalse(); + assertThat(audioBasic03.isLessSpecific(audioBasic07)).isTrue(); + + assertThat(audioBasic.isLessSpecific(MediaType.TEXT_HTML)).isFalse(); + } + @Test public void specificityComparator() throws Exception { MediaType audioBasic = new MediaType("audio", "basic"); @@ -470,4 +509,33 @@ void serialize() throws Exception { assertThat(original).isEqualTo(deserialized); } + @Test + public void sortBySpecificity() { + 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 expected = new ArrayList<>(); + expected.add(audioBasicLevel); + expected.add(audioBasic); + expected.add(audio); + expected.add(all); + expected.add(audio07); + expected.add(audio03); + + List result = new ArrayList<>(expected); + Random rnd = new Random(); + // shuffle & sort 10 times + for (int i = 0; i < 10; i++) { + Collections.shuffle(result, rnd); + MimeTypeUtils.sortBySpecificity(result); + + assertThat(result).containsExactlyElementsOf(expected); + + } + } + }