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