Skip to content

Commit 903493e

Browse files
committed
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
1 parent 97647ef commit 903493e

File tree

12 files changed

+978
-8
lines changed

12 files changed

+978
-8
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,274 @@
1+
/*
2+
* Copyright 2002-2024 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.util;
18+
19+
import java.io.Serializable;
20+
import java.util.AbstractCollection;
21+
import java.util.AbstractMap;
22+
import java.util.AbstractSet;
23+
import java.util.Collection;
24+
import java.util.Collections;
25+
import java.util.Iterator;
26+
import java.util.List;
27+
import java.util.Map;
28+
import java.util.Set;
29+
import java.util.function.BiConsumer;
30+
31+
import org.springframework.lang.Nullable;
32+
33+
/**
34+
* Adapts a given {@link MultiValueMap} to the {@link Map} contract. The
35+
* difference with {@link SingleToMultiValueMapAdapter} and
36+
* {@link MultiValueMapAdapter} is that this class adapts in the opposite
37+
* direction.
38+
*
39+
* @author Arjen Poutsma
40+
* @since 6.2
41+
* @param <K> the key type
42+
* @param <V> the value element type
43+
*/
44+
@SuppressWarnings("serial")
45+
final class MultiToSingleValueMapAdapter<K, V> implements Map<K, V>, Serializable {
46+
47+
private final MultiValueMap<K, V> targetMap;
48+
49+
@Nullable
50+
private transient Collection<V> values;
51+
52+
@Nullable
53+
private transient Set<Entry<K, V>> entries;
54+
55+
56+
/**
57+
* Wrap the given target {@link MultiValueMap} as a {@link Map} adapter.
58+
* @param targetMap the target {@code MultiValue}
59+
*/
60+
public MultiToSingleValueMapAdapter(MultiValueMap<K, V> targetMap) {
61+
Assert.notNull(targetMap, "'targetMap' must not be null");
62+
this.targetMap = targetMap;
63+
}
64+
65+
@Override
66+
public int size() {
67+
return this.targetMap.size();
68+
}
69+
70+
@Override
71+
public boolean isEmpty() {
72+
return this.targetMap.isEmpty();
73+
}
74+
75+
@Override
76+
public boolean containsKey(Object key) {
77+
return this.targetMap.containsKey(key);
78+
}
79+
80+
@Override
81+
public boolean containsValue(@Nullable Object value) {
82+
Iterator<Entry<K, V>> i = entrySet().iterator();
83+
if (value == null) {
84+
while (i.hasNext()) {
85+
Entry<K, V> e = i.next();
86+
if (e.getValue() == null) {
87+
return true;
88+
}
89+
}
90+
}
91+
else {
92+
while (i.hasNext()) {
93+
Entry<K, V> e = i.next();
94+
if (value.equals(e.getValue())) {
95+
return true;
96+
}
97+
}
98+
}
99+
return false;
100+
}
101+
102+
@Override
103+
@Nullable
104+
public V get(Object key) {
105+
return adaptValue(this.targetMap.get(key));
106+
}
107+
108+
@Nullable
109+
@Override
110+
public V put(K key, @Nullable V value) {
111+
return adaptValue(this.targetMap.put(key, adaptValue(value)));
112+
}
113+
114+
@Override
115+
@Nullable
116+
public V remove(Object key) {
117+
return adaptValue(this.targetMap.remove(key));
118+
}
119+
120+
@Override
121+
public void putAll(Map<? extends K, ? extends V> map) {
122+
for (Entry<? extends K, ? extends V> entry : map.entrySet()) {
123+
put(entry.getKey(), entry.getValue());
124+
}
125+
}
126+
127+
@Override
128+
public void clear() {
129+
this.targetMap.clear();
130+
}
131+
132+
@Override
133+
public Set<K> keySet() {
134+
return this.targetMap.keySet();
135+
}
136+
137+
@Override
138+
public Collection<V> values() {
139+
Collection<V> values = this.values;
140+
if (values == null) {
141+
Collection<List<V>> targetValues = this.targetMap.values();
142+
values = new AbstractCollection<V>() {
143+
@Override
144+
public Iterator<V> iterator() {
145+
Iterator<List<V>> targetIterator = targetValues.iterator();
146+
return new Iterator<V>() {
147+
@Override
148+
public boolean hasNext() {
149+
return targetIterator.hasNext();
150+
}
151+
152+
@Override
153+
public V next() {
154+
return targetIterator.next().get(0);
155+
}
156+
};
157+
}
158+
159+
@Override
160+
public int size() {
161+
return targetValues.size();
162+
}
163+
};
164+
this.values = values;
165+
}
166+
return values;
167+
}
168+
169+
170+
171+
@Override
172+
public Set<Entry<K, V>> entrySet() {
173+
Set<Entry<K, V>> entries = this.entries;
174+
if (entries == null) {
175+
Set<Entry<K, List<V>>> targetEntries = this.targetMap.entrySet();
176+
entries = new AbstractSet<>() {
177+
@Override
178+
public Iterator<Entry<K, V>> iterator() {
179+
Iterator<Entry<K, List<V>>> targetIterator = targetEntries.iterator();
180+
return new Iterator<>() {
181+
@Override
182+
public boolean hasNext() {
183+
return targetIterator.hasNext();
184+
}
185+
186+
@Override
187+
public Entry<K, V> next() {
188+
Entry<K, List<V>> entry = targetIterator.next();
189+
return new AbstractMap.SimpleImmutableEntry<>(entry.getKey(), entry.getValue().get(0));
190+
}
191+
};
192+
}
193+
194+
@Override
195+
public int size() {
196+
return targetEntries.size();
197+
}
198+
};
199+
this.entries = entries;
200+
}
201+
return entries;
202+
}
203+
204+
@Override
205+
public void forEach(BiConsumer<? super K, ? super V> action) {
206+
this.targetMap.forEach((k, vs) -> action.accept(k, vs.get(0)));
207+
}
208+
209+
@Override
210+
public boolean equals(@Nullable Object o) {
211+
if (o == this) {
212+
return true;
213+
}
214+
else if (o instanceof Map<?,?> other) {
215+
if (this.size() != other.size()) {
216+
return false;
217+
}
218+
try {
219+
for (Entry<K, V> e : entrySet()) {
220+
K key = e.getKey();
221+
V value = e.getValue();
222+
if (value == null) {
223+
if (other.get(key) != null || !other.containsKey(key)) {
224+
return false;
225+
}
226+
}
227+
else {
228+
if (!value.equals(other.get(key))) {
229+
return false;
230+
}
231+
}
232+
}
233+
}
234+
catch (ClassCastException | NullPointerException ignore) {
235+
return false;
236+
}
237+
return true;
238+
}
239+
else {
240+
return false;
241+
}
242+
}
243+
244+
@Override
245+
public int hashCode() {
246+
return this.targetMap.hashCode();
247+
}
248+
249+
@Override
250+
public String toString() {
251+
return this.targetMap.toString();
252+
}
253+
254+
@Nullable
255+
private V adaptValue(@Nullable List<V> values) {
256+
if (!CollectionUtils.isEmpty(values)) {
257+
return values.get(0);
258+
}
259+
else {
260+
return null;
261+
}
262+
}
263+
264+
@Nullable
265+
private List<V> adaptValue(@Nullable V value) {
266+
if (value != null) {
267+
return Collections.singletonList(value);
268+
}
269+
else {
270+
return null;
271+
}
272+
}
273+
274+
}

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

