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.regex.Pattern;
35  import java.util.zip.ZipEntry;
36  import java.util.zip.ZipInputStream;
37  
38  import org.apache.maven.artifact.Artifact;
39  import org.apache.maven.plugins.annotations.Component;
40  import org.apache.maven.plugins.annotations.Execute;
41  import org.apache.maven.plugins.annotations.Mojo;
42  import org.apache.maven.plugins.annotations.Parameter;
43  import org.apache.maven.tools.plugin.extractor.ExtractionException;
44  import org.apache.maven.tools.plugin.extractor.annotations.datamodel.ComponentAnnotationContent;
45  import org.apache.maven.tools.plugin.extractor.annotations.datamodel.ExecuteAnnotationContent;
46  import org.apache.maven.tools.plugin.extractor.annotations.datamodel.MojoAnnotationContent;
47  import org.apache.maven.tools.plugin.extractor.annotations.datamodel.ParameterAnnotationContent;
48  import org.apache.maven.tools.plugin.extractor.annotations.scanner.visitors.MojoAnnotationVisitor;
49  import org.apache.maven.tools.plugin.extractor.annotations.scanner.visitors.MojoClassVisitor;
50  import org.apache.maven.tools.plugin.extractor.annotations.scanner.visitors.MojoFieldVisitor;
51  import org.apache.maven.tools.plugin.extractor.annotations.scanner.visitors.MojoParameterVisitor;
52  import org.codehaus.plexus.util.DirectoryScanner;
53  import org.codehaus.plexus.util.StringUtils;
54  import org.codehaus.plexus.util.reflection.Reflector;
55  import org.codehaus.plexus.util.reflection.ReflectorException;
56  import org.objectweb.asm.ClassReader;
57  import org.objectweb.asm.Type;
58  import org.slf4j.Logger;
59  import org.slf4j.LoggerFactory;
60  
61  /**
62   * Mojo scanner with java annotations.
63   *
64   * @author Olivier Lamy
65   * @since 3.0
66   */
67  @Named
68  @Singleton
69  public class DefaultMojoAnnotationsScanner implements MojoAnnotationsScanner {
70      private static final Logger LOGGER = LoggerFactory.getLogger(DefaultMojoAnnotationsScanner.class);
71      public static final String MVN4_API = "org.apache.maven.api.plugin.annotations.";
72      public static final String MOJO_V4 = MVN4_API + "Mojo";
73      public static final String EXECUTE_V4 = MVN4_API + "Execute";
74      public static final String PARAMETER_V4 = MVN4_API + "Parameter";
75      public static final String COMPONENT_V4 = MVN4_API + "Component";
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              for (Artifact dependency : request.getDependencies()) {
94                  scan(mojoAnnotatedClasses, dependency.getFile(), request.getIncludePatterns(), dependency, true);
95                  if (request.getMavenApiVersion() == null
96                          && dependency.getGroupId().equals("org.apache.maven")
97                          && (dependency.getArtifactId().equals("maven-plugin-api")
98                                  || dependency.getArtifactId().equals("maven-api-core"))) {
99                      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 }