diff --git a/spring-core/src/main/java/org/springframework/util/CollectionUtils.java b/spring-core/src/main/java/org/springframework/util/CollectionUtils.java index a6eee8fe8757..4127d1adf178 100644 --- a/spring-core/src/main/java/org/springframework/util/CollectionUtils.java +++ b/spring-core/src/main/java/org/springframework/util/CollectionUtils.java @@ -31,6 +31,8 @@ import java.util.Properties; import java.util.Set; import java.util.SortedSet; +import java.util.function.BiFunction; +import java.util.function.Consumer; import org.springframework.lang.Nullable; @@ -506,4 +508,42 @@ public static MultiValueMap unmodifiableMultiValueMap( return new UnmodifiableMultiValueMap<>(targetMap); } + /** + * Return a (partially unmodifiable) map that combines the provided two + * maps. Invoking {@link Map#put(Object, Object)} or {@link Map#putAll(Map)} + * on the returned map results in an {@link UnsupportedOperationException}. + * @param first the first map to compose + * @param second the second map to compose + * @return a new map that composes the given two maps + * @since 6.2 + */ + public static Map compositeMap(Map first, Map second) { + return new CompositeMap<>(first, second); + } + + /** + * Return a map that combines the provided maps. Invoking + * {@link Map#put(Object, Object)} on the returned map will apply + * {@code putFunction}, or will throw an + * {@link UnsupportedOperationException} {@code putFunction} is + * {@code null}. The same applies to {@link Map#putAll(Map)} and + * {@code putAllFunction}. + * @param first the first map to compose + * @param second the second map to compose + * @param putFunction applied when {@code Map::put} is invoked. If + * {@code null}, {@code Map::put} throws an + * {@code UnsupportedOperationException}. + * @param putAllFunction applied when {@code Map::putAll} is invoked. If + * {@code null}, {@code Map::putAll} throws an + * {@code UnsupportedOperationException}. + * @return a new map that composes the give maps + * @since 6.2 + */ + public static Map compositeMap(Map first, Map second, + @Nullable BiFunction putFunction, + @Nullable Consumer> putAllFunction) { + + return new CompositeMap<>(first, second, putFunction, putAllFunction); + } + } diff --git a/spring-core/src/main/java/org/springframework/util/CompositeCollection.java b/spring-core/src/main/java/org/springframework/util/CompositeCollection.java new file mode 100644 index 000000000000..724df89ff413 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/util/CompositeCollection.java @@ -0,0 +1,163 @@ +/* + * 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.lang.reflect.Array; +import java.util.Collection; +import java.util.Iterator; + + +/** + * Composite collection that combines two other collections. This type is only + * exposed through {@link CompositeMap#values()}. + * + * @author Arjen Poutsma + * @since 6.2 + * @param the type of elements maintained by this collection + */ +class CompositeCollection implements Collection { + + private final Collection first; + + private final Collection second; + + + CompositeCollection(Collection first, Collection second) { + Assert.notNull(first, "First must not be null"); + Assert.notNull(second, "Second must not be null"); + this.first = first; + this.second = second; + } + + @Override + public int size() { + return this.first.size() + this.second.size(); + } + + @Override + public boolean isEmpty() { + return this.first.isEmpty() && this.second.isEmpty(); + } + + @Override + public boolean contains(Object o) { + if (this.first.contains(o)) { + return true; + } + else { + return this.second.contains(o); + } + } + + @Override + public Iterator iterator() { + CompositeIterator iterator = new CompositeIterator<>(); + iterator.add(this.first.iterator()); + iterator.add(this.second.iterator()); + return iterator; + } + + @Override + public Object[] toArray() { + Object[] result = new Object[size()]; + Object[] firstArray = this.first.toArray(); + Object[] secondArray = this.second.toArray(); + System.arraycopy(firstArray, 0, result, 0, firstArray.length); + System.arraycopy(secondArray, 0, result, firstArray.length, secondArray.length); + return result; + } + + @Override + @SuppressWarnings("unchecked") + public T[] toArray(T[] a) { + int size = this.size(); + T[] result; + if (a.length >= size) { + result = a; + } + else { + result = (T[]) Array.newInstance(a.getClass().getComponentType(), size); + } + + int idx = 0; + for (E e : this) { + result[idx++] = (T) e; + } + if (result.length > size) { + result[size] = null; + } + return result; + } + + @Override + public boolean add(E e) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean remove(Object o) { + boolean firstResult = this.first.remove(o); + boolean secondResult = this.second.remove(o); + return firstResult || secondResult; + } + + @Override + public boolean containsAll(Collection c) { + for (Object o : c) { + if (!contains(o)) { + return false; + } + } + return true; + } + + @Override + public boolean addAll(Collection c) { + boolean changed = false; + for (E e : c) { + if (add(e)) { + changed = true; + } + } + return changed; + } + + @Override + public boolean removeAll(Collection c) { + if (c.isEmpty()) { + return false; + } + boolean firstResult = this.first.removeAll(c); + boolean secondResult = this.second.removeAll(c); + + return firstResult || secondResult; + } + + @Override + public boolean retainAll(Collection c) { + boolean firstResult = this.first.retainAll(c); + boolean secondResult = this.second.retainAll(c); + + return firstResult || secondResult; + } + + @Override + public void clear() { + this.first.clear(); + this.second.clear(); + } +} diff --git a/spring-core/src/main/java/org/springframework/util/CompositeMap.java b/spring-core/src/main/java/org/springframework/util/CompositeMap.java new file mode 100644 index 000000000000..3a1ff07b07e3 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/util/CompositeMap.java @@ -0,0 +1,189 @@ +/* + * 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.Collection; +import java.util.Iterator; +import java.util.Map; +import java.util.Set; +import java.util.function.BiFunction; +import java.util.function.Consumer; + +import org.springframework.lang.Nullable; + +/** + * Composite map that combines two other maps. This type is created via + * {@link CollectionUtils#compositeMap(Map, Map, BiFunction, Consumer)}. + * + * @author Arjen Poutsma + * @since 6.2 + * @param the type of keys maintained by this map + * @param the type of mapped values + */ +final class CompositeMap implements Map { + + private final Map first; + + private final Map second; + + @Nullable + private final BiFunction putFunction; + + @Nullable + private final Consumer> putAllFunction; + + + CompositeMap(Map first, Map second) { + this(first, second, null, null); + } + + CompositeMap(Map first, Map second, + @Nullable BiFunction putFunction, + @Nullable Consumer> putAllFunction) { + + Assert.notNull(first, "First must not be null"); + Assert.notNull(second, "Second must not be null"); + this.first = first; + this.second = second; + this.putFunction = putFunction; + this.putAllFunction = putAllFunction; + } + + + @Override + public int size() { + return this.first.size() + this.second.size(); + } + + @Override + public boolean isEmpty() { + return this.first.isEmpty() && this.second.isEmpty(); + } + + @Override + public boolean containsKey(Object key) { + if (this.first.containsKey(key)) { + return true; + } + else { + return this.second.containsKey(key); + } + } + + @Override + public boolean containsValue(Object value) { + if (this.first.containsValue(value)) { + return true; + } + else { + return this.second.containsValue(value); + } + } + + @Override + @Nullable + public V get(Object key) { + V firstResult = this.first.get(key); + if (firstResult != null) { + return firstResult; + } + else { + return this.second.get(key); + } + } + + @Override + @Nullable + public V put(K key, V value) { + if (this.putFunction == null) { + throw new UnsupportedOperationException(); + } + else { + return this.putFunction.apply(key, value); + } + } + + @Override + @Nullable + public V remove(Object key) { + V firstResult = this.first.remove(key); + V secondResult = this.second.remove(key); + if (firstResult != null) { + return firstResult; + } + else { + return secondResult; + } + } + + @Override + @SuppressWarnings("unchecked") + public void putAll(Map m) { + if (this.putAllFunction != null) { + this.putAllFunction.accept((Map) m); + } + else { + for (Map.Entry e : m.entrySet()) { + put(e.getKey(), e.getValue()); + } + } + } + + @Override + public void clear() { + this.first.clear(); + this.second.clear(); + } + + @Override + public Set keySet() { + return new CompositeSet<>(this.first.keySet(), this.second.keySet()); + } + + @Override + public Collection values() { + return new CompositeCollection<>(this.first.values(), this.second.values()); + } + + @Override + public Set> entrySet() { + return new CompositeSet<>(this.first.entrySet(), this.second.entrySet()); + } + + @Override + public String toString() { + Iterator> i = entrySet().iterator(); + if (!i.hasNext()) { + return "{}"; + } + + StringBuilder sb = new StringBuilder(); + sb.append('{'); + while (true) { + Entry e = i.next(); + K key = e.getKey(); + V value = e.getValue(); + sb.append(key == this ? "(this Map)" : key); + sb.append('='); + sb.append(value == this ? "(this Map)" : value); + if (!i.hasNext()) { + return sb.append('}').toString(); + } + sb.append(',').append(' '); + } + } +} diff --git a/spring-core/src/main/java/org/springframework/util/CompositeSet.java b/spring-core/src/main/java/org/springframework/util/CompositeSet.java new file mode 100644 index 000000000000..5bee6e65bd26 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/util/CompositeSet.java @@ -0,0 +1,67 @@ +/* + * 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.Set; + +/** + * Composite set that combines two other sets. This type is only exposed through + * {@link CompositeMap#keySet()} and {@link CompositeMap#entrySet()}. + * + * @author Arjen Poutsma + * @since 6.2 + * @param the type of elements maintained by this set + */ +final class CompositeSet extends CompositeCollection implements Set { + + CompositeSet(Set first, Set second) { + super(first, second); + } + + + @Override + public boolean equals(Object obj) { + if (obj == this) { + return true; + } + else if (obj instanceof Set set) { + if (set.size() != size()) { + return false; + } + try { + return containsAll(set); + } + catch (ClassCastException | NullPointerException ignored) { + return false; + } + } + else { + return false; + } + } + + @Override + public int hashCode() { + int hashCode = 0; + for (E obj : this) { + if (obj != null) { + hashCode += obj.hashCode(); + } + } + return hashCode; + } +} diff --git a/spring-core/src/test/java/org/springframework/util/CompositeCollectionTests.java b/spring-core/src/test/java/org/springframework/util/CompositeCollectionTests.java new file mode 100644 index 000000000000..b299c5c36d7d --- /dev/null +++ b/spring-core/src/test/java/org/springframework/util/CompositeCollectionTests.java @@ -0,0 +1,195 @@ +/* + * 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.ArrayList; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +/** + * @author Arjen Poutsma + */ +class CompositeCollectionTests { + + @Test + void size() { + List first = List.of("foo", "bar", "baz"); + List second = List.of("qux", "quux"); + CompositeCollection composite = new CompositeCollection<>(first, second); + + assertThat(composite).hasSize(5); + } + + @Test + void isEmpty() { + List first = List.of("foo", "bar", "baz"); + List second = List.of("qux", "quux"); + CompositeCollection composite = new CompositeCollection<>(first, second); + + assertThat(composite).isNotEmpty(); + + composite = new CompositeCollection<>(Collections.emptyList(), Collections.emptyList()); + assertThat(composite).isEmpty(); + } + + @Test + void contains() { + List first = List.of("foo", "bar"); + List second = List.of("baz", "qux"); + CompositeCollection composite = new CompositeCollection<>(first, second); + + assertThat(composite.contains("foo")).isTrue(); + assertThat(composite.contains("bar")).isTrue(); + assertThat(composite.contains("baz")).isTrue(); + assertThat(composite.contains("qux")).isTrue(); + assertThat(composite.contains("quux")).isFalse(); + } + + @Test + void iterator() { + List first = List.of("foo", "bar"); + List second = List.of("baz", "qux"); + CompositeCollection composite = new CompositeCollection<>(first, second); + + Iterator iterator = composite.iterator(); + assertThat(iterator).hasNext(); + assertThat(iterator.next()).isEqualTo("foo"); + assertThat(iterator).hasNext(); + assertThat(iterator.next()).isEqualTo("bar"); + assertThat(iterator).hasNext(); + assertThat(iterator.next()).isEqualTo("baz"); + assertThat(iterator).hasNext(); + assertThat(iterator.next()).isEqualTo("qux"); + assertThat(iterator).isExhausted(); + } + + @Test + void toArray() { + List first = List.of("foo", "bar"); + List second = List.of("baz", "qux"); + CompositeCollection composite = new CompositeCollection<>(first, second); + + Object[] array = composite.toArray(); + assertThat(array).containsExactly("foo", "bar", "baz", "qux"); + } + + @Test + void toArrayArgs() { + List first = List.of("foo", "bar"); + List second = List.of("baz", "qux"); + CompositeCollection composite = new CompositeCollection<>(first, second); + + String[] array = new String[composite.size()]; + array = composite.toArray(array); + assertThat(array).containsExactly("foo", "bar", "baz", "qux"); + } + + @Test + void add() { + List first = List.of("foo", "bar"); + List second = List.of("baz", "qux"); + CompositeCollection composite = new CompositeCollection<>(first, second); + + assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(() -> composite.add("quux")); + } + + @Test + void remove() { + List first = new ArrayList<>(List.of("foo", "bar")); + List second = new ArrayList<>(List.of("baz", "qux")); + CompositeCollection composite = new CompositeCollection<>(first, second); + + assertThat(composite.remove("foo")).isTrue(); + assertThat(composite.contains("foo")).isFalse(); + assertThat(first).containsExactly("bar"); + + assertThat(composite.remove("quux")).isFalse(); + } + + @Test + void containsAll() { + List first = List.of("foo", "bar"); + List second = List.of("baz", "qux"); + CompositeCollection composite = new CompositeCollection<>(first, second); + + List all = new ArrayList<>(first); + all.addAll(second); + + assertThat(composite.containsAll(all)).isTrue(); + + all.add("quux"); + + assertThat(composite.containsAll(all)).isFalse(); + } + + @Test + void addAll() { + List first = List.of("foo", "bar"); + List second = List.of("baz", "qux"); + CompositeCollection composite = new CompositeCollection<>(first, second); + + assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(() -> composite.addAll(List.of("quux", "corge"))); + } + + @Test + void removeAll() { + List first = new ArrayList<>(List.of("foo", "bar")); + List second = new ArrayList<>(List.of("baz", "qux")); + CompositeCollection composite = new CompositeCollection<>(first, second); + + List all = new ArrayList<>(first); + all.addAll(second); + + assertThat(composite.removeAll(all)).isTrue(); + + assertThat(composite).isEmpty(); + assertThat(first).isEmpty(); + assertThat(second).isEmpty(); + } + + @Test + void retainAll() { + List first = new ArrayList<>(List.of("foo", "bar")); + List second = new ArrayList<>(List.of("baz", "qux")); + CompositeCollection composite = new CompositeCollection<>(first, second); + + assertThat(composite.retainAll(List.of("bar", "baz"))).isTrue(); + + assertThat(composite).containsExactly("bar", "baz"); + assertThat(first).containsExactly("bar"); + assertThat(second).containsExactly("baz"); + } + + @Test + void clear() { + List first = new ArrayList<>(List.of("foo", "bar")); + List second = new ArrayList<>(List.of("baz", "qux")); + CompositeCollection composite = new CompositeCollection<>(first, second); + + composite.clear(); + + assertThat(composite).isEmpty(); + assertThat(first).isEmpty(); + assertThat(second).isEmpty(); + } +} diff --git a/spring-core/src/test/java/org/springframework/util/CompositeMapTests.java b/spring-core/src/test/java/org/springframework/util/CompositeMapTests.java new file mode 100644 index 000000000000..a32dad844656 --- /dev/null +++ b/spring-core/src/test/java/org/springframework/util/CompositeMapTests.java @@ -0,0 +1,221 @@ +/* + * 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.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.atomic.AtomicBoolean; + +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 CompositeMapTests { + + @Test + void size() { + Map first = Map.of("foo", "bar", "baz", "qux"); + Map second = Map.of("quux", "corge"); + CompositeMap composite = new CompositeMap<>(first, second); + + assertThat(composite).hasSize(3); + } + + @Test + void isEmpty() { + Map first = Map.of("foo", "bar", "baz", "qux"); + Map second = Map.of("quux", "corge"); + CompositeMap composite = new CompositeMap<>(first, second); + + assertThat(composite).isNotEmpty(); + + composite = new CompositeMap<>(Collections.emptyMap(), Collections.emptyMap()); + assertThat(composite).isEmpty(); + } + + @Test + void containsKey() { + Map first = Map.of("foo", "bar", "baz", "qux"); + Map second = Map.of("quux", "corge"); + CompositeMap composite = new CompositeMap<>(first, second); + + assertThat(composite.containsKey("foo")).isTrue(); + assertThat(composite.containsKey("bar")).isFalse(); + assertThat(composite.containsKey("baz")).isTrue(); + assertThat(composite.containsKey("qux")).isFalse(); + assertThat(composite.containsKey("quux")).isTrue(); + assertThat(composite.containsKey("corge")).isFalse(); + } + + @Test + void containsValue() { + Map first = Map.of("foo", "bar", "baz", "qux"); + Map second = Map.of("quux", "corge"); + CompositeMap composite = new CompositeMap<>(first, second); + + assertThat(composite.containsValue("foo")).isFalse(); + assertThat(composite.containsValue("bar")).isTrue(); + assertThat(composite.containsValue("baz")).isFalse(); + assertThat(composite.containsValue("qux")).isTrue(); + assertThat(composite.containsValue("quux")).isFalse(); + assertThat(composite.containsValue("corge")).isTrue(); + } + + @Test + void get() { + Map first = Map.of("foo", "bar", "baz", "qux"); + Map second = Map.of("quux", "corge"); + CompositeMap composite = new CompositeMap<>(first, second); + + assertThat(composite.get("foo")).isEqualTo("bar"); + assertThat(composite.get("baz")).isEqualTo("qux"); + assertThat(composite.get("quux")).isEqualTo("corge"); + + assertThat(composite.get("grault")).isNull(); + } + + @Test + void putUnsupported() { + Map first = Map.of("foo", "bar"); + Map second = Map.of("baz", "qux"); + CompositeMap composite = new CompositeMap<>(first, second); + + assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(() -> composite.put("grault", "garply")); + } + @Test + void putSupported() { + Map first = Map.of("foo", "bar"); + Map second = Map.of("baz", "qux"); + CompositeMap composite = new CompositeMap<>(first, second, (k,v) -> { + assertThat(k).isEqualTo("quux"); + assertThat(v).isEqualTo("corge"); + return "grault"; + }, null); + + assertThat(composite.put("quux", "corge")).isEqualTo("grault"); + } + + @Test + void remove() { + Map first = new HashMap<>(Map.of("foo", "bar", "baz", "qux")); + Map second = new HashMap<>(Map.of("quux", "corge")); + CompositeMap composite = new CompositeMap<>(first, second); + + assertThat(composite.remove("foo")).isEqualTo("bar"); + assertThat(composite.containsKey("foo")).isFalse(); + assertThat(first).containsExactly(entry("baz", "qux")); + + assertThat(composite.remove("grault")).isNull(); + } + + @Test + void putAllUnsupported() { + Map first = Map.of("foo", "bar"); + Map second = Map.of("baz", "qux"); + + CompositeMap composite = new CompositeMap<>(first, second); + + assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(() -> composite.putAll(Map.of("quux", "corge", "grault", "garply"))); + } + + @Test + void putAllPutFunction() { + Map first = Map.of("foo", "bar"); + Map second = Map.of("baz", "qux"); + + AtomicBoolean functionInvoked = new AtomicBoolean(); + CompositeMap composite = new CompositeMap<>(first, second, (k,v) -> { + assertThat(k).isEqualTo("quux"); + assertThat(v).isEqualTo("corge"); + functionInvoked.set(true); + return "grault"; + }, null); + + composite.putAll(Map.of("quux", "corge")); + + assertThat(functionInvoked).isTrue(); + } + + @Test + void putAllPutAllFunction() { + Map first = Map.of("foo", "bar"); + Map second = Map.of("baz", "qux"); + + AtomicBoolean functionInvoked = new AtomicBoolean(); + Map argument = Map.of("quux", "corge"); + CompositeMap composite = new CompositeMap<>(first, second, null, + m -> { + assertThat(m).isSameAs(argument); + functionInvoked.set(true); + }); + + composite.putAll(argument); + + assertThat(functionInvoked).isTrue(); + } + + @Test + void clear() { + Map first = new HashMap<>(Map.of("foo", "bar", "baz", "qux")); + Map second = new HashMap<>(Map.of("quux", "corge")); + CompositeMap composite = new CompositeMap<>(first, second); + + composite.clear(); + + assertThat(composite).isEmpty(); + assertThat(first).isEmpty(); + assertThat(second).isEmpty(); + } + + @Test + void keySet() { + Map first = Map.of("foo", "bar"); + Map second = Map.of("baz", "qux"); + CompositeMap composite = new CompositeMap<>(first, second); + + Set keySet = composite.keySet(); + assertThat(keySet).containsExactly("foo", "baz"); + } + + @Test + void values() { + Map first = Map.of("foo", "bar"); + Map second = Map.of("baz", "qux"); + CompositeMap composite = new CompositeMap<>(first, second); + + Collection values = composite.values(); + assertThat(values).containsExactly("bar", "qux"); + } + + @Test + void entrySet() { + Map first = Map.of("foo", "bar"); + Map second = Map.of("baz", "qux"); + CompositeMap composite = new CompositeMap<>(first, second); + + Set> entries = composite.entrySet(); + assertThat(entries).containsExactly(entry("foo", "bar"), entry("baz", "qux")); + } +} diff --git a/spring-core/src/test/java/org/springframework/util/CompositeSetTests.java b/spring-core/src/test/java/org/springframework/util/CompositeSetTests.java new file mode 100644 index 000000000000..f45b53f2d090 --- /dev/null +++ b/spring-core/src/test/java/org/springframework/util/CompositeSetTests.java @@ -0,0 +1,47 @@ +/* + * 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.HashSet; +import java.util.Set; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Arjen Poutsma + */ +class CompositeSetTests { + + @Test + void testEquals() { + Set first = Set.of("foo", "bar"); + Set second = Set.of("baz", "qux"); + CompositeSet composite = new CompositeSet<>(first, second); + + Set all = new HashSet<>(first); + all.addAll(second); + + assertThat(composite.equals(all)).isTrue(); + assertThat(composite.equals(first)).isFalse(); + assertThat(composite.equals(second)).isFalse(); + assertThat(composite.equals(Collections.emptySet())).isFalse(); + } + +} diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/RequestPredicates.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/RequestPredicates.java index 856f9e3f51c2..4599215700c9 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/RequestPredicates.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/server/RequestPredicates.java @@ -329,28 +329,6 @@ private static PathPattern mergePatterns(@Nullable PathPattern oldPattern, PathP } } - private static Map mergeMaps(Map left, Map right) { - if (left.isEmpty()) { - if (right.isEmpty()) { - return Collections.emptyMap(); - } - else { - return right; - } - } - else { - if (right.isEmpty()) { - return left; - } - else { - Map result = CollectionUtils.newLinkedHashMap(left.size() + right.size()); - result.putAll(left); - result.putAll(right); - return result; - } - } - } - /** * Receives notifications from the logical structure of request predicates. @@ -640,7 +618,7 @@ protected Result testInternal(ServerRequest request) { private void modifyAttributes(Map attributes, ServerRequest request, Map variables) { - Map pathVariables = mergeMaps(request.pathVariables(), variables); + Map pathVariables = CollectionUtils.compositeMap(request.pathVariables(), variables); attributes.put(RouterFunctions.URI_TEMPLATE_VARIABLES_ATTRIBUTE, Collections.unmodifiableMap(pathVariables)); @@ -1334,7 +1312,9 @@ private static class ExtendedAttributesServerRequestWrapper extends DelegatingSe public ExtendedAttributesServerRequestWrapper(ServerRequest delegate, Map newAttributes) { super(delegate); Assert.notNull(newAttributes, "NewAttributes must not be null"); - this.attributes = mergeMaps(delegate.attributes(), newAttributes); + Map oldAttributes = delegate.attributes(); + this.attributes = CollectionUtils.compositeMap(newAttributes, oldAttributes, newAttributes::put, + newAttributes::putAll); } @Override @@ -1383,12 +1363,21 @@ private static Map mergeAttributes(ServerRequest request, Map oldPathVariables = request.pathVariables(); + Map pathVariables; + if (oldPathVariables.isEmpty()) { + pathVariables = newPathVariables; + } + else { + pathVariables = CollectionUtils.compositeMap(oldPathVariables, newPathVariables); + } + PathPattern oldPathPattern = (PathPattern) request.attribute(RouterFunctions.MATCHING_PATTERN_ATTRIBUTE) .orElse(null); + PathPattern pathPattern = mergePatterns(oldPathPattern, newPathPattern); - Map result = new LinkedHashMap<>(2); - result.put(RouterFunctions.URI_TEMPLATE_VARIABLES_ATTRIBUTE, mergeMaps(oldPathVariables, newPathVariables)); - result.put(RouterFunctions.MATCHING_PATTERN_ATTRIBUTE, mergePatterns(oldPathPattern, newPathPattern)); + Map result = CollectionUtils.newLinkedHashMap(2); + result.put(RouterFunctions.URI_TEMPLATE_VARIABLES_ATTRIBUTE, pathVariables); + result.put(RouterFunctions.MATCHING_PATTERN_ATTRIBUTE, pathPattern); return result; } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/function/RequestPredicates.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/function/RequestPredicates.java index 5cd3e45a5945..ecd097495cfd 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/function/RequestPredicates.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/function/RequestPredicates.java @@ -327,28 +327,6 @@ private static PathPattern mergePatterns(@Nullable PathPattern oldPattern, PathP } } - private static Map mergeMaps(Map left, Map right) { - if (left.isEmpty()) { - if (right.isEmpty()) { - return Collections.emptyMap(); - } - else { - return right; - } - } - else { - if (right.isEmpty()) { - return left; - } - else { - Map result = CollectionUtils.newLinkedHashMap(left.size() + right.size()); - result.putAll(left); - result.putAll(right); - return result; - } - } - } - /** * Receives notifications from the logical structure of request predicates. @@ -638,7 +616,7 @@ protected Result testInternal(ServerRequest request) { private void modifyAttributes(Map attributes, ServerRequest request, Map variables) { - Map pathVariables = mergeMaps(request.pathVariables(), variables); + Map pathVariables = CollectionUtils.compositeMap(request.pathVariables(), variables); attributes.put(RouterFunctions.URI_TEMPLATE_VARIABLES_ATTRIBUTE, Collections.unmodifiableMap(pathVariables)); @@ -1302,7 +1280,9 @@ private static class ExtendedAttributesServerRequestWrapper extends DelegatingSe public ExtendedAttributesServerRequestWrapper(ServerRequest delegate, Map newAttributes) { super(delegate); Assert.notNull(newAttributes, "NewAttributes must not be null"); - this.attributes = mergeMaps(delegate.attributes(), newAttributes); + Map oldAttributes = delegate.attributes(); + this.attributes = CollectionUtils.compositeMap(newAttributes, oldAttributes, newAttributes::put, + newAttributes::putAll); } @Override @@ -1351,12 +1331,21 @@ private static Map mergeAttributes(ServerRequest request, Map oldPathVariables = request.pathVariables(); + Map pathVariables; + if (oldPathVariables.isEmpty()) { + pathVariables = newPathVariables; + } + else { + pathVariables = CollectionUtils.compositeMap(oldPathVariables, newPathVariables); + } + PathPattern oldPathPattern = (PathPattern) request.attribute(RouterFunctions.MATCHING_PATTERN_ATTRIBUTE) .orElse(null); + PathPattern pathPattern = mergePatterns(oldPathPattern, newPathPattern); Map result = CollectionUtils.newLinkedHashMap(2); - result.put(RouterFunctions.URI_TEMPLATE_VARIABLES_ATTRIBUTE, mergeMaps(oldPathVariables, newPathVariables)); - result.put(RouterFunctions.MATCHING_PATTERN_ATTRIBUTE, mergePatterns(oldPathPattern, newPathPattern)); + result.put(RouterFunctions.URI_TEMPLATE_VARIABLES_ATTRIBUTE, pathVariables); + result.put(RouterFunctions.MATCHING_PATTERN_ATTRIBUTE, pathPattern); return result; }