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.LinkedHashMap;
28  import java.util.LinkedHashSet;
29  import java.util.List;
30  import java.util.Map;
31  import java.util.Set;
32  
33  import org.apache.commons.lang3.StringUtils;
34  import org.apache.maven.artifact.Artifact;
35  import org.apache.maven.artifact.resolver.filter.ArtifactFilter;
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.Parameter;
40  import org.apache.maven.project.MavenProject;
41  import org.apache.maven.shared.artifact.filter.StrictPatternExcludesArtifactFilter;
42  import org.apache.maven.shared.dependency.analyzer.ProjectDependencyAnalysis;
43  import org.apache.maven.shared.dependency.analyzer.ProjectDependencyAnalyzer;
44  import org.apache.maven.shared.dependency.analyzer.ProjectDependencyAnalyzerException;
45  import org.codehaus.plexus.PlexusConstants;
46  import org.codehaus.plexus.PlexusContainer;
47  import org.codehaus.plexus.context.Context;
48  import org.codehaus.plexus.personality.plexus.lifecycle.phase.Contextualizable;
49  import org.codehaus.plexus.util.xml.PrettyPrintXMLWriter;
50  
51  /**
52   * Analyzes the dependencies of this project and determines which are: used and declared; used and undeclared; unused
53   * and declared; compile scoped but only used in tests.
54   *
55   * @author <a href="mailto:markhobson@gmail.com">Mark Hobson</a>
56   * @since 2.0-alpha-5
57   */
58  public abstract class AbstractAnalyzeMojo
59      extends AbstractMojo
60      implements Contextualizable
61  {
62      // fields -----------------------------------------------------------------
63  
64      /**
65       * The plexus context to look-up the right {@link ProjectDependencyAnalyzer} implementation depending on the mojo
66       * configuration.
67       */
68      private Context context;
69  
70      /**
71       * The Maven project to analyze.
72       */
73      @Parameter( defaultValue = "${project}", readonly = true, required = true )
74      private MavenProject project;
75  
76      /**
77       * Specify the project dependency analyzer to use (plexus component role-hint). By default,
78       * <a href="/shared/maven-dependency-analyzer/">maven-dependency-analyzer</a> is used. To use this, you must declare
79       * a dependency for this plugin that contains the code for the analyzer. The analyzer must have a declared Plexus
80       * role name, and you specify the role name here.
81       *
82       * @since 2.2
83       */
84      @Parameter( property = "analyzer", defaultValue = "default" )
85      private String analyzer;
86  
87      /**
88       * Whether to fail the build if a dependency warning is found.
89       */
90      @Parameter( property = "failOnWarning", defaultValue = "false" )
91      private boolean failOnWarning;
92  
93      /**
94       * Output used dependencies.
95       */
96      @Parameter( property = "verbose", defaultValue = "false" )
97      private boolean verbose;
98  
99      /**
100      * Ignore Runtime/Provided/Test/System scopes for unused dependency analysis.
101      *
102      * <code><b>Non-test scoped</b></code> list will be not affected.
103      */
104     @Parameter( property = "ignoreNonCompile", defaultValue = "false" )
105     private boolean ignoreNonCompile;
106 
107     /**
108      * Ignore Runtime scope for unused dependency analysis.
109      *
110      * @since 3.2.0
111      */
112     @Parameter( property = "ignoreUnusedRuntime", defaultValue = "false" )
113     private boolean ignoreUnusedRuntime;
114 
115     /**
116      * Ignore all dependencies that are used only in test but not test-scoped. Setting
117      * this flag has the same effect as adding all dependencies that have been flagged with
118      * the <i>Non-test scoped test only dependencies found</i> warning to the
119      * <code>&lt;ignoredNonTestScopedDependencies&gt;</code> configuration.
120      *
121      * @since 3.3.1-SNAPSHOT
122      */
123     @Parameter( property = "ignoreAllNonTestScoped", defaultValue = "false" )
124     private boolean ignoreAllNonTestScoped;
125 
126     /**
127      * Output the xml for the missing dependencies (used but not declared).
128      *
129      * @since 2.0-alpha-5
130      */
131     @Parameter( property = "outputXML", defaultValue = "false" )
132     private boolean outputXML;
133 
134     /**
135      * Output scriptable values for the missing dependencies (used but not declared).
136      *
137      * @since 2.0-alpha-5
138      */
139     @Parameter( property = "scriptableOutput", defaultValue = "false" )
140     private boolean scriptableOutput;
141 
142     /**
143      * Flag to use for scriptable output.
144      *
145      * @since 2.0-alpha-5
146      */
147     @Parameter( property = "scriptableFlag", defaultValue = "$$$%%%" )
148     private String scriptableFlag;
149 
150     /**
151      * Flag to use for scriptable output
152      *
153      * @since 2.0-alpha-5
154      */
155     @Parameter( defaultValue = "${basedir}", readonly = true )
156     private File baseDir;
157 
158     /**
159      * Target folder
160      *
161      * @since 2.0-alpha-5
162      */
163     @Parameter( defaultValue = "${project.build.directory}", readonly = true )
164     private File outputDirectory;
165 
166     /**
167      * Force dependencies as used, to override incomplete result caused by bytecode-level analysis. Dependency format is
168      * <code>groupId:artifactId</code>.
169      *
170      * @since 2.6
171      */
172     @Parameter
173     private String[] usedDependencies;
174 
175     /**
176      * Skip plugin execution completely.
177      *
178      * @since 2.7
179      */
180     @Parameter( property = "mdep.analyze.skip", defaultValue = "false" )
181     private boolean skip;
182 
183     /**
184      * List of dependencies that will be ignored. Any dependency on this list will be excluded from the "declared but
185      * unused", the "used but undeclared", and the "non-test scoped" list. The filter syntax is:
186      *
187      * <pre>
188      * [groupId]:[artifactId]:[type]:[version]
189      * </pre>
190      *
191      * where each pattern segment is optional and supports full and partial <code>*</code> wildcards. An empty pattern
192      * segment is treated as an implicit wildcard. *
193      * <p>
194      * For example, <code>org.apache.*</code> will match all artifacts whose group id starts with
195      * <code>org.apache.</code>, and <code>:::*-SNAPSHOT</code> will match all snapshot artifacts.
196      * </p>
197      *
198      * @since 2.10
199      */
200     @Parameter
201     private String[] ignoredDependencies = new String[0];
202 
203     /**
204      * List of dependencies that will be ignored if they are used but undeclared. The filter syntax is:
205      *
206      * <pre>
207      * [groupId]:[artifactId]:[type]:[version]
208      * </pre>
209      *
210      * where each pattern segment is optional and supports full and partial <code>*</code> wildcards. An empty pattern
211      * segment is treated as an implicit wildcard. *
212      * <p>
213      * For example, <code>org.apache.*</code> will match all artifacts whose group id starts with
214      * <code>org.apache.</code>, and <code>:::*-SNAPSHOT</code> will match all snapshot artifacts.
215      * </p>
216      *
217      * @since 2.10
218      */
219     @Parameter
220     private String[] ignoredUsedUndeclaredDependencies = new String[0];
221 
222     /**
223      * List of dependencies that will be ignored if they are declared but unused. The filter syntax is:
224      *
225      * <pre>
226      * [groupId]:[artifactId]:[type]:[version]
227      * </pre>
228      *
229      * where each pattern segment is optional and supports full and partial <code>*</code> wildcards. An empty pattern
230      * segment is treated as an implicit wildcard. *
231      * <p>
232      * For example, <code>org.apache.*</code> will match all artifacts whose group id starts with
233      * <code>org.apache.</code>, and <code>:::*-SNAPSHOT</code> will match all snapshot artifacts.
234      * </p>
235      *
236      * @since 2.10
237      */
238     @Parameter
239     private String[] ignoredUnusedDeclaredDependencies = new String[0];
240 
241     /**
242      * List of dependencies that will be ignored if they are in not test scope but are only used in test classes.
243      * The filter syntax is:
244      *
245      * <pre>
246      * [groupId]:[artifactId]:[type]:[version]
247      * </pre>
248      *
249      * where each pattern segment is optional and supports full and partial <code>*</code> wildcards. An empty pattern
250      * segment is treated as an implicit wildcard. *
251      * <p>
252      * For example, <code>org.apache.*</code> will match all artifacts whose group id starts with
253      * <code>org.apache.</code>, and <code>:::*-SNAPSHOT</code> will match all snapshot artifacts.
254      * </p>
255      *
256      * @since 3.3.0
257      */
258     @Parameter
259     private String[] ignoredNonTestScopedDependencies = new String[0];
260 
261     /**
262      * List of project packaging that will be ignored.
263      * <br/>
264      * <b>Default value is<b>: <code>pom, ear</code>
265      *
266      * @since 3.2.1
267      */
268     // defaultValue value on @Parameter - not work with Maven 3.2.5
269     // When is set defaultValue always win, and there is no possibility to override by plugin configuration.
270     @Parameter
271     private List<String> ignoredPackagings = Arrays.asList( "pom", "ear" );
272 
273     // Mojo methods -----------------------------------------------------------
274 
275     /*
276      * @see org.apache.maven.plugin.Mojo#execute()
277      */
278     @Override
279     public void execute()
280         throws MojoExecutionException, MojoFailureException
281     {
282         if ( isSkip() )
283         {
284             getLog().info( "Skipping plugin execution" );
285             return;
286         }
287 
288         if ( ignoredPackagings.contains( project.getPackaging() ) )
289         {
290             getLog().info( "Skipping " + project.getPackaging() + " project" );
291             return;
292         }
293 
294         if ( outputDirectory == null || !outputDirectory.exists() )
295         {
296             getLog().info( "Skipping project with no build directory" );
297             return;
298         }
299 
300         boolean warning = checkDependencies();
301 
302         if ( warning && failOnWarning )
303         {
304             throw new MojoExecutionException( "Dependency problems found" );
305         }
306     }
307 
308     /**
309      * @return {@link ProjectDependencyAnalyzer}
310      * @throws MojoExecutionException in case of an error.
311      */
312     protected ProjectDependencyAnalyzer createProjectDependencyAnalyzer()
313         throws MojoExecutionException
314     {
315         try
316         {
317             final PlexusContainer container = (PlexusContainer) context.get( PlexusConstants.PLEXUS_KEY );
318             return container.lookup( ProjectDependencyAnalyzer.class, analyzer );
319         }
320         catch ( Exception exception )
321         {
322             throw new MojoExecutionException( "Failed to instantiate ProjectDependencyAnalyser"
323                 + " / role-hint " + analyzer, exception );
324         }
325     }
326 
327     @Override
328     public void contextualize( Context theContext )
329     {
330         this.context = theContext;
331     }
332 
333     /**
334      * @return {@link #skip}
335      */
336     protected final boolean isSkip()
337     {
338         return skip;
339     }
340 
341     // private methods --------------------------------------------------------
342 
343     private boolean checkDependencies()
344         throws MojoExecutionException
345     {
346         ProjectDependencyAnalysis analysis;
347         try
348         {
349             analysis = createProjectDependencyAnalyzer().analyze( project );
350 
351             if ( usedDependencies != null )
352             {
353                 analysis = analysis.forceDeclaredDependenciesUsage( usedDependencies );
354             }
355         }
356         catch ( ProjectDependencyAnalyzerException exception )
357         {
358             throw new MojoExecutionException( "Cannot analyze dependencies", exception );
359         }
360 
361         if ( ignoreNonCompile )
362         {
363             analysis = analysis.ignoreNonCompile();
364         }
365 
366         Set<Artifact> usedDeclared = new LinkedHashSet<>( analysis.getUsedDeclaredArtifacts() );
367         Map<Artifact, Set<String>> usedUndeclaredWithClasses =
368                 new LinkedHashMap<>( analysis.getUsedUndeclaredArtifactsWithClasses() );
369         Set<Artifact> unusedDeclared = new LinkedHashSet<>( analysis.getUnusedDeclaredArtifacts() );
370         Set<Artifact> nonTestScope = new LinkedHashSet<>( analysis.getTestArtifactsWithNonTestScope() );
371 
372         Set<Artifact> ignoredUsedUndeclared = new LinkedHashSet<>();
373         Set<Artifact> ignoredUnusedDeclared = new LinkedHashSet<>();
374         Set<Artifact> ignoredNonTestScope = new LinkedHashSet<>();
375 
376         if ( ignoreUnusedRuntime )
377         {
378             filterArtifactsByScope( unusedDeclared, Artifact.SCOPE_RUNTIME );
379         }
380 
381         ignoredUsedUndeclared.addAll( filterDependencies( usedUndeclaredWithClasses.keySet(), ignoredDependencies ) );
382         ignoredUsedUndeclared.addAll( filterDependencies( usedUndeclaredWithClasses.keySet(),
383                 ignoredUsedUndeclaredDependencies ) );
384 
385         ignoredUnusedDeclared.addAll( filterDependencies( unusedDeclared, ignoredDependencies ) );
386         ignoredUnusedDeclared.addAll( filterDependencies( unusedDeclared, ignoredUnusedDeclaredDependencies ) );
387 
388         if ( ignoreAllNonTestScoped )
389         {
390             ignoredNonTestScope.addAll( filterDependencies ( nonTestScope, new String [] { "*" } ) );
391         }
392         else
393         {
394             ignoredNonTestScope.addAll( filterDependencies( nonTestScope, ignoredDependencies ) );
395             ignoredNonTestScope.addAll( filterDependencies( nonTestScope, ignoredNonTestScopedDependencies ) );
396         }
397 
398         boolean reported = false;
399         boolean warning = false;
400 
401         if ( verbose && !usedDeclared.isEmpty() )
402         {
403             getLog().info( "Used declared dependencies found:" );
404 
405             logArtifacts( analysis.getUsedDeclaredArtifacts(), false );
406             reported = true;
407         }
408 
409         if ( !usedUndeclaredWithClasses.isEmpty() )
410         {
411             logDependencyWarning( "Used undeclared dependencies found:" );
412 
413             if ( verbose )
414             {
415                 logArtifacts( usedUndeclaredWithClasses, true );
416             }
417             else
418             {
419                 logArtifacts( usedUndeclaredWithClasses.keySet(), true );
420             }
421             reported = true;
422             warning = true;
423         }
424 
425         if ( !unusedDeclared.isEmpty() )
426         {
427             logDependencyWarning( "Unused declared dependencies found:" );
428 
429             logArtifacts( unusedDeclared, true );
430             reported = true;
431             warning = true;
432         }
433 
434         if ( !nonTestScope.isEmpty() )
435         {
436             logDependencyWarning( "Non-test scoped test only dependencies found:" );
437 
438             logArtifacts( nonTestScope, true );
439             reported = true;
440             warning = true;
441         }
442 
443         if ( verbose && !ignoredUsedUndeclared.isEmpty() )
444         {
445             getLog().info( "Ignored used undeclared dependencies:" );
446 
447             logArtifacts( ignoredUsedUndeclared, false );
448             reported = true;
449         }
450 
451         if ( verbose && !ignoredUnusedDeclared.isEmpty() )
452         {
453             getLog().info( "Ignored unused declared dependencies:" );
454 
455             logArtifacts( ignoredUnusedDeclared, false );
456             reported = true;
457         }
458 
459         if ( verbose && !ignoredNonTestScope.isEmpty() )
460         {
461             getLog().info( "Ignored non-test scoped test only dependencies:" );
462 
463             logArtifacts( ignoredNonTestScope, false );
464             reported = true;
465         }
466 
467         if ( outputXML )
468         {
469             writeDependencyXML( usedUndeclaredWithClasses.keySet() );
470         }
471 
472         if ( scriptableOutput )
473         {
474             writeScriptableOutput( usedUndeclaredWithClasses.keySet() );
475         }
476 
477         if ( !reported )
478         {
479             getLog().info( "No dependency problems found" );
480         }
481 
482         return warning;
483     }
484 
485     private void filterArtifactsByScope( Set<Artifact> artifacts, String scope )
486     {
487         artifacts.removeIf( artifact -> artifact.getScope().equals( scope ) );
488     }
489 
490     private void logArtifacts( Set<Artifact> artifacts, boolean warn )
491     {
492         if ( artifacts.isEmpty() )
493         {
494             getLog().info( "   None" );
495         }
496         else
497         {
498             for ( Artifact artifact : artifacts )
499             {
500                 // called because artifact will set the version to -SNAPSHOT only if I do this. MNG-2961
501                 artifact.isSnapshot();
502 
503                 if ( warn )
504                 {
505                     logDependencyWarning( "   " + artifact );
506                 }
507                 else
508                 {
509                     getLog().info( "   " + artifact );
510                 }
511 
512             }
513         }
514     }
515 
516     private void logArtifacts( Map<Artifact, Set<String>> artifacts, boolean warn )
517     {
518         if ( artifacts.isEmpty() )
519         {
520             getLog().info( "   None" );
521         }
522         else
523         {
524             for ( Map.Entry<Artifact, Set<String>> entry : artifacts.entrySet() )
525             {
526                 // called because artifact will set the version to -SNAPSHOT only if I do this. MNG-2961
527                 entry.getKey().isSnapshot();
528 
529                 if ( warn )
530                 {
531                     logDependencyWarning( "   " + entry.getKey() );
532                     for ( String clazz : entry.getValue() )
533                     {
534                         logDependencyWarning( "      class " + clazz );
535                     }
536                 }
537                 else
538                 {
539                     getLog().info( "   " + entry.getKey() );
540                     for ( String clazz : entry.getValue() )
541                     {
542                         getLog().info( "      class " + clazz );
543                     }
544                 }
545 
546             }
547         }
548     }
549 
550     private void logDependencyWarning( CharSequence content )
551     {
552         if ( failOnWarning )
553         {
554             getLog().error( content );
555         }
556         else
557         {
558             getLog().warn( content );
559         }
560     }
561 
562     private void writeDependencyXML( Set<Artifact> artifacts )
563     {
564         if ( !artifacts.isEmpty() )
565         {
566             getLog().info( "Add the following to your pom to correct the missing dependencies: " );
567 
568             StringWriter out = new StringWriter();
569             PrettyPrintXMLWriter writer = new PrettyPrintXMLWriter( out );
570 
571             for ( Artifact artifact : artifacts )
572             {
573                 // called because artifact will set the version to -SNAPSHOT only if I do this. MNG-2961
574                 artifact.isSnapshot();
575 
576                 writer.startElement( "dependency" );
577                 writer.startElement( "groupId" );
578                 writer.writeText( artifact.getGroupId() );
579                 writer.endElement();
580                 writer.startElement( "artifactId" );
581                 writer.writeText( artifact.getArtifactId() );
582                 writer.endElement();
583                 writer.startElement( "version" );
584                 writer.writeText( artifact.getBaseVersion() );
585                 if ( !StringUtils.isBlank( artifact.getClassifier() ) )
586                 {
587                     writer.startElement( "classifier" );
588                     writer.writeText( artifact.getClassifier() );
589                     writer.endElement();
590                 }
591                 writer.endElement();
592 
593                 if ( !Artifact.SCOPE_COMPILE.equals( artifact.getScope() ) )
594                 {
595                     writer.startElement( "scope" );
596                     writer.writeText( artifact.getScope() );
597                     writer.endElement();
598                 }
599                 writer.endElement();
600             }
601 
602             getLog().info( System.lineSeparator() + out.getBuffer() );
603         }
604     }
605 
606     private void writeScriptableOutput( Set<Artifact> artifacts )
607     {
608         if ( !artifacts.isEmpty() )
609         {
610             getLog().info( "Missing dependencies: " );
611             String pomFile = baseDir.getAbsolutePath() + File.separatorChar + "pom.xml";
612             StringBuilder buf = new StringBuilder();
613 
614             for ( Artifact artifact : artifacts )
615             {
616                 // called because artifact will set the version to -SNAPSHOT only if I do this. MNG-2961
617                 artifact.isSnapshot();
618 
619                 //CHECKSTYLE_OFF: LineLength
620                 buf.append( scriptableFlag )
621                    .append( ":" )
622                    .append( pomFile )
623                    .append( ":" )
624                    .append( artifact.getDependencyConflictId() )
625                    .append( ":" )
626                    .append( artifact.getClassifier() )
627                    .append( ":" )
628                    .append( artifact.getBaseVersion() )
629                    .append( ":" )
630                    .append( artifact.getScope() )
631                    .append( System.lineSeparator() );
632                 //CHECKSTYLE_ON: LineLength
633             }
634             getLog().info( System.lineSeparator() + buf );
635         }
636     }
637 
638     private List<Artifact> filterDependencies( Set<Artifact> artifacts, String[] excludes )
639     {
640         ArtifactFilter filter = new StrictPatternExcludesArtifactFilter( Arrays.asList( excludes ) );
641         List<Artifact> result = new ArrayList<>();
642 
643         for ( Iterator<Artifact> it = artifacts.iterator(); it.hasNext(); )
644         {
645             Artifact artifact = it.next();
646             if ( !filter.include( artifact ) )
647             {
648                 it.remove();
649                 result.add( artifact );
650             }
651         }
652 
653         return result;
654     }
655 }