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}