Skip to content

Commit

Permalink
Match declared parameters on produces condition
Browse files Browse the repository at this point in the history
Closes gh-21670
  • Loading branch information
rstoyanchev committed Mar 29, 2019
1 parent c0be1c5 commit 8dc535c
Show file tree
Hide file tree
Showing 5 changed files with 102 additions and 28 deletions.
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -167,41 +167,48 @@
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 {};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@

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;
Expand Down Expand Up @@ -315,12 +316,23 @@ class ProduceMediaTypeExpression extends AbstractMediaTypeExpression {
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;
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,8 @@
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}.
Expand Down Expand Up @@ -84,6 +80,29 @@ public void matchSingle() {
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"));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@

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;
Expand Down Expand Up @@ -322,12 +323,23 @@ public final boolean match(List<MediaType> acceptedMediaTypes) {

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;
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,30 @@ public void matchSingle() {
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");
Expand Down

1 comment on commit 8dc535c

@jam01
Copy link

@jam01 jam01 commented on 8dc535c Mar 29, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🙌🙏

Please sign in to comment.