+50-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2019 the original author or authors.
2+
* Copyright 2002-2024 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -89,8 +89,57 @@ default void addIfAbsent(K key, @Nullable V value) {
8989

9090
/**
9191
* Return a {@code Map} with the first values contained in this {@code MultiValueMap}.
92+
* The difference between this method and {@link #asSingleValueMap()} is
93+
* that this method returns a copy of the entries of this map, whereas
94+
* the latter returns a view.
9295
* @return a single value representation of this map
9396
*/
9497
Map<K, V> toSingleValueMap();
9598

99+
/**
100+
* Return this map as a {@code Map} with the first values contained in this {@code MultiValueMap}.
101+
* The difference between this method and {@link #toSingleValueMap()} is
102+
* that this method returns a view of the entries of this map, whereas
103+
* the latter returns a copy.
104+
* @return a single value representation of this map
105+
* @since 6.2
106+
*/
107+
default Map<K, V> asSingleValueMap() {
108+
return new MultiToSingleValueMapAdapter<>(this);
109+
}
110+
111+
112+
/**
113+
* Return a {@code MultiValueMap<K, V>} that adapts the given single-value
114+
* {@code Map<K, V>}.
115+
* The returned map cannot map multiple values to the same key, and doing so
116+
* results in an {@link UnsupportedOperationException}. Use
117+
* {@link #fromMultiValue(Map)} to support multiple values.
118+
* @param map the map to be adapted
119+
* @param <K> the key type
120+
* @param <V> the value element type
121+
* @return a multi-value-map that delegates to {@code map}
122+
* @since 6.2
123+
* @see #fromMultiValue(Map)
124+
*/
125+
static <K, V> MultiValueMap<K, V> fromSingleValue(Map<K, V> map) {
126+
Assert.notNull(map, "Map must not be null");
127+
return new SingleToMultiValueMapAdapter<>(map);
128+
}
129+
130+
/**
131+
* Return a {@code MultiValueMap<K, V>} that adapts the given multi-value
132+
* {@code Map<K, List<V>>}.
133+
* @param map the map to be adapted
134+
* @param <K> the key type
135+
* @param <V> the value element type
136+
* @return a multi-value-map that delegates to {@code map}
137+
* @since 6.2
138+
* @see #fromSingleValue(Map)
139+
*/
140+
static <K, V> MultiValueMap<K, V> fromMultiValue(Map<K, List<V>> map) {
141+
Assert.notNull(map, "Map must not be null");
142+
return new MultiValueMapAdapter<>(map);
143+
}
144+
96145
}

0 commit comments

Comments
 (0)