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