001/* 002 * Licensed to the Apache Software Foundation (ASF) under one 003 * or more contributor license agreements. See the NOTICE file 004 * distributed with this work for additional information 005 * regarding copyright ownership. The ASF licenses this file 006 * to you under the Apache License, Version 2.0 (the 007 * "License"); you may not use this file except in compliance 008 * with the License. You may obtain a copy of the License at 009 * 010 * http://www.apache.org/licenses/LICENSE-2.0 011 * 012 * Unless required by applicable law or agreed to in writing, 013 * software distributed under the License is distributed on an 014 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 015 * KIND, either express or implied. See the License for the 016 * specific language governing permissions and limitations 017 * under the License. 018 */ 019package org.apache.maven.tools.plugin.extractor.annotations.scanner; 020 021import javax.inject.Named; 022import javax.inject.Singleton; 023 024import java.io.BufferedInputStream; 025import java.io.File; 026import java.io.FileInputStream; 027import java.io.IOException; 028import java.io.InputStream; 029import java.util.Arrays; 030import java.util.HashMap; 031import java.util.HashSet; 032import java.util.List; 033import java.util.Map; 034import java.util.regex.Pattern; 035import java.util.zip.ZipEntry; 036import java.util.zip.ZipInputStream; 037 038import org.apache.maven.artifact.Artifact; 039import org.apache.maven.plugins.annotations.Component; 040import org.apache.maven.plugins.annotations.Execute; 041import org.apache.maven.plugins.annotations.Mojo; 042import org.apache.maven.plugins.annotations.Parameter; 043import org.apache.maven.tools.plugin.extractor.ExtractionException; 044import org.apache.maven.tools.plugin.extractor.annotations.datamodel.ComponentAnnotationContent; 045import org.apache.maven.tools.plugin.extractor.annotations.datamodel.ExecuteAnnotationContent; 046import org.apache.maven.tools.plugin.extractor.annotations.datamodel.MojoAnnotationContent; 047import org.apache.maven.tools.plugin.extractor.annotations.datamodel.ParameterAnnotationContent; 048import org.apache.maven.tools.plugin.extractor.annotations.scanner.visitors.MojoAnnotationVisitor; 049import org.apache.maven.tools.plugin.extractor.annotations.scanner.visitors.MojoClassVisitor; 050import org.apache.maven.tools.plugin.extractor.annotations.scanner.visitors.MojoFieldVisitor; 051import org.apache.maven.tools.plugin.extractor.annotations.scanner.visitors.MojoParameterVisitor; 052import org.codehaus.plexus.logging.AbstractLogEnabled; 053import org.codehaus.plexus.util.DirectoryScanner; 054import org.codehaus.plexus.util.StringUtils; 055import org.codehaus.plexus.util.reflection.Reflector; 056import org.codehaus.plexus.util.reflection.ReflectorException; 057import org.objectweb.asm.ClassReader; 058import org.objectweb.asm.Type; 059 060/** 061 * Mojo scanner with java annotations. 062 * 063 * @author Olivier Lamy 064 * @since 3.0 065 */ 066@Named 067@Singleton 068public class DefaultMojoAnnotationsScanner extends AbstractLogEnabled implements MojoAnnotationsScanner { 069 public static final String MVN4_API = "org.apache.maven.api.plugin.annotations."; 070 public static final String MOJO_V4 = MVN4_API + "Mojo"; 071 public static final String EXECUTE_V4 = MVN4_API + "Execute"; 072 public static final String PARAMETER_V4 = MVN4_API + "Parameter"; 073 public static final String COMPONENT_V4 = MVN4_API + "Component"; 074 075 public static final String MOJO_V3 = Mojo.class.getName(); 076 public static final String EXECUTE_V3 = Execute.class.getName(); 077 public static final String PARAMETER_V3 = Parameter.class.getName(); 078 public static final String COMPONENT_V3 = Component.class.getName(); 079 080 // classes with a dash must be ignored 081 private static final Pattern SCANNABLE_CLASS = Pattern.compile("[^-]+\\.class"); 082 private static final String EMPTY = ""; 083 084 private Reflector reflector = new Reflector(); 085 086 @Override 087 public Map<String, MojoAnnotatedClass> scan(MojoAnnotationsScannerRequest request) throws ExtractionException { 088 Map<String, MojoAnnotatedClass> mojoAnnotatedClasses = new HashMap<>(); 089 090 try { 091 for (Artifact dependency : request.getDependencies()) { 092 scan(mojoAnnotatedClasses, dependency.getFile(), request.getIncludePatterns(), dependency, true); 093 if (request.getMavenApiVersion() == null 094 && dependency.getGroupId().equals("org.apache.maven") 095 && (dependency.getArtifactId().equals("maven-plugin-api") 096 || dependency.getArtifactId().equals("maven-api-core"))) { 097 request.setMavenApiVersion(dependency.getVersion()); 098 } 099 } 100 101 for (File classDirectory : request.getClassesDirectories()) { 102 scan( 103 mojoAnnotatedClasses, 104 classDirectory, 105 request.getIncludePatterns(), 106 request.getProject().getArtifact(), 107 false); 108 } 109 } catch (IOException e) { 110 throw new ExtractionException(e.getMessage(), e); 111 } 112 113 return mojoAnnotatedClasses; 114 } 115 116 protected void scan( 117 Map<String, MojoAnnotatedClass> mojoAnnotatedClasses, 118 File source, 119 List<String> includePatterns, 120 Artifact artifact, 121 boolean excludeMojo) 122 throws IOException, ExtractionException { 123 if (source == null || !source.exists()) { 124 return; 125 } 126 127 Map<String, MojoAnnotatedClass> scanResult; 128 if (source.isDirectory()) { 129 scanResult = scanDirectory(source, includePatterns, artifact, excludeMojo); 130 } else { 131 scanResult = scanArchive(source, artifact, excludeMojo); 132 } 133 134 mojoAnnotatedClasses.putAll(scanResult); 135 } 136 137 /** 138 * @param archiveFile 139 * @param artifact 140 * @param excludeMojo for dependencies, we exclude Mojo annotations found 141 * @return annotated classes found 142 * @throws IOException 143 * @throws ExtractionException 144 */ 145 protected Map<String, MojoAnnotatedClass> scanArchive(File archiveFile, Artifact artifact, boolean excludeMojo) 146 throws IOException, ExtractionException { 147 Map<String, MojoAnnotatedClass> mojoAnnotatedClasses = new HashMap<>(); 148 149 String zipEntryName = null; 150 try (ZipInputStream archiveStream = new ZipInputStream(new FileInputStream(archiveFile))) { 151 String archiveFilename = archiveFile.getAbsolutePath(); 152 for (ZipEntry zipEntry = archiveStream.getNextEntry(); 153 zipEntry != null; 154 zipEntry = archiveStream.getNextEntry()) { 155 zipEntryName = zipEntry.getName(); 156 if (!SCANNABLE_CLASS.matcher(zipEntryName).matches()) { 157 continue; 158 } 159 analyzeClassStream( 160 mojoAnnotatedClasses, 161 archiveStream, 162 artifact, 163 excludeMojo, 164 archiveFilename, 165 zipEntry.getName()); 166 } 167 } catch (IllegalArgumentException e) { 168 // In case of a class with newer specs an IllegalArgumentException can be thrown 169 getLogger().error("Failed to analyze " + archiveFile.getAbsolutePath() + "!/" + zipEntryName); 170 171 throw e; 172 } 173 174 return mojoAnnotatedClasses; 175 } 176 177 /** 178 * @param classDirectory 179 * @param includePatterns 180 * @param artifact 181 * @param excludeMojo for dependencies, we exclude Mojo annotations found 182 * @return annotated classes found 183 * @throws IOException 184 * @throws ExtractionException 185 */ 186 protected Map<String, MojoAnnotatedClass> scanDirectory( 187 File classDirectory, List<String> includePatterns, Artifact artifact, boolean excludeMojo) 188 throws IOException, ExtractionException { 189 Map<String, MojoAnnotatedClass> mojoAnnotatedClasses = new HashMap<>(); 190 191 DirectoryScanner scanner = new DirectoryScanner(); 192 scanner.setBasedir(classDirectory); 193 scanner.addDefaultExcludes(); 194 if (includePatterns != null) { 195 scanner.setIncludes(includePatterns.toArray(new String[includePatterns.size()])); 196 } 197 scanner.scan(); 198 String[] classFiles = scanner.getIncludedFiles(); 199 String classDirname = classDirectory.getAbsolutePath(); 200 201 for (String classFile : classFiles) { 202 if (!SCANNABLE_CLASS.matcher(classFile).matches()) { 203 continue; 204 } 205 206 try (InputStream is = // 207 new BufferedInputStream(new FileInputStream(new File(classDirectory, classFile)))) { 208 analyzeClassStream(mojoAnnotatedClasses, is, artifact, excludeMojo, classDirname, classFile); 209 } 210 } 211 return mojoAnnotatedClasses; 212 } 213 214 private void analyzeClassStream( 215 Map<String, MojoAnnotatedClass> mojoAnnotatedClasses, 216 InputStream is, 217 Artifact artifact, 218 boolean excludeMojo, 219 String source, 220 String file) 221 throws IOException, ExtractionException { 222 MojoClassVisitor mojoClassVisitor = new MojoClassVisitor(); 223 try { 224 ClassReader rdr = new ClassReader(is); 225 rdr.accept(mojoClassVisitor, ClassReader.SKIP_FRAMES | ClassReader.SKIP_CODE | ClassReader.SKIP_DEBUG); 226 } catch (ArrayIndexOutOfBoundsException aiooe) { 227 getLogger() 228 .warn( 229 "Error analyzing class " + file + " in " + source + ": ignoring class", 230 getLogger().isDebugEnabled() ? aiooe : null); 231 return; 232 } catch (IllegalArgumentException iae) { 233 if (iae.getMessage() == null) { 234 getLogger() 235 .warn( 236 "Error analyzing class " + file + " in " + source + ": ignoring class", 237 getLogger().isDebugEnabled() ? iae : null); 238 return; 239 } else { 240 throw iae; 241 } 242 } 243 244 analyzeVisitors(mojoClassVisitor); 245 246 MojoAnnotatedClass mojoAnnotatedClass = mojoClassVisitor.getMojoAnnotatedClass(); 247 248 if (excludeMojo) { 249 mojoAnnotatedClass.setMojo(null); 250 } 251 252 if (mojoAnnotatedClass != null) // see MPLUGIN-206 we can have intermediate classes without annotations 253 { 254 if (getLogger().isDebugEnabled() && mojoAnnotatedClass.hasAnnotations()) { 255 getLogger() 256 .debug("found MojoAnnotatedClass:" + mojoAnnotatedClass.getClassName() + ":" 257 + mojoAnnotatedClass); 258 } 259 mojoAnnotatedClass.setArtifact(artifact); 260 mojoAnnotatedClasses.put(mojoAnnotatedClass.getClassName(), mojoAnnotatedClass); 261 mojoAnnotatedClass.setClassVersion(mojoClassVisitor.getVersion()); 262 } 263 } 264 265 protected void populateAnnotationContent(Object content, MojoAnnotationVisitor mojoAnnotationVisitor) 266 throws ReflectorException { 267 for (Map.Entry<String, Object> entry : 268 mojoAnnotationVisitor.getAnnotationValues().entrySet()) { 269 reflector.invoke(content, entry.getKey(), new Object[] {entry.getValue()}); 270 } 271 } 272 273 protected void analyzeVisitors(MojoClassVisitor mojoClassVisitor) throws ExtractionException { 274 final MojoAnnotatedClass mojoAnnotatedClass = mojoClassVisitor.getMojoAnnotatedClass(); 275 276 try { 277 // @Mojo annotation 278 MojoAnnotationVisitor mojoAnnotationVisitor = mojoClassVisitor.getAnnotationVisitor(MOJO_V3); 279 if (mojoAnnotationVisitor == null) { 280 mojoAnnotationVisitor = mojoClassVisitor.getAnnotationVisitor(MOJO_V4); 281 } 282 if (mojoAnnotationVisitor != null) { 283 MojoAnnotationContent mojoAnnotationContent = new MojoAnnotationContent(); 284 populateAnnotationContent(mojoAnnotationContent, mojoAnnotationVisitor); 285 286 if (mojoClassVisitor.getAnnotationVisitor(Deprecated.class) != null) { 287 mojoAnnotationContent.setDeprecated(EMPTY); 288 } 289 290 mojoAnnotatedClass.setMojo(mojoAnnotationContent); 291 } 292 293 // @Execute annotation 294 mojoAnnotationVisitor = mojoClassVisitor.getAnnotationVisitor(EXECUTE_V3); 295 if (mojoAnnotationVisitor == null) { 296 mojoAnnotationVisitor = mojoClassVisitor.getAnnotationVisitor(EXECUTE_V4); 297 } 298 if (mojoAnnotationVisitor != null) { 299 ExecuteAnnotationContent executeAnnotationContent = new ExecuteAnnotationContent(); 300 populateAnnotationContent(executeAnnotationContent, mojoAnnotationVisitor); 301 mojoAnnotatedClass.setExecute(executeAnnotationContent); 302 } 303 304 // @Parameter annotations 305 List<MojoParameterVisitor> mojoParameterVisitors = 306 mojoClassVisitor.findParameterVisitors(new HashSet<>(Arrays.asList(PARAMETER_V3, PARAMETER_V4))); 307 for (MojoParameterVisitor parameterVisitor : mojoParameterVisitors) { 308 ParameterAnnotationContent parameterAnnotationContent = new ParameterAnnotationContent( 309 parameterVisitor.getFieldName(), 310 parameterVisitor.getClassName(), 311 parameterVisitor.getTypeParameters(), 312 parameterVisitor.isAnnotationOnMethod()); 313 314 Map<String, MojoAnnotationVisitor> annotationVisitorMap = parameterVisitor.getAnnotationVisitorMap(); 315 MojoAnnotationVisitor fieldAnnotationVisitor = annotationVisitorMap.get(PARAMETER_V3); 316 if (fieldAnnotationVisitor == null) { 317 fieldAnnotationVisitor = annotationVisitorMap.get(PARAMETER_V4); 318 } 319 320 if (fieldAnnotationVisitor != null) { 321 populateAnnotationContent(parameterAnnotationContent, fieldAnnotationVisitor); 322 } 323 324 if (annotationVisitorMap.containsKey(Deprecated.class.getName())) { 325 parameterAnnotationContent.setDeprecated(EMPTY); 326 } 327 328 mojoAnnotatedClass 329 .getParameters() 330 .put(parameterAnnotationContent.getFieldName(), parameterAnnotationContent); 331 } 332 333 // @Component annotations 334 List<MojoFieldVisitor> mojoFieldVisitors = 335 mojoClassVisitor.findFieldWithAnnotation(new HashSet<>(Arrays.asList(COMPONENT_V3, COMPONENT_V4))); 336 for (MojoFieldVisitor mojoFieldVisitor : mojoFieldVisitors) { 337 ComponentAnnotationContent componentAnnotationContent = 338 new ComponentAnnotationContent(mojoFieldVisitor.getFieldName()); 339 340 Map<String, MojoAnnotationVisitor> annotationVisitorMap = mojoFieldVisitor.getAnnotationVisitorMap(); 341 MojoAnnotationVisitor annotationVisitor = annotationVisitorMap.get(COMPONENT_V3); 342 if (annotationVisitor == null) { 343 annotationVisitor = annotationVisitorMap.get(COMPONENT_V4); 344 } 345 346 if (annotationVisitor != null) { 347 for (Map.Entry<String, Object> entry : 348 annotationVisitor.getAnnotationValues().entrySet()) { 349 String methodName = entry.getKey(); 350 if ("role".equals(methodName)) { 351 Type type = (Type) entry.getValue(); 352 componentAnnotationContent.setRoleClassName(type.getClassName()); 353 } else { 354 reflector.invoke( 355 componentAnnotationContent, entry.getKey(), new Object[] {entry.getValue()}); 356 } 357 } 358 359 if (StringUtils.isEmpty(componentAnnotationContent.getRoleClassName())) { 360 componentAnnotationContent.setRoleClassName(mojoFieldVisitor.getClassName()); 361 } 362 } 363 mojoAnnotatedClass 364 .getComponents() 365 .put(componentAnnotationContent.getFieldName(), componentAnnotationContent); 366 } 367 } catch (ReflectorException e) { 368 throw new ExtractionException(e.getMessage(), e); 369 } 370 } 371}