source.ca.odell.glazedlists.GroupingList 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;
import ca.odell.glazedlists.event.ListEvent;
import ca.odell.glazedlists.impl.Grouper;
import ca.odell.glazedlists.impl.adt.barcode2.Element;
import ca.odell.glazedlists.impl.adt.barcode2.SimpleTree;
import java.util.AbstractList;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
/**
* A grouping list contains elements which are themselves Lists. Those Lists
* are infact elements of the source list which have been grouped together into
* a List. The logic of how to group the source elements is specified via a
* Comparator. Elements for which the Comparator returns 0 are guaranteed to be
* contained within the same group within this GroupingList. This implies
* that source elements may only participate in a single group within this
* GroupingList.
*
* Further transformations may be layered on top of this GroupingList to
* transform the group lists into any other desirable form.
*
*
Warning: This class is
* thread ready but not thread safe. See {@link EventList} for an example
* of thread safe code.
*
*
* EventList Overview
* Writable: yes
* Concurrency: thread ready, not thread safe
* Performance: reads: O(log N), writes O(log N)
* Memory:
* Unit Tests: GroupingListTest
* Issues:
* 281
*
*
*
* @author James Lemieux
*/
public final class GroupingList extends TransformedList> {
/** The GroupLists defined by the comparator. They are stored in an SimpleTree so their indices can be quickly updated. */
private SimpleTree groupLists = new SimpleTree();
/** The Grouper manages creating and deleting groups. */
private final Grouper grouper;
/**
* Creates a {@link GroupingList} that determines groupings via the
* {@link Comparable} interface which all elements of the source
* are assumed to implement.
*/
public static > GroupingList create(EventList source) {
return new GroupingList(source);
}
/**
* Creates a {@link GroupingList} that determines groupings via the
* {@link Comparable} interface which all elements of the source
* are assumed to implement.
* Usage of factory method {@link #create(EventList)} is preferable.
*/
public GroupingList(EventList source) {
this(source, (Comparator) GlazedLists.comparableComparator());
}
/**
* Creates a {@link GroupingList} that determines groups using the specified
* {@link Comparator}.
*
* @param source the {@link EventList} containing elements to be grouped
* @param comparator the {@link Comparator} used to determine groupings
*/
public GroupingList(EventList source, Comparator super E> comparator) {
this(new SortedList(source, comparator), comparator, null);
}
/**
* A private constructor which provides a convenient handle to the
* {@link SortedList} which will serve as the source of this list.
*
* @param source the elements to be grouped arranged in sorted order
* @param comparator the {@link Comparator} used to determine groupings
* @param dummyParameter dummy parameter to differentiate between the different
* {@link GroupingList} constructors.
*/
private GroupingList(SortedList source, Comparator super E> comparator, Void dummyParameter) {
super(source);
// the grouper handles changes to the SortedList
this.grouper = new Grouper(source, new GrouperClient());
// initialize the tree of GroupLists
rebuildGroupListTreeFromBarcode();
source.addListEventListener(this);
}
/**
* After the barcode has been updated in response to a change in the
* grouping {@link Comparator}, this method is used to rebuild the tree of
* GroupLists which map those GroupLists to their overall indices.
*/
private void rebuildGroupListTreeFromBarcode() {
// clear the contents of the GroupList tree
groupLists.clear();
// fetch our GrouperClient
final GrouperClient grouperClient = (GrouperClient) grouper.getClient();
// build the tree of GroupLists from the barcode
for (int i = 0, n = grouper.getBarcode().colourSize(Grouper.UNIQUE); i < n; i++) {
grouperClient.insertGroupList(i);
}
}
/**
* Return the index of the group to which the groupElement
* would belong if it were hypothetically added to the source list. Note
* that groupElement
does NOT have to exist
* in a group. This method is essentially a convenient way to locate a
* group based on a prototypical element of that group.
*
* @param groupElement a prototype element of the group to locate
* @return the index of the group that would contain groupElement
* if it were added to the source list or -1
if no
* currently existing group would contain the groupElement
*/
public int indexOfGroup(E groupElement) {
// determine where the groupElement would be positioned in the source List
final int sourceIndex = ((SortedList) source).sortIndex(groupElement);
// if the groupElement is not a member of the group, return -1
if (sourceIndex == source.size() || grouper.getComparator().compare(source.get(sourceIndex), groupElement) != 0)
return -1;
// return the index of the group that includes the element at the source index
return grouper.getBarcode().getColourIndex(sourceIndex, Grouper.UNIQUE);
}
/**
* Handle changes to the grouping list groups.
*/
private class GrouperClient implements Grouper.Client {
public void groupChanged(int index, int groupIndex, int groupChangeType, boolean primary, int elementChangeType, E oldValue, E newValue) {
if(groupChangeType == ListEvent.INSERT) {
insertGroupList(groupIndex);
updates.addInsert(groupIndex);
} else if(groupChangeType == ListEvent.DELETE) {
removeGroupList(groupIndex);
updates.addDelete(groupIndex);
} else if(groupChangeType == ListEvent.UPDATE) {
updates.addUpdate(groupIndex);
} else {
throw new IllegalStateException();
}
}
/**
* Creates and inserts a new GroupList at the specified
* index
.
*
* @param index the location at which to insert an empty GroupList
*/
private void insertGroupList(int index) {
final GroupList groupList = new GroupList();
final Element indexedTreeNode = groupLists.add(index, groupList, 1);
groupList.setTreeNode(indexedTreeNode);
}
/**
* Removes the GroupList at the given index
.
*
* @param index the location at which to remove a GroupList
*/
private void removeGroupList(int index) {
final Element indexedTreeNode = groupLists.get(index);
groupLists.remove(indexedTreeNode);
// for safety, null out the GroupList's reference to its now defunct indexedTreeNode
indexedTreeNode.get().setTreeNode(null);
}
}
/**
* Change the {@link Comparator} which determines the groupings presented
* by this List
*
* @param comparator the {@link Comparator} used to determine groupings;
* null will be treated as {@link GlazedLists#comparableComparator()}
*/
public void setComparator(Comparator super E> comparator) {
if (comparator == null)
comparator = (Comparator) GlazedLists.comparableComparator();
((SortedList) source).setComparator(comparator);
}
/** {@inheritDoc} */
protected int getSourceIndex(int index) {
return grouper.getBarcode().getIndex(index, Grouper.UNIQUE);
}
/** {@inheritDoc} */
protected boolean isWritable() {
return true;
}
/** {@inheritDoc} */
public void listChanged(ListEvent listChanges) {
updates.beginEvent(true);
// check if this ListEvent was caused due to a change in the
// Comparator that creates the groups
final SortedList sortedSource = (SortedList) source;
final Comparator super E> sourceComparator = sortedSource.getComparator();
if (sourceComparator != grouper.getComparator()) {
// when the grouping comparator is changed in the source list, let
// the grouper know so we can rebuild our groups from scratch
// record the impending removal of all groups before adjusting the barcode
for (int i = 0, n = size(); i < n; i++)
updates.elementDeleted(0, get(i));
// adjust the Comparator used by the Grouper (which will change the barcode)
grouper.setComparator(sourceComparator);
// rebuild the tree which maps GroupLists to indices (so the tree matches the new barcode)
rebuildGroupListTreeFromBarcode();
// insert all new groups (represented by the newly formed barcode)
updates.addInsert(0, size() - 1);
} else {
grouper.listChanged(listChanges);
}
updates.commitEvent();
}
/** {@inheritDoc} */
public List get(int index) {
return groupLists.get(index).get();
}
/** {@inheritDoc} */
public List remove(int index) {
if(index < 0 || index >= size()) throw new IndexOutOfBoundsException("Cannot remove at " + index + " on list of size " + size());
final List removed = (List) get(index);
// make a copy of the list to return
final List result = new ArrayList(removed);
removed.clear();
return result;
}
/** {@inheritDoc} */
public List set(int index, List value) {
if(index < 0 || index >= size()) throw new IndexOutOfBoundsException("Cannot set at " + index + " on list of size " + size());
updates.beginEvent(true);
final List result = (List) remove(index);
add(index, value);
updates.commitEvent();
return result;
}
/**
* This version of add will distribute all elements within the given
* value
List into groups. Existing groups will be reused and
* new groups will be created as needed. As such, the index
* argument is meaningless.
*
* Warning: This method
* breaks the contract required by {@link List#add(int, Object)}.
*/
public void add(int index, List value) {
source.addAll(value);
}
/** {@inheritDoc} */
public int size() {
return grouper.getBarcode().colourSize(Grouper.UNIQUE);
}
/** {@inheritDoc} */
public void dispose() {
((SortedList) source).dispose();
super.dispose();
}
/**
* This is the List implementation used to store groups created by this
* GroupingList. It defines all mutator methods by mapping them to mutations
* on the source list of GroupList. Thus, writes to this GroupList effect
* all Lists sitting under GroupList.
*/
private class GroupList extends AbstractList {
/**
* The node within {@link GroupingList#groupLists} that records the
* index of this GroupList within the GroupingList.
*/
private Element treeNode;
/**
* Attach the Element that tracks this GroupLists position to the
* GroupList itself so it can look up its own position.
*/
private void setTreeNode(Element treeNode) {
this.treeNode = treeNode;
}
/**
* Returns the inclusive index of the start of this {@link GroupList}
* within the source {@link SortedList}.
*/
private int getStartIndex() {
if (treeNode == null) return -1;
final int groupIndex = groupLists.indexOfNode(treeNode, (byte)1);
return GroupingList.this.getSourceIndex(groupIndex);
}
/**
* Returns the exclusive index of the end of this {@link GroupList}
* within the source {@link SortedList}.
*/
private int getEndIndex() {
if (treeNode == null) return -1;
final int groupIndex = groupLists.indexOfNode(treeNode, (byte)1);
// if this is before the end, its everything up to the first different element
if(groupIndex < grouper.getBarcode().blackSize() - 1) {
return grouper.getBarcode().getIndex(groupIndex + 1, Grouper.UNIQUE);
// if this is at the end, its everything after
} else {
return grouper.getBarcode().size();
}
}
private int getSourceIndex(int index) {
return getStartIndex() + index;
}
/** {@inheritDoc} */
public E set(int index, E element) {
return source.set(getSourceIndex(index), element);
}
/** {@inheritDoc} */
public E get(int index) {
return source.get(getSourceIndex(index));
}
/** {@inheritDoc} */
public int size() {
return getEndIndex() - getStartIndex();
}
/** {@inheritDoc} */
public void clear() {
source.subList(getStartIndex(), getEndIndex()).clear();
}
/** {@inheritDoc} */
public E remove(int index) {
return source.remove(getSourceIndex(index));
}
/** {@inheritDoc} */
public void add(int index, E element) {
source.add(getSourceIndex(index), element);
}
}
}