View Javadoc
1   package org.codehaus.mojo.animal_sniffer.maven;
2   
3   /*
4    * The MIT License
5    *
6    * Copyright (c) 2008 Kohsuke Kawaguchi and codehaus.org.
7    *
8    * Permission is hereby granted, free of charge, to any person obtaining a copy
9    * of this software and associated documentation files (the "Software"), to deal
10   * in the Software without restriction, including without limitation the rights
11   * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
12   * copies of the Software, and to permit persons to whom the Software is
13   * furnished to do so, subject to the following conditions:
14   *
15   * The above copyright notice and this permission notice shall be included in
16   * all copies or substantial portions of the Software.
17   *
18   * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19   * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20   * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21   * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22   * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
23   * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
24   * THE SOFTWARE.
25   *
26   */
27  
28  import org.apache.maven.artifact.Artifact;
29  import org.apache.maven.artifact.factory.ArtifactFactory;
30  import org.apache.maven.artifact.repository.ArtifactRepository;
31  import org.apache.maven.artifact.resolver.AbstractArtifactResolutionException;
32  import org.apache.maven.artifact.resolver.ArtifactResolver;
33  import org.apache.maven.model.Dependency;
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.Component;
38  import org.apache.maven.plugins.annotations.LifecyclePhase;
39  import org.apache.maven.plugins.annotations.Mojo;
40  import org.apache.maven.plugins.annotations.Parameter;
41  import org.apache.maven.plugins.annotations.ResolutionScope;
42  import org.apache.maven.project.MavenProject;
43  import org.apache.maven.shared.artifact.filter.PatternExcludesArtifactFilter;
44  import org.apache.maven.shared.artifact.filter.PatternIncludesArtifactFilter;
45  import org.codehaus.mojo.animal_sniffer.ClassFileVisitor;
46  import org.codehaus.mojo.animal_sniffer.ClassListBuilder;
47  import org.codehaus.mojo.animal_sniffer.Clazz;
48  import org.codehaus.mojo.animal_sniffer.SignatureChecker;
49  import org.codehaus.plexus.util.StringUtils;
50  
51  import java.io.File;
52  import java.io.FileInputStream;
53  import java.io.IOException;
54  import java.io.InputStream;
55  import java.util.ArrayList;
56  import java.util.Arrays;
57  import java.util.HashSet;
58  import java.util.List;
59  import java.util.Map;
60  import java.util.Set;
61  import java.util.concurrent.ConcurrentHashMap;
62  
63  /**
64   * Checks the classes compiled by this module.
65   *
66   * @author Kohsuke Kawaguchi
67   */
68  @Mojo( name = "check", defaultPhase = LifecyclePhase.PROCESS_TEST_CLASSES, requiresDependencyResolution = ResolutionScope.TEST, threadSafe = true )
69  public class CheckSignatureMojo
70      extends AbstractMojo
71  {
72  
73      /**
74       * The directory for compiled classes.
75       */
76      @Parameter( defaultValue = "${project.build.outputDirectory}", required = true, readonly = true )
77      protected File outputDirectory;
78  
79      /**
80       * The directory for compiled test classes.
81       *
82       * @since 1.19
83       */
84      @Parameter( defaultValue = "${project.build.testOutputDirectory}", required = true, readonly = true )
85      protected File testOutputDirectory;
86  
87      /**
88       * Should test classes be checked.
89       *
90       * @since 1.19
91       */
92      @Parameter( property = "animal.sniffer.checkTestClasses", defaultValue = "false" )
93      protected boolean checkTestClasses;
94  
95      /**
96       * Signature module to use.
97       */
98      @Parameter( required = true, property = "animal.sniffer.signature" )
99      protected Signature signature;
100 
101 	/**
102 	 * @param signatureId
103 	 *            A fully-qualified path to a signature jar. This allows users
104 	 *            to set a signature for command-line invocations, such as:
105 	 *            <p>
106 	 *            <code>mvn org.codehaus.mojo:animal-sniffer-maven-plugin:1.15:check -Dsignature=org.codehaus.mojo.signature:java17:1.0</code>
107 	 */
108     public void setSignature( String signatureId ) {
109 		String[] signatureParts = signatureId.split(":");
110 		if(signatureParts.length == 3) {
111 			this.signature = new Signature();
112 			this.signature.setGroupId(signatureParts[0]);
113 			this.signature.setArtifactId(signatureParts[1]);
114 			this.signature.setVersion(signatureParts[2]);
115 		}
116     }
117 
118     /**
119      * Class names to ignore signatures for (wildcards accepted).
120      *
121      */
122     @Parameter
123     protected String[] ignores;
124 
125     /**
126      * Annotation names to consider to ignore annotated methods, classes or fields.
127      * <p>
128      * By default 'org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement' and
129      * 'org.jvnet.animal_sniffer.IgnoreJRERequirement' are used.
130      *
131      * @see SignatureChecker#ANNOTATION_FQN
132      * @see SignatureChecker#PREVIOUS_ANNOTATION_FQN
133      */
134     @Parameter
135     protected String[] annotations;
136 
137     /**
138      * Should dependencies be ignored.
139      *
140      */
141     @Parameter( defaultValue = "true" )
142     protected boolean ignoreDependencies;
143 
144     /**
145      * A list of artifact patterns to include. Patterns can include <code>*</code> as a wildcard match for any
146      * <b>whole</b> segment, valid patterns are:
147      * <ul>
148      * <li><code>groupId:artifactId</code></li>
149      * <li><code>groupId:artifactId:type</code></li>
150      * <li><code>groupId:artifactId:type:version</code></li>
151      * <li><code>groupId:artifactId:type:classifier</code></li>
152      * <li><code>groupId:artifactId:type:classifier:version</code></li>
153      * </ul>
154      *
155      * @since 1.12
156      */
157     @Parameter
158     private String[] includeDependencies = null;
159 
160     /**
161      * A list of artifact patterns to exclude. Patterns can include <code>*</code> as a wildcard match for any
162      * <b>whole</b> segment, valid patterns are:
163      * <ul>
164      * <li><code>groupId:artifactId</code></li>
165      * <li><code>groupId:artifactId:type</code></li>
166      * <li><code>groupId:artifactId:type:version</code></li>
167      * <li><code>groupId:artifactId:type:classifier</code></li>
168      * <li><code>groupId:artifactId:type:classifier:version</code></li>
169      * </ul>
170      *
171      * @since 1.12
172      */
173     @Parameter
174     private String[] excludeDependencies = null;
175 
176     /**
177      * Should signature checking be skipped?
178      *
179      */
180     @Parameter( defaultValue = "false", property = "animal.sniffer.skip" )
181     protected boolean skip;
182 
183     /**
184      * Should signature check failures throw an error?
185      *
186      */
187     @Parameter( defaultValue = "true", property = "animal.sniffer.failOnError" )
188     protected boolean failOnError;
189 
190     /**
191      */
192     @Component
193     protected ArtifactResolver resolver;
194 
195     /**
196      */
197     @Parameter( defaultValue = "${project}", readonly = true )
198     protected MavenProject project;
199 
200     /**
201      */
202     @Parameter( defaultValue = "${localRepository}", readonly=true )
203     protected ArtifactRepository localRepository;
204 
205     /**
206      */
207     @Component
208     protected ArtifactFactory artifactFactory;
209 
210     static Map<File, Map<String, Clazz>> classes = new ConcurrentHashMap<>();
211 
212     @Override
213     public void execute()
214         throws MojoExecutionException, MojoFailureException
215     {
216         if ( skip )
217         {
218             getLog().info( "Signature checking is skipped." );
219             return;
220         }
221 
222 		if ( signature == null || StringUtils.isBlank(signature.getGroupId()) || signature.getGroupId() == "null") {
223 			getLog().info( "Signature version is: " + signature.getVersion() );
224 			return;
225         }
226 
227         try
228         {
229             if ( StringUtils.isBlank( signature.getVersion() ) )
230             {
231                 getLog().debug( "Resolving signature " + signature.getGroupId() + ":" + signature.getArtifactId()
232                                    + " version from dependencies" );
233                 String source = "dependencies";
234                 Dependency match = findMatchingDependency( signature, project.getDependencies() );
235                 if ( match == null && project.getDependencyManagement() != null )
236                 {
237                     getLog().debug( "Resolving signature " + signature.getGroupId() + ":" + signature.getArtifactId()
238                                        + " version from dependencyManagement" );
239                     source = "dependencyManagement";
240                     match = findMatchingDependency( signature, project.getDependencyManagement().getDependencies() );
241                 }
242                 if ( match != null )
243                 {
244                     getLog().info( "Resolved signature " + signature.getGroupId() + ":" + signature.getArtifactId()
245                                        + " version as " + match.getVersion() + " from " + source);
246                     signature.setVersion( match.getVersion() );
247                 }
248             }
249 
250             getLog().info( "Checking unresolved references to " + signature );
251 
252             Artifact a = signature.createArtifact( artifactFactory );
253 
254             resolver.resolve( a, project.getRemoteArtifactRepositories(), localRepository );
255             // just check code from this module
256             final Set<String> ignoredPackages = buildPackageList();
257 
258             if ( ignores != null )
259             {
260                 for ( String ignore : ignores )
261                 {
262                     if ( ignore == null )
263                     {
264                         continue;
265                     }
266                     ignoredPackages.add( ignore.replace( '.', '/' ) );
267                 }
268             }
269 
270             final SignatureChecker signatureChecker =
271                 new SignatureChecker( loadClasses( a.getFile() ), ignoredPackages,
272                                       new MavenLogger( getLog() ) );
273             signatureChecker.setCheckJars( false ); // don't want to decend into jar files that have been copied to
274                                                     // the output directory as resources.
275 
276             signatureChecker.setSourcePath( buildSourcePathList() );
277 
278             if ( annotations != null )
279             {
280                 signatureChecker.setAnnotationTypes( Arrays.asList( annotations ) );
281             }
282 
283             if ( checkTestClasses )
284             {
285                 signatureChecker.process( new File[] { outputDirectory, testOutputDirectory } );
286             }
287             else
288             {
289                 signatureChecker.process( outputDirectory );
290             }
291 
292             if ( signatureChecker.isSignatureBroken() )
293             {
294                 if (failOnError)
295                 {
296                         throw new MojoFailureException(
297                             "Signature errors found. Verify them and ignore them with the proper annotation if needed." );
298                 }
299                 else
300                 {
301                     getLog().info(
302                     "Signature errors found. Verify them and ignore them with the proper annotation if needed." );
303                 }
304             } else {
305                 getLog().debug( "No signature errors" );
306             }
307         }
308         catch ( IOException e )
309         {
310             throw new MojoExecutionException( "Failed to check signatures", e );
311         }
312         catch ( AbstractArtifactResolutionException e )
313         {
314             throw new MojoExecutionException( "Failed to obtain signature: " + signature, e );
315         }
316     }
317 
318     private static Map<String, Clazz> loadClasses( File f ) throws IOException
319     {
320         Map<String, Clazz> classes = CheckSignatureMojo.classes.get( f );
321         if ( classes == null )
322         {
323             classes = SignatureChecker.loadClasses( new FileInputStream( f ) );
324             CheckSignatureMojo.classes.putIfAbsent( f, classes );
325         }
326         return classes;
327     }
328 
329     private static Dependency findMatchingDependency( Signature signature, List<Dependency> dependencies )
330     {
331         Dependency match = null;
332         for ( Dependency d : dependencies )
333         {
334             if ( StringUtils.isBlank( d.getVersion() ) )
335             {
336                 continue;
337             }
338             if ( StringUtils.equals( d.getGroupId(), signature.getGroupId() ) && StringUtils.equals( d.getArtifactId(),
339                                                                                                      signature.getArtifactId() ) )
340             {
341                 if ( "signature".equals( d.getType() ) )
342                 {
343                     // this is a perfect match
344                     match = d;
345                     break;
346                 }
347                 if ( "pom".equals( d.getType() ) )
348                 {
349                     if ( match == null || "jar".equals( match.getType() ) )
350                     {
351                         match = d;
352                     }
353                 }
354                 if ( "jar".equals( d.getType() ) )
355                 {
356                     if ( match == null )
357                     {
358                         match = d;
359                     }
360                 }
361             }
362         }
363         return match;
364     }
365 
366     /**
367      * List of packages defined in the application.
368      */
369     private Set<String> buildPackageList()
370         throws IOException
371     {
372         ClassListBuilder plb = new ClassListBuilder( new MavenLogger( getLog() ) );
373         apply( plb );
374         return plb.getPackages();
375     }
376 
377     private void apply( ClassFileVisitor v )
378         throws IOException
379     {
380         v.process( outputDirectory );
381         if ( checkTestClasses )
382         {
383             v.process( testOutputDirectory );
384         }
385         if ( ignoreDependencies )
386         {
387             PatternIncludesArtifactFilter includesFilter = includeDependencies == null
388                 ? null
389                 : new PatternIncludesArtifactFilter( Arrays.asList( includeDependencies ) );
390             PatternExcludesArtifactFilter excludesFilter = excludeDependencies == null
391                 ? null
392                 : new PatternExcludesArtifactFilter( Arrays.asList( excludeDependencies ) );
393 
394             getLog().debug( "Building list of classes from dependencies" );
395 
396             Set<String> classpathScopes = new HashSet<>(
397                 Arrays.asList( Artifact.SCOPE_COMPILE, Artifact.SCOPE_PROVIDED, Artifact.SCOPE_SYSTEM ) );
398             if ( checkTestClasses )
399             {
400                 classpathScopes.addAll( Arrays.asList( Artifact.SCOPE_TEST, Artifact.SCOPE_RUNTIME ) );
401             }
402 
403             for ( Artifact artifact : (Iterable<Artifact>) project.getArtifacts() )
404             {
405 
406                 if ( !artifact.getArtifactHandler().isAddedToClasspath() )
407                 {
408                     getLog().debug( "Skipping artifact " + BuildSignaturesMojo.artifactId( artifact )
409                                         + " as it is not added to the classpath." );
410                     continue;
411                 }
412 
413                 if ( !classpathScopes.contains( artifact.getScope() ) )
414                 {
415                     getLog().debug(
416                         "Skipping artifact " + BuildSignaturesMojo.artifactId( artifact ) + " as it is not on the " + (
417                             checkTestClasses
418                                 ? "test"
419                                 : "compile" ) + " classpath." );
420                     continue;
421                 }
422 
423                 if ( includesFilter != null && !includesFilter.include( artifact ) )
424                 {
425                     getLog().debug( "Skipping classes in artifact " + BuildSignaturesMojo.artifactId( artifact )
426                                         + " as it does not match include rules." );
427                     continue;
428                 }
429 
430                 if ( excludesFilter != null && !excludesFilter.include( artifact ) )
431                 {
432                     getLog().debug( "Skipping classes in artifact " + BuildSignaturesMojo.artifactId( artifact )
433                                         + " as it does matches exclude rules." );
434                     continue;
435                 }
436 
437                 getLog().debug(
438                     "Adding classes in artifact " + BuildSignaturesMojo.artifactId( artifact ) + " to the ignores" );
439                 v.process( artifact.getFile() );
440             }
441         }
442     }
443 
444     @SuppressWarnings("unchecked")
445     private List<File> buildSourcePathList( )
446     {
447         List<String> compileSourceRoots = new ArrayList<>( project.getCompileSourceRoots() );
448         if ( checkTestClasses )
449         {
450             compileSourceRoots.addAll( project.getTestCompileSourceRoots() );
451         }
452         List<File> sourcePathList = new ArrayList<>( compileSourceRoots.size() );
453         for ( String compileSourceRoot : compileSourceRoots)
454         {
455             sourcePathList.add( new File( compileSourceRoot ) );
456         }
457         return sourcePathList;
458     }
459 }