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.plugin.plugin; 020 021import java.io.File; 022import java.net.URI; 023import java.util.Arrays; 024import java.util.Collections; 025import java.util.LinkedHashSet; 026import java.util.List; 027import java.util.Set; 028 029import org.apache.maven.artifact.Artifact; 030import org.apache.maven.artifact.resolver.filter.ArtifactFilter; 031import org.apache.maven.artifact.resolver.filter.IncludesArtifactFilter; 032import org.apache.maven.execution.MavenSession; 033import org.apache.maven.plugin.MojoExecutionException; 034import org.apache.maven.plugin.descriptor.InvalidPluginDescriptorException; 035import org.apache.maven.plugin.descriptor.PluginDescriptor; 036import org.apache.maven.plugins.annotations.Component; 037import org.apache.maven.plugins.annotations.LifecyclePhase; 038import org.apache.maven.plugins.annotations.Mojo; 039import org.apache.maven.plugins.annotations.Parameter; 040import org.apache.maven.plugins.annotations.ResolutionScope; 041import org.apache.maven.tools.plugin.DefaultPluginToolsRequest; 042import org.apache.maven.tools.plugin.ExtendedPluginDescriptor; 043import org.apache.maven.tools.plugin.PluginToolsRequest; 044import org.apache.maven.tools.plugin.extractor.ExtractionException; 045import org.apache.maven.tools.plugin.generator.GeneratorException; 046import org.apache.maven.tools.plugin.generator.GeneratorUtils; 047import org.apache.maven.tools.plugin.generator.PluginDescriptorFilesGenerator; 048import org.apache.maven.tools.plugin.scanner.MojoScanner; 049import org.codehaus.plexus.component.repository.ComponentDependency; 050import org.codehaus.plexus.util.ReaderFactory; 051import org.sonatype.plexus.build.incremental.BuildContext; 052 053/** 054 * <p> 055 * Generate a plugin descriptor. 056 * </p> 057 * <p> 058 * <b>Note:</b> Since 3.0, for Java plugin annotations support, 059 * default <a href="http://maven.apache.org/ref/current/maven-core/lifecycles.html">phase</a> 060 * defined by this goal is after the "compilation" of any scripts. This doesn't override 061 * <a href="/ref/current/maven-core/default-bindings.html#Bindings_for_maven-plugin_packaging">the default binding coded 062 * at generate-resources phase</a> in Maven core. 063 * </p> 064 * @author <a href="mailto:jason@maven.org">Jason van Zyl</a> 065 * @since 2.0 066 */ 067@Mojo( 068 name = "descriptor", 069 defaultPhase = LifecyclePhase.PROCESS_CLASSES, 070 requiresDependencyResolution = ResolutionScope.COMPILE_PLUS_RUNTIME, 071 threadSafe = true) 072public class DescriptorGeneratorMojo extends AbstractGeneratorMojo { 073 private static final String VALUE_AUTO = "auto"; 074 075 /** 076 * The directory where the generated <code>plugin.xml</code> file will be put. 077 */ 078 @Parameter(defaultValue = "${project.build.outputDirectory}/META-INF/maven", readonly = true) 079 private File outputDirectory; 080 081 /** 082 * The file encoding of the source files. 083 * 084 * @since 2.5 085 */ 086 @Parameter(property = "encoding", defaultValue = "${project.build.sourceEncoding}") 087 private String encoding; 088 089 /** 090 * A flag to disable generation of the <code>plugin.xml</code> in favor of a hand authored plugin descriptor. 091 * 092 * @since 2.6 093 */ 094 @Parameter(defaultValue = "false") 095 private boolean skipDescriptor; 096 097 /** 098 * <p> 099 * The role names of mojo extractors to use. 100 * </p> 101 * <p> 102 * If not set, all mojo extractors will be used. If set to an empty extractor name, no mojo extractors 103 * will be used. 104 * </p> 105 * Example: 106 * <pre> 107 * <!-- Use all mojo extractors --> 108 * <extractors/> 109 * 110 * <!-- Use no mojo extractors --> 111 * <extractors> 112 * <extractor/> 113 * </extractors> 114 * 115 * <!-- Use only bsh mojo extractor --> 116 * <extractors> 117 * <extractor>bsh</extractor> 118 * </extractors> 119 * </pre> 120 * The extractors with the following names ship with {@code maven-plugin-tools}: 121 * <ol> 122 * <li>{@code java-annotations}</li> 123 * <li>{@code java-javadoc}, deprecated</li> 124 * <li>{@code ant}, deprecated</li> 125 * <li>{@code bsh}, deprecated</li> 126 * </ol> 127 */ 128 @Parameter 129 private Set<String> extractors; 130 131 /** 132 * By default, an exception is throw if no mojo descriptor is found. As the maven-plugin is defined in core, the 133 * descriptor generator mojo is bound to generate-resources phase. 134 * But for annotations, the compiled classes are needed, so skip error 135 * 136 * @since 3.0 137 */ 138 @Parameter(property = "maven.plugin.skipErrorNoDescriptorsFound", defaultValue = "false") 139 private boolean skipErrorNoDescriptorsFound; 140 141 /** 142 * Flag controlling is "expected dependencies in provided scope" check to be performed or not. Default value: 143 * {@code true}. 144 * 145 * @since 3.6.3 146 */ 147 @Parameter(defaultValue = "true", property = "maven.plugin.checkExpectedProvidedScope") 148 private boolean checkExpectedProvidedScope = true; 149 150 /** 151 * List of {@code groupId} strings of artifact coordinates that are expected to be in "provided" scope. Default 152 * value: {@code ["org.apache.maven"]}. 153 * 154 * @since 3.6.3 155 */ 156 @Parameter 157 private List<String> expectedProvidedScopeGroupIds = Collections.singletonList("org.apache.maven"); 158 159 /** 160 * List of {@code groupId:artifactId} strings of artifact coordinates that are to be excluded from "expected 161 * provided scope" check. Default value: 162 * {@code ["org.apache.maven:maven-archiver", "org.apache.maven:maven-jxr", "org.apache.maven:plexus-utils"]}. 163 * 164 * @since 3.6.3 165 */ 166 @Parameter 167 private List<String> expectedProvidedScopeExclusions = Arrays.asList( 168 "org.apache.maven:maven-archiver", "org.apache.maven:maven-jxr", "org.apache.maven:plexus-utils"); 169 170 /** 171 * Specify the dependencies as {@code groupId:artifactId} containing (abstract) Mojos, to filter 172 * dependencies scanned at runtime and focus on dependencies that are really useful to Mojo analysis. 173 * By default, the value is {@code null} and all dependencies are scanned (as before this parameter was added). 174 * If specified in the configuration with no children, no dependencies are scanned. 175 * 176 * @since 3.5 177 */ 178 @Parameter 179 private List<String> mojoDependencies = null; 180 181 /** 182 * Creates links to existing external javadoc-generated documentation. 183 * <br> 184 * <b>Notes</b>: 185 * all given links should have a fetchable {@code /package-list} or {@code /element-list} file. 186 * For instance: 187 * <pre> 188 * <externalJavadocBaseUrls> 189 * <externalJavadocBaseUrl>https://docs.oracle.com/javase/8/docs/api/</externalJavadocBaseUrl> 190 * </externalJavadocBaseUrls> 191 * </pre> 192 * is valid because <code>https://docs.oracle.com/javase/8/docs/api/package-list</code> exists. 193 * See <a href="https://docs.oracle.com/en/java/javase/17/docs/specs/man/javadoc.html#standard-doclet-options"> 194 * link option of the javadoc tool</a>. 195 * Using this parameter requires connectivity to the given URLs during the goal execution. 196 * @since 3.7.0 197 */ 198 @Parameter(property = "externalJavadocBaseUrls", alias = "links") 199 protected List<URI> externalJavadocBaseUrls; 200 201 /** 202 * The base URL for the Javadoc site containing the current project's API documentation. 203 * This may be relative to the root of the generated Maven site. 204 * It does not need to exist yet at the time when this goal is executed. 205 * Must end with a slash. 206 * <b>In case this is set the javadoc reporting goal should be executed prior to 207 * <a href="../maven-plugin-report-plugin/index.html">Plugin Report</a>.</b> 208 * @since 3.7.0 209 */ 210 @Parameter(property = "internalJavadocBaseUrl") 211 protected URI internalJavadocBaseUrl; 212 213 /** 214 * The version of the javadoc tool (equal to the container JDK version) used to generate the internal javadoc 215 * Only relevant if {@link #internalJavadocBaseUrl} is set. 216 * The default value needs to be overwritten in case toolchains are being used for generating Javadoc. 217 * 218 * @since 3.7.0 219 */ 220 @Parameter(property = "internalJavadocVersion", defaultValue = "${java.version}") 221 protected String internalJavadocVersion; 222 223 @Component 224 private MavenSession mavenSession; 225 226 /** 227 * The required Java version to set in the plugin descriptor. This is evaluated by Maven 4 and ignored by earlier 228 * Maven versions. Can be either one of the following formats: 229 * 230 * <ul> 231 * <li>A version range which specifies the supported Java versions. It can either use the usual mathematical 232 * syntax like {@code "[1.7,9),[11,)"} or use a single version like {@code "1.8"}. The latter is a short 233 * form for {@code "[1.8,)"}, i.e. denotes the minimum version required.</li> 234 * <li>{@code "auto"} to determine the minimum Java version from the binary class version being generated during 235 * compilation (determined by the extractor).</li> 236 * </ul> 237 * 238 * @since 3.8.0 239 */ 240 @Parameter(defaultValue = VALUE_AUTO) 241 String requiredJavaVersion; 242 243 /** 244 * The required Maven version to set in the plugin descriptor. This is evaluated by Maven 4 and ignored by earlier 245 * Maven versions. Can be either one of the following formats: 246 * 247 * <ul> 248 * <li>A version range which specifies the supported Maven versions. It can either use the usual mathematical 249 * 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 250 * form for {@code "[2.2.1,)"}, i.e. denotes the minimum version required.</li> 251 * <li>{@code "auto"} to determine the minimum Maven version from the POM's Maven prerequisite, or if not set the 252 * referenced Maven Plugin API version.</li> 253 * </ul> 254 * This value takes precedence over the 255 * <a href="https://maven.apache.org/pom.html#Prerequisites">POM's Maven prerequisite</a> in Maven 4. 256 * 257 * @since 3.8.0 258 */ 259 @Parameter(defaultValue = VALUE_AUTO) 260 String requiredMavenVersion; 261 262 /** 263 * The component used for scanning the source tree for mojos. 264 */ 265 @Component 266 private MojoScanner mojoScanner; 267 268 @Component 269 protected BuildContext buildContext; 270 271 public void generate() throws MojoExecutionException { 272 273 if (!"maven-plugin".equalsIgnoreCase(project.getArtifactId()) 274 && project.getArtifactId().toLowerCase().startsWith("maven-") 275 && project.getArtifactId().toLowerCase().endsWith("-plugin") 276 && !"org.apache.maven.plugins".equals(project.getGroupId())) { 277 getLog().warn(LS + LS + "Artifact Ids of the format maven-___-plugin are reserved for" + LS 278 + "plugins in the Group Id org.apache.maven.plugins" + LS 279 + "Please change your artifactId to the format ___-maven-plugin" + LS 280 + "In the future this error will break the build." + LS + LS); 281 } 282 283 if (skipDescriptor) { 284 getLog().warn("Execution skipped"); 285 return; 286 } 287 288 if (checkExpectedProvidedScope) { 289 Set<Artifact> wrongScopedArtifacts = dependenciesNotInProvidedScope(); 290 if (!wrongScopedArtifacts.isEmpty()) { 291 StringBuilder message = new StringBuilder( 292 LS + LS + "Some dependencies of Maven Plugins are expected to be in provided scope." + LS 293 + "Please make sure that dependencies listed below declared in POM" + LS 294 + "have set '<scope>provided</scope>' as well." + LS + LS 295 + "The following dependencies are in wrong scope:" + LS); 296 for (Artifact artifact : wrongScopedArtifacts) { 297 message.append(" * ").append(artifact).append(LS); 298 } 299 message.append(LS).append(LS); 300 301 getLog().warn(message.toString()); 302 } 303 } 304 305 mojoScanner.setActiveExtractors(extractors); 306 307 // TODO: could use this more, eg in the writing of the plugin descriptor! 308 PluginDescriptor pluginDescriptor = new PluginDescriptor(); 309 310 pluginDescriptor.setGroupId(project.getGroupId()); 311 312 pluginDescriptor.setArtifactId(project.getArtifactId()); 313 314 pluginDescriptor.setVersion(project.getVersion()); 315 316 pluginDescriptor.setGoalPrefix(goalPrefix); 317 318 pluginDescriptor.setName(project.getName()); 319 320 pluginDescriptor.setDescription(project.getDescription()); 321 322 if (encoding == null || encoding.length() < 1) { 323 getLog().warn("Using platform encoding (" + ReaderFactory.FILE_ENCODING 324 + " actually) to read mojo source files, i.e. build is platform dependent!"); 325 } else { 326 getLog().info("Using '" + encoding + "' encoding to read mojo source files."); 327 } 328 329 if (internalJavadocBaseUrl != null && !internalJavadocBaseUrl.getPath().endsWith("/")) { 330 throw new MojoExecutionException("Given parameter 'internalJavadocBaseUrl' must end with a slash but is '" 331 + internalJavadocBaseUrl + "'"); 332 } 333 try { 334 List<ComponentDependency> deps = GeneratorUtils.toComponentDependencies(project.getArtifacts()); 335 pluginDescriptor.setDependencies(deps); 336 337 PluginToolsRequest request = new DefaultPluginToolsRequest(project, pluginDescriptor); 338 request.setEncoding(encoding); 339 request.setSkipErrorNoDescriptorsFound(skipErrorNoDescriptorsFound); 340 request.setDependencies(filterMojoDependencies()); 341 request.setRepoSession(mavenSession.getRepositorySession()); 342 request.setInternalJavadocBaseUrl(internalJavadocBaseUrl); 343 request.setInternalJavadocVersion(internalJavadocVersion); 344 request.setExternalJavadocBaseUrls(externalJavadocBaseUrls); 345 request.setSettings(mavenSession.getSettings()); 346 347 mojoScanner.populatePluginDescriptor(request); 348 request.setPluginDescriptor(extendPluginDescriptor(request)); 349 350 outputDirectory.mkdirs(); 351 352 PluginDescriptorFilesGenerator pluginDescriptorGenerator = new PluginDescriptorFilesGenerator(); 353 pluginDescriptorGenerator.execute(outputDirectory, request); 354 355 buildContext.refresh(outputDirectory); 356 } catch (GeneratorException e) { 357 throw new MojoExecutionException("Error writing plugin descriptor", e); 358 } catch (InvalidPluginDescriptorException | ExtractionException e) { 359 throw new MojoExecutionException( 360 "Error extracting plugin descriptor: '" + e.getLocalizedMessage() + "'", e); 361 } catch (LinkageError e) { 362 throw new MojoExecutionException( 363 "The API of the mojo scanner is not compatible with this plugin version." 364 + " Please check the plugin dependencies configured" 365 + " in the POM and ensure the versions match.", 366 e); 367 } 368 } 369 370 private PluginDescriptor extendPluginDescriptor(PluginToolsRequest request) { 371 ExtendedPluginDescriptor extendedPluginDescriptor = new ExtendedPluginDescriptor(request.getPluginDescriptor()); 372 extendedPluginDescriptor.setRequiredJavaVersion(getRequiredJavaVersion(request)); 373 extendedPluginDescriptor.setRequiredMavenVersion(getRequiredMavenVersion(request)); 374 return extendedPluginDescriptor; 375 } 376 377 private String getRequiredMavenVersion(PluginToolsRequest request) { 378 if (!VALUE_AUTO.equals(requiredMavenVersion)) { 379 return requiredMavenVersion; 380 } 381 getLog().debug("Trying to derive Maven version automatically from project prerequisites..."); 382 String requiredMavenVersion = 383 project.getPrerequisites() != null ? project.getPrerequisites().getMaven() : null; 384 if (requiredMavenVersion == null) { 385 getLog().debug("Trying to derive Maven version automatically from referenced Maven Plugin API artifact " 386 + "version..."); 387 requiredMavenVersion = request.getUsedMavenApiVersion(); 388 } 389 if (requiredMavenVersion == null) { 390 getLog().warn("Cannot determine the required Maven version automatically, it is recommended to " 391 + "configure some explicit value manually."); 392 } 393 return requiredMavenVersion; 394 } 395 396 private String getRequiredJavaVersion(PluginToolsRequest request) { 397 if (!VALUE_AUTO.equals(requiredJavaVersion)) { 398 return requiredJavaVersion; 399 } 400 String minRequiredJavaVersion = request.getRequiredJavaVersion(); 401 if (minRequiredJavaVersion == null) { 402 getLog().warn("Cannot determine the minimally required Java version automatically, it is recommended to " 403 + "configure some explicit value manually."); 404 return null; 405 } 406 407 return minRequiredJavaVersion; 408 } 409 410 /** 411 * Collects all dependencies expected to be in "provided" scope but are NOT in "provided" scope. 412 */ 413 private Set<Artifact> dependenciesNotInProvidedScope() { 414 LinkedHashSet<Artifact> wrongScopedDependencies = new LinkedHashSet<>(); 415 416 for (Artifact dependency : project.getArtifacts()) { 417 String ga = dependency.getGroupId() + ":" + dependency.getArtifactId(); 418 if (expectedProvidedScopeGroupIds.contains(dependency.getGroupId()) 419 && !expectedProvidedScopeExclusions.contains(ga) 420 && !Artifact.SCOPE_PROVIDED.equals(dependency.getScope())) { 421 wrongScopedDependencies.add(dependency); 422 } 423 } 424 425 return wrongScopedDependencies; 426 } 427 428 /** 429 * Get dependencies filtered with mojoDependencies configuration. 430 * 431 * @return eventually filtered dependencies, or even <code>null</code> if configured with empty mojoDependencies 432 * list 433 * @see #mojoDependencies 434 */ 435 private Set<Artifact> filterMojoDependencies() { 436 Set<Artifact> filteredArtifacts; 437 if (mojoDependencies == null) { 438 filteredArtifacts = new LinkedHashSet<>(project.getArtifacts()); 439 } else if (mojoDependencies.isEmpty()) { 440 filteredArtifacts = null; 441 } else { 442 filteredArtifacts = new LinkedHashSet<>(); 443 444 ArtifactFilter filter = new IncludesArtifactFilter(mojoDependencies); 445 446 for (Artifact artifact : project.getArtifacts()) { 447 if (filter.include(artifact)) { 448 filteredArtifacts.add(artifact); 449 } 450 } 451 } 452 453 return filteredArtifacts; 454 } 455}