View Javadoc
1   package org.apache.maven.plugins.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.resolver.filter.ArtifactFilter;
23  import org.apache.maven.artifact.resolver.filter.ScopeArtifactFilter;
24  import org.apache.maven.artifact.versioning.ArtifactVersion;
25  import org.apache.maven.artifact.versioning.Restriction;
26  import org.apache.maven.artifact.versioning.VersionRange;
27  import org.apache.maven.execution.MavenSession;
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.plugins.annotations.Component;
32  import org.apache.maven.plugins.annotations.Mojo;
33  import org.apache.maven.plugins.annotations.Parameter;
34  import org.apache.maven.plugins.annotations.ResolutionScope;
35  import org.apache.maven.plugins.dependency.utils.DependencyUtil;
36  import org.apache.maven.project.DefaultProjectBuildingRequest;
37  import org.apache.maven.project.MavenProject;
38  import org.apache.maven.project.ProjectBuildingRequest;
39  import org.apache.maven.shared.artifact.filter.StrictPatternExcludesArtifactFilter;
40  import org.apache.maven.shared.artifact.filter.StrictPatternIncludesArtifactFilter;
41  import org.apache.maven.shared.dependency.graph.DependencyCollectorBuilder;
42  import org.apache.maven.shared.dependency.graph.DependencyCollectorBuilderException;
43  import org.apache.maven.shared.dependency.graph.DependencyGraphBuilder;
44  import org.apache.maven.shared.dependency.graph.DependencyGraphBuilderException;
45  import org.apache.maven.shared.dependency.graph.DependencyNode;
46  import org.apache.maven.shared.dependency.graph.filter.AncestorOrSelfDependencyNodeFilter;
47  import org.apache.maven.shared.dependency.graph.filter.AndDependencyNodeFilter;
48  import org.apache.maven.shared.dependency.graph.filter.ArtifactDependencyNodeFilter;
49  import org.apache.maven.shared.dependency.graph.filter.DependencyNodeFilter;
50  import org.apache.maven.shared.dependency.graph.traversal.CollectingDependencyNodeVisitor;
51  import org.apache.maven.shared.dependency.graph.traversal.DependencyNodeVisitor;
52  import org.apache.maven.shared.dependency.graph.traversal.FilteringDependencyNodeVisitor;
53  import org.apache.maven.shared.dependency.graph.traversal.SerializingDependencyNodeVisitor;
54  import org.apache.maven.shared.dependency.graph.traversal.SerializingDependencyNodeVisitor.GraphTokens;
55  import org.eclipse.aether.RepositorySystem;
56  import org.eclipse.aether.RepositorySystemSession;
57  import org.eclipse.aether.repository.RemoteRepository;
58  
59  import java.io.File;
60  import java.io.IOException;
61  import java.io.StringWriter;
62  import java.io.Writer;
63  import java.util.ArrayList;
64  import java.util.Arrays;
65  import java.util.List;
66  import java.util.Objects;
67  
68  /**
69   * Displays the dependency tree for this project. Multiple formats are supported: text (by default), but also
70   * <a href="https://en.wikipedia.org/wiki/DOT_language">DOT</a>,
71   * <a href="https://en.wikipedia.org/wiki/GraphML">GraphML</a>, and
72   * <a href="https://en.wikipedia.org/wiki/Trivial_Graph_Format">TGF</a>.
73   *
74   * @author <a href="mailto:markhobson@gmail.com">Mark Hobson</a>
75   * @since 2.0-alpha-5
76   */
77  @Mojo( name = "tree", requiresDependencyCollection = ResolutionScope.TEST, threadSafe = true )
78  public class TreeMojo
79      extends AbstractMojo
80  {
81      // fields -----------------------------------------------------------------
82  
83      /**
84       * The Maven project.
85       */
86      @Parameter( defaultValue = "${project}", readonly = true, required = true )
87      private MavenProject project;
88  
89      @Parameter( defaultValue = "${session}", readonly = true, required = true )
90      private MavenSession session;
91      
92      @Parameter( property = "outputEncoding", defaultValue = "${project.reporting.outputEncoding}" )
93      private String outputEncoding;
94  
95      /**
96       * Contains the full list of projects in the reactor.
97       */
98      @Parameter( defaultValue = "${reactorProjects}", readonly = true, required = true )
99      private List<MavenProject> reactorProjects;
100 
101     @Component
102     private RepositorySystem repositorySystem;
103 
104     @Parameter ( defaultValue = "${repositorySystem}" )
105     RepositorySystem repositorySystemParam;
106 
107     /**
108      * The current repository/network configuration of Maven.
109      */
110     @Parameter( defaultValue = "${repositorySystemSession}" )
111     private RepositorySystemSession repoSession;
112 
113     /**
114      * The project's remote repositories to use for the resolution of project dependencies.
115      */
116     @Parameter( defaultValue = "${project.remoteProjectRepositories}" )
117     private List<RemoteRepository> projectRepos;
118 
119     /**
120      * The dependency collector builder to use.
121      */
122     @Component( hint = "default" )
123     private DependencyCollectorBuilder dependencyCollectorBuilder;
124 
125     /**
126      * The dependency graph builder to use.
127      */
128     @Component( hint = "default" )
129     private DependencyGraphBuilder dependencyGraphBuilder;
130 
131     /**
132      * If specified, this parameter will cause the dependency tree to be written to the path specified, instead of
133      * writing to the console.
134      *
135      * @since 2.0-alpha-5
136      */
137     @Parameter( property = "outputFile" )
138     private File outputFile;
139 
140     /**
141      * If specified, this parameter will cause the dependency tree to be written using the specified format. Currently
142      * supported format are: <code>text</code> (default), <code>dot</code>, <code>graphml</code> and <code>tgf</code>.
143      * These additional formats can be plotted to image files.
144      *
145      * @since 2.2
146      */
147     @Parameter( property = "outputType", defaultValue = "text" )
148     private String outputType;
149 
150     /**
151      * The scope to filter by when resolving the dependency tree, or <code>null</code> to include dependencies from all
152      * scopes.
153      *
154      * @since 2.0-alpha-5
155      */
156     @Parameter( property = "scope" )
157     private String scope;
158 
159     /**
160      * Whether to include omitted nodes in the serialized dependency tree. Notice this feature actually uses Maven 2
161      * algorithm and <a href="https://maven.apache.org/shared/maven-dependency-tree/">may give wrong results when used
162      * with Maven 3</a>.
163      *
164      * @since 2.0-alpha-6
165      */
166     @Parameter( property = "verbose", defaultValue = "false" )
167     private boolean verbose;
168 
169     /**
170      * The token set name to use when outputting the dependency tree. Possible values are <code>whitespace</code>,
171      * <code>standard</code> or <code>extended</code>, which use whitespace, standard (ie ASCII) or extended character
172      * sets respectively.
173      *
174      * @since 2.0-alpha-6
175      */
176     @Parameter( property = "tokens", defaultValue = "standard" )
177     private String tokens;
178 
179     /**
180      * A comma-separated list of artifacts to filter the serialized dependency tree by, or <code>null</code> not to
181      * filter the dependency tree. The filter syntax is:
182      * 
183      * <pre>
184      * [groupId]:[artifactId]:[type]:[version]
185      * </pre>
186      * 
187      * where each pattern segment is optional and supports full and partial <code>*</code> wildcards. An empty pattern
188      * segment is treated as an implicit wildcard.
189      * <p>
190      * For example, <code>org.apache.*</code> will match all artifacts whose group id starts with
191      * <code>org.apache.</code>, and <code>:::*-SNAPSHOT</code> will match all snapshot artifacts.
192      * </p>
193      * 
194      * @see StrictPatternIncludesArtifactFilter
195      * @since 2.0-alpha-6
196      */
197     @Parameter( property = "includes" )
198     private String includes;
199 
200     /**
201      * A comma-separated list of artifacts to filter from the serialized dependency tree, or <code>null</code> not to
202      * filter any artifacts from the dependency tree. The filter syntax is:
203      * 
204      * <pre>
205      * [groupId]:[artifactId]:[type]:[version]
206      * </pre>
207      * 
208      * where each pattern segment is optional and supports full and partial <code>*</code> wildcards. An empty pattern
209      * segment is treated as an implicit wildcard.
210      * <p>
211      * For example, <code>org.apache.*</code> will match all artifacts whose group id starts with
212      * <code>org.apache.</code>, and <code>:::*-SNAPSHOT</code> will match all snapshot artifacts.
213      * </p>
214      *
215      * @see StrictPatternExcludesArtifactFilter
216      * @since 2.0-alpha-6
217      */
218     @Parameter( property = "excludes" )
219     private String excludes;
220 
221     /**
222      * The computed dependency tree root node of the Maven project.
223      */
224     private DependencyNode rootNode;
225 
226     /**
227      * Whether to append outputs into the output file or overwrite it.
228      *
229      * @since 2.2
230      */
231     @Parameter( property = "appendOutput", defaultValue = "false" )
232     private boolean appendOutput;
233 
234     /**
235      * Skip plugin execution completely.
236      *
237      * @since 2.7
238      */
239     @Parameter( property = "skip", defaultValue = "false" )
240     private boolean skip;
241     // Mojo methods -----------------------------------------------------------
242 
243     /*
244      * @see org.apache.maven.plugin.Mojo#execute()
245      */
246     @Override
247     public void execute()
248         throws MojoExecutionException, MojoFailureException
249     {
250         if ( isSkip() )
251         {
252             getLog().info( "Skipping plugin execution" );
253             return;
254         }
255 
256         try
257         {
258             String dependencyTreeString;
259 
260             // TODO: note that filter does not get applied due to MSHARED-4
261             ArtifactFilter artifactFilter = createResolvingArtifactFilter();
262 
263             ProjectBuildingRequest buildingRequest =
264                     new DefaultProjectBuildingRequest( session.getProjectBuildingRequest() );
265 
266             buildingRequest.setProject( project );
267 
268             if ( verbose )
269             {
270                 rootNode = dependencyCollectorBuilder.collectDependencyGraph( buildingRequest, artifactFilter );
271                 dependencyTreeString = serializeDependencyTree( rootNode );
272             }
273             else
274             {
275                 // non-verbose mode use dependency graph component, which gives consistent results with Maven version
276                 // running
277                 rootNode = dependencyGraphBuilder.buildDependencyGraph( buildingRequest, artifactFilter );
278 
279                 dependencyTreeString = serializeDependencyTree( rootNode );
280             }
281 
282             if ( outputFile != null )
283             {
284                 String encoding = Objects.toString( outputEncoding, "UTF-8" );
285                 DependencyUtil.write( dependencyTreeString, outputFile, this.appendOutput, encoding );
286 
287                 getLog().info( "Wrote dependency tree to: " + outputFile );
288             }
289             else
290             {
291                 DependencyUtil.log( dependencyTreeString, getLog() );
292             }
293         }
294         catch ( DependencyGraphBuilderException | DependencyCollectorBuilderException exception )
295         {
296             throw new MojoExecutionException( "Cannot build project dependency graph", exception );
297         }
298         catch ( IOException exception )
299         {
300             throw new MojoExecutionException( "Cannot serialize project dependency graph", exception );
301         }
302     }
303 
304     // public methods ---------------------------------------------------------
305 
306     /**
307      * Gets the Maven project used by this mojo.
308      *
309      * @return the Maven project
310      */
311     public MavenProject getProject()
312     {
313         return project;
314     }
315 
316     /**
317      * Gets the computed dependency graph root node for the Maven project.
318      *
319      * @return the dependency tree root node
320      */
321     public DependencyNode getDependencyGraph()
322     {
323         return rootNode;
324     }
325 
326     /**
327      * @return {@link #skip}
328      */
329     public boolean isSkip()
330     {
331         return skip;
332     }
333 
334     /**
335      * @param skip {@link #skip}
336      */
337     public void setSkip( boolean skip )
338     {
339         this.skip = skip;
340     }
341 
342     // private methods --------------------------------------------------------
343 
344     /**
345      * Gets the artifact filter to use when resolving the dependency tree.
346      *
347      * @return the artifact filter
348      */
349     private ArtifactFilter createResolvingArtifactFilter()
350     {
351         ArtifactFilter filter;
352 
353         // filter scope
354         if ( scope != null )
355         {
356             getLog().debug( "+ Resolving dependency tree for scope '" + scope + "'" );
357 
358             filter = new ScopeArtifactFilter( scope );
359         }
360         else
361         {
362             filter = null;
363         }
364 
365         return filter;
366     }
367 
368     /**
369      * Serializes the specified dependency tree to a string.
370      *
371      * @param theRootNode the dependency tree root node to serialize
372      * @return the serialized dependency tree
373      */
374     private String serializeDependencyTree( DependencyNode theRootNode )
375     {
376         StringWriter writer = new StringWriter();
377 
378         DependencyNodeVisitor visitor = getSerializingDependencyNodeVisitor( writer );
379 
380         // TODO: remove the need for this when the serializer can calculate last nodes from visitor calls only
381         visitor = new BuildingDependencyNodeVisitor( visitor );
382 
383         DependencyNodeFilter filter = createDependencyNodeFilter();
384 
385         if ( filter != null )
386         {
387             CollectingDependencyNodeVisitor collectingVisitor = new CollectingDependencyNodeVisitor();
388             DependencyNodeVisitor firstPassVisitor = new FilteringDependencyNodeVisitor( collectingVisitor, filter );
389             theRootNode.accept( firstPassVisitor );
390 
391             DependencyNodeFilter secondPassFilter =
392                 new AncestorOrSelfDependencyNodeFilter( collectingVisitor.getNodes() );
393             visitor = new FilteringDependencyNodeVisitor( visitor, secondPassFilter );
394         }
395 
396         theRootNode.accept( visitor );
397 
398         return writer.toString();
399     }
400 
401     /**
402      * @param writer {@link Writer}
403      * @return {@link DependencyNodeVisitor}
404      */
405     public DependencyNodeVisitor getSerializingDependencyNodeVisitor( Writer writer )
406     {
407         if ( "graphml".equals( outputType ) )
408         {
409             return new GraphmlDependencyNodeVisitor( writer );
410         }
411         else if ( "tgf".equals( outputType ) )
412         {
413             return new TGFDependencyNodeVisitor( writer );
414         }
415         else if ( "dot".equals( outputType ) )
416         {
417             return new DOTDependencyNodeVisitor( writer );
418         }
419         else
420         {
421             return new SerializingDependencyNodeVisitor( writer, toGraphTokens( tokens ) );
422         }
423     }
424 
425     /**
426      * Gets the graph tokens instance for the specified name.
427      *
428      * @param theTokens the graph tokens name
429      * @return the <code>GraphTokens</code> instance
430      */
431     private GraphTokens toGraphTokens( String theTokens )
432     {
433         GraphTokens graphTokens;
434 
435         if ( "whitespace".equals( theTokens ) )
436         {
437             getLog().debug( "+ Using whitespace tree tokens" );
438 
439             graphTokens = SerializingDependencyNodeVisitor.WHITESPACE_TOKENS;
440         }
441         else if ( "extended".equals( theTokens ) )
442         {
443             getLog().debug( "+ Using extended tree tokens" );
444 
445             graphTokens = SerializingDependencyNodeVisitor.EXTENDED_TOKENS;
446         }
447         else
448         {
449             graphTokens = SerializingDependencyNodeVisitor.STANDARD_TOKENS;
450         }
451 
452         return graphTokens;
453     }
454 
455     /**
456      * Gets the dependency node filter to use when serializing the dependency graph.
457      *
458      * @return the dependency node filter, or <code>null</code> if none required
459      */
460     private DependencyNodeFilter createDependencyNodeFilter()
461     {
462         List<DependencyNodeFilter> filters = new ArrayList<>();
463 
464         // filter includes
465         if ( includes != null )
466         {
467             List<String> patterns = Arrays.asList( includes.split( "," ) );
468 
469             getLog().debug( "+ Filtering dependency tree by artifact include patterns: " + patterns );
470 
471             ArtifactFilter artifactFilter = new StrictPatternIncludesArtifactFilter( patterns );
472             filters.add( new ArtifactDependencyNodeFilter( artifactFilter ) );
473         }
474 
475         // filter excludes
476         if ( excludes != null )
477         {
478             List<String> patterns = Arrays.asList( excludes.split( "," ) );
479 
480             getLog().debug( "+ Filtering dependency tree by artifact exclude patterns: " + patterns );
481 
482             ArtifactFilter artifactFilter = new StrictPatternExcludesArtifactFilter( patterns );
483             filters.add( new ArtifactDependencyNodeFilter( artifactFilter ) );
484         }
485 
486         return filters.isEmpty() ? null : new AndDependencyNodeFilter( filters );
487     }
488 
489     // following is required because the version handling in maven code
490     // doesn't work properly. I ripped it out of the enforcer rules.
491 
492     /**
493      * Copied from Artifact.VersionRange. This is tweaked to handle singular ranges properly. Currently the default
494      * containsVersion method assumes a singular version means allow everything. This method assumes that "2.0.4" ==
495      * "[2.0.4,)"
496      *
497      * @param allowedRange range of allowed versions.
498      * @param theVersion the version to be checked.
499      * @return true if the version is contained by the range.
500      * @deprecated This method is unused in this project and will be removed in the future.
501      */
502     @Deprecated
503     public static boolean containsVersion( VersionRange allowedRange, ArtifactVersion theVersion )
504     {
505         ArtifactVersion recommendedVersion = allowedRange.getRecommendedVersion();
506         if ( recommendedVersion == null )
507         {
508             List<Restriction> restrictions = allowedRange.getRestrictions();
509             for ( Restriction restriction : restrictions )
510             {
511                 if ( restriction.containsVersion( theVersion ) )
512                 {
513                     return true;
514                 }
515             }
516             return false;
517         }
518         else
519         {
520             // only singular versions ever have a recommendedVersion
521             return recommendedVersion.compareTo( theVersion ) <= 0;
522         }
523     }
524 }