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