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.BufferedReader;
22 import java.io.File;
23 import java.io.InputStream;
24 import java.io.Reader;
25 import java.io.StringReader;
26 import java.lang.reflect.AccessibleObject;
27 import java.lang.reflect.Field;
28 import java.lang.reflect.InvocationTargetException;
29 import java.lang.reflect.Method;
30 import java.net.URL;
31 import java.nio.file.Files;
32 import java.nio.file.Path;
33 import java.nio.file.Paths;
34 import java.util.ArrayList;
35 import java.util.Arrays;
36 import java.util.Collection;
37 import java.util.Collections;
38 import java.util.HashMap;
39 import java.util.HashSet;
40 import java.util.List;
41 import java.util.Map;
42 import java.util.Objects;
43 import java.util.Optional;
44 import java.util.Properties;
45 import java.util.Set;
46 import java.util.function.Supplier;
47 import java.util.stream.Collectors;
48 import java.util.stream.Stream;
49
50 import com.google.inject.Binder;
51 import com.google.inject.Module;
52 import com.google.inject.internal.ProviderMethodsModule;
53 import org.apache.maven.api.di.Provides;
54 import org.apache.maven.execution.MavenSession;
55 import org.apache.maven.execution.scope.internal.MojoExecutionScope;
56 import org.apache.maven.lifecycle.internal.MojoDescriptorCreator;
57 import org.apache.maven.plugin.Mojo;
58 import org.apache.maven.plugin.MojoExecution;
59 import org.apache.maven.plugin.PluginParameterExpressionEvaluator;
60 import org.apache.maven.plugin.descriptor.MojoDescriptor;
61 import org.apache.maven.plugin.descriptor.Parameter;
62 import org.apache.maven.plugin.descriptor.PluginDescriptor;
63 import org.apache.maven.plugin.descriptor.PluginDescriptorBuilder;
64 import org.apache.maven.plugin.logging.Log;
65 import org.apache.maven.plugin.testing.MojoLogWrapper;
66 import org.apache.maven.project.MavenProject;
67 import org.apache.maven.session.scope.internal.SessionScope;
68 import org.codehaus.plexus.DefaultPlexusContainer;
69 import org.codehaus.plexus.PlexusContainer;
70 import org.codehaus.plexus.component.configurator.BasicComponentConfigurator;
71 import org.codehaus.plexus.component.configurator.ComponentConfigurator;
72 import org.codehaus.plexus.component.configurator.expression.ExpressionEvaluationException;
73 import org.codehaus.plexus.component.configurator.expression.ExpressionEvaluator;
74 import org.codehaus.plexus.component.configurator.expression.TypeAwareExpressionEvaluator;
75 import org.codehaus.plexus.component.repository.ComponentDescriptor;
76 import org.codehaus.plexus.component.repository.exception.ComponentLookupException;
77 import org.codehaus.plexus.configuration.xml.XmlPlexusConfiguration;
78 import org.codehaus.plexus.testing.PlexusExtension;
79 import org.codehaus.plexus.util.InterpolationFilterReader;
80 import org.codehaus.plexus.util.ReflectionUtils;
81 import org.codehaus.plexus.util.xml.XmlStreamReader;
82 import org.codehaus.plexus.util.xml.Xpp3Dom;
83 import org.codehaus.plexus.util.xml.Xpp3DomBuilder;
84 import org.junit.jupiter.api.extension.ExtensionContext;
85 import org.junit.jupiter.api.extension.ParameterContext;
86 import org.junit.jupiter.api.extension.ParameterResolutionException;
87 import org.junit.jupiter.api.extension.ParameterResolver;
88 import org.junit.platform.commons.support.AnnotationSupport;
89 import org.junit.platform.commons.support.HierarchyTraversalMode;
90 import org.mockito.Mockito;
91 import org.slf4j.LoggerFactory;
92
93 import static org.mockito.Mockito.clearInvocations;
94 import static org.mockito.Mockito.lenient;
95 import static org.mockito.Mockito.mockingDetails;
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138 public class MojoExtension extends PlexusExtension implements ParameterResolver {
139
140
141 private static final ExtensionContext.Namespace MOJO_EXTENSION = ExtensionContext.Namespace.create("MojoExtension");
142
143 public static final String BASEDIR_IS_SET_KEY = "basedirIsSet";
144
145 @Override
146 public boolean supportsParameter(ParameterContext parameterContext, ExtensionContext extensionContext)
147 throws ParameterResolutionException {
148 return parameterContext.isAnnotated(InjectMojo.class)
149 || parameterContext.getDeclaringExecutable().isAnnotationPresent(InjectMojo.class);
150 }
151
152 @Override
153 public Object resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext)
154 throws ParameterResolutionException {
155 try {
156 InjectMojo injectMojo = parameterContext
157 .findAnnotation(InjectMojo.class)
158 .orElseGet(() -> parameterContext.getDeclaringExecutable().getAnnotation(InjectMojo.class));
159
160 Set<MojoParameter> mojoParameters =
161 new HashSet<>(parameterContext.findRepeatableAnnotations(MojoParameter.class));
162
163 Optional.ofNullable(parameterContext.getDeclaringExecutable().getAnnotation(MojoParameter.class))
164 .ifPresent(mojoParameters::add);
165
166 Optional.ofNullable(parameterContext.getDeclaringExecutable().getAnnotation(MojoParameters.class))
167 .map(MojoParameters::value)
168 .map(Arrays::asList)
169 .ifPresent(mojoParameters::addAll);
170
171 Class<?> holder = parameterContext.getTarget().get().getClass();
172 PluginDescriptor descriptor =
173 extensionContext.getStore(MOJO_EXTENSION).get(PluginDescriptor.class, PluginDescriptor.class);
174 return lookupMojo(extensionContext, holder, injectMojo, mojoParameters, descriptor);
175 } catch (Exception e) {
176 throw new ParameterResolutionException("Unable to resolve parameter", e);
177 }
178 }
179
180 @Override
181 public void beforeEach(ExtensionContext context) throws Exception {
182 String basedir = AnnotationSupport.findAnnotation(context.getElement().get(), Basedir.class)
183 .map(Basedir::value)
184 .orElse(null);
185
186 if (basedir == null) {
187 basedir = getBasedir();
188 } else {
189 context.getStore(MOJO_EXTENSION).put(BASEDIR_IS_SET_KEY, Boolean.TRUE);
190 }
191
192 URL resource = context.getRequiredTestClass().getResource(basedir);
193 if (resource != null) {
194 basedir = Paths.get(resource.toURI()).toString();
195 }
196
197
198 basedir = new File(basedir).getAbsolutePath();
199
200 setTestBasedir(basedir, context);
201
202 PlexusContainer plexusContainer = getContainer(context);
203
204 ((DefaultPlexusContainer) plexusContainer).addPlexusInjector(Collections.emptyList(), binder -> {
205 binder.install(ProviderMethodsModule.forObject(context.getRequiredTestInstance()));
206 binder.install(new MavenProvidesModule(context.getRequiredTestInstance()));
207 });
208
209 addMock(plexusContainer, Log.class, () -> new MojoLogWrapper(LoggerFactory.getLogger("anonymous")));
210 MavenProject mavenProject = addMock(plexusContainer, MavenProject.class, this::mockMavenProject);
211 MojoExecution mojoExecution = addMock(plexusContainer, MojoExecution.class, this::mockMojoExecution);
212 MavenSession mavenSession = addMock(plexusContainer, MavenSession.class, this::mockMavenSession);
213
214 SessionScope sessionScope = plexusContainer.lookup(SessionScope.class);
215 sessionScope.enter();
216 sessionScope.seed(MavenSession.class, mavenSession);
217
218 MojoExecutionScope executionScope = plexusContainer.lookup(MojoExecutionScope.class);
219 executionScope.enter();
220 executionScope.seed(MavenProject.class, mavenProject);
221 executionScope.seed(MojoExecution.class, mojoExecution);
222
223 ((DefaultPlexusContainer) plexusContainer).addPlexusInjector(Collections.emptyList(), binder -> {
224 binder.requestInjection(context.getRequiredTestInstance());
225 });
226
227 Map<Object, Object> map = plexusContainer.getContext().getContextData();
228
229 ClassLoader classLoader = context.getRequiredTestClass().getClassLoader();
230 try (InputStream is = Objects.requireNonNull(
231 classLoader.getResourceAsStream(getPluginDescriptorLocation()),
232 "Unable to find plugin descriptor: " + getPluginDescriptorLocation());
233 Reader reader = new BufferedReader(new XmlStreamReader(is));
234 InterpolationFilterReader interpolationReader = new InterpolationFilterReader(reader, map, "${", "}")) {
235
236 PluginDescriptor pluginDescriptor = new PluginDescriptorBuilder().build(interpolationReader);
237
238 context.getStore(MOJO_EXTENSION).put(PluginDescriptor.class, pluginDescriptor);
239
240 for (ComponentDescriptor<?> desc : pluginDescriptor.getComponents()) {
241 plexusContainer.addComponentDescriptor(desc);
242 }
243 }
244 }
245
246 private <T> T addMock(PlexusContainer container, Class<T> role, Supplier<T> supplier)
247 throws ComponentLookupException {
248 if (!container.hasComponent(role)) {
249 T mock = supplier.get();
250 container.addComponent(mock, role, "default");
251 return mock;
252 } else {
253 return container.lookup(role);
254 }
255 }
256
257 @Override
258 public void afterEach(ExtensionContext context) throws Exception {
259 SessionScope sessionScope = getContainer(context).lookup(SessionScope.class);
260 sessionScope.exit();
261
262 MojoExecutionScope executionScope = getContainer(context).lookup(MojoExecutionScope.class);
263 executionScope.exit();
264
265 super.afterEach(context);
266 }
267
268
269
270
271
272
273 private MojoExecution mockMojoExecution() {
274 return Mockito.mock(MojoExecution.class);
275 }
276
277
278
279
280
281
282 private MavenSession mockMavenSession() {
283 MavenSession session = Mockito.mock(MavenSession.class);
284 lenient().when(session.getUserProperties()).thenReturn(new Properties());
285 lenient().when(session.getSystemProperties()).thenReturn(new Properties());
286 return session;
287 }
288
289
290
291
292
293
294 private MavenProject mockMavenProject() {
295 MavenProject mavenProject = Mockito.mock(MavenProject.class);
296 lenient().when(mavenProject.getProperties()).thenReturn(new Properties());
297 return mavenProject;
298 }
299
300 protected String getPluginDescriptorLocation() {
301 return "META-INF/maven/plugin.xml";
302 }
303
304 private Mojo lookupMojo(
305 ExtensionContext extensionContext,
306 Class<?> holder,
307 InjectMojo injectMojo,
308 Collection<MojoParameter> mojoParameters,
309 PluginDescriptor descriptor)
310 throws Exception {
311 String goal = injectMojo.goal();
312 String pom = injectMojo.pom();
313 Path basedir = Paths.get(getTestBasedir(extensionContext));
314 String[] coord = mojoCoordinates(goal, descriptor);
315 Xpp3Dom pomDom;
316 if (pom.startsWith("file:")) {
317 Path path = basedir.resolve(pom.substring("file:".length()));
318 pomDom = Xpp3DomBuilder.build(new XmlStreamReader(path.toFile()));
319 } else if (pom.startsWith("classpath:")) {
320 URL url = holder.getResource(pom.substring("classpath:".length()));
321 if (url == null) {
322 throw new IllegalStateException("Unable to find pom on classpath: " + pom);
323 }
324 pomDom = Xpp3DomBuilder.build(new XmlStreamReader(url.openStream()));
325 } else if (pom.contains("<project>")) {
326 pomDom = Xpp3DomBuilder.build(new StringReader(pom));
327 } else if (!pom.isEmpty()) {
328 Path path = basedir.resolve(pom);
329 pomDom = Xpp3DomBuilder.build(new XmlStreamReader(path.toFile()));
330 } else if (isBasedirSet(extensionContext)) {
331
332 Path path = basedir.resolve("pom.xml");
333 if (Files.exists(path)) {
334 pomDom = Xpp3DomBuilder.build(new XmlStreamReader(path.toFile()));
335 } else {
336 pomDom = new Xpp3Dom("");
337 }
338 } else {
339 pomDom = new Xpp3Dom("");
340 }
341 Xpp3Dom pluginConfiguration = extractPluginConfiguration(coord[1], pomDom);
342 if (!mojoParameters.isEmpty()) {
343 List<Xpp3Dom> children = mojoParameters.stream()
344 .map(mp -> {
345 Xpp3Dom c = new Xpp3Dom(mp.name());
346 c.setValue(mp.value());
347 return c;
348 })
349 .collect(Collectors.toList());
350 Xpp3Dom config = new Xpp3Dom("configuration");
351 children.forEach(config::addChild);
352 pluginConfiguration = Xpp3Dom.mergeXpp3Dom(config, pluginConfiguration);
353 }
354 return lookupMojo(extensionContext, coord, pluginConfiguration, descriptor);
355 }
356
357 private boolean isBasedirSet(ExtensionContext extensionContext) {
358 return extensionContext.getStore(MOJO_EXTENSION).getOrDefault(BASEDIR_IS_SET_KEY, Boolean.class, Boolean.FALSE);
359 }
360
361 protected String[] mojoCoordinates(String goal, PluginDescriptor pluginDescriptor) throws Exception {
362 if (goal.matches(".*:.*:.*:.*")) {
363 return goal.split(":");
364 } else {
365 String artifactId = pluginDescriptor.getArtifactId();
366 String groupId = pluginDescriptor.getGroupId();
367 String version = pluginDescriptor.getVersion();
368 return new String[] {groupId, artifactId, version, goal};
369 }
370 }
371
372
373
374
375 protected Mojo lookupMojo(
376 ExtensionContext extensionContext, String[] coord, Xpp3Dom pluginConfiguration, PluginDescriptor descriptor)
377 throws Exception {
378 PlexusContainer plexusContainer = getContainer(extensionContext);
379
380 Mojo mojo = plexusContainer.lookup(Mojo.class, coord[0] + ":" + coord[1] + ":" + coord[2] + ":" + coord[3]);
381
382 Optional<MojoDescriptor> mojoDescriptor = descriptor.getMojos().stream()
383 .filter(md ->
384 Objects.equals(md.getImplementation(), mojo.getClass().getName()))
385 .findFirst();
386
387 if (mojoDescriptor.isPresent()) {
388 pluginConfiguration = finalizeConfig(pluginConfiguration, mojoDescriptor.get());
389 }
390
391 MavenSession session = plexusContainer.lookup(MavenSession.class);
392 MavenProject mavenProject = plexusContainer.lookup(MavenProject.class);
393 MojoExecution mojoExecution = plexusContainer.lookup(MojoExecution.class);
394
395 if (mockingDetails(session).isMock()) {
396 lenient().when(session.getCurrentProject()).thenReturn(mavenProject);
397 }
398
399 if (mockingDetails(mavenProject).isMock()) {
400 lenient().when(mavenProject.getBasedir()).thenReturn(new File(getTestBasedir(extensionContext)));
401 }
402
403 if (mojoDescriptor.isPresent() && mockingDetails(mojoExecution).isMock()) {
404 lenient().when(mojoExecution.getMojoDescriptor()).thenReturn(mojoDescriptor.get());
405 }
406
407 if (pluginConfiguration != null) {
408 ExpressionEvaluator evaluator =
409 new WrapEvaluator(plexusContainer, new PluginParameterExpressionEvaluator(session, mojoExecution));
410 ComponentConfigurator configurator = new BasicComponentConfigurator();
411 configurator.configureComponent(
412 mojo,
413 new XmlPlexusConfiguration(pluginConfiguration),
414 evaluator,
415 plexusContainer.getContainerRealm());
416 }
417
418 mojo.setLog(plexusContainer.lookup(Log.class));
419
420
421 if (mockingDetails(session).isMock()) {
422 clearInvocations(session);
423 }
424
425 if (mockingDetails(mavenProject).isMock()) {
426 clearInvocations(mavenProject);
427 }
428
429 if (mockingDetails(mojoExecution).isMock()) {
430 clearInvocations(mojoExecution);
431 }
432
433 return mojo;
434 }
435
436 private Xpp3Dom finalizeConfig(Xpp3Dom config, MojoDescriptor mojoDescriptor) {
437 List<Xpp3Dom> children = new ArrayList<>();
438 if (mojoDescriptor != null && mojoDescriptor.getParameters() != null) {
439 Xpp3Dom defaultConfiguration = MojoDescriptorCreator.convert(mojoDescriptor);
440 for (Parameter parameter : mojoDescriptor.getParameters()) {
441 Xpp3Dom parameterConfiguration = config.getChild(parameter.getName());
442 if (parameterConfiguration == null) {
443 parameterConfiguration = config.getChild(parameter.getAlias());
444 }
445 Xpp3Dom parameterDefaults = defaultConfiguration.getChild(parameter.getName());
446 parameterConfiguration = Xpp3Dom.mergeXpp3Dom(parameterConfiguration, parameterDefaults, Boolean.TRUE);
447 if (parameterConfiguration != null) {
448 if (isEmpty(parameterConfiguration.getAttribute("implementation"))
449 && !isEmpty(parameter.getImplementation())) {
450 parameterConfiguration.setAttribute("implementation", parameter.getImplementation());
451 }
452 children.add(parameterConfiguration);
453 }
454 }
455 }
456 Xpp3Dom c = new Xpp3Dom("configuration");
457 children.forEach(c::addChild);
458 return c;
459 }
460
461 private boolean isEmpty(String str) {
462 return str == null || str.isEmpty();
463 }
464
465 private static Optional<Xpp3Dom> child(Xpp3Dom element, String name) {
466 return Optional.ofNullable(element.getChild(name));
467 }
468
469 private static Stream<Xpp3Dom> children(Xpp3Dom element) {
470 return Stream.of(element.getChildren());
471 }
472
473 public static Xpp3Dom extractPluginConfiguration(String artifactId, Xpp3Dom pomDom) throws Exception {
474 Xpp3Dom pluginConfigurationElement = child(pomDom, "build")
475 .flatMap(buildElement -> child(buildElement, "plugins"))
476 .map(MojoExtension::children)
477 .orElseGet(Stream::empty)
478 .filter(e -> e.getChild("artifactId").getValue().equals(artifactId))
479 .findFirst()
480 .flatMap(buildElement -> child(buildElement, "configuration"))
481 .orElse(Xpp3DomBuilder.build(new StringReader("<configuration/>")));
482 return pluginConfigurationElement;
483 }
484
485
486
487
488
489
490 public static Object getVariableValueFromObject(Object object, String variable) throws IllegalAccessException {
491 Field field = ReflectionUtils.getFieldByNameIncludingSuperclasses(variable, object.getClass());
492 field.setAccessible(true);
493 return field.get(object);
494 }
495
496
497
498
499
500
501 public static Map<String, Object> getVariablesAndValuesFromObject(Object object) throws IllegalAccessException {
502 return getVariablesAndValuesFromObject(object.getClass(), object);
503 }
504
505
506
507
508
509
510
511
512 public static Map<String, Object> getVariablesAndValuesFromObject(Class<?> clazz, Object object)
513 throws IllegalAccessException {
514 Map<String, Object> map = new HashMap<>();
515 Field[] fields = clazz.getDeclaredFields();
516 AccessibleObject.setAccessible(fields, true);
517 for (Field field : fields) {
518 map.put(field.getName(), field.get(object));
519 }
520 Class<?> superclass = clazz.getSuperclass();
521 if (!Object.class.equals(superclass)) {
522 map.putAll(getVariablesAndValuesFromObject(superclass, object));
523 }
524 return map;
525 }
526
527
528
529
530
531 public static String getBasedir() {
532 return PlexusExtension.getBasedir();
533 }
534
535
536
537
538 public static void setVariableValueToObject(Object object, String variable, Object value)
539 throws IllegalAccessException {
540 Field field = ReflectionUtils.getFieldByNameIncludingSuperclasses(variable, object.getClass());
541 Objects.requireNonNull(field, "Field " + variable + " not found");
542 field.setAccessible(true);
543 field.set(object, value);
544 }
545
546 private static class WrapEvaluator implements TypeAwareExpressionEvaluator {
547
548 private final PlexusContainer container;
549
550 private final TypeAwareExpressionEvaluator evaluator;
551
552 WrapEvaluator(PlexusContainer container, TypeAwareExpressionEvaluator evaluator) {
553 this.container = container;
554 this.evaluator = evaluator;
555 }
556
557 @Override
558 public Object evaluate(String expression) throws ExpressionEvaluationException {
559 return evaluate(expression, null);
560 }
561
562 @Override
563 public Object evaluate(String expression, Class<?> type) throws ExpressionEvaluationException {
564 Object value = evaluator.evaluate(expression, type);
565 if (value == null) {
566 String expr = stripTokens(expression);
567 if (expr != null) {
568 try {
569 value = container.lookup(type, expr);
570 } catch (ComponentLookupException e) {
571
572 }
573 }
574 }
575 return value;
576 }
577
578 private String stripTokens(String expr) {
579 if (expr.startsWith("${") && expr.endsWith("}")) {
580 return expr.substring(2, expr.length() - 1);
581 }
582 return null;
583 }
584
585 @Override
586 public File alignToBaseDirectory(File path) {
587 return evaluator.alignToBaseDirectory(path);
588 }
589 }
590
591 private static class MavenProvidesModule implements Module {
592 private final Object testInstance;
593
594 MavenProvidesModule(Object testInstance) {
595 this.testInstance = testInstance;
596 }
597
598 @Override
599 @SuppressWarnings("unchecked")
600 public void configure(Binder binder) {
601 List<Method> providesMethods = AnnotationSupport.findAnnotatedMethods(
602 testInstance.getClass(), Provides.class, HierarchyTraversalMode.BOTTOM_UP);
603
604 for (Method method : providesMethods) {
605 if (method.getParameterCount() > 0) {
606 throw new IllegalArgumentException("Parameterized method are not supported " + method);
607 }
608 try {
609 method.setAccessible(true);
610 Object value = method.invoke(testInstance);
611 if (value == null) {
612 throw new IllegalArgumentException("Provides method returned null: " + method);
613 }
614 binder.bind((Class<Object>) method.getReturnType()).toInstance(value);
615 } catch (IllegalAccessException | InvocationTargetException e) {
616 throw new IllegalArgumentException(e);
617 }
618 }
619 }
620 }
621 }