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            
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 );
146            }
147        }
148        catch ( IllegalArgumentException e )
149        {
150            // In case of a class with newer specs an IllegalArgumentException can be thrown
151            getLogger().error( "Failed to analyze " + archiveFile.getAbsolutePath() + "!/" + zipEntryName );
152            
153            throw e;
154        }
155        finally
156        {
157            IOUtil.close( archiveStream );
158        }
159
160        return mojoAnnotatedClasses;
161    }
162
163    /**
164     * @param classDirectory
165     * @param includePatterns
166     * @param artifact
167     * @param excludeMojo     for dependencies, we exclude Mojo annotations found
168     * @return annotated classes found
169     * @throws IOException
170     * @throws ExtractionException
171     */
172    protected Map<String, MojoAnnotatedClass> scanDirectory( File classDirectory, List<String> includePatterns,
173                                                             Artifact artifact, boolean excludeMojo )
174        throws IOException, ExtractionException
175    {
176        Map<String, MojoAnnotatedClass> mojoAnnotatedClasses = new HashMap<String, MojoAnnotatedClass>();
177
178        DirectoryScanner scanner = new DirectoryScanner();
179        scanner.setBasedir( classDirectory );
180        scanner.addDefaultExcludes();
181        if ( includePatterns != null )
182        {
183            scanner.setIncludes( includePatterns.toArray( new String[includePatterns.size()] ) );
184        }
185        scanner.scan();
186        String[] classFiles = scanner.getIncludedFiles();
187
188        for ( String classFile : classFiles )
189        {
190            if ( !SCANNABLE_CLASS.matcher( classFile ).matches() )
191            {
192                continue;
193            }
194
195            InputStream is = new BufferedInputStream( new FileInputStream( new File( classDirectory, classFile ) ) );
196            try
197            {
198                analyzeClassStream( mojoAnnotatedClasses, is, artifact, excludeMojo );
199            }
200            finally
201            {
202                IOUtil.close( is );
203            }
204        }
205        return mojoAnnotatedClasses;
206    }
207
208    private void analyzeClassStream( Map<String, MojoAnnotatedClass> mojoAnnotatedClasses, InputStream is,
209                                     Artifact artifact, boolean excludeMojo )
210        throws IOException, ExtractionException
211    {
212        MojoClassVisitor mojoClassVisitor = new MojoClassVisitor( getLogger() );
213
214        ClassReader rdr = new ClassReader( is );
215        rdr.accept( mojoClassVisitor, ClassReader.SKIP_FRAMES | ClassReader.SKIP_CODE | ClassReader.SKIP_DEBUG );
216
217        analyzeVisitors( mojoClassVisitor );
218
219        MojoAnnotatedClass mojoAnnotatedClass = mojoClassVisitor.getMojoAnnotatedClass();
220
221        if ( excludeMojo )
222        {
223            mojoAnnotatedClass.setMojo( null );
224        }
225
226        if ( mojoAnnotatedClass != null ) // see MPLUGIN-206 we can have intermediate classes without annotations
227        {
228            if ( getLogger().isDebugEnabled() && mojoAnnotatedClass.hasAnnotations() )
229            {
230                getLogger().debug( "found MojoAnnotatedClass:" + mojoAnnotatedClass.getClassName() + ":"
231                                       + mojoAnnotatedClass );
232            }
233            mojoAnnotatedClass.setArtifact( artifact );
234            mojoAnnotatedClasses.put( mojoAnnotatedClass.getClassName(), mojoAnnotatedClass );
235        }
236    }
237
238    protected void populateAnnotationContent( Object content, MojoAnnotationVisitor mojoAnnotationVisitor )
239        throws ReflectorException
240    {
241        for ( Map.Entry<String, Object> entry : mojoAnnotationVisitor.getAnnotationValues().entrySet() )
242        {
243            reflector.invoke( content, entry.getKey(), new Object[] { entry.getValue() } );
244        }
245    }
246
247    protected void analyzeVisitors( MojoClassVisitor mojoClassVisitor )
248        throws ExtractionException
249    {
250        final MojoAnnotatedClass mojoAnnotatedClass = mojoClassVisitor.getMojoAnnotatedClass();
251
252        try
253        {
254            // @Mojo annotation
255            MojoAnnotationVisitor mojoAnnotationVisitor = mojoClassVisitor.getAnnotationVisitor( Mojo.class );
256            if ( mojoAnnotationVisitor != null )
257            {
258                MojoAnnotationContent mojoAnnotationContent = new MojoAnnotationContent();
259                populateAnnotationContent( mojoAnnotationContent, mojoAnnotationVisitor );
260                mojoAnnotatedClass.setMojo( mojoAnnotationContent );
261            }
262
263            // @Execute annotation
264            mojoAnnotationVisitor = mojoClassVisitor.getAnnotationVisitor( Execute.class );
265            if ( mojoAnnotationVisitor != null )
266            {
267                ExecuteAnnotationContent executeAnnotationContent = new ExecuteAnnotationContent();
268                populateAnnotationContent( executeAnnotationContent, mojoAnnotationVisitor );
269                mojoAnnotatedClass.setExecute( executeAnnotationContent );
270            }
271
272            // @Parameter annotations
273            List<MojoFieldVisitor> mojoFieldVisitors = mojoClassVisitor.findFieldWithAnnotation( Parameter.class );
274            for ( MojoFieldVisitor mojoFieldVisitor : mojoFieldVisitors )
275            {
276                ParameterAnnotationContent parameterAnnotationContent =
277                    new ParameterAnnotationContent( mojoFieldVisitor.getFieldName(), mojoFieldVisitor.getClassName() );
278                if ( mojoFieldVisitor.getMojoAnnotationVisitor() != null )
279                {
280                    populateAnnotationContent( parameterAnnotationContent,
281                                               mojoFieldVisitor.getMojoAnnotationVisitor() );
282                }
283
284                mojoAnnotatedClass.getParameters().put( parameterAnnotationContent.getFieldName(),
285                                                        parameterAnnotationContent );
286            }
287
288            // @Component annotations
289            mojoFieldVisitors = mojoClassVisitor.findFieldWithAnnotation( Component.class );
290            for ( MojoFieldVisitor mojoFieldVisitor : mojoFieldVisitors )
291            {
292                ComponentAnnotationContent componentAnnotationContent =
293                    new ComponentAnnotationContent( mojoFieldVisitor.getFieldName() );
294
295                MojoAnnotationVisitor annotationVisitor = mojoFieldVisitor.getMojoAnnotationVisitor();
296                if ( annotationVisitor != null )
297                {
298                    for ( Map.Entry<String, Object> entry : annotationVisitor.getAnnotationValues().entrySet() )
299                    {
300                        String methodName = entry.getKey();
301                        if ( StringUtils.equals( "role", methodName ) )
302                        {
303                            Type type = (Type) entry.getValue();
304                            componentAnnotationContent.setRoleClassName( type.getClassName() );
305                        }
306                        else
307                        {
308                            reflector.invoke( componentAnnotationContent, entry.getKey(),
309                                              new Object[]{ entry.getValue() } );
310                        }
311                    }
312
313                    if ( StringUtils.isEmpty( componentAnnotationContent.getRoleClassName() ) )
314                    {
315                        componentAnnotationContent.setRoleClassName( mojoFieldVisitor.getClassName() );
316                    }
317                }
318                mojoAnnotatedClass.getComponents().put( componentAnnotationContent.getFieldName(),
319                                                        componentAnnotationContent );
320            }
321        }
322        catch ( ReflectorException e )
323        {
324            throw new ExtractionException( e.getMessage(), e );
325        }
326    }
327}