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.logging.AbstractLogEnabled;
053import org.codehaus.plexus.util.DirectoryScanner;
054import org.codehaus.plexus.util.StringUtils;
055import org.codehaus.plexus.util.reflection.Reflector;
056import org.codehaus.plexus.util.reflection.ReflectorException;
057import org.objectweb.asm.ClassReader;
058import org.objectweb.asm.Type;
059
060/**
061 * Mojo scanner with java annotations.
062 *
063 * @author Olivier Lamy
064 * @since 3.0
065 */
066@Named
067@Singleton
068public class DefaultMojoAnnotationsScanner extends AbstractLogEnabled implements MojoAnnotationsScanner {
069    public static final String MVN4_API = "org.apache.maven.api.plugin.annotations.";
070    public static final String MOJO_V4 = MVN4_API + "Mojo";
071    public static final String EXECUTE_V4 = MVN4_API + "Execute";
072    public static final String PARAMETER_V4 = MVN4_API + "Parameter";
073    public static final String COMPONENT_V4 = MVN4_API + "Component";
074
075    public static final String MOJO_V3 = Mojo.class.getName();
076    public static final String EXECUTE_V3 = Execute.class.getName();
077    public static final String PARAMETER_V3 = Parameter.class.getName();
078    public static final String COMPONENT_V3 = Component.class.getName();
079
080    // classes with a dash must be ignored
081    private static final Pattern SCANNABLE_CLASS = Pattern.compile("[^-]+\\.class");
082    private static final String EMPTY = "";
083
084    private Reflector reflector = new Reflector();
085
086    @Override
087    public Map<String, MojoAnnotatedClass> scan(MojoAnnotationsScannerRequest request) throws ExtractionException {
088        Map<String, MojoAnnotatedClass> mojoAnnotatedClasses = new HashMap<>();
089
090        try {
091            for (Artifact dependency : request.getDependencies()) {
092                scan(mojoAnnotatedClasses, dependency.getFile(), request.getIncludePatterns(), dependency, true);
093                if (request.getMavenApiVersion() == null
094                        && dependency.getGroupId().equals("org.apache.maven")
095                        && (dependency.getArtifactId().equals("maven-plugin-api")
096                                || dependency.getArtifactId().equals("maven-api-core"))) {
097                    request.setMavenApiVersion(dependency.getVersion());
098                }
099            }
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}