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