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