/* $Id: ExplorerTreeModel.java 17843 2010-01-12 19:23:29Z linus $
*****************************************************************************
* Copyright (c) 2009 Contributors - see below
* 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:
* bobtarling
*****************************************************************************
*
* Some portions of this file was previously release using the BSD License:
*/
// Copyright (c) 1996-2006 The Regents of the University of California. All
// Rights Reserved. Permission to use, copy, modify, and distribute this
// software and its documentation without fee, and without a written
// agreement is hereby granted, provided that the above copyright notice
// and this paragraph appear in all copies. This software program and
// documentation are copyrighted by The Regents of the University of
// California. The software program and documentation are supplied "AS
// IS", without any accompanying services from The Regents. The Regents
// does not warrant that the operation of the program will be
// uninterrupted or error-free. The end-user understands that the program
// was developed for research purposes and is advised not to rely
// exclusively on the program for any reason. IN NO EVENT SHALL THE
// UNIVERSITY OF CALIFORNIA BE LIABLE TO ANY PARTY FOR DIRECT, INDIRECT,
// SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, INCLUDING LOST PROFITS,
// ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN IF
// THE UNIVERSITY OF CALIFORNIA HAS BEEN ADVISED OF THE POSSIBILITY OF
// SUCH DAMAGE. THE UNIVERSITY OF CALIFORNIA SPECIFICALLY DISCLAIMS ANY
// WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE
// PROVIDED HEREUNDER IS ON AN "AS IS" BASIS, AND THE UNIVERSITY OF
// CALIFORNIA HAS NO OBLIGATIONS TO PROVIDE MAINTENANCE, SUPPORT,
// UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
package org.argouml.ui.explorer;
import java.awt.EventQueue;
import java.awt.event.ItemEvent;
import java.awt.event.ItemListener;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.swing.tree.DefaultMutableTreeNode;
import javax.swing.tree.DefaultTreeModel;
import javax.swing.tree.MutableTreeNode;
import javax.swing.tree.TreeNode;
import javax.swing.tree.TreePath;
import org.apache.log4j.Logger;
import org.argouml.kernel.Project;
import org.argouml.kernel.ProjectManager;
import org.argouml.model.InvalidElementException;
import org.argouml.ui.explorer.rules.PerspectiveRule;
/**
* The model for the Explorer tree view of the uml model.
*
* provides:
* - receives events from the uml model and updates itself and the tree ui.
* - responds to changes in perspective and ordering.
*
* @author alexb
* @since 0.15.2
*/
public class ExplorerTreeModel extends DefaultTreeModel
implements TreeModelUMLEventListener, ItemListener {
/**
* Logger.
*/
private static final Logger LOG =
Logger.getLogger(ExplorerTreeModel.class);
/**
* an array of
* {@link org.argouml.ui.explorer.rules.PerspectiveRule PerspectiveRules},
* that determine the tree view.
*/
private List<PerspectiveRule> rules;
/**
* a map used to resolve model elements to tree nodes when determining
* what effect a model event will have on the tree.
*/
private Map<Object, Set<ExplorerTreeNode>> modelElementMap;
/**
* the global order for siblings in the tree.
*/
private Comparator order;
/**
* The children currently being updated.
*/
private List<ExplorerTreeNode> updatingChildren =
new ArrayList<ExplorerTreeNode>();
/**
* A Runnable object that when executed does update some
* currently pending nodes.
*/
private ExplorerUpdater nodeUpdater = new ExplorerUpdater();
private ExplorerTree tree;
/**
* Help class to semi-lazily update nodes in the tree.
* This class is thread safe.
*/
class ExplorerUpdater implements Runnable {
/**
* The set of nodes pending being updated.
*/
private LinkedList<ExplorerTreeNode> pendingUpdates =
new LinkedList<ExplorerTreeNode>();
/**
* Is this object currently waiting to be run.
*/
private boolean hot;
/**
* The maximum number of nodes to update in one chunk.
*/
public static final int MAX_UPDATES_PER_RUN = 100;
/**
* Schedule this object to run on AWT-EventQueue-0 at some later time.
*/
private synchronized void schedule() {
if (hot) {
return;
}
hot = true;
EventQueue.invokeLater(this);
}
/**
* Schedule updateChildren to be called on node at some later time.
* Does nothing if there already is a pending update of node.
*
* @param node The ExplorerTreeNode to be updated.
* @throws NullPointerException If node is null.
*/
public synchronized void schedule(ExplorerTreeNode node) {
if (node.getPending()) {
return;
}
pendingUpdates.add(node);
node.setPending(true);
schedule();
}
/**
* Call updateChildren for some pending nodes. Will call at most
* MAX_UPDATES_PER_RUN each time. Should there still be pending
* updates after that then it will reschedule itself.<p>
*
* This method should not be called explicitly, instead schedule
* should be called and this method will be called automatically.
*/
public void run() {
boolean done = false;
for (int i = 0; i < MAX_UPDATES_PER_RUN; i++) {
ExplorerTreeNode node = null;
synchronized (this) {
if (!pendingUpdates.isEmpty()) {
node = pendingUpdates.removeFirst();
node.setPending(false);
} else {
done = true;
hot = false;
break;
}
}
updateChildren(new TreePath(getPathToRoot(node)));
}
if (!done) {
schedule();
} else {
// TODO: This seems like a brute force workaround (and a very
// indirect one at that). It appears to be needed though until
// we fix the problem properly. - tfm 20070904
/* This solves issue 2287. */
tree.refreshSelection();
}
}
}
/**
* The constructor of ExplorerTreeModel.
*
* @param root an object to place at the root
* @param myTree the tree
*/
public ExplorerTreeModel(Object root, ExplorerTree myTree) {
super(new DefaultMutableTreeNode());
tree = myTree;
setRoot(new ExplorerTreeNode(root, this));
setAsksAllowsChildren(false);
modelElementMap = new HashMap<Object, Set<ExplorerTreeNode>>();
ExplorerEventAdaptor.getInstance()
.setTreeModelUMLEventListener(this);
order = new TypeThenNameOrder();
}
/*
* @see org.argouml.ui.explorer.TreeModelUMLEventListener#modelElementChanged(java.lang.Object)
*/
public void modelElementChanged(Object node) {
traverseModified((TreeNode) getRoot(), node);
}
/*
* @see org.argouml.ui.explorer.TreeModelUMLEventListener#modelElementAdded(java.lang.Object)
*/
public void modelElementAdded(Object node) {
traverseModified((TreeNode) getRoot(), node);
}
/**
* Traverses the children, finds those affected by the given node,
* and notifies them that they are modified.
*
* @param start the node to start from
* @param node the given node
*/
private void traverseModified(TreeNode start, Object node) {
Enumeration children = start.children();
while (children.hasMoreElements()) {
TreeNode child = (TreeNode) children.nextElement();
traverseModified(child, node);
}
if (start instanceof ExplorerTreeNode) {
((ExplorerTreeNode) start).nodeModified(node);
}
}
/*
* @see org.argouml.ui.explorer.TreeModelUMLEventListener#modelElementRemoved(java.lang.Object)
*/
public void modelElementRemoved(Object node) {
for (ExplorerTreeNode changeNode
: new ArrayList<ExplorerTreeNode>(findNodes(node))) {
if (changeNode.getParent() != null) {
removeNodeFromParent(changeNode);
}
}
traverseModified((TreeNode) getRoot(), node);
}
/*
* the model structure has changed significantly, eg a new project.
* @see org.argouml.ui.explorer.TreeModelUMLEventListener#structureChanged()
*/
public void structureChanged() {
// remove references for gc
if (getRoot() instanceof ExplorerTreeNode) {
((ExplorerTreeNode) getRoot()).remove();
}
// This should only be helpful for old garbage collectors.
for (Collection nodes : modelElementMap.values()) {
nodes.clear();
}
modelElementMap.clear();
// This is somewhat inconsistent with the design of the constructor
// that receives the root object by argument. If this is okay
// then there may be no need for a constructor with that argument.
modelElementMap = new HashMap<Object, Set<ExplorerTreeNode>>();
Project proj = ProjectManager.getManager().getCurrentProject();
ExplorerTreeNode rootNode = new ExplorerTreeNode(proj, this);
addToMap(proj, rootNode);
setRoot(rootNode);
}
/**
* updates next level of the explorer tree for a given tree path.
*
* @param path the path to the node whose children to update.
* @throws IllegalArgumentException if node has a child that is not a
* (descendant of) DefaultMutableTreeNode.
*/
public void updateChildren(TreePath path) {
ExplorerTreeNode node = (ExplorerTreeNode) path.getLastPathComponent();
Object modelElement = node.getUserObject();
// Avoid doing this too early in the initialization process
if (rules == null) {
return;
}
// Avoid recursively updating the same child
if (updatingChildren.contains(node)) {
return;
}
updatingChildren.add(node);
List children = reorderChildren(node);
List newChildren = new ArrayList();
Set deps = new HashSet();
collectChildren(modelElement, newChildren, deps);
node.setModifySet(deps);
mergeChildren(node, children, newChildren);
updatingChildren.remove(node);
}
/**
* Sorts the child nodes of node using the current ordering.<p>
*
* Note: UserObject is only available from descendants of
* DefaultMutableTreeNode, so any other children couldn't be sorted.
* Thus these are currently forbidden. But currently no such node is
* ever inserted into the tree.
*
* @param node the node whose children to sort
* @return the UserObjects of the children, in the same order as the
* children.
* @throws IllegalArgumentException if node has a child that is not a
* (descendant of) DefaultMutableTreeNode.
*/
private List<Object> reorderChildren(ExplorerTreeNode node) {
List<Object> childUserObjects = new ArrayList<Object>();
List<ExplorerTreeNode> reordered = new ArrayList<ExplorerTreeNode>();
// Enumerate the current children of node to find out which now sorts
// in different order, since these must be moved
Enumeration enChld = node.children();
Object lastObj = null;
while (enChld.hasMoreElements()) {
Object child = enChld.nextElement();
if (child instanceof ExplorerTreeNode) {
Object obj = ((ExplorerTreeNode) child).getUserObject();
if (lastObj != null && order.compare(lastObj, obj) > 0) {
/*
* If a node to be moved is currently selected,
* move its predecessors instead so don't lose selection.
* This fixes issue 3249.
* NOTE: this does not deal with the case where
* multiple nodes are selected and they are out
* of order with respect to each other, but I
* don't think more than one node is ever reordered
* at a time - tfm
*/
if (!tree.isPathSelected(new TreePath(
getPathToRoot((ExplorerTreeNode) child)))) {
reordered.add((ExplorerTreeNode) child);
} else {
ExplorerTreeNode prev =
(ExplorerTreeNode) ((ExplorerTreeNode) child)
.getPreviousSibling();
while (prev != null
&& (order.compare(prev.getUserObject(), obj)
>= 0)) {
reordered.add(prev);
childUserObjects.remove(childUserObjects.size() - 1);
prev = (ExplorerTreeNode) prev.getPreviousSibling();
}
childUserObjects.add(obj);
lastObj = obj;
}
} else {
childUserObjects.add(obj);
lastObj = obj;
}
} else {
throw new IllegalArgumentException(
"Incomprehencible child node " + child.toString());
}
}
for (ExplorerTreeNode child : reordered) {
// Avoid our deinitialization here
// The node will be added back to the tree again
super.removeNodeFromParent(child);
}
// For each reordered node, find it's new position among the current
// children and move it there
for (ExplorerTreeNode child : reordered) {
Object obj = child.getUserObject();
int ip = Collections.binarySearch(childUserObjects, obj, order);
if (ip < 0) {
ip = -(ip + 1);
}
// Avoid our initialization here
super.insertNodeInto(child, node, ip);
childUserObjects.add(ip, obj);
}
return childUserObjects;
}
/**
* Collects the set of children modelElement should have at this point in
* time. The children are added to newChildren.<p>
*
* Note: Both newChildren and deps are modified by this function, it
* is in fact it's primary purpose to modify these collections. It is your
* responsibility to make sure that they are empty when it is called, or
* to know what you are doing if they are not.
*
* @param modelElement the element to collect children for.
* @param newChildren the new children of modelElement.
* @param deps the set of objects that should be monitored for changes
* since these could affect this list.
* @throws UnsupportedOperationException if add is not supported by
* newChildren or addAll isn't supported by deps.
* @throws NullPointerException if newChildren or deps is null.
* @throws ClassCastException if newChildren or deps rejects some element.
* @throws IllegalArgumentException if newChildren or deps rejects some
* element.
*/
private void collectChildren(Object modelElement, List newChildren,
Set deps) {
if (modelElement == null) {
return;
}
// Collect the current set of objects that should be children to
// this node
for (PerspectiveRule rule : rules) {
// TODO: A better implementation would be to batch events into
// logical groups and update the tree one time for the entire
// group, synchronizing access to the model repository so that
// it stays consistent during the query. This would likely
// require doing the updates in a different thread than the
// event delivery thread to prevent deadlocks, so for right now
// we protect ourselves with try/catch blocks.
Collection children = Collections.emptySet();
try {
children = rule.getChildren(modelElement);
} catch (InvalidElementException e) {
LOG.debug("InvalidElementException in ExplorerTree : "
+ e.getStackTrace());
}
for (Object child : children) {
if (child == null) {
LOG.warn("PerspectiveRule " + rule + " wanted to "
+ "add null to the explorer tree!");
} else if (!newChildren.contains(child)) {
newChildren.add(child);
}
}
try {
Set dependencies = rule.getDependencies(modelElement);
deps.addAll(dependencies);
} catch (InvalidElementException e) {
LOG.debug("InvalidElementException in ExplorerTree : "
+ e.getStackTrace());
}
}
// Order the new children, the dependencies cannot and
// need not be ordered
Collections.sort(newChildren, order);
deps.addAll(newChildren);
}
/**
* Returns a Set of current children to remove and modifies newChildren
* to only contain the children not already in children and not subsumed
* by any WeakExplorerNode in children.<p>
*
* Note: newChildren will be modified by this call.<p>
*
* Note: It is expected that a WeakExplorerNode will not be reused and
* thus they will always initially be slated for removal, and only those
* nodes are in fact used to check subsumption of new nodes. New nodes
* are not checked among themselves for subsumtion.
*
* @param children is the list of current children.
* @param newChildren is the list of expected children.
* @return the Set of current children to remove.
* @throws UnsupportedOperationException if newChildren doesn't support
* remove or removeAll.
* @throws NullPointerException if either argument is null.
*/
private Set prepareAddRemoveSets(List children, List newChildren) {
Set removeSet = new HashSet();
Set commonObjects = new HashSet();
if (children.size() < newChildren.size()) {
commonObjects.addAll(children);
commonObjects.retainAll(newChildren);
} else {
commonObjects.addAll(newChildren);
commonObjects.retainAll(children);
}
newChildren.removeAll(commonObjects);
removeSet.addAll(children);
removeSet.removeAll(commonObjects);
// Handle WeakExplorerNodes
Iterator it = removeSet.iterator();
List weakNodes = null;
while (it.hasNext()) {
Object obj = it.next();
if (!(obj instanceof WeakExplorerNode)) {
continue;
}
WeakExplorerNode node = (WeakExplorerNode) obj;
if (weakNodes == null) {
weakNodes = new LinkedList();
Iterator it2 = newChildren.iterator();
while (it2.hasNext()) {
Object obj2 = it2.next();
if (obj2 instanceof WeakExplorerNode) {
weakNodes.add(obj2);
}
}
}
Iterator it3 = weakNodes.iterator();
while (it3.hasNext()) {
Object obj3 = it3.next();
if (node.subsumes(obj3)) {
// Remove the node from removeSet
it.remove();
// Remove obj3 from weakNodes and newChildren
newChildren.remove(obj3);
it3.remove();
break;
}
}
}
return removeSet;
}
/**
* Merges the current children with the new children removing children no
* longer present and adding new children in the right place.
*
* @param node the TreeNode were merging lists for.
* @param children the current child UserObjects, in order.
* @param newChildren the expected child UserObjects, in order.
* @throws UnsupportedOperationException if the Iterator returned by
* newChildren doesn't support the remove operation, or if
* newChildren itself doesn't support remove or removeAll.
* @throws NullPointerException if node, children or newChildren are null.
*/
private void mergeChildren(ExplorerTreeNode node, List children,
List newChildren) {
Set removeObjects = prepareAddRemoveSets(children, newChildren);
// Remember that children are not TreeNodes but UserObjects
List<ExplorerTreeNode> actualNodes = new ArrayList<ExplorerTreeNode>();
Enumeration childrenEnum = node.children();
while (childrenEnum.hasMoreElements()) {
actualNodes.add((ExplorerTreeNode) childrenEnum.nextElement());
}
int position = 0;
Iterator childNodes = actualNodes.iterator();
Iterator newNodes = newChildren.iterator();
Object firstNew = newNodes.hasNext() ? newNodes.next() : null;
while (childNodes.hasNext()) {
Object childObj = childNodes.next();
if (!(childObj instanceof ExplorerTreeNode)) {
continue;
}
ExplorerTreeNode child = (ExplorerTreeNode) childObj;
Object userObject = child.getUserObject();
if (removeObjects.contains(userObject)) {
removeNodeFromParent(child);
} else {
while (firstNew != null
&& order.compare(firstNew, userObject) < 0) {
insertNodeInto(new ExplorerTreeNode(firstNew, this),
node,
position);
position++;
firstNew = newNodes.hasNext() ? newNodes.next() : null;
}
position++;
}
}
// Add any remaining nodes
while (firstNew != null) {
insertNodeInto(new ExplorerTreeNode(firstNew, this),
node,
position);
position++;
firstNew = newNodes.hasNext() ? newNodes.next() : null;
}
}
/*
* @see javax.swing.tree.DefaultTreeModel#insertNodeInto(javax.swing.tree.MutableTreeNode, javax.swing.tree.MutableTreeNode, int)
*/
@Override
public void insertNodeInto(MutableTreeNode newChild,
MutableTreeNode parent, int index) {
super.insertNodeInto(newChild, parent, index);
if (newChild instanceof ExplorerTreeNode) {
addNodesToMap((ExplorerTreeNode) newChild);
}
}
/*
* @see javax.swing.tree.DefaultTreeModel#removeNodeFromParent(javax.swing.tree.MutableTreeNode)
*/
@Override
public void removeNodeFromParent(MutableTreeNode node) {
if (node instanceof ExplorerTreeNode) {
removeNodesFromMap((ExplorerTreeNode) node);
((ExplorerTreeNode) node).remove();
}
super.removeNodeFromParent(node);
}
/**
* Map all nodes in the subtree rooted at node.
*
* @param node the node to be added
*/
private void addNodesToMap(ExplorerTreeNode node) {
Enumeration children = node.children();
while (children.hasMoreElements()) {
ExplorerTreeNode child = (ExplorerTreeNode) children.nextElement();
addNodesToMap(child);
}
addToMap(node.getUserObject(), node);
}
/**
* Unmap all nodes in the subtree rooted at the given node.
*
* @param node the given node
*/
private void removeNodesFromMap(ExplorerTreeNode node) {
Enumeration children = node.children();
while (children.hasMoreElements()) {
ExplorerTreeNode child = (ExplorerTreeNode) children.nextElement();
removeNodesFromMap(child);
}
removeFromMap(node.getUserObject(), node);
}
/**
* Adds a new tree node and model element to the map.
* nodes are removed from the map when a {@link #modelElementRemoved(Object)
* modelElementRemoved} event is received.
*
* @param modelElement the modelelement to be added
* @param node the node to be added
*/
private void addToMap(Object modelElement, ExplorerTreeNode node) {
Set<ExplorerTreeNode> nodes = modelElementMap.get(modelElement);
if (nodes != null) {
nodes.add(node);
} else {
nodes = new HashSet<ExplorerTreeNode>();
nodes.add(node);
modelElementMap.put(modelElement, nodes);
}
}
/**
* removes a new tree node and model element from the map.
*
* @param modelElement the modelelement to be removed
* @param node the node to be removed
*/
private void removeFromMap(Object modelElement, ExplorerTreeNode node) {
Collection<ExplorerTreeNode> nodes = modelElementMap.get(modelElement);
if (nodes != null) {
nodes.remove(node);
if (nodes.isEmpty()) {
modelElementMap.remove(modelElement);
}
}
}
/**
* Node lookup for a given model element.
*
* @param modelElement the given modelelement
* @return the nodes sought
*/
private Collection<ExplorerTreeNode> findNodes(Object modelElement) {
Collection<ExplorerTreeNode> nodes = modelElementMap.get(modelElement);
if (nodes == null) {
return Collections.EMPTY_LIST;
}
return nodes;
}
/**
* Updates the explorer for new perspectives / orderings.
*
* {@inheritDoc}
*/
public void itemStateChanged(ItemEvent e) {
if (e.getSource() instanceof PerspectiveComboBox) {
rules = ((ExplorerPerspective) e.getItem()).getList();
} else { // it is the combo for "order"
order = (Comparator) e.getItem();
}
structureChanged();
// TODO: temporary - let tree expand implicitly - tfm
tree.expandPath(tree.getPathForRow(1));
}
/**
* @return Returns the nodeUpdater.
*/
ExplorerUpdater getNodeUpdater() {
return nodeUpdater;
}
/**
* The UID.
*/
private static final long serialVersionUID = 3132732494386565870L;
}