
org.clapper.util.misc.MultiValueMap Maven / Gradle / Ivy
Show all versions of javautil Show documentation
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);
}
}
}