1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19 package org.apache.maven.api.plugin.testing;
20
21 import java.io.*;
22 import java.lang.reflect.AccessibleObject;
23 import java.lang.reflect.AnnotatedElement;
24 import java.lang.reflect.Field;
25 import java.net.URL;
26 import java.nio.file.Files;
27 import java.nio.file.Path;
28 import java.nio.file.Paths;
29 import java.util.*;
30 import java.util.stream.Collectors;
31 import java.util.stream.Stream;
32
33 import org.apache.maven.api.MojoExecution;
34 import org.apache.maven.api.Project;
35 import org.apache.maven.api.Session;
36 import org.apache.maven.api.di.Named;
37 import org.apache.maven.api.di.Priority;
38 import org.apache.maven.api.di.Provides;
39 import org.apache.maven.api.di.Singleton;
40 import org.apache.maven.api.di.testing.MavenDIExtension;
41 import org.apache.maven.api.model.Build;
42 import org.apache.maven.api.model.ConfigurationContainer;
43 import org.apache.maven.api.model.Model;
44 import org.apache.maven.api.plugin.Log;
45 import org.apache.maven.api.plugin.Mojo;
46 import org.apache.maven.api.plugin.descriptor.MojoDescriptor;
47 import org.apache.maven.api.plugin.descriptor.Parameter;
48 import org.apache.maven.api.plugin.descriptor.PluginDescriptor;
49 import org.apache.maven.api.plugin.testing.stubs.*;
50 import org.apache.maven.api.services.ArtifactDeployer;
51 import org.apache.maven.api.services.ArtifactFactory;
52 import org.apache.maven.api.services.ArtifactInstaller;
53 import org.apache.maven.api.services.ArtifactManager;
54 import org.apache.maven.api.services.LocalRepositoryManager;
55 import org.apache.maven.api.services.ProjectBuilder;
56 import org.apache.maven.api.services.ProjectManager;
57 import org.apache.maven.api.services.RepositoryFactory;
58 import org.apache.maven.api.services.VersionParser;
59 import org.apache.maven.api.services.xml.ModelXmlFactory;
60 import org.apache.maven.api.xml.XmlNode;
61 import org.apache.maven.configuration.internal.EnhancedComponentConfigurator;
62 import org.apache.maven.di.Injector;
63 import org.apache.maven.di.Key;
64 import org.apache.maven.di.impl.DIException;
65 import org.apache.maven.internal.impl.DefaultLog;
66 import org.apache.maven.internal.impl.InternalSession;
67 import org.apache.maven.internal.impl.model.DefaultModelPathTranslator;
68 import org.apache.maven.internal.impl.model.DefaultPathTranslator;
69 import org.apache.maven.internal.xml.XmlNodeImpl;
70 import org.apache.maven.internal.xml.XmlPlexusConfiguration;
71 import org.apache.maven.lifecycle.internal.MojoDescriptorCreator;
72 import org.apache.maven.model.v4.MavenMerger;
73 import org.apache.maven.model.v4.MavenStaxReader;
74 import org.apache.maven.plugin.PluginParameterExpressionEvaluatorV4;
75 import org.apache.maven.plugin.descriptor.io.PluginDescriptorStaxReader;
76 import org.codehaus.plexus.component.configurator.expression.ExpressionEvaluationException;
77 import org.codehaus.plexus.component.configurator.expression.ExpressionEvaluator;
78 import org.codehaus.plexus.component.configurator.expression.TypeAwareExpressionEvaluator;
79 import org.codehaus.plexus.util.ReflectionUtils;
80 import org.codehaus.plexus.util.xml.XmlStreamReader;
81 import org.codehaus.plexus.util.xml.Xpp3Dom;
82 import org.codehaus.plexus.util.xml.Xpp3DomBuilder;
83 import org.eclipse.aether.RepositorySystem;
84 import org.junit.jupiter.api.extension.*;
85 import org.junit.platform.commons.support.AnnotationSupport;
86 import org.slf4j.LoggerFactory;
87
88 import static java.util.Objects.requireNonNull;
89
90
91
92
93
94
95
96
97
98
99 public class MojoExtension extends MavenDIExtension implements ParameterResolver, BeforeEachCallback {
100
101 protected static String pluginBasedir;
102 protected static String basedir;
103
104 public static String getTestId() {
105 return context.getRequiredTestClass().getSimpleName() + "-"
106 + context.getRequiredTestMethod().getName();
107 }
108
109 public static String getBasedir() {
110 return requireNonNull(basedir != null ? basedir : MavenDIExtension.basedir);
111 }
112
113 public static String getPluginBasedir() {
114 return requireNonNull(pluginBasedir);
115 }
116
117 @Override
118 public boolean supportsParameter(ParameterContext parameterContext, ExtensionContext extensionContext)
119 throws ParameterResolutionException {
120 return parameterContext.isAnnotated(InjectMojo.class)
121 || parameterContext.getDeclaringExecutable().isAnnotationPresent(InjectMojo.class);
122 }
123
124 @Override
125 public Object resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext)
126 throws ParameterResolutionException {
127 try {
128 Class<?> holder = parameterContext.getTarget().get().getClass();
129 PluginDescriptor descriptor = extensionContext
130 .getStore(ExtensionContext.Namespace.GLOBAL)
131 .get(PluginDescriptor.class, PluginDescriptor.class);
132 Model model =
133 extensionContext.getStore(ExtensionContext.Namespace.GLOBAL).get(Model.class, Model.class);
134 InjectMojo parameterInjectMojo =
135 parameterContext.getAnnotatedElement().getAnnotation(InjectMojo.class);
136 String goal;
137 if (parameterInjectMojo != null) {
138 String pom = parameterInjectMojo.pom();
139 if (pom != null && !pom.isEmpty()) {
140 try (Reader r = openPomUrl(holder, pom, new Path[1])) {
141 Model localModel = new MavenStaxReader().read(r);
142 model = new MavenMerger().merge(localModel, model, false, null);
143 model = new DefaultModelPathTranslator(new DefaultPathTranslator())
144 .alignToBaseDirectory(model, Paths.get(getBasedir()), null);
145 }
146 }
147 goal = parameterInjectMojo.goal();
148 } else {
149 InjectMojo methodInjectMojo = AnnotationSupport.findAnnotation(
150 parameterContext.getDeclaringExecutable(), InjectMojo.class)
151 .orElse(null);
152 if (methodInjectMojo != null) {
153 goal = methodInjectMojo.goal();
154 } else {
155 goal = getGoalFromMojoImplementationClass(
156 parameterContext.getParameter().getType());
157 }
158 }
159
160 Set<MojoParameter> mojoParameters = new LinkedHashSet<>();
161 for (AnnotatedElement ae :
162 Arrays.asList(parameterContext.getDeclaringExecutable(), parameterContext.getAnnotatedElement())) {
163 mojoParameters.addAll(AnnotationSupport.findRepeatableAnnotations(ae, MojoParameter.class));
164 }
165 String[] coord = mojoCoordinates(goal);
166
167 XmlNode pluginConfiguration = model.getBuild().getPlugins().stream()
168 .filter(p ->
169 Objects.equals(p.getGroupId(), coord[0]) && Objects.equals(p.getArtifactId(), coord[1]))
170 .map(ConfigurationContainer::getConfiguration)
171 .findFirst()
172 .orElseGet(() -> new XmlNodeImpl("config"));
173 List<XmlNode> children = mojoParameters.stream()
174 .map(mp -> new XmlNodeImpl(mp.name(), mp.value()))
175 .collect(Collectors.toList());
176 XmlNode config = new XmlNodeImpl("configuration", null, null, children, null);
177 pluginConfiguration = XmlNode.merge(config, pluginConfiguration);
178
179
180
181 Mojo mojo = lookup(Mojo.class, coord[0] + ":" + coord[1] + ":" + coord[2] + ":" + coord[3]);
182 for (MojoDescriptor mojoDescriptor : descriptor.getMojos()) {
183 if (Objects.equals(mojoDescriptor.getGoal(), coord[3])) {
184 if (pluginConfiguration != null) {
185 pluginConfiguration = finalizeConfig(pluginConfiguration, mojoDescriptor);
186 }
187 }
188 }
189
190 Session session = getInjector().getInstance(Session.class);
191 Project project = getInjector().getInstance(Project.class);
192 MojoExecution mojoExecution = getInjector().getInstance(MojoExecution.class);
193 ExpressionEvaluator evaluator = new WrapEvaluator(
194 getInjector(), new PluginParameterExpressionEvaluatorV4(session, project, mojoExecution));
195
196 EnhancedComponentConfigurator configurator = new EnhancedComponentConfigurator();
197 configurator.configureComponent(
198 mojo, new XmlPlexusConfiguration(pluginConfiguration), evaluator, null, null);
199 return mojo;
200 } catch (Exception e) {
201 throw new ParameterResolutionException("Unable to resolve mojo", e);
202 }
203 }
204
205
206
207
208
209
210 private static String getGoalFromMojoImplementationClass(Class<?> cl) throws IOException {
211 return cl.getAnnotation(Named.class).value();
212 }
213
214 @Override
215 public void beforeEach(ExtensionContext context) throws Exception {
216 if (pluginBasedir == null) {
217 pluginBasedir = MavenDIExtension.getBasedir();
218 }
219 basedir = AnnotationSupport.findAnnotation(context.getElement().get(), Basedir.class)
220 .map(Basedir::value)
221 .orElse(pluginBasedir);
222 if (basedir != null) {
223 if (basedir.isEmpty()) {
224 basedir = pluginBasedir + "/target/tests/"
225 + context.getRequiredTestClass().getSimpleName() + "/"
226 + context.getRequiredTestMethod().getName();
227 } else {
228 basedir = basedir.replace("${basedir}", pluginBasedir);
229 }
230 }
231
232 setContext(context);
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266 Path basedirPath = Paths.get(getBasedir());
267
268 InjectMojo mojo = AnnotationSupport.findAnnotation(context.getElement().get(), InjectMojo.class)
269 .orElse(null);
270 Model defaultModel = Model.newBuilder()
271 .groupId("myGroupId")
272 .artifactId("myArtifactId")
273 .version("1.0-SNAPSHOT")
274 .packaging("jar")
275 .build(Build.newBuilder()
276 .directory(basedirPath.resolve("target").toString())
277 .outputDirectory(basedirPath.resolve("target/classes").toString())
278 .sourceDirectory(basedirPath.resolve("src/main/java").toString())
279 .testSourceDirectory(
280 basedirPath.resolve("src/test/java").toString())
281 .testOutputDirectory(
282 basedirPath.resolve("target/test-classes").toString())
283 .build())
284 .build();
285 Path[] modelPath = new Path[] {null};
286 Model tmodel = null;
287 if (mojo != null) {
288 String pom = mojo.pom();
289 if (pom != null && !pom.isEmpty()) {
290 try (Reader r = openPomUrl(context.getRequiredTestClass(), pom, modelPath)) {
291 tmodel = new MavenStaxReader().read(r);
292 }
293 } else {
294 Path pomPath = basedirPath.resolve("pom.xml");
295 if (Files.exists(pomPath)) {
296 try (Reader r = Files.newBufferedReader(pomPath)) {
297 tmodel = new MavenStaxReader().read(r);
298 modelPath[0] = pomPath;
299 }
300 }
301 }
302 }
303 Model model;
304 if (tmodel == null) {
305 model = defaultModel;
306 } else {
307 model = new MavenMerger().merge(tmodel, defaultModel, false, null);
308 }
309 tmodel = new DefaultModelPathTranslator(new DefaultPathTranslator())
310 .alignToBaseDirectory(tmodel, Paths.get(getBasedir()), null);
311 context.getStore(ExtensionContext.Namespace.GLOBAL).put(Model.class, tmodel);
312
313
314
315 PluginDescriptor pluginDescriptor;
316 ClassLoader classLoader = context.getRequiredTestClass().getClassLoader();
317 try (InputStream is = requireNonNull(
318 classLoader.getResourceAsStream(getPluginDescriptorLocation()),
319 "Unable to find plugin descriptor: " + getPluginDescriptorLocation());
320 Reader reader = new BufferedReader(new XmlStreamReader(is))) {
321
322 pluginDescriptor = new PluginDescriptorStaxReader().read(reader);
323 }
324 context.getStore(ExtensionContext.Namespace.GLOBAL).put(PluginDescriptor.class, pluginDescriptor);
325
326
327
328
329 @SuppressWarnings({"unused", "MagicNumber"})
330 class Foo {
331
332 @Provides
333 @Singleton
334 @Priority(-10)
335 private InternalSession createSession() {
336 return SessionMock.getMockSession(getBasedir());
337 }
338
339 @Provides
340 @Singleton
341 @Priority(-10)
342 private Project createProject(InternalSession s) {
343 ProjectStub stub = new ProjectStub();
344 if (!"pom".equals(model.getPackaging())) {
345 ProducedArtifactStub artifact = new ProducedArtifactStub(
346 model.getGroupId(), model.getArtifactId(), "", model.getVersion(), model.getPackaging());
347 stub.setMainArtifact(artifact);
348 }
349 stub.setModel(model);
350 stub.setBasedir(Paths.get(MojoExtension.getBasedir()));
351 stub.setPomPath(modelPath[0]);
352 s.getService(ArtifactManager.class).setPath(stub.getPomArtifact(), modelPath[0]);
353 return stub;
354 }
355
356 @Provides
357 @Singleton
358 @Priority(-10)
359 private MojoExecution createMojoExecution() {
360 MojoExecutionStub mes = new MojoExecutionStub("executionId", null);
361 if (mojo != null) {
362 String goal = mojo.goal();
363 int idx = goal.lastIndexOf(':');
364 if (idx >= 0) {
365 goal = goal.substring(idx + 1);
366 }
367 mes.setGoal(goal);
368 for (MojoDescriptor md : pluginDescriptor.getMojos()) {
369 if (goal.equals(md.getGoal())) {
370 mes.setDescriptor(md);
371 }
372 }
373 requireNonNull(mes.getDescriptor());
374 }
375 PluginStub plugin = new PluginStub();
376 plugin.setDescriptor(pluginDescriptor);
377 mes.setPlugin(plugin);
378 return mes;
379 }
380
381 @Provides
382 @Singleton
383 @Priority(-10)
384 private Log createLog() {
385 return new DefaultLog(LoggerFactory.getLogger("anonymous"));
386 }
387
388 @Provides
389 static RepositorySystemSupplier newRepositorySystemSupplier() {
390 return new RepositorySystemSupplier();
391 }
392
393 @Provides
394 static RepositorySystem newRepositorySystem(RepositorySystemSupplier repositorySystemSupplier) {
395 return repositorySystemSupplier.getRepositorySystem();
396 }
397
398 @Provides
399 @Priority(10)
400 static RepositoryFactory newRepositoryFactory(Session session) {
401 return session.getService(RepositoryFactory.class);
402 }
403
404 @Provides
405 @Priority(10)
406 static VersionParser newVersionParser(Session session) {
407 return session.getService(VersionParser.class);
408 }
409
410 @Provides
411 @Priority(10)
412 static LocalRepositoryManager newLocalRepositoryManager(Session session) {
413 return session.getService(LocalRepositoryManager.class);
414 }
415
416 @Provides
417 @Priority(10)
418 static ArtifactInstaller newArtifactInstaller(Session session) {
419 return session.getService(ArtifactInstaller.class);
420 }
421
422 @Provides
423 @Priority(10)
424 static ArtifactDeployer newArtifactDeployer(Session session) {
425 return session.getService(ArtifactDeployer.class);
426 }
427
428 @Provides
429 @Priority(10)
430 static ArtifactManager newArtifactManager(Session session) {
431 return session.getService(ArtifactManager.class);
432 }
433
434 @Provides
435 @Priority(10)
436 static ProjectManager newProjectManager(Session session) {
437 return session.getService(ProjectManager.class);
438 }
439
440 @Provides
441 @Priority(10)
442 static ArtifactFactory newArtifactFactory(Session session) {
443 return session.getService(ArtifactFactory.class);
444 }
445
446 @Provides
447 @Priority(10)
448 static ProjectBuilder newProjectBuilder(Session session) {
449 return session.getService(ProjectBuilder.class);
450 }
451
452 @Provides
453 @Priority(10)
454 static ModelXmlFactory newModelXmlFactory(Session session) {
455 return session.getService(ModelXmlFactory.class);
456 }
457 }
458
459 getInjector().bindInstance(Foo.class, new Foo());
460
461 getInjector().injectInstance(context.getRequiredTestInstance());
462
463
464
465
466
467
468
469
470
471
472 }
473
474 private Reader openPomUrl(Class<?> holder, String pom, Path[] modelPath) throws IOException {
475 if (pom.startsWith("file:")) {
476 Path path = Paths.get(getBasedir()).resolve(pom.substring("file:".length()));
477 modelPath[0] = path;
478 return Files.newBufferedReader(path);
479 } else if (pom.startsWith("classpath:")) {
480 URL url = holder.getResource(pom.substring("classpath:".length()));
481 if (url == null) {
482 throw new IllegalStateException("Unable to find pom on classpath: " + pom);
483 }
484 return new XmlStreamReader(url.openStream());
485 } else if (pom.contains("<project>")) {
486 return new StringReader(pom);
487 } else {
488 Path path = Paths.get(getBasedir()).resolve(pom);
489 modelPath[0] = path;
490 return Files.newBufferedReader(path);
491 }
492 }
493
494 protected String getPluginDescriptorLocation() {
495 return "META-INF/maven/plugin.xml";
496 }
497
498 protected String[] mojoCoordinates(String goal) throws Exception {
499 if (goal.matches(".*:.*:.*:.*")) {
500 return goal.split(":");
501 } else {
502 Path pluginPom = Paths.get(getPluginBasedir(), "pom.xml");
503 Xpp3Dom pluginPomDom = Xpp3DomBuilder.build(Files.newBufferedReader(pluginPom));
504 String artifactId = pluginPomDom.getChild("artifactId").getValue();
505 String groupId = resolveFromRootThenParent(pluginPomDom, "groupId");
506 String version = resolveFromRootThenParent(pluginPomDom, "version");
507 return new String[] {groupId, artifactId, version, goal};
508 }
509 }
510
511 private XmlNode finalizeConfig(XmlNode config, MojoDescriptor mojoDescriptor) {
512 List<XmlNode> children = new ArrayList<>();
513 if (mojoDescriptor != null && mojoDescriptor.getParameters() != null) {
514 XmlNode defaultConfiguration;
515 defaultConfiguration = MojoDescriptorCreator.convert(mojoDescriptor);
516 for (Parameter parameter : mojoDescriptor.getParameters()) {
517 XmlNode parameterConfiguration = config.getChild(parameter.getName());
518 if (parameterConfiguration == null) {
519 parameterConfiguration = config.getChild(parameter.getAlias());
520 }
521 XmlNode parameterDefaults = defaultConfiguration.getChild(parameter.getName());
522 parameterConfiguration = XmlNode.merge(parameterConfiguration, parameterDefaults, Boolean.TRUE);
523 if (parameterConfiguration != null) {
524 Map<String, String> attributes = new HashMap<>(parameterConfiguration.getAttributes());
525
526
527
528
529 parameterConfiguration = new XmlNodeImpl(
530 parameter.getName(),
531 parameterConfiguration.getValue(),
532 attributes,
533 parameterConfiguration.getChildren(),
534 parameterConfiguration.getInputLocation());
535
536 children.add(parameterConfiguration);
537 }
538 }
539 }
540 return new XmlNodeImpl("configuration", null, null, children, null);
541 }
542
543 private boolean isEmpty(String str) {
544 return str == null || str.isEmpty();
545 }
546
547 private static Optional<Xpp3Dom> child(Xpp3Dom element, String name) {
548 return Optional.ofNullable(element.getChild(name));
549 }
550
551 private static Stream<Xpp3Dom> children(Xpp3Dom element) {
552 return Stream.of(element.getChildren());
553 }
554
555 public static XmlNode extractPluginConfiguration(String artifactId, Xpp3Dom pomDom) throws Exception {
556 Xpp3Dom pluginConfigurationElement = child(pomDom, "build")
557 .flatMap(buildElement -> child(buildElement, "plugins"))
558 .map(MojoExtension::children)
559 .orElseGet(Stream::empty)
560 .filter(e -> e.getChild("artifactId").getValue().equals(artifactId))
561 .findFirst()
562 .flatMap(buildElement -> child(buildElement, "configuration"))
563 .orElseThrow(
564 () -> new ConfigurationException("Cannot find a configuration element for a plugin with an "
565 + "artifactId of " + artifactId + "."));
566 return pluginConfigurationElement.getDom();
567 }
568
569
570
571
572
573
574 private static String resolveFromRootThenParent(Xpp3Dom pluginPomDom, String element) throws Exception {
575 return Optional.ofNullable(child(pluginPomDom, element).orElseGet(() -> child(pluginPomDom, "parent")
576 .flatMap(e -> child(e, element))
577 .orElse(null)))
578 .map(Xpp3Dom::getValue)
579 .orElseThrow(() -> new Exception("unable to determine " + element));
580 }
581
582
583
584
585
586
587 public static Object getVariableValueFromObject(Object object, String variable) throws IllegalAccessException {
588 Field field = ReflectionUtils.getFieldByNameIncludingSuperclasses(variable, object.getClass());
589 field.setAccessible(true);
590 return field.get(object);
591 }
592
593
594
595
596
597
598 public static Map<String, Object> getVariablesAndValuesFromObject(Object object) throws IllegalAccessException {
599 return getVariablesAndValuesFromObject(object.getClass(), object);
600 }
601
602
603
604
605
606
607
608
609 public static Map<String, Object> getVariablesAndValuesFromObject(Class<?> clazz, Object object)
610 throws IllegalAccessException {
611 Map<String, Object> map = new HashMap<>();
612 Field[] fields = clazz.getDeclaredFields();
613 AccessibleObject.setAccessible(fields, true);
614 for (Field field : fields) {
615 map.put(field.getName(), field.get(object));
616 }
617 Class<?> superclass = clazz.getSuperclass();
618 if (!Object.class.equals(superclass)) {
619 map.putAll(getVariablesAndValuesFromObject(superclass, object));
620 }
621 return map;
622 }
623
624
625
626
627 public static void setVariableValueToObject(Object object, String variable, Object value)
628 throws IllegalAccessException {
629 Field field = ReflectionUtils.getFieldByNameIncludingSuperclasses(variable, object.getClass());
630 requireNonNull(field, "Field " + variable + " not found");
631 field.setAccessible(true);
632 field.set(object, value);
633 }
634
635 static class WrapEvaluator implements TypeAwareExpressionEvaluator {
636
637 private final Injector injector;
638 private final TypeAwareExpressionEvaluator evaluator;
639
640 WrapEvaluator(Injector injector, TypeAwareExpressionEvaluator evaluator) {
641 this.injector = injector;
642 this.evaluator = evaluator;
643 }
644
645 @Override
646 public Object evaluate(String expression) throws ExpressionEvaluationException {
647 return evaluate(expression, null);
648 }
649
650 @Override
651 public Object evaluate(String expression, Class<?> type) throws ExpressionEvaluationException {
652 Object value = evaluator.evaluate(expression, type);
653 if (value == null) {
654 String expr = stripTokens(expression);
655 if (expr != null) {
656 try {
657 value = injector.getInstance(Key.of(type, expr));
658 } catch (DIException e) {
659
660 }
661 }
662 }
663 return value;
664 }
665
666 private String stripTokens(String expr) {
667 if (expr.startsWith("${") && expr.endsWith("}")) {
668 return expr.substring(2, expr.length() - 1);
669 }
670 return null;
671 }
672
673 @Override
674 public File alignToBaseDirectory(File path) {
675 return evaluator.alignToBaseDirectory(path);
676 }
677 }
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709 }