From 177b29226d6c1c8df127c8aedbf5e7a0d57d6f6c Mon Sep 17 00:00:00 2001 From: Arjen Poutsma Date: Tue, 19 Oct 2021 18:40:41 +0200 Subject: [PATCH] Refactor MimeType/MediaType specificity This commit makes several changes to MimeType and MediaType related to the topic of specificity. This commit deprecates the MimeType and MediaType Comparators. Comparators require a transitive relationship, and the desired order for these types is not transitive (see #27488). Instead, this commit introduces two new MimeType methods: isMoreSpecific and isLessSpecific, both of which return booleans. MediaType overrides these methods to include the quality factor (q) in the comparison. All MediaType sorting methods have been deprecated in favor of MimeTypeUtils::sortBySpecificity. This sorting method now uses MimeType::isLessSpecific in combination a bubble sort algorithm (which does not require a transitive compare function). Closes gh-27580 --- .../springframework/util/CollectionUtils.java | 33 +++++++ .../org/springframework/util/MimeType.java | 86 +++++++++++++++++- .../springframework/util/MimeTypeUtils.java | 39 ++++---- .../util/CollectionUtilsTests.java | 7 ++ .../springframework/util/MimeTypeTests.java | 56 ++++++++++++ .../org/springframework/http/MediaType.java | 90 ++++++++++++++++++- .../springframework/http/MediaTypeTests.java | 70 ++++++++++++++- 7 files changed, 350 insertions(+), 31 deletions(-) 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..5d5a0b7a19c4 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"); @@ -345,7 +384,7 @@ public void sortBySpecificityUnrelated() { } @Test - public void qualityComparator() throws Exception { + public void qualityComparator() { MediaType audioBasic = new MediaType("audio", "basic"); MediaType audioWave = new MediaType("audio", "wave"); MediaType audio = new MediaType("audio"); @@ -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); + + } + } + }