1919import guru .nidi .graphviz .attribute .Shape ;
2020import guru .nidi .graphviz .engine .Format ;
2121import guru .nidi .graphviz .engine .Graphviz ;
22+ import guru .nidi .graphviz .engine .Renderer ;
2223import guru .nidi .graphviz .model .MutableGraph ;
2324import guru .nidi .graphviz .model .MutableNode ;
2425import 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 .*;
3329import java .util .stream .Collectors ;
3430
3531import 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