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