Browse Source

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<?,List<?>> to the
  MultiValueMap interface

Closes gh-32832
pull/32896/head
Arjen Poutsma 2 years ago
parent
commit
903493e9a9
  1. 274
      spring-core/src/main/java/org/springframework/util/MultiToSingleValueMapAdapter.java
  2. 51
      spring-core/src/main/java/org/springframework/util/MultiValueMap.java
  3. 319
      spring-core/src/main/java/org/springframework/util/SingleToMultiValueMapAdapter.java
  4. 6
      spring-core/src/main/java/org/springframework/util/UnmodifiableMultiValueMap.java
  5. 131
      spring-core/src/test/java/org/springframework/util/MultiToSingleValueMapAdapterTests.java
  6. 184
      spring-core/src/test/java/org/springframework/util/SingleToMultiValueMapAdapterTests.java
  7. 4
      spring-test/src/main/java/org/springframework/mock/web/MockMultipartHttpServletRequest.java
  8. 4
      spring-web/src/main/java/org/springframework/http/HttpHeaders.java
  9. 5
      spring-web/src/main/java/org/springframework/http/ReadOnlyHttpHeaders.java
  10. 4
      spring-web/src/main/java/org/springframework/web/multipart/support/AbstractMultipartHttpServletRequest.java
  11. 2
      spring-webflux/src/test/java/org/springframework/web/reactive/function/MultipartRouterFunctionIntegrationTests.java
  12. 2
      spring-webmvc/src/test/java/org/springframework/web/servlet/view/RedirectViewTests.java

274
spring-core/src/main/java/org/springframework/util/MultiToSingleValueMapAdapter.java

