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