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
This commit is contained in:
Arjen Poutsma
2024-05-21 15:51:31 +02:00
parent 97647ef5f6
commit 903493e9a9
12 changed files with 978 additions and 8 deletions

View File

@@ -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;
}
}
}

View File

@@ -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>> {
/**
* 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);
}
}

View File

@@ -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();
}
}

View File

@@ -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
return this.delegate.toSingleValueMap();
}
@Override
public Map<K, V> asSingleValueMap() {
return this.delegate.asSingleValueMap();
}
@Override
public boolean equals(@Nullable Object other) {