001 /**************************************************************************************
002 * Copyright (C) 2009 Progress Software, Inc. All rights reserved. *
003 * http://fusesource.com *
004 * ---------------------------------------------------------------------------------- *
005 * The software in this package is published under the terms of the AGPL license *
006 * a copy of which has been included with this distribution in the license.txt file. *
007 **************************************************************************************/
008 package org.fusesource.mvnplugins.graph;
009
010 import org.apache.maven.shared.dependency.tree.DependencyNode;
011 import org.apache.maven.plugin.MojoExecutionException;
012 import org.apache.maven.plugin.logging.Log;
013 import org.apache.maven.artifact.Artifact;
014 import org.codehaus.plexus.util.cli.*;
015 import org.codehaus.plexus.util.FileUtils;
016
017 import java.util.*;
018 import java.io.File;
019 import java.io.PrintStream;
020
021 /**
022 * @author chirino
023 */
024 public class DependencyVisualizer {
025
026 LinkedHashMap<String, Node> nodes = new LinkedHashMap<String, Node>();
027 LinkedHashSet<Edge> edges = new LinkedHashSet<Edge>();
028 HashSet<String> hideScopes = new HashSet<String>();
029 boolean hideOptional;
030 boolean hidePoms;
031 boolean hideOmitted;
032 String label;
033 boolean hideTransitive;
034 Log log;
035 boolean cascade;
036 String direction="TB";
037
038 private class Node {
039 private final String id;
040 private final ArrayList<Edge> children = new ArrayList<Edge>();
041 private final ArrayList<Edge> parents = new ArrayList<Edge>();
042 private final Artifact artifact;
043 private int roots;
044
045 public Node(String id, Artifact artifact) {
046 this.id = id;
047 this.artifact = artifact;
048 }
049
050 @Override
051 public boolean equals(Object obj) {
052 return id.equals(((Node) obj).id);
053 }
054
055 @Override
056 public int hashCode() {
057 return id.hashCode();
058 }
059
060 @Override
061 public String toString() {
062 return id;
063 }
064
065 public boolean isHidden() {
066 if ( hidePoms && isExclusivelyType("pom") ) {
067 return true;
068 }
069 return false;
070 }
071
072 public String getId() {
073 return id;
074 }
075
076 public String getLabel() {
077 final Artifact a = artifact;
078 StringBuilder sb = new StringBuilder();
079 sb.append( a.getGroupId());
080 sb.append("\\n" +a.getArtifactId());
081 if (!isExclusivelyType("jar")) {
082 sb.append("\\n");
083 boolean first=true;
084 for (String type : getTypes()) {
085 if( !first ) {
086 sb.append(" | ");
087 }
088 first=false;
089 sb.append(type);
090 }
091 }
092 sb.append("\\n" + a.getVersion());
093 return sb.toString();
094 }
095
096 public String getColor() {
097 if (isScope("test")) {
098 return "cornflowerblue";
099 }
100 return "black";
101 }
102
103 private boolean isScope(String scope) {
104 return roots==0 && !parents.isEmpty() && allMatchScope(parents, scope);
105 }
106
107 public String getFillColor() {
108 if( roots > 0 ) {
109 return "#dddddd";
110 }
111 return "white";
112 }
113 public String getFontColor() {
114 return getColor();
115 }
116
117 public String getLineStyle() {
118 String rc = isOptional() ? "dotted" : "solid";
119 rc += ",filled";
120 return rc;
121 }
122
123 public double getFontSize() {
124 if( roots > 0 ) {
125 return 14;
126 }
127 return 8;
128 }
129
130 public boolean isOptional() {
131 return roots==0 && !parents.isEmpty() && allMatchOptional(parents, true);
132 }
133
134
135 private boolean allMatchScope(ArrayList<Edge> edges, String scope) {
136 for (Edge e : edges) {
137 if (!e.isScope(scope)) {
138 return false;
139 }
140 }
141 return true;
142 }
143 private boolean allMatchOptional(ArrayList<Edge> edges, boolean value) {
144 for (Edge e : edges) {
145 if (e.optional != value) {
146 return false;
147 }
148 }
149 return true;
150 }
151
152 private Set<String> getTypes() {
153 LinkedHashSet<String> rc = new LinkedHashSet<String>();
154 rc.add(artifact.getType() + (artifact.getClassifier()==null? "" : (":" + artifact.getClassifier())));
155 for (Edge e : parents) {
156 Artifact artifact = e.dependencyNode.getArtifact();
157 rc.add(artifact.getType() + (artifact.getClassifier()==null? "" : (":" + artifact.getClassifier())));
158 }
159 return rc;
160 }
161
162 private boolean isExclusivelyType(String value) {
163 Set<String> types = getTypes();
164 return types.size()==1 && types.contains(value);
165 }
166
167 public int getRecursiveChildCount() {
168 int rc = children.size();
169 for (Edge child : children) {
170 int t = child.getRecursiveChildCount();
171 if( t > rc ) {
172 rc = t;
173 }
174 }
175 return rc;
176 }
177
178 }
179
180 private class Edge {
181 private Node parent;
182 private Node child;
183 private String scope;
184 private boolean optional;
185 private DependencyNode dependencyNode;
186
187 public Edge(Node parent, Node child, DependencyNode dependencyNode) {
188 this.parent = parent;
189 this.child = child;
190 this.dependencyNode = dependencyNode;
191 this.scope = dependencyNode.getArtifact().getScope();
192 this.optional = dependencyNode.getArtifact().isOptional();
193 }
194 public Edge(Edge edge) {
195 this.parent = edge.parent;
196 this.child = edge.child;
197 this.dependencyNode = edge.dependencyNode;
198 this.scope = edge.scope;
199 this.optional = edge.optional;
200 }
201
202 public Edge optional(boolean optional) {
203 if ( this.optional == optional) {
204 return this;
205 }
206 Edge rc = new Edge(this);
207 rc.optional = optional;
208 return rc;
209 }
210
211 public Edge scope(String scope) {
212 if ( this.scope.equals(scope) ) {
213 return this;
214 }
215 Edge rc = new Edge(this);
216 rc.scope = scope;
217 return rc;
218 }
219
220 public boolean isHidden() {
221 if( hideTransitive && dependencyNode.getParent().getParent()!=null ) {
222 return true;
223 }
224 if(hideOptional && optional)
225 return true;
226 if(hideScopes.contains(scope) )
227 return true;
228
229 final int state = dependencyNode.getState();
230 if(hideOmitted && (state==DependencyNode.OMITTED_FOR_CONFLICT || state==DependencyNode.OMITTED_FOR_CYCLE) ) {
231 return true;
232 }
233 return false;
234 }
235
236 public boolean isScope(String s) {
237 return scope.equals(s);
238 }
239
240 public String getLineStyle() {
241 if( optional ) {
242 return "dotted";
243 }
244 return "solid";
245 }
246
247 public String getLabel() {
248 StringBuilder sb = new StringBuilder();
249 if ( !isScope("compile")) {
250 sb.append(scope);
251 }
252 if ( optional ) {
253 if( sb.length()!=0 ) {
254 sb.append(",");
255 }
256 sb.append("optional");
257 }
258 return sb.toString();
259 }
260
261 public String getColor() {
262 if (isScope("test")) {
263 return "cornflowerblue";
264 }
265 return "black";
266 }
267
268 double getWeight() {
269 double rc = 1 + getRecursiveChildCount();
270
271 if ( isScope("compile")) {
272 rc *= 2;
273 }
274 if ( !optional ) {
275 rc *= 2;
276 }
277 return rc;
278 }
279
280 private int getRecursiveChildCount() {
281 return child.getRecursiveChildCount();
282 }
283
284 @Override
285 public boolean equals(Object o) {
286 if (this == o) return true;
287 if (o == null || getClass() != o.getClass()) return false;
288
289 Edge edge = (Edge) o;
290
291 if (parent != null ? !parent.equals(edge.parent) : edge.parent != null) return false;
292 if (child != null ? !child.equals(edge.child) : edge.child != null) return false;
293 if (scope != null ? !scope.equals(edge.scope) : edge.scope != null) return false;
294 if (optional != edge.optional) return false;
295 return true;
296 }
297
298 @Override
299 public int hashCode() {
300 int result = parent != null ? parent.hashCode() : 0;
301 result = 31 * result + (child != null ? child.hashCode() : 0);
302 result = 31 * result + (scope != null ? scope.hashCode() : 0);
303 result = 31 * result + (optional ? 1 : 0);
304 return result;
305 }
306
307 @Override
308 public String toString() {
309 return "Edge{" +
310 "parent=" + parent +
311 ", child=" + child +
312 ", scope='" + scope + '\'' +
313 ", optional=" + optional +
314 '}';
315 }
316 }
317
318 public void add(DependencyNode dn) {
319 add(dn, true);
320 }
321
322 private Node add(DependencyNode dn, boolean root) {
323 Node parent = getNode(dn);
324 if( root ) {
325 parent.roots++;
326 }
327 if (dn.hasChildren()) {
328 for (DependencyNode c : (List<DependencyNode>) dn.getChildren()) {
329 Node child = add(c, false);
330 Edge edge = new Edge(parent, child, c);
331 add(edge);
332 }
333 }
334 return parent;
335 }
336
337 private Node getNode(DependencyNode dn) {
338 Artifact artifact = dn.getArtifact();
339 String id = artifact.getGroupId()+":"+artifact.getArtifactId()+":"+artifact.getVersion();
340 if( artifact.getClassifier()!=null ) {
341 id += ":"+artifact.getClassifier();
342 }
343 Node node = nodes.get(id);
344 if (node == null) {
345 node = new Node(id, dn.getArtifact());
346 nodes.put(id, node);
347 }
348 return node;
349 }
350
351 private void add(Edge edge) {
352 if (edges.add(edge)) {
353 edge.child.parents.add(edge);
354 edge.parent.children.add(edge);
355 }
356 }
357
358 private void remove(Node node) {
359 nodes.remove(node.getId());
360
361 // Remove the edges attached to this node...
362 for (Edge edge : new ArrayList<Edge>(node.parents)) {
363 remove(edge);
364 }
365 for (Edge edge : new ArrayList<Edge>(node.children)) {
366 remove(edge);
367 }
368 }
369
370 private void remove(Edge edge) {
371 edge.parent.children.remove(edge);
372 edge.child.parents.remove(edge);
373 edges.remove(edge);
374 }
375
376 public void export(File target) throws MojoExecutionException {
377
378 // Drop nodes and edges which are hidden...
379 for (Node node : new ArrayList<Node>(nodes.values()) ) {
380 if (node.isHidden()) {
381 log.debug("Dropping hidden node: "+node);
382 remove(node);
383 }
384 }
385 for (Edge edge : new ArrayList<Edge>(edges) ) {
386 if (edge.isHidden()) {
387 log.debug("Dropping hidden edge: "+edge);
388 remove(edge);
389 }
390
391 }
392
393 if ( cascade ) {
394 // Propagate the attributes down to the children.
395
396 LinkedList<Node> ll = new LinkedList<Node>(nodes.values());
397 while( !ll.isEmpty() ) {
398 // Optional propagates...
399 Node node = ll.removeFirst();
400 if( node.isOptional() ) {
401 for (Edge edge : new ArrayList<Edge>(node.children)) {
402 if( !edge.optional ) {
403 remove(edge);
404 add(edge.optional(true));
405
406 // If a child filpped to optional.. then we need
407 // to enqueue so we process it's children
408 if( edge.child.isOptional() ) {
409 ll.addLast(edge.child);
410 }
411 }
412 }
413 }
414
415 // scope propagates....
416 if( node.isScope("test") ) {
417 for (Edge edge : new ArrayList<Edge>(node.children)) {
418 if( !edge.isScope("test") ) {
419 remove(edge);
420 add(edge.scope("test"));
421
422 // If a child filpped to test.. then we need
423 // to enqueue so we process it's children
424 if( edge.child.isScope("test") ) {
425 ll.addLast(edge.child);
426 }
427 }
428 }
429 }
430 }
431 }
432
433 // Remove all the non root nodes that are disconnected.
434 for (Node node : new ArrayList<Node>(nodes.values()) ) {
435 if (node.parents.size()==0 && node.roots==0) {
436 log.debug("Dropping orphaned node: "+node);
437 remove(node);
438 }
439 }
440
441 // Write the source file...
442 boolean convertDotFile=true;
443 File source = new File(target.getParentFile(), target.getName() + ".dot");
444
445 // User might just be requesting a dot file..
446 if( target.getName().endsWith(".dot") ) {
447 convertDotFile = false;
448 source = target;
449 }
450
451 PrintStream os = null;
452 try {
453 log.debug("Exporting to: "+source);
454 os = new PrintStream(source);
455 DotExporter exporter = new DotExporter(os);
456 exporter.export();
457 } catch (Exception e) {
458 throw new MojoExecutionException("Could not create the dot file used to generate the image.", e);
459 } finally {
460 os.close();
461 }
462
463
464 if (!convertDotFile) {
465 return;
466 }
467
468 try {
469 Commandline commandline = new Commandline();
470 try {
471 commandline.addSystemEnvironment();
472 } catch (Exception ignore) {
473 }
474 commandline.setExecutable("dot");
475 commandline.addArguments(new String[]{
476 "-T" + FileUtils.getExtension(target.getName()),
477 "-o" + target.getAbsolutePath(),
478 source.getAbsolutePath()
479 });
480
481 log.debug("Executing dot command...");
482 int rc = CommandLineUtils.executeCommandLine(commandline, new DefaultConsumer(), new DefaultConsumer());
483 if (rc != 0) {
484 throw new MojoExecutionException("Execution of the 'dot' command failed. Perhaps it's not installed. See: http://www.graphviz.org/");
485 }
486 log.debug("Graph generated. ");
487 source.delete();
488
489 } catch (CommandLineException e) {
490 throw new MojoExecutionException("Execution of the 'dot' command failed.", e);
491 }
492
493 }
494
495 private class DotExporter {
496 private final PrintStream out;
497 int indent = 0;
498
499 public DotExporter(PrintStream os) {
500 this.out = os;
501 }
502
503 public void export() {
504
505 String graphFont = "Serif";
506 String nodeFont = "SanSerif";
507
508 String osName = System.getProperty("os.name", "NO OS NAME!!");
509 if (osName.contains("Windows")) {
510 graphFont = "arial";
511 nodeFont = "arial";
512 }
513
514 p("digraph dependencies {").i(1);
515 {
516 p("graph [").i(1);
517 {
518 if (label != null) {
519 p("label=" + q(label));
520 }
521 p("labeljust=l");
522 p("labelloc=t");
523 p("fontsize=18");
524 p("fontname="+q(graphFont));
525 p("ranksep=1");
526 p("rankdir="+q(direction));
527 p("nodesep=.05");
528
529 }
530 i(-1).p("];");
531 p("node [").i(1);
532 {
533 p("fontsize=8");
534 p("fontname="+q(nodeFont));
535 p("shape=\"rectangle\"");
536 }
537 i(-1).p("];");
538 p("edge [").i(1);
539 {
540 p("fontsize=8");
541 p("fontname="+q(nodeFont));
542 }
543 i(-1).p("];");
544
545 // Write the nodes..
546 for (Node node : nodes.values()) {
547 p(q(node.getId()) + " [").i(1);
548 {
549 p("fontsize="+node.getFontSize());
550 p("label=" + q(node.getLabel()));
551 p("color=" + q(node.getColor()));
552 p("fontcolor=" + q(node.getFontColor()));
553 p("fillcolor=" + q(node.getFillColor()));
554 p("style=" + q(node.getLineStyle()));
555 }
556 i(-1).p("];");
557 }
558
559 // Write the edges..
560 for (Edge edge : edges) {
561 p(q(edge.parent.getId()) + " -> " + q(edge.child.getId()) + " [").i(1);
562 {
563 p("label=" + q(edge.getLabel()));
564 p("style=" + q(edge.getLineStyle()));
565 p("color=" + q(edge.getColor()));
566 p("fontcolor=" + q(edge.getColor()));
567 p("weight=" + edge.getWeight());
568 }
569 i(-1).p("];");
570 }
571
572 }
573 i(-1).p("}");
574 }
575
576 private String q(String value) {
577 return "\"" + value + "\"";
578 }
579
580 private DotExporter i(int indent) {
581 this.indent += indent;
582 return this;
583 }
584
585 private DotExporter p(String x) {
586 for (int i = 0; i < indent; i++) {
587 out.print(" ");
588 }
589 out.println(x);
590 return this;
591 }
592
593 }
594
595
596 }