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