/*
* Copyright (c) 2007 BUSINESS OBJECTS SOFTWARE LIMITED
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* * Neither the name of Business Objects nor the names of its contributors
* may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
/*
* ArgumentTreeModel.java
* Creation date: May 25, 2004.
* By: Edward Lam
*/
package org.openquark.gems.client;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.WeakHashMap;
import javax.swing.tree.DefaultTreeModel;
import javax.swing.tree.TreePath;
import org.openquark.gems.client.ArgumentTreeNode.ArgumentNode;
import org.openquark.gems.client.ArgumentTreeNode.CollectorNode;
import org.openquark.gems.client.ArgumentTreeNode.RootNode;
import org.openquark.gems.client.Gem.PartInput;
/**
* The TreeModel class used by the ArguementTree.
* @author Edward Lam
*/
public class ArgumentTreeModel extends DefaultTreeModel {
private static final long serialVersionUID = 8818055654152948899L;
/** A comparator used to sort collector gems by unqualified name. */
private static final Comparator<CollectorGem> collectorComparatorByName = new Comparator<CollectorGem>() {
public int compare(CollectorGem o1, CollectorGem o2) {
return o1.getUnqualifiedName().compareTo(o2.getUnqualifiedName());
}
};
/** A comparator used to sort arguments by name. */
private static final Comparator<Gem.PartInput> argumentComparatorByName = new Comparator<Gem.PartInput>() {
public int compare(Gem.PartInput o1, Gem.PartInput o2) {
return o1.getNameString().compareTo(o2.getNameString());
}
};
/** The gem graph backing this model. */
private final GemGraph gemGraph;
/** map from collector gem to the collector node representing it. */
private final Map<CollectorGem, CollectorNode> collectorToNodeMap = new WeakHashMap<CollectorGem, CollectorNode>();
/** map from gem input to the argument node representing it. */
private final Map<PartInput, ArgumentNode> argumentToNodeMap = new WeakHashMap<PartInput, ArgumentNode>();
/** The collector to be displayed by the tree model. If null, all collectors are displayed. */
private CollectorGem collectorToDisplay = null;
/** Whether unused arguments are to be displayed. */
private boolean displayUnusedArguments = false;
// A couple of listeners to update the model when the gem graph changes.
private final CollectorNameChangeListener collectorNameChangeListener = new CollectorNameChangeListener();
private final ModelReflectedInputListener modelReflectedInputListener = new ModelReflectedInputListener();
/** A listener to update on argument name changes. */
private final InputNameListener inputNameListener = new InputNameListener() {
public void inputNameChanged(InputNameEvent e) {
nodeChanged(getArgumentNode(e.getInputChanged()));
}
};
/**
* The listener class for this model to handle collector name changes.
* @author Edward Lam
*/
private class CollectorNameChangeListener implements NameChangeListener {
/**
* {@inheritDoc}
*/
public void nameChanged(NameChangeEvent e) {
CollectorGem changedCollector = (CollectorGem)e.getSource();
// Ignore if the changed collector gem is not displayed.
if (!(collectorToDisplay == null || collectorToDisplay == changedCollector)) {
return;
}
CollectorNode collectorNode = collectorToNodeMap.get(changedCollector);
nodeChanged(collectorNode);
// If we are displaying multiple collectors, ensure that they remain in order.
// Don't reposition the target though.
CollectorGem targetCollectorGem = gemGraph.getTargetCollector();
if (collectorToDisplay == null && changedCollector != targetCollectorGem) {
// Check vs. siblings.
ArgumentTreeNode rootNode = (ArgumentTreeNode)getRoot();
CollectorNode targetCollectorNode = getCollectorNode(targetCollectorGem);
// Get the collector node which should precede this collector's node.
CollectorNode precedingNode = (CollectorNode)collectorNode.getPreviousSibling();
while (precedingNode != null && precedingNode != targetCollectorNode &&
collectorComparatorByName.compare(changedCollector, precedingNode.getCollectorGem()) < 0) {
precedingNode = (CollectorNode)precedingNode.getPreviousSibling();
}
// If it's not equal to the node which currently precedes it, reposition.
if (precedingNode != collectorNode.getPreviousSibling()) {
removeNodeFromParent(collectorNode);
int insertIndex = (precedingNode == null) ? 0 : rootNode.getIndex(precedingNode) + 1;
insertNodeInto(collectorNode, rootNode, insertIndex);
// We can skip the check against the following node, if the nodes are always sorted.
return;
}
// Get the collector node which should follow this collector's node.
CollectorNode followingNode = (CollectorNode)collectorNode.getNextSibling();
while (followingNode != null && collectorComparatorByName.compare(followingNode.getCollectorGem(), changedCollector) < 0) {
followingNode = (CollectorNode)followingNode.getNextSibling();
}
// If it's not equal to the node which currently follows it, reposition.
if (followingNode != collectorNode.getNextSibling()) {
removeNodeFromParent(collectorNode);
int insertIndex = (followingNode == null) ? rootNode.getChildCount() : rootNode.getIndex(followingNode);
insertNodeInto(collectorNode, rootNode, insertIndex);
}
}
}
}
/**
* The listener class for this model to handle changes in the inputs reflected by a collector.
* @author Edward Lam
*/
private class ModelReflectedInputListener implements ReflectedInputListener {
/**
* {@inheritDoc}
*/
public void reflectedInputsChanged(ReflectedInputEvent e) {
CollectorGem collectorGem = (CollectorGem)e.getSource();
// Ignore if the changed collector gem is not displayed.
if (!(collectorToDisplay == null || collectorToDisplay == collectorGem)) {
return;
}
// Just repopulate the entire tree for now.
// If we wanted to, later we could limit the change to just the changed node.
repopulate();
}
}
/**
* Constructor for an ArgumentTreeModel.
* @param gemGraph the gem graph which this model represents.
* @param root the root of the tree model.
*/
ArgumentTreeModel(GemGraph gemGraph, RootNode root) {
super(root);
this.gemGraph = gemGraph;
// Focus on the target collector by default.
this.collectorToDisplay = gemGraph.getTargetCollector();
// Populate the model.
repopulate();
// Add collector listeners for the collectors already in the gem graph.
for (final Gem gem : gemGraph.getCollectors()) {
CollectorGem collectorGem = (CollectorGem)gem;
collectorGem.addNameChangeListener(collectorNameChangeListener);
collectorGem.addReflectedInputListener(modelReflectedInputListener);
}
// Add input listeners for gems already in the gem graph
for (final Gem gem : gemGraph.getGems()) {
gem.addInputNameListener(inputNameListener);
}
// Add change listeners to appropriately update the model on gem graph changes.
gemGraph.addGraphChangeListener(new GemGraphChangeListener() {
public void gemAdded(GemGraphAdditionEvent e) {
Gem addedGem = (Gem)e.getSource();
addedGem.addInputNameListener(inputNameListener);
if (addedGem instanceof CollectorGem) {
CollectorGem addedCollectorGem = (CollectorGem)addedGem;
// Add the collector listeners.
addedCollectorGem.addNameChangeListener(collectorNameChangeListener);
addedCollectorGem.addReflectedInputListener(modelReflectedInputListener);
// If all collector gems are displayed, update the tree for unused arguments.
if (displayUnusedArguments && collectorToDisplay == null) {
repopulate();
}
}
}
public void gemRemoved(GemGraphRemovalEvent e) {
Gem removedGem = (Gem)e.getSource();
removedGem.removeInputNameListener(inputNameListener);
if (e.getSource() instanceof CollectorGem) {
CollectorGem collectorGem = (CollectorGem)e.getSource();
// Update the tree.
repopulate();
// Remove listeners.
collectorGem.removeNameChangeListener(collectorNameChangeListener);
collectorGem.removeReflectedInputListener(modelReflectedInputListener);
}
}
public void gemConnected(GemGraphConnectionEvent e) {
}
public void gemDisconnected(GemGraphDisconnectionEvent e) {
}
});
}
/**
* @return the gem graph upon which this model is based.
*/
public GemGraph getGemGraph() {
return gemGraph;
}
/**
* @return the target collector gem for the model's gem graph.
*/
public CollectorGem getTargetCollectorGem() {
return gemGraph.getTargetCollector();
}
/**
* Return the ArgumentNode used to represent the given argument.
* @param argument the argument to be represented.
* @return an ArgumentNode to use for that argument.
* If possible, any ArgumentNode previously used to represent the argument will be reused.
*/
ArgumentNode getArgumentNode(Gem.PartInput argument) {
ArgumentNode argumentNode = argumentToNodeMap.get(argument);
if (argumentNode == null) {
argumentNode = new ArgumentNode(argument);
argumentToNodeMap.put(argument, argumentNode);
}
return argumentNode;
}
/**
* Return the CollectorNode used to represent the given collector.
* @param collectorGem the collector to be represented.
* @return an CollectorNode to use for that argument.
* If possible, any CollectorNode previously used to represent the collector will be reused.
*/
CollectorNode getCollectorNode(CollectorGem collectorGem) {
CollectorNode collectorNode = collectorToNodeMap.get(collectorGem);
if (collectorNode == null) {
collectorNode = new CollectorNode(collectorGem);
collectorToNodeMap.put(collectorGem, collectorNode);
}
return collectorNode;
}
/**
* Populate the tree model with all the collectors in the gem graph.
* Precondition: the current tree model must be empty.
*/
private void populateAllCollectors() {
Set<CollectorGem> collectors = gemGraph.getCollectors();
// Map collector to the collectors targeting that collector.
Map<CollectorGem, Set<CollectorGem>> targetToCollectorMap = new HashMap<CollectorGem, Set<CollectorGem>>();
for (final Gem gem : collectors) {
CollectorGem collectorGem = (CollectorGem)gem;
CollectorGem target = collectorGem.getTargetCollectorGem();
// Add the collector to the set of collectors targeting its target.
Set<CollectorGem> targetingCollectors = targetToCollectorMap.get(target);
if (targetingCollectors == null) {
targetingCollectors = new HashSet<CollectorGem>();
targetToCollectorMap.put(target, targetingCollectors);
}
targetingCollectors.add(collectorGem);
}
populate(gemGraph.getTargetCollector(), targetToCollectorMap);
}
/**
* Populate the tree model with a subset of the gem graph.
* @param collectorGem the collector which encloses the collectors with which to populate the model.
* @param targetToCollectorMap map from collector to the collectors which target that collector.
* After populating with the current collector gem, populate() will be called recursively on targeting collectors.
*/
private void populate(CollectorGem collectorGem, Map<CollectorGem, Set<CollectorGem>> targetToCollectorMap) {
// Create a node for the collector, and add it to the parent.
CollectorNode collectorNode = getCollectorNode(collectorGem);
((ArgumentTreeNode)getRoot()).add(collectorNode);
collectorNode.removeAllChildren();
// Add node for the arguments.
List<Gem.PartInput> reflectedInputs = collectorGem.getReflectedInputs();
for (final Gem.PartInput reflectedInput : reflectedInputs) {
ArgumentNode reflectedInputNode = getArgumentNode(reflectedInput);
collectorNode.add(reflectedInputNode);
}
if (displayUnusedArguments) {
// Get the unused arguments.
Set<PartInput> unusedArgumentSet = new HashSet<PartInput>(collectorGem.getTargetArguments());
unusedArgumentSet.removeAll(reflectedInputs);
// Sort them.
List<PartInput> unusedArgumentList = new ArrayList<PartInput>(unusedArgumentSet);
Collections.sort(unusedArgumentList, argumentComparatorByName);
// Add the nodes.
for (final PartInput unusedArgument : unusedArgumentList) {
ArgumentNode unusedInputArgumentNode = getArgumentNode(unusedArgument);
collectorNode.add(unusedInputArgumentNode);
}
}
// Call recursively on targeting collectors.
Set<CollectorGem> targetingCollectors = targetToCollectorMap.get(collectorGem);
if (targetingCollectors != null) {
// Sort the targeting collectors by unqualified name.
List<CollectorGem> targetingCollectorList = new ArrayList<CollectorGem>(targetingCollectors);
Collections.sort(targetingCollectorList, collectorComparatorByName);
// Call on the targeting collectors, in order.
for (final CollectorGem targetingCollector : targetingCollectorList) {
populate(targetingCollector, targetToCollectorMap);
}
}
}
/**
* {@inheritDoc}
* Override so that commits for arguments in run mode repopulate the tree.
*/
@Override
public void valueForPathChanged(TreePath path, Object newValue) {
repopulate();
}
/**
* Generate nodeChanged() events for every argument targeting the target collector node.
*/
void targetArgumentNodesChanged() {
ArgumentTreeNode targetCollectorNode = getCollectorNode(gemGraph.getTargetCollector());
int nChildren = targetCollectorNode.getChildCount();
int[] childIndices = new int[nChildren];
for (int i = 0; i < nChildren; i++) {
childIndices[i] = i;
}
nodesChanged(targetCollectorNode, childIndices);
}
/**
* Sort a set of collectors by unqualified name.
* @param collectorSet the set of collectors to sort.
* @return the collectors in collectorSet, sorted by unqualified name.
*/
public static List<CollectorGem> getSortedCollectorList(Set<CollectorGem> collectorSet) {
List<CollectorGem> targetingCollectorList = new ArrayList<CollectorGem>(collectorSet);
Collections.sort(targetingCollectorList, collectorComparatorByName);
return targetingCollectorList;
}
/**
* @return the currently displayed collector, or null if all collectors are currently being displayed.
*/
public CollectorGem getDisplayedCollector() {
return collectorToDisplay;
}
/**
* Change the collector which is the focus of the argument tree.
* @param collectorToDisplay the new collector on which the argument tree should focus.
* If null, all collectors are displayed.
*/
public void setCollectorToDisplay(CollectorGem collectorToDisplay) {
if (this.collectorToDisplay != collectorToDisplay) {
this.collectorToDisplay = collectorToDisplay;
repopulate();
}
}
/**
* @return whether unused arguments are displayed.
*/
public boolean getDisplayUnusedArguments() {
return displayUnusedArguments;
}
/**
* @param displayUnusedArguments whether unused arguments should be displayed.
*/
public void setDisplayUnusedArguments(boolean displayUnusedArguments) {
if (this.displayUnusedArguments != displayUnusedArguments) {
this.displayUnusedArguments = displayUnusedArguments;
repopulate();
}
}
/**
* Repopulate the tree model based on the current state.
*/
private void repopulate() {
// Remove all current children from the root.
ArgumentTreeNode rootNode = (ArgumentTreeNode)getRoot();
rootNode.removeAllChildren();
if (collectorToDisplay == null) {
// Display all collectors
populateAllCollectors();
} else {
// Focus on a single collector.
populate(collectorToDisplay, Collections.<CollectorGem, Set<CollectorGem>>emptyMap());
}
nodeStructureChanged(rootNode);
}
}