package org.angularjs.codeInsight.router;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.Pair;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.psi.*;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.*;
import static org.angularjs.codeInsight.router.Type.state;
import static org.angularjs.codeInsight.router.Type.template;
/**
* @author Irina.Chernushina on 3/9/2016.
*/
public class AngularUiRouterGraphBuilder {
@NotNull private final Project myProject;
private final Map<String, UiRouterState> myStatesMap;
private final Map<VirtualFile, Template> myTemplatesMap;
@Nullable private final RootTemplate myRootTemplate;
private final VirtualFile myKey;
public AngularUiRouterGraphBuilder(@NotNull Project project,
@NotNull Map<String, UiRouterState> statesMap,
@NotNull Map<VirtualFile, Template> templatesMap,
@Nullable RootTemplate rootTemplate, VirtualFile key) {
myProject = project;
myStatesMap = statesMap;
myTemplatesMap = templatesMap;
myRootTemplate = rootTemplate;
myKey = key;
}
public VirtualFile getKey() {
return myKey;
}
public GraphNodesBuilder createDataModel(AngularUiRouterDiagramProvider provider) {
final GraphNodesBuilder nodesBuilder = new GraphNodesBuilder(myStatesMap, myTemplatesMap, myRootTemplate, myKey);
nodesBuilder.build(provider, myProject);
return nodesBuilder;
}
public static class GraphNodesBuilder {
public static final String DEFAULT = "$default";
@NotNull private final Map<String, UiRouterState> myStatesMap;
@NotNull private final Map<VirtualFile, Template> myTemplatesMap;
@Nullable private final RootTemplate myRootTemplate;
private final VirtualFile myKey;
private AngularUiRouterNode myRootNode;
private final Map<String, AngularUiRouterNode> stateNodes = new HashMap<>();
private final Map<String, AngularUiRouterNode> templateNodes = new HashMap<>();
private final Map<Pair<String, String>, DiagramObject> templatePlaceHoldersNodes = new HashMap<>();
private final Map<Pair<String, String>, DiagramObject> viewNodes = new HashMap<>();
private final List<AngularUiRouterEdge> edges = new ArrayList<>();
private final List<AngularUiRouterNode> allNodes = new ArrayList<>();
public GraphNodesBuilder(@NotNull Map<String, UiRouterState> statesMap,
@NotNull Map<VirtualFile, Template> templatesMap, @Nullable RootTemplate rootTemplate, VirtualFile key) {
myStatesMap = statesMap;
myTemplatesMap = templatesMap;
myRootTemplate = rootTemplate;
myKey = key;
}
public AngularUiRouterNode getRootNode() {
return myRootNode;
}
public VirtualFile getKey() {
return myKey;
}
public void build(@NotNull final AngularUiRouterDiagramProvider provider, @NotNull final Project project) {
final DiagramObject rootDiagramObject;
if (myRootTemplate != null) {
myRootNode = getOrCreateTemplateNode(provider, myKey, normalizeTemplateUrl(myRootTemplate.getRelativeUrl()), myRootTemplate.getTemplate());
myRootNode.getIdentifyingElement().setType(Type.topLevelTemplate);
} else {
// todo remove from diagram if not used
final PsiFile psiFile = PsiManager.getInstance(project).findFile(myKey);
rootDiagramObject = new DiagramObject(Type.topLevelTemplate, myKey.getName(), psiFile == null ? null :
SmartPointerManager.getInstance(project).createSmartPsiElementPointer(psiFile));
myRootNode = new AngularUiRouterNode(rootDiagramObject, provider);
}
setParentStates();
for (Map.Entry<String, UiRouterState> entry : myStatesMap.entrySet()) {
final UiRouterState state = entry.getValue();
final DiagramObject stateObject = new DiagramObject(Type.state, state.getName(), state.getPointer());
if (state.getParentName() != null) {
stateObject.setParent(state.getParentName());
}
if (state.getPointer() == null) {
stateObject.addError("Can not find the state definition");
}
final AngularUiRouterNode node = new AngularUiRouterNode(stateObject, provider);
stateNodes.put(state.getName(), node);
final String templateUrl = normalizeTemplateUrl(state.getTemplateUrl());
if (templateUrl != null && !state.hasViews()) {
final AngularUiRouterNode templateNode = getOrCreateTemplateNode(provider, state.getTemplateFile(), templateUrl, null);
edges.add(new AngularUiRouterEdge(templateNode, node, "provides", AngularUiRouterEdge.Type.providesTemplate));
} else if (state.isHasTemplateDefined() && state.getTemplatePointer() != null) {
final PsiElement element = state.getTemplatePointer().getElement();
if (element != null && element.isValid()) {
final AngularUiRouterNode localTemplateNode = createLocalTemplate(element, provider);
edges.add(new AngularUiRouterEdge(localTemplateNode, node, "provides", AngularUiRouterEdge.Type.providesTemplate));
}
}
if (state.hasViews()) {
if (state.isAbstract()) {
stateObject.addWarning("Abstract state can not be instantiated so it makes no sense to define views for it.");
}
else if (templateUrl != null || state.isHasTemplateDefined()) {
stateObject.addWarning("Since 'views' are defined for state, state template information would be ignored.");
}
}
if (state.isHasTemplateDefined() && state.getTemplatePointer() == null) {
stateObject.addNote("Has embedded template definition.");
}
}
for (Map.Entry<String, UiRouterState> entry : myStatesMap.entrySet()) {
final UiRouterState state = entry.getValue();
final AngularUiRouterNode node = stateNodes.get(state.getName());
assert node != null;
final List<UiView> views = state.getViews();
if (views != null && !views.isEmpty()) {
for (UiView view : views) {
final String name = StringUtil.isEmptyOrSpaces(view.getName()) ? DEFAULT : view.getName();
final DiagramObject viewObject = new DiagramObject(Type.view, name, view.getPointer());
viewNodes.put(Pair.create(state.getName(), name), viewObject);
final String template = view.getTemplate();
if (!StringUtil.isEmptyOrSpaces(template)) {
final AngularUiRouterNode templateNode = getOrCreateTemplateNode(provider, view.getTemplateFile(), template, null);
edges.add(new AngularUiRouterEdge(templateNode, node, name + " provides ", AngularUiRouterEdge.Type.providesTemplate).setTargetName(name));
} else if (view.getTemplatePointer() != null) {
final PsiElement element = view.getTemplatePointer().getElement();
if (element != null && element.isValid()) {
final AngularUiRouterNode localTemplateNode = createLocalTemplate(element, provider);
edges.add(new AngularUiRouterEdge(localTemplateNode, node, name + " provides ", AngularUiRouterEdge.Type.providesTemplate).setTargetName(name));
}
}
node.getIdentifyingElement().addChild(viewObject, node);
}
}
}
// views can also refer to different states, so first all state nodes must be created
for (Map.Entry<String, UiRouterState> entry : myStatesMap.entrySet()) {
final UiRouterState state = entry.getValue();
final AngularUiRouterNode node = stateNodes.get(state.getName());
assert node != null;
final List<UiView> views = state.getViews();
if (views != null && !views.isEmpty()) {
for (UiView view : views) {
final String name = StringUtil.isEmptyOrSpaces(view.getName()) ? DEFAULT : view.getName();
final DiagramObject viewNode = viewNodes.get(Pair.create(state.getName(), name));
assert viewNode != null;
final Pair<AngularUiRouterNode, String> pair = getParentTemplateNode(state.getName(), view.getName());
if (pair != null && pair.getFirst() != null) {
connectViewOrStateWithPlaceholder(node, name, pair);
}
}
} else {
//find unnamed parent template for view
final Pair<AngularUiRouterNode, String> pair = getParentTemplateNode(state.getName(), "");
if (pair != null && pair.getFirst() != null) {
connectViewOrStateWithPlaceholder(node, DEFAULT, pair);
}
}
}
createStateParentEdges();
final List<AngularUiRouterNode> list = new ArrayList<>();
list.add(myRootNode);
list.addAll(stateNodes.values());
list.addAll(templateNodes.values());
for (AngularUiRouterNode node : list) {
if (!allNodes.contains(node)) allNodes.add(node);
}
}
private AngularUiRouterNode createLocalTemplate(PsiElement element, AngularUiRouterDiagramProvider provider) {
final String name = element.getContainingFile().getName() + " (local)";
final String key = element.getContainingFile().getVirtualFile().getUrl() + ":" + element.getTextRange().getStartOffset();
final Template template = AngularUiRouterDiagramBuilder.readTemplateFromFile(element.getProject(), name, element);
if (!templateNodes.containsKey(key)) {
final DiagramObject templateObject = new DiagramObject(Type.template, name, template.getPointer());
final AngularUiRouterNode templateNode = new AngularUiRouterNode(templateObject, provider);
templateNodes.put(key, templateNode);
putPlaceholderNodes(key, template, templateNode);
}
final AngularUiRouterNode templateNode = templateNodes.get(key);
assert templateNode != null;
templateNode.getIdentifyingElement().setTooltip(key);
return templateNode;
}
private void connectViewOrStateWithPlaceholder(AngularUiRouterNode stateNode, String viewName, Pair<AngularUiRouterNode, String> pair) {
final String placeholderName = pair.getSecond();
//final String placeholderName = StringUtil.isEmptyOrSpaces(pair.getSecond()) ? DEFAULT : pair.getSecond();
String usedTemplateUrl = null;
final Type nodeType = pair.getFirst().getIdentifyingElement().getType();
if (Type.template.equals(nodeType) || Type.topLevelTemplate.equals(nodeType)) {
usedTemplateUrl = pair.getFirst().getIdentifyingElement().getTooltip();
} else if (state.equals(nodeType)) {
final String parentState = pair.getFirst().getIdentifyingElement().getName();
final UiRouterState parentStateObject = myStatesMap.get(parentState);
if (parentStateObject != null) {
if (parentStateObject.hasViews()) {
final List<UiView> parentViews = parentStateObject.getViews();
for (UiView parentView : parentViews) {
if (placeholderName.equals(parentView.getName())) {
usedTemplateUrl = parentView.getTemplate();
break;
}
}
} else if (!StringUtil.isEmptyOrSpaces(parentStateObject.getTemplateUrl())) {
usedTemplateUrl = parentStateObject.getTemplateUrl();
}
}
}
usedTemplateUrl = normalizeTemplateUrl(usedTemplateUrl);
final DiagramObject placeholder = templatePlaceHoldersNodes.get(Pair.create(usedTemplateUrl, placeholderName));
if (placeholder != null && placeholder.getContainer() != null) {
final AngularUiRouterEdge edge = new AngularUiRouterEdge(placeholder.getContainer(), stateNode, viewName + " populates " + placeholderName,
AngularUiRouterEdge.Type.fillsTemplate).setSourceName(placeholderName)
.setTargetName(viewName);
edge.setTargetAnchor(placeholder);
edges.add(edge);
}
}
private void createStateParentEdges() {
for (Map.Entry<String, AngularUiRouterNode> entry : stateNodes.entrySet()) {
final String key = entry.getKey();
final UiRouterState state = myStatesMap.get(key);
if (state != null && state.getParentName() != null) {
final AngularUiRouterNode parentState = stateNodes.get(state.getParentName());
if (parentState != null) {
edges.add(new AngularUiRouterEdge(parentState, entry.getValue(), "", AngularUiRouterEdge.Type.parent));
}
}
}
}
private void setParentStates() {
for (Map.Entry<String, UiRouterState> entry : myStatesMap.entrySet()) {
if (!StringUtil.isEmptyOrSpaces(entry.getValue().getParentName())) continue;
final String key = entry.getKey();
final int dotIdx = key.lastIndexOf('.');
if (dotIdx > 0) {
final String parentKey = key.substring(0, dotIdx);
entry.getValue().setParentName(parentKey);
}
}
}
public List<AngularUiRouterNode> getStateTemplates(@NotNull final AngularUiRouterNode state) {
final List<AngularUiRouterNode> list = new ArrayList<>();
if (!Type.state.equals(state.getIdentifyingElement().getType())) return Collections.emptyList();
for (AngularUiRouterEdge edge : edges) {
if (AngularUiRouterEdge.Type.providesTemplate.equals(edge.getType()) && edge.getTarget().equals(state) &&
template.equals(edge.getSource().getIdentifyingElement().getType())) {
list.add((AngularUiRouterNode)edge.getSource());
}
}
return list;
}
public List<AngularUiRouterNode> getZeroLevelStates() {
final List<AngularUiRouterNode> list = new ArrayList<>();
for (AngularUiRouterNode current : allNodes) {
if (state.equals(current.getIdentifyingElement().getType()) && current.getIdentifyingElement().getParent() == null) {
list.add(current);
}
}
return list;
}
public List<AngularUiRouterNode> getImmediateChildrenStates(@NotNull AngularUiRouterNode node) {
if (myRootNode.equals(node)) return getZeroLevelStates();
final String name = node.getIdentifyingElement().getName();
final List<AngularUiRouterNode> list = new ArrayList<>();
final DiagramObject diagramObject = node.getIdentifyingElement();
if (!state.equals(diagramObject.getType())) return Collections.emptyList();
for (AngularUiRouterNode current : allNodes) {
if (state.equals(current.getIdentifyingElement().getType()) && name.equals(current.getIdentifyingElement().getParent())) {
list.add(current);
}
}
return list;
}
public List<AngularUiRouterEdge> getEdges() {
return edges;
}
public List<AngularUiRouterNode> getAllNodes() {
return allNodes;
}
@Nullable
private Pair<AngularUiRouterNode, String> getParentTemplateNode(@NotNull final String state, @NotNull final String view) {
final int idx = view.indexOf("@");
if (idx < 0) {
// parent or top level template
if (state.contains(".") || myStatesMap.get(state).getParentName() != null) {
final UiRouterState routerState = myStatesMap.get(state);
if (routerState == null) {
return null;
}
return Pair.create(stateNodes.get(routerState.getParentName()), view);
} else {
return Pair.create(myRootNode, view);
}
} else {
//absolute path
//if (idx == 0) return Pair.create(myRootNode, view.substring(1));
final String placeholderName = view.substring(0, idx);
final String stateName = view.substring(idx + 1);
if (StringUtil.isEmptyOrSpaces(stateName)) {
return Pair.create(myRootNode, placeholderName);
}
return Pair.create(stateNodes.get(stateName), placeholderName);
}
}
@NotNull
private AngularUiRouterNode getOrCreateTemplateNode(AngularUiRouterDiagramProvider provider,
@Nullable VirtualFile templateFile,
@NotNull String templateUrl, @Nullable Template template) {
final String fullUrl = templateUrl;
final int idx = fullUrl.lastIndexOf('/');
templateUrl = idx >= 0 ? templateUrl.substring(idx + 1) : templateUrl;
template = template == null && templateFile != null ? myTemplatesMap.get(templateFile) : template;
if (template == null || template.getPointer() == null || templateFile == null) {
final AngularUiRouterNode templateNode = templateNodes.get(fullUrl);
if (templateNode != null) return templateNode;
// file not found
final DiagramObject templateObject = new DiagramObject(Type.template, templateUrl, null);
templateObject.addError("Can not find template file");
final AngularUiRouterNode fictiveNode = new AngularUiRouterNode(templateObject, provider);
fictiveNode.getIdentifyingElement().setTooltip(fullUrl);
templateNodes.put(fullUrl, fictiveNode);
return fictiveNode;
}
else if (!templateNodes.containsKey(templateFile.getUrl())) {
final DiagramObject templateObject = new DiagramObject(Type.template, templateUrl, template.getPointer());
final AngularUiRouterNode templateNode = new AngularUiRouterNode(templateObject, provider);
templateNodes.put(templateFile.getUrl(), templateNode);
putPlaceholderNodes(fullUrl, template, templateNode);
}
final AngularUiRouterNode templateNode = templateNodes.get(templateFile.getUrl());
assert templateNode != null;
templateNode.getIdentifyingElement().setTooltip(fullUrl);
return templateNode;
}
private void putPlaceholderNodes(@NotNull String templateUrl,
Template template,
AngularUiRouterNode templateNode) {
final Map<String, SmartPsiElementPointer<PsiElement>> placeholders = template.getViewPlaceholders();
if (placeholders != null) {
for (Map.Entry<String, SmartPsiElementPointer<PsiElement>> pointerEntry : placeholders.entrySet()) {
final String placeholder = pointerEntry.getKey();
final DiagramObject placeholderObject = new DiagramObject(Type.templatePlaceholder,
StringUtil.isEmptyOrSpaces(placeholder) ? DEFAULT : placeholder,
pointerEntry.getValue());
//final MyNode placeholderNode = new MyNode(placeholderObject, provider);
templateNode.getIdentifyingElement().addChild(placeholderObject, templateNode);
templatePlaceHoldersNodes.put(Pair.create(templateUrl, placeholder), placeholderObject);
// final MyEdge edge = new MyEdge(templateNode, placeholderNode);
// edges.add(edge);
}
}
}
}
public static String normalizeTemplateUrl(@Nullable String url) {
if (url == null) return null;
url = url.startsWith("/") ? url.substring(1) : url;
url = url.endsWith("/") ? url.substring(0, url.length() - 1) : url;
return url;
}
}