From 3897ea78bb007b58d148a4efb8e37119aba89ff4 Mon Sep 17 00:00:00 2001 From: Arjen Poutsma Date: Thu, 2 May 2024 10:48:48 +0200 Subject: [PATCH] Resolve collisions in composite collections Before this commit, creating a CompositeMap from two maps with the same key has strange results, such as entrySet returning duplicate entries with the same key. After this commit, we give precedence to the first map by filtering out all entries in the second map that are also mapped by the first map. See gh-32245 --- .../springframework/util/CollectionUtils.java | 8 ++ .../springframework/util/CompositeMap.java | 2 +- .../util/FilteredCollection.java | 90 +++++++++++++ .../util/FilteredIterator.java | 86 ++++++++++++ .../org/springframework/util/FilteredMap.java | 124 ++++++++++++++++++ .../org/springframework/util/FilteredSet.java | 66 ++++++++++ .../util/CompositeMapTests.java | 82 ++++++++++++ .../util/FilteredCollectionTests.java | 77 +++++++++++ .../util/FilteredIteratorTests.java | 38 ++++++ .../util/FilteredMapTests.java | 103 +++++++++++++++ .../util/FilteredSetTests.java | 42 ++++++ 11 files changed, 717 insertions(+), 1 deletion(-) create mode 100644 spring-core/src/main/java/org/springframework/util/FilteredCollection.java create mode 100644 spring-core/src/main/java/org/springframework/util/FilteredIterator.java create mode 100644 spring-core/src/main/java/org/springframework/util/FilteredMap.java create mode 100644 spring-core/src/main/java/org/springframework/util/FilteredSet.java create mode 100644 spring-core/src/test/java/org/springframework/util/FilteredCollectionTests.java create mode 100644 spring-core/src/test/java/org/springframework/util/FilteredIteratorTests.java create mode 100644 spring-core/src/test/java/org/springframework/util/FilteredMapTests.java create mode 100644 spring-core/src/test/java/org/springframework/util/FilteredSetTests.java 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 efc2d087c14..4d2db242408 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 3a1ff07b07e..0e6a01bd74b 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 00000000000..a9ae6d8d807 --- /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 00000000000..17a7caac4ad --- /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 00000000000..91254695c21 --- /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 00000000000..97c542c0197 --- /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 a32dad84465..a7b337f8d17 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 00000000000..79cfb2fc979 --- /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 00000000000..2f8eef35bdf --- /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 00000000000..135313b4cdc --- /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 00000000000..e5f39c71724 --- /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(); + } +}