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.slf4j.Logger;
53  import org.slf4j.LoggerFactory;
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 implements MojoDescriptorExtractor, JavadocMojoAnnotation {
70      public static final String NAME = "java-javadoc";
71  
72      private static final Logger LOGGER = LoggerFactory.getLogger(JavaJavadocMojoDescriptorExtractor.class);
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             LOGGER.warn(
197                     "@" + JavadocMojoAnnotation.MULTI_EXECUTION_STRATEGY + " in {} is deprecated: please use '@"
198                             + JavadocMojoAnnotation.EXECUTION_STATEGY + " always' instead.",
199                     javaClass.getFullyQualifiedName());
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 (v == null || v.isEmpty()) {
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 (v == null || v.isEmpty()) {
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 (value != null && !value.isEmpty()) {
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 (value != null && !value.isEmpty()) {
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                 pd.setRequirement(new Requirement(role, roleHint));
404                 pd.setEditable(false);
405             } else {
406                 // Parameter tag
407                 DocletTag parameter = field.getTagByName(JavadocMojoAnnotation.PARAMETER);
408 
409                 pd.setRequired(field.getTagByName(JavadocMojoAnnotation.REQUIRED) != null);
410 
411                 pd.setEditable(field.getTagByName(JavadocMojoAnnotation.READONLY) == null);
412 
413                 String name = parameter.getNamedParameter(JavadocMojoAnnotation.PARAMETER_NAME);
414 
415                 if (!(name == null || name.isEmpty())) {
416                     pd.setName(name);
417                 }
418 
419                 String alias = parameter.getNamedParameter(JavadocMojoAnnotation.PARAMETER_ALIAS);
420 
421                 if (!(alias == null || alias.isEmpty())) {
422                     pd.setAlias(alias);
423                 }
424 
425                 String expression = parameter.getNamedParameter(JavadocMojoAnnotation.PARAMETER_EXPRESSION);
426                 String property = parameter.getNamedParameter(JavadocMojoAnnotation.PARAMETER_PROPERTY);
427 
428                 if ((expression != null && !expression.isEmpty()) && (property != null && !property.isEmpty())) {
429                     LOGGER.error(javaClass.getFullyQualifiedName() + "#" + field.getName() + ":");
430                     LOGGER.error("  Cannot use both:");
431                     LOGGER.error("    @parameter expression=\"${property}\"");
432                     LOGGER.error("  and");
433                     LOGGER.error("    @parameter property=\"property\"");
434                     LOGGER.error("  Second syntax is preferred.");
435                     throw new InvalidParameterException(
436                             javaClass.getFullyQualifiedName() + "#" + field.getName() + ": cannot"
437                                     + " use both @parameter expression and property",
438                             null);
439                 }
440 
441                 if (expression != null && !expression.isEmpty()) {
442                     LOGGER.warn(javaClass.getFullyQualifiedName() + "#" + field.getName() + ":");
443                     LOGGER.warn("  The syntax");
444                     LOGGER.warn("    @parameter expression=\"${property}\"");
445                     LOGGER.warn("  is deprecated, please use");
446                     LOGGER.warn("    @parameter property=\"property\"");
447                     LOGGER.warn("  instead.");
448 
449                 } else if (property != null && !property.isEmpty()) {
450                     expression = "${" + property + "}";
451                 }
452 
453                 pd.setExpression(expression);
454 
455                 if ((expression != null && !expression.isEmpty()) && expression.startsWith("${component.")) {
456                     LOGGER.warn(javaClass.getFullyQualifiedName() + "#" + field.getName() + ":");
457                     LOGGER.warn("  The syntax");
458                     LOGGER.warn("    @parameter expression=\"${component.<role>#<roleHint>}\"");
459                     LOGGER.warn("  is deprecated, please use");
460                     LOGGER.warn("    @component role=\"<role>\" roleHint=\"<roleHint>\"");
461                     LOGGER.warn("  instead.");
462                 }
463 
464                 if ("${reports}".equals(pd.getExpression())) {
465                     mojoDescriptor.setRequiresReports(true);
466                 }
467 
468                 pd.setDefaultValue(parameter.getNamedParameter(JavadocMojoAnnotation.PARAMETER_DEFAULT_VALUE));
469 
470                 pd.setImplementation(parameter.getNamedParameter(JavadocMojoAnnotation.PARAMETER_IMPLEMENTATION));
471             }
472 
473             mojoDescriptor.addParameter(pd);
474         }
475     }
476 
477     /**
478      * extract fields that are either parameters or components.
479      *
480      * @param javaClass not null
481      * @return map with Mojo parameters names as keys
482      */
483     private Map<String, JavaField> extractFieldParameterTags(JavaClass javaClass) {
484         Map<String, JavaField> rawParams;
485 
486         // we have to add the parent fields first, so that they will be overwritten by the local fields if
487         // that actually happens...
488         JavaClass superClass = javaClass.getSuperJavaClass();
489 
490         if (superClass != null) {
491             rawParams = extractFieldParameterTags(superClass);
492         } else {
493             rawParams = new TreeMap<>();
494         }
495 
496         for (JavaField field : javaClass.getFields()) {
497             if (field.getTagByName(JavadocMojoAnnotation.PARAMETER) != null
498                     || field.getTagByName(JavadocMojoAnnotation.COMPONENT) != null) {
499                 rawParams.put(field.getName(), field);
500             }
501         }
502         return rawParams;
503     }
504 
505     @Override
506     public List<MojoDescriptor> execute(PluginToolsRequest request)
507             throws ExtractionException, InvalidPluginDescriptorException {
508         Collection<JavaClass> javaClasses = discoverClasses(request);
509 
510         List<MojoDescriptor> descriptors = new ArrayList<>();
511 
512         for (JavaClass javaClass : javaClasses) {
513             DocletTag tag = javaClass.getTagByName(GOAL);
514 
515             if (tag != null) {
516                 MojoDescriptor mojoDescriptor = createMojoDescriptor(javaClass);
517                 mojoDescriptor.setPluginDescriptor(request.getPluginDescriptor());
518 
519                 // Validate the descriptor as best we can before allowing it to be processed.
520                 validate(mojoDescriptor);
521 
522                 descriptors.add(mojoDescriptor);
523             }
524         }
525 
526         return descriptors;
527     }
528 
529     /**
530      * @param request The plugin request.
531      * @return an array of java class
532      */
533     protected Collection<JavaClass> discoverClasses(final PluginToolsRequest request) {
534         JavaProjectBuilder builder = new JavaProjectBuilder(new SortedClassLibraryBuilder());
535         builder.setEncoding(request.getEncoding());
536 
537         // Build isolated Classloader with only the artifacts of the project (none of this plugin)
538         List<URL> urls = new ArrayList<>(request.getDependencies().size());
539         for (Artifact artifact : request.getDependencies()) {
540             try {
541                 urls.add(artifact.getFile().toURI().toURL());
542             } catch (MalformedURLException e) {
543                 // noop
544             }
545         }
546         builder.addClassLoader(new URLClassLoader(urls.toArray(new URL[0]), ClassLoader.getSystemClassLoader()));
547 
548         MavenProject project = request.getProject();
549 
550         for (String source : project.getCompileSourceRoots()) {
551             builder.addSourceTree(new File(source));
552         }
553 
554         // TODO be more dynamic
555         File generatedPlugin = new File(project.getBasedir(), "target/generated-sources/plugin");
556         if (!project.getCompileSourceRoots().contains(generatedPlugin.getAbsolutePath())) {
557             builder.addSourceTree(generatedPlugin);
558         }
559 
560         return builder.getClasses();
561     }
562 
563     /**
564      * @param mojoDescriptor not null
565      * @throws InvalidParameterException if any
566      */
567     protected void validate(MojoDescriptor mojoDescriptor) throws InvalidParameterException {
568         List<Parameter> parameters = mojoDescriptor.getParameters();
569 
570         if (parameters != null) {
571             for (int j = 0; j < parameters.size(); j++) {
572                 validateParameter(parameters.get(j), j);
573             }
574         }
575     }
576 }