Skip to content

Commit f0e23b6

Browse files
committed
The "consumes" condition compares MediaType parameters
Closes gh-9257
1 parent 4b0531b commit f0e23b6

File tree

11 files changed

+112
-60
lines changed

11 files changed

+112
-60
lines changed

spring-web/src/main/java/org/springframework/web/bind/annotation/RequestMapping.java

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2020 the original author or authors.
2+
* Copyright 2002-2022 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -169,7 +169,12 @@
169169
* consumes = {"text/plain", "application/*"}
170170
* consumes = MediaType.TEXT_PLAIN_VALUE
171171
* </pre>
172-
* Expressions can be negated by using the "!" operator, as in
172+
* <p>If a declared media type contains a parameter, and if the
173+
* {@code "content-type"} from the request also has that parameter, then
174+
* the parameter values must match. Otherwise, if the media type from the
175+
* request {@code "content-type"} does not contain the parameter, then the
176+
* parameter is ignored for matching purposes.
177+
* <p>Expressions can be negated by using the "!" operator, as in
173178
* "!text/plain", which matches all requests with a {@code Content-Type}
174179
* other than "text/plain".
175180
* <p><b>Supported at the type level as well as at the method level!</b>
@@ -194,7 +199,7 @@
194199
* </pre>
195200
* <p>If a declared media type contains a parameter (e.g. "charset=UTF-8",
196201
* "type=feed", "type=entry") and if a compatible media type from the request
197-
* has that parameter too, then the parameter values must match. Otherwise
202+
* has that parameter too, then the parameter values must match. Otherwise,
198203
* if the media type from the request does not contain the parameter, it is
199204
* assumed the client accepts any value.
200205
* <p>Expressions can be negated by using the "!" operator, as in "!text/plain",

spring-webflux/src/main/java/org/springframework/web/reactive/result/condition/AbstractMediaTypeExpression.java

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2020 the original author or authors.
2+
* Copyright 2002-2022 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -16,8 +16,11 @@
1616

1717
package org.springframework.web.reactive.result.condition;
1818

19+
import java.util.Map;
20+
1921
import org.springframework.http.MediaType;
2022
import org.springframework.lang.Nullable;
23+
import org.springframework.util.StringUtils;
2124
import org.springframework.web.bind.annotation.RequestMapping;
2225
import org.springframework.web.server.NotAcceptableStatusException;
2326
import org.springframework.web.server.ServerWebExchange;
@@ -78,6 +81,18 @@ public final boolean match(ServerWebExchange exchange) {
7881
protected abstract boolean matchMediaType(ServerWebExchange exchange)
7982
throws NotAcceptableStatusException, UnsupportedMediaTypeStatusException;
8083

84+
protected boolean matchParameters(MediaType contentType) {
85+
for (Map.Entry<String, String> entry : getMediaType().getParameters().entrySet()) {
86+
if (StringUtils.hasText(entry.getValue())) {
87+
String value = contentType.getParameter(entry.getKey());
88+
if (StringUtils.hasText(value) && !entry.getValue().equals(value)) {
89+
return false;
90+
}
91+
}
92+
}
93+
return true;
94+
}
95+
8196
@Override
8297
public int compareTo(AbstractMediaTypeExpression other) {
8398
MediaType mediaType1 = this.getMediaType();

spring-webflux/src/main/java/org/springframework/web/reactive/result/condition/ConsumesRequestCondition.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2020 the original author or authors.
2+
* Copyright 2002-2022 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -274,7 +274,7 @@ protected boolean matchMediaType(ServerWebExchange exchange) throws UnsupportedM
274274
try {
275275
MediaType contentType = exchange.getRequest().getHeaders().getContentType();
276276
contentType = (contentType != null ? contentType : MediaType.APPLICATION_OCTET_STREAM);
277-
return getMediaType().includes(contentType);
277+
return (getMediaType().includes(contentType) && matchParameters(contentType));
278278
}
279279
catch (InvalidMediaTypeException ex) {
280280
throw new UnsupportedMediaTypeStatusException("Can't parse Content-Type [" +

spring-webflux/src/main/java/org/springframework/web/reactive/result/condition/ProducesRequestCondition.java

Lines changed: 1 addition & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2020 the original author or authors.
2+
* Copyright 2002-2022 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -27,7 +27,6 @@
2727
import org.springframework.util.CollectionUtils;
2828
import org.springframework.util.MimeType;
2929
import org.springframework.util.ObjectUtils;
30-
import org.springframework.util.StringUtils;
3130
import org.springframework.web.accept.ContentNegotiationManager;
3231
import org.springframework.web.bind.annotation.RequestMapping;
3332
import org.springframework.web.cors.reactive.CorsUtils;
@@ -361,17 +360,6 @@ protected boolean matchMediaType(ServerWebExchange exchange) throws NotAcceptabl
361360
}
362361
return false;
363362
}
364-
365-
private boolean matchParameters(MediaType acceptedMediaType) {
366-
for (String name : getMediaType().getParameters().keySet()) {
367-
String s1 = getMediaType().getParameter(name);
368-
String s2 = acceptedMediaType.getParameter(name);
369-
if (StringUtils.hasText(s1) && StringUtils.hasText(s2) && !s1.equalsIgnoreCase(s2)) {
370-
return false;
371-
}
372-
}
373-
return true;
374-
}
375363
}
376364

377365
}