@ -0,0 +1,274 @@ @@ -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 <K> the key type
* @param <V> the value element type
*/
@SuppressWarnings("serial")
final class MultiToSingleValueMapAdapter<K, V> implements Map<K, V>, Serializable {
private final MultiValueMap<K, V> targetMap;
@Nullable
private transient Collection<V> values;
@Nullable
private transient Set<Entry<K, V>> entries;
/**
* Wrap the given target {@link MultiValueMap} as a {@link Map} adapter.
* @param targetMap the target {@code MultiValue}
*/
public MultiToSingleValueMapAdapter(MultiValueMap<K, V> 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<Entry<K, V>> i = entrySet().iterator();
if (value == null) {
while (i.hasNext()) {
Entry<K, V> e = i.next();
if (e.getValue() == null) {
return true;
}
}
}
else {
while (i.hasNext()) {
Entry<K, V> 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<? extends K, ? extends V> map) {
for (Entry<? extends K, ? extends V> entry : map.entrySet()) {
put(entry.getKey(), entry.getValue());
}
}
@Override
public void clear() {
this.targetMap.clear();
}
@Override
public Set<K> keySet() {
return this.targetMap.keySet();
}
@Override
public Collection<V> values() {
Collection<V> values = this.values;
if (values == null) {
Collection<List<V>> targetValues = this.targetMap.values();
values = new AbstractCollection<V>() {
@Override
public Iterator<V> iterator() {
Iterator<List<V>> targetIterator = targetValues.iterator();
return new Iterator<V>() {
@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<Entry<K, V>> entrySet() {
Set<Entry<K, V>> entries = this.entries;
if (entries == null) {
Set<Entry<K, List<V>>> targetEntries = this.targetMap.entrySet();
entries = new AbstractSet<>() {
@Override
public Iterator<Entry<K, V>> iterator() {
Iterator<Entry<K, List<V>>> targetIterator = targetEntries.iterator();
return new Iterator<>() {
@Override
public boolean hasNext() {
return targetIterator.hasNext();
}
@Override
public Entry<K, V> next() {
Entry<K, List<V>> 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<? super K, ? super V> 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<K, V> 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<V> values) {
if (!CollectionUtils.isEmpty(values)) {
return values.get(0);
}
else {
return null;
}
}
@Nullable
private List<V> adaptValue(@Nullable V value) {
if (value != null) {
return Collections.singletonList(value);
}
else {
return null;
}
}
}

51
spring-core/src/main/java/org/springframework/util/MultiValueMap.java

@ -1,5 +1,5 @@ @@ -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<K, V> extends Map<K, List<V>> { @@ -89,8 +89,57 @@ public interface MultiValueMap<K, V> extends Map<K, List<V>> {
/**
* 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<K, V> 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<K, V> asSingleValueMap() {
return new MultiToSingleValueMapAdapter<>(this);
}
/**
* Return a {@code MultiValueMap<K, V>} that adapts the given single-value
* {@code Map<K, V>}.
* 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 <K> the key type
* @param <V> the value element type
* @return a multi-value-map that delegates to {@code map}
* @since 6.2
* @see #fromMultiValue(Map)
*/
static <K, V> MultiValueMap<K, V> fromSingleValue(Map<K, V> map) {
Assert.notNull(map, "Map must not be null");
return new SingleToMultiValueMapAdapter<>(map);
}
/**
* Return a {@code MultiValueMap<K, V>} that adapts the given multi-value
* {@code Map<K, List<V>>}.
* @param map the map to be adapted
* @param <K> the key type
* @param <V> the value element type
* @return a multi-value-map that delegates to {@code map}
* @since 6.2
* @see #fromSingleValue(Map)
*/
static <K, V> MultiValueMap<K, V> fromMultiValue(Map<K, List<V>> map) {
Assert.notNull(map, "Map must not be null");
return new MultiValueMapAdapter<>(map);
}
}

319
spring-core/src/main/java/org/springframework/util/SingleToMultiValueMapAdapter.java

@ -0,0 +1,319 @@ @@ -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<K, V>}, whereas {@link MultiValueMapAdapter} needs a
* {@code Map<K, List<V>>}. {@link MultiToSingleValueMapAdapter} adapts in the
* opposite direction as this class.
*
* @author Arjen Poutsma
* @since 6.2
* @param <K> the key type
* @param <V> the value element type
*/
@SuppressWarnings("serial")
final class SingleToMultiValueMapAdapter<K, V> implements MultiValueMap<K, V>, Serializable {
private final Map<K, V> targetMap;
@Nullable
private transient Collection<List<V>> values;
@Nullable
private transient Set<Entry<K, List<V>>> entries;
/**
* Wrap the given target {@link Map} as a {@link MultiValueMap} adapter.
* @param targetMap the plain target {@code Map}
*/
public SingleToMultiValueMapAdapter(Map<K, V> 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<? extends V> values) {
if (!this.targetMap.containsKey(key)) {
put(key, (List<V>) values);
}
else {
throw new UnsupportedOperationException("Duplicate key: " + key);
}
}
@Override
public void addAll(MultiValueMap<K, V> values) {
values.forEach(this::addAll);
}
@Override
public void set(K key, @Nullable V value) {
this.targetMap.put(key, value);
}
@Override
public void setAll(Map<K, V> values) {
this.targetMap.putAll(values);
}
@Override
public Map<K, V> 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<Entry<K, List<V>>> i = entrySet().iterator();
if (value == null) {
while (i.hasNext()) {
Entry<K, List<V>> e = i.next();
if (e.getValue() == null || e.getValue().isEmpty()) {
return true;
}
}
}
else {
while (i.hasNext()) {
Entry<K, List<V>> e = i.next();
if (value.equals(e.getValue())) {
return true;
}
}
}
return false;
}
@Override
@Nullable
public List<V> get(Object key) {
V value = this.targetMap.get(key);
return (value != null) ? Collections.singletonList(value) : null;
}
@Override
@Nullable
public List<V> put(K key, List<V> 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<V> remove(Object key) {
V result = this.targetMap.remove(key);
return (result != null) ? Collections.singletonList(result) : null;
}
@Override
public void putAll(Map<? extends K, ? extends List<V>> map) {
for (Entry<? extends K, ? extends List<V>> entry : map.entrySet()) {
put(entry.getKey(), entry.getValue());
}
}
@Override
public void clear() {
this.targetMap.clear();
}
@Override
public Set<K> keySet() {
return this.targetMap.keySet();
}
@Override
public Collection<List<V>> values() {
Collection<List<V>> values = this.values;
if (values == null) {
Collection<V> targetValues = this.targetMap.values();
values = new AbstractCollection<>() {
@Override
public Iterator<List<V>> iterator() {
Iterator<V> targetIterator = targetValues.iterator();
return new Iterator<>() {
@Override
public boolean hasNext() {
return targetIterator.hasNext();
}
@Override
public List<V> next() {
return Collections.singletonList(targetIterator.next());
}
};
}
@Override
public int size() {
return targetValues.size();
}
};
this.values = values;
}
return values;
}
@Override
public Set<Entry<K, List<V>>> entrySet() {
Set<Entry<K, List<V>>> entries = this.entries;
if (entries == null) {
Set<Entry<K, V>> targetEntries = this.targetMap.entrySet();
entries = new AbstractSet<>() {
@Override
public Iterator<Entry<K, List<V>>> iterator() {
Iterator<Entry<K, V>> targetIterator = targetEntries.iterator();
return new Iterator<>() {
@Override
public boolean hasNext() {
return targetIterator.hasNext();
}
@Override
public Entry<K, List<V>> next() {
Entry<K, V> 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<? super K, ? super List<V>> 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<K, List<V>> e : entrySet()) {
K key = e.getKey();
List<V> 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();
}
}

6
spring-core/src/main/java/org/springframework/util/UnmodifiableMultiValueMap.java

@ -1,5 +1,5 @@ @@ -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<K,V> implements MultiValueMap<K,V>, Serial @@ -120,6 +120,10 @@ final class UnmodifiableMultiValueMap<K,V> implements MultiValueMap<K,V>, Serial
return this.delegate.toSingleValueMap();
}
@Override
public Map<K, V> asSingleValueMap() {
return this.delegate.asSingleValueMap();
}
@Override
public boolean equals(@Nullable Object other) {

131
spring-core/src/test/java/org/springframework/util/MultiToSingleValueMapAdapterTests.java

@ -0,0 +1,131 @@ @@ -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<String, String> delegate;
private Map<String, String> 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<String, String> 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"));
}
}

184
spring-core/src/test/java/org/springframework/util/SingleToMultiValueMapAdapterTests.java

@ -0,0 +1,184 @@ @@ -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<String, String> delegate;
private MultiValueMap<String, String> 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<String, String> 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<String, String> 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<String, String> seen = new LinkedMultiValueMap<>();
this.adapter.forEach(seen::put);
assertThat(seen).containsExactly(entry("foo", List.of("bar")), entry("qux", List.of("quux")));
}
}

4
spring-test/src/main/java/org/springframework/mock/web/MockMultipartHttpServletRequest.java

@ -1,5 +1,5 @@ @@ -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 @@ -109,7 +109,7 @@ public class MockMultipartHttpServletRequest extends MockHttpServletRequest impl
@Override
public Map<String, MultipartFile> getFileMap() {
return this.multipartFiles.toSingleValueMap();
return this.multipartFiles.asSingleValueMap();
}
@Override

4
spring-web/src/main/java/org/springframework/http/HttpHeaders.java

@ -1774,6 +1774,10 @@ public class HttpHeaders implements MultiValueMap<String, String>, Serializable @@ -1774,6 +1774,10 @@ public class HttpHeaders implements MultiValueMap<String, String>, Serializable
return this.headers.toSingleValueMap();
}
@Override
public Map<String, String> asSingleValueMap() {
return this.headers.asSingleValueMap();
}
// Map implementation

5
spring-web/src/main/java/org/springframework/http/ReadOnlyHttpHeaders.java

@ -122,6 +122,11 @@ class ReadOnlyHttpHeaders extends HttpHeaders { @@ -122,6 +122,11 @@ class ReadOnlyHttpHeaders extends HttpHeaders {
return Collections.unmodifiableMap(this.headers.toSingleValueMap());
}
@Override
public Map<String, String> asSingleValueMap() {
return Collections.unmodifiableMap(this.headers.asSingleValueMap());
}
@Override
public Set<String> keySet() {
return Collections.unmodifiableSet(this.headers.keySet());

4
spring-web/src/main/java/org/springframework/web/multipart/support/AbstractMultipartHttpServletRequest.java

@ -1,5 +1,5 @@ @@ -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 @@ -102,7 +102,7 @@ public abstract class AbstractMultipartHttpServletRequest extends HttpServletReq
@Override
public Map<String, MultipartFile> getFileMap() {
return getMultipartFiles().toSingleValueMap();
return getMultipartFiles().asSingleValueMap();
}
@Override

2
spring-webflux/src/test/java/org/springframework/web/reactive/function/MultipartRouterFunctionIntegrationTests.java

@ -206,7 +206,7 @@ class MultipartRouterFunctionIntegrationTests extends AbstractRouterFunctionInte @@ -206,7 +206,7 @@ class MultipartRouterFunctionIntegrationTests extends AbstractRouterFunctionInte
return request
.body(BodyExtractors.toMultipartData())
.flatMap(map -> {
Map<String, Part> parts = map.toSingleValueMap();
Map<String, Part> parts = map.asSingleValueMap();
try {
assertThat(parts).hasSize(2);
assertThat(((FilePart) parts.get("fooPart")).filename()).isEqualTo("foo.txt");

2
spring-webmvc/src/test/java/org/springframework/web/servlet/view/RedirectViewTests.java

@ -146,7 +146,7 @@ class RedirectViewTests { @@ -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

Loading…
Cancel
Save