Match declared parameters on produces condition
Closes gh-21670
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2018 the original author or authors.
|
||||
* Copyright 2002-2019 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -167,41 +167,48 @@ public @interface RequestMapping {
|
||||
String[] headers() default {};
|
||||
|
||||
/**
|
||||
* The consumable media types of the mapped request, narrowing the primary mapping.
|
||||
* <p>The format is a single media type or a sequence of media types,
|
||||
* with a request only mapped if the {@code Content-Type} matches one of these media types.
|
||||
* Examples:
|
||||
* Narrows the primary mapping by media types that can be consumed by the
|
||||
* mapped handler. Consists of one or more media types one of which must
|
||||
* match to the request {@code Content-Type} header. Examples:
|
||||
* <pre class="code">
|
||||
* consumes = "text/plain"
|
||||
* consumes = {"text/plain", "application/*"}
|
||||
* consumes = MediaType.TEXT_PLAIN_VALUE
|
||||
* </pre>
|
||||
* Expressions can be negated by using the "!" operator, as in "!text/plain", which matches
|
||||
* all requests with a {@code Content-Type} other than "text/plain".
|
||||
* Expressions can be negated by using the "!" operator, as in
|
||||
* "!text/plain", which matches all requests with a {@code Content-Type}
|
||||
* other than "text/plain".
|
||||
* <p><b>Supported at the type level as well as at the method level!</b>
|
||||
* When used at the type level, all method-level mappings override
|
||||
* this consumes restriction.
|
||||
* If specified at both levels, the method level consumes condition overrides
|
||||
* the type level condition.
|
||||
* @see org.springframework.http.MediaType
|
||||
* @see javax.servlet.http.HttpServletRequest#getContentType()
|
||||
*/
|
||||
String[] consumes() default {};
|
||||
|
||||
/**
|
||||
* The producible media types of the mapped request, narrowing the primary mapping.
|
||||
* <p>The format is a single media type or a sequence of media types,
|
||||
* with a request only mapped if the {@code Accept} matches one of these media types.
|
||||
* Examples:
|
||||
* Narrows the primary mapping by media types that can be produced by the
|
||||
* mapped handler. Consists of one or more media types one of which must
|
||||
* be chosen via content negotiation against the "acceptable" media types
|
||||
* of the request. Typically those are extracted from the {@code "Accept"}
|
||||
* header but may be derived from query parameters, or other. Examples:
|
||||
* <pre class="code">
|
||||
* produces = "text/plain"
|
||||
* produces = {"text/plain", "application/*"}
|
||||
* produces = MediaType.APPLICATION_JSON_UTF8_VALUE
|
||||
* produces = MediaType.TEXT_PLAIN_VALUE
|
||||
* produces = "text/plain;charset=UTF-8"
|
||||
* </pre>
|
||||
* <p>It affects the actual content type written, for example to produce a JSON response
|
||||
* with UTF-8 encoding, {@link org.springframework.http.MediaType#APPLICATION_JSON_UTF8_VALUE} should be used.
|
||||
* <p>Expressions can be negated by using the "!" operator, as in "!text/plain", which matches
|
||||
* all requests with a {@code Accept} other than "text/plain".
|
||||
* <p>If a declared media type contains a parameter (e.g. "charset=UTF-8",
|
||||
* "type=feed", type="entry") and if a compatible media type from the request
|
||||
* has that parameter too, then the parameter values must match. Otherwise
|
||||
* if the media type from the request does not contain the parameter, it is
|
||||
* assumed the client accepts any value.
|
||||
* <p>Expressions can be negated by using the "!" operator, as in "!text/plain",
|
||||
* which matches all requests with a {@code Accept} other than "text/plain".
|
||||
* <p><b>Supported at the type level as well as at the method level!</b>
|
||||
* When used at the type level, all method-level mappings override
|
||||
* this produces restriction.
|
||||
* If specified at both levels, the method level produces condition overrides
|
||||
* the type level condition.
|
||||
* @see org.springframework.http.MediaType
|
||||
* @see org.springframework.http.MediaType
|
||||
*/
|
||||
String[] produces() default {};
|
||||
|
||||
@@ -25,6 +25,7 @@ import java.util.Set;
|
||||
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.accept.ContentNegotiationManager;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.cors.reactive.CorsUtils;
|
||||
@@ -315,12 +316,23 @@ public final class ProducesRequestCondition extends AbstractRequestCondition<Pro
|
||||
protected boolean matchMediaType(ServerWebExchange exchange) throws NotAcceptableStatusException {
|
||||
List<MediaType> acceptedMediaTypes = getAcceptedMediaTypes(exchange);
|
||||
for (MediaType acceptedMediaType : acceptedMediaTypes) {
|
||||
if (getMediaType().isCompatibleWith(acceptedMediaType)) {
|
||||
if (getMediaType().isCompatibleWith(acceptedMediaType) && matchParameters(acceptedMediaType)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean matchParameters(MediaType acceptedMediaType) {
|
||||
for (String name : getMediaType().getParameters().keySet()) {
|
||||
String s1 = getMediaType().getParameter(name);
|
||||
String s2 = acceptedMediaType.getParameter(name);
|
||||
if (StringUtils.hasText(s1) && StringUtils.hasText(s2) && !s1.equalsIgnoreCase(s2)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -24,12 +24,8 @@ import org.junit.Test;
|
||||
import org.springframework.mock.web.test.server.MockServerWebExchange;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.junit.Assert.fail;
|
||||
import static org.springframework.mock.http.server.reactive.test.MockServerHttpRequest.get;
|
||||
import static org.junit.Assert.*;
|
||||
import static org.springframework.mock.http.server.reactive.test.MockServerHttpRequest.*;
|
||||
|
||||
/**
|
||||
* Unit tests for {@link ProducesRequestCondition}.
|
||||
@@ -84,6 +80,29 @@ public class ProducesRequestConditionTests {
|
||||
assertNull(condition.getMatchingCondition(exchange));
|
||||
}
|
||||
|
||||
@Test // gh-21670
|
||||
public void matchWithParameters() {
|
||||
String base = "application/atom+xml";
|
||||
ProducesRequestCondition condition = new ProducesRequestCondition(base + ";type=feed");
|
||||
MockServerWebExchange exchange = MockServerWebExchange.from(get("/").header("Accept", base + ";type=feed"));
|
||||
assertNotNull("Declared parameter value must match if present in request",
|
||||
condition.getMatchingCondition(exchange));
|
||||
|
||||
condition = new ProducesRequestCondition(base + ";type=feed");
|
||||
exchange = MockServerWebExchange.from(get("/").header("Accept", base + ";type=entry"));
|
||||
assertNull("Declared parameter value must match if present in request",
|
||||
condition.getMatchingCondition(exchange));
|
||||
|
||||
condition = new ProducesRequestCondition(base + ";type=feed");
|
||||
exchange = MockServerWebExchange.from(get("/").header("Accept", base));
|
||||
assertNotNull("Declared parameter has no impact if not present in request",
|
||||
condition.getMatchingCondition(exchange));
|
||||
|
||||
condition = new ProducesRequestCondition(base);
|
||||
exchange = MockServerWebExchange.from(get("/").header("Accept", base + ";type=feed"));
|
||||
assertNotNull("No impact from other parameters in request", condition.getMatchingCondition(exchange));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void matchParseError() {
|
||||
MockServerWebExchange exchange = MockServerWebExchange.from(get("/").header("Accept", "bogus"));
|
||||
|
||||
@@ -26,6 +26,7 @@ import javax.servlet.http.HttpServletRequest;
|
||||
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.HttpMediaTypeException;
|
||||
import org.springframework.web.HttpMediaTypeNotAcceptableException;
|
||||
import org.springframework.web.accept.ContentNegotiationManager;
|
||||
@@ -322,12 +323,23 @@ public final class ProducesRequestCondition extends AbstractRequestCondition<Pro
|
||||
|
||||
private boolean matchMediaType(List<MediaType> acceptedMediaTypes) {
|
||||
for (MediaType acceptedMediaType : acceptedMediaTypes) {
|
||||
if (getMediaType().isCompatibleWith(acceptedMediaType)) {
|
||||
if (getMediaType().isCompatibleWith(acceptedMediaType) && matchParameters(acceptedMediaType)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean matchParameters(MediaType acceptedMediaType) {
|
||||
for (String name : getMediaType().getParameters().keySet()) {
|
||||
String s1 = getMediaType().getParameter(name);
|
||||
String s2 = acceptedMediaType.getParameter(name);
|
||||
if (StringUtils.hasText(s1) && StringUtils.hasText(s2) && !s1.equalsIgnoreCase(s2)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -90,6 +90,30 @@ public class ProducesRequestConditionTests {
|
||||
assertNull(condition.getMatchingCondition(request));
|
||||
}
|
||||
|
||||
@Test // gh-21670
|
||||
public void matchWithParameters() {
|
||||
String base = "application/atom+xml";
|
||||
ProducesRequestCondition condition = new ProducesRequestCondition(base + ";type=feed");
|
||||
HttpServletRequest request = createRequest(base + ";type=entry");
|
||||
assertNull("Declared parameter value must match if present in request",
|
||||
condition.getMatchingCondition(request));
|
||||
|
||||
condition = new ProducesRequestCondition(base + ";type=feed");
|
||||
request = createRequest(base + ";type=feed");
|
||||
assertNotNull("Declared parameter value must match if present in request",
|
||||
condition.getMatchingCondition(request));
|
||||
|
||||
condition = new ProducesRequestCondition(base + ";type=feed");
|
||||
request = createRequest(base);
|
||||
assertNotNull("Declared parameter has no impact if not present in request",
|
||||
condition.getMatchingCondition(request));
|
||||
|
||||
condition = new ProducesRequestCondition(base);
|
||||
request = createRequest(base + ";type=feed");
|
||||
assertNotNull("No impact from other parameters in request",
|
||||
condition.getMatchingCondition(request));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void matchParseError() {
|
||||
ProducesRequestCondition condition = new ProducesRequestCondition("text/plain");
|
||||
|
||||
Reference in New Issue
Block a user