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.ArrayList; 030import java.util.Arrays; 031import java.util.HashMap; 032import java.util.HashSet; 033import java.util.List; 034import java.util.Map; 035import java.util.Objects; 036import java.util.regex.Pattern; 037import java.util.zip.ZipEntry; 038import java.util.zip.ZipInputStream; 039 040import org.apache.maven.artifact.Artifact; 041import org.apache.maven.plugins.annotations.Component; 042import org.apache.maven.plugins.annotations.Execute; 043import org.apache.maven.plugins.annotations.Mojo; 044import org.apache.maven.plugins.annotations.Parameter; 045import org.apache.maven.tools.plugin.extractor.ExtractionException; 046import org.apache.maven.tools.plugin.extractor.annotations.datamodel.AfterAnnotationContent; 047import org.apache.maven.tools.plugin.extractor.annotations.datamodel.ComponentAnnotationContent; 048import org.apache.maven.tools.plugin.extractor.annotations.datamodel.ExecuteAnnotationContent; 049import org.apache.maven.tools.plugin.extractor.annotations.datamodel.MojoAnnotationContent; 050import org.apache.maven.tools.plugin.extractor.annotations.datamodel.ParameterAnnotationContent; 051import org.apache.maven.tools.plugin.extractor.annotations.scanner.visitors.MojoAnnotationVisitor; 052import org.apache.maven.tools.plugin.extractor.annotations.scanner.visitors.MojoClassVisitor; 053import org.apache.maven.tools.plugin.extractor.annotations.scanner.visitors.MojoFieldVisitor; 054import org.apache.maven.tools.plugin.extractor.annotations.scanner.visitors.MojoParameterVisitor; 055import org.codehaus.plexus.util.DirectoryScanner; 056import org.codehaus.plexus.util.StringUtils; 057import org.codehaus.plexus.util.reflection.Reflector; 058import org.codehaus.plexus.util.reflection.ReflectorException; 059import org.objectweb.asm.ClassReader; 060import org.objectweb.asm.Type; 061import org.slf4j.Logger; 062import org.slf4j.LoggerFactory; 063 064/** 065 * Mojo scanner with java annotations. 066 * 067 * @author Olivier Lamy 068 * @since 3.0 069 */ 070@Named 071@Singleton 072public class DefaultMojoAnnotationsScanner implements MojoAnnotationsScanner { 073 private static final Logger LOGGER = LoggerFactory.getLogger(DefaultMojoAnnotationsScanner.class); 074 public static final String MVN4_API = "org.apache.maven.api.plugin.annotations."; 075 public static final String MOJO_V4 = MVN4_API + "Mojo"; 076 public static final String EXECUTE_V4 = MVN4_API + "Execute"; 077 public static final String PARAMETER_V4 = MVN4_API + "Parameter"; 078 public static final String AFTER_V4 = MVN4_API + "After"; 079 public static final String AFTERS_V4 = MVN4_API + "Afters"; 080 081 public static final String MOJO_V3 = Mojo.class.getName(); 082 public static final String EXECUTE_V3 = Execute.class.getName(); 083 public static final String PARAMETER_V3 = Parameter.class.getName(); 084 public static final String COMPONENT_V3 = Component.class.getName(); 085 086 // classes with a dash must be ignored 087 private static final Pattern SCANNABLE_CLASS = Pattern.compile("[^-]+\\.class"); 088 private static final String EMPTY = ""; 089 090 private Reflector reflector = new Reflector(); 091 092 @Override 093 public Map<String, MojoAnnotatedClass> scan(MojoAnnotationsScannerRequest request) throws ExtractionException { 094 Map<String, MojoAnnotatedClass> mojoAnnotatedClasses = new HashMap<>(); 095 096 try { 097 String mavenApiVersion = null; 098 for (Artifact dependency : request.getDependencies()) { 099 scan(mojoAnnotatedClasses, dependency.getFile(), request.getIncludePatterns(), dependency, true); 100 if (request.getMavenApiVersion() == null 101 && dependency.getGroupId().equals("org.apache.maven") 102 && (dependency.getArtifactId().equals("maven-plugin-api") 103 || dependency.getArtifactId().equals("maven-api-core"))) { 104 String version = dependency.getVersion(); 105 if (mavenApiVersion != null && !Objects.equals(version, mavenApiVersion)) { 106 throw new UnsupportedOperationException("Mixing Maven 3 and Maven 4 plugins is not supported." 107 + " Fix your dependencies so that you depend either on maven-plugin-api for a Maven 3 plugin," 108 + " or maven-api-core for a Maven 4 plugin."); 109 } 110 mavenApiVersion = version; 111 } 112 } 113 request.setMavenApiVersion(mavenApiVersion); 114 115 for (File classDirectory : request.getClassesDirectories()) { 116 scan( 117 mojoAnnotatedClasses, 118 classDirectory, 119 request.getIncludePatterns(), 120 request.getProject().getArtifact(), 121 false); 122 } 123 } catch (IOException e) { 124 throw new ExtractionException(e.getMessage(), e); 125 } 126 127 return mojoAnnotatedClasses; 128 } 129 130 protected void scan( 131 Map<String, MojoAnnotatedClass> mojoAnnotatedClasses, 132 File source, 133 List<String> includePatterns, 134 Artifact artifact, 135 boolean excludeMojo) 136 throws IOException, ExtractionException { 137 if (source == null || !source.exists()) { 138 return; 139 } 140 141 Map<String, MojoAnnotatedClass> scanResult; 142 if (source.isDirectory()) { 143 scanResult = scanDirectory(source, includePatterns, artifact, excludeMojo); 144 } else { 145 scanResult = scanArchive(source, artifact, excludeMojo); 146 } 147 148 mojoAnnotatedClasses.putAll(scanResult); 149 } 150 151 /** 152 * @param archiveFile 153 * @param artifact 154 * @param excludeMojo for dependencies, we exclude Mojo annotations found 155 * @return annotated classes found 156 * @throws IOException 157 * @throws ExtractionException 158 */ 159 protected Map<String, MojoAnnotatedClass> scanArchive(File archiveFile, Artifact artifact, boolean excludeMojo) 160 throws IOException, ExtractionException { 161 Map<String, MojoAnnotatedClass> mojoAnnotatedClasses = new HashMap<>(); 162 163 String zipEntryName = null; 164 try (ZipInputStream archiveStream = new ZipInputStream(new FileInputStream(archiveFile))) { 165 String archiveFilename = archiveFile.getAbsolutePath(); 166 for (ZipEntry zipEntry = archiveStream.getNextEntry(); 167 zipEntry != null; 168 zipEntry = archiveStream.getNextEntry()) { 169 zipEntryName = zipEntry.getName(); 170 if (!SCANNABLE_CLASS.matcher(zipEntryName).matches()) { 171 continue; 172 } 173 analyzeClassStream( 174 mojoAnnotatedClasses, 175 archiveStream, 176 artifact, 177 excludeMojo, 178 archiveFilename, 179 zipEntry.getName()); 180 } 181 } catch (IllegalArgumentException e) { 182 // In case of a class with newer specs an IllegalArgumentException can be thrown 183 LOGGER.error("Failed to analyze " + archiveFile.getAbsolutePath() + "!/" + zipEntryName); 184 185 throw e; 186 } 187 188 return mojoAnnotatedClasses; 189 } 190 191 /** 192 * @param classDirectory 193 * @param includePatterns 194 * @param artifact 195 * @param excludeMojo for dependencies, we exclude Mojo annotations found 196 * @return annotated classes found 197 * @throws IOException 198 * @throws ExtractionException 199 */ 200 protected Map<String, MojoAnnotatedClass> scanDirectory( 201 File classDirectory, List<String> includePatterns, Artifact artifact, boolean excludeMojo) 202 throws IOException, ExtractionException { 203 Map<String, MojoAnnotatedClass> mojoAnnotatedClasses = new HashMap<>(); 204 205 DirectoryScanner scanner = new DirectoryScanner(); 206 scanner.setBasedir(classDirectory); 207 scanner.addDefaultExcludes(); 208 if (includePatterns != null) { 209 scanner.setIncludes(includePatterns.toArray(new String[includePatterns.size()])); 210 } 211 scanner.scan(); 212 String[] classFiles = scanner.getIncludedFiles(); 213 String classDirname = classDirectory.getAbsolutePath(); 214 215 for (String classFile : classFiles) { 216 if (!SCANNABLE_CLASS.matcher(classFile).matches()) { 217 continue; 218 } 219 220 try (InputStream is = // 221 new BufferedInputStream(new FileInputStream(new File(classDirectory, classFile)))) { 222 analyzeClassStream(mojoAnnotatedClasses, is, artifact, excludeMojo, classDirname, classFile); 223 } 224 } 225 return mojoAnnotatedClasses; 226 } 227 228 private void analyzeClassStream( 229 Map<String, MojoAnnotatedClass> mojoAnnotatedClasses, 230 InputStream is, 231 Artifact artifact, 232 boolean excludeMojo, 233 String source, 234 String file) 235 throws IOException, ExtractionException { 236 MojoClassVisitor mojoClassVisitor = new MojoClassVisitor(); 237 try { 238 ClassReader rdr = new ClassReader(is); 239 rdr.accept(mojoClassVisitor, ClassReader.SKIP_FRAMES | ClassReader.SKIP_CODE | ClassReader.SKIP_DEBUG); 240 } catch (ArrayIndexOutOfBoundsException aiooe) { 241 LOGGER.warn( 242 "Error analyzing class " + file + " in " + source + ": ignoring class", 243 LOGGER.isDebugEnabled() ? aiooe : null); 244 return; 245 } catch (IllegalArgumentException iae) { 246 if (iae.getMessage() == null) { 247 LOGGER.warn( 248 "Error analyzing class " + file + " in " + source + ": ignoring class", 249 LOGGER.isDebugEnabled() ? iae : null); 250 return; 251 } else { 252 throw iae; 253 } 254 } 255 256 analyzeVisitors(mojoClassVisitor); 257 258 MojoAnnotatedClass mojoAnnotatedClass = mojoClassVisitor.getMojoAnnotatedClass(); 259 260 if (excludeMojo) { 261 mojoAnnotatedClass.setMojo(null); 262 } 263 264 if (mojoAnnotatedClass != null) // see MPLUGIN-206 we can have intermediate classes without annotations 265 { 266 if (LOGGER.isDebugEnabled() && mojoAnnotatedClass.hasAnnotations()) { 267 LOGGER.debug( 268 "found MojoAnnotatedClass:" + mojoAnnotatedClass.getClassName() + ":" + mojoAnnotatedClass); 269 } 270 mojoAnnotatedClass.setArtifact(artifact); 271 mojoAnnotatedClasses.put(mojoAnnotatedClass.getClassName(), mojoAnnotatedClass); 272 mojoAnnotatedClass.setClassVersion(mojoClassVisitor.getVersion()); 273 } 274 } 275 276 protected void populateAnnotationContent(Object content, MojoAnnotationVisitor mojoAnnotationVisitor) 277 throws ReflectorException { 278 for (Map.Entry<String, Object> entry : 279 mojoAnnotationVisitor.getAnnotationValues().entrySet()) { 280 reflector.invoke(content, entry.getKey(), new Object[] {entry.getValue()}); 281 } 282 } 283 284 protected void analyzeVisitors(MojoClassVisitor mojoClassVisitor) throws ExtractionException { 285 final MojoAnnotatedClass mojoAnnotatedClass = mojoClassVisitor.getMojoAnnotatedClass(); 286 287 try { 288 // @Mojo annotation 289 MojoAnnotationVisitor mojoAnnotationVisitor = mojoClassVisitor.getAnnotationVisitor(MOJO_V3); 290 if (mojoAnnotationVisitor == null) { 291 mojoAnnotationVisitor = mojoClassVisitor.getAnnotationVisitor(MOJO_V4); 292 } 293 if (mojoAnnotationVisitor != null) { 294 MojoAnnotationContent mojoAnnotationContent = new MojoAnnotationContent(); 295 populateAnnotationContent(mojoAnnotationContent, mojoAnnotationVisitor); 296 297 if (mojoClassVisitor.getAnnotationVisitor(Deprecated.class) != null) { 298 mojoAnnotationContent.setDeprecated(EMPTY); 299 } 300 301 mojoAnnotatedClass.setMojo(mojoAnnotationContent); 302 } 303 304 // @Execute annotation 305 mojoAnnotationVisitor = mojoClassVisitor.getAnnotationVisitor(EXECUTE_V3); 306 if (mojoAnnotationVisitor == null) { 307 mojoAnnotationVisitor = mojoClassVisitor.getAnnotationVisitor(EXECUTE_V4); 308 } 309 if (mojoAnnotationVisitor != null) { 310 ExecuteAnnotationContent executeAnnotationContent = new ExecuteAnnotationContent(); 311 populateAnnotationContent(executeAnnotationContent, mojoAnnotationVisitor); 312 mojoAnnotatedClass.setExecute(executeAnnotationContent); 313 } 314 315 // @After annotation(s) — v4 API only 316 List<AfterAnnotationContent> afterAnnotations = new ArrayList<>(); 317 318 // single @After 319 mojoAnnotationVisitor = mojoClassVisitor.getAnnotationVisitor(AFTER_V4); 320 if (mojoAnnotationVisitor != null) { 321 AfterAnnotationContent afterAnnotationContent = new AfterAnnotationContent(); 322 populateAnnotationContent(afterAnnotationContent, mojoAnnotationVisitor); 323 afterAnnotations.add(afterAnnotationContent); 324 } 325 326 // @Afters repeatable container wraps multiple @After in its "value" array attribute 327 mojoAnnotationVisitor = mojoClassVisitor.getAnnotationVisitor(AFTERS_V4); 328 if (mojoAnnotationVisitor != null) { 329 // The @Afters annotation has a "value" array attribute; visitArray("value") 330 // creates a sub-visitor, and each nested @After is visited via visitAnnotation 331 // on that sub-visitor. 332 MojoAnnotationVisitor arrayVisitor = mojoAnnotationVisitor.getArrayVisitor("value"); 333 if (arrayVisitor != null) { 334 for (MojoAnnotationVisitor nestedVisitor : arrayVisitor.getNestedAnnotationVisitors()) { 335 AfterAnnotationContent afterAnnotationContent = new AfterAnnotationContent(); 336 populateAnnotationContent(afterAnnotationContent, nestedVisitor); 337 afterAnnotations.add(afterAnnotationContent); 338 } 339 } 340 } 341 342 if (!afterAnnotations.isEmpty()) { 343 mojoAnnotatedClass.setAfterAnnotations(afterAnnotations); 344 } 345 346 // @Parameter annotations 347 List<MojoParameterVisitor> mojoParameterVisitors = 348 mojoClassVisitor.findParameterVisitors(new HashSet<>(Arrays.asList(PARAMETER_V3, PARAMETER_V4))); 349 for (MojoParameterVisitor parameterVisitor : mojoParameterVisitors) { 350 ParameterAnnotationContent parameterAnnotationContent = new ParameterAnnotationContent( 351 parameterVisitor.getFieldName(), 352 parameterVisitor.getClassName(), 353 parameterVisitor.getTypeParameters(), 354 parameterVisitor.isAnnotationOnMethod()); 355 356 Map<String, MojoAnnotationVisitor> annotationVisitorMap = parameterVisitor.getAnnotationVisitorMap(); 357 MojoAnnotationVisitor fieldAnnotationVisitor = annotationVisitorMap.get(PARAMETER_V3); 358 if (fieldAnnotationVisitor == null) { 359 fieldAnnotationVisitor = annotationVisitorMap.get(PARAMETER_V4); 360 } 361 362 if (fieldAnnotationVisitor != null) { 363 populateAnnotationContent(parameterAnnotationContent, fieldAnnotationVisitor); 364 } 365 366 if (annotationVisitorMap.containsKey(Deprecated.class.getName())) { 367 parameterAnnotationContent.setDeprecated(EMPTY); 368 } 369 370 mojoAnnotatedClass 371 .getParameters() 372 .put(parameterAnnotationContent.getFieldName(), parameterAnnotationContent); 373 } 374 375 // @Component annotations 376 List<MojoFieldVisitor> mojoComponentVisitors = 377 mojoClassVisitor.findFieldWithAnnotation(new HashSet<>(Arrays.asList(COMPONENT_V3))); 378 for (MojoFieldVisitor mojoComponentVisitor : mojoComponentVisitors) { 379 ComponentAnnotationContent componentAnnotationContent = 380 new ComponentAnnotationContent(mojoComponentVisitor.getFieldName()); 381 382 Map<String, MojoAnnotationVisitor> annotationVisitorMap = 383 mojoComponentVisitor.getAnnotationVisitorMap(); 384 MojoAnnotationVisitor annotationVisitor = annotationVisitorMap.get(COMPONENT_V3); 385 386 if (annotationVisitor != null) { 387 for (Map.Entry<String, Object> entry : 388 annotationVisitor.getAnnotationValues().entrySet()) { 389 String methodName = entry.getKey(); 390 if ("role".equals(methodName)) { 391 Type type = (Type) entry.getValue(); 392 componentAnnotationContent.setRoleClassName(type.getClassName()); 393 } else { 394 reflector.invoke( 395 componentAnnotationContent, entry.getKey(), new Object[] {entry.getValue()}); 396 } 397 } 398 399 if (StringUtils.isEmpty(componentAnnotationContent.getRoleClassName())) { 400 componentAnnotationContent.setRoleClassName(mojoComponentVisitor.getClassName()); 401 } 402 } 403 mojoAnnotatedClass 404 .getComponents() 405 .put(componentAnnotationContent.getFieldName(), componentAnnotationContent); 406 } 407 } catch (ReflectorException e) { 408 throw new ExtractionException(e.getMessage(), e); 409 } 410 } 411}