diff --git a/src/changes/changes.xml b/src/changes/changes.xml index 9084d14bf..a98d8066f 100644 --- a/src/changes/changes.xml +++ b/src/changes/changes.xml @@ -22,6 +22,10 @@ + + Added "IndexedCollection" collection decorator which provides a map-like + view on an existing collection. + Added "DualLinkedHashBidiMap" bidi map implementation. diff --git a/src/main/java/org/apache/commons/collections/collection/IndexedCollection.java b/src/main/java/org/apache/commons/collections/collection/IndexedCollection.java index c2317f09a..7f98140ea 100644 --- a/src/main/java/org/apache/commons/collections/collection/IndexedCollection.java +++ b/src/main/java/org/apache/commons/collections/collection/IndexedCollection.java @@ -18,9 +18,10 @@ package org.apache.commons.collections.collection; import java.util.Collection; import java.util.HashMap; -import java.util.Map; +import org.apache.commons.collections.MultiMap; import org.apache.commons.collections.Transformer; +import org.apache.commons.collections.map.MultiValueMap; /** * An IndexedCollection is a Map-like view onto a Collection. It accepts a @@ -40,7 +41,6 @@ import org.apache.commons.collections.Transformer; * @since 4.0 * @version $Id$ */ -// TODO support MultiMap/non-unique index behavior public class IndexedCollection extends AbstractCollectionDecorator { /** Serialization version */ @@ -50,10 +50,16 @@ public class IndexedCollection extends AbstractCollectionDecorator { private final Transformer keyTransformer; /** The map of indexes to collected objects. */ - private final Map index; + private final MultiMap index; + + /** The uniqueness constraint for the index. */ + private final boolean uniqueIndex; /** * Create an {@link IndexedCollection} for a unique index. + *

