Improve MimeType parser for RFC compliance

Prior to this commit, the `MimeType` class would compare raw parameter
values for the equals/hashcode contract. This went against the RFC which
states that quoted and unquoted parameter values are equivalent.

This commit rewrote the entire `MimeType` parser in `MimeTypeUtils`
as a state parser to improve robustness and performance.
The `MimeType` equals, compareTo and hascode contracts now unquote
parameter values before comparing them.

This change also optimizes the `tokenize` function that splits many
comma-separated MIME types into a list. Now that this method isn't used
anywhere else, it is also deprecated as of 7.1. This method was
initially made public to be reused within Spring Framework and has no
particular use in Spring applications in general.

Finally, this also makes `MediaType` and `MimeType` leverage the
`MimeType` LRU cache as much as possible, including when parsing
`Accept:` HTTP headers.

Closes gh-36729
This commit is contained in:
Brian Clozel
2026-07-24 14:29:36 +02:00
parent 9bdeadcfbd
commit 079992021c
4 changed files with 789 additions and 459 deletions
@@ -107,6 +107,8 @@ public class MimeType implements Comparable<MimeType>, Serializable {
private volatile @Nullable String toStringValue;
private volatile int hash;
/**
* Create a new {@code MimeType} for the given primary type.
@@ -207,6 +209,7 @@ public class MimeType implements Comparable<MimeType>, Serializable {
this.parameters = other.parameters;
this.resolvedCharset = other.resolvedCharset;
this.toStringValue = other.toStringValue;
this.hash = other.hash;
}
/**
@@ -254,8 +257,29 @@ public class MimeType implements Comparable<MimeType>, Serializable {
return ((s.startsWith("\"") && s.endsWith("\"")) || (s.startsWith("'") && s.endsWith("'")));
}
/**
* Unquote the given string, resolving any quoted-pair escapes it might
* contain (for example, {@code "1\"2"} becomes {@code 1"2}) in the process.
* <p>Returns the given string as-is if it is not a quoted string.
*/
protected String unquote(String s) {
return (isQuotedString(s) ? s.substring(1, s.length() - 1) : s);
if (!isQuotedString(s)) {
return s;
}
String inner = s.substring(1, s.length() - 1);
if (inner.indexOf('\\') == -1) {
return inner;
}
StringBuilder sb = new StringBuilder(inner.length());
for (int i = 0; i < inner.length(); i++) {
char c = inner.charAt(i);
if (c == '\\' && i + 1 < inner.length()) {
i++;
c = inner.charAt(i);
}
sb.append(c);
}
return sb.toString();
}
/**
@@ -462,7 +486,9 @@ public class MimeType implements Comparable<MimeType>, Serializable {
/**
* Determine if the parameters in this {@code MimeType} and the supplied
* {@code MimeType} are equal, performing case-insensitive comparisons
* for {@link Charset Charsets}.
* for {@link Charset Charsets} and disregarding quoting of parameter
* values, so that, for example, {@code spring="framework"} and
* {@code spring=framework} are considered equal.
* @since 4.2
*/
private boolean parametersAreEqual(MimeType other) {
@@ -480,7 +506,7 @@ public class MimeType implements Comparable<MimeType>, Serializable {
return false;
}
}
else if (!ObjectUtils.nullSafeEquals(entry.getValue(), other.parameters.get(key))) {
else if (!ObjectUtils.nullSafeEquals(unquote(entry.getValue()), unquote(other.parameters.get(key)))) {
return false;
}
}
@@ -490,9 +516,28 @@ public class MimeType implements Comparable<MimeType>, Serializable {
@Override
public int hashCode() {
int result = this.type.hashCode();
result = 31 * result + this.subtype.hashCode();
result = 31 * result + this.parameters.hashCode();
int result = this.hash;
if (result == 0) {
result = this.type.hashCode();
result = 31 * result + this.subtype.hashCode();
result = 31 * result + parametersHashCode();
this.hash = result;
}
return result;
}
/**
* Compute a hash code for the parameters map, consistent with
* {@link #parametersAreEqual}: normalizing {@link Charset Charsets} and
* disregarding quoting of parameter values.
*/
private int parametersHashCode() {
int result = 0;
for (Map.Entry<String, String> entry : this.parameters.entrySet()) {
String key = entry.getKey();
Object value = (PARAM_CHARSET.equals(key) ? getCharset() : unquote(entry.getValue()));
result += key.hashCode() ^ ObjectUtils.nullSafeHashCode(value);
}
return result;
}
@@ -580,7 +625,7 @@ public class MimeType implements Comparable<MimeType>, Serializable {
if (otherValue == null) {
otherValue = "";
}
comp = thisValue.compareTo(otherValue);
comp = unquote(thisValue).compareTo(unquote(otherValue));
if (comp != 0) {
return comp;
}
@@ -28,7 +28,6 @@ import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.function.BiPredicate;
import java.util.stream.Collectors;
import org.jspecify.annotations.Nullable;
@@ -204,72 +203,7 @@ public abstract class MimeTypeUtils {
}
private static MimeType parseMimeTypeInternal(String mimeType) {
int index = mimeType.indexOf(';');
String fullType = (index >= 0 ? mimeType.substring(0, index) : mimeType).trim();
if (fullType.isEmpty()) {
throw new InvalidMimeTypeException(mimeType, "'mimeType' must not be empty");
}
// java.net.HttpURLConnection returns a *; q=.2 Accept header
if (MimeType.WILDCARD_TYPE.equals(fullType)) {
fullType = "*/*";
}
int subIndex = fullType.indexOf('/');
if (subIndex == -1) {
throw new InvalidMimeTypeException(mimeType, "does not contain '/'");
}
if (subIndex == fullType.length() - 1) {
throw new InvalidMimeTypeException(mimeType, "does not contain subtype after '/'");
}
String type = fullType.substring(0, subIndex);
String subtype = fullType.substring(subIndex + 1);
if (MimeType.WILDCARD_TYPE.equals(type) && !MimeType.WILDCARD_TYPE.equals(subtype)) {
throw new InvalidMimeTypeException(mimeType, "wildcard type is legal only in '*/*' (all mime types)");
}
Map<String, String> parameters = null;
do {
int nextIndex = index + 1;
boolean quoted = false;
while (nextIndex < mimeType.length()) {
char ch = mimeType.charAt(nextIndex);
if (ch == ';') {
if (!quoted) {
break;
}
}
else if (ch == '"' && (nextIndex == 0 || mimeType.charAt(nextIndex - 1) != '\\')) {
quoted = !quoted;
}
nextIndex++;
}
String parameter = mimeType.substring(index + 1, nextIndex).trim();
if (parameter.length() > 0) {
if (parameters == null) {
parameters = new LinkedHashMap<>(4);
}
int eqIndex = parameter.indexOf('=');
if (eqIndex >= 0) {
String attribute = parameter.substring(0, eqIndex).trim();
String value = parameter.substring(eqIndex + 1).trim();
if (parameters.put(attribute, value) != null) {
throw new InvalidMimeTypeException(mimeType, "duplicate parameter '" + parameter + "'");
}
}
}
index = nextIndex;
}
while (index < mimeType.length());
try {
return new MimeType(type, subtype, parameters);
}
catch (UnsupportedCharsetException ex) {
throw new InvalidMimeTypeException(mimeType, "unsupported charset '" + ex.getCharsetName() + "'");
}
catch (IllegalArgumentException ex) {
throw new InvalidMimeTypeException(mimeType, ex.getMessage());
}
return new MimeTypeParser(mimeType).parse();
}
/**
@@ -278,14 +212,17 @@ public abstract class MimeTypeUtils {
* @return the list of mime types
* @throws InvalidMimeTypeException if the string cannot be parsed
*/
@SuppressWarnings("removal")
public static List<MimeType> parseMimeTypes(String mimeTypes) {
if (!StringUtils.hasLength(mimeTypes)) {
return Collections.emptyList();
}
return tokenize(mimeTypes).stream()
.filter(StringUtils::hasText)
.map(MimeTypeUtils::parseMimeType)
.collect(Collectors.toList());
List<String> tokens = tokenize(mimeTypes);
List<MimeType> result = new ArrayList<>(tokens.size());
for (String token : tokens) {
result.add(parseMimeType(token));
}
return result;
}
/**
@@ -295,11 +232,18 @@ public abstract class MimeTypeUtils {
* @param mimeTypes the string to tokenize
* @return the list of tokens
* @since 5.1.3
* @deprecated as of 7.1.0 without any replacement.
* Use {@link #parseMimeType(String)} and {@link #parseMimeTypes(String)} instead.
*/
@Deprecated(since = "7.1.0", forRemoval = true)
public static List<String> tokenize(String mimeTypes) {
if (!StringUtils.hasLength(mimeTypes)) {
return Collections.emptyList();
}
// Without quoted parameter values, there is no need to track quotes
if (mimeTypes.indexOf('"') == -1) {
return splitByComma(mimeTypes);
}
List<String> tokens = new ArrayList<>();
boolean inQuotes = false;
int startIndex = 0;
@@ -309,7 +253,10 @@ public abstract class MimeTypeUtils {
case '"' -> inQuotes = !inQuotes;
case ',' -> {
if (!inQuotes) {
tokens.add(mimeTypes.substring(startIndex, i));
String trimmed = mimeTypes.substring(startIndex, i).trim();
if (!trimmed.isEmpty()) {
tokens.add(trimmed);
}
startIndex = i + 1;
}
}
@@ -317,7 +264,28 @@ public abstract class MimeTypeUtils {
}
i++;
}
tokens.add(mimeTypes.substring(startIndex));
String trimmed = mimeTypes.substring(startIndex).trim();
if (!trimmed.isEmpty()) {
tokens.add(trimmed);
}
return tokens;
}
private static List<String> splitByComma(String mimeTypes) {
List<String> tokens = new ArrayList<>();
int startIndex = 0;
int commaIndex;
while ((commaIndex = mimeTypes.indexOf(',', startIndex)) != -1) {
String trimmed = mimeTypes.substring(startIndex, commaIndex).trim();
if (!trimmed.isEmpty()) {
tokens.add(trimmed);
}
startIndex = commaIndex + 1;
}
String trimmed = mimeTypes.substring(startIndex).trim();
if (!trimmed.isEmpty()) {
tokens.add(trimmed);
}
return tokens;
}
@@ -411,4 +379,263 @@ public abstract class MimeTypeUtils {
return new String(generateMultipartBoundary(), StandardCharsets.US_ASCII);
}
private static final class MimeTypeParser {
private final String input;
private int index = 0;
private int mark = 0;
private String type = "";
private String subtype = "";
private @Nullable Map<String, String> parameters;
private @Nullable String paramName;
private @Nullable MimeType parsed;
private MimeTypeParser(String input) {
this.input = input;
}
/**
* Parse the entire input as exactly one MIME type: any leftover
* content the state machine cannot make sense of (for example, a
* comma-separated second MIME type) is rejected rather than ignored.
*/
private MimeType parse() {
ParserState state = ParserState.INITIAL;
for (; this.index < this.input.length(); this.index++) {
char c = this.input.charAt(this.index);
state = state.process(c, this);
}
state.onEof(this);
if (this.parsed == null) {
throw new InvalidMimeTypeException(this.input, "'mimeType' must not be empty");
}
return this.parsed;
}
/**
* Resolve a type for which no '/' was found before a terminator (or
* the end of input) was reached. The only valid case is the bare
* {@code *} some clients (for example, {@code java.net.HttpURLConnection})
* send as shorthand for the {@code *; q=.2}-style wildcard
* (&#42;/&#42;) Accept header; anything else is rejected.
*/
private void resolveBareType(String candidate) {
if (!MimeType.WILDCARD_TYPE.equals(candidate)) {
throw new InvalidMimeTypeException(this.input, "does not contain '/'");
}
this.type = MimeType.WILDCARD_TYPE;
this.subtype = MimeType.WILDCARD_TYPE;
}
private void putParameter(String name, String value) {
if (this.parameters == null) {
this.parameters = new LinkedHashMap<>(4);
}
if (this.parameters.put(name, value) != null) {
throw new InvalidMimeTypeException(this.input, "duplicate parameter '" + name + "=" + value + "'");
}
}
private void emitMimeType() {
this.parsed = buildMimeType();
}
private MimeType buildMimeType() {
if (MimeType.WILDCARD_TYPE.equals(this.type) && !MimeType.WILDCARD_TYPE.equals(this.subtype)) {
throw new InvalidMimeTypeException(this.input, "wildcard type is legal only in '*/*' (all mime types)");
}
try {
return new MimeType(this.type, this.subtype, this.parameters);
}
catch (UnsupportedCharsetException ex) {
throw new InvalidMimeTypeException(this.input, "unsupported charset '" + ex.getCharsetName() + "'");
}
catch (IllegalArgumentException ex) {
throw new InvalidMimeTypeException(this.input, ex.getMessage());
}
}
}
enum ParserState {
INITIAL {
@Override
ParserState process(char c, MimeTypeParser parser) {
if (c == ' ' || c == '\t') {
return this;
}
parser.mark = parser.index;
return TYPE;
}
},
TYPE {
@Override
ParserState process(char c, MimeTypeParser parser) {
if (c == '/') {
parser.type = parser.input.substring(parser.mark, parser.index);
if (parser.type.isEmpty()) {
throw new InvalidMimeTypeException(parser.input, "'type' must not be empty");
}
parser.mark = parser.index + 1;
return SUBTYPE;
}
if (c == ';' || c == ' ' || c == '\t') {
parser.resolveBareType(parser.input.substring(parser.mark, parser.index));
return WHITESPACE;
}
return this;
}
@Override
void onEof(MimeTypeParser parser) {
parser.resolveBareType(parser.input.substring(parser.mark));
parser.emitMimeType();
}
},
SUBTYPE {
@Override
ParserState process(char c, MimeTypeParser parser) {
if (c == ';' || c == ' ' || c == '\t') {
parser.subtype = parser.input.substring(parser.mark, parser.index);
return WHITESPACE;
}
return this;
}
@Override
void onEof(MimeTypeParser parser) {
parser.subtype = parser.input.substring(parser.mark);
parser.emitMimeType();
}
},
WHITESPACE {
@Override
ParserState process(char c, MimeTypeParser parser) {
if (c == ' ' || c == '\t' || c == ';') {
return this;
}
parser.mark = parser.index;
return PARAM_NAME;
}
@Override
void onEof(MimeTypeParser parser) {
if (!parser.type.isEmpty()) {
parser.emitMimeType();
}
}
},
PARAM_NAME {
@Override
ParserState process(char c, MimeTypeParser parser) {
if (c == '=') {
parser.paramName = parser.input.substring(parser.mark, parser.index);
return PARAM_VALUE_START;
}
if (c == ' ' || c == '\t') {
parser.paramName = parser.input.substring(parser.mark, parser.index);
return PARAM_NAME_END;
}
return this;
}
},
PARAM_NAME_END {
@Override
ParserState process(char c, MimeTypeParser parser) {
if (c == ' ' || c == '\t') {
return this;
}
if (c == '=') {
return PARAM_VALUE_START;
}
throw new InvalidMimeTypeException(parser.input, "Unexpected character '" + c + "' after parameter name");
}
},
PARAM_VALUE_START {
@Override
ParserState process(char c, MimeTypeParser parser) {
if (c == ' ' || c == '\t') {
return this;
}
if (c == '"') {
parser.mark = parser.index;
return PARAM_VALUE_QUOTED;
}
parser.mark = parser.index;
return PARAM_VALUE_TOKEN;
}
},
PARAM_VALUE_TOKEN {
@Override
ParserState process(char c, MimeTypeParser parser) {
if (c == ';' || c == ' ' || c == '\t') {
extractParameter(parser, parser.input.substring(parser.mark, parser.index));
return WHITESPACE;
}
return this;
}
@Override
void onEof(MimeTypeParser parser) {
extractParameter(parser, parser.input.substring(parser.mark));
parser.emitMimeType();
}
},
PARAM_VALUE_QUOTED {
@Override
ParserState process(char c, MimeTypeParser parser) {
if (c == '"') {
extractParameter(parser, parser.input.substring(parser.mark, parser.index + 1));
return WHITESPACE;
}
if (c == '\\') {
return PARAM_VALUE_ESCAPED;
}
return this;
}
@Override
void onEof(MimeTypeParser parser) {
extractParameter(parser, parser.input.substring(parser.mark));
parser.emitMimeType();
}
},
PARAM_VALUE_ESCAPED {
@Override
ParserState process(char c, MimeTypeParser parser) {
return PARAM_VALUE_QUOTED;
}
@Override
void onEof(MimeTypeParser parser) {
extractParameter(parser, parser.input.substring(parser.mark));
parser.emitMimeType();
}
};
private static void extractParameter(MimeTypeParser parser, String input) {
Assert.hasText(parser.paramName, "'paramName' must not be empty");
parser.putParameter(parser.paramName, input);
}
abstract ParserState process(char c, MimeTypeParser parser);
void onEof(MimeTypeParser parser) {
// Only specific states need to flush here.
}
}
}
@@ -22,6 +22,7 @@ import java.util.Collections;
import java.util.List;
import java.util.Random;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.springframework.core.convert.ConversionService;
@@ -44,86 +45,415 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy;
*/
class MimeTypeTests {
@Test
void slashInSubtype() {
assertThatIllegalArgumentException().isThrownBy(() ->
new MimeType("text", "/"));
@Nested
class InvalidMimeTypeTests {
@Test
void parseMimeTypeNull() {
assertThatExceptionOfType(InvalidMimeTypeException.class).isThrownBy(() ->
MimeType.valueOf(null));
}
@Test
void valueOfSlashInSubtype() {
assertThatIllegalArgumentException().isThrownBy(() ->
new MimeType("text", "/"));
}
@Test
void valueOfNoSubtype() {
assertThatExceptionOfType(InvalidMimeTypeException.class).isThrownBy(() ->
MimeType.valueOf("audio"));
}
@Test
void valueOfNoSubtypeOnlySlash() {
assertThatExceptionOfType(InvalidMimeTypeException.class).isThrownBy(() ->
MimeType.valueOf("audio/"));
}
@Test
void valueOfIllegalType() {
assertThatExceptionOfType(InvalidMimeTypeException.class).isThrownBy(() ->
MimeType.valueOf("audio(/basic"));
}
@Test
void valueOfIllegalSubtype() {
assertThatExceptionOfType(InvalidMimeTypeException.class).isThrownBy(() ->
MimeType.valueOf("audio/basic)"));
}
@Test
void valueOfIllegalCharset() {
assertThatExceptionOfType(InvalidMimeTypeException.class).isThrownBy(() ->
MimeType.valueOf("text/html; charset=foo-bar"));
}
@Test
void parseMimeTypeNoSubtype() {
assertThatExceptionOfType(InvalidMimeTypeException.class).isThrownBy(() ->
MimeTypeUtils.parseMimeType("audio"));
}
@Test
void parseMimeTypeNoSubtypeSlash() {
assertThatExceptionOfType(InvalidMimeTypeException.class).isThrownBy(() ->
MimeTypeUtils.parseMimeType("audio/"));
}
@Test
void parseMimeTypeTypeRange() {
assertThatExceptionOfType(InvalidMimeTypeException.class).isThrownBy(() ->
MimeTypeUtils.parseMimeType("*/json"));
}
@Test // gh-36729
void parseMimeTypeRejectsSecondCommaSeparatedType() {
assertThatExceptionOfType(InvalidMimeTypeException.class).isThrownBy(() ->
MimeTypeUtils.parseMimeType("text/plain,text/html"));
}
@Test // gh-36729
void parseMimeTypeRejectsLeadingComma() {
assertThatExceptionOfType(InvalidMimeTypeException.class).isThrownBy(() ->
MimeTypeUtils.parseMimeType(",text/plain"));
}
@Test
void parseMimeTypeIllegalType() {
assertThatExceptionOfType(InvalidMimeTypeException.class).isThrownBy(() ->
MimeTypeUtils.parseMimeType("audio(/basic"));
}
@Test
void parseMimeTypeIllegalSubtype() {
assertThatExceptionOfType(InvalidMimeTypeException.class).isThrownBy(() ->
MimeTypeUtils.parseMimeType("audio/basic)"));
}
@Test
void parseMimeTypeMissingTypeAndSubtype() {
assertThatExceptionOfType(InvalidMimeTypeException.class).isThrownBy(() ->
MimeTypeUtils.parseMimeType(" ;a=b"));
}
@Test
void parseMimeTypeEmptyParameterAttribute() {
assertThatExceptionOfType(InvalidMimeTypeException.class).isThrownBy(() ->
MimeTypeUtils.parseMimeType("audio/*;=value"));
}
@Test
void parseMimeTypeEmptyParameterValue() {
assertThatExceptionOfType(InvalidMimeTypeException.class).isThrownBy(() ->
MimeTypeUtils.parseMimeType("audio/*;attr="));
}
@Test
void parseMimeTypeIllegalParameterAttribute() {
assertThatExceptionOfType(InvalidMimeTypeException.class).isThrownBy(() ->
MimeTypeUtils.parseMimeType("audio/*;attr<=value"));
}
@Test
void parseMimeTypeIllegalParameterValue() {
assertThatExceptionOfType(InvalidMimeTypeException.class).isThrownBy(() ->
MimeTypeUtils.parseMimeType("audio/*;attr=v>alue"));
}
@Test
void parseMimeTypeIllegalCharset() {
assertThatExceptionOfType(InvalidMimeTypeException.class).isThrownBy(() ->
MimeTypeUtils.parseMimeType("text/html; charset=foo-bar"));
}
@Test
void valueOfDuplicateParameter() {
assertThatThrownBy(() -> MimeType.valueOf("text/plain;dupe=\"1\";dupe=\"2\"")).isInstanceOf(InvalidMimeTypeException.class)
.hasMessageContaining("Invalid mime type \"text/plain;dupe=\"1\";dupe=\"2\"\": duplicate parameter 'dupe=\"2\"'");
}
}
@Test
void valueOfNoSubtype() {
assertThatExceptionOfType(InvalidMimeTypeException.class).isThrownBy(() ->
MimeType.valueOf("audio"));
@Nested
class QuotePairsTests {
@Test
void parseQuotedCharset() {
MimeType mimeType = MimeType.valueOf("application/xml;charset=\"utf-8\"");
assertThat(mimeType.getType()).as("Invalid type").isEqualTo("application");
assertThat(mimeType.getSubtype()).as("Invalid subtype").isEqualTo("xml");
assertThat(mimeType.getCharset()).as("Invalid charset").isEqualTo(StandardCharsets.UTF_8);
}
@Test
void parseQuotedParameterValue() {
MimeType mimeType = MimeType.valueOf("application/xop+xml;charset=utf-8;type=\"application/soap+xml;action=\\\"https://x.y.z\\\"\"");
assertThat(mimeType.getType()).as("Invalid type").isEqualTo("application");
assertThat(mimeType.getSubtype()).as("Invalid subtype").isEqualTo("xop+xml");
assertThat(mimeType.getCharset()).as("Invalid charset").isEqualTo(StandardCharsets.UTF_8);
assertThat(mimeType.getParameter("type")).isEqualTo("\"application/soap+xml;action=\\\"https://x.y.z\\\"\"");
}
@Test // gh-36730
void parseParameterWithQuotedPair() {
MimeType mimeType = MimeType.valueOf("text/plain;twelve=\"1\\\"2\"");
assertThat(mimeType.getType()).as("Invalid type").isEqualTo("text");
assertThat(mimeType.getSubtype()).as("Invalid subtype").isEqualTo("plain");
assertThat(mimeType.getParameter("twelve")).isEqualTo("\"1\\\"2\"");
}
@Test // SPR-8917
void parseMimeTypeQuotedParameterValue() {
MimeType mimeType = MimeTypeUtils.parseMimeType("audio/*;attr=\"v>alue\"");
assertThat(mimeType.getParameter("attr")).isEqualTo("\"v>alue\"");
}
@Test // SPR-8917
void parseMimeTypeSingleQuotedParameterValue() {
MimeType mimeType = MimeTypeUtils.parseMimeType("audio/*;attr='v>alue'");
assertThat(mimeType.getParameter("attr")).isEqualTo("'v>alue'");
}
@Test // SPR-16630
void parseMimeTypeWithSpacesAroundEqualsAndQuotedValue() {
MimeType mimeType = MimeTypeUtils.parseMimeType("text/plain; foo = \" bar \" ");
assertThat(mimeType.getParameter("foo")).isEqualTo("\" bar \"");
}
@Test
void parseMimeTypeIllegalQuotedParameterValue() {
assertThatExceptionOfType(InvalidMimeTypeException.class).isThrownBy(() ->
MimeTypeUtils.parseMimeType("audio/*;attr=\""));
}
@Test // gh-36971
void parseMimeTypeWrappedInQuotes() {
assertThatExceptionOfType(InvalidMimeTypeException.class)
.isThrownBy(() -> MimeTypeUtils.parseMimeType("\"application/xml\""))
.withMessageContaining("Invalid token character '\"'");
}
@Test // SPR-17459
void parseMimeTypesWithQuotedParameters() {
testWithQuotedParameters("foo/bar;param=\",\"");
testWithQuotedParameters("foo/bar;param=\"s,a,\"");
testWithQuotedParameters("foo/bar;param=\"s,\"", "text/x-c");
testWithQuotedParameters("foo/bar;param=\"a\\\"b,c\"");
testWithQuotedParameters("foo/bar;param=\"\\\\\"");
testWithQuotedParameters("foo/bar;param=\"\\,\\\"");
}
private void testWithQuotedParameters(String... mimeTypes) {
String s = String.join(",", mimeTypes);
List<MimeType> actual = MimeTypeUtils.parseMimeTypes(s);
assertThat(actual).hasSameSizeAs(mimeTypes);
for (int i = 0; i < mimeTypes.length; i++) {
assertThat(actual.get(i).toString()).isEqualTo(mimeTypes[i]);
}
}
}
@Test
void valueOfNoSubtypeSlash() {
assertThatExceptionOfType(InvalidMimeTypeException.class).isThrownBy(() ->
MimeType.valueOf("audio/"));
}
@Nested
class SpecificityTests {
@Test
void valueOfIllegalType() {
assertThatExceptionOfType(InvalidMimeTypeException.class).isThrownBy(() ->
MimeType.valueOf("audio(/basic"));
}
@Test
void includes() {
MimeType textPlain = MimeTypeUtils.TEXT_PLAIN;
assertThat(textPlain.includes(textPlain)).as("Equal types is not inclusive").isTrue();
MimeType allText = new MimeType("text");
@Test
void valueOfIllegalSubtype() {
assertThatExceptionOfType(InvalidMimeTypeException.class).isThrownBy(() ->
MimeType.valueOf("audio/basic)"));
}
assertThat(allText.includes(textPlain)).as("All subtypes is not inclusive").isTrue();
assertThat(textPlain.includes(allText)).as("All subtypes is inclusive").isFalse();
assertThat(MimeTypeUtils.ALL.includes(textPlain)).as("All types is not inclusive").isTrue();
assertThat(textPlain.includes(MimeTypeUtils.ALL)).as("All types is inclusive").isFalse();
assertThat(MimeTypeUtils.ALL.includes(textPlain)).as("All types is not inclusive").isTrue();
assertThat(textPlain.includes(MimeTypeUtils.ALL)).as("All types is inclusive").isFalse();
MimeType applicationSoapXml = new MimeType("application", "soap+xml");
MimeType applicationWildcardXml = new MimeType("application", "*+xml");
MimeType suffixXml = new MimeType("application", "x.y+z+xml"); // SPR-15795
assertThat(applicationSoapXml.includes(applicationSoapXml)).isTrue();
assertThat(applicationWildcardXml.includes(applicationWildcardXml)).isTrue();
assertThat(applicationWildcardXml.includes(suffixXml)).isTrue();
assertThat(applicationWildcardXml.includes(applicationSoapXml)).isTrue();
assertThat(applicationSoapXml.includes(applicationWildcardXml)).isFalse();
assertThat(suffixXml.includes(applicationWildcardXml)).isFalse();
assertThat(applicationWildcardXml.includes(MimeTypeUtils.APPLICATION_JSON)).isFalse();
}
@Test
void isCompatible() {
MimeType textPlain = MimeTypeUtils.TEXT_PLAIN;
assertThat(textPlain.isCompatibleWith(textPlain)).as("Equal types is not compatible").isTrue();
MimeType allText = new MimeType("text");
assertThat(allText.isCompatibleWith(textPlain)).as("All subtypes is not compatible").isTrue();
assertThat(textPlain.isCompatibleWith(allText)).as("All subtypes is not compatible").isTrue();
assertThat(MimeTypeUtils.ALL.isCompatibleWith(textPlain)).as("All types is not compatible").isTrue();
assertThat(textPlain.isCompatibleWith(MimeTypeUtils.ALL)).as("All types is not compatible").isTrue();
assertThat(MimeTypeUtils.ALL.isCompatibleWith(textPlain)).as("All types is not compatible").isTrue();
assertThat(textPlain.isCompatibleWith(MimeTypeUtils.ALL)).as("All types is compatible").isTrue();
MimeType applicationSoapXml = new MimeType("application", "soap+xml");
MimeType applicationWildcardXml = new MimeType("application", "*+xml");
MimeType suffixXml = new MimeType("application", "x.y+z+xml"); // SPR-15795
assertThat(applicationSoapXml.isCompatibleWith(applicationSoapXml)).isTrue();
assertThat(applicationWildcardXml.isCompatibleWith(applicationWildcardXml)).isTrue();
assertThat(applicationWildcardXml.isCompatibleWith(suffixXml)).isTrue();
assertThat(applicationWildcardXml.isCompatibleWith(applicationSoapXml)).isTrue();
assertThat(applicationSoapXml.isCompatibleWith(applicationWildcardXml)).isTrue();
assertThat(suffixXml.isCompatibleWith(applicationWildcardXml)).isTrue();
assertThat(applicationWildcardXml.isCompatibleWith(MimeTypeUtils.APPLICATION_JSON)).isFalse();
}
@Test
void compareTo() {
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"));
// equal
assertThat(audioBasic.compareTo(audioBasic)).as("Invalid comparison result").isEqualTo(0);
assertThat(audio.compareTo(audio)).as("Invalid comparison result").isEqualTo(0);
assertThat(audioBasicLevel.compareTo(audioBasicLevel)).as("Invalid comparison result").isEqualTo(0);
assertThat(audioBasicLevel.compareTo(audio)).as("Invalid comparison result").isGreaterThan(0);
List<MimeType> expected = new ArrayList<>();
expected.add(audio);
expected.add(audioBasic);
expected.add(audioBasicLevel);
expected.add(audioWave);
List<MimeType> result = new ArrayList<>(expected);
Random rnd = new Random();
// shuffle & sort 10 times
for (int i = 0; i < 10; i++) {
Collections.shuffle(result, rnd);
Collections.sort(result);
for (int j = 0; j < result.size(); j++) {
assertThat(result.get(j)).as("Invalid media type at " + j + ", run " + i).isSameAs(expected.get(j));
}
}
}
@Test
void compareToCaseSensitivity() {
MimeType m1 = new MimeType("audio", "basic");
MimeType m2 = new MimeType("Audio", "Basic");
assertThat(m1.compareTo(m2)).as("Invalid comparison result").isEqualTo(0);
assertThat(m2.compareTo(m1)).as("Invalid comparison result").isEqualTo(0);
m1 = new MimeType("audio", "basic", singletonMap("foo", "bar"));
m2 = new MimeType("audio", "basic", singletonMap("Foo", "bar"));
assertThat(m1.compareTo(m2)).as("Invalid comparison result").isEqualTo(0);
assertThat(m2.compareTo(m1)).as("Invalid comparison result").isEqualTo(0);
m1 = new MimeType("audio", "basic", singletonMap("foo", "bar"));
m2 = new MimeType("audio", "basic", singletonMap("foo", "Bar"));
assertThat(m1.compareTo(m2)).as("Invalid comparison result").isNotEqualTo(0);
assertThat(m2.compareTo(m1)).as("Invalid comparison result").isNotEqualTo(0);
}
@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
void bubbleSort() {
List<Integer> list = new ArrayList<>(List.of(10, 9, 8, 7, 6, 5, 4, 3, 2, 1));
MimeTypeUtils.bubbleSort(list, (i1, i2) -> i1 > i2);
assertThat(list).containsExactly(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
}
@Test // gh-25350
void wildcardSubtypeCompatibleWithSuffix() {
MimeType applicationStar = new MimeType("application", "*");
MimeType applicationVndJson = new MimeType("application", "vnd.something+json");
assertThat(applicationStar.isCompatibleWith(applicationVndJson)).isTrue();
}
@Test
void valueOfIllegalCharset() {
assertThatExceptionOfType(InvalidMimeTypeException.class).isThrownBy(() ->
MimeType.valueOf("text/html; charset=foo-bar"));
}
@Test
void parseCharset() {
String s = "text/html; charset=iso-8859-1";
MimeType mimeType = MimeType.valueOf(s);
MimeType mimeType = MimeType.valueOf("text/html; charset=iso-8859-1");
assertThat(mimeType.getType()).as("Invalid type").isEqualTo("text");
assertThat(mimeType.getSubtype()).as("Invalid subtype").isEqualTo("html");
assertThat(mimeType.getCharset()).as("Invalid charset").isEqualTo(StandardCharsets.ISO_8859_1);
}
@Test
void parseQuotedCharset() {
String s = "application/xml;charset=\"utf-8\"";
MimeType mimeType = MimeType.valueOf(s);
assertThat(mimeType.getType()).as("Invalid type").isEqualTo("application");
assertThat(mimeType.getSubtype()).as("Invalid subtype").isEqualTo("xml");
assertThat(mimeType.getCharset()).as("Invalid charset").isEqualTo(StandardCharsets.UTF_8);
}
@Test
void parseQuotedParameterValue() {
String s = "application/xop+xml;charset=utf-8;type=\"application/soap+xml;action=\\\"https://x.y.z\\\"\"";
MimeType mimeType = MimeType.valueOf(s);
assertThat(mimeType.getType()).as("Invalid type").isEqualTo("application");
assertThat(mimeType.getSubtype()).as("Invalid subtype").isEqualTo("xop+xml");
assertThat(mimeType.getCharset()).as("Invalid charset").isEqualTo(StandardCharsets.UTF_8);
assertThat(mimeType.getParameter("type")).isEqualTo("\"application/soap+xml;action=\\\"https://x.y.z\\\"\"");
}
@Test // gh-36730
void parseParameterWithQuotedPair() {
String s = "text/plain;twelve=\"1\\\"2\"";
MimeType mimeType = MimeType.valueOf(s);
assertThat(mimeType.getType()).as("Invalid type").isEqualTo("text");
assertThat(mimeType.getSubtype()).as("Invalid subtype").isEqualTo("plain");
assertThat(mimeType.getParameter("twelve")).isEqualTo("\"1\\\"2\"");
}
@Test
void rejectsDuplicateParameter() {
String s = "text/plain;dupe=\"1\";dupe=\"2\"";
assertThatThrownBy(() -> MimeType.valueOf(s)).isInstanceOf(InvalidMimeTypeException.class)
.hasMessageContaining("Invalid mime type \"text/plain;dupe=\"1\";dupe=\"2\"\": duplicate parameter 'dupe=\"2\"'");
}
@Test
void withConversionService() {
ConversionService conversionService = new DefaultConversionService();
@@ -132,66 +462,6 @@ class MimeTypeTests {
assertThat(conversionService.convert("application/xml", MimeType.class)).isEqualTo(mimeType);
}
@Test
void includes() {
MimeType textPlain = MimeTypeUtils.TEXT_PLAIN;
assertThat(textPlain.includes(textPlain)).as("Equal types is not inclusive").isTrue();
MimeType allText = new MimeType("text");
assertThat(allText.includes(textPlain)).as("All subtypes is not inclusive").isTrue();
assertThat(textPlain.includes(allText)).as("All subtypes is inclusive").isFalse();
assertThat(MimeTypeUtils.ALL.includes(textPlain)).as("All types is not inclusive").isTrue();
assertThat(textPlain.includes(MimeTypeUtils.ALL)).as("All types is inclusive").isFalse();
assertThat(MimeTypeUtils.ALL.includes(textPlain)).as("All types is not inclusive").isTrue();
assertThat(textPlain.includes(MimeTypeUtils.ALL)).as("All types is inclusive").isFalse();
MimeType applicationSoapXml = new MimeType("application", "soap+xml");
MimeType applicationWildcardXml = new MimeType("application", "*+xml");
MimeType suffixXml = new MimeType("application", "x.y+z+xml"); // SPR-15795
assertThat(applicationSoapXml.includes(applicationSoapXml)).isTrue();
assertThat(applicationWildcardXml.includes(applicationWildcardXml)).isTrue();
assertThat(applicationWildcardXml.includes(suffixXml)).isTrue();
assertThat(applicationWildcardXml.includes(applicationSoapXml)).isTrue();
assertThat(applicationSoapXml.includes(applicationWildcardXml)).isFalse();
assertThat(suffixXml.includes(applicationWildcardXml)).isFalse();
assertThat(applicationWildcardXml.includes(MimeTypeUtils.APPLICATION_JSON)).isFalse();
}
@Test
void isCompatible() {
MimeType textPlain = MimeTypeUtils.TEXT_PLAIN;
assertThat(textPlain.isCompatibleWith(textPlain)).as("Equal types is not compatible").isTrue();
MimeType allText = new MimeType("text");
assertThat(allText.isCompatibleWith(textPlain)).as("All subtypes is not compatible").isTrue();
assertThat(textPlain.isCompatibleWith(allText)).as("All subtypes is not compatible").isTrue();
assertThat(MimeTypeUtils.ALL.isCompatibleWith(textPlain)).as("All types is not compatible").isTrue();
assertThat(textPlain.isCompatibleWith(MimeTypeUtils.ALL)).as("All types is not compatible").isTrue();
assertThat(MimeTypeUtils.ALL.isCompatibleWith(textPlain)).as("All types is not compatible").isTrue();
assertThat(textPlain.isCompatibleWith(MimeTypeUtils.ALL)).as("All types is compatible").isTrue();
MimeType applicationSoapXml = new MimeType("application", "soap+xml");
MimeType applicationWildcardXml = new MimeType("application", "*+xml");
MimeType suffixXml = new MimeType("application", "x.y+z+xml"); // SPR-15795
assertThat(applicationSoapXml.isCompatibleWith(applicationSoapXml)).isTrue();
assertThat(applicationWildcardXml.isCompatibleWith(applicationWildcardXml)).isTrue();
assertThat(applicationWildcardXml.isCompatibleWith(suffixXml)).isTrue();
assertThat(applicationWildcardXml.isCompatibleWith(applicationSoapXml)).isTrue();
assertThat(applicationSoapXml.isCompatibleWith(applicationWildcardXml)).isTrue();
assertThat(suffixXml.isCompatibleWith(applicationWildcardXml)).isTrue();
assertThat(applicationWildcardXml.isCompatibleWith(MimeTypeUtils.APPLICATION_JSON)).isFalse();
}
@Test
void toStringOutput() {
MimeType mimeType = new MimeType("text", "plain");
@@ -200,122 +470,34 @@ class MimeTypeTests {
}
@Test
void parseMimeType() {
String s = "audio/*";
MimeType mimeType = MimeTypeUtils.parseMimeType(s);
void parseWildcardSubtype() {
MimeType mimeType = MimeTypeUtils.parseMimeType("audio/*");
assertThat(mimeType.getType()).as("Invalid type").isEqualTo("audio");
assertThat(mimeType.getSubtype()).as("Invalid subtype").isEqualTo("*");
}
@Test
void parseMimeTypeNoSubtype() {
assertThatExceptionOfType(InvalidMimeTypeException.class).isThrownBy(() ->
MimeTypeUtils.parseMimeType("audio"));
@Test // gh-36729
void parseBareWildcardShorthand() {
MimeType mimeType = MimeTypeUtils.parseMimeType("*");
assertThat(mimeType.getType()).as("Invalid type").isEqualTo("*");
assertThat(mimeType.getSubtype()).as("Invalid subtype").isEqualTo("*");
}
@Test
void parseMimeTypeNoSubtypeSlash() {
assertThatExceptionOfType(InvalidMimeTypeException.class).isThrownBy(() ->
MimeTypeUtils.parseMimeType("audio/"));
@Test // gh-36729
void parseBareWildcardShorthandWithParameter() {
// java.net.HttpURLConnection returns a "*; q=.2" Accept header
MimeType mimeType = MimeTypeUtils.parseMimeType("*; q=.2");
assertThat(mimeType.getType()).as("Invalid type").isEqualTo("*");
assertThat(mimeType.getSubtype()).as("Invalid subtype").isEqualTo("*");
assertThat(mimeType.getParameter("q")).isEqualTo(".2");
}
@Test
void parseMimeTypeTypeRange() {
assertThatExceptionOfType(InvalidMimeTypeException.class).isThrownBy(() ->
MimeTypeUtils.parseMimeType("*/json"));
}
@Test
void parseMimeTypeIllegalType() {
assertThatExceptionOfType(InvalidMimeTypeException.class).isThrownBy(() ->
MimeTypeUtils.parseMimeType("audio(/basic"));
}
@Test
void parseMimeTypeIllegalSubtype() {
assertThatExceptionOfType(InvalidMimeTypeException.class).isThrownBy(() ->
MimeTypeUtils.parseMimeType("audio/basic)"));
}
@Test
void parseMimeTypeMissingTypeAndSubtype() {
assertThatExceptionOfType(InvalidMimeTypeException.class).isThrownBy(() ->
MimeTypeUtils.parseMimeType(" ;a=b"));
}
@Test
void parseMimeTypeEmptyParameterAttribute() {
assertThatExceptionOfType(InvalidMimeTypeException.class).isThrownBy(() ->
MimeTypeUtils.parseMimeType("audio/*;=value"));
}
@Test
void parseMimeTypeEmptyParameterValue() {
assertThatExceptionOfType(InvalidMimeTypeException.class).isThrownBy(() ->
MimeTypeUtils.parseMimeType("audio/*;attr="));
}
@Test
void parseMimeTypeIllegalParameterAttribute() {
assertThatExceptionOfType(InvalidMimeTypeException.class).isThrownBy(() ->
MimeTypeUtils.parseMimeType("audio/*;attr<=value"));
}
@Test
void parseMimeTypeIllegalParameterValue() {
assertThatExceptionOfType(InvalidMimeTypeException.class).isThrownBy(() ->
MimeTypeUtils.parseMimeType("audio/*;attr=v>alue"));
}
@Test
void parseMimeTypeIllegalCharset() {
assertThatExceptionOfType(InvalidMimeTypeException.class).isThrownBy(() ->
MimeTypeUtils.parseMimeType("text/html; charset=foo-bar"));
}
@Test // SPR-8917
void parseMimeTypeQuotedParameterValue() {
MimeType mimeType = MimeTypeUtils.parseMimeType("audio/*;attr=\"v>alue\"");
assertThat(mimeType.getParameter("attr")).isEqualTo("\"v>alue\"");
}
@Test // SPR-8917
void parseMimeTypeSingleQuotedParameterValue() {
MimeType mimeType = MimeTypeUtils.parseMimeType("audio/*;attr='v>alue'");
assertThat(mimeType.getParameter("attr")).isEqualTo("'v>alue'");
}
@Test // SPR-16630
@Test // SPR-16630
void parseMimeTypeWithSpacesAroundEquals() {
MimeType mimeType = MimeTypeUtils.parseMimeType("multipart/x-mixed-replace;boundary = --myboundary");
assertThat(mimeType.getParameter("boundary")).isEqualTo("--myboundary");
}
@Test // SPR-16630
void parseMimeTypeWithSpacesAroundEqualsAndQuotedValue() {
MimeType mimeType = MimeTypeUtils.parseMimeType("text/plain; foo = \" bar \" ");
assertThat(mimeType.getParameter("foo")).isEqualTo("\" bar \"");
}
@Test
void parseMimeTypeIllegalQuotedParameterValue() {
assertThatExceptionOfType(InvalidMimeTypeException.class).isThrownBy(() ->
MimeTypeUtils.parseMimeType("audio/*;attr=\""));
}
@Test // gh-36971
void parseMimeTypeWrappedInQuotes() {
assertThatExceptionOfType(InvalidMimeTypeException.class)
.isThrownBy(() -> MimeTypeUtils.parseMimeType("\"application/xml\""))
.withMessageContaining("Invalid token character '\"'");
}
@Test
void parseMimeTypeNull() {
assertThatExceptionOfType(InvalidMimeTypeException.class).isThrownBy(() ->
MimeTypeUtils.parseMimeType(null));
}
@Test
void parseMimeTypes() {
String s = "text/plain, text/html, text/x-dvi, text/x-c";
@@ -328,23 +510,13 @@ class MimeTypeTests {
assertThat(mimeTypes).as("Invalid amount of mime types").isEmpty();
}
@Test // gh-23241
@Test // gh-23241
void parseMimeTypesWithTrailingComma() {
List<MimeType> mimeTypes = MimeTypeUtils.parseMimeTypes("text/plain, text/html,");
assertThat(mimeTypes).as("No mime types returned").isNotNull();
assertThat(mimeTypes).as("Incorrect number of mime types").hasSize(2);
}
@Test // SPR-17459
void parseMimeTypesWithQuotedParameters() {
testWithQuotedParameters("foo/bar;param=\",\"");
testWithQuotedParameters("foo/bar;param=\"s,a,\"");
testWithQuotedParameters("foo/bar;param=\"s,\"", "text/x-c");
testWithQuotedParameters("foo/bar;param=\"a\\\"b,c\"");
testWithQuotedParameters("foo/bar;param=\"\\\\\"");
testWithQuotedParameters("foo/bar;param=\"\\,\\\"");
}
@Test
void parseSubtypeSuffix() {
MimeType type = new MimeType("application", "vdn.something+json");
@@ -357,140 +529,7 @@ class MimeTypeTests {
assertThat(type.getSubtypeSuffix()).isEqualTo("json");
}
@Test // gh-25350
void wildcardSubtypeCompatibleWithSuffix() {
MimeType applicationStar = new MimeType("application", "*");
MimeType applicationVndJson = new MimeType("application", "vnd.something+json");
assertThat(applicationStar.isCompatibleWith(applicationVndJson)).isTrue();
}
private void testWithQuotedParameters(String... mimeTypes) {
String s = String.join(",", mimeTypes);
List<MimeType> actual = MimeTypeUtils.parseMimeTypes(s);
assertThat(actual).hasSameSizeAs(mimeTypes);
for (int i = 0; i < mimeTypes.length; i++) {
assertThat(actual.get(i).toString()).isEqualTo(mimeTypes[i]);
}
}
@Test
void compareTo() {
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"));
// equal
assertThat(audioBasic.compareTo(audioBasic)).as("Invalid comparison result").isEqualTo(0);
assertThat(audio.compareTo(audio)).as("Invalid comparison result").isEqualTo(0);
assertThat(audioBasicLevel.compareTo(audioBasicLevel)).as("Invalid comparison result").isEqualTo(0);
assertThat(audioBasicLevel.compareTo(audio)).as("Invalid comparison result").isGreaterThan(0);
List<MimeType> expected = new ArrayList<>();
expected.add(audio);
expected.add(audioBasic);
expected.add(audioBasicLevel);
expected.add(audioWave);
List<MimeType> result = new ArrayList<>(expected);
Random rnd = new Random();
// shuffle & sort 10 times
for (int i = 0; i < 10; i++) {
Collections.shuffle(result, rnd);
Collections.sort(result);
for (int j = 0; j < result.size(); j++) {
assertThat(result.get(j)).as("Invalid media type at " + j + ", run " + i).isSameAs(expected.get(j));
}
}
}
@Test
void compareToCaseSensitivity() {
MimeType m1 = new MimeType("audio", "basic");
MimeType m2 = new MimeType("Audio", "Basic");
assertThat(m1.compareTo(m2)).as("Invalid comparison result").isEqualTo(0);
assertThat(m2.compareTo(m1)).as("Invalid comparison result").isEqualTo(0);
m1 = new MimeType("audio", "basic", singletonMap("foo", "bar"));
m2 = new MimeType("audio", "basic", singletonMap("Foo", "bar"));
assertThat(m1.compareTo(m2)).as("Invalid comparison result").isEqualTo(0);
assertThat(m2.compareTo(m1)).as("Invalid comparison result").isEqualTo(0);
m1 = new MimeType("audio", "basic", singletonMap("foo", "bar"));
m2 = new MimeType("audio", "basic", singletonMap("foo", "Bar"));
assertThat(m1.compareTo(m2)).as("Invalid comparison result").isNotEqualTo(0);
assertThat(m2.compareTo(m1)).as("Invalid comparison result").isNotEqualTo(0);
}
@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
void bubbleSort() {
List<Integer> list = new ArrayList<>(List.of(10, 9, 8, 7, 6, 5, 4, 3, 2, 1));
MimeTypeUtils.bubbleSort(list, (i1, i2) -> i1 > i2);
assertThat(list).containsExactly(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
}
@Test // SPR-13157
@Test // SPR-13157
void equalsIsCaseInsensitiveForCharsets() {
MimeType m1 = new MimeType("text", "plain", singletonMap("charset", "UTF-8"));
MimeType m2 = new MimeType("text", "plain", singletonMap("charset", "utf-8"));
@@ -500,7 +539,18 @@ class MimeTypeTests {
assertThat(m2.compareTo(m1)).isEqualTo(0);
}
@Test // gh-26127
@Test // gh-36729
void equalsIgnoresParameterValueQuoting() {
MimeType m1 = MimeTypeUtils.parseMimeType("text/plain; spring=\"framework\"");
MimeType m2 = MimeTypeUtils.parseMimeType("text/plain; spring=framework");
assertThat(m1).isEqualTo(m2);
assertThat(m2).isEqualTo(m1);
assertThat(m1.hashCode()).isEqualTo(m2.hashCode());
assertThat(m1.compareTo(m2)).isEqualTo(0);
assertThat(m2.compareTo(m1)).isEqualTo(0);
}
@Test // gh-26127
void serialize() throws Exception {
MimeType original = new MimeType("text", "plain", StandardCharsets.UTF_8);
MimeType deserialized = SerializationTestUtils.serializeAndDeserialize(original);
@@ -701,12 +701,20 @@ public class MediaType extends MimeType implements Serializable {
if (!StringUtils.hasLength(mediaTypes)) {
return Collections.emptyList();
}
// Avoid using java.util.stream.Stream in hot paths
List<String> tokenizedTypes = MimeTypeUtils.tokenize(mediaTypes);
List<MediaType> result = new ArrayList<>(tokenizedTypes.size());
for (String type : tokenizedTypes) {
if (StringUtils.hasText(type)) {
result.add(parseMediaType(type));
List<MimeType> mimeTypes;
try {
mimeTypes = MimeTypeUtils.parseMimeTypes(mediaTypes);
}
catch (InvalidMimeTypeException ex) {
throw new InvalidMediaTypeException(ex);
}
List<MediaType> result = new ArrayList<>(mimeTypes.size());
for (MimeType mimeType : mimeTypes) {
try {
result.add(new MediaType(mimeType));
}
catch (IllegalArgumentException ex) {
throw new InvalidMediaTypeException(mimeType.toString(), ex.getMessage());
}
}
return result;