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.Arrays;
30  import java.util.HashMap;
31  import java.util.HashSet;
32  import java.util.List;
33  import java.util.Map;
34  import java.util.Objects;
35  import java.util.regex.Pattern;
36  import java.util.zip.ZipEntry;
37  import java.util.zip.ZipInputStream;
38  
39  import org.apache.maven.artifact.Artifact;
40  import org.apache.maven.plugins.annotations.Component;
41  import org.apache.maven.plugins.annotations.Execute;
42  import org.apache.maven.plugins.annotations.Mojo;
43  import org.apache.maven.plugins.annotations.Parameter;
44  import org.apache.maven.tools.plugin.extractor.ExtractionException;
45  import org.apache.maven.tools.plugin.extractor.annotations.datamodel.ComponentAnnotationContent;
46  import org.apache.maven.tools.plugin.extractor.annotations.datamodel.ExecuteAnnotationContent;
47  import org.apache.maven.tools.plugin.extractor.annotations.datamodel.MojoAnnotationContent;
48  import org.apache.maven.tools.plugin.extractor.annotations.datamodel.ParameterAnnotationContent;
49  import org.apache.maven.tools.plugin.extractor.annotations.scanner.visitors.MojoAnnotationVisitor;
50  import org.apache.maven.tools.plugin.extractor.annotations.scanner.visitors.MojoClassVisitor;
51  import org.apache.maven.tools.plugin.extractor.annotations.scanner.visitors.MojoFieldVisitor;
52  import org.apache.maven.tools.plugin.extractor.annotations.scanner.visitors.MojoParameterVisitor;
53  import org.codehaus.plexus.util.DirectoryScanner;
54  import org.codehaus.plexus.util.StringUtils;
55  import org.codehaus.plexus.util.reflection.Reflector;
56  import org.codehaus.plexus.util.reflection.ReflectorException;
57  import org.objectweb.asm.ClassReader;
58  import org.objectweb.asm.Type;
59  import org.slf4j.Logger;
60  import org.slf4j.LoggerFactory;
61  
62  /**
63   * Mojo scanner with java annotations.
64   *
65   * @author Olivier Lamy
66   * @since 3.0
67   */
68  @Named
69  @Singleton
70  public class DefaultMojoAnnotationsScanner implements MojoAnnotationsScanner {
71      private static final Logger LOGGER = LoggerFactory.getLogger(DefaultMojoAnnotationsScanner.class);
72      public static final String MVN4_API = "org.apache.maven.api.plugin.annotations.";
73      public static final String MOJO_V4 = MVN4_API + "Mojo";
74      public static final String EXECUTE_V4 = MVN4_API + "Execute";
75      public static final String PARAMETER_V4 = MVN4_API + "Parameter";
76  
77      public static final String MOJO_V3 = Mojo.class.getName();
78      public static final String EXECUTE_V3 = Execute.class.getName();
79      public static final String PARAMETER_V3 = Parameter.class.getName();
80      public static final String COMPONENT_V3 = Component.class.getName();
81  
82      // classes with a dash must be ignored
83      private static final Pattern SCANNABLE_CLASS = Pattern.compile("[^-]+\\.class");
84      private static final String EMPTY = "";
85  
86      private Reflector reflector = new Reflector();
87  
88      @Override
89      public Map<String, MojoAnnotatedClass> scan(MojoAnnotationsScannerRequest request) throws ExtractionException {
90          Map<String, MojoAnnotatedClass> mojoAnnotatedClasses = new HashMap<>();
91  
92          try {
93              String mavenApiVersion = null;
94              for (Artifact dependency : request.getDependencies()) {
95                  scan(mojoAnnotatedClasses, dependency.getFile(), request.getIncludePatterns(), dependency, true);
96                  if (request.getMavenApiVersion() == null
97                          && dependency.getGroupId().equals("org.apache.maven")
98                          && (dependency.getArtifactId().equals("maven-plugin-api")
99                                  || dependency.getArtifactId().equals("maven-api-core"))) {
100                     String version = dependency.getVersion();
101                     if (mavenApiVersion != null && !Objects.equals(version, mavenApiVersion)) {
102                         throw new UnsupportedOperationException("Mixing Maven 3 and Maven 4 plugins is not supported."
103                                 + " Fix your dependencies so that you depend either on maven-plugin-api for a Maven 3 plugin,"
104                                 + " or maven-api-core for a Maven 4 plugin.");
105                     }
106                     mavenApiVersion = version;
107                 }
108             }
109             request.setMavenApiVersion(mavenApiVersion);
110 
111             for (File classDirectory : request.getClassesDirectories()) {
112                 scan(
113                         mojoAnnotatedClasses,
114                         classDirectory,
115                         request.getIncludePatterns(),
116                         request.getProject().getArtifact(),
117                         false);
118             }
119         } catch (IOException e) {
120             throw new ExtractionException(e.getMessage(), e);
121         }
122 
123         return mojoAnnotatedClasses;
124     }
125 
126     protected void scan(
127             Map<String, MojoAnnotatedClass> mojoAnnotatedClasses,
128             File source,
129             List<String> includePatterns,
130             Artifact artifact,
131             boolean excludeMojo)
132             throws IOException, ExtractionException {
133         if (source == null || !source.exists()) {
134             return;
135         }
136 
137         Map<String, MojoAnnotatedClass> scanResult;
138         if (source.isDirectory()) {
139             scanResult = scanDirectory(source, includePatterns, artifact, excludeMojo);
140         } else {
141             scanResult = scanArchive(source, artifact, excludeMojo);
142         }
143 
144         mojoAnnotatedClasses.putAll(scanResult);
145     }
146 
147     /**
148      * @param archiveFile
149      * @param artifact
150      * @param excludeMojo     for dependencies, we exclude Mojo annotations found
151      * @return annotated classes found
152      * @throws IOException
153      * @throws ExtractionException
154      */
155     protected Map<String, MojoAnnotatedClass> scanArchive(File archiveFile, Artifact artifact, boolean excludeMojo)
156             throws IOException, ExtractionException {
157         Map<String, MojoAnnotatedClass> mojoAnnotatedClasses = new HashMap<>();
158 
159         String zipEntryName = null;
160         try (ZipInputStream archiveStream = new ZipInputStream(new FileInputStream(archiveFile))) {
161             String archiveFilename = archiveFile.getAbsolutePath();
162             for (ZipEntry zipEntry = archiveStream.getNextEntry();
163                     zipEntry != null;
164                     zipEntry = archiveStream.getNextEntry()) {
165                 zipEntryName = zipEntry.getName();
166                 if (!SCANNABLE_CLASS.matcher(zipEntryName).matches()) {
167                     continue;
168                 }
169                 analyzeClassStream(
170                         mojoAnnotatedClasses,
171                         archiveStream,
172                         artifact,
173                         excludeMojo,
174                         archiveFilename,
175                         zipEntry.getName());
176             }
177         } catch (IllegalArgumentException e) {
178             // In case of a class with newer specs an IllegalArgumentException can be thrown
179             LOGGER.error("Failed to analyze " + archiveFile.getAbsolutePath() + "!/" + zipEntryName);
180 
181             throw e;
182         }
183 
184         return mojoAnnotatedClasses;
185     }
186 
187     /**
188      * @param classDirectory
189      * @param includePatterns
190      * @param artifact
191      * @param excludeMojo     for dependencies, we exclude Mojo annotations found
192      * @return annotated classes found
193      * @throws IOException
194      * @throws ExtractionException
195      */
196     protected Map<String, MojoAnnotatedClass> scanDirectory(
197             File classDirectory, List<String> includePatterns, Artifact artifact, boolean excludeMojo)
198             throws IOException, ExtractionException {
199         Map<String, MojoAnnotatedClass> mojoAnnotatedClasses = new HashMap<>();
200 
201         DirectoryScanner scanner = new DirectoryScanner();
202         scanner.setBasedir(classDirectory);
203         scanner.addDefaultExcludes();
204         if (includePatterns != null) {
205             scanner.setIncludes(includePatterns.toArray(new String[includePatterns.size()]));
206         }
207         scanner.scan();
208         String[] classFiles = scanner.getIncludedFiles();
209         String classDirname = classDirectory.getAbsolutePath();
210 
211         for (String classFile : classFiles) {
212             if (!SCANNABLE_CLASS.matcher(classFile).matches()) {
213                 continue;
214             }
215 
216             try (InputStream is = //
217                     new BufferedInputStream(new FileInputStream(new File(classDirectory, classFile)))) {
218                 analyzeClassStream(mojoAnnotatedClasses, is, artifact, excludeMojo, classDirname, classFile);
219             }
220         }
221         return mojoAnnotatedClasses;
222     }
223 
224     private void analyzeClassStream(
225             Map<String, MojoAnnotatedClass> mojoAnnotatedClasses,
226             InputStream is,
227             Artifact artifact,
228             boolean excludeMojo,
229             String source,
230             String file)
231             throws IOException, ExtractionException {
232         MojoClassVisitor mojoClassVisitor = new MojoClassVisitor();
233         try {
234             ClassReader rdr = new ClassReader(is);
235             rdr.accept(mojoClassVisitor, ClassReader.SKIP_FRAMES | ClassReader.SKIP_CODE | ClassReader.SKIP_DEBUG);
236         } catch (ArrayIndexOutOfBoundsException aiooe) {
237             LOGGER.warn(
238                     "Error analyzing class " + file + " in " + source + ": ignoring class",
239                     LOGGER.isDebugEnabled() ? aiooe : null);
240             return;
241         } catch (IllegalArgumentException iae) {
242             if (iae.getMessage() == null) {
243                 LOGGER.warn(
244                         "Error analyzing class " + file + " in " + source + ": ignoring class",
245                         LOGGER.isDebugEnabled() ? iae : null);
246                 return;
247             } else {
248                 throw iae;
249             }
250         }
251 
252         analyzeVisitors(mojoClassVisitor);
253 
254         MojoAnnotatedClass mojoAnnotatedClass = mojoClassVisitor.getMojoAnnotatedClass();
255 
256         if (excludeMojo) {
257             mojoAnnotatedClass.setMojo(null);
258         }
259 
260         if (mojoAnnotatedClass != null) // see MPLUGIN-206 we can have intermediate classes without annotations
261         {
262             if (LOGGER.isDebugEnabled() && mojoAnnotatedClass.hasAnnotations()) {
263                 LOGGER.debug(
264                         "found MojoAnnotatedClass:" + mojoAnnotatedClass.getClassName() + ":" + mojoAnnotatedClass);
265             }
266             mojoAnnotatedClass.setArtifact(artifact);
267             mojoAnnotatedClasses.put(mojoAnnotatedClass.getClassName(), mojoAnnotatedClass);
268             mojoAnnotatedClass.setClassVersion(mojoClassVisitor.getVersion());
269         }
270     }
271 
272     protected void populateAnnotationContent(Object content, MojoAnnotationVisitor mojoAnnotationVisitor)
273             throws ReflectorException {
274         for (Map.Entry<String, Object> entry :
275                 mojoAnnotationVisitor.getAnnotationValues().entrySet()) {
276             reflector.invoke(content, entry.getKey(), new Object[] {entry.getValue()});
277         }
278     }
279 
280     protected void analyzeVisitors(MojoClassVisitor mojoClassVisitor) throws ExtractionException {
281         final MojoAnnotatedClass mojoAnnotatedClass = mojoClassVisitor.getMojoAnnotatedClass();
282 
283         try {
284             // @Mojo annotation
285             MojoAnnotationVisitor mojoAnnotationVisitor = mojoClassVisitor.getAnnotationVisitor(MOJO_V3);
286             if (mojoAnnotationVisitor == null) {
287                 mojoAnnotationVisitor = mojoClassVisitor.getAnnotationVisitor(MOJO_V4);
288             }
289             if (mojoAnnotationVisitor != null) {
290                 MojoAnnotationContent mojoAnnotationContent = new MojoAnnotationContent();
291                 populateAnnotationContent(mojoAnnotationContent, mojoAnnotationVisitor);
292 
293                 if (mojoClassVisitor.getAnnotationVisitor(Deprecated.class) != null) {
294                     mojoAnnotationContent.setDeprecated(EMPTY);
295                 }
296 
297                 mojoAnnotatedClass.setMojo(mojoAnnotationContent);
298             }
299 
300             // @Execute annotation
301             mojoAnnotationVisitor = mojoClassVisitor.getAnnotationVisitor(EXECUTE_V3);
302             if (mojoAnnotationVisitor == null) {
303                 mojoAnnotationVisitor = mojoClassVisitor.getAnnotationVisitor(EXECUTE_V4);
304             }
305             if (mojoAnnotationVisitor != null) {
306                 ExecuteAnnotationContent executeAnnotationContent = new ExecuteAnnotationContent();
307                 populateAnnotationContent(executeAnnotationContent, mojoAnnotationVisitor);
308                 mojoAnnotatedClass.setExecute(executeAnnotationContent);
309             }
310 
311             // @Parameter annotations
312             List<MojoParameterVisitor> mojoParameterVisitors =
313                     mojoClassVisitor.findParameterVisitors(new HashSet<>(Arrays.asList(PARAMETER_V3, PARAMETER_V4)));
314             for (MojoParameterVisitor parameterVisitor : mojoParameterVisitors) {
315                 ParameterAnnotationContent parameterAnnotationContent = new ParameterAnnotationContent(
316                         parameterVisitor.getFieldName(),
317                         parameterVisitor.getClassName(),
318                         parameterVisitor.getTypeParameters(),
319                         parameterVisitor.isAnnotationOnMethod());
320 
321                 Map<String, MojoAnnotationVisitor> annotationVisitorMap = parameterVisitor.getAnnotationVisitorMap();
322                 MojoAnnotationVisitor fieldAnnotationVisitor = annotationVisitorMap.get(PARAMETER_V3);
323                 if (fieldAnnotationVisitor == null) {
324                     fieldAnnotationVisitor = annotationVisitorMap.get(PARAMETER_V4);
325                 }
326 
327                 if (fieldAnnotationVisitor != null) {
328                     populateAnnotationContent(parameterAnnotationContent, fieldAnnotationVisitor);
329                 }
330 
331                 if (annotationVisitorMap.containsKey(Deprecated.class.getName())) {
332                     parameterAnnotationContent.setDeprecated(EMPTY);
333                 }
334 
335                 mojoAnnotatedClass
336                         .getParameters()
337                         .put(parameterAnnotationContent.getFieldName(), parameterAnnotationContent);
338             }
339 
340             // @Component annotations
341             List<MojoFieldVisitor> mojoComponentVisitors =
342                     mojoClassVisitor.findFieldWithAnnotation(new HashSet<>(Arrays.asList(COMPONENT_V3)));
343             for (MojoFieldVisitor mojoComponentVisitor : mojoComponentVisitors) {
344                 ComponentAnnotationContent componentAnnotationContent =
345                         new ComponentAnnotationContent(mojoComponentVisitor.getFieldName());
346 
347                 Map<String, MojoAnnotationVisitor> annotationVisitorMap =
348                         mojoComponentVisitor.getAnnotationVisitorMap();
349                 MojoAnnotationVisitor annotationVisitor = annotationVisitorMap.get(COMPONENT_V3);
350 
351                 if (annotationVisitor != null) {
352                     for (Map.Entry<String, Object> entry :
353                             annotationVisitor.getAnnotationValues().entrySet()) {
354                         String methodName = entry.getKey();
355                         if ("role".equals(methodName)) {
356                             Type type = (Type) entry.getValue();
357                             componentAnnotationContent.setRoleClassName(type.getClassName());
358                         } else {
359                             reflector.invoke(
360                                     componentAnnotationContent, entry.getKey(), new Object[] {entry.getValue()});
361                         }
362                     }
363 
364                     if (StringUtils.isEmpty(componentAnnotationContent.getRoleClassName())) {
365                         componentAnnotationContent.setRoleClassName(mojoComponentVisitor.getClassName());
366                     }
367                 }
368                 mojoAnnotatedClass
369                         .getComponents()
370                         .put(componentAnnotationContent.getFieldName(), componentAnnotationContent);
371             }
372         } catch (ReflectorException e) {
373             throw new ExtractionException(e.getMessage(), e);
374         }
375     }
376 }