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