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 java.io.BufferedInputStream;
023import java.io.File;
024import java.io.FileInputStream;
025import java.io.IOException;
026import java.io.InputStream;
027import java.util.HashMap;
028import java.util.List;
029import java.util.Map;
030import java.util.regex.Pattern;
031import java.util.zip.ZipEntry;
032import java.util.zip.ZipInputStream;
033
034import org.apache.maven.artifact.Artifact;
035import org.apache.maven.plugins.annotations.Component;
036import org.apache.maven.plugins.annotations.Execute;
037import org.apache.maven.plugins.annotations.Mojo;
038import org.apache.maven.plugins.annotations.Parameter;
039import org.apache.maven.tools.plugin.extractor.ExtractionException;
040import org.apache.maven.tools.plugin.extractor.annotations.datamodel.ComponentAnnotationContent;
041import org.apache.maven.tools.plugin.extractor.annotations.datamodel.ExecuteAnnotationContent;
042import org.apache.maven.tools.plugin.extractor.annotations.datamodel.MojoAnnotationContent;
043import org.apache.maven.tools.plugin.extractor.annotations.datamodel.ParameterAnnotationContent;
044import org.apache.maven.tools.plugin.extractor.annotations.scanner.visitors.MojoAnnotationVisitor;
045import org.apache.maven.tools.plugin.extractor.annotations.scanner.visitors.MojoClassVisitor;
046import org.apache.maven.tools.plugin.extractor.annotations.scanner.visitors.MojoFieldVisitor;
047import org.codehaus.plexus.logging.AbstractLogEnabled;
048import org.codehaus.plexus.util.DirectoryScanner;
049import org.codehaus.plexus.util.IOUtil;
050import org.codehaus.plexus.util.StringUtils;
051import org.codehaus.plexus.util.reflection.Reflector;
052import org.codehaus.plexus.util.reflection.ReflectorException;
053import org.objectweb.asm.ClassReader;
054import org.objectweb.asm.Type;
055
056/**
057 * @author Olivier Lamy
058 * @since 3.0
059 */
060@org.codehaus.plexus.component.annotations.Component( role = MojoAnnotationsScanner.class )
061public class DefaultMojoAnnotationsScanner
062    extends AbstractLogEnabled
063    implements MojoAnnotationsScanner
064{
065    // classes with a dash must be ignored
066    private static final Pattern SCANNABLE_CLASS = Pattern.compile( "[^-]+\\.class" );
067    
068    private Reflector reflector = new Reflector();
069
070    public Map<String, MojoAnnotatedClass> scan( MojoAnnotationsScannerRequest request )
071        throws ExtractionException
072    {
073        Map<String, MojoAnnotatedClass> mojoAnnotatedClasses = new HashMap<String, MojoAnnotatedClass>();
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<String, MojoAnnotatedClass>();
130
131        ZipInputStream archiveStream = new ZipInputStream( new FileInputStream( archiveFile ) );
132
133        String zipEntryName = null;
134        try
135        {
136            String archiveFilename = archiveFile.getAbsolutePath();
137            for ( ZipEntry zipEntry = archiveStream.getNextEntry(); zipEntry != null;
138                  zipEntry = archiveStream.getNextEntry() )
139            {
140                zipEntryName = zipEntry.getName();
141                if ( !SCANNABLE_CLASS.matcher( zipEntryName ).matches() )
142                {
143                    continue;
144                }
145                analyzeClassStream( mojoAnnotatedClasses, archiveStream, artifact, excludeMojo, archiveFilename,
146                                    zipEntry.getName() );
147            }
148        }
149        catch ( IllegalArgumentException e )
150        {
151            // In case of a class with newer specs an IllegalArgumentException can be thrown
152            getLogger().error( "Failed to analyze " + archiveFile.getAbsolutePath() + "!/" + zipEntryName );
153            
154            throw e;
155        }
156        finally
157        {
158            IOUtil.close( archiveStream );
159        }
160
161        return mojoAnnotatedClasses;
162    }
163
164    /**
165     * @param classDirectory
166     * @param includePatterns
167     * @param artifact
168     * @param excludeMojo     for dependencies, we exclude Mojo annotations found
169     * @return annotated classes found
170     * @throws IOException
171     * @throws ExtractionException
172     */
173    protected Map<String, MojoAnnotatedClass> scanDirectory( File classDirectory, List<String> includePatterns,
174                                                             Artifact artifact, boolean excludeMojo )
175        throws IOException, ExtractionException
176    {
177        Map<String, MojoAnnotatedClass> mojoAnnotatedClasses = new HashMap<String, MojoAnnotatedClass>();
178
179        DirectoryScanner scanner = new DirectoryScanner();
180        scanner.setBasedir( classDirectory );
181        scanner.addDefaultExcludes();
182        if ( includePatterns != null )
183        {
184            scanner.setIncludes( includePatterns.toArray( new String[includePatterns.size()] ) );
185        }
186        scanner.scan();
187        String[] classFiles = scanner.getIncludedFiles();
188        String classDirname = classDirectory.getAbsolutePath();
189
190        for ( String classFile : classFiles )
191        {
192            if ( !SCANNABLE_CLASS.matcher( classFile ).matches() )
193            {
194                continue;
195            }
196
197            InputStream is = new BufferedInputStream( new FileInputStream( new File( classDirectory, classFile ) ) );
198            try
199            {
200                analyzeClassStream( mojoAnnotatedClasses, is, artifact, excludeMojo, classDirname, classFile );
201            }
202            finally
203            {
204                IOUtil.close( is );
205            }
206        }
207        return mojoAnnotatedClasses;
208    }
209
210    private void analyzeClassStream( Map<String, MojoAnnotatedClass> mojoAnnotatedClasses, InputStream is,
211                                     Artifact artifact, boolean excludeMojo, String source, String file )
212        throws IOException, ExtractionException
213    {
214        MojoClassVisitor mojoClassVisitor = new MojoClassVisitor( getLogger() );
215
216        try
217        {
218            ClassReader rdr = new ClassReader( is );
219            rdr.accept( mojoClassVisitor, ClassReader.SKIP_FRAMES | ClassReader.SKIP_CODE | ClassReader.SKIP_DEBUG );
220        }
221        catch ( ArrayIndexOutOfBoundsException aiooe )
222        {
223            getLogger().warn( "Error analyzing class " + file + " in " + source + ": ignoring class",
224                              getLogger().isDebugEnabled() ? aiooe : null );
225            return;
226        }
227        catch ( IllegalArgumentException iae )
228        {
229            if ( iae.getMessage() == null )
230            {
231                getLogger().warn( "Error analyzing class " + file + " in " + source + ": ignoring class",
232                        getLogger().isDebugEnabled() ? iae : null );
233                return;
234            }
235            else
236            {
237                throw iae;
238            }
239        }
240
241        analyzeVisitors( mojoClassVisitor );
242
243        MojoAnnotatedClass mojoAnnotatedClass = mojoClassVisitor.getMojoAnnotatedClass();
244
245        if ( excludeMojo )
246        {
247            mojoAnnotatedClass.setMojo( null );
248        }
249
250        if ( mojoAnnotatedClass != null ) // see MPLUGIN-206 we can have intermediate classes without annotations
251        {
252            if ( getLogger().isDebugEnabled() && mojoAnnotatedClass.hasAnnotations() )
253            {
254                getLogger().debug( "found MojoAnnotatedClass:" + mojoAnnotatedClass.getClassName() + ":"
255                                       + mojoAnnotatedClass );
256            }
257            mojoAnnotatedClass.setArtifact( artifact );
258            mojoAnnotatedClasses.put( mojoAnnotatedClass.getClassName(), mojoAnnotatedClass );
259        }
260    }
261
262    protected void populateAnnotationContent( Object content, MojoAnnotationVisitor mojoAnnotationVisitor )
263        throws ReflectorException
264    {
265        for ( Map.Entry<String, Object> entry : mojoAnnotationVisitor.getAnnotationValues().entrySet() )
266        {
267            reflector.invoke( content, entry.getKey(), new Object[] { entry.getValue() } );
268        }
269    }
270
271    protected void analyzeVisitors( MojoClassVisitor mojoClassVisitor )
272        throws ExtractionException
273    {
274        final MojoAnnotatedClass mojoAnnotatedClass = mojoClassVisitor.getMojoAnnotatedClass();
275
276        try
277        {
278            // @Mojo annotation
279            MojoAnnotationVisitor mojoAnnotationVisitor = mojoClassVisitor.getAnnotationVisitor( Mojo.class );
280            if ( mojoAnnotationVisitor != null )
281            {
282                MojoAnnotationContent mojoAnnotationContent = new MojoAnnotationContent();
283                populateAnnotationContent( mojoAnnotationContent, mojoAnnotationVisitor );
284                mojoAnnotatedClass.setMojo( mojoAnnotationContent );
285            }
286
287            // @Execute annotation
288            mojoAnnotationVisitor = mojoClassVisitor.getAnnotationVisitor( Execute.class );
289            if ( mojoAnnotationVisitor != null )
290            {
291                ExecuteAnnotationContent executeAnnotationContent = new ExecuteAnnotationContent();
292                populateAnnotationContent( executeAnnotationContent, mojoAnnotationVisitor );
293                mojoAnnotatedClass.setExecute( executeAnnotationContent );
294            }
295
296            // @Parameter annotations
297            List<MojoFieldVisitor> mojoFieldVisitors = mojoClassVisitor.findFieldWithAnnotation( Parameter.class );
298            for ( MojoFieldVisitor mojoFieldVisitor : mojoFieldVisitors )
299            {
300                ParameterAnnotationContent parameterAnnotationContent =
301                    new ParameterAnnotationContent( mojoFieldVisitor.getFieldName(), mojoFieldVisitor.getClassName() );
302                if ( mojoFieldVisitor.getMojoAnnotationVisitor() != null )
303                {
304                    populateAnnotationContent( parameterAnnotationContent,
305                                               mojoFieldVisitor.getMojoAnnotationVisitor() );
306                }
307
308                mojoAnnotatedClass.getParameters().put( parameterAnnotationContent.getFieldName(),
309                                                        parameterAnnotationContent );
310            }
311
312            // @Component annotations
313            mojoFieldVisitors = mojoClassVisitor.findFieldWithAnnotation( Component.class );
314            for ( MojoFieldVisitor mojoFieldVisitor : mojoFieldVisitors )
315            {
316                ComponentAnnotationContent componentAnnotationContent =
317                    new ComponentAnnotationContent( mojoFieldVisitor.getFieldName() );
318
319                MojoAnnotationVisitor annotationVisitor = mojoFieldVisitor.getMojoAnnotationVisitor();
320                if ( annotationVisitor != null )
321                {
322                    for ( Map.Entry<String, Object> entry : annotationVisitor.getAnnotationValues().entrySet() )
323                    {
324                        String methodName = entry.getKey();
325                        if ( StringUtils.equals( "role", methodName ) )
326                        {
327                            Type type = (Type) entry.getValue();
328                            componentAnnotationContent.setRoleClassName( type.getClassName() );
329                        }
330                        else
331                        {
332                            reflector.invoke( componentAnnotationContent, entry.getKey(),
333                                              new Object[]{ entry.getValue() } );
334                        }
335                    }
336
337                    if ( StringUtils.isEmpty( componentAnnotationContent.getRoleClassName() ) )
338                    {
339                        componentAnnotationContent.setRoleClassName( mojoFieldVisitor.getClassName() );
340                    }
341                }
342                mojoAnnotatedClass.getComponents().put( componentAnnotationContent.getFieldName(),
343                                                        componentAnnotationContent );
344            }
345        }
346        catch ( ReflectorException e )
347        {
348            throw new ExtractionException( e.getMessage(), e );
349        }
350    }
351}