View Javadoc
1   package org.apache.maven.plugin.dependency.tree;
2   
3   /*
4    * Licensed to the Apache Software Foundation (ASF) under one
5    * or more contributor license agreements.  See the NOTICE file
6    * distributed with this work for additional information
7    * regarding copyright ownership.  The ASF licenses this file
8    * to you under the Apache License, Version 2.0 (the
9    * "License"); you may not use this file except in compliance
10   * with the License.  You may obtain a copy of the License at
11   *
12   * http://www.apache.org/licenses/LICENSE-2.0
13   *
14   * Unless required by applicable law or agreed to in writing,
15   * software distributed under the License is distributed on an
16   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
17   * KIND, either express or implied.  See the License for the
18   * specific language governing permissions and limitations
19   * under the License.
20   */
21  
22  import org.apache.maven.artifact.repository.ArtifactRepository;
23  import org.apache.maven.artifact.resolver.filter.ArtifactFilter;
24  import org.apache.maven.artifact.resolver.filter.ScopeArtifactFilter;
25  import org.apache.maven.artifact.versioning.ArtifactVersion;
26  import org.apache.maven.artifact.versioning.Restriction;
27  import org.apache.maven.artifact.versioning.VersionRange;
28  import org.apache.maven.plugin.AbstractMojo;
29  import org.apache.maven.plugin.MojoExecutionException;
30  import org.apache.maven.plugin.MojoFailureException;
31  import org.apache.maven.plugin.dependency.utils.DependencyUtil;
32  import org.apache.maven.plugins.annotations.Component;
33  import org.apache.maven.plugins.annotations.Mojo;
34  import org.apache.maven.plugins.annotations.Parameter;
35  import org.apache.maven.plugins.annotations.ResolutionScope;
36  import org.apache.maven.project.MavenProject;
37  import org.apache.maven.shared.artifact.filter.StrictPatternExcludesArtifactFilter;
38  import org.apache.maven.shared.artifact.filter.StrictPatternIncludesArtifactFilter;
39  import org.apache.maven.shared.dependency.graph.DependencyGraphBuilder;
40  import org.apache.maven.shared.dependency.graph.DependencyGraphBuilderException;
41  import org.apache.maven.shared.dependency.graph.DependencyNode;
42  import org.apache.maven.shared.dependency.graph.filter.AncestorOrSelfDependencyNodeFilter;
43  import org.apache.maven.shared.dependency.graph.filter.AndDependencyNodeFilter;
44  import org.apache.maven.shared.dependency.graph.filter.ArtifactDependencyNodeFilter;
45  import org.apache.maven.shared.dependency.graph.filter.DependencyNodeFilter;
46  import org.apache.maven.shared.dependency.graph.traversal.BuildingDependencyNodeVisitor;
47  import org.apache.maven.shared.dependency.graph.traversal.CollectingDependencyNodeVisitor;
48  import org.apache.maven.shared.dependency.graph.traversal.DependencyNodeVisitor;
49  import org.apache.maven.shared.dependency.graph.traversal.FilteringDependencyNodeVisitor;
50  import org.apache.maven.shared.dependency.graph.traversal.SerializingDependencyNodeVisitor;
51  import org.apache.maven.shared.dependency.graph.traversal.SerializingDependencyNodeVisitor.GraphTokens;
52  import org.apache.maven.shared.dependency.tree.DependencyTreeBuilder;
53  import org.apache.maven.shared.dependency.tree.DependencyTreeBuilderException;
54  import org.apache.maven.shared.dependency.tree.traversal.SerializingDependencyNodeVisitor.TreeTokens;
55  
56  import java.io.File;
57  import java.io.IOException;
58  import java.io.StringWriter;
59  import java.io.Writer;
60  import java.util.ArrayList;
61  import java.util.Arrays;
62  import java.util.List;
63  
64  /**
65   * Displays the dependency tree for this project.
66   *
67   * @author <a href="mailto:markhobson@gmail.com">Mark Hobson</a>
68   * @version $Id: TreeMojo.html 937155 2015-01-21 21:53:50Z khmarbaise $
69   * @since 2.0-alpha-5
70   */
71  @Mojo( name = "tree", requiresDependencyResolution = ResolutionScope.TEST, threadSafe = true )
72  public class TreeMojo
73      extends AbstractMojo
74  {
75      // fields -----------------------------------------------------------------
76  
77      /**
78       * The Maven project.
79       */
80      @Parameter( defaultValue = "${project}", readonly = true, required = true )
81      private MavenProject project;
82  
83      /**
84       * The dependency tree builder to use.
85       */
86      @Component( hint = "default" )
87      private DependencyGraphBuilder dependencyGraphBuilder;
88  
89      /**
90       * The dependency tree builder to use for verbose output.
91       */
92      @Component
93      private DependencyTreeBuilder dependencyTreeBuilder;
94  
95      /**
96       * If specified, this parameter will cause the dependency tree to be written to the path specified, instead of
97       * writing to the console.
98       *
99       * @deprecated use outputFile instead.
100      */
101     @Parameter( property = "output" )
102     private File output;
103 
104     /**
105      * If specified, this parameter will cause the dependency tree to be written to the path specified, instead of
106      * writing to the console.
107      *
108      * @since 2.0-alpha-5
109      */
110     @Parameter( property = "outputFile" )
111     private File outputFile;
112 
113     /**
114      * If specified, this parameter will cause the dependency tree to be written using the specified format. Currently
115      * supported format are: <code>text</code>, <code>dot</code>, <code>graphml</code> and <code>tgf</code>.
116      * <p/>
117      * These formats can be plotted to image files. An example of how to plot a dot file using
118      * pygraphviz can be found <a href="http://networkx.lanl.gov/pygraphviz/tutorial.html#layout-and-drawing">here</a>.
119      *
120      * @since 2.2
121      */
122     @Parameter( property = "outputType", defaultValue = "text" )
123     private String outputType;
124 
125     /**
126      * The scope to filter by when resolving the dependency tree, or <code>null</code> to include dependencies from
127      * all scopes. Note that this feature does not currently work due to MNG-3236.
128      *
129      * @see <a href="http://jira.codehaus.org/browse/MNG-3236">MNG-3236</a>
130      * @since 2.0-alpha-5
131      */
132     @Parameter( property = "scope" )
133     private String scope;
134 
135     /**
136      * Whether to include omitted nodes in the serialized dependency tree. Notice this feature actually uses Maven 2
137      * algorithm and <a href="http://maven.apache.org/shared/maven-dependency-tree/">may give wrong results when used
138      * with Maven 3</a>.
139      *
140      * @since 2.0-alpha-6
141      */
142     @Parameter( property = "verbose", defaultValue = "false" )
143     private boolean verbose;
144 
145     /**
146      * The token set name to use when outputting the dependency tree. Possible values are <code>whitespace</code>,
147      * <code>standard</code> or <code>extended</code>, which use whitespace, standard (ie ASCII) or extended
148      * character sets respectively.
149      *
150      * @since 2.0-alpha-6
151      */
152     @Parameter( property = "tokens", defaultValue = "standard" )
153     private String tokens;
154 
155     /**
156      * A comma-separated list of artifacts to filter the serialized dependency tree by, or <code>null</code> not to
157      * filter the dependency tree. The filter syntax is:
158      * 
159      * <pre>
160      * [groupId]:[artifactId]:[type]:[version]
161      * </pre>
162      * 
163      * where each pattern segment is optional and supports full and partial <code>*</code> wildcards. An empty pattern
164      * segment is treated as an implicit wildcard.
165      * <p>For example, <code>org.apache.*</code> will match all artifacts whose group id starts with
166      * <code>org.apache.</code>, and <code>:::*-SNAPSHOT</code> will match all snapshot artifacts.</p>
167      * 
168      * @see StrictPatternIncludesArtifactFilter
169      * @since 2.0-alpha-6
170      */
171     @Parameter( property = "includes" )
172     private String includes;
173 
174     /**
175      * A comma-separated list of artifacts to filter from the serialized dependency tree, or <code>null</code> not to
176      * filter any artifacts from the dependency tree. The filter syntax is:
177      * 
178      * <pre>
179      * [groupId]:[artifactId]:[type]:[version]
180      * </pre>
181      * 
182      * where each pattern segment is optional and supports full and partial <code>*</code> wildcards. An empty pattern
183      * segment is treated as an implicit wildcard.
184      * <p>For example, <code>org.apache.*</code> will match all artifacts whose group id starts with
185      * <code>org.apache.</code>, and <code>:::*-SNAPSHOT</code> will match all snapshot artifacts.</p>
186      *
187      * @see StrictPatternExcludesArtifactFilter
188      * @since 2.0-alpha-6
189      */
190     @Parameter( property = "excludes" )
191     private String excludes;
192 
193     /**
194      * The computed dependency tree root node of the Maven project.
195      */
196     private DependencyNode rootNode;
197 
198     /**
199      * Whether to append outputs into the output file or overwrite it.
200      *
201      * @since 2.2
202      */
203     @Parameter( property = "appendOutput", defaultValue = "false" )
204     private boolean appendOutput;
205 
206     /**
207      * Skip plugin execution completely.
208      *
209      * @since 2.7
210      */
211     @Parameter( property = "skip", defaultValue = "false" )
212     private boolean skip;
213 
214     @Parameter( defaultValue = "${localRepository}", readonly = true )
215     private ArtifactRepository localRepository;
216 
217     // Mojo methods -----------------------------------------------------------
218 
219     /*
220      * @see org.apache.maven.plugin.Mojo#execute()
221      */
222     public void execute()
223         throws MojoExecutionException, MojoFailureException
224     {
225         if ( isSkip() )
226         {
227             getLog().info( "Skipping plugin execution" );
228             return;
229         }
230 
231         if ( output != null )
232         {
233             getLog().warn( "The parameter output is deprecated. Use outputFile instead." );
234             this.outputFile = output;
235         }
236 
237         try
238         {
239             String dependencyTreeString;
240 
241             // TODO: note that filter does not get applied due to MNG-3236
242 
243             ArtifactFilter artifactFilter = createResolvingArtifactFilter();
244 
245             if ( verbose )
246             {
247                 // verbose mode force Maven 2 dependency tree component use
248                 if ( ! isMaven2x() )
249                 {
250                     getLog().warn( "Using Maven 2 dependency tree to get verbose output, "
251                                        + "which may be inconsistent with actual Maven 3 resolution" );
252                 }
253                 dependencyTreeString =
254                     serializeVerboseDependencyTree( dependencyTreeBuilder.buildDependencyTree( project,
255                                                                                                localRepository,
256                                                                                                artifactFilter ) );
257             }
258             else
259             {
260                 // non-verbose mode use dependency graph component, which gives consistent results with Maven version
261                 // running
262                 rootNode = dependencyGraphBuilder.buildDependencyGraph( project, artifactFilter );
263 
264                 dependencyTreeString = serializeDependencyTree( rootNode );
265             }
266 
267             if ( outputFile != null )
268             {
269                 DependencyUtil.write( dependencyTreeString, outputFile, this.appendOutput, getLog() );
270 
271                 getLog().info( "Wrote dependency tree to: " + outputFile );
272             }
273             else
274             {
275                 DependencyUtil.log( dependencyTreeString, getLog() );
276             }
277         }
278         catch ( DependencyGraphBuilderException exception )
279         {
280             throw new MojoExecutionException( "Cannot build project dependency graph", exception );
281         }
282         catch ( DependencyTreeBuilderException exception )
283         {
284             throw new MojoExecutionException( "Cannot build project dependency tree", exception );
285         }
286         catch ( IOException exception )
287         {
288             throw new MojoExecutionException( "Cannot serialise project dependency graph", exception );
289         }
290     }
291 
292     // public methods ---------------------------------------------------------
293 
294     /**
295      * Gets the Maven project used by this mojo.
296      *
297      * @return the Maven project
298      */
299     public MavenProject getProject()
300     {
301         return project;
302     }
303 
304     /**
305      * Gets the computed dependency graph root node for the Maven project.
306      *
307      * @return the dependency tree root node
308      */
309     public DependencyNode getDependencyGraph()
310     {
311         return rootNode;
312     }
313 
314     public boolean isSkip()
315     {
316         return skip;
317     }
318 
319     public void setSkip( boolean skip )
320     {
321         this.skip = skip;
322     }
323 
324     // private methods --------------------------------------------------------
325 
326     /**
327      * Gets the artifact filter to use when resolving the dependency tree.
328      *
329      * @return the artifact filter
330      */
331     private ArtifactFilter createResolvingArtifactFilter()
332     {
333         ArtifactFilter filter;
334 
335         // filter scope
336         if ( scope != null )
337         {
338             getLog().debug( "+ Resolving dependency tree for scope '" + scope + "'" );
339 
340             filter = new ScopeArtifactFilter( scope );
341         }
342         else
343         {
344             filter = null;
345         }
346 
347         return filter;
348     }
349 
350     /**
351      * Serializes the specified dependency tree to a string.
352      *
353      * @param rootNode the dependency tree root node to serialize
354      * @return the serialized dependency tree
355      */
356     private String serializeDependencyTree( DependencyNode rootNode )
357     {
358         StringWriter writer = new StringWriter();
359 
360         DependencyNodeVisitor visitor = getSerializingDependencyNodeVisitor( writer );
361 
362         // TODO: remove the need for this when the serializer can calculate last nodes from visitor calls only
363         visitor = new BuildingDependencyNodeVisitor( visitor );
364 
365         DependencyNodeFilter filter = createDependencyNodeFilter();
366 
367         if ( filter != null )
368         {
369             CollectingDependencyNodeVisitor collectingVisitor = new CollectingDependencyNodeVisitor();
370             DependencyNodeVisitor firstPassVisitor = new FilteringDependencyNodeVisitor( collectingVisitor, filter );
371             rootNode.accept( firstPassVisitor );
372 
373             DependencyNodeFilter secondPassFilter =
374                 new AncestorOrSelfDependencyNodeFilter( collectingVisitor.getNodes() );
375             visitor = new FilteringDependencyNodeVisitor( visitor, secondPassFilter );
376         }
377 
378         rootNode.accept( visitor );
379 
380         return writer.toString();
381     }
382 
383     /**
384      * Serializes the specified dependency tree to a string.
385      *
386      * @param rootNode the dependency tree root node to serialize
387      * @return the serialized dependency tree
388      */
389     private String serializeVerboseDependencyTree( org.apache.maven.shared.dependency.tree.DependencyNode rootNode )
390     {
391         StringWriter writer = new StringWriter();
392 
393         final TreeTokens treeTokens = toTreeTokens( tokens );
394         org.apache.maven.shared.dependency.tree.traversal.DependencyNodeVisitor visitor =
395             new org.apache.maven.shared.dependency.tree.traversal.SerializingDependencyNodeVisitor( writer,
396                                                                                                     treeTokens );
397 
398         // TODO: remove the need for this when the serializer can calculate last nodes from visitor calls only
399         visitor = new org.apache.maven.shared.dependency.tree.traversal.BuildingDependencyNodeVisitor( visitor );
400 
401         org.apache.maven.shared.dependency.tree.filter.DependencyNodeFilter filter =
402             createVerboseDependencyNodeFilter();
403 
404         if ( filter != null )
405         {
406             org.apache.maven.shared.dependency.tree.traversal.CollectingDependencyNodeVisitor collectingVisitor =
407                 new org.apache.maven.shared.dependency.tree.traversal.CollectingDependencyNodeVisitor();
408             org.apache.maven.shared.dependency.tree.traversal.DependencyNodeVisitor firstPassVisitor =
409                 new org.apache.maven.shared.dependency.tree.traversal.FilteringDependencyNodeVisitor( collectingVisitor,
410                                                                                                       filter );
411             rootNode.accept( firstPassVisitor );
412 
413             final List<org.apache.maven.shared.dependency.tree.DependencyNode> nodes = collectingVisitor.getNodes();
414             org.apache.maven.shared.dependency.tree.filter.DependencyNodeFilter secondPass =
415                 new org.apache.maven.shared.dependency.tree.filter.AncestorOrSelfDependencyNodeFilter( nodes );
416             visitor =
417                 new org.apache.maven.shared.dependency.tree.traversal.FilteringDependencyNodeVisitor( visitor,
418                                                                                                       secondPass );
419         }
420 
421         rootNode.accept( visitor );
422 
423         return writer.toString();
424     }
425 
426     public DependencyNodeVisitor getSerializingDependencyNodeVisitor( Writer writer )
427     {
428         if ( "graphml".equals( outputType ) )
429         {
430             return new GraphmlDependencyNodeVisitor( writer );
431         }
432         else if ( "tgf".equals( outputType ) )
433         {
434             return new TGFDependencyNodeVisitor( writer );
435         }
436         else if ( "dot".equals( outputType ) )
437         {
438             return new DOTDependencyNodeVisitor( writer );
439         }
440         else
441         {
442             return new SerializingDependencyNodeVisitor( writer, toGraphTokens( tokens ) );
443         }
444     }
445 
446     /**
447      * Gets the graph tokens instance for the specified name.
448      *
449      * @param tokens the graph tokens name
450      * @return the <code>GraphTokens</code> instance
451      */
452     private GraphTokens toGraphTokens( String tokens )
453     {
454         GraphTokens graphTokens;
455 
456         if ( "whitespace".equals( tokens ) )
457         {
458             getLog().debug( "+ Using whitespace tree tokens" );
459 
460             graphTokens = SerializingDependencyNodeVisitor.WHITESPACE_TOKENS;
461         }
462         else if ( "extended".equals( tokens ) )
463         {
464             getLog().debug( "+ Using extended tree tokens" );
465 
466             graphTokens = SerializingDependencyNodeVisitor.EXTENDED_TOKENS;
467         }
468         else
469         {
470             graphTokens = SerializingDependencyNodeVisitor.STANDARD_TOKENS;
471         }
472 
473         return graphTokens;
474     }
475 
476     /**
477      * Gets the tree tokens instance for the specified name.
478      *
479      * @param tokens the tree tokens name
480      * @return the <code>TreeTokens</code> instance
481      */
482     private TreeTokens toTreeTokens( String tokens )
483     {
484         TreeTokens treeTokens;
485 
486         if ( "whitespace".equals( tokens ) )
487         {
488             getLog().debug( "+ Using whitespace tree tokens" );
489 
490             treeTokens =
491                 org.apache.maven.shared.dependency.tree.traversal.SerializingDependencyNodeVisitor.WHITESPACE_TOKENS;
492         }
493         else if ( "extended".equals( tokens ) )
494         {
495             getLog().debug( "+ Using extended tree tokens" );
496 
497             treeTokens =
498                 org.apache.maven.shared.dependency.tree.traversal.SerializingDependencyNodeVisitor.EXTENDED_TOKENS;
499         }
500         else
501         {
502             treeTokens =
503                 org.apache.maven.shared.dependency.tree.traversal.SerializingDependencyNodeVisitor.STANDARD_TOKENS;
504         }
505 
506         return treeTokens;
507     }
508 
509     /**
510      * Gets the dependency node filter to use when serializing the dependency graph.
511      *
512      * @return the dependency node filter, or <code>null</code> if none required
513      */
514     private DependencyNodeFilter createDependencyNodeFilter()
515     {
516         List<DependencyNodeFilter> filters = new ArrayList<DependencyNodeFilter>();
517 
518         // filter includes
519         if ( includes != null )
520         {
521             List<String> patterns = Arrays.asList( includes.split( "," ) );
522 
523             getLog().debug( "+ Filtering dependency tree by artifact include patterns: " + patterns );
524 
525             ArtifactFilter artifactFilter = new StrictPatternIncludesArtifactFilter( patterns );
526             filters.add( new ArtifactDependencyNodeFilter( artifactFilter ) );
527         }
528 
529         // filter excludes
530         if ( excludes != null )
531         {
532             List<String> patterns = Arrays.asList( excludes.split( "," ) );
533 
534             getLog().debug( "+ Filtering dependency tree by artifact exclude patterns: " + patterns );
535 
536             ArtifactFilter artifactFilter = new StrictPatternExcludesArtifactFilter( patterns );
537             filters.add( new ArtifactDependencyNodeFilter( artifactFilter ) );
538         }
539 
540         return filters.isEmpty() ? null : new AndDependencyNodeFilter( filters );
541     }
542 
543     /**
544      * Gets the dependency node filter to use when serializing the dependency tree.
545      *
546      * @return the dependency node filter, or <code>null</code> if none required
547      */
548     private org.apache.maven.shared.dependency.tree.filter.DependencyNodeFilter createVerboseDependencyNodeFilter()
549     {
550         List<org.apache.maven.shared.dependency.tree.filter.DependencyNodeFilter> filters =
551             new ArrayList<org.apache.maven.shared.dependency.tree.filter.DependencyNodeFilter>();
552         org.apache.maven.shared.dependency.tree.filter.DependencyNodeFilter filter;
553 
554         // filter includes
555         if ( includes != null )
556         {
557             List<String> patterns = Arrays.asList( includes.split( "," ) );
558 
559             getLog().debug( "+ Filtering dependency tree by artifact include patterns: " + patterns );
560 
561             ArtifactFilter artifactFilter = new StrictPatternIncludesArtifactFilter( patterns );
562             filter = new org.apache.maven.shared.dependency.tree.filter.ArtifactDependencyNodeFilter( artifactFilter );
563             filters.add( filter );
564         }
565 
566         // filter excludes
567         if ( excludes != null )
568         {
569             List<String> patterns = Arrays.asList( excludes.split( "," ) );
570 
571             getLog().debug( "+ Filtering dependency tree by artifact exclude patterns: " + patterns );
572 
573             ArtifactFilter artifactFilter = new StrictPatternExcludesArtifactFilter( patterns );
574             filter = new org.apache.maven.shared.dependency.tree.filter.ArtifactDependencyNodeFilter( artifactFilter );
575             filters.add( filter );
576         }
577 
578         return filters.isEmpty() ? null
579                         : new org.apache.maven.shared.dependency.tree.filter.AndDependencyNodeFilter( filters );
580     }
581 
582     //following is required because the version handling in maven code
583     //doesn't work properly. I ripped it out of the enforcer rules.
584 
585 
586     /**
587      * Copied from Artifact.VersionRange. This is tweaked to handle singular ranges properly. Currently the default
588      * containsVersion method assumes a singular version means allow everything. This method assumes that "2.0.4" ==
589      * "[2.0.4,)"
590      *
591      * @param allowedRange range of allowed versions.
592      * @param theVersion   the version to be checked.
593      * @return true if the version is contained by the range.
594      */
595     public static boolean containsVersion( VersionRange allowedRange, ArtifactVersion theVersion )
596     {
597         ArtifactVersion recommendedVersion = allowedRange.getRecommendedVersion();
598         if ( recommendedVersion == null )
599         {
600             @SuppressWarnings( "unchecked" )
601             List<Restriction> restrictions = allowedRange.getRestrictions();
602             for ( Restriction restriction : restrictions )
603             {
604                 if ( restriction.containsVersion( theVersion ) )
605                 {
606                     return true;
607                 }
608             }
609         }
610 
611         // only singular versions ever have a recommendedVersion
612         return recommendedVersion.compareTo( theVersion ) <= 0;
613     }
614 
615     /**
616      * Check the current Maven version to see if it's Maven 2.x.
617      */
618     protected static boolean isMaven2x()
619     {
620         return !canFindCoreClass( "org.apache.maven.project.DependencyResolutionRequest" ); // Maven 3 specific
621     }
622 
623     private static boolean canFindCoreClass( String className )
624     {
625         try
626         {
627             Thread.currentThread().getContextClassLoader().loadClass( className );
628 
629             return true;
630         }
631         catch ( ClassNotFoundException e )
632         {
633             return false;
634         }
635     }
636 }