Skip to content

Commit

Permalink
[minor] feat: Generate graph legend (#110)
Browse files Browse the repository at this point in the history
  • Loading branch information
manikmagar authored Oct 1, 2020
1 parent 76addf2 commit 2fd95d8
Show file tree
Hide file tree
Showing 5 changed files with 3,151 additions and 503 deletions.
138 changes: 106 additions & 32 deletions src/main/java/com/javastreets/mulefd/drawings/GraphDiagram.java
Original file line number Diff line number Diff line change
@@ -1,16 +1,19 @@
package com.javastreets.mulefd.drawings;

import static com.javastreets.mulefd.util.FileUtil.sanitizeFilename;
import static guru.nidi.graphviz.attribute.Arrow.*;
import static guru.nidi.graphviz.attribute.Arrow.DirType;
import static guru.nidi.graphviz.attribute.Arrow.VEE;
import static guru.nidi.graphviz.model.Factory.*;

import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.*;
import java.util.function.Consumer;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
Expand All @@ -25,6 +28,7 @@
import guru.nidi.graphviz.engine.Format;
import guru.nidi.graphviz.engine.Graphviz;
import guru.nidi.graphviz.engine.GraphvizV8Engine;
import guru.nidi.graphviz.model.Link;
import guru.nidi.graphviz.model.MutableGraph;
import guru.nidi.graphviz.model.MutableNode;

Expand All @@ -34,9 +38,9 @@ public class GraphDiagram implements Diagram {

@Override
public boolean draw(DrawingContext drawingContext) {
MutableGraph rootGraph = initNewGraph();
MutableGraph rootGraph = initNewGraphWithLegend(true);
MutableGraph appGraph = addApplicationGraph(rootGraph);
File targetDirectory = drawingContext.getOutputFile().getParentFile();

Map<String, Component> flowRefs = new HashMap<>();
List<String> mappedFlowKinds = new ArrayList<>();
List<Component> flows = drawingContext.getComponents();
Expand All @@ -48,7 +52,7 @@ public boolean draw(DrawingContext drawingContext) {
.findFirst().orElseThrow(() -> new DrawingException(
"Target flow not found - " + drawingContext.getFlowName()));
MutableNode flowNode = processComponent(component, drawingContext, flowRefs, mappedFlowKinds);
flowNode.addTo(rootGraph);
flowNode.addTo(appGraph);
}

for (Component component : flows) {
Expand All @@ -59,26 +63,61 @@ public boolean draw(DrawingContext drawingContext) {
processComponent(component, drawingContext, flowRefs, mappedFlowKinds);

if (drawingContext.isGenerateSingles() && component.isaFlow()) {
MutableGraph flowGraph = initNewGraph();
flowNode.addTo(flowGraph);
MutableGraph flowRootGraph = initNewGraph(getDiagramHeaderLines());
flowNode.addTo(flowRootGraph);
for (Component component2 : flows) {
if (mappedFlowKinds.contains(component2.qualifiedName())) {
MutableNode flowNode3 =
processComponent(component2, drawingContext, flowRefs, mappedFlowKinds);
flowNode3.addTo(flowGraph);
flowNode3.addTo(flowRootGraph);
}
}
writeFlowGraph(component, singleFlowDirPath, flowGraph);
writeFlowGraph(component, singleFlowDirPath, flowRootGraph);
}
flowNode.addTo(rootGraph);
flowNode.addTo(appGraph);
}
}
if (drawingContext.getFlowName() == null) {
checkUnusedNodes(rootGraph);
checkUnusedNodes(appGraph);
}
return writGraphToFile(drawingContext.getOutputFile(), rootGraph);
}

MutableGraph addApplicationGraph(MutableGraph rootGraph) {
MutableGraph appGraph = initNewGraph("Application graph");
appGraph.setCluster(true).graphAttrs().add(Style.INVIS);
appGraph.addTo(rootGraph);
return appGraph;
}

MutableNode sizedNode(String label, double width) {
return mutNode(label).add(Size.mode(Size.Mode.FIXED).size(width, 0.25));
}

void addLegends(MutableGraph rootGraph) {
log.debug("Adding legend to graph - {}", rootGraph.name());
graph("legend").directed().cluster().graphAttr().with(Label.html("<b>Legend</b>"), Style.DASHED)
.with(
asFlow(sizedNode("flow", 1))
.addLink(to(asSubFlow(sizedNode("sub-flow", 1))).with(Style.INVIS)),
asSubFlow(sizedNode("sub-flow", 1))
.addLink(to(asUnusedFlow(sizedNode("Unused sub/-flow", 2))).with(Style.INVIS)),
sizedNode("Flow A", 1).addLink(callSequenceLink(1, sizedNode("sub-flow-1", 1.25))
.with(Label.lines("Call Sequence").tail(-5, 8))),
sizedNode("Flow C", 1).addLink(asAsyncLink(1, sizedNode("sub-flow-C1", 1.25))
.with(Label.lines("Asynchronous call").tail(-5, 8))),
asSourceNode(sizedNode("flow source", 1.5))
.addLink(to(asFlow(sizedNode("flow self-call", 1.25))).with(Style.INVIS)),
asFlow(sizedNode("flow self-call", 2)).addLink(asFlow(sizedNode("flow self-call", 2)))
.addLink(to(asSubFlow(sizedNode("sub-flow self-call", 2))
.addLink(asSubFlow(sizedNode("sub-flow self-call", 2)))).with(Style.INVIS)))
.addTo(rootGraph);
// legend-space is added to create gap between application graph and legend.
// this is a hidden cluster
graph("legend-space").cluster().graphAttr().with(Label.of(""), Style.INVIS)
.with(node("").with(Shape.NONE, Size.std().size(2, 1))).addTo(rootGraph);
}

boolean writeFlowGraph(Component flowComponent, Path targetDirectory, MutableGraph flowGraph) {
if (!flowComponent.isaFlow())
return false;
Expand Down Expand Up @@ -112,30 +151,60 @@ boolean writGraphToFile(File outputFilename, MutableGraph graph) {
}
}

MutableGraph initNewGraph() {
MutableGraph initNewGraphWithLegend(boolean legend) {
MutableGraph rootGraph = initNewGraph(getDiagramHeaderLines());
if (legend)
addLegends(rootGraph);
return rootGraph;
}

MutableGraph initNewGraph(String label) {
return initNewGraph(new String[] {label});
}

MutableGraph initNewGraph(String[] label) {
return mutGraph("mule").setDirected(true).linkAttrs().add(VEE.dir(DirType.FORWARD)).graphAttrs()
.add(Rank.dir(Rank.RankDir.LEFT_TO_RIGHT), GraphAttr.splines(GraphAttr.SplineMode.SPLINE),
GraphAttr.pad(2.0), GraphAttr.dpi(150),
Label.htmlLines(getDiagramHeaderLines()).locate(Label.Location.TOP));
GraphAttr.pad(1, 0.5), GraphAttr.dpi(150),
Label.htmlLines(label).locate(Label.Location.TOP));
}

private void checkUnusedNodes(MutableGraph graph) {
graph.nodes().stream()
.filter(node -> node.links().isEmpty() && graph.edges().stream().noneMatch(
edge -> edge.to().name().equals(node.name()) || edge.from().name().equals(node.name())))
.forEach(node -> node.add(Color.RED, Style.FILLED, Color.GRAY));
.forEach(this::asUnusedFlow);
}

MutableNode asUnusedFlow(MutableNode node) {
return node.add(Color.RED, Style.FILLED, Color.GRAY);
}

MutableNode asFlow(MutableNode node) {
return node.add(Shape.RECTANGLE).add(Color.BLUE);
}

MutableNode asSubFlow(MutableNode node) {
return node.add(Color.BLACK).add(Shape.ELLIPSE);
}

MutableNode asApikitNode(String name) {
return mutNode(name).add(Shape.DOUBLE_CIRCLE, Color.CYAN, Style.FILLED);
}

MutableNode asSourceNode(MutableNode node) {
return node.add(Shape.HEXAGON, Style.FILLED, Color.CYAN).add("sourceNode", Boolean.TRUE);
}

MutableNode processComponent(Component component, DrawingContext drawingContext,
Map<String, Component> flowRefs, List<String> mappedFlowKinds) {
log.debug("Processing flow - {}", component.qualifiedName());
FlowContainer flow = (FlowContainer) component;
Consumer<MutableNode> asFlow = flowNode -> flowNode.add(Shape.RECTANGLE).add(Color.BLUE);
MutableNode flowNode = mutNode(flow.qualifiedName()).add(Label.markdown(getNodeLabel(flow)));
if (flow.isaSubFlow()) {
flowNode.add(Color.BLACK).add(Shape.ELLIPSE);
asSubFlow(flowNode);
} else {
asFlow.accept(flowNode);
asFlow(flowNode);
}
MutableNode sourceNode = null;
boolean hasSource = false;
Expand Down Expand Up @@ -163,9 +232,8 @@ MutableNode processComponent(Component component, DrawingContext drawingContext,
}
if (muleComponent.isSource()) {
hasSource = true;
sourceNode =
mutNode(name).add(Shape.HEXAGON, Color.DARKORANGE).add("sourceNode", Boolean.TRUE).add(
Label.htmlLines("<b>" + muleComponent.getType() + "</b>", muleComponent.getName()));
sourceNode = asSourceNode(mutNode(name)).add(
Label.htmlLines("<b>" + muleComponent.getType() + "</b>", muleComponent.getName()));
} else if (muleComponent.getType().equals("apikit")) {
// APIKit auto generated flows follow a naming pattern
// "{httpMethod}:\{resource-name}:{apikitConfigName}"
Expand All @@ -174,27 +242,25 @@ MutableNode processComponent(Component component, DrawingContext drawingContext,
// 3. Link those flows with apiKit flow.
log.debug("Processing apikit component - {}", component.qualifiedName());
MutableNode apiKitNode =
mutNode(muleComponent.getType().concat(muleComponent.getConfigRef().getValue()))
asApikitNode(muleComponent.getType().concat(muleComponent.getConfigRef().getValue()))
.add(Label.htmlLines("<b>" + muleComponent.getType() + "</b>",
muleComponent.getConfigRef().getValue()))
.add(Shape.DOUBLE_CIRCLE, Color.CYAN, Style.FILLED);
muleComponent.getConfigRef().getValue()));
for (Component component1 : searchFlowBySuffix(
":" + muleComponent.getConfigRef().getValue(), drawingContext.getComponents())) {
MutableNode node =
mutNode(component1.qualifiedName()).add(Label.markdown(getNodeLabel(component1)));
asFlow.accept(node);
asFlow(node);
apiKitNode.addLink(to(node).with(Style.SOLID));
}
flowNode
.addLink(to(apiKitNode).with(Style.SOLID, Label.of("(" + (componentIdx - 1) + ")")));
flowNode.addLink(callSequenceLink(componentIdx - 1, apiKitNode));
} else {
addSubNodes(flowNode, hasSource ? componentIdx - 1 : componentIdx, muleComponent, name);
}

mappedFlowKinds.add(name);
}
if (sourceNode != null) {
flowNode = sourceNode.add(Style.FILLED, Color.CYAN).addLink(to(flowNode).with(Style.BOLD));
flowNode = sourceNode.addLink(to(flowNode).with(Style.BOLD));
}
return flowNode;
}
Expand All @@ -206,13 +272,21 @@ private String getNodeLabel(Component component) {
private void addSubNodes(MutableNode flowNode, int callSequence, MuleComponent muleComponent,
String name) {
if (muleComponent.isAsync()) {
flowNode.addLink(to(mutNode(name)).with(Style.DASHED.and(Style.BOLD),
Label.of("(" + callSequence + ") Async"), Color.BROWN));
flowNode.addLink(asAsyncLink(callSequence, mutNode(name)));
} else {
flowNode.addLink(to(mutNode(name)).with(Style.SOLID, Label.of("(" + callSequence + ")")));
flowNode.addLink(callSequenceLink(callSequence, mutNode(name)));
}
}

Link callSequenceLink(int callSequence, MutableNode node) {
return to(node).with(Style.SOLID, Label.of("(" + callSequence + ")"));
}

Link asAsyncLink(int callSequence, MutableNode node) {
return to(node).with(Style.DASHED.and(Style.BOLD),
Label.of("(" + callSequence + ") Async").external(), Color.LIGHTBLUE3);
}

@Override
public boolean supports(DiagramType diagramType) {
return DiagramType.GRAPH.equals(diagramType);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,6 @@ void drawToValidateGraph_SingleFlow() throws Exception {
MutableGraph generatedGraph = graphArgumentCaptor.getValue();
Graphviz.useEngine(new GraphvizV8Engine());
String jsonGraph = Graphviz.fromGraph(generatedGraph).render(Format.JSON).toString();
System.out.println(jsonGraph);
String ref = new String(
Files.readAllBytes(Paths.get("src/test/resources/single-flow-generation-example.json")));
JSONAssert.assertEquals(ref, jsonGraph, JSONCompareMode.STRICT);
Expand Down Expand Up @@ -313,24 +312,24 @@ void initNewGraph() {
Mockito.when(graphDiagram.getDiagramHeaderLines()).thenReturn(new String[] {"Test"});
MutableGraph graph = mutGraph("mule").setDirected(true).linkAttrs()
.add(VEE.dir(Arrow.DirType.FORWARD)).graphAttrs().add(Rank.dir(Rank.RankDir.LEFT_TO_RIGHT),
GraphAttr.splines(GraphAttr.SplineMode.SPLINE), GraphAttr.pad(2.0), GraphAttr.dpi(150),
Label.htmlLines("Test").locate(Label.Location.TOP));
MutableGraph returnedGraph = graphDiagram.initNewGraph();
GraphAttr.splines(GraphAttr.SplineMode.SPLINE), GraphAttr.pad(1, 0.5),
GraphAttr.dpi(150), Label.htmlLines("Test").locate(Label.Location.TOP));
MutableGraph returnedGraph = graphDiagram.initNewGraph("Test");
assertThat(returnedGraph).isEqualTo(graph);
}

@Test
void writGraphToFile() throws Exception {
GraphDiagram graphDiagram = new GraphDiagram();
MutableGraph graph = graphDiagram.initNewGraph();
MutableGraph graph = graphDiagram.initNewGraph("Test");
boolean generated = graphDiagram.writGraphToFile(new File(tempDir, "test.png"), graph);
assertThat(generated).isTrue();
}

@Test
void writeFlowGraphWithFlow() {
GraphDiagram graphDiagram = new GraphDiagram();
MutableGraph graph = graphDiagram.initNewGraph();
MutableGraph graph = graphDiagram.initNewGraph("Test");
FlowContainer flowContainer = new FlowContainer("flow", "test-flow");
flowContainer.addComponent(new MuleComponent("flow-ref", "test-sub-flow"));
FlowContainer subflow = new FlowContainer("sub-flow", "test-sub-flow");
Expand All @@ -345,7 +344,7 @@ void writeFlowGraphWithFlow() {
@Test
void writeFlowGraphWithSubFlow() {
GraphDiagram graphDiagram = new GraphDiagram();
MutableGraph graph = graphDiagram.initNewGraph();
MutableGraph graph = graphDiagram.initNewGraph("Test");
FlowContainer subflow = new FlowContainer("sub-flow", "test-sub-flow");
Path outputFilePath = Paths.get(tempDir.getAbsolutePath(), "dummy");
boolean written = graphDiagram.writeFlowGraph(subflow, outputFilePath, graph);
Expand Down
Loading

0 comments on commit 2fd95d8

Please sign in to comment.