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    }