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