From 6f0275e40f92282c12d700a5bcb21ff152066065 Mon Sep 17 00:00:00 2001 From: Paul King Date: Fri, 12 Dec 2025 21:15:50 +1000 Subject: [PATCH] provide an inverse method for multimaps --- .../commons/collections4/MultiMapUtils.java | 22 ++++++++++++++++ .../commons/collections4/MultiValuedMap.java | 11 ++++++++ .../multimap/ArrayListValuedHashMap.java | 6 +++++ .../ArrayListValuedLinkedHashMap.java | 6 +++++ .../multimap/HashSetValuedHashMap.java | 6 +++++ .../LinkedHashSetValuedLinkedHashMap.java | 6 +++++ .../collections4/MultiMapUtilsTest.java | 26 +++++++++++++++++++ .../multimap/ArrayListValuedHashMapTest.java | 13 ++++++++++ .../ArrayListValuedLinkedHashMapTest.java | 13 ++++++++++ .../multimap/HashSetValuedHashMapTest.java | 13 ++++++++++ .../LinkedHashSetValuedLinkedHashMapTest.java | 13 ++++++++++ .../TransformedMultiValuedMapTest.java | 6 +++++ 12 files changed, 141 insertions(+) diff --git a/src/main/java/org/apache/commons/collections4/MultiMapUtils.java b/src/main/java/org/apache/commons/collections4/MultiMapUtils.java index ef7fc8c96c..c698a38e50 100644 --- a/src/main/java/org/apache/commons/collections4/MultiMapUtils.java +++ b/src/main/java/org/apache/commons/collections4/MultiMapUtils.java @@ -20,6 +20,7 @@ import java.util.Collection; import java.util.HashSet; import java.util.List; +import java.util.Map; import java.util.Set; import org.apache.commons.collections4.bag.HashBag; @@ -170,6 +171,27 @@ public static boolean isEmpty(final MultiValuedMap map) { return map == null || map.isEmpty(); } + /** + * A utility method to invert the mappings from an input MultiValuedMap + * and add them to an output MultiValuedMap. Use this method to have complete + * control of the output MultiValuedMap or when merging several inverse mappings. + * In simple cases, consider just using the {@link MultiValuedMap#inverted()} method. + * + * @param input take key-to-value mappings from here + * @param output add value-to-key mappings here + * @param the output MultiValuedMap key type + * @param the output MultiValuedMap value type + * @param the output MultiValuedMap with key and value types reversed compared with input + * @return the updated output MultiValuedMap + */ + public static > + M invert(MultiValuedMap input, M output) { + for (Map.Entry e : input.entries()) { + output.put(e.getValue(), e.getKey()); + } + return output; + } + /** * Creates a {@link ListValuedMap} with an {@link java.util.ArrayList ArrayList} as * collection class to store the values mapped to a key. diff --git a/src/main/java/org/apache/commons/collections4/MultiValuedMap.java b/src/main/java/org/apache/commons/collections4/MultiValuedMap.java index 96d2f6b086..350e74e7e1 100644 --- a/src/main/java/org/apache/commons/collections4/MultiValuedMap.java +++ b/src/main/java/org/apache/commons/collections4/MultiValuedMap.java @@ -140,6 +140,17 @@ public interface MultiValuedMap { */ Collection get(K key); + /** + * Returns a new MultiValuedMap with inverted mappings. + * The new multimap will have a value-to-key mapping + * for each key-to-value mapping in the original. + * + * @return a new MultiValuedMap with inverted mappings + */ + default MultiValuedMap inverted() { + throw new UnsupportedOperationException(); + } + /** * Returns {@code true} if this map contains no key-value mappings. * diff --git a/src/main/java/org/apache/commons/collections4/multimap/ArrayListValuedHashMap.java b/src/main/java/org/apache/commons/collections4/multimap/ArrayListValuedHashMap.java index a9d2a11537..47586dc631 100644 --- a/src/main/java/org/apache/commons/collections4/multimap/ArrayListValuedHashMap.java +++ b/src/main/java/org/apache/commons/collections4/multimap/ArrayListValuedHashMap.java @@ -25,6 +25,7 @@ import java.util.HashMap; import java.util.Map; +import org.apache.commons.collections4.MultiMapUtils; import org.apache.commons.collections4.MultiValuedMap; /** @@ -118,6 +119,11 @@ protected ArrayList createCollection() { return new ArrayList<>(initialListCapacity); } + @Override + public ArrayListValuedHashMap inverted() { + return MultiMapUtils.invert(this, new ArrayListValuedHashMap()); + } + /** * Deserializes an instance from an ObjectInputStream. * diff --git a/src/main/java/org/apache/commons/collections4/multimap/ArrayListValuedLinkedHashMap.java b/src/main/java/org/apache/commons/collections4/multimap/ArrayListValuedLinkedHashMap.java index f1b797b532..2556ab3f74 100644 --- a/src/main/java/org/apache/commons/collections4/multimap/ArrayListValuedLinkedHashMap.java +++ b/src/main/java/org/apache/commons/collections4/multimap/ArrayListValuedLinkedHashMap.java @@ -25,6 +25,7 @@ import java.util.LinkedHashMap; import java.util.Map; +import org.apache.commons.collections4.MultiMapUtils; import org.apache.commons.collections4.MultiValuedMap; /** @@ -118,6 +119,11 @@ protected ArrayList createCollection() { return new ArrayList<>(initialListCapacity); } + @Override + public ArrayListValuedLinkedHashMap inverted() { + return MultiMapUtils.invert(this, new ArrayListValuedLinkedHashMap<>()); + } + /** * Deserializes an instance from an ObjectInputStream. * diff --git a/src/main/java/org/apache/commons/collections4/multimap/HashSetValuedHashMap.java b/src/main/java/org/apache/commons/collections4/multimap/HashSetValuedHashMap.java index b122e2c410..184730cbfb 100644 --- a/src/main/java/org/apache/commons/collections4/multimap/HashSetValuedHashMap.java +++ b/src/main/java/org/apache/commons/collections4/multimap/HashSetValuedHashMap.java @@ -24,6 +24,7 @@ import java.util.HashSet; import java.util.Map; +import org.apache.commons.collections4.MultiMapUtils; import org.apache.commons.collections4.MultiValuedMap; /** @@ -117,6 +118,11 @@ protected HashSet createCollection() { return new HashSet<>(initialSetCapacity); } + @Override + public HashSetValuedHashMap inverted() { + return MultiMapUtils.invert(this, new HashSetValuedHashMap()); + } + /** * Deserializes an instance from an ObjectInputStream. * diff --git a/src/main/java/org/apache/commons/collections4/multimap/LinkedHashSetValuedLinkedHashMap.java b/src/main/java/org/apache/commons/collections4/multimap/LinkedHashSetValuedLinkedHashMap.java index c8be8751dc..c381893b6a 100644 --- a/src/main/java/org/apache/commons/collections4/multimap/LinkedHashSetValuedLinkedHashMap.java +++ b/src/main/java/org/apache/commons/collections4/multimap/LinkedHashSetValuedLinkedHashMap.java @@ -24,6 +24,7 @@ import java.util.LinkedHashSet; import java.util.Map; +import org.apache.commons.collections4.MultiMapUtils; import org.apache.commons.collections4.MultiValuedMap; /** @@ -117,6 +118,11 @@ protected LinkedHashSet createCollection() { return new LinkedHashSet<>(initialSetCapacity); } + @Override + public LinkedHashSetValuedLinkedHashMap inverted() { + return MultiMapUtils.invert(this, new LinkedHashSetValuedLinkedHashMap()); + } + /** * Deserializes an instance from an ObjectInputStream. * diff --git a/src/test/java/org/apache/commons/collections4/MultiMapUtilsTest.java b/src/test/java/org/apache/commons/collections4/MultiMapUtilsTest.java index 14f177f2b4..7ff98ba078 100644 --- a/src/test/java/org/apache/commons/collections4/MultiMapUtilsTest.java +++ b/src/test/java/org/apache/commons/collections4/MultiMapUtilsTest.java @@ -29,6 +29,8 @@ import java.util.Set; import org.apache.commons.collections4.multimap.ArrayListValuedHashMap; +import org.apache.commons.collections4.multimap.HashSetValuedHashMap; +import org.apache.commons.collections4.multimap.LinkedHashSetValuedLinkedHashMap; import org.junit.jupiter.api.Test; /** @@ -116,6 +118,30 @@ void testGetValuesAsSet() { assertEquals(new HashSet<>(Arrays.asList(values)), set); } + @Test + void testInvert() { + final HashSetValuedHashMap usages = new HashSetValuedHashMap<>(); + + final LinkedHashSetValuedLinkedHashMap deps = new LinkedHashSetValuedLinkedHashMap<>(); + deps.put("commons-configuration2", "commons-logging"); + deps.put("commons-configuration2", "commons-lang3"); + deps.put("commons-configuration2", "commons-text"); + deps.put("commons-beanutils", "commons-collections"); + deps.put("commons-beanutils", "commons-logging"); + MultiMapUtils.invert(deps, usages); + final Set loggingUsagesCompile = usages.get("commons-logging"); + assertEquals("[commons-configuration2, commons-beanutils]", loggingUsagesCompile.toString()); + final Set codecUsagesCompile = usages.get("commons-codec"); + assertEquals("[]", codecUsagesCompile.toString()); + + final LinkedHashSetValuedLinkedHashMap optionalDeps = new LinkedHashSetValuedLinkedHashMap<>(); + optionalDeps.put("commons-configuration2", "commons-codec"); + optionalDeps.put("commons-collections", "commons-codec"); + MultiMapUtils.invert(optionalDeps, usages); + final Set codecUsagesAll = usages.get("commons-codec"); + assertEquals("[commons-collections, commons-configuration2]", codecUsagesAll.toString()); + } + @Test void testIsEmptyWithEmptyMap() { assertTrue(MultiMapUtils.isEmpty(new ArrayListValuedHashMap<>())); diff --git a/src/test/java/org/apache/commons/collections4/multimap/ArrayListValuedHashMapTest.java b/src/test/java/org/apache/commons/collections4/multimap/ArrayListValuedHashMapTest.java index 47e9a63e60..f65ee297c0 100644 --- a/src/test/java/org/apache/commons/collections4/multimap/ArrayListValuedHashMapTest.java +++ b/src/test/java/org/apache/commons/collections4/multimap/ArrayListValuedHashMapTest.java @@ -93,6 +93,19 @@ void testEqualsHashCodeContract() { assertNotSame(map1.hashCode(), map2.hashCode()); } + @Test + void testInverted() { + final ArrayListValuedHashMap shopping = new ArrayListValuedHashMap<>(4); + shopping.put("Alice", "Bread"); + shopping.put("Alice", "Milk"); + shopping.put("Alice", "Milk"); + shopping.put("Bob", "Pizza"); + shopping.put("Bob", "Bread"); + shopping.put("Bob", "Bread"); + assertEquals("{Pizza=[Bob], Bread=[Bob, Bob, Alice], Milk=[Alice, Alice]}", + shopping.inverted().toString()); + } + @Test @SuppressWarnings("unchecked") void testListValuedMapAdd() { diff --git a/src/test/java/org/apache/commons/collections4/multimap/ArrayListValuedLinkedHashMapTest.java b/src/test/java/org/apache/commons/collections4/multimap/ArrayListValuedLinkedHashMapTest.java index 74353b50ab..550a192409 100644 --- a/src/test/java/org/apache/commons/collections4/multimap/ArrayListValuedLinkedHashMapTest.java +++ b/src/test/java/org/apache/commons/collections4/multimap/ArrayListValuedLinkedHashMapTest.java @@ -99,6 +99,19 @@ void testEqualsHashCodeContract() { assertNotSame(map1.hashCode(), map2.hashCode()); } + @Test + void testInverted() { + final ArrayListValuedLinkedHashMap shopping = new ArrayListValuedLinkedHashMap<>(4); + shopping.put("Alice", "Bread"); + shopping.put("Alice", "Milk"); + shopping.put("Alice", "Milk"); + shopping.put("Bob", "Pizza"); + shopping.put("Bob", "Bread"); + shopping.put("Bob", "Bread"); + assertEquals("{Bread=[Alice, Bob, Bob], Milk=[Alice, Alice], Pizza=[Bob]}", + shopping.inverted().toString()); + } + @Test @SuppressWarnings("unchecked") void testListValuedMapAdd() { diff --git a/src/test/java/org/apache/commons/collections4/multimap/HashSetValuedHashMapTest.java b/src/test/java/org/apache/commons/collections4/multimap/HashSetValuedHashMapTest.java index 7b50d6cd13..419ae3814f 100644 --- a/src/test/java/org/apache/commons/collections4/multimap/HashSetValuedHashMapTest.java +++ b/src/test/java/org/apache/commons/collections4/multimap/HashSetValuedHashMapTest.java @@ -110,6 +110,19 @@ void testHashSetValueHashMap_1() { assertEquals("{}", map3.toString()); } + @Test + void testInverted() { + final HashSetValuedHashMap dependencies = new HashSetValuedHashMap<>(); + dependencies.put("commons-configuration2", "commons-logging"); + dependencies.put("commons-configuration2", "commons-lang3"); + dependencies.put("commons-configuration2", "commons-text"); + dependencies.put("commons-beanutils", "commons-collections"); + dependencies.put("commons-beanutils", "commons-logging"); + final Set loggingUsages = dependencies.inverted().get("commons-logging"); + assertEquals("[commons-beanutils, commons-configuration2]", + loggingUsages.toString()); + } + @Test @SuppressWarnings("unchecked") void testSetValuedMapAdd() { diff --git a/src/test/java/org/apache/commons/collections4/multimap/LinkedHashSetValuedLinkedHashMapTest.java b/src/test/java/org/apache/commons/collections4/multimap/LinkedHashSetValuedLinkedHashMapTest.java index 2b37cb76d2..561432363b 100644 --- a/src/test/java/org/apache/commons/collections4/multimap/LinkedHashSetValuedLinkedHashMapTest.java +++ b/src/test/java/org/apache/commons/collections4/multimap/LinkedHashSetValuedLinkedHashMapTest.java @@ -96,6 +96,19 @@ void testHashSetValueHashMap_1() { assertEquals("{}", map3.toString()); } + @Test + void testInverted() { + final LinkedHashSetValuedLinkedHashMap citiesLived = new LinkedHashSetValuedLinkedHashMap<>(4); + citiesLived.put("Alice", "N.Y."); + citiesLived.put("Alice", "L.A."); + citiesLived.put("Alice", "Chicago"); + citiesLived.put("Bob", "N.Y."); + citiesLived.put("Cara", "L.A."); + citiesLived.put("Cara", "Chicago"); + assertEquals("{N.Y.=[Alice, Bob], L.A.=[Alice, Cara], Chicago=[Alice, Cara]}", + citiesLived.inverted().toString()); + } + @Test void testLinkedHashSetValuedLinkedHashMap_2() { final Map map = new HashMap<>(); diff --git a/src/test/java/org/apache/commons/collections4/multimap/TransformedMultiValuedMapTest.java b/src/test/java/org/apache/commons/collections4/multimap/TransformedMultiValuedMapTest.java index 37ef87d8f0..d83b47108a 100644 --- a/src/test/java/org/apache/commons/collections4/multimap/TransformedMultiValuedMapTest.java +++ b/src/test/java/org/apache/commons/collections4/multimap/TransformedMultiValuedMapTest.java @@ -19,6 +19,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import java.util.Collection; @@ -99,6 +100,11 @@ void testFactory_decorateTransform() { assertTrue(transMap.get((K) "D").contains(Integer.valueOf(4))); } + @Test + void testInvertedIsUnsupportedByDefault() { + assertThrows(UnsupportedOperationException.class, () -> makeObject().inverted()); + } + @Test @SuppressWarnings("unchecked") void testKeyTransformedMap() {