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    }