blob: 358180afe10c22a84576a79de308e93ef9195bb6 [file] [log] [blame]
package org.apache.maven.api.plugin.testing;
/*
* 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.
*/
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.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
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.Dom;
import org.apache.maven.configuration.internal.EnhancedComponentConfigurator;
import org.apache.maven.internal.impl.DefaultLog;
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;
/**
*
*/
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 ) );
List<MojoParameter> mojoParameters = parameterContext.findRepeatableAnnotations( MojoParameter.class );
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
{
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 );
// Artifact artifact =
// lookup( RepositorySystem.class ).createArtifact( pluginDescriptor.getGroupId(),
// pluginDescriptor.getArtifactId(),
// pluginDescriptor.getVersion(), ".jar" );
//
// artifact.setFile( getPluginArtifactFile() );
// pluginDescriptor.setPluginArtifact( artifact );
// pluginDescriptor.setArtifacts( Collections.singletonList( artifact ) );
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, List<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() ) );
}
Dom pluginConfiguration = extractPluginConfiguration( coord[1], pomDom );
if ( !mojoParameters.isEmpty() )
{
List<Dom> children = mojoParameters.stream()
.map( mp -> new org.apache.maven.internal.xml.Xpp3Dom( mp.name(), mp.value() ) )
.collect( Collectors.toList() );
Dom config = new org.apache.maven.internal.xml.Xpp3Dom( "configuration",
null, null, children, null );
pluginConfiguration = Dom.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 of the relavent information
*/
protected Mojo lookupMojo( String[] coord, Dom 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 Dom finalizeConfig( Dom config, MojoDescriptor mojoDescriptor )
{
List<Dom> children = new ArrayList<>();
if ( mojoDescriptor != null && mojoDescriptor.getParameters() != null )
{
Dom defaultConfiguration = MojoDescriptorCreator.convert( mojoDescriptor ).getDom();
for ( Parameter parameter : mojoDescriptor.getParameters() )
{
Dom parameterConfiguration = config.getChild( parameter.getName() );
if ( parameterConfiguration == null )
{
parameterConfiguration = config.getChild( parameter.getAlias() );
}
Dom parameterDefaults = defaultConfiguration.getChild( parameter.getName() );
parameterConfiguration = Dom.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 org.apache.maven.internal.xml.Xpp3Dom(
parameter.getName(), parameterConfiguration.getValue(),
attributes, parameterConfiguration.getChildren(),
parameterConfiguration.getInputLocation() );
children.add( parameterConfiguration );
}
}
}
return new org.apache.maven.internal.xml.Xpp3Dom( "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 Dom 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.
*
* NOTE: the caller is responsible for casting to to what the desired type is.
*
* @param object
* @param variable
* @return object value of variable
* @throws IllegalArgumentException
*/
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)
*
* Note: the values in the map are of type Object so the caller is responsible for casting to desired types.
*
* @param object
* @return map of variable names and values
*/
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)
*
* Note: the values in the map are of type Object so the caller is responsible for casting to desired types.
*
* @param clazz
* @param object
* @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
*
* @param object
* @param variable
* @param value
* @throws IllegalAccessException
*/
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 );
}
}
}