ca.odell.glazedlists.impl.Grouper Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of glazedlists_java15 Show documentation
Show all versions of glazedlists_java15 Show documentation
Event-driven lists for dynamically filtered and sorted tables
/* Glazed Lists (c) 2003-2006 */
/* http://publicobject.com/glazedlists/ publicobject.com,*/
/* O'Dell Engineering Ltd.*/
package ca.odell.glazedlists.impl;
import ca.odell.glazedlists.impl.adt.Barcode;
import ca.odell.glazedlists.event.ListEvent;
import ca.odell.glazedlists.SortedList;
import java.util.Comparator;
import java.util.LinkedList;
/**
* This helper class manages the groups created by dividing up a {@link ca.odell.glazedlists.SortedList}
* using a {@link java.util.Comparator}. This class uses a delegate interface {@link Client}
* to fire the appropriate events as groups are inserted, updated and changed.
*
* @author James Lemieux
* @author Jesse Wilson
*/
public class Grouper {
/** Entries in barcode are one of these constants to indicate if the element at an index is a UNIQUE or DUPLICATE. */
public static final Object UNIQUE = Barcode.BLACK;
public static final Object DUPLICATE = Barcode.WHITE;
/** Used only in temporary data structures to flag deleting of the FIRST group element when more elements exist. */
private static final Object UNIQUE_WITH_DUPLICATE = null;
/**
* A temporary barcode data structure is created when processing ListEvents in this GroupingList.
* These constants identify which indexes remain to be processed.
*/
private static final Object TODO = Barcode.BLACK;
private static final Object DONE = Barcode.WHITE;
/** For reporting which side of an element its group is located. */
private static final int LEFT_GROUP = -1;
private static final int NO_GROUP = 0;
private static final int RIGHT_GROUP = 1;
/** the sorted source of the grouping service */
private SortedList sortedList;
/** The comparator used to determine the groups. */
private Comparator super E> comparator;
/** the grouping list client to notify of group changes */
private Client client;
/**
* The data structure which tracks which source elements are considered UNIQUE
* (the first element of a group) and which are DUPLICATE (any group element NOT the first).
*/
private Barcode barcode;
/**
* Create a new {@link ca.odell.glazedlists.impl.Grouper} that manages groups for the
* specified {@link ca.odell.glazedlists.SortedList}.
*/
public Grouper(SortedList sortedList, Client client) {
this.sortedList = sortedList;
this.client = client;
setComparator(sortedList.getComparator());
}
/**
* Set the comparator used to determine which elements are grouped together. As
* a consequence this will rebuild the grouping state.
*/
public void setComparator(Comparator super E> comparator) {
if(this.comparator == comparator) return;
this.comparator = comparator;
// Populate the barcode by examining adjacent entries within the
// source SortedList to check if they belong to the same group.
barcode = new Barcode();
for (int i = 0, n = sortedList.size(); i < n; i++) {
barcode.add(i, groupTogether(i, i-1) ? DUPLICATE : UNIQUE, 1);
}
}
/**
* Get the comparator used to determine which elements are grouped together.
*/
public Comparator super E> getComparator() {
return comparator;
}
/**
* Get the barcode that designates where groups start. {@link #UNIQUE}
* colored elements are the starts of new groups, with a
* {@link #DUPLICATE} element for each following group memeber.
*/
public Barcode getBarcode() {
return barcode;
}
/**
* Handle changes from the {@link SortedList} by modifying the grouping state.
* During this method, callbacks will be made to the {@link Client} who is
* responsible for firing the appropriate change events to its listeners.
*/
public void listChanged(ListEvent listChanges) {
// The approach to handling list changes in GroupingList uses two passes
// through the list of change events.
//
// In the first pass, the barcode is updated to reflect the changes but
// no events are fired. Deleted and updated original barcode values are
// stored in a temporary LinkedList. Updated values' barcode entries are
// set to UNIQUE.
//
// In pass 2, the change events are reviewed again. During this second pass
// we bring the IndexedTree of GroupLists up to date and also fire ListEvents.
//
// a) Inserted elements are tested to see if they were simply added to
// existing GroupLists (in which case an update event should be fired
// rather than an insert)
//
// b) For update events the value is tested for group membership with
// adjacent values, and the result may be a combination of DELETE, INSERT
// or UPDATE events.
//
// c) Deleted events may be forwarded with a resultant DELETE or UPDATE
// event, depending on whether the deleted value was the last member of
// its group.
// a Barcode to track values that must be revisited to determine if they are UNIQUE or DUPLICATE
final Barcode toDoList = new Barcode();
toDoList.addWhite(0, barcode.size());
// first pass -> update the barcode and accumulate the type of values removed (UNIQUE or DUPLICATE or UNIQUE_WITH_DUPLICATE)
final LinkedList removedValues = new LinkedList();
while (listChanges.next()) {
final int changeIndex = listChanges.getIndex();
final int changeType = listChanges.getType();
if (changeType == ListEvent.INSERT) {
// assume the inserted element is unique until we determine otherwise
barcode.add(changeIndex, UNIQUE, 1);
// indicate we must revisit this index later to determine its uniqueness
toDoList.add(changeIndex, TODO, 1);
} else if (changeType == ListEvent.UPDATE) {
// case: AACCC -> AABCC
// if the updated index is a UNIQUE index, we MAY have just created a
// new group (by modifying an element in place). Consequently, we must
// mark the NEXT element as UNIQUE and revisit it later to determine
// if it really is
if (barcode.get(changeIndex) == UNIQUE) {
if (changeIndex+1 < barcode.size() && barcode.get(changeIndex+1) == DUPLICATE) {
barcode.set(changeIndex, UNIQUE, 2);
toDoList.set(changeIndex, TODO, 1);
}
}
} else if (changeType == ListEvent.DELETE) {
Object deleted = barcode.get(changeIndex);
barcode.remove(changeIndex, 1);
toDoList.remove(changeIndex, 1);
if (deleted == UNIQUE) {
// if the deleted UNIQUE value has a DUPLICATE, promote the DUPLICATE to be the new UNIQUE
if (changeIndex < barcode.size() && barcode.get(changeIndex) == DUPLICATE) {
// case: AABB -> AAB
barcode.set(changeIndex, UNIQUE, 1);
deleted = UNIQUE_WITH_DUPLICATE;
}
}
removedValues.addLast(deleted);
}
}
// second pass, handle toDoList, update groupLists, and fire events
listChanges.reset();
while(listChanges.next()) {
final int changeIndex = listChanges.getIndex();
final int changeType = listChanges.getType();
// inserts can result in UPDATE or INSERT events
if(changeType == ListEvent.INSERT) {
// if no group already exists to join, create a new group
if(tryJoinExistingGroup(changeIndex, toDoList) == NO_GROUP) {
final int groupIndex = barcode.getColourIndex(changeIndex, UNIQUE);
client.groupChanged(changeIndex, groupIndex, ListEvent.INSERT, true, changeType);
} else {
int groupIndex = barcode.getColourIndex(changeIndex, true, UNIQUE);
client.groupChanged(changeIndex, groupIndex, ListEvent.UPDATE, true, changeType);
}
// updates can result in INSERT, UPDATE and DELETE events
} else if(changeType == ListEvent.UPDATE) {
// get the location of the group before the update occurred
int oldGroup = 0;
if(toDoList.get(changeIndex) == TODO) oldGroup = RIGHT_GROUP;
else if(barcode.get(changeIndex) == DUPLICATE) oldGroup = LEFT_GROUP;
else if(barcode.get(changeIndex) == UNIQUE) oldGroup = NO_GROUP;
// get the new group location
int newGroup = tryJoinExistingGroup(changeIndex, toDoList);
// the index of the GroupList being updated (it may or may not exist yet)
int groupIndex = barcode.getColourIndex(changeIndex, true, UNIQUE);
// we're the first element in a new group
if(newGroup == NO_GROUP) {
if(oldGroup == NO_GROUP) {
client.groupChanged(changeIndex, groupIndex, ListEvent.UPDATE, true, changeType);
} else if(oldGroup == LEFT_GROUP) {
client.groupChanged(changeIndex, groupIndex - 1, ListEvent.UPDATE, false, changeType);
client.groupChanged(changeIndex, groupIndex, ListEvent.INSERT, true, changeType);
} else if(oldGroup == RIGHT_GROUP) {
client.groupChanged(changeIndex, groupIndex, ListEvent.INSERT, true, changeType);
client.groupChanged(changeIndex, groupIndex + 1, ListEvent.UPDATE, false, changeType);
}
// we are joining an existing group to our left
} else if(newGroup == LEFT_GROUP) {
if(oldGroup == NO_GROUP) {
client.groupChanged(changeIndex, groupIndex, ListEvent.UPDATE, true, changeType);
client.groupChanged(changeIndex, groupIndex + 1, ListEvent.DELETE, false, changeType);
} else if(oldGroup == LEFT_GROUP) {
client.groupChanged(changeIndex, groupIndex, ListEvent.UPDATE, true, changeType);
} else if(oldGroup == RIGHT_GROUP) {
client.groupChanged(changeIndex, groupIndex, ListEvent.UPDATE, true, changeType);
if(groupIndex + 1 < barcode.blackSize()) {
client.groupChanged(changeIndex, groupIndex + 1, ListEvent.UPDATE, false, changeType);
}
}
// we are joining an existing group to our right
} else if(newGroup == RIGHT_GROUP) {
if (oldGroup == NO_GROUP) {
client.groupChanged(changeIndex, groupIndex, ListEvent.DELETE, false, changeType);
client.groupChanged(changeIndex, groupIndex, ListEvent.UPDATE, true, changeType);
} else if(oldGroup == LEFT_GROUP) {
if(groupIndex - 1 >= 0) client.groupChanged(changeIndex, groupIndex - 1, ListEvent.UPDATE, false, changeType);
client.groupChanged(changeIndex, groupIndex, ListEvent.UPDATE, true, changeType);
} else if(oldGroup == RIGHT_GROUP) {
client.groupChanged(changeIndex, groupIndex, ListEvent.UPDATE, true, changeType);
}
}
// deletes can result in UPDATE or DELETE events
} else if(changeType == ListEvent.DELETE) {
// figure out if we deleted a UNIQUE or DUPLICATE or UNIQUE_WITH_DUPLICATE
final Object deleted = removedValues.removeFirst();
// get the index of the element removed from the source list
final int sourceDeletedIndex = deleted == DUPLICATE ? changeIndex - 1 : changeIndex;
// determine the index of the GroupList the removal effects
final int groupDeletedIndex = sourceDeletedIndex < barcode.size() ? barcode.getBlackIndex(sourceDeletedIndex, true) : barcode.blackSize();
// fire the change event
if(deleted == UNIQUE) {
// if we removed a UNIQUE element then it was the last one and we must remove the group
client.groupChanged(changeIndex, groupDeletedIndex, ListEvent.DELETE, true, changeType);
} else {
client.groupChanged(changeIndex, groupDeletedIndex, ListEvent.UPDATE, true, changeType);
}
}
}
}
/**
* Tests if the specified values should be grouped together.
*
* @param sourceIndex0 the first index of the source list to test
* @param sourceIndex1 the second index of the source list to test
*
* @return true iff the values at the specified index are equal according
* to the Comparator which defines the grouping logic; false if
* either index is out of range or the values shall not be grouped
*/
private boolean groupTogether(int sourceIndex0, int sourceIndex1) {
if(sourceIndex0 < 0 || sourceIndex0 >= sortedList.size()) return false;
if(sourceIndex1 < 0 || sourceIndex1 >= sortedList.size()) return false;
return comparator.compare(sortedList.get(sourceIndex0), sortedList.get(sourceIndex1)) == 0;
}
/**
* Handles whether this change index has a neighbour that existed prior
* to a current change and that the values are equal. This adjusts the
* duplicates list a found pair of such changes if they exist.
*
* @return NO_GROUP if no neighbour was found. In this case the value at the specified
* index will be marked as unique, but not temporarily so. Returns LEFT_GROUP if
* a neighbour was found on the left, and RIGHT_GROUP if a neighbour was found on the
* right. In non-zero cases the duplicates list is updated.
*/
private int tryJoinExistingGroup(int changeIndex, Barcode toDoList) {
// test if values at changeIndex and its predecessor should be grouped
int predecessorIndex = changeIndex - 1;
if (groupTogether(predecessorIndex, changeIndex)) {
barcode.set(changeIndex, DUPLICATE, 1);
return LEFT_GROUP;
}
// search for an OLD successor that should be in the same group as changeIndex
int successorIndex = changeIndex + 1;
while (true) {
// we have found a successor that belongs in the same group
if (groupTogether(changeIndex, successorIndex)) {
// if the successor is OLD, have changeIndex join the existing group
if (toDoList.get(successorIndex) == DONE) {
barcode.set(changeIndex, UNIQUE, 1);
barcode.set(successorIndex, DUPLICATE, 1);
return RIGHT_GROUP;
// this successor is NEW, not OLD, so skip it
} else {
successorIndex++;
}
// we have no more successors that belong in the same group
} else {
barcode.set(changeIndex, UNIQUE, 1);
return NO_GROUP;
}
}
}
/**
* The grouper client is responsible for turning grouping events into
* {@link ListEvent}s if desired. The client receives callbacks as groups
* are created, modified and destroyed.
*/
public interface Client {
/**
* Handle the structure of a group changing.
*
* @param index the location in the source {@link ca.odell.glazedlists.SortedList} that
* was inserted, updated or deleted.
* @param groupIndex the corresponding group that the {@link ca.odell.glazedlists.SortedList}
* element now belongs to, or belonged to in the case of
* {@link ca.odell.glazedlists.event.ListEvent.DELETE} events.
* @param groupChangeType one of {@link ca.odell.glazedlists.event.ListEvent#INSERT},
* {@link ca.odell.glazedlists.event.ListEvent#UPDATE} or {@link ca.odell.glazedlists.event.ListEvent#DELETE} signalling
* what happened to the group in response to this element change.
* This is potentially different from what happened to the original
* list element.
* @param primary whether this is a first event for this change,
* impacting the current group for this element. Sometimes multiple
* groups will be effected, in which case only a single callback will
* be the primary callback.
* @param elementChangeType the change type that caused this event. Sometimes
* an {@link ListEvent.UPDATE} event will cause a group to become inserted
* or deleted, in which case the elementChangeType represents the original
* event type.
*/
public void groupChanged(int index, int groupIndex, int groupChangeType, boolean primary, int elementChangeType);
}
}