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    public Map<String, MojoAnnotatedClass> scan( MojoAnnotationsScannerRequest request )
070        throws ExtractionException
071    {
072        Map<String, MojoAnnotatedClass> mojoAnnotatedClasses = new HashMap<>();
073
074        try
075        {
076            for ( Artifact dependency : request.getDependencies() )
077            {
078                scan( mojoAnnotatedClasses, dependency.getFile(), request.getIncludePatterns(), dependency, true );
079            }
080
081            for ( File classDirectory : request.getClassesDirectories() )
082            {
083                scan( mojoAnnotatedClasses, classDirectory, request.getIncludePatterns(),
084                      request.getProject().getArtifact(), false );
085            }
086        }
087        catch ( IOException e )
088        {
089            throw new ExtractionException( e.getMessage(), e );
090        }
091
092        return mojoAnnotatedClasses;
093    }
094
095    protected void scan( Map<String, MojoAnnotatedClass> mojoAnnotatedClasses, File source,
096                         List<String> includePatterns, Artifact artifact, boolean excludeMojo )
097        throws IOException, ExtractionException
098    {
099        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}