Introduce HttpHeaders#headerSet to guarantee case-insensitive iteration
The `HttpHeaders#headerSet` method is intended as a drop-in replacement for `entrySet` that guarantees a single casing for all header names reported during the iteration, as the cost of some overhead but with support for iterator removal and entry value-setting. The `formatHeaders` static method is also altered to do a similar deduplication of casing variants, but now additionally mentions "with native header names [native name set]" if the native name set contains casing variants. Closes gh-33823
This commit is contained in:
		
							parent
							
								
									4ef2b429e0
								
							
						
					
					
						commit
						9b3cb15389
					
				| 
						 | 
				
			
			@ -0,0 +1,124 @@
 | 
			
		|||
/*
 | 
			
		||||
 * Copyright 2002-2024 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.
 | 
			
		||||
 * You may obtain a copy of the License at
 | 
			
		||||
 *
 | 
			
		||||
 *      https://www.apache.org/licenses/LICENSE-2.0
 | 
			
		||||
 *
 | 
			
		||||
 * Unless required by applicable law or agreed to in writing, software
 | 
			
		||||
 * distributed under the License is distributed on an "AS IS" BASIS,
 | 
			
		||||
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
			
		||||
 * See the License for the specific language governing permissions and
 | 
			
		||||
 * limitations under the License.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
package org.springframework.http.support;
 | 
			
		||||
 | 
			
		||||
import java.util.List;
 | 
			
		||||
import java.util.Map;
 | 
			
		||||
import java.util.Set;
 | 
			
		||||
import java.util.function.Function;
 | 
			
		||||
 | 
			
		||||
import io.netty.handler.codec.http.DefaultHttpHeaders;
 | 
			
		||||
import org.apache.hc.client5.http.classic.methods.HttpGet;
 | 
			
		||||
import org.eclipse.jetty.http.HttpFields;
 | 
			
		||||
import org.openjdk.jmh.annotations.Benchmark;
 | 
			
		||||
import org.openjdk.jmh.annotations.BenchmarkMode;
 | 
			
		||||
import org.openjdk.jmh.annotations.Level;
 | 
			
		||||
import org.openjdk.jmh.annotations.Mode;
 | 
			
		||||
import org.openjdk.jmh.annotations.Param;
 | 
			
		||||
import org.openjdk.jmh.annotations.Scope;
 | 
			
		||||
import org.openjdk.jmh.annotations.Setup;
 | 
			
		||||
import org.openjdk.jmh.annotations.State;
 | 
			
		||||
import org.openjdk.jmh.infra.Blackhole;
 | 
			
		||||
 | 
			
		||||
import org.springframework.http.HttpHeaders;
 | 
			
		||||
import org.springframework.util.MultiValueMap;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Benchmark for implementations of MultiValueMap adapters over native HTTP
 | 
			
		||||
 * headers implementations.
 | 
			
		||||
 * <p>Run JMH with {@code -p implementation=Netty,Netty5,HttpComponents,Jetty}
 | 
			
		||||
 * to cover all implementations
 | 
			
		||||
 * @author Simon Baslé
 | 
			
		||||
 */
 | 
			
		||||
@BenchmarkMode(Mode.Throughput)
 | 
			
		||||
public class HeadersAdapterBenchmark {
 | 
			
		||||
 | 
			
		||||
	@Benchmark
 | 
			
