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