blob: b217fab6bb64e74e02c9a91556a9d7784b402877 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.maven.api.plugin.testing;
import java.io.BufferedReader;
import java.io.File;
import java.io.InputStream;
import java.io.Reader;
import java.io.StringReader;
import java.lang.reflect.AccessibleObject;
import java.lang.reflect.Field;
import java.net.URL;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import com.google.inject.internal.ProviderMethodsModule;
import org.apache.maven.api.MojoExecution;
import org.apache.maven.api.Project;
import org.apache.maven.api.Session;
import org.apache.maven.api.plugin.Log;
import org.apache.maven.api.plugin.Mojo;
import org.apache.maven.api.xml.XmlNode;
import org.apache.maven.configuration.internal.EnhancedComponentConfigurator;
import org.apache.maven.internal.impl.DefaultLog;
import org.apache.maven.internal.xml.XmlNodeImpl;
import org.apache.maven.lifecycle.internal.MojoDescriptorCreator;
import org.apache.maven.plugin.PluginParameterExpressionEvaluatorV4;
import org.apache.maven.plugin.descriptor.MojoDescriptor;
import org.apache.maven.plugin.descriptor.Parameter;
import org.apache.maven.plugin.descriptor.PluginDescriptor;
import org.apache.maven.plugin.descriptor.PluginDescriptorBuilder;
import org.codehaus.plexus.DefaultPlexusContainer;
import org.codehaus.plexus.PlexusContainer;
import org.codehaus.plexus.component.configurator.ComponentConfigurator;
import org.codehaus.plexus.component.configurator.expression.ExpressionEvaluationException;
import org.codehaus.plexus.component.configurator.expression.ExpressionEvaluator;
import org.codehaus.plexus.component.configurator.expression.TypeAwareExpressionEvaluator;
import org.codehaus.plexus.component.repository.ComponentDescriptor;
import org.codehaus.plexus.component.repository.exception.ComponentLookupException;
import org.codehaus.plexus.configuration.xml.XmlPlexusConfiguration;
import org.codehaus.plexus.testing.PlexusExtension;
import org.codehaus.plexus.util.InterpolationFilterReader;
import org.codehaus.plexus.util.ReaderFactory;
import org.codehaus.plexus.util.ReflectionUtils;
import org.codehaus.plexus.util.xml.XmlStreamReader;
import org.codehaus.plexus.util.xml.Xpp3Dom;
import org.codehaus.plexus.util.xml.Xpp3DomBuilder;
import org.junit.jupiter.api.extension.ExtensionContext;
import org.junit.jupiter.api.extension.ParameterContext;
import org.junit.jupiter.api.extension.ParameterResolutionException;
import org.junit.jupiter.api.extension.ParameterResolver;
import org.slf4j.LoggerFactory;
/**
* JUnit extension to help testing Mojos. The extension should be automatically registered
* by adding the {@link MojoTest} annotation on the test class.
*
* @see MojoTest
* @see InjectMojo
* @see MojoParameter
*/
public class MojoExtension extends PlexusExtension implements ParameterResolver {
@Override
public boolean supportsParameter(ParameterContext parameterContext, ExtensionContext extensionContext)
throws ParameterResolutionException {
return parameterContext.isAnnotated(InjectMojo.class)
|| parameterContext.getDeclaringExecutable().isAnnotationPresent(InjectMojo.class);
}
@Override
public Object resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext)
throws ParameterResolutionException {
try {
InjectMojo injectMojo = parameterContext
.findAnnotation(InjectMojo.class)
.orElseGet(() -> parameterContext.getDeclaringExecutable().getAnnotation(InjectMojo.class));
Set<MojoParameter> mojoParameters =
new HashSet<>(parameterContext.findRepeatableAnnotations(MojoParameter.class));
Optional.ofNullable(parameterContext.getDeclaringExecutable().getAnnotation(MojoParameter.class))
.ifPresent(mojoParameters::add);
Optional.ofNullable(parameterContext.getDeclaringExecutable().getAnnotation(MojoParameters.class))
.map(MojoParameters::value)
.map(Arrays::asList)
.ifPresent(mojoParameters::addAll);
Class<?> holder = parameterContext.getTarget().get().getClass();
PluginDescriptor descriptor = extensionContext
.getStore(ExtensionContext.Namespace.GLOBAL)
.get(PluginDescriptor.class, PluginDescriptor.class);
return lookupMojo(holder, injectMojo, mojoParameters, descriptor);
} catch (Exception e) {
throw new ParameterResolutionException("Unable to resolve parameter", e);
}
}
@Override
public void beforeEach(ExtensionContext context) throws Exception {
// TODO provide protected setters in PlexusExtension
Field field = PlexusExtension.class.getDeclaredField("basedir");
field.setAccessible(true);
field.set(null, getBasedir());
field = PlexusExtension.class.getDeclaredField("context");
field.setAccessible(true);
field.set(this, context);
getContainer().addComponent(getContainer(), PlexusContainer.class.getName());
((DefaultPlexusContainer) getContainer()).addPlexusInjector(Collections.emptyList(), binder -> {
binder.install(ProviderMethodsModule.forObject(context.getRequiredTestInstance()));
binder.requestInjection(context.getRequiredTestInstance());
binder.bind(Log.class).toInstance(new DefaultLog(LoggerFactory.getLogger("anonymous")));
});
Map<Object, Object> map = getContainer().getContext().getContextData();
ClassLoader classLoader = context.getRequiredTestClass().getClassLoader();
try (InputStream is = Objects.requireNonNull(
classLoader.getResourceAsStream(getPluginDescriptorLocation()),
"Unable to find plugin descriptor: " + getPluginDescriptorLocation());
Reader reader = new BufferedReader(new XmlStreamReader(is));
InterpolationFilterReader interpolationReader = new InterpolationFilterReader(reader, map, "${", "}")) {
PluginDescriptor pluginDescriptor = new PluginDescriptorBuilder().build(interpolationReader);
context.getStore(ExtensionContext.Namespace.GLOBAL).put(PluginDescriptor.class, pluginDescriptor);
for (ComponentDescriptor<?> desc : pluginDescriptor.getComponents()) {
getContainer().addComponentDescriptor(desc);
}
}
}
protected String getPluginDescriptorLocation() {
return "META-INF/maven/plugin.xml";
}
private Mojo lookupMojo(
Class<?> holder,
InjectMojo injectMojo,
Collection<MojoParameter> mojoParameters,
PluginDescriptor descriptor)
throws Exception {
String goal = injectMojo.goal();
String pom = injectMojo.pom();
String[] coord = mojoCoordinates(goal);
Xpp3Dom pomDom;
if (pom.startsWith("file:")) {
Path path = Paths.get(getBasedir()).resolve(pom.substring("file:".length()));
pomDom = Xpp3DomBuilder.build(ReaderFactory.newXmlReader(path.toFile()));
} else if (pom.startsWith("classpath:")) {
URL url = holder.getResource(pom.substring("classpath:".length()));
if (url == null) {
throw new IllegalStateException("Unable to find pom on classpath: " + pom);
}
pomDom = Xpp3DomBuilder.build(ReaderFactory.newXmlReader(url.openStream()));
} else if (pom.contains("<project>")) {
pomDom = Xpp3DomBuilder.build(new StringReader(pom));
} else {
Path path = Paths.get(getBasedir()).resolve(pom);
pomDom = Xpp3DomBuilder.build(ReaderFactory.newXmlReader(path.toFile()));
}
XmlNode pluginConfiguration = extractPluginConfiguration(coord[1], pomDom);
if (!mojoParameters.isEmpty()) {
List<XmlNode> children = mojoParameters.stream()
.map(mp -> new XmlNodeImpl(mp.name(), mp.value()))
.collect(Collectors.toList());
XmlNode config = new XmlNodeImpl("configuration", null, null, children, null);
pluginConfiguration = XmlNode.merge(config, pluginConfiguration);
}
Mojo mojo = lookupMojo(coord, pluginConfiguration, descriptor);
return mojo;
}
protected String[] mojoCoordinates(String goal) throws Exception {
if (goal.matches(".*:.*:.*:.*")) {
return goal.split(":");
} else {
Path pluginPom = Paths.get(getBasedir(), "pom.xml");
Xpp3Dom pluginPomDom = Xpp3DomBuilder.build(ReaderFactory.newXmlReader(pluginPom.toFile()));
String artifactId = pluginPomDom.getChild("artifactId").getValue();
String groupId = resolveFromRootThenParent(pluginPomDom, "groupId");
String version = resolveFromRootThenParent(pluginPomDom, "version");
return new String[] {groupId, artifactId, version, goal};
}
}
/**
* lookup the mojo while we have all the relevent information
*/
protected Mojo lookupMojo(String[] coord, XmlNode pluginConfiguration, PluginDescriptor descriptor)
throws Exception {
// pluginkey = groupId : artifactId : version : goal
Mojo mojo = lookup(Mojo.class, coord[0] + ":" + coord[1] + ":" + coord[2] + ":" + coord[3]);
for (MojoDescriptor mojoDescriptor : descriptor.getMojos()) {
if (Objects.equals(
mojoDescriptor.getImplementation(), mojo.getClass().getName())) {
if (pluginConfiguration != null) {
pluginConfiguration = finalizeConfig(pluginConfiguration, mojoDescriptor);
}
}
}
if (pluginConfiguration != null) {
Session session = getContainer().lookup(Session.class);
Project project;
try {
project = getContainer().lookup(Project.class);
} catch (ComponentLookupException e) {
project = null;
}
org.apache.maven.plugin.MojoExecution mojoExecution;
try {
MojoExecution me = getContainer().lookup(MojoExecution.class);
mojoExecution = new org.apache.maven.plugin.MojoExecution(
new org.apache.maven.model.Plugin(me.getPlugin()), me.getGoal(), me.getExecutionId());
} catch (ComponentLookupException e) {
mojoExecution = null;
}
ExpressionEvaluator evaluator = new WrapEvaluator(
getContainer(), new PluginParameterExpressionEvaluatorV4(session, project, mojoExecution));
ComponentConfigurator configurator = new EnhancedComponentConfigurator();
configurator.configureComponent(
mojo,
new XmlPlexusConfiguration(new Xpp3Dom(pluginConfiguration)),
evaluator,
getContainer().getContainerRealm());
}
return mojo;
}
private XmlNode finalizeConfig(XmlNode config, MojoDescriptor mojoDescriptor) {
List<XmlNode> children = new ArrayList<>();
if (mojoDescriptor != null && mojoDescriptor.getParameters() != null) {
XmlNode defaultConfiguration =
MojoDescriptorCreator.convert(mojoDescriptor).getDom();
for (Parameter parameter : mojoDescriptor.getParameters()) {
XmlNode parameterConfiguration = config.getChild(parameter.getName());
if (parameterConfiguration == null) {
parameterConfiguration = config.getChild(parameter.getAlias());
}
XmlNode parameterDefaults = defaultConfiguration.getChild(parameter.getName());
parameterConfiguration = XmlNode.merge(parameterConfiguration, parameterDefaults, Boolean.TRUE);
if (parameterConfiguration != null) {
Map<String, String> attributes = new HashMap<>(parameterConfiguration.getAttributes());
if (isEmpty(parameterConfiguration.getAttribute("implementation"))
&& !isEmpty(parameter.getImplementation())) {
attributes.put("implementation", parameter.getImplementation());
}
parameterConfiguration = new XmlNodeImpl(
parameter.getName(),
parameterConfiguration.getValue(),
attributes,
parameterConfiguration.getChildren(),
parameterConfiguration.getInputLocation());
children.add(parameterConfiguration);
}
}
}
return new XmlNodeImpl("configuration", null, null, children, null);
}
private boolean isEmpty(String str) {
return str == null || str.isEmpty();
}
private static Optional<Xpp3Dom> child(Xpp3Dom element, String name) {
return Optional.ofNullable(element.getChild(name));
}
private static Stream<Xpp3Dom> children(Xpp3Dom element) {
return Stream.of(element.getChildren());
}
public static XmlNode extractPluginConfiguration(String artifactId, Xpp3Dom pomDom) throws Exception {
Xpp3Dom pluginConfigurationElement = child(pomDom, "build")
.flatMap(buildElement -> child(buildElement, "plugins"))
.map(MojoExtension::children)
.orElseGet(Stream::empty)
.filter(e -> e.getChild("artifactId").getValue().equals(artifactId))
.findFirst()
.flatMap(buildElement -> child(buildElement, "configuration"))
.orElseThrow(
() -> new ConfigurationException("Cannot find a configuration element for a plugin with an "
+ "artifactId of " + artifactId + "."));
return pluginConfigurationElement.getDom();
}
/**
* sometimes the parent element might contain the correct value so generalize that access
*
* TODO find out where this is probably done elsewhere
*/
private static String resolveFromRootThenParent(Xpp3Dom pluginPomDom, String element) throws Exception {
return Optional.ofNullable(child(pluginPomDom, element).orElseGet(() -> child(pluginPomDom, "parent")
.flatMap(e -> child(e, element))
.orElse(null)))
.map(Xpp3Dom::getValue)
.orElseThrow(() -> new Exception("unable to determine " + element));
}
/**
* Convenience method to obtain the value of a variable on a mojo that might not have a getter.
* <br>
* NOTE: the caller is responsible for casting to what the desired type is.
*/
public static Object getVariableValueFromObject(Object object, String variable) throws IllegalAccessException {
Field field = ReflectionUtils.getFieldByNameIncludingSuperclasses(variable, object.getClass());
field.setAccessible(true);
return field.get(object);
}
/**
* Convenience method to obtain all variables and values from the mojo (including its superclasses)
* <br>
* Note: the values in the map are of type Object so the caller is responsible for casting to desired types.
*/
public static Map<String, Object> getVariablesAndValuesFromObject(Object object) throws IllegalAccessException {
return getVariablesAndValuesFromObject(object.getClass(), object);
}
/**
* Convenience method to obtain all variables and values from the mojo (including its superclasses)
* <br>
* Note: the values in the map are of type Object so the caller is responsible for casting to desired types.
*
* @return map of variable names and values
*/
public static Map<String, Object> getVariablesAndValuesFromObject(Class<?> clazz, Object object)
throws IllegalAccessException {
Map<String, Object> map = new HashMap<>();
Field[] fields = clazz.getDeclaredFields();
AccessibleObject.setAccessible(fields, true);
for (Field field : fields) {
map.put(field.getName(), field.get(object));
}
Class<?> superclass = clazz.getSuperclass();
if (!Object.class.equals(superclass)) {
map.putAll(getVariablesAndValuesFromObject(superclass, object));
}
return map;
}
/**
* Convenience method to set values to variables in objects that don't have setters
*/
public static void setVariableValueToObject(Object object, String variable, Object value)
throws IllegalAccessException {
Field field = ReflectionUtils.getFieldByNameIncludingSuperclasses(variable, object.getClass());
Objects.requireNonNull(field, "Field " + variable + " not found");
field.setAccessible(true);
field.set(object, value);
}
static class WrapEvaluator implements TypeAwareExpressionEvaluator {
private final PlexusContainer container;
private final TypeAwareExpressionEvaluator evaluator;
WrapEvaluator(PlexusContainer container, TypeAwareExpressionEvaluator evaluator) {
this.container = container;
this.evaluator = evaluator;
}
@Override
public Object evaluate(String expression) throws ExpressionEvaluationException {
return evaluate(expression, null);
}
@Override
public Object evaluate(String expression, Class<?> type) throws ExpressionEvaluationException {
Object value = evaluator.evaluate(expression, type);
if (value == null) {
String expr = stripTokens(expression);
if (expr != null) {
try {
value = container.lookup(type, expr);
} catch (ComponentLookupException e) {
// nothing
}
}
}
return value;
}
private String stripTokens(String expr) {
if (expr.startsWith("${") && expr.endsWith("}")) {
return expr.substring(2, expr.length() - 1);
}
return null;
}
@Override
public File alignToBaseDirectory(File path) {
return evaluator.alignToBaseDirectory(path);
}
}
}