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