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;
020
021import javax.inject.Inject;
022import javax.inject.Named;
023import javax.inject.Singleton;
024
025import java.io.File;
026import java.net.MalformedURLException;
027import java.net.URL;
028import java.net.URLClassLoader;
029import java.util.ArrayList;
030import java.util.Arrays;
031import java.util.Collection;
032import java.util.Collections;
033import java.util.Comparator;
034import java.util.HashMap;
035import java.util.HashSet;
036import java.util.List;
037import java.util.Map;
038import java.util.Objects;
039import java.util.Optional;
040import java.util.Set;
041import java.util.TreeMap;
042import java.util.TreeSet;
043import java.util.stream.Collectors;
044
045import com.thoughtworks.qdox.JavaProjectBuilder;
046import com.thoughtworks.qdox.library.SortedClassLibraryBuilder;
047import com.thoughtworks.qdox.model.DocletTag;
048import com.thoughtworks.qdox.model.JavaAnnotatedElement;
049import com.thoughtworks.qdox.model.JavaClass;
050import com.thoughtworks.qdox.model.JavaField;
051import com.thoughtworks.qdox.model.JavaMember;
052import com.thoughtworks.qdox.model.JavaMethod;
053import org.apache.maven.artifact.Artifact;
054import org.apache.maven.artifact.versioning.ComparableVersion;
055import org.apache.maven.plugin.descriptor.InvalidParameterException;
056import org.apache.maven.plugin.descriptor.InvalidPluginDescriptorException;
057import org.apache.maven.plugin.descriptor.MojoDescriptor;
058import org.apache.maven.plugin.descriptor.PluginDescriptor;
059import org.apache.maven.plugin.descriptor.Requirement;
060import org.apache.maven.project.MavenProject;
061import org.apache.maven.tools.plugin.ExtendedMojoDescriptor;
062import org.apache.maven.tools.plugin.PluginToolsRequest;
063import org.apache.maven.tools.plugin.extractor.ExtractionException;
064import org.apache.maven.tools.plugin.extractor.GroupKey;
065import org.apache.maven.tools.plugin.extractor.MojoDescriptorExtractor;
066import org.apache.maven.tools.plugin.extractor.annotations.converter.ConverterContext;
067import org.apache.maven.tools.plugin.extractor.annotations.converter.JavaClassConverterContext;
068import org.apache.maven.tools.plugin.extractor.annotations.converter.JavadocBlockTagsToXhtmlConverter;
069import org.apache.maven.tools.plugin.extractor.annotations.converter.JavadocInlineTagsToXhtmlConverter;
070import org.apache.maven.tools.plugin.extractor.annotations.datamodel.ComponentAnnotationContent;
071import org.apache.maven.tools.plugin.extractor.annotations.datamodel.ExecuteAnnotationContent;
072import org.apache.maven.tools.plugin.extractor.annotations.datamodel.MojoAnnotationContent;
073import org.apache.maven.tools.plugin.extractor.annotations.datamodel.ParameterAnnotationContent;
074import org.apache.maven.tools.plugin.extractor.annotations.scanner.MojoAnnotatedClass;
075import org.apache.maven.tools.plugin.extractor.annotations.scanner.MojoAnnotationsScanner;
076import org.apache.maven.tools.plugin.extractor.annotations.scanner.MojoAnnotationsScannerRequest;
077import org.apache.maven.tools.plugin.javadoc.JavadocLinkGenerator;
078import org.codehaus.plexus.archiver.ArchiverException;
079import org.codehaus.plexus.archiver.UnArchiver;
080import org.codehaus.plexus.archiver.manager.ArchiverManager;
081import org.codehaus.plexus.archiver.manager.NoSuchArchiverException;
082import org.codehaus.plexus.util.StringUtils;
083import org.eclipse.aether.RepositorySystem;
084import org.eclipse.aether.artifact.DefaultArtifact;
085import org.eclipse.aether.resolution.ArtifactRequest;
086import org.eclipse.aether.resolution.ArtifactResolutionException;
087import org.eclipse.aether.resolution.ArtifactResult;
088import org.objectweb.asm.Opcodes;
089import org.slf4j.Logger;
090import org.slf4j.LoggerFactory;
091
092/**
093 * JavaMojoDescriptorExtractor, a MojoDescriptor extractor to read descriptors from java classes with annotations.
094 * Notice that source files are also parsed to get description, since and deprecation information.
095 *
096 * @author Olivier Lamy
097 * @since 3.0
098 */
099@Named(JavaAnnotationsMojoDescriptorExtractor.NAME)
100@Singleton
101public class JavaAnnotationsMojoDescriptorExtractor implements MojoDescriptorExtractor {
102    private static final Logger LOGGER = LoggerFactory.getLogger(JavaAnnotationsMojoDescriptorExtractor.class);
103    public static final String NAME = "java-annotations";
104
105    private static final GroupKey GROUP_KEY = new GroupKey(GroupKey.JAVA_GROUP, 100);
106
107    /**
108     *
109     * @see <a href="https://docs.oracle.com/javase/specs/jvms/se19/html/jvms-4.html#jvms-4.1">JVMS 4.1</a>
110     */
111    private static final Map<Integer, String> CLASS_VERSION_TO_JAVA_STRING;
112
113    static {
114        CLASS_VERSION_TO_JAVA_STRING = new HashMap<>();
115        CLASS_VERSION_TO_JAVA_STRING.put(Opcodes.V1_1, "1.1");
116        CLASS_VERSION_TO_JAVA_STRING.put(Opcodes.V1_2, "1.2");
117        CLASS_VERSION_TO_JAVA_STRING.put(Opcodes.V1_3, "1.3");
118        CLASS_VERSION_TO_JAVA_STRING.put(Opcodes.V1_4, "1.4");
119        CLASS_VERSION_TO_JAVA_STRING.put(Opcodes.V1_5, "1.5");
120        CLASS_VERSION_TO_JAVA_STRING.put(Opcodes.V1_6, "1.6");
121        CLASS_VERSION_TO_JAVA_STRING.put(Opcodes.V1_7, "1.7");
122        CLASS_VERSION_TO_JAVA_STRING.put(Opcodes.V1_8, "1.8");
123        CLASS_VERSION_TO_JAVA_STRING.put(Opcodes.V9, "9");
124        CLASS_VERSION_TO_JAVA_STRING.put(Opcodes.V10, "10");
125        CLASS_VERSION_TO_JAVA_STRING.put(Opcodes.V11, "11");
126        CLASS_VERSION_TO_JAVA_STRING.put(Opcodes.V12, "12");
127        CLASS_VERSION_TO_JAVA_STRING.put(Opcodes.V13, "13");
128        CLASS_VERSION_TO_JAVA_STRING.put(Opcodes.V14, "14");
129        CLASS_VERSION_TO_JAVA_STRING.put(Opcodes.V15, "15");
130        CLASS_VERSION_TO_JAVA_STRING.put(Opcodes.V16, "16");
131        CLASS_VERSION_TO_JAVA_STRING.put(Opcodes.V17, "17");
132        CLASS_VERSION_TO_JAVA_STRING.put(Opcodes.V18, "18");
133        CLASS_VERSION_TO_JAVA_STRING.put(Opcodes.V19, "19");
134        CLASS_VERSION_TO_JAVA_STRING.put(Opcodes.V20, "20");
135        CLASS_VERSION_TO_JAVA_STRING.put(Opcodes.V21, "21");
136        CLASS_VERSION_TO_JAVA_STRING.put(Opcodes.V22, "22");
137        CLASS_VERSION_TO_JAVA_STRING.put(Opcodes.V23, "23");
138        CLASS_VERSION_TO_JAVA_STRING.put(Opcodes.V24, "24");
139        CLASS_VERSION_TO_JAVA_STRING.put(Opcodes.V25, "25");
140        CLASS_VERSION_TO_JAVA_STRING.put(Opcodes.V26, "26");
141    }
142
143    @Inject
144    MojoAnnotationsScanner mojoAnnotationsScanner;
145
146    @Inject
147    private RepositorySystem repositorySystem;
148
149    @Inject
150    private ArchiverManager archiverManager;
151
152    @Inject
153    private JavadocInlineTagsToXhtmlConverter javadocInlineTagsToHtmlConverter;
154
155    @Inject
156    private JavadocBlockTagsToXhtmlConverter javadocBlockTagsToHtmlConverter;
157
158    @Override
159    public String getName() {
160        return NAME;
161    }
162
163    @Override
164    public boolean isDeprecated() {
165        return false; // this is the "current way" to write Java Mojos
166    }
167
168    @Override
169    public GroupKey getGroupKey() {
170        return GROUP_KEY;
171    }
172
173    /**
174     * Compares class file format versions.
175     * @see <a href="https://docs.oracle.com/javase/specs/jvms/se19/html/jvms-4.html#jvms-4.1">JVMS 4.1</a>
176     *
177     */
178    @SuppressWarnings("checkstyle:magicnumber")
179    static final class ClassVersionComparator implements Comparator<Integer> {
180        @Override
181        public int compare(Integer classVersion1, Integer classVersion2) {
182            // first compare major version (
183            int result = Integer.compare(classVersion1 & 0x00FF, classVersion2 & 0x00FF);
184            if (result == 0) {
185                // compare minor version if major is equal
186                result = Integer.compare(classVersion1, classVersion2);
187            }
188            return result;
189        }
190    }
191
192    @Override
193    public List<MojoDescriptor> execute(PluginToolsRequest request)
194            throws ExtractionException, InvalidPluginDescriptorException {
195        Map<String, MojoAnnotatedClass> mojoAnnotatedClasses = scanAnnotations(request);
196
197        Optional<Integer> maxClassVersion = mojoAnnotatedClasses.values().stream()
198                .map(MojoAnnotatedClass::getClassVersion)
199                .max(new ClassVersionComparator());
200        if (maxClassVersion.isPresent()) {
201            String requiredJavaVersion = CLASS_VERSION_TO_JAVA_STRING.get(maxClassVersion.get());
202            if (StringUtils.isBlank(request.getRequiredJavaVersion())
203                    || new ComparableVersion(request.getRequiredJavaVersion())
204                                    .compareTo(new ComparableVersion(requiredJavaVersion))
205                            < 0) {
206                request.setRequiredJavaVersion(requiredJavaVersion);
207            }
208        }
209        JavaProjectBuilder builder = scanJavadoc(request, mojoAnnotatedClasses.values());
210        Map<String, JavaClass> javaClassesMap = discoverClasses(builder);
211
212        final JavadocLinkGenerator linkGenerator;
213        if (request.getInternalJavadocBaseUrl() != null
214                || (request.getExternalJavadocBaseUrls() != null
215                        && !request.getExternalJavadocBaseUrls().isEmpty())) {
216            linkGenerator = new JavadocLinkGenerator(
217                    request.getInternalJavadocBaseUrl(),
218                    request.getInternalJavadocVersion(),
219                    request.getExternalJavadocBaseUrls(),
220                    request.getSettings());
221        } else {
222            linkGenerator = null;
223        }
224
225        populateDataFromJavadoc(builder, mojoAnnotatedClasses, javaClassesMap, linkGenerator);
226
227        return toMojoDescriptors(mojoAnnotatedClasses, request.getPluginDescriptor());
228    }
229
230    private Map<String, MojoAnnotatedClass> scanAnnotations(PluginToolsRequest request) throws ExtractionException {
231        MojoAnnotationsScannerRequest mojoAnnotationsScannerRequest = new MojoAnnotationsScannerRequest();
232
233        File output = new File(request.getProject().getBuild().getOutputDirectory());
234        mojoAnnotationsScannerRequest.setClassesDirectories(Arrays.asList(output));
235
236        mojoAnnotationsScannerRequest.setDependencies(request.getDependencies());
237
238        mojoAnnotationsScannerRequest.setProject(request.getProject());
239
240        Map<String, MojoAnnotatedClass> result = mojoAnnotationsScanner.scan(mojoAnnotationsScannerRequest);
241        request.setUsedMavenApiVersion(mojoAnnotationsScannerRequest.getMavenApiVersion());
242        return result;
243    }
244
245    private JavaProjectBuilder scanJavadoc(
246            PluginToolsRequest request, Collection<MojoAnnotatedClass> mojoAnnotatedClasses)
247            throws ExtractionException {
248        // found artifact from reactors to scan sources
249        // we currently only scan sources from reactors
250        List<MavenProject> mavenProjects = new ArrayList<>();
251
252        // if we need to scan sources from external artifacts
253        Set<Artifact> externalArtifacts = new HashSet<>();
254
255        JavaProjectBuilder builder = new JavaProjectBuilder(new SortedClassLibraryBuilder());
256        builder.setEncoding(request.getEncoding());
257        extendJavaProjectBuilder(builder, request.getProject());
258
259        for (MojoAnnotatedClass mojoAnnotatedClass : mojoAnnotatedClasses) {
260            if (Objects.equals(
261                    mojoAnnotatedClass.getArtifact().getArtifactId(),
262                    request.getProject().getArtifact().getArtifactId())) {
263                continue;
264            }
265
266            if (!isMojoAnnnotatedClassCandidate(mojoAnnotatedClass)) {
267                // we don't scan sources for classes without mojo annotations
268                continue;
269            }
270
271            MavenProject mavenProject =
272                    getFromProjectReferences(mojoAnnotatedClass.getArtifact(), request.getProject());
273
274            if (mavenProject != null) {
275                mavenProjects.add(mavenProject);
276            } else {
277                externalArtifacts.add(mojoAnnotatedClass.getArtifact());
278            }
279        }
280
281        // try to get artifact with sources classifier, extract somewhere then scan for @since, @deprecated
282        for (Artifact artifact : externalArtifacts) {
283            // parameter for test-sources too ?? olamy I need that for it test only
284            if (StringUtils.equalsIgnoreCase("tests", artifact.getClassifier())) {
285                extendJavaProjectBuilderWithSourcesJar(builder, artifact, request, "test-sources");
286            } else {
287                extendJavaProjectBuilderWithSourcesJar(builder, artifact, request, "sources");
288            }
289        }
290
291        for (MavenProject mavenProject : mavenProjects) {
292            extendJavaProjectBuilder(builder, mavenProject);
293        }
294
295        return builder;
296    }
297
298    private boolean isMojoAnnnotatedClassCandidate(MojoAnnotatedClass mojoAnnotatedClass) {
299        return mojoAnnotatedClass != null && mojoAnnotatedClass.hasAnnotations();
300    }
301
302    /**
303     * from sources scan to get @since and @deprecated and description of classes and fields.
304     */
305    protected void populateDataFromJavadoc(
306            JavaProjectBuilder javaProjectBuilder,
307            Map<String, MojoAnnotatedClass> mojoAnnotatedClasses,
308            Map<String, JavaClass> javaClassesMap,
309            JavadocLinkGenerator linkGenerator) {
310
311        for (Map.Entry<String, MojoAnnotatedClass> entry : mojoAnnotatedClasses.entrySet()) {
312            JavaClass javaClass = javaClassesMap.get(entry.getKey());
313            if (javaClass == null) {
314                continue;
315            }
316            // populate class-level content
317            MojoAnnotationContent mojoAnnotationContent = entry.getValue().getMojo();
318            if (mojoAnnotationContent != null) {
319                JavaClassConverterContext context = new JavaClassConverterContext(
320                        javaClass, javaProjectBuilder, mojoAnnotatedClasses, linkGenerator, javaClass.getLineNumber());
321                mojoAnnotationContent.setDescription(getDescriptionFromElement(javaClass, context));
322
323                DocletTag since = findInClassHierarchy(javaClass, "since");
324                if (since != null) {
325                    mojoAnnotationContent.setSince(getRawValueFromTaglet(since, context));
326                }
327
328                DocletTag deprecated = findInClassHierarchy(javaClass, "deprecated");
329                if (deprecated != null) {
330                    mojoAnnotationContent.setDeprecated(getRawValueFromTaglet(deprecated, context));
331                }
332            }
333
334            Map<String, JavaAnnotatedElement> fieldsMap = extractFieldsAnnotations(javaClass, javaClassesMap);
335            Map<String, JavaAnnotatedElement> methodsMap = extractMethodsAnnotations(javaClass, javaClassesMap);
336
337            // populate parameters
338            Map<String, ParameterAnnotationContent> parameters =
339                    getParametersParentHierarchy(entry.getValue(), mojoAnnotatedClasses);
340            parameters = new TreeMap<>(parameters);
341            for (Map.Entry<String, ParameterAnnotationContent> parameter : parameters.entrySet()) {
342                JavaAnnotatedElement element;
343                if (parameter.getValue().isAnnotationOnMethod()) {
344                    element = methodsMap.get(parameter.getKey());
345                } else {
346                    element = fieldsMap.get(parameter.getKey());
347                }
348
349                if (element == null) {
350                    continue;
351                }
352
353                JavaClassConverterContext context = new JavaClassConverterContext(
354                        javaClass, ((JavaMember) element).getDeclaringClass(),
355                        javaProjectBuilder, mojoAnnotatedClasses,
356                        linkGenerator, element.getLineNumber());
357                ParameterAnnotationContent parameterAnnotationContent = parameter.getValue();
358                parameterAnnotationContent.setDescription(getDescriptionFromElement(element, context));
359
360                DocletTag deprecated = element.getTagByName("deprecated");
361                if (deprecated != null) {
362                    parameterAnnotationContent.setDeprecated(getRawValueFromTaglet(deprecated, context));
363                }
364
365                DocletTag since = element.getTagByName("since");
366                if (since != null) {
367                    parameterAnnotationContent.setSince(getRawValueFromTaglet(since, context));
368                }
369            }
370
371            // populate components
372            Map<String, ComponentAnnotationContent> components =
373                    entry.getValue().getComponents();
374            for (Map.Entry<String, ComponentAnnotationContent> component : components.entrySet()) {
375                JavaAnnotatedElement element = fieldsMap.get(component.getKey());
376                if (element == null) {
377                    continue;
378                }
379
380                JavaClassConverterContext context = new JavaClassConverterContext(
381                        javaClass, ((JavaMember) element).getDeclaringClass(),
382                        javaProjectBuilder, mojoAnnotatedClasses,
383                        linkGenerator, javaClass.getLineNumber());
384                ComponentAnnotationContent componentAnnotationContent = component.getValue();
385                componentAnnotationContent.setDescription(getDescriptionFromElement(element, context));
386
387                DocletTag deprecated = element.getTagByName("deprecated");
388                if (deprecated != null) {
389                    componentAnnotationContent.setDeprecated(getRawValueFromTaglet(deprecated, context));
390                }
391
392                DocletTag since = element.getTagByName("since");
393                if (since != null) {
394                    componentAnnotationContent.setSince(getRawValueFromTaglet(since, context));
395                }
396            }
397        }
398    }
399
400    /**
401     * Returns the XHTML description from the given element.
402     * This may refer to either goal, parameter or component.
403     * @param element the element for which to generate the description
404     * @param context the context with which to call the converter
405     * @return the generated description
406     */
407    String getDescriptionFromElement(JavaAnnotatedElement element, JavaClassConverterContext context) {
408
409        String comment = element.getComment();
410        if (comment == null) {
411            return null;
412        }
413        StringBuilder description = new StringBuilder(javadocInlineTagsToHtmlConverter.convert(comment, context));
414        for (DocletTag docletTag : element.getTags()) {
415            // also consider see block tags
416            if ("see".equals(docletTag.getName())) {
417                description.append(javadocBlockTagsToHtmlConverter.convert(docletTag, context));
418            }
419        }
420        return description.toString();
421    }
422
423    String getRawValueFromTaglet(DocletTag docletTag, ConverterContext context) {
424        // just resolve inline tags and convert to XHTML
425        return javadocInlineTagsToHtmlConverter.convert(docletTag.getValue(), context);
426    }
427
428    /**
429     * @param javaClass not null
430     * @param tagName   not null
431     * @return docletTag instance
432     */
433    private DocletTag findInClassHierarchy(JavaClass javaClass, String tagName) {
434        try {
435            DocletTag tag = javaClass.getTagByName(tagName);
436
437            if (tag == null) {
438                JavaClass superClass = javaClass.getSuperJavaClass();
439
440                if (superClass != null) {
441                    tag = findInClassHierarchy(superClass, tagName);
442                }
443            }
444
445            return tag;
446        } catch (NoClassDefFoundError e) {
447            if (e.getMessage().replace('/', '.').contains(MojoAnnotationsScanner.V4_API_PLUGIN_PACKAGE)) {
448                return null;
449            }
450            String str;
451            try {
452                str = javaClass.getFullyQualifiedName();
453            } catch (Throwable t) {
454                str = javaClass.getValue();
455            }
456            LOGGER.warn("Failed extracting tag '" + tagName + "' from class " + str);
457            throw (NoClassDefFoundError) new NoClassDefFoundError(e.getMessage()).initCause(e);
458        }
459    }
460
461    /**
462     * extract fields that are either parameters or components.
463     *
464     * @param javaClass not null
465     * @return map with Mojo parameters names as keys
466     */
467    private Map<String, JavaAnnotatedElement> extractFieldsAnnotations(
468            JavaClass javaClass, Map<String, JavaClass> javaClassesMap) {
469        try {
470            Map<String, JavaAnnotatedElement> rawParams = new TreeMap<>();
471
472            // we have to add the parent fields first, so that they will be overwritten by the local fields if
473            // that actually happens...
474            JavaClass superClass = javaClass.getSuperJavaClass();
475
476            if (superClass != null) {
477                if (!superClass.getFields().isEmpty()) {
478                    rawParams = extractFieldsAnnotations(superClass, javaClassesMap);
479                }
480                // maybe sources comes from scan of sources artifact
481                superClass = javaClassesMap.get(superClass.getFullyQualifiedName());
482                if (superClass != null && !superClass.getFields().isEmpty()) {
483                    rawParams = extractFieldsAnnotations(superClass, javaClassesMap);
484                }
485            } else {
486
487                rawParams = new TreeMap<>();
488            }
489
490            for (JavaField field : javaClass.getFields()) {
491                rawParams.put(field.getName(), field);
492            }
493
494            return rawParams;
495        } catch (NoClassDefFoundError e) {
496            LOGGER.warn("Failed extracting parameters from " + javaClass);
497            throw e;
498        }
499    }
500
501    /**
502     * extract methods that are parameters.
503     *
504     * @param javaClass not null
505     * @return map with Mojo parameters names as keys
506     */
507    private Map<String, JavaAnnotatedElement> extractMethodsAnnotations(
508            JavaClass javaClass, Map<String, JavaClass> javaClassesMap) {
509        try {
510            Map<String, JavaAnnotatedElement> rawParams = new TreeMap<>();
511
512            // we have to add the parent methods first, so that they will be overwritten by the local methods if
513            // that actually happens...
514            JavaClass superClass = javaClass.getSuperJavaClass();
515
516            if (superClass != null) {
517                if (!superClass.getMethods().isEmpty()) {
518                    rawParams = extractMethodsAnnotations(superClass, javaClassesMap);
519                }
520                // maybe sources comes from scan of sources artifact
521                superClass = javaClassesMap.get(superClass.getFullyQualifiedName());
522                if (superClass != null && !superClass.getMethods().isEmpty()) {
523                    rawParams = extractMethodsAnnotations(superClass, javaClassesMap);
524                }
525            } else {
526
527                rawParams = new TreeMap<>();
528            }
529
530            for (JavaMethod method : javaClass.getMethods()) {
531                if (isPublicSetterMethod(method)) {
532                    rawParams.put(
533                            StringUtils.lowercaseFirstLetter(method.getName().substring(3)), method);
534                }
535            }
536
537            return rawParams;
538        } catch (NoClassDefFoundError e) {
539            if (e.getMessage().replace('/', '.').contains(MojoAnnotationsScanner.V4_API_PLUGIN_PACKAGE)) {
540                return new TreeMap<>();
541            }
542            String str;
543            try {
544                str = javaClass.getFullyQualifiedName();
545            } catch (Throwable t) {
546                str = javaClass.getValue();
547            }
548            LOGGER.warn("Failed extracting methods from " + str);
549            throw (NoClassDefFoundError) new NoClassDefFoundError(e.getMessage()).initCause(e);
550        }
551    }
552
553    private boolean isPublicSetterMethod(JavaMethod method) {
554        return method.isPublic()
555                && !method.isStatic()
556                && method.getName().length() > 3
557                && (method.getName().startsWith("add") || method.getName().startsWith("set"))
558                && "void".equals(method.getReturnType().getValue())
559                && method.getParameters().size() == 1;
560    }
561
562    protected Map<String, JavaClass> discoverClasses(JavaProjectBuilder builder) {
563        Collection<JavaClass> javaClasses = builder.getClasses();
564
565        if (javaClasses == null || javaClasses.size() < 1) {
566            return Collections.emptyMap();
567        }
568
569        Map<String, JavaClass> javaClassMap = new HashMap<>(javaClasses.size());
570
571        for (JavaClass javaClass : javaClasses) {
572            javaClassMap.put(javaClass.getFullyQualifiedName(), javaClass);
573        }
574
575        return javaClassMap;
576    }
577
578    protected void extendJavaProjectBuilderWithSourcesJar(
579            JavaProjectBuilder builder, Artifact artifact, PluginToolsRequest request, String classifier)
580            throws ExtractionException {
581        try {
582            org.eclipse.aether.artifact.Artifact sourcesArtifact = new DefaultArtifact(
583                    artifact.getGroupId(),
584                    artifact.getArtifactId(),
585                    classifier,
586                    artifact.getArtifactHandler().getExtension(),
587                    artifact.getVersion());
588
589            ArtifactRequest resolveRequest =
590                    new ArtifactRequest(sourcesArtifact, request.getProject().getRemoteProjectRepositories(), null);
591            try {
592                ArtifactResult result = repositorySystem.resolveArtifact(request.getRepoSession(), resolveRequest);
593                sourcesArtifact = result.getArtifact();
594            } catch (ArtifactResolutionException e) {
595                String message = "Unable to get sources artifact for " + artifact.getId()
596                        + ". Some javadoc tags (@since, @deprecated and comments) won't be used";
597                if (LOGGER.isDebugEnabled()) {
598                    LOGGER.warn(message, e);
599                } else {
600                    LOGGER.warn(message);
601                }
602                return;
603            }
604
605            if (sourcesArtifact.getFile() == null || !sourcesArtifact.getFile().exists()) {
606                // could not get artifact sources
607                return;
608            }
609
610            if (sourcesArtifact.getFile().isFile()) {
611                // extract sources to target/maven-plugin-plugin-sources/${groupId}/${artifact}/sources
612                File extractDirectory = new File(
613                        request.getProject().getBuild().getDirectory(),
614                        "maven-plugin-plugin-sources/" + sourcesArtifact.getGroupId() + "/"
615                                + sourcesArtifact.getArtifactId() + "/" + sourcesArtifact.getVersion()
616                                + "/" + sourcesArtifact.getClassifier());
617                extractDirectory.mkdirs();
618
619                UnArchiver unArchiver = archiverManager.getUnArchiver("jar");
620                unArchiver.setSourceFile(sourcesArtifact.getFile());
621                unArchiver.setDestDirectory(extractDirectory);
622                unArchiver.extract();
623
624                extendJavaProjectBuilder(builder, Arrays.asList(extractDirectory), request.getDependencies());
625            } else if (sourcesArtifact.getFile().isDirectory()) {
626                extendJavaProjectBuilder(builder, Arrays.asList(sourcesArtifact.getFile()), request.getDependencies());
627            }
628        } catch (ArchiverException | NoSuchArchiverException e) {
629            throw new ExtractionException(e.getMessage(), e);
630        }
631    }
632
633    private void extendJavaProjectBuilder(JavaProjectBuilder builder, final MavenProject project) {
634        List<File> sources = new ArrayList<>();
635
636        for (String source : project.getCompileSourceRoots()) {
637            sources.add(new File(source));
638        }
639
640        // TODO be more dynamic
641        File generatedPlugin = new File(project.getBasedir(), "target/generated-sources/plugin");
642        if (!project.getCompileSourceRoots().contains(generatedPlugin.getAbsolutePath()) && generatedPlugin.exists()) {
643            sources.add(generatedPlugin);
644        }
645        extendJavaProjectBuilder(builder, sources, project.getArtifacts());
646    }
647
648    private void extendJavaProjectBuilder(
649            JavaProjectBuilder builder, List<File> sourceDirectories, Set<Artifact> artifacts) {
650
651        // Build isolated Classloader with only the artifacts of the project (none of this plugin)
652        List<URL> urls = new ArrayList<>(artifacts.size());
653        for (Artifact artifact : artifacts) {
654            try {
655                urls.add(artifact.getFile().toURI().toURL());
656            } catch (MalformedURLException e) {
657                // noop
658            }
659        }
660        builder.addClassLoader(new URLClassLoader(urls.toArray(new URL[0]), ClassLoader.getSystemClassLoader()));
661
662        for (File source : sourceDirectories) {
663            builder.addSourceTree(source);
664        }
665    }
666
667    private List<MojoDescriptor> toMojoDescriptors(
668            Map<String, MojoAnnotatedClass> mojoAnnotatedClasses, PluginDescriptor pluginDescriptor)
669            throws InvalidPluginDescriptorException {
670        List<MojoDescriptor> mojoDescriptors = new ArrayList<>(mojoAnnotatedClasses.size());
671        for (MojoAnnotatedClass mojoAnnotatedClass : mojoAnnotatedClasses.values()) {
672            // no mojo so skip it
673            if (mojoAnnotatedClass.getMojo() == null) {
674                continue;
675            }
676
677            ExtendedMojoDescriptor mojoDescriptor = new ExtendedMojoDescriptor(true);
678
679            // mojoDescriptor.setRole( mojoAnnotatedClass.getClassName() );
680            // mojoDescriptor.setRoleHint( "default" );
681            mojoDescriptor.setImplementation(mojoAnnotatedClass.getClassName());
682            mojoDescriptor.setLanguage("java");
683
684            mojoDescriptor.setV4Api(mojoAnnotatedClass.isV4Api());
685
686            MojoAnnotationContent mojo = mojoAnnotatedClass.getMojo();
687
688            mojoDescriptor.setDescription(mojo.getDescription());
689            mojoDescriptor.setSince(mojo.getSince());
690            mojo.setDeprecated(mojo.getDeprecated());
691
692            mojoDescriptor.setProjectRequired(mojo.requiresProject());
693
694            mojoDescriptor.setRequiresReports(mojo.requiresReports());
695
696            mojoDescriptor.setComponentConfigurator(mojo.configurator());
697
698            mojoDescriptor.setInheritedByDefault(mojo.inheritByDefault());
699
700            mojoDescriptor.setInstantiationStrategy(mojo.instantiationStrategy().id());
701
702            mojoDescriptor.setAggregator(mojo.aggregator());
703            mojoDescriptor.setDependencyResolutionRequired(
704                    mojo.requiresDependencyResolution().id());
705            mojoDescriptor.setDependencyCollectionRequired(
706                    mojo.requiresDependencyCollection().id());
707
708            mojoDescriptor.setDirectInvocationOnly(mojo.requiresDirectInvocation());
709            mojoDescriptor.setDeprecated(mojo.getDeprecated());
710            mojoDescriptor.setThreadSafe(mojo.threadSafe());
711
712            MojoAnnotatedClass mojoAnnotatedClassWithExecute =
713                    findClassWithExecuteAnnotationInParentHierarchy(mojoAnnotatedClass, mojoAnnotatedClasses);
714            if (mojoAnnotatedClassWithExecute != null && mojoAnnotatedClassWithExecute.getExecute() != null) {
715                ExecuteAnnotationContent execute = mojoAnnotatedClassWithExecute.getExecute();
716                mojoDescriptor.setExecuteGoal(execute.goal());
717                mojoDescriptor.setExecuteLifecycle(execute.lifecycle());
718                if (execute.phase() != null) {
719                    mojoDescriptor.setExecutePhase(execute.phase().id());
720                    if (StringUtils.isNotEmpty(execute.customPhase())) {
721                        throw new InvalidPluginDescriptorException(
722                                "@Execute annotation must only use either 'phase' "
723                                        + "or 'customPhase' but not both. Both are used though on "
724                                        + mojoAnnotatedClassWithExecute.getClassName(),
725                                null);
726                    }
727                } else if (StringUtils.isNotEmpty(execute.customPhase())) {
728                    mojoDescriptor.setExecutePhase(execute.customPhase());
729                }
730            }
731
732            mojoDescriptor.setExecutionStrategy(mojo.executionStrategy());
733            // ???
734            // mojoDescriptor.alwaysExecute(mojo.a)
735
736            mojoDescriptor.setGoal(mojo.name());
737            mojoDescriptor.setOnlineRequired(mojo.requiresOnline());
738
739            mojoDescriptor.setPhase(mojo.defaultPhase().id());
740
741            // Parameter annotations
742            Map<String, ParameterAnnotationContent> parameters =
743                    getParametersParentHierarchy(mojoAnnotatedClass, mojoAnnotatedClasses);
744
745            for (ParameterAnnotationContent parameterAnnotationContent : new TreeSet<>(parameters.values())) {
746                org.apache.maven.plugin.descriptor.Parameter parameter =
747                        new org.apache.maven.plugin.descriptor.Parameter();
748                String name = StringUtils.isEmpty(parameterAnnotationContent.name())
749                        ? parameterAnnotationContent.getFieldName()
750                        : parameterAnnotationContent.name();
751                parameter.setName(name);
752                parameter.setAlias(parameterAnnotationContent.alias());
753                parameter.setDefaultValue(parameterAnnotationContent.defaultValue());
754                parameter.setDeprecated(parameterAnnotationContent.getDeprecated());
755                parameter.setDescription(parameterAnnotationContent.getDescription());
756                parameter.setEditable(!parameterAnnotationContent.readonly());
757                String property = parameterAnnotationContent.property();
758                if (StringUtils.contains(property, '$')
759                        || StringUtils.contains(property, '{')
760                        || StringUtils.contains(property, '}')) {
761                    throw new InvalidParameterException(
762                            "Invalid property for parameter '" + parameter.getName() + "', "
763                                    + "forbidden characters ${}: " + property,
764                            null);
765                }
766                parameter.setExpression((property == null || property.isEmpty()) ? "" : "${" + property + "}");
767                StringBuilder type = new StringBuilder(parameterAnnotationContent.getClassName());
768                if (!parameterAnnotationContent.getTypeParameters().isEmpty()) {
769                    type.append(parameterAnnotationContent.getTypeParameters().stream()
770                            .collect(Collectors.joining(", ", "<", ">")));
771                }
772                parameter.setType(type.toString());
773                parameter.setSince(parameterAnnotationContent.getSince());
774                parameter.setRequired(parameterAnnotationContent.required());
775
776                mojoDescriptor.addParameter(parameter);
777            }
778
779            // Component annotations
780            Map<String, ComponentAnnotationContent> components =
781                    getComponentsParentHierarchy(mojoAnnotatedClass, mojoAnnotatedClasses);
782
783            for (ComponentAnnotationContent componentAnnotationContent : new TreeSet<>(components.values())) {
784                org.apache.maven.plugin.descriptor.Parameter parameter =
785                        new org.apache.maven.plugin.descriptor.Parameter();
786                parameter.setName(componentAnnotationContent.getFieldName());
787
788                parameter.setRequirement(new Requirement(
789                        componentAnnotationContent.getRoleClassName(), componentAnnotationContent.hint()));
790                parameter.setDeprecated(componentAnnotationContent.getDeprecated());
791                parameter.setSince(componentAnnotationContent.getSince());
792
793                // same behaviour as JavaJavadocMojoDescriptorExtractor
794                parameter.setEditable(false);
795
796                mojoDescriptor.addParameter(parameter);
797            }
798
799            mojoDescriptor.setPluginDescriptor(pluginDescriptor);
800
801            mojoDescriptors.add(mojoDescriptor);
802        }
803        return mojoDescriptors;
804    }
805
806    protected MojoAnnotatedClass findClassWithExecuteAnnotationInParentHierarchy(
807            MojoAnnotatedClass mojoAnnotatedClass, Map<String, MojoAnnotatedClass> mojoAnnotatedClasses) {
808        if (mojoAnnotatedClass.getExecute() != null) {
809            return mojoAnnotatedClass;
810        }
811        String parentClassName = mojoAnnotatedClass.getParentClassName();
812        if (parentClassName == null || parentClassName.isEmpty()) {
813            return null;
814        }
815        MojoAnnotatedClass parent = mojoAnnotatedClasses.get(parentClassName);
816        if (parent == null) {
817            return null;
818        }
819        return findClassWithExecuteAnnotationInParentHierarchy(parent, mojoAnnotatedClasses);
820    }
821
822    protected Map<String, ParameterAnnotationContent> getParametersParentHierarchy(
823            MojoAnnotatedClass mojoAnnotatedClass, Map<String, MojoAnnotatedClass> mojoAnnotatedClasses) {
824        List<ParameterAnnotationContent> parameterAnnotationContents = new ArrayList<>();
825
826        parameterAnnotationContents =
827                getParametersParent(mojoAnnotatedClass, parameterAnnotationContents, mojoAnnotatedClasses);
828
829        // move to parent first to build the Map
830        Collections.reverse(parameterAnnotationContents);
831
832        Map<String, ParameterAnnotationContent> map = new HashMap<>(parameterAnnotationContents.size());
833
834        for (ParameterAnnotationContent parameterAnnotationContent : parameterAnnotationContents) {
835            map.put(parameterAnnotationContent.getFieldName(), parameterAnnotationContent);
836        }
837        return map;
838    }
839
840    protected List<ParameterAnnotationContent> getParametersParent(
841            MojoAnnotatedClass mojoAnnotatedClass,
842            List<ParameterAnnotationContent> parameterAnnotationContents,
843            Map<String, MojoAnnotatedClass> mojoAnnotatedClasses) {
844        parameterAnnotationContents.addAll(mojoAnnotatedClass.getParameters().values());
845        String parentClassName = mojoAnnotatedClass.getParentClassName();
846        if (parentClassName != null) {
847            MojoAnnotatedClass parent = mojoAnnotatedClasses.get(parentClassName);
848            if (parent != null) {
849                return getParametersParent(parent, parameterAnnotationContents, mojoAnnotatedClasses);
850            }
851        }
852        return parameterAnnotationContents;
853    }
854
855    protected Map<String, ComponentAnnotationContent> getComponentsParentHierarchy(
856            MojoAnnotatedClass mojoAnnotatedClass, Map<String, MojoAnnotatedClass> mojoAnnotatedClasses) {
857        List<ComponentAnnotationContent> componentAnnotationContents = new ArrayList<>();
858
859        componentAnnotationContents =
860                getComponentParent(mojoAnnotatedClass, componentAnnotationContents, mojoAnnotatedClasses);
861
862        // move to parent first to build the Map
863        Collections.reverse(componentAnnotationContents);
864
865        Map<String, ComponentAnnotationContent> map = new HashMap<>(componentAnnotationContents.size());
866
867        for (ComponentAnnotationContent componentAnnotationContent : componentAnnotationContents) {
868            map.put(componentAnnotationContent.getFieldName(), componentAnnotationContent);
869        }
870        return map;
871    }
872
873    protected List<ComponentAnnotationContent> getComponentParent(
874            MojoAnnotatedClass mojoAnnotatedClass,
875            List<ComponentAnnotationContent> componentAnnotationContents,
876            Map<String, MojoAnnotatedClass> mojoAnnotatedClasses) {
877        componentAnnotationContents.addAll(mojoAnnotatedClass.getComponents().values());
878        String parentClassName = mojoAnnotatedClass.getParentClassName();
879        if (parentClassName != null) {
880            MojoAnnotatedClass parent = mojoAnnotatedClasses.get(parentClassName);
881            if (parent != null) {
882                return getComponentParent(parent, componentAnnotationContents, mojoAnnotatedClasses);
883            }
884        }
885        return componentAnnotationContents;
886    }
887
888    protected MavenProject getFromProjectReferences(Artifact artifact, MavenProject project) {
889        if (project.getProjectReferences() == null
890                || project.getProjectReferences().isEmpty()) {
891            return null;
892        }
893        Collection<MavenProject> mavenProjects = project.getProjectReferences().values();
894        for (MavenProject mavenProject : mavenProjects) {
895            if (Objects.equals(mavenProject.getId(), artifact.getId())) {
896                return mavenProject;
897            }
898        }
899        return null;
900    }
901}