From 0c6f5d7d297108be6592120acfa420a1341f853c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Basl=C3=A9?= Date: Mon, 2 Dec 2024 14:55:27 +0100 Subject: [PATCH] `HttpHeaders` are no longer a `MultiValueMap` This change removes the `MultiValueMap` nature of `HttpHeaders`, since it inherits APIs that do not align well with underlying server implementations. Notably, methods that allows to iterate over the whole collection of headers are susceptible to artificially introduced duplicates when multiple casings are used for a given header, depending on the underlying implementation. This change includes a dedicated key set implementation to support iterator-based removal, and either keeps map method implementations that are relevant or introduces header-focused methods that have a similar responsibility (like `hasHeaderValues(String, List)` and `containsHeaderValue(String, String)`). In order to nudge users away from using an HttpHeaders as a Map, the `asSingleValueMap` view is deprecated. In order to offer an escape hatch to users that do make use of the `MultiValueMap` API, a similar `asMultiValueMap` view is introduced but is immediately marked as deprecated. This change also adds map-like but header-focused assertions to `HttpHeadersAssert`, since it cannot extend `AbstractMapAssert` anymore. Closes gh-33913 --- .../reactive/MockServerHttpRequest.java | 15 + .../springframework/mock/web/MockPart.java | 2 +- .../test/http/HttpHeadersAssert.java | 203 +++++++- .../client/match/MockRestRequestMatchers.java | 26 +- .../reactive/server/DefaultWebTestClient.java | 4 +- .../web/reactive/server/ExchangeResult.java | 2 +- .../web/reactive/server/HeaderAssertions.java | 4 +- .../test/http/HttpHeadersAssertTests.java | 16 +- .../response/ResponseCreatorsTests.java | 14 +- .../web/reactive/server/MockServerTests.java | 6 +- .../MockMvcTesterIntegrationTests.java | 4 +- .../htmlunit/MockWebResponseBuilderTests.java | 2 +- .../standalone/FrameworkExtensionTests.java | 2 +- .../standalone/FrameworkExtensionTests.java | 2 +- .../org/springframework/http/HttpEntity.java | 30 +- .../org/springframework/http/HttpHeaders.java | 470 +++++++++++++----- .../http/ReadOnlyHttpHeaders.java | 46 +- .../springframework/http/RequestEntity.java | 57 ++- .../springframework/http/ResponseEntity.java | 45 +- .../http/client/MultipartBodyBuilder.java | 2 +- .../HttpComponentsClientHttpRequest.java | 2 +- .../client/reactive/JdkClientHttpRequest.java | 4 +- .../reactive/JettyClientHttpRequest.java | 2 +- .../BasicAuthenticationInterceptor.java | 2 +- .../multipart/MultipartHttpMessageWriter.java | 2 +- .../multipart/MultipartWriterSupport.java | 2 +- .../multipart/PartHttpMessageWriter.java | 2 +- .../AbstractHttpMessageConverter.java | 2 +- .../converter/FormHttpMessageConverter.java | 2 +- .../http/server/ServletServerHttpRequest.java | 2 +- .../server/ServletServerHttpResponse.java | 11 +- .../reactive/AbstractServerHttpRequest.java | 15 + .../ReactorNetty2ServerHttpRequest.java | 3 +- .../reactive/ReactorServerHttpRequest.java | 3 +- .../reactive/ServletServerHttpRequest.java | 12 +- .../reactive/TomcatHttpHandlerAdapter.java | 5 +- .../reactive/UndertowServerHttpRequest.java | 3 +- .../web/client/DefaultRestClient.java | 4 +- .../web/cors/DefaultCorsProcessor.java | 2 +- .../web/cors/reactive/CorsUtils.java | 6 +- .../cors/reactive/DefaultCorsProcessor.java | 2 +- ...equestHeaderMapMethodArgumentResolver.java | 51 +- .../RequestHeaderMethodArgumentResolver.java | 4 +- .../adapter/ForwardedHeaderTransformer.java | 2 +- .../springframework/http/HttpEntityTests.java | 28 +- .../http/HttpHeadersTests.java | 52 +- .../http/RequestEntityTests.java | 6 +- .../http/ResponseEntityTests.java | 16 +- .../AbstractHttpRequestFactoryTests.java | 8 +- ...ufferingClientHttpRequestFactoryTests.java | 4 +- .../DefaultPartHttpMessageReaderTests.java | 4 +- .../MultipartHttpMessageWriterTests.java | 2 +- .../PartEventHttpMessageReaderTests.java | 15 +- .../server/ServletServerHttpRequestTests.java | 6 +- .../ServletServerHttpResponseTests.java | 2 +- .../server/reactive/HeadersAdaptersTests.java | 9 +- .../reactive/ServerHttpResponseTests.java | 4 +- .../web/ErrorResponseExceptionTests.java | 44 +- .../web/ErrorResponseTests.java | 4 +- .../RestClientResponseExceptionTests.java | 2 +- .../reactive/DefaultCorsProcessorTests.java | 80 +-- .../ForwardedHeaderTransformerTests.java | 2 +- ...ializationCborHttpMessageConverterTests.kt | 12 +- ...ializationJsonHttpMessageConverterTests.kt | 21 +- ...zationProtobufHttpMessageConverterTests.kt | 12 +- .../reactive/MockServerHttpRequest.java | 16 + .../web/testfixture/servlet/MockPart.java | 2 +- .../function/client/DefaultWebClient.java | 4 +- .../client/WebClientRequestException.java | 2 +- .../client/WebClientResponseException.java | 2 +- .../server/DefaultServerResponseBuilder.java | 6 + .../ExtendedWebExchangeDataBinder.java | 2 +- ...equestHeaderMapMethodArgumentResolver.java | 9 +- .../RequestHeaderMethodArgumentResolver.java | 3 +- .../view/ViewResolutionResultHandler.java | 2 +- .../socket/client/JettyWebSocketClient.java | 2 +- .../client/StandardWebSocketClient.java | 4 +- .../client/UndertowWebSocketClient.java | 2 +- .../DefaultClientRequestBuilderTests.java | 2 +- .../DefaultClientResponseBuilderTests.java | 2 +- .../client/DefaultClientResponseTests.java | 4 +- .../client/DefaultWebClientTests.java | 6 +- .../client/ExchangeFilterFunctionsTests.java | 12 +- .../WebClientDataBufferAllocatingTests.java | 2 +- .../client/WebClientObservationTests.java | 2 +- .../DefaultServerRequestBuilderTests.java | 2 +- .../resource/ResourceWebHandlerTests.java | 6 +- ...bstractRequestMappingIntegrationTests.java | 2 +- .../ResponseEntityResultHandlerTests.java | 12 +- .../view/DefaultRenderingBuilderTests.java | 4 +- .../function/DefaultServerRequest.java | 4 +- ...stractMessageConverterMethodProcessor.java | 2 +- .../annotation/HttpEntityMethodProcessor.java | 2 +- .../ResponseEntityExceptionHandlerTests.java | 2 +- ...nnotationControllerHandlerMethodTests.java | 16 +- .../web/socket/WebSocketHttpHeaders.java | 40 +- .../standard/StandardWebSocketClient.java | 2 +- .../StandardWebSocketClientTests.java | 2 +- .../sockjs/client/SockJsClientTests.java | 8 +- .../sockjs/client/XhrTransportTests.java | 4 +- 100 files changed, 1116 insertions(+), 508 deletions(-) diff --git a/spring-test/src/main/java/org/springframework/mock/http/server/reactive/MockServerHttpRequest.java b/spring-test/src/main/java/org/springframework/mock/http/server/reactive/MockServerHttpRequest.java index e9ca3ee6d1..f3e10eca52 100644 --- a/spring-test/src/main/java/org/springframework/mock/http/server/reactive/MockServerHttpRequest.java +++ b/spring-test/src/main/java/org/springframework/mock/http/server/reactive/MockServerHttpRequest.java @@ -298,9 +298,17 @@ public final class MockServerHttpRequest extends AbstractServerHttpRequest { /** * Add the given header values. * @param headers the header values + * @deprecated Use {@link #headers(HttpHeaders)} */ + @Deprecated B headers(MultiValueMap headers); + /** + * Add the given header values. + * @param headers the header values + */ + B headers(HttpHeaders headers); + /** * Set the list of acceptable {@linkplain MediaType media types}, as * specified by the {@code Accept} header. @@ -484,11 +492,18 @@ public final class MockServerHttpRequest extends AbstractServerHttpRequest { } @Override + @Deprecated public BodyBuilder headers(MultiValueMap headers) { this.headers.putAll(headers); return this; } + @Override + public BodyBuilder headers(HttpHeaders headers) { + this.headers.putAll(headers); + return this; + } + @Override public BodyBuilder accept(MediaType... acceptableMediaTypes) { this.headers.setAccept(Arrays.asList(acceptableMediaTypes)); diff --git a/spring-test/src/main/java/org/springframework/mock/web/MockPart.java b/spring-test/src/main/java/org/springframework/mock/web/MockPart.java index 82b679b4b2..96b99e21df 100644 --- a/spring-test/src/main/java/org/springframework/mock/web/MockPart.java +++ b/spring-test/src/main/java/org/springframework/mock/web/MockPart.java @@ -129,7 +129,7 @@ public class MockPart implements Part { @Override public Collection getHeaderNames() { - return this.headers.keySet(); + return this.headers.headerNames(); } /** diff --git a/spring-test/src/main/java/org/springframework/test/http/HttpHeadersAssert.java b/spring-test/src/main/java/org/springframework/test/http/HttpHeadersAssert.java index a03bda4bc2..d0c2653300 100644 --- a/spring-test/src/main/java/org/springframework/test/http/HttpHeadersAssert.java +++ b/spring-test/src/main/java/org/springframework/test/http/HttpHeadersAssert.java @@ -20,10 +20,16 @@ import java.time.Instant; import java.time.ZoneId; import java.time.ZonedDateTime; import java.time.temporal.ChronoUnit; +import java.util.Arrays; +import java.util.Collection; import java.util.List; -import org.assertj.core.api.AbstractMapAssert; +import org.assertj.core.api.AbstractCollectionAssert; +import org.assertj.core.api.AbstractObjectAssert; import org.assertj.core.api.Assertions; +import org.assertj.core.api.ObjectAssert; +import org.assertj.core.presentation.Representation; +import org.assertj.core.presentation.StandardRepresentation; import org.springframework.http.HttpHeaders; @@ -34,54 +40,87 @@ import org.springframework.http.HttpHeaders; * @author Stephane Nicoll * @since 6.2 */ -public class HttpHeadersAssert extends AbstractMapAssert> { +public class HttpHeadersAssert extends AbstractObjectAssert { private static final ZoneId GMT = ZoneId.of("GMT"); + private final AbstractCollectionAssert, String, ObjectAssert> namesAssert; + public HttpHeadersAssert(HttpHeaders actual) { super(actual, HttpHeadersAssert.class); as("HTTP headers"); + withRepresentation(new Representation() { + @Override + public String toStringOf(Object object) { + if (object instanceof HttpHeaders headers) { + return headers.toString(); + } + return StandardRepresentation.STANDARD_REPRESENTATION.toStringOf(object); + } + }); + this.namesAssert = Assertions.assertThat(actual.headerNames()) + .as("HTTP header names"); } /** * Verify that the actual HTTP headers contain a header with the given * {@code name}. * @param name the name of an expected HTTP header - * @see #containsKey */ public HttpHeadersAssert containsHeader(String name) { - return containsKey(name); + this.namesAssert + .as("check headers contain HTTP header '%s'", name) + .contains(name); + return this.myself; } /** * Verify that the actual HTTP headers contain the headers with the given * {@code names}. * @param names the names of expected HTTP headers - * @see #containsKeys */ public HttpHeadersAssert containsHeaders(String... names) { - return containsKeys(names); + this.namesAssert + .as("check headers contain HTTP headers '%s'", Arrays.toString(names)) + .contains(names); + return this.myself; + } + + /** + * Verify that the actual HTTP headers contain only the headers with the + * given {@code names}, in any order and in a case-insensitive manner. + * @param names the names of expected HTTP headers + */ + public HttpHeadersAssert containsOnlyHeaders(String... names) { + this.namesAssert + .as("check headers contains only HTTP headers '%s'", Arrays.toString(names)) + .containsOnly(names); + return this.myself; } /** * Verify that the actual HTTP headers do not contain a header with the * given {@code name}. * @param name the name of an HTTP header that should not be present - * @see #doesNotContainKey */ public HttpHeadersAssert doesNotContainHeader(String name) { - return doesNotContainKey(name); + this.namesAssert + .as("check headers does not contain HTTP header '%s'", name) + .doesNotContain(name); + return this.myself; } /** * Verify that the actual HTTP headers do not contain any of the headers * with the given {@code names}. * @param names the names of HTTP headers that should not be present - * @see #doesNotContainKeys */ public HttpHeadersAssert doesNotContainsHeaders(String... names) { - return doesNotContainKeys(names); + this.namesAssert + .as("check headers does not contain HTTP headers '%s'", Arrays.toString(names)) + .doesNotContain(names); + return this.myself; } /** @@ -91,7 +130,7 @@ public class HttpHeadersAssert extends AbstractMapAssert values) { + containsHeader(name); + Assertions.assertThat(this.actual.get(name)) + .as("check all values of HTTP header '%s'", name) + .containsExactlyElementsOf(values); + return this.myself; + } + + /** + * Verify that the given header has a full list of values exactly equal to + * the given list of values, in any order. + * @param name the considered header name (case-insensitive) + * @param values the exhaustive list of expected values + */ + public HttpHeadersAssert hasExactlyValuesInAnyOrder(String name, List values) { + containsHeader(name); + Assertions.assertThat(this.actual.get(name)) + .as("check all values of HTTP header '%s' in any order", name) + .containsExactlyInAnyOrderElementsOf(values); + return this.myself; + } + + /** + * Verify that headers are empty and no headers are present. + */ + public HttpHeadersAssert isEmpty() { + this.namesAssert + .as("check headers are empty") + .isEmpty(); + return this.myself; + } + + /** + * Verify that headers are not empty and at least one header is present. + */ + public HttpHeadersAssert isNotEmpty() { + this.namesAssert + .as("check headers are not empty") + .isNotEmpty(); + return this.myself; + } + + /** + * Verify that there is exactly {@code expected} headers present, when + * considering header names in a case-insensitive manner. + * @param expected the expected number of headers + */ + public HttpHeadersAssert hasSize(int expected) { + this.namesAssert + .as("check headers have size '%i'", expected) + .hasSize(expected); + return this.myself; + } + + /** + * Verify that the number of headers present is strictly greater than the + * given boundary, when considering header names in a case-insensitive + * manner. + * @param boundary the given value to compare actual header size to + */ + public HttpHeadersAssert hasSizeGreaterThan(int boundary) { + this.namesAssert + .as("check headers have size > '%i'", boundary) + .hasSizeGreaterThan(boundary); + return this.myself; + } + + /** + * Verify that the number of headers present is greater or equal to the + * given boundary, when considering header names in a case-insensitive + * manner. + * @param boundary the given value to compare actual header size to + */ + public HttpHeadersAssert hasSizeGreaterThanOrEqualTo(int boundary) { + this.namesAssert + .as("check headers have size >= '%i'", boundary) + .hasSizeGreaterThanOrEqualTo(boundary); + return this.myself; + } + + /** + * Verify that the number of headers present is strictly less than the + * given boundary, when considering header names in a case-insensitive + * manner. + * @param boundary the given value to compare actual header size to + */ + public HttpHeadersAssert hasSizeLessThan(int boundary) { + this.namesAssert + .as("check headers have size < '%i'", boundary) + .hasSizeLessThan(boundary); + return this.myself; + } + + /** + * Verify that the number of headers present is less than or equal to the + * given boundary, when considering header names in a case-insensitive + * manner. + * @param boundary the given value to compare actual header size to + */ + public HttpHeadersAssert hasSizeLessThanOrEqualTo(int boundary) { + this.namesAssert + .as("check headers have size <= '%i'", boundary) + .hasSizeLessThanOrEqualTo(boundary); + return this.myself; + } + + /** + * Verify that the number of headers present is between the given boundaries + * (inclusive), when considering header names in a case-insensitive manner. + * @param lowerBoundary the lower boundary compared to which actual size + * should be greater than or equal to + * @param higherBoundary the higher boundary compared to which actual size + * should be less than or equal to + */ + public HttpHeadersAssert hasSizeBetween(int lowerBoundary, int higherBoundary) { + this.namesAssert + .as("check headers have size between '%i' and '%i'", lowerBoundary, higherBoundary) + .hasSizeBetween(lowerBoundary, higherBoundary); + return this.myself; + } + + /** + * Verify that the number actual headers is the same as in the given + * {@code HttpHeaders}. + * @param other the {@code HttpHeaders} to compare size with + */ + public HttpHeadersAssert hasSameSizeAs(HttpHeaders other) { + this.namesAssert + .as("check headers have same size as '%s'", other) + .hasSize(other.size()); + return this.myself; + } } diff --git a/spring-test/src/main/java/org/springframework/test/web/client/match/MockRestRequestMatchers.java b/spring-test/src/main/java/org/springframework/test/web/client/match/MockRestRequestMatchers.java index 203b4a8538..e68e169aa7 100644 --- a/spring-test/src/main/java/org/springframework/test/web/client/match/MockRestRequestMatchers.java +++ b/spring-test/src/main/java/org/springframework/test/web/client/match/MockRestRequestMatchers.java @@ -24,6 +24,7 @@ import javax.xml.xpath.XPathExpressionException; import org.hamcrest.Matcher; +import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; import org.springframework.http.client.ClientHttpRequest; import org.springframework.test.web.client.MockRestServiceServer; @@ -162,7 +163,7 @@ public abstract class MockRestRequestMatchers { public static RequestMatcher queryParam(String name, Matcher... matchers) { return request -> { MultiValueMap params = getQueryParams(request); - assertValueCount("query param", name, params, matchers.length); + assertValueCount(name, params, matchers.length); for (int i = 0 ; i < matchers.length; i++) { assertThat("Query param", params.get(name).get(i), matchers[i]); } @@ -190,7 +191,7 @@ public abstract class MockRestRequestMatchers { public static RequestMatcher queryParam(String name, String... expectedValues) { return request -> { MultiValueMap params = getQueryParams(request); - assertValueCount("query param", name, params, expectedValues.length); + assertValueCount(name, params, expectedValues.length); for (int i = 0 ; i < expectedValues.length; i++) { assertEquals("Query param [" + name + "]", expectedValues[i], params.get(name).get(i)); } @@ -246,7 +247,7 @@ public abstract class MockRestRequestMatchers { @SafeVarargs public static RequestMatcher header(String name, Matcher... matchers) { return request -> { - assertValueCount("header", name, request.getHeaders(), matchers.length); + assertValueCount(name, request.getHeaders(), matchers.length); List headerValues = request.getHeaders().get(name); Assert.state(headerValues != null, "No header values"); for (int i = 0; i < matchers.length; i++) { @@ -273,7 +274,7 @@ public abstract class MockRestRequestMatchers { */ public static RequestMatcher header(String name, String... expectedValues) { return request -> { - assertValueCount("header", name, request.getHeaders(), expectedValues.length); + assertValueCount(name, request.getHeaders(), expectedValues.length); List headerValues = request.getHeaders().get(name); Assert.state(headerValues != null, "No header values"); for (int i = 0; i < expectedValues.length; i++) { @@ -356,11 +357,22 @@ public abstract class MockRestRequestMatchers { } - private static void assertValueCount( - String valueType, String name, MultiValueMap map, int count) { + private static void assertValueCount(String name, MultiValueMap map, int count) { List values = map.get(name); - String message = "Expected " + valueType + " <" + name + ">"; + String message = "Expected query param <" + name + ">"; + if (values == null) { + fail(message + " to exist but was null"); + } + else if (count > values.size()) { + fail(message + " to have at least <" + count + "> values but found " + values); + } + } + + private static void assertValueCount(String name, HttpHeaders headers, int count) { + + List values = headers.get(name); + String message = "Expected header <" + name + ">"; if (values == null) { fail(message + " to exist but was null"); } diff --git a/spring-test/src/main/java/org/springframework/test/web/reactive/server/DefaultWebTestClient.java b/spring-test/src/main/java/org/springframework/test/web/reactive/server/DefaultWebTestClient.java index 476da5c5a0..7636b9b36b 100644 --- a/spring-test/src/main/java/org/springframework/test/web/reactive/server/DefaultWebTestClient.java +++ b/spring-test/src/main/java/org/springframework/test/web/reactive/server/DefaultWebTestClient.java @@ -373,10 +373,10 @@ class DefaultWebTestClient implements WebTestClient { private ClientRequest.Builder initRequestBuilder() { return ClientRequest.create(this.httpMethod, initUri()) .headers(headersToUse -> { - if (!CollectionUtils.isEmpty(DefaultWebTestClient.this.defaultHeaders)) { + if (!(DefaultWebTestClient.this.defaultHeaders == null || DefaultWebTestClient.this.defaultHeaders.isEmpty())) { headersToUse.putAll(DefaultWebTestClient.this.defaultHeaders); } - if (!CollectionUtils.isEmpty(this.headers)) { + if (!this.headers.isEmpty()) { headersToUse.putAll(this.headers); } }) diff --git a/spring-test/src/main/java/org/springframework/test/web/reactive/server/ExchangeResult.java b/spring-test/src/main/java/org/springframework/test/web/reactive/server/ExchangeResult.java index c68c349eeb..68a78ddbf0 100644 --- a/spring-test/src/main/java/org/springframework/test/web/reactive/server/ExchangeResult.java +++ b/spring-test/src/main/java/org/springframework/test/web/reactive/server/ExchangeResult.java @@ -249,7 +249,7 @@ public class ExchangeResult { } private String formatHeaders(HttpHeaders headers, String delimiter) { - return headers.entrySet().stream() + return headers.headerSet().stream() .map(entry -> entry.getKey() + ": " + entry.getValue()) .collect(Collectors.joining(delimiter)); } diff --git a/spring-test/src/main/java/org/springframework/test/web/reactive/server/HeaderAssertions.java b/spring-test/src/main/java/org/springframework/test/web/reactive/server/HeaderAssertions.java index f1c0558edf..4c5548a936 100644 --- a/spring-test/src/main/java/org/springframework/test/web/reactive/server/HeaderAssertions.java +++ b/spring-test/src/main/java/org/springframework/test/web/reactive/server/HeaderAssertions.java @@ -213,7 +213,7 @@ public class HeaderAssertions { * @since 5.0.3 */ public WebTestClient.ResponseSpec exists(String name) { - if (!getHeaders().containsKey(name)) { + if (!getHeaders().containsHeader(name)) { String message = getMessage(name) + " does not exist"; this.exchangeResult.assertWithDiagnostics(() -> fail(message)); } @@ -224,7 +224,7 @@ public class HeaderAssertions { * Expect that the header with the given name is not present. */ public WebTestClient.ResponseSpec doesNotExist(String name) { - if (getHeaders().containsKey(name)) { + if (getHeaders().containsHeader(name)) { String message = getMessage(name) + " exists with value=[" + getHeaders().getFirst(name) + "]"; this.exchangeResult.assertWithDiagnostics(() -> fail(message)); } diff --git a/spring-test/src/test/java/org/springframework/test/http/HttpHeadersAssertTests.java b/spring-test/src/test/java/org/springframework/test/http/HttpHeadersAssertTests.java index 36dc3c9253..2bb0b191df 100644 --- a/spring-test/src/test/java/org/springframework/test/http/HttpHeadersAssertTests.java +++ b/spring-test/src/test/java/org/springframework/test/http/HttpHeadersAssertTests.java @@ -45,7 +45,7 @@ class HttpHeadersAssertTests { Map map = Map.of("first", "1"); assertThatExceptionOfType(AssertionError.class) .isThrownBy(() -> assertThat(map).containsHeader("wrong-name")) - .withMessageContainingAll("HTTP headers", "first", "wrong-name"); + .withMessageContainingAll("HTTP header", "first", "wrong-name"); } @Test @@ -71,8 +71,8 @@ class HttpHeadersAssertTests { void doesNotContainHeaderWithNamePresent() { Map map = Map.of("first", "1"); assertThatExceptionOfType(AssertionError.class) - .isThrownBy(() -> assertThat(map).doesNotContainKey("first")) - .withMessageContainingAll("HTTP headers", "first"); + .isThrownBy(() -> assertThat(map).doesNotContainHeader("first")) + .withMessageContainingAll("HTTP header", "first"); } @Test @@ -112,7 +112,7 @@ class HttpHeadersAssertTests { headers.addAll("header", List.of("first", "second", "third")); assertThatExceptionOfType(AssertionError.class) .isThrownBy(() -> assertThat(headers).hasValue("wrong-name", "second")) - .withMessageContainingAll("HTTP headers", "header", "wrong-name"); + .withMessageContainingAll("HTTP header", "header", "wrong-name"); } @Test @@ -121,7 +121,7 @@ class HttpHeadersAssertTests { map.add("test-header", "a"); assertThatExceptionOfType(AssertionError.class) .isThrownBy(() -> assertThat(map).hasValue("wrong-name", "a")) - .withMessageContainingAll("HTTP headers", "test-header", "wrong-name"); + .withMessageContainingAll("HTTP header", "test-header", "wrong-name"); } @Test @@ -146,7 +146,7 @@ class HttpHeadersAssertTests { map.addAll("header", List.of("123", "456", "789")); assertThatExceptionOfType(AssertionError.class) .isThrownBy(() -> assertThat(map).hasValue("wrong-name", 456)) - .withMessageContainingAll("HTTP headers", "header", "wrong-name"); + .withMessageContainingAll("HTTP header", "header", "wrong-name"); } @Test @@ -164,7 +164,7 @@ class HttpHeadersAssertTests { map.setInstant("header", instant); assertThatExceptionOfType(AssertionError.class) .isThrownBy(() -> assertThat(map).hasValue("wrong-name", instant.minusSeconds(30))) - .withMessageContainingAll("HTTP headers", "header", "wrong-name"); + .withMessageContainingAll("HTTP header", "header", "wrong-name"); } @Test @@ -174,7 +174,7 @@ class HttpHeadersAssertTests { map.setInstant("header", instant); assertThatExceptionOfType(AssertionError.class) .isThrownBy(() -> assertThat(map).hasValue("wrong-name", instant.minusSeconds(1))) - .withMessageContainingAll("HTTP headers", "header", "wrong-name"); + .withMessageContainingAll("HTTP header", "header", "wrong-name"); } diff --git a/spring-test/src/test/java/org/springframework/test/web/client/response/ResponseCreatorsTests.java b/spring-test/src/test/java/org/springframework/test/web/client/response/ResponseCreatorsTests.java index 5a0ffe6625..7d7c31c03c 100644 --- a/spring-test/src/test/java/org/springframework/test/web/client/response/ResponseCreatorsTests.java +++ b/spring-test/src/test/java/org/springframework/test/web/client/response/ResponseCreatorsTests.java @@ -45,7 +45,7 @@ class ResponseCreatorsTests { MockClientHttpResponse response = (MockClientHttpResponse) MockRestResponseCreators.withSuccess().createResponse(null); assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); - assertThat(response.getHeaders()).isEmpty(); + assertThat(response.getHeaders().isEmpty()).as("isEmpty").isTrue(); assertThat(StreamUtils.copyToByteArray(response.getBody())).isEmpty(); } @@ -95,7 +95,7 @@ class ResponseCreatorsTests { MockClientHttpResponse response = (MockClientHttpResponse) responseCreator.createResponse(null); assertThat(response.getStatusCode()).isEqualTo(HttpStatus.NO_CONTENT); - assertThat(response.getHeaders()).isEmpty(); + assertThat(response.getHeaders().isEmpty()).as("isEmpty").isTrue(); assertThat(StreamUtils.copyToByteArray(response.getBody())).isEmpty(); } @@ -105,7 +105,7 @@ class ResponseCreatorsTests { MockClientHttpResponse response = (MockClientHttpResponse) responseCreator.createResponse(null); assertThat(response.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST); - assertThat(response.getHeaders()).isEmpty(); + assertThat(response.getHeaders().isEmpty()).as("isEmpty").isTrue(); assertThat(StreamUtils.copyToByteArray(response.getBody())).isEmpty(); } @@ -115,7 +115,7 @@ class ResponseCreatorsTests { MockClientHttpResponse response = (MockClientHttpResponse) responseCreator.createResponse(null); assertThat(response.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED); - assertThat(response.getHeaders()).isEmpty(); + assertThat(response.getHeaders().isEmpty()).as("isEmpty").isTrue(); assertThat(StreamUtils.copyToByteArray(response.getBody())).isEmpty(); } @@ -152,7 +152,7 @@ class ResponseCreatorsTests { MockClientHttpResponse response = (MockClientHttpResponse) responseCreator.createResponse(null); assertThat(response.getStatusCode()).isEqualTo(HttpStatus.TOO_MANY_REQUESTS); - assertThat(response.getHeaders()).doesNotContainKey(HttpHeaders.RETRY_AFTER); + assertThat(response.getHeaders().headerNames()).doesNotContain(HttpHeaders.RETRY_AFTER); assertThat(StreamUtils.copyToByteArray(response.getBody())).isEmpty(); } @@ -172,7 +172,7 @@ class ResponseCreatorsTests { MockClientHttpResponse response = (MockClientHttpResponse) responseCreator.createResponse(null); assertThat(response.getStatusCode()).isEqualTo(HttpStatus.INTERNAL_SERVER_ERROR); - assertThat(response.getHeaders()).isEmpty(); + assertThat(response.getHeaders().isEmpty()).as("isEmpty").isTrue(); assertThat(StreamUtils.copyToByteArray(response.getBody())).isEmpty(); } @@ -209,7 +209,7 @@ class ResponseCreatorsTests { MockClientHttpResponse response = (MockClientHttpResponse) responseCreator.createResponse(null); assertThat(response.getStatusCode()).isEqualTo(HttpStatus.FORBIDDEN); - assertThat(response.getHeaders()).isEmpty(); + assertThat(response.getHeaders().isEmpty()).as("isEmpty").isTrue(); assertThat(StreamUtils.copyToByteArray(response.getBody())).isEmpty(); } diff --git a/spring-test/src/test/java/org/springframework/test/web/reactive/server/MockServerTests.java b/spring-test/src/test/java/org/springframework/test/web/reactive/server/MockServerTests.java index 9bf980ce50..8e93c92baf 100644 --- a/spring-test/src/test/java/org/springframework/test/web/reactive/server/MockServerTests.java +++ b/spring-test/src/test/java/org/springframework/test/web/reactive/server/MockServerTests.java @@ -100,15 +100,15 @@ public class MockServerTests { WebTestClient clientFromMutatedBuilder = mutatedBuilder.build(); client1.mutate().filters(filters -> assertThat(filters).hasSize(1)); - client1.mutate().defaultHeaders(headers -> assertThat(headers).hasSize(1)); + client1.mutate().defaultHeaders(headers -> assertThat(headers.size()).isOne()); client1.mutate().defaultCookies(cookies -> assertThat(cookies).hasSize(1)); client2.mutate().filters(filters -> assertThat(filters).hasSize(2)); - client2.mutate().defaultHeaders(headers -> assertThat(headers).hasSize(2)); + client2.mutate().defaultHeaders(headers -> assertThat(headers.size()).isEqualTo(2)); client2.mutate().defaultCookies(cookies -> assertThat(cookies).hasSize(2)); clientFromMutatedBuilder.mutate().filters(filters -> assertThat(filters).hasSize(2)); - clientFromMutatedBuilder.mutate().defaultHeaders(headers -> assertThat(headers).hasSize(2)); + clientFromMutatedBuilder.mutate().defaultHeaders(headers -> assertThat(headers.size()).isEqualTo(2)); clientFromMutatedBuilder.mutate().defaultCookies(cookies -> assertThat(cookies).hasSize(2)); } diff --git a/spring-test/src/test/java/org/springframework/test/web/servlet/assertj/MockMvcTesterIntegrationTests.java b/spring-test/src/test/java/org/springframework/test/web/servlet/assertj/MockMvcTesterIntegrationTests.java index 2c0a8d45ad..bcb05345aa 100644 --- a/spring-test/src/test/java/org/springframework/test/web/servlet/assertj/MockMvcTesterIntegrationTests.java +++ b/spring-test/src/test/java/org/springframework/test/web/servlet/assertj/MockMvcTesterIntegrationTests.java @@ -322,8 +322,8 @@ public class MockMvcTesterIntegrationTests { } private Consumer textContent(String charset) { - return headers -> assertThat(headers).containsEntry( - "Content-Type", List.of("text/plain;charset=%s".formatted(charset))); + return headers -> assertThat(headers.hasHeaderValues("Content-Type", List.of("text/plain;charset=%s".formatted(charset)))) + .as("hasHeaderValues").isTrue(); } } diff --git a/spring-test/src/test/java/org/springframework/test/web/servlet/htmlunit/MockWebResponseBuilderTests.java b/spring-test/src/test/java/org/springframework/test/web/servlet/htmlunit/MockWebResponseBuilderTests.java index a38c09d52b..6854f4b761 100644 --- a/spring-test/src/test/java/org/springframework/test/web/servlet/htmlunit/MockWebResponseBuilderTests.java +++ b/spring-test/src/test/java/org/springframework/test/web/servlet/htmlunit/MockWebResponseBuilderTests.java @@ -126,7 +126,7 @@ public class MockWebResponseBuilderTests { WebResponse webResponse = this.responseBuilder.build(); List responseHeaders = webResponse.getResponseHeaders(); - assertThat(responseHeaders).hasSize(1); + assertThat(responseHeaders.size()).as("size").isOne(); NameValuePair header = responseHeaders.get(0); assertThat(header.getName()).isEqualTo("Set-Cookie"); assertThat(header.getValue()).isEqualTo("cookieA=valueA"); diff --git a/spring-test/src/test/java/org/springframework/test/web/servlet/samples/client/standalone/FrameworkExtensionTests.java b/spring-test/src/test/java/org/springframework/test/web/servlet/samples/client/standalone/FrameworkExtensionTests.java index bfbc8d6a10..ee3fcd6484 100644 --- a/spring-test/src/test/java/org/springframework/test/web/servlet/samples/client/standalone/FrameworkExtensionTests.java +++ b/spring-test/src/test/java/org/springframework/test/web/servlet/samples/client/standalone/FrameworkExtensionTests.java @@ -130,7 +130,7 @@ public class FrameworkExtensionTests { @Override public MockHttpServletRequest postProcessRequest(MockHttpServletRequest request) { - for (String headerName : this.headers.keySet()) { + for (String headerName : this.headers.headerNames()) { request.addHeader(headerName, this.headers.get(headerName)); } return request; diff --git a/spring-test/src/test/java/org/springframework/test/web/servlet/samples/standalone/FrameworkExtensionTests.java b/spring-test/src/test/java/org/springframework/test/web/servlet/samples/standalone/FrameworkExtensionTests.java index 0af7bbccce..90609c22d6 100644 --- a/spring-test/src/test/java/org/springframework/test/web/servlet/samples/standalone/FrameworkExtensionTests.java +++ b/spring-test/src/test/java/org/springframework/test/web/servlet/samples/standalone/FrameworkExtensionTests.java @@ -94,7 +94,7 @@ public class FrameworkExtensionTests { @Override public MockHttpServletRequest postProcessRequest(MockHttpServletRequest request) { - for (String headerName : this.headers.keySet()) { + for (String headerName : this.headers.headerNames()) { request.addHeader(headerName, this.headers.get(headerName)); } return request; diff --git a/spring-web/src/main/java/org/springframework/http/HttpEntity.java b/spring-web/src/main/java/org/springframework/http/HttpEntity.java index ffb2bf4d25..05aeab7b87 100644 --- a/spring-web/src/main/java/org/springframework/http/HttpEntity.java +++ b/spring-web/src/main/java/org/springframework/http/HttpEntity.java @@ -73,7 +73,7 @@ public class HttpEntity { * Create a new, empty {@code HttpEntity}. */ protected HttpEntity() { - this(null, null); + this(null, (HttpHeaders) null); } /** @@ -81,13 +81,35 @@ public class HttpEntity { * @param body the entity body */ public HttpEntity(T body) { - this(body, null); + this(body, (HttpHeaders) null); } /** * Create a new {@code HttpEntity} with the given headers and no body. * @param headers the entity headers + * @since 7.0 */ + public HttpEntity(HttpHeaders headers) { + this(null, headers); + } + + /** + * Create a new {@code HttpEntity} with the given body and headers. + * @param body the entity body + * @param headers the entity headers + * @since 7.0 + */ + public HttpEntity(@Nullable T body, @Nullable HttpHeaders headers) { + this.body = body; + this.headers = HttpHeaders.readOnlyHttpHeaders(headers != null ? headers : new HttpHeaders()); + } + + /** + * Create a new {@code HttpEntity} with the given headers and no body. + * @param headers the entity headers + * @deprecated Use {@link #HttpEntity(HttpHeaders)} + */ + @Deprecated public HttpEntity(MultiValueMap headers) { this(null, headers); } @@ -96,10 +118,12 @@ public class HttpEntity { * Create a new {@code HttpEntity} with the given body and headers. * @param body the entity body * @param headers the entity headers + * @deprecated Use {@link #HttpEntity(Object, HttpHeaders)} */ + @Deprecated public HttpEntity(@Nullable T body, @Nullable MultiValueMap headers) { this.body = body; - this.headers = HttpHeaders.readOnlyHttpHeaders(headers != null ? headers : new HttpHeaders()); + this.headers = HttpHeaders.readOnlyHttpHeaders(headers != null ? new HttpHeaders(headers) : new HttpHeaders()); } diff --git a/spring-web/src/main/java/org/springframework/http/HttpHeaders.java b/spring-web/src/main/java/org/springframework/http/HttpHeaders.java index 958d1012fa..4c68a7e1da 100644 --- a/spring-web/src/main/java/org/springframework/http/HttpHeaders.java +++ b/spring-web/src/main/java/org/springframework/http/HttpHeaders.java @@ -39,6 +39,7 @@ import java.util.Iterator; import java.util.List; import java.util.Locale; import java.util.Map; +import java.util.Map.Entry; import java.util.Objects; import java.util.Set; import java.util.StringJoiner; @@ -52,6 +53,7 @@ import org.springframework.util.CollectionUtils; import org.springframework.util.LinkedCaseInsensitiveMap; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; +import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; /** @@ -71,23 +73,18 @@ import org.springframework.util.StringUtils; * {@link #HttpHeaders(MultiValueMap)} constructor like those instantiated * internally by the framework to adapt to existing HTTP headers data structures * do guarantee per-header get/set/add operations to be case-insensitive as - * mandated by the HTTP specification. However, it is not necessarily the case - * for operations that deal with the collection as a whole (like {@code size()}, - * {@code values()}, {@code keySet()} and {@code entrySet()}). Prefer using - * {@link #headerSet()} for these cases. - * - *

Some backing implementations can store header names in a case-sensitive - * manner, which will lead to duplicates during the entrySet() iteration where - * multiple occurrences of a header name can surface depending on letter casing - * but each such entry has the full {@code List} of values. — This can be - * problematic for example when copying headers into a new instance by iterating - * over the old instance's {@code entrySet()} and using - * {@link #addAll(String, List)} rather than {@link #put(String, List)}. + * mandated by the HTTP specification. However, it is not necessarily how + * entries are actually stored, and this can lead to the reported {@code size()} + * being inflated. Prefer using {@link #headerSet()} or {@link #headerNames()} + * to ensure a case-insensitive view. * *

This class is meant to reference "well-known" headers supported by Spring * Framework. If your application or library relies on other headers defined in RFCs, * please use methods that accept the header name as a parameter. * + *

Since 7.0, this class no longer implements the {@code MultiValueMap} + * contract. + * * @author Arjen Poutsma * @author Sebastien Deleuze * @author Brian Clozel @@ -97,7 +94,7 @@ import org.springframework.util.StringUtils; * @author Simon Baslé * @since 3.0 */ -public class HttpHeaders implements MultiValueMap, Serializable { +public class HttpHeaders implements Serializable { private static final long serialVersionUID = -8578554704772377436L; @@ -472,6 +469,28 @@ public class HttpHeaders implements MultiValueMap, Serializable } } + /** + * Construct a new {@code HttpHeaders} instance by removing any read-only + * wrapper that may have been previously applied around the given + * {@code HttpHeaders} via {@link #readOnlyHttpHeaders(HttpHeaders)}. + *

Once the writable instance is mutated, the read-only instance is + * likely to be out of sync and should be discarded. + * @param httpHeaders the headers to expose + * @since 7.0 + */ + public HttpHeaders(HttpHeaders httpHeaders) { + Assert.notNull(httpHeaders, "HttpHeaders must not be null"); + if (httpHeaders == EMPTY) { + this.headers = CollectionUtils.toMultiValueMap(new LinkedCaseInsensitiveMap<>(8, Locale.ENGLISH)); + } + else { + while (httpHeaders.headers instanceof HttpHeaders wrapped) { + httpHeaders = wrapped; + } + this.headers = httpHeaders.headers; + } + } + /** * Get the list of header values for the given header name, if any. @@ -479,9 +498,20 @@ public class HttpHeaders implements MultiValueMap, Serializable * @return the list of header values, or an empty list * @since 5.2 */ - public List getOrEmpty(Object headerName) { + public List getOrEmpty(String headerName) { + return getOrDefault(headerName, Collections.emptyList()); + } + + /** + * Get the list of header values for the given header name, or the given + * default list of values if the header is not present. + * @param headerName the header name + * @param defaultValue the fallback list if header is not present + * @return the list of header values, or a default list of values + */ + public List getOrDefault(String headerName, List defaultValue) { List values = get(headerName); - return (values != null ? values : Collections.emptyList()); + return (values != null ? values : defaultValue); } /** @@ -1723,14 +1753,13 @@ public class HttpHeaders implements MultiValueMap, Serializable } - // MultiValueMap implementation + // MultiValueMap-like methods /** * Return the first header value for the given header name, if any. * @param headerName the header name * @return the first header value, or {@code null} if none */ - @Override public @Nullable String getFirst(String headerName) { return this.headers.getFirst(headerName); } @@ -1743,19 +1772,33 @@ public class HttpHeaders implements MultiValueMap, Serializable * @see #put(String, List) * @see #set(String, String) */ - @Override public void add(String headerName, @Nullable String headerValue) { this.headers.add(headerName, headerValue); } - @Override - public void addAll(String key, List values) { - this.headers.addAll(key, values); + /** + * Add all the given values under the given name. + *

As values are represented as a {@code List}, duplicate values can be + * introduced. See {@link #put(String, List)} to replace the list of values + * instead. + * @param headerName the header name + * @param headerValues the values to add + * @see #put(String, List) + */ + public void addAll(String headerName, List headerValues) { + this.headers.addAll(headerName, headerValues); } - @Override - public void addAll(MultiValueMap values) { - this.headers.addAll(values); + /** + * Add all the values of the given {@code HttpHeaders} to the current header. + *

As values are represented as a {@code List}, duplicate values can be + * introduced. See {@link #putAll(HttpHeaders)} to replace the list of + * values of each individual header name instead. + * @param headers the headers to add + * @see #putAll(HttpHeaders) + */ + public void addAll(HttpHeaders headers) { + this.headers.addAll(headers.headers); } /** @@ -1766,132 +1809,202 @@ public class HttpHeaders implements MultiValueMap, Serializable * @see #put(String, List) * @see #add(String, String) */ - @Override public void set(String headerName, @Nullable String headerValue) { this.headers.set(headerName, headerValue); } - @Override + /** + * Set all single header value from the given Map under each of their + * corresponding name. + * @param values the name-single-value pairs + * @see #putAll(Map) + */ public void setAll(Map values) { this.headers.setAll(values); } - @Override + /** + * Return this HttpHeaders as a {@code Map} with the first values for each + * header name. + *

The difference between this method and {@link #asSingleValueMap()} is + * that this method returns a copy of the headers, whereas the latter + * returns a view. This copy also ensures that collection-iterating methods + * like {@code entrySet()} are case-insensitive. + * @return a single value representation of these headers + */ public Map toSingleValueMap() { return this.headers.toSingleValueMap(); } - @Override + /** + * Return this HttpHeaders as a {@code Map} with the first values for each + * header name. + *

The difference between this method and {@link #toSingleValueMap()} is + * that this method returns a view of the headers, whereas the latter + * returns a copy. This method is also susceptible to include multiple + * casing variants of a given header name, see {@link #asMultiValueMap()} + * javadoc. + * @return a single value representation of these headers + * @deprecated Use {@link #toSingleValueMap()} which performs a copy but + * ensures that collection-iterating methods like {@code entrySet()} are + * case-insensitive + */ + @Deprecated public Map asSingleValueMap() { return this.headers.asSingleValueMap(); } - // Map implementation + /** + * Return this HttpHeaders as a {@code MultiValueMap} with the full list + * of values for each header name. + *

Note that some backing server headers implementations can store header + * names in a case-sensitive manner, which will lead to duplicates during + * iteration in methods like {@code entrySet()}, where multiple occurrences + * of a header name can surface depending on letter casing but each such + * entry has the full {@code List} of values. + * @return a MultiValueMap representation of these headers + * @deprecated This method is provided for backward compatibility with APIs + * that would only accept maps. Generally avoid using HttpHeaders as a Map + * or MultiValueMap. + */ + @Deprecated + public MultiValueMap asMultiValueMap() { + return this.headers; + } - @Override + // Map-like implementation + + /** + * Returns {@code true} if this HttpHeaders contains no header entry. + */ public boolean isEmpty() { return this.headers.isEmpty(); } - @Override - public boolean containsKey(Object key) { - return this.headers.containsKey(key); + /** + * Returns {@code true} if this HttpHeaders contains an entry for the + * given header name. + * @param headerName the header name + * @since 7.0 + */ + public boolean containsHeader(String headerName) { + return this.headers.containsKey(headerName); } - @Override - public boolean containsValue(Object value) { - return this.headers.containsValue(value); + /** + * Returns {@code true} if this HttpHeaders contains exactly the given list + * of values for the given header name. + * @param headerName the header name + * @param values the expected list of values + * @since 7.0 + */ + public boolean hasHeaderValues(String headerName, List values) { + return ObjectUtils.nullSafeEquals(this.headers.get(headerName), values); } - @Override - public @Nullable List get(Object key) { - return this.headers.get(key); + /** + * Returns {@code true} if this HttpHeaders contains the given header and + * its list of values contains the given value. + * @param headerName the header name + * @param value the value expected to be in the list of values + * @since 7.0 + */ + public boolean containsHeaderValue(String headerName, String value) { + final List values = this.headers.get(headerName); + if (values == null) { + return false; + } + return values.contains(value); } - @Override - public List put(String key, List value) { - return this.headers.put(key, value); + /** + * Get the list of values associated with the given header name. + * @param headerName the header name + */ + @Nullable + public List get(String headerName) { + return this.headers.get(headerName); } - @Override - public List remove(Object key) { + /** + * Set the list of values associated with the given header name. Returns the + * previous list of values, or {@code null} if the header was not present. + * @param headerName the header name + * @param headerValues the new values + * @return the old values for the given header name + */ + public @Nullable List put(String headerName, List headerValues) { + return this.headers.put(headerName, headerValues); + } + + /** + * Set header values for the given header name if that header name isn't + * already present in this HttpHeaders and return {@code null}. If the + * header is already present, returns the associated value list instead. + * @param headerName the header name + * @param headerValues the header values to set if header is not present + * @return the previous value or {@code null} + */ + public @Nullable List putIfAbsent(String headerName, List headerValues) { + return this.headers.putIfAbsent(headerName, headerValues); + } + + /** + * Put all the entries from the given HttpHeaders into this HttpHeaders. + * @param headers the given headers + * @see #put(String, List) + */ + public void putAll(HttpHeaders headers) { + this.headers.putAll(headers.headers); + } + + /** + * Put all the entries from the given {@code MultiValueMap} into this + * HttpHeaders. + * @param headers the given headers + * @see #put(String, List) + */ + public void putAll(Map> headers) { + this.headers.putAll(headers); + } + + /** + * Remove a header from this HttpHeaders instance, and return the associated + * value list or {@code null} if that header wasn't present. + * @param key the name of the header to remove + * @return the value list associated with the removed header name + */ + public @Nullable List remove(String key) { return this.headers.remove(key); } - @Override - public void putAll(Map> map) { - this.headers.putAll(map); - } - - @Override + /** + * Remove all headers from this HttpHeaders instance. + */ public void clear() { this.headers.clear(); } - @Override - public List putIfAbsent(String key, List value) { - return this.headers.putIfAbsent(key, value); - } - - // Map/MultiValueMap methods that can have duplicate header names: size/keySet/values/entrySet/forEach - /** * Return the number of headers in the collection. This can be inflated, * see {@link HttpHeaders class level javadoc}. */ - @Override public int size() { return this.headers.size(); } - /** - * Return a {@link Set} view of header names. This can include multiple - * casing variants of a given header name, see - * {@link HttpHeaders class level javadoc}. - */ - @Override - public Set keySet() { - return this.headers.keySet(); - } - - /** - * Return a {@link Collection} view of all the header values, reconstructed - * from iterating over the {@link #keySet()}. This can include duplicates if - * multiple casing variants of a given header name are tracked, see - * {@link HttpHeaders class level javadoc}. - */ - @Override - public Collection> values() { - return this.headers.values(); - } - - /** - * Return a {@link Set} views of header entries, reconstructed from - * iterating over the {@link #keySet()}. This can include duplicate entries - * if multiple casing variants of a given header name are tracked, see - * {@link HttpHeaders class level javadoc}. - * @see #headerSet() - */ - @Override - public Set>> entrySet() { - return this.headers.entrySet(); - } - /** * Perform an action over each header, as when iterated via - * {@link #entrySet()}. This can include duplicate entries - * if multiple casing variants of a given header name are tracked, see - * {@link HttpHeaders class level javadoc}. + * {@link #headerSet()}. * @param action the action to be performed for each entry */ - @Override public void forEach(BiConsumer> action) { - this.headers.forEach(action); + this.headerSet().forEach(e -> action.accept(e.getKey(), e.getValue())); } /** * Return a view of the headers as an entry {@code Set} of key-list pairs. - * Both {@link Iterator#remove()} and {@link java.util.Map.Entry#setValue} + * Both {@link Iterator#remove()} and {@link Entry#setValue} * are supported and mutate the headers. *

This collection is guaranteed to contain one entry per header name * even if the backing structure stores multiple casing variants of names, @@ -1905,6 +2018,20 @@ public class HttpHeaders implements MultiValueMap, Serializable return new CaseInsensitiveEntrySet(this.headers); } + /** + * Return the set of header names. Both {@link Set#remove(Object)} and + * {@link Set#clear()} operations are supported and mutate the headers. + *

This collection is guaranteed to contain only one casing variant + * of each header name even if the backing structure stores multiple casing + * variants of names. The first encountered variant is the one that is + * retained. + * @return a {@code Set} of all the headers names + * @since 7.0 + */ + public Set headerNames() { + return new CaseInsensitiveHeaderNameSet(this.headers); + } + @Override public boolean equals(@Nullable Object other) { @@ -1959,7 +2086,7 @@ public class HttpHeaders implements MultiValueMap, Serializable * @since 5.1.4 */ public static String formatHeaders(MultiValueMap headers) { - Set headerNames = toCaseInsensitiveSet(headers.keySet()); + Set headerNames = new CaseInsensitiveHeaderNameSet(headers); String suffix = "]"; if (headerNames.size() != headers.size()) { suffix = "] with native header names " + headers.keySet(); @@ -2027,34 +2154,28 @@ public class HttpHeaders implements MultiValueMap, Serializable } - private static Set toCaseInsensitiveSet(Set originalSet) { - final Set deduplicatedSet = Collections.newSetFromMap( - new LinkedCaseInsensitiveMap<>(originalSet.size(), Locale.ROOT)); - // add/addAll (put/putAll in LinkedCaseInsensitiveMap) retain the casing of the last occurrence. - // Here we prefer the first. - for (String header : originalSet) { - //noinspection RedundantCollectionOperation - if (!deduplicatedSet.contains(header)) { - deduplicatedSet.add(header); - } - } - return deduplicatedSet; - } + private static final class CaseInsensitiveHeaderNameSet extends AbstractSet { - - private static final class CaseInsensitiveEntrySet extends AbstractSet>> { + private static final Object VALUE = new Object(); private final MultiValueMap headers; - private final Set deduplicatedNames; + private final Map deduplicatedNames; - public CaseInsensitiveEntrySet(MultiValueMap headers) { + public CaseInsensitiveHeaderNameSet(MultiValueMap headers) { this.headers = headers; - this.deduplicatedNames = toCaseInsensitiveSet(headers.keySet()); + this.deduplicatedNames = new LinkedCaseInsensitiveMap<>(headers.size(), Locale.ROOT); + // add/addAll (put/putAll in LinkedCaseInsensitiveMap) retain the casing of the last occurrence. + // Here we prefer the first. + for (String header : headers.keySet()) { + if (!this.deduplicatedNames.containsKey(header)) { + this.deduplicatedNames.put(header, VALUE); + } + } } @Override - public Iterator>> iterator() { - return new CaseInsensitiveIterator(this.deduplicatedNames.iterator()); + public Iterator iterator() { + return new HeaderNamesIterator(this.headers, this.deduplicatedNames); } @Override @@ -2062,15 +2183,87 @@ public class HttpHeaders implements MultiValueMap, Serializable return this.deduplicatedNames.size(); } - private final class CaseInsensitiveIterator implements Iterator>> { + @Override + public boolean contains(Object o) { + return this.headers.containsKey(o); + } + + @Override + public boolean remove(Object o) { + return this.headers.remove(o) != null && this.deduplicatedNames.remove(o) != null; + } + + @Override + public void clear() { + this.headers.clear(); + this.deduplicatedNames.clear(); + } + } + + private static class HeaderNamesIterator implements Iterator { + + + private @Nullable String currentName; + + private final MultiValueMap headers; + private final Iterator namesIterator; + + public HeaderNamesIterator (MultiValueMap headers, Map caseInsensitiveNames) { + this.headers = headers; + this.namesIterator = caseInsensitiveNames.keySet().iterator(); + this.currentName = null; + } + + @Override + public boolean hasNext() { + return this.namesIterator.hasNext(); + } + + @Override + public String next() { + this.currentName = this.namesIterator.next(); + return this.currentName; + } + + @Override + public void remove() { + if (this.currentName == null) { + throw new IllegalStateException("No current Header in iterator"); + } + if (!this.headers.containsKey(this.currentName)) { + throw new IllegalStateException("Header not present: " + this.currentName); + } + this.headers.remove(this.currentName); + } + } + + + private static final class CaseInsensitiveEntrySet extends AbstractSet>> { + + private final MultiValueMap headers; + private final CaseInsensitiveHeaderNameSet nameSet; + + public CaseInsensitiveEntrySet(MultiValueMap headers) { + this.headers = headers; + this.nameSet = new CaseInsensitiveHeaderNameSet(headers); + } + + @Override + public Iterator>> iterator() { + return new CaseInsensitiveIterator(this.nameSet.iterator()); + } + + @Override + public int size() { + return this.nameSet.size(); + } + + private final class CaseInsensitiveIterator implements Iterator>> { private final Iterator namesIterator; - private @Nullable String currentName; - - private CaseInsensitiveIterator(Iterator namesIterator) { + public CaseInsensitiveIterator(Iterator namesIterator) { this.namesIterator = namesIterator; - this.currentName = null; } @Override @@ -2079,24 +2272,17 @@ public class HttpHeaders implements MultiValueMap, Serializable } @Override - public Map.Entry> next() { - this.currentName = this.namesIterator.next(); - return new CaseInsensitiveEntry(this.currentName); + public Entry> next() { + return new CaseInsensitiveEntrySet.CaseInsensitiveEntry(this.namesIterator.next()); } @Override public void remove() { - if (this.currentName == null) { - throw new IllegalStateException("No current Header in iterator"); - } - if (!CaseInsensitiveEntrySet.this.headers.containsKey(this.currentName)) { - throw new IllegalStateException("Header not present: " + this.currentName); - } - CaseInsensitiveEntrySet.this.headers.remove(this.currentName); + this.namesIterator.remove(); } } - private final class CaseInsensitiveEntry implements Map.Entry> { + private final class CaseInsensitiveEntry implements Entry> { private final String key; @@ -2121,6 +2307,22 @@ public class HttpHeaders implements MultiValueMap, Serializable CaseInsensitiveEntrySet.this.headers.put(this.key, value); return previousValues; } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof Map.Entry that)) { + return false; + } + return ObjectUtils.nullSafeEquals(getKey(), that.getKey()) && ObjectUtils.nullSafeEquals(getValue(), that.getValue()); + } + + @Override + public int hashCode() { + return ObjectUtils.nullSafeHash(getKey(), getValue()); + } } } diff --git a/spring-web/src/main/java/org/springframework/http/ReadOnlyHttpHeaders.java b/spring-web/src/main/java/org/springframework/http/ReadOnlyHttpHeaders.java index e40118ad54..681e043e46 100644 --- a/spring-web/src/main/java/org/springframework/http/ReadOnlyHttpHeaders.java +++ b/spring-web/src/main/java/org/springframework/http/ReadOnlyHttpHeaders.java @@ -17,17 +17,18 @@ package org.springframework.http; import java.util.AbstractMap.SimpleImmutableEntry; -import java.util.Collection; import java.util.Collections; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; +import java.util.Map.Entry; import java.util.Set; import java.util.function.BiConsumer; import java.util.stream.Collectors; import org.jspecify.annotations.Nullable; +import org.springframework.util.CollectionUtils; import org.springframework.util.MultiValueMap; /** @@ -84,8 +85,8 @@ class ReadOnlyHttpHeaders extends HttpHeaders { } @Override - public @Nullable List get(Object key) { - List values = this.headers.get(key); + public @Nullable List get(String headerName) { + List values = this.headers.get(headerName); return (values != null ? Collections.unmodifiableList(values) : null); } @@ -95,12 +96,12 @@ class ReadOnlyHttpHeaders extends HttpHeaders { } @Override - public void addAll(String key, List values) { + public void addAll(String key, List headerValues) { throw new UnsupportedOperationException(); } @Override - public void addAll(MultiValueMap values) { + public void addAll(HttpHeaders values) { throw new UnsupportedOperationException(); } @@ -120,27 +121,45 @@ class ReadOnlyHttpHeaders extends HttpHeaders { } @Override + @Deprecated public Map asSingleValueMap() { return Collections.unmodifiableMap(this.headers.asSingleValueMap()); } @Override - public Set keySet() { - return Collections.unmodifiableSet(this.headers.keySet()); + @Deprecated + public MultiValueMap asMultiValueMap() { + return CollectionUtils.unmodifiableMultiValueMap(this.headers); } + @Override + public Set headerNames() { + return Collections.unmodifiableSet(super.headerNames()); + } + + @Override public List put(String key, List value) { throw new UnsupportedOperationException(); } @Override - public List remove(Object key) { + public @Nullable List putIfAbsent(String headerName, List headerValues) { throw new UnsupportedOperationException(); } @Override - public void putAll(Map> map) { + public void putAll(@Nullable HttpHeaders values) { + throw new UnsupportedOperationException(); + } + + @Override + public void putAll(Map> headers) { + throw new UnsupportedOperationException(); + } + + @Override + public List remove(String key) { throw new UnsupportedOperationException(); } @@ -150,13 +169,8 @@ class ReadOnlyHttpHeaders extends HttpHeaders { } @Override - public Collection> values() { - return Collections.unmodifiableCollection(this.headers.values()); - } - - @Override - public Set>> entrySet() { - return this.headers.entrySet().stream().map(SimpleImmutableEntry::new) + public Set>> headerSet() { + return super.headerSet().stream().map(SimpleImmutableEntry::new) .collect(Collectors.collectingAndThen( Collectors.toCollection(LinkedHashSet::new), // Retain original ordering of entries Collections::unmodifiableSet)); diff --git a/spring-web/src/main/java/org/springframework/http/RequestEntity.java b/spring-web/src/main/java/org/springframework/http/RequestEntity.java index a31e78cc03..71dd7079da 100644 --- a/spring-web/src/main/java/org/springframework/http/RequestEntity.java +++ b/spring-web/src/main/java/org/springframework/http/RequestEntity.java @@ -79,7 +79,7 @@ public class RequestEntity extends HttpEntity { * @param url the URL */ public RequestEntity(HttpMethod method, URI url) { - this(null, null, method, url, null); + this(null, (HttpHeaders) null, method, url, null); } /** @@ -89,7 +89,7 @@ public class RequestEntity extends HttpEntity { * @param url the URL */ public RequestEntity(@Nullable T body, HttpMethod method, URI url) { - this(body, null, method, url, null); + this(body, (HttpHeaders) null, method, url, null); } /** @@ -101,7 +101,7 @@ public class RequestEntity extends HttpEntity { * @since 4.3 */ public RequestEntity(@Nullable T body, HttpMethod method, URI url, Type type) { - this(body, null, method, url, type); + this(body, (HttpHeaders) null, method, url, type); } /** @@ -109,7 +109,52 @@ public class RequestEntity extends HttpEntity { * @param headers the headers * @param method the method * @param url the URL + * @since 7.0 */ + public RequestEntity(HttpHeaders headers, HttpMethod method, URI url) { + this(null, headers, method, url, null); + } + + /** + * Constructor with method, URL, headers and body. + * @param body the body + * @param headers the headers + * @param method the method + * @param url the URL + * @since 7.0 + */ + public RequestEntity(@Nullable T body, @Nullable HttpHeaders headers, + @Nullable HttpMethod method, URI url) { + + this(body, headers, method, url, null); + } + + /** + * Constructor with method, URL, headers, body and type. + * @param body the body + * @param headers the headers + * @param method the method + * @param url the URL + * @param type the type used for generic type resolution + * @since 7.0 + */ + public RequestEntity(@Nullable T body, @Nullable HttpHeaders headers, + @Nullable HttpMethod method, @Nullable URI url, @Nullable Type type) { + + super(body, headers); + this.method = method; + this.url = url; + this.type = type; + } + + /** + * Constructor with method, URL and headers but without body. + * @param headers the headers + * @param method the method + * @param url the URL + * @deprecated Use {@link #RequestEntity(HttpHeaders, HttpMethod, URI)} + */ + @Deprecated public RequestEntity(MultiValueMap headers, HttpMethod method, URI url) { this(null, headers, method, url, null); } @@ -120,7 +165,9 @@ public class RequestEntity extends HttpEntity { * @param headers the headers * @param method the method * @param url the URL + * @deprecated Use {@link #RequestEntity(Object, HttpHeaders, HttpMethod, URI)} */ + @Deprecated public RequestEntity(@Nullable T body, @Nullable MultiValueMap headers, @Nullable HttpMethod method, URI url) { @@ -135,7 +182,9 @@ public class RequestEntity extends HttpEntity { * @param url the URL * @param type the type used for generic type resolution * @since 4.3 + * @deprecated Use {@link #RequestEntity(Object, HttpHeaders, HttpMethod, URI, Type)} */ + @Deprecated public RequestEntity(@Nullable T body, @Nullable MultiValueMap headers, @Nullable HttpMethod method, @Nullable URI url, @Nullable Type type) { @@ -694,7 +743,7 @@ public class RequestEntity extends HttpEntity { private final @Nullable Map uriVarsMap; UriTemplateRequestEntity( - @Nullable T body, @Nullable MultiValueMap headers, + @Nullable T body, @Nullable HttpHeaders headers, @Nullable HttpMethod method, @Nullable Type type, String uriTemplate, Object @Nullable [] uriVarsArray, @Nullable Map uriVarsMap) { diff --git a/spring-web/src/main/java/org/springframework/http/ResponseEntity.java b/spring-web/src/main/java/org/springframework/http/ResponseEntity.java index f5ea7cfc0d..e4e54c5a33 100644 --- a/spring-web/src/main/java/org/springframework/http/ResponseEntity.java +++ b/spring-web/src/main/java/org/springframework/http/ResponseEntity.java @@ -88,7 +88,7 @@ public class ResponseEntity extends HttpEntity { * @param status the status code */ public ResponseEntity(HttpStatusCode status) { - this(null, null, status); + this(null, (HttpHeaders) null, status); } /** @@ -97,14 +97,51 @@ public class ResponseEntity extends HttpEntity { * @param status the status code */ public ResponseEntity(@Nullable T body, HttpStatusCode status) { - this(body, null, status); + this(body, (HttpHeaders) null, status); } /** * Create a {@code ResponseEntity} with headers and a status code. * @param headers the entity headers * @param status the status code + * @since 7.0 */ + public ResponseEntity(HttpHeaders headers, HttpStatusCode status) { + this(null, headers, status); + } + + /** + * Create a {@code ResponseEntity} with a body, headers, and a raw status code. + * @param body the entity body + * @param headers the entity headers + * @param rawStatus the status code value + * @since 7.0 + */ + public ResponseEntity(@Nullable T body, @Nullable HttpHeaders headers, int rawStatus) { + this(body, headers, HttpStatusCode.valueOf(rawStatus)); + } + + /** + * Create a {@code ResponseEntity} with a body, headers, and a status code. + * @param body the entity body + * @param headers the entity headers + * @param statusCode the status code + * @since 7.0 + */ + public ResponseEntity(@Nullable T body, @Nullable HttpHeaders headers, HttpStatusCode statusCode) { + super(body, headers); + Assert.notNull(statusCode, "HttpStatusCode must not be null"); + + this.status = statusCode; + } + + /** + * Create a {@code ResponseEntity} with headers and a status code. + * @param headers the entity headers + * @param status the status code + * @deprecated Use {@link #ResponseEntity(HttpHeaders, HttpStatusCode)} + */ + @Deprecated public ResponseEntity(MultiValueMap headers, HttpStatusCode status) { this(null, headers, status); } @@ -115,7 +152,9 @@ public class ResponseEntity extends HttpEntity { * @param headers the entity headers * @param rawStatus the status code value * @since 5.3.2 + * @deprecated Use {@link #ResponseEntity(Object, HttpHeaders, int)} */ + @Deprecated public ResponseEntity(@Nullable T body, @Nullable MultiValueMap headers, int rawStatus) { this(body, headers, HttpStatusCode.valueOf(rawStatus)); } @@ -125,7 +164,9 @@ public class ResponseEntity extends HttpEntity { * @param body the entity body * @param headers the entity headers * @param statusCode the status code + * @deprecated Use {@link #ResponseEntity(Object, HttpHeaders, HttpStatusCode)} */ + @Deprecated public ResponseEntity(@Nullable T body, @Nullable MultiValueMap headers, HttpStatusCode statusCode) { super(body, headers); Assert.notNull(statusCode, "HttpStatusCode must not be null"); diff --git a/spring-web/src/main/java/org/springframework/http/client/MultipartBodyBuilder.java b/spring-web/src/main/java/org/springframework/http/client/MultipartBodyBuilder.java index 01223b32fa..cd4cd88960 100644 --- a/spring-web/src/main/java/org/springframework/http/client/MultipartBodyBuilder.java +++ b/spring-web/src/main/java/org/springframework/http/client/MultipartBodyBuilder.java @@ -366,7 +366,7 @@ public final class MultipartBodyBuilder { private final ResolvableType resolvableType; PublisherEntity( - @Nullable MultiValueMap headers, P publisher, ResolvableType resolvableType) { + @Nullable HttpHeaders headers, P publisher, ResolvableType resolvableType) { super(publisher, headers); Assert.notNull(publisher, "'publisher' must not be null"); diff --git a/spring-web/src/main/java/org/springframework/http/client/reactive/HttpComponentsClientHttpRequest.java b/spring-web/src/main/java/org/springframework/http/client/reactive/HttpComponentsClientHttpRequest.java index b2ac22eade..636912864a 100644 --- a/spring-web/src/main/java/org/springframework/http/client/reactive/HttpComponentsClientHttpRequest.java +++ b/spring-web/src/main/java/org/springframework/http/client/reactive/HttpComponentsClientHttpRequest.java @@ -125,7 +125,7 @@ class HttpComponentsClientHttpRequest extends AbstractClientHttpRequest { protected void applyHeaders() { HttpHeaders headers = getHeaders(); - headers.entrySet() + headers.headerSet() .stream() .filter(entry -> !HttpHeaders.CONTENT_LENGTH.equals(entry.getKey())) .forEach(entry -> entry.getValue().forEach(v -> this.httpRequest.addHeader(entry.getKey(), v))); diff --git a/spring-web/src/main/java/org/springframework/http/client/reactive/JdkClientHttpRequest.java b/spring-web/src/main/java/org/springframework/http/client/reactive/JdkClientHttpRequest.java index 8130238fa7..678a2dcf19 100644 --- a/spring-web/src/main/java/org/springframework/http/client/reactive/JdkClientHttpRequest.java +++ b/spring-web/src/main/java/org/springframework/http/client/reactive/JdkClientHttpRequest.java @@ -99,7 +99,7 @@ class JdkClientHttpRequest extends AbstractClientHttpRequest { @Override protected void applyHeaders() { - for (Map.Entry> entry : getHeaders().entrySet()) { + for (Map.Entry> entry : getHeaders().headerSet()) { if (entry.getKey().equalsIgnoreCase(HttpHeaders.CONTENT_LENGTH)) { // content-length is specified when writing continue; @@ -108,7 +108,7 @@ class JdkClientHttpRequest extends AbstractClientHttpRequest { this.builder.header(entry.getKey(), value); } } - if (!getHeaders().containsKey(HttpHeaders.ACCEPT)) { + if (!getHeaders().containsHeader(HttpHeaders.ACCEPT)) { this.builder.header(HttpHeaders.ACCEPT, "*/*"); } } diff --git a/spring-web/src/main/java/org/springframework/http/client/reactive/JettyClientHttpRequest.java b/spring-web/src/main/java/org/springframework/http/client/reactive/JettyClientHttpRequest.java index 573e34e4d6..89b8cef4ab 100644 --- a/spring-web/src/main/java/org/springframework/http/client/reactive/JettyClientHttpRequest.java +++ b/spring-web/src/main/java/org/springframework/http/client/reactive/JettyClientHttpRequest.java @@ -143,7 +143,7 @@ class JettyClientHttpRequest extends AbstractClientHttpRequest { HttpHeaders headers = getHeaders(); this.jettyRequest.headers(fields -> { headers.forEach((key, value) -> value.forEach(v -> fields.add(key, v))); - if (!headers.containsKey(HttpHeaders.ACCEPT)) { + if (!headers.containsHeader(HttpHeaders.ACCEPT)) { fields.add(HttpHeaders.ACCEPT, "*/*"); } }); diff --git a/spring-web/src/main/java/org/springframework/http/client/support/BasicAuthenticationInterceptor.java b/spring-web/src/main/java/org/springframework/http/client/support/BasicAuthenticationInterceptor.java index d812a8d31e..e155d07dc1 100644 --- a/spring-web/src/main/java/org/springframework/http/client/support/BasicAuthenticationInterceptor.java +++ b/spring-web/src/main/java/org/springframework/http/client/support/BasicAuthenticationInterceptor.java @@ -74,7 +74,7 @@ public class BasicAuthenticationInterceptor implements ClientHttpRequestIntercep HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException { HttpHeaders headers = request.getHeaders(); - if (!headers.containsKey(HttpHeaders.AUTHORIZATION)) { + if (!headers.containsHeader(HttpHeaders.AUTHORIZATION)) { headers.setBasicAuth(this.encodedCredentials); } return execution.execute(request, body); diff --git a/spring-web/src/main/java/org/springframework/http/codec/multipart/MultipartHttpMessageWriter.java b/spring-web/src/main/java/org/springframework/http/codec/multipart/MultipartHttpMessageWriter.java index cf7be2ea1b..5a12a77ba2 100644 --- a/spring-web/src/main/java/org/springframework/http/codec/multipart/MultipartHttpMessageWriter.java +++ b/spring-web/src/main/java/org/springframework/http/codec/multipart/MultipartHttpMessageWriter.java @@ -261,7 +261,7 @@ public class MultipartHttpMessageWriter extends MultipartWriterSupport resolvableType = ResolvableType.forClass(body.getClass()); } - if (!headers.containsKey(HttpHeaders.CONTENT_DISPOSITION)) { + if (!headers.containsHeader(HttpHeaders.CONTENT_DISPOSITION)) { if (body instanceof Resource resource) { headers.setContentDispositionFormData(name, resource.getFilename()); } diff --git a/spring-web/src/main/java/org/springframework/http/codec/multipart/MultipartWriterSupport.java b/spring-web/src/main/java/org/springframework/http/codec/multipart/MultipartWriterSupport.java index dba5351a73..0f5658f215 100644 --- a/spring-web/src/main/java/org/springframework/http/codec/multipart/MultipartWriterSupport.java +++ b/spring-web/src/main/java/org/springframework/http/codec/multipart/MultipartWriterSupport.java @@ -150,7 +150,7 @@ public class MultipartWriterSupport extends LoggingCodecSupport { return Mono.fromCallable(() -> { @SuppressWarnings("resource") FastByteArrayOutputStream bos = new FastByteArrayOutputStream(); - for (Map.Entry> entry : headers.entrySet()) { + for (Map.Entry> entry : headers.headerSet()) { byte[] headerName = entry.getKey().getBytes(getCharset()); for (String headerValueString : entry.getValue()) { byte[] headerValue = headerValueString.getBytes(getCharset()); diff --git a/spring-web/src/main/java/org/springframework/http/codec/multipart/PartHttpMessageWriter.java b/spring-web/src/main/java/org/springframework/http/codec/multipart/PartHttpMessageWriter.java index 76cd4b6751..fe33e3b211 100644 --- a/spring-web/src/main/java/org/springframework/http/codec/multipart/PartHttpMessageWriter.java +++ b/spring-web/src/main/java/org/springframework/http/codec/multipart/PartHttpMessageWriter.java @@ -94,7 +94,7 @@ public class PartHttpMessageWriter extends MultipartWriterSupport implements Htt HttpHeaders headers = new HttpHeaders(part.headers()); String name = part.name(); - if (!headers.containsKey(HttpHeaders.CONTENT_DISPOSITION)) { + if (!headers.containsHeader(HttpHeaders.CONTENT_DISPOSITION)) { headers.setContentDispositionFormData(name, (part instanceof FilePart filePart ? filePart.filename() : null)); } diff --git a/spring-web/src/main/java/org/springframework/http/converter/AbstractHttpMessageConverter.java b/spring-web/src/main/java/org/springframework/http/converter/AbstractHttpMessageConverter.java index 8c3c280d40..6289bd650f 100644 --- a/spring-web/src/main/java/org/springframework/http/converter/AbstractHttpMessageConverter.java +++ b/spring-web/src/main/java/org/springframework/http/converter/AbstractHttpMessageConverter.java @@ -262,7 +262,7 @@ public abstract class AbstractHttpMessageConverter implements HttpMessageConv headers.setContentType(contentTypeToUse); } } - if (headers.getContentLength() < 0 && !headers.containsKey(HttpHeaders.TRANSFER_ENCODING)) { + if (headers.getContentLength() < 0 && !headers.containsHeader(HttpHeaders.TRANSFER_ENCODING)) { Long contentLength = getContentLength(t, headers.getContentType()); if (contentLength != null) { headers.setContentLength(contentLength); diff --git a/spring-web/src/main/java/org/springframework/http/converter/FormHttpMessageConverter.java b/spring-web/src/main/java/org/springframework/http/converter/FormHttpMessageConverter.java index be83a29f92..ca1e374011 100644 --- a/spring-web/src/main/java/org/springframework/http/converter/FormHttpMessageConverter.java +++ b/spring-web/src/main/java/org/springframework/http/converter/FormHttpMessageConverter.java @@ -645,7 +645,7 @@ public class FormHttpMessageConverter implements HttpMessageConverter> entry : this.headers.entrySet()) { + for (Map.Entry> entry : this.headers.headerSet()) { byte[] headerName = getBytes(entry.getKey()); for (String headerValueString : entry.getValue()) { byte[] headerValue = getBytes(headerValueString); diff --git a/spring-web/src/main/java/org/springframework/http/server/ServletServerHttpRequest.java b/spring-web/src/main/java/org/springframework/http/server/ServletServerHttpRequest.java index 32da447629..a056a68a01 100644 --- a/spring-web/src/main/java/org/springframework/http/server/ServletServerHttpRequest.java +++ b/spring-web/src/main/java/org/springframework/http/server/ServletServerHttpRequest.java @@ -291,7 +291,7 @@ public class ServletServerHttpRequest implements ServerHttpRequest { writer.flush(); byte[] bytes = bos.toByteArray(); - if (bytes.length > 0 && getHeaders().containsKey(HttpHeaders.CONTENT_LENGTH)) { + if (bytes.length > 0 && getHeaders().containsHeader(HttpHeaders.CONTENT_LENGTH)) { getHeaders().setContentLength(bytes.length); } diff --git a/spring-web/src/main/java/org/springframework/http/server/ServletServerHttpResponse.java b/spring-web/src/main/java/org/springframework/http/server/ServletServerHttpResponse.java index f09d8a9018..e0b9581adf 100644 --- a/spring-web/src/main/java/org/springframework/http/server/ServletServerHttpResponse.java +++ b/spring-web/src/main/java/org/springframework/http/server/ServletServerHttpResponse.java @@ -151,8 +151,8 @@ public class ServletServerHttpResponse implements ServerHttpResponse { private static final long serialVersionUID = 3410708522401046302L; @Override - public boolean containsKey(Object key) { - return (super.containsKey(key) || (get(key) != null)); + public boolean containsHeader(String key) { + return (super.containsHeader(key) || (get(key) != null)); } @Override @@ -169,10 +169,7 @@ public class ServletServerHttpResponse implements ServerHttpResponse { } @Override - public @Nullable List get(Object key) { - Assert.isInstanceOf(String.class, key, "Key must be a String-based header name"); - - String headerName = (String) key; + public @Nullable List get(String headerName) { if (headerName.equalsIgnoreCase(CONTENT_TYPE)) { // Content-Type is written as an override so don't merge String value = getFirst(headerName); @@ -185,7 +182,7 @@ public class ServletServerHttpResponse implements ServerHttpResponse { } boolean isEmpty1 = CollectionUtils.isEmpty(values1); - List values2 = super.get(key); + List values2 = super.get(headerName); boolean isEmpty2 = CollectionUtils.isEmpty(values2); if (isEmpty1 && isEmpty2) { diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/AbstractServerHttpRequest.java b/spring-web/src/main/java/org/springframework/http/server/reactive/AbstractServerHttpRequest.java index 19abe1646b..4cb0d82917 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/AbstractServerHttpRequest.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/AbstractServerHttpRequest.java @@ -79,9 +79,24 @@ public abstract class AbstractServerHttpRequest implements ServerHttpRequest { * @param contextPath the context path for the request * @param headers the headers for the request (as {@link MultiValueMap}) * @since 6.0.8 + * @deprecated Use {@link #AbstractServerHttpRequest(HttpMethod, URI, String, HttpHeaders)} */ + @Deprecated public AbstractServerHttpRequest(HttpMethod method, URI uri, @Nullable String contextPath, MultiValueMap headers) { + this(method, uri, contextPath, new HttpHeaders(headers)); + } + + /** + * Constructor with the method, URI and headers for the request. + * @param method the HTTP method for the request + * @param uri the URI for the request + * @param contextPath the context path for the request + * @param headers the headers for the request (as {@link MultiValueMap}) + * @since 7.0 + */ + public AbstractServerHttpRequest(HttpMethod method, URI uri, @Nullable String contextPath, + HttpHeaders headers) { Assert.notNull(method, "Method must not be null"); Assert.notNull(uri, "Uri must not be null"); diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/ReactorNetty2ServerHttpRequest.java b/spring-web/src/main/java/org/springframework/http/server/reactive/ReactorNetty2ServerHttpRequest.java index f9dadffcc9..833e67aaa0 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/ReactorNetty2ServerHttpRequest.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/ReactorNetty2ServerHttpRequest.java @@ -37,6 +37,7 @@ import reactor.netty5.http.server.HttpServerRequest; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.Netty5DataBufferFactory; import org.springframework.http.HttpCookie; +import org.springframework.http.HttpHeaders; import org.springframework.http.HttpLogging; import org.springframework.http.HttpMethod; import org.springframework.http.support.Netty5HeadersAdapter; @@ -70,7 +71,7 @@ class ReactorNetty2ServerHttpRequest extends AbstractServerHttpRequest { throws URISyntaxException { super(HttpMethod.valueOf(request.method().name()), initUri(request), "", - new Netty5HeadersAdapter(request.requestHeaders())); + new HttpHeaders(new Netty5HeadersAdapter(request.requestHeaders()))); Assert.notNull(bufferFactory, "DataBufferFactory must not be null"); this.request = request; this.bufferFactory = bufferFactory; diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/ReactorServerHttpRequest.java b/spring-web/src/main/java/org/springframework/http/server/reactive/ReactorServerHttpRequest.java index 6f71068a90..53715fb248 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/ReactorServerHttpRequest.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/ReactorServerHttpRequest.java @@ -35,6 +35,7 @@ import reactor.netty.http.server.HttpServerRequest; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.NettyDataBufferFactory; import org.springframework.http.HttpCookie; +import org.springframework.http.HttpHeaders; import org.springframework.http.HttpLogging; import org.springframework.http.HttpMethod; import org.springframework.http.support.Netty4HeadersAdapter; @@ -67,7 +68,7 @@ class ReactorServerHttpRequest extends AbstractServerHttpRequest { super(HttpMethod.valueOf(request.method().name()), ReactorUriHelper.createUri(request), request.forwardedPrefix(), - new Netty4HeadersAdapter(request.requestHeaders())); + new HttpHeaders(new Netty4HeadersAdapter(request.requestHeaders()))); Assert.notNull(bufferFactory, "DataBufferFactory must not be null"); this.request = request; this.bufferFactory = bufferFactory; diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/ServletServerHttpRequest.java b/spring-web/src/main/java/org/springframework/http/server/reactive/ServletServerHttpRequest.java index a5bd9df508..d205165ef8 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/ServletServerHttpRequest.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/ServletServerHttpRequest.java @@ -24,7 +24,6 @@ import java.nio.ByteBuffer; import java.nio.charset.Charset; import java.security.cert.X509Certificate; import java.util.Enumeration; -import java.util.Locale; import java.util.Map; import jakarta.servlet.AsyncContext; @@ -47,7 +46,6 @@ import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; import org.springframework.http.MediaType; import org.springframework.util.Assert; -import org.springframework.util.CollectionUtils; import org.springframework.util.LinkedCaseInsensitiveMap; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; @@ -89,7 +87,7 @@ class ServletServerHttpRequest extends AbstractServerHttpRequest { this(createDefaultHttpHeaders(request), request, asyncContext, servletPath, bufferFactory, bufferSize); } - public ServletServerHttpRequest(MultiValueMap headers, HttpServletRequest request, + public ServletServerHttpRequest(HttpHeaders headers, HttpServletRequest request, AsyncContext asyncContext, String servletPath, DataBufferFactory bufferFactory, int bufferSize) throws IOException, URISyntaxException { @@ -112,9 +110,8 @@ class ServletServerHttpRequest extends AbstractServerHttpRequest { } - private static MultiValueMap createDefaultHttpHeaders(HttpServletRequest request) { - MultiValueMap headers = - CollectionUtils.toMultiValueMap(new LinkedCaseInsensitiveMap<>(8, Locale.ROOT)); + private static HttpHeaders createDefaultHttpHeaders(HttpServletRequest request) { + HttpHeaders headers = new HttpHeaders(); for (Enumeration names = request.getHeaderNames(); names.hasMoreElements(); ) { String name = (String) names.nextElement(); for (Enumeration values = request.getHeaders(name); values.hasMoreElements(); ) { @@ -163,8 +160,7 @@ class ServletServerHttpRequest extends AbstractServerHttpRequest { } @SuppressWarnings("NullAway") - private static MultiValueMap initHeaders( - MultiValueMap headerValues, HttpServletRequest request) { + private static HttpHeaders initHeaders(HttpHeaders headerValues, HttpServletRequest request) { HttpHeaders headers = null; MediaType contentType = null; diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/TomcatHttpHandlerAdapter.java b/spring-web/src/main/java/org/springframework/http/server/reactive/TomcatHttpHandlerAdapter.java index aaae97ba03..6fd3af0148 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/TomcatHttpHandlerAdapter.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/TomcatHttpHandlerAdapter.java @@ -34,7 +34,6 @@ import org.apache.coyote.Response; import org.springframework.core.io.buffer.DataBufferFactory; import org.springframework.http.HttpHeaders; import org.springframework.util.Assert; -import org.springframework.util.MultiValueMap; import org.springframework.util.ReflectionUtils; /** @@ -91,13 +90,13 @@ public class TomcatHttpHandlerAdapter extends ServletHttpHandlerAdapter { super(createTomcatHttpHeaders(request), request, context, servletPath, factory, bufferSize); } - private static MultiValueMap createTomcatHttpHeaders(HttpServletRequest request) { + private static HttpHeaders createTomcatHttpHeaders(HttpServletRequest request) { RequestFacade requestFacade = getRequestFacade(request); org.apache.catalina.connector.Request connectorRequest = (org.apache.catalina.connector.Request) ReflectionUtils.getField(COYOTE_REQUEST_FIELD, requestFacade); Assert.state(connectorRequest != null, "No Tomcat connector request"); Request tomcatRequest = connectorRequest.getCoyoteRequest(); - return new TomcatHeadersAdapter(tomcatRequest.getMimeHeaders()); + return new HttpHeaders(new TomcatHeadersAdapter(tomcatRequest.getMimeHeaders())); } private static RequestFacade getRequestFacade(HttpServletRequest request) { diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/UndertowServerHttpRequest.java b/spring-web/src/main/java/org/springframework/http/server/reactive/UndertowServerHttpRequest.java index 008023ee74..e1c6494fe6 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/UndertowServerHttpRequest.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/UndertowServerHttpRequest.java @@ -36,6 +36,7 @@ import reactor.core.publisher.Flux; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DataBufferFactory; import org.springframework.http.HttpCookie; +import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; import org.springframework.util.Assert; import org.springframework.util.LinkedMultiValueMap; @@ -65,7 +66,7 @@ class UndertowServerHttpRequest extends AbstractServerHttpRequest { throws URISyntaxException { super(HttpMethod.valueOf(exchange.getRequestMethod().toString()), initUri(exchange), "", - new UndertowHeadersAdapter(exchange.getRequestHeaders())); + new HttpHeaders(new UndertowHeadersAdapter(exchange.getRequestHeaders()))); this.exchange = exchange; this.body = new RequestBodyPublisher(exchange, bufferFactory); this.body.registerListeners(exchange); diff --git a/spring-web/src/main/java/org/springframework/web/client/DefaultRestClient.java b/spring-web/src/main/java/org/springframework/web/client/DefaultRestClient.java index 3660a6f30b..d548c14eb5 100644 --- a/spring-web/src/main/java/org/springframework/web/client/DefaultRestClient.java +++ b/spring-web/src/main/java/org/springframework/web/client/DefaultRestClient.java @@ -623,10 +623,10 @@ final class DefaultRestClient implements RestClient { private @Nullable HttpHeaders initHeaders() { HttpHeaders defaultHeaders = DefaultRestClient.this.defaultHeaders; - if (CollectionUtils.isEmpty(this.headers)) { + if (this.headers == null || this.headers.isEmpty()) { return defaultHeaders; } - else if (CollectionUtils.isEmpty(defaultHeaders)) { + else if (defaultHeaders == null || defaultHeaders.isEmpty()) { return this.headers; } else { diff --git a/spring-web/src/main/java/org/springframework/web/cors/DefaultCorsProcessor.java b/spring-web/src/main/java/org/springframework/web/cors/DefaultCorsProcessor.java index 2bbd283c09..f6cedf292a 100644 --- a/spring-web/src/main/java/org/springframework/web/cors/DefaultCorsProcessor.java +++ b/spring-web/src/main/java/org/springframework/web/cors/DefaultCorsProcessor.java @@ -220,7 +220,7 @@ public class DefaultCorsProcessor implements CorsProcessor { private List getHeadersToUse(ServerHttpRequest request, boolean isPreFlight) { HttpHeaders headers = request.getHeaders(); - return (isPreFlight ? headers.getAccessControlRequestHeaders() : new ArrayList<>(headers.keySet())); + return (isPreFlight ? headers.getAccessControlRequestHeaders() : new ArrayList<>(headers.headerNames())); } } diff --git a/spring-web/src/main/java/org/springframework/web/cors/reactive/CorsUtils.java b/spring-web/src/main/java/org/springframework/web/cors/reactive/CorsUtils.java index e50839398c..4961315566 100644 --- a/spring-web/src/main/java/org/springframework/web/cors/reactive/CorsUtils.java +++ b/spring-web/src/main/java/org/springframework/web/cors/reactive/CorsUtils.java @@ -42,7 +42,7 @@ public abstract class CorsUtils { */ @SuppressWarnings("deprecation") public static boolean isCorsRequest(ServerHttpRequest request) { - return request.getHeaders().containsKey(HttpHeaders.ORIGIN) && !isSameOrigin(request); + return request.getHeaders().containsHeader(HttpHeaders.ORIGIN) && !isSameOrigin(request); } /** @@ -52,8 +52,8 @@ public abstract class CorsUtils { public static boolean isPreFlightRequest(ServerHttpRequest request) { HttpHeaders headers = request.getHeaders(); return (request.getMethod() == HttpMethod.OPTIONS - && headers.containsKey(HttpHeaders.ORIGIN) - && headers.containsKey(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD)); + && headers.containsHeader(HttpHeaders.ORIGIN) + && headers.containsHeader(HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD)); } /** diff --git a/spring-web/src/main/java/org/springframework/web/cors/reactive/DefaultCorsProcessor.java b/spring-web/src/main/java/org/springframework/web/cors/reactive/DefaultCorsProcessor.java index 369a32fd93..87d273769c 100644 --- a/spring-web/src/main/java/org/springframework/web/cors/reactive/DefaultCorsProcessor.java +++ b/spring-web/src/main/java/org/springframework/web/cors/reactive/DefaultCorsProcessor.java @@ -217,7 +217,7 @@ public class DefaultCorsProcessor implements CorsProcessor { private List getHeadersToUse(ServerHttpRequest request, boolean isPreFlight) { HttpHeaders headers = request.getHeaders(); - return (isPreFlight ? headers.getAccessControlRequestHeaders() : new ArrayList<>(headers.keySet())); + return (isPreFlight ? headers.getAccessControlRequestHeaders() : new ArrayList<>(headers.headerNames())); } } diff --git a/spring-web/src/main/java/org/springframework/web/method/annotation/RequestHeaderMapMethodArgumentResolver.java b/spring-web/src/main/java/org/springframework/web/method/annotation/RequestHeaderMapMethodArgumentResolver.java index 33ec1e7398..5ba3e1f207 100644 --- a/spring-web/src/main/java/org/springframework/web/method/annotation/RequestHeaderMapMethodArgumentResolver.java +++ b/spring-web/src/main/java/org/springframework/web/method/annotation/RequestHeaderMapMethodArgumentResolver.java @@ -19,6 +19,7 @@ package org.springframework.web.method.annotation; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.Map; +import java.util.function.BiConsumer; import org.jspecify.annotations.Nullable; @@ -33,13 +34,14 @@ import org.springframework.web.method.support.HandlerMethodArgumentResolver; import org.springframework.web.method.support.ModelAndViewContainer; /** - * Resolves {@link Map} method arguments annotated with {@code @RequestHeader}. - * For individual header values annotated with {@code @RequestHeader} see + * Resolves {@link Map}, {@link MultiValueMap} and {@link HttpHeaders} method + * arguments annotated with {@code @RequestHeader}. For individual header values + * annotated with {@code @RequestHeader} see * {@link RequestHeaderMethodArgumentResolver} instead. * *

The created {@link Map} contains all request header name/value pairs. - * The method parameter type may be a {@link MultiValueMap} to receive all - * values for a header, not only the first one. + * The method parameter type may be a {@link HttpHeaders} or a {@link MultiValueMap} + * to receive all values for a header, not only the first one. * * @author Arjen Poutsma * @author Rossen Stoyanchev @@ -50,7 +52,8 @@ public class RequestHeaderMapMethodArgumentResolver implements HandlerMethodArgu @Override public boolean supportsParameter(MethodParameter parameter) { return (parameter.hasParameterAnnotation(RequestHeader.class) && - Map.class.isAssignableFrom(parameter.getParameterType())); + (Map.class.isAssignableFrom(parameter.getParameterType()) || + HttpHeaders.class.isAssignableFrom(parameter.getParameterType()))); } @Override @@ -58,23 +61,15 @@ public class RequestHeaderMapMethodArgumentResolver implements HandlerMethodArgu NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception { Class paramType = parameter.getParameterType(); - if (MultiValueMap.class.isAssignableFrom(paramType)) { - MultiValueMap result; - if (HttpHeaders.class.isAssignableFrom(paramType)) { - result = new HttpHeaders(); - } - else { - result = new LinkedMultiValueMap<>(); - } - for (Iterator iterator = webRequest.getHeaderNames(); iterator.hasNext();) { - String headerName = iterator.next(); - String[] headerValues = webRequest.getHeaderValues(headerName); - if (headerValues != null) { - for (String headerValue : headerValues) { - result.add(headerName, headerValue); - } - } - } + + if (HttpHeaders.class.isAssignableFrom(paramType)) { + HttpHeaders result = new HttpHeaders(); + copyHeaderValues(webRequest, result::add); + return result; + } + else if (MultiValueMap.class.isAssignableFrom(paramType)) { + MultiValueMap result = new LinkedMultiValueMap<>(); + copyHeaderValues(webRequest, result::add); return result; } else { @@ -90,4 +85,16 @@ public class RequestHeaderMapMethodArgumentResolver implements HandlerMethodArgu } } + void copyHeaderValues(NativeWebRequest webRequest, BiConsumer consumer) { + for (Iterator iterator = webRequest.getHeaderNames(); iterator.hasNext();) { + String headerName = iterator.next(); + String[] headerValues = webRequest.getHeaderValues(headerName); + if (headerValues != null) { + for (String headerValue : headerValues) { + consumer.accept(headerName, headerValue); + } + } + } + } + } diff --git a/spring-web/src/main/java/org/springframework/web/method/annotation/RequestHeaderMethodArgumentResolver.java b/spring-web/src/main/java/org/springframework/web/method/annotation/RequestHeaderMethodArgumentResolver.java index 45e9129ecd..f068f70990 100644 --- a/spring-web/src/main/java/org/springframework/web/method/annotation/RequestHeaderMethodArgumentResolver.java +++ b/spring-web/src/main/java/org/springframework/web/method/annotation/RequestHeaderMethodArgumentResolver.java @@ -22,6 +22,7 @@ import org.jspecify.annotations.Nullable; import org.springframework.beans.factory.config.ConfigurableBeanFactory; import org.springframework.core.MethodParameter; +import org.springframework.http.HttpHeaders; import org.springframework.util.Assert; import org.springframework.web.bind.MissingRequestHeaderException; import org.springframework.web.bind.ServletRequestBindingException; @@ -61,7 +62,8 @@ public class RequestHeaderMethodArgumentResolver extends AbstractNamedValueMetho @Override public boolean supportsParameter(MethodParameter parameter) { return (parameter.hasParameterAnnotation(RequestHeader.class) && - !Map.class.isAssignableFrom(parameter.nestedIfOptional().getNestedParameterType())); + !Map.class.isAssignableFrom(parameter.nestedIfOptional().getNestedParameterType())) && + !HttpHeaders.class.isAssignableFrom(parameter.nestedIfOptional().getNestedParameterType()); } @Override diff --git a/spring-web/src/main/java/org/springframework/web/server/adapter/ForwardedHeaderTransformer.java b/spring-web/src/main/java/org/springframework/web/server/adapter/ForwardedHeaderTransformer.java index 0a00a58d28..9c0ef3913a 100644 --- a/spring-web/src/main/java/org/springframework/web/server/adapter/ForwardedHeaderTransformer.java +++ b/spring-web/src/main/java/org/springframework/web/server/adapter/ForwardedHeaderTransformer.java @@ -144,7 +144,7 @@ public class ForwardedHeaderTransformer implements Function entity = new HttpEntity<>(body); assertThat(entity.getBody()).isSameAs(body); - assertThat(entity.getHeaders()).isEmpty(); + assertThat(entity.getHeaders().isEmpty()).as("isEmpty").isTrue(); } @Test @@ -62,25 +62,25 @@ class HttpEntityTests { @Test void testEquals() { - MultiValueMap map1 = new LinkedMultiValueMap<>(); - map1.set("Content-Type", "text/plain"); + HttpHeaders headers1 = new HttpHeaders(); + headers1.set("Content-Type", "text/plain"); - MultiValueMap map2 = new LinkedMultiValueMap<>(); - map2.set("Content-Type", "application/json"); + HttpHeaders headers2 = new HttpHeaders(); + headers2.set("Content-Type", "application/json"); assertThat(new HttpEntity<>().equals(new HttpEntity<>())).isTrue(); - assertThat(new HttpEntity<>(map1).equals(new HttpEntity<>())).isFalse(); - assertThat(new HttpEntity<>().equals(new HttpEntity<>(map2))).isFalse(); + assertThat(new HttpEntity<>(headers1).equals(new HttpEntity<>())).isFalse(); + assertThat(new HttpEntity<>().equals(new HttpEntity<>(headers2))).isFalse(); - assertThat(new HttpEntity<>(map1).equals(new HttpEntity<>(map1))).isTrue(); - assertThat(new HttpEntity<>(map1).equals(new HttpEntity<>(map2))).isFalse(); + assertThat(new HttpEntity<>(headers1).equals(new HttpEntity<>(headers1))).isTrue(); + assertThat(new HttpEntity<>(headers1).equals(new HttpEntity<>(headers2))).isFalse(); - assertThat(new HttpEntity(null, null).equals(new HttpEntity<>(null, null))).isTrue(); - assertThat(new HttpEntity<>("foo", null).equals(new HttpEntity<>(null, null))).isFalse(); - assertThat(new HttpEntity(null, null).equals(new HttpEntity<>("bar", null))).isFalse(); + assertThat(new HttpEntity(null, (HttpHeaders) null).equals(new HttpEntity<>(null, (HttpHeaders) null))).isTrue(); + assertThat(new HttpEntity<>("foo", (HttpHeaders) null).equals(new HttpEntity<>(null, (HttpHeaders) null))).isFalse(); + assertThat(new HttpEntity(null, (HttpHeaders) null).equals(new HttpEntity<>("bar", (HttpHeaders) null))).isFalse(); - assertThat(new HttpEntity<>("foo", map1).equals(new HttpEntity<>("foo", map1))).isTrue(); - assertThat(new HttpEntity<>("foo", map1).equals(new HttpEntity<>("bar", map1))).isFalse(); + assertThat(new HttpEntity<>("foo", headers1).equals(new HttpEntity<>("foo", headers1))).isTrue(); + assertThat(new HttpEntity<>("foo", headers1).equals(new HttpEntity<>("bar", headers1))).isFalse(); } @Test diff --git a/spring-web/src/test/java/org/springframework/http/HttpHeadersTests.java b/spring-web/src/test/java/org/springframework/http/HttpHeadersTests.java index 2507b4e5d8..9045bcdfef 100644 --- a/spring-web/src/test/java/org/springframework/http/HttpHeadersTests.java +++ b/spring-web/src/test/java/org/springframework/http/HttpHeadersTests.java @@ -645,7 +645,7 @@ class HttpHeadersTests { void keySetOperations() { headers.add("Alpha", "apple"); headers.add("Bravo", "banana"); - Set keySet = headers.keySet(); + Set keySet = headers.headerNames(); // Please DO NOT simplify the following with AssertJ's fluent API. // @@ -677,17 +677,17 @@ class HttpHeadersTests { // remove() assertThat(keySet.remove("Alpha")).isTrue(); assertThat(keySet).hasSize(1); - assertThat(headers).hasSize(1); + assertThat(headers.size()).isOne(); assertThat(keySet.remove("Alpha")).isFalse(); assertThat(keySet).hasSize(1); - assertThat(headers).hasSize(1); + assertThat(headers.size()).isOne(); // clear() keySet.clear(); assertThat(keySet).isEmpty(); assertThat(keySet).isEmpty(); - assertThat(headers).isEmpty(); - assertThat(headers).isEmpty(); + assertThat(headers.isEmpty()).as("isEmpty").isTrue(); + assertThat(headers.isEmpty()).as("isEmpty").isTrue(); // Unsupported operations assertThatExceptionOfType(UnsupportedOperationException.class) @@ -705,10 +705,10 @@ class HttpHeadersTests { // --- Given --- headers.add("Alpha", "apple"); headers.add("Bravo", "banana"); - assertThat(headers).containsOnlyKeys("Alpha", "Bravo"); + assertThat(headers.headerNames()).containsOnly("Alpha", "Bravo"); // --- When --- - boolean removed = headers.keySet().remove("Alpha"); + boolean removed = headers.headerNames().remove("Alpha"); // --- Then --- @@ -718,12 +718,12 @@ class HttpHeadersTests { // the behavior of the entire contract. assertThat(removed).isTrue(); - assertThat(headers.keySet().remove("Alpha")).isFalse(); - assertThat(headers).hasSize(1); - assertThat(headers.containsKey("Alpha")).as("Alpha should have been removed").isFalse(); - assertThat(headers.containsKey("Bravo")).as("Bravo should be present").isTrue(); - assertThat(headers.keySet()).containsOnly("Bravo"); - assertThat(headers.entrySet()).containsOnly(entry("Bravo", List.of("banana"))); + assertThat(headers.headerNames().remove("Alpha")).isFalse(); + assertThat(headers.size()).isOne(); + assertThat(headers.containsHeader("Alpha")).as("Alpha should have been removed").isFalse(); + assertThat(headers.containsHeader("Bravo")).as("Bravo should be present").isTrue(); + assertThat(headers.headerNames()).containsOnly("Bravo"); + assertThat(headers.headerSet()).containsOnly(entry("Bravo", List.of("banana"))); } @Test @@ -731,11 +731,11 @@ class HttpHeadersTests { String headerName = "MyHeader"; String headerValue = "value"; - assertThat(headers).isEmpty(); + assertThat(headers.isEmpty()).as("isEmpty").isTrue(); headers.add(headerName, headerValue); - assertThat(headers.containsKey(headerName)).isTrue(); - headers.keySet().removeIf(key -> key.equals(headerName)); - assertThat(headers).isEmpty(); + assertThat(headers.containsHeader(headerName)).isTrue(); + headers.headerNames().removeIf(key -> key.equals(headerName)); + assertThat(headers.isEmpty()).as("isEmpty").isTrue(); headers.add(headerName, headerValue); assertThat(headers.get(headerName)).containsExactly(headerValue); } @@ -745,11 +745,11 @@ class HttpHeadersTests { String headerName = "MyHeader"; String headerValue = "value"; - assertThat(headers).isEmpty(); + assertThat(headers.isEmpty()).as("isEmpty").isTrue(); headers.add(headerName, headerValue); - assertThat(headers.containsKey(headerName)).isTrue(); - headers.entrySet().removeIf(entry -> entry.getKey().equals(headerName)); - assertThat(headers).isEmpty(); + assertThat(headers.containsHeader(headerName)).isTrue(); + headers.headerSet().removeIf(entry -> entry.getKey().equals(headerName)); + assertThat(headers.isEmpty()).as("isEmpty").isTrue(); headers.add(headerName, headerValue); assertThat(headers.get(headerName)).containsExactly(headerValue); } @@ -764,10 +764,10 @@ class HttpHeadersTests { String[] expectedKeys = new String[] { "aardvark", "beaver", "cat", "dog", "elephant" }; - assertThat(headers.entrySet()).extracting(Entry::getKey).containsExactly(expectedKeys); + assertThat(headers.headerSet()).extracting(Entry::getKey).containsExactly(expectedKeys); HttpHeaders readOnlyHttpHeaders = HttpHeaders.readOnlyHttpHeaders(headers); - assertThat(readOnlyHttpHeaders.entrySet()).extracting(Entry::getKey).containsExactly(expectedKeys); + assertThat(readOnlyHttpHeaders.headerSet()).extracting(Entry::getKey).containsExactly(expectedKeys); } @Test @@ -784,15 +784,15 @@ class HttpHeadersTests { HttpHeaders forEachHeaders = new HttpHeaders(); readOnlyHttpHeaders.forEach(forEachHeaders::putIfAbsent); - assertThat(forEachHeaders.entrySet()).extracting(Entry::getKey).containsExactly(expectedKeys); + assertThat(forEachHeaders.headerSet()).extracting(Entry::getKey).containsExactly(expectedKeys); HttpHeaders putAllHeaders = new HttpHeaders(); putAllHeaders.putAll(readOnlyHttpHeaders); - assertThat(putAllHeaders.entrySet()).extracting(Entry::getKey).containsExactly(expectedKeys); + assertThat(putAllHeaders.headerSet()).extracting(Entry::getKey).containsExactly(expectedKeys); HttpHeaders addAllHeaders = new HttpHeaders(); addAllHeaders.addAll(readOnlyHttpHeaders); - assertThat(addAllHeaders.entrySet()).extracting(Entry::getKey).containsExactly(expectedKeys); + assertThat(addAllHeaders.headerSet()).extracting(Entry::getKey).containsExactly(expectedKeys); } @Test // gh-25034 diff --git a/spring-web/src/test/java/org/springframework/http/RequestEntityTests.java b/spring-web/src/test/java/org/springframework/http/RequestEntityTests.java index cda567b7fb..080c3e7dda 100644 --- a/spring-web/src/test/java/org/springframework/http/RequestEntityTests.java +++ b/spring-web/src/test/java/org/springframework/http/RequestEntityTests.java @@ -52,7 +52,7 @@ class RequestEntityTests { assertThat(requestEntity).isNotNull(); assertThat(requestEntity.getMethod()).isEqualTo(HttpMethod.GET); - assertThat(requestEntity.getHeaders().containsKey(headerName)).isTrue(); + assertThat(requestEntity.getHeaders().containsHeader(headerName)).isTrue(); assertThat(requestEntity.getHeaders().getFirst(headerName)).isEqualTo(headerValue); assertThat(requestEntity.getBody()).isEqualTo(entity); } @@ -100,7 +100,7 @@ class RequestEntityTests { assertThat(requestEntity).isNotNull(); assertThat(requestEntity.getMethod()).isEqualTo(HttpMethod.GET); - assertThat(requestEntity.getHeaders().containsKey(HttpHeaders.ACCEPT)).isTrue(); + assertThat(requestEntity.getHeaders().containsHeader(HttpHeaders.ACCEPT)).isTrue(); assertThat(requestEntity.getHeaders().getFirst(HttpHeaders.ACCEPT)).isEqualTo("image/gif, image/jpeg, image/png"); assertThat(requestEntity.getBody()).isNull(); } @@ -120,7 +120,7 @@ class RequestEntityTests { ifNoneMatch(ifNoneMatch). contentLength(contentLength). contentType(contentType). - headers(headers -> assertThat(headers).hasSize(6)). + headers(headers -> assertThat(headers.size()).isEqualTo(6)). build(); assertThat(responseEntity).isNotNull(); diff --git a/spring-web/src/test/java/org/springframework/http/ResponseEntityTests.java b/spring-web/src/test/java/org/springframework/http/ResponseEntityTests.java index 7c52e12edf..56659ce698 100644 --- a/spring-web/src/test/java/org/springframework/http/ResponseEntityTests.java +++ b/spring-web/src/test/java/org/springframework/http/ResponseEntityTests.java @@ -44,7 +44,7 @@ class ResponseEntityTests { assertThat(responseEntity).isNotNull(); assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.OK); - assertThat(responseEntity.getHeaders().containsKey(headerName)).isTrue(); + assertThat(responseEntity.getHeaders().containsHeader(headerName)).isTrue(); assertThat(responseEntity.getHeaders().get(headerName)).containsExactly(headerValue1, headerValue2); assertThat(responseEntity.getBody()).isEqualTo(entity); } @@ -113,7 +113,7 @@ class ResponseEntityTests { assertThat(responseEntity).isNotNull(); assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.CREATED); - assertThat(responseEntity.getHeaders().containsKey(HttpHeaders.LOCATION)).isTrue(); + assertThat(responseEntity.getHeaders().containsHeader(HttpHeaders.LOCATION)).isTrue(); assertThat(responseEntity.getHeaders().getFirst(HttpHeaders.LOCATION)).isEqualTo(location.toString()); assertThat(responseEntity.getBody()).isNull(); @@ -195,7 +195,7 @@ class ResponseEntityTests { location(location). contentLength(contentLength). contentType(contentType). - headers(headers -> assertThat(headers).hasSize(5)). + headers(headers -> assertThat(headers.size()).isEqualTo(5)). build(); assertThat(responseEntity).isNotNull(); @@ -236,7 +236,7 @@ class ResponseEntityTests { HttpHeaders responseHeaders = responseEntity.getHeaders(); assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.OK); - assertThat(responseHeaders).hasSize(1); + assertThat(responseHeaders.size()).isOne(); assertThat(responseHeaders.get("X-CustomHeader")).hasSize(1); assertThat(responseHeaders.getFirst("X-CustomHeader")).isEqualTo("vale"); @@ -250,7 +250,7 @@ class ResponseEntityTests { ResponseEntity.ok().headers((HttpHeaders) null).build(); assertThat(responseEntityWithEmptyHeaders.getStatusCode()).isEqualTo(HttpStatus.OK); - assertThat(responseEntityWithEmptyHeaders.getHeaders()).isEmpty(); + assertThat(responseEntityWithEmptyHeaders.getHeaders().isEmpty()).isTrue(); assertThat(responseEntityWithNullHeaders.toString()).isEqualTo(responseEntityWithEmptyHeaders.toString()); } @@ -265,7 +265,7 @@ class ResponseEntityTests { assertThat(responseEntity).isNotNull(); assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.OK); - assertThat(responseEntity.getHeaders().containsKey(HttpHeaders.CACHE_CONTROL)).isFalse(); + assertThat(responseEntity.getHeaders().containsHeader(HttpHeaders.CACHE_CONTROL)).isFalse(); assertThat(responseEntity.getBody()).isEqualTo(entity); } @@ -281,7 +281,7 @@ class ResponseEntityTests { assertThat(responseEntity).isNotNull(); assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.OK); - assertThat(responseEntity.getHeaders().containsKey(HttpHeaders.CACHE_CONTROL)).isTrue(); + assertThat(responseEntity.getHeaders().containsHeader(HttpHeaders.CACHE_CONTROL)).isTrue(); assertThat(responseEntity.getBody()).isEqualTo(entity); String cacheControlHeader = responseEntity.getHeaders().getCacheControl(); assertThat(cacheControlHeader).isEqualTo( @@ -299,7 +299,7 @@ class ResponseEntityTests { assertThat(responseEntity).isNotNull(); assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.OK); - assertThat(responseEntity.getHeaders().containsKey(HttpHeaders.CACHE_CONTROL)).isTrue(); + assertThat(responseEntity.getHeaders().containsHeader(HttpHeaders.CACHE_CONTROL)).isTrue(); assertThat(responseEntity.getBody()).isEqualTo(entity); String cacheControlHeader = responseEntity.getHeaders().getCacheControl(); diff --git a/spring-web/src/test/java/org/springframework/http/client/AbstractHttpRequestFactoryTests.java b/spring-web/src/test/java/org/springframework/http/client/AbstractHttpRequestFactoryTests.java index 22653ebca2..c9aa51bc70 100644 --- a/spring-web/src/test/java/org/springframework/http/client/AbstractHttpRequestFactoryTests.java +++ b/spring-web/src/test/java/org/springframework/http/client/AbstractHttpRequestFactoryTests.java @@ -98,9 +98,11 @@ abstract class AbstractHttpRequestFactoryTests extends AbstractMockWebServerTest try (ClientHttpResponse response = request.execute()) { assertThat(response.getStatusCode()).as("Invalid status code").isEqualTo(HttpStatus.OK); - assertThat(response.getHeaders()).as("Header not found").containsKey(headerName); - assertThat(response.getHeaders()).as("Header value not found") - .containsEntry(headerName, Arrays.asList(headerValue1, headerValue2)); + assertThat(response.getHeaders().containsHeader(headerName)) + .as("Header not found").isTrue(); + assertThat(response.getHeaders().hasHeaderValues(headerName, Arrays.asList(headerValue1, headerValue2))) + .as("Header value not found") + .isTrue(); byte[] result = FileCopyUtils.copyToByteArray(response.getBody()); assertThat(Arrays.equals(body, result)).as("Invalid body").isTrue(); } diff --git a/spring-web/src/test/java/org/springframework/http/client/BufferingClientHttpRequestFactoryTests.java b/spring-web/src/test/java/org/springframework/http/client/BufferingClientHttpRequestFactoryTests.java index 4716e351af..2355dc72c3 100644 --- a/spring-web/src/test/java/org/springframework/http/client/BufferingClientHttpRequestFactoryTests.java +++ b/spring-web/src/test/java/org/springframework/http/client/BufferingClientHttpRequestFactoryTests.java @@ -51,8 +51,8 @@ class BufferingClientHttpRequestFactoryTests extends AbstractHttpRequestFactoryT assertThat(response.getStatusCode()).as("Invalid status code").isEqualTo(HttpStatus.OK); assertThat(response.getStatusCode()).as("Invalid status code").isEqualTo(HttpStatus.OK); - assertThat(response.getHeaders().containsKey(headerName)).as("Header not found").isTrue(); - assertThat(response.getHeaders().containsKey(headerName)).as("Header not found").isTrue(); + assertThat(response.getHeaders().containsHeader(headerName)).as("Header not found").isTrue(); + assertThat(response.getHeaders().containsHeader(headerName)).as("Header not found").isTrue(); assertThat(response.getHeaders().get(headerName)).as("Header value not found").isEqualTo(Arrays.asList(headerValue1, headerValue2)); assertThat(response.getHeaders().get(headerName)).as("Header value not found").isEqualTo(Arrays.asList(headerValue1, headerValue2)); diff --git a/spring-web/src/test/java/org/springframework/http/codec/multipart/DefaultPartHttpMessageReaderTests.java b/spring-web/src/test/java/org/springframework/http/codec/multipart/DefaultPartHttpMessageReaderTests.java index 692f06a108..2ba573dda4 100644 --- a/spring-web/src/test/java/org/springframework/http/codec/multipart/DefaultPartHttpMessageReaderTests.java +++ b/spring-web/src/test/java/org/springframework/http/codec/multipart/DefaultPartHttpMessageReaderTests.java @@ -103,7 +103,7 @@ class DefaultPartHttpMessageReaderTests { StepVerifier.create(result) .consumeNextWith(part -> { - assertThat(part.headers()).isEmpty(); + assertThat(part.headers().isEmpty()).isTrue(); part.content().subscribe(DataBufferUtils::release); }) .verifyComplete(); @@ -266,7 +266,7 @@ class DefaultPartHttpMessageReaderTests { CountDownLatch latch = new CountDownLatch(1); StepVerifier.create(result) .consumeNextWith(part -> { - assertThat(part.headers()).containsEntry("Føø", Collections.singletonList("Bår")); + assertThat(part.headers().hasHeaderValues("Føø", Collections.singletonList("Bår"))).isTrue(); testPart(part, null, "This is plain ASCII text.", latch); }) .verifyComplete(); diff --git a/spring-web/src/test/java/org/springframework/http/codec/multipart/MultipartHttpMessageWriterTests.java b/spring-web/src/test/java/org/springframework/http/codec/multipart/MultipartHttpMessageWriterTests.java index 841926d968..b54413c306 100644 --- a/spring-web/src/test/java/org/springframework/http/codec/multipart/MultipartHttpMessageWriterTests.java +++ b/spring-web/src/test/java/org/springframework/http/codec/multipart/MultipartHttpMessageWriterTests.java @@ -175,7 +175,7 @@ class MultipartHttpMessageWriterTests extends AbstractLeakCheckingTests { part = requestParts.getFirst("filePublisher"); assertThat(part).isNotNull(); assertThat(part.name()).isEqualTo("filePublisher"); - assertThat(part.headers()).containsEntry("foo", Collections.singletonList("bar")); + assertThat(part.headers().hasHeaderValues("foo", Collections.singletonList("bar"))).isTrue(); assertThat(((FilePart) part).filename()).isEqualTo("file.txt"); value = decodeToString(part); assertThat(value).isEqualTo("AaBbCc"); diff --git a/spring-web/src/test/java/org/springframework/http/codec/multipart/PartEventHttpMessageReaderTests.java b/spring-web/src/test/java/org/springframework/http/codec/multipart/PartEventHttpMessageReaderTests.java index 0e739d8307..4626002164 100644 --- a/spring-web/src/test/java/org/springframework/http/codec/multipart/PartEventHttpMessageReaderTests.java +++ b/spring-web/src/test/java/org/springframework/http/codec/multipart/PartEventHttpMessageReaderTests.java @@ -42,7 +42,6 @@ import static java.nio.charset.StandardCharsets.UTF_8; import static java.util.Collections.emptyMap; import static java.util.Collections.singletonMap; import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.AssertionsForClassTypes.entry; import static org.springframework.core.ResolvableType.forClass; /** @@ -72,7 +71,7 @@ class PartEventHttpMessageReaderTests { Flux result = this.reader.read(forClass(PartEvent.class), request, emptyMap()); StepVerifier.create(result) - .assertNext(form(headers -> assertThat(headers).isEmpty(), "This is implicitly typed plain ASCII text.\r\nIt does NOT end with a linebreak.")) + .assertNext(form(headers -> assertThat(headers.isEmpty()).isTrue(), "This is implicitly typed plain ASCII text.\r\nIt does NOT end with a linebreak.")) .assertNext(form(headers -> assertThat(headers.getContentType()).isEqualTo(TEXT_PLAIN_ASCII), "This is explicitly typed plain ASCII text.\r\nIt DOES end with a linebreak.\r\n")) .verifyComplete(); @@ -85,7 +84,7 @@ class PartEventHttpMessageReaderTests { Flux result = this.reader.read(forClass(PartEvent.class), request, emptyMap()); StepVerifier.create(result) - .assertNext(data(headers -> assertThat(headers).isEmpty(), bodyText("a"), true)) + .assertNext(data(headers -> assertThat(headers.isEmpty()).isTrue(), bodyText("a"), true)) .verifyComplete(); } @@ -143,8 +142,8 @@ class PartEventHttpMessageReaderTests { Flux result = this.reader.read(forClass(PartEvent.class), request, emptyMap()); StepVerifier.create(result) - .assertNext(form(headers -> assertThat(headers).contains(entry("Part", List.of("1"))), "")) - .assertNext(data(headers -> assertThat(headers).contains(entry("Part", List.of("2"))), bodyText("a"), true)) + .assertNext(form(headers -> assertThat(headers.hasHeaderValues("Part", List.of("1"))).isTrue(), "")) + .assertNext(data(headers -> assertThat(headers.hasHeaderValues("Part", List.of("2"))).isTrue(), bodyText("a"), true)) .verifyComplete(); } @@ -156,7 +155,7 @@ class PartEventHttpMessageReaderTests { Flux result = this.reader.read(forClass(PartEvent.class), request, emptyMap()); StepVerifier.create(result, 3) - .assertNext(form(headers -> assertThat(headers).isEmpty(), + .assertNext(form(headers -> assertThat(headers.isEmpty()).isTrue(), "This is implicitly typed plain ASCII text.\r\nIt does NOT end with a linebreak.")) .thenCancel() .verify(); @@ -238,7 +237,7 @@ class PartEventHttpMessageReaderTests { Flux result = reader.read(forClass(PartEvent.class), request, emptyMap()); StepVerifier.create(result) - .assertNext(form(headers -> assertThat(headers).isEmpty(), "This is implicitly typed plain ASCII text.\r\nIt does NOT end with a linebreak.")) + .assertNext(form(headers -> assertThat(headers.isEmpty()).isTrue(), "This is implicitly typed plain ASCII text.\r\nIt does NOT end with a linebreak.")) .expectError(DecodingException.class) .verify(); } @@ -283,7 +282,7 @@ class PartEventHttpMessageReaderTests { Flux result = this.reader.read(forClass(PartEvent.class), request, emptyMap()); StepVerifier.create(result) - .assertNext(data(headers -> assertThat(headers).containsEntry("Føø", List.of("Bår")), + .assertNext(data(headers -> assertThat(headers.hasHeaderValues("Føø", List.of("Bår"))).isTrue(), bodyText("This is plain ASCII text."), true)) .verifyComplete(); } diff --git a/spring-web/src/test/java/org/springframework/http/server/ServletServerHttpRequestTests.java b/spring-web/src/test/java/org/springframework/http/server/ServletServerHttpRequestTests.java index 6dad7e7ab2..4215d95fc1 100644 --- a/spring-web/src/test/java/org/springframework/http/server/ServletServerHttpRequestTests.java +++ b/spring-web/src/test/java/org/springframework/http/server/ServletServerHttpRequestTests.java @@ -129,7 +129,7 @@ class ServletServerHttpRequestTests { HttpHeaders headers = request.getHeaders(); assertThat(headers).as("No HttpHeaders returned").isNotNull(); - assertThat(headers.containsKey(headerName)).as("Invalid headers returned").isTrue(); + assertThat(headers.containsHeader(headerName)).as("Invalid headers returned").isTrue(); List headerValues = headers.get(headerName); assertThat(headerValues).as("No header values returned").isNotNull(); assertThat(headerValues.size()).as("Invalid header values returned").isEqualTo(2); @@ -150,7 +150,7 @@ class ServletServerHttpRequestTests { HttpHeaders headers = request.getHeaders(); assertThat(headers).as("No HttpHeaders returned").isNotNull(); - assertThat(headers.containsKey(headerName)).as("Invalid headers returned").isTrue(); + assertThat(headers.containsHeader(headerName)).as("Invalid headers returned").isTrue(); List headerValues = headers.get(headerName); assertThat(headerValues.size()).as("Invalid header values returned").isEqualTo(2); assertThat(headerValues.contains(headerValue1)).as("Invalid header values returned").isTrue(); @@ -162,7 +162,7 @@ class ServletServerHttpRequestTests { void getHeadersWithWildcardContentType() { mockRequest.setContentType("*/*"); mockRequest.removeHeader("Content-Type"); - assertThat(request.getHeaders()).as("Invalid content-type should not raise exception").isEmpty(); + assertThat(request.getHeaders().isEmpty()).as("Invalid content-type should not raise exception").isTrue(); } @Test diff --git a/spring-web/src/test/java/org/springframework/http/server/ServletServerHttpResponseTests.java b/spring-web/src/test/java/org/springframework/http/server/ServletServerHttpResponseTests.java index 34a40016ac..33c973fa6d 100644 --- a/spring-web/src/test/java/org/springframework/http/server/ServletServerHttpResponseTests.java +++ b/spring-web/src/test/java/org/springframework/http/server/ServletServerHttpResponseTests.java @@ -99,7 +99,7 @@ class ServletServerHttpResponseTests { assertThat(this.response.getHeaders().getFirst(headerName)).isEqualTo(headerValue); assertThat(this.response.getHeaders().get(headerName)).containsExactly(headerValue); - assertThat(this.response.getHeaders()).containsKey(headerName); + assertThat(this.response.getHeaders().containsHeader(headerName)).isTrue(); assertThat(this.response.getHeaders().getAccessControlAllowOrigin()).isEqualTo(headerValue); } diff --git a/spring-web/src/test/java/org/springframework/http/server/reactive/HeadersAdaptersTests.java b/spring-web/src/test/java/org/springframework/http/server/reactive/HeadersAdaptersTests.java index 823121e781..9067a092f4 100644 --- a/spring-web/src/test/java/org/springframework/http/server/reactive/HeadersAdaptersTests.java +++ b/spring-web/src/test/java/org/springframework/http/server/reactive/HeadersAdaptersTests.java @@ -117,7 +117,7 @@ class HeadersAdaptersTests { assertThat(headers2.get("TestHeader")).as("TestHeader") .containsExactly("first", "second", "third"); // Using the headerSet approach, we keep the first encountered casing of any given key - assertThat(headers2.keySet()).as("first casing variant").containsExactlyInAnyOrder("TestHeader", "SecondHeader"); + assertThat(headers2.headerNames()).as("first casing variant").containsExactlyInAnyOrder("TestHeader", "SecondHeader"); assertThat(headers2.toString()).as("similar toString, no 'with native headers' dump") .isEqualTo(headers.toString().substring(0, headers.toString().indexOf(']') + 1)); } @@ -132,7 +132,7 @@ class HeadersAdaptersTests { assertThat(headers2.get("TestHeader")).as("TestHeader") .containsExactly("first", "second", "third"); // Ordering and casing are not guaranteed using the entrySet+put approach - assertThat(headers2).as("two keys") + assertThat(headers2.asMultiValueMap()).as("two keys") .containsKey("testheader") .containsKey("secondheader") .hasSize(2); @@ -148,7 +148,10 @@ class HeadersAdaptersTests { assertThat(headers2.get("TestHeader")).as("TestHeader") .containsExactly("first", "second", "third"); // Ordering and casing are not guaranteed using the putAll approach - assertThat(headers2).as("two keys").containsOnlyKeys("testheader", "secondheader"); + assertThat(headers2.asMultiValueMap()).as("two keys") + .containsKey("testheader") + .containsKey("secondheader") + .hasSize(2); assertThat(headers2.toString()).as("similar toString, no 'with native headers' dump") .isEqualToIgnoringCase(headers.toString().substring(0, headers.toString().indexOf(']') + 1)); } diff --git a/spring-web/src/test/java/org/springframework/http/server/reactive/ServerHttpResponseTests.java b/spring-web/src/test/java/org/springframework/http/server/reactive/ServerHttpResponseTests.java index 7e80029cac..40710368c1 100644 --- a/spring-web/src/test/java/org/springframework/http/server/reactive/ServerHttpResponseTests.java +++ b/spring-web/src/test/java/org/springframework/http/server/reactive/ServerHttpResponseTests.java @@ -110,7 +110,7 @@ class ServerHttpResponseTests { assertThat(response.statusCodeWritten).isFalse(); assertThat(response.headersWritten).isFalse(); assertThat(response.cookiesWritten).isFalse(); - assertThat(headers).doesNotContainKeys(HttpHeaders.CONTENT_TYPE, HttpHeaders.CONTENT_LENGTH, + assertThat(headers.headerNames()).doesNotContain(HttpHeaders.CONTENT_TYPE, HttpHeaders.CONTENT_LENGTH, HttpHeaders.CONTENT_ENCODING); assertThat(response.body).isEmpty(); } @@ -178,7 +178,7 @@ class ServerHttpResponseTests { assertThat(response.headersWritten).isFalse(); assertThat(response.cookiesWritten).isFalse(); assertThat(response.isCommitted()).isFalse(); - assertThat(response.getHeaders()).isEmpty(); + assertThat(response.getHeaders().isEmpty()).isTrue(); // Handle the error response.setStatusCode(HttpStatus.SERVICE_UNAVAILABLE); diff --git a/spring-web/src/test/java/org/springframework/web/ErrorResponseExceptionTests.java b/spring-web/src/test/java/org/springframework/web/ErrorResponseExceptionTests.java index fc39bff8a1..5513e59577 100644 --- a/spring-web/src/test/java/org/springframework/web/ErrorResponseExceptionTests.java +++ b/spring-web/src/test/java/org/springframework/web/ErrorResponseExceptionTests.java @@ -104,7 +104,7 @@ class ErrorResponseExceptionTests { assertDetail(ex, "Could not parse Content-Type."); assertDetailMessageCode(ex, "parseError", null); - assertThat(ex.getHeaders()).isEmpty(); + assertThat(ex.getHeaders().isEmpty()).isTrue(); } @Test @@ -117,7 +117,7 @@ class ErrorResponseExceptionTests { assertDetail(ex, "Acceptable representations: [application/json, application/cbor]."); assertDetailMessageCode(ex, null, new Object[] {ex.getSupportedMediaTypes()}); - assertThat(ex.getHeaders()).hasSize(1); + assertThat(ex.getHeaders().size()).isOne(); assertThat(ex.getHeaders().getAccept()).isEqualTo(mediaTypes); } @@ -131,7 +131,7 @@ class ErrorResponseExceptionTests { assertDetail(ex, "Could not parse Accept header."); assertDetailMessageCode(ex, "parseError", null); - assertThat(ex.getHeaders()).isEmpty(); + assertThat(ex.getHeaders().isEmpty()).isTrue(); } @Test @@ -143,7 +143,7 @@ class ErrorResponseExceptionTests { assertStatus(ex, HttpStatus.SERVICE_UNAVAILABLE); assertDetail(ex, null); - assertThat(ex.getHeaders()).isEmpty(); + assertThat(ex.getHeaders().isEmpty()).isTrue(); } @Test @@ -156,7 +156,7 @@ class ErrorResponseExceptionTests { assertDetail(ex, "Method 'PUT' is not supported."); assertDetailMessageCode(ex, null, new Object[] {ex.getMethod(), ex.getSupportedHttpMethods()}); - assertThat(ex.getHeaders()).hasSize(1); + assertThat(ex.getHeaders().size()).isOne(); assertThat(ex.getHeaders().getAllow()).containsExactly(HttpMethod.GET, HttpMethod.POST); } @@ -169,7 +169,7 @@ class ErrorResponseExceptionTests { assertDetail(ex, "Required header 'Authorization' is not present."); assertDetailMessageCode(ex, null, new Object[] {ex.getHeaderName()}); - assertThat(ex.getHeaders()).isEmpty(); + assertThat(ex.getHeaders().isEmpty()).isTrue(); } @Test @@ -181,7 +181,7 @@ class ErrorResponseExceptionTests { assertDetail(ex, "Required parameter 'query' is not present."); assertDetailMessageCode(ex, null, new Object[] {ex.getParameterName()}); - assertThat(ex.getHeaders()).isEmpty(); + assertThat(ex.getHeaders().isEmpty()).isTrue(); } @Test @@ -194,7 +194,7 @@ class ErrorResponseExceptionTests { assertDetail(ex, "Required path parameter 'region' is not present."); assertDetailMessageCode(ex, null, new Object[] {ex.getVariableName()}); - assertThat(ex.getHeaders()).isEmpty(); + assertThat(ex.getHeaders().isEmpty()).isTrue(); } @Test @@ -206,7 +206,7 @@ class ErrorResponseExceptionTests { assertDetail(ex, "Required path variable 'id' is not present."); assertDetailMessageCode(ex, null, new Object[] {ex.getVariableName()}); - assertThat(ex.getHeaders()).isEmpty(); + assertThat(ex.getHeaders().isEmpty()).isTrue(); } @Test @@ -218,7 +218,7 @@ class ErrorResponseExceptionTests { assertDetail(ex, "Required cookie 'oreo' is not present."); assertDetailMessageCode(ex, null, new Object[] {ex.getCookieName()}); - assertThat(ex.getHeaders()).isEmpty(); + assertThat(ex.getHeaders().isEmpty()).isTrue(); } @Test @@ -231,7 +231,7 @@ class ErrorResponseExceptionTests { assertDetail(ex, "Invalid request parameters."); assertDetailMessageCode(ex, null, new Object[] {List.of("\"foo=bar, bar=baz\"")}); - assertThat(ex.getHeaders()).isEmpty(); + assertThat(ex.getHeaders().isEmpty()).isTrue(); } @Test @@ -243,7 +243,7 @@ class ErrorResponseExceptionTests { assertDetail(ex, "Required part 'file' is not present."); assertDetailMessageCode(ex, null, new Object[] {ex.getRequestPartName()}); - assertThat(ex.getHeaders()).isEmpty(); + assertThat(ex.getHeaders().isEmpty()).isTrue(); } @Test @@ -258,7 +258,7 @@ class ErrorResponseExceptionTests { assertDetail(ex, "Invalid request content."); testHelper.assertMessages(ex, ex.getAllErrors()); - assertThat(ex.getHeaders()).isEmpty(); + assertThat(ex.getHeaders().isEmpty()).isTrue(); } @Test @@ -306,7 +306,7 @@ class ErrorResponseExceptionTests { assertDetail(ex, "Could not parse Content-Type."); assertDetailMessageCode(ex, "parseError", null); - assertThat(ex.getHeaders()).isEmpty(); + assertThat(ex.getHeaders().isEmpty()).isTrue(); } @Test @@ -319,7 +319,7 @@ class ErrorResponseExceptionTests { assertDetail(ex, "Acceptable representations: [application/json, application/cbor]."); assertDetailMessageCode(ex, null, new Object[] {ex.getSupportedMediaTypes()}); - assertThat(ex.getHeaders()).hasSize(1); + assertThat(ex.getHeaders().size()).isOne(); assertThat(ex.getHeaders().getAccept()).isEqualTo(mediaTypes); } @@ -333,7 +333,7 @@ class ErrorResponseExceptionTests { assertDetail(ex, "Could not parse Accept header."); assertDetailMessageCode(ex, "parseError", null); - assertThat(ex.getHeaders()).isEmpty(); + assertThat(ex.getHeaders().isEmpty()).isTrue(); } @Test @@ -345,7 +345,7 @@ class ErrorResponseExceptionTests { assertDetail(ex, "Failure"); assertDetailMessageCode(ex, null, new Object[] {ex.getReason()}); - assertThat(ex.getHeaders()).isEmpty(); + assertThat(ex.getHeaders().isEmpty()).isTrue(); } @Test @@ -358,7 +358,7 @@ class ErrorResponseExceptionTests { assertDetail(ex, "Required header 'foo' is not present."); assertDetailMessageCode(ex, null, new Object[] {ex.getLabel(), ex.getName()}); - assertThat(ex.getHeaders()).isEmpty(); + assertThat(ex.getHeaders().isEmpty()).isTrue(); } @Test @@ -373,7 +373,7 @@ class ErrorResponseExceptionTests { assertDetail(ex, "Invalid request parameters."); assertDetailMessageCode(ex, null, new Object[] {ex.getConditions()}); - assertThat(ex.getHeaders()).isEmpty(); + assertThat(ex.getHeaders().isEmpty()).isTrue(); } @Test @@ -388,7 +388,7 @@ class ErrorResponseExceptionTests { assertDetail(ex, "Invalid request content."); testHelper.assertMessages(ex, ex.getAllErrors()); - assertThat(ex.getHeaders()).isEmpty(); + assertThat(ex.getHeaders().isEmpty()).isTrue(); } @Test @@ -401,7 +401,7 @@ class ErrorResponseExceptionTests { assertDetail(ex, "Supported methods: [GET, POST]"); assertDetailMessageCode(ex, null, new Object[] {ex.getHttpMethod(), supportedMethods}); - assertThat(ex.getHeaders()).hasSize(1); + assertThat(ex.getHeaders().size()).isOne(); assertThat(ex.getHeaders().getAllow()).containsExactly(HttpMethod.GET, HttpMethod.POST); } @@ -414,7 +414,7 @@ class ErrorResponseExceptionTests { assertDetail(ex, "Request method 'PUT' is not supported."); assertDetailMessageCode(ex, null, new Object[] {ex.getHttpMethod(), Collections.emptyList()}); - assertThat(ex.getHeaders()).isEmpty(); + assertThat(ex.getHeaders().isEmpty()).isTrue(); } @Test // gh-30300 diff --git a/spring-web/src/test/java/org/springframework/web/ErrorResponseTests.java b/spring-web/src/test/java/org/springframework/web/ErrorResponseTests.java index f39afad7ad..43a4126a5b 100644 --- a/spring-web/src/test/java/org/springframework/web/ErrorResponseTests.java +++ b/spring-web/src/test/java/org/springframework/web/ErrorResponseTests.java @@ -36,7 +36,7 @@ class ErrorResponseTests { void createWithHttpHeader() { ErrorResponse response = ErrorResponse.builder(new IllegalStateException(), HttpStatus.BAD_REQUEST, "test") .header("header", "value").build(); - assertThat(response.getHeaders()).containsOnly(entry("header", List.of("value"))); + assertThat(response.getHeaders().asMultiValueMap()).containsOnly(entry("header", List.of("value"))); } @Test @@ -47,7 +47,7 @@ class ErrorResponseTests { headers.add("header", "value2"); headers.add("another", "value3"); }).build(); - assertThat(response.getHeaders()).containsOnly(entry("header", List.of("value", "value2")), + assertThat(response.getHeaders().asMultiValueMap()).containsOnly(entry("header", List.of("value", "value2")), entry("another", List.of("value3"))); } diff --git a/spring-web/src/test/java/org/springframework/web/client/RestClientResponseExceptionTests.java b/spring-web/src/test/java/org/springframework/web/client/RestClientResponseExceptionTests.java index cc13bb9ef0..7518739068 100644 --- a/spring-web/src/test/java/org/springframework/web/client/RestClientResponseExceptionTests.java +++ b/spring-web/src/test/java/org/springframework/web/client/RestClientResponseExceptionTests.java @@ -36,7 +36,7 @@ class RestClientResponseExceptionTests { HttpHeaders responseHeaders = ex.getResponseHeaders(); assertThat(responseHeaders).isNotNull(); - assertThat(responseHeaders).hasSize(1); + assertThat(responseHeaders.size()).isOne(); assertThat(responseHeaders.getFirst("FOO")).isEqualTo("bar"); } diff --git a/spring-web/src/test/java/org/springframework/web/cors/reactive/DefaultCorsProcessorTests.java b/spring-web/src/test/java/org/springframework/web/cors/reactive/DefaultCorsProcessorTests.java index df86b40e8b..cdafe515ae 100644 --- a/spring-web/src/test/java/org/springframework/web/cors/reactive/DefaultCorsProcessorTests.java +++ b/spring-web/src/test/java/org/springframework/web/cors/reactive/DefaultCorsProcessorTests.java @@ -65,7 +65,7 @@ class DefaultCorsProcessorTests { this.processor.process(this.conf, exchange); ServerHttpResponse response = exchange.getResponse(); - assertThat(response.getHeaders().containsKey(ACCESS_CONTROL_ALLOW_ORIGIN)).isFalse(); + assertThat(response.getHeaders().containsHeader(ACCESS_CONTROL_ALLOW_ORIGIN)).isFalse(); assertThat(response.getHeaders().get(VARY)).contains(ORIGIN, ACCESS_CONTROL_REQUEST_METHOD, ACCESS_CONTROL_REQUEST_HEADERS); assertThat(response.getStatusCode()).isNull(); @@ -81,7 +81,7 @@ class DefaultCorsProcessorTests { this.processor.process(this.conf, exchange); ServerHttpResponse response = exchange.getResponse(); - assertThat(response.getHeaders().containsKey(ACCESS_CONTROL_ALLOW_ORIGIN)).isFalse(); + assertThat(response.getHeaders().containsHeader(ACCESS_CONTROL_ALLOW_ORIGIN)).isFalse(); assertThat(response.getHeaders().get(VARY)).contains(ORIGIN, ACCESS_CONTROL_REQUEST_METHOD, ACCESS_CONTROL_REQUEST_HEADERS); assertThat(response.getStatusCode()).isNull(); @@ -93,7 +93,7 @@ class DefaultCorsProcessorTests { this.processor.process(this.conf, exchange); ServerHttpResponse response = exchange.getResponse(); - assertThat(response.getHeaders().containsKey(ACCESS_CONTROL_ALLOW_ORIGIN)).isFalse(); + assertThat(response.getHeaders().containsHeader(ACCESS_CONTROL_ALLOW_ORIGIN)).isFalse(); assertThat(response.getHeaders().get(VARY)).contains(ORIGIN, ACCESS_CONTROL_REQUEST_METHOD, ACCESS_CONTROL_REQUEST_HEADERS); assertThat(response.getStatusCode()).isEqualTo(HttpStatus.FORBIDDEN); @@ -105,7 +105,7 @@ class DefaultCorsProcessorTests { this.processor.process(null, exchange); ServerHttpResponse response = exchange.getResponse(); - assertThat(response.getHeaders().containsKey(ACCESS_CONTROL_ALLOW_ORIGIN)).isFalse(); + assertThat(response.getHeaders().containsHeader(ACCESS_CONTROL_ALLOW_ORIGIN)).isFalse(); assertThat(response.getStatusCode()).isNull(); } @@ -116,10 +116,10 @@ class DefaultCorsProcessorTests { this.processor.process(this.conf, exchange); ServerHttpResponse response = exchange.getResponse(); - assertThat(response.getHeaders().containsKey(ACCESS_CONTROL_ALLOW_ORIGIN)).isTrue(); + assertThat(response.getHeaders().containsHeader(ACCESS_CONTROL_ALLOW_ORIGIN)).isTrue(); assertThat(response.getHeaders().getFirst(ACCESS_CONTROL_ALLOW_ORIGIN)).isEqualTo("*"); - assertThat(response.getHeaders().containsKey(HttpHeaders.ACCESS_CONTROL_MAX_AGE)).isFalse(); - assertThat(response.getHeaders().containsKey(HttpHeaders.ACCESS_CONTROL_EXPOSE_HEADERS)).isFalse(); + assertThat(response.getHeaders().containsHeader(HttpHeaders.ACCESS_CONTROL_MAX_AGE)).isFalse(); + assertThat(response.getHeaders().containsHeader(HttpHeaders.ACCESS_CONTROL_EXPOSE_HEADERS)).isFalse(); assertThat(response.getHeaders().get(VARY)).contains(ORIGIN, ACCESS_CONTROL_REQUEST_METHOD, ACCESS_CONTROL_REQUEST_HEADERS); assertThat(response.getStatusCode()).isNull(); @@ -135,9 +135,9 @@ class DefaultCorsProcessorTests { this.processor.process(this.conf, exchange); ServerHttpResponse response = exchange.getResponse(); - assertThat(response.getHeaders().containsKey(ACCESS_CONTROL_ALLOW_ORIGIN)).isTrue(); + assertThat(response.getHeaders().containsHeader(ACCESS_CONTROL_ALLOW_ORIGIN)).isTrue(); assertThat(response.getHeaders().getFirst(ACCESS_CONTROL_ALLOW_ORIGIN)).isEqualTo("https://domain2.com"); - assertThat(response.getHeaders().containsKey(HttpHeaders.ACCESS_CONTROL_ALLOW_CREDENTIALS)).isTrue(); + assertThat(response.getHeaders().containsHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_CREDENTIALS)).isTrue(); assertThat(response.getHeaders().getFirst(HttpHeaders.ACCESS_CONTROL_ALLOW_CREDENTIALS)).isEqualTo("true"); assertThat(response.getHeaders().get(VARY)).contains(ORIGIN, ACCESS_CONTROL_REQUEST_METHOD, ACCESS_CONTROL_REQUEST_HEADERS); @@ -156,9 +156,9 @@ class DefaultCorsProcessorTests { this.processor.process(this.conf, exchange); ServerHttpResponse response = exchange.getResponse(); - assertThat(response.getHeaders().containsKey(ACCESS_CONTROL_ALLOW_ORIGIN)).isTrue(); + assertThat(response.getHeaders().containsHeader(ACCESS_CONTROL_ALLOW_ORIGIN)).isTrue(); assertThat(response.getHeaders().getFirst(ACCESS_CONTROL_ALLOW_ORIGIN)).isEqualTo("https://domain2.com"); - assertThat(response.getHeaders().containsKey(HttpHeaders.ACCESS_CONTROL_ALLOW_CREDENTIALS)).isTrue(); + assertThat(response.getHeaders().containsHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_CREDENTIALS)).isTrue(); assertThat(response.getHeaders().getFirst(HttpHeaders.ACCESS_CONTROL_ALLOW_CREDENTIALS)).isEqualTo("true"); assertThat(response.getHeaders().get(VARY)).contains(ORIGIN, ACCESS_CONTROL_REQUEST_METHOD, ACCESS_CONTROL_REQUEST_HEADERS); @@ -173,7 +173,7 @@ class DefaultCorsProcessorTests { ServerHttpResponse response = exchange.getResponse(); assertThat(response.getStatusCode()).isNull(); - assertThat(response.getHeaders().containsKey(ACCESS_CONTROL_ALLOW_ORIGIN)).isTrue(); + assertThat(response.getHeaders().containsHeader(ACCESS_CONTROL_ALLOW_ORIGIN)).isTrue(); } @Test // gh-26892 @@ -187,7 +187,7 @@ class DefaultCorsProcessorTests { ServerHttpResponse response = exchange.getResponse(); assertThat(response.getStatusCode()).isNull(); - assertThat(response.getHeaders().containsKey(ACCESS_CONTROL_ALLOW_ORIGIN)).isTrue(); + assertThat(response.getHeaders().containsHeader(ACCESS_CONTROL_ALLOW_ORIGIN)).isTrue(); } @Test // gh-33682 @@ -202,7 +202,7 @@ class DefaultCorsProcessorTests { assertThat(result).isFalse(); assertThat(response.getStatusCode()).isEqualTo(HttpStatus.FORBIDDEN); - assertThat(response.getHeaders().containsKey(ACCESS_CONTROL_ALLOW_ORIGIN)).isFalse(); + assertThat(response.getHeaders().containsHeader(ACCESS_CONTROL_ALLOW_ORIGIN)).isFalse(); } @Test @@ -214,9 +214,9 @@ class DefaultCorsProcessorTests { this.processor.process(this.conf, exchange); ServerHttpResponse response = exchange.getResponse(); - assertThat(response.getHeaders().containsKey(ACCESS_CONTROL_ALLOW_ORIGIN)).isTrue(); + assertThat(response.getHeaders().containsHeader(ACCESS_CONTROL_ALLOW_ORIGIN)).isTrue(); assertThat(response.getHeaders().getFirst(ACCESS_CONTROL_ALLOW_ORIGIN)).isEqualTo("https://domain2.com"); - assertThat(response.getHeaders().containsKey(HttpHeaders.ACCESS_CONTROL_EXPOSE_HEADERS)).isTrue(); + assertThat(response.getHeaders().containsHeader(HttpHeaders.ACCESS_CONTROL_EXPOSE_HEADERS)).isTrue(); assertThat(response.getHeaders().getFirst(HttpHeaders.ACCESS_CONTROL_EXPOSE_HEADERS)).contains("header1"); assertThat(response.getHeaders().getFirst(HttpHeaders.ACCESS_CONTROL_EXPOSE_HEADERS)).contains("header2"); assertThat(response.getHeaders().get(VARY)).contains(ORIGIN, @@ -271,7 +271,7 @@ class DefaultCorsProcessorTests { this.processor.process(this.conf, exchange); ServerHttpResponse response = exchange.getResponse(); - assertThat(response.getHeaders().containsKey(ACCESS_CONTROL_ALLOW_ORIGIN)).isFalse(); + assertThat(response.getHeaders().containsHeader(ACCESS_CONTROL_ALLOW_ORIGIN)).isFalse(); assertThat(response.getHeaders().get(VARY)).contains(ORIGIN, ACCESS_CONTROL_REQUEST_METHOD, ACCESS_CONTROL_REQUEST_HEADERS); assertThat(response.getStatusCode()).isEqualTo(HttpStatus.FORBIDDEN); @@ -284,7 +284,7 @@ class DefaultCorsProcessorTests { this.processor.process(this.conf, exchange); ServerHttpResponse response = exchange.getResponse(); - assertThat(response.getHeaders().containsKey(ACCESS_CONTROL_ALLOW_ORIGIN)).isFalse(); + assertThat(response.getHeaders().containsHeader(ACCESS_CONTROL_ALLOW_ORIGIN)).isFalse(); assertThat(response.getHeaders().get(VARY)).contains(ORIGIN, ACCESS_CONTROL_REQUEST_METHOD, ACCESS_CONTROL_REQUEST_HEADERS); assertThat(response.getStatusCode()).isEqualTo(HttpStatus.FORBIDDEN); @@ -299,7 +299,7 @@ class DefaultCorsProcessorTests { this.processor.process(this.conf, exchange); ServerHttpResponse response = exchange.getResponse(); - assertThat(response.getHeaders().containsKey(ACCESS_CONTROL_ALLOW_ORIGIN)).isFalse(); + assertThat(response.getHeaders().containsHeader(ACCESS_CONTROL_ALLOW_ORIGIN)).isFalse(); assertThat(response.getHeaders().get(VARY)).contains(ORIGIN, ACCESS_CONTROL_REQUEST_METHOD, ACCESS_CONTROL_REQUEST_HEADERS); assertThat(response.getStatusCode()).isEqualTo(HttpStatus.FORBIDDEN); @@ -320,11 +320,11 @@ class DefaultCorsProcessorTests { this.processor.process(this.conf, exchange); ServerHttpResponse response = exchange.getResponse(); - assertThat(response.getHeaders().containsKey(ACCESS_CONTROL_ALLOW_ORIGIN)).isTrue(); + assertThat(response.getHeaders().containsHeader(ACCESS_CONTROL_ALLOW_ORIGIN)).isTrue(); assertThat(response.getHeaders().getFirst(ACCESS_CONTROL_ALLOW_ORIGIN)).isEqualTo("*"); - assertThat(response.getHeaders().containsKey(HttpHeaders.ACCESS_CONTROL_ALLOW_METHODS)).isTrue(); + assertThat(response.getHeaders().containsHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_METHODS)).isTrue(); assertThat(response.getHeaders().getFirst(HttpHeaders.ACCESS_CONTROL_ALLOW_METHODS)).isEqualTo("GET,PUT"); - assertThat(response.getHeaders().containsKey(HttpHeaders.ACCESS_CONTROL_MAX_AGE)).isFalse(); + assertThat(response.getHeaders().containsHeader(HttpHeaders.ACCESS_CONTROL_MAX_AGE)).isFalse(); assertThat(response.getHeaders().get(VARY)).contains(ORIGIN, ACCESS_CONTROL_REQUEST_METHOD, ACCESS_CONTROL_REQUEST_HEADERS); assertThat(response.getStatusCode()).isNull(); @@ -345,9 +345,9 @@ class DefaultCorsProcessorTests { this.processor.process(this.conf, exchange); ServerHttpResponse response = exchange.getResponse(); - assertThat(response.getHeaders().containsKey(ACCESS_CONTROL_ALLOW_ORIGIN)).isTrue(); + assertThat(response.getHeaders().containsHeader(ACCESS_CONTROL_ALLOW_ORIGIN)).isTrue(); assertThat(response.getHeaders().getFirst(ACCESS_CONTROL_ALLOW_ORIGIN)).isEqualTo("https://domain2.com"); - assertThat(response.getHeaders().containsKey(HttpHeaders.ACCESS_CONTROL_ALLOW_CREDENTIALS)).isTrue(); + assertThat(response.getHeaders().containsHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_CREDENTIALS)).isTrue(); assertThat(response.getHeaders().getFirst(HttpHeaders.ACCESS_CONTROL_ALLOW_CREDENTIALS)).isEqualTo("true"); assertThat(response.getHeaders().get(VARY)).contains(ORIGIN, ACCESS_CONTROL_REQUEST_METHOD, ACCESS_CONTROL_REQUEST_HEADERS); @@ -372,7 +372,7 @@ class DefaultCorsProcessorTests { this.processor.process(this.conf, exchange); ServerHttpResponse response = exchange.getResponse(); - assertThat(response.getHeaders().containsKey(ACCESS_CONTROL_ALLOW_ORIGIN)).isTrue(); + assertThat(response.getHeaders().containsHeader(ACCESS_CONTROL_ALLOW_ORIGIN)).isTrue(); assertThat(response.getHeaders().getFirst(ACCESS_CONTROL_ALLOW_ORIGIN)).isEqualTo("https://domain2.com"); assertThat(response.getHeaders().get(VARY)).contains(ORIGIN, ACCESS_CONTROL_REQUEST_METHOD, ACCESS_CONTROL_REQUEST_HEADERS); @@ -398,8 +398,8 @@ class DefaultCorsProcessorTests { this.processor.process(this.conf, exchange); ServerHttpResponse response = exchange.getResponse(); - assertThat(response.getHeaders().containsKey(ACCESS_CONTROL_ALLOW_ORIGIN)).isTrue(); - assertThat(response.getHeaders().containsKey(DefaultCorsProcessor.ACCESS_CONTROL_ALLOW_PRIVATE_NETWORK)).isTrue(); + assertThat(response.getHeaders().containsHeader(ACCESS_CONTROL_ALLOW_ORIGIN)).isTrue(); + assertThat(response.getHeaders().containsHeader(DefaultCorsProcessor.ACCESS_CONTROL_ALLOW_PRIVATE_NETWORK)).isTrue(); assertThat(response.getHeaders().getFirst(ACCESS_CONTROL_ALLOW_ORIGIN)).isEqualTo("https://domain2.com"); assertThat(response.getHeaders().get(VARY)).contains(ORIGIN, ACCESS_CONTROL_REQUEST_METHOD, ACCESS_CONTROL_REQUEST_HEADERS); @@ -420,8 +420,8 @@ class DefaultCorsProcessorTests { this.processor.process(this.conf, exchange); ServerHttpResponse response = exchange.getResponse(); - assertThat(response.getHeaders().containsKey(ACCESS_CONTROL_ALLOW_ORIGIN)).isTrue(); - assertThat(response.getHeaders().containsKey(ACCESS_CONTROL_ALLOW_HEADERS)).isTrue(); + assertThat(response.getHeaders().containsHeader(ACCESS_CONTROL_ALLOW_ORIGIN)).isTrue(); + assertThat(response.getHeaders().containsHeader(ACCESS_CONTROL_ALLOW_HEADERS)).isTrue(); assertThat(response.getHeaders().getFirst(ACCESS_CONTROL_ALLOW_HEADERS)).contains("Header1"); assertThat(response.getHeaders().getFirst(ACCESS_CONTROL_ALLOW_HEADERS)).contains("Header2"); assertThat(response.getHeaders().getFirst(ACCESS_CONTROL_ALLOW_HEADERS)).doesNotContain("Header3"); @@ -442,8 +442,8 @@ class DefaultCorsProcessorTests { this.processor.process(this.conf, exchange); ServerHttpResponse response = exchange.getResponse(); - assertThat(response.getHeaders().containsKey(ACCESS_CONTROL_ALLOW_ORIGIN)).isTrue(); - assertThat(response.getHeaders().containsKey(ACCESS_CONTROL_ALLOW_HEADERS)).isTrue(); + assertThat(response.getHeaders().containsHeader(ACCESS_CONTROL_ALLOW_ORIGIN)).isTrue(); + assertThat(response.getHeaders().containsHeader(ACCESS_CONTROL_ALLOW_HEADERS)).isTrue(); assertThat(response.getHeaders().getFirst(ACCESS_CONTROL_ALLOW_HEADERS)).contains("Header1"); assertThat(response.getHeaders().getFirst(ACCESS_CONTROL_ALLOW_HEADERS)).contains("Header2"); assertThat(response.getHeaders().getFirst(ACCESS_CONTROL_ALLOW_HEADERS)).doesNotContain("*"); @@ -464,8 +464,8 @@ class DefaultCorsProcessorTests { this.processor.process(this.conf, exchange); ServerHttpResponse response = exchange.getResponse(); - assertThat(response.getHeaders().containsKey(ACCESS_CONTROL_ALLOW_ORIGIN)).isTrue(); - assertThat(response.getHeaders().containsKey(ACCESS_CONTROL_ALLOW_HEADERS)).isFalse(); + assertThat(response.getHeaders().containsHeader(ACCESS_CONTROL_ALLOW_ORIGIN)).isTrue(); + assertThat(response.getHeaders().containsHeader(ACCESS_CONTROL_ALLOW_HEADERS)).isFalse(); assertThat(response.getHeaders().get(VARY)).contains(ORIGIN, ACCESS_CONTROL_REQUEST_METHOD, ACCESS_CONTROL_REQUEST_HEADERS); assertThat(response.getStatusCode()).isNull(); @@ -479,7 +479,7 @@ class DefaultCorsProcessorTests { this.processor.process(null, exchange); ServerHttpResponse response = exchange.getResponse(); - assertThat(response.getHeaders().containsKey(ACCESS_CONTROL_ALLOW_ORIGIN)).isFalse(); + assertThat(response.getHeaders().containsHeader(ACCESS_CONTROL_ALLOW_ORIGIN)).isFalse(); assertThat(response.getStatusCode()).isEqualTo(HttpStatus.FORBIDDEN); } @@ -513,8 +513,8 @@ class DefaultCorsProcessorTests { this.processor.process(this.conf, exchange); ServerHttpResponse response = exchange.getResponse(); - assertThat(response.getHeaders().containsKey(ACCESS_CONTROL_ALLOW_ORIGIN)).isTrue(); - assertThat(response.getHeaders().containsKey(DefaultCorsProcessor.ACCESS_CONTROL_ALLOW_PRIVATE_NETWORK)).isFalse(); + assertThat(response.getHeaders().containsHeader(ACCESS_CONTROL_ALLOW_ORIGIN)).isTrue(); + assertThat(response.getHeaders().containsHeader(DefaultCorsProcessor.ACCESS_CONTROL_ALLOW_PRIVATE_NETWORK)).isFalse(); assertThat(response.getStatusCode()).isNull(); } @@ -530,8 +530,8 @@ class DefaultCorsProcessorTests { this.processor.process(this.conf, exchange); ServerHttpResponse response = exchange.getResponse(); - assertThat(response.getHeaders().containsKey(ACCESS_CONTROL_ALLOW_ORIGIN)).isTrue(); - assertThat(response.getHeaders().containsKey(DefaultCorsProcessor.ACCESS_CONTROL_ALLOW_PRIVATE_NETWORK)).isFalse(); + assertThat(response.getHeaders().containsHeader(ACCESS_CONTROL_ALLOW_ORIGIN)).isTrue(); + assertThat(response.getHeaders().containsHeader(DefaultCorsProcessor.ACCESS_CONTROL_ALLOW_PRIVATE_NETWORK)).isFalse(); assertThat(response.getStatusCode()).isNull(); } @@ -548,8 +548,8 @@ class DefaultCorsProcessorTests { this.processor.process(this.conf, exchange); ServerHttpResponse response = exchange.getResponse(); - assertThat(response.getHeaders().containsKey(ACCESS_CONTROL_ALLOW_ORIGIN)).isTrue(); - assertThat(response.getHeaders().containsKey(DefaultCorsProcessor.ACCESS_CONTROL_ALLOW_PRIVATE_NETWORK)).isTrue(); + assertThat(response.getHeaders().containsHeader(ACCESS_CONTROL_ALLOW_ORIGIN)).isTrue(); + assertThat(response.getHeaders().containsHeader(DefaultCorsProcessor.ACCESS_CONTROL_ALLOW_PRIVATE_NETWORK)).isTrue(); assertThat(response.getStatusCode()).isNull(); } diff --git a/spring-web/src/test/java/org/springframework/web/server/adapter/ForwardedHeaderTransformerTests.java b/spring-web/src/test/java/org/springframework/web/server/adapter/ForwardedHeaderTransformerTests.java index c1244707a7..82a0324613 100644 --- a/spring-web/src/test/java/org/springframework/web/server/adapter/ForwardedHeaderTransformerTests.java +++ b/spring-web/src/test/java/org/springframework/web/server/adapter/ForwardedHeaderTransformerTests.java @@ -240,7 +240,7 @@ class ForwardedHeaderTransformerTests { private void assertForwardedHeadersRemoved(ServerHttpRequest request) { ForwardedHeaderTransformer.FORWARDED_HEADER_NAMES - .forEach(name -> assertThat(request.getHeaders().containsKey(name)).isFalse()); + .forEach(name -> assertThat(request.getHeaders().containsHeader(name)).isFalse()); } } diff --git a/spring-web/src/test/kotlin/org/springframework/http/converter/cbor/KotlinSerializationCborHttpMessageConverterTests.kt b/spring-web/src/test/kotlin/org/springframework/http/converter/cbor/KotlinSerializationCborHttpMessageConverterTests.kt index 52e55ab22d..91ac9ced07 100644 --- a/spring-web/src/test/kotlin/org/springframework/http/converter/cbor/KotlinSerializationCborHttpMessageConverterTests.kt +++ b/spring-web/src/test/kotlin/org/springframework/http/converter/cbor/KotlinSerializationCborHttpMessageConverterTests.kt @@ -170,7 +170,8 @@ class KotlinSerializationCborHttpMessageConverterTests { this.converter.write(serializableBean, null, outputMessage) - assertThat(outputMessage.headers).containsEntry("Content-Type", listOf("application/cbor")) + @Suppress("DEPRECATION") + assertThat(outputMessage.headers.asMultiValueMap()).containsEntry("Content-Type", listOf("application/cbor")) assertThat(outputMessage.bodyAsBytes.isNotEmpty()).isTrue() } @@ -181,7 +182,8 @@ class KotlinSerializationCborHttpMessageConverterTests { this.converter.write(serializableBean, null, outputMessage) - assertThat(outputMessage.headers).containsEntry("Content-Type", listOf("application/cbor")) + @Suppress("DEPRECATION") + assertThat(outputMessage.headers.asMultiValueMap()).containsEntry("Content-Type", listOf("application/cbor")) assertThat(outputMessage.bodyAsBytes.isNotEmpty()).isTrue() } @@ -191,7 +193,8 @@ class KotlinSerializationCborHttpMessageConverterTests { this.converter.write(serializableBeanArray, null, outputMessage) - assertThat(outputMessage.headers).containsEntry("Content-Type", listOf("application/cbor")) + @Suppress("DEPRECATION") + assertThat(outputMessage.headers.asMultiValueMap()).containsEntry("Content-Type", listOf("application/cbor")) assertThat(outputMessage.bodyAsBytes.isNotEmpty()).isTrue() } @@ -202,7 +205,8 @@ class KotlinSerializationCborHttpMessageConverterTests { this.converter.write(listOf(serializableBean), ResolvableType.forType(typeOf>().javaType), null, outputMessage, null) - assertThat(outputMessage.headers).containsEntry("Content-Type", listOf("application/cbor")) + @Suppress("DEPRECATION") + assertThat(outputMessage.headers.asMultiValueMap()).containsEntry("Content-Type", listOf("application/cbor")) assertThat(outputMessage.bodyAsBytes.isNotEmpty()).isTrue() } diff --git a/spring-web/src/test/kotlin/org/springframework/http/converter/json/KotlinSerializationJsonHttpMessageConverterTests.kt b/spring-web/src/test/kotlin/org/springframework/http/converter/json/KotlinSerializationJsonHttpMessageConverterTests.kt index e90047201f..4c3b9b5dd0 100644 --- a/spring-web/src/test/kotlin/org/springframework/http/converter/json/KotlinSerializationJsonHttpMessageConverterTests.kt +++ b/spring-web/src/test/kotlin/org/springframework/http/converter/json/KotlinSerializationJsonHttpMessageConverterTests.kt @@ -258,7 +258,8 @@ class KotlinSerializationJsonHttpMessageConverterTests { val result = outputMessage.getBodyAsString(StandardCharsets.UTF_8) - assertThat(outputMessage.headers).containsEntry("Content-Type", listOf("application/json")) + @Suppress("DEPRECATION") + assertThat(outputMessage.headers.asMultiValueMap()).containsEntry("Content-Type", listOf("application/json")) assertThat(result) .contains("\"bytes\":[1,2]") .contains("\"array\":[\"Foo\",\"Bar\"]") @@ -277,7 +278,8 @@ class KotlinSerializationJsonHttpMessageConverterTests { val result = outputMessage.getBodyAsString(StandardCharsets.UTF_8) - assertThat(outputMessage.headers).containsEntry("Content-Type", listOf("application/json")) + @Suppress("DEPRECATION") + assertThat(outputMessage.headers.asMultiValueMap()).containsEntry("Content-Type", listOf("application/json")) assertThat(result) .contains("\"bytes\":[1,2]") .contains("\"array\":[\"Foo\",\"Bar\"]") @@ -299,7 +301,8 @@ class KotlinSerializationJsonHttpMessageConverterTests { val result = outputMessage.getBodyAsString(StandardCharsets.UTF_8) - assertThat(outputMessage.headers).containsEntry("Content-Type", listOf("application/json")) + @Suppress("DEPRECATION") + assertThat(outputMessage.headers.asMultiValueMap()).containsEntry("Content-Type", listOf("application/json")) assertThat(result).isEqualTo(expectedJson) } @@ -317,7 +320,8 @@ class KotlinSerializationJsonHttpMessageConverterTests { val result = outputMessage.getBodyAsString(StandardCharsets.UTF_8) - assertThat(outputMessage.headers).containsEntry("Content-Type", listOf("application/json")) + @Suppress("DEPRECATION") + assertThat(outputMessage.headers.asMultiValueMap()).containsEntry("Content-Type", listOf("application/json")) assertThat(result).isEqualTo(expectedJson) } @@ -331,7 +335,8 @@ class KotlinSerializationJsonHttpMessageConverterTests { val result = outputMessage.getBodyAsString(StandardCharsets.UTF_16BE) - assertThat(outputMessage.headers).containsEntry("Content-Type", listOf(contentType.toString())) + @Suppress("DEPRECATION") + assertThat(outputMessage.headers.asMultiValueMap()).containsEntry("Content-Type", listOf(contentType.toString())) assertThat(result).isEqualTo("\"H\u00e9llo W\u00f6rld\"") } @@ -367,7 +372,8 @@ class KotlinSerializationJsonHttpMessageConverterTests { val result = outputMessage.getBodyAsString(StandardCharsets.UTF_8) - assertThat(outputMessage.headers).containsEntry("Content-Type", listOf("application/json")) + @Suppress("DEPRECATION") + assertThat(outputMessage.headers.asMultiValueMap()).containsEntry("Content-Type", listOf("application/json")) assertThat(result).isEqualTo("1.0") } @@ -384,7 +390,8 @@ class KotlinSerializationJsonHttpMessageConverterTests { val result = outputMessage.getBodyAsString(StandardCharsets.UTF_8) - assertThat(outputMessage.headers).containsEntry("Content-Type", listOf("application/json")) + @Suppress("DEPRECATION") + assertThat(outputMessage.headers.asMultiValueMap()).containsEntry("Content-Type", listOf("application/json")) assertThat(result).isEqualTo(expectedJson) } diff --git a/spring-web/src/test/kotlin/org/springframework/http/converter/protobuf/KotlinSerializationProtobufHttpMessageConverterTests.kt b/spring-web/src/test/kotlin/org/springframework/http/converter/protobuf/KotlinSerializationProtobufHttpMessageConverterTests.kt index b79efc9929..8449cec11d 100644 --- a/spring-web/src/test/kotlin/org/springframework/http/converter/protobuf/KotlinSerializationProtobufHttpMessageConverterTests.kt +++ b/spring-web/src/test/kotlin/org/springframework/http/converter/protobuf/KotlinSerializationProtobufHttpMessageConverterTests.kt @@ -186,7 +186,8 @@ class KotlinSerializationProtobufHttpMessageConverterTests { this.converter.write(serializableBean, null, outputMessage) - assertThat(outputMessage.headers).containsEntry("Content-Type", listOf("application/x-protobuf")) + @Suppress("DEPRECATION") + assertThat(outputMessage.headers.asMultiValueMap()).containsEntry("Content-Type", listOf("application/x-protobuf")) assertThat(outputMessage.bodyAsBytes.isNotEmpty()).isTrue() } @@ -197,7 +198,8 @@ class KotlinSerializationProtobufHttpMessageConverterTests { this.converter.write(serializableBean, null, outputMessage) - assertThat(outputMessage.headers).containsEntry("Content-Type", listOf("application/x-protobuf")) + @Suppress("DEPRECATION") + assertThat(outputMessage.headers.asMultiValueMap()).containsEntry("Content-Type", listOf("application/x-protobuf")) assertThat(outputMessage.bodyAsBytes.isNotEmpty()).isTrue() } @@ -207,7 +209,8 @@ class KotlinSerializationProtobufHttpMessageConverterTests { this.converter.write(serializableBeanArray, null, outputMessage) - assertThat(outputMessage.headers).containsEntry("Content-Type", listOf("application/x-protobuf")) + @Suppress("DEPRECATION") + assertThat(outputMessage.headers.asMultiValueMap()).containsEntry("Content-Type", listOf("application/x-protobuf")) assertThat(outputMessage.bodyAsBytes.isNotEmpty()).isTrue() } @@ -218,7 +221,8 @@ class KotlinSerializationProtobufHttpMessageConverterTests { this.converter.write(listOf(serializableBean), ResolvableType.forType(typeOf>().javaType), null, outputMessage, null) - assertThat(outputMessage.headers).containsEntry("Content-Type", listOf("application/x-protobuf")) + @Suppress("DEPRECATION") + assertThat(outputMessage.headers.asMultiValueMap()).containsEntry("Content-Type", listOf("application/x-protobuf")) assertThat(outputMessage.bodyAsBytes.isNotEmpty()).isTrue() } diff --git a/spring-web/src/testFixtures/java/org/springframework/web/testfixture/http/server/reactive/MockServerHttpRequest.java b/spring-web/src/testFixtures/java/org/springframework/web/testfixture/http/server/reactive/MockServerHttpRequest.java index 4bd546ace4..491ea6b6b7 100644 --- a/spring-web/src/testFixtures/java/org/springframework/web/testfixture/http/server/reactive/MockServerHttpRequest.java +++ b/spring-web/src/testFixtures/java/org/springframework/web/testfixture/http/server/reactive/MockServerHttpRequest.java @@ -299,9 +299,18 @@ public final class MockServerHttpRequest extends AbstractServerHttpRequest { /** * Add the given header values. * @param headers the header values + * @deprecated Use {@link #headers(HttpHeaders)} */ + @Deprecated B headers(MultiValueMap headers); + /** + * Add the given header values. + * @param headers the header values + * @since 7.0 + */ + B headers(HttpHeaders headers); + /** * Set the list of acceptable {@linkplain MediaType media types}, as * specified by the {@code Accept} header. @@ -491,11 +500,18 @@ public final class MockServerHttpRequest extends AbstractServerHttpRequest { } @Override + @Deprecated public BodyBuilder headers(MultiValueMap headers) { this.headers.putAll(headers); return this; } + @Override + public BodyBuilder headers(HttpHeaders headers) { + this.headers.putAll(headers); + return this; + } + @Override public BodyBuilder accept(MediaType... acceptableMediaTypes) { this.headers.setAccept(Arrays.asList(acceptableMediaTypes)); diff --git a/spring-web/src/testFixtures/java/org/springframework/web/testfixture/servlet/MockPart.java b/spring-web/src/testFixtures/java/org/springframework/web/testfixture/servlet/MockPart.java index 99715488dc..36ae5fcb5f 100644 --- a/spring-web/src/testFixtures/java/org/springframework/web/testfixture/servlet/MockPart.java +++ b/spring-web/src/testFixtures/java/org/springframework/web/testfixture/servlet/MockPart.java @@ -129,7 +129,7 @@ public class MockPart implements Part { @Override public Collection getHeaderNames() { - return this.headers.keySet(); + return this.headers.headerNames(); } /** diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultWebClient.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultWebClient.java index 08109bf3ae..86174a9cc4 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultWebClient.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/DefaultWebClient.java @@ -495,10 +495,10 @@ final class DefaultWebClient implements WebClient { } private void initHeaders(HttpHeaders out) { - if (!CollectionUtils.isEmpty(defaultHeaders)) { + if (defaultHeaders != null && !defaultHeaders.isEmpty()) { out.putAll(defaultHeaders); } - if (!CollectionUtils.isEmpty(this.headers)) { + if (this.headers != null && !this.headers.isEmpty()) { out.putAll(this.headers); } } diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/WebClientRequestException.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/WebClientRequestException.java index b8e7256037..0265a3dcfc 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/WebClientRequestException.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/WebClientRequestException.java @@ -58,7 +58,7 @@ public class WebClientRequestException extends WebClientException { */ private static HttpHeaders copy(HttpHeaders headers) { HttpHeaders result = new HttpHeaders(); - for (Map.Entry> entry : headers.entrySet()) { + for (Map.Entry> entry : headers.headerSet()) { for (String value : entry.getValue()) { result.add(entry.getKey(), value); } diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/WebClientResponseException.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/WebClientResponseException.java index 9e08f919e7..258159c55b 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/WebClientResponseException.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/WebClientResponseException.java @@ -150,7 +150,7 @@ public class WebClientResponseException extends WebClientException { } else { HttpHeaders result = new HttpHeaders(); - for (Map.Entry> entry : headers.entrySet()) { + for (Map.Entry> entry : headers.headerSet()) { for (String value : entry.getValue()) { result.add(entry.getKey(), value); } diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/DefaultServerResponseBuilder.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/DefaultServerResponseBuilder.java index d2736b05ff..e732ec0e57 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/DefaultServerResponseBuilder.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/DefaultServerResponseBuilder.java @@ -357,6 +357,12 @@ class DefaultServerResponseBuilder implements ServerResponse.BodyBuilder { dst.putAll(src); } } + + private static void copy(HttpHeaders src, HttpHeaders dst) { + if (!src.isEmpty()) { + dst.putAll(src); + } + } } diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/ExtendedWebExchangeDataBinder.java b/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/ExtendedWebExchangeDataBinder.java index a018372462..9e7bf0537d 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/ExtendedWebExchangeDataBinder.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/ExtendedWebExchangeDataBinder.java @@ -85,7 +85,7 @@ public class ExtendedWebExchangeDataBinder extends WebExchangeDataBinder { vars.forEach((key, value) -> addValueIfNotPresent(map, "URI variable", key, value)); } HttpHeaders headers = exchange.getRequest().getHeaders(); - for (Map.Entry> entry : headers.entrySet()) { + for (Map.Entry> entry : headers.headerSet()) { String name = entry.getKey(); if (!this.headerPredicate.test(entry.getKey())) { continue; diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestHeaderMapMethodArgumentResolver.java b/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestHeaderMapMethodArgumentResolver.java index f4da262a4e..fd2d4c3265 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestHeaderMapMethodArgumentResolver.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestHeaderMapMethodArgumentResolver.java @@ -55,17 +55,22 @@ public class RequestHeaderMapMethodArgumentResolver extends HandlerMethodArgumen } private boolean allParams(RequestHeader annotation, Class type) { - return Map.class.isAssignableFrom(type); + return Map.class.isAssignableFrom(type) || HttpHeaders.class.isAssignableFrom(type); } + @SuppressWarnings("deprecation") @Override public Object resolveArgumentValue( MethodParameter methodParameter, BindingContext context, ServerWebExchange exchange) { boolean isMultiValueMap = MultiValueMap.class.isAssignableFrom(methodParameter.getParameterType()); HttpHeaders headers = exchange.getRequest().getHeaders(); - return (isMultiValueMap ? headers : headers.toSingleValueMap()); + if (isMultiValueMap) { + return headers.asMultiValueMap(); + } + boolean isHttpHeaders = HttpHeaders.class.isAssignableFrom(methodParameter.getParameterType()); + return (isHttpHeaders ? headers : headers.toSingleValueMap()); } } diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestHeaderMethodArgumentResolver.java b/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestHeaderMethodArgumentResolver.java index c4b3205368..e183a9908c 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestHeaderMethodArgumentResolver.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestHeaderMethodArgumentResolver.java @@ -25,6 +25,7 @@ import org.springframework.beans.factory.config.ConfigurableBeanFactory; import org.springframework.core.MethodParameter; import org.springframework.core.ReactiveAdapterRegistry; import org.springframework.core.convert.ConversionService; +import org.springframework.http.HttpHeaders; import org.springframework.util.Assert; import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.server.MissingRequestValueException; @@ -68,7 +69,7 @@ public class RequestHeaderMethodArgumentResolver extends AbstractNamedValueSyncA } private boolean singleParam(RequestHeader annotation, Class type) { - return !Map.class.isAssignableFrom(type); + return !Map.class.isAssignableFrom(type) && !HttpHeaders.class.isAssignableFrom(type); } @Override diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/result/view/ViewResolutionResultHandler.java b/spring-webflux/src/main/java/org/springframework/web/reactive/result/view/ViewResolutionResultHandler.java index a574679df6..afad15a841 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/result/view/ViewResolutionResultHandler.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/result/view/ViewResolutionResultHandler.java @@ -473,7 +473,7 @@ public class ViewResolutionResultHandler extends HandlerResultHandlerSupport imp @Override public HttpHeaders getHeaders() { - if (!super.getHeaders().containsKey(HttpHeaders.CONTENT_TYPE)) { + if (!super.getHeaders().containsHeader(HttpHeaders.CONTENT_TYPE)) { return super.getHeaders(); } // Content-type is set, ignore further updates diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/socket/client/JettyWebSocketClient.java b/spring-webflux/src/main/java/org/springframework/web/reactive/socket/client/JettyWebSocketClient.java index d0b6f352ff..3ba4d10ca6 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/socket/client/JettyWebSocketClient.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/socket/client/JettyWebSocketClient.java @@ -92,7 +92,7 @@ public class JettyWebSocketClient implements WebSocketClient, Lifecycle { ClientUpgradeRequest upgradeRequest = new ClientUpgradeRequest(); upgradeRequest.setSubProtocols(handler.getSubProtocols()); if (headers != null) { - headers.keySet().forEach(header -> upgradeRequest.setHeader(header, headers.getValuesAsList(header))); + headers.headerNames().forEach(header -> upgradeRequest.setHeader(header, headers.getValuesAsList(header))); } final AtomicReference handshakeInfo = new AtomicReference<>(); diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/socket/client/StandardWebSocketClient.java b/spring-webflux/src/main/java/org/springframework/web/reactive/socket/client/StandardWebSocketClient.java index 8158eda5f9..e07724e9d6 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/socket/client/StandardWebSocketClient.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/socket/client/StandardWebSocketClient.java @@ -179,12 +179,12 @@ public class StandardWebSocketClient implements WebSocketClient { @Override public void beforeRequest(Map> requestHeaders) { - requestHeaders.putAll(this.requestHeaders); + this.requestHeaders.forEach(requestHeaders::put); } @Override public void afterResponse(HandshakeResponse response) { - this.responseHeaders.putAll(response.getHeaders()); + response.getHeaders().forEach(this.responseHeaders::put); } } diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/socket/client/UndertowWebSocketClient.java b/spring-webflux/src/main/java/org/springframework/web/reactive/socket/client/UndertowWebSocketClient.java index 8b68b19c8b..b211c6c0cf 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/socket/client/UndertowWebSocketClient.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/socket/client/UndertowWebSocketClient.java @@ -242,7 +242,7 @@ public class UndertowWebSocketClient implements WebSocketClient { @Override public void beforeRequest(Map> headers) { - headers.putAll(this.requestHeaders); + this.requestHeaders.forEach(headers::put); if (this.delegate != null) { this.delegate.beforeRequest(headers); } diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/DefaultClientRequestBuilderTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/DefaultClientRequestBuilderTests.java index 90fb4c3b66..b3de5dedc7 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/DefaultClientRequestBuilderTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/DefaultClientRequestBuilderTests.java @@ -70,7 +70,7 @@ class DefaultClientRequestBuilderTests { assertThat(result.url()).isEqualTo(DEFAULT_URL); assertThat(result.method()).isEqualTo(GET); - assertThat(result.headers()).hasSize(1); + assertThat(result.headers().size()).isOne(); assertThat(result.headers().getFirst("foo")).isEqualTo("baar"); assertThat(result.cookies()).hasSize(1); assertThat(result.cookies().getFirst("baz")).isEqualTo("quux"); diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/DefaultClientResponseBuilderTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/DefaultClientResponseBuilderTests.java index 63d193b8ce..c9212f86af 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/DefaultClientResponseBuilderTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/DefaultClientResponseBuilderTests.java @@ -88,7 +88,7 @@ class DefaultClientResponseBuilderTests { assertThat(result.statusCode()).isEqualTo(HttpStatus.BAD_REQUEST); - assertThat(result.headers().asHttpHeaders()).hasSize(3); + assertThat(result.headers().asHttpHeaders().size()).isEqualTo(3); assertThat(result.headers().asHttpHeaders().getFirst("foo")).isEqualTo("baar"); assertThat(result.headers().asHttpHeaders().getFirst("bar")).isEqualTo("baz"); assertThat(result.cookies()).hasSize(1); diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/DefaultClientResponseTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/DefaultClientResponseTests.java index d6f9a24dad..0c0a4c8695 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/DefaultClientResponseTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/DefaultClientResponseTests.java @@ -338,7 +338,7 @@ class DefaultClientResponseTests { WebClientResponseException exception = resultMono.block(); assertThat(exception.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND); assertThat(exception.getMessage()).isEqualTo("404 Not Found from UNKNOWN https://example.org:9999/app/path"); - assertThat(exception.getHeaders()).containsExactly(entry("Content-Type", List.of("text/plain"))); + assertThat(exception.getHeaders().asMultiValueMap()).containsExactly(entry("Content-Type", List.of("text/plain"))); assertThat(exception.getResponseBodyAsByteArray()).isEqualTo(bytes); } @@ -397,7 +397,7 @@ class DefaultClientResponseTests { WebClientResponseException exception = (WebClientResponseException) t; assertThat(exception.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND); assertThat(exception.getMessage()).isEqualTo("404 Not Found"); - assertThat(exception.getHeaders()).containsExactly(entry("Content-Type",List.of("text/plain"))); + assertThat(exception.getHeaders().asMultiValueMap()).containsExactly(entry("Content-Type",List.of("text/plain"))); assertThat(exception.getResponseBodyAsByteArray()).isEqualTo(bytes); }) .verify(); diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/DefaultWebClientTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/DefaultWebClientTests.java index 16c433429f..3c7fa0b21d 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/DefaultWebClientTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/DefaultWebClientTests.java @@ -295,17 +295,17 @@ public class DefaultWebClientTests { WebClient.Builder builder1 = client1.mutate(); builder1.filters(filters -> assertThat(filters).hasSize(1)); - builder1.defaultHeaders(headers -> assertThat(headers).hasSize(1)); + builder1.defaultHeaders(headers -> assertThat(headers.size()).isOne()); builder1.defaultCookies(cookies -> assertThat(cookies).hasSize(1)); WebClient.Builder builder2 = client2.mutate(); builder2.filters(filters -> assertThat(filters).hasSize(2)); - builder2.defaultHeaders(headers -> assertThat(headers).hasSize(2)); + builder2.defaultHeaders(headers -> assertThat(headers.size()).isEqualTo(2)); builder2.defaultCookies(cookies -> assertThat(cookies).hasSize(2)); WebClient.Builder builder1a = client1a.mutate(); builder1a.filters(filters -> assertThat(filters).hasSize(2)); - builder1a.defaultHeaders(headers -> assertThat(headers).hasSize(2)); + builder1a.defaultHeaders(headers -> assertThat(headers.size()).isEqualTo(2)); builder1a.defaultCookies(cookies -> assertThat(cookies).hasSize(2)); } diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/ExchangeFilterFunctionsTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/ExchangeFilterFunctionsTests.java index 8a7c0ab0ef..e9ff0e264a 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/ExchangeFilterFunctionsTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/ExchangeFilterFunctionsTests.java @@ -103,13 +103,13 @@ class ExchangeFilterFunctionsTests { ClientResponse response = mock(); ExchangeFunction exchange = r -> { - assertThat(r.headers().containsKey(HttpHeaders.AUTHORIZATION)).isTrue(); + assertThat(r.headers().containsHeader(HttpHeaders.AUTHORIZATION)).isTrue(); assertThat(r.headers().getFirst(HttpHeaders.AUTHORIZATION)).startsWith("Basic "); return Mono.just(response); }; ExchangeFilterFunction auth = ExchangeFilterFunctions.basicAuthentication("foo", "bar"); - assertThat(request.headers().containsKey(HttpHeaders.AUTHORIZATION)).isFalse(); + assertThat(request.headers().containsHeader(HttpHeaders.AUTHORIZATION)).isFalse(); ClientResponse result = auth.filter(request, exchange).block(); assertThat(result).isEqualTo(response); } @@ -133,13 +133,13 @@ class ExchangeFilterFunctionsTests { ClientResponse response = mock(); ExchangeFunction exchange = r -> { - assertThat(r.headers().containsKey(HttpHeaders.AUTHORIZATION)).isTrue(); + assertThat(r.headers().containsHeader(HttpHeaders.AUTHORIZATION)).isTrue(); assertThat(r.headers().getFirst(HttpHeaders.AUTHORIZATION)).startsWith("Basic "); return Mono.just(response); }; ExchangeFilterFunction auth = ExchangeFilterFunctions.basicAuthentication(); - assertThat(request.headers().containsKey(HttpHeaders.AUTHORIZATION)).isFalse(); + assertThat(request.headers().containsHeader(HttpHeaders.AUTHORIZATION)).isFalse(); ClientResponse result = auth.filter(request, exchange).block(); assertThat(result).isEqualTo(response); } @@ -151,12 +151,12 @@ class ExchangeFilterFunctionsTests { ClientResponse response = mock(); ExchangeFunction exchange = r -> { - assertThat(r.headers().containsKey(HttpHeaders.AUTHORIZATION)).isFalse(); + assertThat(r.headers().containsHeader(HttpHeaders.AUTHORIZATION)).isFalse(); return Mono.just(response); }; ExchangeFilterFunction auth = ExchangeFilterFunctions.basicAuthentication(); - assertThat(request.headers().containsKey(HttpHeaders.AUTHORIZATION)).isFalse(); + assertThat(request.headers().containsHeader(HttpHeaders.AUTHORIZATION)).isFalse(); ClientResponse result = auth.filter(request, exchange).block(); assertThat(result).isEqualTo(response); } diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/WebClientDataBufferAllocatingTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/WebClientDataBufferAllocatingTests.java index 8b67d28902..f335c732d7 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/WebClientDataBufferAllocatingTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/WebClientDataBufferAllocatingTests.java @@ -204,7 +204,7 @@ class WebClientDataBufferAllocatingTests extends AbstractDataBufferAllocatingTes StepVerifier.create(result) .assertNext(entity -> { assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.CREATED); - assertThat(entity.getHeaders()).containsEntry("Foo", Collections.singletonList("bar")); + assertThat(entity.getHeaders().hasHeaderValues("Foo", Collections.singletonList("bar"))).isTrue(); assertThat(entity.getBody()).isNull(); }) .expectComplete() diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/WebClientObservationTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/WebClientObservationTests.java index 1e5699e367..5f403d812a 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/WebClientObservationTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/WebClientObservationTests.java @@ -91,7 +91,7 @@ class WebClientObservationTests { assertThatHttpObservation().hasLowCardinalityKeyValue("outcome", "SUCCESS") .hasLowCardinalityKeyValue("uri", "/base/resource/{id}"); - assertThat(clientRequest.headers()).containsEntry("foo", Collections.singletonList("bar")); + assertThat(clientRequest.headers().hasHeaderValues("foo", Collections.singletonList("bar"))).isTrue(); } @Test diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/function/server/DefaultServerRequestBuilderTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/function/server/DefaultServerRequestBuilderTests.java index ff21883457..ee82d209e6 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/function/server/DefaultServerRequestBuilderTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/function/server/DefaultServerRequestBuilderTests.java @@ -73,7 +73,7 @@ class DefaultServerRequestBuilderTests { assertThat(result.uri()).isEqualTo(uri); assertThat(result.requestPath().pathWithinApplication().value()).isEqualTo("/bar"); assertThat(result.requestPath().contextPath().value()).isEqualTo("/foo"); - assertThat(result.headers().asHttpHeaders()).hasSize(1); + assertThat(result.headers().asHttpHeaders().size()).isOne(); assertThat(result.headers().asHttpHeaders().getFirst("foo")).isEqualTo("baar"); assertThat(result.cookies()).hasSize(1); assertThat(result.cookies().getFirst("baz").getValue()).isEqualTo("quux"); diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/resource/ResourceWebHandlerTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/resource/ResourceWebHandlerTests.java index c6574131ee..00aefd8f8d 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/resource/ResourceWebHandlerTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/resource/ResourceWebHandlerTests.java @@ -417,7 +417,7 @@ class ResourceWebHandlerTests { this.handler.handle(exchange).block(TIMEOUT); HttpHeaders headers = exchange.getResponse().getHeaders(); - assertThat(headers.containsKey("Last-Modified")).isTrue(); + assertThat(headers.containsHeader("Last-Modified")).isTrue(); assertThat(resourceLastModifiedDate("test/foo.css") / 1000).isEqualTo(headers.getLastModified() / 1000); } @@ -448,7 +448,7 @@ class ResourceWebHandlerTests { MockServerHttpResponse response = exchange.getResponse(); assertThat(response.getHeaders().getCacheControl()).isEqualTo("no-store"); - assertThat(response.getHeaders().containsKey("Last-Modified")).isTrue(); + assertThat(response.getHeaders().containsHeader("Last-Modified")).isTrue(); assertThat(resourceLastModifiedDate("test/foo.css") / 1000).isEqualTo(response.getHeaders().getLastModified() / 1000); } @@ -561,7 +561,7 @@ class ResourceWebHandlerTests { HttpHeaders headers = exchange.getResponse().getHeaders(); assertThat(headers.getContentType()).isEqualTo(MediaType.parseMediaType("text/css")); assertThat(headers.getContentLength()).isEqualTo(17); - assertThat(headers.containsKey("Last-Modified")).isFalse(); + assertThat(headers.containsHeader("Last-Modified")).isFalse(); assertResponseBody(exchange, "h1 { color:red; }"); } diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/AbstractRequestMappingIntegrationTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/AbstractRequestMappingIntegrationTests.java index 5c2b0559e4..a126f2b380 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/AbstractRequestMappingIntegrationTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/AbstractRequestMappingIntegrationTests.java @@ -123,7 +123,7 @@ public abstract class AbstractRequestMappingIntegrationTests extends AbstractHtt } private void addHeaders(RequestEntity.HeadersBuilder builder, HttpHeaders headers) { - for (Map.Entry> entry : headers.entrySet()) { + for (Map.Entry> entry : headers.headerSet()) { for (String value : entry.getValue()) { builder.header(entry.getKey(), value); } diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/ResponseEntityResultHandlerTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/ResponseEntityResultHandlerTests.java index de0452b586..28009efd3c 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/ResponseEntityResultHandlerTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/ResponseEntityResultHandlerTests.java @@ -157,7 +157,7 @@ class ResponseEntityResultHandlerTests { this.resultHandler.handleResult(exchange, result).block(Duration.ofSeconds(5)); assertThat(exchange.getResponse().getStatusCode()).isEqualTo(HttpStatus.NO_CONTENT); - assertThat(exchange.getResponse().getHeaders()).isEmpty(); + assertThat(exchange.getResponse().getHeaders().isEmpty()).isTrue(); assertResponseBodyIsEmpty(exchange); } @@ -171,7 +171,7 @@ class ResponseEntityResultHandlerTests { this.resultHandler.handleResult(exchange, result).block(Duration.ofSeconds(5)); assertThat(exchange.getResponse().getStatusCode()).isEqualTo(HttpStatus.OK); - assertThat(exchange.getResponse().getHeaders()).hasSize(1); + assertThat(exchange.getResponse().getHeaders().size()).isOne(); assertThat(exchange.getResponse().getHeaders().getFirst("Allow")).isEqualTo("GET,POST,OPTIONS"); assertResponseBodyIsEmpty(exchange); } @@ -186,7 +186,7 @@ class ResponseEntityResultHandlerTests { this.resultHandler.handleResult(exchange, result).block(Duration.ofSeconds(5)); assertThat(exchange.getResponse().getStatusCode()).isEqualTo(HttpStatus.CREATED); - assertThat(exchange.getResponse().getHeaders()).hasSize(1); + assertThat(exchange.getResponse().getHeaders().size()).isOne(); assertThat(exchange.getResponse().getHeaders().getLocation()).isEqualTo(location); assertResponseBodyIsEmpty(exchange); } @@ -236,7 +236,7 @@ class ResponseEntityResultHandlerTests { this.resultHandler.handleResult(exchange, result).block(Duration.ofSeconds(5)); assertThat(exchange.getResponse().getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST); - assertThat(exchange.getResponse().getHeaders()).hasSize(3); + assertThat(exchange.getResponse().getHeaders().size()).isEqualTo(3); assertThat(exchange.getResponse().getHeaders().get("foo")).containsExactly("bar"); assertThat(exchange.getResponse().getHeaders().getContentType()).isEqualTo(MediaType.APPLICATION_PROBLEM_JSON); assertResponseBody(exchange, @@ -256,7 +256,7 @@ class ResponseEntityResultHandlerTests { this.resultHandler.handleResult(exchange, result).block(Duration.ofSeconds(5)); assertThat(exchange.getResponse().getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST); - assertThat(exchange.getResponse().getHeaders()).hasSize(2); + assertThat(exchange.getResponse().getHeaders().size()).isEqualTo(2); assertThat(exchange.getResponse().getHeaders().getContentType()).isEqualTo(MediaType.APPLICATION_PROBLEM_JSON); assertResponseBody(exchange, "{\"type\":\"about:blank\"," + @@ -385,7 +385,7 @@ class ResponseEntityResultHandlerTests { this.resultHandler.handleResult(exchange, result).block(Duration.ofSeconds(5)); assertThat(exchange.getResponse().getStatusCode()).isEqualTo(HttpStatus.OK); - assertThat(exchange.getResponse().getHeaders()).hasSize(1); + assertThat(exchange.getResponse().getHeaders().size()).isOne(); assertThat(exchange.getResponse().getHeaders().getContentType()).isEqualTo(MediaType.APPLICATION_JSON); assertResponseBodyIsEmpty(exchange); } diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/result/view/DefaultRenderingBuilderTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/result/view/DefaultRenderingBuilderTests.java index 287a0377fa..d8e44b4b89 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/result/view/DefaultRenderingBuilderTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/result/view/DefaultRenderingBuilderTests.java @@ -41,7 +41,7 @@ class DefaultRenderingBuilderTests { assertThat(rendering.view()).isEqualTo("abc"); assertThat(rendering.modelAttributes()).isEqualTo(Collections.emptyMap()); assertThat(rendering.status()).isNull(); - assertThat(rendering.headers()).isEmpty(); + assertThat(rendering.headers().isEmpty()).isTrue(); } @Test @@ -97,7 +97,7 @@ class DefaultRenderingBuilderTests { void header() { Rendering rendering = Rendering.view("foo").header("foo", "bar").build(); - assertThat(rendering.headers()).hasSize(1); + assertThat(rendering.headers().size()).isOne(); assertThat(rendering.headers().get("foo")).isEqualTo(Collections.singletonList("bar")); } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/function/DefaultServerRequest.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/function/DefaultServerRequest.java index 2e168c72c4..45881cd478 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/function/DefaultServerRequest.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/function/DefaultServerRequest.java @@ -606,7 +606,7 @@ class DefaultServerRequest implements ServerRequest { @Override public boolean containsHeader(String name) { - return this.headers.containsKey(name); + return this.headers.containsHeader(name); } @Override @@ -647,7 +647,7 @@ class DefaultServerRequest implements ServerRequest { @Override public Collection getHeaderNames() { - return this.headers.keySet(); + return this.headers.headerNames(); } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/AbstractMessageConverterMethodProcessor.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/AbstractMessageConverterMethodProcessor.java index 5fcf91ddb1..32084d33e5 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/AbstractMessageConverterMethodProcessor.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/AbstractMessageConverterMethodProcessor.java @@ -489,7 +489,7 @@ public abstract class AbstractMessageConverterMethodProcessor extends AbstractMe */ private void addContentDispositionHeader(ServletServerHttpRequest request, ServletServerHttpResponse response) { HttpHeaders headers = response.getHeaders(); - if (headers.containsKey(HttpHeaders.CONTENT_DISPOSITION)) { + if (headers.containsHeader(HttpHeaders.CONTENT_DISPOSITION)) { return; } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/HttpEntityMethodProcessor.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/HttpEntityMethodProcessor.java index 97a97e1a31..1dcb6f94ed 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/HttpEntityMethodProcessor.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/HttpEntityMethodProcessor.java @@ -224,7 +224,7 @@ public class HttpEntityMethodProcessor extends AbstractMessageConverterMethodPro HttpHeaders entityHeaders = httpEntity.getHeaders(); if (!entityHeaders.isEmpty()) { entityHeaders.forEach((key, value) -> { - if (HttpHeaders.VARY.equals(key) && outputHeaders.containsKey(HttpHeaders.VARY)) { + if (HttpHeaders.VARY.equals(key) && outputHeaders.containsHeader(HttpHeaders.VARY)) { List values = getVaryRequestHeadersToAdd(outputHeaders, entityHeaders); if (!values.isEmpty()) { outputHeaders.setVary(values); diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/ResponseEntityExceptionHandlerTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/ResponseEntityExceptionHandlerTests.java index 3b47c09bc7..8d6b079be1 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/ResponseEntityExceptionHandlerTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/ResponseEntityExceptionHandlerTests.java @@ -301,7 +301,7 @@ class ResponseEntityExceptionHandlerTests { ResponseEntity responseEntity = testException(new NoHandlerFoundException("GET", "/resource", requestHeaders)); - assertThat(responseEntity.getHeaders()).isEmpty(); + assertThat(responseEntity.getHeaders().isEmpty()).isTrue(); } @Test diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/ServletAnnotationControllerHandlerMethodTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/ServletAnnotationControllerHandlerMethodTests.java index a8435ec981..e302d28d24 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/ServletAnnotationControllerHandlerMethodTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/method/annotation/ServletAnnotationControllerHandlerMethodTests.java @@ -3576,7 +3576,21 @@ class ServletAnnotationControllerHandlerMethodTests extends AbstractServletHandl @RequestMapping("/httpHeaders") public void httpHeaders(@RequestHeader HttpHeaders headers, Writer writer) throws IOException { assertThat(headers.getContentType()).as("Invalid Content-Type").isEqualTo(new MediaType("text", "html")); - multiValueMap(headers, writer); + for (Iterator>> it1 = headers.headerSet().iterator(); it1.hasNext();) { + Map.Entry> entry = it1.next(); + writer.write(entry.getKey() + "=["); + for (Iterator it2 = entry.getValue().iterator(); it2.hasNext();) { + String value = it2.next(); + writer.write(value); + if (it2.hasNext()) { + writer.write(','); + } + } + writer.write(']'); + if (it1.hasNext()) { + writer.write(','); + } + } } } diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/WebSocketHttpHeaders.java b/spring-websocket/src/main/java/org/springframework/web/socket/WebSocketHttpHeaders.java index 167714ccd8..06ebf19469 100644 --- a/spring-websocket/src/main/java/org/springframework/web/socket/WebSocketHttpHeaders.java +++ b/spring-websocket/src/main/java/org/springframework/web/socket/WebSocketHttpHeaders.java @@ -17,7 +17,6 @@ package org.springframework.web.socket; import java.util.ArrayList; -import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.Map; @@ -244,30 +243,30 @@ public class WebSocketHttpHeaders extends HttpHeaders { } @Override - public boolean containsKey(Object key) { - return this.headers.containsKey(key); + public boolean containsHeader(String key) { + return this.headers.containsHeader(key); } @Override - public boolean containsValue(Object value) { - return this.headers.containsValue(value); + public @Nullable List get(String headerName) { + return this.headers.get(headerName); } @Override - public @Nullable List get(Object key) { - return this.headers.get(key); - } - - @Override - public List put(String key, List value) { + public @Nullable List put(String key, List value) { return this.headers.put(key, value); } @Override - public List remove(Object key) { + public @Nullable List remove(String key) { return this.headers.remove(key); } + @Override + public void putAll(HttpHeaders headers) { + this.headers.putAll(headers); + } + @Override public void putAll(Map> m) { this.headers.putAll(m); @@ -279,18 +278,13 @@ public class WebSocketHttpHeaders extends HttpHeaders { } @Override - public Set keySet() { - return this.headers.keySet(); + public Set headerNames() { + return this.headers.headerNames(); } @Override - public Collection> values() { - return this.headers.values(); - } - - @Override - public Set>> entrySet() { - return this.headers.entrySet(); + public Set>> headerSet() { + return this.headers.headerSet(); } @Override @@ -299,8 +293,8 @@ public class WebSocketHttpHeaders extends HttpHeaders { } @Override - public List putIfAbsent(String key, List value) { - return this.headers.putIfAbsent(key, value); + public @Nullable List putIfAbsent(String headerName, List headerValues) { + return this.headers.putIfAbsent(headerName, headerValues); } diff --git a/spring-websocket/src/main/java/org/springframework/web/socket/client/standard/StandardWebSocketClient.java b/spring-websocket/src/main/java/org/springframework/web/socket/client/standard/StandardWebSocketClient.java index 6d778c55b5..aa9557c977 100644 --- a/spring-websocket/src/main/java/org/springframework/web/socket/client/standard/StandardWebSocketClient.java +++ b/spring-websocket/src/main/java/org/springframework/web/socket/client/standard/StandardWebSocketClient.java @@ -216,7 +216,7 @@ public class StandardWebSocketClient extends AbstractWebSocketClient { @Override public void beforeRequest(Map> requestHeaders) { - requestHeaders.putAll(this.headers); + this.headers.forEach(requestHeaders::put); if (logger.isTraceEnabled()) { logger.trace("Handshake request headers: " + requestHeaders); } diff --git a/spring-websocket/src/test/java/org/springframework/web/socket/client/standard/StandardWebSocketClientTests.java b/spring-websocket/src/test/java/org/springframework/web/socket/client/standard/StandardWebSocketClientTests.java index eef50c7d85..bc9314775c 100644 --- a/spring-websocket/src/test/java/org/springframework/web/socket/client/standard/StandardWebSocketClientTests.java +++ b/spring-websocket/src/test/java/org/springframework/web/socket/client/standard/StandardWebSocketClientTests.java @@ -100,7 +100,7 @@ class StandardWebSocketClientTests { WebSocketSession session = this.wsClient.execute(this.wsHandler, this.headers, uri).get(); - assertThat(session.getHandshakeHeaders()).hasSize(1); + assertThat(session.getHandshakeHeaders().size()).isOne(); assertThat(session.getHandshakeHeaders().getFirst("foo")).isEqualTo("bar"); } diff --git a/spring-websocket/src/test/java/org/springframework/web/socket/sockjs/client/SockJsClientTests.java b/spring-websocket/src/test/java/org/springframework/web/socket/sockjs/client/SockJsClientTests.java index 88b3321c8e..6aeb5e0a72 100644 --- a/spring-websocket/src/test/java/org/springframework/web/socket/sockjs/client/SockJsClientTests.java +++ b/spring-websocket/src/test/java/org/springframework/web/socket/sockjs/client/SockJsClientTests.java @@ -108,12 +108,12 @@ class SockJsClientTests { this.sockJsClient.execute(handler, headers, URI.create(URL)).whenComplete(this.connectCallback); HttpHeaders httpHeaders = headersCaptor.getValue(); - assertThat(httpHeaders).hasSize(2); + assertThat(httpHeaders.size()).isEqualTo(2); assertThat(httpHeaders.getFirst("foo")).isEqualTo("bar"); assertThat(httpHeaders.getFirst("auth")).isEqualTo("123"); httpHeaders = this.xhrTransport.getRequest().getHttpRequestHeaders(); - assertThat(httpHeaders).hasSize(2); + assertThat(httpHeaders.size()).isEqualTo(2); assertThat(httpHeaders.getFirst("foo")).isEqualTo("bar"); assertThat(httpHeaders.getFirst("auth")).isEqualTo("123"); } @@ -129,9 +129,9 @@ class SockJsClientTests { this.sockJsClient.setHttpHeaderNames("auth"); this.sockJsClient.execute(handler, headers, URI.create(URL)).whenComplete(this.connectCallback); - assertThat(headersCaptor.getValue()).hasSize(1); + assertThat(headersCaptor.getValue().size()).isEqualTo(1); assertThat(headersCaptor.getValue().getFirst("auth")).isEqualTo("123"); - assertThat(this.xhrTransport.getRequest().getHttpRequestHeaders()).hasSize(1); + assertThat(this.xhrTransport.getRequest().getHttpRequestHeaders().size()).isEqualTo(1); assertThat(this.xhrTransport.getRequest().getHttpRequestHeaders().getFirst("auth")).isEqualTo("123"); } diff --git a/spring-websocket/src/test/java/org/springframework/web/socket/sockjs/client/XhrTransportTests.java b/spring-websocket/src/test/java/org/springframework/web/socket/sockjs/client/XhrTransportTests.java index 51ba2749b0..fc7be911b6 100644 --- a/spring-websocket/src/test/java/org/springframework/web/socket/sockjs/client/XhrTransportTests.java +++ b/spring-websocket/src/test/java/org/springframework/web/socket/sockjs/client/XhrTransportTests.java @@ -69,7 +69,7 @@ class XhrTransportTests { transport.sendMessageResponseToReturn = new ResponseEntity<>(HttpStatus.NO_CONTENT); URI url = URI.create("https://example.com"); transport.executeSendRequest(url, requestHeaders, new TextMessage("payload")); - assertThat(transport.actualSendRequestHeaders).hasSize(2); + assertThat(transport.actualSendRequestHeaders.size()).isEqualTo(2); assertThat(transport.actualSendRequestHeaders.getFirst("foo")).isEqualTo("bar"); assertThat(transport.actualSendRequestHeaders.getContentType()).isEqualTo(MediaType.APPLICATION_JSON); } @@ -105,7 +105,7 @@ class XhrTransportTests { verify(request).getHttpRequestHeaders(); verifyNoMoreInteractions(request); - assertThat(transport.actualHandshakeHeaders).hasSize(1); + assertThat(transport.actualHandshakeHeaders.size()).isOne(); assertThat(transport.actualHandshakeHeaders.getOrigin()).isEqualTo("foo"); assertThat(transport.actualSession.isDisconnected()).isFalse();