/******************************************************************************* * Copyright (c) 2007, 2010 Oracle. 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: * Oracle - initial API and implementation ******************************************************************************/ package org.eclipse.jpt.common.utility.tests.internal.model.value.swing; import java.io.OutputStream; import java.io.OutputStreamWriter; import java.io.StringWriter; import java.util.Arrays; import java.util.Collection; import java.util.Iterator; import javax.swing.Icon; import javax.swing.JTree; import javax.swing.event.TreeModelEvent; import javax.swing.event.TreeModelListener; import javax.swing.tree.TreeModel; import junit.framework.TestCase; import org.eclipse.jpt.common.utility.IndentingPrintWriter; import org.eclipse.jpt.common.utility.internal.HashBag; import org.eclipse.jpt.common.utility.internal.StringTools; import org.eclipse.jpt.common.utility.internal.iterators.ReadOnlyIterator; import org.eclipse.jpt.common.utility.internal.model.AbstractModel; import org.eclipse.jpt.common.utility.internal.model.value.AbstractTreeNodeValueModel; import org.eclipse.jpt.common.utility.internal.model.value.CollectionAspectAdapter; import org.eclipse.jpt.common.utility.internal.model.value.ItemPropertyListValueModelAdapter; import org.eclipse.jpt.common.utility.internal.model.value.NullListValueModel; import org.eclipse.jpt.common.utility.internal.model.value.PropertyAspectAdapter; import org.eclipse.jpt.common.utility.internal.model.value.SimpleListValueModel; import org.eclipse.jpt.common.utility.internal.model.value.SimplePropertyValueModel; import org.eclipse.jpt.common.utility.internal.model.value.SortedListValueModelWrapper; import org.eclipse.jpt.common.utility.internal.model.value.StaticPropertyValueModel; import org.eclipse.jpt.common.utility.internal.model.value.TransformationListValueModel; import org.eclipse.jpt.common.utility.internal.model.value.swing.TreeModelAdapter; import org.eclipse.jpt.common.utility.internal.swing.Displayable; import org.eclipse.jpt.common.utility.model.event.PropertyChangeEvent; import org.eclipse.jpt.common.utility.model.listener.ListChangeListener; import org.eclipse.jpt.common.utility.model.listener.PropertyChangeListener; import org.eclipse.jpt.common.utility.model.listener.StateChangeListener; import org.eclipse.jpt.common.utility.model.value.CollectionValueModel; import org.eclipse.jpt.common.utility.model.value.ListValueModel; import org.eclipse.jpt.common.utility.model.value.PropertyValueModel; import org.eclipse.jpt.common.utility.model.value.TreeNodeValueModel; import org.eclipse.jpt.common.utility.model.value.WritablePropertyValueModel; @SuppressWarnings("nls") public class TreeModelAdapterTests extends TestCase { boolean eventFired; public TreeModelAdapterTests(String name) { super(name); } public void testGetRoot() { TreeModel treeModel = this.buildSortedTreeModel(); treeModel.addTreeModelListener(new TestTreeModelListener()); TestNode rootNode = (TestNode) treeModel.getRoot(); TestModel root = rootNode.getTestModel(); assertEquals("root", root.getName()); // root.dump(); // rootNode.dump(); } public void testGetChild() { TreeModel treeModel = this.buildSortedTreeModel(); treeModel.addTreeModelListener(new TestTreeModelListener()); TestNode rootNode = (TestNode) treeModel.getRoot(); TestNode expected = rootNode.childNamed("node 1"); TestNode actual = (TestNode) treeModel.getChild(rootNode, 1); assertEquals(expected, actual); expected = rootNode.childNamed("node 2"); actual = (TestNode) treeModel.getChild(rootNode, 2); assertEquals(expected, actual); } public void testGetChildCount() { TreeModel treeModel = this.buildSortedTreeModel(); treeModel.addTreeModelListener(new TestTreeModelListener()); TestNode rootNode = (TestNode) treeModel.getRoot(); assertEquals(5, treeModel.getChildCount(rootNode)); TestNode node = rootNode.childNamed("node 1"); assertEquals(1, treeModel.getChildCount(node)); } public void testGetIndexOfChild() { TreeModel treeModel = this.buildSortedTreeModel(); treeModel.addTreeModelListener(new TestTreeModelListener()); TestNode rootNode = (TestNode) treeModel.getRoot(); TestNode child = rootNode.childNamed("node 0"); assertEquals(0, treeModel.getIndexOfChild(rootNode, child)); child = rootNode.childNamed("node 1"); assertEquals(1, treeModel.getIndexOfChild(rootNode, child)); child = rootNode.childNamed("node 2"); assertEquals(2, treeModel.getIndexOfChild(rootNode, child)); TestNode grandchild = child.childNamed("node 2.2"); assertEquals(2, treeModel.getIndexOfChild(child, grandchild)); } public void testIsLeaf() { TreeModel treeModel = this.buildSortedTreeModel(); treeModel.addTreeModelListener(new TestTreeModelListener()); TestNode rootNode = (TestNode) treeModel.getRoot(); assertFalse(treeModel.isLeaf(rootNode)); TestNode node = rootNode.childNamed("node 1"); assertFalse(treeModel.isLeaf(node)); node = rootNode.childNamed("node 3"); assertTrue(treeModel.isLeaf(node)); } public void testTreeNodesChanged() { // the only way to trigger a "node changed" event is to use an unsorted tree; // a sorted tree will will trigger only "node removed" and "node inserted" events TreeModel treeModel = this.buildUnsortedTreeModel(); this.eventFired = false; treeModel.addTreeModelListener(new TestTreeModelListener() { @Override public void treeNodesChanged(TreeModelEvent e) { TreeModelAdapterTests.this.eventFired = true; } }); TestNode rootNode = (TestNode) treeModel.getRoot(); TestNode node = rootNode.childNamed("node 1"); TestModel tm = node.getTestModel(); tm.setName("node 1++"); assertTrue(this.eventFired); this.eventFired = false; node = node.childNamed("node 1.1"); tm = node.getTestModel(); tm.setName("node 1.1++"); assertTrue(this.eventFired); } public void testTreeNodesInserted() { // use an unsorted tree so the nodes are not re-shuffled... TreeModel treeModel = this.buildUnsortedTreeModel(); this.eventFired = false; treeModel.addTreeModelListener(new TestTreeModelListener() { @Override public void treeNodesInserted(TreeModelEvent e) { TreeModelAdapterTests.this.eventFired = true; } }); TestNode rootNode = (TestNode) treeModel.getRoot(); TestNode node = rootNode.childNamed("node 1"); TestModel tm = node.getTestModel(); tm.addChild("new child..."); assertTrue(this.eventFired); this.eventFired = false; node = node.childNamed("node 1.1"); tm = node.getTestModel(); tm.addChild("another new child..."); assertTrue(this.eventFired); } public void testTreeNodesRemoved() { TreeModel treeModel = this.buildUnsortedTreeModel(); this.eventFired = false; treeModel.addTreeModelListener(new TestTreeModelListener() { @Override public void treeNodesRemoved(TreeModelEvent e) { TreeModelAdapterTests.this.eventFired = true; } }); TestNode rootNode = (TestNode) treeModel.getRoot(); TestModel root = rootNode.getTestModel(); root.removeChild(root.childNamed("node 3")); assertTrue(this.eventFired); this.eventFired = false; TestNode node = rootNode.childNamed("node 2"); TestModel tm = node.getTestModel(); tm.removeChild(tm.childNamed("node 2.2")); assertTrue(this.eventFired); } public void testTreeStructureChanged() { WritablePropertyValueModel> nodeHolder = new SimplePropertyValueModel>(this.buildSortedRootNode()); TreeModel treeModel = this.buildTreeModel(nodeHolder); this.eventFired = false; treeModel.addTreeModelListener(new TestTreeModelListener() { @Override public void treeNodesInserted(TreeModelEvent e) { // do nothing } @Override public void treeNodesRemoved(TreeModelEvent e) { // do nothing } @Override public void treeStructureChanged(TreeModelEvent e) { TreeModelAdapterTests.this.eventFired = true; } }); nodeHolder.setValue(this.buildUnsortedRootNode()); assertTrue(this.eventFired); } /** * test a problem we had where removing a child from a tree would cause * the JTree to call #equals(Object) on each node removed (actually, it was * TreePath, but that was because its own #equals(Object) was called by * JTree); and since we had already removed the last listener from the * aspect adapter, the aspect adapter would say its value was null; this * would cause a NPE until we tweaked TreeModelAdapter to remove its * listeners from a node only *after* the node had been completely * removed from the JTree * @see TreeModelAdapter#removeNode(Object[], int, TreeNodeValueModel) * @see TreeModelAdapter#addNode(Object[], int, TreeNodeValueModel) */ public void testLazyInitialization() { TreeModel treeModel = this.buildSpecialTreeModel(); JTree jTree = new JTree(treeModel); TestNode rootNode = (TestNode) treeModel.getRoot(); TestModel root = rootNode.getTestModel(); // this would cause a NPE: root.removeChild(root.childNamed("node 3")); assertEquals(treeModel, jTree.getModel()); } private TreeModel buildSortedTreeModel() { return this.buildTreeModel(this.buildSortedRootNode()); } private TestNode buildSortedRootNode() { return new SortedTestNode(this.buildRoot()); } private TreeModel buildUnsortedTreeModel() { return this.buildTreeModel(this.buildUnsortedRootNode()); } private TestNode buildUnsortedRootNode() { return new UnsortedTestNode(this.buildRoot()); } private TreeModel buildSpecialTreeModel() { return this.buildTreeModel(this.buildSpecialRootNode()); } private TestNode buildSpecialRootNode() { return new SpecialTestNode(this.buildRoot()); } private TestModel buildRoot() { TestModel root = new TestModel("root"); /*Node node_0 = */root.addChild("node 0"); TestModel node_1 = root.addChild("node 1"); TestModel node_1_1 = node_1.addChild("node 1.1"); /*Node node_1_1_1 = */node_1_1.addChild("node 1.1.1"); TestModel node_2 = root.addChild("node 2"); /*Node node_2_0 = */node_2.addChild("node 2.0"); /*Node node_2_1 = */node_2.addChild("node 2.1"); /*Node node_2_2 = */node_2.addChild("node 2.2"); /*Node node_2_3 = */node_2.addChild("node 2.3"); /*Node node_2_4 = */node_2.addChild("node 2.4"); /*Node node_2_5 = */node_2.addChild("node 2.5"); /*Node node_3 = */root.addChild("node 3"); /*Node node_4 = */root.addChild("node 4"); return root; } // ********** member classes ********** /** * This is a typical model class with the typical change notifications * for #name and #children. */ public static class TestModel extends AbstractModel { // the parent is immutable; the root's parent is null private final TestModel parent; // the name is mutable; so I guess it isn't the "primary key" :-) private String name; public static final String NAME_PROPERTY = "name"; private final Collection children; public static final String CHILDREN_COLLECTION = "children"; public TestModel(String name) { // root ctor this(null, name); } private TestModel(TestModel parent, String name) { super(); this.parent = parent; this.name = name; this.children = new HashBag(); } public TestModel getParent() { return this.parent; } public String getName() { return this.name; } public void setName(String name) { Object old = this.name; this.name = name; this.firePropertyChanged(NAME_PROPERTY, old, name); } public Iterator children() { return new ReadOnlyIterator(this.children); } public int childrenSize() { return this.children.size(); } public TestModel addChild(String childName) { TestModel child = new TestModel(this, childName); this.addItemToCollection(child, this.children, CHILDREN_COLLECTION); return child; } public TestModel[] addChildren(String[] childNames) { TestModel[] newChildren = new TestModel[childNames.length]; for (int i = 0; i < childNames.length; i++) { newChildren[i] = new TestModel(this, childNames[i]); } this.addItemsToCollection(newChildren, this.children, CHILDREN_COLLECTION); return newChildren; } public void removeChild(TestModel child) { this.removeItemFromCollection(child, this.children, CHILDREN_COLLECTION); } public void removeChildren(TestModel[] testModels) { this.removeItemsFromCollection(testModels, this.children, CHILDREN_COLLECTION); } public void clearChildren() { this.clearCollection(this.children, CHILDREN_COLLECTION); } public TestModel childNamed(String childName) { for (TestModel child : this.children) { if (child.getName().equals(childName)) { return child; } } throw new RuntimeException("child not found: " + childName); } public String dumpString() { StringWriter sw = new StringWriter(); IndentingPrintWriter ipw = new IndentingPrintWriter(sw); this.dumpOn(ipw); return sw.toString(); } public void dumpOn(IndentingPrintWriter writer) { writer.println(this); writer.indent(); for (TestModel child : this.children) { child.dumpOn(writer); } writer.undent(); } public void dumpOn(OutputStream stream) { IndentingPrintWriter writer = new IndentingPrintWriter(new OutputStreamWriter(stream)); this.dumpOn(writer); writer.flush(); } public void dump() { this.dumpOn(System.out); } @Override public String toString() { return StringTools.buildToStringFor(this, this.name); } } /** * This Node wraps a TestModel and converts into something that can * be used by TreeModelAdapter. It converts changes to the TestModel's * name into "state changes" to the Node; and converts the * TestModel's children into a ListValueModel of Nodes whose order is * determined by subclass implementations. */ public static abstract class TestNode extends AbstractTreeNodeValueModel implements Displayable, Comparable { /** the model object wrapped by this node */ private final TestModel testModel; /** this node's parent node; null for the root node */ private final TestNode parent; /** this node's child nodes */ private final ListValueModel> childrenModel; /** a listener that notifies us when the model object's "internal state" changes */ private final PropertyChangeListener testModelListener; // ********** constructors/initialization ********** /** * root node constructor */ public TestNode(TestModel testModel) { this(null, testModel); } /** * branch or leaf node constructor */ public TestNode(TestNode parent, TestModel testModel) { super(); this.parent = parent; this.testModel = testModel; this.childrenModel = this.buildChildrenModel(testModel); this.testModelListener = this.buildTestModelListener(); } private PropertyChangeListener buildTestModelListener() { return new PropertyChangeListener() { public void propertyChanged(PropertyChangeEvent e) { TestNode.this.testModelChanged(e); } }; } /** * subclasses decide the order of the child nodes */ protected abstract ListValueModel> buildChildrenModel(TestModel model); /** * used by subclasses; * transform the test model children into nodes */ protected ListValueModel> buildNodeAdapter(TestModel model) { return new TransformationListValueModel>(this.buildChildrenAdapter(model)) { @Override protected TestNode transformItem(TestModel item) { return TestNode.this.buildChildNode(item); } }; } /** * subclasses must build a concrete node for the specified test model */ protected abstract TestNode buildChildNode(TestModel childTestModel); /** * return a collection value model on the specified model's children */ protected CollectionValueModel buildChildrenAdapter(TestModel model) { return new CollectionAspectAdapter(TestModel.CHILDREN_COLLECTION, model) { @Override protected Iterator iterator_() { return this.subject.children(); } @Override protected int size_() { return this.subject.childrenSize(); } }; } // ********** TreeNodeValueModel implementation ********** public TestModel getValue() { return this.testModel; } public TreeNodeValueModel parent() { return this.parent; } public ListValueModel> childrenModel() { return this.childrenModel; } // ********** AbstractTreeNodeValueModel implementation ********** @Override protected void engageValue() { this.testModel.addPropertyChangeListener(TestModel.NAME_PROPERTY, this.testModelListener); } @Override protected void disengageValue() { this.testModel.removePropertyChangeListener(TestModel.NAME_PROPERTY, this.testModelListener); } // ********** Displayable implementation ********** public String displayString() { return this.testModel.getName(); } public Icon icon() { return null; } // ********** debugging support ********** public String dumpString() { StringWriter sw = new StringWriter(); IndentingPrintWriter ipw = new IndentingPrintWriter(sw); this.dumpOn(ipw); return sw.toString(); } public void dumpOn(IndentingPrintWriter writer) { writer.println(this); writer.indent(); for (Iterator> stream = this.childrenModel.iterator(); stream.hasNext(); ) { // cast to a TestNode (i.e. this won't work with a NameTestNode in the tree) ((TestNode) stream.next()).dumpOn(writer); } writer.undent(); } public void dumpOn(OutputStream stream) { IndentingPrintWriter writer = new IndentingPrintWriter(new OutputStreamWriter(stream)); this.dumpOn(writer); writer.flush(); } public void dump() { this.dumpOn(System.out); } // ********** behavior ********** /** * the model's name has changed, forward the event to our listeners */ protected void testModelChanged(PropertyChangeEvent e) { // we need to notify listeners that our "internal state" has changed this.fireStateChanged(); // our display string stays in synch with the model's name this.firePropertyChanged(DISPLAY_STRING_PROPERTY, e.getOldValue(), e.getNewValue()); } // ********** queries ********** public TestModel getTestModel() { return this.testModel; } /** * testing convenience method */ public TestNode childNamed(String name) { for (Iterator> stream = this.childrenModel.iterator(); stream.hasNext(); ) { TreeNodeValueModel childNode = stream.next(); if (childNode instanceof TestNode) { if (((TestNode) childNode).getTestModel().getName().equals(name)) { return (TestNode) childNode; } } } throw new IllegalArgumentException("child not found: " + name); } // ********** standard methods ********** public int compareTo(TestNode o) { return this.displayString().compareTo(o.displayString()); } @Override public String toString() { return "Node(" + this.testModel + ")"; } } /** * concrete implementation that keeps its children sorted */ public static class SortedTestNode extends TestNode { // ********** constructors ********** public SortedTestNode(TestModel testModel) { super(testModel); } public SortedTestNode(TestNode parent, TestModel testModel) { super(parent, testModel); } // ********** initialization ********** /** the list should be sorted */ @Override protected ListValueModel> buildChildrenModel(TestModel testModel) { return new SortedListValueModelWrapper>(this.buildDisplayStringAdapter(testModel)); } /** the display string (name) of each node can change */ protected ListValueModel> buildDisplayStringAdapter(TestModel testModel) { return new ItemPropertyListValueModelAdapter>(this.buildNodeAdapter(testModel), DISPLAY_STRING_PROPERTY); } /** children are also sorted nodes */ @Override protected TestNode buildChildNode(TestModel childNode) { return new SortedTestNode(this, childNode); } } /** * concrete implementation that leaves its children unsorted */ public static class UnsortedTestNode extends TestNode { // ********** constructors ********** public UnsortedTestNode(TestModel testModel) { super(testModel); } public UnsortedTestNode(TestNode parent, TestModel testModel) { super(parent, testModel); } // ********** initialization ********** /** the list should NOT be sorted */ @Override protected ListValueModel> buildChildrenModel(TestModel testModel) { return this.buildNodeAdapter(testModel); } /** children are also unsorted nodes */ @Override protected TestNode buildChildNode(TestModel childNode) { return new UnsortedTestNode(this, childNode); } } /** * concrete implementation that leaves its children unsorted * and has a special set of children for "node 3" */ public static class SpecialTestNode extends UnsortedTestNode { // ********** constructors ********** public SpecialTestNode(TestModel testModel) { super(testModel); } public SpecialTestNode(TestNode parent, TestModel testModel) { super(parent, testModel); } // ********** initialization ********** /** return a different list of children for "node 3" */ @Override protected ListValueModel> buildChildrenModel(TestModel testModel) { if (testModel.getName().equals("node 3")) { return this.buildSpecialChildrenModel(); } return super.buildChildrenModel(testModel); } protected ListValueModel> buildSpecialChildrenModel() { TreeNodeValueModel[] children = new NameTestNode[1]; children[0] = new NameTestNode(this); return new SimpleListValueModel>(Arrays.asList(children)); } /** children are also special nodes */ @Override protected TestNode buildChildNode(TestModel childNode) { return new SpecialTestNode(this, childNode); } } public static class NameTestNode extends AbstractTreeNodeValueModel { private final WritablePropertyValueModel nameAdapter; private final SpecialTestNode specialNode; // parent node private final PropertyChangeListener nameListener; private final ListValueModel> childrenModel; // ********** construction/initialization ********** public NameTestNode(SpecialTestNode specialNode) { super(); this.nameListener = this.buildNameListener(); this.specialNode = specialNode; this.nameAdapter = this.buildNameAdapter(); this.childrenModel = new NullListValueModel>(); } protected PropertyChangeListener buildNameListener() { return new PropertyChangeListener() { public void propertyChanged(PropertyChangeEvent e) { NameTestNode.this.nameChanged(e); } }; } protected WritablePropertyValueModel buildNameAdapter() { return new PropertyAspectAdapter(TestModel.NAME_PROPERTY, this.getTestModel()) { @Override protected String buildValue_() { return this.subject.getName(); } @Override protected void setValue_(String value) { this.subject.setName(value); } }; } public TestModel getTestModel() { return this.specialNode.getTestModel(); } // ********** TreeNodeValueModel implementation ********** public String getValue() { return this.nameAdapter.getValue(); } @Override public void setValue(Object value) { this.nameAdapter.setValue((String) value); } public TreeNodeValueModel parent() { return this.specialNode; } public ListValueModel> childrenModel() { return this.childrenModel; } // ********** AbstractTreeNodeValueModel implementation ********** @Override protected void engageValue() { this.nameAdapter.addPropertyChangeListener(PropertyValueModel.VALUE, this.nameListener); } @Override protected void disengageValue() { this.nameAdapter.removePropertyChangeListener(PropertyValueModel.VALUE, this.nameListener); } // ********** behavior ********** protected void nameChanged(PropertyChangeEvent e) { // we need to notify listeners that our "value" has changed this.firePropertyChanged(VALUE, e.getOldValue(), e.getNewValue()); } } private TreeModel buildTreeModel(TestNode root) { return this.buildTreeModel(new StaticPropertyValueModel>(root)); } private TreeModel buildTreeModel(PropertyValueModel> rootHolder) { return new TreeModelAdapter(rootHolder) { @Override protected ListChangeListener buildChildrenListener() { return this.buildChildrenListener_(); } @Override protected StateChangeListener buildNodeStateListener() { return this.buildNodeStateListener_(); } @Override protected PropertyChangeListener buildNodeValueListener() { return this.buildNodeValueListener_(); } @Override protected PropertyChangeListener buildRootListener() { return this.buildRootListener_(); } }; } /** * listener that will blow up with any event; * override and implement expected event methods */ public class TestTreeModelListener implements TreeModelListener { public void treeNodesChanged(TreeModelEvent e) { fail("unexpected event"); } public void treeNodesInserted(TreeModelEvent e) { fail("unexpected event"); } public void treeNodesRemoved(TreeModelEvent e) { fail("unexpected event"); } public void treeStructureChanged(TreeModelEvent e) { fail("unexpected event"); } } }