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}