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