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.util.DirectoryScanner; 053import org.codehaus.plexus.util.StringUtils; 054import org.codehaus.plexus.util.reflection.Reflector; 055import org.codehaus.plexus.util.reflection.ReflectorException; 056import org.objectweb.asm.ClassReader; 057import org.objectweb.asm.Type; 058import org.slf4j.Logger; 059import org.slf4j.LoggerFactory; 060 061/** 062 * Mojo scanner with java annotations. 063 * 064 * @author Olivier Lamy 065 * @since 3.0 066 */ 067@Named 068@Singleton 069public class DefaultMojoAnnotationsScanner implements MojoAnnotationsScanner { 070 private static final Logger LOGGER = LoggerFactory.getLogger(DefaultMojoAnnotationsScanner.class); 071 public static final String MVN4_API = "org.apache.maven.api.plugin.annotations."; 072 public static final String MOJO_V4 = MVN4_API + "Mojo"; 073 public static final String EXECUTE_V4 = MVN4_API + "Execute"; 074 public static final String PARAMETER_V4 = MVN4_API + "Parameter"; 075 public static final String COMPONENT_V4 = MVN4_API + "Component"; 076 077 public static final String MOJO_V3 = Mojo.class.getName(); 078 public static final String EXECUTE_V3 = Execute.class.getName(); 079 public static final String PARAMETER_V3 = Parameter.class.getName(); 080 public static final String COMPONENT_V3 = Component.class.getName(); 081 082 // classes with a dash must be ignored 083 private static final Pattern SCANNABLE_CLASS = Pattern.compile("[^-]+\\.class"); 084 private static final String EMPTY = ""; 085 086 private Reflector reflector = new Reflector(); 087 088 @Override 089 public Map<String, MojoAnnotatedClass> scan(MojoAnnotationsScannerRequest request) throws ExtractionException { 090 Map<String, MojoAnnotatedClass> mojoAnnotatedClasses = new HashMap<>(); 091 092 try { 093 for (Artifact dependency : request.getDependencies()) { 094 scan(mojoAnnotatedClasses, dependency.getFile(), request.getIncludePatterns(), dependency, true); 095 if (request.getMavenApiVersion() == null 096 && dependency.getGroupId().equals("org.apache.maven") 097 && (dependency.getArtifactId().equals("maven-plugin-api") 098 || dependency.getArtifactId().equals("maven-api-core"))) { 099 request.setMavenApiVersion(dependency.getVersion()); 100 } 101 } 102 103 for (File classDirectory : request.getClassesDirectories()) { 104 scan( 105 mojoAnnotatedClasses, 106 classDirectory, 107 request.getIncludePatterns(), 108 request.getProject().getArtifact(), 109 false); 110 } 111 } catch (IOException e) { 112 throw new ExtractionException(e.getMessage(), e); 113 } 114 115 return mojoAnnotatedClasses; 116 } 117 118 protected void scan( 119 Map<String, MojoAnnotatedClass> mojoAnnotatedClasses, 120 File source, 121 List<String> includePatterns, 122 Artifact artifact, 123 boolean excludeMojo) 124 throws IOException, ExtractionException { 125 if (source == null || !source.exists()) { 126 return; 127 } 128 129 Map<String, MojoAnnotatedClass> scanResult; 130 if (source.isDirectory()) { 131 scanResult = scanDirectory(source, includePatterns, artifact, excludeMojo); 132 } else { 133 scanResult = scanArchive(source, artifact, excludeMojo); 134 } 135 136 mojoAnnotatedClasses.putAll(scanResult); 137 } 138 139 /** 140 * @param archiveFile 141 * @param artifact 142 * @param excludeMojo for dependencies, we exclude Mojo annotations found 143 * @return annotated classes found 144 * @throws IOException 145 * @throws ExtractionException 146 */ 147 protected Map<String, MojoAnnotatedClass> scanArchive(File archiveFile, Artifact artifact, boolean excludeMojo) 148 throws IOException, ExtractionException { 149 Map<String, MojoAnnotatedClass> mojoAnnotatedClasses = new HashMap<>(); 150 151 String zipEntryName = null; 152 try (ZipInputStream archiveStream = new ZipInputStream(new FileInputStream(archiveFile))) { 153 String archiveFilename = archiveFile.getAbsolutePath(); 154 for (ZipEntry zipEntry = archiveStream.getNextEntry(); 155 zipEntry != null; 156 zipEntry = archiveStream.getNextEntry()) { 157 zipEntryName = zipEntry.getName(); 158 if (!SCANNABLE_CLASS.matcher(zipEntryName).matches()) { 159 continue; 160 } 161 analyzeClassStream( 162 mojoAnnotatedClasses, 163 archiveStream, 164 artifact, 165 excludeMojo, 166 archiveFilename, 167 zipEntry.getName()); 168 } 169 } catch (IllegalArgumentException e) { 170 // In case of a class with newer specs an IllegalArgumentException can be thrown 171 LOGGER.error("Failed to analyze " + archiveFile.getAbsolutePath() + "!/" + zipEntryName); 172 173 throw e; 174 } 175 176 return mojoAnnotatedClasses; 177 } 178 179 /** 180 * @param classDirectory 181 * @param includePatterns 182 * @param artifact 183 * @param excludeMojo for dependencies, we exclude Mojo annotations found 184 * @return annotated classes found 185 * @throws IOException 186 * @throws ExtractionException 187 */ 188 protected Map<String, MojoAnnotatedClass> scanDirectory( 189 File classDirectory, List<String> includePatterns, Artifact artifact, boolean excludeMojo) 190 throws IOException, ExtractionException { 191 Map<String, MojoAnnotatedClass> mojoAnnotatedClasses = new HashMap<>(); 192 193 DirectoryScanner scanner = new DirectoryScanner(); 194 scanner.setBasedir(classDirectory); 195 scanner.addDefaultExcludes(); 196 if (includePatterns != null) { 197 scanner.setIncludes(includePatterns.toArray(new String[includePatterns.size()])); 198 } 199 scanner.scan(); 200 String[] classFiles = scanner.getIncludedFiles(); 201 String classDirname = classDirectory.getAbsolutePath(); 202 203 for (String classFile : classFiles) { 204 if (!SCANNABLE_CLASS.matcher(classFile).matches()) { 205 continue; 206 } 207 208 try (InputStream is = // 209 new BufferedInputStream(new FileInputStream(new File(classDirectory, classFile)))) { 210 analyzeClassStream(mojoAnnotatedClasses, is, artifact, excludeMojo, classDirname, classFile); 211 } 212 } 213 return mojoAnnotatedClasses; 214 } 215 216 private void analyzeClassStream( 217 Map<String, MojoAnnotatedClass> mojoAnnotatedClasses, 218 InputStream is, 219 Artifact artifact, 220 boolean excludeMojo, 221 String source, 222 String file) 223 throws IOException, ExtractionException { 224 MojoClassVisitor mojoClassVisitor = new MojoClassVisitor(); 225 try { 226 ClassReader rdr = new ClassReader(is); 227 rdr.accept(mojoClassVisitor, ClassReader.SKIP_FRAMES | ClassReader.SKIP_CODE | ClassReader.SKIP_DEBUG); 228 } catch (ArrayIndexOutOfBoundsException aiooe) { 229 LOGGER.warn( 230 "Error analyzing class " + file + " in " + source + ": ignoring class", 231 LOGGER.isDebugEnabled() ? aiooe : null); 232 return; 233 } catch (IllegalArgumentException iae) { 234 if (iae.getMessage() == null) { 235 LOGGER.warn( 236 "Error analyzing class " + file + " in " + source + ": ignoring class", 237 LOGGER.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 (LOGGER.isDebugEnabled() && mojoAnnotatedClass.hasAnnotations()) { 255 LOGGER.debug( 256 "found MojoAnnotatedClass:" + mojoAnnotatedClass.getClassName() + ":" + mojoAnnotatedClass); 257 } 258 mojoAnnotatedClass.setArtifact(artifact); 259 mojoAnnotatedClasses.put(mojoAnnotatedClass.getClassName(), mojoAnnotatedClass); 260 mojoAnnotatedClass.setClassVersion(mojoClassVisitor.getVersion()); 261 } 262 } 263 264 protected void populateAnnotationContent(Object content, MojoAnnotationVisitor mojoAnnotationVisitor) 265 throws ReflectorException { 266 for (Map.Entry<String, Object> entry : 267 mojoAnnotationVisitor.getAnnotationValues().entrySet()) { 268 reflector.invoke(content, entry.getKey(), new Object[] {entry.getValue()}); 269 } 270 } 271 272 protected void analyzeVisitors(MojoClassVisitor mojoClassVisitor) throws ExtractionException { 273 final MojoAnnotatedClass mojoAnnotatedClass = mojoClassVisitor.getMojoAnnotatedClass(); 274 275 try { 276 // @Mojo annotation 277 MojoAnnotationVisitor mojoAnnotationVisitor = mojoClassVisitor.getAnnotationVisitor(MOJO_V3); 278 if (mojoAnnotationVisitor == null) { 279 mojoAnnotationVisitor = mojoClassVisitor.getAnnotationVisitor(MOJO_V4); 280 } 281 if (mojoAnnotationVisitor != null) { 282 MojoAnnotationContent mojoAnnotationContent = new MojoAnnotationContent(); 283 populateAnnotationContent(mojoAnnotationContent, mojoAnnotationVisitor); 284 285 if (mojoClassVisitor.getAnnotationVisitor(Deprecated.class) != null) { 286 mojoAnnotationContent.setDeprecated(EMPTY); 287 } 288 289 mojoAnnotatedClass.setMojo(mojoAnnotationContent); 290 } 291 292 // @Execute annotation 293 mojoAnnotationVisitor = mojoClassVisitor.getAnnotationVisitor(EXECUTE_V3); 294 if (mojoAnnotationVisitor == null) { 295 mojoAnnotationVisitor = mojoClassVisitor.getAnnotationVisitor(EXECUTE_V4); 296 } 297 if (mojoAnnotationVisitor != null) { 298 ExecuteAnnotationContent executeAnnotationContent = new ExecuteAnnotationContent(); 299 populateAnnotationContent(executeAnnotationContent, mojoAnnotationVisitor); 300 mojoAnnotatedClass.setExecute(executeAnnotationContent); 301 } 302 303 // @Parameter annotations 304 List<MojoParameterVisitor> mojoParameterVisitors = 305 mojoClassVisitor.findParameterVisitors(new HashSet<>(Arrays.asList(PARAMETER_V3, PARAMETER_V4))); 306 for (MojoParameterVisitor parameterVisitor : mojoParameterVisitors) { 307 ParameterAnnotationContent parameterAnnotationContent = new ParameterAnnotationContent( 308 parameterVisitor.getFieldName(), 309 parameterVisitor.getClassName(), 310 parameterVisitor.getTypeParameters(), 311 parameterVisitor.isAnnotationOnMethod()); 312 313 Map<String, MojoAnnotationVisitor> annotationVisitorMap = parameterVisitor.getAnnotationVisitorMap(); 314 MojoAnnotationVisitor fieldAnnotationVisitor = annotationVisitorMap.get(PARAMETER_V3); 315 if (fieldAnnotationVisitor == null) { 316 fieldAnnotationVisitor = annotationVisitorMap.get(PARAMETER_V4); 317 } 318 319 if (fieldAnnotationVisitor != null) { 320 populateAnnotationContent(parameterAnnotationContent, fieldAnnotationVisitor); 321 } 322 323 if (annotationVisitorMap.containsKey(Deprecated.class.getName())) { 324 parameterAnnotationContent.setDeprecated(EMPTY); 325 } 326 327 mojoAnnotatedClass 328 .getParameters() 329 .put(parameterAnnotationContent.getFieldName(), parameterAnnotationContent); 330 } 331 332 // @Component annotations 333 List<MojoFieldVisitor> mojoFieldVisitors = 334 mojoClassVisitor.findFieldWithAnnotation(new HashSet<>(Arrays.asList(COMPONENT_V3, COMPONENT_V4))); 335 for (MojoFieldVisitor mojoFieldVisitor : mojoFieldVisitors) { 336 ComponentAnnotationContent componentAnnotationContent = 337 new ComponentAnnotationContent(mojoFieldVisitor.getFieldName()); 338 339 Map<String, MojoAnnotationVisitor> annotationVisitorMap = mojoFieldVisitor.getAnnotationVisitorMap(); 340 MojoAnnotationVisitor annotationVisitor = annotationVisitorMap.get(COMPONENT_V3); 341 if (annotationVisitor == null) { 342 annotationVisitor = annotationVisitorMap.get(COMPONENT_V4); 343 } 344 345 if (annotationVisitor != null) { 346 for (Map.Entry<String, Object> entry : 347 annotationVisitor.getAnnotationValues().entrySet()) { 348 String methodName = entry.getKey(); 349 if ("role".equals(methodName)) { 350 Type type = (Type) entry.getValue(); 351 componentAnnotationContent.setRoleClassName(type.getClassName()); 352 } else { 353 reflector.invoke( 354 componentAnnotationContent, entry.getKey(), new Object[] {entry.getValue()}); 355 } 356 } 357 358 if (StringUtils.isEmpty(componentAnnotationContent.getRoleClassName())) { 359 componentAnnotationContent.setRoleClassName(mojoFieldVisitor.getClassName()); 360 } 361 } 362 mojoAnnotatedClass 363 .getComponents() 364 .put(componentAnnotationContent.getFieldName(), componentAnnotationContent); 365 } 366 } catch (ReflectorException e) { 367 throw new ExtractionException(e.getMessage(), e); 368 } 369 } 370}