spring-webflux/src/test/java/org/springframework/web/reactive/result/condition/ConsumesRequestConditionTests.java

Lines changed: 34 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2019 the original author or authors.
2+
* Copyright 2002-2022 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -35,61 +35,81 @@
3535
public class ConsumesRequestConditionTests {
3636

3737
@Test
38-
public void consumesMatch() throws Exception {
38+
public void consumesMatch() {
3939
MockServerWebExchange exchange = postExchange("text/plain");
4040
ConsumesRequestCondition condition = new ConsumesRequestCondition("text/plain");
4141

4242
assertThat(condition.getMatchingCondition(exchange)).isNotNull();
4343
}
4444

4545
@Test
46-
public void negatedConsumesMatch() throws Exception {
46+
public void negatedConsumesMatch() {
4747
MockServerWebExchange exchange = postExchange("text/plain");
4848
ConsumesRequestCondition condition = new ConsumesRequestCondition("!text/plain");
4949

5050
assertThat(condition.getMatchingCondition(exchange)).isNull();
5151
}
5252

5353
@Test
54-
public void getConsumableMediaTypesNegatedExpression() throws Exception {
54+
public void getConsumableMediaTypesNegatedExpression() {
5555
ConsumesRequestCondition condition = new ConsumesRequestCondition("!application/xml");
5656
assertThat(condition.getConsumableMediaTypes()).isEqualTo(Collections.emptySet());
5757
}
5858

5959
@Test
60-
public void consumesWildcardMatch() throws Exception {
60+
public void consumesWildcardMatch() {
6161
MockServerWebExchange exchange = postExchange("text/plain");
6262
ConsumesRequestCondition condition = new ConsumesRequestCondition("text/*");
6363

6464
assertThat(condition.getMatchingCondition(exchange)).isNotNull();
6565
}
6666

6767
@Test
68-
public void consumesMultipleMatch() throws Exception {
68+
public void consumesMultipleMatch() {
6969
MockServerWebExchange exchange = postExchange("text/plain");
7070
ConsumesRequestCondition condition = new ConsumesRequestCondition("text/plain", "application/xml");
7171

7272
assertThat(condition.getMatchingCondition(exchange)).isNotNull();
7373
}
7474

7575
@Test
76-
public void consumesSingleNoMatch() throws Exception {
76+
public void consumesSingleNoMatch() {
7777
MockServerWebExchange exchange = postExchange("application/xml");
7878
ConsumesRequestCondition condition = new ConsumesRequestCondition("text/plain");
7979

8080
assertThat(condition.getMatchingCondition(exchange)).isNull();
8181
}
8282

83+
@Test // gh-28024
84+
public void matchWithParameters() {
85+
String base = "application/hal+json";
86+
ConsumesRequestCondition condition = new ConsumesRequestCondition(base + ";profile=\"a\"");
87+
MockServerWebExchange exchange = postExchange(base + ";profile=\"a\"");
88+
assertThat(condition.getMatchingCondition(exchange)).isNotNull();
89+
90+
condition = new ConsumesRequestCondition(base + ";profile=\"a\"");
91+
exchange = postExchange(base + ";profile=\"b\"");
92+
assertThat(condition.getMatchingCondition(exchange)).isNull();
93+
94+
condition = new ConsumesRequestCondition(base + ";profile=\"a\"");
95+
exchange = postExchange(base);
96+
assertThat(condition.getMatchingCondition(exchange)).isNotNull();
97+
98+
condition = new ConsumesRequestCondition(base);
99+
exchange = postExchange(base + ";profile=\"a\"");
100+
assertThat(condition.getMatchingCondition(exchange)).isNotNull();
101+
}
102+
83103
@Test
84-
public void consumesParseError() throws Exception {
104+
public void consumesParseError() {
85105
MockServerWebExchange exchange = postExchange("01");
86106
ConsumesRequestCondition condition = new ConsumesRequestCondition("text/plain");
87107

88108
assertThat(condition.getMatchingCondition(exchange)).isNull();
89109
}
90110

91111
@Test
92-
public void consumesParseErrorWithNegation() throws Exception {
112+
public void consumesParseErrorWithNegation() {
93113
MockServerWebExchange exchange = postExchange("01");
94114
ConsumesRequestCondition condition = new ConsumesRequestCondition("!text/plain");
95115

@@ -115,7 +135,7 @@ public void consumesNoContent() {
115135
}
116136

117137
@Test
118-
public void compareToSingle() throws Exception {
138+
public void compareToSingle() {
119139
MockServerWebExchange exchange = MockServerWebExchange.from(MockServerHttpRequest.get("/"));
120140

121141
ConsumesRequestCondition condition1 = new ConsumesRequestCondition("text/plain");
@@ -129,7 +149,7 @@ public void compareToSingle() throws Exception {
129149
}
130150

131151
@Test
132-
public void compareToMultiple() throws Exception {
152+
public void compareToMultiple() {
133153
MockServerWebExchange exchange = MockServerWebExchange.from(MockServerHttpRequest.get("/"));
134154

135155
ConsumesRequestCondition condition1 = new ConsumesRequestCondition("*/*", "text/plain");
@@ -153,7 +173,7 @@ public void combine() throws Exception {
153173
}
154174

155175
@Test
156-
public void combineWithDefault() throws Exception {
176+
public void combineWithDefault() {
157177
ConsumesRequestCondition condition1 = new ConsumesRequestCondition("text/plain");
158178
ConsumesRequestCondition condition2 = new ConsumesRequestCondition();
159179

@@ -162,7 +182,7 @@ public void combineWithDefault() throws Exception {
162182
}
163183

164184
@Test
165-
public void parseConsumesAndHeaders() throws Exception {
185+
public void parseConsumesAndHeaders() {
166186
String[] consumes = new String[] {"text/plain"};
167187
String[] headers = new String[]{"foo=bar", "content-type=application/xml,application/pdf"};
168188
ConsumesRequestCondition condition = new ConsumesRequestCondition(consumes, headers);
@@ -171,7 +191,7 @@ public void parseConsumesAndHeaders() throws Exception {
171191
}
172192

173193
@Test
174-
public void getMatchingCondition() throws Exception {
194+
public void getMatchingCondition() {
175195
MockServerWebExchange exchange = postExchange("text/plain");
176196
ConsumesRequestCondition condition = new ConsumesRequestCondition("text/plain", "application/xml");
177197

spring-webflux/src/test/java/org/springframework/web/reactive/result/condition/ProducesRequestConditionTests.java

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2019 the original author or authors.
2+
* Copyright 2002-2022 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -89,19 +89,19 @@ public void matchWithParameters() {
8989
String base = "application/atom+xml";
9090
ProducesRequestCondition condition = new ProducesRequestCondition(base + ";type=feed");
9191
MockServerWebExchange exchange = MockServerWebExchange.from(get("/").header("Accept", base + ";type=feed"));
92-
assertThat(condition.getMatchingCondition(exchange)).as("Declared parameter value must match if present in request").isNotNull();
92+
assertThat(condition.getMatchingCondition(exchange)).isNotNull();
9393

9494
condition = new ProducesRequestCondition(base + ";type=feed");
9595
exchange = MockServerWebExchange.from(get("/").header("Accept", base + ";type=entry"));
96-
assertThat(condition.getMatchingCondition(exchange)).as("Declared parameter value must match if present in request").isNull();
96+
assertThat(condition.getMatchingCondition(exchange)).isNull();
9797

9898
condition = new ProducesRequestCondition(base + ";type=feed");
9999
exchange = MockServerWebExchange.from(get("/").header("Accept", base));
100-
assertThat(condition.getMatchingCondition(exchange)).as("Declared parameter has no impact if not present in request").isNotNull();
100+
assertThat(condition.getMatchingCondition(exchange)).isNotNull();
101101

102102
condition = new ProducesRequestCondition(base);
103103
exchange = MockServerWebExchange.from(get("/").header("Accept", base + ";type=feed"));
104-
assertThat(condition.getMatchingCondition(exchange)).as("No impact from other parameters in request").isNotNull();
104+
assertThat(condition.getMatchingCondition(exchange)).isNotNull();
105105
}
106106

107107
@Test

spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/condition/AbstractMediaTypeExpression.java

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2020 the original author or authors.
2+
* Copyright 2002-2022 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -16,8 +16,11 @@
1616

1717
package org.springframework.web.servlet.mvc.condition;
1818

19+
import java.util.Map;
20+
1921
import org.springframework.http.MediaType;
2022
import org.springframework.lang.Nullable;
23+
import org.springframework.util.StringUtils;
2124
import org.springframework.web.bind.annotation.RequestMapping;
2225

2326
/**
@@ -78,6 +81,18 @@ else if (mediaType1.isLessSpecific(mediaType2)) {
7881
}
7982
}
8083

84+
protected boolean matchParameters(MediaType contentType) {
85+
for (Map.Entry<String, String> entry : getMediaType().getParameters().entrySet()) {
86+
if (StringUtils.hasText(entry.getValue())) {
87+
String value = contentType.getParameter(entry.getKey());
88+
if (StringUtils.hasText(value) && !entry.getValue().equals(value)) {
89+
return false;
90+
}
91+
}
92+
}
93+
return true;
94+
}
95+
8196
@Override
8297
public boolean equals(@Nullable Object other) {
8398
if (this == other) {

spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/condition/ConsumesRequestCondition.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2020 the original author or authors.
2+
* Copyright 2002-2022 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -285,7 +285,7 @@ static class ConsumeMediaTypeExpression extends AbstractMediaTypeExpression {
285285
}
286286

287287
public final boolean match(MediaType contentType) {
288-
boolean match = getMediaType().includes(contentType);
288+
boolean match = (getMediaType().includes(contentType) && matchParameters(contentType));
289289
return !isNegated() == match;
290290
}
291291
}

spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/condition/ProducesRequestCondition.java

Lines changed: 1 addition & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2020 the original author or authors.
2+
* Copyright 2002-2022 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -29,7 +29,6 @@
2929
import org.springframework.util.CollectionUtils;
3030
import org.springframework.util.MimeType;
3131
import org.springframework.util.ObjectUtils;
32-
import org.springframework.util.StringUtils;
3332
import org.springframework.web.HttpMediaTypeException;
3433
import org.springframework.web.HttpMediaTypeNotAcceptableException;
3534
import org.springframework.web.accept.ContentNegotiationManager;
@@ -373,17 +372,6 @@ private boolean matchMediaType(List<MediaType> acceptedMediaTypes) {
373372
}
374373
return false;
375374
}
376-
377-
private boolean matchParameters(MediaType acceptedMediaType) {
378-
for (String name : getMediaType().getParameters().keySet()) {
379-
String s1 = getMediaType().getParameter(name);
380-
String s2 = acceptedMediaType.getParameter(name);
381-
if (StringUtils.hasText(s1) && StringUtils.hasText(s2) && !s1.equalsIgnoreCase(s2)) {
382-
return false;
383-
}
384-
}
385-
return true;
386-
}
387375
}
388376

389377
}

0 commit comments

Comments
 (0)