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