1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19 package org.apache.maven.plugin.testing.junit5;
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.net.URL;
29 import java.nio.file.Path;
30 import java.nio.file.Paths;
31 import java.util.ArrayList;
32 import java.util.Arrays;
33 import java.util.Collection;
34 import java.util.Collections;
35 import java.util.HashMap;
36 import java.util.HashSet;
37 import java.util.List;
38 import java.util.Map;
39 import java.util.Objects;
40 import java.util.Optional;
41 import java.util.Properties;
42 import java.util.Set;
43 import java.util.stream.Collectors;
44 import java.util.stream.Stream;
45
46 import com.google.inject.internal.ProviderMethodsModule;
47 import org.apache.maven.api.plugin.testing.MojoTest;
48 import org.apache.maven.execution.MavenSession;
49 import org.apache.maven.lifecycle.internal.MojoDescriptorCreator;
50 import org.apache.maven.plugin.Mojo;
51 import org.apache.maven.plugin.MojoExecution;
52 import org.apache.maven.plugin.PluginParameterExpressionEvaluator;
53 import org.apache.maven.plugin.descriptor.MojoDescriptor;
54 import org.apache.maven.plugin.descriptor.Parameter;
55 import org.apache.maven.plugin.descriptor.PluginDescriptor;
56 import org.apache.maven.plugin.descriptor.PluginDescriptorBuilder;
57 import org.apache.maven.plugin.logging.Log;
58 import org.apache.maven.plugin.testing.ConfigurationException;
59 import org.apache.maven.plugin.testing.MojoLogWrapper;
60 import org.apache.maven.project.MavenProject;
61 import org.codehaus.plexus.DefaultPlexusContainer;
62 import org.codehaus.plexus.PlexusContainer;
63 import org.codehaus.plexus.component.configurator.BasicComponentConfigurator;
64 import org.codehaus.plexus.component.configurator.ComponentConfigurator;
65 import org.codehaus.plexus.component.configurator.expression.ExpressionEvaluationException;
66 import org.codehaus.plexus.component.configurator.expression.ExpressionEvaluator;
67 import org.codehaus.plexus.component.configurator.expression.TypeAwareExpressionEvaluator;
68 import org.codehaus.plexus.component.repository.ComponentDescriptor;
69 import org.codehaus.plexus.component.repository.exception.ComponentLookupException;
70 import org.codehaus.plexus.configuration.xml.XmlPlexusConfiguration;
71 import org.codehaus.plexus.testing.PlexusExtension;
72 import org.codehaus.plexus.util.InterpolationFilterReader;
73 import org.codehaus.plexus.util.ReaderFactory;
74 import org.codehaus.plexus.util.ReflectionUtils;
75 import org.codehaus.plexus.util.xml.XmlStreamReader;
76 import org.codehaus.plexus.util.xml.Xpp3Dom;
77 import org.codehaus.plexus.util.xml.Xpp3DomBuilder;
78 import org.junit.jupiter.api.extension.ExtensionContext;
79 import org.junit.jupiter.api.extension.ParameterContext;
80 import org.junit.jupiter.api.extension.ParameterResolutionException;
81 import org.junit.jupiter.api.extension.ParameterResolver;
82 import org.mockito.Mockito;
83 import org.slf4j.LoggerFactory;
84
85
86
87
88
89
90
91
92
93 public class MojoExtension extends PlexusExtension implements ParameterResolver {
94
95 @Override
96 public boolean supportsParameter(ParameterContext parameterContext, ExtensionContext extensionContext)
97 throws ParameterResolutionException {
98 return parameterContext.isAnnotated(InjectMojo.class)
99 || parameterContext.getDeclaringExecutable().isAnnotationPresent(InjectMojo.class);
100 }
101
102 @Override
103 public Object resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext)
104 throws ParameterResolutionException {
105 try {
106 InjectMojo injectMojo = parameterContext
107 .findAnnotation(InjectMojo.class)
108 .orElseGet(() -> parameterContext.getDeclaringExecutable().getAnnotation(InjectMojo.class));
109
110 Set<MojoParameter> mojoParameters =
111 new HashSet<>(parameterContext.findRepeatableAnnotations(MojoParameter.class));
112
113 Optional.ofNullable(parameterContext.getDeclaringExecutable().getAnnotation(MojoParameter.class))
114 .ifPresent(mojoParameters::add);
115
116 Optional.ofNullable(parameterContext.getDeclaringExecutable().getAnnotation(MojoParameters.class))
117 .map(MojoParameters::value)
118 .map(Arrays::asList)
119 .ifPresent(mojoParameters::addAll);
120
121 Class<?> holder = parameterContext.getTarget().get().getClass();
122 PluginDescriptor descriptor = extensionContext
123 .getStore(ExtensionContext.Namespace.GLOBAL)
124 .get(PluginDescriptor.class, PluginDescriptor.class);
125 return lookupMojo(holder, injectMojo, mojoParameters, descriptor);
126 } catch (Exception e) {
127 throw new ParameterResolutionException("Unable to resolve parameter", e);
128 }
129 }
130
131 @Override
132 public void beforeEach(ExtensionContext context) throws Exception {
133
134 Field field = PlexusExtension.class.getDeclaredField("basedir");
135 field.setAccessible(true);
136 field.set(null, getBasedir());
137 field = PlexusExtension.class.getDeclaredField("context");
138 field.setAccessible(true);
139 field.set(this, context);
140
141 getContainer().addComponent(getContainer(), PlexusContainer.class.getName());
142
143 ((DefaultPlexusContainer) getContainer()).addPlexusInjector(Collections.emptyList(), binder -> {
144 binder.install(ProviderMethodsModule.forObject(context.getRequiredTestInstance()));
145 binder.requestInjection(context.getRequiredTestInstance());
146 binder.bind(Log.class).toInstance(new MojoLogWrapper(LoggerFactory.getLogger("anonymous")));
147 binder.bind(MavenSession.class).toInstance(mockMavenSession());
148 binder.bind(MojoExecution.class).toInstance(mockMojoExecution());
149 });
150
151 Map<Object, Object> map = getContainer().getContext().getContextData();
152
153 ClassLoader classLoader = context.getRequiredTestClass().getClassLoader();
154 try (InputStream is = Objects.requireNonNull(
155 classLoader.getResourceAsStream(getPluginDescriptorLocation()),
156 "Unable to find plugin descriptor: " + getPluginDescriptorLocation());
157 Reader reader = new BufferedReader(new XmlStreamReader(is));
158 InterpolationFilterReader interpolationReader = new InterpolationFilterReader(reader, map, "${", "}")) {
159
160 PluginDescriptor pluginDescriptor = new PluginDescriptorBuilder().build(interpolationReader);
161
162 context.getStore(ExtensionContext.Namespace.GLOBAL).put(PluginDescriptor.class, pluginDescriptor);
163
164 for (ComponentDescriptor<?> desc : pluginDescriptor.getComponents()) {
165 getContainer().addComponentDescriptor(desc);
166 }
167 }
168 }
169
170
171
172
173
174
175 private MojoExecution mockMojoExecution() {
176 return Mockito.mock(MojoExecution.class);
177 }
178
179
180
181
182
183
184 private MavenSession mockMavenSession() {
185 MavenSession session = Mockito.mock(MavenSession.class);
186 Mockito.when(session.getUserProperties()).thenReturn(new Properties());
187 Mockito.when(session.getSystemProperties()).thenReturn(new Properties());
188 return session;
189 }
190
191 protected String getPluginDescriptorLocation() {
192 return "META-INF/maven/plugin.xml";
193 }
194
195 private Mojo lookupMojo(
196 Class<?> holder,
197 InjectMojo injectMojo,
198 Collection<MojoParameter> mojoParameters,
199 PluginDescriptor descriptor)
200 throws Exception {
201 String goal = injectMojo.goal();
202 String pom = injectMojo.pom();
203 String[] coord = mojoCoordinates(goal);
204 Xpp3Dom pomDom;
205 if (pom.startsWith("file:")) {
206 Path path = Paths.get(getBasedir()).resolve(pom.substring("file:".length()));
207 pomDom = Xpp3DomBuilder.build(ReaderFactory.newXmlReader(path.toFile()));
208 } else if (pom.startsWith("classpath:")) {
209 URL url = holder.getResource(pom.substring("classpath:".length()));
210 if (url == null) {
211 throw new IllegalStateException("Unable to find pom on classpath: " + pom);
212 }
213 pomDom = Xpp3DomBuilder.build(ReaderFactory.newXmlReader(url.openStream()));
214 } else if (pom.contains("<project>")) {
215 pomDom = Xpp3DomBuilder.build(new StringReader(pom));
216 } else {
217 Path path = Paths.get(getBasedir()).resolve(pom);
218 pomDom = Xpp3DomBuilder.build(ReaderFactory.newXmlReader(path.toFile()));
219 }
220 Xpp3Dom pluginConfiguration = extractPluginConfiguration(coord[1], pomDom);
221 if (!mojoParameters.isEmpty()) {
222 List<Xpp3Dom> children = mojoParameters.stream()
223 .map(mp -> {
224 Xpp3Dom c = new Xpp3Dom(mp.name());
225 c.setValue(mp.value());
226 return c;
227 })
228 .collect(Collectors.toList());
229 Xpp3Dom config = new Xpp3Dom("configuration");
230 children.forEach(config::addChild);
231 pluginConfiguration = Xpp3Dom.mergeXpp3Dom(config, pluginConfiguration);
232 }
233 Mojo mojo = lookupMojo(coord, pluginConfiguration, descriptor);
234 return mojo;
235 }
236
237 protected String[] mojoCoordinates(String goal) throws Exception {
238 if (goal.matches(".*:.*:.*:.*")) {
239 return goal.split(":");
240 } else {
241 Path pluginPom = Paths.get(getBasedir(), "pom.xml");
242 Xpp3Dom pluginPomDom = Xpp3DomBuilder.build(ReaderFactory.newXmlReader(pluginPom.toFile()));
243 String artifactId = pluginPomDom.getChild("artifactId").getValue();
244 String groupId = resolveFromRootThenParent(pluginPomDom, "groupId");
245 String version = resolveFromRootThenParent(pluginPomDom, "version");
246 return new String[] {groupId, artifactId, version, goal};
247 }
248 }
249
250
251
252
253 protected Mojo lookupMojo(String[] coord, Xpp3Dom pluginConfiguration, PluginDescriptor descriptor)
254 throws Exception {
255
256 Mojo mojo = lookup(Mojo.class, coord[0] + ":" + coord[1] + ":" + coord[2] + ":" + coord[3]);
257 for (MojoDescriptor mojoDescriptor : descriptor.getMojos()) {
258 if (Objects.equals(
259 mojoDescriptor.getImplementation(), mojo.getClass().getName())) {
260 if (pluginConfiguration != null) {
261 pluginConfiguration = finalizeConfig(pluginConfiguration, mojoDescriptor);
262 }
263 }
264 }
265 if (pluginConfiguration != null) {
266 MavenSession session = getContainer().lookup(MavenSession.class);
267 MavenProject project;
268 try {
269 project = getContainer().lookup(MavenProject.class);
270 } catch (ComponentLookupException e) {
271 project = null;
272 }
273 MojoExecution mojoExecution;
274 try {
275 mojoExecution = getContainer().lookup(MojoExecution.class);
276 } catch (ComponentLookupException e) {
277 mojoExecution = null;
278 }
279 ExpressionEvaluator evaluator =
280 new WrapEvaluator(getContainer(), new PluginParameterExpressionEvaluator(session, mojoExecution));
281 ComponentConfigurator configurator = new BasicComponentConfigurator();
282 configurator.configureComponent(
283 mojo,
284 new XmlPlexusConfiguration(pluginConfiguration),
285 evaluator,
286 getContainer().getContainerRealm());
287 }
288
289 mojo.setLog(getContainer().lookup(Log.class));
290
291 return mojo;
292 }
293
294 private Xpp3Dom finalizeConfig(Xpp3Dom config, MojoDescriptor mojoDescriptor) {
295 List<Xpp3Dom> children = new ArrayList<>();
296 if (mojoDescriptor != null && mojoDescriptor.getParameters() != null) {
297 Xpp3Dom defaultConfiguration = MojoDescriptorCreator.convert(mojoDescriptor);
298 for (Parameter parameter : mojoDescriptor.getParameters()) {
299 Xpp3Dom parameterConfiguration = config.getChild(parameter.getName());
300 if (parameterConfiguration == null) {
301 parameterConfiguration = config.getChild(parameter.getAlias());
302 }
303 Xpp3Dom parameterDefaults = defaultConfiguration.getChild(parameter.getName());
304 parameterConfiguration = Xpp3Dom.mergeXpp3Dom(parameterConfiguration, parameterDefaults, Boolean.TRUE);
305 if (parameterConfiguration != null) {
306 if (isEmpty(parameterConfiguration.getAttribute("implementation"))
307 && !isEmpty(parameter.getImplementation())) {
308 parameterConfiguration.setAttribute("implementation", parameter.getImplementation());
309 }
310 children.add(parameterConfiguration);
311 }
312 }
313 }
314 Xpp3Dom c = new Xpp3Dom("configuration");
315 children.forEach(c::addChild);
316 return c;
317 }
318
319 private boolean isEmpty(String str) {
320 return str == null || str.isEmpty();
321 }
322
323 private static Optional<Xpp3Dom> child(Xpp3Dom element, String name) {
324 return Optional.ofNullable(element.getChild(name));
325 }
326
327 private static Stream<Xpp3Dom> children(Xpp3Dom element) {
328 return Stream.of(element.getChildren());
329 }
330
331 public static Xpp3Dom extractPluginConfiguration(String artifactId, Xpp3Dom pomDom) throws Exception {
332 Xpp3Dom pluginConfigurationElement = child(pomDom, "build")
333 .flatMap(buildElement -> child(buildElement, "plugins"))
334 .map(MojoExtension::children)
335 .orElseGet(Stream::empty)
336 .filter(e -> e.getChild("artifactId").getValue().equals(artifactId))
337 .findFirst()
338 .flatMap(buildElement -> child(buildElement, "configuration"))
339 .orElseThrow(
340 () -> new ConfigurationException("Cannot find a configuration element for a plugin with an "
341 + "artifactId of " + artifactId + "."));
342 return pluginConfigurationElement;
343 }
344
345
346
347
348
349
350 private static String resolveFromRootThenParent(Xpp3Dom pluginPomDom, String element) throws Exception {
351 return Optional.ofNullable(child(pluginPomDom, element).orElseGet(() -> child(pluginPomDom, "parent")
352 .flatMap(e -> child(e, element))
353 .orElse(null)))
354 .map(Xpp3Dom::getValue)
355 .orElseThrow(() -> new Exception("unable to determine " + element));
356 }
357
358
359
360
361
362
363 public static Object getVariableValueFromObject(Object object, String variable) throws IllegalAccessException {
364 Field field = ReflectionUtils.getFieldByNameIncludingSuperclasses(variable, object.getClass());
365 field.setAccessible(true);
366 return field.get(object);
367 }
368
369
370
371
372
373
374 public static Map<String, Object> getVariablesAndValuesFromObject(Object object) throws IllegalAccessException {
375 return getVariablesAndValuesFromObject(object.getClass(), object);
376 }
377
378
379
380
381
382
383
384
385 public static Map<String, Object> getVariablesAndValuesFromObject(Class<?> clazz, Object object)
386 throws IllegalAccessException {
387 Map<String, Object> map = new HashMap<>();
388 Field[] fields = clazz.getDeclaredFields();
389 AccessibleObject.setAccessible(fields, true);
390 for (Field field : fields) {
391 map.put(field.getName(), field.get(object));
392 }
393 Class<?> superclass = clazz.getSuperclass();
394 if (!Object.class.equals(superclass)) {
395 map.putAll(getVariablesAndValuesFromObject(superclass, object));
396 }
397 return map;
398 }
399
400
401
402
403 public static void setVariableValueToObject(Object object, String variable, Object value)
404 throws IllegalAccessException {
405 Field field = ReflectionUtils.getFieldByNameIncludingSuperclasses(variable, object.getClass());
406 Objects.requireNonNull(field, "Field " + variable + " not found");
407 field.setAccessible(true);
408 field.set(object, value);
409 }
410
411 static class WrapEvaluator implements TypeAwareExpressionEvaluator {
412
413 private final PlexusContainer container;
414 private final TypeAwareExpressionEvaluator evaluator;
415
416 WrapEvaluator(PlexusContainer container, TypeAwareExpressionEvaluator evaluator) {
417 this.container = container;
418 this.evaluator = evaluator;
419 }
420
421 @Override
422 public Object evaluate(String expression) throws ExpressionEvaluationException {
423 return evaluate(expression, null);
424 }
425
426 @Override
427 public Object evaluate(String expression, Class<?> type) throws ExpressionEvaluationException {
428 Object value = evaluator.evaluate(expression, type);
429 if (value == null) {
430 String expr = stripTokens(expression);
431 if (expr != null) {
432 try {
433 value = container.lookup(type, expr);
434 } catch (ComponentLookupException e) {
435
436 }
437 }
438 }
439 return value;
440 }
441
442 private String stripTokens(String expr) {
443 if (expr.startsWith("${") && expr.endsWith("}")) {
444 return expr.substring(2, expr.length() - 1);
445 }
446 return null;
447 }
448
449 @Override
450 public File alignToBaseDirectory(File path) {
451 return evaluator.alignToBaseDirectory(path);
452 }
453 }
454 }