From 2a846c95942289c8ffd4f4a03c1bb67e03c11e01 Mon Sep 17 00:00:00 2001 From: "Bryce J. Fisher" Date: Wed, 29 Jan 2025 17:07:56 -0700 Subject: [PATCH] Add HttpHeaders.copyOf factory method Prior to this commit, the `HttpHeaders` class would provide constructor variants where the instances are are backed by the existing headers collection given as a parameter. While such constructors are clearly documented and meant for internal usage, there are cases where developers would like to copy the data from an existing headers instance without being backed by the same collection instance and thus, being mutated from some place else. This commit introduces new factory methods `HttpHeaders.copyOf` for this purpose. While this name aligns with some of the Java collections factory methods, in this case the returned instance is not immutable, on purpose. `HttpHeaders` does not extends `MultiValueMap` anymore and shouldn't be seen as such. Closes: gh-34341 Signed-off-by: Bryce J. Fisher [brian.clozel@broadcom.com: reduce scope and update javadoc] Signed-off-by: Brian Clozel --- .../org/springframework/http/HttpHeaders.java | 19 +++++++++++++++++++ .../http/ReadOnlyHttpHeaders.java | 1 - .../http/HttpHeadersTests.java | 18 +++++++++++++++++- 3 files changed, 36 insertions(+), 2 deletions(-) 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 f0432d50502..c9dc894eeca 100644 --- a/spring-web/src/main/java/org/springframework/http/HttpHeaders.java +++ b/spring-web/src/main/java/org/springframework/http/HttpHeaders.java @@ -491,6 +491,25 @@ public class HttpHeaders implements Serializable { } } + /** + * Create a new {@code HttpHeaders} mutable instance and copy all header values given as a parameter. + * @param headers the headers to copy + * @since 7.0 + */ + public static HttpHeaders copyOf(MultiValueMap headers) { + HttpHeaders httpHeadersCopy = new HttpHeaders(); + headers.forEach((key, values) -> httpHeadersCopy.put(key, new ArrayList<>(values))); + return httpHeadersCopy; + } + + /** + * Create a new {@code HttpHeaders} mutable instance and copy all header values given as a parameter. + * @param httpHeaders the headers to copy + * @since 7.0 + */ + public static HttpHeaders copyOf(HttpHeaders httpHeaders) { + return copyOf(httpHeaders.headers); + } /** * Get the list of header values for the given header name, if any. 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 eb5fafdc41d..8ec4278c569 100644 --- a/spring-web/src/main/java/org/springframework/http/ReadOnlyHttpHeaders.java +++ b/spring-web/src/main/java/org/springframework/http/ReadOnlyHttpHeaders.java @@ -49,7 +49,6 @@ class ReadOnlyHttpHeaders extends HttpHeaders { @SuppressWarnings("serial") private @Nullable List cachedAccept; - ReadOnlyHttpHeaders(MultiValueMap headers) { super(headers); } 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 9045bcdfefb..0fdc071b407 100644 --- a/spring-web/src/test/java/org/springframework/http/HttpHeadersTests.java +++ b/spring-web/src/test/java/org/springframework/http/HttpHeadersTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -77,6 +77,22 @@ class HttpHeadersTests { writeable.setContentType(MediaType.APPLICATION_JSON); } + @Test + void copyOfCopiesHeaders() { + headers.setContentType(MediaType.APPLICATION_JSON); + headers.add("X-Project", "Spring"); + headers.add("X-Project", "Framework"); + HttpHeaders readOnly = HttpHeaders.readOnlyHttpHeaders(headers); + assertThat(readOnly.getContentType()).isEqualTo(MediaType.APPLICATION_JSON); + + HttpHeaders writable = HttpHeaders.copyOf(readOnly); + writable.setContentType(MediaType.TEXT_PLAIN); + // content-type value is cached by ReadOnlyHttpHeaders + assertThat(readOnly.getContentType()).isEqualTo(MediaType.APPLICATION_JSON); + assertThat(writable.getContentType()).isEqualTo(MediaType.TEXT_PLAIN); + assertThat(writable.get("X-Project")).contains("Spring", "Framework"); + } + @Test void getOrEmpty() { String key = "FOO";