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