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    }