View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  package org.apache.maven.tools.plugin.extractor.annotations;
20  
21  import javax.inject.Inject;
22  import javax.inject.Named;
23  import javax.inject.Singleton;
24  
25  import java.io.File;
26  import java.io.IOException;
27  import java.nio.charset.Charset;
28  import java.nio.charset.StandardCharsets;
29  import java.util.ArrayList;
30  import java.util.Arrays;
31  import java.util.Collection;
32  import java.util.Collections;
33  import java.util.Comparator;
34  import java.util.HashMap;
35  import java.util.HashSet;
36  import java.util.List;
37  import java.util.Map;
38  import java.util.Objects;
39  import java.util.Optional;
40  import java.util.Set;
41  import java.util.TreeMap;
42  import java.util.TreeSet;
43  import java.util.stream.Collectors;
44  
45  import com.github.javaparser.ast.Node;
46  import com.github.javaparser.ast.body.ClassOrInterfaceDeclaration;
47  import com.github.javaparser.ast.body.FieldDeclaration;
48  import com.github.javaparser.ast.body.MethodDeclaration;
49  import com.github.javaparser.ast.body.TypeDeclaration;
50  import com.github.javaparser.ast.nodeTypes.NodeWithJavadoc;
51  import com.github.javaparser.ast.type.ClassOrInterfaceType;
52  import com.github.javaparser.javadoc.Javadoc;
53  import com.github.javaparser.javadoc.JavadocBlockTag;
54  import com.github.javaparser.resolution.UnsolvedSymbolException;
55  import org.apache.maven.artifact.Artifact;
56  import org.apache.maven.artifact.versioning.ComparableVersion;
57  import org.apache.maven.plugin.descriptor.InvalidParameterException;
58  import org.apache.maven.plugin.descriptor.InvalidPluginDescriptorException;
59  import org.apache.maven.plugin.descriptor.MojoDescriptor;
60  import org.apache.maven.plugin.descriptor.PluginDescriptor;
61  import org.apache.maven.plugin.descriptor.Requirement;
62  import org.apache.maven.project.MavenProject;
63  import org.apache.maven.tools.plugin.ExtendedMojoDescriptor;
64  import org.apache.maven.tools.plugin.PluginToolsRequest;
65  import org.apache.maven.tools.plugin.extractor.ExtractionException;
66  import org.apache.maven.tools.plugin.extractor.GroupKey;
67  import org.apache.maven.tools.plugin.extractor.MojoDescriptorExtractor;
68  import org.apache.maven.tools.plugin.extractor.annotations.converter.ConverterContext;
69  import org.apache.maven.tools.plugin.extractor.annotations.converter.JavaClassConverterContext;
70  import org.apache.maven.tools.plugin.extractor.annotations.converter.JavadocBlockTagsToXhtmlConverter;
71  import org.apache.maven.tools.plugin.extractor.annotations.converter.JavadocInlineTagsToXhtmlConverter;
72  import org.apache.maven.tools.plugin.extractor.annotations.datamodel.AfterAnnotationContent;
73  import org.apache.maven.tools.plugin.extractor.annotations.datamodel.ComponentAnnotationContent;
74  import org.apache.maven.tools.plugin.extractor.annotations.datamodel.ExecuteAnnotationContent;
75  import org.apache.maven.tools.plugin.extractor.annotations.datamodel.MojoAnnotationContent;
76  import org.apache.maven.tools.plugin.extractor.annotations.datamodel.ParameterAnnotationContent;
77  import org.apache.maven.tools.plugin.extractor.annotations.scanner.MojoAnnotatedClass;
78  import org.apache.maven.tools.plugin.extractor.annotations.scanner.MojoAnnotationsScanner;
79  import org.apache.maven.tools.plugin.extractor.annotations.scanner.MojoAnnotationsScannerRequest;
80  import org.apache.maven.tools.plugin.javadoc.JavadocLinkGenerator;
81  import org.codehaus.plexus.archiver.ArchiverException;
82  import org.codehaus.plexus.archiver.UnArchiver;
83  import org.codehaus.plexus.archiver.manager.ArchiverManager;
84  import org.codehaus.plexus.archiver.manager.NoSuchArchiverException;
85  import org.codehaus.plexus.util.StringUtils;
86  import org.eclipse.aether.RepositorySystem;
87  import org.eclipse.aether.artifact.DefaultArtifact;
88  import org.eclipse.aether.resolution.ArtifactRequest;
89  import org.eclipse.aether.resolution.ArtifactResolutionException;
90  import org.eclipse.aether.resolution.ArtifactResult;
91  import org.objectweb.asm.Opcodes;
92  import org.slf4j.Logger;
93  import org.slf4j.LoggerFactory;
94  
95  /**
96   * JavaMojoDescriptorExtractor, a MojoDescriptor extractor to read descriptors from java classes with annotations.
97   * Notice that source files are also parsed to get description, since and deprecation information.
98   *
99   * @author Olivier Lamy
100  * @since 3.0
101  */
102 @Named(JavaAnnotationsMojoDescriptorExtractor.NAME)
103 @Singleton
104 public class JavaAnnotationsMojoDescriptorExtractor implements MojoDescriptorExtractor {
105     private static final Logger LOGGER = LoggerFactory.getLogger(JavaAnnotationsMojoDescriptorExtractor.class);
106     public static final String NAME = "java-annotations";
107 
108     private static final GroupKey GROUP_KEY = new GroupKey(GroupKey.JAVA_GROUP, 100);
109 
110     /**
111      *
112      * @see <a href="https://docs.oracle.com/javase/specs/jvms/se19/html/jvms-4.html#jvms-4.1">JVMS 4.1</a>
113      */
114     private static final Map<Integer, String> CLASS_VERSION_TO_JAVA_STRING;
115 
116     static {
117         CLASS_VERSION_TO_JAVA_STRING = new HashMap<>();
118         CLASS_VERSION_TO_JAVA_STRING.put(Opcodes.V1_1, "1.1");
119         CLASS_VERSION_TO_JAVA_STRING.put(Opcodes.V1_2, "1.2");
120         CLASS_VERSION_TO_JAVA_STRING.put(Opcodes.V1_3, "1.3");
121         CLASS_VERSION_TO_JAVA_STRING.put(Opcodes.V1_4, "1.4");
122         CLASS_VERSION_TO_JAVA_STRING.put(Opcodes.V1_5, "1.5");
123         CLASS_VERSION_TO_JAVA_STRING.put(Opcodes.V1_6, "1.6");
124         CLASS_VERSION_TO_JAVA_STRING.put(Opcodes.V1_7, "1.7");
125         CLASS_VERSION_TO_JAVA_STRING.put(Opcodes.V1_8, "1.8");
126         CLASS_VERSION_TO_JAVA_STRING.put(Opcodes.V9, "9");
127         CLASS_VERSION_TO_JAVA_STRING.put(Opcodes.V10, "10");
128         CLASS_VERSION_TO_JAVA_STRING.put(Opcodes.V11, "11");
129         CLASS_VERSION_TO_JAVA_STRING.put(Opcodes.V12, "12");
130         CLASS_VERSION_TO_JAVA_STRING.put(Opcodes.V13, "13");
131         CLASS_VERSION_TO_JAVA_STRING.put(Opcodes.V14, "14");
132         CLASS_VERSION_TO_JAVA_STRING.put(Opcodes.V15, "15");
133         CLASS_VERSION_TO_JAVA_STRING.put(Opcodes.V16, "16");
134         CLASS_VERSION_TO_JAVA_STRING.put(Opcodes.V17, "17");
135         CLASS_VERSION_TO_JAVA_STRING.put(Opcodes.V18, "18");
136         CLASS_VERSION_TO_JAVA_STRING.put(Opcodes.V19, "19");
137         CLASS_VERSION_TO_JAVA_STRING.put(Opcodes.V20, "20");
138         CLASS_VERSION_TO_JAVA_STRING.put(Opcodes.V21, "21");
139         CLASS_VERSION_TO_JAVA_STRING.put(Opcodes.V22, "22");
140         CLASS_VERSION_TO_JAVA_STRING.put(Opcodes.V23, "23");
141         CLASS_VERSION_TO_JAVA_STRING.put(Opcodes.V24, "24");
142         CLASS_VERSION_TO_JAVA_STRING.put(Opcodes.V25, "25");
143         CLASS_VERSION_TO_JAVA_STRING.put(Opcodes.V26, "26");
144     }
145 
146     @Inject
147     MojoAnnotationsScanner mojoAnnotationsScanner;
148 
149     @Inject
150     private RepositorySystem repositorySystem;
151 
152     @Inject
153     private ArchiverManager archiverManager;
154 
155     @Inject
156     private JavadocInlineTagsToXhtmlConverter javadocInlineTagsToHtmlConverter;
157 
158     @Inject
159     private JavadocBlockTagsToXhtmlConverter javadocBlockTagsToHtmlConverter;
160 
161     @Override
162     public String getName() {
163         return NAME;
164     }
165 
166     @Override
167     public boolean isDeprecated() {
168         return false; // this is the "current way" to write Java Mojos
169     }
170 
171     @Override
172     public GroupKey getGroupKey() {
173         return GROUP_KEY;
174     }
175 
176     /**
177      * Compares class file format versions.
178      * @see <a href="https://docs.oracle.com/javase/specs/jvms/se19/html/jvms-4.html#jvms-4.1">JVMS 4.1</a>
179      *
180      */
181     @SuppressWarnings("checkstyle:magicnumber")
182     static final class ClassVersionComparator implements Comparator<Integer> {
183         @Override
184         public int compare(Integer classVersion1, Integer classVersion2) {
185             // first compare major version (
186             int result = Integer.compare(classVersion1 & 0x00FF, classVersion2 & 0x00FF);
187             if (result == 0) {
188                 // compare minor version if major is equal
189                 result = Integer.compare(classVersion1, classVersion2);
190             }
191             return result;
192         }
193     }
194 
195     @Override
196     public List<MojoDescriptor> execute(PluginToolsRequest request)
197             throws ExtractionException, InvalidPluginDescriptorException {
198         Map<String, MojoAnnotatedClass> mojoAnnotatedClasses = scanAnnotations(request);
199 
200         Optional<Integer> maxClassVersion = mojoAnnotatedClasses.values().stream()
201                 .map(MojoAnnotatedClass::getClassVersion)
202                 .max(new ClassVersionComparator());
203         if (maxClassVersion.isPresent()) {
204             String requiredJavaVersion = CLASS_VERSION_TO_JAVA_STRING.get(maxClassVersion.get());
205             if (StringUtils.isBlank(request.getRequiredJavaVersion())
206                     || new ComparableVersion(request.getRequiredJavaVersion())
207                                     .compareTo(new ComparableVersion(requiredJavaVersion))
208                             < 0) {
209                 request.setRequiredJavaVersion(requiredJavaVersion);
210             }
211         }
212         final JavadocLinkGenerator linkGenerator;
213         if (request.getInternalJavadocBaseUrl() != null
214                 || (request.getExternalJavadocBaseUrls() != null
215                         && !request.getExternalJavadocBaseUrls().isEmpty())) {
216             linkGenerator = new JavadocLinkGenerator(
217                     request.getInternalJavadocBaseUrl(),
218                     request.getInternalJavadocVersion(),
219                     request.getExternalJavadocBaseUrls(),
220                     request.getSettings());
221         } else {
222             linkGenerator = null;
223         }
224 
225         // parse() allocates the class loader backing the model, so it must run inside the
226         // try-with-resources: a parse failure would otherwise leak it together with every
227         // dependency and reactor jar it holds open.
228         try (JavaSourceModel sourceModel = scanJavadoc(request, mojoAnnotatedClasses.values())) {
229             sourceModel.parse();
230             Map<String, TypeDeclaration<?>> javaClassesMap = discoverClasses(sourceModel);
231             populateDataFromJavadoc(sourceModel, mojoAnnotatedClasses, javaClassesMap, linkGenerator);
232         } catch (IOException e) {
233             throw new ExtractionException("Could not parse Java sources: " + e.getMessage(), e);
234         }
235 
236         return toMojoDescriptors(mojoAnnotatedClasses, request.getPluginDescriptor());
237     }
238 
239     private Map<String, MojoAnnotatedClass> scanAnnotations(PluginToolsRequest request) throws ExtractionException {
240         MojoAnnotationsScannerRequest mojoAnnotationsScannerRequest = new MojoAnnotationsScannerRequest();
241 
242         File output = new File(request.getProject().getBuild().getOutputDirectory());
243         mojoAnnotationsScannerRequest.setClassesDirectories(Arrays.asList(output));
244 
245         mojoAnnotationsScannerRequest.setDependencies(request.getDependencies());
246 
247         mojoAnnotationsScannerRequest.setProject(request.getProject());
248 
249         Map<String, MojoAnnotatedClass> result = mojoAnnotationsScanner.scan(mojoAnnotationsScannerRequest);
250         request.setUsedMavenApiVersion(mojoAnnotationsScannerRequest.getMavenApiVersion());
251         return result;
252     }
253 
254     private JavaSourceModel scanJavadoc(PluginToolsRequest request, Collection<MojoAnnotatedClass> mojoAnnotatedClasses)
255             throws ExtractionException, IOException {
256         // found artifact from reactors to scan sources
257         // we currently only scan sources from reactors
258         List<MavenProject> mavenProjects = new ArrayList<>();
259 
260         // if we need to scan sources from external artifacts
261         Set<Artifact> externalArtifacts = new HashSet<>();
262 
263         Charset encoding =
264                 request.getEncoding() == null ? StandardCharsets.UTF_8 : Charset.forName(request.getEncoding());
265         JavaSourceModel sourceModel = new JavaSourceModel(encoding);
266         extendJavaSourceModel(request, sourceModel, request.getProject());
267 
268         for (MojoAnnotatedClass mojoAnnotatedClass : mojoAnnotatedClasses) {
269             if (Objects.equals(
270                     mojoAnnotatedClass.getArtifact().getArtifactId(),
271                     request.getProject().getArtifact().getArtifactId())) {
272                 continue;
273             }
274 
275             if (!isMojoAnnnotatedClassCandidate(mojoAnnotatedClass)) {
276                 // we don't scan sources for classes without mojo annotations
277                 continue;
278             }
279 
280             MavenProject mavenProject =
281                     getFromProjectReferences(mojoAnnotatedClass.getArtifact(), request.getProject());
282 
283             if (mavenProject != null) {
284                 mavenProjects.add(mavenProject);
285             } else {
286                 externalArtifacts.add(mojoAnnotatedClass.getArtifact());
287             }
288         }
289 
290         // try to get artifact with sources classifier, extract somewhere then scan for @since, @deprecated
291         for (Artifact artifact : externalArtifacts) {
292             // parameter for test-sources too ?? olamy I need that for it test only
293             if (StringUtils.equalsIgnoreCase("tests", artifact.getClassifier())) {
294                 extendJavaSourceModelWithSourcesJar(sourceModel, artifact, request, "test-sources");
295             } else {
296                 extendJavaSourceModelWithSourcesJar(sourceModel, artifact, request, "sources");
297             }
298         }
299 
300         for (MavenProject mavenProject : mavenProjects) {
301             extendJavaSourceModel(request, sourceModel, mavenProject);
302         }
303 
304         return sourceModel;
305     }
306 
307     private boolean isMojoAnnnotatedClassCandidate(MojoAnnotatedClass mojoAnnotatedClass) {
308         return mojoAnnotatedClass != null && mojoAnnotatedClass.hasAnnotations();
309     }
310 
311     /**
312      * from sources scan to get @since and @deprecated and description of classes and fields.
313      */
314     protected void populateDataFromJavadoc(
315             JavaSourceModel sourceModel,
316             Map<String, MojoAnnotatedClass> mojoAnnotatedClasses,
317             Map<String, TypeDeclaration<?>> javaClassesMap,
318             JavadocLinkGenerator linkGenerator) {
319         for (Map.Entry<String, MojoAnnotatedClass> entry : mojoAnnotatedClasses.entrySet()) {
320             TypeDeclaration<?> javaClass = javaClassesMap.get(entry.getKey());
321             if (javaClass == null) {
322                 continue;
323             }
324             MojoAnnotationContent mojo = entry.getValue().getMojo();
325             if (mojo != null) {
326                 JavaClassConverterContext context = new JavaClassConverterContext(
327                         javaClass, sourceModel, mojoAnnotatedClasses, linkGenerator, lineNumber(javaClass));
328                 mojo.setDescription(getDescriptionFromElement(javaClass, context));
329                 findInClassHierarchy(sourceModel, javaClass, "since")
330                         .ifPresent(tag -> mojo.setSince(getRawValueFromTaglet(tag, context)));
331                 findInClassHierarchy(sourceModel, javaClass, "deprecated")
332                         .ifPresent(tag -> mojo.setDeprecated(getRawValueFromTaglet(tag, context)));
333             }
334 
335             Map<String, SourceMember> fields = extractFields(sourceModel, javaClass);
336             Map<String, SourceMember> methods = extractMethods(sourceModel, javaClass);
337             Map<String, ParameterAnnotationContent> parameters =
338                     new TreeMap<>(getParametersParentHierarchy(entry.getValue(), mojoAnnotatedClasses));
339             for (Map.Entry<String, ParameterAnnotationContent> parameter : parameters.entrySet()) {
340                 SourceMember member = parameter.getValue().isAnnotationOnMethod()
341                         ? methods.get(parameter.getKey())
342                         : fields.get(parameter.getKey());
343                 if (member != null) {
344                     populateMemberJavadoc(
345                             javaClass, member, parameter.getValue(), sourceModel, mojoAnnotatedClasses, linkGenerator);
346                 }
347             }
348 
349             for (Map.Entry<String, ComponentAnnotationContent> component :
350                     entry.getValue().getComponents().entrySet()) {
351                 SourceMember member = fields.get(component.getKey());
352                 if (member != null) {
353                     populateMemberJavadoc(
354                             javaClass, member, component.getValue(), sourceModel, mojoAnnotatedClasses, linkGenerator);
355                 }
356             }
357         }
358     }
359 
360     private void populateMemberJavadoc(
361             TypeDeclaration<?> mojoClass,
362             SourceMember member,
363             Object annotation,
364             JavaSourceModel sourceModel,
365             Map<String, MojoAnnotatedClass> mojoAnnotatedClasses,
366             JavadocLinkGenerator linkGenerator) {
367         JavaClassConverterContext context = new JavaClassConverterContext(
368                 mojoClass,
369                 member.declaringClass,
370                 member.element,
371                 sourceModel,
372                 mojoAnnotatedClasses,
373                 linkGenerator,
374                 lineNumber(member.element));
375         String description = getDescriptionFromElement(member.javadocElement, context);
376         Optional<JavadocBlockTag> deprecated = getTag(member.javadocElement, "deprecated");
377         Optional<JavadocBlockTag> since = getTag(member.javadocElement, "since");
378         if (annotation instanceof ParameterAnnotationContent) {
379             ParameterAnnotationContent parameter = (ParameterAnnotationContent) annotation;
380             parameter.setDescription(description);
381             deprecated.ifPresent(tag -> parameter.setDeprecated(getRawValueFromTaglet(tag, context)));
382             since.ifPresent(tag -> parameter.setSince(getRawValueFromTaglet(tag, context)));
383         } else {
384             ComponentAnnotationContent component = (ComponentAnnotationContent) annotation;
385             component.setDescription(description);
386             deprecated.ifPresent(tag -> component.setDeprecated(getRawValueFromTaglet(tag, context)));
387             since.ifPresent(tag -> component.setSince(getRawValueFromTaglet(tag, context)));
388         }
389     }
390 
391     String getDescriptionFromElement(NodeWithJavadoc<?> element, JavaClassConverterContext context) {
392         Optional<Javadoc> javadoc = element.getJavadoc();
393         if (!javadoc.isPresent()) {
394             return null;
395         }
396         StringBuilder description = new StringBuilder(javadocInlineTagsToHtmlConverter.convert(
397                 javadoc.get().getDescription().toText(), context));
398         javadoc.get().getBlockTags().stream()
399                 .filter(tag -> "see".equals(tag.getTagName()))
400                 .forEach(tag -> description.append(javadocBlockTagsToHtmlConverter.convert(
401                         tag.getTagName(), tag.getContent().toText(), context)));
402         return description.toString();
403     }
404 
405     String getRawValueFromTaglet(JavadocBlockTag tag, ConverterContext context) {
406         return javadocInlineTagsToHtmlConverter.convert(tag.getContent().toText(), context);
407     }
408 
409     private Optional<JavadocBlockTag> findInClassHierarchy(
410             JavaSourceModel sourceModel, TypeDeclaration<?> javaClass, String tagName) {
411         Optional<JavadocBlockTag> tag = getTag(javaClass, tagName);
412         if (tag.isPresent()) {
413             return tag;
414         }
415         return getSuperSourceClass(sourceModel, javaClass)
416                 .flatMap(parent -> findInClassHierarchy(sourceModel, parent, tagName));
417     }
418 
419     private static Optional<JavadocBlockTag> getTag(NodeWithJavadoc<?> element, String tagName) {
420         return element.getJavadoc()
421                 .flatMap(javadoc -> javadoc.getBlockTags().stream()
422                         .filter(tag -> tagName.equals(tag.getTagName()))
423                         .findFirst());
424     }
425 
426     private Map<String, SourceMember> extractFields(JavaSourceModel sourceModel, TypeDeclaration<?> javaClass) {
427         Map<String, SourceMember> result = getSuperSourceClass(sourceModel, javaClass)
428                 .map(parent -> extractFields(sourceModel, parent))
429                 .orElseGet(TreeMap::new);
430         for (FieldDeclaration field : javaClass.getFields()) {
431             field.getVariables()
432                     .forEach(variable ->
433                             result.put(variable.getNameAsString(), new SourceMember(javaClass, field, field)));
434         }
435         return result;
436     }
437 
438     private Map<String, SourceMember> extractMethods(JavaSourceModel sourceModel, TypeDeclaration<?> javaClass) {
439         Map<String, SourceMember> result = getSuperSourceClass(sourceModel, javaClass)
440                 .map(parent -> extractMethods(sourceModel, parent))
441                 .orElseGet(TreeMap::new);
442         for (MethodDeclaration method : javaClass.getMethods()) {
443             if (isPublicSetterMethod(method)) {
444                 result.put(
445                         StringUtils.lowercaseFirstLetter(
446                                 method.getNameAsString().substring(3)),
447                         new SourceMember(javaClass, method, method));
448             }
449         }
450         return result;
451     }
452 
453     private static boolean isPublicSetterMethod(MethodDeclaration method) {
454         return method.isPublic()
455                 && !method.isStatic()
456                 && method.getNameAsString().length() > 3
457                 && (method.getNameAsString().startsWith("add")
458                         || method.getNameAsString().startsWith("set"))
459                 && method.getType().isVoidType()
460                 && method.getParameters().size() == 1;
461     }
462 
463     private Optional<TypeDeclaration<?>> getSuperSourceClass(
464             JavaSourceModel sourceModel, TypeDeclaration<?> javaClass) {
465         if (!(javaClass instanceof ClassOrInterfaceDeclaration)
466                 || ((ClassOrInterfaceDeclaration) javaClass).isInterface()) {
467             return Optional.empty();
468         }
469         Optional<ClassOrInterfaceType> superClass = ((ClassOrInterfaceDeclaration) javaClass)
470                 .getExtendedTypes().stream().findFirst();
471         if (!superClass.isPresent()) {
472             return Optional.empty();
473         }
474         try {
475             return superClass
476                     .get()
477                     .resolve()
478                     .asReferenceType()
479                     .getTypeDeclaration()
480                     .flatMap(sourceModel::getType);
481         } catch (UnsolvedSymbolException e) {
482             // Sources artifacts need not contain the sources of every superclass. In that case there is no
483             // source Javadoc to inherit, just as when no sources artifact is available at all.
484             LOGGER.debug("Could not resolve source superclass {}", superClass.get(), e);
485             return Optional.empty();
486         }
487     }
488 
489     protected Map<String, TypeDeclaration<?>> discoverClasses(JavaSourceModel sourceModel) {
490         Map<String, TypeDeclaration<?>> result = new HashMap<>();
491         for (TypeDeclaration<?> type : sourceModel.getTypes()) {
492             type.getFullyQualifiedName().ifPresent(name -> {
493                 result.put(name, type);
494                 // The annotation scanner keys classes by their binary name ("pkg.Outer$Inner"), while
495                 // JavaParser reports the canonical one ("pkg.Outer.Inner"). Index both so that mojos
496                 // declared as nested classes still find their Javadoc.
497                 binaryName(type).ifPresent(binary -> result.put(binary, type));
498             });
499         }
500         return result;
501     }
502 
503     private static Optional<String> binaryName(TypeDeclaration<?> type) {
504         return type.getFullyQualifiedName().flatMap(fullyQualifiedName -> {
505             String canonicalNestedName = type.getNameAsString();
506             Node current = type;
507             while (current.getParentNode().isPresent()
508                     && current.getParentNode().get() instanceof TypeDeclaration) {
509                 TypeDeclaration<?> parent =
510                         (TypeDeclaration<?>) current.getParentNode().get();
511                 canonicalNestedName = parent.getNameAsString() + "." + canonicalNestedName;
512                 current = parent;
513             }
514             if (!canonicalNestedName.contains(".")) {
515                 // top level type: binary and canonical names are identical
516                 return Optional.empty();
517             }
518             String binaryNestedName = canonicalNestedName.replace('.', '$');
519             return Optional.of(
520                     fullyQualifiedName.substring(0, fullyQualifiedName.length() - canonicalNestedName.length())
521                             + binaryNestedName);
522         });
523     }
524 
525     protected void extendJavaSourceModelWithSourcesJar(
526             JavaSourceModel sourceModel, Artifact artifact, PluginToolsRequest request, String classifier)
527             throws ExtractionException, IOException {
528         try {
529             org.eclipse.aether.artifact.Artifact sourcesArtifact = new DefaultArtifact(
530                     artifact.getGroupId(),
531                     artifact.getArtifactId(),
532                     classifier,
533                     artifact.getArtifactHandler().getExtension(),
534                     artifact.getVersion());
535 
536             ArtifactRequest resolveRequest =
537                     new ArtifactRequest(sourcesArtifact, request.getProject().getRemoteProjectRepositories(), null);
538             try {
539                 ArtifactResult result = repositorySystem.resolveArtifact(request.getRepoSession(), resolveRequest);
540                 sourcesArtifact = result.getArtifact();
541             } catch (ArtifactResolutionException e) {
542                 String message = "Unable to get sources artifact for " + artifact.getId()
543                         + ". Some javadoc tags (@since, @deprecated and comments) won't be used";
544                 if (LOGGER.isDebugEnabled()) {
545                     LOGGER.warn(message, e);
546                 } else {
547                     LOGGER.warn(message);
548                 }
549                 return;
550             }
551 
552             if (sourcesArtifact.getFile() == null || !sourcesArtifact.getFile().exists()) {
553                 // could not get artifact sources
554                 return;
555             }
556 
557             if (sourcesArtifact.getFile().isFile()) {
558                 // extract sources to target/maven-plugin-plugin-sources/${groupId}/${artifact}/sources
559                 File extractDirectory = new File(
560                         request.getProject().getBuild().getDirectory(),
561                         "maven-plugin-plugin-sources/" + sourcesArtifact.getGroupId() + "/"
562                                 + sourcesArtifact.getArtifactId() + "/" + sourcesArtifact.getVersion()
563                                 + "/" + sourcesArtifact.getClassifier());
564                 extractDirectory.mkdirs();
565 
566                 UnArchiver unArchiver = archiverManager.getUnArchiver("jar");
567                 unArchiver.setSourceFile(sourcesArtifact.getFile());
568                 unArchiver.setDestDirectory(extractDirectory);
569                 unArchiver.extract();
570 
571                 extendJavaSourceModel(sourceModel, Arrays.asList(extractDirectory), request.getDependencies());
572             } else if (sourcesArtifact.getFile().isDirectory()) {
573                 extendJavaSourceModel(sourceModel, Arrays.asList(sourcesArtifact.getFile()), request.getDependencies());
574             }
575         } catch (ArchiverException | NoSuchArchiverException e) {
576             throw new ExtractionException(e.getMessage(), e);
577         }
578     }
579 
580     private void extendJavaSourceModel(
581             PluginToolsRequest request, JavaSourceModel sourceModel, final MavenProject project) throws IOException {
582         List<File> sources = new ArrayList<>();
583 
584         for (String source : project.getCompileSourceRoots()) {
585             File sourceFile = new File(source);
586 
587             // Allow users to exclude certain paths such as generated sources from being scanned, in the case that
588             // this may be problematic for them (e.g. using obscure unsupported syntax by the parser, comments that
589             // cannot be controlled, etc.)
590             if (!request.isExcludedScanDirectory(sourceFile)) {
591                 sources.add(sourceFile);
592             }
593         }
594 
595         // TODO be more dynamic
596         File generatedPlugin = new File(project.getBasedir(), "target/generated-sources/plugin");
597         if (!project.getCompileSourceRoots().contains(generatedPlugin.getAbsolutePath()) && generatedPlugin.exists()) {
598             sources.add(generatedPlugin);
599         }
600 
601         extendJavaSourceModel(sourceModel, sources, project.getArtifacts());
602     }
603 
604     private void extendJavaSourceModel(
605             JavaSourceModel sourceModel, List<File> sourceDirectories, Set<Artifact> artifacts) throws IOException {
606         for (Artifact artifact : artifacts) {
607             sourceModel.addClassPathEntry(artifact.getFile());
608         }
609         for (File source : sourceDirectories) {
610             sourceModel.addSourceDirectory(source);
611         }
612     }
613 
614     private static int lineNumber(Node node) {
615         return node.getBegin().map(position -> position.line).orElse(0);
616     }
617 
618     private static final class SourceMember {
619         private final TypeDeclaration<?> declaringClass;
620         private final Node element;
621         private final NodeWithJavadoc<?> javadocElement;
622 
623         private SourceMember(TypeDeclaration<?> declaringClass, Node element, NodeWithJavadoc<?> javadocElement) {
624             this.declaringClass = declaringClass;
625             this.element = element;
626             this.javadocElement = javadocElement;
627         }
628     }
629 
630     private List<MojoDescriptor> toMojoDescriptors(
631             Map<String, MojoAnnotatedClass> mojoAnnotatedClasses, PluginDescriptor pluginDescriptor)
632             throws InvalidPluginDescriptorException {
633         List<MojoDescriptor> mojoDescriptors = new ArrayList<>(mojoAnnotatedClasses.size());
634         for (MojoAnnotatedClass mojoAnnotatedClass : mojoAnnotatedClasses.values()) {
635             // no mojo so skip it
636             if (mojoAnnotatedClass.getMojo() == null) {
637                 continue;
638             }
639 
640             ExtendedMojoDescriptor mojoDescriptor = new ExtendedMojoDescriptor(true);
641 
642             // mojoDescriptor.setRole( mojoAnnotatedClass.getClassName() );
643             // mojoDescriptor.setRoleHint( "default" );
644             mojoDescriptor.setImplementation(mojoAnnotatedClass.getClassName());
645             mojoDescriptor.setLanguage("java");
646 
647             mojoDescriptor.setV4Api(mojoAnnotatedClass.isV4Api());
648 
649             MojoAnnotationContent mojo = mojoAnnotatedClass.getMojo();
650 
651             mojoDescriptor.setDescription(mojo.getDescription());
652             mojoDescriptor.setSince(mojo.getSince());
653             mojo.setDeprecated(mojo.getDeprecated());
654 
655             mojoDescriptor.setProjectRequired(mojo.requiresProject());
656 
657             mojoDescriptor.setRequiresReports(mojo.requiresReports());
658 
659             mojoDescriptor.setComponentConfigurator(mojo.configurator());
660 
661             mojoDescriptor.setInheritedByDefault(mojo.inheritByDefault());
662 
663             mojoDescriptor.setInstantiationStrategy(mojo.instantiationStrategy().id());
664 
665             mojoDescriptor.setAggregator(mojo.aggregator());
666             mojoDescriptor.setDependencyResolutionRequired(
667                     mojo.requiresDependencyResolution().id());
668             mojoDescriptor.setDependencyCollectionRequired(
669                     mojo.requiresDependencyCollection().id());
670 
671             mojoDescriptor.setDirectInvocationOnly(mojo.requiresDirectInvocation());
672             mojoDescriptor.setDeprecated(mojo.getDeprecated());
673             mojoDescriptor.setThreadSafe(mojo.threadSafe());
674 
675             MojoAnnotatedClass mojoAnnotatedClassWithExecute =
676                     findClassWithExecuteAnnotationInParentHierarchy(mojoAnnotatedClass, mojoAnnotatedClasses);
677             if (mojoAnnotatedClassWithExecute != null && mojoAnnotatedClassWithExecute.getExecute() != null) {
678                 ExecuteAnnotationContent execute = mojoAnnotatedClassWithExecute.getExecute();
679                 mojoDescriptor.setExecuteGoal(execute.goal());
680                 mojoDescriptor.setExecuteLifecycle(execute.lifecycle());
681                 if (execute.phase() != null) {
682                     mojoDescriptor.setExecutePhase(execute.phase().id());
683                     if (StringUtils.isNotEmpty(execute.customPhase())) {
684                         throw new InvalidPluginDescriptorException(
685                                 "@Execute annotation must only use either 'phase' "
686                                         + "or 'customPhase' but not both. Both are used though on "
687                                         + mojoAnnotatedClassWithExecute.getClassName(),
688                                 null);
689                     }
690                 } else if (StringUtils.isNotEmpty(execute.customPhase())) {
691                     mojoDescriptor.setExecutePhase(execute.customPhase());
692                 }
693             }
694 
695             // @After annotations — collect from class hierarchy
696             List<AfterAnnotationContent> afterAnnotations =
697                     getAfterAnnotationsFromHierarchy(mojoAnnotatedClass, mojoAnnotatedClasses);
698             for (AfterAnnotationContent after : afterAnnotations) {
699                 mojoDescriptor.addAfterLink(
700                         new ExtendedMojoDescriptor.AfterLink(after.phase(), after.type(), after.scope()));
701             }
702 
703             mojoDescriptor.setExecutionStrategy(mojo.executionStrategy());
704             // ???
705             // mojoDescriptor.alwaysExecute(mojo.a)
706 
707             mojoDescriptor.setGoal(mojo.name());
708             mojoDescriptor.setOnlineRequired(mojo.requiresOnline());
709 
710             mojoDescriptor.setPhase(mojo.defaultPhase().id());
711 
712             // Parameter annotations
713             Map<String, ParameterAnnotationContent> parameters =
714                     getParametersParentHierarchy(mojoAnnotatedClass, mojoAnnotatedClasses);
715 
716             for (ParameterAnnotationContent parameterAnnotationContent : new TreeSet<>(parameters.values())) {
717                 org.apache.maven.plugin.descriptor.Parameter parameter =
718                         new org.apache.maven.plugin.descriptor.Parameter();
719                 String name = StringUtils.isEmpty(parameterAnnotationContent.name())
720                         ? parameterAnnotationContent.getFieldName()
721                         : parameterAnnotationContent.name();
722                 parameter.setName(name);
723                 parameter.setAlias(parameterAnnotationContent.alias());
724                 parameter.setDefaultValue(parameterAnnotationContent.defaultValue());
725                 parameter.setDeprecated(parameterAnnotationContent.getDeprecated());
726                 parameter.setDescription(parameterAnnotationContent.getDescription());
727                 parameter.setEditable(!parameterAnnotationContent.readonly());
728                 String property = parameterAnnotationContent.property();
729                 if (StringUtils.contains(property, '$')
730                         || StringUtils.contains(property, '{')
731                         || StringUtils.contains(property, '}')) {
732                     throw new InvalidParameterException(
733                             "Invalid property for parameter '" + parameter.getName() + "', "
734                                     + "forbidden characters ${}: " + property,
735                             null);
736                 }
737                 parameter.setExpression((property == null || property.isEmpty()) ? "" : "${" + property + "}");
738                 StringBuilder type = new StringBuilder(parameterAnnotationContent.getClassName());
739                 if (!parameterAnnotationContent.getTypeParameters().isEmpty()) {
740                     type.append(parameterAnnotationContent.getTypeParameters().stream()
741                             .collect(Collectors.joining(", ", "<", ">")));
742                 }
743                 parameter.setType(type.toString());
744                 parameter.setSince(parameterAnnotationContent.getSince());
745                 parameter.setRequired(parameterAnnotationContent.required());
746 
747                 mojoDescriptor.addParameter(parameter);
748             }
749 
750             // Component annotations
751             Map<String, ComponentAnnotationContent> components =
752                     getComponentsParentHierarchy(mojoAnnotatedClass, mojoAnnotatedClasses);
753 
754             for (ComponentAnnotationContent componentAnnotationContent : new TreeSet<>(components.values())) {
755                 org.apache.maven.plugin.descriptor.Parameter parameter =
756                         new org.apache.maven.plugin.descriptor.Parameter();
757                 parameter.setName(componentAnnotationContent.getFieldName());
758 
759                 parameter.setRequirement(new Requirement(
760                         componentAnnotationContent.getRoleClassName(), componentAnnotationContent.hint()));
761                 parameter.setDeprecated(componentAnnotationContent.getDeprecated());
762                 parameter.setSince(componentAnnotationContent.getSince());
763 
764                 // same behaviour as JavaJavadocMojoDescriptorExtractor
765                 parameter.setEditable(false);
766 
767                 mojoDescriptor.addParameter(parameter);
768             }
769 
770             mojoDescriptor.setPluginDescriptor(pluginDescriptor);
771 
772             mojoDescriptors.add(mojoDescriptor);
773         }
774         return mojoDescriptors;
775     }
776 
777     protected MojoAnnotatedClass findClassWithExecuteAnnotationInParentHierarchy(
778             MojoAnnotatedClass mojoAnnotatedClass, Map<String, MojoAnnotatedClass> mojoAnnotatedClasses) {
779         if (mojoAnnotatedClass.getExecute() != null) {
780             return mojoAnnotatedClass;
781         }
782         String parentClassName = mojoAnnotatedClass.getParentClassName();
783         if (parentClassName == null || parentClassName.isEmpty()) {
784             return null;
785         }
786         MojoAnnotatedClass parent = mojoAnnotatedClasses.get(parentClassName);
787         if (parent == null) {
788             return null;
789         }
790         return findClassWithExecuteAnnotationInParentHierarchy(parent, mojoAnnotatedClasses);
791     }
792 
793     /**
794      * Collects all {@code @After} annotations from the class hierarchy, starting from the
795      * most-derived class.  Unlike {@code @Execute}, {@code @After} is repeatable, so
796      * multiple entries may exist on a single class and across the hierarchy.
797      */
798     protected List<AfterAnnotationContent> getAfterAnnotationsFromHierarchy(
799             MojoAnnotatedClass mojoAnnotatedClass, Map<String, MojoAnnotatedClass> mojoAnnotatedClasses) {
800         List<AfterAnnotationContent> result = new ArrayList<>();
801         collectAfterAnnotations(mojoAnnotatedClass, mojoAnnotatedClasses, result);
802         return result;
803     }
804 
805     private void collectAfterAnnotations(
806             MojoAnnotatedClass mojoAnnotatedClass,
807             Map<String, MojoAnnotatedClass> mojoAnnotatedClasses,
808             List<AfterAnnotationContent> result) {
809         result.addAll(mojoAnnotatedClass.getAfterAnnotations());
810         String parentClassName = mojoAnnotatedClass.getParentClassName();
811         if (parentClassName != null && !parentClassName.isEmpty()) {
812             MojoAnnotatedClass parent = mojoAnnotatedClasses.get(parentClassName);
813             if (parent != null) {
814                 collectAfterAnnotations(parent, mojoAnnotatedClasses, result);
815             }
816         }
817     }
818 
819     protected Map<String, ParameterAnnotationContent> getParametersParentHierarchy(
820             MojoAnnotatedClass mojoAnnotatedClass, Map<String, MojoAnnotatedClass> mojoAnnotatedClasses) {
821         List<ParameterAnnotationContent> parameterAnnotationContents = new ArrayList<>();
822 
823         parameterAnnotationContents =
824                 getParametersParent(mojoAnnotatedClass, parameterAnnotationContents, mojoAnnotatedClasses);
825 
826         // move to parent first to build the Map
827         Collections.reverse(parameterAnnotationContents);
828 
829         Map<String, ParameterAnnotationContent> map = new HashMap<>(parameterAnnotationContents.size());
830 
831         for (ParameterAnnotationContent parameterAnnotationContent : parameterAnnotationContents) {
832             map.put(parameterAnnotationContent.getFieldName(), parameterAnnotationContent);
833         }
834         return map;
835     }
836 
837     protected List<ParameterAnnotationContent> getParametersParent(
838             MojoAnnotatedClass mojoAnnotatedClass,
839             List<ParameterAnnotationContent> parameterAnnotationContents,
840             Map<String, MojoAnnotatedClass> mojoAnnotatedClasses) {
841         parameterAnnotationContents.addAll(mojoAnnotatedClass.getParameters().values());
842         String parentClassName = mojoAnnotatedClass.getParentClassName();
843         if (parentClassName != null) {
844             MojoAnnotatedClass parent = mojoAnnotatedClasses.get(parentClassName);
845             if (parent != null) {
846                 return getParametersParent(parent, parameterAnnotationContents, mojoAnnotatedClasses);
847             }
848         }
849         return parameterAnnotationContents;
850     }
851 
852     protected Map<String, ComponentAnnotationContent> getComponentsParentHierarchy(
853             MojoAnnotatedClass mojoAnnotatedClass, Map<String, MojoAnnotatedClass> mojoAnnotatedClasses) {
854         List<ComponentAnnotationContent> componentAnnotationContents = new ArrayList<>();
855 
856         componentAnnotationContents =
857                 getComponentParent(mojoAnnotatedClass, componentAnnotationContents, mojoAnnotatedClasses);
858 
859         // move to parent first to build the Map
860         Collections.reverse(componentAnnotationContents);
861 
862         Map<String, ComponentAnnotationContent> map = new HashMap<>(componentAnnotationContents.size());
863 
864         for (ComponentAnnotationContent componentAnnotationContent : componentAnnotationContents) {
865             map.put(componentAnnotationContent.getFieldName(), componentAnnotationContent);
866         }
867         return map;
868     }
869 
870     protected List<ComponentAnnotationContent> getComponentParent(
871             MojoAnnotatedClass mojoAnnotatedClass,
872             List<ComponentAnnotationContent> componentAnnotationContents,
873             Map<String, MojoAnnotatedClass> mojoAnnotatedClasses) {
874         componentAnnotationContents.addAll(mojoAnnotatedClass.getComponents().values());
875         String parentClassName = mojoAnnotatedClass.getParentClassName();
876         if (parentClassName != null) {
877             MojoAnnotatedClass parent = mojoAnnotatedClasses.get(parentClassName);
878             if (parent != null) {
879                 return getComponentParent(parent, componentAnnotationContents, mojoAnnotatedClasses);
880             }
881         }
882         return componentAnnotationContents;
883     }
884 
885     protected MavenProject getFromProjectReferences(Artifact artifact, MavenProject project) {
886         if (project.getProjectReferences() == null
887                 || project.getProjectReferences().isEmpty()) {
888             return null;
889         }
890         Collection<MavenProject> mavenProjects = project.getProjectReferences().values();
891         for (MavenProject mavenProject : mavenProjects) {
892             if (Objects.equals(mavenProject.getId(), artifact.getId())) {
893                 return mavenProject;
894             }
895         }
896         return null;
897     }
898 }