diff --git a/src/main/java/org/apache/commons/collections4/ListValuedMap.java b/src/main/java/org/apache/commons/collections4/ListValuedMap.java new file mode 100644 index 000000000..b994228e1 --- /dev/null +++ b/src/main/java/org/apache/commons/collections4/ListValuedMap.java @@ -0,0 +1,69 @@ +/* + * 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.List; + +/** + * Defines a map that holds a list of values against each key. + *

+ * A ListValuedMap is a Map with slightly different semantics: + *

+ * + * @since 4.1 + * @version $Id$ + */ +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. + * + * @param key the key to retrieve + * @return the Collection of values, implementations should + * return null for no mapping, but may return an empty collection + * @throws ClassCastException if the key is of an invalid type + * @throws NullPointerException if the key is null and null keys are invalid + */ + List get(Object key); + + /** + * 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. + * + * @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 + * @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 + */ + List remove(Object key); + +} diff --git a/src/main/java/org/apache/commons/collections4/MultiValuedMap.java b/src/main/java/org/apache/commons/collections4/MultiValuedMap.java new file mode 100644 index 000000000..89c340e62 --- /dev/null +++ b/src/main/java/org/apache/commons/collections4/MultiValuedMap.java @@ -0,0 +1,287 @@ +/* + * 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.Map; +import java.util.Map.Entry; +import java.util.Set; + +/** + * Defines a map that holds a collection of values against each key. + *

+ * A MultiValuedMap is a Map with slightly different semantics: + *

+ *

+ * For example: + *

+ * MultiValuedMap<K, String> map = new MultiValuedHashMap<K, String>();
+ * map.put(key, "A");
+ * map.put(key, "B");
+ * map.put(key, "C");
+ * Collection<String> coll = map.get(key);
+ * 
+ *

+ * coll will be a collection containing "A", "B", "C". + *

+ * + * @since 4.1 + * @version $Id$ + */ +public interface MultiValuedMap { + // Query operations + + /** + * Gets the total size of the map. + *

+ * Implementations would return the total size of the map which is the count + * of the values from all keys. + * + * @return the total size of the map + */ + int size(); + + /** + * Returns true if this map contains no key-value mappings. + * + * @return true if this map contains no key-value mappings + */ + boolean isEmpty(); + + /** + * Returns true if this map contains a mapping for the specified + * key. More formally, returns true if and only if this map + * contains a mapping for a key k such that + * (key==null ? k==null : key.equals(k)). (There can be at most one + * such mapping.) + * + * @param key key whose presence in this map is to be tested + * @return true if this map contains a mapping for the specified key + * @throws ClassCastException if the key is of an inappropriate type for this map (optional) + * @throws NullPointerException if the specified key is null and this map + * does not permit null keys (optional) + */ + boolean containsKey(Object key); + + /** + * Checks whether the map contains at least one mapping for the specified value. + * + * @param value the value to search for + * @return true if the map contains the value + * @throws ClassCastException if the value is of an invalid type + * @throws NullPointerException if the value is null and null value are invalid + */ + boolean containsValue(Object value); + + /** + * Checks whether the map contains a mapping for the specified key and value. + * + * @param key the key to search for + * @param value the value to search for + * @return true if the map contains the value + */ + boolean containsMapping(Object key, Object value); + + /** + * Gets the collection of values associated with the specified key. + *

+ * Implementations are free to declare that they return + * Collection subclasses such as List or + * Set. + *

+ * 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. + * + * @param key the key to retrieve + * @return the Collection of values, implementations should + * return null for no mapping, but may return an empty collection + * @throws ClassCastException if the key is of an invalid type + * @throws NullPointerException if the key is null and null keys are invalid + */ + Collection get(Object key); + + // Modification operations + + /** + * Adds the value to the collection associated with the specified key. + *

+ * Unlike a normal Map the previous value is not replaced. + * Instead the new value is added to the collection stored against the key. + * The collection may be a List, Set or other + * collection dependent on implementation. + * + * @param key the key to store against + * @param value the value to add to the collection at the key + * @return typically the value added if the map changed and null if the map + * did not change + * @throws UnsupportedOperationException if the map is unmodifiable + * @throws ClassCastException if the key or value is of an invalid type + * @throws NullPointerException if the key or value is null and null is invalid + * @throws IllegalArgumentException if the key or value is invalid + */ + V put(K key, V value); + + /** + * Adds Iterable values to the collection associated with the specified key. + * + * @param key the key to store against + * @param values the values to add to the collection at the key, null ignored + * @return true if this map changed + */ + boolean putAll(K key, Iterable values); + + /** + * Copies all of the mappings from the specified map to this map (optional + * operation). The effect of this call is equivalent to that of calling + * {@link #put(Object,Object) put(k, v)} on this map once for each mapping + * from key k to value v in the specified map. The + * behavior of this operation is undefined if the specified map is modified + * while the operation is in progress. + * + * @param m mappings to be stored in this map + * @throws UnsupportedOperationException if the putAll operation is + * not supported by this map + * @throws ClassCastException if the class of a key or value in the + * specified map prevents it from being stored in this map + * @throws NullPointerException if the specified map is null, or if this map + * does not permit null keys or values, and the specified map + * contains null keys or values + * @throws IllegalArgumentException if some property of a key or value in + * the specified map prevents it from being stored in this map + */ + void putAll(Map m); + + /** + * Copies all of the mappings from the specified MultiValuedMap to this map + * (optional operation). The effect of this call is equivalent to that of + * calling {@link #put(Object,Object) put(k, v)} on this map once for each + * mapping from key k to value v in the specified map. The + * behavior of this operation is undefined if the specified map is modified + * while the operation is in progress. + * + * @param m mappings to be stored in this map + * @throws UnsupportedOperationException if the putAll operation is + * not supported by this map + * @throws ClassCastException if the class of a key or value in the + * specified map prevents it from being stored in this map + * @throws NullPointerException if the specified map is null, or if this map + * does not permit null keys or values, and the specified map + * contains null keys or values + * @throws IllegalArgumentException if some property of a key or value in + * the specified map prevents it from being stored in this map + */ + void putAll(MultiValuedMap m); + + /** + * 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. + * + * @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 + * @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 + */ + Collection remove(Object key); + + /** + * Removes a key-value mapping from the map. + *

+ * 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, implementations typically return + * null from a subsequent get(Object), however + * they may choose to return an empty collection. + * + * @param key the key to remove from + * @param item the item to remove + * @return {@code true} if the mapping was removed, {@code false} otherwise + * @throws UnsupportedOperationException if the map is unmodifiable + * @throws ClassCastException if the key or value is of an invalid type + * @throws NullPointerException if the key or value is null and null is + * invalid + */ + boolean removeMapping(K key, V item); + + /** + * Removes all of the mappings from this map (optional operation). + * The map will be empty after this call returns. + * + * @throws UnsupportedOperationException if the map is unmodifiable + */ + void clear(); + + // Views + + /** + * Returns a {@link Collection} view of the mappings contained in this map. + * The collection is backed by the map, so changes to the map are reflected + * in this, and vice-versa. + * + * @return a set view of the mappings contained in this map + */ + Collection> entries(); + + /** + * Returns a {@link Bag} view of the key mapping contained in this map. + *

+ * Implementations typically return a Bag of keys with its values count as + * the count of the Bag. This bag is backed by the map, so any changes in + * the map is reflected here. + * + * @return a bag view of the key mapping contained in this map + */ + Bag keys(); + + /** + * Returns a {@link Set} view of the keys contained in this map. The set is + * backed by the map, so changes to the map are reflected in the set, and + * vice-versa. If the map is modified while an iteration over the set is in + * progress (except through the iterator's own remove operation), + * the results of the iteration are undefined. The set supports element + * removal, which removes the corresponding mapping from the map, via the + * Iterator.remove, Set.remove, removeAll, + * retainAll, and clear operations. It does not support + * the add or addAll operations. + * + * @return a set view of the keys contained in this map + */ + Set keySet(); + + /** + * Gets a collection containing all the values in the map. + *

+ * Implementations typically return a collection containing the combination + * of values from all keys. + * + * @return a collection view of the values contained in this map + */ + Collection values(); + +} diff --git a/src/main/java/org/apache/commons/collections4/multimap/AbstractMultiValuedMap.java b/src/main/java/org/apache/commons/collections4/multimap/AbstractMultiValuedMap.java new file mode 100644 index 000000000..2b09cd1b2 --- /dev/null +++ b/src/main/java/org/apache/commons/collections4/multimap/AbstractMultiValuedMap.java @@ -0,0 +1,694 @@ +/* + * 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.lang.reflect.Array; +import java.util.AbstractCollection; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Iterator; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; + +import org.apache.commons.collections4.Bag; +import org.apache.commons.collections4.Factory; +import org.apache.commons.collections4.MultiValuedMap; +import org.apache.commons.collections4.Transformer; +import org.apache.commons.collections4.bag.HashBag; +import org.apache.commons.collections4.functors.InstantiateFactory; +import org.apache.commons.collections4.iterators.EmptyIterator; +import org.apache.commons.collections4.iterators.IteratorChain; +import org.apache.commons.collections4.iterators.LazyIteratorChain; +import org.apache.commons.collections4.iterators.TransformIterator; + +/** + * Abstract implementation of the {@link MultiValuedMap} interface to simplify + * the creation of subclass implementations. + *

+ * Subclasses specify a Map implementation to use as the internal storage. + * + * @since 4.1 + * @version $Id$ + */ +public class AbstractMultiValuedMap implements MultiValuedMap, Serializable { + + /** Serialization Version */ + private static final long serialVersionUID = 7994988366330224277L; + + /** The factory for creating value collections. */ + private final Factory> collectionFactory; + + /** The values view */ + private transient Collection valuesView; + + /** The EntryValues view */ + private transient EntryValues entryValuesView; + + /** The KeyBag view */ + private transient KeysBag keysBagView; + + /** The map used to store the data */ + private final Map> map; + + /** + * Constructor that wraps (not copies). + * + * @param the collection type + * @param map the map to wrap, must not be null + * @param collectionClazz the collection class + * @throws IllegalArgumentException if the map is null + */ + @SuppressWarnings("unchecked") + protected > AbstractMultiValuedMap(final Map map, + final Class collectionClazz) { + if (map == null) { + throw new IllegalArgumentException("Map must not be null"); + } + this.map = (Map>) map; + this.collectionFactory = new InstantiateFactory(collectionClazz); + } + + /** + * Gets the map being wrapped. + * + * @return the wrapped map + */ + protected Map> getMap() { + return map; + } + + /** + * {@inheritDoc} + */ + public boolean containsKey(Object key) { + return getMap().containsKey(key); + } + + /** + * {@inheritDoc} + */ + public boolean containsValue(final Object value) { + final Set>> pairs = getMap().entrySet(); + if (pairs != null) { + for (final Map.Entry> entry : pairs) { + if (entry.getValue().contains(value)) { + return true; + } + } + } + return false; + } + + /** + * {@inheritDoc} + */ + public boolean containsMapping(Object key, Object value) { + final Collection col = get(key); + if (col == null) { + return false; + } + return col.contains(value); + } + + /** + * {@inheritDoc} + */ + public Collection> entries() { + return entryValuesView != null ? entryValuesView : (entryValuesView = new EntryValues()); + } + + /** + * Gets the collection of values associated with the specified key. + * + * @param key the key to retrieve + * @return the Collection of values, will return + * null for no mapping + * @throws ClassCastException if the key is of an invalid type + */ + public Collection get(Object key) { + return getMap().get(key); + } + + /** + * Removes all values associated with the specified key. + *

+ * A subsequent get(Object) would return null collection. + * + * @param key the key to remove values from + * @return the Collection of values removed, will return + * null for no mapping found. + * @throws ClassCastException if the key is of an invalid type + */ + public Collection remove(Object key) { + return getMap().remove(key); + } + + /** + * Removes a specific value from map. + *

+ * 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 + * returned from a subsequent get(Object). + * + * @param key the key to remove from + * @param item the item to remove + * @return {@code true} if the mapping was removed, {@code false} otherwise + */ + public boolean removeMapping(K key, V item) { + boolean result = false; + final Collection col = get(key); + if (col == null) { + return false; + } + result = col.remove(item); + if (!result) { + return false; + } + if (col.isEmpty()) { + remove(key); + } + return true; + } + + /** + * {@inheritDoc} + */ + public boolean isEmpty() { + return getMap().isEmpty(); + } + + /** + * {@inheritDoc} + */ + public Set keySet() { + return getMap().keySet(); + } + + /** + * {@inheritDoc} + */ + public int size() { + int size = 0; + for (Collection col : getMap().values()) { + size += col.size(); + } + return size; + } + + /** + * Gets a collection containing all the values in the map. + *

+ * Returns a collection containing all the values from all keys. + * + * @return a collection view of the values contained in this map + */ + public Collection values() { + final Collection vs = valuesView; + return vs != null ? vs : (valuesView = new Values()); + } + + /** + * {@inheritDoc} + */ + public void clear() { + getMap().clear(); + } + + /** + * Adds the value to the collection associated with the specified key. + *

+ * Unlike a normal Map the previous value is not replaced. + * Instead the new value is added to the collection stored against the key. + * + * @param key the key to store against + * @param value the value to add to the collection at the key + * @return the value added if the map changed and null if the map did not + * change + */ + public V put(K key, V value) { + boolean result = false; + Collection coll = get(key); + if (coll == null) { + coll = createCollection(); + coll.add(value); + if (coll.size() > 0) { + // only add if non-zero size to maintain class state + getMap().put(key, coll); + result = true; // map definitely changed + } + } else { + result = coll.add(value); + } + return result ? value : null; + } + + /** + * Copies all of the mappings from the specified map to this map. The effect + * of this call is equivalent to that of calling {@link #put(Object,Object) + * put(k, v)} on this map once for each mapping from key k to value + * v in the specified map. The behavior of this operation is + * undefined if the specified map is modified while the operation is in + * progress. + * + * @param map mappings to be stored in this map + */ + public void putAll(final Map map) { + if (map != null) { + for (final Map.Entry entry : map.entrySet()) { + put((K) entry.getKey(), (V) entry.getValue()); + } + } + } + + /** + * Copies all of the mappings from the specified MultiValuedMap to this map. + * The effect of this call is equivalent to that of calling + * {@link #put(Object,Object) put(k, v)} on this map once for each mapping + * from key k to value v in the specified map. The + * behavior of this operation is undefined if the specified map is modified + * while the operation is in progress. + * + * @param map mappings to be stored in this map + */ + @SuppressWarnings("unchecked") + public void putAll(MultiValuedMap map) { + if (map != null) { + for (final K key : map.keySet()) { + putAll(key, (Collection) map.get(key)); + } + } + } + + /** + * Returns a {@link Bag} view of the key mapping contained in this map. + *

+ * Returns a Bag of keys with its values count as the count of the Bag. This + * bag is backed by the map, so any changes in the map is reflected here. + * Any method which modifies this bag like add, remove, + * Iterator.remove etc throws + * UnsupportedOperationException + * + * @return a bag view of the key mapping contained in this map + */ + public Bag keys() { + return keysBagView != null ? keysBagView : (keysBagView = new KeysBag()); + } + + /** + * Adds Iterable values to the collection associated with the specified key. + * + * @param key the key to store against + * @param values the values to add to the collection at the key, null + * ignored + * @return true if this map changed + */ + public boolean putAll(final K key, final Iterable values) { + if (values == null || values.iterator() == null || !values.iterator().hasNext()) { + return false; + } + Iterator it = values.iterator(); + boolean result = false; + Collection coll = get(key); + if (coll == null) { + coll = createCollection(); // might produce a non-empty collection + while (it.hasNext()) { + coll.add(it.next()); + } + if (coll.size() > 0) { + // only add if non-zero size to maintain class state + getMap().put(key, coll); + result = true; // map definitely changed + } + } else { + while (it.hasNext()) { + boolean tmpResult = coll.add(it.next()); + if (!result && tmpResult) { + // If any one of the values have been added, the map has + // changed + result = true; + } + } + } + return result; + } + + /** + * Gets an iterator for the collection mapped to the specified key. + * + * @param key the key to get an iterator for + * @return the iterator of the collection at the key, empty iterator if key + * not in map + */ + public Iterator iterator(final Object key) { + if (!containsKey(key)) { + return EmptyIterator. emptyIterator(); + } + return new ValuesIterator(key); + } + + /** + * Gets the size of the collection mapped to the specified key. + * + * @param key the key to get size for + * @return the size of the collection at the key, zero if key not in map + */ + public int size(final Object key) { + final Collection coll = get(key); + if (coll == null) { + return 0; + } + return coll.size(); + } + + @SuppressWarnings("rawtypes") + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (obj instanceof MultiValuedMap == false) { + return false; + } + MultiValuedMap other = (MultiValuedMap) obj; + if (other.size() != size()) { + return false; + } + Iterator it = keySet().iterator(); + while (it.hasNext()) { + Object key = it.next(); + Collection col = get(key); + Collection otherCol = other.get(key); + if (otherCol == null) { + return false; + } + if (col.size() != otherCol.size()) { + return false; + } + for (Object value : col) { + if (!otherCol.contains(value)) { + return false; + } + } + } + return true; + } + + @Override + public int hashCode() { + return getMap().hashCode(); + } + + @Override + public String toString() { + return getMap().toString(); + } + + // ----------------------------------------------------------------------- + + protected Collection createCollection() { + return collectionFactory.create(); + } + + // ----------------------------------------------------------------------- + + /** + * Inner class that provides a Bag keys view + */ + private class KeysBag implements Bag { + + public boolean addAll(Collection c) { + throw new UnsupportedOperationException(); + } + + public void clear() { + throw new UnsupportedOperationException(); + } + + public boolean contains(Object o) { + return getMap().containsKey(o); + } + + public boolean isEmpty() { + return getMap().isEmpty(); + } + + public Object[] toArray() { + final Object[] result = new Object[size()]; + int i = 0; + final Iterator it = getMap().keySet().iterator(); + while (it.hasNext()) { + final K current = it.next(); + for (int index = getCount(current); index > 0; index--) { + result[i++] = current; + } + } + return result; + } + + public T[] toArray(T[] array) { + final int size = size(); + if (array.length < size) { + @SuppressWarnings("unchecked") + // safe as both are of type T + final T[] unchecked = (T[]) Array.newInstance(array.getClass().getComponentType(), size); + array = unchecked; + } + + int i = 0; + final Iterator it = getMap().keySet().iterator(); + while (it.hasNext()) { + final K current = it.next(); + for (int index = getCount(current); index > 0; index--) { + // unsafe, will throw ArrayStoreException if types are not + // compatible, see javadoc + @SuppressWarnings("unchecked") + final T unchecked = (T) current; + array[i++] = unchecked; + } + } + while (i < array.length) { + array[i++] = null; + } + return array; + } + + public int getCount(Object object) { + int count = 0; + Collection col = AbstractMultiValuedMap.this.getMap().get(object); + if (col != null) { + count = col.size(); + } + return count; + } + + public boolean add(K object) { + throw new UnsupportedOperationException(); + } + + public boolean add(K object, int nCopies) { + throw new UnsupportedOperationException(); + } + + public boolean remove(Object object) { + throw new UnsupportedOperationException(); + } + + public boolean remove(Object object, int nCopies) { + throw new UnsupportedOperationException(); + } + + public Set uniqueSet() { + return keySet(); + } + + public int size() { + return AbstractMultiValuedMap.this.size(); + } + + public boolean containsAll(Collection coll) { + if (coll instanceof Bag) { + return containsAll((Bag) coll); + } + return containsAll(new HashBag(coll)); + } + + private boolean containsAll(final Bag other) { + final Iterator it = other.uniqueSet().iterator(); + while (it.hasNext()) { + final Object current = it.next(); + if (getCount(current) < other.getCount(current)) { + return false; + } + } + return true; + } + + public boolean removeAll(Collection coll) { + throw new UnsupportedOperationException(); + } + + public boolean retainAll(Collection coll) { + throw new UnsupportedOperationException(); + } + + public Iterator iterator() { + return new LazyIteratorChain() { + + final Iterator keyIterator = getMap().keySet().iterator(); + + @Override + protected Iterator nextIterator(int count) { + if (!keyIterator.hasNext()) { + return null; + } + final K key = keyIterator.next(); + final Iterator colIterator = getMap().get(key).iterator(); + Iterator nextIt = new Iterator() { + + public boolean hasNext() { + return colIterator.hasNext(); + } + + public K next() { + colIterator.next();// Increment the iterator + // The earlier statement would throw + // NoSuchElementException anyway in case it ends + return key; + } + + public void remove() { + throw new UnsupportedOperationException(); + } + }; + return nextIt; + } + }; + } + + } + + /** + * Inner class that provides the Entry view + */ + private class EntryValues extends AbstractCollection> { + + @Override + public Iterator> iterator() { + return new LazyIteratorChain>() { + + final Collection keysCol = new ArrayList(getMap().keySet()); + final Iterator keyIterator = keysCol.iterator(); + + @Override + protected Iterator> nextIterator(int count) { + if (!keyIterator.hasNext()) { + return null; + } + final K key = keyIterator.next(); + final Transformer> entryTransformer = new Transformer>() { + + public Entry transform(final V input) { + return new Entry() { + + public K getKey() { + return key; + } + + public V getValue() { + return input; + } + + public V setValue(V value) { + throw new UnsupportedOperationException(); + } + }; + } + }; + return new TransformIterator>(new ValuesIterator(key), entryTransformer); + } + }; + } + + @Override + public int size() { + return AbstractMultiValuedMap.this.size(); + } + + } + + /** + * Inner class that provides the values view. + */ + private class Values extends AbstractCollection { + @Override + public Iterator iterator() { + final IteratorChain chain = new IteratorChain(); + for (final K k : keySet()) { + chain.addIterator(new ValuesIterator(k)); + } + return chain; + } + + @Override + public int size() { + return AbstractMultiValuedMap.this.size(); + } + + @Override + public void clear() { + AbstractMultiValuedMap.this.clear(); + } + } + + /** + * Inner class that provides the values iterator. + */ + private class ValuesIterator implements Iterator { + private final Object key; + private final Collection values; + private final Iterator iterator; + + public ValuesIterator(final Object key) { + this.key = key; + this.values = get(key); + this.iterator = values.iterator(); + } + + public void remove() { + iterator.remove(); + if (values.isEmpty()) { + AbstractMultiValuedMap.this.remove(key); + } + } + + public boolean hasNext() { + return iterator.hasNext(); + } + + public V next() { + return iterator.next(); + } + } + +} diff --git a/src/main/java/org/apache/commons/collections4/multimap/AbstractMultiValuedMapDecorator.java b/src/main/java/org/apache/commons/collections4/multimap/AbstractMultiValuedMapDecorator.java new file mode 100644 index 000000000..bf54ec57b --- /dev/null +++ b/src/main/java/org/apache/commons/collections4/multimap/AbstractMultiValuedMapDecorator.java @@ -0,0 +1,153 @@ +/* + * 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.Map; +import java.util.Map.Entry; +import java.util.Set; + +import org.apache.commons.collections4.Bag; +import org.apache.commons.collections4.MultiValuedMap; + +/** + * Decorates another MultiValuedMap to provide additional behaviour. + *

+ * Each method call made on this MultiValuedMap is forwarded to the + * decorated MultiValuedMap. This class is used as a framework to + * build to extensions such as synchronized and unmodifiable behaviour. + * + * @param the type of key elements + * @param the type of value elements + * + * @since 4.1 + * @version $Id$ + */ +public class AbstractMultiValuedMapDecorator + implements MultiValuedMap, Serializable { + + /** Serialization version */ + private static final long serialVersionUID = -9184930955231260637L; + + /** MultiValuedMap to decorate */ + private final MultiValuedMap map; + + /** + * Constructor that wraps (not copies). + * + * @param map the map to decorate, must not be null + * @throws IllegalArgumentException if the map is null + */ + protected AbstractMultiValuedMapDecorator(final MultiValuedMap map) { + if (map == null) { + throw new IllegalArgumentException("MultiValuedMap must not be null"); + } + this.map = map; + } + + protected MultiValuedMap decorated() { + return map; + } + + public int size() { + return decorated().size(); + } + + public boolean isEmpty() { + return decorated().isEmpty(); + } + + public boolean containsKey(Object key) { + return decorated().containsKey(key); + } + + public boolean containsValue(Object value) { + return decorated().containsValue(value); + } + + public boolean containsMapping(Object key, Object value) { + return decorated().containsMapping(key, value); + } + + public Collection get(Object key) { + return decorated().get(key); + } + + public Collection remove(Object key) { + return decorated().remove(key); + } + + public boolean removeMapping(K key, V item) { + return decorated().removeMapping(key, item); + } + + public void clear() { + decorated().clear(); + } + + public V put(K key, V value) { + return decorated().put(key, value); + } + + public Set keySet() { + return decorated().keySet(); + } + + public Collection> entries() { + return decorated().entries(); + } + + public Bag keys() { + return decorated().keys(); + } + + public Collection values() { + return decorated().values(); + } + + public boolean putAll(K key, Iterable values) { + return decorated().putAll(key, values); + } + + public void putAll(Map m) { + decorated().putAll(m); + } + + public void putAll(MultiValuedMap m) { + decorated().putAll(m); + } + + @Override + public boolean equals(final Object object) { + if (object == this) { + return true; + } + return decorated().equals(object); + } + + @Override + public int hashCode() { + return decorated().hashCode(); + } + + @Override + public String toString() { + return decorated().toString(); + } + +} diff --git a/src/main/java/org/apache/commons/collections4/multimap/MultiValuedHashMap.java b/src/main/java/org/apache/commons/collections4/multimap/MultiValuedHashMap.java new file mode 100644 index 000000000..c6ce934e6 --- /dev/null +++ b/src/main/java/org/apache/commons/collections4/multimap/MultiValuedHashMap.java @@ -0,0 +1,111 @@ +/* + * 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.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; + +import org.apache.commons.collections4.MultiValuedMap; + +/** + * Implements a {@link MultiValuedMap}, using a {@link HashMap} to provide data + * storage. This is the standard implementation of a MultiValuedMap + *

+ * A MultiValuedMap is a Map with slightly different semantics. + * Putting a value into the map will add the value to a Collection at that key. + * Getting a value will return a Collection, holding all the values put to that + * key + *

+ * In addition, this implementation allows the type of collection used for the + * values to be controlled. By default, an ArrayList is used, + * however a Class to instantiate the value + * collection may be specified. + *

+ * Note that MultiValuedHashMap is not synchronized and is not + * thread-safe. If you wish to use this map from multiple threads + * concurrently, you must use appropriate synchronization. This class may throw + * exceptions when accessed by concurrent threads without synchronization. + * + * @since 4.1 + * @version $Id$ + */ +public class MultiValuedHashMap extends AbstractMultiValuedMap implements MultiValuedMap { + + /** Serialization Version */ + private static final long serialVersionUID = -5845183518195365857L; + + /** + * Creates a MultiValuedHashMap which maps keys to collections of type + * collectionClass. + * + * @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 + */ + public static > MultiValuedMap multiValuedMap( + final Class collectionClass) { + return new MultiValuedHashMap(collectionClass); + } + + /** + * Creates a MultiValueMap based on a HashMap which stores the + * multiple values in an ArrayList. + */ + @SuppressWarnings("unchecked") + public MultiValuedHashMap() { + this(ArrayList.class); + } + + /** + * Creates a MultiValuedHashMap copying all the mappings of the given map. + * + * @param map a MultiValuedMap to copy into this map + */ + @SuppressWarnings("unchecked") + public MultiValuedHashMap(final MultiValuedMap map) { + this(ArrayList.class); + super.putAll(map); + } + + /** + * Creates a MultiValuedHashMap copying all the mappings of the given map. + * + * @param map a Map to copy into this map + */ + @SuppressWarnings("unchecked") + public MultiValuedHashMap(final Map map) { + this(ArrayList.class); + super.putAll(map); + } + + /** + * Creates a MultiValuedHashMap which creates the value collections using + * the supplied collectionClazz. + * + * @param the collection type + * @param collectionClazz the class of the Collection to use to + * create the value collections + */ + protected > MultiValuedHashMap(final Class collectionClazz) { + super(new HashMap>(), collectionClazz); + } + +} diff --git a/src/main/java/org/apache/commons/collections4/multimap/TransformedMultiValuedMap.java b/src/main/java/org/apache/commons/collections4/multimap/TransformedMultiValuedMap.java new file mode 100644 index 000000000..06bf0934d --- /dev/null +++ b/src/main/java/org/apache/commons/collections4/multimap/TransformedMultiValuedMap.java @@ -0,0 +1,236 @@ +/* + * 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.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; + +import org.apache.commons.collections4.MultiValuedMap; +import org.apache.commons.collections4.Transformer; +import org.apache.commons.collections4.map.LinkedMap; + +/** + * Decorates another MultiValuedMap to transform objects that are added. + *

+ * This class affects the MultiValuedMap put methods. Thus objects must be + * removed or searched for using their transformed form. For example, if the + * transformation converts Strings to Integers, you must use the Integer form to + * remove objects. + *

+ * Note that TransformedMultiValuedMap is not synchronized and is not thread-safe. + * + * @since 4.1 + * @version $Id$ + */ +public class TransformedMultiValuedMap extends AbstractMultiValuedMapDecorator { + + /** Serialization Version */ + private static final long serialVersionUID = -1254147899086470720L; + + private final Transformer keyTransformer; + + private final Transformer valueTransformer; + + /** + * Factory method to create a transforming MultiValuedMap. + *

+ * If there are any elements already in the map being decorated, they are + * NOT transformed. Contrast this with + * {@link #transformedMap(MultiValuedMap, Transformer, Transformer)}. + * + * @param the key type + * @param the value type + * @param map the MultiValuedMap to decorate, must not be null + * @param keyTransformer the transformer to use for key conversion, null + * means no transformation + * @param valueTransformer the transformer to use for value conversion, null + * means no transformation + * @return a new transformed MultiValuedMap + * @throws IllegalArgumentException if map is null + */ + public static TransformedMultiValuedMap transformingMap(final MultiValuedMap map, + final Transformer keyTransformer, + final Transformer valueTransformer) { + return new TransformedMultiValuedMap(map, keyTransformer, valueTransformer); + } + + /** + * Factory method to create a transforming MultiValuedMap that will + * transform existing contents of the specified map. + *

+ * If there are any elements already in the map being decorated, they will + * be transformed by this method. Contrast this with + * {@link #transformingMap(MultiValuedMap, Transformer, Transformer)}. + * + * @param the key type + * @param the value type + * @param map the MultiValuedMap to decorate, must not be null + * @param keyTransformer the transformer to use for key conversion, null + * means no transformation + * @param valueTransformer the transformer to use for value conversion, null + * means no transformation + * @return a new transformed MultiValuedMap + * @throws IllegalArgumentException if map is null + */ + public static TransformedMultiValuedMap transformedMap(final MultiValuedMap map, + final Transformer keyTransformer, + final Transformer valueTransformer) { + final TransformedMultiValuedMap decorated = + new TransformedMultiValuedMap(map, keyTransformer, valueTransformer); + if (map.size() > 0) { + MultiValuedMap transformed = decorated.transformMultiValuedMap(map); + decorated.clear(); + // to avoid double transform + decorated.decorated().putAll(transformed); + } + return decorated; + } + + // ----------------------------------------------------------------------- + /** + * Constructor that wraps (not copies). + *

+ * If there are any elements already in the collection being decorated, they + * are NOT transformed. + * + * @param map the MultiValuedMap to decorate, must not be null + * @param keyTransformer the transformer to use for key conversion, null + * means no conversion + * @param valueTransformer the transformer to use for value conversion, null + * means no conversion + * @throws IllegalArgumentException if map is null + */ + protected TransformedMultiValuedMap(MultiValuedMap map, + Transformer keyTransformer, Transformer valueTransformer) { + super(map); + this.keyTransformer = keyTransformer; + this.valueTransformer = valueTransformer; + } + + /** + * Transforms a key. + *

+ * The transformer itself may throw an exception if necessary. + * + * @param object the object to transform + * @return the transformed object + */ + protected K transformKey(final K object) { + if (keyTransformer == null) { + return object; + } + return keyTransformer.transform(object); + } + + /** + * Transforms a value. + *

+ * The transformer itself may throw an exception if necessary. + * + * @param object the object to transform + * @return the transformed object + */ + protected V transformValue(final V object) { + if (valueTransformer == null) { + return object; + } + return valueTransformer.transform(object); + } + + /** + * Transforms a map. + *

+ * The transformer itself may throw an exception if necessary. + * + * @param map the map to transform + * @return the transformed object + */ + @SuppressWarnings("unchecked") + protected Map transformMap(final Map map) { + if (map.isEmpty()) { + return (Map) map; + } + final Map result = new LinkedMap(map.size()); + + for (final Map.Entry entry : map.entrySet()) { + result.put(transformKey(entry.getKey()), transformValue(entry.getValue())); + } + return result; + } + + /** + * Transforms a MultiValuedMap. + *

+ * The transformer itself may throw an exception if necessary. + * + * @param map the MultiValuedMap to transform + * @return the transformed object + */ + @SuppressWarnings("unchecked") + protected MultiValuedMap transformMultiValuedMap(final MultiValuedMap map) { + if (map.isEmpty()) { + return (MultiValuedMap) map; + } + final MultiValuedMap result = new MultiValuedHashMap(); + + for (final Map.Entry entry : map.entries()) { + result.put(transformKey(entry.getKey()), transformValue(entry.getValue())); + } + return result; + } + + @Override + public V put(K key, V value) { + K transformedKey = transformKey(key); + V transformedValue = transformValue(value); + return decorated().put(transformedKey, transformedValue); + } + + @Override + @SuppressWarnings("unchecked") + public boolean putAll(K key, Iterable values) { + if (values == null || values.iterator() == null || !values.iterator().hasNext()) { + return false; + } + K transformedKey = transformKey(key); + List transformedValues = new LinkedList(); + Iterator it = (Iterator) values.iterator(); + while (it.hasNext()) { + transformedValues.add(transformValue(it.next())); + } + return decorated().putAll(transformedKey, transformedValues); + } + + @Override + public void putAll(Map m) { + if (m == null) { + return; + } + decorated().putAll(transformMap(m)); + } + + @Override + public void putAll(MultiValuedMap m) { + if (m == null) { + return; + } + decorated().putAll(transformMultiValuedMap(m)); + } + +} diff --git a/src/main/java/org/apache/commons/collections4/multimap/UnmodifiableMultiValuedMap.java b/src/main/java/org/apache/commons/collections4/multimap/UnmodifiableMultiValuedMap.java new file mode 100644 index 000000000..1ef918732 --- /dev/null +++ b/src/main/java/org/apache/commons/collections4/multimap/UnmodifiableMultiValuedMap.java @@ -0,0 +1,134 @@ +/* + * 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.Collection; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; + +import org.apache.commons.collections4.Bag; +import org.apache.commons.collections4.MultiValuedMap; +import org.apache.commons.collections4.Unmodifiable; +import org.apache.commons.collections4.bag.UnmodifiableBag; +import org.apache.commons.collections4.collection.UnmodifiableCollection; +import org.apache.commons.collections4.set.UnmodifiableSet; + +/** + * Decorates another {@link MultiValuedMap} to ensure it can't be altered. + *

+ * Attempts to modify it will result in an UnsupportedOperationException. + * + * @param the type of key elements + * @param the type of value elements + * + * @since 4.1 + * @version $Id$ + */ +public class UnmodifiableMultiValuedMap + extends AbstractMultiValuedMapDecorator implements Unmodifiable { + + /** Serialization version */ + private static final long serialVersionUID = 1418669828214151566L; + + /** + * Factory method to create an unmodifiable MultiValuedMap. + *

+ * If the map passed in is already unmodifiable, it is returned. + * + * @param the type of key elements + * @param the type of value elements + * @param map the map to decorate, must not be null + * @return an unmodifiable MultiValuedMap + * @throws IllegalArgumentException if map is null + */ + @SuppressWarnings("unchecked") + public static UnmodifiableMultiValuedMap + unmodifiableMultiValuedMap(MultiValuedMap map) { + if (map instanceof Unmodifiable) { + return (UnmodifiableMultiValuedMap) map; + } + return new UnmodifiableMultiValuedMap(map); + } + + /** + * Constructor that wraps (not copies). + * + * @param map the MultiValuedMap to decorate, must not be null + * @throws IllegalArgumentException if the map is null + */ + @SuppressWarnings("unchecked") + private UnmodifiableMultiValuedMap(final MultiValuedMap map) { + super((MultiValuedMap) map); + } + + @Override + public Collection remove(Object key) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean removeMapping(K key, V item) { + throw new UnsupportedOperationException(); + } + + @Override + public void clear() { + throw new UnsupportedOperationException(); + } + + @Override + public V put(K key, V value) { + throw new UnsupportedOperationException(); + } + + @Override + public Set keySet() { + return UnmodifiableSet.unmodifiableSet(decorated().keySet()); + } + + @Override + public Collection> entries() { + return UnmodifiableCollection.>unmodifiableCollection(decorated().entries()); + } + + @Override + public Bag keys() { + return UnmodifiableBag.unmodifiableBag(decorated().keys()); + } + + @Override + public Collection values() { + return UnmodifiableCollection.unmodifiableCollection(decorated().values()); + } + + @Override + public boolean putAll(K key, Iterable values) { + throw new UnsupportedOperationException(); + } + + @Override + public void putAll(Map m) { + throw new UnsupportedOperationException(); + } + + @Override + public void putAll(MultiValuedMap m) { + throw new UnsupportedOperationException(); + } + +} diff --git a/src/main/java/org/apache/commons/collections4/multimap/package-info.java b/src/main/java/org/apache/commons/collections4/multimap/package-info.java new file mode 100644 index 000000000..a884a8edc --- /dev/null +++ b/src/main/java/org/apache/commons/collections4/multimap/package-info.java @@ -0,0 +1,34 @@ +/* + * 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. + */ +/** + * This package contains implementations of the {@link org.apache.commons.collections4.MultiValuedMap} interfaces. + * A MultiValuedMap holds a collection of values against each key. + *

+ * The following implementations are provided in the package: + *

    + *
  • MultiValuedHashMap - implementation that uses a HashMap to store the data + *
+ *

+ * The following decorators are provided in the package: + *

    + *
  • Transformed - transforms elements added to the MultiValuedMap + *
  • Unmodifiable - ensures the collection cannot be altered + *
+ * + * @version $Id$ + */ +package org.apache.commons.collections4.multimap; diff --git a/src/test/java/org/apache/commons/collections4/multimap/AbstractMultiValuedMapTest.java b/src/test/java/org/apache/commons/collections4/multimap/AbstractMultiValuedMapTest.java new file mode 100644 index 000000000..771ea17e8 --- /dev/null +++ b/src/test/java/org/apache/commons/collections4/multimap/AbstractMultiValuedMapTest.java @@ -0,0 +1,494 @@ +/* + * 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.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Map; + +import org.apache.commons.collections4.AbstractObjectTest; +import org.apache.commons.collections4.Bag; +import org.apache.commons.collections4.MultiValuedMap; +import org.apache.commons.collections4.bag.HashBag; + +/** + * Abstract test class for {@link MultiValuedMap} contract and methods. + *

+ * To use, extend this class and implement the {@link #makeObject} method and if + * necessary override the {@link #makeFullMap()} method. + * + * @since 4.1 + * @version $Id$ + */ +public abstract class AbstractMultiValuedMapTest extends AbstractObjectTest { + + public AbstractMultiValuedMapTest(String testName) { + super(testName); + } + + @Override + abstract public MultiValuedMap makeObject(); + + @Override + public String getCompatibilityVersion() { + return "4.1"; // MultiValuedMap has been added in version 4.1 + } + + /** + * Returns true if the maps produced by {@link #makeObject()} and + * {@link #makeFullMap()} support the put and + * putAll operations adding new mappings. + *

+ * Default implementation returns true. Override if your collection class + * does not support put adding. + */ + public boolean isAddSupported() { + return true; + } + + /** + * Returns true if the maps produced by {@link #makeObject()} and + * {@link #makeFullMap()} support the remove and + * clear operations. + *

+ * Default implementation returns true. Override if your collection class + * does not support removal operations. + */ + public boolean isRemoveSupported() { + return true; + } + + protected MultiValuedMap makeFullMap() { + final MultiValuedMap map = makeObject(); + addSampleMappings(map); + return map; + } + + @SuppressWarnings("unchecked") + protected void addSampleMappings(MultiValuedMap map) { + map.put((K) "one", (V) "uno"); + map.put((K) "one", (V) "un"); + map.put((K) "two", (V) "dos"); + map.put((K) "two", (V) "deux"); + map.put((K) "three", (V) "tres"); + map.put((K) "three", (V) "trois"); + } + + public void testNoMappingReturnsNull() { + final MultiValuedMap map = makeFullMap(); + assertNull(map.get("whatever")); + } + + public void testMultipleValues() { + final MultiValuedMap map = makeFullMap(); + Collection col = map.get("one"); + assertTrue(col.contains("uno")); + assertTrue(col.contains("un")); + } + + public void testGet() { + final MultiValuedMap map = makeFullMap(); + assertTrue(map.get("one").contains("uno")); + assertTrue(map.get("one").contains("un")); + assertTrue(map.get("two").contains("dos")); + assertTrue(map.get("two").contains("deux")); + assertTrue(map.get("three").contains("tres")); + assertTrue(map.get("three").contains("trois")); + } + + public void testContainsValue() { + final MultiValuedMap map = makeFullMap(); + assertTrue(map.containsValue("uno")); + assertTrue(map.containsValue("un")); + assertTrue(map.containsValue("dos")); + assertTrue(map.containsValue("deux")); + assertTrue(map.containsValue("tres")); + assertTrue(map.containsValue("trois")); + assertFalse(map.containsValue("quatro")); + } + + public void testKeyContainsValue() { + final MultiValuedMap map = makeFullMap(); + assertTrue(map.containsMapping("one", "uno")); + assertTrue(map.containsMapping("one", "un")); + assertTrue(map.containsMapping("two", "dos")); + assertTrue(map.containsMapping("two", "deux")); + assertTrue(map.containsMapping("three", "tres")); + assertTrue(map.containsMapping("three", "trois")); + assertFalse(map.containsMapping("four", "quatro")); + } + + @SuppressWarnings("unchecked") + public void testValues() { + final MultiValuedMap map = makeFullMap(); + final HashSet expected = new HashSet(); + expected.add((V) "uno"); + expected.add((V) "dos"); + expected.add((V) "tres"); + expected.add((V) "un"); + expected.add((V) "deux"); + expected.add((V) "trois"); + final Collection c = map.values(); + assertEquals(6, c.size()); + assertEquals(expected, new HashSet(c)); + } + +// public void testKeyedIterator() { +// final MultiValuedMap map = makeFullMap(); +// final ArrayList actual = new ArrayList(IteratorUtils.toList(map.iterator("one"))); +// final ArrayList expected = new ArrayList(Arrays.asList("uno", "un")); +// assertEquals(expected, actual); +// } + + public void testRemoveAllViaIterator() { + if (!isRemoveSupported()) { + return; + } + final MultiValuedMap map = makeFullMap(); + for (final Iterator i = map.values().iterator(); i.hasNext();) { + i.next(); + i.remove(); + } + assertNull(map.get("one")); + assertTrue(map.isEmpty()); + } + +// public void testRemoveAllViaKeyedIterator() { +// if (!isRemoveSupported()) { +// return; +// } +// final MultiValuedMap map = makeFullMap(); +// for (final Iterator i = map.iterator("one"); i.hasNext();) { +// i.next(); +// i.remove(); +// } +// assertNull(map.get("one")); +// assertEquals(4, map.size()); +// } + + public void testEntriesCollectionIterator() { + final MultiValuedMap map = makeFullMap(); + Collection values = new ArrayList(map.values()); + Iterator> iterator = map.entries().iterator(); + while (iterator.hasNext()) { + Map.Entry entry = iterator.next(); + assertTrue(map.containsMapping(entry.getKey(), entry.getValue())); + assertTrue(values.contains(entry.getValue())); + if (isRemoveSupported()) { + assertTrue(values.remove(entry.getValue())); + } + } + if (isRemoveSupported()) { + assertTrue(values.isEmpty()); + } + } + + public void testRemoveAllViaEntriesIterator() { + if (!isRemoveSupported()) { + return; + } + final MultiValuedMap map = makeFullMap(); + for (final Iterator i = map.entries().iterator(); i.hasNext();) { + i.next(); + i.remove(); + } + assertNull(map.get("one")); + assertEquals(0, map.size()); + } + + public void testSize() { + assertEquals(6, makeFullMap().size()); + } + + // ----------------------------------------------------------------------- + @SuppressWarnings("unchecked") + public void testMapEquals() { + if (!isAddSupported()) { + return; + } + final MultiValuedMap one = makeObject(); + final Integer value = Integer.valueOf(1); + one.put((K) "One", (V) value); + one.removeMapping((K) "One", (V) value); + + final MultiValuedMap two = makeObject(); + assertEquals(two, one); + } + + @SuppressWarnings("unchecked") + public void testSizeWithPutRemove() { + if (!isRemoveSupported() || !isAddSupported()) { + return; + } + final MultiValuedMap map = makeObject(); + assertEquals(0, map.size()); + map.put((K) "A", (V) "AA"); + assertEquals(1, map.size()); + map.put((K) "B", (V) "BA"); + assertEquals(2, map.size()); + map.put((K) "B", (V) "BB"); + assertEquals(3, map.size()); + map.put((K) "B", (V) "BC"); + assertEquals(4, map.size()); + map.remove("A"); + assertEquals(3, map.size()); + map.removeMapping((K) "B", (V) "BC"); + assertEquals(2, map.size()); + } + + public void testKeySetSize() { + final MultiValuedMap map = makeFullMap(); + assertEquals(3, map.keySet().size()); + } + + @SuppressWarnings("unchecked") + public void testSize_Key() { + final MultiValuedMap map = makeFullMap(); + assertEquals(2, map.get("one").size()); + assertEquals(2, map.get("two").size()); + assertEquals(2, map.get("three").size()); + if (!isAddSupported()) { + return; + } + map.put((K) "A", (V) "AA"); + assertEquals(1, map.get("A").size()); + //assertEquals(0, map.get("B").size()); + map.put((K) "B", (V) "BA"); + assertEquals(1, map.get("A").size()); + assertEquals(1, map.get("B").size()); + map.put((K) "B", (V) "BB"); + assertEquals(1, map.get("A").size()); + assertEquals(2, map.get("B").size()); + map.put((K) "B", (V) "BC"); + assertEquals(1, map.get("A").size()); + assertEquals(3, map.get("B").size()); + if (!isRemoveSupported()) { + return; + } + map.remove("A"); + //assertEquals(0, map.get("A").size()); + assertEquals(3, map.get("B").size()); + map.removeMapping((K) "B", (V) "BC"); + //assertEquals(0, map.get("A").size()); + assertEquals(2, map.get("B").size()); + } + +// @SuppressWarnings("unchecked") +// public void testIterator_Key() { +// final MultiValuedMap map = makeFullMap(); +// Iterator it = map.iterator("one"); +// assertEquals(true, it.hasNext()); +// Set values = new HashSet(); +// while (it.hasNext()) { +// values.add(it.next()); +// } +// assertEquals(true, values.contains("un")); +// assertEquals(true, values.contains("uno")); +// assertEquals(false, map.iterator("A").hasNext()); +// assertEquals(false, map.iterator("A").hasNext()); +// if (!isAddSupported()) { +// return; +// } +// map.put((K) "A", (V) "AA"); +// it = map.iterator("A"); +// assertEquals(true, it.hasNext()); +// it.next(); +// assertEquals(false, it.hasNext()); +// } + + @SuppressWarnings("unchecked") + public void testContainsValue_Key() { + final MultiValuedMap map = makeFullMap(); + assertEquals(true, map.containsMapping("one", (V) "uno")); + assertEquals(false, map.containsMapping("two", (V) "2")); + if (!isAddSupported()) { + return; + } + map.put((K) "A", (V) "AA"); + assertEquals(true, map.containsMapping("A", (V) "AA")); + assertEquals(false, map.containsMapping("A", (V) "AB")); + } + + @SuppressWarnings("unchecked") + public void testPutAll_Map1() { + if (!isAddSupported()) { + return; + } + final MultiValuedMap original = makeObject(); + original.put((K) "key", (V) "object1"); + original.put((K) "key", (V) "object2"); + + final MultiValuedMap test = makeObject(); + test.put((K) "keyA", (V) "objectA"); + test.put((K) "key", (V) "object0"); + test.putAll(original); + + assertEquals(2, test.keySet().size()); + assertEquals(4, test.size()); + assertEquals(1, test.get("keyA").size()); + assertEquals(3, test.get("key").size()); + assertEquals(true, test.containsValue("objectA")); + assertEquals(true, test.containsValue("object0")); + assertEquals(true, test.containsValue("object1")); + assertEquals(true, test.containsValue("object2")); + } + + @SuppressWarnings("unchecked") + public void testPutAll_Map2() { + if (!isAddSupported()) { + return; + } + final Map original = new HashMap(); + original.put((K) "keyX", (V) "object1"); + original.put((K) "keyY", (V) "object2"); + + final MultiValuedMap test = makeObject(); + test.put((K) "keyA", (V) "objectA"); + test.put((K) "keyX", (V) "object0"); + test.putAll(original); + + assertEquals(3, test.keySet().size()); + assertEquals(4, test.size()); + assertEquals(1, test.get("keyA").size()); + assertEquals(2, test.get("keyX").size()); + assertEquals(1, test.get("keyY").size()); + assertEquals(true, test.containsValue("objectA")); + assertEquals(true, test.containsValue("object0")); + assertEquals(true, test.containsValue("object1")); + assertEquals(true, test.containsValue("object2")); + } + + @SuppressWarnings("unchecked") + public void testPutAll_KeyIterable() { + if (!isAddSupported()) { + return; + } + final MultiValuedMap map = makeObject(); + Collection coll = (Collection) Arrays.asList("X", "Y", "Z"); + + assertEquals(true, map.putAll((K) "A", coll)); + assertEquals(3, map.get("A").size()); + assertEquals(true, map.containsMapping("A", (V) "X")); + assertEquals(true, map.containsMapping("A", (V) "Y")); + assertEquals(true, map.containsMapping("A", (V) "Z")); + + assertEquals(false, map.putAll((K) "A", null)); + assertEquals(3, map.get("A").size()); + assertEquals(true, map.containsMapping("A", (V) "X")); + assertEquals(true, map.containsMapping("A", (V) "Y")); + assertEquals(true, map.containsMapping("A", (V) "Z")); + + assertEquals(false, map.putAll((K) "A", new ArrayList())); + assertEquals(3, map.get("A").size()); + assertEquals(true, map.containsMapping("A", (V) "X")); + assertEquals(true, map.containsMapping("A", (V) "Y")); + assertEquals(true, map.containsMapping("A", (V) "Z")); + + coll = (Collection) Arrays.asList("M"); + assertEquals(true, map.putAll((K) "A", coll)); + assertEquals(4, map.get("A").size()); + assertEquals(true, map.containsMapping("A", (V) "X")); + assertEquals(true, map.containsMapping("A", (V) "Y")); + assertEquals(true, map.containsMapping("A", (V) "Z")); + assertEquals(true, map.containsMapping("A", (V) "M")); + } + + @SuppressWarnings("unchecked") + public void testRemove_KeyItem() { + if (!isRemoveSupported() || !isAddSupported()) { + return; + } + final MultiValuedMap map = makeObject(); + map.put((K) "A", (V) "AA"); + map.put((K) "A", (V) "AB"); + map.put((K) "A", (V) "AC"); + assertEquals(false, map.removeMapping((K) "C", (V) "CA")); + assertEquals(false, map.removeMapping((K) "A", (V) "AD")); + assertEquals(true, map.removeMapping((K) "A", (V) "AC")); + assertEquals(true, map.removeMapping((K) "A", (V) "AB")); + assertEquals(true, map.removeMapping((K) "A", (V) "AA")); + //assertEquals(new MultiValuedHashMap(), map); + } + + public void testKeysBag() { + MultiValuedMap map = makeFullMap(); + Bag keyBag = map.keys(); + assertEquals(2, keyBag.getCount("one")); + assertEquals(2, keyBag.getCount("two")); + assertEquals(2, keyBag.getCount("three")); + assertEquals(6, keyBag.size()); + } + + public void testKeysBagIterator() { + MultiValuedMap map = makeFullMap(); + Collection col = new ArrayList(); + Iterator it = map.keys().iterator(); + while (it.hasNext()) { + col.add(it.next()); + } + Bag bag = new HashBag(col); + assertEquals(2, bag.getCount("one")); + assertEquals(2, bag.getCount("two")); + assertEquals(2, bag.getCount("three")); + assertEquals(6, bag.size()); + } + + @SuppressWarnings("unchecked") + public void testKeysBagContainsAll() { + MultiValuedMap map = makeFullMap(); + Bag keyBag = map.keys(); + Collection col = (Collection) Arrays.asList("one", "two", "three", "one", "two", "three"); + assertTrue(keyBag.containsAll(col)); + } + +// public void testMapEqulas() { +// MultiValuedMap map1 = makeFullMap(); +// MultiValuedMap map2 = makeFullMap(); +// assertEquals(true, map1.equals(map2)); +// } + + // ----------------------------------------------------------------------- + // Manual serialization testing as this class cannot easily + // extend the AbstractTestMap + // ----------------------------------------------------------------------- + + public void testEmptyMapCompatibility() throws Exception { + final MultiValuedMap map = makeObject(); + final MultiValuedMap map2 = (MultiValuedMap) readExternalFormFromDisk(getCanonicalEmptyCollectionName(map)); + assertEquals("Map is empty", 0, map2.size()); + } + + public void testFullMapCompatibility() throws Exception { + final MultiValuedMap map = (MultiValuedMap) makeFullMap(); + final MultiValuedMap map2 = (MultiValuedMap) readExternalFormFromDisk(getCanonicalFullCollectionName(map)); + assertEquals("Map is the right size", map.size(), map2.size()); + for (final Object key : map.keySet()) { + assertEquals("Map had inequal elements", map.get(key), map2.get(key)); + if (isRemoveSupported()) { + map2.remove(key); + } + } + if (isRemoveSupported()) { + assertEquals("Map had extra values", 0, map2.size()); + } + } + +} diff --git a/src/test/java/org/apache/commons/collections4/multimap/MultiValuedHashMapTest.java b/src/test/java/org/apache/commons/collections4/multimap/MultiValuedHashMapTest.java new file mode 100644 index 000000000..81cfd9503 --- /dev/null +++ b/src/test/java/org/apache/commons/collections4/multimap/MultiValuedHashMapTest.java @@ -0,0 +1,85 @@ +/* + * 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.ArrayList; +import java.util.Collection; +import java.util.HashSet; +import java.util.LinkedList; + +import org.apache.commons.collections4.MultiValuedMap; + +/** + * Test MultValuedHashMap + * + * @since 4.1 + * @version $Id$ + */ +public class MultiValuedHashMapTest extends AbstractMultiValuedMapTest { + + public MultiValuedHashMapTest(String testName) { + super(testName); + } + + @Override + public MultiValuedMap makeObject() { + final MultiValuedMap m = new MultiValuedHashMap(); + return m; + } + + private > MultiValuedHashMap createTestMap(final Class collectionClass) { + final MultiValuedHashMap map = + (MultiValuedHashMap) MultiValuedHashMap. multiValuedMap(collectionClass); + addSampleMappings(map); + return map; + } + + @SuppressWarnings("unchecked") + public void testValueCollectionType() { + final MultiValuedHashMap map = createTestMap(LinkedList.class); + assertTrue(map.get("one") instanceof LinkedList); + } + + @SuppressWarnings("unchecked") + public void testPutWithList() { + final MultiValuedHashMap test = (MultiValuedHashMap) MultiValuedHashMap.multiValuedMap(ArrayList.class); + assertEquals("a", test.put((K) "A", (V) "a")); + assertEquals("b", test.put((K) "A", (V) "b")); + assertEquals(1, test.keySet().size()); + assertEquals(2, test.size("A")); + assertEquals(2, test.size()); + } + + @SuppressWarnings("unchecked") + public void testPutWithSet() { + final MultiValuedHashMap test = (MultiValuedHashMap) MultiValuedHashMap.multiValuedMap(HashSet.class); + assertEquals("a", test.put((K) "A", (V) "a")); + assertEquals("b", test.put((K) "A", (V) "b")); + assertEquals(null, test.put((K) "A", (V) "a")); + assertEquals(1, test.keySet().size()); + assertEquals(2, test.size("A")); + assertEquals(2, test.size()); + } + +// public void testCreate() throws Exception { +// writeExternalFormToDisk((java.io.Serializable) makeObject(), +// "src/test/resources/data/test/MultiValuedHashMap.emptyCollection.version4.1.obj"); +// writeExternalFormToDisk((java.io.Serializable) makeFullMap(), +// "src/test/resources/data/test/MultiValuedHashMap.fullCollection.version4.1.obj"); +// } + +} diff --git a/src/test/java/org/apache/commons/collections4/multimap/TransformedMultiValuedMapTest.java b/src/test/java/org/apache/commons/collections4/multimap/TransformedMultiValuedMapTest.java new file mode 100644 index 000000000..46a8a7cb8 --- /dev/null +++ b/src/test/java/org/apache/commons/collections4/multimap/TransformedMultiValuedMapTest.java @@ -0,0 +1,132 @@ +/* + * 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 org.apache.commons.collections4.MultiValuedMap; +import org.apache.commons.collections4.Transformer; +import org.apache.commons.collections4.TransformerUtils; +import org.apache.commons.collections4.collection.TransformedCollectionTest; +import org.apache.commons.collections4.multimap.TransformedMultiValuedMap; + +/** + * Tests for TransformedMultiValuedMap + * + * @since 4.1 + * @version $Id$ + */ +public class TransformedMultiValuedMapTest extends AbstractMultiValuedMapTest { + + public TransformedMultiValuedMapTest(String testName) { + super(testName); + } + + @Override + public MultiValuedMap makeObject() { + return TransformedMultiValuedMap.transformingMap(new MultiValuedHashMap(), + TransformerUtils. nopTransformer(), TransformerUtils. nopTransformer()); + } + + @SuppressWarnings("unchecked") + public void testKeyTransformedMap() { + final Object[] els = new Object[] { "1", "3", "5", "7", "2", "4", "6" }; + + MultiValuedMap map = TransformedMultiValuedMap.transformingMap( + new MultiValuedHashMap(), + (Transformer) TransformedCollectionTest.STRING_TO_INTEGER_TRANSFORMER, + null); + assertEquals(0, map.size()); + for (int i = 0; i < els.length; i++) { + map.put((K) els[i], (V) els[i]); + assertEquals(i + 1, map.size()); + assertEquals(true, map.containsKey(Integer.valueOf((String) els[i]))); + assertEquals(false, map.containsKey(els[i])); + assertEquals(true, map.containsValue(els[i])); + assertEquals(true, map.get(Integer.valueOf((String) els[i])).contains(els[i])); + } + + assertEquals(null, map.remove(els[0])); + assertEquals(true, map.remove(Integer.valueOf((String) els[0])).contains(els[0])); + } + + @SuppressWarnings("unchecked") + public void testValueTransformedMap() { + final Object[] els = new Object[] { "1", "3", "5", "7", "2", "4", "6" }; + + MultiValuedMap map = TransformedMultiValuedMap.transformingMap( + new MultiValuedHashMap(), null, + (Transformer) TransformedCollectionTest.STRING_TO_INTEGER_TRANSFORMER); + assertEquals(0, map.size()); + for (int i = 0; i < els.length; i++) { + map.put((K) els[i], (V) els[i]); + assertEquals(i + 1, map.size()); + assertEquals(true, map.containsValue(Integer.valueOf((String) els[i]))); + assertEquals(false, map.containsValue(els[i])); + assertEquals(true, map.containsKey(els[i])); + assertEquals(true, map.get(els[i]).contains(Integer.valueOf((String) els[i]))); + } + assertEquals(true, map.remove(els[0]).contains(Integer.valueOf((String) els[0]))); + } + + // ----------------------------------------------------------------------- + @SuppressWarnings("unchecked") + public void testFactory_Decorate() { + final MultiValuedMap base = new MultiValuedHashMap(); + base.put((K) "A", (V) "1"); + base.put((K) "B", (V) "2"); + base.put((K) "C", (V) "3"); + + final MultiValuedMap trans = TransformedMultiValuedMap + .transformingMap( + base, + null, + (Transformer) TransformedCollectionTest.STRING_TO_INTEGER_TRANSFORMER); + assertEquals(3, trans.size()); + assertEquals(true, trans.get("A").contains("1")); + assertEquals(true, trans.get("B").contains("2")); + assertEquals(true, trans.get("C").contains("3")); + trans.put((K) "D", (V) "4"); + assertEquals(true, trans.get("D").contains(Integer.valueOf(4))); + } + + @SuppressWarnings("unchecked") + public void testFactory_decorateTransform() { + final MultiValuedMap base = new MultiValuedHashMap(); + base.put((K) "A", (V) "1"); + base.put((K) "B", (V) "2"); + base.put((K) "C", (V) "3"); + + final MultiValuedMap trans = TransformedMultiValuedMap + .transformedMap( + base, + null, + (Transformer) TransformedCollectionTest.STRING_TO_INTEGER_TRANSFORMER); + assertEquals(3, trans.size()); + assertEquals(true, trans.get("A").contains(Integer.valueOf(1))); + assertEquals(true, trans.get("B").contains(Integer.valueOf(2))); + assertEquals(true, trans.get("C").contains(Integer.valueOf(3))); + trans.put((K) "D", (V) "4"); + assertEquals(true, trans.get("D").contains(Integer.valueOf(4))); + } + +// public void testCreate() throws Exception { +// writeExternalFormToDisk((java.io.Serializable) makeObject(), +// "src/test/resources/data/test/TransformedMultiValuedMap.emptyCollection.version4.1.obj"); +// writeExternalFormToDisk((java.io.Serializable) makeFullMap(), +// "src/test/resources/data/test/TransformedMultiValuedMap.fullCollection.version4.1.obj"); +// } + +} diff --git a/src/test/java/org/apache/commons/collections4/multimap/UnmodifiableMultiValuedMapTest.java b/src/test/java/org/apache/commons/collections4/multimap/UnmodifiableMultiValuedMapTest.java new file mode 100644 index 000000000..eec85c5fe --- /dev/null +++ b/src/test/java/org/apache/commons/collections4/multimap/UnmodifiableMultiValuedMapTest.java @@ -0,0 +1,88 @@ +/* + * 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 org.apache.commons.collections4.MultiValuedMap; +import org.apache.commons.collections4.Unmodifiable; + +/** + * Tests for UnmodifiableMultiValuedMap + * + * @since 4.1 + * @version $Id$ + */ +public class UnmodifiableMultiValuedMapTest extends AbstractMultiValuedMapTest { + + public UnmodifiableMultiValuedMapTest(String testName) { + super(testName); + } + + public boolean isAddSupported() { + return false; + } + + public boolean isRemoveSupported() { + return false; + } + + @Override + public MultiValuedMap makeObject() { + return UnmodifiableMultiValuedMap. unmodifiableMultiValuedMap(new MultiValuedHashMap()); + } + + protected MultiValuedMap makeFullMap() { + final MultiValuedMap map = new MultiValuedHashMap(); + addSampleMappings(map); + return UnmodifiableMultiValuedMap. unmodifiableMultiValuedMap(map); + } + + public void testUnmodifiable() { + assertTrue(makeObject() instanceof Unmodifiable); + assertTrue(makeFullMap() instanceof Unmodifiable); + } + + public void testDecorateFactory() { + final MultiValuedMap map = makeFullMap(); + assertSame(map, UnmodifiableMultiValuedMap.unmodifiableMultiValuedMap(map)); + } + + public void testDecoratorFactoryNullMap() { + try { + UnmodifiableMultiValuedMap.unmodifiableMultiValuedMap(null); + fail(); + } catch (IllegalArgumentException e) { + } + } + + @SuppressWarnings("unchecked") + public void testAddException() { + MultiValuedMap map = makeObject(); + try { + map.put((K) "one", (V) "uno"); + fail(); + } catch (UnsupportedOperationException e) { + } + } + +// public void testCreate() throws Exception { +// writeExternalFormToDisk((java.io.Serializable) makeObject(), +// "src/test/resources/data/test/UnmodifiableMultiValuedMap.emptyCollection.version4.1.obj"); +// writeExternalFormToDisk((java.io.Serializable) makeFullMap(), +// "src/test/resources/data/test/UnmodifiableMultiValuedMap.fullCollection.version4.1.obj"); +// } + +} diff --git a/src/test/resources/data/test/MultiValuedHashMap.emptyCollection.version4.1.obj b/src/test/resources/data/test/MultiValuedHashMap.emptyCollection.version4.1.obj new file mode 100644 index 000000000..356ffbbb9 Binary files /dev/null and b/src/test/resources/data/test/MultiValuedHashMap.emptyCollection.version4.1.obj differ diff --git a/src/test/resources/data/test/MultiValuedHashMap.fullCollection.version4.1.obj b/src/test/resources/data/test/MultiValuedHashMap.fullCollection.version4.1.obj new file mode 100644 index 000000000..fc69efeab Binary files /dev/null and b/src/test/resources/data/test/MultiValuedHashMap.fullCollection.version4.1.obj differ diff --git a/src/test/resources/data/test/TransformedMultiValuedMap.emptyCollection.version4.1.obj b/src/test/resources/data/test/TransformedMultiValuedMap.emptyCollection.version4.1.obj new file mode 100644 index 000000000..1abc5516a Binary files /dev/null and b/src/test/resources/data/test/TransformedMultiValuedMap.emptyCollection.version4.1.obj differ diff --git a/src/test/resources/data/test/TransformedMultiValuedMap.fullCollection.version4.1.obj b/src/test/resources/data/test/TransformedMultiValuedMap.fullCollection.version4.1.obj new file mode 100644 index 000000000..336748491 Binary files /dev/null and b/src/test/resources/data/test/TransformedMultiValuedMap.fullCollection.version4.1.obj differ diff --git a/src/test/resources/data/test/UnmodifiableMultiValuedMap.emptyCollection.version4.1.obj b/src/test/resources/data/test/UnmodifiableMultiValuedMap.emptyCollection.version4.1.obj new file mode 100644 index 000000000..77cebbb82 Binary files /dev/null and b/src/test/resources/data/test/UnmodifiableMultiValuedMap.emptyCollection.version4.1.obj differ diff --git a/src/test/resources/data/test/UnmodifiableMultiValuedMap.fullCollection.version4.1.obj b/src/test/resources/data/test/UnmodifiableMultiValuedMap.fullCollection.version4.1.obj new file mode 100644 index 000000000..88366c211 Binary files /dev/null and b/src/test/resources/data/test/UnmodifiableMultiValuedMap.fullCollection.version4.1.obj differ