From 903493e9a9c85d5be3148e3cb352b5261076f759 Mon Sep 17 00:00:00 2001 From: Arjen Poutsma Date: Tue, 21 May 2024 15:51:31 +0200 Subject: [PATCH] Various MultiValueMap improvements This commit makes several improvements to MultiValueMap: - asSingleValueMap offers a single-value view (as opposed to the existing toSingleValueMap, which offers a copy) - fromSingleValue is a static method that adapts a Map to the MultiValueMap interface - fromMultiValue is a static method that adapts a Map> to the MultiValueMap interface Closes gh-32832 --- .../util/MultiToSingleValueMapAdapter.java | 274 +++++++++++++++ .../springframework/util/MultiValueMap.java | 51 ++- .../util/SingleToMultiValueMapAdapter.java | 319 ++++++++++++++++++ .../util/UnmodifiableMultiValueMap.java | 6 +- .../MultiToSingleValueMapAdapterTests.java | 131 +++++++ .../SingleToMultiValueMapAdapterTests.java | 184 ++++++++++ .../web/MockMultipartHttpServletRequest.java | 4 +- .../org/springframework/http/HttpHeaders.java | 4 + .../http/ReadOnlyHttpHeaders.java | 5 + .../AbstractMultipartHttpServletRequest.java | 4 +- ...ltipartRouterFunctionIntegrationTests.java | 2 +- .../web/servlet/view/RedirectViewTests.java | 2 +- 12 files changed, 978 insertions(+), 8 deletions(-) create mode 100644 spring-core/src/main/java/org/springframework/util/MultiToSingleValueMapAdapter.java create mode 100644 spring-core/src/main/java/org/springframework/util/SingleToMultiValueMapAdapter.java create mode 100644 spring-core/src/test/java/org/springframework/util/MultiToSingleValueMapAdapterTests.java create mode 100644 spring-core/src/test/java/org/springframework/util/SingleToMultiValueMapAdapterTests.java diff --git a/spring-core/src/main/java/org/springframework/util/MultiToSingleValueMapAdapter.java b/spring-core/src/main/java/org/springframework/util/MultiToSingleValueMapAdapter.java new file mode 100644 index 00000000000..bc56680d7a9 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/util/MultiToSingleValueMapAdapter.java @@ -0,0 +1,274 @@ +/* + * 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.util; + +import java.io.Serializable; +import java.util.AbstractCollection; +import java.util.AbstractMap; +import java.util.AbstractSet; +import java.util.Collection; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.BiConsumer; + +import org.springframework.lang.Nullable; + +/** + * Adapts a given {@link MultiValueMap} to the {@link Map} contract. The + * difference with {@link SingleToMultiValueMapAdapter} and + * {@link MultiValueMapAdapter} is that this class adapts in the opposite + * direction. + * + * @author Arjen Poutsma + * @since 6.2 + * @param the key type + * @param the value element type + */ +@SuppressWarnings("serial") +final class MultiToSingleValueMapAdapter implements Map, Serializable { + + private final MultiValueMap targetMap; + + @Nullable + private transient Collection values; + + @Nullable + private transient Set> entries; + + + /** + * Wrap the given target {@link MultiValueMap} as a {@link Map} adapter. + * @param targetMap the target {@code MultiValue} + */ + public MultiToSingleValueMapAdapter(MultiValueMap targetMap) { + Assert.notNull(targetMap, "'targetMap' must not be null"); + this.targetMap = targetMap; + } + + @Override + public int size() { + return this.targetMap.size(); + } + + @Override + public boolean isEmpty() { + return this.targetMap.isEmpty(); + } + + @Override + public boolean containsKey(Object key) { + return this.targetMap.containsKey(key); + } + + @Override + public boolean containsValue(@Nullable Object value) { + Iterator> i = entrySet().iterator(); + if (value == null) { + while (i.hasNext()) { + Entry e = i.next(); + if (e.getValue() == null) { + return true; + } + } + } + else { + while (i.hasNext()) { + Entry e = i.next(); + if (value.equals(e.getValue())) { + return true; + } + } + } + return false; + } + + @Override + @Nullable + public V get(Object key) { + return adaptValue(this.targetMap.get(key)); + } + + @Nullable + @Override + public V put(K key, @Nullable V value) { + return adaptValue(this.targetMap.put(key, adaptValue(value))); + } + + @Override + @Nullable + public V remove(Object key) { + return adaptValue(this.targetMap.remove(key)); + } + + @Override + public void putAll(Map map) { + for (Entry entry : map.entrySet()) { + put(entry.getKey(), entry.getValue()); + } + } + + @Override + public void clear() { + this.targetMap.clear(); + } + + @Override + public Set keySet() { + return this.targetMap.keySet(); + } + + @Override + public Collection values() { + Collection values = this.values; + if (values == null) { + Collection> targetValues = this.targetMap.values(); + values = new AbstractCollection() { + @Override + public Iterator iterator() { + Iterator> targetIterator = targetValues.iterator(); + return new Iterator() { + @Override + public boolean hasNext() { + return targetIterator.hasNext(); + } + + @Override + public V next() { + return targetIterator.next().get(0); + } + }; + } + + @Override + public int size() { + return targetValues.size(); + } + }; + this.values = values; + } + return values; + } + + + + @Override + public Set> entrySet() { + Set> entries = this.entries; + if (entries == null) { + Set>> targetEntries = this.targetMap.entrySet(); + entries = new AbstractSet<>() { + @Override + public Iterator> iterator() { + Iterator>> targetIterator = targetEntries.iterator(); + return new Iterator<>() { + @Override + public boolean hasNext() { + return targetIterator.hasNext(); + } + + @Override + public Entry next() { + Entry> entry = targetIterator.next(); + return new AbstractMap.SimpleImmutableEntry<>(entry.getKey(), entry.getValue().get(0)); + } + }; + } + + @Override + public int size() { + return targetEntries.size(); + } + }; + this.entries = entries; + } + return entries; + } + + @Override + public void forEach(BiConsumer action) { + this.targetMap.forEach((k, vs) -> action.accept(k, vs.get(0))); + } + + @Override + public boolean equals(@Nullable Object o) { + if (o == this) { + return true; + } + else if (o instanceof Map other) { + if (this.size() != other.size()) { + return false; + } + try { + for (Entry e : entrySet()) { + K key = e.getKey(); + V value = e.getValue(); + if (value == null) { + if (other.get(key) != null || !other.containsKey(key)) { + return false; + } + } + else { + if (!value.equals(other.get(key))) { + return false; + } + } + } + } + catch (ClassCastException | NullPointerException ignore) { + return false; + } + return true; + } + else { + return false; + } + } + + @Override + public int hashCode() { + return this.targetMap.hashCode(); + } + + @Override + public String toString() { + return this.targetMap.toString(); + } + + @Nullable + private V adaptValue(@Nullable List values) { + if (!CollectionUtils.isEmpty(values)) { + return values.get(0); + } + else { + return null; + } + } + + @Nullable + private List adaptValue(@Nullable V value) { + if (value != null) { + return Collections.singletonList(value); + } + else { + return null; + } + } + +} diff --git a/spring-core/src/main/java/org/springframework/util/MultiValueMap.java b/spring-core/src/main/java/org/springframework/util/MultiValueMap.java index 2429f556502..1792bda78fe 100644 --- a/spring-core/src/main/java/org/springframework/util/MultiValueMap.java +++ b/spring-core/src/main/java/org/springframework/util/MultiValueMap.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 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. @@ -89,8 +89,57 @@ public interface MultiValueMap extends Map> { /** * Return a {@code Map} with the first values contained in this {@code MultiValueMap}. + * The difference between this method and {@link #asSingleValueMap()} is + * that this method returns a copy of the entries of this map, whereas + * the latter returns a view. * @return a single value representation of this map */ Map toSingleValueMap(); + /** + * Return this map as a {@code Map} with the first values contained in this {@code MultiValueMap}. + * The difference between this method and {@link #toSingleValueMap()} is + * that this method returns a view of the entries of this map, whereas + * the latter returns a copy. + * @return a single value representation of this map + * @since 6.2 + */ + default Map asSingleValueMap() { + return new MultiToSingleValueMapAdapter<>(this); + } + + + /** + * Return a {@code MultiValueMap} that adapts the given single-value + * {@code Map}. + * The returned map cannot map multiple values to the same key, and doing so + * results in an {@link UnsupportedOperationException}. Use + * {@link #fromMultiValue(Map)} to support multiple values. + * @param map the map to be adapted + * @param the key type + * @param the value element type + * @return a multi-value-map that delegates to {@code map} + * @since 6.2 + * @see #fromMultiValue(Map) + */ + static MultiValueMap fromSingleValue(Map map) { + Assert.notNull(map, "Map must not be null"); + return new SingleToMultiValueMapAdapter<>(map); + } + + /** + * Return a {@code MultiValueMap} that adapts the given multi-value + * {@code Map>}. + * @param map the map to be adapted + * @param the key type + * @param the value element type + * @return a multi-value-map that delegates to {@code map} + * @since 6.2 + * @see #fromSingleValue(Map) + */ + static MultiValueMap fromMultiValue(Map> map) { + Assert.notNull(map, "Map must not be null"); + return new MultiValueMapAdapter<>(map); + } + } diff --git a/spring-core/src/main/java/org/springframework/util/SingleToMultiValueMapAdapter.java b/spring-core/src/main/java/org/springframework/util/SingleToMultiValueMapAdapter.java new file mode 100644 index 00000000000..c6d424e41e8 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/util/SingleToMultiValueMapAdapter.java @@ -0,0 +1,319 @@ +/* + * 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.util; + +import java.io.Serializable; +import java.util.AbstractCollection; +import java.util.AbstractMap; +import java.util.AbstractSet; +import java.util.Collection; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.BiConsumer; + +import org.springframework.lang.Nullable; + +/** + * Adapts a given {@link MultiValueMap} to the {@link Map} contract. The + * difference with {@link MultiValueMapAdapter} is that this class delegates to + * a {@code Map}, whereas {@link MultiValueMapAdapter} needs a + * {@code Map>}. {@link MultiToSingleValueMapAdapter} adapts in the + * opposite direction as this class. + * + * @author Arjen Poutsma + * @since 6.2 + * @param the key type + * @param the value element type + */ +@SuppressWarnings("serial") +final class SingleToMultiValueMapAdapter implements MultiValueMap, Serializable { + + private final Map targetMap; + + @Nullable + private transient Collection> values; + + @Nullable + private transient Set>> entries; + + + /** + * Wrap the given target {@link Map} as a {@link MultiValueMap} adapter. + * @param targetMap the plain target {@code Map} + */ + public SingleToMultiValueMapAdapter(Map targetMap) { + Assert.notNull(targetMap, "'targetMap' must not be null"); + this.targetMap = targetMap; + } + + + // MultiValueMap implementation + + @Override + @Nullable + public V getFirst(K key) { + return this.targetMap.get(key); + } + + @Override + public void add(K key, @Nullable V value) { + if (!this.targetMap.containsKey(key)) { + this.targetMap.put(key, value); + } + else { + throw new UnsupportedOperationException("Duplicate key: " + key); + } + } + + @Override + @SuppressWarnings("unchecked") + public void addAll(K key, List values) { + if (!this.targetMap.containsKey(key)) { + put(key, (List) values); + } + else { + throw new UnsupportedOperationException("Duplicate key: " + key); + } + } + + @Override + public void addAll(MultiValueMap values) { + values.forEach(this::addAll); + } + + @Override + public void set(K key, @Nullable V value) { + this.targetMap.put(key, value); + } + + @Override + public void setAll(Map values) { + this.targetMap.putAll(values); + } + + @Override + public Map toSingleValueMap() { + return Collections.unmodifiableMap(this.targetMap); + } + + + // Map implementation + + @Override + public int size() { + return this.targetMap.size(); + } + + @Override + public boolean isEmpty() { + return this.targetMap.isEmpty(); + } + + @Override + public boolean containsKey(Object key) { + return this.targetMap.containsKey(key); + } + + @Override + public boolean containsValue(@Nullable Object value) { + Iterator>> i = entrySet().iterator(); + if (value == null) { + while (i.hasNext()) { + Entry> e = i.next(); + if (e.getValue() == null || e.getValue().isEmpty()) { + return true; + } + } + } + else { + while (i.hasNext()) { + Entry> e = i.next(); + if (value.equals(e.getValue())) { + return true; + } + } + } + return false; + } + + @Override + @Nullable + public List get(Object key) { + V value = this.targetMap.get(key); + return (value != null) ? Collections.singletonList(value) : null; + } + + @Override + @Nullable + public List put(K key, List values) { + if (values.isEmpty()) { + V result = this.targetMap.put(key, null); + return (result != null) ? Collections.singletonList(result) : null; + } + else if (values.size() == 1) { + V result = this.targetMap.put(key, values.get(0)); + return (result != null) ? Collections.singletonList(result) : null; + } + else { + throw new UnsupportedOperationException("Duplicate key: " + key); + } + } + + @Override + @Nullable + public List remove(Object key) { + V result = this.targetMap.remove(key); + return (result != null) ? Collections.singletonList(result) : null; + } + + @Override + public void putAll(Map> map) { + for (Entry> entry : map.entrySet()) { + put(entry.getKey(), entry.getValue()); + } + } + + @Override + public void clear() { + this.targetMap.clear(); + } + + @Override + public Set keySet() { + return this.targetMap.keySet(); + } + + @Override + public Collection> values() { + Collection> values = this.values; + if (values == null) { + Collection targetValues = this.targetMap.values(); + values = new AbstractCollection<>() { + @Override + public Iterator> iterator() { + Iterator targetIterator = targetValues.iterator(); + return new Iterator<>() { + @Override + public boolean hasNext() { + return targetIterator.hasNext(); + } + + @Override + public List next() { + return Collections.singletonList(targetIterator.next()); + } + }; + } + + @Override + public int size() { + return targetValues.size(); + } + }; + this.values = values; + } + return values; + } + + @Override + public Set>> entrySet() { + Set>> entries = this.entries; + if (entries == null) { + Set> targetEntries = this.targetMap.entrySet(); + entries = new AbstractSet<>() { + @Override + public Iterator>> iterator() { + Iterator> targetIterator = targetEntries.iterator(); + return new Iterator<>() { + @Override + public boolean hasNext() { + return targetIterator.hasNext(); + } + + @Override + public Entry> next() { + Entry entry = targetIterator.next(); + return new AbstractMap.SimpleImmutableEntry<>(entry.getKey(), + Collections.singletonList(entry.getValue())); + } + }; + } + + @Override + public int size() { + return targetEntries.size(); + } + }; + this.entries = entries; + } + return entries; + } + + @Override + public void forEach(BiConsumer> action) { + this.targetMap.forEach((k, v) -> action.accept(k, Collections.singletonList(v))); + } + + @Override + public boolean equals(@Nullable Object o) { + if (o == this) { + return true; + } + else if (o instanceof Map other) { + if (this.size() != other.size()) { + return false; + } + try { + for (Entry> e : entrySet()) { + K key = e.getKey(); + List values = e.getValue(); + if (values == null) { + if (other.get(key) != null || !other.containsKey(key)) { + return false; + } + } + else { + if (!values .equals(other.get(key))) { + return false; + } + } + } + } + catch (ClassCastException | NullPointerException ignore) { + return false; + } + return true; + } + else { + return false; + } + } + + @Override + public int hashCode() { + return this.targetMap.hashCode(); + } + + @Override + public String toString() { + return this.targetMap.toString(); + } + +} diff --git a/spring-core/src/main/java/org/springframework/util/UnmodifiableMultiValueMap.java b/spring-core/src/main/java/org/springframework/util/UnmodifiableMultiValueMap.java index 3d2f980575f..9a14150156c 100644 --- a/spring-core/src/main/java/org/springframework/util/UnmodifiableMultiValueMap.java +++ b/spring-core/src/main/java/org/springframework/util/UnmodifiableMultiValueMap.java @@ -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. @@ -120,6 +120,10 @@ final class UnmodifiableMultiValueMap implements MultiValueMap, Serial return this.delegate.toSingleValueMap(); } + @Override + public Map asSingleValueMap() { + return this.delegate.asSingleValueMap(); + } @Override public boolean equals(@Nullable Object other) { diff --git a/spring-core/src/test/java/org/springframework/util/MultiToSingleValueMapAdapterTests.java b/spring-core/src/test/java/org/springframework/util/MultiToSingleValueMapAdapterTests.java new file mode 100644 index 00000000000..562ecf95277 --- /dev/null +++ b/spring-core/src/test/java/org/springframework/util/MultiToSingleValueMapAdapterTests.java @@ -0,0 +1,131 @@ +/* + * 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.util; + +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.entry; + +/** + * @author Arjen Poutsma + */ +class MultiToSingleValueMapAdapterTests { + + private LinkedMultiValueMap delegate; + + private Map adapter; + + + @BeforeEach + void setUp() { + this.delegate = new LinkedMultiValueMap<>(); + this.delegate.add("foo", "bar"); + this.delegate.add("foo", "baz"); + this.delegate.add("qux", "quux"); + + this.adapter = new MultiToSingleValueMapAdapter<>(this.delegate); + } + + @Test + void size() { + assertThat(this.adapter.size()).isEqualTo(this.delegate.size()).isEqualTo(2); + } + + @Test + void isEmpty() { + assertThat(this.adapter.isEmpty()).isFalse(); + + this.adapter = new MultiToSingleValueMapAdapter<>(new LinkedMultiValueMap<>()); + assertThat(this.adapter.isEmpty()).isTrue(); + } + + @Test + void containsKey() { + assertThat(this.adapter.containsKey("foo")).isTrue(); + assertThat(this.adapter.containsKey("qux")).isTrue(); + assertThat(this.adapter.containsKey("corge")).isFalse(); + } + + @Test + void containsValue() { + assertThat(this.adapter.containsValue("bar")).isTrue(); + assertThat(this.adapter.containsValue("quux")).isTrue(); + assertThat(this.adapter.containsValue("corge")).isFalse(); + } + + @Test + void get() { + assertThat(this.adapter.get("foo")).isEqualTo("bar"); + assertThat(this.adapter.get("qux")).isEqualTo("quux"); + assertThat(this.adapter.get("corge")).isNull(); + } + + @Test + void put() { + String result = this.adapter.put("foo", "bar"); + assertThat(result).isEqualTo("bar"); + assertThat(this.delegate.get("foo")).containsExactly("bar"); + } + + @Test + void remove() { + this.adapter.remove("foo"); + assertThat(this.adapter.containsKey("foo")).isFalse(); + assertThat(this.delegate.containsKey("foo")).isFalse(); + } + + @Test + void putAll() { + LinkedHashMap map = new LinkedHashMap<>(); + map.put("foo", "bar"); + map.put("qux", null); + this.adapter.putAll(map); + assertThat(this.adapter.get("foo")).isEqualTo("bar"); + assertThat(this.adapter.get("qux")).isNull(); + + assertThat(this.delegate.get("foo")).isEqualTo(List.of("bar")); + assertThat(this.adapter.get("qux")).isNull(); + } + + @Test + void clear() { + this.adapter.clear(); + assertThat(this.adapter).isEmpty(); + assertThat(this.delegate).isEmpty(); + } + + @Test + void keySet() { + assertThat(this.adapter.keySet()).containsExactly("foo", "qux"); + } + + @Test + void values() { + assertThat(this.adapter.values()).containsExactly("bar", "quux"); + } + + @Test + void entrySet() { + assertThat(this.adapter.entrySet()).containsExactly(entry("foo", "bar"), entry("qux", "quux")); + } +} diff --git a/spring-core/src/test/java/org/springframework/util/SingleToMultiValueMapAdapterTests.java b/spring-core/src/test/java/org/springframework/util/SingleToMultiValueMapAdapterTests.java new file mode 100644 index 00000000000..eb155fca3fe --- /dev/null +++ b/spring-core/src/test/java/org/springframework/util/SingleToMultiValueMapAdapterTests.java @@ -0,0 +1,184 @@ +/* + * 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.util; + +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.entry; + +/** + * @author Arjen Poutsma + */ +class SingleToMultiValueMapAdapterTests { + + + private Map delegate; + + private MultiValueMap adapter; + + @BeforeEach + void setUp() { + this.delegate = new LinkedHashMap<>(); + this.delegate.put("foo", "bar"); + this.delegate.put("qux", "quux"); + + this.adapter = new SingleToMultiValueMapAdapter<>(this.delegate); + } + + @Test + void getFirst() { + assertThat(this.adapter.getFirst("foo")).isEqualTo("bar"); + assertThat(this.adapter.getFirst("qux")).isEqualTo("quux"); + assertThat(this.adapter.getFirst("corge")).isNull(); + } + + @Test + void add() { + this.adapter.add("corge", "grault"); + assertThat(this.adapter.getFirst("corge")).isEqualTo("grault"); + assertThat(this.delegate.get("corge")).isEqualTo("grault"); + + assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(() -> + this.adapter.add("foo", "garply")); + } + + @Test + void addAll() { + MultiValueMap map = new LinkedMultiValueMap<>(); + map.add("corge", "grault"); + this.adapter.addAll(map); + + assertThat(this.adapter.getFirst("corge")).isEqualTo("grault"); + assertThat(this.delegate.get("corge")).isEqualTo("grault"); + + assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(() -> + this.adapter.addAll(map)); + } + + @Test + void set() { + this.adapter.set("foo", "baz"); + assertThat(this.delegate.get("foo")).isEqualTo("baz"); + } + + @Test + void setAll() { + this.adapter.setAll(Map.of("foo", "baz")); + assertThat(this.delegate.get("foo")).isEqualTo("baz"); + } + + @Test + void size() { + assertThat(this.adapter.size()).isEqualTo(this.delegate.size()).isEqualTo(2); + } + + @Test + void isEmpty() { + assertThat(this.adapter.isEmpty()).isFalse(); + + this.adapter = new SingleToMultiValueMapAdapter<>(Collections.emptyMap()); + assertThat(this.adapter.isEmpty()).isTrue(); + } + + @Test + void containsKey() { + assertThat(this.adapter.containsKey("foo")).isTrue(); + assertThat(this.adapter.containsKey("qux")).isTrue(); + assertThat(this.adapter.containsKey("corge")).isFalse(); + } + + @Test + void containsValue() { + assertThat(this.adapter.containsValue(List.of("bar"))).isTrue(); + assertThat(this.adapter.containsValue(List.of("quux"))).isTrue(); + assertThat(this.adapter.containsValue(List.of("corge"))).isFalse(); + } + + @Test + void get() { + assertThat(this.adapter.get("foo")).isEqualTo(List.of("bar")); + assertThat(this.adapter.get("qux")).isEqualTo(List.of("quux")); + assertThat(this.adapter.get("corge")).isNull(); + } + + @Test + void put() { + assertThat(this.adapter.put("foo", List.of("baz"))).containsExactly("bar"); + assertThat(this.adapter.put("qux", Collections.emptyList())).containsExactly("quux"); + assertThat(this.adapter.put("grault", List.of("garply"))).isNull(); + + assertThat(this.delegate).containsExactly(entry("foo", "baz"), entry("qux", null), entry("grault", "garply")); + + assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(() -> + this.adapter.put("foo", List.of("bar", "baz"))); + } + + @Test + void remove() { + assertThat(this.adapter.remove("foo")).isEqualTo(List.of("bar")); + assertThat(this.adapter.containsKey("foo")).isFalse(); + assertThat(this.delegate.containsKey("foo")).isFalse(); + } + + @Test + void putAll() { + MultiValueMap map = new LinkedMultiValueMap<>(); + map.add("foo", "baz"); + map.add("qux", null); + map.add("grault", "garply"); + this.adapter.putAll(map); + + assertThat(this.delegate).containsExactly(entry("foo", "baz"), entry("qux", null), entry("grault", "garply")); + } + + @Test + void clear() { + this.adapter.clear(); + assertThat(this.adapter).isEmpty(); + assertThat(this.delegate).isEmpty(); + } + + @Test + void keySet() { + assertThat(this.adapter.keySet()).containsExactly("foo", "qux"); + } + + @Test + void values() { + assertThat(this.adapter.values()).containsExactly(List.of("bar"), List.of("quux")); + } + + @Test + void entrySet() { + assertThat(this.adapter.entrySet()).containsExactly(entry("foo", List.of("bar")), entry("qux", List.of("quux"))); + } + + @Test + void forEach() { + MultiValueMap seen = new LinkedMultiValueMap<>(); + this.adapter.forEach(seen::put); + assertThat(seen).containsExactly(entry("foo", List.of("bar")), entry("qux", List.of("quux"))); + } +} diff --git a/spring-test/src/main/java/org/springframework/mock/web/MockMultipartHttpServletRequest.java b/spring-test/src/main/java/org/springframework/mock/web/MockMultipartHttpServletRequest.java index d6c1afa84ab..41411d366cc 100644 --- a/spring-test/src/main/java/org/springframework/mock/web/MockMultipartHttpServletRequest.java +++ b/spring-test/src/main/java/org/springframework/mock/web/MockMultipartHttpServletRequest.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 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. @@ -109,7 +109,7 @@ public class MockMultipartHttpServletRequest extends MockHttpServletRequest impl @Override public Map getFileMap() { - return this.multipartFiles.toSingleValueMap(); + return this.multipartFiles.asSingleValueMap(); } @Override 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 16ee323dce6..17abfa180a5 100644 --- a/spring-web/src/main/java/org/springframework/http/HttpHeaders.java +++ b/spring-web/src/main/java/org/springframework/http/HttpHeaders.java @@ -1774,6 +1774,10 @@ public class HttpHeaders implements MultiValueMap, Serializable return this.headers.toSingleValueMap(); } + @Override + public Map asSingleValueMap() { + return this.headers.asSingleValueMap(); + } // Map implementation 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 affefa5b9f3..b667207f936 100644 --- a/spring-web/src/main/java/org/springframework/http/ReadOnlyHttpHeaders.java +++ b/spring-web/src/main/java/org/springframework/http/ReadOnlyHttpHeaders.java @@ -122,6 +122,11 @@ class ReadOnlyHttpHeaders extends HttpHeaders { return Collections.unmodifiableMap(this.headers.toSingleValueMap()); } + @Override + public Map asSingleValueMap() { + return Collections.unmodifiableMap(this.headers.asSingleValueMap()); + } + @Override public Set keySet() { return Collections.unmodifiableSet(this.headers.keySet()); diff --git a/spring-web/src/main/java/org/springframework/web/multipart/support/AbstractMultipartHttpServletRequest.java b/spring-web/src/main/java/org/springframework/web/multipart/support/AbstractMultipartHttpServletRequest.java index 132e2514268..1299f15c7f9 100644 --- a/spring-web/src/main/java/org/springframework/web/multipart/support/AbstractMultipartHttpServletRequest.java +++ b/spring-web/src/main/java/org/springframework/web/multipart/support/AbstractMultipartHttpServletRequest.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 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. @@ -102,7 +102,7 @@ public abstract class AbstractMultipartHttpServletRequest extends HttpServletReq @Override public Map getFileMap() { - return getMultipartFiles().toSingleValueMap(); + return getMultipartFiles().asSingleValueMap(); } @Override diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/function/MultipartRouterFunctionIntegrationTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/function/MultipartRouterFunctionIntegrationTests.java index 1268d8a15ac..5388b7566f9 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/function/MultipartRouterFunctionIntegrationTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/function/MultipartRouterFunctionIntegrationTests.java @@ -206,7 +206,7 @@ class MultipartRouterFunctionIntegrationTests extends AbstractRouterFunctionInte return request .body(BodyExtractors.toMultipartData()) .flatMap(map -> { - Map parts = map.toSingleValueMap(); + Map parts = map.asSingleValueMap(); try { assertThat(parts).hasSize(2); assertThat(((FilePart) parts.get("fooPart")).filename()).isEqualTo("foo.txt"); diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/view/RedirectViewTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/view/RedirectViewTests.java index 4f420215e11..3f83c68b1b4 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/view/RedirectViewTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/view/RedirectViewTests.java @@ -146,7 +146,7 @@ class RedirectViewTests { assertThat(response.getHeader("Location")).isEqualTo("https://url.somewhere.com/path?id=1"); assertThat(flashMap.getTargetRequestPath()).isEqualTo("/path"); - assertThat(flashMap.getTargetRequestParams().toSingleValueMap()).isEqualTo(model); + assertThat(flashMap.getTargetRequestParams().asSingleValueMap()).isEqualTo(model); } @Test