Skip to content

Commit 071213f

Browse files
authored
Enhanced context map generator with team maps (distinguish between BCs of type generic and/or teams) (#6)
1 parent 8ea2726 commit 071213f

File tree

10 files changed

+628
-68
lines changed

10 files changed

+628
-68
lines changed

build.gradle

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,14 @@ publish.finalizedBy(publishP2Repo)
211211

212212
test {
213213
useJUnitPlatform()
214+
215+
testLogging {
216+
showExceptions true
217+
exceptionFormat "full"
218+
219+
showCauses true
220+
showStackTraces true
221+
}
214222
}
215223

216224
jacocoTestReport {

src/main/java/guru/nidi/graphviz/engine/Graphviz.java

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,16 @@
1818
import guru.nidi.graphviz.model.Graph;
1919
import guru.nidi.graphviz.model.MutableGraph;
2020

21-
import java.io.*;
22-
import java.util.*;
23-
import java.util.concurrent.*;
21+
import java.io.File;
22+
import java.io.FileInputStream;
23+
import java.io.IOException;
24+
import java.io.InputStream;
25+
import java.util.ArrayList;
26+
import java.util.Arrays;
27+
import java.util.List;
28+
import java.util.concurrent.ArrayBlockingQueue;
29+
import java.util.concurrent.BlockingQueue;
30+
import java.util.concurrent.TimeUnit;
2431
import java.util.function.Consumer;
2532
import java.util.regex.Matcher;
2633
import java.util.regex.Pattern;

src/main/java/org/contextmapper/contextmap/generator/ContextMapGenerator.java

Lines changed: 196 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -19,17 +19,13 @@
1919
import guru.nidi.graphviz.attribute.Shape;
2020
import guru.nidi.graphviz.engine.Format;
2121
import guru.nidi.graphviz.engine.Graphviz;
22+
import guru.nidi.graphviz.engine.Renderer;
2223
import guru.nidi.graphviz.model.MutableGraph;
2324
import guru.nidi.graphviz.model.MutableNode;
2425
import org.contextmapper.contextmap.generator.model.*;
2526

26-
import java.io.File;
27-
import java.io.IOException;
28-
import java.io.OutputStream;
29-
import java.util.Map;
30-
import java.util.Random;
31-
import java.util.Set;
32-
import java.util.TreeMap;
27+
import java.io.*;
28+
import java.util.*;
3329
import java.util.stream.Collectors;
3430

3531
import static guru.nidi.graphviz.attribute.Attributes.attr;
@@ -45,12 +41,31 @@ public class ContextMapGenerator {
4541
private static final String EDGE_SPACING_UNIT = " ";
4642

4743
private Map<String, MutableNode> bcNodesMap;
44+
private Set<MutableNode> genericNodes;
45+
private Set<MutableNode> teamNodes;
46+
private File baseDir; // used for Graphviz images
4847

4948
protected int labelSpacingFactor = 1;
5049
protected int height = 1000;
5150
protected int width = 2000;
5251
protected boolean useHeight = false;
5352
protected boolean useWidth = true;
53+
protected boolean clusterTeams = true;
54+
55+
public ContextMapGenerator() {
56+
this.baseDir = new File(System.getProperty("java.io.tmpdir") + File.separator + "GraphvizJava");
57+
}
58+
59+
/**
60+
* Sets the base directory for included images (team maps).
61+
* In case you work with SVG or DOT files it is recommended to set the directory into which you generate the images.
62+
*
63+
* @param baseDir the baseDir into which we copy the team map image.
64+
*/
65+
public ContextMapGenerator setBaseDir(File baseDir) {
66+
this.baseDir = baseDir;
67+
return this;
68+
}
5469

5570
/**
5671
* Defines how much spacing we add to push the edges apart from each other.
@@ -94,6 +109,17 @@ public ContextMapGenerator setWidth(int width) {
94109
return this;
95110
}
96111

112+
/**
113+
* Defines whether teams (also generic contexts) are clustered together; is only relevant for mixed team maps
114+
* containing both types of BCs. If true, the resulting layout clusters BCs of the same types.
115+
*
116+
* @param clusterTeams whether BCs of the same type shall be clustered or not
117+
*/
118+
public ContextMapGenerator clusterTeams(boolean clusterTeams) {
119+
this.clusterTeams = clusterTeams;
120+
return this;
121+
}
122+
97123
/**
98124
* Generates the graphical Context Map.
99125
*
@@ -103,13 +129,7 @@ public ContextMapGenerator setWidth(int width) {
103129
* @throws IOException
104130
*/
105131
public void generateContextMapGraphic(ContextMap contextMap, Format format, String fileName) throws IOException {
106-
MutableGraph graph = createGraph(contextMap);
107-
108-
// store file
109-
if (useWidth)
110-
Graphviz.fromGraph(graph).width(width).render(format).toFile(new File(fileName));
111-
else
112-
Graphviz.fromGraph(graph).height(height).render(format).toFile(new File(fileName));
132+
generateContextMapGraphic(contextMap, format).toFile(new File(fileName));
113133
}
114134

115135
/**
@@ -121,69 +141,170 @@ public void generateContextMapGraphic(ContextMap contextMap, Format format, Stri
121141
* @throws IOException
122142
*/
123143
public void generateContextMapGraphic(ContextMap contextMap, Format format, OutputStream outputStream) throws IOException {
144+
generateContextMapGraphic(contextMap, format).toOutputStream(outputStream);
145+
}
146+
147+
private Renderer generateContextMapGraphic(ContextMap contextMap, Format format) throws IOException {
148+
exportImages();
124149
MutableGraph graph = createGraph(contextMap);
125150

126151
// store file
127152
if (useWidth)
128-
Graphviz.fromGraph(graph).width(width).render(format).toOutputStream(outputStream);
153+
return Graphviz.fromGraph(graph).basedir(baseDir).width(width).render(format);
129154
else
130-
Graphviz.fromGraph(graph).height(height).render(format).toOutputStream(outputStream);
155+
return Graphviz.fromGraph(graph).basedir(baseDir).height(height).render(format);
131156
}
132157

133158
private MutableGraph createGraph(ContextMap contextMap) {
134159
this.bcNodesMap = new TreeMap<>();
135-
MutableGraph graph = mutGraph("ContextMapGraph");
136-
137-
// create nodes
138-
contextMap.getBoundedContexts().forEach(bc -> {
139-
MutableNode node = mutNode(bc.getName());
140-
node.add(Label.lines(bc.getName()));
141-
node.add(Shape.EGG);
142-
node.add(attr("margin", "0.3"));
143-
node.add(attr("orientation", orientationDegree()));
144-
node.add(attr("fontname", "sans-serif"));
145-
node.add(attr("fontsize", "16"));
146-
node.add(attr("style", "bold"));
160+
this.genericNodes = new HashSet<>();
161+
this.teamNodes = new HashSet<>();
162+
MutableGraph rootGraph = createGraph("ContextMapGraph");
163+
164+
createNodes(contextMap.getBoundedContexts());
165+
166+
if (!needsSubGraphs(contextMap)) {
167+
addNodesToGraph(rootGraph, bcNodesMap.values());
168+
createRelationshipLinks4ExistingNodes(contextMap.getRelationships());
169+
} else {
170+
MutableGraph genericGraph = createGraph(getSubgraphName("GenericSubgraph"))
171+
.graphAttrs().add("color", "white");
172+
addNodesToGraph(genericGraph, genericNodes);
173+
MutableGraph teamGraph = createGraph(getSubgraphName("Teams_Subgraph"))
174+
.graphAttrs().add("color", "white");
175+
addNodesToGraph(teamGraph, teamNodes);
176+
genericGraph.addTo(rootGraph);
177+
teamGraph.addTo(rootGraph);
178+
179+
createRelationshipLinks4ExistingNodes(contextMap.getRelationships().stream().filter(rel -> rel.getFirstParticipant().getType() == rel.getSecondParticipant().getType())
180+
.collect(Collectors.toSet()));
181+
createRelationshipLinks(rootGraph, contextMap.getRelationships().stream().filter(rel -> rel.getFirstParticipant().getType() != rel.getSecondParticipant().getType())
182+
.collect(Collectors.toSet()));
183+
createTeamImplementationLinks(rootGraph, contextMap.getBoundedContexts().stream().filter(bc -> bc.getType() == BoundedContextType.TEAM
184+
&& !bc.getRealizedBoundedContexts().isEmpty()).collect(Collectors.toList()));
185+
}
186+
return rootGraph;
187+
}
188+
189+
private String getSubgraphName(String baseName) {
190+
return clusterTeams ? "cluster_" + baseName : baseName;
191+
}
192+
193+
private boolean needsSubGraphs(ContextMap contextMap) {
194+
boolean hasTeams = contextMap.getBoundedContexts().stream().anyMatch(bc -> bc.getType() == BoundedContextType.TEAM);
195+
boolean hasGenericContexts = contextMap.getBoundedContexts().stream().anyMatch(bc -> bc.getType() == BoundedContextType.GENERIC);
196+
return hasGenericContexts && hasTeams;
197+
}
198+
199+
private MutableGraph createGraph(String name) {
200+
MutableGraph rootGraph = mutGraph(name);
201+
rootGraph.setDirected(true);
202+
rootGraph.graphAttrs().add(attr("imagepath", baseDir.getAbsolutePath()));
203+
return rootGraph;
204+
}
205+
206+
private void addNodesToGraph(MutableGraph graph, Collection<MutableNode> nodes) {
207+
for (MutableNode node : nodes) {
208+
graph.add(node);
209+
}
210+
}
211+
212+
private void createNodes(Set<BoundedContext> boundedContexts) {
213+
boundedContexts.forEach(bc -> {
214+
MutableNode node = createNode(bc);
147215
bcNodesMap.put(bc.getName(), node);
216+
if (bc.getType() == BoundedContextType.TEAM)
217+
teamNodes.add(node);
218+
else
219+
genericNodes.add(node);
220+
});
221+
}
222+
223+
private MutableNode createNode(BoundedContext bc) {
224+
MutableNode node = mutNode(bc.getName());
225+
node.add(createNodeLabel(bc));
226+
node.add(Shape.EGG);
227+
node.add(attr("margin", "0.3"));
228+
node.add(attr("orientation", orientationDegree()));
229+
node.add(attr("fontname", "sans-serif"));
230+
node.add(attr("fontsize", "16"));
231+
node.add(attr("style", "bold"));
232+
return node;
233+
}
234+
235+
private void createRelationshipLinks4ExistingNodes(Set<Relationship> relationships) {
236+
relationships.forEach(rel -> {
237+
createRelationshipLink(this.bcNodesMap.get(rel.getFirstParticipant().getName()),
238+
this.bcNodesMap.get(rel.getSecondParticipant().getName()), rel);
148239
});
240+
}
149241

150-
// link nodes
151-
contextMap.getRelationships().forEach(rel -> {
152-
MutableNode node1 = this.bcNodesMap.get(rel.getFirstParticipant().getName());
153-
MutableNode node2 = this.bcNodesMap.get(rel.getSecondParticipant().getName());
154-
155-
if (rel instanceof Partnership) {
156-
node1.addLink(to(node2).with(createLabel("Partnership", rel.getName(), rel.getImplementationTechnology()))
157-
.add(attr("fontname", "sans-serif"))
158-
.add(attr("style", "bold"))
159-
.add(attr("fontsize", "12")));
160-
} else if (rel instanceof SharedKernel) {
161-
node1.addLink(to(node2).with(createLabel("Shared Kernel", rel.getName(), rel.getImplementationTechnology()))
162-
.add(attr("fontname", "sans-serif"))
163-
.add(attr("style", "bold"))
164-
.add(attr("fontsize", "12")));
165-
} else {
166-
UpstreamDownstreamRelationship upDownRel = (UpstreamDownstreamRelationship) rel;
167-
node1.addLink(to(node2).with(
168-
createLabel(upDownRel.isCustomerSupplier() ? "Customer/Supplier" : "", rel.getName(), rel.getImplementationTechnology()),
169-
attr("labeldistance", "0"),
170-
attr("fontname", "sans-serif"),
171-
attr("fontsize", "12"),
172-
attr("style", "bold"),
173-
attr("headlabel", getEdgeHTMLLabel("D", downstreamPatternsToStrings(upDownRel.getDownstreamPatterns()))),
174-
attr("taillabel", getEdgeHTMLLabel("U", upstreamPatternsToStrings(upDownRel.getUpstreamPatterns())))
175-
));
176-
}
242+
private void createRelationshipLinks(MutableGraph graph, Set<Relationship> relationships) {
243+
relationships.forEach(rel -> {
244+
MutableNode node1 = createNode(rel.getFirstParticipant());
245+
MutableNode node2 = createNode(rel.getSecondParticipant());
246+
createRelationshipLink(node1, node2, rel);
247+
graph.add(node1);
248+
graph.add(node2);
177249
});
250+
}
178251

179-
// add nodes to graph
180-
for (MutableNode node : this.bcNodesMap.values()) {
181-
graph.add(node);
252+
private void createRelationshipLink(MutableNode node1, MutableNode node2, Relationship rel) {
253+
if (rel instanceof Partnership) {
254+
node1.addLink(to(node2).with(createRelationshipLabel("Partnership", rel.getName(), rel.getImplementationTechnology()))
255+
.add(attr("dir", "none"))
256+
.add(attr("fontname", "sans-serif"))
257+
.add(attr("style", "bold"))
258+
.add(attr("fontsize", "12")));
259+
} else if (rel instanceof SharedKernel) {
260+
node1.addLink(to(node2).with(createRelationshipLabel("Shared Kernel", rel.getName(), rel.getImplementationTechnology()))
261+
.add(attr("dir", "none"))
262+
.add(attr("fontname", "sans-serif"))
263+
.add(attr("style", "bold"))
264+
.add(attr("fontsize", "12")));
265+
} else {
266+
UpstreamDownstreamRelationship upDownRel = (UpstreamDownstreamRelationship) rel;
267+
node1.addLink(to(node2).with(
268+
createRelationshipLabel(upDownRel.isCustomerSupplier() ? "Customer/Supplier" : "", rel.getName(), rel.getImplementationTechnology()),
269+
attr("dir", "none"),
270+
attr("labeldistance", "0"),
271+
attr("fontname", "sans-serif"),
272+
attr("fontsize", "12"),
273+
attr("style", "bold"),
274+
attr("headlabel", getEdgeHTMLLabel("D", downstreamPatternsToStrings(upDownRel.getDownstreamPatterns()))),
275+
attr("taillabel", getEdgeHTMLLabel("U", upstreamPatternsToStrings(upDownRel.getUpstreamPatterns())))
276+
));
182277
}
183-
return graph;
184278
}
185279

186-
private Label createLabel(String relationshipType, String relationshipName, String implementationTechnology) {
280+
private void createTeamImplementationLinks(MutableGraph graph, List<BoundedContext> teams) {
281+
for (BoundedContext team : teams) {
282+
team.getRealizedBoundedContexts().forEach(system -> {
283+
if (bcNodesMap.containsKey(team.getName()) && bcNodesMap.containsKey(system.getName())) {
284+
MutableNode node1 = createNode(team);
285+
MutableNode node2 = createNode(system);
286+
node1.addLink(to(node2).with(
287+
Label.lines(" «realizes»"),
288+
attr("color", "#686868"),
289+
attr("fontname", "sans-serif"),
290+
attr("fontsize", "12"),
291+
attr("fontcolor", "#686868"),
292+
attr("style", "dashed")));
293+
graph.add(node1);
294+
graph.add(node2);
295+
}
296+
});
297+
}
298+
}
299+
300+
private Label createNodeLabel(BoundedContext boundedContext) {
301+
if (boundedContext.getType() == BoundedContextType.TEAM)
302+
return Label.html("<table cellspacing=\"0\" cellborder=\"0\" border=\"0\"><tr><td rowspan=\"2\"><img src='team-icon.png' /></td><td width=\"10px\">" +
303+
"</td><td><b>Team</b></td></tr><tr><td width=\"10px\"></td><td>" + boundedContext.getName() + "</td></tr></table>");
304+
return Label.lines(boundedContext.getName());
305+
}
306+
307+
private Label createRelationshipLabel(String relationshipType, String relationshipName, String implementationTechnology) {
187308
boolean relationshipTypeDefined = relationshipType != null && !"".equals(relationshipType);
188309
boolean nameDefined = relationshipName != null && !"".equals(relationshipName);
189310
boolean implementationTechnologyDefined = implementationTechnology != null && !"".equals(implementationTechnology);
@@ -243,4 +364,19 @@ private Label getEdgeHTMLLabel(String upstreamDownstreamLabel, Set<String> patte
243364
"</table>");
244365
}
245366

367+
private void exportImages() throws IOException {
368+
if (!baseDir.exists())
369+
baseDir.mkdir();
370+
if (!new File(baseDir, "team-icon.png").exists()) {
371+
InputStream teamIconInputStream = ContextMapGenerator.class.getClassLoader().getResourceAsStream("team-icon.png");
372+
byte[] buffer = new byte[teamIconInputStream.available()];
373+
teamIconInputStream.read(buffer);
374+
File targetFile = new File(baseDir, "team-icon.png");
375+
OutputStream outStream = new FileOutputStream(targetFile);
376+
outStream.write(buffer);
377+
outStream.flush();
378+
outStream.close();
379+
}
380+
}
381+
246382
}

0 commit comments

Comments
 (0)