View Javadoc
1   package org.apache.maven.plugins.dependency.analyze;
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 java.io.File;
23  import java.io.StringWriter;
24  import java.util.ArrayList;
25  import java.util.Arrays;
26  import java.util.Iterator;
27  import java.util.LinkedHashSet;
28  import java.util.List;
29  import java.util.Set;
30  
31  import org.apache.commons.lang.StringUtils;
32  import org.apache.maven.artifact.Artifact;
33  import org.apache.maven.artifact.resolver.filter.ArtifactFilter;
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.Parameter;
38  import org.apache.maven.project.MavenProject;
39  import org.apache.maven.shared.artifact.filter.StrictPatternExcludesArtifactFilter;
40  import org.apache.maven.shared.dependency.analyzer.ProjectDependencyAnalysis;
41  import org.apache.maven.shared.dependency.analyzer.ProjectDependencyAnalyzer;
42  import org.apache.maven.shared.dependency.analyzer.ProjectDependencyAnalyzerException;
43  import org.codehaus.plexus.PlexusConstants;
44  import org.codehaus.plexus.PlexusContainer;
45  import org.codehaus.plexus.context.Context;
46  import org.codehaus.plexus.context.ContextException;
47  import org.codehaus.plexus.personality.plexus.lifecycle.phase.Contextualizable;
48  import org.codehaus.plexus.util.xml.PrettyPrintXMLWriter;
49  
50  /**
51   * Analyzes the dependencies of this project and determines which are: used and declared; used and undeclared; unused
52   * and declared.
53   *
54   * @author <a href="mailto:markhobson@gmail.com">Mark Hobson</a>
55   * @since 2.0-alpha-5
56   */
57  public abstract class AbstractAnalyzeMojo
58      extends AbstractMojo
59      implements Contextualizable
60  {
61      // fields -----------------------------------------------------------------
62  
63      /**
64       * The plexus context to look-up the right {@link ProjectDependencyAnalyzer} implementation depending on the mojo
65       * configuration.
66       */
67      private Context context;
68  
69      /**
70       * The Maven project to analyze.
71       */
72      @Parameter( defaultValue = "${project}", readonly = true, required = true )
73      private MavenProject project;
74  
75      /**
76       * Specify the project dependency analyzer to use (plexus component role-hint). By default,
77       * <a href="/shared/maven-dependency-analyzer/">maven-dependency-analyzer</a> is used. To use this, you must declare
78       * a dependency for this plugin that contains the code for the analyzer. The analyzer must have a declared Plexus
79       * role name, and you specify the role name here.
80       *
81       * @since 2.2
82       */
83      @Parameter( property = "analyzer", defaultValue = "default" )
84      private String analyzer;
85  
86      /**
87       * Whether to fail the build if a dependency warning is found.
88       */
89      @Parameter( property = "failOnWarning", defaultValue = "false" )
90      private boolean failOnWarning;
91  
92      /**
93       * Output used dependencies.
94       */
95      @Parameter( property = "verbose", defaultValue = "false" )
96      private boolean verbose;
97  
98      /**
99       * Ignore Runtime/Provided/Test/System scopes for unused dependency analysis.
100      */
101     @Parameter( property = "ignoreNonCompile", defaultValue = "false" )
102     private boolean ignoreNonCompile;
103 
104     /**
105      * Output the xml for the missing dependencies (used but not declared).
106      *
107      * @since 2.0-alpha-5
108      */
109     @Parameter( property = "outputXML", defaultValue = "false" )
110     private boolean outputXML;
111 
112     /**
113      * Output scriptable values for the missing dependencies (used but not declared).
114      *
115      * @since 2.0-alpha-5
116      */
117     @Parameter( property = "scriptableOutput", defaultValue = "false" )
118     private boolean scriptableOutput;
119 
120     /**
121      * Flag to use for scriptable output.
122      *
123      * @since 2.0-alpha-5
124      */
125     @Parameter( property = "scriptableFlag", defaultValue = "$$$%%%" )
126     private String scriptableFlag;
127 
128     /**
129      * Flag to use for scriptable output
130      *
131      * @since 2.0-alpha-5
132      */
133     @Parameter( defaultValue = "${basedir}", readonly = true )
134     private File baseDir;
135 
136     /**
137      * Target folder
138      *
139      * @since 2.0-alpha-5
140      */
141     @Parameter( defaultValue = "${project.build.directory}", readonly = true )
142     private File outputDirectory;
143 
144     /**
145      * Force dependencies as used, to override incomplete result caused by bytecode-level analysis. Dependency format is
146      * <code>groupId:artifactId</code>.
147      *
148      * @since 2.6
149      */
150     @Parameter
151     private String[] usedDependencies;
152 
153     /**
154      * Skip plugin execution completely.
155      *
156      * @since 2.7
157      */
158     @Parameter( property = "mdep.analyze.skip", defaultValue = "false" )
159     private boolean skip;
160 
161     /**
162      * List of dependencies that will be ignored. Any dependency on this list will be excluded from the "declared but
163      * unused" and the "used but undeclared" list. The filter syntax is:
164      *
165      * <pre>
166      * [groupId]:[artifactId]:[type]:[version]
167      * </pre>
168      *
169      * where each pattern segment is optional and supports full and partial <code>*</code> wildcards. An empty pattern
170      * segment is treated as an implicit wildcard. *
171      * <p>
172      * For example, <code>org.apache.*</code> will match all artifacts whose group id starts with
173      * <code>org.apache.</code>, and <code>:::*-SNAPSHOT</code> will match all snapshot artifacts.
174      * </p>
175      *
176      * @since 2.10
177      */
178     @Parameter
179     private String[] ignoredDependencies = new String[0];
180 
181     /**
182      * List of dependencies that will be ignored if they are used but undeclared. The filter syntax is:
183      *
184      * <pre>
185      * [groupId]:[artifactId]:[type]:[version]
186      * </pre>
187      *
188      * where each pattern segment is optional and supports full and partial <code>*</code> wildcards. An empty pattern
189      * segment is treated as an implicit wildcard. *
190      * <p>
191      * For example, <code>org.apache.*</code> will match all artifacts whose group id starts with
192      * <code>org.apache.</code>, and <code>:::*-SNAPSHOT</code> will match all snapshot artifacts.
193      * </p>
194      *
195      * @since 2.10
196      */
197     @Parameter
198     private String[] ignoredUsedUndeclaredDependencies = new String[0];
199 
200     /**
201      * List of dependencies that will be ignored if they are declared but unused. 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      * @since 2.10
215      */
216     @Parameter
217     private String[] ignoredUnusedDeclaredDependencies = new String[0];
218 
219     // Mojo methods -----------------------------------------------------------
220 
221     /*
222      * @see org.apache.maven.plugin.Mojo#execute()
223      */
224     @Override
225     public void execute()
226         throws MojoExecutionException, MojoFailureException
227     {
228         if ( isSkip() )
229         {
230             getLog().info( "Skipping plugin execution" );
231             return;
232         }
233 
234         if ( "pom".equals( project.getPackaging() ) )
235         {
236             getLog().info( "Skipping pom project" );
237             return;
238         }
239 
240         if ( outputDirectory == null || !outputDirectory.exists() )
241         {
242             getLog().info( "Skipping project with no build directory" );
243             return;
244         }
245 
246         boolean warning = checkDependencies();
247 
248         if ( warning && failOnWarning )
249         {
250             throw new MojoExecutionException( "Dependency problems found" );
251         }
252     }
253 
254     /**
255      * @return {@link ProjectDependencyAnalyzer}
256      * @throws MojoExecutionException in case of an error.
257      */
258     protected ProjectDependencyAnalyzer createProjectDependencyAnalyzer()
259         throws MojoExecutionException
260     {
261 
262         final String role = ProjectDependencyAnalyzer.ROLE;
263         final String roleHint = analyzer;
264 
265         try
266         {
267             final PlexusContainer container = (PlexusContainer) context.get( PlexusConstants.PLEXUS_KEY );
268 
269             return (ProjectDependencyAnalyzer) container.lookup( role, roleHint );
270         }
271         catch ( Exception exception )
272         {
273             throw new MojoExecutionException( "Failed to instantiate ProjectDependencyAnalyser with role " + role
274                 + " / role-hint " + roleHint, exception );
275         }
276     }
277 
278     @Override
279     public void contextualize( Context theContext )
280         throws ContextException
281     {
282         this.context = theContext;
283     }
284 
285     /**
286      * @return {@link #skip}
287      */
288     protected final boolean isSkip()
289     {
290         return skip;
291     }
292 
293     // private methods --------------------------------------------------------
294 
295     private boolean checkDependencies()
296         throws MojoExecutionException
297     {
298         ProjectDependencyAnalysis analysis;
299         try
300         {
301             analysis = createProjectDependencyAnalyzer().analyze( project );
302 
303             if ( usedDependencies != null )
304             {
305                 analysis = analysis.forceDeclaredDependenciesUsage( usedDependencies );
306             }
307         }
308         catch ( ProjectDependencyAnalyzerException exception )
309         {
310             throw new MojoExecutionException( "Cannot analyze dependencies", exception );
311         }
312 
313         if ( ignoreNonCompile )
314         {
315             analysis = analysis.ignoreNonCompile();
316         }
317 
318         Set<Artifact> usedDeclared = new LinkedHashSet<>( analysis.getUsedDeclaredArtifacts() );
319         Set<Artifact> usedUndeclared = new LinkedHashSet<>( analysis.getUsedUndeclaredArtifacts() );
320         Set<Artifact> unusedDeclared = new LinkedHashSet<>( analysis.getUnusedDeclaredArtifacts() );
321 
322         Set<Artifact> ignoredUsedUndeclared = new LinkedHashSet<>();
323         Set<Artifact> ignoredUnusedDeclared = new LinkedHashSet<>();
324 
325         ignoredUsedUndeclared.addAll( filterDependencies( usedUndeclared, ignoredDependencies ) );
326         ignoredUsedUndeclared.addAll( filterDependencies( usedUndeclared, ignoredUsedUndeclaredDependencies ) );
327 
328         ignoredUnusedDeclared.addAll( filterDependencies( unusedDeclared, ignoredDependencies ) );
329         ignoredUnusedDeclared.addAll( filterDependencies( unusedDeclared, ignoredUnusedDeclaredDependencies ) );
330 
331         boolean reported = false;
332         boolean warning = false;
333 
334         if ( verbose && !usedDeclared.isEmpty() )
335         {
336             getLog().info( "Used declared dependencies found:" );
337 
338             logArtifacts( analysis.getUsedDeclaredArtifacts(), false );
339             reported = true;
340         }
341 
342         if ( !usedUndeclared.isEmpty() )
343         {
344             getLog().warn( "Used undeclared dependencies found:" );
345 
346             logArtifacts( usedUndeclared, true );
347             reported = true;
348             warning = true;
349         }
350 
351         if ( !unusedDeclared.isEmpty() )
352         {
353             getLog().warn( "Unused declared dependencies found:" );
354 
355             logArtifacts( unusedDeclared, true );
356             reported = true;
357             warning = true;
358         }
359 
360         if ( verbose && !ignoredUsedUndeclared.isEmpty() )
361         {
362             getLog().info( "Ignored used undeclared dependencies:" );
363 
364             logArtifacts( ignoredUsedUndeclared, false );
365             reported = true;
366         }
367 
368         if ( verbose && !ignoredUnusedDeclared.isEmpty() )
369         {
370             getLog().info( "Ignored unused declared dependencies:" );
371 
372             logArtifacts( ignoredUnusedDeclared, false );
373             reported = true;
374         }
375 
376         if ( outputXML )
377         {
378             writeDependencyXML( usedUndeclared );
379         }
380 
381         if ( scriptableOutput )
382         {
383             writeScriptableOutput( usedUndeclared );
384         }
385 
386         if ( !reported )
387         {
388             getLog().info( "No dependency problems found" );
389         }
390 
391         return warning;
392     }
393 
394     private void logArtifacts( Set<Artifact> artifacts, boolean warn )
395     {
396         if ( artifacts.isEmpty() )
397         {
398             getLog().info( "   None" );
399         }
400         else
401         {
402             for ( Artifact artifact : artifacts )
403             {
404                 // called because artifact will set the version to -SNAPSHOT only if I do this. MNG-2961
405                 artifact.isSnapshot();
406 
407                 if ( warn )
408                 {
409                     getLog().warn( "   " + artifact );
410                 }
411                 else
412                 {
413                     getLog().info( "   " + artifact );
414                 }
415 
416             }
417         }
418     }
419 
420     private void writeDependencyXML( Set<Artifact> artifacts )
421     {
422         if ( !artifacts.isEmpty() )
423         {
424             getLog().info( "Add the following to your pom to correct the missing dependencies: " );
425 
426             StringWriter out = new StringWriter();
427             PrettyPrintXMLWriter writer = new PrettyPrintXMLWriter( out );
428 
429             for ( Artifact artifact : artifacts )
430             {
431                 // called because artifact will set the version to -SNAPSHOT only if I do this. MNG-2961
432                 artifact.isSnapshot();
433 
434                 writer.startElement( "dependency" );
435                 writer.startElement( "groupId" );
436                 writer.writeText( artifact.getGroupId() );
437                 writer.endElement();
438                 writer.startElement( "artifactId" );
439                 writer.writeText( artifact.getArtifactId() );
440                 writer.endElement();
441                 writer.startElement( "version" );
442                 writer.writeText( artifact.getBaseVersion() );
443                 if ( !StringUtils.isBlank( artifact.getClassifier() ) )
444                 {
445                     writer.startElement( "classifier" );
446                     writer.writeText( artifact.getClassifier() );
447                     writer.endElement();
448                 }
449                 writer.endElement();
450 
451                 if ( !Artifact.SCOPE_COMPILE.equals( artifact.getScope() ) )
452                 {
453                     writer.startElement( "scope" );
454                     writer.writeText( artifact.getScope() );
455                     writer.endElement();
456                 }
457                 writer.endElement();
458             }
459 
460             getLog().info( System.lineSeparator() + out.getBuffer() );
461         }
462     }
463 
464     private void writeScriptableOutput( Set<Artifact> artifacts )
465     {
466         if ( !artifacts.isEmpty() )
467         {
468             getLog().info( "Missing dependencies: " );
469             String pomFile = baseDir.getAbsolutePath() + File.separatorChar + "pom.xml";
470             StringBuilder buf = new StringBuilder();
471 
472             for ( Artifact artifact : artifacts )
473             {
474                 // called because artifact will set the version to -SNAPSHOT only if I do this. MNG-2961
475                 artifact.isSnapshot();
476 
477                 //CHECKSTYLE_OFF: LineLength
478                 buf.append( scriptableFlag )
479                    .append( ":" )
480                    .append( pomFile )
481                    .append( ":" )
482                    .append( artifact.getDependencyConflictId() )
483                    .append( ":" )
484                    .append( artifact.getClassifier() )
485                    .append( ":" )
486                    .append( artifact.getBaseVersion() )
487                    .append( ":" )
488                    .append( artifact.getScope() )
489                    .append( System.lineSeparator() );
490                 //CHECKSTYLE_ON: LineLength
491             }
492             getLog().info( System.lineSeparator() + buf );
493         }
494     }
495 
496     private List<Artifact> filterDependencies( Set<Artifact> artifacts, String[] excludes )
497         throws MojoExecutionException
498     {
499         ArtifactFilter filter = new StrictPatternExcludesArtifactFilter( Arrays.asList( excludes ) );
500         List<Artifact> result = new ArrayList<>();
501 
502         for ( Iterator<Artifact> it = artifacts.iterator(); it.hasNext(); )
503         {
504             Artifact artifact = it.next();
505             if ( !filter.include( artifact ) )
506             {
507                 it.remove();
508                 result.add( artifact );
509             }
510         }
511 
512         return result;
513     }
514 }