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 }