View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  package org.apache.maven.tools.plugin.extractor.annotations.scanner;
20  
21  import javax.inject.Named;
22  import javax.inject.Singleton;
23  
24  import java.io.BufferedInputStream;
25  import java.io.File;
26  import java.io.FileInputStream;
27  import java.io.IOException;
28  import java.io.InputStream;
29  import java.util.ArrayList;
30  import java.util.Arrays;
31  import java.util.HashMap;
32  import java.util.HashSet;
33  import java.util.List;
34  import java.util.Map;
35  import java.util.Objects;
36  import java.util.regex.Pattern;
37  import java.util.zip.ZipEntry;
38  import java.util.zip.ZipInputStream;
39  
40  import org.apache.maven.artifact.Artifact;
41  import org.apache.maven.plugins.annotations.Component;
42  import org.apache.maven.plugins.annotations.Execute;
43  import org.apache.maven.plugins.annotations.Mojo;
44  import org.apache.maven.plugins.annotations.Parameter;
45  import org.apache.maven.tools.plugin.extractor.ExtractionException;
46  import org.apache.maven.tools.plugin.extractor.annotations.datamodel.AfterAnnotationContent;
47  import org.apache.maven.tools.plugin.extractor.annotations.datamodel.ComponentAnnotationContent;
48  import org.apache.maven.tools.plugin.extractor.annotations.datamodel.ExecuteAnnotationContent;
49  import org.apache.maven.tools.plugin.extractor.annotations.datamodel.MojoAnnotationContent;
50  import org.apache.maven.tools.plugin.extractor.annotations.datamodel.ParameterAnnotationContent;
51  import org.apache.maven.tools.plugin.extractor.annotations.scanner.visitors.MojoAnnotationVisitor;
52  import org.apache.maven.tools.plugin.extractor.annotations.scanner.visitors.MojoClassVisitor;
53  import org.apache.maven.tools.plugin.extractor.annotations.scanner.visitors.MojoFieldVisitor;
54  import org.apache.maven.tools.plugin.extractor.annotations.scanner.visitors.MojoParameterVisitor;
55  import org.codehaus.plexus.util.DirectoryScanner;
56  import org.codehaus.plexus.util.StringUtils;
57  import org.codehaus.plexus.util.reflection.Reflector;
58  import org.codehaus.plexus.util.reflection.ReflectorException;
59  import org.objectweb.asm.ClassReader;
60  import org.objectweb.asm.Type;
61  import org.slf4j.Logger;
62  import org.slf4j.LoggerFactory;
63  
64  /**
65   * Mojo scanner with java annotations.
66   *
67   * @author Olivier Lamy
68   * @since 3.0
69   */
70  @Named
71  @Singleton
72  public class DefaultMojoAnnotationsScanner implements MojoAnnotationsScanner {
73      private static final Logger LOGGER = LoggerFactory.getLogger(DefaultMojoAnnotationsScanner.class);
74      public static final String MVN4_API = "org.apache.maven.api.plugin.annotations.";
75      public static final String MOJO_V4 = MVN4_API + "Mojo";
76      public static final String EXECUTE_V4 = MVN4_API + "Execute";
77      public static final String PARAMETER_V4 = MVN4_API + "Parameter";
78      public static final String AFTER_V4 = MVN4_API + "After";
79      public static final String AFTERS_V4 = MVN4_API + "Afters";
80  
81      public static final String MOJO_V3 = Mojo.class.getName();
82      public static final String EXECUTE_V3 = Execute.class.getName();
83      public static final String PARAMETER_V3 = Parameter.class.getName();
84      public static final String COMPONENT_V3 = Component.class.getName();
85  
86      // classes with a dash must be ignored
87      private static final Pattern SCANNABLE_CLASS = Pattern.compile("[^-]+\\.class");
88      private static final String EMPTY = "";
89  
90      private Reflector reflector = new Reflector();
91  
92      @Override
93      public Map<String, MojoAnnotatedClass> scan(MojoAnnotationsScannerRequest request) throws ExtractionException {
94          Map<String, MojoAnnotatedClass> mojoAnnotatedClasses = new HashMap<>();
95  
96          try {
97              String mavenApiVersion = null;
98              for (Artifact dependency : request.getDependencies()) {
99                  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 }