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 efc2d087c1..4d2db24240 100644 --- a/spring-core/src/main/java/org/springframework/util/CollectionUtils.java +++ b/spring-core/src/main/java/org/springframework/util/CollectionUtils.java @@ -515,6 +515,10 @@ public abstract class CollectionUtils { * 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}. + * + *

In the case of a key collision, {@code first} takes precedence over + * {@code second}. In other words, entries in {@code second} with a key + * that is also mapped by {@code first} are effectively ignored. * @param first the first map to compose * @param second the second map to compose * @return a new map that composes the given two maps @@ -531,6 +535,10 @@ public abstract class CollectionUtils { * {@link UnsupportedOperationException} {@code putFunction} is * {@code null}. The same applies to {@link Map#putAll(Map)} and * {@code putAllFunction}. + * + *

In the case of a key collision, {@code first} takes precedence over + * {@code second}. In other words, entries in {@code second} with a key + * that is also mapped by {@code first} are effectively ignored. * @param first the first map to compose * @param second the second map to compose * @param putFunction applied when {@code Map::put} is invoked. If diff --git a/spring-core/src/main/java/org/springframework/util/CompositeMap.java b/spring-core/src/main/java/org/springframework/util/CompositeMap.java index 3a1ff07b07..0e6a01bd74 100644 --- a/spring-core/src/main/java/org/springframework/util/CompositeMap.java +++ b/spring-core/src/main/java/org/springframework/util/CompositeMap.java @@ -58,7 +58,7 @@ final class CompositeMap implements Map { Assert.notNull(first, "First must not be null"); Assert.notNull(second, "Second must not be null"); this.first = first; - this.second = second; + this.second = new FilteredMap<>(second, key -> !this.first.containsKey(key)); this.putFunction = putFunction; this.putAllFunction = putAllFunction; } diff --git a/spring-core/src/main/java/org/springframework/util/FilteredCollection.java b/spring-core/src/main/java/org/springframework/util/FilteredCollection.java new file mode 100644 index 0000000000..a9ae6d8d80 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/util/FilteredCollection.java @@ -0,0 +1,90 @@ +/* + * 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.AbstractCollection; +import java.util.Collection; +import java.util.Iterator; +import java.util.function.Predicate; + +/** + * Collection that filters out values that do not match a predicate. + * This type is used by {@link CompositeMap}. + * @author Arjen Poutsma + * @since 6.2 + * @param the type of elements maintained by this collection + */ +class FilteredCollection extends AbstractCollection { + + private final Collection delegate; + + private final Predicate filter; + + + public FilteredCollection(Collection delegate, Predicate filter) { + Assert.notNull(delegate, "Delegate must not be null"); + Assert.notNull(filter, "Filter must not be null"); + + this.delegate = delegate; + this.filter = filter; + } + + @Override + public int size() { + int size = 0; + for (E e : this.delegate) { + if (this.filter.test(e)) { + size++; + } + } + return size; + } + + @Override + public Iterator iterator() { + return new FilteredIterator<>(this.delegate.iterator(), this.filter); + } + + @Override + public boolean add(E e) { + boolean added = this.delegate.add(e); + return added && this.filter.test(e); + } + + @Override + @SuppressWarnings("unchecked") + public boolean remove(Object o) { + boolean removed = this.delegate.remove(o); + return removed && this.filter.test((E) o); + } + + @Override + @SuppressWarnings("unchecked") + public boolean contains(Object o) { + if (this.delegate.contains(o)) { + return this.filter.test((E) o); + } + else { + return false; + } + } + + @Override + public void clear() { + this.delegate.clear(); + } +} diff --git a/spring-core/src/main/java/org/springframework/util/FilteredIterator.java b/spring-core/src/main/java/org/springframework/util/FilteredIterator.java new file mode 100644 index 0000000000..17a7caac4a --- /dev/null +++ b/spring-core/src/main/java/org/springframework/util/FilteredIterator.java @@ -0,0 +1,86 @@ +/* + * 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.Iterator; +import java.util.NoSuchElementException; +import java.util.function.Predicate; + +import org.springframework.lang.Nullable; + +/** + * Iterator that filters out values that do not match a predicate. + * This type is used by {@link CompositeMap}. + * @author Arjen Poutsma + * @since 6.2 + * @param the type of elements returned by this iterator + */ +final class FilteredIterator implements Iterator { + + private final Iterator delegate; + + private final Predicate filter; + + @Nullable + private E next; + + private boolean nextSet; + + + public FilteredIterator(Iterator delegate, Predicate filter) { + Assert.notNull(delegate, "Delegate must not be null"); + Assert.notNull(filter, "Filter must not be null"); + + this.delegate = delegate; + this.filter = filter; + } + + + @Override + public boolean hasNext() { + if (this.nextSet) { + return true; + } + else { + return setNext(); + } + } + + @Override + public E next() { + if (!this.nextSet) { + if (!setNext()) { + throw new NoSuchElementException(); + } + } + this.nextSet = false; + Assert.state(this.next != null, "Next should not be null"); + return this.next; + } + + private boolean setNext() { + while (this.delegate.hasNext()) { + E next = this.delegate.next(); + if (this.filter.test(next)) { + this.next = next; + this.nextSet = true; + return true; + } + } + return false; + } +} diff --git a/spring-core/src/main/java/org/springframework/util/FilteredMap.java b/spring-core/src/main/java/org/springframework/util/FilteredMap.java new file mode 100644 index 0000000000..91254695c2 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/util/FilteredMap.java @@ -0,0 +1,124 @@ +/* + * 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.AbstractMap; +import java.util.Map; +import java.util.Set; +import java.util.function.Predicate; + +import org.springframework.lang.Nullable; + +/** + * Map that filters out values that do not match a predicate. + * This type is used by {@link CompositeMap}. + * @author Arjen Poutsma + * @since 6.2 + * @param the type of keys maintained by this map + * @param the type of mapped values + */ +final class FilteredMap extends AbstractMap { + + private final Map delegate; + + private final Predicate filter; + + + public FilteredMap(Map delegate, Predicate filter) { + Assert.notNull(delegate, "Delegate must not be null"); + Assert.notNull(filter, "Filter must not be null"); + + this.delegate = delegate; + this.filter = filter; + } + + @Override + public Set> entrySet() { + return new FilteredSet<>(this.delegate.entrySet(), entry -> this.filter.test(entry.getKey())); + } + + @Override + public int size() { + int size = 0; + for (K k : keySet()) { + if (this.filter.test(k)) { + size++; + } + } + return size; + } + + @Override + @SuppressWarnings("unchecked") + public boolean containsKey(Object key) { + if (this.delegate.containsKey(key)) { + return this.filter.test((K) key); + } + else { + return false; + } + } + + @Override + @SuppressWarnings("unchecked") + @Nullable + public V get(Object key) { + V value = this.delegate.get(key); + if (value != null && this.filter.test((K) key)) { + return value; + } + else { + return null; + } + } + + @Override + @Nullable + public V put(K key, V value) { + V oldValue = this.delegate.put(key, value); + if (oldValue != null && this.filter.test(key)) { + return oldValue; + } + else { + return null; + } + } + + @Override + @SuppressWarnings("unchecked") + @Nullable + public V remove(Object key) { + V oldValue = this.delegate.remove(key); + if (oldValue != null && this.filter.test((K) key)) { + return oldValue; + } + else { + return null; + } + } + + @Override + public void clear() { + this.delegate.clear(); + } + + @Override + public Set keySet() { + return new FilteredSet<>(this.delegate.keySet(), this.filter); + } + +} diff --git a/spring-core/src/main/java/org/springframework/util/FilteredSet.java b/spring-core/src/main/java/org/springframework/util/FilteredSet.java new file mode 100644 index 0000000000..97c542c019 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/util/FilteredSet.java @@ -0,0 +1,66 @@ +/* + * 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; +import java.util.function.Predicate; + +/** + * Set that filters out values that do not match a predicate. + * This type is used by {@link CompositeMap}. + * @author Arjen Poutsma + * @since 6.2 + * @param the type of elements maintained by this set + */ +final class FilteredSet extends FilteredCollection implements Set { + + public FilteredSet(Set delegate, Predicate filter) { + super(delegate, filter); + } + + @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/CompositeMapTests.java b/spring-core/src/test/java/org/springframework/util/CompositeMapTests.java index a32dad8446..a7b337f8d1 100644 --- a/spring-core/src/test/java/org/springframework/util/CompositeMapTests.java +++ b/spring-core/src/test/java/org/springframework/util/CompositeMapTests.java @@ -23,6 +23,7 @@ import java.util.Map; import java.util.Set; import java.util.concurrent.atomic.AtomicBoolean; +import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; @@ -218,4 +219,85 @@ class CompositeMapTests { Set> entries = composite.entrySet(); assertThat(entries).containsExactly(entry("foo", "bar"), entry("baz", "qux")); } + + @Nested + class CollisionTests { + + @Test + void size() { + Map first = Map.of("foo", "bar", "baz", "qux"); + Map second = Map.of("baz", "quux", "corge", "grault"); + CompositeMap composite = new CompositeMap<>(first, second); + + assertThat(composite).hasSize(3); + } + + @Test + void containsValue() { + Map first = Map.of("foo", "bar", "baz", "qux"); + Map second = Map.of("baz", "quux", "corge", "grault"); + CompositeMap composite = new CompositeMap<>(first, second); + + assertThat(composite.containsValue("bar")).isTrue(); + assertThat(composite.containsValue("qux")).isTrue(); + assertThat(composite.containsValue("quux")).isFalse(); + assertThat(composite.containsValue("grault")).isTrue(); + } + + @Test + void get() { + Map first = Map.of("foo", "bar", "baz", "qux"); + Map second = Map.of("baz", "quux", "corge", "grault"); + CompositeMap composite = new CompositeMap<>(first, second); + + assertThat(composite.get("foo")).isEqualTo("bar"); + assertThat(composite.get("baz")).isEqualTo("qux"); + assertThat(composite.get("corge")).isEqualTo("grault"); + } + + @Test + void remove() { + Map first = new HashMap<>(Map.of("foo", "bar", "baz", "qux")); + Map second = new HashMap<>(Map.of("baz", "quux", "corge", "grault")); + CompositeMap composite = new CompositeMap<>(first, second); + + assertThat(composite.remove("baz")).isEqualTo("qux"); + assertThat(composite.containsKey("baz")).isFalse(); + assertThat(first).containsExactly(entry("foo", "bar")); + assertThat(second).containsExactly(entry("corge", "grault")); + } + + @Test + void keySet() { + Map first = Map.of("foo", "bar", "baz", "qux"); + Map second = Map.of("baz", "quux", "corge", "grault"); + CompositeMap composite = new CompositeMap<>(first, second); + + Set keySet = composite.keySet(); + assertThat(keySet).containsExactlyInAnyOrder("foo", "baz", "corge"); + } + + + @Test + void values() { + Map first = Map.of("foo", "bar", "baz", "qux"); + Map second = Map.of("baz", "quux", "corge", "grault"); + CompositeMap composite = new CompositeMap<>(first, second); + + Collection values = composite.values(); + assertThat(values).containsExactlyInAnyOrder("bar", "qux", "grault"); + } + + @Test + void entrySet() { + Map first = Map.of("foo", "bar", "baz", "qux"); + Map second = Map.of("baz", "quux", "corge", "grault"); + CompositeMap composite = new CompositeMap<>(first, second); + + Set> entries = composite.entrySet(); + assertThat(entries).containsExactlyInAnyOrder(entry("foo", "bar"), entry("baz", "qux"), entry("corge", "grault")); + } + + + } } diff --git a/spring-core/src/test/java/org/springframework/util/FilteredCollectionTests.java b/spring-core/src/test/java/org/springframework/util/FilteredCollectionTests.java new file mode 100644 index 0000000000..79cfb2fc97 --- /dev/null +++ b/spring-core/src/test/java/org/springframework/util/FilteredCollectionTests.java @@ -0,0 +1,77 @@ +/* + * 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.List; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Arjen Poutsma + */ +class FilteredCollectionTests { + + @Test + void size() { + List list = List.of("foo", "bar", "baz"); + FilteredCollection filtered = new FilteredCollection<>(list, s -> !s.equals("bar")); + + assertThat(filtered).hasSize(2); + } + + @Test + void iterator() { + List list = List.of("foo", "bar", "baz"); + FilteredCollection filtered = new FilteredCollection<>(list, s -> !s.equals("bar")); + + assertThat(filtered.iterator()).toIterable().containsExactly("foo", "baz"); + } + + @Test + void add() { + List list = new ArrayList<>(List.of("foo")); + FilteredCollection filtered = new FilteredCollection<>(list, s -> !s.equals("bar")); + boolean added = filtered.add("bar"); + assertThat(added).isFalse(); + assertThat(filtered).containsExactly("foo"); + assertThat(list).containsExactly("foo", "bar"); + } + + @Test + void remove() { + List list = new ArrayList<>(List.of("foo", "bar")); + FilteredCollection filtered = new FilteredCollection<>(list, s -> !s.equals("bar")); + assertThat(list).contains("bar"); + assertThat(filtered).doesNotContain("bar"); + boolean removed = filtered.remove("bar"); + assertThat(removed).isFalse(); + assertThat(filtered).doesNotContain("bar"); + assertThat(list).doesNotContain("bar"); + } + + @Test + void contains() { + List list = List.of("foo", "bar", "baz"); + FilteredCollection filtered = new FilteredCollection<>(list, s -> !s.equals("bar")); + boolean contained = filtered.contains("bar"); + assertThat(contained).isFalse(); + } + +} diff --git a/spring-core/src/test/java/org/springframework/util/FilteredIteratorTests.java b/spring-core/src/test/java/org/springframework/util/FilteredIteratorTests.java new file mode 100644 index 0000000000..2f8eef35bd --- /dev/null +++ b/spring-core/src/test/java/org/springframework/util/FilteredIteratorTests.java @@ -0,0 +1,38 @@ +/* + * 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.List; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Arjen Poutsma + */ +final class FilteredIteratorTests { + + @Test + void filter() { + List list = List.of("foo", "bar", "baz"); + FilteredIterator filtered = new FilteredIterator<>(list.iterator(), s -> !s.equals("bar")); + + assertThat(filtered).toIterable().containsExactly("foo", "baz"); + } + +} diff --git a/spring-core/src/test/java/org/springframework/util/FilteredMapTests.java b/spring-core/src/test/java/org/springframework/util/FilteredMapTests.java new file mode 100644 index 0000000000..135313b4cd --- /dev/null +++ b/spring-core/src/test/java/org/springframework/util/FilteredMapTests.java @@ -0,0 +1,103 @@ +/* + * 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.HashMap; +import java.util.Map; +import java.util.Set; + +import org.junit.jupiter.api.Test; + +import static java.util.Map.entry; +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Arjen Poutsma + */ +class FilteredMapTests { + + @Test + void size() { + Map map = Map.of("foo", "bar", "baz", "qux", "quux", "corge"); + FilteredMap filtered = new FilteredMap<>(map, s -> !s.equals("baz")); + + assertThat(filtered).hasSize(2); + } + + @Test + void entrySet() { + Map map = Map.of("foo", "bar", "baz", "qux", "quux", "corge"); + FilteredMap filtered = new FilteredMap<>(map, s -> !s.equals("baz")); + + assertThat(filtered.entrySet()).containsExactlyInAnyOrder(entry("foo", "bar"), entry("quux", "corge")); + } + + @Test + void containsKey() { + Map map = Map.of("foo", "bar", "baz", "qux", "quux", "corge"); + FilteredMap filtered = new FilteredMap<>(map, s -> !s.equals("baz")); + + boolean contained = filtered.containsKey("baz"); + assertThat(contained).isFalse(); + } + + @Test + void get() { + Map map = Map.of("foo", "bar", "baz", "qux", "quux", "corge"); + FilteredMap filtered = new FilteredMap<>(map, s -> !s.equals("baz")); + + String value = filtered.get("baz"); + assertThat(value).isNull(); + } + + @Test + void put() { + Map map = new HashMap<>(Map.of("foo", "bar", "quux", "corge")); + FilteredMap filtered = new FilteredMap<>(map, s -> !s.equals("baz")); + + String value = filtered.put("baz", "qux"); + assertThat(value).isNull(); + assertThat(filtered.containsKey("baz")).isFalse(); + assertThat(map.get("baz")).isEqualTo("qux"); + + // overwrite + value = filtered.put("baz", "QUX"); + assertThat(value).isNull(); + assertThat(filtered.containsKey("baz")).isFalse(); + assertThat(map.get("baz")).isEqualTo("QUX"); + } + + @Test + void remove() { + Map map = new HashMap<>(Map.of("foo", "bar", "baz", "qux", "quux", "corge")); + FilteredMap filtered = new FilteredMap<>(map, s -> !s.equals("baz")); + + String value = filtered.remove("baz"); + assertThat(value).isNull(); + assertThat(filtered.containsKey("baz")).isFalse(); + assertThat(map.containsKey("baz")).isFalse(); + } + + @Test + void keySet() { + Map map = Map.of("foo", "bar", "baz", "qux", "quux", "corge"); + FilteredMap filtered = new FilteredMap<>(map, s -> !s.equals("baz")); + + Set keySet = filtered.keySet(); + assertThat(keySet).containsExactlyInAnyOrder("foo", "quux"); + } +} diff --git a/spring-core/src/test/java/org/springframework/util/FilteredSetTests.java b/spring-core/src/test/java/org/springframework/util/FilteredSetTests.java new file mode 100644 index 0000000000..e5f39c7172 --- /dev/null +++ b/spring-core/src/test/java/org/springframework/util/FilteredSetTests.java @@ -0,0 +1,42 @@ +/* + * 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.Set; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Arjen Poutsma + */ +class FilteredSetTests { + + @Test + void testEquals() { + Set set = Set.of("foo", "bar", "baz"); + FilteredSet filtered = new FilteredSet<>(set, s -> !s.equals("bar")); + + Set expected = Set.of("foo", "baz"); + + assertThat(filtered.equals(expected)).isTrue(); + assertThat(filtered.equals(set)).isFalse(); + assertThat(filtered.equals(Collections.emptySet())).isFalse(); + } +}