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.ArrayList;
030import java.util.Arrays;
031import java.util.HashMap;
032import java.util.HashSet;
033import java.util.List;
034import java.util.Map;
035import java.util.Objects;
036import java.util.regex.Pattern;
037import java.util.zip.ZipEntry;
038import java.util.zip.ZipInputStream;
039
040import org.apache.maven.artifact.Artifact;
041import org.apache.maven.plugins.annotations.Component;
042import org.apache.maven.plugins.annotations.Execute;
043import org.apache.maven.plugins.annotations.Mojo;
044import org.apache.maven.plugins.annotations.Parameter;
045import org.apache.maven.tools.plugin.extractor.ExtractionException;
046import org.apache.maven.tools.plugin.extractor.annotations.datamodel.AfterAnnotationContent;
047import org.apache.maven.tools.plugin.extractor.annotations.datamodel.ComponentAnnotationContent;
048import org.apache.maven.tools.plugin.extractor.annotations.datamodel.ExecuteAnnotationContent;
049import org.apache.maven.tools.plugin.extractor.annotations.datamodel.MojoAnnotationContent;
050import org.apache.maven.tools.plugin.extractor.annotations.datamodel.ParameterAnnotationContent;
051import org.apache.maven.tools.plugin.extractor.annotations.scanner.visitors.MojoAnnotationVisitor;
052import org.apache.maven.tools.plugin.extractor.annotations.scanner.visitors.MojoClassVisitor;
053import org.apache.maven.tools.plugin.extractor.annotations.scanner.visitors.MojoFieldVisitor;
054import org.apache.maven.tools.plugin.extractor.annotations.scanner.visitors.MojoParameterVisitor;
055import org.codehaus.plexus.util.DirectoryScanner;
056import org.codehaus.plexus.util.StringUtils;
057import org.codehaus.plexus.util.reflection.Reflector;
058import org.codehaus.plexus.util.reflection.ReflectorException;
059import org.objectweb.asm.ClassReader;
060import org.objectweb.asm.Type;
061import org.slf4j.Logger;
062import org.slf4j.LoggerFactory;
063
064/**
065 * Mojo scanner with java annotations.
066 *
067 * @author Olivier Lamy
068 * @since 3.0
069 */
070@Named
071@Singleton
072public class DefaultMojoAnnotationsScanner implements MojoAnnotationsScanner {
073    private static final Logger LOGGER = LoggerFactory.getLogger(DefaultMojoAnnotationsScanner.class);
074    public static final String MVN4_API = "org.apache.maven.api.plugin.annotations.";
075    public static final String MOJO_V4 = MVN4_API + "Mojo";
076    public static final String EXECUTE_V4 = MVN4_API + "Execute";
077    public static final String PARAMETER_V4 = MVN4_API + "Parameter";
078    public static final String AFTER_V4 = MVN4_API + "After";
079    public static final String AFTERS_V4 = MVN4_API + "Afters";
080
081    public static final String MOJO_V3 = Mojo.class.getName();
082    public static final String EXECUTE_V3 = Execute.class.getName();
083    public static final String PARAMETER_V3 = Parameter.class.getName();
084    public static final String COMPONENT_V3 = Component.class.getName();
085
086    // classes with a dash must be ignored
087    private static final Pattern SCANNABLE_CLASS = Pattern.compile("[^-]+\\.class");
088    private static final String EMPTY = "";
089
090    private Reflector reflector = new Reflector();
091
092    @Override
093    public Map<String, MojoAnnotatedClass> scan(MojoAnnotationsScannerRequest request) throws ExtractionException {
094        Map<String, MojoAnnotatedClass> mojoAnnotatedClasses = new HashMap<>();
095
096        try {
097            String mavenApiVersion = null;
098            for (Artifact dependency : request.getDependencies()) {
099                scan(mojoAnnotatedClasses, dependency.getFile(), request.getIncludePatterns(), dependency, true);
100                if (request.getMavenApiVersion() == null
101                        && dependency.getGroupId().equals("org.apache.maven")
102                        && (dependency.getArtifactId().equals("maven-plugin-api")
103                                || dependency.getArtifactId().equals("maven-api-core"))) {
104                    String version = dependency.getVersion();
105                    if (mavenApiVersion != null && !Objects.equals(version, mavenApiVersion)) {
106                        throw new UnsupportedOperationException("Mixing Maven 3 and Maven 4 plugins is not supported."
107                                + " Fix your dependencies so that you depend either on maven-plugin-api for a Maven 3 plugin,"
108                                + " or maven-api-core for a Maven 4 plugin.");
109                    }
110                    mavenApiVersion = version;
111                }
112            }
113            request.setMavenApiVersion(mavenApiVersion);
114
115            for (File classDirectory : request.getClassesDirectories()) {
116                scan(
117                        mojoAnnotatedClasses,
118                        classDirectory,
119                        request.getIncludePatterns(),
120                        request.getProject().getArtifact(),
121                        false);
122            }
123        } catch (IOException e) {
124            throw new ExtractionException(e.getMessage(), e);
125        }
126
127        return mojoAnnotatedClasses;
128    }
129
130    protected void scan(
131            Map<String, MojoAnnotatedClass> mojoAnnotatedClasses,
132            File source,
133            List<String> includePatterns,
134            Artifact artifact,
135            boolean excludeMojo)
136            throws IOException, ExtractionException {
137        if (source == null || !source.exists()) {
138            return;
139        }
140
141        Map<String, MojoAnnotatedClass> scanResult;
142        if (source.isDirectory()) {
143            scanResult = scanDirectory(source, includePatterns, artifact, excludeMojo);
144        } else {
145            scanResult = scanArchive(source, artifact, excludeMojo);
146        }
147
148        mojoAnnotatedClasses.putAll(scanResult);
149    }
150
151    /**
152     * @param archiveFile
153     * @param artifact
154     * @param excludeMojo     for dependencies, we exclude Mojo annotations found
155     * @return annotated classes found
156     * @throws IOException
157     * @throws ExtractionException
158     */
159    protected Map<String, MojoAnnotatedClass> scanArchive(File archiveFile, Artifact artifact, boolean excludeMojo)
160            throws IOException, ExtractionException {
161        Map<String, MojoAnnotatedClass> mojoAnnotatedClasses = new HashMap<>();
162
163        String zipEntryName = null;
164        try (ZipInputStream archiveStream = new ZipInputStream(new FileInputStream(archiveFile))) {
165            String archiveFilename = archiveFile.getAbsolutePath();
166            for (ZipEntry zipEntry = archiveStream.getNextEntry();
167                    zipEntry != null;
168                    zipEntry = archiveStream.getNextEntry()) {
169                zipEntryName = zipEntry.getName();
170                if (!SCANNABLE_CLASS.matcher(zipEntryName).matches()) {
171                    continue;
172                }
173                analyzeClassStream(
174                        mojoAnnotatedClasses,
175                        archiveStream,
176                        artifact,
177                        excludeMojo,
178                        archiveFilename,
179                        zipEntry.getName());
180            }
181        } catch (IllegalArgumentException e) {
182            // In case of a class with newer specs an IllegalArgumentException can be thrown
183            LOGGER.error("Failed to analyze " + archiveFile.getAbsolutePath() + "!/" + zipEntryName);
184
185            throw e;
186        }
187
188        return mojoAnnotatedClasses;
189    }
190
191    /**
192     * @param classDirectory
193     * @param includePatterns
194     * @param artifact
195     * @param excludeMojo     for dependencies, we exclude Mojo annotations found
196     * @return annotated classes found
197     * @throws IOException
198     * @throws ExtractionException
199     */
200    protected Map<String, MojoAnnotatedClass> scanDirectory(
201            File classDirectory, List<String> includePatterns, Artifact artifact, boolean excludeMojo)
202            throws IOException, ExtractionException {
203        Map<String, MojoAnnotatedClass> mojoAnnotatedClasses = new HashMap<>();
204
205        DirectoryScanner scanner = new DirectoryScanner();
206        scanner.setBasedir(classDirectory);
207        scanner.addDefaultExcludes();
208        if (includePatterns != null) {
209            scanner.setIncludes(includePatterns.toArray(new String[includePatterns.size()]));
210        }
211        scanner.scan();
212        String[] classFiles = scanner.getIncludedFiles();
213        String classDirname = classDirectory.getAbsolutePath();
214
215        for (String classFile : classFiles) {
216            if (!SCANNABLE_CLASS.matcher(classFile).matches()) {
217                continue;
218            }
219
220            try (InputStream is = //
221                    new BufferedInputStream(new FileInputStream(new File(classDirectory, classFile)))) {
222                analyzeClassStream(mojoAnnotatedClasses, is, artifact, excludeMojo, classDirname, classFile);
223            }
224        }
225        return mojoAnnotatedClasses;
226    }
227
228    private void analyzeClassStream(
229            Map<String, MojoAnnotatedClass> mojoAnnotatedClasses,
230            InputStream is,
231            Artifact artifact,
232            boolean excludeMojo,
233            String source,
234            String file)
235            throws IOException, ExtractionException {
236        MojoClassVisitor mojoClassVisitor = new MojoClassVisitor();
237        try {
238            ClassReader rdr = new ClassReader(is);
239            rdr.accept(mojoClassVisitor, ClassReader.SKIP_FRAMES | ClassReader.SKIP_CODE | ClassReader.SKIP_DEBUG);
240        } catch (ArrayIndexOutOfBoundsException aiooe) {
241            LOGGER.warn(
242                    "Error analyzing class " + file + " in " + source + ": ignoring class",
243                    LOGGER.isDebugEnabled() ? aiooe : null);
244            return;
245        } catch (IllegalArgumentException iae) {
246            if (iae.getMessage() == null) {
247                LOGGER.warn(
248                        "Error analyzing class " + file + " in " + source + ": ignoring class",
249                        LOGGER.isDebugEnabled() ? iae : null);
250                return;
251            } else {
252                throw iae;
253            }
254        }
255
256        analyzeVisitors(mojoClassVisitor);
257
258        MojoAnnotatedClass mojoAnnotatedClass = mojoClassVisitor.getMojoAnnotatedClass();
259
260        if (excludeMojo) {
261            mojoAnnotatedClass.setMojo(null);
262        }
263
264        if (mojoAnnotatedClass != null) // see MPLUGIN-206 we can have intermediate classes without annotations
265        {
266            if (LOGGER.isDebugEnabled() && mojoAnnotatedClass.hasAnnotations()) {
267                LOGGER.debug(
268                        "found MojoAnnotatedClass:" + mojoAnnotatedClass.getClassName() + ":" + mojoAnnotatedClass);
269            }
270            mojoAnnotatedClass.setArtifact(artifact);
271            mojoAnnotatedClasses.put(mojoAnnotatedClass.getClassName(), mojoAnnotatedClass);
272            mojoAnnotatedClass.setClassVersion(mojoClassVisitor.getVersion());
273        }
274    }
275
276    protected void populateAnnotationContent(Object content, MojoAnnotationVisitor mojoAnnotationVisitor)
277            throws ReflectorException {
278        for (Map.Entry<String, Object> entry :
279                mojoAnnotationVisitor.getAnnotationValues().entrySet()) {
280            reflector.invoke(content, entry.getKey(), new Object[] {entry.getValue()});
281        }
282    }
283
284    protected void analyzeVisitors(MojoClassVisitor mojoClassVisitor) throws ExtractionException {
285        final MojoAnnotatedClass mojoAnnotatedClass = mojoClassVisitor.getMojoAnnotatedClass();
286
287        try {
288            // @Mojo annotation
289            MojoAnnotationVisitor mojoAnnotationVisitor = mojoClassVisitor.getAnnotationVisitor(MOJO_V3);
290            if (mojoAnnotationVisitor == null) {
291                mojoAnnotationVisitor = mojoClassVisitor.getAnnotationVisitor(MOJO_V4);
292            }
293            if (mojoAnnotationVisitor != null) {
294                MojoAnnotationContent mojoAnnotationContent = new MojoAnnotationContent();
295                populateAnnotationContent(mojoAnnotationContent, mojoAnnotationVisitor);
296
297                if (mojoClassVisitor.getAnnotationVisitor(Deprecated.class) != null) {
298                    mojoAnnotationContent.setDeprecated(EMPTY);
299                }
300
301                mojoAnnotatedClass.setMojo(mojoAnnotationContent);
302            }
303
304            // @Execute annotation
305            mojoAnnotationVisitor = mojoClassVisitor.getAnnotationVisitor(EXECUTE_V3);
306            if (mojoAnnotationVisitor == null) {
307                mojoAnnotationVisitor = mojoClassVisitor.getAnnotationVisitor(EXECUTE_V4);
308            }
309            if (mojoAnnotationVisitor != null) {
310                ExecuteAnnotationContent executeAnnotationContent = new ExecuteAnnotationContent();
311                populateAnnotationContent(executeAnnotationContent, mojoAnnotationVisitor);
312                mojoAnnotatedClass.setExecute(executeAnnotationContent);
313            }
314
315            // @After annotation(s) — v4 API only
316            List<AfterAnnotationContent> afterAnnotations = new ArrayList<>();
317
318            // single @After
319            mojoAnnotationVisitor = mojoClassVisitor.getAnnotationVisitor(AFTER_V4);
320            if (mojoAnnotationVisitor != null) {
321                AfterAnnotationContent afterAnnotationContent = new AfterAnnotationContent();
322                populateAnnotationContent(afterAnnotationContent, mojoAnnotationVisitor);
323                afterAnnotations.add(afterAnnotationContent);
324            }
325
326            // @Afters repeatable container wraps multiple @After in its "value" array attribute
327            mojoAnnotationVisitor = mojoClassVisitor.getAnnotationVisitor(AFTERS_V4);
328            if (mojoAnnotationVisitor != null) {
329                // The @Afters annotation has a "value" array attribute; visitArray("value")
330                // creates a sub-visitor, and each nested @After is visited via visitAnnotation
331                // on that sub-visitor.
332                MojoAnnotationVisitor arrayVisitor = mojoAnnotationVisitor.getArrayVisitor("value");
333                if (arrayVisitor != null) {
334                    for (MojoAnnotationVisitor nestedVisitor : arrayVisitor.getNestedAnnotationVisitors()) {
335                        AfterAnnotationContent afterAnnotationContent = new AfterAnnotationContent();
336                        populateAnnotationContent(afterAnnotationContent, nestedVisitor);
337                        afterAnnotations.add(afterAnnotationContent);
338                    }
339                }
340            }
341
342            if (!afterAnnotations.isEmpty()) {
343                mojoAnnotatedClass.setAfterAnnotations(afterAnnotations);
344            }
345
346            // @Parameter annotations
347            List<MojoParameterVisitor> mojoParameterVisitors =
348                    mojoClassVisitor.findParameterVisitors(new HashSet<>(Arrays.asList(PARAMETER_V3, PARAMETER_V4)));
349            for (MojoParameterVisitor parameterVisitor : mojoParameterVisitors) {
350                ParameterAnnotationContent parameterAnnotationContent = new ParameterAnnotationContent(
351                        parameterVisitor.getFieldName(),
352                        parameterVisitor.getClassName(),
353                        parameterVisitor.getTypeParameters(),
354                        parameterVisitor.isAnnotationOnMethod());
355
356                Map<String, MojoAnnotationVisitor> annotationVisitorMap = parameterVisitor.getAnnotationVisitorMap();
357                MojoAnnotationVisitor fieldAnnotationVisitor = annotationVisitorMap.get(PARAMETER_V3);
358                if (fieldAnnotationVisitor == null) {
359                    fieldAnnotationVisitor = annotationVisitorMap.get(PARAMETER_V4);
360                }
361
362                if (fieldAnnotationVisitor != null) {
363                    populateAnnotationContent(parameterAnnotationContent, fieldAnnotationVisitor);
364                }
365
366                if (annotationVisitorMap.containsKey(Deprecated.class.getName())) {
367                    parameterAnnotationContent.setDeprecated(EMPTY);
368                }
369
370                mojoAnnotatedClass
371                        .getParameters()
372                        .put(parameterAnnotationContent.getFieldName(), parameterAnnotationContent);
373            }
374
375            // @Component annotations
376            List<MojoFieldVisitor> mojoComponentVisitors =
377                    mojoClassVisitor.findFieldWithAnnotation(new HashSet<>(Arrays.asList(COMPONENT_V3)));
378            for (MojoFieldVisitor mojoComponentVisitor : mojoComponentVisitors) {
379                ComponentAnnotationContent componentAnnotationContent =
380                        new ComponentAnnotationContent(mojoComponentVisitor.getFieldName());
381
382                Map<String, MojoAnnotationVisitor> annotationVisitorMap =
383                        mojoComponentVisitor.getAnnotationVisitorMap();
384                MojoAnnotationVisitor annotationVisitor = annotationVisitorMap.get(COMPONENT_V3);
385
386                if (annotationVisitor != null) {
387                    for (Map.Entry<String, Object> entry :
388                            annotationVisitor.getAnnotationValues().entrySet()) {
389                        String methodName = entry.getKey();
390                        if ("role".equals(methodName)) {
391                            Type type = (Type) entry.getValue();
392                            componentAnnotationContent.setRoleClassName(type.getClassName());
393                        } else {
394                            reflector.invoke(
395                                    componentAnnotationContent, entry.getKey(), new Object[] {entry.getValue()});
396                        }
397                    }
398
399                    if (StringUtils.isEmpty(componentAnnotationContent.getRoleClassName())) {
400                        componentAnnotationContent.setRoleClassName(mojoComponentVisitor.getClassName());
401                    }
402                }
403                mojoAnnotatedClass
404                        .getComponents()
405                        .put(componentAnnotationContent.getFieldName(), componentAnnotationContent);
406            }
407        } catch (ReflectorException e) {
408            throw new ExtractionException(e.getMessage(), e);
409        }
410    }
411}