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