Browse Source

Match declared parameters on produces condition

Closes gh-21670
pull/22709/head
Rossen Stoyanchev 6 years ago
parent
commit
8dc535c15c
  1. 47
      spring-web/src/main/java/org/springframework/web/bind/annotation/RequestMapping.java
  2. 14
      spring-webflux/src/main/java/org/springframework/web/reactive/result/condition/ProducesRequestCondition.java
  3. 31
      spring-webflux/src/test/java/org/springframework/web/reactive/result/condition/ProducesRequestConditionTests.java
  4. 14
      spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/condition/ProducesRequestCondition.java
  5. 24
      spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/condition/ProducesRequestConditionTests.java

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

@ -1,5 +1,5 @@ @@ -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 { @@ -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 {};

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

@ -25,6 +25,7 @@ import java.util.Set; @@ -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 @@ -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;
}
}
}

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

@ -24,12 +24,8 @@ import org.junit.Test; @@ -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 { @@ -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"));

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

@ -26,6 +26,7 @@ import javax.servlet.http.HttpServletRequest; @@ -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 @@ -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;
}
}
}

24
spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/condition/ProducesRequestConditionTests.java

@ -90,6 +90,30 @@ public class ProducesRequestConditionTests { @@ -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");

Loading…
Cancel
Save