View Javadoc
1   package org.codehaus.mojo.animal_sniffer.enforcer;
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.enforcer.rule.api.EnforcerRule;
34  import org.apache.maven.enforcer.rule.api.EnforcerRuleException;
35  import org.apache.maven.enforcer.rule.api.EnforcerRuleHelper;
36  import org.apache.maven.model.Dependency;
37  import org.apache.maven.project.MavenProject;
38  import org.apache.maven.shared.artifact.filter.PatternExcludesArtifactFilter;
39  import org.apache.maven.shared.artifact.filter.PatternIncludesArtifactFilter;
40  import org.codehaus.mojo.animal_sniffer.ClassFileVisitor;
41  import org.codehaus.mojo.animal_sniffer.ClassListBuilder;
42  import org.codehaus.mojo.animal_sniffer.SignatureChecker;
43  import org.codehaus.mojo.animal_sniffer.logging.Logger;
44  import org.codehaus.plexus.component.configurator.expression.ExpressionEvaluationException;
45  import org.codehaus.plexus.component.repository.exception.ComponentLookupException;
46  import org.codehaus.plexus.util.StringUtils;
47  
48  import java.io.File;
49  import java.io.FileInputStream;
50  import java.io.IOException;
51  import java.util.ArrayList;
52  import java.util.Arrays;
53  import java.util.HashSet;
54  import java.util.Iterator;
55  import java.util.List;
56  import java.util.Random;
57  import java.util.Set;
58  
59  /**
60   * Created by IntelliJ IDEA.
61   *
62   * @author connollys
63   * @since Sep 4, 2009 2:44:29 PM
64   */
65  public class CheckSignatureRule
66      implements EnforcerRule
67  {
68      /**
69       * Signature module to use.
70       *
71       * //required
72       * //parameter
73       */
74      protected Signature signature;
75  
76      /**
77       * Class names to ignore signatures for (wildcards accepted).
78       *
79       * //parameter
80       */
81      protected String[] ignores;
82  
83      /**
84       * Annotation names to consider to ignore annotated methods, classes or fields.
85       * <p>
86       * By default {@value SignatureChecker#ANNOTATION_FQN} and
87       * {@value SignatureChecker#PREVIOUS_ANNOTATION_FQN} are used.
88       *
89       * //parameter
90       * @see SignatureChecker#ANNOTATION_FQN
91       * @see SignatureChecker#PREVIOUS_ANNOTATION_FQN
92       */
93      protected String[] annotations;
94  
95      /**
96       * Should dependencies be ignored.
97       *
98       * //parameter default-value="true"
99       */
100     protected boolean ignoreDependencies = true;
101 
102     /**
103      * A list of artifact patterns to include. Patterns can include <code>*</code> as a wildcard match for any
104      * <b>whole</b> segment, valid patterns are:
105      * <ul>
106      * <li><code>groupId:artifactId</code></li>
107      * <li><code>groupId:artifactId:type</code></li>
108      * <li><code>groupId:artifactId:type:version</code></li>
109      * <li><code>groupId:artifactId:type:classifier</code></li>
110      * <li><code>groupId:artifactId:type:classifier:version</code></li>
111      * </ul>
112      *
113      * //parameter
114      * @since 1.12
115      */
116     private String[] includeDependencies = null;
117 
118     /**
119      * A list of artifact patterns to exclude. Patterns can include <code>*</code> as a wildcard match for any
120      * <b>whole</b> segment, valid patterns are:
121      * <ul>
122      * <li><code>groupId:artifactId</code></li>
123      * <li><code>groupId:artifactId:type</code></li>
124      * <li><code>groupId:artifactId:type:version</code></li>
125      * <li><code>groupId:artifactId:type:classifier</code></li>
126      * <li><code>groupId:artifactId:type:classifier:version</code></li>
127      * </ul>
128      *
129      * //parameter
130      * @since 1.12
131      */
132     private String[] excludeDependencies = null;
133 
134     /**
135      * Should test classes be checked.
136      *
137      * //parameter default-value="false"
138      * @since 1.19
139      */
140     private boolean checkTestClasses = false;
141 
142     public void execute( EnforcerRuleHelper helper )
143         throws EnforcerRuleException
144     {
145         try
146         {
147             File outputDirectory = new File( (String) helper.evaluate( "${project.build.outputDirectory}" ) );
148 
149             File testOutputDirectory = new File( (String) helper.evaluate( "${project.build.testOutputDirectory}" ) );
150 
151             ArtifactResolver resolver = (ArtifactResolver) helper.getComponent( ArtifactResolver.class );
152 
153             MavenProject project = (MavenProject) helper.evaluate( "${project}" );
154 
155             ArtifactRepository localRepository = (ArtifactRepository) helper.evaluate( "${localRepository}" );
156 
157             ArtifactFactory artifactFactory = (ArtifactFactory) helper.getComponent( ArtifactFactory.class );
158 
159             if ( StringUtils.isEmpty( signature.getVersion() ) )
160             {
161                 helper.getLog().debug( "Resolving signature " + signature.getGroupId() + ":" + signature.getArtifactId()
162                                    + " version from dependencies" );
163                 String source = "dependencies";
164                 Dependency match = findMatchingDependency( signature, project.getDependencies() );
165                 if ( match == null )
166                 {
167                     helper.getLog().debug( "Resolving signature " + signature.getGroupId() + ":" + signature.getArtifactId()
168                                        + " version from dependencyManagement" );
169                     source = "dependencyManagement";
170                     match = findMatchingDependency( signature, project.getDependencyManagement().getDependencies() );
171                 }
172                 if ( match != null )
173                 {
174                     helper.getLog().info( "Resolved signature " + signature.getGroupId() + ":" + signature.getArtifactId()
175                                        + " version as " + match.getVersion() + " from " + source);
176                     signature.setVersion( match.getVersion() );
177                 }
178             }
179 
180             helper.getLog().info( "Checking unresolved references to " + signature );
181 
182             org.apache.maven.artifact.Artifact a = signature.createArtifact( artifactFactory );
183 
184             resolver.resolve( a, project.getRemoteArtifactRepositories(), localRepository );
185             // just check code from this module
186 
187             MavenLogger logger = new MavenLogger( helper.getLog() );
188 
189             final Set<String> ignoredPackages = buildPackageList( outputDirectory, testOutputDirectory, project, logger );
190 
191             if ( ignores != null )
192             {
193                 for ( String ignore : ignores )
194                 {
195                     if ( ignore == null )
196                     {
197                         continue;
198                     }
199                     ignoredPackages.add( ignore.replace( '.', '/' ) );
200                 }
201             }
202 
203             final SignatureChecker signatureChecker =
204                 new SignatureChecker( new FileInputStream( a.getFile() ), ignoredPackages, logger );
205             signatureChecker.setCheckJars( false ); // don't want to descend into jar files that have been copied to
206             // the output directory as resources.
207 
208             signatureChecker.setSourcePath( buildSourcePathList( project ) );
209 
210             if ( annotations != null )
211             {
212                 signatureChecker.setAnnotationTypes( Arrays.asList( annotations ) );
213             }
214 
215             if ( checkTestClasses )
216             {
217                 signatureChecker.process( new File[] { outputDirectory, testOutputDirectory } );
218             }
219             else
220             {
221                 signatureChecker.process( outputDirectory );
222             }
223 
224             if ( signatureChecker.isSignatureBroken() )
225             {
226                 throw new EnforcerRuleException(
227                     "Signature errors found. Verify them and ignore them with the proper annotation if needed." );
228             }
229         }
230         catch ( IOException e )
231         {
232             throw new EnforcerRuleException( "Failed to check signatures", e );
233         }
234         catch ( AbstractArtifactResolutionException e )
235         {
236             throw new EnforcerRuleException( "Failed to obtain signature: " + signature, e );
237         }
238         catch ( ComponentLookupException e )
239         {
240             throw new EnforcerRuleException( "Unable to lookup a component " + e.getLocalizedMessage(), e );
241         }
242         catch ( ExpressionEvaluationException e )
243         {
244             throw new EnforcerRuleException( "Unable to lookup an expression " + e.getLocalizedMessage(), e );
245         }
246     }
247 
248     private static Dependency findMatchingDependency( Signature signature, List<Dependency> dependencies )
249     {
250         Dependency match = null;
251         for ( Dependency d : dependencies )
252         {
253             if ( StringUtils.isEmpty( d.getVersion() ) )
254             {
255                 continue;
256             }
257             if ( StringUtils.equals( d.getGroupId(), signature.getGroupId() ) && StringUtils.equals( d.getArtifactId(),
258                                                                                                      signature.getArtifactId() ) )
259             {
260                 if ( "signature".equals( d.getType() ) )
261                 {
262                     // this is a perfect match
263                     match = d;
264                     break;
265                 }
266                 if ( "pom".equals( d.getType() ) )
267                 {
268                     if ( match == null || "jar".equals( match.getType() ) )
269                     {
270                         match = d;
271                     }
272                 }
273                 if ( "jar".equals( d.getType() ) )
274                 {
275                     if ( match == null )
276                     {
277                         match = d;
278                     }
279                 }
280             }
281         }
282         return match;
283     }
284 
285     /**
286      * List of packages defined in the application.
287      *
288      * @param outputDirectory
289      * @param logger
290      */
291     private Set<String> buildPackageList( File outputDirectory, File testOutputDirectory, MavenProject project, Logger logger )
292         throws IOException
293     {
294         ClassListBuilder plb = new ClassListBuilder( logger );
295         apply( plb, outputDirectory, testOutputDirectory, project, logger );
296         return plb.getPackages();
297     }
298 
299     private void apply( ClassFileVisitor v, File outputDirectory, File testOutputDirectory, MavenProject project, Logger logger )
300         throws IOException
301     {
302         v.process( outputDirectory );
303         if ( checkTestClasses )
304         {
305             v.process( testOutputDirectory );
306         }
307         if ( ignoreDependencies )
308         {
309             PatternIncludesArtifactFilter includesFilter = includeDependencies == null
310                 ? null
311                 : new PatternIncludesArtifactFilter( Arrays.asList( includeDependencies ) );
312             PatternExcludesArtifactFilter excludesFilter = excludeDependencies == null
313                 ? null
314                 : new PatternExcludesArtifactFilter( Arrays.asList( excludeDependencies ) );
315 
316             Set<String> classpathScopes = new HashSet<>(
317                 Arrays.asList( Artifact.SCOPE_COMPILE, Artifact.SCOPE_PROVIDED, Artifact.SCOPE_SYSTEM ) );
318             if ( checkTestClasses )
319             {
320                 classpathScopes.addAll( Arrays.asList( Artifact.SCOPE_TEST, Artifact.SCOPE_RUNTIME ) );
321             }
322 
323             logger.debug( "Building list of classes from dependencies" );
324             for ( Object o : project.getArtifacts() )
325             {
326 
327                 Artifact artifact = (Artifact) o;
328 
329                 if ( !artifact.getArtifactHandler().isAddedToClasspath() )
330                 {
331                     logger.debug(
332                         "Skipping artifact " + artifactId( artifact ) + " as it is not added to the classpath." );
333                     continue;
334                 }
335 
336                 if ( !classpathScopes.contains( artifact.getScope() ) )
337                 {
338                     logger.debug( "Skipping artifact " + artifactId( artifact ) + " as it is not on the " + (
339                         checkTestClasses
340                             ? "test"
341                             : "compile" ) + " classpath." );
342                     continue;
343                 }
344 
345                 if ( includesFilter != null && !includesFilter.include( artifact ) )
346                 {
347                     logger.debug( "Skipping classes in artifact " + artifactId( artifact )
348                                       + " as it does not match include rules." );
349                     continue;
350                 }
351 
352                 if ( excludesFilter != null && !excludesFilter.include( artifact ) )
353                 {
354                     logger.debug( "Skipping classes in artifact " + artifactId( artifact )
355                                       + " as it does matches exclude rules." );
356                     continue;
357                 }
358 
359                 if ( artifact.getFile() == null )
360                 {
361                     logger.warn( "Skipping classes in artifact " + artifactId( artifact )
362                                      + " as there are unresolved dependencies." );
363                     continue;
364                 }
365 
366                 logger.debug( "Adding classes in artifact " + artifactId( artifact ) + " to the ignores" );
367                 v.process( artifact.getFile() );
368             }
369         }
370     }
371 
372     public boolean isCacheable()
373     {
374         return false;
375     }
376 
377     public boolean isResultValid( EnforcerRule enforcerRule )
378     {
379         return false;
380     }
381 
382     public String getCacheId()
383     {
384         return getClass().getName() + new Random().nextLong();
385     }
386 
387     private static String artifactId( Artifact artifact )
388     {
389         return artifact.getGroupId() + ":" + artifact.getArtifactId() + ":" + artifact.getType() + (
390             artifact.getClassifier() != null ? ":" + artifact.getClassifier() : "" ) + ":" + artifact.getBaseVersion();
391 
392     }
393 
394     @SuppressWarnings("unchecked")
395     private List<File> buildSourcePathList( MavenProject project )
396     {
397         List<String> compileSourceRoots = new ArrayList<>( project.getCompileSourceRoots() );
398         if ( checkTestClasses )
399         {
400             compileSourceRoots.addAll( project.getTestCompileSourceRoots() );
401         }
402         List<File> sourcePathList = new ArrayList<>( compileSourceRoots.size() );
403         for ( String compileSourceRoot : compileSourceRoots)
404         {
405             sourcePathList.add( new File( compileSourceRoot ) );
406         }
407         return sourcePathList;
408     }
409 }