/*
* 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.
*/
/*
* BrowserTreeModel.java
* Creation date: (9/14/00 10:58:58 AM)
* By: Luke Evans
*/
package org.openquark.gems.client.browser;
import java.awt.EventQueue;
import java.util.ArrayList;
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.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.WeakHashMap;
import java.util.regex.Pattern;
import javax.swing.SwingUtilities;
import javax.swing.tree.DefaultTreeModel;
import javax.swing.tree.TreeNode;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.collections.Predicate;
import org.openquark.cal.compiler.ModuleName;
import org.openquark.cal.compiler.ModuleNameResolver;
import org.openquark.cal.compiler.ModuleTypeInfo;
import org.openquark.cal.compiler.ScopedEntityNamingPolicy;
import org.openquark.cal.compiler.ScopedEntityNamingPolicy.UnqualifiedUnlessAmbiguous;
import org.openquark.cal.services.GemComparatorSorter;
import org.openquark.cal.services.GemEntity;
import org.openquark.cal.services.GemFormSorter;
import org.openquark.cal.services.GemSorter;
import org.openquark.cal.services.GemUnqualifiedNameCaseInsensitiveSorter;
import org.openquark.cal.services.MetaModule;
import org.openquark.cal.services.Perspective;
import org.openquark.gems.client.FeatureVisibilityPolicy;
import org.openquark.gems.client.ModuleNameDisplayUtilities;
import org.openquark.gems.client.ModuleNameDisplayUtilities.TreeViewDisplayMode;
import org.openquark.gems.client.browser.BrowserTreeNode.NodeReuseInfo;
import org.openquark.gems.client.browser.GemCategory.CategoryKey;
import org.openquark.util.UnsafeCast;
import org.openquark.util.WildcardPatternMatcher;
/**
* The model for the browser tree displayed in the outline control.
* This model contains the abstract tree model and has various methods to construct the displayed tree.
*
* @author Luke Evans
*/
public class BrowserTreeModel extends DefaultTreeModel {
private static final long serialVersionUID = 2746652802349469424L;
/** Refers to the root of the abstract tree. */
private final RootAbstractTreeNode workspaceAbstractTreeNode;
/** Refers to the tree node for the workspace. */
private final WorkspaceRootTreeNode workspaceTreeNode = new WorkspaceRootTreeNode(BrowserMessages.getString("GB_WorkspaceNodeName"));
/** Refers to the tree node that holds the search results. */
private final SearchResultRootTreeNode searchResultsTreeNode = new SearchResultRootTreeNode ();
/**
* The last search string that was used to search the model. Reload uses this to
* restore the model state if it is reloaded.
*/
private String lastSearchString = null;
/** a map from an entity to a corresponding GemTreeNode.
* This is used for updating the UI when we want to open "the" node for an entity.
* There may be more than one, but it doesn't matter which one we pick, so this
* map stores only one of the possibilities. */
private final Map<GemEntity, GemTreeNode> entityToTreeNodeMap;
/** The perspective which this model represents (if any). */
private Perspective perspective = null;
/** If true unimported modules will be shown. */
private boolean showAllModules = true;
/** If true show only public gems. default is to show all gems*/
private boolean showPublicGemsOnly = false;
/** This predicate decides if a CAL feature is visible to the user */
private FeatureVisibilityPolicy visibilityPolicy;
/** This module name resolver maintains the module name resolution rules for the currently displayed set of modules. */
private ModuleNameResolver workspaceModuleNameResolver = ModuleNameResolver.make(Collections.<ModuleName>emptySet());
static final class GemDrawerCategoryInfo {
private final ModuleName moduleName;
private final boolean isNamespaceNode;
/**
* @param moduleName
* @param isNamespaceNode
*/
GemDrawerCategoryInfo(final ModuleName moduleName, final boolean isNamespaceNode) {
this.moduleName = moduleName;
this.isNamespaceNode = isNamespaceNode;
}
ModuleName getModuleName() {
return moduleName;
}
boolean isNamespaceNode() {
return isNamespaceNode;
}
@Override
public boolean equals(Object other) {
if (other instanceof GemDrawerCategoryInfo) {
return equals((GemDrawerCategoryInfo)other);
} else {
return false;
}
}
public boolean equals(GemDrawerCategoryInfo other) {
return this.moduleName.equals(other.moduleName) && this.isNamespaceNode == other.isNamespaceNode;
}
@Override
public int hashCode() {
return 37 * moduleName.hashCode() + Boolean.valueOf(isNamespaceNode).hashCode();
}
}
private final class MaterialGemDrawerCategoryNodeProvider implements CategoryNodeProvider {
private final TreeViewDisplayMode moduleTreeDisplayMode;
private MaterialGemDrawerCategoryNodeProvider(TreeViewDisplayMode moduleTreeDisplayMode) {
if (moduleTreeDisplayMode == null) {
throw new NullPointerException();
}
this.moduleTreeDisplayMode = moduleTreeDisplayMode;
}
private TreeViewDisplayMode getModuleTreeDisplayMode() {
return moduleTreeDisplayMode;
}
public BrowserTreeNode addCategoryNodeToParent(final BrowserTreeNode parent, final GemCategory category, final NodeReuseInfo nodeReuseInfo) {
final GemDrawerCategoryInfo categoryInfo = (GemDrawerCategoryInfo)(category.getCategoryKey()).getValue();
final ModuleName moduleName = categoryInfo.getModuleName();
if (nodeReuseInfo != null) {
final GemDrawer reusedGemDrawer = nodeReuseInfo.getDescendantGemDrawer(moduleName);
// the namespace node state must match before the node can be reused
if (reusedGemDrawer != null && !reusedGemDrawer.isNamespaceNode()) {
if (getModuleTreeDisplayMode() != TreeViewDisplayMode.HIERARCHICAL) {
return parent.addCategoryNode(reusedGemDrawer, category);
} else {
final int nComponents = moduleName.getNComponents();
BrowserTreeNode parentNode = parent;
for (int i = 0; i < nComponents; i++) {
final ModuleName prefix = moduleName.getPrefix(i + 1); // goes from 1 to nComponents through the loop iterations
final boolean isIntermediateNamespaceNode = i != nComponents - 1;
final GemCategory prefixCategory = new GemCategory(new GemCategory.CategoryKey<GemDrawerCategoryInfo>(new GemDrawerCategoryInfo(prefix, isIntermediateNamespaceNode)));
final GemDrawer gemDrawer;
if (!isIntermediateNamespaceNode) {
// for the final node in the sequence, use the reused node
gemDrawer = reusedGemDrawer;
} else {
gemDrawer = new MaterialGemDrawer(prefix, workspaceModuleNameResolver, getModuleTreeDisplayMode(), true);
}
// prefixNode may be different from materialGemDrawer - if the parent already has a node of the same category
final BrowserTreeNode prefixNode = parentNode.addCategoryNode(gemDrawer, prefixCategory);
// for the next loop iteration:
parentNode = prefixNode;
}
return parentNode;
}
}
}
if (getModuleTreeDisplayMode() != TreeViewDisplayMode.HIERARCHICAL) {
return parent.addCategoryNode(new MaterialGemDrawer(moduleName, workspaceModuleNameResolver, getModuleTreeDisplayMode(), false), category);
} else {
final int nComponents = moduleName.getNComponents();
BrowserTreeNode parentNode = parent;
for (int i = 0; i < nComponents; i++) {
final ModuleName prefix = moduleName.getPrefix(i + 1); // goes from 1 to nComponents through the loop iterations
final boolean isIntermediateNamespaceNode = i != nComponents - 1;
final GemCategory prefixCategory = new GemCategory(new GemCategory.CategoryKey<GemDrawerCategoryInfo>(new GemDrawerCategoryInfo(prefix, isIntermediateNamespaceNode)));
final MaterialGemDrawer materialGemDrawer = new MaterialGemDrawer(prefix, workspaceModuleNameResolver, getModuleTreeDisplayMode(), isIntermediateNamespaceNode);
// prefixNode may be different from materialGemDrawer - if the parent already has a node of the same category
final BrowserTreeNode prefixNode = parentNode.addCategoryNode(materialGemDrawer, prefixCategory);
// for the next loop iteration:
parentNode = prefixNode;
}
return parentNode;
}
}
}
/**
* The abstract super class for nodes in the abstract tree.
* The abstract tree has three levels.
* 1) The root workspace level (the root of the tree).
* 2) The Drawer level (corresponding to modules).
* 3) The Entity level
*
* It is possible to get the entire subtree rooted at the node that you are looking
* at simply by using its children list, and its children's children list...etc.
*/
private static abstract class ModelAbstractTreeNode {
/** The name of this node */
private String name;
/**
* Constructor for a ModelAbstractTreeNode
* @param nodeName the name of the node
*/
public ModelAbstractTreeNode(String nodeName) {
this.name = nodeName;
}
public String getName() {
return name;
}
public void setName(String nodeName) {
name = nodeName;
}
}
/**
* A model tree node that can have children.
* @author Edward Lam
*/
private static class AbstractParentTreeNode extends ModelAbstractTreeNode {
/** The children of this node. */
private final List<ModelAbstractTreeNode> childrenList;
/** Map from child node name to the child node. */
private final Map<String, ModelAbstractTreeNode> childMap;
public AbstractParentTreeNode(String viewName) {
super(viewName);
childrenList = new ArrayList<ModelAbstractTreeNode>();
childMap = new HashMap<String, ModelAbstractTreeNode>();
}
/**
* Add a child to this abstract tree node.
* @param childNode ModelAbstractTreeNode the child node to add.
* @return boolean whether the add was successful.
*/
public boolean addChild(ModelAbstractTreeNode childNode) {
childMap.put(childNode.getName(), childNode);
return childrenList.add(childNode);
}
/**
* Get a child of this node by name.
* @param childName String the name of the child to retrieve
* @return ModelAbstractTreeNode the child node with the given name
*/
public ModelAbstractTreeNode getChild(String childName) {
return childMap.get(childName);
}
/**
* Returns whether there is a child whose name is prefixed by the given prefix.
* @param namePrefix
* @return true if there is a child whose name is prefixed by the given prefix.
*/
public boolean hasChildWithPrefix(final String namePrefix) {
for (final String childName : childMap.keySet()) {
if (childName.startsWith(namePrefix)) {
return true;
}
}
return false;
}
/**
* Get the children of this node.
* @return List the children of the node.
*/
public List<ModelAbstractTreeNode> getChildrenList() {
return (childrenList != null) ? new ArrayList<ModelAbstractTreeNode>(childrenList) : new ArrayList<ModelAbstractTreeNode>();
}
/**
* Clear all children. Needed for hack GemCutter reentrancy.
* Only needed by ViewAbstractTreeNode in order to clear all drawers..
*/
void clearChildren() {
childMap.clear();
childrenList.clear();
}
/**
* Compare two children by their order in the abstract tree..
* @param child1 the first child to compare
* @param child2 the second child to compare
* @return int the result of the comparison.
*/
int compareOrder(ModelAbstractTreeNode child1, ModelAbstractTreeNode child2) {
if (child1 == null || child2 == null) {
throw new IllegalArgumentException("Attempt to compare nodes which are null.");
}
if (child1 == child2) {
return 0;
}
// Get the children's relative order
int index1 = childrenList.indexOf(child1);
int index2 = childrenList.indexOf(child2);
if (index1 < 0 || index2 < 0) {
throw new IllegalArgumentException("Attempt to compare nodes which are not children.");
}
return index1 - index2;
}
/**
* Compare two children by their order in the abstract tree..
* @param childName1 the name of the first child to compare
* @param childName2 the name of the second child to compare
* @return int the result of the comparison.
*/
int compareOrder(String childName1, String childName2) {
return compareOrder(getChild(childName1), getChild(childName2));
}
}
/**
* The abstract tree node for the root.
* There exists only one root for the tree.
*/
private static class RootAbstractTreeNode extends AbstractParentTreeNode {
public RootAbstractTreeNode(String nodeName) {
super(nodeName);
}
}
/**
* The abstract GemDrawer tree node.
* A GemDrawer is a module (or possibly some other source of entities).
*/
private static abstract class GemDrawerAbstractTreeNode extends AbstractParentTreeNode {
public GemDrawerAbstractTreeNode(String gemDrawerName) {
super(gemDrawerName);
}
}
/**
* The abstract MaterialGemDrawer tree node.
* It represents material (compiled) modules and entities.
*/
private static class MaterialGemDrawerAbstractTreeNode extends GemDrawerAbstractTreeNode {
public MaterialGemDrawerAbstractTreeNode(ModuleName moduleName) {
super(getGemDrawerNameForModule(moduleName));
}
}
/**
* The model tree node that holds an entity.
* @author Edward Lam
*/
private static class EntityAbstractTreeNode extends ModelAbstractTreeNode {
private final GemEntity gemEntity;
public EntityAbstractTreeNode(GemEntity gemEntity) {
super(gemEntity.getName().getUnqualifiedName());
this.gemEntity = gemEntity;
}
public GemEntity getEntity() {
return gemEntity;
}
}
/**
* Get a comparator on entities according to the name of the gem drawer they are in.
* The primary key is whether the drawer actually exists.
* The secondary key is the name of the drawer.
* @author Edward Lam
*/
private class GemDrawerNameComparator implements Comparator<GemCategory.CategoryKey<GemDrawerCategoryInfo>> {
private final TreeViewDisplayMode moduleTreeDisplayMode;
private GemDrawerNameComparator(final TreeViewDisplayMode moduleTreeDisplayMode) {
if (moduleTreeDisplayMode == null) {
throw new NullPointerException();
}
this.moduleTreeDisplayMode = moduleTreeDisplayMode;
}
private TreeViewDisplayMode getModuleTreeDisplayMode() {
return moduleTreeDisplayMode;
}
public int compare(final GemCategory.CategoryKey<GemDrawerCategoryInfo> e1, final GemCategory.CategoryKey<GemDrawerCategoryInfo> e2) {
final GemDrawerCategoryInfo categoryInfo1 = e1.getValue();
final GemDrawerCategoryInfo categoryInfo2 = e2.getValue();
final ModuleName moduleName1 = categoryInfo1.getModuleName();
final ModuleName moduleName2 = categoryInfo2.getModuleName();
final boolean isNamespaceNode1 = categoryInfo1.isNamespaceNode();
final boolean isNamespaceNode2 = categoryInfo2.isNamespaceNode();
return compareDrawers(moduleName1, isNamespaceNode1, moduleName2, isNamespaceNode2);
}
/**
* Compares the 2 given drawers by their names and by whether they are namespace nodes or not.
* @param moduleName1
* @param isNamespaceNode1
* @param moduleName2
* @param isNamespaceNode2
*/
private int compareDrawers(final ModuleName moduleName1, final boolean isNamespaceNode1, final ModuleName moduleName2, final boolean isNamespaceNode2) {
final boolean isVisible1 = isNamespaceNode1 ? doesNamespaceContainVisibleModules(moduleName1) : isVisibleModule(moduleName1);
final boolean isVisible2 = isNamespaceNode2 ? doesNamespaceContainVisibleModules(moduleName2) : isVisibleModule(moduleName2);
if (getModuleTreeDisplayMode() == TreeViewDisplayMode.HIERARCHICAL) {
final ModuleName commonPrefix = moduleName1.getCommonPrefix(moduleName2);
final int commonPrefixLength = (commonPrefix == null) ? 0 : commonPrefix.getNComponents();
final int moduleNameLength1 = moduleName1.getNComponents();
final int moduleNameLength2 = moduleName2.getNComponents();
if (commonPrefixLength == moduleNameLength1) {
if (commonPrefixLength == moduleNameLength2) {
// the module names are equal, so the namespace node comes after the module node
if (isNamespaceNode1) {
if (isNamespaceNode2) {
return 0;
} else {
return 1;
}
} else if (isNamespaceNode2) {
return -1;
} else {
return 0;
}
} else {
// moduleName1 is a prefix of moduleName2, so moduleName2 comes after because its namespace node
// which shares the name of moduleName1 comes after
return -1;
}
} else if (commonPrefixLength == moduleNameLength2) {
// moduleName2 is a prefix of moduleName1, so moduleName1 comes after because its namespace node
// which shares the name of moduleName2 comes after
return 1;
} else {
// neither module name is a prefix of the other, so order by the immediate children of the common prefix
final ModuleName shortestDisambiguator1 = moduleName1.getPrefix(commonPrefixLength + 1);
final ModuleName shortestDisambiguator2 = moduleName2.getPrefix(commonPrefixLength + 1);
final boolean shortestDisambiguatorIsNamespaceNode1 =
(shortestDisambiguator1.getNComponents() == moduleName1.getNComponents()) ? isNamespaceNode1 : true;
final boolean shortestDisambiguatorIsNamespaceNode2 =
(shortestDisambiguator2.getNComponents() == moduleName2.getNComponents()) ? isNamespaceNode2 : true;
final boolean shortestDisambiguatorIsVisible1 =
shortestDisambiguatorIsNamespaceNode1 ? doesNamespaceContainVisibleModules(shortestDisambiguator1) : isVisibleModule(shortestDisambiguator1);
final boolean shortestDisambiguatorIsVisible2 =
shortestDisambiguatorIsNamespaceNode2 ? doesNamespaceContainVisibleModules(shortestDisambiguator2) : isVisibleModule(shortestDisambiguator2);
// Note that drawers which are visible should always come before those that aren't.
if (shortestDisambiguatorIsVisible1 && !shortestDisambiguatorIsVisible2) {
return -1;
} else if (!shortestDisambiguatorIsVisible1 && shortestDisambiguatorIsVisible2) {
return 1;
} else {
// These drawers either are both visible, or both invisible... in both cases they are treated in a similar fashion
return shortestDisambiguator1.compareTo(shortestDisambiguator2);
}
}
} else if (getModuleTreeDisplayMode() == TreeViewDisplayMode.FLAT_FULLY_QUALIFIED) {
// Note that drawers which are visible should always come before those that aren't.
if (isVisible1 && !isVisible2) {
return -1;
} else if (!isVisible1 && isVisible2) {
return 1;
} else {
// These drawers either are both visible, or both invisible... in both cases they are treated in a similar fashion
return moduleName1.compareTo(moduleName2);
}
} else if (getModuleTreeDisplayMode() == TreeViewDisplayMode.FLAT_ABBREVIATED) {
// Note that drawers which are visible should always come before those that aren't.
if (isVisible1 && !isVisible2) {
return -1;
} else if (!isVisible1 && isVisible2) {
return 1;
} else {
// These drawers either are both visible, or both invisible... in both cases they are treated in a similar fashion
final int nameBasedResult = moduleName1.getLastComponent().compareTo(moduleName2.getLastComponent());
if (nameBasedResult != 0) {
return nameBasedResult;
} else {
// if the last components are the same (and thus the names are abbreviated to the same name), they
// must be distinguished by their immediate prefixes.
final ModuleName immediatePrefix1 = moduleName1.getImmediatePrefix();
final ModuleName immediatePrefix2 = moduleName2.getImmediatePrefix();
if (immediatePrefix1 == null) {
if (immediatePrefix2 == null) {
return 0;
} else {
return -1;
}
} else if (immediatePrefix2 == null) {
return 1;
} else {
return immediatePrefix1.compareTo(immediatePrefix2);
}
}
}
} else {
throw new IllegalStateException("Unexpected TreeViewDisplayMode: " + getModuleTreeDisplayMode());
}
}
}
/**
* A category node provider provides a tree node for a given category.
* @author Edward Lam
*/
interface CategoryNodeProvider {
/**
* Get the tree node appropriate to hold nodes in a given category.
* @param parent the parent to which the node is to be added.
* @param category the category
* @param nodeReuseInfo the node reuse info
* @return BrowserTreeNode the node that ends up as a child of the parent.
*/
public BrowserTreeNode addCategoryNodeToParent(BrowserTreeNode parent, GemCategory category, NodeReuseInfo nodeReuseInfo);
}
/**
* The default category node provider just returns an empty default mutable tree node.
* @author Edward Lam
*/
public static class DefaultCategoryNodeProvider implements CategoryNodeProvider {
/** {@inheritDoc} */
public BrowserTreeNode addCategoryNodeToParent(final BrowserTreeNode parent, final GemCategory category, final NodeReuseInfo nodeReuseInfo) {
if (nodeReuseInfo == null) {
return parent.addCategoryNode(new GemCategoryNode(category.getCategoryKey()), category);
} else {
final BrowserTreeNode reusedNode = nodeReuseInfo.getChildNodeFromDisplayedString(category.getCategoryKey().getName(), GemCategoryNode.class);
if (reusedNode != null) {
return parent.addCategoryNode(reusedNode, category);
} else {
return parent.addCategoryNode(new GemCategoryNode(category.getCategoryKey()), category);
}
}
}
}
/**
* The special implementation of the org.apache.commons.collections
* <code>Predicate</code> interface that filters out modules that fail
* the visibility check.
*/
private class MetaModuleListFilter implements Predicate {
/** @see org.apache.commons.collections.Predicate#evaluate(java.lang.Object) */
public boolean evaluate(Object object) {
if (object instanceof MetaModule) {
return visibilityPolicy.isModuleVisible((MetaModule) object);
}
return false;
}
}
/**
* A GemCategorizer which categorizes by a GemEntity's module name.
* @author Edward Lam
*/
private class ModuleCategorizer extends GemCategorizer<GemDrawerCategoryInfo> {
private final GemDrawerNameComparator drawerComparator;
private final TreeViewDisplayMode moduleTreeDisplayMode;
/**
* Constructor for a VaultModel.ModuleCategorizer.
*/
public ModuleCategorizer(final List<GemDrawerCategoryInfo> categoriesToInclude, final boolean formNewCategories, final TreeViewDisplayMode moduleTreeDisplayMode) {
super(categoriesToInclude, formNewCategories);
if (moduleTreeDisplayMode == null) {
throw new NullPointerException();
}
this.drawerComparator = new GemDrawerNameComparator(moduleTreeDisplayMode);
this.moduleTreeDisplayMode = moduleTreeDisplayMode;
}
TreeViewDisplayMode getModuleTreeDisplayMode() {
return moduleTreeDisplayMode;
}
@Override
public List<GemCategory.CategoryKey<GemDrawerCategoryInfo>> getCategoryKeyList(GemEntity gemEntity) {
final ModuleName moduleName = gemEntity.getName().getModuleName();
return Collections.singletonList(new GemCategory.CategoryKey<GemDrawerCategoryInfo>(new GemDrawerCategoryInfo(moduleName, false)));
}
@Override
public int compareKey(GemCategory.CategoryKey<GemDrawerCategoryInfo> key1, GemCategory.CategoryKey<GemDrawerCategoryInfo> key2) {
return drawerComparator.compare(key1, key2);
}
}
/**
* Default constructor for BrowserTreeModel.
* Automatically constructs a root node in the abstract tree and display
* all modules/gems found in the workspace.
*/
public BrowserTreeModel() {
// The main root node will not be visible
super(new BrowserTreeRootNode(), true);
// Add the search results and workspace nodes
((BrowserTreeNode) getRoot()).add(workspaceTreeNode);
((BrowserTreeNode) getRoot()).add(searchResultsTreeNode);
// Create a new root abstract tree node.
workspaceAbstractTreeNode = new RootAbstractTreeNode(workspaceTreeNode.getName());
// Use a weak hashmap for the entities so that GemEntities which are no longer
// loaded are automatically cleared from the map.
entityToTreeNodeMap = new WeakHashMap<GemEntity, GemTreeNode>();
// visibility predicate can never be null
visibilityPolicy = FeatureVisibilityPolicy.getDefault();
}
/**
* Returns the visibility policy used for this browser tree model. The return value can never be null.
* @return FeatureVisibilityPolicy
*/
public FeatureVisibilityPolicy getFeatureVisibilityPolicy() {
return visibilityPolicy;
}
/**
* Sets the visibility policy used for this browser tree model. A visibility policy is a predicate
* function that determines in a module or a gem is visible in the browser tree. If
* <code>null</code> is used as an argument, then the default visibility predicate is used.
* @param predicate
*/
public void setFeatureVisibilityPolicy(FeatureVisibilityPolicy predicate) {
if (predicate == null) {
visibilityPolicy = FeatureVisibilityPolicy.getDefault();
} else {
visibilityPolicy = predicate;
}
}
/**
* Return whether a given module is visible from the perspective from which the tree was last populated.
* @param moduleName
* @return whether the module is visible from the perspective from which the tree was last populated.
*/
public boolean isVisibleModule(ModuleName moduleName) {
if (perspective == null) {
return false;
}
return perspective.isVisibleModule(moduleName);
}
/**
* Return whether a given namespace contains modules visible from the perspective from which the tree was last populated.
* @param namespaceName
* @return whether the namespace contains modules visible from the perspective from which the tree was last populated.
*/
public boolean doesNamespaceContainVisibleModules(ModuleName namespaceName) {
if (perspective == null) {
return false;
}
final MetaModule workingModule = perspective.getWorkingModule();
if (workingModule == null) {
return false;
}
if (namespaceName.isProperPrefixOf(workingModule.getName())) {
return true;
}
final ModuleTypeInfo moduleTypeInfo = workingModule.getTypeInfo();
final int n = moduleTypeInfo.getNImportedModules();
for (int i = 0; i < n; i++) {
if (namespaceName.isProperPrefixOf(moduleTypeInfo.getNthImportedModule(i).getModuleName())) {
return true;
}
}
return false;
}
/**
* Return whether a given gem is visible from the perspective from which the tree was last populated.
* @param gemEntity
* @return whether the gem is visible from the perspective from which the tree was last populated.
*/
public boolean isVisibleGem(GemEntity gemEntity) {
ModuleName moduleName = gemEntity.getName().getModuleName();
return isVisibleModule(moduleName);
}
/**
* Return whether a given module is the working module (according to the perspective when the tree was last populated).
* @param moduleName the name of the module.
* @return whether the name is the name of the working module.
*/
boolean isWorkingModule(ModuleName moduleName) {
if (perspective == null) {
return false;
}
ModuleName workingModuleName = perspective.getWorkingModuleName();
return workingModuleName.equals(moduleName);
}
/**
* Get a GemTreeNode corresponding to a given entity.
* This is usually, but NOT ALWAYS, the only node that corresponds to the entity.
* Some entities will have multiple associated tree nodes; this function returns one
* chosen arbitrarily.
* @param newGemEntity the entity to look up
* @return GemTreeNode the corresponding node.
*/
public GemTreeNode getTreeNode(GemEntity newGemEntity) {
return entityToTreeNodeMap.get(newGemEntity);
}
/**
* Returns all the entities in the leaf nodes (GemTreeNodes) descended from the specified tree node.
* @param treeNode the tree node to analyze.
* @return List of all entities in GemTreeNodes descended from the current node.
*/
private List<GemEntity> getAllEntities(BrowserTreeNode treeNode) {
Set<GemEntity> entitySet = new HashSet<GemEntity>();
getAllEntitiesHelper(treeNode, entitySet);
return new ArrayList<GemEntity>(entitySet);
}
/**
* Adds all the entities in the leaf nodes (GemTreeNodes) descended from the specified tree node to a
* provided Set.
* @param treeNode the tree node to analyze
* @param entitySet the Set to add the descended entities to
*/
private void getAllEntitiesHelper(BrowserTreeNode treeNode, Set<GemEntity> entitySet) {
if (treeNode instanceof GemTreeNode) {
entitySet.add((GemEntity)treeNode.getUserObject());
return;
}
int numChildren = treeNode.getChildCount();
for (int i = 0; i < numChildren; i++) {
BrowserTreeNode childNode = (BrowserTreeNode)treeNode.getChildAt(i);
if (childNode instanceof GemTreeNode) {
entitySet.add((GemEntity)childNode.getUserObject());
} else {
// must be a node that holds other nodes
getAllEntitiesHelper(childNode, entitySet);
}
}
}
/**
* Categorize all entities descending from this node according to a given categorizer.
* @param selectedNode The node below which all entities will be categorized.
* @param categorizer The categorizer to use to categorize the gementities.
*/
private void categorizeByX(BrowserTreeNode selectedNode, GemCategorizer<?> categorizer) {
categorizeByX(selectedNode, categorizer, new DefaultCategoryNodeProvider());
}
/**
* Categorize all entities descending from this node according to a given categorizer, and producing category
* nodes according to a given category node provider.
* @param selectedNode The node below which all entities will be categorized.
* @param categorizer The categorizer to use to categorize the gementities.
* @param provider The CategoryNodeProvider to provide custom nodes to represent the categories.
*/
private void categorizeByX(BrowserTreeNode selectedNode, GemCategorizer<?> categorizer, CategoryNodeProvider provider) {
categorizeByX(selectedNode, categorizer, provider, false);
}
/**
* Categorize all entities descending from this node according to a given categorizer, and producing category
* nodes according to a given category node provider.
* @param selectedNode The node below which all entities will be categorized.
* @param categorizer The categorizer to use to categorize the gementities.
* @param provider The CategoryNodeProvider to provide custom nodes to represent the categories.
* @param reuseNodes whether to try to reuse child nodes when re-categorizing a given node.
*/
private void categorizeByX(final BrowserTreeNode selectedNode, GemCategorizer<?> categorizer, CategoryNodeProvider provider, boolean reuseNodes) {
ModuleTypeInfo workingModuleTypeInfo = perspective.getWorkingModuleTypeInfo();
// Before categorizing sort gems alphabetically so they appear
// in alphabetical order inside their categories.
GemComparatorSorter sorter = new GemUnqualifiedNameCaseInsensitiveSorter();
List<GemEntity> childList = sorter.getSortedList(getAllEntities(selectedNode));
// Attach the categorizer info to this node.
selectedNode.setCategorizer(categorizer, provider);
// Categorize the children list.
List<GemCategory> categories = categorizer.formCategories(childList);
// Construct the map for reuse of nodes. Populate if necessary.
BrowserTreeNode.NodeReuseInfo nodeReuseInfo = reuseNodes ? selectedNode.getChildNameInfo() : null;
if (selectedNode instanceof GemDrawer) {
// Now, remove all the children of the selected node, and re-insert them into categories.
selectedNode.removeAllGemNodeDescendantsInGivenDrawerOnly((GemDrawer)selectedNode);
removeAllChildrenExceptForVisibleDescendantDrawers(selectedNode);
} else {
// Now, remove all the children of the selected node, and re-insert them into categories.
selectedNode.removeAllGemNodeDescendants();
selectedNode.removeAllChildren();
}
int nCategories = categories.size();
for (int i = 0; i < nCategories; i++) {
GemCategory category = categories.get(i);
// Create the category node.
BrowserTreeNode categoryNode = selectedNode.addNewCategoryNode(category, nodeReuseInfo);
// Add the entity nodes to the category node.
List<GemEntity> categoryItems = category.getCategoryItems();
int numEntities = categoryItems.size();
for (int j = 0; j < numEntities; j++) {
GemEntity gemEntity = categoryItems.get(j);
if (selectedNode instanceof SearchResultRootTreeNode) {
categoryNode.add(new SearchResultGemTreeNode(gemEntity, workingModuleTypeInfo));
} else {
GemTreeNode node = new GemTreeNode(gemEntity, workingModuleTypeInfo);
categoryNode.add(node);
// This node is guaranteed to be in the tree, so set it to be the node associated with the gemEntity
entityToTreeNodeMap.put(gemEntity, node);
}
}
}
// Notify the model about the changes.
invokeOnEventDispatchThread(new Runnable() {
public void run() {
nodeStructureChanged(selectedNode);
}
});
}
/**
* Starting from the currently selected node, we categorize the currently
* selected node's children according to the child's arity.
* Note: Categorize means that the children will be put into different folders based on its arity.
* @param selectedNode The node below which all entities will be categorized.
*/
public void categorizeByArity(BrowserTreeNode selectedNode) {
// Categorize by arity
GemCategorizer<Integer> categorizer = new GemCategorizer<Integer>() {
@Override
public List<GemCategory.CategoryKey<Integer>> getCategoryKeyList(GemEntity gemEntity) {
return Collections.singletonList(new GemCategory.CategoryKey<Integer>(Integer.valueOf(gemEntity.getTypeArity())));
}
@Override
public int compareKey(CategoryKey<Integer> key1, CategoryKey<Integer> key2) {
return key1.getValue().compareTo(key2.getValue());
}
};
categorizeByX(selectedNode, categorizer);
}
/**
* Starting from the currently selected node, we categorize the currently
* selected node's children according to the child's gem type (type signature).
* Note: Categorize means that the children will be put into different folders based on their type.
* @param selectedNode The node below which all entities will be categorized.
*/
public void categorizeByGemType(BrowserTreeNode selectedNode) {
// Categorize by type expr
GemCategorizer<String> categorizer = new GemCategorizer<String>() {
private final ScopedEntityNamingPolicy namingPolicy = new UnqualifiedUnlessAmbiguous(perspective.getWorkingModuleTypeInfo());
@Override
public List<GemCategory.CategoryKey<String>> getCategoryKeyList(GemEntity gemEntity) {
return Collections.singletonList(new GemCategory.CategoryKey<String>(gemEntity.getTypeExpr().toString(namingPolicy)));
}
// Sort categories alphabetically
@Override
public int compareKey(GemCategory.CategoryKey<String> key1, GemCategory.CategoryKey<String> key2) {
return key1.getValue().compareTo(key2.getValue());
}
};
categorizeByX(selectedNode, categorizer);
}
/**
* Starting from the currently selected node, we categorize the currently
* selected node's children according to the child's output type.
* Note: Categorize means that the children will be put into different folders based on its arity.
* @param selectedNode The node below which all entities will be categorized.
*/
public void categorizeByOutput(BrowserTreeNode selectedNode) {
// Categorize by output
GemCategorizer<String> categorizer = new GemCategorizer<String>() {
private final ScopedEntityNamingPolicy namingPolicy = new UnqualifiedUnlessAmbiguous(perspective.getWorkingModuleTypeInfo());
@Override
public List<GemCategory.CategoryKey<String>> getCategoryKeyList(GemEntity gemEntity) {
return Collections.singletonList(new GemCategory.CategoryKey<String>(gemEntity.getTypeExpr().getResultType().toString(namingPolicy)));
}
// Sort categories alphabetically
@Override
public int compareKey(GemCategory.CategoryKey<String> key1, GemCategory.CategoryKey<String> key2) {
return key1.getValue().compareTo(key2.getValue());
}
};
categorizeByX(selectedNode, categorizer);
}
/**
* Starting from the currently selected node, we categorize the currently
* selected node's children according to the child's input type.
* Note: Categorize means that the children will be put into different folders based on their input type.
* @param selectedNode The node below which all entities will be categorized.
*/
public void categorizeByInput(BrowserTreeNode selectedNode) {
// Categorize by input
GemCategorizer<String> categorizer = new GemCategorizer<String>() {
// the category key to use for gems with no inputs
private final GemCategory.CategoryKey<String> noInputsKey = new GemCategory.CategoryKey<String>(BrowserMessages.getString("GB_NoInputs"));
private final ScopedEntityNamingPolicy namingPolicy = new UnqualifiedUnlessAmbiguous(perspective.getWorkingModuleTypeInfo());
// Each gem belongs to the category of each of its argument types
@Override
public List<GemCategory.CategoryKey<String>> getCategoryKeyList(GemEntity gemEntity) {
int arity = gemEntity.getTypeExpr().getArity();
if(arity == 0) {
return Collections.singletonList(noInputsKey);
}
Set<GemCategory.CategoryKey<String>> categories = new HashSet<GemCategory.CategoryKey<String>>();
for (int i = 0; i < arity; i++) {
String categoryName = gemEntity.getTypeExpr().getTypePieces()[i].toString(namingPolicy);
categories.add(new GemCategory.CategoryKey<String>(categoryName));
}
return new ArrayList<GemCategory.CategoryKey<String>>(categories);
}
// Sort categories alphabetically
@Override
public int compareKey(GemCategory.CategoryKey<String> key1, GemCategory.CategoryKey<String> key2) {
// always sort no inputs to the top before everything else
if (key1.equals(key2)) {
return 0;
}
if (key1.equals(noInputsKey)) {
return -1;
}
if (key2.equals(noInputsKey)) {
return 1;
}
return key1.getValue().compareTo(key2.getValue());
}
};
categorizeByX(selectedNode, categorizer);
}
/**
* Starting from the currently selected node, we categorize the currently
* selected node's children according to the child's module.
* @param selectedNode The node below which all entities will be categorized.
* @param moduleTreeDisplayMode the display mode for the module tree.
*/
public void categorizeByModule(BrowserTreeNode selectedNode, TreeViewDisplayMode moduleTreeDisplayMode) {
// Create the categorizer.
GemCategorizer<GemDrawerCategoryInfo> categorizer = getModuleCategorizer(selectedNode, moduleTreeDisplayMode);
// Provide a custom node for the module.
CategoryNodeProvider provider = new MaterialGemDrawerCategoryNodeProvider(moduleTreeDisplayMode);
categorizeByX(selectedNode, categorizer, provider);
}
/**
* Get a ModuleCategorizer based on the current perspective.
* @param browserTreeNode the tree node for which the categorizer will be provided.
* @param moduleTreeDisplayMode the display mode for the module tree.
* @return a ModuleCategorizer for that node.
*/
private ModuleCategorizer getModuleCategorizer(BrowserTreeNode browserTreeNode, TreeViewDisplayMode moduleTreeDisplayMode) {
// If it's the first level under the workspace root, include all modules.
// Otherwise, only include those modules which actually contain any gems.
List<GemDrawerCategoryInfo> categoriesToInclude = (browserTreeNode == workspaceTreeNode) ? getGemDrawerCategoryInfoList(moduleTreeDisplayMode) : Collections.<GemDrawerCategoryInfo>emptyList();
return new ModuleCategorizer(categoriesToInclude, true, moduleTreeDisplayMode);
}
/**
* @param moduleTreeDisplayMode the display mode for the module tree.
* @return the list of pairs (drawer name, isNamespaceNode), ordered according to the current visibility policy of the vault.
* ie. Visible modules in alphabetical order followed optionally by non-visible modules in alphabetical order.
*/
private List<GemDrawerCategoryInfo> getGemDrawerCategoryInfoList(final TreeViewDisplayMode moduleTreeDisplayMode) {
List<GemDrawerCategoryInfo> drawerNamesList = new ArrayList<GemDrawerCategoryInfo>();
List<MetaModule> visibleModules = new ArrayList<MetaModule>();
CollectionUtils.select(perspective.getVisibleMetaModules(), new MetaModuleListFilter(), visibleModules);
List<MetaModule> hiddenModules = perspective.getInvisibleMetaModules();
// Collect all module names for creating a module name resolver
Set<ModuleName> moduleNames = new HashSet<ModuleName>();
for (final MetaModule visibleModule : visibleModules) {
moduleNames.add(visibleModule.getName());
}
for (final MetaModule hiddenModule: hiddenModules) {
moduleNames.add(hiddenModule.getName());
}
ModuleNameResolver workspaceModuleNameResolver = ModuleNameResolver.make(moduleNames);
// Add the visible modules in alphabetical order.
Collections.sort(visibleModules, getModuleAlphaComparator(workspaceModuleNameResolver, moduleTreeDisplayMode));
for (final MetaModule metaModule : visibleModules) {
drawerNamesList.add(new GemDrawerCategoryInfo(metaModule.getName(), false));
}
// If we are supposed to show all modules then add the invisible ones.
if (showAllModules) {
Collections.sort(hiddenModules, getModuleAlphaComparator(workspaceModuleNameResolver, moduleTreeDisplayMode));
for (final MetaModule module : hiddenModules) {
drawerNamesList.add(new GemDrawerCategoryInfo(module.getName(), false));
}
}
return drawerNamesList;
}
/**
* Starting from the currently selected node, we sort the currently selected node's
* children according to the given sorter.
* @param selectedNode The node below which all entities will be sorted.
* @param sorter the sorter to use.
*/
private void sortByX(final BrowserTreeNode selectedNode, GemComparatorSorter sorter) {
List<GemEntity> childList = getAllEntities(selectedNode);
List<GemEntity> sortedList = sorter.getSortedList(childList);
// Attach the sorter to the selected node.
selectedNode.setSorter(sorter);
// Now, remove all the children of the selected node, and re-insert them in correct order.
selectedNode.removeAllChildren();
// Add the entity nodes in the correct sorting order
int numEntities = sortedList.size();
for (int j = 0; j < numEntities; j++) {
GemEntity gemEntity = sortedList.get(j);
if (selectedNode instanceof SearchResultRootTreeNode) {
selectedNode.add(new SearchResultGemTreeNode(gemEntity, perspective.getWorkingModuleTypeInfo()));
} else {
GemTreeNode node = new GemTreeNode(gemEntity, perspective.getWorkingModuleTypeInfo());
selectedNode.add(node);
// This node is guaranteed to be in the tree, so set it to be the node associated with the gemEntity
entityToTreeNodeMap.put(gemEntity, node);
}
}
// Notify the model about the changes.
invokeOnEventDispatchThread(new Runnable() {
public void run() {
nodeStructureChanged(selectedNode);
}
});
}
/**
* Starting from the currently selected node, we sort the currently selected node's
* children according to the alphabet: A, a, B, b, etc...
* Side Note: This method assumes that the gems will have a name of length greater than 0.
* @param selectedNode The node below which all entities will be sorted.
*/
public void sortByUnqualifiedName(BrowserTreeNode selectedNode) {
sortByX(selectedNode, new GemUnqualifiedNameCaseInsensitiveSorter());
}
/**
* Starting from the currently selected node, we sort the currently selected node's
* children according to its form (Data Constructor, Supercombinator, Class Method, ...)
* @param treeNode the node below which all entities will be sorted.
*/
public void sortByForm(BrowserTreeNode treeNode) {
sortByX(treeNode, new GemFormSorter());
}
/**
* Determines whether modules not visible from the current perspective should be shown
* in the tree view. If you change this property you have to reload the module.
*
* @param showAllModules if true all modules will be shown, if false only visible modules
*/
public void setShowAllModules(boolean showAllModules) {
this.showAllModules = showAllModules;
}
/**
* Determines whether modules not visible from the current perspective should be shown
* in the tree view. This value may not reflect what is currently shown in the view
* since the model needs to be reloaded if the property is changed.
*
* @return boolean true if all modules should be shown, false if only visible modules should be shown
*/
public boolean getShowAllModules() {
return showAllModules;
}
/**
* Determines whether only public gems should be displayed. The value may not reflect what
* is currently shown in the view, since the model needs to be reloaded if the property is changed.
*
* @param showPublicGemsOnly true if only public gems should be shown, false if all gems
* (public, private, protected)should be shown.
*/
public void setShowPublicGemsOnly(boolean showPublicGemsOnly) {
this.showPublicGemsOnly = showPublicGemsOnly;
}
/**
* Determines whether only public gems should be displayed. The value may not reflect what
* is currently shown in the view, since the model needs to be reloaded if the property is changed.
*
* @return boolean true is only public gems should be shown, false if all gems
* (public, private, protected)should be shown.
*/
public boolean getShowPublicGemsOnly() {
return showPublicGemsOnly;
}
/**
* Reverts the browser tree model to the "original" structure.
* Drawers are organized in import-dependent order - the order depends on the order of imports into the current drawer
* (based on the perspective at the time the model was populated).
* Entities are organized in alphabetical order within drawers.
* @param moduleTreeDisplayMode the display mode for the module tree.
*/
public void arrangeAsDefaultTreeStructure(TreeViewDisplayMode moduleTreeDisplayMode) {
// Gems appear in alphabetical order on their unqualified name.
sortByUnqualifiedName(workspaceTreeNode);
// Categorize the first level by module.
categorizeByModule(workspaceTreeNode, moduleTreeDisplayMode);
}
/**
* Returns the tree node that represents the "Search Results" node.
* @return the search results tree node
*/
public BrowserTreeNode getSearchResultsNode () {
return searchResultsTreeNode;
}
/**
* Returns the tree node that represents the "Workspace" node.
* @return the workspace node
*/
public BrowserTreeNode getWorkspaceNode () {
return workspaceTreeNode;
}
/**
* Sets the name of the Workspace node in the tree to include the name of
* the current workspace in square brackets.
* @param name the name to have in square brackets
*/
public void setWorkspaceNodeName(String name) {
String fullName = BrowserMessages.getString("GB_WorkspaceNodeName") + " [" + name + "]";
getWorkspaceNode().setUserObject(fullName);
workspaceAbstractTreeNode.setName(fullName);
}
/**
* Performs a sub-string search on all node names and type expressions in the model
* and inserts nodes for matching gems into the search results node. The search is
* case insensitive.
* @param searchString the string to search for
*/
public void doSearch (String searchString) {
searchResultsTreeNode.removeAllChildren();
// If the search string is empty we don't do anything
if (searchString.trim().equals("")) {
nodeStructureChanged(searchResultsTreeNode);
lastSearchString = null;
return;
}
// We want to search case insensitively.
Pattern searchPattern = Pattern.compile(WildcardPatternMatcher.wildcardPatternToRegExp(searchString), Pattern.CASE_INSENSITIVE);
// Keep a list of all the matching gem nodes.
List<GemEntity> resultList = new ArrayList<GemEntity>();
for (final GemEntity gemEntity : entityToTreeNodeMap.keySet()) {
if (!showAllModules && !isVisibleGem(gemEntity)) {
continue;
}
String nameString = gemEntity.getName().getUnqualifiedName();
String typeExprQualifiedString = gemEntity.getTypeExpr().toString();
String typeExprUnqualifiedString = gemEntity.getTypeExpr().toString(ScopedEntityNamingPolicy.UNQUALIFIED);
// we use find() instead of matches() on the Matcher since we want to simply find a matching subsequence
// and not whether the *whole* string matches the pattern.
if (searchPattern.matcher(nameString).find() ||
searchPattern.matcher(typeExprUnqualifiedString).find() ||
searchPattern.matcher(typeExprQualifiedString).find()) {
resultList.add(gemEntity);
}
}
GemComparatorSorter sorter = new GemUnqualifiedNameCaseInsensitiveSorter();
List<GemEntity> sortedList = sorter.getSortedList(resultList);
// Add the matching gem nodes to the results node.
for (final GemEntity gemEntity : sortedList) {
searchResultsTreeNode.add(new SearchResultGemTreeNode(gemEntity, perspective.getWorkingModuleTypeInfo()));
}
nodeStructureChanged(searchResultsTreeNode);
lastSearchString = searchString;
}
public String getSearchString() {
return lastSearchString;
}
/**
* Reloads the model from the perspective it was originally populated from and uses the
* same parameters as when originally populated. Tree structure state is not remembered
* by the model. Use GemBrowser.refresh() to reload the model and maintain the tree structure.
*
* @see BrowserTreeModel#populate
* @see org.openquark.gems.client.browser.GemBrowser#refresh()
*/
@Override
public void reload() {
// Clear the existing drawers and nodes
clearDrawers();
entityToTreeNodeMap.clear();
// Repopulate the model with the previous perspective.
populate (perspective, showAllModules, TreeViewDisplayMode.FLAT_ABBREVIATED); // this choice of TreeViewDisplayMode shouldn't matter at all, so we just choose the default...
// Update node categorizers on the model.
updateModuleCategorizers();
// Repeat the previous search to restore the search node.
// Some new gems might have been added that need to be included in the search.
if (lastSearchString != null) {
doSearch (lastSearchString);
}
// Apply any categorization or sorting that is associated with the search node.
GemComparatorSorter sorter = searchResultsTreeNode.getSorter();
if (sorter != null) {
sortByX(searchResultsTreeNode, sorter);
}
GemCategorizer<?> categorizer = searchResultsTreeNode.getCategorizer();
if (categorizer != null) {
categorizeByX(searchResultsTreeNode, categorizer, searchResultsTreeNode.getNodeProvider());
}
super.reload();
}
/**
* Update any module categorizers held by nodes in the tree model to hold on to the current list of modules.
* This method should be invoked if the modules to display in the tree are changed.
*/
private void updateModuleCategorizers() {
for (Enumeration<TreeNode> nodeEnum = UnsafeCast.unsafeCast(((BrowserTreeNode)getRoot()).breadthFirstEnumeration()); nodeEnum.hasMoreElements(); ) {
BrowserTreeNode nextNode = (BrowserTreeNode)nodeEnum.nextElement();
GemCategorizer<?> categorizer = nextNode.getCategorizer();
if (categorizer != null) {
if (categorizer instanceof ModuleCategorizer) {
categorizer = getModuleCategorizer(nextNode, ((ModuleCategorizer)categorizer).getModuleTreeDisplayMode());
}
// recategorize, re-using previously existing categories if possible..
categorizeByX(nextNode, categorizer, nextNode.getNodeProvider(), true);
}
}
}
/**
* Populate the model with entities.
* Note: any modules which already exist both in the tree and in the program will be replaced by the program definition.
* @param perspective the perspective from which entities are gathered.
* @param showAllModules whether modules not visible from the given perspective should also be visible.
* @param moduleTreeDisplayMode the display mode for the module tree.
*/
public void populate(final Perspective perspective, final boolean showAllModules, final TreeViewDisplayMode moduleTreeDisplayMode) {
this.perspective = perspective;
this.showAllModules = showAllModules;
// A set of all GemEntities in the modules.
Set<GemEntity> allEntities = new LinkedHashSet<GemEntity>();
// Get all visible modules for this perspective.
List<MetaModule> visibleModules = new ArrayList<MetaModule>();
CollectionUtils.select(perspective.getVisibleMetaModules(), new MetaModuleListFilter(), visibleModules);
List<MetaModule> hiddenModules = perspective.getInvisibleMetaModules();
// Collect all module names for creating a module name resolver
Set<ModuleName> moduleNames = new HashSet<ModuleName>();
for (final MetaModule metaModule : visibleModules) {
moduleNames.add(metaModule.getName());
}
for (final MetaModule metaModule : hiddenModules) {
moduleNames.add(metaModule.getName());
}
// Refresh the module name resolver with the current set of module names
workspaceModuleNameResolver = ModuleNameResolver.make(moduleNames);
Collections.sort(visibleModules, getModuleAlphaComparator(workspaceModuleNameResolver, moduleTreeDisplayMode));
// Add drawers for each of the modules.
for (int i = 0, size = visibleModules.size(); i < size; i++) {
MetaModule module = visibleModules.get(i);
ModuleName moduleName = module.getName();
Set<GemEntity> visibleEntitySet = perspective.getVisibleGemEntities(module);
addDrawer(moduleName, visibleEntitySet);
allEntities.addAll(visibleEntitySet);
}
if (showAllModules) {
// If we are supposed to show all modules then add the invisible ones.
Collections.sort(hiddenModules, getModuleAlphaComparator(workspaceModuleNameResolver, moduleTreeDisplayMode));
// Add drawers for each of the modules.
for (final MetaModule module : hiddenModules) {
ModuleName moduleName = module.getName();
// Create a set with all the public entities from the module.
Set<GemEntity> publicEntitySet = perspective.getVisibleGemEntities(module);
addDrawer(moduleName, publicEntitySet);
allEntities.addAll(publicEntitySet);
}
}
// Make sure tree nodes for all the entities exist.
for (final GemEntity gemEntity : allEntities) {
if (entityToTreeNodeMap.containsKey(gemEntity)) {
continue; // the entity is not new.
}
GemTreeNode newNode = new GemTreeNode(gemEntity, perspective.getWorkingModuleTypeInfo());
entityToTreeNodeMap.put(gemEntity, newNode);
}
// Sort the set into a list by unqualified name. This way all the gems
// appear in alphabetical order in whatever category they fall into.
GemComparatorSorter sorter = new GemUnqualifiedNameCaseInsensitiveSorter();
List<GemEntity> sortedList = sorter.getSortedList(allEntities);
// Fill up the workspace node with the entities
fillTreeStructure(workspaceTreeNode, sortedList);
invokeOnEventDispatchThread(new Runnable() {
public void run() {
nodeStructureChanged(workspaceTreeNode);
}
});
}
/**
* Needed for Swing consistency. Check which thread the event is currently on.
* If already on the AWT event thread, just run; else place the event on the AWT event thread queue.
* @param runnable the event that needs to run on the AWT event thread
*/
static void invokeOnEventDispatchThread(final Runnable runnable) {
if (EventQueue.isDispatchThread()) {
runnable.run();
} else {
SwingUtilities.invokeLater(runnable);
}
}
/**
* Helper method to populate the model with an abstract tree node for a gem drawer.
* @param moduleName the name of the drawer.
* @param entitySet The gem entities which "belong" under this drawer.
* Nodes will be created for these gem entities and will be put into the drawer named drawerName.
*/
private void addDrawer(ModuleName moduleName, Set<GemEntity> entitySet) {
// Get the corresponding drawer. Create and add it if it doesn't already exist.
GemDrawerAbstractTreeNode drawerNode = (GemDrawerAbstractTreeNode)workspaceAbstractTreeNode.getChild(getGemDrawerNameForModule(moduleName));
if (drawerNode == null) {
drawerNode = new MaterialGemDrawerAbstractTreeNode(moduleName);
workspaceAbstractTreeNode.addChild(drawerNode);
}
// Make sure the drawer is empty
drawerNode.clearChildren();
// Now add back new EntityNodes for the entities.
for (final GemEntity gemEntity : entitySet) {
EntityAbstractTreeNode entityNode = new EntityAbstractTreeNode(gemEntity);
drawerNode.addChild(entityNode);
}
}
/**
* Returns the actual name of a gem drawer for a module.
* @param moduleName the name of the module.
* @return the corresponding gem drawer name.
*/
static String getGemDrawerNameForModule(ModuleName moduleName) {
return moduleName.toSourceText();
}
/**
* @param workspaceModuleNameResolver the module name resolver to use to generate an appropriate module name
* @param moduleTreeDisplayMode
* @return the moduleAlphaComparator
*/
private static Comparator<MetaModule> getModuleAlphaComparator(final ModuleNameResolver workspaceModuleNameResolver, final TreeViewDisplayMode moduleTreeDisplayMode) {
return new Comparator<MetaModule>() {
public int compare(final MetaModule m1, final MetaModule m2) {
final String gemDrawerNameForModule1 = ModuleNameDisplayUtilities.getDisplayNameForModuleInTreeView(m1.getName(), workspaceModuleNameResolver, moduleTreeDisplayMode);
final String gemDrawerNameForModule2 = ModuleNameDisplayUtilities.getDisplayNameForModuleInTreeView(m2.getName(), workspaceModuleNameResolver, moduleTreeDisplayMode);
return gemDrawerNameForModule1.compareTo(gemDrawerNameForModule2);
}
};
}
/**
* Fill an empty TreeNode structure with gems, using any previously-used policies.
* Preconditions: subtree is empty, entities map to GemTreeNodes
* @param rootNode the root of the subtree to fill
* @param entityList the gems that should go into the subtree
*/
private void fillTreeStructure(BrowserTreeNode rootNode, List<GemEntity> entityList) {
// Remove all old children of this node
if (rootNode instanceof GemDrawer) {
removeAllChildrenExceptForVisibleDescendantDrawers(rootNode);
} else {
rootNode.removeAllChildren();
}
// Apply any sorter associated with this node
GemSorter sorter = rootNode.getSorter();
if (sorter != null) {
entityList = sorter.getSortedList(entityList);
}
// Apply any categorization associated with this node
GemCategorizer<?> categorizer = rootNode.getCategorizer();
if (categorizer != null) {
List<GemCategory> categoryList = categorizer.formCategories(entityList);
// Iterate over the categories
int nCategories = categoryList.size();
for (int i = 0; i < nCategories; i++) {
GemCategory category = categoryList.get(i);
// Special case handling for gem drawer categories.
// Don't show hidden modules if we're not supposed to.
if (rootNode.getNodeProvider() instanceof MaterialGemDrawerCategoryNodeProvider && !showAllModules) {
GemDrawerCategoryInfo pair = (GemDrawerCategoryInfo)(category.getCategoryKey()).getValue();
if (!isVisibleModule(pair.getModuleName())) {
continue;
}
}
BrowserTreeNode categoryNode = rootNode.addNewCategoryNode(category);
// Recurse into that node.
fillTreeStructure(categoryNode, category.getCategoryItems());
}
} else {
// No categorizer.
// This means this level isn't categorized - all entities go in here.
for (final GemEntity gemEntity : entityList) {
// display all gems, else only add the public gems
if (!showPublicGemsOnly || gemEntity.getScope().isPublic()){
rootNode.add(getTreeNode(gemEntity));
}
}
}
}
/**
* Removes all children except for those whose descendants include visible drawers.
* @param rootNode the root of the tree to process.
*/
private void removeAllChildrenExceptForVisibleDescendantDrawers(BrowserTreeNode rootNode) {
List<GemDrawer> subDrawersToKeep = new ArrayList<GemDrawer>();
rootNode.collectAllGemDrawerDescendants(subDrawersToKeep);
for (Iterator<GemDrawer> it = subDrawersToKeep.iterator(); it.hasNext(); ) {
if (!isDrawerInAbstractTree(it.next().getModuleName().toSourceText())) {
it.remove();
}
}
for (int i = rootNode.getChildCount()-1; i >= 0; i--) {
final TreeNode child = rootNode.getChildAt(i);
if (child instanceof GemDrawer) {
// we do not remove the child if it, or a descendant of it, is a drawer that is supposed to be visible
boolean shouldRemove = true;
for (int j = 0, n = subDrawersToKeep.size(); j < n; j++) {
if (((BrowserTreeNode)child).isNodeDescendant(subDrawersToKeep.get(j))) {
shouldRemove = false;
}
}
if (shouldRemove) {
rootNode.remove(i);
}
} else {
rootNode.remove(i);
}
}
}
/**
* Clear all gem drawers from the workspace.
*/
public void clearDrawers() {
workspaceAbstractTreeNode.clearChildren();
}
/**
* @return the perspective the model was populated with
*/
Perspective getPerspective() {
return perspective;
}
/**
* Refreshes with a new perspective.
* @param perspective the new perspective
*/
public void setPerspectiveAndReload(Perspective perspective) {
this.perspective = perspective;
reload();
}
/**
* Returns whether the given drawer is in the abstract tree.
* @param drawerName the name of the drawer.
* @return true if the drawer is in the abstract tree (i.e. it will be displayed), false otherwise.
*/
private boolean isDrawerInAbstractTree(final String drawerName) {
return (workspaceAbstractTreeNode.getChild(drawerName) != null);
}
}