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