View Javadoc
1   package org.apache.maven.tools.plugin.extractor.annotations.scanner;
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 org.apache.maven.artifact.Artifact;
23  import org.apache.maven.plugins.annotations.Component;
24  import org.apache.maven.plugins.annotations.Execute;
25  import org.apache.maven.plugins.annotations.Mojo;
26  import org.apache.maven.plugins.annotations.Parameter;
27  import org.apache.maven.tools.plugin.extractor.ExtractionException;
28  import org.apache.maven.tools.plugin.extractor.annotations.datamodel.ComponentAnnotationContent;
29  import org.apache.maven.tools.plugin.extractor.annotations.datamodel.ExecuteAnnotationContent;
30  import org.apache.maven.tools.plugin.extractor.annotations.datamodel.MojoAnnotationContent;
31  import org.apache.maven.tools.plugin.extractor.annotations.datamodel.ParameterAnnotationContent;
32  import org.apache.maven.tools.plugin.extractor.annotations.scanner.visitors.MojoAnnotationVisitor;
33  import org.apache.maven.tools.plugin.extractor.annotations.scanner.visitors.MojoClassVisitor;
34  import org.apache.maven.tools.plugin.extractor.annotations.scanner.visitors.MojoFieldVisitor;
35  import org.codehaus.plexus.logging.AbstractLogEnabled;
36  import org.codehaus.plexus.util.DirectoryScanner;
37  import org.codehaus.plexus.util.StringUtils;
38  import org.codehaus.plexus.util.reflection.Reflector;
39  import org.codehaus.plexus.util.reflection.ReflectorException;
40  import org.objectweb.asm.ClassReader;
41  import org.objectweb.asm.Type;
42  
43  import java.io.BufferedInputStream;
44  import java.io.File;
45  import java.io.FileInputStream;
46  import java.io.IOException;
47  import java.io.InputStream;
48  import java.util.HashMap;
49  import java.util.List;
50  import java.util.Map;
51  import java.util.regex.Pattern;
52  import java.util.zip.ZipEntry;
53  import java.util.zip.ZipInputStream;
54  
55  /**
56   * @author Olivier Lamy
57   * @since 3.0
58   */
59  @org.codehaus.plexus.component.annotations.Component( role = MojoAnnotationsScanner.class )
60  public class DefaultMojoAnnotationsScanner
61      extends AbstractLogEnabled
62      implements MojoAnnotationsScanner
63  {
64      // classes with a dash must be ignored
65      private static final Pattern SCANNABLE_CLASS = Pattern.compile( "[^-]+\\.class" );
66      
67      private Reflector reflector = new Reflector();
68  
69      public Map<String, MojoAnnotatedClass> scan( MojoAnnotationsScannerRequest request )
70          throws ExtractionException
71      {
72          Map<String, MojoAnnotatedClass> mojoAnnotatedClasses = new HashMap<>();
73  
74          try
75          {
76              for ( Artifact dependency : request.getDependencies() )
77              {
78                  scan( mojoAnnotatedClasses, dependency.getFile(), request.getIncludePatterns(), dependency, true );
79              }
80  
81              for ( File classDirectory : request.getClassesDirectories() )
82              {
83                  scan( mojoAnnotatedClasses, classDirectory, request.getIncludePatterns(),
84                        request.getProject().getArtifact(), false );
85              }
86          }
87          catch ( IOException e )
88          {
89              throw new ExtractionException( e.getMessage(), e );
90          }
91  
92          return mojoAnnotatedClasses;
93      }
94  
95      protected void scan( Map<String, MojoAnnotatedClass> mojoAnnotatedClasses, File source,
96                           List<String> includePatterns, Artifact artifact, boolean excludeMojo )
97          throws IOException, ExtractionException
98      {
99          if ( source == null || ! source.exists() )
100         {
101             return;
102         }
103 
104         Map<String, MojoAnnotatedClass> scanResult;
105         if ( source.isDirectory() )
106         {
107             scanResult = scanDirectory( source, includePatterns, artifact, excludeMojo );
108         }
109         else
110         {
111             scanResult = scanArchive( source, artifact, excludeMojo );
112         }
113 
114         mojoAnnotatedClasses.putAll( scanResult );
115     }
116 
117     /**
118      * @param archiveFile
119      * @param artifact
120      * @param excludeMojo     for dependencies, we exclude Mojo annotations found
121      * @return annotated classes found
122      * @throws IOException
123      * @throws ExtractionException
124      */
125     protected Map<String, MojoAnnotatedClass> scanArchive( File archiveFile, Artifact artifact, boolean excludeMojo )
126         throws IOException, ExtractionException
127     {
128         Map<String, MojoAnnotatedClass> mojoAnnotatedClasses = new HashMap<>();
129 
130         String zipEntryName = null;
131         try ( ZipInputStream archiveStream = new ZipInputStream( new FileInputStream( archiveFile ) ) )
132         {
133             String archiveFilename = archiveFile.getAbsolutePath();
134             for ( ZipEntry zipEntry = archiveStream.getNextEntry(); zipEntry != null;
135                   zipEntry = archiveStream.getNextEntry() )
136             {
137                 zipEntryName = zipEntry.getName();
138                 if ( !SCANNABLE_CLASS.matcher( zipEntryName ).matches() )
139                 {
140                     continue;
141                 }
142                 analyzeClassStream( mojoAnnotatedClasses, archiveStream, artifact, excludeMojo, archiveFilename,
143                                     zipEntry.getName() );
144             }
145         }
146         catch ( IllegalArgumentException e )
147         {
148             // In case of a class with newer specs an IllegalArgumentException can be thrown
149             getLogger().error( "Failed to analyze " + archiveFile.getAbsolutePath() + "!/" + zipEntryName );
150             
151             throw e;
152         }
153 
154         return mojoAnnotatedClasses;
155     }
156 
157     /**
158      * @param classDirectory
159      * @param includePatterns
160      * @param artifact
161      * @param excludeMojo     for dependencies, we exclude Mojo annotations found
162      * @return annotated classes found
163      * @throws IOException
164      * @throws ExtractionException
165      */
166     protected Map<String, MojoAnnotatedClass> scanDirectory( File classDirectory, List<String> includePatterns,
167                                                              Artifact artifact, boolean excludeMojo )
168         throws IOException, ExtractionException
169     {
170         Map<String, MojoAnnotatedClass> mojoAnnotatedClasses = new HashMap<>();
171 
172         DirectoryScanner scanner = new DirectoryScanner();
173         scanner.setBasedir( classDirectory );
174         scanner.addDefaultExcludes();
175         if ( includePatterns != null )
176         {
177             scanner.setIncludes( includePatterns.toArray( new String[includePatterns.size()] ) );
178         }
179         scanner.scan();
180         String[] classFiles = scanner.getIncludedFiles();
181         String classDirname = classDirectory.getAbsolutePath();
182 
183         for ( String classFile : classFiles )
184         {
185             if ( !SCANNABLE_CLASS.matcher( classFile ).matches() )
186             {
187                 continue;
188             }
189 
190             try ( InputStream is = //
191                     new BufferedInputStream( new FileInputStream( new File( classDirectory, classFile ) ) ) )
192             {
193                 analyzeClassStream( mojoAnnotatedClasses, is, artifact, excludeMojo, classDirname, classFile );
194             }
195         }
196         return mojoAnnotatedClasses;
197     }
198 
199     private void analyzeClassStream( Map<String, MojoAnnotatedClass> mojoAnnotatedClasses, InputStream is,
200                                      Artifact artifact, boolean excludeMojo, String source, String file )
201         throws IOException, ExtractionException
202     {
203         MojoClassVisitor mojoClassVisitor = new MojoClassVisitor( getLogger() );
204 
205         try
206         {
207             ClassReader rdr = new ClassReader( is );
208             rdr.accept( mojoClassVisitor, ClassReader.SKIP_FRAMES | ClassReader.SKIP_CODE | ClassReader.SKIP_DEBUG );
209         }
210         catch ( ArrayIndexOutOfBoundsException aiooe )
211         {
212             getLogger().warn( "Error analyzing class " + file + " in " + source + ": ignoring class",
213                               getLogger().isDebugEnabled() ? aiooe : null );
214             return;
215         }
216         catch ( IllegalArgumentException iae )
217         {
218             if ( iae.getMessage() == null )
219             {
220                 getLogger().warn( "Error analyzing class " + file + " in " + source + ": ignoring class",
221                         getLogger().isDebugEnabled() ? iae : null );
222                 return;
223             }
224             else
225             {
226                 throw iae;
227             }
228         }
229 
230         analyzeVisitors( mojoClassVisitor );
231 
232         MojoAnnotatedClass mojoAnnotatedClass = mojoClassVisitor.getMojoAnnotatedClass();
233 
234         if ( excludeMojo )
235         {
236             mojoAnnotatedClass.setMojo( null );
237         }
238 
239         if ( mojoAnnotatedClass != null ) // see MPLUGIN-206 we can have intermediate classes without annotations
240         {
241             if ( getLogger().isDebugEnabled() && mojoAnnotatedClass.hasAnnotations() )
242             {
243                 getLogger().debug( "found MojoAnnotatedClass:" + mojoAnnotatedClass.getClassName() + ":"
244                                        + mojoAnnotatedClass );
245             }
246             mojoAnnotatedClass.setArtifact( artifact );
247             mojoAnnotatedClasses.put( mojoAnnotatedClass.getClassName(), mojoAnnotatedClass );
248         }
249     }
250 
251     protected void populateAnnotationContent( Object content, MojoAnnotationVisitor mojoAnnotationVisitor )
252         throws ReflectorException
253     {
254         for ( Map.Entry<String, Object> entry : mojoAnnotationVisitor.getAnnotationValues().entrySet() )
255         {
256             reflector.invoke( content, entry.getKey(), new Object[] { entry.getValue() } );
257         }
258     }
259 
260     protected void analyzeVisitors( MojoClassVisitor mojoClassVisitor )
261         throws ExtractionException
262     {
263         final MojoAnnotatedClass mojoAnnotatedClass = mojoClassVisitor.getMojoAnnotatedClass();
264 
265         try
266         {
267             // @Mojo annotation
268             MojoAnnotationVisitor mojoAnnotationVisitor = mojoClassVisitor.getAnnotationVisitor( Mojo.class );
269             if ( mojoAnnotationVisitor != null )
270             {
271                 MojoAnnotationContent mojoAnnotationContent = new MojoAnnotationContent();
272                 populateAnnotationContent( mojoAnnotationContent, mojoAnnotationVisitor );
273                 mojoAnnotatedClass.setMojo( mojoAnnotationContent );
274             }
275 
276             // @Execute annotation
277             mojoAnnotationVisitor = mojoClassVisitor.getAnnotationVisitor( Execute.class );
278             if ( mojoAnnotationVisitor != null )
279             {
280                 ExecuteAnnotationContent executeAnnotationContent = new ExecuteAnnotationContent();
281                 populateAnnotationContent( executeAnnotationContent, mojoAnnotationVisitor );
282                 mojoAnnotatedClass.setExecute( executeAnnotationContent );
283             }
284 
285             // @Parameter annotations
286             List<MojoFieldVisitor> mojoFieldVisitors = mojoClassVisitor.findFieldWithAnnotation( Parameter.class );
287             for ( MojoFieldVisitor mojoFieldVisitor : mojoFieldVisitors )
288             {
289                 ParameterAnnotationContent parameterAnnotationContent =
290                     new ParameterAnnotationContent( mojoFieldVisitor.getFieldName(), mojoFieldVisitor.getClassName() );
291                 if ( mojoFieldVisitor.getMojoAnnotationVisitor() != null )
292                 {
293                     populateAnnotationContent( parameterAnnotationContent,
294                                                mojoFieldVisitor.getMojoAnnotationVisitor() );
295                 }
296 
297                 mojoAnnotatedClass.getParameters().put( parameterAnnotationContent.getFieldName(),
298                                                         parameterAnnotationContent );
299             }
300 
301             // @Component annotations
302             mojoFieldVisitors = mojoClassVisitor.findFieldWithAnnotation( Component.class );
303             for ( MojoFieldVisitor mojoFieldVisitor : mojoFieldVisitors )
304             {
305                 ComponentAnnotationContent componentAnnotationContent =
306                     new ComponentAnnotationContent( mojoFieldVisitor.getFieldName() );
307 
308                 MojoAnnotationVisitor annotationVisitor = mojoFieldVisitor.getMojoAnnotationVisitor();
309                 if ( annotationVisitor != null )
310                 {
311                     for ( Map.Entry<String, Object> entry : annotationVisitor.getAnnotationValues().entrySet() )
312                     {
313                         String methodName = entry.getKey();
314                         if ( StringUtils.equals( "role", methodName ) )
315                         {
316                             Type type = (Type) entry.getValue();
317                             componentAnnotationContent.setRoleClassName( type.getClassName() );
318                         }
319                         else
320                         {
321                             reflector.invoke( componentAnnotationContent, entry.getKey(),
322                                               new Object[]{ entry.getValue() } );
323                         }
324                     }
325 
326                     if ( StringUtils.isEmpty( componentAnnotationContent.getRoleClassName() ) )
327                     {
328                         componentAnnotationContent.setRoleClassName( mojoFieldVisitor.getClassName() );
329                     }
330                 }
331                 mojoAnnotatedClass.getComponents().put( componentAnnotationContent.getFieldName(),
332                                                         componentAnnotationContent );
333             }
334         }
335         catch ( ReflectorException e )
336         {
337             throw new ExtractionException( e.getMessage(), e );
338         }
339     }
340 }