All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.clapper.util.misc.MultiValueMap Maven / Gradle / Ivy

The newest version!
package org.clapper.util.misc;

import java.util.AbstractMap;
import java.util.AbstractSet;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Set;

/**
 * 

MultivalueMap implements a hash table that permits multiple * values per key. It's very similar to the MultiValueMap class * provided by the * Jakarta Commons * Collections API, except that this class uses Java 5 generics. * *

Any value placed into a MultivalueMap must implement * java.lang.Comparable.

*/ public class MultiValueMap extends AbstractMap implements Cloneable { /*----------------------------------------------------------------------*\ Public Inner Classes \*----------------------------------------------------------------------*/ /** * Used to allocate a new Collection for the values associated * with a key. Callers may specified their own implementation of this * interface to cause a MultiValueMap object to use a different * Collection class other ArrayList. */ public interface ValuesCollectionAllocator { /** * Allocate a new Collection class for use in storing the * values for a key. * * @return a Collection object */ public Collection newValuesCollection(); } /*----------------------------------------------------------------------*\ Inner Classes \*----------------------------------------------------------------------*/ private class MultiValueMapEntry implements Map.Entry { private K key; private V value; MultiValueMapEntry(K key, V value) { this.key = key; this.value = value; } public boolean equals(Object o) { boolean eq = false; if (o instanceof Map.Entry) { Map.Entry other = (Map.Entry) o; eq = other.getKey().equals(key) && other.getValue().equals(value); } return eq; } public K getKey() { return key; } public V getValue() { return value; } public int hashCode() { return keyValueHashCode(key, value); } public V setValue(V value) { throw new UnsupportedOperationException(); } } private class MultiValueMapEntryIterator implements Iterator> { private Iterator keys = MultiValueMap.this.keySet().iterator(); private Iterator curValues = null; private MultiValueMapEntry lastReturned = null; MultiValueMapEntryIterator() { } public boolean hasNext() { boolean has = (curValues != null) && (curValues.hasNext()); if (! has) { // Values exhausted. Are there any keys left? has = keys.hasNext(); } return has; } public Map.Entry next() { Map.Entry result = null; if (! hasNext()) throw new NoSuchElementException(); if ((curValues == null) || (! curValues.hasNext())) { // Exhausted the values for this key. Move on to next // key. final K key = keys.next(); curValues = MultiValueMap.this.getCollection(key) .iterator(); final V value = curValues.next(); lastReturned = new MultiValueMapEntry(key, value); result = lastReturned; } return result; } public void remove() { if (lastReturned == null) throw new IllegalStateException("Nothing to remove"); MultiValueMap.this.remove(lastReturned.getKey(), lastReturned.getValue()); } } private class EntrySet extends AbstractSet> { private EntrySet() { // Nothing to do } public void clear() { throw new UnsupportedOperationException(); } public boolean contains(Map.Entry o) { return MultiValueMap.this.containsKeyValue(o.getKey(), o.getValue()); } public Iterator> iterator() { return new MultiValueMapEntryIterator(); } public boolean remove(Object o) { MultiValueMapEntry entry = (MultiValueMapEntry) o; return MultiValueMap.this.remove(entry.getKey(), entry.getValue()); } public int size() { return MultiValueMap.this.size(); } } /*----------------------------------------------------------------------*\ Private Data Items \*----------------------------------------------------------------------*/ /** * The underlying Map where items are really stored. */ private Map> map = null; /** * The collection values allocator. */ private ValuesCollectionAllocator valuesCollectionAllocator = new ValuesCollectionAllocator() { public Collection newValuesCollection() { return new ArrayList(); } }; /*----------------------------------------------------------------------*\ Constructors \*----------------------------------------------------------------------*/ /** * Constructs a new, empty map with a default capacity and load factor. * This class's default load factor is the same as the default load factor * for the java.util.HashMap class. */ public MultiValueMap() { this.map = new HashMap>(); } /** * Constructs a new, empty map with a default capacity and load factor. * This class's default load factor is the same as the default load factor * for the java.util.HashMap class. * * @param valuesCollectionAllocator object to use to allocate collections * of values for a key. */ public MultiValueMap(ValuesCollectionAllocator valuesCollectionAllocator) { this.map = new HashMap>(); this.valuesCollectionAllocator = valuesCollectionAllocator; } /** * Constructs a new, empty map with the specified initial capacity and * the specified load factor. Note that the load factor and capacity * refer to the number of keys in the table, not the number of values. * * @param initialCapacity the initial capacity of the Map * @param loadFactor the load factor * * @throws IllegalArgumentException if the initial capacity is negative, * or if the load factor is nonpostive. */ public MultiValueMap(int initialCapacity, float loadFactor) { this.map = new HashMap>(initialCapacity, loadFactor); } /** * Constructs a new, empty map with the specified initial capacity and * the specified load factor. Note that the load factor and capacity * refer to the number of keys in the table, not the number of values. * * @param initialCapacity the initial capacity * @param loadFactor the load factor * @param valuesCollectionAllocator object to use to allocate collections * of values for a key. * * @throws IllegalArgumentException if the initial capacity is negative, * or if the load factor is nonpostive. */ public MultiValueMap(int initialCapacity, float loadFactor, ValuesCollectionAllocator valuesCollectionAllocator) { this.map = new HashMap>(initialCapacity, loadFactor); this.valuesCollectionAllocator = valuesCollectionAllocator; } /** * Constructs a new, empty map with the specified initial capacity and * the default load factor. Note that the load factor and capacity * refer to the number of keys in the table, not the number of values. * * @param initialCapacity the initial capacity * * @throws IllegalArgumentException if the initial capacity is negative, * or if the load factor is nonpostive. */ public MultiValueMap(int initialCapacity) { this.map = new HashMap>(initialCapacity); } /** * Construct a new map from the contents of an existing map. The new * map is a shallow copy of the existing map. * * @param otherMap the map to clone */ public MultiValueMap(MultiValueMap otherMap) { otherMap.makeShallowCopyInto(this); } /*----------------------------------------------------------------------*\ Public Methods \*----------------------------------------------------------------------*/ /** * Removes all mappings from this map. */ public void clear() { map.clear(); } /** * Returns a shallow copy of this MultivalueMap instance. * The keys and values themselves are not cloned. * * @throws CloneNotSupportedException never, but it's part of the signature */ public Object clone() // NOPMD throws CloneNotSupportedException { MultiValueMap newMap = new MultiValueMap(); makeShallowCopyInto (newMap); return newMap; } /** * Returns true if this map contains at least one value for * the specified key. * * @param key key whose presence in this map is to be tested * * @return true if this map contains at least one value for the * key, false otherwise. * * @throws ClassCastException if the key is of an inappropriate type for * this map. * @throws NullPointerException if the key is null and this map * does not not permit null keys. * * @see #totalValuesForKey */ public boolean containsKey (Object key) { return map.containsKey(key); } /** * Returns true if this map maps one or more keys to the * specified value. The values are compared via their * compareTo() and/or equals() methods, so this method * is only useful if the map contains values of the same type. * * @param value value whose presence in this map is to be tested. * * @return true if this map maps one or more keys to the * specified value, false otherwise. */ public boolean containsValue(Object value) { boolean found = false; Iterator> it = map.values().iterator(); while ((! found) && it.hasNext()) { Collection values = it.next(); if (values.contains(value)) found = true; } return found; } /** * Returns true if this map contains the specified value for * the specified key. The values are compared via their * compareTo() and/or equals() methods. * * @param key the key * @param value the value * * @return true if the set of values for the specified key * contains the specified value */ public boolean containsKeyValue(K key, V value) { boolean found = false; Collection values = getCollection(key); if (values != null) { for (V possibleValue : values) { found = value.equals(possibleValue); if (found) break; } } return found; } /** * Returns an unmodifiable Set view of the mappings contained * in this map. Each element in the returned collection is a * Map.Entry; each Map.Entry contains a key and a * value. Even though the return value is a Set, it will still * contain all key/value pairs for a given key. The returned * Set is backed by this map, so any changes to the map are * automatically reflected in the set. * * @return a Set view of the mappings contained in this map * * @see #keySet * @see #values */ public Set> entrySet() { return new EntrySet(); } /** *

Compares the specified object with this map for equality. Returns * true if the given object is also a MultivalueMap * and the two maps represent the same mappings. * maps t1 and t2 represent the same mappings if * t1.entrySet().equals(t2.entrySet()). This ensures that the * equals method works properly across different * implementations of the Map interface.

* *

Warning:: Because this method must compare the actual * values stored in the map, and the values in a file, this method can * be quite slow.

* * @param o object to be compared for equality with this map. * * @return true if the specified object is equal to this map. */ public boolean equals (Object o) { boolean eq = false; if (o instanceof MultiValueMap) eq = ((MultiValueMap) o).entrySet().equals(this.entrySet()); return eq; } /** *

Returns an unmodifiable Collection containing all values * associated with the the specified key. Returns null if the * map contains no mapping for this key.

* * @param key key whose associated collection of values is to be returned. * * @return an unmodifiable Collection containing all values * associated with the the specified key, or * null if * the map contains no values for this key. * * @throws ClassCastException if the key is of an inappropriate type for * this map. * @throws NullPointerException if the key is null and this map * does not not permit null keys. * * @see #containsKey * @see #getFirstValueForKey */ public Collection getCollection(K key) { Collection values = map.get(key); if (values != null) values = Collections.unmodifiableCollection(values); return values; } /** * Synonym for {@link #getFirstValueForKey}, required by the Map * interface. * * @param key the key * * @return the first value for the key, or null if not found */ public V get(Object key) { V result = null; Collection values = map.get(key); if (values != null) result = values.iterator().next(); return result; } /** *

Returns the first value in the set of values associated with a * key. This method is especially useful when you know that there is * only a single value associated with the key. Note that "first" * does not mean "first value ever associated with the key." Instead, * it means "first value in the sorted list of values for the key."

* * @param key key whose associated value is to be returned. * * @return the first value for the key, where first is defined as above, * or null, if the key has no values * * @throws NullPointerException if the key is null and this map * does not not permit null keys. * * @see #containsKey * @see #get * @see #totalValuesForKey */ public V getFirstValueForKey(K key) { V result = null; Collection values = map.get(key); if (values != null) { Iterator it = values.iterator(); if (it.hasNext()) result = it.next(); } return result; } /** *

Returns the hash code value for this map. The hash code of a map * is defined to be the sum of the hash codes of each entry in the * map's entrySet() view. This ensures that * t1.equals(t2) implies that * t1.hashCode()==t2.hashCode() for any two maps t1 * and t2, as required by the general contract of * Object.hashCode.

* * @return the hash code value for this map. * * @see #equals(Object) */ public int hashCode() { Set> entries = this.entrySet(); int result = 0; for (Map.Entry entry : entries) result |= entry.hashCode(); return result; } /** * Determine whether the map is empty. * * @return true if this map contains no key-value mappings. */ public boolean isEmpty() { return map.isEmpty(); } /** * Returns a Set containing all the keys in this map. * *

The set is backed by the map, so changes to the map are reflected * in the set. If the map is modified while an iteration over the set * is in progress, the results of the iteration are undefined. Neither * the set nor its associated iterator supports any of the * set-modification methods (e.g., add(), remove(), * etc). If you attempt to call any of those methods, the called method * will throw an UnsupportedOperationException.

* * @return a set view of the keys contained in this map. * * @see #getValuesForKey * @see #values() */ public Set keySet() { return map.keySet(); } /** *

Associates the specified value with the specified key in this * map. If the map previously contained a mapping for this key, this * value is added to the list of values associated with the key. This * map class does not permit a null value to be stored.

* * @param key key with which the specified value is to be associated. * @param value value to be associated with the specified key. * * @return null, always * * @throws ClassCastException if the class of the specified key or * value prevents it from being stored * in this map. * @throws IllegalArgumentException if some aspect of this key or value * prevents it from being stored in this * map. * @throws NullPointerException the specified key or value is * null. */ public V put(K key, V value) { Collection values = (Collection) map.get(key); if (values == null) { values = valuesCollectionAllocator.newValuesCollection(); map.put(key, values); } values.add(value); return null; } /** *

Copies all of the mappings from the specified Map to * this map. These mappings will be added to any mappings that this map * had for any of the keys currently in the specified map.

* * @param fromMap Mappings to be stored in 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 IllegalArgumentException some aspect of a key or value in the * specified map prevents it from being * stored in this map. * @throws NullPointerException the specified key or value is * null. */ public void putAll(Map fromMap) { for (K key : fromMap.keySet()) { V value = fromMap.get(key); if (value != null) this.put(key, value); } } /** *

Copies all of the mappings from the specified * MultivalueMap to this map. These mappings will be added to * any mappings that this map had for any of the keys currently in the * specified map.

* * @param fromMap Mappings to be stored in 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 IllegalArgumentException some aspect of a key or value in the * specified map prevents it from being * stored in this map. * @throws NullPointerException the specified key or value is * null. */ public void putAll (MultiValueMap fromMap) { for (K key : fromMap.keySet()) { Collection values = fromMap.getCollection(key); if (values != null) { for (V value : values) this.put(key, value); } } } /** * Assocates all the objects in a Collection with a key. This * method is equivalent to the following code fragment: * *
     * for (Iterator it = values.iterator(); it.hasNext(); )
     *    map.put (key, it.next());
     * 
* * @param key the key * @param values the collection of values to associate with the key */ public void putAll(K key, Collection values) { for (V value : values) put (key, value); } /** *

Removes all mappings for a key from this map, if present.

* * @param key key whose mappings are to be removed from the map. * * @return Collection of values associated with specified key, * or null if there was no mapping for key. */ public Collection delete(K key) { return this.map.remove(key); } /** * Removes a single value from the set of values associated with a * key. * * @param key the key * @param value the value to find and remove * * @return true if the value was found and removed. * false if the value isn't associated with the key. */ public boolean remove(Object key, Object value) { boolean removed = false; synchronized (this) { Collection values = map.get(key); if (values != null) { removed = values.remove(value); if (values.size() == 0) map.remove(key); } } return removed; } /** *

Returns the number of key-value mappings in this map. If the map * contains more than Integer.MAX_VALUE elements, returns * Integer.MAX_VALUE.

* * @return the number of key-value mappings in this map. */ public int size() { int total = 0; for (K key : keySet()) { Collection valuesForKey = map.get(key); if (valuesForKey != null) total += valuesForKey.size(); } return total; } /** * Gets the total number of values mapped to a specific key. * * @param key the key to test * * @return the number of values mapped to the key, or 0 if the key * isn't present in the map. * * @throws NullPointerException if the key is null and this map * does not not permit null keys. * * @see #getValuesForKey */ public int totalValuesForKey(K key) { int total = 0; Collection values = getCollection(key); if (values != null) total = values.size(); return total; } /** *

Returns a collection view of the values contained in this map. * The returned collection is a "thin" view of the values contained in * this map. If a value is associated with more than one key (as * determined by the value's equals() method), it will only * appear once in the returned Collection. The values are * sorted (via their compareTo() methods) in the returned * Collection.

* *

Warning: Unlike the SDK's Map class, the returned * Collection is not backed by this map; instead, it * represents a snapshot of the values in the map. Subsequent changes * to this map object are not reflected in the returned * Collection.

* * @return a collection view of the values contained in this map. * * @see #keySet * @see #getValuesForKey */ public Collection values() { Collection result = new ArrayList(); for (K key : keySet()) result.addAll(getCollection(key)); return result; } /** * Return an unmodifiable Collection of all the values for a * specific key. * * @param key The key * * @return an unmodifiable Collection of all the values * associated with the key, or null if there are no * values associated with the key * * @throws NullPointerException if the key is null and this map * does not not permit null keys. * * @see #keySet() * @see #totalValuesForKey * @see #values() */ public Collection getValuesForKey (K key) { Collection values = getCollection(key); if (values != null) values = Collections.unmodifiableCollection(values); return values; } /** * Copy all the values for a specific key into a caller-supplied * Collection. * * @param key The key * @param values The Collection to receive the values * * @return the number of values copied to the collection * * @throws NullPointerException if the key is null and this map * does not not permit null keys. * * @see #keySet() * @see #totalValuesForKey * @see #values() */ public int getValuesForKey(K key, Collection values) { Collection valuesForKey = map.get (key); int total = 0; if (valuesForKey != null) { values.addAll(valuesForKey); total = valuesForKey.size(); } return total; } /*----------------------------------------------------------------------*\ Private Methods \*----------------------------------------------------------------------*/ /** * Calculate a combined hash code for a key/value pair. * * @param key the key * @param value the value * * @return the hash code */ private int keyValueHashCode (Object key, Object value) { // Put the string representations of the key and value together, // with a delimiter that's unlikely to be in the string // representation, and use that string's hash code. If the value // is null, use an unlikely placeholder. if (value == null) value = "\u0002"; return (new String (key.toString() + "\u0001" + value.toString())) .hashCode(); } /** * Create a shallow copy of this map into another, existing (presumably * empty) map. * * @param otherMap the other map to receive the values */ private void makeShallowCopyInto (MultiValueMap otherMap) { for (K key : map.keySet()) { // Copy the collection, though, don't just pass a reference to // the same one. Collection values = this.map.get(key); Collection newValues = this.valuesCollectionAllocator.newValuesCollection(); newValues.addAll(values); otherMap.map.put(key, newValues); } } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy