/*
* Copyright 2003-2017 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package jetbrains.mps.ide.findusages.view.treeholder.tree;
import jetbrains.mps.icons.MPSIcons;
import jetbrains.mps.ide.findusages.CantLoadSomethingException;
import jetbrains.mps.ide.findusages.CantSaveSomethingException;
import jetbrains.mps.ide.findusages.IExternalizeable;
import jetbrains.mps.ide.findusages.model.CategoryKind;
import jetbrains.mps.ide.findusages.model.SearchResult;
import jetbrains.mps.ide.findusages.model.SearchResults;
import jetbrains.mps.ide.findusages.view.treeholder.tree.nodedatatypes.AbstractResultNodeData;
import jetbrains.mps.ide.findusages.view.treeholder.tree.nodedatatypes.BaseNodeData;
import jetbrains.mps.ide.findusages.view.treeholder.tree.nodedatatypes.CategoryNodeData;
import jetbrains.mps.ide.findusages.view.treeholder.tree.nodedatatypes.MainNodeData;
import jetbrains.mps.ide.findusages.view.treeholder.tree.nodedatatypes.ModelNodeData;
import jetbrains.mps.ide.findusages.view.treeholder.tree.nodedatatypes.ModuleNodeData;
import jetbrains.mps.ide.findusages.view.treeholder.tree.nodedatatypes.NodeNodeData;
import jetbrains.mps.ide.findusages.view.treeholder.tree.nodedatatypes.PresentationContext;
import jetbrains.mps.ide.findusages.view.treeholder.tree.nodedatatypes.ResultsNodeData;
import jetbrains.mps.ide.findusages.view.treeholder.tree.nodedatatypes.SearchedNodesNodeData;
import jetbrains.mps.ide.findusages.view.treeholder.treeview.INodeRepresentator;
import jetbrains.mps.ide.findusages.view.treeholder.treeview.path.PathItem;
import jetbrains.mps.ide.findusages.view.treeholder.treeview.path.PathItemRole;
import jetbrains.mps.ide.findusages.view.treeholder.treeview.path.PathProvider;
import jetbrains.mps.openapi.navigation.ProjectPaneNavigator;
import jetbrains.mps.project.Project;
import jetbrains.mps.util.Pair;
import org.jdom.Element;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.mps.openapi.language.SLanguage;
import org.jetbrains.mps.openapi.model.SModel;
import org.jetbrains.mps.openapi.model.SModelReference;
import org.jetbrains.mps.openapi.model.SNode;
import org.jetbrains.mps.openapi.model.SNodeReference;
import org.jetbrains.mps.openapi.module.SModule;
import org.jetbrains.mps.openapi.module.SModuleReference;
import org.jetbrains.mps.openapi.module.SRepository;
import javax.swing.Icon;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
public class DataTree implements IExternalizeable, IChangeListener {
private DataNode myTreeRoot = createTreeRoot();
private final List<IChangeListener> myListeners = new ArrayList<IChangeListener>(2);
private final DataTreeChangesNotifier myChangesNotifier;
//this is only used in 3.2 to make rebuild faster in case of many nodes.
//in 3.3 it will be fixed by introducing path providers
//this cache is only alive during read action in build() method
private Map<Pair<DataNode, Object>, DataNode> myRebuildCache;
public DataTree(@NotNull DataTreeChangesNotifier changeDispatch) {
myChangesNotifier = changeDispatch;
}
public DataNode getTreeRoot() {
return myTreeRoot;
}
//----EXCLUSION/EXPANSION----
public void setExcluded(Set<DataNode> nodes, boolean value) {
for (DataNode node : nodes) {
setExcludedRecursively(nodes, node, value);
}
checkExcluded();
notifyChangeListeners();
}
//doNotProcess is needed as there might be many pairs of nodes in the list, one of which is a child of another
private void setExcludedRecursively(Set<DataNode> doNotProcess, DataNode node, boolean value) {
node.getData().setExcluded(value);
for (DataNode child : node.getChildren()) {
if (doNotProcess.contains(child)) continue;
setExcludedRecursively(doNotProcess, child, value);
}
}
private void checkExcluded() {
checkNodeExcluded(myTreeRoot);
}
private void checkNodeExcluded(DataNode node) {
if (node.getChildren().size() == 0) {
return;
}
for (DataNode child : node.getChildren()) {
checkNodeExcluded(child);
}
boolean allChildrenExcluded = true;
for (DataNode child : node.getChildren()) {
allChildrenExcluded = allChildrenExcluded && child.getData().isExcluded();
}
node.getData().setExcluded(allChildrenExcluded);
}
//----DATA QUERY----
public Set<SModel> getIncludedModels(SRepository repository) {
return getResultsNode().getIncludedModels(repository);
}
public Set<SModel> getAllModels(SRepository repository) {
return getResultsNode().getAllModels(repository);
}
public List<SNodeReference> getIncludedResultNodes() {
return getResultsNode().getIncludedResultNodes();
}
public List<SNodeReference> getAllResultNodes() {
return getResultsNode().getAllResultNodes();
}
private DataNode getResultsNode() {
return myTreeRoot.getChildren().get(1);
}
//----CONTENT MANAGEMENT----
public void setContents(SearchResults results, INodeRepresentator nodeRepresentator) {
setContents(build(results, nodeRepresentator));
}
protected void setContents(DataNode root) {
myTreeRoot = root;
stopListening();
startListening();
notifyChangeListeners();
}
private void startListening() {
HashSet<SNodeReference> nodes = new HashSet<SNodeReference>();
HashSet<SModelReference> models = new HashSet<SModelReference>();
HashSet<SModuleReference> modules = new HashSet<SModuleReference>();
nodes.addAll(Arrays.asList(myTreeRoot.getNodeDataStream().filter(nd -> nd instanceof NodeNodeData).map(
nd -> ((NodeNodeData) nd).getNodePointer()).toArray(SNodeReference[]::new)));
models.addAll(Arrays.asList(myTreeRoot.getNodeDataStream().filter(nd -> nd instanceof ModelNodeData).map(
nd -> ((ModelNodeData) nd).getModelReference()).toArray(SModelReference[]::new)));
modules.addAll(Arrays.asList(myTreeRoot.getNodeDataStream().filter(nd -> nd instanceof ModuleNodeData).map(
nd -> ((ModuleNodeData) nd).getModuleReference()).toArray(SModuleReference[]::new)));
myChangesNotifier.trackNodes(this, nodes);
myChangesNotifier.trackModels(this, models);
myChangesNotifier.trackModules(this, modules);
}
private void stopListening() {
myChangesNotifier.unregister(this);
}
public void dispose() {
stopListening();
}
//----TREE BUILD STUFF----
private static DataNode createTreeRoot() {
return new DataNode(new MainNodeData(PathItemRole.ROLE_MAIN_ROOT));
}
private DataNode build(final SearchResults<?> results, final INodeRepresentator nodeRepresentator) {
myRebuildCache = new HashMap<Pair<DataNode, Object>, DataNode>();
DataNode root = createTreeRoot();
DataNode nodesRoot = new DataNode(new SearchedNodesNodeData(PathItemRole.ROLE_MAIN_SEARCHED_NODES));
for (Object node : results.getAliveNodes()) {
addSearchedNode(nodesRoot, node);
}
root.add(nodesRoot);
DataNode resultsRoot = new DataNode(new ResultsNodeData(PathItemRole.ROLE_MAIN_RESULTS, nodeRepresentator));
for (SearchResult<?> result : results.getAliveResults()) {
addResultWithPresentation(resultsRoot, result, nodeRepresentator);
}
root.add(resultsRoot);
myRebuildCache = null;
return root;
}
private void addSearchedNode(DataNode root, Object node) {
List<PathItem> path = PathProvider.getPathForSearchResult(new SearchResult<Object>(node, SearchedNodesNodeData.CATEGORY_NAME));
createPath(path, root, null, false, null);
}
private void addResultWithPresentation(DataNode root, SearchResult result, INodeRepresentator nodeRepresentator) {
List<PathItem> path = PathProvider.getPathForSearchResult(result);
createPath(path, root, nodeRepresentator, true, result);
}
private void createPath(List<PathItem> path, DataNode parent, @Nullable INodeRepresentator<Object> nodeRepresentator,
boolean results, @Nullable SearchResult result) {
assert !path.isEmpty();
final PathItem pathTail = path.get(path.size() - 1);
final String tailCustomCaption;
if (result != null && nodeRepresentator != null) {
tailCustomCaption = nodeRepresentator.getPresentation(result.getObject());
} else {
tailCustomCaption = null;
}
for (PathItem currentPathItem : path) {
Object currentIdObject = currentPathItem.getIdObject();
final boolean isPathTail = currentPathItem == pathTail;
DataNode next = myRebuildCache.get(new Pair<DataNode, Object>(parent, currentIdObject));
if (next == null) {
PathItemRole creator = currentPathItem.getRole();
BaseNodeData data = null;
final String caption = isPathTail ? tailCustomCaption : null;
if (currentIdObject instanceof SModule) {
data = new ModuleNodeData(creator, caption, ((SModule) currentIdObject).getModuleReference(), isPathTail, results);
} else if (currentIdObject instanceof SModuleReference) {
data = new ModuleNodeData(creator, caption, (SModuleReference) currentIdObject, isPathTail, results);
} else if (currentIdObject instanceof SModelReference) {
data = new ModelNodeData(creator, caption, (SModelReference) currentIdObject, isPathTail, results);
} else if (currentIdObject instanceof SNode) {
data = new NodeNodeData(creator, caption, (SNode) currentIdObject, isPathTail, results);
} else if (currentIdObject instanceof SLanguage) {
final SLanguage l = (SLanguage) currentIdObject;
data = new AbstractResultNodeData(creator, caption == null ? l.getQualifiedName() : caption, "", false, isPathTail, results) {
@Override
protected String createIdObject() {
return l.toString();
}
@Override
public Icon getIcon(PresentationContext presentationContext) {
return MPSIcons.LanguageRuntime;
}
@Override
public void navigate(Project mpsProject, boolean useProjectTree, boolean focus) {
new ProjectPaneNavigator(mpsProject).shallFocus(focus).select(l);
}
};
} else if (currentIdObject instanceof Pair) {
Pair<CategoryKind, String> category = (Pair<CategoryKind, String>) currentIdObject;
data = new CategoryNodeData(creator, category.o1.getName(), category.o2, results, nodeRepresentator);
}
assert data != null : currentIdObject;
next = new DataNode(data);
parent.add(next);
myRebuildCache.put(new Pair<DataNode, Object>(parent, data.getIdObject()), next);
} else {
if (isPathTail) {
next.getData().setIsPathTail_internal(true);
}
}
parent = next;
}
}
//----READ/WRITE STUFF----
@Override
public void read(Element element, Project project) throws CantLoadSomethingException {
myTreeRoot.read(element, project);
}
@Override
public void write(Element element, Project project) throws CantSaveSomethingException {
myTreeRoot.write(element, project);
}
//----LISTENERS STUFF----
public void addChangeListener(IChangeListener listener) {
myListeners.add(listener);
}
public void removeChangeListeners(IChangeListener listener) {
myListeners.remove(listener);
}
public void notifyChangeListeners() {
for (IChangeListener listener : myListeners) {
listener.changed();
}
}
@Override
public void changed() {
notifyChangeListeners();
}
}