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