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.plugin.plugin;
20  
21  import javax.inject.Inject;
22  
23  import java.io.File;
24  import java.net.URI;
25  import java.util.Arrays;
26  import java.util.Collections;
27  import java.util.LinkedHashSet;
28  import java.util.List;
29  import java.util.Set;
30  
31  import org.apache.maven.artifact.Artifact;
32  import org.apache.maven.artifact.resolver.filter.ArtifactFilter;
33  import org.apache.maven.artifact.resolver.filter.IncludesArtifactFilter;
34  import org.apache.maven.execution.MavenSession;
35  import org.apache.maven.plugin.MojoExecutionException;
36  import org.apache.maven.plugin.descriptor.InvalidPluginDescriptorException;
37  import org.apache.maven.plugin.descriptor.PluginDescriptor;
38  import org.apache.maven.plugins.annotations.LifecyclePhase;
39  import org.apache.maven.plugins.annotations.Mojo;
40  import org.apache.maven.plugins.annotations.Parameter;
41  import org.apache.maven.plugins.annotations.ResolutionScope;
42  import org.apache.maven.project.MavenProject;
43  import org.apache.maven.tools.plugin.DefaultPluginToolsRequest;
44  import org.apache.maven.tools.plugin.PluginDescriptorHelper;
45  import org.apache.maven.tools.plugin.PluginToolsRequest;
46  import org.apache.maven.tools.plugin.extractor.ExtractionException;
47  import org.apache.maven.tools.plugin.generator.GeneratorException;
48  import org.apache.maven.tools.plugin.generator.GeneratorUtils;
49  import org.apache.maven.tools.plugin.generator.PluginDescriptorFilesGenerator;
50  import org.apache.maven.tools.plugin.scanner.MojoScanner;
51  import org.codehaus.plexus.component.repository.ComponentDependency;
52  import org.codehaus.plexus.util.ReaderFactory;
53  import org.sonatype.plexus.build.incremental.BuildContext;
54  
55  /**
56   * <p>
57   * Generate a plugin descriptor.
58   * </p>
59   * <p>
60   * <b>Note:</b> Since 3.0, for Java plugin annotations support,
61   * default <a href="http://maven.apache.org/ref/current/maven-core/lifecycles.html">phase</a>
62   * defined by this goal is after the "compilation" of any scripts. This doesn't override
63   * <a href="/ref/current/maven-core/default-bindings.html#Bindings_for_maven-plugin_packaging">the default binding coded
64   * at generate-resources phase</a> in Maven core.
65   * </p>
66   * @author <a href="mailto:jason@maven.org">Jason van Zyl</a>
67   * @since 2.0
68   */
69  @Mojo(
70          name = "descriptor",
71          defaultPhase = LifecyclePhase.PROCESS_CLASSES,
72          requiresDependencyResolution = ResolutionScope.COMPILE_PLUS_RUNTIME,
73          threadSafe = true)
74  public class DescriptorGeneratorMojo extends AbstractGeneratorMojo {
75      private static final String VALUE_AUTO = "auto";
76  
77      /**
78       * The directory where the generated <code>plugin.xml</code> file will be put.
79       */
80      @Parameter(defaultValue = "${project.build.outputDirectory}/META-INF/maven", readonly = true)
81      private File outputDirectory;
82  
83      /**
84       * The file encoding of the source files.
85       *
86       * @since 2.5
87       */
88      @Parameter(property = "encoding", defaultValue = "${project.build.sourceEncoding}")
89      private String encoding;
90  
91      /**
92       * A flag to disable generation of the <code>plugin.xml</code> in favor of a hand authored plugin descriptor.
93       *
94       * @since 2.6
95       */
96      @Parameter(defaultValue = "false")
97      private boolean skipDescriptor;
98  
99      /**
100      * <p>
101      * The role names of mojo extractors to use.
102      * </p>
103      * <p>
104      * If not set, all mojo extractors will be used. If set to an empty extractor name, no mojo extractors
105      * will be used.
106      * </p>
107      * Example:
108      * <pre>
109      *  &lt;!-- Use all mojo extractors --&gt;
110      *  &lt;extractors/&gt;
111      *
112      *  &lt;!-- Use no mojo extractors --&gt;
113      *  &lt;extractors&gt;
114      *      &lt;extractor/&gt;
115      *  &lt;/extractors&gt;
116      *
117      *  &lt;!-- Use only bsh mojo extractor --&gt;
118      *  &lt;extractors&gt;
119      *      &lt;extractor&gt;bsh&lt;/extractor&gt;
120      *  &lt;/extractors&gt;
121      * </pre>
122      * The extractors with the following names ship with {@code maven-plugin-tools}:
123      * <ol>
124      *  <li>{@code java-annotations}</li>
125      *  <li>{@code java-javadoc}, deprecated</li>
126      *  <li>{@code ant}, deprecated</li>
127      *  <li>{@code bsh}, deprecated</li>
128      * </ol>
129      */
130     @Parameter
131     private Set<String> extractors;
132 
133     /**
134      * A set of root directories to exclude from being scanned, if the extractor scans source directories
135      * to obtain metadata.
136      *
137      * <p>Globs are also supported here.
138      *
139      * <p>Users can specify this to prevent certain generated source roots from being parsed by this plugin
140      * in the event that those source roots contain potentially malformed or incompatible code.
141      *
142      * <p>This is primarily designed to facilitate allowing this plugin to operate with generated sources
143      * that use annotations or documentation in an uncontrollable format that may conflict with the parsing
144      * rules we utilise.
145      *
146      * <p>Note that this only accepts <strong>source roots</strong>. It will not accept
147      * specific paths within a source root (e.g. specific packages). In this context, a source root
148      * would be considered to be a directory holding a full Java package structure which can be
149      * passed directly to {@code javac} for compilation, or {@code javadoc} for documentation.
150      *
151      * <p>As an example, the following configuration will prevent this goal scanning any
152      * generated sources from annotation processors:
153      *
154      * <pre>{@code
155      *   <excludedScanDirectories>
156      *     <excludedScanDirectory>${project.build.directory}/generated-sources/annotations</excludedScanDirectory>
157      *     <excludedScanDirectory>${project.build.directory}/generated-test-sources/annotations</excludedScanDirectory>
158      *   </excludedScanDirectories>
159      * }</pre>
160      *
161      * @since 3.16.0
162      */
163     @Parameter
164     private Set<String> excludedScanDirectories = Collections.emptySet();
165 
166     /**
167      * By default, an exception is throw if no mojo descriptor is found. As the maven-plugin is defined in core, the
168      * descriptor generator mojo is bound to generate-resources phase.
169      * But for annotations, the compiled classes are needed, so skip error
170      *
171      * @since 3.0
172      */
173     @Parameter(property = "maven.plugin.skipErrorNoDescriptorsFound", defaultValue = "false")
174     private boolean skipErrorNoDescriptorsFound;
175 
176     /**
177      * Flag controlling is "expected dependencies in provided scope" check to be performed or not. Default value:
178      * {@code true}.
179      *
180      * @since 3.6.3
181      */
182     @Parameter(defaultValue = "true", property = "maven.plugin.checkExpectedProvidedScope")
183     private boolean checkExpectedProvidedScope = true;
184 
185     /**
186      * List of {@code groupId} strings of artifact coordinates that are expected to be in "provided" scope. Default
187      * value: {@code ["org.apache.maven"]}.
188      *
189      * @since 3.6.3
190      */
191     @Parameter
192     private List<String> expectedProvidedScopeGroupIds = Collections.singletonList("org.apache.maven");
193 
194     /**
195      * List of {@code groupId:artifactId} strings of artifact coordinates that are to be excluded from "expected
196      * provided scope" check. Default value:
197      * {@code ["org.apache.maven:maven-archiver", "org.apache.maven:maven-jxr", "org.apache.maven:plexus-utils"]}.
198      *
199      * @since 3.6.3
200      */
201     @Parameter
202     private List<String> expectedProvidedScopeExclusions = Arrays.asList(
203             "org.apache.maven:maven-archiver", "org.apache.maven:maven-jxr", "org.apache.maven:plexus-utils");
204 
205     /**
206      * Specify the dependencies as {@code groupId:artifactId} containing (abstract) Mojos, to filter
207      * dependencies scanned at runtime and focus on dependencies that are really useful to Mojo analysis.
208      * By default, the value is {@code null} and all dependencies are scanned (as before this parameter was added).
209      * If specified in the configuration with no children, no dependencies are scanned.
210      *
211      * @since 3.5
212      */
213     @Parameter
214     private List<String> mojoDependencies = null;
215 
216     /**
217      * Creates links to existing external javadoc-generated documentation.
218      * <br>
219      * <b>Notes</b>:
220      * all given links should have a fetchable {@code /package-list} or {@code /element-list} file.
221      * For instance:
222      * <pre>
223      * &lt;externalJavadocBaseUrls&gt;
224      *   &lt;externalJavadocBaseUrl&gt;https://docs.oracle.com/javase/8/docs/api/&lt;/externalJavadocBaseUrl&gt;
225      * &lt;/externalJavadocBaseUrls&gt;
226      * </pre>
227      * is valid because <code>https://docs.oracle.com/javase/8/docs/api/package-list</code> exists.
228      * See <a href="https://docs.oracle.com/en/java/javase/17/docs/specs/man/javadoc.html#standard-doclet-options">
229      * link option of the javadoc tool</a>.
230      * Using this parameter requires connectivity to the given URLs during the goal execution.
231      * @since 3.7.0
232      */
233     @Parameter(property = "externalJavadocBaseUrls", alias = "links")
234     protected List<URI> externalJavadocBaseUrls;
235 
236     /**
237      * The base URL for the Javadoc site containing the current project's API documentation.
238      * This may be relative to the root of the generated Maven site.
239      * It does not need to exist yet at the time when this goal is executed.
240      * Must end with a slash.
241      * <b>In case this is set the javadoc reporting goal should be executed prior to
242      * <a href="../maven-plugin-report-plugin/index.html">Plugin Report</a>.</b>
243      * @since 3.7.0
244      */
245     @Parameter(property = "internalJavadocBaseUrl")
246     protected URI internalJavadocBaseUrl;
247 
248     /**
249      * The version of the javadoc tool (equal to the container JDK version) used to generate the internal javadoc
250      * Only relevant if {@link #internalJavadocBaseUrl} is set.
251      * The default value needs to be overwritten in case toolchains are being used for generating Javadoc.
252      *
253      * @since 3.7.0
254      */
255     @Parameter(property = "internalJavadocVersion", defaultValue = "${java.version}")
256     protected String internalJavadocVersion;
257 
258     /**
259      * The required Java version to set in the plugin descriptor. This is evaluated by Maven 4 and ignored by earlier
260      * Maven versions. Can be either one of the following formats:
261      *
262      * <ul>
263      * <li>A version range which specifies the supported Java versions. It can either use the usual mathematical
264      * syntax like {@code "[1.7,9),[11,)"} or use a single version like {@code "1.8"}. The latter is a short
265      * form for {@code "[1.8,)"}, i.e. denotes the minimum version required.</li>
266      * <li>{@code "auto"} to determine the minimum Java version from the binary class version being generated during
267      * compilation (determined by the extractor).</li>
268      * </ul>
269      *
270      * @since 3.8.0
271      */
272     @Parameter(defaultValue = VALUE_AUTO)
273     String requiredJavaVersion;
274 
275     /**
276      * The required Maven version to set in the plugin descriptor. This is evaluated by Maven 4 and ignored by earlier
277      * Maven versions. Can be either one of the following formats:
278      *
279      * <ul>
280      * <li>A version range which specifies the supported Maven versions. It can either use the usual mathematical
281      * syntax like {@code "[2.0.10,2.1.0),[3.0,)"} or use a single version like {@code "2.2.1"}. The latter is a short
282      * form for {@code "[2.2.1,)"}, i.e. denotes the minimum version required.</li>
283      * <li>{@code "auto"} to determine the minimum Maven version from the POM's Maven prerequisite, or if not set the
284      * referenced Maven Plugin API version.</li>
285      * </ul>
286      * This value takes precedence over the
287      * <a href="https://maven.apache.org/pom.html#Prerequisites">POM's Maven prerequisite</a> in Maven 4.
288      *
289      * @since 3.8.0
290      */
291     @Parameter(defaultValue = VALUE_AUTO)
292     String requiredMavenVersion;
293 
294     private final MavenSession mavenSession;
295 
296     /**
297      * The component used for scanning the source tree for mojos.
298      */
299     private final MojoScanner mojoScanner;
300 
301     protected final BuildContext buildContext;
302 
303     @Inject
304     public DescriptorGeneratorMojo(
305             MavenProject project, MavenSession mavenSession, MojoScanner mojoScanner, BuildContext buildContext) {
306         super(project);
307         this.mavenSession = mavenSession;
308         this.mojoScanner = mojoScanner;
309         this.buildContext = buildContext;
310     }
311 
312     public void generate() throws MojoExecutionException {
313 
314         if (!"maven-plugin".equalsIgnoreCase(project.getArtifactId())
315                 && project.getArtifactId().toLowerCase().startsWith("maven-")
316                 && project.getArtifactId().toLowerCase().endsWith("-plugin")
317                 && !"org.apache.maven.plugins".equals(project.getGroupId())) {
318             getLog().warn(LS + LS + "Artifact Ids of the format maven-___-plugin are reserved for" + LS
319                     + "plugins in the Group Id org.apache.maven.plugins" + LS
320                     + "Please change your artifactId to the format ___-maven-plugin" + LS
321                     + "In the future this error will break the build." + LS + LS);
322         }
323 
324         if (skipDescriptor) {
325             getLog().warn("Execution skipped");
326             return;
327         }
328 
329         if (checkExpectedProvidedScope) {
330             Set<Artifact> wrongScopedArtifacts = dependenciesNotInProvidedScope();
331             if (!wrongScopedArtifacts.isEmpty()) {
332                 StringBuilder message = new StringBuilder(
333                         LS + LS + "Some dependencies of Maven Plugins are expected to be in provided scope." + LS
334                                 + "Please make sure that dependencies listed below declared in POM" + LS
335                                 + "have set '<scope>provided</scope>' as well." + LS + LS
336                                 + "The following dependencies are in wrong scope:" + LS);
337                 for (Artifact artifact : wrongScopedArtifacts) {
338                     message.append(" * ").append(artifact).append(LS);
339                 }
340                 message.append(LS).append(LS);
341 
342                 getLog().warn(message.toString());
343             }
344         }
345 
346         mojoScanner.setActiveExtractors(extractors);
347 
348         // TODO: could use this more, eg in the writing of the plugin descriptor!
349         PluginDescriptor pluginDescriptor = new PluginDescriptor();
350 
351         pluginDescriptor.setGroupId(project.getGroupId());
352 
353         pluginDescriptor.setArtifactId(project.getArtifactId());
354 
355         pluginDescriptor.setVersion(project.getVersion());
356 
357         pluginDescriptor.setGoalPrefix(goalPrefix);
358 
359         pluginDescriptor.setName(project.getName());
360 
361         pluginDescriptor.setDescription(project.getDescription());
362 
363         if (encoding == null || encoding.length() < 1) {
364             getLog().warn("Using platform encoding (" + ReaderFactory.FILE_ENCODING
365                     + " actually) to read mojo source files, i.e. build is platform dependent!");
366         } else {
367             getLog().info("Using '" + encoding + "' encoding to read mojo source files.");
368         }
369 
370         if (internalJavadocBaseUrl != null && !internalJavadocBaseUrl.getPath().endsWith("/")) {
371             throw new MojoExecutionException("Given parameter 'internalJavadocBaseUrl' must end with a slash but is '"
372                     + internalJavadocBaseUrl + "'");
373         }
374         try {
375             List<ComponentDependency> deps = GeneratorUtils.toComponentDependencies(project.getArtifacts());
376             pluginDescriptor.setDependencies(deps);
377 
378             PluginToolsRequest request = new DefaultPluginToolsRequest(project, pluginDescriptor);
379             request.setEncoding(encoding);
380             request.setSkipErrorNoDescriptorsFound(skipErrorNoDescriptorsFound);
381             request.setDependencies(filterMojoDependencies());
382             request.setRepoSession(mavenSession.getRepositorySession());
383             request.setInternalJavadocBaseUrl(internalJavadocBaseUrl);
384             request.setInternalJavadocVersion(internalJavadocVersion);
385             request.setExternalJavadocBaseUrls(externalJavadocBaseUrls);
386             request.setSettings(mavenSession.getSettings());
387             request.setExcludedScanDirectories(excludedScanDirectories);
388 
389             mojoScanner.populatePluginDescriptor(request);
390             request.setPluginDescriptor(extendPluginDescriptor(request));
391 
392             outputDirectory.mkdirs();
393 
394             ValidateComponentRequirement validateComponentRequirement = new ValidateComponentRequirement();
395             validateComponentRequirement.validate(request.getPluginDescriptor(), getLog());
396 
397             PluginDescriptorFilesGenerator pluginDescriptorGenerator = new PluginDescriptorFilesGenerator();
398             pluginDescriptorGenerator.execute(outputDirectory, request);
399 
400             buildContext.refresh(outputDirectory);
401         } catch (GeneratorException e) {
402             throw new MojoExecutionException("Error writing plugin descriptor", e);
403         } catch (InvalidPluginDescriptorException | ExtractionException e) {
404             throw new MojoExecutionException(
405                     "Error extracting plugin descriptor: '" + e.getLocalizedMessage() + "'", e);
406         } catch (LinkageError e) {
407             throw new MojoExecutionException(
408                     "The API of the mojo scanner is not compatible with this plugin version."
409                             + " Please check the plugin dependencies configured"
410                             + " in the POM and ensure the versions match.",
411                     e);
412         }
413     }
414 
415     private PluginDescriptor extendPluginDescriptor(PluginToolsRequest request) {
416         PluginDescriptor pluginDescriptor = request.getPluginDescriptor();
417         pluginDescriptor.setRequiredMavenVersion(getRequiredMavenVersion(request));
418         return PluginDescriptorHelper.setRequiredJavaVersion(pluginDescriptor, getRequiredJavaVersion(request));
419     }
420 
421     private String getRequiredMavenVersion(PluginToolsRequest request) {
422         if (!VALUE_AUTO.equals(requiredMavenVersion)) {
423             return requiredMavenVersion;
424         }
425         getLog().debug("Trying to derive Maven version automatically from project prerequisites...");
426         String requiredMavenVersion =
427                 project.getPrerequisites() != null ? project.getPrerequisites().getMaven() : null;
428         if (requiredMavenVersion == null) {
429             getLog().debug("Trying to derive Maven version automatically from referenced Maven Plugin API artifact "
430                     + "version...");
431             requiredMavenVersion = request.getUsedMavenApiVersion();
432         }
433         if (requiredMavenVersion == null) {
434             getLog().warn("Cannot determine the required Maven version automatically, it is recommended to "
435                     + "configure some explicit value manually.");
436         }
437         return requiredMavenVersion;
438     }
439 
440     private String getRequiredJavaVersion(PluginToolsRequest request) {
441         if (!VALUE_AUTO.equals(requiredJavaVersion)) {
442             return requiredJavaVersion;
443         }
444         String minRequiredJavaVersion = request.getRequiredJavaVersion();
445         if (minRequiredJavaVersion == null) {
446             getLog().warn("Cannot determine the minimally required Java version automatically, it is recommended to "
447                     + "configure some explicit value manually.");
448             return null;
449         }
450 
451         return minRequiredJavaVersion;
452     }
453 
454     /**
455      * Collects all dependencies expected to be in "provided" scope but are NOT in "provided" scope.
456      */
457     private Set<Artifact> dependenciesNotInProvidedScope() {
458         LinkedHashSet<Artifact> wrongScopedDependencies = new LinkedHashSet<>();
459 
460         for (Artifact dependency : project.getArtifacts()) {
461             String ga = dependency.getGroupId() + ":" + dependency.getArtifactId();
462             if (expectedProvidedScopeGroupIds.contains(dependency.getGroupId())
463                     && !expectedProvidedScopeExclusions.contains(ga)
464                     && !Artifact.SCOPE_PROVIDED.equals(dependency.getScope())) {
465                 wrongScopedDependencies.add(dependency);
466             }
467         }
468 
469         return wrongScopedDependencies;
470     }
471 
472     /**
473      * Get dependencies filtered with mojoDependencies configuration.
474      *
475      * @return eventually filtered dependencies, or even <code>null</code> if configured with empty mojoDependencies
476      * list
477      * @see #mojoDependencies
478      */
479     private Set<Artifact> filterMojoDependencies() {
480         Set<Artifact> filteredArtifacts;
481         if (mojoDependencies == null) {
482             filteredArtifacts = new LinkedHashSet<>(project.getArtifacts());
483         } else if (mojoDependencies.isEmpty()) {
484             filteredArtifacts = null;
485         } else {
486             filteredArtifacts = new LinkedHashSet<>();
487 
488             ArtifactFilter filter = new IncludesArtifactFilter(mojoDependencies);
489 
490             for (Artifact artifact : project.getArtifacts()) {
491                 if (filter.include(artifact)) {
492                     filteredArtifacts.add(artifact);
493                 }
494             }
495         }
496 
497         return filteredArtifacts;
498     }
499 }