| /******************************************************************************* |
| * 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); |
| } |
| } |
| } |