		||||
	public void iterateEntries(BenchmarkData data, Blackhole bh) {
 | 
			
		||||
		for (Map.Entry<String, List<String>> entry : data.entriesProvider.apply(data.headers)) {
 | 
			
		||||
			bh.consume(entry.getKey());
 | 
			
		||||
			for (String s : entry.getValue()) {
 | 
			
		||||
				bh.consume(s);
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@Benchmark
 | 
			
		||||
	public void toString(BenchmarkData data, Blackhole bh) {
 | 
			
		||||
		bh.consume(data.headers.toString());
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@State(Scope.Benchmark)
 | 
			
		||||
	public static class BenchmarkData {
 | 
			
		||||
 | 
			
		||||
		@Param({"NONE"})
 | 
			
		||||
		public String implementation;
 | 
			
		||||
 | 
			
		||||
		@Param({"true"})
 | 
			
		||||
		public boolean duplicate;
 | 
			
		||||
 | 
			
		||||
		public MultiValueMap<String, String> headers;
 | 
			
		||||
		public Function<MultiValueMap<String, String>, Set<Map.Entry<String, List<String>>>> entriesProvider;
 | 
			
		||||
 | 
			
		||||
		//Uncomment the following line and comment the similar line for setupImplementationBaseline below
 | 
			
		||||
		//to benchmark current implementations
 | 
			
		||||
		@Setup(Level.Trial)
 | 
			
		||||
		public void initImplementationNew() {
 | 
			
		||||
			this.entriesProvider = map -> new HttpHeaders(map).headerSet();
 | 
			
		||||
 | 
			
		||||
			this.headers = switch (this.implementation) {
 | 
			
		||||
				case "Netty" -> new Netty4HeadersAdapter(new DefaultHttpHeaders());
 | 
			
		||||
				case "HttpComponents" -> new HttpComponentsHeadersAdapter(new HttpGet("https://example.com"));
 | 
			
		||||
				case "Netty5" -> new Netty5HeadersAdapter(io.netty5.handler.codec.http.headers.HttpHeaders.newHeaders());
 | 
			
		||||
				case "Jetty" -> new JettyHeadersAdapter(HttpFields.build());
 | 
			
		||||
				//FIXME tomcat/undertow implementations (in another package)
 | 
			
		||||
//				case "Tomcat" -> new TomcatHeadersAdapter(new MimeHeaders());
 | 
			
		||||
//				case "Undertow" -> new UndertowHeadersAdapter(new HeaderMap());
 | 
			
		||||
				default -> throw new IllegalArgumentException("Unsupported implementation: " + this.implementation);
 | 
			
		||||
			};
 | 
			
		||||
			initHeaders();
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		//Uncomment the following line and comment the similar line for setupImplementationNew above
 | 
			
		||||
		//to benchmark old implementations
 | 
			
		||||
//		@Setup(Level.Trial)
 | 
			
		||||
		public void setupImplementationBaseline() {
 | 
			
		||||
			this.entriesProvider = MultiValueMap::entrySet;
 | 
			
		||||
 | 
			
		||||
			this.headers = switch (this.implementation) {
 | 
			
		||||
				case "Netty" -> new HeadersAdaptersBaseline.Netty4(new DefaultHttpHeaders());
 | 
			
		||||
				case "HttpComponents" -> new HeadersAdaptersBaseline.HttpComponents(new HttpGet("https://example.com"));
 | 
			
		||||
				case "Netty5" -> new HeadersAdaptersBaseline.Netty5(io.netty5.handler.codec.http.headers.HttpHeaders.newHeaders());
 | 
			
		||||
				case "Jetty" -> new HeadersAdaptersBaseline.Jetty(HttpFields.build());
 | 
			
		||||
				default -> throw new IllegalArgumentException("Unsupported implementation: " + this.implementation);
 | 
			
		||||
			};
 | 
			
		||||
			initHeaders();
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		private void initHeaders() {
 | 
			
		||||
			this.headers.add("TestHeader", "first");
 | 
			
		||||
			this.headers.add("SecondHeader", "value");
 | 
			
		||||
			if (this.duplicate) {
 | 
			
		||||
				this.headers.add("TestHEADER", "second");
 | 
			
		||||
			}
 | 
			
		||||
			else {
 | 
			
		||||
				this.headers.add("TestHeader", "second");
 | 
			
		||||
			}
 | 
			
		||||
			this.headers.add("TestHeader", "third");
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| 
						 | 
				
			
			@ -30,14 +30,17 @@ import java.time.ZoneId;
 | 
			
		|||
import java.time.ZonedDateTime;
 | 
			
		||||
import java.time.format.DateTimeFormatter;
 | 
			
		||||
import java.time.format.DateTimeParseException;
 | 
			
		||||
import java.util.AbstractSet;
 | 
			
		||||
import java.util.ArrayList;
 | 
			
		||||
import java.util.Base64;
 | 
			
		||||
import java.util.Collection;
 | 
			
		||||
import java.util.Collections;
 | 
			
		||||
import java.util.Iterator;
 | 
			
		||||
import java.util.LinkedHashSet;
 | 
			
		||||
import java.util.List;
 | 
			
		||||
import java.util.Locale;
 | 
			
		||||
import java.util.Map;
 | 
			
		||||
import java.util.Objects;
 | 
			
		||||
import java.util.Set;
 | 
			
		||||
import java.util.StringJoiner;
 | 
			
		||||
import java.util.function.BiConsumer;
 | 
			
		||||
| 
						 | 
				
			
			@ -63,7 +66,23 @@ import org.springframework.util.StringUtils;
 | 
			
		|||
 * <li>{@link #set(String, String)} sets the header value to a single string value</li>
 | 
			
		||||
 * </ul>
 | 
			
		||||
 *
 | 
			
		||||
 * <p>Note that {@code HttpHeaders} generally treats header names in a case-insensitive manner.
 | 
			
		||||
 * <p>Note that {@code HttpHeaders} instances created by the default constructor
 | 
			
		||||
 * treat header names in a case-insensitive manner. Instances created with the
 | 
			
		||||
 * {@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.
 | 
			
		||||
 *
 | 
			
		||||
 * <p>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)}.
 | 
			
		||||
 *
 | 
			
		||||
 * @author Arjen Poutsma
 | 
			
		||||
 * @author Sebastien Deleuze
 | 
			
		||||
| 
						 | 
				
			
			@ -71,6 +90,7 @@ import org.springframework.util.StringUtils;
 | 
			
		|||
 * @author Juergen Hoeller
 | 
			
		||||
 * @author Josh Long
 | 
			
		||||
 * @author Sam Brannen
 | 
			
		||||
 * @author Simon Baslé
 | 
			
		||||
 * @since 3.0
 | 
			
		||||
 */
 | 
			
		||||
public class HttpHeaders implements MultiValueMap<String, String>, Serializable {
 | 
			
		||||
| 
						 | 
				
			
			@ -418,8 +438,8 @@ public class HttpHeaders implements MultiValueMap<String, String>, Serializable
 | 
			
		|||
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Construct a new, empty instance of the {@code HttpHeaders} object.
 | 
			
		||||
	 * <p>This is the common constructor, using a case-insensitive map structure.
 | 
			
		||||
	 * Construct a new, empty instance of the {@code HttpHeaders} object
 | 
			
		||||
	 * using an underlying case-insensitive map.
 | 
			
		||||
	 */
 | 
			
		||||
	public HttpHeaders() {
 | 
			
		||||
		this(CollectionUtils.toMultiValueMap(new LinkedCaseInsensitiveMap<>(8, Locale.ROOT)));
 | 
			
		||||
| 
						 | 
				
			
			@ -1748,11 +1768,6 @@ public class HttpHeaders implements MultiValueMap<String, String>, Serializable
 | 
			
		|||
 | 
			
		||||
	// Map implementation
 | 
			
		||||
 | 
			
		||||
	@Override
 | 
			
		||||
	public int size() {
 | 
			
		||||
		return this.headers.size();
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@Override
 | 
			
		||||
	public boolean isEmpty() {
 | 
			
		||||
		return this.headers.isEmpty();
 | 
			
		||||
| 
						 | 
				
			
			@ -1794,29 +1809,81 @@ public class HttpHeaders implements MultiValueMap<String, String>, Serializable
 | 
			
		|||
		this.headers.clear();
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@Override
 | 
			
		||||
	public List<String> putIfAbsent(String key, List<String> 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<String> 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<List<String>> 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<Entry<String, List<String>>> 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}.
 | 
			
		||||
	 * @param action the action to be performed for each entry
 | 
			
		||||
	 */
 | 
			
		||||
	@Override
 | 
			
		||||
	public void forEach(BiConsumer<? super String, ? super List<String>> action) {
 | 
			
		||||
		this.headers.forEach(action);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@Override
 | 
			
		||||
	public List<String> putIfAbsent(String key, List<String> value) {
 | 
			
		||||
		return this.headers.putIfAbsent(key, value);
 | 
			
		||||
	/**
 | 
			
		||||
	 * 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}
 | 
			
		||||
	 * are supported and mutate the headers.
 | 
			
		||||
	 * <p>This collection is guaranteed to contain one entry per header name
 | 
			
		||||
	 * even if the backing structure stores multiple casing variants of names,
 | 
			
		||||
	 * at the cost of first copying the names into a case-insensitive set for
 | 
			
		||||
	 * filtering the iteration.
 | 
			
		||||
	 * @return a {@code Set} view that iterates over all headers in a
 | 
			
		||||
	 * case-insensitive manner
 | 
			
		||||
	 * @since 6.1.15
 | 
			
		||||
	 */
 | 
			
		||||
	public Set<Entry<String, List<String>>> headerSet() {
 | 
			
		||||
		return new CaseInsensitiveEntrySet(this.headers);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -1882,19 +1949,29 @@ public class HttpHeaders implements MultiValueMap<String, String>, Serializable
 | 
			
		|||
	 * Helps to format HTTP header values, as HTTP header values themselves can
 | 
			
		||||
	 * contain comma-separated values, can become confusing with regular
 | 
			
		||||
	 * {@link Map} formatting that also uses commas between entries.
 | 
			
		||||
	 * <p>Additionally, this method displays the native list of header names
 | 
			
		||||
	 * with the mention {@code with native header names} if the underlying
 | 
			
		||||
	 * implementation stores multiple casing variants of header names (see
 | 
			
		||||
	 * {@link HttpHeaders class level javadoc}).
 | 
			
		||||
	 * @param headers the headers to format
 | 
			
		||||
	 * @return the headers to a String
 | 
			
		||||
	 * @since 5.1.4
 | 
			
		||||
	 */
 | 
			
		||||
	public static String formatHeaders(MultiValueMap<String, String> headers) {
 | 
			
		||||
		return headers.entrySet().stream()
 | 
			
		||||
				.map(entry -> {
 | 
			
		||||
					List<String> values = entry.getValue();
 | 
			
		||||
					return entry.getKey() + ":" + (values.size() == 1 ?
 | 
			
		||||
		Set<String> headerNames = toCaseInsensitiveSet(headers.keySet());
 | 
			
		||||
		String suffix = "]";
 | 
			
		||||
		if (headerNames.size() != headers.size()) {
 | 
			
		||||
			suffix = "] with native header names " + headers.keySet();
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		return headerNames.stream()
 | 
			
		||||
				.map(headerName -> {
 | 
			
		||||
					List<String> values = headers.get(headerName);
 | 
			
		||||
					return headerName + ":" + (values.size() == 1 ?
 | 
			
		||||
							"\"" + values.get(0) + "\"" :
 | 
			
		||||
							values.stream().map(s -> "\"" + s + "\"").collect(Collectors.joining(", ")));
 | 
			
		||||
				})
 | 
			
		||||
				.collect(Collectors.joining(", ", "[", "]"));
 | 
			
		||||
				.collect(Collectors.joining(", ", "[", suffix));
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
| 
						 | 
				
			
			@ -1947,4 +2024,103 @@ public class HttpHeaders implements MultiValueMap<String, String>, Serializable
 | 
			
		|||
		return DATE_FORMATTER.format(time);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
	private static Set<String> toCaseInsensitiveSet(Set<String> originalSet) {
 | 
			
		||||
		final Set<String> 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 CaseInsensitiveEntrySet extends AbstractSet<Entry<String, List<String>>> {
 | 
			
		||||
 | 
			
		||||
		private final MultiValueMap<String, String> headers;
 | 
			
		||||
		private final Set<String> deduplicatedNames;
 | 
			
		||||
 | 
			
		||||
		public CaseInsensitiveEntrySet(MultiValueMap<String, String> headers) {
 | 
			
		||||
			this.headers = headers;
 | 
			
		||||
			this.deduplicatedNames = toCaseInsensitiveSet(headers.keySet());
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		@Override
 | 
			
		||||
		public Iterator<Map.Entry<String, List<String>>> iterator() {
 | 
			
		||||
			return new CaseInsensitiveIterator(this.deduplicatedNames.iterator());
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		@Override
 | 
			
		||||
		public int size() {
 | 
			
		||||
			return this.deduplicatedNames.size();
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		private final class CaseInsensitiveIterator implements Iterator<Map.Entry<String, List<String>>> {
 | 
			
		||||
 | 
			
		||||
			private final Iterator<String> namesIterator;
 | 
			
		||||
 | 
			
		||||
			@Nullable
 | 
			
		||||
			private String currentName;
 | 
			
		||||
 | 
			
		||||
			private CaseInsensitiveIterator(Iterator<String> namesIterator) {
 | 
			
		||||
				this.namesIterator = namesIterator;
 | 
			
		||||
				this.currentName = null;
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			@Override
 | 
			
		||||
			public boolean hasNext() {
 | 
			
		||||
				return this.namesIterator.hasNext();
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			@Override
 | 
			
		||||
			public Map.Entry<String, List<String>> next() {
 | 
			
		||||
				this.currentName = this.namesIterator.next();
 | 
			
		||||
				return new CaseInsensitiveEntry(this.currentName);
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			@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);
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		private final class CaseInsensitiveEntry implements Map.Entry<String, List<String>> {
 | 
			
		||||
 | 
			
		||||
			private final String key;
 | 
			
		||||
 | 
			
		||||
			CaseInsensitiveEntry(String key) {
 | 
			
		||||
				this.key = key;
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			@Override
 | 
			
		||||
			public String getKey() {
 | 
			
		||||
				return this.key;
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			@Override
 | 
			
		||||
			public List<String> getValue() {
 | 
			
		||||
				return Objects.requireNonNull(CaseInsensitiveEntrySet.this.headers.get(this.key));
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			@Override
 | 
			
		||||
			public List<String> setValue(List<String> value) {
 | 
			
		||||
				List<String> previousValues = Objects.requireNonNull(
 | 
			
		||||
						CaseInsensitiveEntrySet.this.headers.get(this.key));
 | 
			
		||||
				CaseInsensitiveEntrySet.this.headers.put(this.key, value);
 | 
			
		||||
				return previousValues;
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -21,7 +21,9 @@ import java.util.Collection;
 | 
			
		|||
import java.util.Collections;
 | 
			
		||||
import java.util.Enumeration;
 | 
			
		||||
import java.util.Iterator;
 | 
			
		||||
import java.util.LinkedHashSet;
 | 
			
		||||
import java.util.List;
 | 
			
		||||
import java.util.Locale;
 | 
			
		||||
import java.util.Map;
 | 
			
		||||
import java.util.Set;
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -38,6 +40,7 @@ import org.springframework.util.MultiValueMap;
 | 
			
		|||
 *
 | 
			
		||||
 * @author Brian Clozel
 | 
			
		||||
 * @author Sam Brannen
 | 
			
		||||
 * @author Simon Baslé
 | 
			
		||||
 * @since 5.1.1
 | 
			
		||||
 */
 | 
			
		||||
class TomcatHeadersAdapter implements MultiValueMap<String, String> {
 | 
			
		||||
| 
						 | 
				
			
			@ -90,12 +93,11 @@ class TomcatHeadersAdapter implements MultiValueMap<String, String> {
 | 
			
		|||
	@Override
 | 
			
		||||
	public int size() {
 | 
			
		||||
		Enumeration<String> names = this.headers.names();
 | 
			
		||||
		int size = 0;
 | 
			
		||||
		Set<String> deduplicated = new LinkedHashSet<>();
 | 
			
		||||
		while (names.hasMoreElements()) {
 | 
			
		||||
			size++;
 | 
			
		||||
			names.nextElement();
 | 
			
		||||
			deduplicated.add(names.nextElement().toLowerCase(Locale.ROOT));
 | 
			
		||||
		}
 | 
			
		||||
		return size;
 | 
			
		||||
		return deduplicated.size();
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@Override
 | 
			
		||||
| 
						 | 
				
			
			@ -185,7 +187,7 @@ class TomcatHeadersAdapter implements MultiValueMap<String, String> {
 | 
			
		|||
 | 
			
		||||
			@Override
 | 
			
		||||
			public int size() {
 | 
			
		||||
				return headers.size();
 | 
			
		||||
				return TomcatHeadersAdapter.this.size();
 | 
			
		||||
			}
 | 
			
		||||
		};
 | 
			
		||||
	}
 | 
			
		||||
| 
						 | 
				
			
			@ -289,11 +291,17 @@ class TomcatHeadersAdapter implements MultiValueMap<String, String> {
 | 
			
		|||
			if (this.currentName == null) {
 | 
			
		||||
				throw new IllegalStateException("No current Header in iterator");
 | 
			
		||||
			}
 | 
			
		||||
			int index = headers.findHeader(this.currentName, 0);
 | 
			
		||||
			if (index == -1) {
 | 
			
		||||
			//implement a mix of removeHeader(String) and removeHeader(int)
 | 
			
		||||
			boolean found = false;
 | 
			
		||||
			for (int i = 0; i < headers.size(); i++) {
 | 
			
		||||
				if (headers.getName(i).equalsIgnoreCase(this.currentName)) {
 | 
			
		||||
					headers.removeHeader(i--);
 | 
			
		||||
					found = true;
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
			if (!found) {
 | 
			
		||||
				throw new IllegalStateException("Header not present: " + this.currentName);
 | 
			
		||||
			}
 | 
			
		||||
			headers.removeHeader(index);
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,5 +1,5 @@
 | 
			
		|||
/*
 | 
			
		||||
 * Copyright 2002-2023 the original author or authors.
 | 
			
		||||
 * Copyright 2002-2024 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.
 | 
			
		||||
| 
						 | 
				
			
			@ -22,8 +22,8 @@ import java.util.Arrays;
 | 
			
		|||
import java.util.Collection;
 | 
			
		||||
import java.util.Collections;
 | 
			
		||||
import java.util.Iterator;
 | 
			
		||||
import java.util.LinkedHashSet;
 | 
			
		||||
import java.util.List;
 | 
			
		||||
import java.util.Locale;
 | 
			
		||||
import java.util.Map;
 | 
			
		||||
import java.util.Set;
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -33,7 +33,7 @@ import org.apache.hc.core5.http.HttpMessage;
 | 
			
		|||
import org.springframework.http.HttpHeaders;
 | 
			
		||||
import org.springframework.lang.Nullable;
 | 
			
		||||
import org.springframework.util.Assert;
 | 
			
		||||
import org.springframework.util.CollectionUtils;
 | 
			
		||||
import org.springframework.util.LinkedCaseInsensitiveMap;
 | 
			
		||||
import org.springframework.util.MultiValueMap;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
| 
						 | 
				
			
			@ -41,12 +41,13 @@ import org.springframework.util.MultiValueMap;
 | 
			
		|||
 * HttpClient headers.
 | 
			
		||||
 *
 | 
			
		||||
 * @author Rossen Stoyanchev
 | 
			
		||||
 * @author Simon Baslé
 | 
			
		||||
 * @since 6.1
 | 
			
		||||
 */
 | 
			
		||||
public final class HttpComponentsHeadersAdapter implements MultiValueMap<String, String> {
 | 
			
		||||
 | 
			
		||||
	private final HttpMessage message;
 | 
			
		||||
 | 
			
		||||
	private final Set<String> headerNames;
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
	 * Create a new {@code HttpComponentsHeadersAdapter} based on the given
 | 
			
		||||
| 
						 | 
				
			
			@ -55,6 +56,11 @@ public final class HttpComponentsHeadersAdapter implements MultiValueMap<String,
 | 
			
		|||
	public HttpComponentsHeadersAdapter(HttpMessage message) {
 | 
			
		||||
		Assert.notNull(message, "Message must not be null");
 | 
			
		||||
		this.message = message;
 | 
			
		||||
		this.headerNames = Collections.newSetFromMap(new LinkedCaseInsensitiveMap<>(
 | 
			
		||||
				message.getHeaders().length, Locale.ROOT));
 | 
			
		||||
		for (Header header : message.getHeaders()) {
 | 
			
		||||
			this.headerNames.add(header.getName());
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -67,6 +73,7 @@ public final class HttpComponentsHeadersAdapter implements MultiValueMap<String,
 | 
			
		|||
 | 
			
		||||
	@Override
 | 
			
		||||
	public void add(String key, @Nullable String value) {
 | 
			
		||||
		this.headerNames.add(key);
 | 
			
		||||
		this.message.addHeader(key, value);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -82,6 +89,7 @@ public final class HttpComponentsHeadersAdapter implements MultiValueMap<String,
 | 
			
		|||
 | 
			
		||||
	@Override
 | 
			
		||||
	public void set(String key, @Nullable String value) {
 | 
			
		||||
		this.headerNames.add(key);
 | 
			
		||||
		this.message.setHeader(key, value);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -92,14 +100,14 @@ public final class HttpComponentsHeadersAdapter implements MultiValueMap<String,
 | 
			
		|||
 | 
			
		||||
	@Override
 | 
			
		||||
	public Map<String, String> toSingleValueMap() {
 | 
			
		||||
		Map<String, String> map = CollectionUtils.newLinkedHashMap(size());
 | 
			
		||||
		Map<String, String> map = new LinkedCaseInsensitiveMap<>(this.message.getHeaders().length, Locale.ROOT);
 | 
			
		||||
		this.message.headerIterator().forEachRemaining(h -> map.putIfAbsent(h.getName(), h.getValue()));
 | 
			
		||||
		return map;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@Override
 | 
			
		||||
	public int size() {
 | 
			
		||||
		return this.message.getHeaders().length;
 | 
			
		||||
		return this.headerNames.size();
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@Override
 | 
			
		||||
| 
						 | 
				
			
			@ -145,6 +153,7 @@ public final class HttpComponentsHeadersAdapter implements MultiValueMap<String,
 | 
			
		|||
	public List<String> remove(Object key) {
 | 
			
		||||
		if (key instanceof String headerName) {
 | 
			
		||||
			List<String> oldValues = get(key);
 | 
			
		||||
			this.headerNames.remove(headerName);
 | 
			
		||||
			this.message.removeHeaders(headerName);
 | 
			
		||||
			return oldValues;
 | 
			
		||||
		}
 | 
			
		||||
| 
						 | 
				
			
			@ -158,23 +167,20 @@ public final class HttpComponentsHeadersAdapter implements MultiValueMap<String,
 | 
			
		|||
 | 
			
		||||
	@Override
 | 
			
		||||
	public void clear() {
 | 
			
		||||
		this.headerNames.clear();
 | 
			
		||||
		this.message.setHeaders();
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@Override
 | 
			
		||||
	public Set<String> keySet() {
 | 
			
		||||
		Set<String> keys = new LinkedHashSet<>(size());
 | 
			
		||||
		for (Header header : this.message.getHeaders()) {
 | 
			
		||||
			keys.add(header.getName());
 | 
			
		||||
		}
 | 
			
		||||
		return keys;
 | 
			
		||||
		return new HeaderNames();
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@Override
 | 
			
		||||
	public Collection<List<String>> values() {
 | 
			
		||||
		Collection<List<String>> values = new ArrayList<>(size());
 | 
			
		||||
		for (Header header : this.message.getHeaders()) {
 | 
			
		||||
			values.add(get(header.getName()));
 | 
			
		||||
		Collection<List<String>> values = new ArrayList<>(this.message.getHeaders().length);
 | 
			
		||||
		for (String headerName : keySet()) {
 | 
			
		||||
			values.add(get(headerName));
 | 
			
		||||
		}
 | 
			
		||||
		return values;
 | 
			
		||||
	}
 | 
			
		||||
| 
						 | 
				
			
			@ -200,10 +206,22 @@ public final class HttpComponentsHeadersAdapter implements MultiValueMap<String,
 | 
			
		|||
		return HttpHeaders.formatHeaders(this);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	private class HeaderNames extends AbstractSet<String> {
 | 
			
		||||
 | 
			
		||||
		@Override
 | 
			
		||||
		public Iterator<String> iterator() {
 | 
			
		||||
			return new HeaderNamesIterator(headerNames.iterator());
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		@Override
 | 
			
		||||
		public int size() {
 | 
			
		||||
			return headerNames.size();
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	private class EntryIterator implements Iterator<Entry<String, List<String>>> {
 | 
			
		||||
 | 
			
		||||
		private final Iterator<Header> iterator = message.headerIterator();
 | 
			
		||||
		private final Iterator<String> iterator = keySet().iterator();
 | 
			
		||||
 | 
			
		||||
		@Override
 | 
			
		||||
		public boolean hasNext() {
 | 
			
		||||
| 
						 | 
				
			
			@ -212,7 +230,7 @@ public final class HttpComponentsHeadersAdapter implements MultiValueMap<String,
 | 
			
		|||
 | 
			
		||||
		@Override
 | 
			
		||||
		public Entry<String, List<String>> next() {
 | 
			
		||||
			return new HeaderEntry(this.iterator.next().getName());
 | 
			
		||||
			return new HeaderEntry(this.iterator.next());
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -244,4 +262,40 @@ public final class HttpComponentsHeadersAdapter implements MultiValueMap<String,
 | 
			
		|||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	private final class HeaderNamesIterator implements Iterator<String> {
 | 
			
		||||
 | 
			
		||||
		private final Iterator<String> iterator;
 | 
			
		||||
 | 
			
		||||
		@Nullable
 | 
			
		||||
		private String currentName;
 | 
			
		||||
 | 
			
		||||
		private HeaderNamesIterator(Iterator<String> iterator) {
 | 
			
		||||
			this.iterator = iterator;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		@Override
 | 
			
		||||
		public boolean hasNext() {
 | 
			
		||||
			return this.iterator.hasNext();
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		@Override
 | 
			
		||||
		public String next() {
 | 
			
		||||
			this.currentName = this.iterator.next();
 | 
			
		||||
			return this.currentName;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		@Override
 | 
			
		||||
		public void remove() {
 | 
			
		||||
			if (this.currentName == null) {
 | 
			
		||||
				throw new IllegalStateException("No current Header in iterator");
 | 
			
		||||
			}
 | 
			
		||||
			if (!message.containsHeader(this.currentName)) {
 | 
			
		||||
				throw new IllegalStateException("Header not present: " + this.currentName);
 | 
			
		||||
			}
 | 
			
		||||
			headerNames.remove(this.currentName);
 | 
			
		||||
			message.removeHeaders(this.currentName);
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -20,6 +20,7 @@ import java.util.AbstractSet;
 | 
			
		|||
import java.util.Collection;
 | 
			
		||||
import java.util.Iterator;
 | 
			
		||||
import java.util.List;
 | 
			
		||||
import java.util.Locale;
 | 
			
		||||
import java.util.Map;
 | 
			
		||||
import java.util.Set;
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -29,7 +30,7 @@ import org.eclipse.jetty.http.HttpFields;
 | 
			
		|||
import org.springframework.http.HttpHeaders;
 | 
			
		||||
import org.springframework.lang.Nullable;
 | 
			
		||||
import org.springframework.util.Assert;
 | 
			
		||||
import org.springframework.util.CollectionUtils;
 | 
			
		||||
import org.springframework.util.LinkedCaseInsensitiveMap;
 | 
			
		||||
import org.springframework.util.MultiValueMap;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
| 
						 | 
				
			
			@ -38,6 +39,7 @@ import org.springframework.util.MultiValueMap;
 | 
			
		|||
 * @author Rossen Stoyanchev
 | 
			
		||||
 * @author Juergen Hoeller
 | 
			
		||||
 * @author Sam Brannen
 | 
			
		||||
 * @author Simon Baslé
 | 
			
		||||
 * @since 6.1
 | 
			
		||||
 */
 | 
			
		||||
public final class JettyHeadersAdapter implements MultiValueMap<String, String> {
 | 
			
		||||
| 
						 | 
				
			
			@ -97,7 +99,8 @@ public final class JettyHeadersAdapter implements MultiValueMap<String, String>
 | 
			
		|||
 | 
			
		||||
	@Override
 | 
			
		||||
	public Map<String, String> toSingleValueMap() {
 | 
			
		||||
		Map<String, String> singleValueMap = CollectionUtils.newLinkedHashMap(this.headers.size());
 | 
			
		||||
		Map<String, String> singleValueMap = new LinkedCaseInsensitiveMap<>(
 | 
			
		||||
				this.headers.size(), Locale.ROOT);
 | 
			
		||||
		Iterator<HttpField> iterator = this.headers.iterator();
 | 
			
		||||
		iterator.forEachRemaining(field -> {
 | 
			
		||||
			if (!singleValueMap.containsKey(field.getName())) {
 | 
			
		||||
| 
						 | 
				
			
			@ -189,7 +192,7 @@ public final class JettyHeadersAdapter implements MultiValueMap<String, String>
 | 
			
		|||
			}
 | 
			
		||||
			@Override
 | 
			
		||||
			public int size() {
 | 
			
		||||
				return headers.size();
 | 
			
		||||
				return headers.getFieldNamesCollection().size();
 | 
			
		||||
			}
 | 
			
		||||
		};
 | 
			
		||||
	}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -20,6 +20,7 @@ import java.util.AbstractSet;
 | 
			
		|||
import java.util.Collection;
 | 
			
		||||
import java.util.Iterator;
 | 
			
		||||
import java.util.List;
 | 
			
		||||
import java.util.Locale;
 | 
			
		||||
import java.util.Map;
 | 
			
		||||
import java.util.Set;
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -27,7 +28,7 @@ import io.netty.handler.codec.http.HttpHeaders;
 | 
			
		|||
 | 
			
		||||
import org.springframework.lang.Nullable;
 | 
			
		||||
import org.springframework.util.Assert;
 | 
			
		||||
import org.springframework.util.CollectionUtils;
 | 
			
		||||
import org.springframework.util.LinkedCaseInsensitiveMap;
 | 
			
		||||
import org.springframework.util.MultiValueMap;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
| 
						 | 
				
			
			@ -35,6 +36,7 @@ import org.springframework.util.MultiValueMap;
 | 
			
		|||
 *
 | 
			
		||||
 * @author Rossen Stoyanchev
 | 
			
		||||
 * @author Sam Brannen
 | 
			
		||||
 * @author Simon Baslé
 | 
			
		||||
 * @since 6.1
 | 
			
		||||
 */
 | 
			
		||||
public final class Netty4HeadersAdapter implements MultiValueMap<String, String> {
 | 
			
		||||
| 
						 | 
				
			
			@ -89,7 +91,8 @@ public final class Netty4HeadersAdapter implements MultiValueMap<String, String>
 | 
			
		|||
 | 
			
		||||
	@Override
 | 
			
		||||
	public Map<String, String> toSingleValueMap() {
 | 
			
		||||
		Map<String, String> singleValueMap = CollectionUtils.newLinkedHashMap(this.headers.size());
 | 
			
		||||
		Map<String, String> singleValueMap = new LinkedCaseInsensitiveMap<>(
 | 
			
		||||
				this.headers.size(), Locale.ROOT);
 | 
			
		||||
		this.headers.entries()
 | 
			
		||||
				.forEach(entry -> {
 | 
			
		||||
					if (!singleValueMap.containsKey(entry.getKey())) {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -22,6 +22,7 @@ import java.util.Collection;
 | 
			
		|||
import java.util.Collections;
 | 
			
		||||
import java.util.Iterator;
 | 
			
		||||
import java.util.List;
 | 
			
		||||
import java.util.Locale;
 | 
			
		||||
import java.util.Map;
 | 
			
		||||
import java.util.Set;
 | 
			
		||||
import java.util.stream.StreamSupport;
 | 
			
		||||
| 
						 | 
				
			
			@ -30,13 +31,14 @@ import io.netty5.handler.codec.http.headers.HttpHeaders;
 | 
			
		|||
 | 
			
		||||
import org.springframework.lang.Nullable;
 | 
			
		||||
import org.springframework.util.Assert;
 | 
			
		||||
import org.springframework.util.CollectionUtils;
 | 
			
		||||
import org.springframework.util.LinkedCaseInsensitiveMap;
 | 
			
		||||
import org.springframework.util.MultiValueMap;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * {@code MultiValueMap} implementation for wrapping Netty HTTP headers.
 | 
			
		||||
 *
 | 
			
		||||
 * @author Violeta Georgieva
 | 
			
		||||
 * @author Simon Baslé
 | 
			
		||||
 * @since 6.1
 | 
			
		||||
 */
 | 
			
		||||
public final class Netty5HeadersAdapter implements MultiValueMap<String, String> {
 | 
			
		||||
| 
						 | 
				
			
			@ -92,7 +94,8 @@ public final class Netty5HeadersAdapter implements MultiValueMap<String, String>
 | 
			
		|||
 | 
			
		||||
	@Override
 | 
			
		||||
	public Map<String, String> toSingleValueMap() {
 | 
			
		||||
		Map<String, String> singleValueMap = CollectionUtils.newLinkedHashMap(this.headers.size());
 | 
			
		||||
		Map<String, String> singleValueMap = new LinkedCaseInsensitiveMap<>(
 | 
			
		||||
				this.headers.size(), Locale.ROOT);
 | 
			
		||||
		this.headers.forEach(entry -> singleValueMap.putIfAbsent(
 | 
			
		||||
				entry.getKey().toString(), entry.getValue().toString()));
 | 
			
		||||
		return singleValueMap;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -23,16 +23,23 @@ import java.lang.annotation.Target;
 | 
			
		|||
import java.util.Iterator;
 | 
			
		||||
import java.util.List;
 | 
			
		||||
import java.util.Locale;
 | 
			
		||||
import java.util.Map;
 | 
			
		||||
import java.util.function.BiConsumer;
 | 
			
		||||
import java.util.function.Function;
 | 
			
		||||
import java.util.stream.Stream;
 | 
			
		||||
 | 
			
		||||
import io.netty.handler.codec.http.DefaultHttpHeaders;
 | 
			
		||||
import io.undertow.util.HeaderMap;
 | 
			
		||||
import io.undertow.util.HttpString;
 | 
			
		||||
import org.apache.hc.client5.http.classic.methods.HttpGet;
 | 
			
		||||
import org.apache.tomcat.util.http.MimeHeaders;
 | 
			
		||||
import org.eclipse.jetty.http.HttpFields;
 | 
			
		||||
import org.junit.jupiter.params.ParameterizedTest;
 | 
			
		||||
import org.junit.jupiter.params.provider.Arguments;
 | 
			
		||||
import org.junit.jupiter.params.provider.MethodSource;
 | 
			
		||||
 | 
			
		||||
import org.springframework.http.HttpHeaders;
 | 
			
		||||
import org.springframework.http.support.HttpComponentsHeadersAdapter;
 | 
			
		||||
import org.springframework.http.support.JettyHeadersAdapter;
 | 
			
		||||
import org.springframework.http.support.Netty4HeadersAdapter;
 | 
			
		||||
import org.springframework.http.support.Netty5HeadersAdapter;
 | 
			
		||||
| 
						 | 
				
			
			@ -50,15 +57,109 @@ import static org.junit.jupiter.params.provider.Arguments.arguments;
 | 
			
		|||
 *
 | 
			
		||||
 * @author Brian Clozel
 | 
			
		||||
 * @author Sam Brannen
 | 
			
		||||
 * @author Simon Baslé
 | 
			
		||||
 */
 | 
			
		||||
class HeadersAdaptersTests {
 | 
			
		||||
 | 
			
		||||
	@ParameterizedPopulatedHeadersTest
 | 
			
		||||
	void toSingleValueMapIsCaseInsensitive(MultiValueMap<String, String> headers) {
 | 
			
		||||
		assertThat(headers.toSingleValueMap()).as("toSingleValueMap")
 | 
			
		||||
				.containsEntry("TestHeader", "first")
 | 
			
		||||
				.containsEntry("SecondHeader", "value")
 | 
			
		||||
				.hasSize(2);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@ParameterizedHeadersTest
 | 
			
		||||
	void shouldRemoveCaseInsensitiveFromKeySet(MultiValueMap<String, String> headers) {
 | 
			
		||||
		headers.add("TestHeader", "first");
 | 
			
		||||
		headers.add("TestHEADER", "second");
 | 
			
		||||
		headers.add("TestHeader", "third");
 | 
			
		||||
 | 
			
		||||
		Iterator<String> iterator = headers.keySet().iterator();
 | 
			
		||||
		iterator.next();
 | 
			
		||||
		iterator.remove();
 | 
			
		||||
 | 
			
		||||
		assertThat(headers)
 | 
			
		||||
				.doesNotContainKey("TestHeader")
 | 
			
		||||
				.doesNotContainKey("TestHEADER")
 | 
			
		||||
				.doesNotContainKey("testheader")
 | 
			
		||||
				.hasSize(0);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@ParameterizedPopulatedHeadersTest
 | 
			
		||||
	void toString(MultiValueMap<String, String> headers) {
 | 
			
		||||
		String expectedFirstHeader = "TestHeader:\"first\", \"second\", \"third\"";
 | 
			
		||||
		String expectedSecondHeader = "SecondHeader:\"value\"";
 | 
			
		||||
		int minimumLength = expectedFirstHeader.length() + expectedSecondHeader.length() + 4;
 | 
			
		||||
 | 
			
		||||
		String result = headers.toString();
 | 
			
		||||
		assertThat(result)
 | 
			
		||||
				.startsWith("[").endsWith("]")
 | 
			
		||||
				// Using contains here because some native headers iterate over names in reverse insertion order
 | 
			
		||||
				.contains(expectedFirstHeader)
 | 
			
		||||
				.contains(expectedSecondHeader)
 | 
			
		||||
				.hasSizeGreaterThanOrEqualTo(minimumLength);
 | 
			
		||||
 | 
			
		||||
		if (result.length() > minimumLength) {
 | 
			
		||||
			String expectedEnd = " with native header names " + headers.keySet();
 | 
			
		||||
			assertThat(result).as("toString() with a dump of native duplicate headers")
 | 
			
		||||
					.endsWith(expectedEnd)
 | 
			
		||||
					.hasSize(minimumLength + expectedEnd.length());
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@ParameterizedPopulatedHeadersTest
 | 
			
		||||
	void copyUsingHeaderSetAddAllIsCaseInsensitive(MultiValueMap<String, String> headers) {
 | 
			
		||||
		HttpHeaders headers2 = new HttpHeaders();
 | 
			
		||||
		for (Map.Entry<String, List<String>> entry : new HttpHeaders(headers).headerSet()) {
 | 
			
		||||
			headers2.addAll(entry.getKey(), entry.getValue());
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		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.toString()).as("similar toString, no 'with native headers' dump")
 | 
			
		||||
				.isEqualTo(headers.toString().substring(0, headers.toString().indexOf(']') + 1));
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@ParameterizedPopulatedHeadersTest
 | 
			
		||||
	void copyUsingEntrySetPutRemovesDuplicates(MultiValueMap<String, String> headers) {
 | 
			
		||||
		HttpHeaders headers2 = new HttpHeaders();
 | 
			
		||||
		for (Map.Entry<String, List<String>> entry : headers.entrySet()) {
 | 
			
		||||
			headers2.put(entry.getKey(), entry.getValue());
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		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")
 | 
			
		||||
				.containsKey("testheader")
 | 
			
		||||
				.containsKey("secondheader")
 | 
			
		||||
				.hasSize(2);
 | 
			
		||||
		assertThat(headers2.toString()).as("no 'with native headers' dump")
 | 
			
		||||
				.doesNotContain("with native headers");
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@ParameterizedPopulatedHeadersTest
 | 
			
		||||
	void copyUsingPutAllRemovesDuplicates(MultiValueMap<String, String> headers) {
 | 
			
		||||
		HttpHeaders headers2 = new HttpHeaders();
 | 
			
		||||
		headers2.putAll(headers);
 | 
			
		||||
 | 
			
		||||
		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.toString()).as("similar toString, no 'with native headers' dump")
 | 
			
		||||
				.isEqualToIgnoringCase(headers.toString().substring(0, headers.toString().indexOf(']') + 1));
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@ParameterizedPopulatedHeadersTest
 | 
			
		||||
	void getWithUnknownHeaderShouldReturnNull(MultiValueMap<String, String> headers) {
 | 
			
		||||
		assertThat(headers.get("Unknown")).isNull();
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@ParameterizedHeadersTest
 | 
			
		||||
	@ParameterizedPopulatedHeadersTest
 | 
			
		||||
	void getFirstWithUnknownHeaderShouldReturnNull(MultiValueMap<String, String> headers) {
 | 
			
		||||
		assertThat(headers.getFirst("Unknown")).isNull();
 | 
			
		||||
	}
 | 
			
		||||
| 
						 | 
				
			
			@ -78,9 +179,8 @@ class HeadersAdaptersTests {
 | 
			
		|||
		assertThat(headers.keySet()).hasSize(2);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@ParameterizedHeadersTest
 | 
			
		||||
	@ParameterizedPopulatedHeadersTest
 | 
			
		||||
	void containsKeyShouldBeCaseInsensitive(MultiValueMap<String, String> headers) {
 | 
			
		||||
		headers.add("TestHeader", "first");
 | 
			
		||||
		assertThat(headers.containsKey("testheader")).isTrue();
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -127,6 +227,39 @@ class HeadersAdaptersTests {
 | 
			
		|||
		assertThatThrownBy(names::remove).isInstanceOf(IllegalStateException.class);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@ParameterizedPopulatedHeadersTest
 | 
			
		||||
	void headerSetEntryCanSetList(MultiValueMap<String, String> headers) {
 | 
			
		||||
		for (Map.Entry<String, List<String>> entry : new HttpHeaders(headers).headerSet()) {
 | 
			
		||||
			entry.setValue(List.of(entry.getKey()));
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		assertThat(headers).hasSize(2);
 | 
			
		||||
		assertThat(headers.get("TestHeader")).containsExactly("TestHeader");
 | 
			
		||||
		assertThat(headers.get("SecondHeader")).containsExactly("SecondHeader");
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@ParameterizedPopulatedHeadersTest
 | 
			
		||||
	void headerSetIteratorCanRemove(MultiValueMap<String, String> headers) {
 | 
			
		||||
		Iterator<Map.Entry<String, List<String>>> iterator = new HttpHeaders(headers).headerSet().iterator();
 | 
			
		||||
		while (iterator.hasNext()) {
 | 
			
		||||
			iterator.next();
 | 
			
		||||
			iterator.remove();
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		assertThat(headers).isEmpty();
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
	static <T> T withHeaders(T nativeHeader, Function<T, BiConsumer<String, String>> addMethod) {
 | 
			
		||||
		BiConsumer<String, String> add = addMethod.apply(nativeHeader);
 | 
			
		||||
		add.accept("TestHeader", "first");
 | 
			
		||||
		add.accept("TestHEADER", "second");
 | 
			
		||||
		add.accept("SecondHeader", "value");
 | 
			
		||||
		add.accept("TestHeader", "third");
 | 
			
		||||
 | 
			
		||||
		return nativeHeader;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@Retention(RetentionPolicy.RUNTIME)
 | 
			
		||||
	@Target(ElementType.METHOD)
 | 
			
		||||
	@ParameterizedTest(name = "[{index}] {0}")
 | 
			
		||||
| 
						 | 
				
			
			@ -138,10 +271,33 @@ class HeadersAdaptersTests {
 | 
			
		|||
		return Stream.of(
 | 
			
		||||
				arguments(named("Map", CollectionUtils.toMultiValueMap(new LinkedCaseInsensitiveMap<>(8, Locale.ENGLISH)))),
 | 
			
		||||
				arguments(named("Netty", new Netty4HeadersAdapter(new DefaultHttpHeaders()))),
 | 
			
		||||
				arguments(named("Netty", new Netty5HeadersAdapter(io.netty5.handler.codec.http.headers.HttpHeaders.newHeaders()))),
 | 
			
		||||
				arguments(named("Netty5", new Netty5HeadersAdapter(io.netty5.handler.codec.http.headers.HttpHeaders.newHeaders()))),
 | 
			
		||||
				arguments(named("Tomcat", new TomcatHeadersAdapter(new MimeHeaders()))),
 | 
			
		||||
				arguments(named("Undertow", new UndertowHeadersAdapter(new HeaderMap()))),
 | 
			
		||||
				arguments(named("Jetty", new JettyHeadersAdapter(HttpFields.build())))
 | 
			
		||||
				arguments(named("Jetty", new JettyHeadersAdapter(HttpFields.build()))),
 | 
			
		||||
				arguments(named("HttpComponents", new HttpComponentsHeadersAdapter(new HttpGet("https://example.com"))))
 | 
			
		||||
		);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@Retention(RetentionPolicy.RUNTIME)
 | 
			
		||||
	@Target(ElementType.METHOD)
 | 
			
		||||
	@ParameterizedTest
 | 
			
		||||
	@MethodSource("nativeHeadersWithCasedEntries")
 | 
			
		||||
	@interface ParameterizedPopulatedHeadersTest {
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	static Stream<Arguments> nativeHeadersWithCasedEntries() {
 | 
			
		||||
		return Stream.of(
 | 
			
		||||
				arguments(named("Netty", new Netty4HeadersAdapter(withHeaders(new DefaultHttpHeaders(), h -> h::add)))),
 | 
			
		||||
				arguments(named("Netty5", new Netty5HeadersAdapter(withHeaders(io.netty5.handler.codec.http.headers.HttpHeaders.newHeaders(),
 | 
			
		||||
						h -> h::add)))),
 | 
			
		||||
				arguments(named("Tomcat", new TomcatHeadersAdapter(withHeaders(new MimeHeaders(),
 | 
			
		||||
						h -> (k, v) -> h.addValue(k).setString(v))))),
 | 
			
		||||
				arguments(named("Undertow", new UndertowHeadersAdapter(withHeaders(new HeaderMap(),
 | 
			
		||||
						h -> (k, v) -> h.add(HttpString.tryFromString(k), v))))),
 | 
			
		||||
				arguments(named("Jetty", new JettyHeadersAdapter(withHeaders(HttpFields.build(), h -> h::add)))),
 | 
			
		||||
				arguments(named("HttpComponents", new HttpComponentsHeadersAdapter(withHeaders(new HttpGet("https://example.com"),
 | 
			
		||||
						h -> h::addHeader))))
 | 
			
		||||
		);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in New Issue