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.lang3.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; compile scoped but only used in tests.
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      * Ignore Runtime scope for unused dependency analysis.
106      */
107     @Parameter( property = "ignoreUnusedRuntime", defaultValue = "false" )
108     private boolean ignoreUnusedRuntime;
109 
110     /**
111      * Output the xml for the missing dependencies (used but not declared).
112      *
113      * @since 2.0-alpha-5
114      */
115     @Parameter( property = "outputXML", defaultValue = "false" )
116     private boolean outputXML;
117 
118     /**
119      * Output scriptable values for the missing dependencies (used but not declared).
120      *
121      * @since 2.0-alpha-5
122      */
123     @Parameter( property = "scriptableOutput", defaultValue = "false" )
124     private boolean scriptableOutput;
125 
126     /**
127      * Flag to use for scriptable output.
128      *
129      * @since 2.0-alpha-5
130      */
131     @Parameter( property = "scriptableFlag", defaultValue = "$$$%%%" )
132     private String scriptableFlag;
133 
134     /**
135      * Flag to use for scriptable output
136      *
137      * @since 2.0-alpha-5
138      */
139     @Parameter( defaultValue = "${basedir}", readonly = true )
140     private File baseDir;
141 
142     /**
143      * Target folder
144      *
145      * @since 2.0-alpha-5
146      */
147     @Parameter( defaultValue = "${project.build.directory}", readonly = true )
148     private File outputDirectory;
149 
150     /**
151      * Force dependencies as used, to override incomplete result caused by bytecode-level analysis. Dependency format is
152      * <code>groupId:artifactId</code>.
153      *
154      * @since 2.6
155      */
156     @Parameter
157     private String[] usedDependencies;
158 
159     /**
160      * Skip plugin execution completely.
161      *
162      * @since 2.7
163      */
164     @Parameter( property = "mdep.analyze.skip", defaultValue = "false" )
165     private boolean skip;
166 
167     /**
168      * List of dependencies that will be ignored. Any dependency on this list will be excluded from the "declared but
169      * unused" and the "used but undeclared" list. 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      * @since 2.10
183      */
184     @Parameter
185     private String[] ignoredDependencies = new String[0];
186 
187     /**
188      * List of dependencies that will be ignored if they are used but undeclared. The filter syntax is:
189      *
190      * <pre>
191      * [groupId]:[artifactId]:[type]:[version]
192      * </pre>
193      *
194      * where each pattern segment is optional and supports full and partial <code>*</code> wildcards. An empty pattern
195      * segment is treated as an implicit wildcard. *
196      * <p>
197      * For example, <code>org.apache.*</code> will match all artifacts whose group id starts with
198      * <code>org.apache.</code>, and <code>:::*-SNAPSHOT</code> will match all snapshot artifacts.
199      * </p>
200      *
201      * @since 2.10
202      */
203     @Parameter
204     private String[] ignoredUsedUndeclaredDependencies = new String[0];
205 
206     /**
207      * List of dependencies that will be ignored if they are declared but unused. The filter syntax is:
208      *
209      * <pre>
210      * [groupId]:[artifactId]:[type]:[version]
211      * </pre>
212      *
213      * where each pattern segment is optional and supports full and partial <code>*</code> wildcards. An empty pattern
214      * segment is treated as an implicit wildcard. *
215      * <p>
216      * For example, <code>org.apache.*</code> will match all artifacts whose group id starts with
217      * <code>org.apache.</code>, and <code>:::*-SNAPSHOT</code> will match all snapshot artifacts.
218      * </p>
219      *
220      * @since 2.10
221      */
222     @Parameter
223     private String[] ignoredUnusedDeclaredDependencies = new String[0];
224 
225     // Mojo methods -----------------------------------------------------------
226 
227     /*
228      * @see org.apache.maven.plugin.Mojo#execute()
229      */
230     @Override
231     public void execute()
232         throws MojoExecutionException, MojoFailureException
233     {
234         if ( isSkip() )
235         {
236             getLog().info( "Skipping plugin execution" );
237             return;
238         }
239 
240         if ( "pom".equals( project.getPackaging() ) )
241         {
242             getLog().info( "Skipping pom project" );
243             return;
244         }
245 
246         if ( outputDirectory == null || !outputDirectory.exists() )
247         {
248             getLog().info( "Skipping project with no build directory" );
249             return;
250         }
251 
252         boolean warning = checkDependencies();
253 
254         if ( warning && failOnWarning )
255         {
256             throw new MojoExecutionException( "Dependency problems found" );
257         }
258     }
259 
260     /**
261      * @return {@link ProjectDependencyAnalyzer}
262      * @throws MojoExecutionException in case of an error.
263      */
264     protected ProjectDependencyAnalyzer createProjectDependencyAnalyzer()
265         throws MojoExecutionException
266     {
267 
268         final String role = ProjectDependencyAnalyzer.ROLE;
269         final String roleHint = analyzer;
270 
271         try
272         {
273             final PlexusContainer container = (PlexusContainer) context.get( PlexusConstants.PLEXUS_KEY );
274 
275             return (ProjectDependencyAnalyzer) container.lookup( role, roleHint );
276         }
277         catch ( Exception exception )
278         {
279             throw new MojoExecutionException( "Failed to instantiate ProjectDependencyAnalyser with role " + role
280                 + " / role-hint " + roleHint, exception );
281         }
282     }
283 
284     @Override
285     public void contextualize( Context theContext )
286         throws ContextException
287     {
288         this.context = theContext;
289     }
290 
291     /**
292      * @return {@link #skip}
293      */
294     protected final boolean isSkip()
295     {
296         return skip;
297     }
298 
299     // private methods --------------------------------------------------------
300 
301     private boolean checkDependencies()
302         throws MojoExecutionException
303     {
304         ProjectDependencyAnalysis analysis;
305         try
306         {
307             analysis = createProjectDependencyAnalyzer().analyze( project );
308 
309             if ( usedDependencies != null )
310             {
311                 analysis = analysis.forceDeclaredDependenciesUsage( usedDependencies );
312             }
313         }
314         catch ( ProjectDependencyAnalyzerException exception )
315         {
316             throw new MojoExecutionException( "Cannot analyze dependencies", exception );
317         }
318 
319         if ( ignoreNonCompile )
320         {
321             analysis = analysis.ignoreNonCompile();
322         }
323 
324         Set<Artifact> usedDeclared = new LinkedHashSet<>( analysis.getUsedDeclaredArtifacts() );
325         Set<Artifact> usedUndeclared = new LinkedHashSet<>( analysis.getUsedUndeclaredArtifacts() );
326         Set<Artifact> unusedDeclared = new LinkedHashSet<>( analysis.getUnusedDeclaredArtifacts() );
327         Set<Artifact> testArtifactsWithNonTestScope = new LinkedHashSet<>(
328                 analysis.getTestArtifactsWithNonTestScope() );
329 
330         Set<Artifact> ignoredUsedUndeclared = new LinkedHashSet<>();
331         Set<Artifact> ignoredUnusedDeclared = new LinkedHashSet<>();
332 
333         if ( ignoreUnusedRuntime )
334         {
335             filterArtifactsByScope( unusedDeclared, Artifact.SCOPE_RUNTIME );
336         }
337 
338         ignoredUsedUndeclared.addAll( filterDependencies( usedUndeclared, ignoredDependencies ) );
339         ignoredUsedUndeclared.addAll( filterDependencies( usedUndeclared, ignoredUsedUndeclaredDependencies ) );
340 
341         ignoredUnusedDeclared.addAll( filterDependencies( unusedDeclared, ignoredDependencies ) );
342         ignoredUnusedDeclared.addAll( filterDependencies( unusedDeclared, ignoredUnusedDeclaredDependencies ) );
343 
344         boolean reported = false;
345         boolean warning = false;
346 
347         if ( verbose && !usedDeclared.isEmpty() )
348         {
349             getLog().info( "Used declared dependencies found:" );
350 
351             logArtifacts( analysis.getUsedDeclaredArtifacts(), false );
352             reported = true;
353         }
354 
355         if ( !usedUndeclared.isEmpty() )
356         {
357             getLog().warn( "Used undeclared dependencies found:" );
358 
359             logArtifacts( usedUndeclared, true );
360             reported = true;
361             warning = true;
362         }
363 
364         if ( !unusedDeclared.isEmpty() )
365         {
366             getLog().warn( "Unused declared dependencies found:" );
367 
368             logArtifacts( unusedDeclared, true );
369             reported = true;
370             warning = true;
371         }
372 
373         if ( !testArtifactsWithNonTestScope.isEmpty() )
374         {
375             getLog().warn( "Non-test scoped test only dependencies found:" );
376 
377             logArtifacts( testArtifactsWithNonTestScope, true );
378             reported = true;
379             warning = true;
380         }
381 
382         if ( verbose && !ignoredUsedUndeclared.isEmpty() )
383         {
384             getLog().info( "Ignored used undeclared dependencies:" );
385 
386             logArtifacts( ignoredUsedUndeclared, false );
387             reported = true;
388         }
389 
390         if ( verbose && !ignoredUnusedDeclared.isEmpty() )
391         {
392             getLog().info( "Ignored unused declared dependencies:" );
393 
394             logArtifacts( ignoredUnusedDeclared, false );
395             reported = true;
396         }
397 
398         if ( outputXML )
399         {
400             writeDependencyXML( usedUndeclared );
401         }
402 
403         if ( scriptableOutput )
404         {
405             writeScriptableOutput( usedUndeclared );
406         }
407 
408         if ( !reported )
409         {
410             getLog().info( "No dependency problems found" );
411         }
412 
413         return warning;
414     }
415 
416     private void filterArtifactsByScope( Set<Artifact> artifacts, String scope )
417     {
418         for ( Iterator<Artifact> iterator = artifacts.iterator(); iterator.hasNext(); )
419         {
420             Artifact artifact = iterator.next();
421             if ( artifact.getScope().equals( scope ) )
422             {
423                 iterator.remove();
424             }
425         }
426     }
427 
428     private void logArtifacts( Set<Artifact> artifacts, boolean warn )
429     {
430         if ( artifacts.isEmpty() )
431         {
432             getLog().info( "   None" );
433         }
434         else
435         {
436             for ( Artifact artifact : artifacts )
437             {
438                 // called because artifact will set the version to -SNAPSHOT only if I do this. MNG-2961
439                 artifact.isSnapshot();
440 
441                 if ( warn )
442                 {
443                     getLog().warn( "   " + artifact );
444                 }
445                 else
446                 {
447                     getLog().info( "   " + artifact );
448                 }
449 
450             }
451         }
452     }
453 
454     private void writeDependencyXML( Set<Artifact> artifacts )
455     {
456         if ( !artifacts.isEmpty() )
457         {
458             getLog().info( "Add the following to your pom to correct the missing dependencies: " );
459 
460             StringWriter out = new StringWriter();
461             PrettyPrintXMLWriter writer = new PrettyPrintXMLWriter( out );
462 
463             for ( Artifact artifact : artifacts )
464             {
465                 // called because artifact will set the version to -SNAPSHOT only if I do this. MNG-2961
466                 artifact.isSnapshot();
467 
468                 writer.startElement( "dependency" );
469                 writer.startElement( "groupId" );
470                 writer.writeText( artifact.getGroupId() );
471                 writer.endElement();
472                 writer.startElement( "artifactId" );
473                 writer.writeText( artifact.getArtifactId() );
474                 writer.endElement();
475                 writer.startElement( "version" );
476                 writer.writeText( artifact.getBaseVersion() );
477                 if ( !StringUtils.isBlank( artifact.getClassifier() ) )
478                 {
479                     writer.startElement( "classifier" );
480                     writer.writeText( artifact.getClassifier() );
481                     writer.endElement();
482                 }
483                 writer.endElement();
484 
485                 if ( !Artifact.SCOPE_COMPILE.equals( artifact.getScope() ) )
486                 {
487                     writer.startElement( "scope" );
488                     writer.writeText( artifact.getScope() );
489                     writer.endElement();
490                 }
491                 writer.endElement();
492             }
493 
494             getLog().info( System.lineSeparator() + out.getBuffer() );
495         }
496     }
497 
498     private void writeScriptableOutput( Set<Artifact> artifacts )
499     {
500         if ( !artifacts.isEmpty() )
501         {
502             getLog().info( "Missing dependencies: " );
503             String pomFile = baseDir.getAbsolutePath() + File.separatorChar + "pom.xml";
504             StringBuilder buf = new StringBuilder();
505 
506             for ( Artifact artifact : artifacts )
507             {
508                 // called because artifact will set the version to -SNAPSHOT only if I do this. MNG-2961
509                 artifact.isSnapshot();
510 
511                 //CHECKSTYLE_OFF: LineLength
512                 buf.append( scriptableFlag )
513                    .append( ":" )
514                    .append( pomFile )
515                    .append( ":" )
516                    .append( artifact.getDependencyConflictId() )
517                    .append( ":" )
518                    .append( artifact.getClassifier() )
519                    .append( ":" )
520                    .append( artifact.getBaseVersion() )
521                    .append( ":" )
522                    .append( artifact.getScope() )
523                    .append( System.lineSeparator() );
524                 //CHECKSTYLE_ON: LineLength
525             }
526             getLog().info( System.lineSeparator() + buf );
527         }
528     }
529 
530     private List<Artifact> filterDependencies( Set<Artifact> artifacts, String[] excludes )
531         throws MojoExecutionException
532     {
533         ArtifactFilter filter = new StrictPatternExcludesArtifactFilter( Arrays.asList( excludes ) );
534         List<Artifact> result = new ArrayList<>();
535 
536         for ( Iterator<Artifact> it = artifacts.iterator(); it.hasNext(); )
537         {
538             Artifact artifact = it.next();
539             if ( !filter.include( artifact ) )
540             {
541                 it.remove();
542                 result.add( artifact );
543             }
544         }
545 
546         return result;
547     }
548 }