ca.odell.glazedlists.impl.GroupingListMultiMap Maven / Gradle / Ivy
Show all versions of glazedlists_java15 Show documentation
/* Glazed Lists (c) 2003-2006 */
/* http://publicobject.com/glazedlists/ publicobject.com,*/
/* O'Dell Engineering Ltd.*/
package ca.odell.glazedlists.impl;
import ca.odell.glazedlists.*;
import ca.odell.glazedlists.event.ListEventListener;
import ca.odell.glazedlists.event.ListEvent;
import java.util.*;
/**
* This multimap implementation sits atop an {@link EventList} and makes it
* accessible via the convenient {@link Map} interface. It is constructed with
* a {@link FunctionList.Function} which is used to create the keys of the map.
* The values of the map are the lists of values from the {@link EventList}
* which all map to a common key.
*
* For example, a list containing
*
*
* {Cherry, Plum, Cranberry, Pineapple, Banana, Prune}
*
*
* paired with a Function that returns the first letter of the fruit name
* produces the multi map:
*
*
* "B" -> {Banana}
* "C" -> {Cherry, Cranberry}
* "P" -> {Plum, Pineapple, Prune}
*
*
* @author James Lemieux
*/
public class GroupingListMultiMap implements Map, List>, ListEventListener> {
/** The raw values of this Map in an {@link EventList}. */
private final GroupingList groupingList;
/** The polished values of this Map in an {@link EventList}. */
private final FunctionList, List> valueList;
/** The keys of this Map (used to remove entries from the {@link #delegate}) */
private final List> keyList;
/** The keys of this Map made to look like a Set (it is build lazily in {@link #keySet()}) */
private Set> keySet;
/** The function which produces keys for this multimap. */
private final FunctionList.Function> keyFunction;
/** The delegate Map which is kept in synch with {@link #groupingList} changes. */
private final Map, List> delegate;
/** The set of Map.Entry objects in this Map (it is build lazily in {@link #entrySet()}) */
private Set, List>> entrySet;
/**
* Construct a multimap which maps the keys produced by the
* keyFunction
, to groups of values from source
* that agree on their keys.
*
* @param source the raw data which has not yet been grouped
* @param keyFunction the function capable of producing the keys of this
* {@link Map}; the keys themselves are {@link Comparable} and thus
* also determine the content of the {@link List}s which are the
* values of this {@link Map}.
*/
public GroupingListMultiMap(EventList source, FunctionList.Function> keyFunction) {
this.keyFunction = keyFunction;
// construct a GroupingList which groups together the source elements for common keys
this.groupingList = new GroupingList(source, new FunctionComparator(keyFunction));
// wrap each List in the GroupingList in a layer that enforces the keyFunction constraints for writes
this.valueList = new FunctionList, List>(this.groupingList, new ValueListFunction());
this.valueList.addListEventListener(this);
// it is important that the keyList is a BasicEventList since we use its ListIterator, which remains
// consistent with changes to its underlying data (any other Iterator would throw a ConcurrentModificationException)
this.keyList = new BasicEventList>(this.groupingList.size());
this.delegate = new HashMap,List>(this.groupingList.size());
// initialize both the keyList and the delegate Map
for (Iterator> i = this.valueList.iterator(); i.hasNext();) {
final List value = i.next();
final Comparable key = key(value);
this.keyList.add(key);
this.delegate.put(key, value);
}
}
/** {@inheritDoc} */
public int size() {
return this.delegate.size();
}
/** {@inheritDoc} */
public boolean isEmpty() {
return this.delegate.isEmpty();
}
/** {@inheritDoc} */
public boolean containsKey(Object key) {
return this.delegate.containsKey(key);
}
/** {@inheritDoc} */
public boolean containsValue(Object value) {
return this.delegate.containsValue(value);
}
/** {@inheritDoc} */
public List get(Object key) {
return this.delegate.get(key);
}
/** {@inheritDoc} */
public List put(Comparable key, List value) {
this.checkKeyValueAgreement(key, value);
final List removed = (List) this.remove(key);
this.groupingList.add(value);
return removed;
}
/** {@inheritDoc} */
public void putAll(Map extends Comparable, ? extends List> m) {
// verify the contents of the given Map and ensure all key/value pairs agree with the keyFunction
for (Iterator extends Entry extends Comparable, ? extends List>> i = m.entrySet().iterator(); i.hasNext();) {
final Entry extends Comparable, ? extends List> entry = i.next();
final Comparable key = entry.getKey();
final List value = entry.getValue();
this.checkKeyValueAgreement(key, value);
}
// remove all values currently associated with the keys
for (Iterator extends Comparable> i = m.keySet().iterator(); i.hasNext();)
this.remove(i.next());
// add all new values into this Map
this.groupingList.addAll(m.values());
}
/**
* This convenience method ensures that the key
matches the
* key values produced by each of the value
objects. If a
* mismatch is found, an {@link IllegalArgumentException} is thrown.
*
* @param key the expected key value of each value object
* @param value the value objects which should produce the given key when
* run through the key function
*/
private void checkKeyValueAgreement(Comparable key, Collection extends V> value) {
for (Iterator extends V> i = value.iterator(); i.hasNext();)
checkKeyValueAgreement(key, i.next());
}
/**
* This convenience method ensures that the key
matches the
* key value produced for the value
object. If a
* mismatch is found, an {@link IllegalArgumentException} is thrown.
*
* @param key the expected key value of each value object
* @param value the value object which should produce the given key when
* run through the key function
*/
private void checkKeyValueAgreement(Comparable key, V value) {
final Comparable k = key(value);
if (!GlazedListsImpl.equal(key, k))
throw new IllegalArgumentException("The calculated key for the given value (" + k + ") does not match the given key (" + key + ")");
}
/** {@inheritDoc} */
public void clear() {
this.groupingList.clear();
}
/** {@inheritDoc} */
public List remove(Object key) {
final int index = this.keyList.indexOf(key);
return index == -1 ? null : this.groupingList.remove(index);
}
/** {@inheritDoc} */
public Collection> values() {
return this.groupingList;
}
/** {@inheritDoc} */
public Set> keySet() {
if (this.keySet == null)
this.keySet = new KeySet();
return this.keySet;
}
/** {@inheritDoc} */
public Set, List>> entrySet() {
if (this.entrySet == null)
this.entrySet = new EntrySet();
return this.entrySet;
}
/**
* Updates this MultiMap datastructure to reflect changes in the underlying
* {@link GroupingList}. Specifically, new entries are added to this
* MultiMap by calculating a key using key function this MultiMap was
* constructed with.
*
* @param listChanges an event describing the changes in the GroupingList
*/
public void listChanged(ListEvent> listChanges) {
while (listChanges.next()) {
final int changeIndex = listChanges.getIndex();
final int changeType = listChanges.getType();
if (changeType == ListEvent.INSERT) {
final List inserted = (List) listChanges.getSourceList().get(changeIndex);
final Comparable key = key(inserted);
this.keyList.add(changeIndex, key);
this.delegate.put(key, inserted);
} else if (changeType == ListEvent.DELETE) {
final Comparable deleted = keyList.remove(changeIndex);
this.delegate.remove(deleted);
}
}
}
/**
* Uses the key function to return the key for a given list of values.
*
* @param values a non-empty list of values from the source
* {@link GroupingList} which share the same key value
* @return the shared key which maps to each of the given values
*/
private Comparable key(List values) {
return key(values.get(0));
}
/**
* Uses the key function to return the key for a given value.
*
* @param value a single value from the source list
* @return the key which maps to the given value
*/
private Comparable key(V value) {
return this.keyFunction.evaluate(value);
}
/**
* This private {@link Set} implementation represents the {@link Map.Entry}
* objects within this MultiMap. All mutating methods are implemented to
* "write through" to the backing {@link EventList} which ensures that both
* the {@link EventList} and this MultiMap always remain in sync.
*/
private class EntrySet extends AbstractSet, List>> {
/** {@inheritDoc} */
public int size() {
return keyList.size();
}
/** {@inheritDoc} */
public Iterator, List>> iterator() {
return new EntrySetIterator(keyList.listIterator());
}
/** {@inheritDoc} */
public boolean contains(Object o) {
if (!(o instanceof Map.Entry))
return false;
final Entry, List> e = (Entry, List>) o;
final Comparable key = e.getKey();
final List value = e.getValue();
final List mapValue = (List) GroupingListMultiMap.this.get(key);
return GlazedListsImpl.equal(value, mapValue);
}
/** {@inheritDoc} */
public boolean remove(Object o) {
if (!contains(o)) return false;
GroupingListMultiMap.this.remove(((Map.Entry) o).getKey());
return true;
}
/** {@inheritDoc} */
public void clear() {
GroupingListMultiMap.this.clear();
}
}
/**
* This private {@link Iterator} implementation iterates the {@link Set} of
* {@link Map.Entry} objects within this MultiMap. All mutating methods are
* implemented to "write through" to the backing {@link EventList} which
* ensures that both the {@link EventList} and this MultiMap always remain
* in sync.
*
* Note: This implementation returns a new
* {@link Map.Entry} object each time {@link #next} is called. Identity is
* not preserved.
*/
private class EntrySetIterator implements Iterator, List>> {
/** The delegate Iterator walks a List of keys for the MultiMap. */
private final ListIterator> keyIter;
/**
* Construct a new EntrySetIterator using a delegate Iterator that
* walks the keys of the MultMap.
*
* @param keyIter a {@link ListIterator} that walks the keys of the MultiMap
*/
EntrySetIterator(ListIterator> keyIter) {
this.keyIter = keyIter;
}
/** {@inheritDoc} */
public boolean hasNext() {
return keyIter.hasNext();
}
/**
* Returns a new {@link Map.Entry} each time this method is called.
*/
public Entry, List> next() {
final Comparable key = keyIter.next();
return new MultiMapEntry(key, (List) get(key));
}
/** {@inheritDoc} */
public void remove() {
final int index = keyIter.previousIndex();
if (index == -1) throw new IllegalStateException("Cannot remove() without a prior call to next()");
groupingList.remove(index);
}
}
/**
* This is an implementation of the {@link Map.Entry} interface that is
* appropriate for this MultiMap. All mutating methods are implemented to
* "write through" to the backing {@link EventList} which ensures that
* both the {@link EventList} and this MultiMap always remain in sync.
*/
private class MultiMapEntry implements Map.Entry, List> {
/** The MultiMap key for this Entry object. */
private final Comparable key;
/** The MultiMap value for this Entry object. */
private List value;
/**
* Constructs a new MultiMapEntry with the given key
and
* initial value
.
*/
MultiMapEntry(Comparable key, List value) {
if (value == null) throw new IllegalArgumentException("value cannot be null");
this.value = value;
this.key = key;
}
/** {@inheritDoc} */
public Comparable getKey() {
return key;
}
/** {@inheritDoc} */
public List getValue() {
return value;
}
/**
* Since {@link GroupingList} is particular about the identity of the
* Lists it contains, and this MultiMap uses those same
* Lists as its values, this method is implemented to simply
* replace the contents of the List with the contents
* of the given newValue
. So, the data is changed, but the
* identity of the List in the MultiMap and {@link GroupingList} is not.
*
* @param newValue the new values use as elements of the value List
* @return the old value List of this Entry
*/
public List setValue(List newValue) {
// ensure all of the newValue elements agree with the key of this Entry
checkKeyValueAgreement((Comparable) getKey(), newValue);
// record the old value List elements (to return)
final List oldValue = new ArrayList(value);
// replace all elements within the List
//
// (GroupingList actually removes Lists the moment they become *empty*
// so we first insert the new values rather than removing the old values
// to avoid the temporary existence of an empty List)
value.addAll(newValue);
value.removeAll(oldValue);
return oldValue;
}
/**
* Two MultiMapEntry entry objects are equal iff their keys and values
* are equal.
*/
public boolean equals(Object o) {
if (!(o instanceof Map.Entry))
return false;
Map.Entry e = (Map.Entry) o;
final boolean keysEqual = GlazedListsImpl.equal(getKey(), e.getKey());
return keysEqual && GlazedListsImpl.equal(getValue(), e.getValue());
}
/** {@inheritDoc} */
public int hashCode() {
return (key == null ? 0 : key.hashCode()) ^ value.hashCode();
}
/** {@inheritDoc} */
public String toString() {
return getKey() + "=" + getValue();
}
}
/**
* This private {@link Set} implementation represents the keys within this
* MultiMap. All mutating methods are implemented to "write through" to the
* backing {@link EventList} which ensures that both the {@link EventList}
* and this MultiMap always remain in sync.
*/
private class KeySet extends AbstractSet> {
/** {@inheritDoc} */
public int size() {
return keyList.size();
}
/** {@inheritDoc} */
public Iterator> iterator() {
return new KeySetIterator(keyList.listIterator());
}
/** {@inheritDoc} */
public boolean contains(Object o) {
return GroupingListMultiMap.this.containsKey(o);
}
/** {@inheritDoc} */
public boolean remove(Object o) {
return GroupingListMultiMap.this.remove(o) != null;
}
/** {@inheritDoc} */
public void clear() {
GroupingListMultiMap.this.clear();
}
}
/**
* This private {@link Iterator} implementation iterates the {@link Set} of
* keys within this MultiMap. All mutating methods are implemented to
* "write through" to the backing {@link EventList} which ensures that both
* the {@link EventList} and this MultiMap always remain in sync.
*/
private class KeySetIterator implements Iterator> {
/** The delegate Iterator walks a List of keys for the MultiMap. */
private final ListIterator> keyIter;
/**
* Construct a new KeySetIterator using a delegate Iterator that walks
* the list of unique keys of the MultMap.
*
* @param keyIter a {@link ListIterator} that walks the keys of the MultiMap
*/
KeySetIterator(ListIterator> keyIter) {
this.keyIter = keyIter;
}
/** {@inheritDoc} */
public boolean hasNext() {
return keyIter.hasNext();
}
/** {@inheritDoc} */
public Comparable next() {
return keyIter.next();
}
/** {@inheritDoc} */
public void remove() {
final int index = keyIter.previousIndex();
if (index == -1) throw new IllegalStateException("Cannot remove() without a prior call to next()");
groupingList.remove(index);
}
}
/**
* This Comparator first runs each value through a
* {@link FunctionList.Function} to produce {@link Comparable} objects
* which are then compared to determine a relative ordering.
*/
private final class FunctionComparator implements Comparator {
/** A Comparator that orders {@link Comparable} objects. */
private final Comparator delegate = GlazedLists.comparableComparator();
/** A function that extracts {@link Comparable} values from given objects. */
private final FunctionList.Function> function;
/**
* Construct a new FunctionComparator that uses the given
* function
to extract {@link Comparable} values from
* given objects.
*/
FunctionComparator(FunctionList.Function> function) {
if (function == null) throw new IllegalArgumentException("function may not be null");
this.function = function;
}
/** {@inheritDoc} */
public int compare(V o1, V o2) {
final Comparable c1 = function.evaluate(o1);
final Comparable c2 = function.evaluate(o2);
return delegate.compare(c1, c2);
}
}
/**
* This Function wraps each List produced by the GroupingList with a layer
* that ensures that mutations to it don't violate the keyFunction
* constraints required by this MultiMap.
*/
private final class ValueListFunction implements FunctionList.Function, List> {
public List evaluate(List sourceValue) {
return new ValueList(sourceValue);
}
}
/**
* This class wraps each element of the GroupingList with a layer of
* checking to ensure that mutations to it don't violate the keyFunction
* constraints required by this MultiMap.
*/
private final class ValueList implements List {
/** The List that actually implements the List operations */
private final List delegate;
/** The key that all values in this List must share. */
private final Comparable key;
public ValueList(List delegate) {
this.delegate = delegate;
this.key = key(delegate.get(0));
}
public int size() { return delegate.size(); }
public boolean isEmpty() { return delegate.isEmpty(); }
public boolean contains(Object o) { return delegate.contains(o); }
public Iterator iterator() { return delegate.iterator(); }
public Object[] toArray() { return delegate.toArray(); }
public T[] toArray(T[] a) { return delegate.toArray(a); }
public boolean add(V o) {
checkKeyValueAgreement(this.key, o);
return delegate.add(o);
}
public boolean addAll(Collection extends V> c) {
checkKeyValueAgreement(this.key, c);
return delegate.addAll(c);
}
public boolean addAll(int index, Collection extends V> c) {
checkKeyValueAgreement(this.key, c);
return delegate.addAll(index, c);
}
public void add(int index, V element) {
checkKeyValueAgreement(this.key, element);
delegate.add(index, element);
}
public V set(int index, V element) {
checkKeyValueAgreement(this.key, element);
return delegate.set(index, element);
}
public List subList(int fromIndex, int toIndex) {
return new ValueList(delegate.subList(fromIndex, toIndex));
}
public ListIterator listIterator() {
return new ValueListIterator(delegate.listIterator());
}
public ListIterator listIterator(int index) {
return new ValueListIterator(delegate.listIterator(index));
}
public boolean remove(Object o) { return delegate.remove(o); }
public boolean containsAll(Collection> c) { return delegate.containsAll(c); }
public boolean removeAll(Collection> c) { return delegate.removeAll(c); }
public boolean retainAll(Collection> c) { return delegate.retainAll(c); }
public void clear() { delegate.clear(); }
public boolean equals(Object o) { return delegate.equals(o); }
public int hashCode() { return delegate.hashCode(); }
public V get(int index) { return delegate.get(index); }
public V remove(int index) { return delegate.remove(index); }
public int indexOf(Object o) { return delegate.indexOf(o); }
public int lastIndexOf(Object o) { return delegate.lastIndexOf(o); }
public String toString() { return delegate.toString(); }
/**
* This class wraps the normal ListIterator returned by the GroupingList
* elements with extra checking to ensure mutations to it don't violate
* the keyFunction constraints required by this MultiMap.
*/
private final class ValueListIterator implements ListIterator {
private final ListIterator delegate;
public ValueListIterator(ListIterator delegate) {
this.delegate = delegate;
}
public void set(V o) {
checkKeyValueAgreement(key, o);
delegate.set(o);
}
public void add(V o) {
checkKeyValueAgreement(key, o);
delegate.add(o);
}
public boolean hasNext() { return delegate.hasNext(); }
public V next() { return delegate.next(); }
public boolean hasPrevious() { return delegate.hasPrevious(); }
public V previous() { return delegate.previous(); }
public int nextIndex() { return delegate.nextIndex(); }
public void remove() { delegate.remove(); }
public int previousIndex() { return delegate.previousIndex(); }
}
}
}