1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401
|
/* 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.
*
* <p>Further transformations may be layered on top of this GroupingList to
* transform the group lists into any other desirable form.
*
* <p><strong><font color="#FF0000">Warning:</font></strong> This class is
* thread ready but not thread safe. See {@link EventList} for an example
* of thread safe code.
*
* <p><table border="1" width="100%" cellpadding="3" cellspacing="0">
* <tr class="TableHeadingColor"><td colspan=2><font size="+2"><b>EventList Overview</b></font></td></tr>
* <tr><td class="TableSubHeadingColor"><b>Writable:</b></td><td>yes</td></tr>
* <tr><td class="TableSubHeadingColor"><b>Concurrency:</b></td><td>thread ready, not thread safe</td></tr>
* <tr><td class="TableSubHeadingColor"><b>Performance:</b></td><td>reads: O(log N), writes O(log N)</td></tr>
* <tr><td class="TableSubHeadingColor"><b>Memory:</b></td><td></td></tr>
* <tr><td class="TableSubHeadingColor"><b>Unit Tests:</b></td><td>GroupingListTest</td></tr>
* <tr><td class="TableSubHeadingColor"><b>Issues:</b></td><td>
* <a href="https://glazedlists.dev.java.net/issues/show_bug.cgi?id=281">281</a>
* <a href="https://glazedlists.dev.java.net/issues/show_bug.cgi?id=491">491</a>
* </td></tr>
* </table>
*
* @author James Lemieux
*/
public final class GroupingList<E> extends TransformedList<E, List<E>> {
/** The GroupLists defined by the comparator. They are stored in an SimpleTree so their indices can be quickly updated. */
private SimpleTree<GroupList> groupLists = new SimpleTree<GroupList>();
/** The Grouper manages creating and deleting groups. */
private final Grouper<E> grouper;
/**
* Creates a {@link GroupingList} that determines groupings via the
* {@link Comparable} interface which all elements of the <code>source</code>
* are assumed to implement.
*/
public static <E extends Comparable<? super E>> GroupingList<E> create(EventList<E> source) {
return new GroupingList<E>(source);
}
/**
* Creates a {@link GroupingList} that determines groupings via the
* {@link Comparable} interface which all elements of the <code>source</code>
* are assumed to implement.
* <p>Usage of factory method {@link #create(EventList)} is preferable.
*/
public GroupingList(EventList<E> source) {
this(source, (Comparator<E>) 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<E> source, Comparator<? super E> comparator) {
this(new SortedList<E>(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<E> source, Comparator<? super E> comparator, Void dummyParameter) {
super(source);
// the grouper handles changes to the SortedList
this.grouper = new Grouper<E>(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 <code>groupElement</code>
* would belong if it were hypothetically added to the source list. Note
* that <code>groupElement</code> does <strong>NOT</strong> 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 <code>groupElement</code>
* if it were added to the source list or <code>-1</code> if no
* currently existing group would contain the <code>groupElement</code>
*/
public int indexOfGroup(E groupElement) {
// determine where the groupElement would be positioned in the source List
final int sourceIndex = ((SortedList<E>) 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<E> {
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
* <code>index</code>.
*
* @param index the location at which to insert an empty GroupList
*/
private void insertGroupList(int index) {
final GroupList groupList = new GroupList();
final Element<GroupList> indexedTreeNode = groupLists.add(index, groupList, 1);
groupList.setTreeNode(indexedTreeNode);
}
/**
* Removes the GroupList at the given <code>index</code>.
*
* @param index the location at which to remove a GroupList
*/
private void removeGroupList(int index) {
final Element<GroupList> 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;
* <tt>null</tt> will be treated as {@link GlazedLists#comparableComparator()}
*/
public void setComparator(Comparator<? super E> comparator) {
if (comparator == null)
comparator = (Comparator) GlazedLists.comparableComparator();
((SortedList<E>) source).setComparator(comparator);
}
/** {@inheritDoc} */
@Override
protected int getSourceIndex(int index) {
return grouper.getBarcode().getIndex(index, Grouper.UNIQUE);
}
/** {@inheritDoc} */
@Override
protected boolean isWritable() {
return true;
}
/** {@inheritDoc} */
@Override
public void listChanged(ListEvent<E> listChanges) {
updates.beginEvent(true);
// check if this ListEvent was caused due to a change in the
// Comparator that creates the groups
final SortedList<E> sortedSource = (SortedList<E>) 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} */
@Override
public List<E> get(int index) {
return groupLists.get(index).get();
}
/** {@inheritDoc} */
@Override
public List<E> remove(int index) {
if(index < 0 || index >= size()) throw new IndexOutOfBoundsException("Cannot remove at " + index + " on list of size " + size());
final List<E> removed = get(index);
// make a copy of the list to return
final List<E> result = new ArrayList<E>(removed);
removed.clear();
return result;
}
/** {@inheritDoc} */
@Override
public List<E> set(int index, List<E> value) {
if(index < 0 || index >= size()) throw new IndexOutOfBoundsException("Cannot set at " + index + " on list of size " + size());
updates.beginEvent(true);
final List<E> result = remove(index);
add(index, value);
updates.commitEvent();
return result;
}
/**
* This version of add will distribute all elements within the given
* <code>value</code> List into groups. Existing groups will be reused and
* new groups will be created as needed. As such, the <code>index</code>
* argument is meaningless.
*
* <p><strong><font color="#FF0000">Warning:</font></strong> This method
* breaks the contract required by {@link List#add(int, Object)}.
*/
@Override
public void add(int index, List<E> value) {
source.addAll(value);
}
/** {@inheritDoc} */
@Override
public int size() {
return grouper.getBarcode().colourSize(Grouper.UNIQUE);
}
/** {@inheritDoc} */
@Override
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<E> {
/**
* The node within {@link GroupingList#groupLists} that records the
* index of this GroupList within the GroupingList.
*/
private Element<GroupList> 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<GroupList> 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} */
@Override
public E set(int index, E element) {
return source.set(getSourceIndex(index), element);
}
/** {@inheritDoc} */
@Override
public E get(int index) {
return source.get(getSourceIndex(index));
}
/** {@inheritDoc} */
@Override
public int size() {
return getEndIndex() - getStartIndex();
}
/** {@inheritDoc} */
@Override
public void clear() {
source.subList(getStartIndex(), getEndIndex()).clear();
}
/** {@inheritDoc} */
@Override
public E remove(int index) {
return source.remove(getSourceIndex(index));
}
/** {@inheritDoc} */
@Override
public void add(int index, E element) {
source.add(getSourceIndex(index), element);
}
}
}
|