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