This is an automated email from the ASF dual-hosted git repository.

garydgregory pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/commons-collections.git


The following commit(s) were added to refs/heads/master by this push:
     new dbf599d62 do not mutate the input map in switch closure/transformer 
factories (#700)
dbf599d62 is described below

commit dbf599d62866a28f7d6ca7d054575b68beaaf55c
Author: Dexter.k <[email protected]>
AuthorDate: Thu Jul 2 12:21:09 2026 +0000

    do not mutate the input map in switch closure/transformer factories (#700)
    
    * do not mutate input map in switch closure/transformer factories
    
    * assert built switch result is valid in map-copy tests
    
    Signed-off-by: Naveed Khan <[email protected]>
    
    ---------
    
    Signed-off-by: Naveed Khan <[email protected]>
---
 src/changes/changes.xml                            |  1 +
 .../apache/commons/collections4/ClosureUtils.java  |  9 ++++---
 .../commons/collections4/TransformerUtils.java     |  9 ++++---
 .../collections4/functors/SwitchClosure.java       | 10 ++++----
 .../commons/collections4/ClosureUtilsTest.java     | 28 ++++++++++++++++++++++
 .../commons/collections4/TransformerUtilsTest.java | 12 ++++++++++
 .../functors/SwitchTransformerMapMutatesTest.java  |  5 +++-
 7 files changed, 63 insertions(+), 11 deletions(-)

diff --git a/src/changes/changes.xml b/src/changes/changes.xml
index eb1d1315d..ad12f961a 100644
--- a/src/changes/changes.xml
+++ b/src/changes/changes.xml
@@ -64,6 +64,7 @@
     <action type="fix" dev="ggregory" due-to="Vasiliy Mikhailov, Gary 
Gregory">Fix integer overflow in SetOperations.cosineSimilarity (#693).</action>
     <action type="fix" dev="ggregory" due-to="Vasiliy Mikhailov, Gary 
Gregory">Validate deserialized size in CircularFifoQueue.readObject 
(#678).</action>
     <action type="fix" dev="ggregory" due-to="Vasiliy Mikhailov, Gary 
Gregory">Do not mutate the input map in SwitchTransformer.switchTransformer 
(#696).</action>
+    <action type="fix" dev="ggregory" due-to="Naveed Khan, Gary Gregory">Do 
not mutate the input map in SwitchClosure.switchClosure, 
ClosureUtils.switchMapClosure and 
TransformerUtils.switchMapTransformer.</action>
     <!-- ADD -->
     <action type="add" dev="ggregory" due-to="Gary Gregory">Add generics to 
UnmodifiableIterator for the wrapped type.</action>
     <action type="add" dev="ggregory" due-to="Gary Gregory">Add a Maven 
benchmark profile for JMH.</action>
diff --git a/src/main/java/org/apache/commons/collections4/ClosureUtils.java 
b/src/main/java/org/apache/commons/collections4/ClosureUtils.java
index 4ad525c4d..b90584a57 100644
--- a/src/main/java/org/apache/commons/collections4/ClosureUtils.java
+++ b/src/main/java/org/apache/commons/collections4/ClosureUtils.java
@@ -17,6 +17,7 @@
 package org.apache.commons.collections4;
 
 import java.util.Collection;
+import java.util.LinkedHashMap;
 import java.util.Map;
 import java.util.Objects;
 
@@ -324,12 +325,14 @@ public class ClosureUtils {
     @SuppressWarnings("unchecked")
     public static <E> Closure<E> switchMapClosure(final Map<? extends E, 
Closure<E>> objectsAndClosures) {
         Objects.requireNonNull(objectsAndClosures, "objectsAndClosures");
-        final Closure<? super E> def = objectsAndClosures.remove(null);
-        final int size = objectsAndClosures.size();
+        // copy so the caller's map is not mutated
+        final Map<? extends E, Closure<E>> objects = new 
LinkedHashMap<>(objectsAndClosures);
+        final Closure<? super E> def = objects.remove(null);
+        final int size = objects.size();
         final Closure<? super E>[] trs = new Closure[size];
         final Predicate<E>[] preds = new Predicate[size];
         int i = 0;
-        for (final Map.Entry<? extends E, Closure<E>> entry : 
objectsAndClosures.entrySet()) {
+        for (final Map.Entry<? extends E, Closure<E>> entry : 
objects.entrySet()) {
             preds[i] = EqualPredicate.<E>equalPredicate(entry.getKey());
             trs[i] = entry.getValue();
             i++;
diff --git 
a/src/main/java/org/apache/commons/collections4/TransformerUtils.java 
b/src/main/java/org/apache/commons/collections4/TransformerUtils.java
index 3b1bdceab..8376b78c2 100644
--- a/src/main/java/org/apache/commons/collections4/TransformerUtils.java
+++ b/src/main/java/org/apache/commons/collections4/TransformerUtils.java
@@ -17,6 +17,7 @@
 package org.apache.commons.collections4;
 
 import java.util.Collection;
+import java.util.LinkedHashMap;
 import java.util.Map;
 import java.util.Objects;
 
@@ -372,12 +373,14 @@ public class TransformerUtils {
             final Map<I, Transformer<I, O>> objectsAndTransformers) {
 
         Objects.requireNonNull(objectsAndTransformers, 
"objectsAndTransformers");
-        final Transformer<? super I, ? extends O> def = 
objectsAndTransformers.remove(null);
-        final int size = objectsAndTransformers.size();
+        // copy so the caller's map is not mutated
+        final Map<I, Transformer<I, O>> objects = new 
LinkedHashMap<>(objectsAndTransformers);
+        final Transformer<? super I, ? extends O> def = objects.remove(null);
+        final int size = objects.size();
         final Transformer<? super I, ? extends O>[] trs = new 
Transformer[size];
         final Predicate<I>[] preds = new Predicate[size];
         int i = 0;
-        for (final Map.Entry<I, Transformer<I, O>> entry : 
objectsAndTransformers.entrySet()) {
+        for (final Map.Entry<I, Transformer<I, O>> entry : objects.entrySet()) 
{
             preds[i] = EqualPredicate.<I>equalPredicate(entry.getKey());
             trs[i++] = entry.getValue();
         }
diff --git 
a/src/main/java/org/apache/commons/collections4/functors/SwitchClosure.java 
b/src/main/java/org/apache/commons/collections4/functors/SwitchClosure.java
index d3aff1985..12329473b 100644
--- a/src/main/java/org/apache/commons/collections4/functors/SwitchClosure.java
+++ b/src/main/java/org/apache/commons/collections4/functors/SwitchClosure.java
@@ -17,6 +17,7 @@
 package org.apache.commons.collections4.functors;
 
 import java.io.Serializable;
+import java.util.LinkedHashMap;
 import java.util.Map;
 import java.util.Objects;
 
@@ -57,16 +58,17 @@ public class SwitchClosure<T> implements Closure<T>, 
Serializable {
     @SuppressWarnings("unchecked")
     public static <E> Closure<E> switchClosure(final Map<Predicate<E>, 
Closure<E>> predicatesAndClosures) {
         Objects.requireNonNull(predicatesAndClosures, "predicatesAndClosures");
-        // convert to array like this to guarantee iterator() ordering
-        final Closure<? super E> defaultClosure = 
predicatesAndClosures.remove(null);
-        final int size = predicatesAndClosures.size();
+        // copy so the caller's map is not mutated; LinkedHashMap preserves 
iterator() ordering
+        final Map<Predicate<E>, Closure<E>> entries = new 
LinkedHashMap<>(predicatesAndClosures);
+        final Closure<? super E> defaultClosure = entries.remove(null);
+        final int size = entries.size();
         if (size == 0) {
             return (Closure<E>) (defaultClosure == null ? 
NOPClosure.<E>nopClosure() : defaultClosure);
         }
         final Closure<E>[] closures = new Closure[size];
         final Predicate<E>[] preds = new Predicate[size];
         int i = 0;
-        for (final Map.Entry<Predicate<E>, Closure<E>> entry : 
predicatesAndClosures.entrySet()) {
+        for (final Map.Entry<Predicate<E>, Closure<E>> entry : 
entries.entrySet()) {
             preds[i] = entry.getKey();
             closures[i] = entry.getValue();
             i++;
diff --git 
a/src/test/java/org/apache/commons/collections4/ClosureUtilsTest.java 
b/src/test/java/org/apache/commons/collections4/ClosureUtilsTest.java
index 546b2e0dd..4a7ea5f19 100644
--- a/src/test/java/org/apache/commons/collections4/ClosureUtilsTest.java
+++ b/src/test/java/org/apache/commons/collections4/ClosureUtilsTest.java
@@ -20,6 +20,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertNotNull;
 import static org.junit.jupiter.api.Assertions.assertSame;
 import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
 
 import java.io.IOException;
 import java.util.ArrayList;
@@ -309,6 +310,33 @@ class ClosureUtilsTest {
         assertThrows(NullPointerException.class, () -> 
ClosureUtils.switchMapClosure(null));
     }
 
+    @Test
+    void testSwitchClosureDoesNotMutateInputMap() {
+        final MockClosure<String> def = new MockClosure<>();
+        final MockClosure<String> match = new MockClosure<>();
+        final Map<Predicate<String>, Closure<String>> predicateMap = new 
HashMap<>();
+        predicateMap.put(null, def);
+        predicateMap.put(EqualPredicate.equalPredicate("HELLO"), match);
+        final Closure<String> closure = 
ClosureUtils.switchClosure(predicateMap);
+        assertTrue(predicateMap.containsKey(null));
+        closure.execute("HELLO");
+        closure.execute("WORLD");
+        assertEquals(1, match.count);
+        assertEquals(1, def.count);
+
+        final MockClosure<String> mapDef = new MockClosure<>();
+        final MockClosure<String> mapMatch = new MockClosure<>();
+        final Map<String, Closure<String>> objectMap = new HashMap<>();
+        objectMap.put(null, mapDef);
+        objectMap.put("HELLO", mapMatch);
+        final Closure<String> mapClosure = 
ClosureUtils.switchMapClosure(objectMap);
+        assertTrue(objectMap.containsKey(null));
+        mapClosure.execute("HELLO");
+        mapClosure.execute("WORLD");
+        assertEquals(1, mapMatch.count);
+        assertEquals(1, mapDef.count);
+    }
+
     @Test
     void testTransformerClosure() {
         final MockTransformer<Object> mock = new MockTransformer<>();
diff --git 
a/src/test/java/org/apache/commons/collections4/TransformerUtilsTest.java 
b/src/test/java/org/apache/commons/collections4/TransformerUtilsTest.java
index 5029f2e60..9be5e3c78 100644
--- a/src/test/java/org/apache/commons/collections4/TransformerUtilsTest.java
+++ b/src/test/java/org/apache/commons/collections4/TransformerUtilsTest.java
@@ -21,6 +21,7 @@ import static org.junit.jupiter.api.Assertions.assertNotNull;
 import static org.junit.jupiter.api.Assertions.assertNull;
 import static org.junit.jupiter.api.Assertions.assertSame;
 import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
 
 import java.io.IOException;
 import java.util.ArrayList;
@@ -284,6 +285,17 @@ class TransformerUtilsTest {
         assertThrows(NullPointerException.class, () -> 
TransformerUtils.switchMapTransformer(null));
     }
 
+    @Test
+    void testSwitchMapTransformerDoesNotMutateInputMap() {
+        final Map<String, Transformer<String, String>> map = new HashMap<>();
+        map.put(null, TransformerUtils.constantTransformer("default"));
+        map.put("HELLO", TransformerUtils.constantTransformer("A"));
+        final Transformer<String, String> transformer = 
TransformerUtils.switchMapTransformer(map);
+        assertTrue(map.containsKey(null));
+        assertEquals("A", transformer.transform("HELLO"));
+        assertEquals("default", transformer.transform("WORLD"));
+    }
+
     @Test
     @SuppressWarnings("unchecked")
     void testSwitchTransformer() {
diff --git 
a/src/test/java/org/apache/commons/collections4/functors/SwitchTransformerMapMutatesTest.java
 
b/src/test/java/org/apache/commons/collections4/functors/SwitchTransformerMapMutatesTest.java
index 3cde12068..1bad1dbe4 100644
--- 
a/src/test/java/org/apache/commons/collections4/functors/SwitchTransformerMapMutatesTest.java
+++ 
b/src/test/java/org/apache/commons/collections4/functors/SwitchTransformerMapMutatesTest.java
@@ -44,9 +44,12 @@ class SwitchTransformerMapMutatesTest {
         final int sizeBefore = map.size();
         assertEquals(2, sizeBefore);
         // Call the factory method
-        SwitchTransformer.switchTransformer(map);
+        final Transformer<String, String> result = 
SwitchTransformer.switchTransformer(map);
         // The map should NOT have been mutated - null key must still be 
present
         final int sizeAfter = map.size();
         assertEquals(sizeBefore, sizeAfter, "switchTransformer must not mutate 
the input map; expected size");
+        // The returned transformer must still route via the copied map
+        assertEquals("value", result.transform(null));
+        assertEquals("default", result.transform("other"));
     }
 }

Reply via email to