001package org.apache.maven.tools.plugin.extractor.annotations.scanner;
002
003/*
004 * Licensed to the Apache Software Foundation (ASF) under one
005 * or more contributor license agreements.  See the NOTICE file
006 * distributed with this work for additional information
007 * regarding copyright ownership.  The ASF licenses this file
008 * to you under the Apache License, Version 2.0 (the
009 * "License"); you may not use this file except in compliance
010 * with the License.  You may obtain a copy of the License at
011 *
012 *   http://www.apache.org/licenses/LICENSE-2.0
013 *
014 * Unless required by applicable law or agreed to in writing,
015 * software distributed under the License is distributed on an
016 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
017 * KIND, either express or implied.  See the License for the
018 * specific language governing permissions and limitations
019 * under the License.
020 */
021
022import org.apache.maven.artifact.Artifact;
023import org.apache.maven.plugins.annotations.Component;
024import org.apache.maven.plugins.annotations.Execute;
025import org.apache.maven.plugins.annotations.Mojo;
026import org.apache.maven.plugins.annotations.Parameter;
027import org.apache.maven.tools.plugin.extractor.ExtractionException;
028import org.apache.maven.tools.plugin.extractor.annotations.datamodel.ComponentAnnotationContent;
029import org.apache.maven.tools.plugin.extractor.annotations.datamodel.ExecuteAnnotationContent;
030import org.apache.maven.tools.plugin.extractor.annotations.datamodel.MojoAnnotationContent;
031import org.apache.maven.tools.plugin.extractor.annotations.datamodel.ParameterAnnotationContent;
032import org.apache.maven.tools.plugin.extractor.annotations.scanner.visitors.MojoAnnotationVisitor;
033import org.apache.maven.tools.plugin.extractor.annotations.scanner.visitors.MojoClassVisitor;
034import org.apache.maven.tools.plugin.extractor.annotations.scanner.visitors.MojoFieldVisitor;
035import org.codehaus.plexus.logging.AbstractLogEnabled;
036import org.codehaus.plexus.util.DirectoryScanner;
037import org.codehaus.plexus.util.StringUtils;
038import org.codehaus.plexus.util.reflection.Reflector;
039import org.codehaus.plexus.util.reflection.ReflectorException;
040import org.objectweb.asm.ClassReader;
041import org.objectweb.asm.Type;
042
043import java.io.BufferedInputStream;
044import java.io.File;
045import java.io.FileInputStream;
046import java.io.IOException;
047import java.io.InputStream;
048import java.util.HashMap;
049import java.util.List;
050import java.util.Map;
051import java.util.regex.Pattern;
052import java.util.zip.ZipEntry;
053import java.util.zip.ZipInputStream;
054
055/**
056 * @author Olivier Lamy
057 * @since 3.0
058 */
059@org.codehaus.plexus.component.annotations.Component( role = MojoAnnotationsScanner.class )
060public class DefaultMojoAnnotationsScanner
061    extends AbstractLogEnabled
062    implements MojoAnnotationsScanner
063{
064    // classes with a dash must be ignored
065    private static final Pattern SCANNABLE_CLASS = Pattern.compile( "[^-]+\\.class" );
066    
067    private Reflector reflector = new Reflector();
068
069    @Override
070    public Map<String, MojoAnnotatedClass> scan( MojoAnnotationsScannerRequest request )
071        throws ExtractionException
072    {
073        Map<String, MojoAnnotatedClass> mojoAnnotatedClasses = new HashMap<>();
074
075        try
076        {
077            for ( Artifact dependency : request.getDependencies() )
078            {
079                scan( mojoAnnotatedClasses, dependency.getFile(), request.getIncludePatterns(), dependency, true );
080            }
081
082            for ( File classDirectory : request.getClassesDirectories() )
083            {
084                scan( mojoAnnotatedClasses, classDirectory, request.getIncludePatterns(),
085                      request.getProject().getArtifact(), false );
086            }
087        }
088        catch ( IOException e )
089        {
090            throw new ExtractionException( e.getMessage(), e );
091        }
092
093        return mojoAnnotatedClasses;
094    }
095
096    protected void scan( Map<String, MojoAnnotatedClass> mojoAnnotatedClasses, File source,
097                         List<String> includePatterns, Artifact artifact, boolean excludeMojo )
098        throws IOException, ExtractionException
099    {
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}