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