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.javadoc;
20  
21  import javax.inject.Named;
22  import javax.inject.Singleton;
23  
24  import java.io.File;
25  import java.net.MalformedURLException;
26  import java.net.URL;
27  import java.net.URLClassLoader;
28  import java.util.ArrayList;
29  import java.util.Collection;
30  import java.util.List;
31  import java.util.Map;
32  import java.util.TreeMap;
33  
34  import com.thoughtworks.qdox.JavaProjectBuilder;
35  import com.thoughtworks.qdox.library.SortedClassLibraryBuilder;
36  import com.thoughtworks.qdox.model.DocletTag;
37  import com.thoughtworks.qdox.model.JavaClass;
38  import com.thoughtworks.qdox.model.JavaField;
39  import com.thoughtworks.qdox.model.JavaType;
40  import org.apache.maven.artifact.Artifact;
41  import org.apache.maven.plugin.descriptor.InvalidParameterException;
42  import org.apache.maven.plugin.descriptor.InvalidPluginDescriptorException;
43  import org.apache.maven.plugin.descriptor.MojoDescriptor;
44  import org.apache.maven.plugin.descriptor.Parameter;
45  import org.apache.maven.plugin.descriptor.Requirement;
46  import org.apache.maven.project.MavenProject;
47  import org.apache.maven.tools.plugin.ExtendedMojoDescriptor;
48  import org.apache.maven.tools.plugin.PluginToolsRequest;
49  import org.apache.maven.tools.plugin.extractor.ExtractionException;
50  import org.apache.maven.tools.plugin.extractor.GroupKey;
51  import org.apache.maven.tools.plugin.extractor.MojoDescriptorExtractor;
52  import org.apache.maven.tools.plugin.util.PluginUtils;
53  import org.codehaus.plexus.logging.AbstractLogEnabled;
54  import org.codehaus.plexus.util.StringUtils;
55  
56  /**
57   * <p>
58   * Extracts Mojo descriptors from <a href="https://www.oracle.com/java/technologies//">Java</a> source
59   * javadoc comments only. New mojos should rather rely on annotations and comments which are evaluated
60   * by extractor named {@code java}.
61   * </p>
62   * For more information about the usage tag, have a look to:
63   * <a href="https://maven.apache.org/developers/mojo-api-specification.html">
64   * https://maven.apache.org/developers/mojo-api-specification.html</a>
65   *
66   * @see org.apache.maven.plugin.descriptor.MojoDescriptor
67   */
68  @Named(JavaJavadocMojoDescriptorExtractor.NAME)
69  @Singleton
70  public class JavaJavadocMojoDescriptorExtractor extends AbstractLogEnabled
71          implements MojoDescriptorExtractor, JavadocMojoAnnotation {
72      public static final String NAME = "java-javadoc";
73  
74      private static final GroupKey GROUP_KEY = new GroupKey(GroupKey.JAVA_GROUP, 200);
75  
76      @Override
77      public String getName() {
78          return NAME;
79      }
80  
81      @Override
82      public boolean isDeprecated() {
83          return true; // one should use Java5 annotations instead
84      }
85  
86      @Override
87      public GroupKey getGroupKey() {
88          return GROUP_KEY;
89      }
90  
91      /**
92       * @param parameter not null
93       * @param i positive number
94       * @throws InvalidParameterException if any
95       */
96      protected void validateParameter(Parameter parameter, int i) throws InvalidParameterException {
97          // TODO: remove when backward compatibility is no longer an issue.
98          String name = parameter.getName();
99  
100         if (name == null) {
101             throw new InvalidParameterException("name", i);
102         }
103 
104         // TODO: remove when backward compatibility is no longer an issue.
105         String type = parameter.getType();
106 
107         if (type == null) {
108             throw new InvalidParameterException("type", i);
109         }
110 
111         // TODO: remove when backward compatibility is no longer an issue.
112         String description = parameter.getDescription();
113 
114         if (description == null) {
115             throw new InvalidParameterException("description", i);
116         }
117     }
118 
119     // ----------------------------------------------------------------------
120     // Mojo descriptor creation from @tags
121     // ----------------------------------------------------------------------
122 
123     /**
124      * @param javaClass not null
125      * @return a mojo descriptor
126      * @throws InvalidPluginDescriptorException if any
127      */
128     protected MojoDescriptor createMojoDescriptor(JavaClass javaClass) throws InvalidPluginDescriptorException {
129         ExtendedMojoDescriptor mojoDescriptor = new ExtendedMojoDescriptor();
130         mojoDescriptor.setLanguage("java");
131         mojoDescriptor.setImplementation(javaClass.getFullyQualifiedName());
132         mojoDescriptor.setDescription(javaClass.getComment());
133 
134         // ----------------------------------------------------------------------
135         // Mojo annotations in alphabetical order
136         // ----------------------------------------------------------------------
137 
138         // Aggregator flag
139         DocletTag aggregator = findInClassHierarchy(javaClass, JavadocMojoAnnotation.AGGREGATOR);
140         if (aggregator != null) {
141             mojoDescriptor.setAggregator(true);
142         }
143 
144         // Configurator hint
145         DocletTag configurator = findInClassHierarchy(javaClass, JavadocMojoAnnotation.CONFIGURATOR);
146         if (configurator != null) {
147             mojoDescriptor.setComponentConfigurator(configurator.getValue());
148         }
149 
150         // Additional phase to execute first
151         DocletTag execute = findInClassHierarchy(javaClass, JavadocMojoAnnotation.EXECUTE);
152         if (execute != null) {
153             String executePhase = execute.getNamedParameter(JavadocMojoAnnotation.EXECUTE_PHASE);
154             String executeGoal = execute.getNamedParameter(JavadocMojoAnnotation.EXECUTE_GOAL);
155 
156             if (executePhase == null && executeGoal == null) {
157                 throw new InvalidPluginDescriptorException(javaClass.getFullyQualifiedName()
158                         + ": @execute tag requires either a 'phase' or 'goal' parameter");
159             } else if (executePhase != null && executeGoal != null) {
160                 throw new InvalidPluginDescriptorException(javaClass.getFullyQualifiedName()
161                         + ": @execute tag can have only one of a 'phase' or 'goal' parameter");
162             }
163             mojoDescriptor.setExecutePhase(executePhase);
164             mojoDescriptor.setExecuteGoal(executeGoal);
165 
166             String lifecycle = execute.getNamedParameter(JavadocMojoAnnotation.EXECUTE_LIFECYCLE);
167             if (lifecycle != null) {
168                 mojoDescriptor.setExecuteLifecycle(lifecycle);
169                 if (mojoDescriptor.getExecuteGoal() != null) {
170                     throw new InvalidPluginDescriptorException(javaClass.getFullyQualifiedName()
171                             + ": @execute lifecycle requires a phase instead of a goal");
172                 }
173             }
174         }
175 
176         // Goal name
177         DocletTag goal = findInClassHierarchy(javaClass, JavadocMojoAnnotation.GOAL);
178         if (goal != null) {
179             mojoDescriptor.setGoal(goal.getValue());
180         }
181 
182         // inheritByDefault flag
183         boolean value = getBooleanTagValue(
184                 javaClass, JavadocMojoAnnotation.INHERIT_BY_DEFAULT, mojoDescriptor.isInheritedByDefault());
185         mojoDescriptor.setInheritedByDefault(value);
186 
187         // instantiationStrategy
188         DocletTag tag = findInClassHierarchy(javaClass, JavadocMojoAnnotation.INSTANTIATION_STRATEGY);
189         if (tag != null) {
190             mojoDescriptor.setInstantiationStrategy(tag.getValue());
191         }
192 
193         // executionStrategy (and deprecated @attainAlways)
194         tag = findInClassHierarchy(javaClass, JavadocMojoAnnotation.MULTI_EXECUTION_STRATEGY);
195         if (tag != null) {
196             getLogger()
197                     .warn("@" + JavadocMojoAnnotation.MULTI_EXECUTION_STRATEGY + " in "
198                             + javaClass.getFullyQualifiedName() + " is deprecated: please use '@"
199                             + JavadocMojoAnnotation.EXECUTION_STATEGY + " always' instead.");
200             mojoDescriptor.setExecutionStrategy(MojoDescriptor.MULTI_PASS_EXEC_STRATEGY);
201         } else {
202             mojoDescriptor.setExecutionStrategy(MojoDescriptor.SINGLE_PASS_EXEC_STRATEGY);
203         }
204         tag = findInClassHierarchy(javaClass, JavadocMojoAnnotation.EXECUTION_STATEGY);
205         if (tag != null) {
206             mojoDescriptor.setExecutionStrategy(tag.getValue());
207         }
208 
209         // Phase name
210         DocletTag phase = findInClassHierarchy(javaClass, JavadocMojoAnnotation.PHASE);
211         if (phase != null) {
212             mojoDescriptor.setPhase(phase.getValue());
213         }
214 
215         // Dependency resolution flag
216         DocletTag requiresDependencyResolution =
217                 findInClassHierarchy(javaClass, JavadocMojoAnnotation.REQUIRES_DEPENDENCY_RESOLUTION);
218         if (requiresDependencyResolution != null) {
219             String v = requiresDependencyResolution.getValue();
220 
221             if (StringUtils.isEmpty(v)) {
222                 v = "runtime";
223             }
224 
225             mojoDescriptor.setDependencyResolutionRequired(v);
226         }
227 
228         // Dependency collection flag
229         DocletTag requiresDependencyCollection =
230                 findInClassHierarchy(javaClass, JavadocMojoAnnotation.REQUIRES_DEPENDENCY_COLLECTION);
231         if (requiresDependencyCollection != null) {
232             String v = requiresDependencyCollection.getValue();
233 
234             if (StringUtils.isEmpty(v)) {
235                 v = "runtime";
236             }
237 
238             mojoDescriptor.setDependencyCollectionRequired(v);
239         }
240 
241         // requiresDirectInvocation flag
242         value = getBooleanTagValue(
243                 javaClass, JavadocMojoAnnotation.REQUIRES_DIRECT_INVOCATION, mojoDescriptor.isDirectInvocationOnly());
244         mojoDescriptor.setDirectInvocationOnly(value);
245 
246         // Online flag
247         value = getBooleanTagValue(javaClass, JavadocMojoAnnotation.REQUIRES_ONLINE, mojoDescriptor.isOnlineRequired());
248         mojoDescriptor.setOnlineRequired(value);
249 
250         // Project flag
251         value = getBooleanTagValue(
252                 javaClass, JavadocMojoAnnotation.REQUIRES_PROJECT, mojoDescriptor.isProjectRequired());
253         mojoDescriptor.setProjectRequired(value);
254 
255         // requiresReports flag
256         value = getBooleanTagValue(
257                 javaClass, JavadocMojoAnnotation.REQUIRES_REPORTS, mojoDescriptor.isRequiresReports());
258         mojoDescriptor.setRequiresReports(value);
259 
260         // ----------------------------------------------------------------------
261         // Javadoc annotations in alphabetical order
262         // ----------------------------------------------------------------------
263 
264         // Deprecation hint
265         DocletTag deprecated = javaClass.getTagByName(JavadocMojoAnnotation.DEPRECATED);
266         if (deprecated != null) {
267             mojoDescriptor.setDeprecated(deprecated.getValue());
268         }
269 
270         // What version it was introduced in
271         DocletTag since = findInClassHierarchy(javaClass, JavadocMojoAnnotation.SINCE);
272         if (since != null) {
273             mojoDescriptor.setSince(since.getValue());
274         }
275 
276         // Thread-safe mojo
277 
278         value = getBooleanTagValue(javaClass, JavadocMojoAnnotation.THREAD_SAFE, true, mojoDescriptor.isThreadSafe());
279         mojoDescriptor.setThreadSafe(value);
280 
281         extractParameters(mojoDescriptor, javaClass);
282 
283         return mojoDescriptor;
284     }
285 
286     /**
287      * @param javaClass not null
288      * @param tagName not null
289      * @param defaultValue the wanted default value
290      * @return the boolean value of the given tagName
291      * @see #findInClassHierarchy(JavaClass, String)
292      */
293     private static boolean getBooleanTagValue(JavaClass javaClass, String tagName, boolean defaultValue) {
294         DocletTag tag = findInClassHierarchy(javaClass, tagName);
295 
296         if (tag != null) {
297             String value = tag.getValue();
298 
299             if (StringUtils.isNotEmpty(value)) {
300                 defaultValue = Boolean.valueOf(value).booleanValue();
301             }
302         }
303         return defaultValue;
304     }
305 
306     /**
307      * @param javaClass     not null
308      * @param tagName       not null
309      * @param defaultForTag The wanted default value when only the tagname is present
310      * @param defaultValue  the wanted default value when the tag is not specified
311      * @return the boolean value of the given tagName
312      * @see #findInClassHierarchy(JavaClass, String)
313      */
314     private static boolean getBooleanTagValue(
315             JavaClass javaClass, String tagName, boolean defaultForTag, boolean defaultValue) {
316         DocletTag tag = findInClassHierarchy(javaClass, tagName);
317 
318         if (tag != null) {
319             String value = tag.getValue();
320 
321             if (StringUtils.isNotEmpty(value)) {
322                 return Boolean.valueOf(value).booleanValue();
323             } else {
324                 return defaultForTag;
325             }
326         }
327         return defaultValue;
328     }
329 
330     /**
331      * @param javaClass not null
332      * @param tagName not null
333      * @return docletTag instance
334      */
335     private static DocletTag findInClassHierarchy(JavaClass javaClass, String tagName) {
336         DocletTag tag = javaClass.getTagByName(tagName);
337 
338         if (tag == null) {
339             JavaClass superClass = javaClass.getSuperJavaClass();
340 
341             if (superClass != null) {
342                 tag = findInClassHierarchy(superClass, tagName);
343             }
344         }
345 
346         return tag;
347     }
348 
349     /**
350      * @param mojoDescriptor not null
351      * @param javaClass not null
352      * @throws InvalidPluginDescriptorException if any
353      */
354     private void extractParameters(MojoDescriptor mojoDescriptor, JavaClass javaClass)
355             throws InvalidPluginDescriptorException {
356         // ---------------------------------------------------------------------------------
357         // We're resolving class-level, ancestor-class-field, local-class-field order here.
358         // ---------------------------------------------------------------------------------
359 
360         Map<String, JavaField> rawParams = extractFieldParameterTags(javaClass);
361 
362         for (Map.Entry<String, JavaField> entry : rawParams.entrySet()) {
363             JavaField field = entry.getValue();
364 
365             JavaType type = field.getType();
366 
367             Parameter pd = new Parameter();
368 
369             pd.setName(entry.getKey());
370 
371             pd.setType(type.getFullyQualifiedName());
372 
373             pd.setDescription(field.getComment());
374 
375             DocletTag deprecationTag = field.getTagByName(JavadocMojoAnnotation.DEPRECATED);
376 
377             if (deprecationTag != null) {
378                 pd.setDeprecated(deprecationTag.getValue());
379             }
380 
381             DocletTag sinceTag = field.getTagByName(JavadocMojoAnnotation.SINCE);
382             if (sinceTag != null) {
383                 pd.setSince(sinceTag.getValue());
384             }
385 
386             DocletTag componentTag = field.getTagByName(JavadocMojoAnnotation.COMPONENT);
387 
388             if (componentTag != null) {
389                 // Component tag
390                 String role = componentTag.getNamedParameter(JavadocMojoAnnotation.COMPONENT_ROLE);
391 
392                 if (role == null) {
393                     role = field.getType().toString();
394                 }
395 
396                 String roleHint = componentTag.getNamedParameter(JavadocMojoAnnotation.COMPONENT_ROLEHINT);
397 
398                 if (roleHint == null) {
399                     // support alternate syntax for better compatibility with the Plexus CDC.
400                     roleHint = componentTag.getNamedParameter("role-hint");
401                 }
402 
403                 // recognize Maven-injected objects as components annotations instead of parameters
404                 // Note: the expressions we are looking for, i.e. "${project}", are in the values of the Map,
405                 // so the lookup mechanism is different here than in maven-plugin-tools-annotations
406                 boolean isDeprecated = PluginUtils.MAVEN_COMPONENTS.containsValue(role);
407 
408                 if (!isDeprecated) {
409                     // normal component
410                     pd.setRequirement(new Requirement(role, roleHint));
411                 } else {
412                     // not a component but a Maven object to be transformed into an expression/property
413                     getLogger()
414                             .warn("Deprecated @component Javadoc tag for '" + pd.getName() + "' field in "
415                                     + javaClass.getFullyQualifiedName()
416                                     + ": replace with @Parameter( defaultValue = \"" + role
417                                     + "\", readonly = true )");
418                     pd.setDefaultValue(role);
419                     pd.setRequired(true);
420                 }
421 
422                 pd.setEditable(false);
423                 /* TODO: or better like this? Need @component fields be editable for the user?
424                 pd.setEditable( field.getTagByName( READONLY ) == null );
425                 */
426             } else {
427                 // Parameter tag
428                 DocletTag parameter = field.getTagByName(JavadocMojoAnnotation.PARAMETER);
429 
430                 pd.setRequired(field.getTagByName(JavadocMojoAnnotation.REQUIRED) != null);
431 
432                 pd.setEditable(field.getTagByName(JavadocMojoAnnotation.READONLY) == null);
433 
434                 String name = parameter.getNamedParameter(JavadocMojoAnnotation.PARAMETER_NAME);
435 
436                 if (!StringUtils.isEmpty(name)) {
437                     pd.setName(name);
438                 }
439 
440                 String alias = parameter.getNamedParameter(JavadocMojoAnnotation.PARAMETER_ALIAS);
441 
442                 if (!StringUtils.isEmpty(alias)) {
443                     pd.setAlias(alias);
444                 }
445 
446                 String expression = parameter.getNamedParameter(JavadocMojoAnnotation.PARAMETER_EXPRESSION);
447                 String property = parameter.getNamedParameter(JavadocMojoAnnotation.PARAMETER_PROPERTY);
448 
449                 if (StringUtils.isNotEmpty(expression) && StringUtils.isNotEmpty(property)) {
450                     getLogger().error(javaClass.getFullyQualifiedName() + "#" + field.getName() + ":");
451                     getLogger().error("  Cannot use both:");
452                     getLogger().error("    @parameter expression=\"${property}\"");
453                     getLogger().error("  and");
454                     getLogger().error("    @parameter property=\"property\"");
455                     getLogger().error("  Second syntax is preferred.");
456                     throw new InvalidParameterException(
457                             javaClass.getFullyQualifiedName() + "#" + field.getName() + ": cannot"
458                                     + " use both @parameter expression and property",
459                             null);
460                 }
461 
462                 if (StringUtils.isNotEmpty(expression)) {
463                     getLogger().warn(javaClass.getFullyQualifiedName() + "#" + field.getName() + ":");
464                     getLogger().warn("  The syntax");
465                     getLogger().warn("    @parameter expression=\"${property}\"");
466                     getLogger().warn("  is deprecated, please use");
467                     getLogger().warn("    @parameter property=\"property\"");
468                     getLogger().warn("  instead.");
469 
470                 } else if (StringUtils.isNotEmpty(property)) {
471                     expression = "${" + property + "}";
472                 }
473 
474                 pd.setExpression(expression);
475 
476                 if (StringUtils.isNotEmpty(expression) && expression.startsWith("${component.")) {
477                     getLogger().warn(javaClass.getFullyQualifiedName() + "#" + field.getName() + ":");
478                     getLogger().warn("  The syntax");
479                     getLogger().warn("    @parameter expression=\"${component.<role>#<roleHint>}\"");
480                     getLogger().warn("  is deprecated, please use");
481                     getLogger().warn("    @component role=\"<role>\" roleHint=\"<roleHint>\"");
482                     getLogger().warn("  instead.");
483                 }
484 
485                 if ("${reports}".equals(pd.getExpression())) {
486                     mojoDescriptor.setRequiresReports(true);
487                 }
488 
489                 pd.setDefaultValue(parameter.getNamedParameter(JavadocMojoAnnotation.PARAMETER_DEFAULT_VALUE));
490 
491                 pd.setImplementation(parameter.getNamedParameter(JavadocMojoAnnotation.PARAMETER_IMPLEMENTATION));
492             }
493 
494             mojoDescriptor.addParameter(pd);
495         }
496     }
497 
498     /**
499      * extract fields that are either parameters or components.
500      *
501      * @param javaClass not null
502      * @return map with Mojo parameters names as keys
503      */
504     private Map<String, JavaField> extractFieldParameterTags(JavaClass javaClass) {
505         Map<String, JavaField> rawParams;
506 
507         // we have to add the parent fields first, so that they will be overwritten by the local fields if
508         // that actually happens...
509         JavaClass superClass = javaClass.getSuperJavaClass();
510 
511         if (superClass != null) {
512             rawParams = extractFieldParameterTags(superClass);
513         } else {
514             rawParams = new TreeMap<String, JavaField>();
515         }
516 
517         for (JavaField field : javaClass.getFields()) {
518             if (field.getTagByName(JavadocMojoAnnotation.PARAMETER) != null
519                     || field.getTagByName(JavadocMojoAnnotation.COMPONENT) != null) {
520                 rawParams.put(field.getName(), field);
521             }
522         }
523         return rawParams;
524     }
525 
526     @Override
527     public List<MojoDescriptor> execute(PluginToolsRequest request)
528             throws ExtractionException, InvalidPluginDescriptorException {
529         Collection<JavaClass> javaClasses = discoverClasses(request);
530 
531         List<MojoDescriptor> descriptors = new ArrayList<>();
532 
533         for (JavaClass javaClass : javaClasses) {
534             DocletTag tag = javaClass.getTagByName(GOAL);
535 
536             if (tag != null) {
537                 MojoDescriptor mojoDescriptor = createMojoDescriptor(javaClass);
538                 mojoDescriptor.setPluginDescriptor(request.getPluginDescriptor());
539 
540                 // Validate the descriptor as best we can before allowing it to be processed.
541                 validate(mojoDescriptor);
542 
543                 descriptors.add(mojoDescriptor);
544             }
545         }
546 
547         return descriptors;
548     }
549 
550     /**
551      * @param request The plugin request.
552      * @return an array of java class
553      */
554     protected Collection<JavaClass> discoverClasses(final PluginToolsRequest request) {
555         JavaProjectBuilder builder = new JavaProjectBuilder(new SortedClassLibraryBuilder());
556         builder.setEncoding(request.getEncoding());
557 
558         // Build isolated Classloader with only the artifacts of the project (none of this plugin)
559         List<URL> urls = new ArrayList<>(request.getDependencies().size());
560         for (Artifact artifact : request.getDependencies()) {
561             try {
562                 urls.add(artifact.getFile().toURI().toURL());
563             } catch (MalformedURLException e) {
564                 // noop
565             }
566         }
567         builder.addClassLoader(new URLClassLoader(urls.toArray(new URL[0]), ClassLoader.getSystemClassLoader()));
568 
569         MavenProject project = request.getProject();
570 
571         for (String source : project.getCompileSourceRoots()) {
572             builder.addSourceTree(new File(source));
573         }
574 
575         // TODO be more dynamic
576         File generatedPlugin = new File(project.getBasedir(), "target/generated-sources/plugin");
577         if (!project.getCompileSourceRoots().contains(generatedPlugin.getAbsolutePath())) {
578             builder.addSourceTree(generatedPlugin);
579         }
580 
581         return builder.getClasses();
582     }
583 
584     /**
585      * @param mojoDescriptor not null
586      * @throws InvalidParameterException if any
587      */
588     protected void validate(MojoDescriptor mojoDescriptor) throws InvalidParameterException {
589         List<Parameter> parameters = mojoDescriptor.getParameters();
590 
591         if (parameters != null) {
592             for (int j = 0; j < parameters.size(); j++) {
593                 validateParameter(parameters.get(j), j);
594             }
595         }
596     }
597 }