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