From 0721f49bf0d8b954c8a4b8d75e5375de43af981e Mon Sep 17 00:00:00 2001 From: Thomas Neidhart Date: Thu, 17 Apr 2014 19:45:56 +0000 Subject: [PATCH] [COLLECTIONS-508] Further additions. git-svn-id: https://svn.apache.org/repos/asf/commons/proper/collections/trunk@1588354 13f79535-47bb-0310-9956-ffa450edef68 --- .../commons/collections4/ListValuedMap.java | 24 +- .../commons/collections4/MultiMapUtils.java | 290 ++++++++++++++++++ .../commons/collections4/MultiValuedMap.java | 15 +- .../commons/collections4/SetValuedMap.java | 17 +- .../multimap/AbstractListValuedMap.java | 246 +++++++++++++++ .../multimap/AbstractMultiValuedMap.java | 24 +- .../multimap/AbstractSetValuedMap.java | 108 +++++++ .../multimap/MultiValuedHashMap.java | 100 +++++- .../collections4/MultiMapUtilsTest.java | 154 ++++++++++ .../multimap/AbstractMultiValuedMapTest.java | 2 +- .../multimap/MultiValuedHashMapTest.java | 142 +++++++-- 11 files changed, 1036 insertions(+), 86 deletions(-) create mode 100644 src/main/java/org/apache/commons/collections4/MultiMapUtils.java create mode 100644 src/main/java/org/apache/commons/collections4/multimap/AbstractListValuedMap.java create mode 100644 src/main/java/org/apache/commons/collections4/multimap/AbstractSetValuedMap.java create mode 100644 src/test/java/org/apache/commons/collections4/MultiMapUtilsTest.java diff --git a/src/main/java/org/apache/commons/collections4/ListValuedMap.java b/src/main/java/org/apache/commons/collections4/ListValuedMap.java index b994228e1..765e55096 100644 --- a/src/main/java/org/apache/commons/collections4/ListValuedMap.java +++ b/src/main/java/org/apache/commons/collections4/ListValuedMap.java @@ -35,15 +35,14 @@ public interface ListValuedMap extends MultiValuedMap { /** * Gets the list of values associated with the specified key. *

- * Implementations typically return null if no values have been - * mapped to the key, however the implementation may choose to return an - * empty collection. - *