+ * If an element is added, which maps to an existing key, an {@link IllegalArgumentException} + * will be thrown. * * @param the index object type. * @param the collection type. @@ -63,24 +69,50 @@ public class IndexedCollection extends AbstractCollectionDecorator { */ public static IndexedCollection uniqueIndexedCollection(final Collection coll, final Transformer keyTransformer) { - return new IndexedCollection(coll, keyTransformer, new HashMap()); + return new IndexedCollection(coll, keyTransformer, + MultiValueMap.multiValueMap(new HashMap>()), + true); } /** - * Create a {@link IndexedCollection} for a unique index. + * Create an {@link IndexedCollection} for a non-unique index. + * + * @param the index object type. + * @param the collection type. + * @param coll the decorated {@link Collection}. + * @param keyTransformer the {@link Transformer} for generating index keys. + * @return the created {@link IndexedCollection}. + */ + public static IndexedCollection nonUniqueIndexedCollection(final Collection coll, + final Transformer keyTransformer) { + return new IndexedCollection(coll, keyTransformer, + MultiValueMap.multiValueMap(new HashMap>()), + false); + } + + /** + * Create a {@link IndexedCollection}. * * @param coll decorated {@link Collection} * @param keyTransformer {@link Transformer} for generating index keys * @param map map to use as index + * @param uniqueIndex if the index shall enforce uniqueness of index keys */ public IndexedCollection(final Collection coll, final Transformer keyTransformer, - final HashMap map) { + final MultiMap map, final boolean uniqueIndex) { super(coll); this.keyTransformer = keyTransformer; - this.index = new HashMap(); + this.index = map; + this.uniqueIndex = uniqueIndex; reindex(); } + /** + * {@inheritDoc} + * + * @throws IllegalArgumentException if the object maps to an existing key and the index + * enforces a uniqueness constraint + */ @Override public boolean add(final C object) { final boolean added = super.add(object); @@ -133,12 +165,30 @@ public class IndexedCollection extends AbstractCollectionDecorator { /** * Get the element associated with the given key. + *

+ * In case of a non-unique index, this method will return the first + * value associated with the given key. To retrieve all elements associated + * with a key, use {@link #values(Object)}. * * @param key key to look up * @return element found + * @see #values(Object) */ public C get(final K key) { - return index.get(key); + @SuppressWarnings("unchecked") // index is a MultiMap which returns a Collection + Collection coll = (Collection) index.get(key); + return coll == null ? null : coll.iterator().next(); + } + + /** + * Get all elements associated with the given key. + * + * @param key key to look up + * @return a collection of elements found, or null if {@code contains(key) == false} + */ + @SuppressWarnings("unchecked") // index is a MultiMap which returns a Collection + public Collection values(final K key) { + return (Collection) index.get(key); } /** @@ -185,12 +235,15 @@ public class IndexedCollection extends AbstractCollectionDecorator { * Provides checking for adding the index. * * @param object the object to index + * @throws IllegalArgumentException if the object maps to an existing key and the index + * enforces a uniqueness constraint */ private void addToIndex(final C object) { - final C existingObject = index.put(keyTransformer.transform(object), object); - if (existingObject != null) { + final K key = keyTransformer.transform(object); + if (uniqueIndex && index.containsKey(key)) { throw new IllegalArgumentException("Duplicate key in uniquely indexed collection."); } + index.put(key, object); } /** diff --git a/src/test/java/org/apache/commons/collections/collection/IndexedCollectionTest.java b/src/test/java/org/apache/commons/collections/collection/IndexedCollectionTest.java index cdcf45f01..10c071842 100644 --- a/src/test/java/org/apache/commons/collections/collection/IndexedCollectionTest.java +++ b/src/test/java/org/apache/commons/collections/collection/IndexedCollectionTest.java @@ -26,7 +26,6 @@ import java.util.List; import org.apache.commons.collections.Transformer; import org.apache.commons.collections.collection.IndexedCollection; -import org.junit.Test; /** * Extension of {@link AbstractCollectionTest} for exercising the @@ -45,6 +44,10 @@ public class IndexedCollectionTest extends AbstractCollectionTest { //------------------------------------------------------------------------ protected Collection decorateCollection(final Collection collection) { + return IndexedCollection.nonUniqueIndexedCollection(collection, new IntegerTransformer()); + } + + protected IndexedCollection decorateUniqueCollection(final Collection collection) { return IndexedCollection.uniqueIndexedCollection(collection, new IntegerTransformer()); } @@ -90,6 +93,14 @@ public class IndexedCollectionTest extends AbstractCollectionTest { return list; } + public Collection makeTestCollection() { + return decorateCollection(new ArrayList()); + } + + public Collection makeUniqueTestCollection() { + return decorateUniqueCollection(new ArrayList()); + } + @Override protected boolean skipSerializedCanonicalTests() { // FIXME: support canonical tests @@ -98,22 +109,15 @@ public class IndexedCollectionTest extends AbstractCollectionTest { //------------------------------------------------------------------------ - @Override - public void testCollectionAddAll() { - // FIXME: does not work as we do not support multi-keys yet - } - - @Test - public void addedObjectsCanBeRetrievedByKey() throws Exception { - final Collection coll = getCollection(); + public void testAddedObjectsCanBeRetrievedByKey() throws Exception { + final Collection coll = makeTestCollection(); coll.add("12"); coll.add("16"); coll.add("1"); coll.addAll(asList("2","3","4")); @SuppressWarnings("unchecked") - final - IndexedCollection indexed = (IndexedCollection) coll; + final IndexedCollection indexed = (IndexedCollection) coll; assertEquals("12", indexed.get(12)); assertEquals("16", indexed.get(16)); assertEquals("1", indexed.get(1)); @@ -122,40 +126,43 @@ public class IndexedCollectionTest extends AbstractCollectionTest { assertEquals("4", indexed.get(4)); } - @Test(expected=IllegalArgumentException.class) - public void ensureDuplicateObjectsCauseException() throws Exception { - getCollection().add("1"); - getCollection().add("1"); + public void testEnsureDuplicateObjectsCauseException() throws Exception { + final Collection coll = makeUniqueTestCollection(); + + coll.add("1"); + try { + coll.add("1"); + fail(); + } catch (IllegalArgumentException e) { + // expected + } } -// @Test -// public void decoratedCollectionIsIndexedOnCreation() throws Exception { -// original.add("1"); -// original.add("2"); -// original.add("3"); -// -// indexed = IndexedCollection.uniqueIndexedCollection(original, new Transformer() { -// public Integer transform(String input) { -// return Integer.parseInt(input); -// } -// }); -// assertEquals("1", indexed.get(1)); -// assertEquals("2", indexed.get(2)); -// assertEquals("3", indexed.get(3)); -// } -// -// @Test -// public void reindexUpdatesIndexWhenTheDecoratedCollectionIsModifiedSeparately() throws Exception { -// original.add("1"); -// original.add("2"); -// original.add("3"); -// -// assertNull(indexed.get(1)); -// assertNull(indexed.get(2)); -// assertNull(indexed.get(3)); -// indexed.reindex(); -// assertEquals("1", indexed.get(1)); -// assertEquals("2", indexed.get(2)); -// assertEquals("3", indexed.get(3)); -// } + public void testDecoratedCollectionIsIndexedOnCreation() throws Exception { + Collection original = makeFullCollection(); + IndexedCollection indexed = decorateUniqueCollection(original); + + assertEquals("1", indexed.get(1)); + assertEquals("2", indexed.get(2)); + assertEquals("3", indexed.get(3)); + } + + public void testReindexUpdatesIndexWhenDecoratedCollectionIsModifiedSeparately() throws Exception { + Collection original = new ArrayList(); + IndexedCollection indexed = decorateUniqueCollection(original); + + original.add("1"); + original.add("2"); + original.add("3"); + + assertNull(indexed.get(1)); + assertNull(indexed.get(2)); + assertNull(indexed.get(3)); + + indexed.reindex(); + + assertEquals("1", indexed.get(1)); + assertEquals("2", indexed.get(2)); + assertEquals("3", indexed.get(3)); + } }