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