Skip to content

Commit

Permalink
Refactor MimeType/MediaType specificity
Browse files Browse the repository at this point in the history
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 spring-projects#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 spring-projectsgh-27580
  • Loading branch information
poutsma committed Oct 20, 2021
1 parent db4e98e commit 9b3e46d
Show file tree
Hide file tree
Showing 7 changed files with 351 additions and 31 deletions.
Expand Up @@ -20,6 +20,7 @@
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Iterator;
Expand All @@ -29,6 +30,7 @@
import java.util.Properties;
import java.util.Set;
import java.util.SortedSet;
import java.util.function.BiPredicate;

import org.springframework.lang.Nullable;

Expand Down Expand Up @@ -480,6 +482,38 @@ public static <K, V> MultiValueMap<K, V> unmodifiableMultiValueMap(
return toMultiValueMap(unmodifiableMap);
}

/**
* Sort the specified list with the (inefficient) bubble sort algorithm,
* using the specified swapping function.
*
* <p><b>Note:</b> for general purpose sorting,
* {@link Collections#sort(List, Comparator)} is far more efficient.
* However, bubble sort does not require a transitive comparison operation,
* whereas {@link Comparator#compare(Object, Object)} does.
* @param <T> 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, Comparator)
*/
public static <T> void bubbleSort(List<T> list, BiPredicate<? super T, ? super T> 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.
Expand Down
86 changes: 84 additions & 2 deletions spring-core/src/main/java/org/springframework/util/MimeType.java
Expand Up @@ -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;
Expand Down Expand Up @@ -529,7 +528,6 @@ private void appendTo(Map<String, String> 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) {
Expand Down Expand Up @@ -592,6 +590,88 @@ public int compareTo(MimeType other) {
return 0;
}

/**
* Indicates whether this {@code MimeType} is more specific than the given
* type.
* <ol>
* <li>if this mime type has a {@linkplain #isWildcardType() wildcard type},
* and the other does not, then this method returns {@code false}.</li>
* <li>if this mime type does not have a {@linkplain #isWildcardType() wildcard type},
* and the other does, then this method returns {@code true}.</li>
* <li>if this mime type has a {@linkplain #isWildcardType() wildcard type},
* and the other does not, then this method returns {@code false}.</li>
* <li>if this mime type does not have a {@linkplain #isWildcardType() wildcard type},
* and the other does, then this method returns {@code true}.</li>
* <li>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.</li>
* <li>Otherwise, this method returns {@code false}.</li>
* </ol>
* @param other the {@code MimeType} to be compared
* @return the result of the comparison
* @since 6.0
* @see #isLessSpecific(MimeType)
* @see <a href="https://tools.ietf.org/html/rfc7231#section-5.3.2">HTTP 1.1: Semantics
* and Content, section 5.3.2</a>
*/
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.
* <ol>
* <li>if this mime type has a {@linkplain #isWildcardType() wildcard type},
* and the other does not, then this method returns {@code true}.</li>
* <li>if this mime type does not have a {@linkplain #isWildcardType() wildcard type},
* and the other does, then this method returns {@code false}.</li>
* <li>if this mime type has a {@linkplain #isWildcardType() wildcard type},
* and the other does not, then this method returns {@code true}.</li>
* <li>if this mime type does not have a {@linkplain #isWildcardType() wildcard type},
* and the other does, then this method returns {@code false}.</li>
* <li>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.</li>
* <li>Otherwise, this method returns {@code false}.</li>
* </ol>
* @param other the {@code MimeType} to be compared
* @return the result of the comparison
* @since 6.0
* @see #isMoreSpecific(MimeType)
* @see <a href="https://tools.ietf.org/html/rfc7231#section-5.3.2">HTTP 1.1: Semantics
* and Content, section 5.3.2</a>
*/
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();
Expand Down Expand Up @@ -625,7 +705,9 @@ private static Map<String, String> addCharsetParameter(Charset charset, Map<Stri
* Comparator to sort {@link MimeType MimeTypes} in order of specificity.
*
* @param <T> 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<T extends MimeType> implements Comparator<T> {

@Override
Expand Down
Expand Up @@ -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<MimeType> SPECIFICITY_COMPARATOR = new MimeType.SpecificityComparator<>();

/**
Expand Down Expand Up @@ -334,34 +336,23 @@ public static String toString(Collection<? extends MimeType> mimeTypes) {
}

/**
* Sorts the given list of {@code MimeType} objects by specificity.
* <p>Given two mime types:
* <ol>
* <li>if either mime type has a {@linkplain MimeType#isWildcardType() wildcard type},
* then the mime type without the wildcard is ordered before the other.</li>
* <li>if the two mime types have different {@linkplain MimeType#getType() types},
* then they are considered equal and remain their current order.</li>
* <li>if either mime type has a {@linkplain MimeType#isWildcardSubtype() wildcard subtype}
* , then the mime type without the wildcard is sorted before the other.</li>
* <li>if the two mime types have different {@linkplain MimeType#getSubtype() subtypes},
* then they are considered equal and remain their current order.</li>
* <li>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.</li>
* </ol>
* <p>For example: <blockquote>audio/basic &lt; audio/* &lt; *&#047;*</blockquote>
* <blockquote>audio/basic;level=1 &lt; audio/basic</blockquote>
* <blockquote>audio/basic == text/html</blockquote> <blockquote>audio/basic ==
* audio/wave</blockquote>
* Sorts the given list of {@code MimeType} objects by
* {@linkplain MimeType#isMoreSpecific(MimeType) specificity}.
*
* <p>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 <a href="https://tools.ietf.org/html/rfc7231#section-5.3.2">HTTP 1.1: Semantics
* and Content, section 5.3.2</a>
* @see MimeType#isMoreSpecific(MimeType)
*/
public static void sortBySpecificity(List<MimeType> mimeTypes) {
public static <T extends MimeType> void sortBySpecificity(List<T> 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);
}


Expand Down
Expand Up @@ -209,6 +209,13 @@ void hasUniqueObject() {
assertThat(CollectionUtils.hasUniqueObject(list)).isFalse();
}

@Test
void bubbleSort() {
List<Integer> 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 {

Expand Down
Expand Up @@ -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<MimeType> 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"));
Expand Down

0 comments on commit 9b3e46d

Please sign in to comment.