- * Implementations may choose to return a clone of the internal collection. + * This method will return an empty list if + * {@link #containsKey(Object)} returns {@code false}. Changes to the + * returned list will update the underlying {@code ListValuedMap} and + * vice-versa. * * @param key the key to retrieve - * @return the Collection of values, implementations should - * return null for no mapping, but may return an empty collection + * @return the List of values, implementations should return an + * empty list for no mapping * @throws ClassCastException if the key is of an invalid type * @throws NullPointerException if the key is null and null keys are invalid */ @@ -52,14 +51,13 @@ public interface ListValuedMap extends MultiValuedMap { /** * Removes all values associated with the specified key. *

- * Implementations typically return null from a subsequent - * get(Object), however they may choose to return an empty - * collection. + * The returned list may be modifiable, but updates will not be + * propagated to this list-valued map. In case no mapping was stored for the + * specified key, an empty, unmodifiable list will be returned. * * @param key the key to remove values from - * @return the Collection of values removed, implementations - * should return null for no mapping found, but may - * return an empty collection + * @return the List of values removed, implementations + * typically return an empty, unmodifiable List for no mapping found * @throws UnsupportedOperationException if the map is unmodifiable * @throws ClassCastException if the key is of an invalid type * @throws NullPointerException if the key is null and null keys are invalid diff --git a/src/main/java/org/apache/commons/collections4/MultiMapUtils.java b/src/main/java/org/apache/commons/collections4/MultiMapUtils.java new file mode 100644 index 000000000..be90de479 --- /dev/null +++ b/src/main/java/org/apache/commons/collections4/MultiMapUtils.java @@ -0,0 +1,290 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 + * + * http://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.apache.commons.collections4; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import org.apache.commons.collections4.bag.HashBag; +import org.apache.commons.collections4.multimap.MultiValuedHashMap; +import org.apache.commons.collections4.multimap.TransformedMultiValuedMap; +import org.apache.commons.collections4.multimap.UnmodifiableMultiValuedMap; + +/** + * Provides utility methods and decorators for {@link MultiValuedMap} instances. + *

+ * It contains various type safe and null safe methods. + *

+ * It also provides the following decorators: + * + *

    + *
  • {@link #unmodifiableMultiValuedMap(MultiValuedMap)}
  • + *
  • {@link #transformedMultiValuedMap(MultiValuedMap, Transformer, Transformer)}
  • + *
+ * + * @since 4.1 + * @version $Id$ + */ +public class MultiMapUtils { + + /** + * MultiMapUtils should not normally be instantiated. + */ + private MultiMapUtils() { + } + + /** + * An empty {@link UnmodifiableMultiValuedMap}. + */ + @SuppressWarnings({ "rawtypes", "unchecked" }) + public static final MultiValuedMap EMPTY_MULTI_VALUED_MAP = + UnmodifiableMultiValuedMap.unmodifiableMultiValuedMap(new MultiValuedHashMap()); + + /** + * Returns immutable EMPTY_MULTI_VALUED_MAP with generic type safety. + * + * @param the type of key in the map + * @param the type of value in the map + * @return immutable and empty MultiValuedMap + */ + @SuppressWarnings("unchecked") + public static MultiValuedMap emptyMultiValuedMap() { + return EMPTY_MULTI_VALUED_MAP; + } + + // Null safe methods + + /** + * Returns an immutable empty MultiValuedMap if the argument is + * null, or the argument itself otherwise. + * + * @param the type of key in the map + * @param the type of value in the map + * @param map the map, possibly null + * @return an empty MultiValuedMap if the argument is null + */ + @SuppressWarnings("unchecked") + public static MultiValuedMap emptyIfNull(final MultiValuedMap map) { + return map == null ? EMPTY_MULTI_VALUED_MAP : map; + } + + /** + * Null-safe check if the specified MultiValuedMap is empty. + *

+ * Null returns true. + * + * @param map the map to check, may be null + * @return true if empty or null + */ + public static boolean isEmpty(final MultiValuedMap map) { + return map == null || map.isEmpty(); + } + + // Null safe getters + // ------------------------------------------------------------------------- + + /** + * Gets a Collection from MultiValuedMap in a null-safe manner. + * + * @param the key type + * @param the value type + * @param map the MultiValuedMap to use + * @param key the key to look up + * @return the Collection in the MultiValuedMap, null if map input is null + */ + public static Collection getCollection(final MultiValuedMap map, final K key) { + if (map != null) { + return map.get(key); + } + return null; + } + + /** + * Gets a List from MultiValuedMap in a null-safe manner. + * + * @param the key type + * @param the value type + * @param map the MultiValuedMap to use + * @param key the key to look up + * @return the Collection in the MultiValuedMap as List, + * null if map input is null + */ + public static List getList(MultiValuedMap map, K key) { + if (map != null) { + Collection col = map.get(key); + if (col instanceof List) { + return (List) col; + } + return new ArrayList(col); + } + return null; + } + + /** + * Gets a Set from MultiValuedMap in a null-safe manner. + * + * @param the key type + * @param the value type + * @param map the MultiValuedMap to use + * @param key the key to look up + * @return the Collection in the MultiValuedMap as Set, + * null if map input is null + */ + public static Set getSet(MultiValuedMap map, K key) { + if (map != null) { + Collection col = map.get(key); + if (col instanceof Set) { + return (Set) col; + } + return new HashSet(col); + } + return null; + } + + /** + * Gets a Bag from MultiValuedMap in a null-safe manner. + * + * @param the key type + * @param the value type + * @param map the MultiValuedMap to use + * @param key the key to look up + * @return the Collection in the MultiValuedMap as Bag, + * null if map input is null + */ + public static Bag getBag(MultiValuedMap map, K key) { + if (map != null) { + Collection col = map.get(key); + if (col instanceof Bag) { + return (Bag) col; + } + return new HashBag(col); + } + return null; + } + + // Factory Methods + // ----------------------------------------------------------------------- + + /** + * Creates a {@link ListValuedMap} with a {@link HashMap} as its internal storage. + * + * @param the key type + * @param the value type + * @return a new ListValuedMap + */ + public static ListValuedMap createListValuedHashMap() { + return MultiValuedHashMap.listValuedHashMap(); + } + + /** + * Creates a {@link ListValuedMap} with a {@link HashMap} as its internal + * storage which maps the keys to list of type listClass. + * + * @param the key type + * @param the value type + * @param the List class type + * @param listClass the class of the list + * @return a new ListValuedMap + */ + public static > ListValuedMap createListValuedHashMap(final Class listClass) { + return MultiValuedHashMap.listValuedHashMap(listClass); + } + + /** + * Creates a {@link SetValuedMap} with a {@link HashMap} as its internal + * storage + * + * @param the key type + * @param the value type + * @return a new SetValuedMap + */ + public static SetValuedMap createSetValuedHashMap() { + return MultiValuedHashMap.setValuedHashMap(); + } + + /** + * Creates a {@link SetValuedMap} with a {@link HashMap} as its internal + * storage which maps the keys to a set of type setClass + * + * @param the key type + * @param the value type + * @param the Set class type + * @param setClass the class of the set + * @return a new SetValuedMap + */ + public static > SetValuedMap createSetValuedHashMap(final Class setClass) { + return MultiValuedHashMap.setValuedHashMap(setClass); + } + + // MultiValuedMap Decorators + // ----------------------------------------------------------------------- + + /** + * Returns an UnmodifiableMultiValuedMap backed by the given + * map. + * + * @param the key type + * @param the value type + * @param map the MultiValuedMap to make unmodifiable, must not + * be null + * @return an UnmodifiableMultiValuedMap backed by the given + * map + * @throws IllegalArgumentException if the map is null + */ + public static MultiValuedMap unmodifiableMultiValuedMap( + final MultiValuedMap map) { + return UnmodifiableMultiValuedMap.unmodifiableMultiValuedMap(map); + } + + /** + * Returns a TransformedMultiValuedMap backed by the given map. + *

+ * This method returns a new MultiValuedMap (decorating the + * specified map) that will transform any new entries added to it. Existing + * entries in the specified map will not be transformed. If you want that + * behaviour, see {@link TransformedMultiValuedMap#transformedMap}. + *

+ * Each object is passed through the transformers as it is added to the Map. + * It is important not to use the original map after invoking this method, + * as it is a back door for adding untransformed objects. + *

+ * If there are any elements already in the map being decorated, they are + * NOT transformed. + * + * @param the key type + * @param the value type + * @param map the MultiValuedMap to transform, must not be + * null, typically empty + * @param keyTransformer the transformer for the map keys, null means no + * transformation + * @param valueTransformer the transformer for the map values, null means no + * transformation + * @return a transformed MultiValuedMap backed by the given map + * @throws IllegalArgumentException if the MultiValuedMap is + * null + */ + public static MultiValuedMap transformedMultiValuedMap(final MultiValuedMap map, + final Transformer keyTransformer, + final Transformer valueTransformer) { + return TransformedMultiValuedMap.transformingMap(map, keyTransformer, valueTransformer); + } + +} diff --git a/src/main/java/org/apache/commons/collections4/MultiValuedMap.java b/src/main/java/org/apache/commons/collections4/MultiValuedMap.java index b27ce03a5..9ebfadd7e 100644 --- a/src/main/java/org/apache/commons/collections4/MultiValuedMap.java +++ b/src/main/java/org/apache/commons/collections4/MultiValuedMap.java @@ -102,15 +102,17 @@ public interface MultiValuedMap { boolean containsMapping(Object key, Object value); /** - * Returns a view collection of the values associated with the specified key. + * Returns a view collection of the values associated with the specified + * key. *

- * This method will return an empty collection if {@link #containsKey(Object)} - * returns {@code false}. Changes to the returned collection will update the - * underlying {@code MultiValuedMap} and vice-versa. + * This method will return an empty collection if + * {@link #containsKey(Object)} returns {@code false}. Changes to the + * returned collection will update the underlying {@code MultiValuedMap} and + * vice-versa. * * @param key the key to retrieve * @return the Collection of values, implementations should - * return null for no mapping, but may return an empty collection + * return an empty collection for no mapping * @throws ClassCastException if the key is of an invalid type (optional) * @throws NullPointerException if the key is null and null keys are invalid (optional) */ @@ -220,8 +222,7 @@ public interface MultiValuedMap { * Other values attached to that key are unaffected. *

* If the last value for a key is removed, implementations typically return - * null from a subsequent get(Object), however - * they may choose to return an empty collection. + * an empty collection from a subsequent get(Object). * * @param key the key to remove from * @param item the item to remove diff --git a/src/main/java/org/apache/commons/collections4/SetValuedMap.java b/src/main/java/org/apache/commons/collections4/SetValuedMap.java index de0806388..29edf3cdc 100644 --- a/src/main/java/org/apache/commons/collections4/SetValuedMap.java +++ b/src/main/java/org/apache/commons/collections4/SetValuedMap.java @@ -37,16 +37,13 @@ public interface SetValuedMap extends MultiValuedMap { /** * Gets the set of values associated with the specified key. *

- * Implementations typically return null if no values have been - * mapped to the key, however the implementation may choose to return an - * empty collection. + * Implementations typically return an empty Set if no values + * have been mapped to the key. *

- * Implementations may choose to return a clone of the internal collection. * * @param key the key to retrieve - * @return the Set of values, implementations should return - * null for no mapping, but may return an empty - * collection + * @return the Set of values, implementations should return an + * empty Set for no mapping * @throws ClassCastException if the key is of an invalid type * @throws NullPointerException if the key is null and null keys are invalid */ @@ -55,9 +52,9 @@ public interface SetValuedMap extends MultiValuedMap { /** * Removes all values associated with the specified key. *

- * Implementations typically return null from a subsequent - * get(Object), however they may choose to return an empty - * collection. + * The returned set may be modifiable, but updates will not be + * propagated to this set-valued map. In case no mapping was stored for the + * specified key, an empty, unmodifiable set will be returned. * * @param key the key to remove values from * @return the Set of values removed, implementations should diff --git a/src/main/java/org/apache/commons/collections4/multimap/AbstractListValuedMap.java b/src/main/java/org/apache/commons/collections4/multimap/AbstractListValuedMap.java new file mode 100644 index 000000000..e8cc9c961 --- /dev/null +++ b/src/main/java/org/apache/commons/collections4/multimap/AbstractListValuedMap.java @@ -0,0 +1,246 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 + * + * http://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.apache.commons.collections4.multimap; + +import java.io.Serializable; +import java.util.Collection; +import java.util.List; +import java.util.ListIterator; +import java.util.Map; + +import org.apache.commons.collections4.ListUtils; +import org.apache.commons.collections4.ListValuedMap; + +/** + * Abstract implementation of the {@link ListValuedMap} interface to simplify + * the creation of subclass implementations. + *

+ * Subclasses specify a Map implementation to use as the internal storage and + * the List implementation to use as values. + * + * @since 4.1 + * @version $Id$ + */ +public abstract class AbstractListValuedMap extends AbstractMultiValuedMap + implements ListValuedMap, Serializable { + + /** The serialization version */ + private static final long serialVersionUID = 6024950625989666915L; + + /** + * A constructor that wraps, not copies + * + * @param the list type + * @param map the map to wrap, must not be null + * @param listClazz the collection class + * @throws IllegalArgumentException if the map is null + */ + protected > AbstractListValuedMap(Map map, Class listClazz) { + super(map, listClazz); + } + + /** + * A constructor that wraps, not copies + * + * @param the list type + * @param map the map to wrap, must not be null + * @param listClazz the collection class + * @param initialListCapacity the initial size of the values list + * @throws IllegalArgumentException if the map is null or if + * initialListCapacity is negative + */ + protected > AbstractListValuedMap(Map map, Class listClazz, + int initialListCapacity) { + super(map, listClazz, initialListCapacity); + } + + /** + * Gets the list of values associated with the specified key. This would + * return an empty list in case the mapping is not present + * + * @param key the key to retrieve + * @return the List of values, will return an empty + * List for no mapping + * @throws ClassCastException if the key is of an invalid type + */ + @Override + public List get(Object key) { + return new WrappedList(key); + } + + /** + * Removes all values associated with the specified key. + *

+ * A subsequent get(Object) would return an empty list. + * + * @param key the key to remove values from + * @return the List of values removed, will return an empty, + * unmodifiable list for no mapping found. + * @throws ClassCastException if the key is of an invalid type + */ + @Override + public List remove(Object key) { + return ListUtils.emptyIfNull((List) getMap().remove(key)); + } + + /** + * Wrapped list to handle add and remove on the list returned by get(object) + */ + private class WrappedList extends WrappedCollection implements List { + + public WrappedList(Object key) { + super(key); + } + + @SuppressWarnings("unchecked") + public void add(int index, V value) { + List list = (List) getMapping(); + if (list == null) { + list = (List) AbstractListValuedMap.this.createCollection(); + list.add(index, value); + getMap().put((K) key, list); + } + list.add(index, value); + } + + @SuppressWarnings("unchecked") + public boolean addAll(int index, Collection c) { + List list = (List) getMapping(); + if (list == null) { + list = (List) createCollection(); + boolean result = list.addAll(index, c); + if (result) { + getMap().put((K) key, list); + } + return result; + } + return list.addAll(index, c); + } + + public V get(int index) { + final List list = ListUtils.emptyIfNull((List) getMapping()); + return list.get(index); + } + + public int indexOf(Object o) { + final List list = ListUtils.emptyIfNull((List) getMapping()); + return list.indexOf(o); + } + + public int lastIndexOf(Object o) { + final List list = ListUtils.emptyIfNull((List) getMapping()); + return list.indexOf(o); + } + + public ListIterator listIterator() { + return new ValuesListIterator(key); + } + + public ListIterator listIterator(int index) { + return new ValuesListIterator(key, index); + } + + public V remove(int index) { + final List list = ListUtils.emptyIfNull((List) getMapping()); + V value = list.remove(index); + if (list.isEmpty()) { + AbstractListValuedMap.this.remove(key); + } + return value; + } + + public V set(int index, V value) { + final List list = ListUtils.emptyIfNull((List) getMapping()); + return list.set(index, value); + } + + public List subList(int fromIndex, int toIndex) { + final List list = ListUtils.emptyIfNull((List) getMapping()); + return list.subList(fromIndex, toIndex); + } + + } + + /** Values ListItrerator */ + private class ValuesListIterator implements ListIterator{ + + private final Object key; + + private List values; + private ListIterator iterator; + + public ValuesListIterator(Object key){ + this.key = key; + this.values = ListUtils.emptyIfNull((List) getMap().get(key)); + this.iterator = values.listIterator(); + } + + public ValuesListIterator(Object key, int index){ + this.key = key; + this.values = ListUtils.emptyIfNull((List) getMap().get(key)); + this.iterator = values.listIterator(index); + } + + @SuppressWarnings("unchecked") + public void add(V value) { + if (getMap().get(key) == null) { + List list = (List) createCollection(); + getMap().put((K) key, list); + this.values = list; + this.iterator = list.listIterator(); + } + this.iterator.add(value); + } + + public boolean hasNext() { + return iterator.hasNext(); + } + + public boolean hasPrevious() { + return iterator.hasPrevious(); + } + + public V next() { + return iterator.next(); + } + + public int nextIndex() { + return iterator.nextIndex(); + } + + public V previous() { + return iterator.previous(); + } + + public int previousIndex() { + return iterator.previousIndex(); + } + + public void remove() { + iterator.remove(); + if (values.isEmpty()) { + getMap().remove(key); + } + } + + public void set(V value) { + iterator.set(value); + } + + } + +} diff --git a/src/main/java/org/apache/commons/collections4/multimap/AbstractMultiValuedMap.java b/src/main/java/org/apache/commons/collections4/multimap/AbstractMultiValuedMap.java index eaeaa70fa..238675284 100644 --- a/src/main/java/org/apache/commons/collections4/multimap/AbstractMultiValuedMap.java +++ b/src/main/java/org/apache/commons/collections4/multimap/AbstractMultiValuedMap.java @@ -21,7 +21,6 @@ import java.lang.reflect.Array; import java.util.AbstractCollection; import java.util.ArrayList; import java.util.Collection; -import java.util.Collections; import java.util.Iterator; import java.util.Map; import java.util.Map.Entry; @@ -95,14 +94,14 @@ public class AbstractMultiValuedMap implements MultiValuedMap, Seria * * @param the collection type * @param map the map to wrap, must not be null - * @param initialCollectionCapacity the initial capacity of the collection * @param collectionClazz the collection class + * @param initialCollectionCapacity the initial capacity of the collection * @throws IllegalArgumentException if the map is null or if - * initialCollectionCapacity is negetive + * initialCollectionCapacity is negative */ @SuppressWarnings("unchecked") protected > AbstractMultiValuedMap(final Map map, - int initialCollectionCapacity, final Class collectionClazz) { + final Class collectionClazz, final int initialCollectionCapacity) { if (map == null) { throw new IllegalArgumentException("Map must not be null"); } @@ -179,16 +178,15 @@ public class AbstractMultiValuedMap implements MultiValuedMap, Seria /** * Removes all values associated with the specified key. *

- * A subsequent get(Object) would return null collection. + * A subsequent get(Object) would return an empty collection. * * @param key the key to remove values from - * @return the Collection of values removed, will return - * null for no mapping found. + * @return the Collection of values removed, will return an + * empty, unmodifiable collection for no mapping found. * @throws ClassCastException if the key is of an invalid type */ public Collection remove(Object key) { - Collection coll = getMap().remove(key); - return coll == null ? Collections.emptyList() : coll; + return CollectionUtils.emptyIfNull(getMap().remove(key)); } /** @@ -197,7 +195,7 @@ public class AbstractMultiValuedMap implements MultiValuedMap, Seria * The item is removed from the collection mapped to the specified key. * Other values attached to that key are unaffected. *

- * If the last value for a key is removed, null would be + * If the last value for a key is removed, an empty collection would be * returned from a subsequent get(Object). * * @param key the key to remove from @@ -455,15 +453,15 @@ public class AbstractMultiValuedMap implements MultiValuedMap, Seria /** * Wrapped collection to handle add and remove on the collection returned by get(object) */ - private class WrappedCollection implements Collection { + protected class WrappedCollection implements Collection { - private final Object key; + protected final Object key; public WrappedCollection(Object key) { this.key = key; } - private Collection getMapping() { + protected Collection getMapping() { return getMap().get(key); } diff --git a/src/main/java/org/apache/commons/collections4/multimap/AbstractSetValuedMap.java b/src/main/java/org/apache/commons/collections4/multimap/AbstractSetValuedMap.java new file mode 100644 index 000000000..13778b727 --- /dev/null +++ b/src/main/java/org/apache/commons/collections4/multimap/AbstractSetValuedMap.java @@ -0,0 +1,108 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 + * + * http://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.apache.commons.collections4.multimap; + +import java.util.Map; +import java.util.Set; + +import org.apache.commons.collections4.SetUtils; +import org.apache.commons.collections4.SetValuedMap; + +/** + * Abstract implementation of the {@link SetValuedMap} interface to simplify the + * creation of subclass implementations. + *

+ * Subclasses specify a Map implementation to use as the internal storage and + * the Set implementation to use as values. + * + * @since 4.1 + * @version $Id$ + */ +public abstract class AbstractSetValuedMap extends AbstractMultiValuedMap implements SetValuedMap { + + /** Serialization version */ + private static final long serialVersionUID = 3383617478898639862L; + + /** + * A constructor that wraps, not copies + * + * @param the set type + * @param map the map to wrap, must not be null + * @param setClazz the collection class + * @throws IllegalArgumentException if the map is null + */ + protected > AbstractSetValuedMap(Map map, Class setClazz) { + super(map, setClazz); + } + + /** + * A constructor that wraps, not copies + * + * @param the set type + * @param map the map to wrap, must not be null + * @param setClazz the collection class + * @param initialSetCapacity the initial size of the values set + * @throws IllegalArgumentException if the map is null or if + * initialSetCapacity is negative + */ + protected > AbstractSetValuedMap(Map map, Class setClazz, + int initialSetCapacity) { + super(map, setClazz, initialSetCapacity); + } + + /** + * Gets the set of values associated with the specified key. This would + * return an empty set in case the mapping is not present + * + * @param key the key to retrieve + * @return the Set of values, will return an empty + * Set for no mapping + * @throws ClassCastException if the key is of an invalid type + */ + @Override + public Set get(Object key) { + return new WrappedSet(key); + } + + /** + * Removes all values associated with the specified key. + *

+ * A subsequent get(Object) would return an empty set. + * + * @param key the key to remove values from + * @return the Set of values removed, will return an empty, + * unmodifiable set for no mapping found. + * @throws ClassCastException if the key is of an invalid type + */ + @Override + public Set remove(Object key) { + return SetUtils.emptyIfNull((Set) getMap().remove(key)); + } + + /** + * Wrapped set to handle add and remove on the collection returned by + * get(object) + */ + protected class WrappedSet extends WrappedCollection implements Set { + + public WrappedSet(Object key) { + super(key); + } + + } + +} diff --git a/src/main/java/org/apache/commons/collections4/multimap/MultiValuedHashMap.java b/src/main/java/org/apache/commons/collections4/multimap/MultiValuedHashMap.java index c098503b1..67c156ecb 100644 --- a/src/main/java/org/apache/commons/collections4/multimap/MultiValuedHashMap.java +++ b/src/main/java/org/apache/commons/collections4/multimap/MultiValuedHashMap.java @@ -19,9 +19,14 @@ package org.apache.commons.collections4.multimap; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; +import java.util.HashSet; +import java.util.List; import java.util.Map; +import java.util.Set; +import org.apache.commons.collections4.ListValuedMap; import org.apache.commons.collections4.MultiValuedMap; +import org.apache.commons.collections4.SetValuedMap; /** * Implements a {@link MultiValuedMap}, using a {@link HashMap} to provide data @@ -61,18 +66,59 @@ public class MultiValuedHashMap extends AbstractMultiValuedMap imple static final float DEFAULT_LOAD_FACTOR = 0.75f; /** - * Creates a MultiValuedHashMap which maps keys to collections of type - * collectionClass. + * Creates a {@link ListValuedMap} with a {@link HashMap} as its internal + * storage * * @param the key type * @param the value type - * @param the collection class type - * @param collectionClass the type of the collection class - * @return a new MultiValuedMap + * @return a new ListValuedMap */ - public static > MultiValuedMap multiValuedMap( - final Class collectionClass) { - return new MultiValuedHashMap(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR, collectionClass); + @SuppressWarnings({ "unchecked", "rawtypes" }) + public static ListValuedMap listValuedHashMap() { + return new ListValuedHashMap(ArrayList.class); + } + + /** + * Creates a {@link ListValuedMap} with a {@link HashMap} as its internal + * storage which maps the keys to list of type listClass + * + * @param the key type + * @param the value type + * @param the List class type + * @param listClass the class of the list + * @return a new ListValuedMap + */ + @SuppressWarnings({ "unchecked", "rawtypes" }) + public static > ListValuedMap listValuedHashMap(final Class listClass) { + return new ListValuedHashMap(listClass); + } + + /** + * Creates a {@link SetValuedMap} with a {@link HashMap} as its internal + * storage + * + * @param the key type + * @param the value type + * @return a new SetValuedMap + */ + @SuppressWarnings({ "unchecked", "rawtypes" }) + public static SetValuedMap setValuedHashMap() { + return new SetValuedHashMap(HashSet.class); + } + + /** + * Creates a {@link SetValuedMap} with a {@link HashMap} as its internal + * storage which maps the keys to a set of type setClass + * + * @param the key type + * @param the value type + * @param the Set class type + * @param setClass the class of the set + * @return a new SetValuedMap + */ + @SuppressWarnings({ "unchecked", "rawtypes" }) + public static > SetValuedMap setValuedHashMap(final Class setClass) { + return new SetValuedHashMap(setClass); } /** @@ -122,7 +168,7 @@ public class MultiValuedHashMap extends AbstractMultiValuedMap imple */ @SuppressWarnings("unchecked") public MultiValuedHashMap(int initialCapacity, float loadFactor, int initialCollectionCapacity) { - this(initialCapacity, loadFactor, initialCollectionCapacity, ArrayList.class); + this(initialCapacity, loadFactor, ArrayList.class, initialCollectionCapacity); } /** @@ -178,8 +224,40 @@ public class MultiValuedHashMap extends AbstractMultiValuedMap imple * create the value collections */ protected > MultiValuedHashMap(int initialCapacity, float loadFactor, - int initialCollectionCapacity, final Class collectionClazz) { - super(new HashMap>(initialCapacity, loadFactor), initialCollectionCapacity, collectionClazz); + final Class collectionClazz, int initialCollectionCapacity) { + super(new HashMap>(initialCapacity, loadFactor), collectionClazz, initialCollectionCapacity); + } + + /** Inner class for ListValuedMap */ + private static class ListValuedHashMap extends AbstractListValuedMap { + + private static final long serialVersionUID = 3667581458573135234L; + + public > ListValuedHashMap(Class listClazz) { + super(new HashMap>(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR), listClazz); + } + + public > ListValuedHashMap(Class listClazz, int initialListCapacity) { + super(new HashMap>(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR), listClazz, + initialListCapacity); + } + + } + + /** Inner class for SetValuedMap */ + private static class SetValuedHashMap extends AbstractSetValuedMap { + + private static final long serialVersionUID = -3817515514829894543L; + + public > SetValuedHashMap(Class setClazz) { + super(new HashMap>(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR), setClazz); + } + + public > SetValuedHashMap(Class setClazz, int initialSetCapacity) { + super(new HashMap>(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR), setClazz, + initialSetCapacity); + } + } } diff --git a/src/test/java/org/apache/commons/collections4/MultiMapUtilsTest.java b/src/test/java/org/apache/commons/collections4/MultiMapUtilsTest.java new file mode 100644 index 000000000..bf7bec40d --- /dev/null +++ b/src/test/java/org/apache/commons/collections4/MultiMapUtilsTest.java @@ -0,0 +1,154 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 + * + * http://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.apache.commons.collections4; + +import java.util.Collection; +import java.util.List; +import java.util.Set; + +import org.apache.commons.collections4.multimap.MultiValuedHashMap; + +import junit.framework.Test; + +/** + * Tests for MultiMapUtils + * + * @since 4.1 + * @version $Id$ + */ +public class MultiMapUtilsTest extends BulkTest { + + public static Test suite() { + return BulkTest.makeSuite(MultiMapUtilsTest.class); + } + + public MultiMapUtilsTest(String name) { + super(name); + } + + @SuppressWarnings({ "unchecked", "rawtypes" }) + public void testEmptyUnmodifiableMultiValuedMap() { + final MultiValuedMap map = MultiMapUtils.EMPTY_MULTI_VALUED_MAP; + assertTrue(map.isEmpty()); + try { + map.put("key", "value"); + fail("Should throw UnsupportedOperationException"); + } catch (UnsupportedOperationException e) { + } + } + + public void testTypeSafeEmptyMultiValuedMap() { + final MultiValuedMap map = MultiMapUtils.emptyMultiValuedMap(); + assertTrue(map.isEmpty()); + try { + map.put("key", "value"); + fail("Should throw UnsupportedOperationException"); + } catch (UnsupportedOperationException e) { + } + } + + public void testEmptyIfNull() { + assertTrue(MultiMapUtils.emptyIfNull(null).isEmpty()); + + final MultiValuedMap map = new MultiValuedHashMap(); + map.put("item", "value"); + assertFalse(MultiMapUtils.emptyIfNull(map).isEmpty()); + } + + public void testIsEmptyWithEmptyMap() { + final MultiValuedMap map = new MultiValuedHashMap(); + assertEquals(true, MultiMapUtils.isEmpty(map)); + } + + public void testIsEmptyWithNonEmptyMap() { + final MultiValuedMap map = new MultiValuedHashMap(); + map.put("item", "value"); + assertEquals(false, MultiMapUtils.isEmpty(map)); + } + + public void testIsEmptyWithNull() { + final MultiValuedMap map = null; + assertEquals(true, MultiMapUtils.isEmpty(map)); + } + + public void testGetCollection() { + assertNull(MultiMapUtils.getCollection(null, "key1")); + + String values[] = { "v1", "v2", "v3" }; + final MultiValuedMap map = new MultiValuedHashMap(); + for (String val : values) { + map.put("key1", val); + } + + Collection col = MultiMapUtils.getCollection(map, "key1"); + for (String val : values) { + assertTrue(col.contains(val)); + } + } + + public void testGetList() { + assertNull(MultiMapUtils.getList(null, "key1")); + + String values[] = { "v1", "v2", "v3" }; + final MultiValuedMap map = new MultiValuedHashMap(); + for (String val : values) { + map.put("key1", val); + } + + List list = MultiMapUtils.getList(map, "key1"); + int i = 0; + for (String val : list) { + assertTrue(val.equals(values[i++])); + } + } + + public void testGetSet() { + assertNull(MultiMapUtils.getList(null, "key1")); + + String values[] = { "v1", "v2", "v3" }; + final MultiValuedMap map = new MultiValuedHashMap(); + for (String val : values) { + map.put("key1", val); + map.put("key1", val); + } + + Set set = MultiMapUtils.getSet(map, "key1"); + assertEquals(3, set.size()); + for (String val : values) { + assertTrue(set.contains(val)); + } + } + + public void testGetBag() { + assertNull(MultiMapUtils.getBag(null, "key1")); + + String values[] = { "v1", "v2", "v3" }; + final MultiValuedMap map = new MultiValuedHashMap(); + for (String val : values) { + map.put("key1", val); + map.put("key1", val); + } + + Bag bag = MultiMapUtils.getBag(map, "key1"); + assertEquals(6, bag.size()); + for (String val : values) { + assertTrue(bag.contains(val)); + assertEquals(2, bag.getCount(val)); + } + } + +} diff --git a/src/test/java/org/apache/commons/collections4/multimap/AbstractMultiValuedMapTest.java b/src/test/java/org/apache/commons/collections4/multimap/AbstractMultiValuedMapTest.java index c326c6dc4..a43e8240b 100644 --- a/src/test/java/org/apache/commons/collections4/multimap/AbstractMultiValuedMapTest.java +++ b/src/test/java/org/apache/commons/collections4/multimap/AbstractMultiValuedMapTest.java @@ -43,7 +43,7 @@ import org.apache.commons.collections4.set.AbstractSetTest; *

* To use, extend this class and implement the {@link #makeObject} method and if * necessary override the {@link #makeFullMap()} method. - * + * * @since 4.1 * @version $Id$ */ diff --git a/src/test/java/org/apache/commons/collections4/multimap/MultiValuedHashMapTest.java b/src/test/java/org/apache/commons/collections4/multimap/MultiValuedHashMapTest.java index 804027bcd..267d79d20 100644 --- a/src/test/java/org/apache/commons/collections4/multimap/MultiValuedHashMapTest.java +++ b/src/test/java/org/apache/commons/collections4/multimap/MultiValuedHashMapTest.java @@ -16,17 +16,21 @@ */ package org.apache.commons.collections4.multimap; -import java.util.ArrayList; -import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.ListIterator; +import java.util.Set; import junit.framework.Test; import org.apache.commons.collections4.BulkTest; +import org.apache.commons.collections4.ListValuedMap; import org.apache.commons.collections4.MultiValuedMap; +import org.apache.commons.collections4.SetValuedMap; /** * Test MultValuedHashMap - * + * * @since 4.1 * @version $Id$ */ @@ -46,41 +50,117 @@ public class MultiValuedHashMapTest extends AbstractMultiValuedMapTest> MultiValuedHashMap createTestMap(final Class collectionClass) { - final MultiValuedHashMap map = - (MultiValuedHashMap) MultiValuedHashMap. multiValuedMap(collectionClass); - addSampleMappings(map); - return map; + @SuppressWarnings("unchecked") + public void testSetValuedMapAdd() { + final SetValuedMap setMap = MultiValuedHashMap.setValuedHashMap(); + assertTrue(setMap.get("whatever") instanceof Set); + + Set set = setMap.get("A"); + assertTrue(set.add((V) "a1")); + assertTrue(set.add((V) "a2")); + assertFalse(set.add((V) "a1")); + assertEquals(2, setMap.size()); + assertTrue(setMap.containsKey("A")); } @SuppressWarnings("unchecked") - public void testValueCollectionType() { - final MultiValuedHashMap map = createTestMap(LinkedList.class); - assertTrue(map.get("one") instanceof LinkedList); - }*/ + public void testSetValuedMapRemove() { + final SetValuedMap setMap = MultiValuedHashMap.setValuedHashMap(); + assertTrue(setMap.get("whatever") instanceof Set); - @SuppressWarnings("unchecked") - public void testPutWithList() { - final MultiValuedHashMap test = - (MultiValuedHashMap) MultiValuedHashMap.multiValuedMap(ArrayList.class); - assertEquals(true, test.put((K) "A", (V) "a")); - assertEquals(true, test.put((K) "A", (V) "b")); - assertEquals(true, test.put((K) "A", (V) "a")); - assertEquals(1, test.keySet().size()); - assertEquals(3, test.get("A").size()); - assertEquals(3, test.size()); + Set set = setMap.get("A"); + assertTrue(set.add((V) "a1")); + assertTrue(set.add((V) "a2")); + assertFalse(set.add((V) "a1")); + assertEquals(2, setMap.size()); + assertTrue(setMap.containsKey("A")); + + assertTrue(set.remove("a1")); + assertTrue(set.remove("a2")); + assertFalse(set.remove("a1")); + + assertEquals(0, setMap.size()); + assertFalse(setMap.containsKey("A")); } @SuppressWarnings("unchecked") - public void testPutWithSet() { - final MultiValuedHashMap test = - (MultiValuedHashMap) MultiValuedHashMap.multiValuedMap(HashSet.class); - assertEquals(true, test.put((K) "A", (V) "a")); - assertEquals(true, test.put((K) "A", (V) "b")); - assertEquals(false, test.put((K) "A", (V) "a")); - assertEquals(1, test.keySet().size()); - assertEquals(2, test.get("A").size()); - assertEquals(2, test.size()); + public void testSetValuedMapRemoveViaIterator() { + final SetValuedMap setMap = MultiValuedHashMap.setValuedHashMap(); + assertTrue(setMap.get("whatever") instanceof Set); + + Set set = setMap.get("A"); + set.add((V) "a1"); + set.add((V) "a2"); + set.add((V) "a1"); + + Iterator it = set.iterator(); + while (it.hasNext()) { + it.next(); + it.remove(); + } + assertEquals(0, setMap.size()); + assertFalse(setMap.containsKey("A")); + } + + @SuppressWarnings("unchecked") + public void testListValuedMapAdd() { + final ListValuedMap listMap = MultiValuedHashMap.listValuedHashMap(); + assertTrue(listMap.get("whatever") instanceof List); + List list = listMap.get("A"); + list.add((V) "a1"); + assertEquals(1, listMap.size()); + assertTrue(listMap.containsKey("A")); + } + + @SuppressWarnings("unchecked") + public void testListValuedMapAddViaListIterator() { + final ListValuedMap listMap = MultiValuedHashMap.listValuedHashMap(); + ListIterator listIt = listMap.get("B").listIterator(); + assertFalse(listIt.hasNext()); + listIt.add((V) "b1"); + listIt.add((V) "b2"); + listIt.add((V) "b3"); + assertEquals(3, listMap.size()); + assertTrue(listMap.containsKey("B")); + // As ListIterator always adds before the current cursor + assertFalse(listIt.hasNext()); + } + + @SuppressWarnings("unchecked") + public void testListValuedMapRemove() { + final ListValuedMap listMap = MultiValuedHashMap.listValuedHashMap(); + List list = listMap.get("A"); + list.add((V) "a1"); + list.add((V) "a2"); + list.add((V) "a3"); + assertEquals(3, listMap.size()); + assertEquals("a1", list.remove(0)); + assertEquals(2, listMap.size()); + assertEquals("a2", list.remove(0)); + assertEquals(1, listMap.size()); + assertEquals("a3", list.remove(0)); + assertEquals(0, listMap.size()); + assertFalse(listMap.containsKey("A")); + } + + @SuppressWarnings("unchecked") + public void testListValuedMapRemoveViaListIterator() { + final ListValuedMap listMap = MultiValuedHashMap.listValuedHashMap(); + ListIterator listIt = listMap.get("B").listIterator(); + listIt.add((V) "b1"); + listIt.add((V) "b2"); + assertEquals(2, listMap.size()); + assertTrue(listMap.containsKey("B")); + listIt = listMap.get("B").listIterator(); + while (listIt.hasNext()) { + listIt.next(); + listIt.remove(); + } + assertFalse(listMap.containsKey("B")); + listIt.add((V) "b1"); + listIt.add((V) "b2"); + assertTrue(listMap.containsKey("B")); + assertEquals(2, listMap.get("B").size()); } // public void testCreate() throws Exception {