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.codehaus.plexus.logging.AbstractLogEnabled;
053
054/**
055 * <p>
056 * Extracts Mojo descriptors from <a href="https://www.oracle.com/java/technologies//">Java</a> source
057 * javadoc comments only. New mojos should rather rely on annotations and comments which are evaluated
058 * by extractor named {@code java}.
059 * </p>
060 * For more information about the usage tag, have a look to:
061 * <a href="https://maven.apache.org/developers/mojo-api-specification.html">
062 * https://maven.apache.org/developers/mojo-api-specification.html</a>
063 *
064 * @see org.apache.maven.plugin.descriptor.MojoDescriptor
065 */
066@Named(JavaJavadocMojoDescriptorExtractor.NAME)
067@Singleton
068public class JavaJavadocMojoDescriptorExtractor extends AbstractLogEnabled
069        implements MojoDescriptorExtractor, JavadocMojoAnnotation {
070    public static final String NAME = "java-javadoc";
071
072    private static final GroupKey GROUP_KEY = new GroupKey(GroupKey.JAVA_GROUP, 200);
073
074    @Override
075    public String getName() {
076        return NAME;
077    }
078
079    @Override
080    public boolean isDeprecated() {
081        return true; // one should use Java5 annotations instead
082    }
083
084    @Override
085    public GroupKey getGroupKey() {
086        return GROUP_KEY;
087    }
088
089    /**
090     * @param parameter not null
091     * @param i positive number
092     * @throws InvalidParameterException if any
093     */
094    protected void validateParameter(Parameter parameter, int i) throws InvalidParameterException {
095        // TODO: remove when backward compatibility is no longer an issue.
096        String name = parameter.getName();
097
098        if (name == null) {
099            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}