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