blob: 7cfbe19af7b7e9268f93d7ab36ad8daf546d5043 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2010-2015 BSI Business Systems Integration AG.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* BSI Business Systems Integration AG - initial API and implementation
******************************************************************************/
package org.eclipse.scout.rt.client.ui.basic.tree;
import static org.eclipse.scout.rt.platform.util.Assertions.assertNotNull;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
import java.util.Set;
import org.eclipse.scout.rt.client.ui.AbstractEventBuffer;
import org.eclipse.scout.rt.platform.util.CollectionUtility;
import org.eclipse.scout.rt.platform.util.CompareUtility;
/**
* A buffer for tree events ({@link TreeEvent}s) with coalesce functionality:
* <p>
* <ul>
* <li>Unnecessary events are removed.
* <li>Events are merged, if possible.
* </ul>
* </p>
* Not thread safe, to be accessed in client model job.
*/
public class TreeEventBuffer extends AbstractEventBuffer<TreeEvent> {
/**
* Removes unnecessary events or combines events in the list.
*/
@Override
protected List<TreeEvent> coalesce(List<TreeEvent> events) {
removeObsolete(events);
removeNodesContainedInPreviousEvents(events, TreeEvent.TYPE_NODES_UPDATED, TreeEvent.TYPE_NODES_INSERTED);
removeNodesContainedInPreviousEvents(events, TreeEvent.TYPE_NODE_CHANGED, TreeEvent.TYPE_NODES_INSERTED);
removeNodesContainedInPreviousEvents(events, TreeEvent.TYPE_NODES_INSERTED, TreeEvent.TYPE_NODES_INSERTED);
removeEmptyEvents(events);
removeIdenticalEvents(events);
coalesceSameType(events);
return events;
}
/**
* Remove previous events that are now obsolete.
*/
protected void removeObsolete(List<TreeEvent> events) {
//traverse the list in reversed order
//previous events may be deleted from the list
for (int j = 0; j < events.size() - 1; j++) {
int i = events.size() - 1 - j;
TreeEvent event = events.get(i);
int type = event.getType();
if (isIgnorePrevious(type)) {
//remove all previous events of the same type
remove(type, events.subList(0, i));
}
else if (type == TreeEvent.TYPE_NODES_DELETED || type == TreeEvent.TYPE_ALL_CHILD_NODES_DELETED) {
List<ITreeNode> remainingNodes = removeNodesFromPreviousEvents(event.getNodes(), events.subList(0, i), TreeEvent.TYPE_NODES_INSERTED);
events.set(i, replaceNodesInEvent(event, remainingNodes));
}
else if (type == TreeEvent.TYPE_NODE_EXPANDED_RECURSIVE || type == TreeEvent.TYPE_NODE_COLLAPSED_RECURSIVE) {
remove(getExpansionRelatedEvents(), events.subList(0, i));
}
}
}
/**
* Removes the given 'nodesToRemove' from all 'events'. Recursive child nodes are also removed (but will not be
* returned). The event list is traversed backwards.
*
* @return a list with the same nodes as 'nodesToRemove', except those that were removed from an event whose type
* matches one of the 'creationTypes'. This allows for completely removing a node that was created and deleted
* in the same request.
*/
protected List<ITreeNode> removeNodesFromPreviousEvents(Collection<ITreeNode> nodesToRemove, List<TreeEvent> events, Integer... creationTypes) {
List<Integer> creationTypesList = Arrays.asList(creationTypes);
List<ITreeNode> remainingNodes = new ArrayList<ITreeNode>();
for (ITreeNode nodeToRemove : nodesToRemove) {
// Unwrap resolved node to get the real answer to "getChildNodes()"
nodeToRemove = TreeUtility.unwrapResolvedNode(nodeToRemove);
Collection<ITreeNode> allChildNodes = collectAllNodesRec(nodeToRemove.getChildNodes());
boolean nodeRemovedFromCreationEvent = false;
for (ListIterator<TreeEvent> it = events.listIterator(events.size()); it.hasPrevious();) {
TreeEvent event = it.previous();
// Remove the current node from the event and check if was removed from a creation event
TreeEvent newEvent = removeNode(event, nodeToRemove);
if (creationTypesList.contains(event.getType())) {
boolean removed = (event.getNodeCount() != newEvent.getNodeCount());
if (removed) {
nodeRemovedFromCreationEvent = true;
}
else {
// Also consider it as "removed from creation event" if one of the parents of nodeToRemove
// is a directly inserted node. The nodeToRemove will then not be contained in the insertion
// event, but because one of its parents was inserted recently, the deletion event is not
// required anymore (the insertion event does not contain deleted nodes).
ITreeNode parentToCheck = nodeToRemove.getParentNode();
while (parentToCheck != null) {
if (newEvent.containsNode(parentToCheck)) {
nodeRemovedFromCreationEvent = true;
break;
}
parentToCheck = parentToCheck.getParentNode();
}
}
}
// Now remove all recursive children (without considering them for the 'remainingNodes' list)
for (ITreeNode childNode : allChildNodes) {
newEvent = removeNode(newEvent, childNode);
}
// Replace the updated event
it.set(newEvent);
if (nodeRemovedFromCreationEvent) {
break;
}
}
if (!nodeRemovedFromCreationEvent) {
remainingNodes.add(nodeToRemove);
}
}
return remainingNodes;
}
protected TreeEvent removeNode(TreeEvent event, ITreeNode nodeToRemove) {
Collection<ITreeNode> nodes = event.getNodes();
for (Iterator<ITreeNode> it = nodes.iterator(); it.hasNext();) {
ITreeNode node = it.next();
if (CompareUtility.equals(node, nodeToRemove)) {
it.remove();
}
}
return replaceNodesInEvent(event, event.getCommonParentNode(), nodes);
}
/**
* Serves as a replacement for the missing "event.setNodes()" method. The same "common parent node" as in the given
* event is used. If you want to replace the common parent node, use
* {@link #replaceNodesInEvent(TreeEvent, ITreeNode, Collection)}.
*/
protected TreeEvent replaceNodesInEvent(TreeEvent event, Collection<ITreeNode> nodes) {
return replaceNodesInEvent(event, event.getCommonParentNode(), nodes);
}
/**
* Serves as a replacement for the missing "event.setNodes()" method.
*/
protected TreeEvent replaceNodesInEvent(TreeEvent event, ITreeNode commonParentNode, Collection<ITreeNode> nodes) {
return new TreeEvent(event.getTree(), event.getType(), commonParentNode, nodes);
}
/**
* Traverses the given list of events backwards, and checks for each event of type 'newType' if there is an older
* event of type 'oldType' that already contains the same nodes. If yes, the corresponding nodes are removed from the
* newer event.
* <p>
* Example: INSERT(A[B,C[E],D]), UPDATE(C[E],F) => UPDATE(C[E]) can be removed because INSERT already contains C[E] as
* child of A.
*/
protected void removeNodesContainedInPreviousEvents(List<TreeEvent> events, int newType, int oldType) {
for (int j = 0; j < events.size() - 1; j++) {
int i = events.size() - 1 - j;
TreeEvent event = events.get(i);
if (event.getType() == newType) {
// Build a mutable list of nodes, filter it and then replace it in the event (if there were some nodes removed)
List<ITreeNode> nodes = new ArrayList<>(event.getChildNodes());
boolean removed = filterNodesByPreviousEvents(nodes, events.subList(0, i), oldType);
if (removed) {
events.set(i, replaceNodesInEvent(event, nodes));
}
}
}
}
/**
* Removes all nodes from the list 'nodes' that are contained in one of the events of 'events' matching the type
* 'oldType'. A node is considered contained if it is a member of an events node list, or a child (at any level) of
* such a node. Please note that 'nodes' is a live-list, i.e. it is manipulated directly by this method.
*
* @return <code>true</code> if one or more nodes have been removed from 'nodes'.
*/
protected boolean filterNodesByPreviousEvents(List<ITreeNode> nodes, List<TreeEvent> events, int oldType) {
boolean removed = false;
for (Iterator<ITreeNode> it = nodes.iterator(); it.hasNext();) {
ITreeNode node = it.next();
for (TreeEvent event : events) {
if (event.getType() == oldType && event.containsNodeRecursive(node)) {
it.remove();
removed = true;
break; // no need to look further
}
}
}
return removed;
}
/**
* @return all the given nodes, including their recursive child nodes.
*/
protected Collection<ITreeNode> collectAllNodesRec(Collection<ITreeNode> nodes) {
List<ITreeNode> result = new ArrayList<ITreeNode>();
for (ITreeNode node : nodes) {
// Unwrap resolved node to get the real answer to "getChildNodes()"
node = TreeUtility.unwrapResolvedNode(node);
result.add(node);
result.addAll(collectAllNodesRec(node.getChildNodes()));
}
return result;
}
/**
* Merge previous events of the same type into the current and delete the previous events
*/
protected void coalesceSameType(List<TreeEvent> events) {
if (events.size() < 2) {
return;
}
final Map<ITreeNode, TreeEvent> initialEventByPrentNode = new HashMap<>();
final Map<ITreeNode, TreeEventMerger> eventMergerByParent = new HashMap<>();
int previousEventType = -1;
for (ListIterator<TreeEvent> it = events.listIterator(events.size()); it.hasPrevious();) {
final TreeEvent event = it.previous();
final int type = event.getType();
// clean-up initial event and event merger maps
if (previousEventType != type && !initialEventByPrentNode.isEmpty()) {
for (TreeEventMerger merger : eventMergerByParent.values()) {
merger.complete();
}
initialEventByPrentNode.clear();
eventMergerByParent.clear();
}
if (!isCoalesceConsecutivePrevious(type)) {
continue;
}
previousEventType = type;
final ITreeNode parentNode = event.getCommonParentNode();
final TreeEvent initialEvent = initialEventByPrentNode.get(event.getCommonParentNode());
if (initialEvent == null) {
// this is the first event with given common parent node.
// put it into the initial event cache and continue with the next event
initialEventByPrentNode.put(parentNode, event);
continue;
}
// there is already an initial event.
// check if there is already an event merger or create one
TreeEventMerger eventMerger = eventMergerByParent.get(parentNode);
if (eventMerger == null) {
eventMerger = new TreeEventMerger(initialEvent);
eventMergerByParent.put(parentNode, eventMerger);
}
// merge current event and remove it from the original event list
eventMerger.merge(event);
it.remove();
}
// complete "open" event mergers
for (TreeEventMerger eventMerger : eventMergerByParent.values()) {
eventMerger.complete();
}
}
protected void removeEmptyEvents(List<TreeEvent> events) {
for (Iterator<TreeEvent> it = events.iterator(); it.hasNext();) {
TreeEvent event = it.next();
if (isNodesRequired(event.getType()) && !event.hasNodes()
|| isCommonParentNodeRequired(event.getType()) && event.getCommonParentNode() == null) {
it.remove();
}
}
}
/**
* Removes identical events (same type and content) when they occur consecutively (not necessarily directly, but
* within the same type group). The oldest event is preserved.
*/
protected void removeIdenticalEvents(List<TreeEvent> events) {
// Please note: In contrast to all other methods in this class, this method loops through the
// list in FORWARD direction (so the oldest event will be kept).
for (int i = 0; i < events.size(); i++) {
TreeEvent event = events.get(i);
List<TreeEvent> subList = events.subList(i + 1, events.size());
for (Iterator<TreeEvent> it = subList.iterator(); it.hasNext();) {
TreeEvent next = it.next();
if (next.getType() != event.getType()) {
// Stop when a node of different type occurs
break;
}
if (isIdenticalEvent(event, next)) {
it.remove();
}
}
}
}
@Override
protected boolean isIdenticalEvent(TreeEvent event1, TreeEvent event2) {
if (event1 == null && event2 == null) {
return true;
}
if (event1 == null || event2 == null) {
return false;
}
boolean identical = (event1.getType() == event2.getType()
&& CompareUtility.equals(event1.getCommonParentNode(), event2.getCommonParentNode())
&& CollectionUtility.equalsCollection(event1.getNodes(), event2.getNodes(), true)
&& CollectionUtility.equalsCollection(event1.getDeselectedNodes(), event2.getDeselectedNodes(), true)
&& CollectionUtility.equalsCollection(event1.getNewSelectedNodes(), event2.getNewSelectedNodes(), true)
&& CollectionUtility.equalsCollection(event1.getPopupMenus(), event2.getPopupMenus())
&& event1.isConsumed() == event2.isConsumed()
&& CompareUtility.equals(event1.getDragObject(), event2.getDragObject())
&& CompareUtility.equals(event1.getDropObject(), event2.getDropObject()));
return identical;
}
protected Set<Integer> getExpansionRelatedEvents() {
Set<Integer> res = new HashSet<>();
res.add(TreeEvent.TYPE_NODE_EXPANDED);
res.add(TreeEvent.TYPE_NODE_EXPANDED_RECURSIVE);
res.add(TreeEvent.TYPE_NODE_COLLAPSED);
res.add(TreeEvent.TYPE_NODE_COLLAPSED_RECURSIVE);
return res;
}
/**
* @param type
* {@link TreeEvent} type
* @return <code>true</code>, if previous events of the same type can be ignored. <code>false</code> otherwise
*/
protected boolean isIgnorePrevious(int type) {
switch (type) {
case TreeEvent.TYPE_NODES_SELECTED:
case TreeEvent.TYPE_BEFORE_NODES_SELECTED:
case TreeEvent.TYPE_SCROLL_TO_SELECTION: {
return true;
}
default: {
return false;
}
}
}
/**
* @return true, if previous consecutive events of the same type can be coalesced.
*/
protected boolean isCoalesceConsecutivePrevious(int type) {
switch (type) {
case TreeEvent.TYPE_NODES_UPDATED:
case TreeEvent.TYPE_NODES_INSERTED:
case TreeEvent.TYPE_NODES_DELETED:
case TreeEvent.TYPE_NODES_CHECKED: {
return true;
}
default: {
return false;
}
}
}
protected boolean isNodesRequired(int type) {
switch (type) {
case TreeEvent.TYPE_CHILD_NODE_ORDER_CHANGED:
case TreeEvent.TYPE_NODES_DELETED:
case TreeEvent.TYPE_NODES_DRAG_REQUEST:
case TreeEvent.TYPE_NODES_INSERTED:
case TreeEvent.TYPE_NODES_UPDATED:
case TreeEvent.TYPE_ALL_CHILD_NODES_DELETED: {
// Multiple nodes
return true;
}
case TreeEvent.TYPE_NODE_ACTION:
case TreeEvent.TYPE_NODE_CHANGED:
case TreeEvent.TYPE_NODE_CLICK:
case TreeEvent.TYPE_NODE_COLLAPSED:
case TreeEvent.TYPE_NODE_COLLAPSED_RECURSIVE:
case TreeEvent.TYPE_NODE_DROP_ACTION:
case TreeEvent.TYPE_NODE_DROP_TARGET_CHANGED:
case TreeEvent.TYPE_NODE_ENSURE_VISIBLE:
case TreeEvent.TYPE_NODE_EXPANDED:
case TreeEvent.TYPE_NODE_EXPANDED_RECURSIVE:
case TreeEvent.TYPE_NODE_FILTER_CHANGED: {
// Single node
return true;
}
case TreeEvent.TYPE_BEFORE_NODES_SELECTED:
case TreeEvent.TYPE_NODES_SELECTED:
case TreeEvent.TYPE_DRAG_FINISHED:
case TreeEvent.TYPE_NODE_REQUEST_FOCUS:
case TreeEvent.TYPE_REQUEST_FOCUS:
case TreeEvent.TYPE_SCROLL_TO_SELECTION:
case TreeEvent.TYPE_NODES_CHECKED:
default: {
return false;
}
}
}
protected boolean isCommonParentNodeRequired(int type) {
switch (type) {
case TreeEvent.TYPE_ALL_CHILD_NODES_DELETED: {
return true;
}
case TreeEvent.TYPE_NODES_INSERTED:
case TreeEvent.TYPE_CHILD_NODE_ORDER_CHANGED:
case TreeEvent.TYPE_NODES_UPDATED:
case TreeEvent.TYPE_NODES_DELETED:
case TreeEvent.TYPE_NODE_FILTER_CHANGED:
case TreeEvent.TYPE_BEFORE_NODES_SELECTED:
case TreeEvent.TYPE_NODES_SELECTED:
case TreeEvent.TYPE_NODE_EXPANDED:
case TreeEvent.TYPE_NODE_COLLAPSED:
case TreeEvent.TYPE_NODE_EXPANDED_RECURSIVE:
case TreeEvent.TYPE_NODE_COLLAPSED_RECURSIVE:
case TreeEvent.TYPE_NODE_ACTION:
case TreeEvent.TYPE_NODES_DRAG_REQUEST:
case TreeEvent.TYPE_DRAG_FINISHED:
case TreeEvent.TYPE_NODE_DROP_ACTION:
case TreeEvent.TYPE_NODE_REQUEST_FOCUS:
case TreeEvent.TYPE_NODE_ENSURE_VISIBLE:
case TreeEvent.TYPE_REQUEST_FOCUS:
case TreeEvent.TYPE_NODE_CLICK:
case TreeEvent.TYPE_SCROLL_TO_SELECTION:
case TreeEvent.TYPE_NODE_CHANGED:
case TreeEvent.TYPE_NODE_DROP_TARGET_CHANGED:
case TreeEvent.TYPE_NODES_CHECKED:
default: {
return false;
}
}
}
/**
* Helper for merging nodes form other {@link TreeEvent}s into the initial target event. <br/>
* <b>Note</b>: The {@link #merge(TreeEvent)} method does not check any rules. The given event's nodes are merged in
* any case.<br/>
* <b>Usage</b>:
*
* <pre>
* TreeEventMerger eventMerger = new TreeEventMerger(targetEvent);
* eventMerger.merge(e1);
* eventMerger.merge(e2);
* eventMerger.complete();
* </pre>
*/
protected static class TreeEventMerger {
private final TreeEvent m_targetEvent;
private Collection<ITreeNode> m_targetNodes;
private HashSet<ITreeNode> m_targetNodeSet;
private List<ITreeNode> m_mergedNodes;
public TreeEventMerger(TreeEvent targetEvent) {
m_targetEvent = assertNotNull(targetEvent, "targetEvent must not be null");
m_mergedNodes = new LinkedList<>();
}
/**
* Merges nodes. Using this method after invoking {@link #complete()} throws an {@link IllegalStateException}.
*/
public void merge(TreeEvent event) {
if (m_mergedNodes == null) {
throw new IllegalStateException("Invocations of merge is not allowed after complete has been invoked.");
}
if (!event.hasNodes()) {
return;
}
ensureInitialized();
mergeCollections(event.getNodes(), m_mergedNodes, m_targetNodeSet);
}
protected void ensureInitialized() {
if (m_targetNodes != null) {
return;
}
m_targetNodes = m_targetEvent.getNodes();
m_targetNodeSet = new HashSet<>(m_targetNodes);
}
/**
* Completes the merge process. Subsequent invocations of this method does not have any effects.
*/
public void complete() {
if (m_mergedNodes == null) {
return;
}
if (m_targetNodes != null) {
m_mergedNodes.addAll(m_targetNodes);
m_targetEvent.setNodes(m_mergedNodes);
}
m_mergedNodes = null;
}
/**
* Merge collections, such that, if an element is in both collections, only the one of the second collection (later
* event) is kept.
*/
protected <TYPE> void mergeCollections(Collection<TYPE> source, List<TYPE> target, HashSet<TYPE> targetSet) {
for (Iterator<TYPE> it = source.iterator(); it.hasNext();) {
TYPE sourceElement = it.next();
if (!targetSet.add(sourceElement)) { // returns true, if the sourceElement has been added; false, if it was already in the set.
it.remove();
}
}
target.addAll(0, source);
}
}
}