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