001/*
002 * Licensed to the Apache Software Foundation (ASF) under one
003 * or more contributor license agreements.  See the NOTICE file
004 * distributed with this work for additional information
005 * regarding copyright ownership.  The ASF licenses this file
006 * to you under the Apache License, Version 2.0 (the
007 * "License"); you may not use this file except in compliance
008 * with the License.  You may obtain a copy of the License at
009 *
010 *   http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing,
013 * software distributed under the License is distributed on an
014 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
015 * KIND, either express or implied.  See the License for the
016 * specific language governing permissions and limitations
017 * under the License.
018 */
019package org.apache.maven.tools.plugin.extractor.javadoc;
020
021import javax.inject.Named;
022import javax.inject.Singleton;
023
024import java.io.File;
025import java.net.MalformedURLException;
026import java.net.URL;
027import java.net.URLClassLoader;
028import java.util.ArrayList;
029import java.util.Collection;
030import java.util.List;
031import java.util.Map;
032import java.util.TreeMap;
033
034import com.thoughtworks.qdox.JavaProjectBuilder;
035import com.thoughtworks.qdox.library.SortedClassLibraryBuilder;
036import com.thoughtworks.qdox.model.DocletTag;
037import com.thoughtworks.qdox.model.JavaClass;
038import com.thoughtworks.qdox.model.JavaField;
039import com.thoughtworks.qdox.model.JavaType;
040import org.apache.maven.artifact.Artifact;
041import org.apache.maven.plugin.descriptor.InvalidParameterException;
042import org.apache.maven.plugin.descriptor.InvalidPluginDescriptorException;
043import org.apache.maven.plugin.descriptor.MojoDescriptor;
044import org.apache.maven.plugin.descriptor.Parameter;
045import org.apache.maven.plugin.descriptor.Requirement;
046import org.apache.maven.project.MavenProject;
047import org.apache.maven.tools.plugin.ExtendedMojoDescriptor;
048import org.apache.maven.tools.plugin.PluginToolsRequest;
049import org.apache.maven.tools.plugin.extractor.ExtractionException;
050import org.apache.maven.tools.plugin.extractor.GroupKey;
051import org.apache.maven.tools.plugin.extractor.MojoDescriptorExtractor;
052import org.apache.maven.tools.plugin.util.PluginUtils;
053import org.codehaus.plexus.logging.AbstractLogEnabled;
054
055/**
056 * <p>
057 * Extracts Mojo descriptors from <a href="https://www.oracle.com/java/technologies//">Java</a> source
058 * javadoc comments only. New mojos should rather rely on annotations and comments which are evaluated
059 * by extractor named {@code java}.
060 * </p>
061 * For more information about the usage tag, have a look to:
062 * <a href="https://maven.apache.org/developers/mojo-api-specification.html">
063 * https://maven.apache.org/developers/mojo-api-specification.html</a>
064 *
065 * @see org.apache.maven.plugin.descriptor.MojoDescriptor
066 */
067@Named(JavaJavadocMojoDescriptorExtractor.NAME)
068@Singleton
069public class JavaJavadocMojoDescriptorExtractor extends AbstractLogEnabled
070        implements MojoDescriptorExtractor, JavadocMojoAnnotation {
071    public static final String NAME = "java-javadoc";
072
073    private static final GroupKey GROUP_KEY = new GroupKey(GroupKey.JAVA_GROUP, 200);
074
075    @Override
076    public String getName() {
077        return NAME;
078    }
079
080    @Override
081    public boolean isDeprecated() {
082        return true; // one should use Java5 annotations instead
083    }
084
085    @Override
086    public GroupKey getGroupKey() {
087        return GROUP_KEY;
088    }
089
090    /**
091     * @param parameter not null
092     * @param i positive number
093     * @throws InvalidParameterException if any
094     */
095    protected void validateParameter(Parameter parameter, int i) throws InvalidParameterException {
096        // TODO: remove when backward compatibility is no longer an issue.
097        String name = parameter.getName();
098
099        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<>();
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}