1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19 package org.apache.maven.plugin.plugin;
20
21 import javax.inject.Inject;
22
23 import java.io.File;
24 import java.io.IOException;
25 import java.io.InputStream;
26 import java.io.OutputStream;
27 import java.net.URI;
28 import java.nio.charset.StandardCharsets;
29 import java.nio.file.Files;
30 import java.nio.file.Path;
31 import java.util.*;
32 import java.util.stream.Collectors;
33 import java.util.stream.Stream;
34
35 import org.apache.maven.artifact.Artifact;
36 import org.apache.maven.artifact.resolver.filter.ArtifactFilter;
37 import org.apache.maven.artifact.resolver.filter.IncludesArtifactFilter;
38 import org.apache.maven.execution.MavenSession;
39 import org.apache.maven.plugin.MojoExecutionException;
40 import org.apache.maven.plugin.descriptor.InvalidPluginDescriptorException;
41 import org.apache.maven.plugin.descriptor.MojoDescriptor;
42 import org.apache.maven.plugin.descriptor.PluginDescriptor;
43 import org.apache.maven.plugins.annotations.LifecyclePhase;
44 import org.apache.maven.plugins.annotations.Mojo;
45 import org.apache.maven.plugins.annotations.Parameter;
46 import org.apache.maven.plugins.annotations.ResolutionScope;
47 import org.apache.maven.project.MavenProject;
48 import org.apache.maven.tools.plugin.DefaultPluginToolsRequest;
49 import org.apache.maven.tools.plugin.ExtendedMojoDescriptor;
50 import org.apache.maven.tools.plugin.PluginDescriptorHelper;
51 import org.apache.maven.tools.plugin.PluginToolsRequest;
52 import org.apache.maven.tools.plugin.extractor.ExtractionException;
53 import org.apache.maven.tools.plugin.generator.GeneratorException;
54 import org.apache.maven.tools.plugin.generator.GeneratorUtils;
55 import org.apache.maven.tools.plugin.generator.PluginDescriptorFilesGenerator;
56 import org.apache.maven.tools.plugin.scanner.MojoScanner;
57 import org.codehaus.plexus.component.repository.ComponentDependency;
58 import org.codehaus.plexus.util.ReaderFactory;
59 import org.codehaus.plexus.util.io.CachingOutputStream;
60 import org.codehaus.plexus.util.io.CachingWriter;
61 import org.objectweb.asm.*;
62 import org.sonatype.plexus.build.incremental.BuildContext;
63
64 import static org.objectweb.asm.Opcodes.*;
65 import static org.objectweb.asm.Opcodes.ACC_PUBLIC;
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81 @Mojo(
82 name = "descriptor",
83 defaultPhase = LifecyclePhase.PROCESS_CLASSES,
84 requiresDependencyResolution = ResolutionScope.COMPILE_PLUS_RUNTIME,
85 threadSafe = true)
86 public class DescriptorGeneratorMojo extends AbstractGeneratorMojo {
87 private static final String VALUE_AUTO = "auto";
88
89
90
91
92 @Parameter(defaultValue = "${project.build.outputDirectory}/META-INF/maven", readonly = true)
93 private File outputDirectory;
94
95
96
97
98 @Parameter(defaultValue = "${project.build.outputDirectory}", readonly = true)
99 private File classesOutputDirectory;
100
101
102
103
104
105
106 @Parameter(property = "encoding", defaultValue = "${project.build.sourceEncoding}")
107 private String encoding;
108
109
110
111
112
113
114 @Parameter(defaultValue = "false")
115 private boolean skipDescriptor;
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148 @Parameter
149 private Set<String> extractors;
150
151
152
153
154
155
156
157
158 @Parameter(property = "maven.plugin.skipErrorNoDescriptorsFound", defaultValue = "false")
159 private boolean skipErrorNoDescriptorsFound;
160
161
162
163
164
165
166
167 @Parameter(defaultValue = "true", property = "maven.plugin.checkExpectedProvidedScope")
168 private boolean checkExpectedProvidedScope = true;
169
170
171
172
173
174
175
176 @Parameter
177 private List<String> expectedProvidedScopeGroupIds = Collections.singletonList("org.apache.maven");
178
179
180
181
182
183
184
185
186 @Parameter
187 private List<String> expectedProvidedScopeExclusions = Arrays.asList(
188 "org.apache.maven:maven-archiver", "org.apache.maven:maven-jxr", "org.apache.maven:plexus-utils");
189
190
191
192
193
194
195
196
197
198 @Parameter
199 private List<String> mojoDependencies = null;
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218 @Parameter(property = "externalJavadocBaseUrls", alias = "links")
219 protected List<URI> externalJavadocBaseUrls;
220
221
222
223
224
225
226
227
228
229
230 @Parameter(property = "internalJavadocBaseUrl")
231 protected URI internalJavadocBaseUrl;
232
233
234
235
236
237
238
239
240 @Parameter(property = "internalJavadocVersion", defaultValue = "${java.version}")
241 protected String internalJavadocVersion;
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257 @Parameter(defaultValue = VALUE_AUTO)
258 String requiredJavaVersion;
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276 @Parameter(defaultValue = VALUE_AUTO)
277 String requiredMavenVersion;
278
279 private final MavenSession mavenSession;
280
281
282
283
284 private final MojoScanner mojoScanner;
285
286 protected final BuildContext buildContext;
287
288 @Inject
289 public DescriptorGeneratorMojo(
290 MavenProject project, MavenSession mavenSession, MojoScanner mojoScanner, BuildContext buildContext) {
291 super(project);
292 this.mavenSession = mavenSession;
293 this.mojoScanner = mojoScanner;
294 this.buildContext = buildContext;
295 }
296
297 public void generate() throws MojoExecutionException {
298 if (!"maven-plugin".equalsIgnoreCase(project.getArtifactId())
299 && project.getArtifactId().toLowerCase().startsWith("maven-")
300 && project.getArtifactId().toLowerCase().endsWith("-plugin")
301 && !"org.apache.maven.plugins".equals(project.getGroupId())) {
302 getLog().warn(LS + LS + "Artifact Ids of the format maven-___-plugin are reserved for" + LS
303 + "plugins in the Group Id org.apache.maven.plugins" + LS
304 + "Please change your artifactId to the format ___-maven-plugin" + LS
305 + "In the future this error will break the build." + LS + LS);
306 }
307
308 if (skipDescriptor) {
309 getLog().warn("Execution skipped");
310 return;
311 }
312
313 if (checkExpectedProvidedScope) {
314 Set<Artifact> wrongScopedArtifacts = dependenciesNotInProvidedScope();
315 if (!wrongScopedArtifacts.isEmpty()) {
316 StringBuilder message = new StringBuilder(
317 LS + LS + "Some dependencies of Maven Plugins are expected to be in provided scope." + LS
318 + "Please make sure that dependencies listed below declared in POM" + LS
319 + "have set '<scope>provided</scope>' as well." + LS + LS
320 + "The following dependencies are in wrong scope:" + LS);
321 for (Artifact artifact : wrongScopedArtifacts) {
322 message.append(" * ").append(artifact).append(LS);
323 }
324 message.append(LS).append(LS);
325
326 getLog().warn(message.toString());
327 }
328 }
329
330 mojoScanner.setActiveExtractors(extractors);
331
332
333 PluginDescriptor pluginDescriptor = new PluginDescriptor();
334
335 pluginDescriptor.setGroupId(project.getGroupId());
336
337 pluginDescriptor.setArtifactId(project.getArtifactId());
338
339 pluginDescriptor.setVersion(project.getVersion());
340
341 pluginDescriptor.setGoalPrefix(goalPrefix);
342
343 pluginDescriptor.setName(project.getName());
344
345 pluginDescriptor.setDescription(project.getDescription());
346
347 if (encoding == null || encoding.length() < 1) {
348 getLog().warn("Using platform encoding (" + ReaderFactory.FILE_ENCODING
349 + " actually) to read mojo source files, i.e. build is platform dependent!");
350 } else {
351 getLog().info("Using '" + encoding + "' encoding to read mojo source files.");
352 }
353
354 if (internalJavadocBaseUrl != null && !internalJavadocBaseUrl.getPath().endsWith("/")) {
355 throw new MojoExecutionException("Given parameter 'internalJavadocBaseUrl' must end with a slash but is '"
356 + internalJavadocBaseUrl + "'");
357 }
358 try {
359 List<ComponentDependency> deps = GeneratorUtils.toComponentDependencies(project.getArtifacts());
360 pluginDescriptor.setDependencies(deps);
361
362 PluginToolsRequest request = new DefaultPluginToolsRequest(project, pluginDescriptor);
363 request.setEncoding(encoding);
364 request.setSkipErrorNoDescriptorsFound(skipErrorNoDescriptorsFound);
365 request.setDependencies(filterMojoDependencies());
366 request.setRepoSession(mavenSession.getRepositorySession());
367 request.setInternalJavadocBaseUrl(internalJavadocBaseUrl);
368 request.setInternalJavadocVersion(internalJavadocVersion);
369 request.setExternalJavadocBaseUrls(externalJavadocBaseUrls);
370 request.setSettings(mavenSession.getSettings());
371
372 mojoScanner.populatePluginDescriptor(request);
373 request.setPluginDescriptor(extendPluginDescriptor(request));
374
375 outputDirectory.mkdirs();
376
377 PluginDescriptorFilesGenerator pluginDescriptorGenerator = new PluginDescriptorFilesGenerator();
378 pluginDescriptorGenerator.execute(outputDirectory, request);
379
380
381 generateFactories(request.getPluginDescriptor());
382
383
384 generateIndex();
385
386 buildContext.refresh(outputDirectory);
387 } catch (GeneratorException e) {
388 throw new MojoExecutionException("Error writing plugin descriptor", e);
389 } catch (InvalidPluginDescriptorException | ExtractionException e) {
390 throw new MojoExecutionException(
391 "Error extracting plugin descriptor: '" + e.getLocalizedMessage() + "'", e);
392 } catch (LinkageError e) {
393 throw new MojoExecutionException(
394 "The API of the mojo scanner is not compatible with this plugin version."
395 + " Please check the plugin dependencies configured"
396 + " in the POM and ensure the versions match.",
397 e);
398 }
399 }
400
401 private void generateIndex() throws GeneratorException {
402 try {
403 Set<String> diBeans = new TreeSet<>();
404 try (Stream<Path> paths = Files.walk(classesOutputDirectory.toPath())) {
405 List<Path> classes = paths.filter(
406 p -> p.getFileName().toString().endsWith(".class"))
407 .collect(Collectors.toList());
408 for (Path classFile : classes) {
409 String fileString = classFile.toString();
410 String className = fileString
411 .substring(0, fileString.length() - ".class".length())
412 .replace('/', '.');
413 try (InputStream is = Files.newInputStream(classFile)) {
414 ClassReader rdr = new ClassReader(is);
415 rdr.accept(
416 new ClassVisitor(Opcodes.ASM9) {
417 String className;
418
419 @Override
420 public void visit(
421 int version,
422 int access,
423 String name,
424 String signature,
425 String superName,
426 String[] interfaces) {
427 super.visit(version, access, name, signature, superName, interfaces);
428 className = name;
429 }
430
431 @Override
432 public AnnotationVisitor visitAnnotation(String descriptor, boolean visible) {
433 if ("Lorg/apache/maven/api/di/Named;".equals(descriptor)) {
434 diBeans.add(className.replace('/', '.'));
435 }
436 return null;
437 }
438 },
439 ClassReader.SKIP_FRAMES | ClassReader.SKIP_CODE | ClassReader.SKIP_DEBUG);
440 }
441
442
443
444
445
446
447
448
449
450 }
451 }
452 Path path = outputDirectory.toPath().resolve("org.apache.maven.api.di.Inject");
453 if (diBeans.isEmpty()) {
454 Files.deleteIfExists(path);
455 } else {
456 String nl = System.lineSeparator();
457 try (CachingWriter w = new CachingWriter(path, StandardCharsets.UTF_8)) {
458 String content = diBeans.stream().collect(Collectors.joining(nl, "", nl));
459 w.write(content);
460 }
461 }
462 } catch (Exception e) {
463 throw new GeneratorException("Unable to generate index for v4 beans", e);
464 }
465 }
466
467 private void generateFactories(PluginDescriptor pd) throws GeneratorException {
468 try {
469 for (MojoDescriptor md : pd.getMojos()) {
470 if (md instanceof ExtendedMojoDescriptor && ((ExtendedMojoDescriptor) md).isV4Api()) {
471 generateFactory(md);
472 }
473 }
474 } catch (IOException e) {
475 throw new GeneratorException("Unable to generate factories for v4 mojos", e);
476 }
477 }
478
479 private void generateFactory(MojoDescriptor md) throws IOException {
480 String mojoClassName = md.getImplementation();
481 String packageName = mojoClassName.substring(0, mojoClassName.lastIndexOf('.'));
482 String generatorClassName = mojoClassName.substring(mojoClassName.lastIndexOf('.') + 1) + "Factory";
483 String mojoName = md.getId();
484
485 getLog().debug("Generating v4 factory for " + mojoClassName);
486
487 byte[] bin = computeGeneratorClassBytes(packageName, generatorClassName, mojoName, mojoClassName);
488
489 try (OutputStream os = new CachingOutputStream(classesOutputDirectory
490 .toPath()
491 .resolve(packageName.replace('.', '/') + "/" + generatorClassName + ".class"))) {
492 os.write(bin);
493 }
494 }
495
496 static byte[] computeGeneratorClassBytes(
497 String packageName, String generatorClassName, String mojoName, String mojoClassName) {
498 String mojo = mojoClassName.replace('.', '/');
499 ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES | ClassWriter.COMPUTE_MAXS);
500 cw.visitSource(generatorClassName + ".java", null);
501 AnnotationVisitor av = cw.visitAnnotation("Lorg/apache/maven/api/di/Named;", true);
502 av.visit("value", mojoName);
503 av.visitEnd();
504 cw.visitAnnotation("Lorg/apache/maven/api/annotations/Generated;", true).visitEnd();
505 cw.visit(
506 V1_8,
507 ACC_PUBLIC + ACC_SUPER + ACC_SYNTHETIC,
508 packageName.replace(".", "/") + "/" + generatorClassName,
509 null,
510 mojo,
511 null);
512 MethodVisitor mv = cw.visitMethod(ACC_PUBLIC + ACC_SYNTHETIC, "<init>", "()V", null, null);
513 mv.visitCode();
514 mv.visitVarInsn(Opcodes.ALOAD, 0);
515 mv.visitMethodInsn(Opcodes.INVOKESPECIAL, mojo, "<init>", "()V", false);
516 mv.visitInsn(Opcodes.RETURN);
517 mv.visitMaxs(-1, -1);
518 mv.visitEnd();
519 cw.visitEnd();
520 return cw.toByteArray();
521 }
522
523 private PluginDescriptor extendPluginDescriptor(PluginToolsRequest request) {
524 PluginDescriptor pluginDescriptor = request.getPluginDescriptor();
525 pluginDescriptor.setRequiredMavenVersion(getRequiredMavenVersion(request));
526 return PluginDescriptorHelper.setRequiredJavaVersion(pluginDescriptor, getRequiredJavaVersion(request));
527 }
528
529 private String getRequiredMavenVersion(PluginToolsRequest request) {
530 if (!VALUE_AUTO.equals(requiredMavenVersion)) {
531 return requiredMavenVersion;
532 }
533 getLog().debug("Trying to derive Maven version automatically from project prerequisites...");
534 String requiredMavenVersion =
535 project.getPrerequisites() != null ? project.getPrerequisites().getMaven() : null;
536 if (requiredMavenVersion == null) {
537 getLog().debug("Trying to derive Maven version automatically from referenced Maven Plugin API artifact "
538 + "version...");
539 requiredMavenVersion = request.getUsedMavenApiVersion();
540 }
541 if (requiredMavenVersion == null) {
542 getLog().warn("Cannot determine the required Maven version automatically, it is recommended to "
543 + "configure some explicit value manually.");
544 }
545 return requiredMavenVersion;
546 }
547
548 private String getRequiredJavaVersion(PluginToolsRequest request) {
549 if (!VALUE_AUTO.equals(requiredJavaVersion)) {
550 return requiredJavaVersion;
551 }
552 String minRequiredJavaVersion = request.getRequiredJavaVersion();
553 if (minRequiredJavaVersion == null) {
554 getLog().warn("Cannot determine the minimally required Java version automatically, it is recommended to "
555 + "configure some explicit value manually.");
556 return null;
557 }
558
559 return minRequiredJavaVersion;
560 }
561
562
563
564
565 private Set<Artifact> dependenciesNotInProvidedScope() {
566 LinkedHashSet<Artifact> wrongScopedDependencies = new LinkedHashSet<>();
567
568 for (Artifact dependency : project.getArtifacts()) {
569 String ga = dependency.getGroupId() + ":" + dependency.getArtifactId();
570 if (expectedProvidedScopeGroupIds.contains(dependency.getGroupId())
571 && !expectedProvidedScopeExclusions.contains(ga)
572 && !Artifact.SCOPE_PROVIDED.equals(dependency.getScope())) {
573 wrongScopedDependencies.add(dependency);
574 }
575 }
576
577 return wrongScopedDependencies;
578 }
579
580
581
582
583
584
585
586
587 private Set<Artifact> filterMojoDependencies() {
588 Set<Artifact> filteredArtifacts;
589 if (mojoDependencies == null) {
590 filteredArtifacts = new LinkedHashSet<>(project.getArtifacts());
591 } else if (mojoDependencies.isEmpty()) {
592 filteredArtifacts = null;
593 } else {
594 filteredArtifacts = new LinkedHashSet<>();
595
596 ArtifactFilter filter = new IncludesArtifactFilter(mojoDependencies);
597
598 for (Artifact artifact : project.getArtifacts()) {
599 if (filter.include(artifact)) {
600 filteredArtifacts.add(artifact);
601 }
602 }
603 }
604
605 return filteredArtifacts;
606 }
607 }