View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
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             //            Artifact artifact =
138             //                    lookup( RepositorySystem.class ).createArtifact( pluginDescriptor.getGroupId(),
139             //                            pluginDescriptor.getArtifactId(),
140             //                            pluginDescriptor.getVersion(), ".jar" );
141             //
142             //            artifact.setFile( getPluginArtifactFile() );
143             //            pluginDescriptor.setPluginArtifact( artifact );
144             //            pluginDescriptor.setArtifacts( Collections.singletonList( artifact ) );
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      * lookup the mojo while we have all of the relavent information
207      */
208     protected Mojo lookupMojo(String[] coord, XmlNode pluginConfiguration, PluginDescriptor descriptor)
209             throws Exception {
210         // pluginkey = groupId : artifactId : version : goal
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      * sometimes the parent element might contain the correct value so generalize that access
309      *
310      * TODO find out where this is probably done elsewhere
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      * Convenience method to obtain the value of a variable on a mojo that might not have a getter.
322      *
323      * NOTE: the caller is responsible for casting to to what the desired type is.
324      *
325      * @param object
326      * @param variable
327      * @return object value of variable
328      * @throws IllegalArgumentException
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      * Convenience method to obtain all variables and values from the mojo (including its superclasses)
338      *
339      * Note: the values in the map are of type Object so the caller is responsible for casting to desired types.
340      *
341      * @param object
342      * @return map of variable names and values
343      */
344     public static Map<String, Object> getVariablesAndValuesFromObject(Object object) throws IllegalAccessException {
345         return getVariablesAndValuesFromObject(object.getClass(), object);
346     }
347 
348     /**
349      * Convenience method to obtain all variables and values from the mojo (including its superclasses)
350      *
351      * Note: the values in the map are of type Object so the caller is responsible for casting to desired types.
352      *
353      * @param clazz
354      * @param object
355      * @return map of variable names and values
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      * Convenience method to set values to variables in objects that don't have setters
374      *
375      * @param object
376      * @param variable
377      * @param value
378      * @throws IllegalAccessException
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                         // nothing
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 }