TreeEventBuffer: performance improvements
o added several methods on TreeEvent so that the internal rows must not
be copied:
o containsNode()
o hasNodes()
o added TreeEvent.setNodes
o improved TreeEventBuffer.coalesceSameType
(runs now in O(n) instead O(n^2))
o simplified TreeUtility.calculateCommonParentNode and added tests
Change-Id: I7e4751b117fb80d295f3457ab3f59ba5686d8c03
Reviewed-on: https://git.eclipse.org/r/75452
Tested-by: Hudson CI
Reviewed-by: Andi Bur <andi.bur@gmail.com>
diff --git a/org.eclipse.scout.rt.client.test/src/test/java/org/eclipse/scout/rt/client/ui/basic/table/TableEventBufferTest.java b/org.eclipse.scout.rt.client.test/src/test/java/org/eclipse/scout/rt/client/ui/basic/table/TableEventBufferTest.java
index 2afc1d2..4dfc6ac 100644
--- a/org.eclipse.scout.rt.client.test/src/test/java/org/eclipse/scout/rt/client/ui/basic/table/TableEventBufferTest.java
+++ b/org.eclipse.scout.rt.client.test/src/test/java/org/eclipse/scout/rt/client/ui/basic/table/TableEventBufferTest.java
@@ -602,7 +602,7 @@
final int rowCount = 10;
List<ITableRow> rows = new ArrayList<>(rowCount);
LinkedList<TableEvent> events = new LinkedList<>();
- for (int i = 0; i < 10; i++) {
+ for (int i = 0; i < rowCount; i++) {
ITableRow row = mockRow(i);
rows.add(row);
events.add(new TableEvent(table, TableEvent.TYPE_ROWS_INSERTED, Collections.singletonList(row)));
@@ -621,7 +621,7 @@
final int rowCount = 10;
List<ITableRow> rows = new ArrayList<>(rowCount);
LinkedList<TableEvent> events = new LinkedList<>();
- for (int i = 0; i < 10; i++) {
+ for (int i = 0; i < rowCount; i++) {
ITableRow row1 = mockRow(2 * i);
ITableRow row2 = mockRow(2 * i + 1);
rows.add(row1);
@@ -727,7 +727,7 @@
assertEquals(Collections.emptyList(), e5.getRows());
}
- @Test(timeout = 2000)
+ @Test(timeout = 10000)
public void testCoalesceSameTypeWithManyInsertEvents() throws Exception {
final int eventCount = 10000;
ITable table = mock(ITable.class);
@@ -742,7 +742,7 @@
assertEquals(eventCount, CollectionUtility.firstElement(tableEvents).getRowCount());
}
- @Test(timeout = 2000)
+ @Test(timeout = 10000)
public void testRemoveObsoleteWithManyInsertAndOneDeleteAllRowsEvent() throws Exception {
final int insertEventCount = 10000;
ITable table = mock(ITable.class);
@@ -761,7 +761,7 @@
}
- @Test(timeout = 2000)
+ @Test(timeout = 10000)
public void testRemoveObsoleteWithManyInsertEvents() throws Exception {
final int insertEventCount = 10000;
ITable table = mock(ITable.class);
@@ -776,7 +776,7 @@
}
- @Test(timeout = 2000)
+ @Test(timeout = 10000)
public void testRemoveIdenticalEventsWithManyInsertEvents() throws Exception {
final int eventCount = 10000;
ITable table = mock(ITable.class);
@@ -792,7 +792,7 @@
assertEquals(eventCount, tableEvents.size());
}
- @Test(timeout = 2000)
+ @Test(timeout = 10000)
public void testRemoveIdenticalEventsWithManyInsertAndDeleteEvents() throws Exception {
final int eventCount = 10000;
ITable table = mock(ITable.class);
diff --git a/org.eclipse.scout.rt.client.test/src/test/java/org/eclipse/scout/rt/client/ui/basic/tree/TreeEventBufferTest.java b/org.eclipse.scout.rt.client.test/src/test/java/org/eclipse/scout/rt/client/ui/basic/tree/TreeEventBufferTest.java
index c4d6a1a..9b1eb80 100644
--- a/org.eclipse.scout.rt.client.test/src/test/java/org/eclipse/scout/rt/client/ui/basic/tree/TreeEventBufferTest.java
+++ b/org.eclipse.scout.rt.client.test/src/test/java/org/eclipse/scout/rt/client/ui/basic/tree/TreeEventBufferTest.java
@@ -11,8 +11,10 @@
package org.eclipse.scout.rt.client.ui.basic.tree;
import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
@@ -21,11 +23,12 @@
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
+import java.util.LinkedList;
import java.util.List;
import java.util.Map;
+import org.eclipse.scout.rt.platform.util.Assertions.AssertionException;
import org.eclipse.scout.rt.platform.util.CollectionUtility;
-
import org.junit.Before;
import org.junit.Test;
@@ -96,11 +99,11 @@
final List<TreeEvent> coalesced = m_testBuffer.consumeAndCoalesceEvents();
assertEquals(3, coalesced.size());
assertEquals(TreeEvent.TYPE_NODES_UPDATED, coalesced.get(0).getType());
- assertEquals(3, coalesced.get(0).getNodes().size());
+ assertEquals(3, coalesced.get(0).getNodeCount());
assertEquals(TreeEvent.TYPE_CHILD_NODE_ORDER_CHANGED, coalesced.get(1).getType());
- assertEquals(3, coalesced.get(1).getNodes().size());
+ assertEquals(3, coalesced.get(1).getNodeCount());
assertEquals(TreeEvent.TYPE_NODES_UPDATED, coalesced.get(2).getType());
- assertEquals(4, coalesced.get(2).getNodes().size());
+ assertEquals(4, coalesced.get(2).getNodeCount());
}
/**
@@ -137,7 +140,7 @@
final List<TreeEvent> coalesced = m_testBuffer.consumeAndCoalesceEvents();
assertEquals(2, coalesced.size());
assertEquals(TreeEvent.TYPE_NODES_INSERTED, coalesced.get(1).getType());
- assertEquals(1, coalesced.get(1).getNodes().size());
+ assertEquals(1, coalesced.get(1).getNodeCount());
}
/**
@@ -166,21 +169,21 @@
final List<TreeEvent> coalesced = m_testBuffer.consumeAndCoalesceEvents();
assertEquals(8, coalesced.size());
assertEquals(TreeEvent.TYPE_NODE_CHANGED, coalesced.get(0).getType());
- assertEquals(1, coalesced.get(0).getNodes().size());
+ assertEquals(1, coalesced.get(0).getNodeCount());
assertEquals(TreeEvent.TYPE_NODE_CHANGED, coalesced.get(1).getType());
- assertEquals(1, coalesced.get(1).getNodes().size());
+ assertEquals(1, coalesced.get(1).getNodeCount());
assertEquals(TreeEvent.TYPE_NODE_CHANGED, coalesced.get(2).getType());
- assertEquals(1, coalesced.get(2).getNodes().size());
+ assertEquals(1, coalesced.get(2).getNodeCount());
assertEquals(TreeEvent.TYPE_CHILD_NODE_ORDER_CHANGED, coalesced.get(3).getType());
- assertEquals(3, coalesced.get(3).getNodes().size());
+ assertEquals(3, coalesced.get(3).getNodeCount());
assertEquals(TreeEvent.TYPE_NODE_CHANGED, coalesced.get(4).getType());
- assertEquals(1, coalesced.get(4).getNodes().size());
+ assertEquals(1, coalesced.get(4).getNodeCount());
assertEquals(TreeEvent.TYPE_NODE_CHANGED, coalesced.get(5).getType());
- assertEquals(1, coalesced.get(5).getNodes().size());
+ assertEquals(1, coalesced.get(5).getNodeCount());
assertEquals(TreeEvent.TYPE_NODE_CHANGED, coalesced.get(6).getType());
- assertEquals(1, coalesced.get(6).getNodes().size());
+ assertEquals(1, coalesced.get(6).getNodeCount());
assertEquals(TreeEvent.TYPE_NODE_CHANGED, coalesced.get(7).getType());
- assertEquals(1, coalesced.get(7).getNodes().size());
+ assertEquals(1, coalesced.get(7).getNodeCount());
}
/**
@@ -199,11 +202,11 @@
final List<TreeEvent> coalesced = m_testBuffer.consumeAndCoalesceEvents();
assertEquals(3, coalesced.size());
assertEquals(TreeEvent.TYPE_NODES_INSERTED, coalesced.get(0).getType());
- assertEquals(3, coalesced.get(0).getNodes().size());
+ assertEquals(3, coalesced.get(0).getNodeCount());
assertEquals(TreeEvent.TYPE_CHILD_NODE_ORDER_CHANGED, coalesced.get(1).getType());
- assertEquals(3, coalesced.get(1).getNodes().size());
+ assertEquals(3, coalesced.get(1).getNodeCount());
assertEquals(TreeEvent.TYPE_NODES_UPDATED, coalesced.get(2).getType());
- assertEquals(1, coalesced.get(2).getNodes().size());
+ assertEquals(1, coalesced.get(2).getNodeCount());
}
/**
@@ -222,7 +225,7 @@
final List<TreeEvent> coalesced = m_testBuffer.consumeAndCoalesceEvents();
assertEquals(1, coalesced.size());
assertEquals(TreeEvent.TYPE_NODES_DELETED, coalesced.get(0).getType());
- assertEquals(1, coalesced.get(0).getNodes().size());
+ assertEquals(1, coalesced.get(0).getNodeCount());
}
/**
@@ -300,7 +303,7 @@
List<TreeEvent> coalesced = m_testBuffer.consumeAndCoalesceEvents();
assertEquals(1, coalesced.size()); // 1x NODES_INSERTED
assertEquals(TreeEvent.TYPE_NODES_INSERTED, coalesced.get(0).getType());
- assertEquals(3, coalesced.get(0).getNodes().size());
+ assertEquals(3, coalesced.get(0).getNodeCount());
// --- Test case 2: NODES_DELETED --------------------------------
@@ -316,7 +319,7 @@
coalesced = m_testBuffer.consumeAndCoalesceEvents();
assertEquals(1, coalesced.size()); // 1x NODES_INSERTED
assertEquals(TreeEvent.TYPE_NODES_INSERTED, coalesced.get(0).getType());
- assertEquals(3, coalesced.get(0).getNodes().size());
+ assertEquals(3, coalesced.get(0).getNodeCount());
}
/**
@@ -550,21 +553,21 @@
assertType(TreeEvent.TYPE_NODES_CHECKED, coalesced, 0);
assertEquals(nodeA, coalesced.get(0).getCommonParentNode());
- assertEquals(1, coalesced.get(0).getNodes().size());
+ assertEquals(1, coalesced.get(0).getNodeCount());
assertType(TreeEvent.TYPE_NODES_UPDATED, coalesced, 1);
assertType(TreeEvent.TYPE_NODES_CHECKED, coalesced, 2);
assertEquals(nodeB, coalesced.get(2).getCommonParentNode());
- assertEquals(1, coalesced.get(2).getNodes().size());
+ assertEquals(1, coalesced.get(2).getNodeCount());
assertType(TreeEvent.TYPE_NODES_CHECKED, coalesced, 3);
assertEquals(nodeC, coalesced.get(3).getCommonParentNode());
- assertEquals(1, coalesced.get(3).getNodes().size());
+ assertEquals(1, coalesced.get(3).getNodeCount());
assertType(TreeEvent.TYPE_NODES_CHECKED, coalesced, 4);
assertEquals(nodeA, coalesced.get(4).getCommonParentNode());
- assertEquals(2, coalesced.get(4).getNodes().size());
+ assertEquals(2, coalesced.get(4).getNodeCount());
}
/**
@@ -629,11 +632,197 @@
assertContainsNode(coalesced, 1, nodeA, nodeB);
}
+ @Test
+ public void testCoalesceSameTypeCheckOrderSingleNodeEvents() {
+ ITree tree = mock(ITree.class);
+ final int nodeCount = 10;
+ List<ITreeNode> nodes = new ArrayList<>(nodeCount);
+ LinkedList<TreeEvent> events = new LinkedList<>();
+ for (int i = 0; i < nodeCount; i++) {
+ ITreeNode node = mockNode(String.valueOf(i));
+ nodes.add(node);
+ events.add(new TreeEvent(tree, TreeEvent.TYPE_NODES_INSERTED, Collections.singletonList(node)));
+ }
+
+ assertEquals(nodeCount, events.size());
+ m_testBuffer.coalesceSameType(events);
+
+ assertEquals(1, events.size());
+ assertEquals(nodes, events.get(0).getNodes());
+ }
+
+ @Test
+ public void testCoalesceSameTypeCheckOrderMultipleNodesEvents() {
+ ITree tree = mock(ITree.class);
+ final int nodeCount = 10;
+ List<ITreeNode> nodes = new ArrayList<>(nodeCount);
+ LinkedList<TreeEvent> events = new LinkedList<>();
+ for (int i = 0; i < nodeCount; i++) {
+ ITreeNode node1 = mockNode(String.valueOf(2 * i));
+ ITreeNode node2 = mockNode(String.valueOf(2 * i + 1));
+ nodes.add(node1);
+ nodes.add(node2);
+ events.add(new TreeEvent(tree, TreeEvent.TYPE_NODES_INSERTED, Arrays.asList(node1, node2)));
+ }
+
+ assertEquals(nodeCount, events.size());
+ m_testBuffer.coalesceSameType(events);
+
+ assertEquals(1, events.size());
+ assertEquals(nodes, events.get(0).getNodes());
+ }
+
+ @Test(timeout = 10000)
+ public void testCoalesceSameTypeWithManyInsertEvents() throws Exception {
+ final int eventCount = 10000;
+ ITree tree = mock(ITree.class);
+ ITreeNode parentA = mockNode("parentA");
+ LinkedList<TreeEvent> treeEvents = new LinkedList<>();
+ for (int i = 0; i < eventCount; i++) {
+ treeEvents.add(new TreeEvent(tree, TreeEvent.TYPE_NODES_INSERTED, Collections.singletonList(mockNode(String.valueOf(i), parentA))));
+ }
+
+ assertEquals(eventCount, treeEvents.size());
+ m_testBuffer.coalesceSameType(treeEvents);
+ assertEquals(1, treeEvents.size());
+ assertEquals(eventCount, CollectionUtility.firstElement(treeEvents).getNodeCount());
+ }
+
+ @Test(timeout = 10000)
+ public void testCoalesceSameTypeWithManyInsertHavingDifferentParentsEvents() throws Exception {
+ final int eventCount = 10000;
+ ITree tree = mock(ITree.class);
+ LinkedList<TreeEvent> treeEvents = new LinkedList<>();
+ ITreeNode parentA = mockNode("parentA");
+ ITreeNode parentB = mockNode("parentB");
+ for (int i = 0; i < eventCount; i++) {
+ treeEvents.add(new TreeEvent(tree, TreeEvent.TYPE_NODES_INSERTED, Collections.singletonList(mockNode(String.valueOf(2 * i), parentA))));
+ treeEvents.add(new TreeEvent(tree, TreeEvent.TYPE_NODES_INSERTED, Collections.singletonList(mockNode(String.valueOf(2 * i + 1), parentB))));
+ }
+
+ assertEquals(2 * eventCount, treeEvents.size());
+ m_testBuffer.coalesceSameType(treeEvents);
+ assertEquals(2, treeEvents.size());
+
+ TreeEvent firstEvent = treeEvents.get(0);
+ TreeEvent secondEvent = treeEvents.get(1);
+ assertEquals(eventCount, firstEvent.getNodeCount());
+ assertEquals(eventCount, secondEvent.getNodeCount());
+
+ assertSame(parentA, firstEvent.getCommonParentNode());
+ assertSame(parentB, secondEvent.getCommonParentNode());
+ }
+
+ @Test(timeout = 10000)
+ public void testCoalesceSameTypeWithManyInsertUpdateEvents() throws Exception {
+ final int eventCount = 10000;
+ ITree tree = mock(ITree.class);
+ LinkedList<TreeEvent> treeEvents = new LinkedList<>();
+ ITreeNode parentA = mockNode("parentA");
+ for (int i = 0; i < eventCount; i++) {
+ ITreeNode node = mockNode(String.valueOf(i), parentA);
+ treeEvents.add(new TreeEvent(tree, TreeEvent.TYPE_NODES_INSERTED, Collections.singletonList(node)));
+ treeEvents.add(new TreeEvent(tree, TreeEvent.TYPE_NODES_UPDATED, Collections.singletonList(node)));
+ }
+
+ assertEquals(2 * eventCount, treeEvents.size());
+ m_testBuffer.coalesceSameType(treeEvents);
+ assertEquals(2 * eventCount, treeEvents.size());
+ }
+
+ @Test(timeout = 10000)
+ public void testCoalesceSameTypeWithManyInsertInsertUpdateUpdateEvents() throws Exception {
+ final int eventCount = 10000;
+ ITree tree = mock(ITree.class);
+ LinkedList<TreeEvent> treeEvents = new LinkedList<>();
+ ITreeNode parentA = mockNode("parentA");
+ for (int i = 0; i < eventCount; i++) {
+ ITreeNode node1 = mockNode(String.valueOf(2 * i), parentA);
+ ITreeNode node2 = mockNode(String.valueOf(2 * i + 1), parentA);
+ treeEvents.add(new TreeEvent(tree, TreeEvent.TYPE_NODES_INSERTED, Collections.singletonList(node1)));
+ treeEvents.add(new TreeEvent(tree, TreeEvent.TYPE_NODES_INSERTED, Collections.singletonList(node2)));
+ treeEvents.add(new TreeEvent(tree, TreeEvent.TYPE_NODES_UPDATED, Collections.singletonList(node1)));
+ treeEvents.add(new TreeEvent(tree, TreeEvent.TYPE_NODES_UPDATED, Collections.singletonList(node2)));
+ }
+
+ assertEquals(4 * eventCount, treeEvents.size());
+ m_testBuffer.coalesceSameType(treeEvents);
+ assertEquals(2 * eventCount, treeEvents.size());
+ }
+
+ @Test(expected = AssertionException.class)
+ public void testTreeEventMergerNullInitilaEvent() {
+ new TreeEventBuffer.TreeEventMerger(null);
+ }
+
+ @Test
+ public void testTreeEventMerger() {
+ ITree tree = mock(ITree.class);
+ ITreeNode nodeA = mockNode("a");
+ ITreeNode nodeB = mockNode("b");
+ TreeEvent initialEvent = new TreeEvent(tree, TreeEvent.TYPE_NODE_CHANGED, Arrays.asList(nodeA, nodeB));
+ TreeEventBuffer.TreeEventMerger eventMerger = new TreeEventBuffer.TreeEventMerger(initialEvent);
+
+ // add first event
+ ITreeNode nodeC = mockNode("c");
+ TreeEvent e1 = new TreeEvent(tree, TreeEvent.TYPE_NODE_CHANGED, Arrays.asList(nodeA, nodeB, nodeC));
+ eventMerger.merge(e1);
+
+ // add second, empty event
+ TreeEvent e2 = new TreeEvent(tree, TreeEvent.TYPE_NODE_CHANGED);
+ eventMerger.merge(e2);
+
+ // add third event
+ ITreeNode nodeD = mockNode("d");
+ ITreeNode nodeE = mockNode("e");
+ TreeEvent e3 = new TreeEvent(tree, TreeEvent.TYPE_NODE_CHANGED, Arrays.asList(nodeD, nodeE));
+ eventMerger.merge(e3);
+
+ eventMerger.complete();
+ assertEquals(Arrays.asList(nodeD, nodeE, nodeC, nodeA, nodeB), initialEvent.getNodes());
+ assertNull(initialEvent.getCommonParentNode());
+
+ // invoke complete a second time has no effect
+ eventMerger.complete();
+ assertEquals(Arrays.asList(nodeD, nodeE, nodeC, nodeA, nodeB), initialEvent.getNodes());
+ assertNull(initialEvent.getCommonParentNode());
+ }
+
+ @Test
+ public void testTableEventMergerCompleteWithoutMerge() {
+ ITree tree = mock(ITree.class);
+ ITreeNode nodeA = mockNode("a");
+ ITreeNode nodeB = mockNode("b");
+ TreeEvent initialEvent = new TreeEvent(tree, TreeEvent.TYPE_NODE_CHANGED, Arrays.asList(nodeA, nodeB));
+ TreeEventBuffer.TreeEventMerger eventMerger = new TreeEventBuffer.TreeEventMerger(initialEvent);
+
+ eventMerger.complete();
+
+ assertEquals(Arrays.asList(nodeA, nodeB), initialEvent.getNodes());
+ assertNull(initialEvent.getCommonParentNode());
+ }
+
+ @Test
+ public void testTableEventMergerMergeAfterComplete() {
+ ITree tree = mock(ITree.class);
+ TreeEvent initialEvent = new TreeEvent(tree, TreeEvent.TYPE_NODE_CHANGED, mockNodes("a", "b"));
+ TreeEventBuffer.TreeEventMerger eventMerger = new TreeEventBuffer.TreeEventMerger(initialEvent);
+ eventMerger.complete();
+
+ TreeEvent e1 = new TreeEvent(tree, TreeEvent.TYPE_NODE_CHANGED, mockNodes("c", "d"));
+ try {
+ eventMerger.merge(e1);
+ fail("merge after complete must throw an " + IllegalStateException.class.getSimpleName());
+ }
+ catch (IllegalStateException epxected) {
+ }
+ }
+
private void assertContainsNode(List<TreeEvent> events, int index, ITreeNode... expectedNodes) {
TreeEvent event = events.get(index);
- assertEquals(expectedNodes.length, event.getNodes().size());
+ assertEquals(expectedNodes.length, event.getNodeCount());
for (ITreeNode node : expectedNodes) {
- assertTrue(event.getNodes().contains(node));
+ assertTrue(event.containsNode(node));
}
}
@@ -681,6 +870,10 @@
}
private ITreeNode mockNode(String nodeId) {
+ return mockNode(nodeId, null);
+ }
+
+ private ITreeNode mockNode(String nodeId, ITreeNode parentNode) {
ITreeNode node = m_mockNodes.get(nodeId);
if (node != null) {
return node;
@@ -688,6 +881,7 @@
// Create a new
node = mock(ITreeNode.class, "MockNode[" + nodeId + "]");
when(node.getNodeId()).thenReturn(nodeId);
+ when(node.getParentNode()).thenReturn(parentNode);
m_mockNodes.put(nodeId, node);
return node;
}
@@ -700,5 +894,4 @@
when(childNode.getParentNode()).thenReturn(node);
}
}
-
}
diff --git a/org.eclipse.scout.rt.client.test/src/test/java/org/eclipse/scout/rt/client/ui/basic/tree/TreeEventTest.java b/org.eclipse.scout.rt.client.test/src/test/java/org/eclipse/scout/rt/client/ui/basic/tree/TreeEventTest.java
new file mode 100644
index 0000000..37ba33c
--- /dev/null
+++ b/org.eclipse.scout.rt.client.test/src/test/java/org/eclipse/scout/rt/client/ui/basic/tree/TreeEventTest.java
@@ -0,0 +1,271 @@
+package org.eclipse.scout.rt.client.ui.basic.tree;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * @since 5.2
+ */
+public class TreeEventTest {
+
+ private Map<String, ITreeNode> m_mockNodes;
+
+ @Before
+ public void setup() {
+ m_mockNodes = new HashMap<>();
+ }
+
+ @Test
+ public void testGetNodeNoNodes() {
+ ITree tree = mock(ITree.class);
+ TreeEvent event = new TreeEvent(tree, TreeEvent.TYPE_NODE_ACTION);
+ assertNull(event.getNode());
+ }
+
+ @Test
+ public void testGetNodeSingleNode() {
+ ITree tree = mock(ITree.class);
+ ITreeNode root = mockNode("root");
+ ITreeNode node = mockNode("a");
+ when(node.getParentNode()).thenReturn(root);
+ TreeEvent event = new TreeEvent(tree, TreeEvent.TYPE_NODE_ACTION, node);
+ assertSame(node, event.getNode());
+ }
+
+ @Test
+ public void testGetNodeMultipleNode() {
+ ITree tree = mock(ITree.class);
+ ITreeNode root = mockNode("root");
+ ITreeNode nodeA = mockNode("a");
+ ITreeNode nodeB = mockNode("b");
+ when(nodeA.getParentNode()).thenReturn(root);
+ when(nodeB.getParentNode()).thenReturn(root);
+ TreeEvent event = new TreeEvent(tree, TreeEvent.TYPE_NODE_ACTION, Arrays.asList(nodeB, nodeA));
+ assertSame(nodeB, event.getNode());
+ }
+
+ @Test
+ public void testGetNodesNoNodes() {
+ ITree tree = mock(ITree.class);
+ TreeEvent event = new TreeEvent(tree, TreeEvent.TYPE_NODE_ACTION);
+ assertEquals(Collections.emptyList(), event.getNodes());
+ }
+
+ @Test
+ public void testGetNodesSingleNode() {
+ ITree tree = mock(ITree.class);
+ ITreeNode node = mockNode("a");
+ TreeEvent event = new TreeEvent(tree, TreeEvent.TYPE_NODE_ACTION, node);
+ assertEquals(Collections.singletonList(node), event.getNodes());
+ }
+
+ @Test
+ public void testGetNodesMultipleNode() {
+ ITree tree = mock(ITree.class);
+ ITreeNode nodeA = mockNode("a");
+ ITreeNode nodeB = mockNode("b");
+ TreeEvent event = new TreeEvent(tree, TreeEvent.TYPE_NODE_ACTION, Arrays.asList(nodeB, nodeA));
+ assertEquals(Arrays.asList(nodeB, nodeA), event.getNodes());
+ }
+
+ @Test
+ public void testHasNodesNoNodes() {
+ ITree tree = mock(ITree.class);
+ TreeEvent event = new TreeEvent(tree, TreeEvent.TYPE_NODE_ACTION);
+ assertFalse(event.hasNodes());
+ }
+
+ @Test
+ public void testHasNodesSingleNode() {
+ ITree tree = mock(ITree.class);
+ TreeEvent event = new TreeEvent(tree, TreeEvent.TYPE_NODE_ACTION, mockNode("a"));
+ assertTrue(event.hasNodes());
+ }
+
+ @Test
+ public void testHasNodesMultipleNode() {
+ ITree tree = mock(ITree.class);
+ TreeEvent event = new TreeEvent(tree, TreeEvent.TYPE_NODE_ACTION, mockNodes("a", "b"));
+ assertTrue(event.hasNodes());
+ }
+
+ @Test
+ public void testGetNodeCountNoNodes() {
+ ITree tree = mock(ITree.class);
+ TreeEvent event = new TreeEvent(tree, TreeEvent.TYPE_NODE_ACTION);
+ assertEquals(0, event.getNodeCount());
+ }
+
+ @Test
+ public void testGetNodeCountSingleNode() {
+ ITree tree = mock(ITree.class);
+ TreeEvent event = new TreeEvent(tree, TreeEvent.TYPE_NODE_ACTION, mockNode("a"));
+ assertEquals(1, event.getNodeCount());
+ }
+
+ @Test
+ public void testGetNodeCountMultipleNode() {
+ ITree tree = mock(ITree.class);
+ TreeEvent event = new TreeEvent(tree, TreeEvent.TYPE_NODE_ACTION, mockNodes("a", "b"));
+ assertEquals(2, event.getNodeCount());
+ }
+
+ @Test
+ public void testContainsNodeNoNodes() {
+ ITree tree = mock(ITree.class);
+ ITreeNode nodeA = mockNode("a");
+ TreeEvent event = new TreeEvent(tree, TreeEvent.TYPE_NODE_ACTION);
+ assertFalse(event.containsNode(nodeA));
+ }
+
+ @Test
+ public void testContainsNodeSingleNode() {
+ ITree tree = mock(ITree.class);
+ ITreeNode nodeA = mockNode("a");
+ TreeEvent event = new TreeEvent(tree, TreeEvent.TYPE_NODE_ACTION, nodeA);
+ assertTrue(event.containsNode(nodeA));
+ }
+
+ @Test
+ public void testContainsNodeMultipleNode() {
+ ITree tree = mock(ITree.class);
+ ITreeNode nodeA = mockNode("a");
+ ITreeNode nodeB = mockNode("b");
+ TreeEvent event = new TreeEvent(tree, TreeEvent.TYPE_NODE_ACTION, Arrays.asList(nodeA, nodeB));
+ assertTrue(event.containsNode(nodeA));
+ assertTrue(event.containsNode(nodeB));
+ }
+
+ @Test
+ public void testContainsNodeSingleVirtualResolvedNode() {
+ ITree tree = mock(ITree.class);
+ ITreeNode nodeA = mockNode("a");
+ TreeEvent event = new TreeEvent(tree, TreeEvent.TYPE_NODE_ACTION, wrapByVirtualNode(nodeA));
+ assertTrue(event.containsNode(nodeA));
+ }
+
+ @Test
+ public void testContainsNodeMultipleVirtualResolvedNode() {
+ ITree tree = mock(ITree.class);
+ ITreeNode nodeA = mockNode("a");
+ ITreeNode nodeB = mockNode("b");
+ TreeEvent event = new TreeEvent(tree, TreeEvent.TYPE_NODE_ACTION, Arrays.asList(wrapByVirtualNode(nodeA), wrapByVirtualNode(nodeB)));
+ assertTrue(event.containsNode(nodeA));
+ assertTrue(event.containsNode(nodeB));
+ }
+
+ @Test
+ public void testContainsNodeSingleVirtualUnresolvedNode() {
+ ITree tree = mock(ITree.class);
+ ITreeNode nodeA = mockNode("a");
+ TreeEvent event = new TreeEvent(tree, TreeEvent.TYPE_NODE_ACTION, new VirtualTreeNode());
+ assertFalse(event.containsNode(nodeA));
+ }
+
+ @Test
+ public void testContainsNodeMultipleVirtualUnrResolvedNode() {
+ ITree tree = mock(ITree.class);
+ ITreeNode nodeA = mockNode("a");
+ ITreeNode nodeB = mockNode("b");
+ TreeEvent event = new TreeEvent(tree, TreeEvent.TYPE_NODE_ACTION, Arrays.asList(new VirtualTreeNode(), new VirtualTreeNode()));
+ assertFalse(event.containsNode(nodeA));
+ assertFalse(event.containsNode(nodeB));
+ }
+
+ /**
+ * Currently it is not supported that a {@link VirtualTreeNode} references another {@link VirtualTreeNode}.
+ */
+ @Test
+ public void testContainsNodeSingleRecursiveVirtualResolvedNode() {
+ ITree tree = mock(ITree.class);
+ ITreeNode nodeA = mockNode("a");
+ TreeEvent event = new TreeEvent(tree, TreeEvent.TYPE_NODE_ACTION, wrapByVirtualNode(wrapByVirtualNode(wrapByVirtualNode(nodeA))));
+ assertFalse(event.containsNode(nodeA));
+ }
+
+ @Test
+ public void testSetNodesSameParent() {
+ ITree tree = mock(ITree.class);
+ ITreeNode parent = mockNode("parent");
+ ITreeNode nodeA = mockNode("a", parent);
+ TreeEvent event = new TreeEvent(tree, TreeEvent.TYPE_NODE_ACTION, nodeA);
+
+ assertEquals(Arrays.asList(nodeA), event.getNodes());
+ assertEquals(parent, event.getCommonParentNode());
+
+ ITreeNode nodeB = mockNode("b", parent);
+ event.setNodes(Arrays.asList(nodeA, nodeB));
+ assertEquals(Arrays.asList(nodeA, nodeB), event.getNodes());
+ assertEquals(parent, event.getCommonParentNode());
+ }
+
+ @Test
+ public void testSetNodesDifferentParents() {
+ ITree tree = mock(ITree.class);
+ ITreeNode parentA = mockNode("parentA");
+ ITreeNode nodeA = mockNode("a", parentA);
+ TreeEvent event = new TreeEvent(tree, TreeEvent.TYPE_NODE_ACTION, nodeA);
+
+ assertEquals(Arrays.asList(nodeA), event.getNodes());
+ assertEquals(parentA, event.getCommonParentNode());
+
+ ITreeNode parentB = mockNode("parentB");
+ ITreeNode nodeB = mockNode("b", parentB);
+
+ event.setNodes(Arrays.asList(nodeA, nodeB));
+
+ assertEquals(Arrays.asList(nodeA, nodeB), event.getNodes());
+ assertNull(event.getCommonParentNode());
+ }
+
+ private VirtualTreeNode wrapByVirtualNode(ITreeNode nodeA) {
+ VirtualTreeNode virtualNodaA = new VirtualTreeNode();
+ virtualNodaA.setResolvedNode(nodeA);
+ return virtualNodaA;
+ }
+
+ private List<ITreeNode> mockNodes(String... nodeIds) {
+ if (nodeIds == null) {
+ return null;
+ }
+ List<ITreeNode> rows = new ArrayList<>();
+ for (String nodeId : nodeIds) {
+ rows.add(mockNode(nodeId));
+ }
+ return rows;
+ }
+
+ private ITreeNode mockNode(String nodeId) {
+ return mockNode(nodeId, null);
+ }
+
+ private ITreeNode mockNode(String nodeId, ITreeNode parentNode) {
+ ITreeNode node = m_mockNodes.get(nodeId);
+ if (node != null) {
+ return node;
+ }
+ // Create a new
+ node = mock(ITreeNode.class, "MockNode[" + nodeId + "]");
+ when(node.getNodeId()).thenReturn(nodeId);
+ if (parentNode != null) {
+ when(node.getParentNode()).thenReturn(parentNode);
+ }
+ m_mockNodes.put(nodeId, node);
+ return node;
+ }
+}
diff --git a/org.eclipse.scout.rt.client.test/src/test/java/org/eclipse/scout/rt/client/ui/basic/tree/TreeNodeTest.java b/org.eclipse.scout.rt.client.test/src/test/java/org/eclipse/scout/rt/client/ui/basic/tree/TreeNodeTest.java
new file mode 100644
index 0000000..eb75445
--- /dev/null
+++ b/org.eclipse.scout.rt.client.test/src/test/java/org/eclipse/scout/rt/client/ui/basic/tree/TreeNodeTest.java
@@ -0,0 +1,153 @@
+package org.eclipse.scout.rt.client.ui.basic.tree;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * @since 5.2
+ */
+public class TreeNodeTest {
+
+ private P_TreeNode m_rootNode;
+ private Map<String, ITreeNode> m_mockNodes;
+
+ @Before
+ public void before() {
+ m_rootNode = new P_TreeNode();
+ m_mockNodes = new HashMap<>();
+ }
+
+ @Test
+ public void testContainsChildNodeNoChildNodes() {
+ ITreeNode nodeA = mockNode("a");
+ assertFalse(m_rootNode.containsChildNode(nodeA, false));
+ assertFalse(m_rootNode.containsChildNode(nodeA, true));
+ }
+
+ @Test
+ public void testContainsChildNodeOneChildNodes() {
+ ITreeNode nodeA = mockNode("a");
+ m_rootNode.addChildNodesInternal(0, Collections.singletonList(nodeA), false);
+ assertTrue(m_rootNode.containsChildNode(nodeA, false));
+ assertTrue(m_rootNode.containsChildNode(nodeA, true));
+ }
+
+ @Test
+ public void testContainsChildNodeMultipleChildNodes() {
+ ITreeNode nodeA = mockNode("a");
+ ITreeNode nodeB = mockNode("b");
+ m_rootNode.addChildNodesInternal(0, Arrays.asList(nodeA, nodeB), false);
+ assertTrue(m_rootNode.containsChildNode(nodeA, false));
+ assertTrue(m_rootNode.containsChildNode(nodeA, true));
+ assertTrue(m_rootNode.containsChildNode(nodeB, false));
+ assertTrue(m_rootNode.containsChildNode(nodeB, true));
+ }
+
+ @Test
+ public void testContainsChildNodeHierarchicalChildNodes() {
+ P_TreeNode nodeA = new P_TreeNode();
+ m_rootNode.addChildNodesInternal(0, Collections.singletonList(nodeA), true);
+ ITreeNode nodeB = mockNode("b");
+ nodeA.addChildNodesInternal(0, Collections.singletonList(nodeB), false);
+
+ assertTrue(m_rootNode.containsChildNode(nodeA, false));
+ assertTrue(m_rootNode.containsChildNode(nodeA, true));
+
+ assertFalse(m_rootNode.containsChildNode(nodeB, false));
+ assertTrue(m_rootNode.containsChildNode(nodeB, true));
+
+ assertTrue(nodeA.containsChildNode(nodeB, false));
+ assertTrue(nodeA.containsChildNode(nodeB, true));
+ }
+
+ @Test
+ public void testContainsChildNodeMultipleHierarchicalChildNodes() {
+ P_TreeNode nodeA = new P_TreeNode();
+ P_TreeNode nodeB = new P_TreeNode();
+ m_rootNode.addChildNodesInternal(0, Arrays.asList(nodeA, nodeB), false);
+
+ ITreeNode nodeX = mockNode("x");
+ nodeA.addChildNodesInternal(0, Collections.singletonList(nodeX), false);
+
+ ITreeNode nodeY = mockNode("y");
+ nodeB.addChildNodesInternal(0, Collections.singletonList(nodeY), false);
+
+ assertTrue(m_rootNode.containsChildNode(nodeA, false));
+ assertTrue(m_rootNode.containsChildNode(nodeA, true));
+
+ assertTrue(m_rootNode.containsChildNode(nodeB, false));
+ assertTrue(m_rootNode.containsChildNode(nodeB, true));
+
+ assertFalse(m_rootNode.containsChildNode(nodeX, false));
+ assertTrue(m_rootNode.containsChildNode(nodeX, true));
+ assertTrue(nodeA.containsChildNode(nodeX, false));
+ assertTrue(nodeA.containsChildNode(nodeX, true));
+ assertFalse(nodeB.containsChildNode(nodeX, false));
+ assertFalse(nodeB.containsChildNode(nodeX, true));
+
+ assertFalse(m_rootNode.containsChildNode(nodeY, false));
+ assertTrue(m_rootNode.containsChildNode(nodeY, true));
+ assertFalse(nodeA.containsChildNode(nodeY, false));
+ assertFalse(nodeA.containsChildNode(nodeY, true));
+ assertTrue(nodeB.containsChildNode(nodeY, false));
+ assertTrue(nodeB.containsChildNode(nodeY, true));
+ }
+
+ @Test
+ public void testContainsChildNodeResolvedVirtualNode() {
+ ITreeNode nodeA = mockNode("a");
+ m_rootNode.addChildNodesInternal(0, Collections.singletonList(wrapByVirtualNode(nodeA)), false);
+
+ assertTrue(m_rootNode.containsChildNode(nodeA, false));
+ assertTrue(m_rootNode.containsChildNode(nodeA, true));
+ }
+
+ @Test
+ public void testContainsChildNodeRecursiveResolvedVirtualNode() {
+ P_TreeNode nodeA = new P_TreeNode();
+ m_rootNode.addChildNodesInternal(0, Collections.singletonList(wrapByVirtualNode(nodeA)), false);
+
+ ITreeNode nodeB = mockNode("b");
+ nodeA.addChildNodesInternal(0, Collections.singletonList(wrapByVirtualNode(nodeB)), false);
+
+ assertTrue(m_rootNode.containsChildNode(nodeA, false));
+ assertTrue(m_rootNode.containsChildNode(nodeA, true));
+
+ assertFalse(m_rootNode.containsChildNode(nodeB, false));
+ assertTrue(m_rootNode.containsChildNode(nodeB, true));
+
+ assertTrue(nodeA.containsChildNode(nodeB, false));
+ assertTrue(nodeA.containsChildNode(nodeB, true));
+ }
+
+ private VirtualTreeNode wrapByVirtualNode(ITreeNode nodeA) {
+ VirtualTreeNode virtualNodaA = new VirtualTreeNode();
+ virtualNodaA.setResolvedNode(nodeA);
+ return virtualNodaA;
+ }
+
+ private ITreeNode mockNode(String nodeId) {
+ ITreeNode node = m_mockNodes.get(nodeId);
+ if (node != null) {
+ return node;
+ }
+ // Create a new
+ node = mock(ITreeNode.class, "MockNode[" + nodeId + "]");
+ when(node.getNodeId()).thenReturn(nodeId);
+ m_mockNodes.put(nodeId, node);
+ return node;
+ }
+
+ private static class P_TreeNode extends AbstractTreeNode {
+ }
+}
diff --git a/org.eclipse.scout.rt.client.test/src/test/java/org/eclipse/scout/rt/client/ui/basic/tree/TreeUtilityTest.java b/org.eclipse.scout.rt.client.test/src/test/java/org/eclipse/scout/rt/client/ui/basic/tree/TreeUtilityTest.java
new file mode 100644
index 0000000..01f530e
--- /dev/null
+++ b/org.eclipse.scout.rt.client.test/src/test/java/org/eclipse/scout/rt/client/ui/basic/tree/TreeUtilityTest.java
@@ -0,0 +1,77 @@
+package org.eclipse.scout.rt.client.ui.basic.tree;
+
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertSame;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import java.util.Arrays;
+import java.util.Collections;
+
+import org.junit.Test;
+
+/**
+ * @since 5.2
+ */
+public class TreeUtilityTest {
+
+ @Test
+ public void testCalculateCommonParentNodeNullAndEmpty() {
+ assertNull(TreeUtility.calculateCommonParentNode(null));
+ assertNull(TreeUtility.calculateCommonParentNode(Collections.<ITreeNode> emptySet()));
+ }
+
+ @Test
+ public void testCalculateCommonParentNodeOneNodeWithoutParent() {
+ ITreeNode nodeA = mockNode("a", null);
+ assertNull(TreeUtility.calculateCommonParentNode(Collections.singleton(nodeA)));
+ }
+
+ @Test
+ public void testCalculateCommonParentNodeOneNodeWithParent() {
+ ITreeNode rootNode = mockNode("root", null);
+ ITreeNode nodeA = mockNode("a", rootNode);
+ assertSame(rootNode, TreeUtility.calculateCommonParentNode(Collections.singleton(nodeA)));
+ }
+
+ @Test
+ public void testCalculateCommonParentNodeMultipleNodesHavingSameParent() {
+ ITreeNode rootNode = mockNode("root", null);
+ ITreeNode nodeA = mockNode("a", rootNode);
+ ITreeNode nodeB = mockNode("b", rootNode);
+ assertSame(rootNode, TreeUtility.calculateCommonParentNode(Arrays.asList(nodeA, nodeB)));
+ }
+
+ @Test
+ public void testCalculateCommonParentNodeMultipleNodesHavingNullParent() {
+ ITreeNode nodeA = mockNode("a", null);
+ ITreeNode nodeB = mockNode("b", null);
+ assertNull(TreeUtility.calculateCommonParentNode(Arrays.asList(nodeA, nodeB)));
+ }
+
+ @Test
+ public void testCalculateCommonParentNodeMultipleNodesHavingDifferentParent() {
+ ITreeNode parentA = mockNode("parentA", null);
+ ITreeNode nodeA = mockNode("a", parentA);
+ ITreeNode parentB = mockNode("parentB", null);
+ ITreeNode nodeB = mockNode("b", parentB);
+ assertNull(TreeUtility.calculateCommonParentNode(Arrays.asList(nodeA, nodeB)));
+ }
+
+ @Test
+ public void testCalculateCommonParentNodeMultipleNodesOneHavingNullParent() {
+ ITreeNode parentA = mockNode("parentA", null);
+ ITreeNode nodeA = mockNode("a", parentA);
+ ITreeNode nodeB = mockNode("b", null);
+ assertNull(TreeUtility.calculateCommonParentNode(Arrays.asList(nodeA, nodeB)));
+ }
+
+ private ITreeNode mockNode(String nodeId, ITreeNode parentNode) {
+ ITreeNode node = mock(ITreeNode.class, "MockNode[" + nodeId + "]");
+ when(node.getNodeId()).thenReturn(nodeId);
+ if (parentNode != null) {
+ when(node.getParentNode()).thenReturn(parentNode);
+ }
+ return node;
+ }
+}
diff --git a/org.eclipse.scout.rt.client.test/src/test/java/org/eclipse/scout/rt/client/ui/basic/tree/VirtualTreeNodeTest.java b/org.eclipse.scout.rt.client.test/src/test/java/org/eclipse/scout/rt/client/ui/basic/tree/VirtualTreeNodeTest.java
new file mode 100644
index 0000000..baf03c1
--- /dev/null
+++ b/org.eclipse.scout.rt.client.test/src/test/java/org/eclipse/scout/rt/client/ui/basic/tree/VirtualTreeNodeTest.java
@@ -0,0 +1,73 @@
+package org.eclipse.scout.rt.client.ui.basic.tree;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ * @since 5.2
+ */
+public class VirtualTreeNodeTest {
+
+ private VirtualTreeNode m_treeNode;
+ private Map<String, ITreeNode> m_mockNodes;
+ private ITreeNode m_nodeA;
+
+ @Before
+ public void before() {
+ m_treeNode = new VirtualTreeNode();
+ m_mockNodes = new HashMap<>();
+ m_nodeA = mockNode("a");
+ }
+
+ @Test
+ public void testContainsChildNodeNullNode() {
+ assertFalse(m_treeNode.containsChildNode(null, false));
+ assertFalse(m_treeNode.containsChildNode(null, true));
+ }
+
+ @Test
+ public void testContainsChildNodeUnresolved() {
+ assertFalse(m_treeNode.containsChildNode(m_nodeA, false));
+ assertFalse(m_treeNode.containsChildNode(m_nodeA, true));
+ }
+
+ @Test
+ public void testContainsChildNodeResolved() {
+ m_treeNode.setResolvedNode(m_nodeA);
+ assertTrue(m_treeNode.containsChildNode(m_nodeA, false));
+ assertTrue(m_treeNode.containsChildNode(m_nodeA, true));
+ }
+
+ @Test
+ public void testContainsChildNodeCascadingResolved() {
+ m_treeNode.setResolvedNode(wrapByVirtualNode(m_nodeA));
+ assertFalse(m_treeNode.containsChildNode(m_nodeA, false));
+ assertTrue(m_treeNode.containsChildNode(m_nodeA, true));
+ }
+
+ private VirtualTreeNode wrapByVirtualNode(ITreeNode nodeA) {
+ VirtualTreeNode virtualNodaA = new VirtualTreeNode();
+ virtualNodaA.setResolvedNode(nodeA);
+ return virtualNodaA;
+ }
+
+ private ITreeNode mockNode(String nodeId) {
+ ITreeNode node = m_mockNodes.get(nodeId);
+ if (node != null) {
+ return node;
+ }
+ // Create a new
+ node = mock(ITreeNode.class, "MockNode[" + nodeId + "]");
+ when(node.getNodeId()).thenReturn(nodeId);
+ m_mockNodes.put(nodeId, node);
+ return node;
+ }
+}
diff --git a/org.eclipse.scout.rt.client/src/main/java/org/eclipse/scout/rt/client/ui/basic/table/TableEventBuffer.java b/org.eclipse.scout.rt.client/src/main/java/org/eclipse/scout/rt/client/ui/basic/table/TableEventBuffer.java
index de9ded5..552a94d 100644
--- a/org.eclipse.scout.rt.client/src/main/java/org/eclipse/scout/rt/client/ui/basic/table/TableEventBuffer.java
+++ b/org.eclipse.scout.rt.client/src/main/java/org/eclipse/scout/rt/client/ui/basic/table/TableEventBuffer.java
@@ -572,7 +572,7 @@
}
/**
- * Adds rows and columns. Using this method after invoking {@link #merge(TableEvent)} throws an
+ * Merges rows and columns. Using this method after invoking {@link #complete()} throws an
* {@link IllegalStateException}.
*/
public void merge(TableEvent event) {
diff --git a/org.eclipse.scout.rt.client/src/main/java/org/eclipse/scout/rt/client/ui/basic/tree/AbstractTreeNode.java b/org.eclipse.scout.rt.client/src/main/java/org/eclipse/scout/rt/client/ui/basic/tree/AbstractTreeNode.java
index 27269e9..9a71f54 100644
--- a/org.eclipse.scout.rt.client/src/main/java/org/eclipse/scout/rt/client/ui/basic/tree/AbstractTreeNode.java
+++ b/org.eclipse.scout.rt.client/src/main/java/org/eclipse/scout/rt/client/ui/basic/tree/AbstractTreeNode.java
@@ -35,6 +35,7 @@
import org.eclipse.scout.rt.platform.annotations.ConfigProperty;
import org.eclipse.scout.rt.platform.reflect.ConfigurationUtility;
import org.eclipse.scout.rt.platform.util.CollectionUtility;
+import org.eclipse.scout.rt.platform.util.CompareUtility;
import org.eclipse.scout.rt.platform.util.collection.OrderedCollection;
import org.eclipse.scout.rt.platform.util.concurrent.OptimisticLock;
import org.eclipse.scout.rt.shared.extension.AbstractExtension;
@@ -878,6 +879,25 @@
}
@Override
+ public boolean containsChildNode(final ITreeNode node, final boolean recursive) {
+ synchronized (m_childNodeList) {
+ for (ITreeNode childNode : m_childNodeList) {
+ if (CompareUtility.equals(childNode, node)) {
+ return true;
+ }
+ }
+ if (recursive) {
+ for (ITreeNode childNode : m_childNodeList) {
+ if (childNode.containsChildNode(node, recursive)) {
+ return true;
+ }
+ }
+ }
+ }
+ return false;
+ }
+
+ @Override
public ITreeNode findParentNode(Class<?> interfaceType) {
ITreeNode test = getParentNode();
while (test != null) {
diff --git a/org.eclipse.scout.rt.client/src/main/java/org/eclipse/scout/rt/client/ui/basic/tree/ITreeNode.java b/org.eclipse.scout.rt.client/src/main/java/org/eclipse/scout/rt/client/ui/basic/tree/ITreeNode.java
index 3d9f41f..40e4b8d 100644
--- a/org.eclipse.scout.rt.client/src/main/java/org/eclipse/scout/rt/client/ui/basic/tree/ITreeNode.java
+++ b/org.eclipse.scout.rt.client/src/main/java/org/eclipse/scout/rt/client/ui/basic/tree/ITreeNode.java
@@ -324,6 +324,12 @@
List<ITreeNode> getChildNodes();
/**
+ * @return Returns <code>true</code> if this node contains the given child node. Visits child nodes if recursive is
+ * <code>true</code>.
+ */
+ boolean containsChildNode(ITreeNode node, boolean recursive);
+
+ /**
* @see ITree#getNodeFilters() This method is Thread-Safe
*/
List<ITreeNode> getFilteredChildNodes();
diff --git a/org.eclipse.scout.rt.client/src/main/java/org/eclipse/scout/rt/client/ui/basic/tree/TreeEvent.java b/org.eclipse.scout.rt.client/src/main/java/org/eclipse/scout/rt/client/ui/basic/tree/TreeEvent.java
index 7397939..328517f 100644
--- a/org.eclipse.scout.rt.client/src/main/java/org/eclipse/scout/rt/client/ui/basic/tree/TreeEvent.java
+++ b/org.eclipse.scout.rt.client/src/main/java/org/eclipse/scout/rt/client/ui/basic/tree/TreeEvent.java
@@ -22,6 +22,7 @@
import org.eclipse.scout.rt.client.ui.action.menu.IMenu;
import org.eclipse.scout.rt.client.ui.dnd.TransferObject;
import org.eclipse.scout.rt.platform.util.CollectionUtility;
+import org.eclipse.scout.rt.platform.util.CompareUtility;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -201,12 +202,7 @@
}
public ITreeNode getDeselectedNode() {
- if (CollectionUtility.hasElements(m_deselectedNodes)) {
- return CollectionUtility.firstElement(m_deselectedNodes);
- }
- else {
- return null;
- }
+ return CollectionUtility.firstElement(m_deselectedNodes);
}
public Collection<ITreeNode> getDeselectedNodes() {
@@ -218,12 +214,7 @@
}
public ITreeNode getNewSelectedNode() {
- if (CollectionUtility.hasElements(m_newSelectedNodes)) {
- return CollectionUtility.firstElement(m_newSelectedNodes);
- }
- else {
- return null;
- }
+ return CollectionUtility.firstElement(m_newSelectedNodes);
}
public Collection<ITreeNode> getNewSelectedNodes() {
@@ -235,18 +226,56 @@
}
public ITreeNode getNode() {
- if (CollectionUtility.hasElements(m_nodes)) {
- return CollectionUtility.firstElement(m_nodes);
- }
- else {
- return null;
- }
+ return CollectionUtility.firstElement(m_nodes);
}
public Collection<ITreeNode> getNodes() {
return CollectionUtility.arrayList(m_nodes);
}
+ /**
+ * Updates the nodes of this events and computes the new common parent node.
+ */
+ protected void setNodes(Collection<ITreeNode> nodes) {
+ m_nodes = CollectionUtility.arrayList(nodes);
+ m_commonParentNode = TreeUtility.calculateCommonParentNode(nodes);
+ }
+
+ public boolean hasNodes() {
+ return m_nodes == null ? false : !m_nodes.isEmpty();
+ }
+
+ public int getNodeCount() {
+ return m_nodes == null ? 0 : m_nodes.size();
+ }
+
+ public boolean containsNode(ITreeNode nodeToFind) {
+ if (CollectionUtility.isEmpty(m_nodes)) {
+ return false;
+ }
+ for (ITreeNode node : m_nodes) {
+ if (CompareUtility.equals(node, nodeToFind)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ public boolean containsNodeRecursive(ITreeNode nodeToFind) {
+ if (CollectionUtility.isEmpty(m_nodes)) {
+ return false;
+ }
+ if (containsNode(nodeToFind)) {
+ return true;
+ }
+ for (ITreeNode node : m_nodes) {
+ if (node.containsChildNode(nodeToFind, true)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
public ITreeNode getChildNode() {
return getNode();
}
diff --git a/org.eclipse.scout.rt.client/src/main/java/org/eclipse/scout/rt/client/ui/basic/tree/TreeEventBuffer.java b/org.eclipse.scout.rt.client/src/main/java/org/eclipse/scout/rt/client/ui/basic/tree/TreeEventBuffer.java
index e97a67d..7cfbe19 100644
--- a/org.eclipse.scout.rt.client/src/main/java/org/eclipse/scout/rt/client/ui/basic/tree/TreeEventBuffer.java
+++ b/org.eclipse.scout.rt.client/src/main/java/org/eclipse/scout/rt/client/ui/basic/tree/TreeEventBuffer.java
@@ -10,13 +10,18 @@
******************************************************************************/
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;
@@ -99,7 +104,7 @@
// 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.getNodes().size() != newEvent.getNodes().size());
+ boolean removed = (event.getNodeCount() != newEvent.getNodeCount());
if (removed) {
nodeRemovedFromCreationEvent = true;
}
@@ -110,7 +115,7 @@
// required anymore (the insertion event does not contain deleted nodes).
ITreeNode parentToCheck = nodeToRemove.getParentNode();
while (parentToCheck != null) {
- if (containsNode(newEvent.getNodes(), parentToCheck)) {
+ if (newEvent.containsNode(parentToCheck)) {
nodeRemovedFromCreationEvent = true;
break;
}
@@ -203,7 +208,7 @@
for (Iterator<ITreeNode> it = nodes.iterator(); it.hasNext();) {
ITreeNode node = it.next();
for (TreeEvent event : events) {
- if (event.getType() == oldType && containsNodeRec(event.getNodes(), node)) {
+ if (event.getType() == oldType && event.containsNodeRecursive(node)) {
it.remove();
removed = true;
break; // no need to look further
@@ -214,37 +219,6 @@
}
/**
- * @return <code>true</code> if 'nodes' contains 'nodeToFind'. Children of 'nodes are <b>not</b> considered, use
- * {@link #containsNodeRec(Collection, ITreeNode)} if they should be checked as well.
- */
- protected boolean containsNode(Collection<ITreeNode> nodes, ITreeNode nodeToFind) {
- for (ITreeNode node : nodes) {
- node = TreeUtility.unwrapResolvedNode(node);
- if (CompareUtility.equals(node, nodeToFind)) {
- return true;
- }
- }
- return false;
- }
-
- /**
- * @return <code>true</code> if 'nodes' contains 'nodeToFind' or one of the children does so.
- */
- protected boolean containsNodeRec(Collection<ITreeNode> nodes, ITreeNode nodeToFind) {
- for (ITreeNode node : nodes) {
- // Unwrap resolved node to get the real answer to "getChildNodes()"
- node = TreeUtility.unwrapResolvedNode(node);
- if (CompareUtility.equals(node, nodeToFind)) {
- return true;
- }
- if (containsNodeRec(node.getChildNodes(), nodeToFind)) {
- return true;
- }
- }
- return false;
- }
-
- /**
* @return all the given nodes, including their recursive child nodes.
*/
protected Collection<ITreeNode> collectAllNodesRec(Collection<ITreeNode> nodes) {
@@ -262,76 +236,65 @@
* Merge previous events of the same type into the current and delete the previous events
*/
protected void coalesceSameType(List<TreeEvent> events) {
- for (int j = 0; j < events.size() - 1; j++) {
- int i = events.size() - 1 - j;
- TreeEvent event = events.get(i);
+ if (events.size() < 2) {
+ return;
+ }
- if (isCoalesceConsecutivePrevious(event.getType())) {
- TreeEvent mergedEvent = coalesceConsecutivePrevious(event, events.subList(0, i));
- if (mergedEvent != event) {
- // replace in (now shorter) list
- i = events.size() - 1 - j;
- events.set(i, mergedEvent);
+ 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();
}
- }
- }
- /**
- * Merge events of the same type in the given list into the current and delete the other events from the list.
- *
- * @return the updated event (with other events merged into it)
- */
- protected TreeEvent coalesceConsecutivePrevious(TreeEvent event, List<TreeEvent> list) {
- for (ListIterator<TreeEvent> it = list.listIterator(list.size()); it.hasPrevious();) {
- TreeEvent previous = it.previous();
- if (event.getType() == previous.getType()) {
- if (hasSameCommonParentNode(event, previous)) {
- event = merge(previous, event);
- it.remove();
- }
+ if (!isCoalesceConsecutivePrevious(type)) {
+ continue;
}
- else {
- return event;
+
+ 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;
}
- }
- return event;
- }
- protected boolean hasSameCommonParentNode(TreeEvent event1, TreeEvent event2) {
- ITreeNode node1 = event1.getCommonParentNode();
- ITreeNode node2 = event2.getCommonParentNode();
- return CompareUtility.equals(node1, node2);
- }
-
- /**
- * @return a new event with same same property as 'second' but with the nodes of 'first' merged into
- */
- protected TreeEvent merge(TreeEvent first, TreeEvent second) {
- return replaceNodesInEvent(second, mergeNodes(first.getNodes(), second.getNodes()));
- }
-
- /**
- * Merge list of nodes, such that, if the same node is in both lists, only the one of the second list (later event) is
- * kept.
- */
- protected List<ITreeNode> mergeNodes(Collection<ITreeNode> first, Collection<ITreeNode> second) {
- List<ITreeNode> nodes = new ArrayList<>();
- for (ITreeNode node : first) {
- if (!second.contains(node)) {
- nodes.add(node);
+ // 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();
}
- for (ITreeNode node : second) {
- nodes.add(node);
+
+ // complete "open" event mergers
+ for (TreeEventMerger eventMerger : eventMergerByParent.values()) {
+ eventMerger.complete();
}
- return nodes;
}
protected void removeEmptyEvents(List<TreeEvent> events) {
for (Iterator<TreeEvent> it = events.iterator(); it.hasNext();) {
TreeEvent event = it.next();
- if (isNodesRequired(event.getType()) && event.getNodes().isEmpty()
+ if (isNodesRequired(event.getType()) && !event.hasNodes()
|| isCommonParentNodeRequired(event.getType()) && event.getCommonParentNode() == null) {
it.remove();
}
@@ -371,7 +334,7 @@
return false;
}
boolean identical = (event1.getType() == event2.getType()
- && hasSameCommonParentNode(event1, event2)
+ && 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)
@@ -497,4 +460,80 @@
}
}
}
+
+ /**
+ * 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);
+ }
+ }
}
diff --git a/org.eclipse.scout.rt.client/src/main/java/org/eclipse/scout/rt/client/ui/basic/tree/TreeUtility.java b/org.eclipse.scout.rt.client/src/main/java/org/eclipse/scout/rt/client/ui/basic/tree/TreeUtility.java
index 8205716..7754298 100644
--- a/org.eclipse.scout.rt.client/src/main/java/org/eclipse/scout/rt/client/ui/basic/tree/TreeUtility.java
+++ b/org.eclipse.scout.rt.client/src/main/java/org/eclipse/scout/rt/client/ui/basic/tree/TreeUtility.java
@@ -28,22 +28,17 @@
* If every given node has the same parent, that common parent node will be returned, else null.
*/
public static ITreeNode calculateCommonParentNode(Collection<? extends ITreeNode> nodes) {
- if (!CollectionUtility.hasElements(nodes)) {
+ if (CollectionUtility.isEmpty(nodes)) {
return null;
}
-
- if (nodes.size() == 1) {
- return CollectionUtility.firstElement(nodes).getParentNode();
- }
Iterator<? extends ITreeNode> nodeIt = nodes.iterator();
- ITreeNode test = nodeIt.next().getParentNode();
+ final ITreeNode commonParent = nodeIt.next().getParentNode();
while (nodeIt.hasNext()) {
- if (nodeIt.next().getParentNode() != test) {
- test = null;
- break;
+ if (nodeIt.next().getParentNode() != commonParent) {
+ return null;
}
}
- return test;
+ return commonParent;
}
/**
diff --git a/org.eclipse.scout.rt.client/src/main/java/org/eclipse/scout/rt/client/ui/basic/tree/VirtualTreeNode.java b/org.eclipse.scout.rt.client/src/main/java/org/eclipse/scout/rt/client/ui/basic/tree/VirtualTreeNode.java
index 0ce0259..336a348 100644
--- a/org.eclipse.scout.rt.client/src/main/java/org/eclipse/scout/rt/client/ui/basic/tree/VirtualTreeNode.java
+++ b/org.eclipse.scout.rt.client/src/main/java/org/eclipse/scout/rt/client/ui/basic/tree/VirtualTreeNode.java
@@ -436,6 +436,20 @@
}
@Override
+ public boolean containsChildNode(ITreeNode node, boolean recursive) {
+ if (equals(node)) {
+ return true;
+ }
+ if (recursive) {
+ final ITreeNode resolvedNode = getResolvedNode();
+ if (resolvedNode != null) {
+ return resolvedNode.containsChildNode(node, recursive);
+ }
+ }
+ return false;
+ }
+
+ @Override
public ITreeNode findParentNode(Class interfaceType) {
ITreeNode test = getParentNode();
while (test != null) {