[MNG-8134] Add a @Resolution annotation to mojos to inject project dependencies collection / resolution result

This commit is contained in:
Guillaume Nodet 2024-06-04 21:09:03 +02:00
parent 8d483f922b
commit b852f74084
6 changed files with 231 additions and 13 deletions

View File

@ -29,10 +29,23 @@ import org.apache.maven.api.annotations.Experimental;
import org.apache.maven.api.annotations.Nonnull;
/**
* This annotation will mark your class as a Mojo (ie. goal in a Maven plugin).
* The mojo can be annotated with {@code jakarta.inject.*} annotations.
* This annotation will mark your class as a Mojo, which is the implementation of a goal in a Maven plugin.
* <p>
* The mojo can be annotated with {@code org.apache.maven.api.di.*} annotations to
* control the lifecycle of the mojo itself, and to inject other beans.
* </p>
* <p>
* The mojo class can also be injected with an {@link Execute} annotation to specify a
* forked lifecycle.
* </p>
* <p>
* The {@link Parameter} annotation can be added on fields to inject data
* from the plugin configuration or from other components.
* </p>
* <p>
* Fields can also be annotated with the {@link Resolution} annotation to be injected
* with the dependency collection or resolution result for the project.
* </p>
*
* @since 4.0.0
*/
@ -81,4 +94,22 @@ public @interface Mojo {
*/
@Nonnull
String configurator() default "";
/**
* Indicates whether dependency collection will be
* required when executing the Mojo.
* If not set, it will be inferred from the fields
* annotated with the {@link Resolution} annotation.
*/
@Nonnull
boolean dependencyCollection() default false;
/**
* Comma separated list of path scopes that will be
* required for dependency resolution.
* If not set, it will be inferred from the fields
* annotated with the {@link Resolution} annotation.
*/
@Nonnull
String dependencyResolutionPathScopes() default "";
}

View File

@ -0,0 +1,74 @@
/*
* 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.annotations;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.apache.maven.api.annotations.Experimental;
/**
* Indicates that a given field will be injected with the result of
* a dependency collection or resolution request. Whether a collection
* or resolution request is performed is controlled by the {@link #pathScope()}
* field, the injected field type and the {@link #requestType()}.
* <p>
* If the {@code requestType} is not set explicitly, it will be inferred
* from the {@code pathScope} and the injected field type. If the type
* is {@link org.apache.maven.api.Node Node} and {@code pathScope == ""},
* then the dependencies will be <i>collected</i>.
* If the type is {@link org.apache.maven.api.Node Node} or
* {@code List<}{@link org.apache.maven.api.Node Node}{@code >},
* and {@code pathScope != ""}, the dependencies will be <i>flattened</i>.
* Else the dependencies will be <i>resolved</i> and {@code pathScope} must be non empty,
* and the field type can be {@link org.apache.maven.api.Node Node},
* {@code List<}{@link org.apache.maven.api.Node Node}{@code >},
* {@link org.apache.maven.api.services.DependencyResolverResult DependencyResolverResult},
* {@code List<}{@link java.nio.file.Path Path}{@code >},
* {@code Map<}{@link org.apache.maven.api.PathType PathType}{@code , List<}{@link java.nio.file.Path Path}{@code >>},
* or {@code Map<}{@link org.apache.maven.api.Dependency Dependency}{@code , }{@link java.nio.file.Path Path}{@code >}.
*
* @since 4.0.0
*/
@Experimental
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface Resolution {
/**
* The id of a {@link org.apache.maven.api.PathScope} enum value.
* If specified, a dependency resolution request will be issued,
* else a dependency collection request will be done.
*
* @return the id of the path scope
*/
String pathScope() default "";
/**
* The request type, in case the default one is not correct.
* Valid values are {@code collect}, {@code flatten}, or {@code resolve}.
*
* @return the request type
*/
String requestType() default "";
}

View File

@ -397,6 +397,15 @@ under the License.
<multiplicity>*</multiplicity>
</association>
</field>
<field xdoc.separator="blank">
<name>resolutions</name>
<version>2.0.0+</version>
<description></description>
<association>
<type>Resolution</type>
<multiplicity>*</multiplicity>
</association>
</field>
<field xdoc.separator="blank">
<name>requirements</name>
<version>1.0.0/1.1.0</version>
@ -580,5 +589,34 @@ under the License.
</field>
</fields>
</class>
<class xdoc.anchorName="resolution">
<name>Resolution</name>
<version>2.0.0+</version>
<description>Dependency collection or resolution injection.</description>
<fields>
<field>
<name>field</name>
<required>false</required>
<version>2.0.0+</version>
<type>String</type>
<description>the name of the field to be injected</description>
</field>
<field>
<name>pathScope</name>
<required>false</required>
<version>2.0.0+</version>
<type>String</type>
<description>pathScope used to flatten dependencies</description>
</field>
<field>
<name>requestType</name>
<required>false</required>
<version>2.0.0+</version>
<type>String</type>
<description>either {@code collect}, {@code flatten}, or {@code resolve}</description>
</field>
</fields>
</class>
</classes>
</model>

View File

@ -24,15 +24,27 @@ import javax.inject.Singleton;
import java.io.*;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.*;
import java.util.jar.JarFile;
import java.util.stream.Collectors;
import java.util.zip.ZipEntry;
import org.apache.maven.RepositoryUtils;
import org.apache.maven.api.Dependency;
import org.apache.maven.api.Node;
import org.apache.maven.api.PathScope;
import org.apache.maven.api.PathType;
import org.apache.maven.api.Project;
import org.apache.maven.api.Session;
import org.apache.maven.api.plugin.descriptor.Resolution;
import org.apache.maven.api.services.DependencyResolver;
import org.apache.maven.api.services.DependencyResolverResult;
import org.apache.maven.api.services.PathScopeRegistry;
import org.apache.maven.api.services.ProjectManager;
import org.apache.maven.api.xml.XmlNode;
import org.apache.maven.artifact.Artifact;
@ -575,6 +587,78 @@ public class DefaultMavenPluginManager implements MavenPluginManager {
pomConfiguration,
expressionEvaluator);
for (Resolution resolution : mojoDescriptor.getMojoDescriptorV4().getResolutions()) {
Field field = null;
for (Class<?> clazz = mojo.getClass(); clazz != Object.class; clazz = clazz.getSuperclass()) {
try {
field = clazz.getDeclaredField(resolution.getField());
break;
} catch (NoSuchFieldException e) {
// continue
}
}
if (field == null) {
throw new PluginConfigurationException(
pluginDescriptor,
"Unable to find field '" + resolution.getField() + "' annotated with @Resolution");
}
field.setAccessible(true);
String pathScope = resolution.getPathScope();
Object result = null;
if (pathScope != null && !pathScope.isEmpty()) {
// resolution
PathScope ps = sessionV4.getService(PathScopeRegistry.class).require(pathScope);
DependencyResolverResult res =
sessionV4.getService(DependencyResolver.class).resolve(sessionV4, project, ps);
if (field.getType() == DependencyResolverResult.class) {
result = res;
} else if (field.getType() == Node.class) {
result = res.getRoot();
} else if (field.getType() == List.class && field.getGenericType() instanceof ParameterizedType pt) {
Type t = pt.getActualTypeArguments()[0];
if (t == Node.class) {
result = res.getNodes();
} else if (t == Path.class) {
result = res.getPaths();
}
} else if (field.getType() == Map.class && field.getGenericType() instanceof ParameterizedType pt) {
Type k = pt.getActualTypeArguments()[0];
Type v = pt.getActualTypeArguments()[1];
if (k == PathType.class
&& v instanceof ParameterizedType ptv
&& ptv.getRawType() == List.class
&& ptv.getActualTypeArguments()[0] == Path.class) {
result = res.getDispatchedPaths();
} else if (k == Dependency.class && v == Path.class) {
result = res.getDependencies();
}
}
} else {
// collection
DependencyResolverResult res =
sessionV4.getService(DependencyResolver.class).collect(sessionV4, project);
if (field.getType() == DependencyResolverResult.class) {
result = res;
} else if (field.getType() == Node.class) {
result = res.getRoot();
}
}
if (result == null) {
throw new PluginConfigurationException(
pluginDescriptor,
"Unable to inject field '" + resolution.getField()
+ "' annotated with @Dependencies. Unsupported type " + field.getGenericType());
}
try {
field.set(mojo, result);
} catch (IllegalAccessException e) {
throw new PluginConfigurationException(
pluginDescriptor,
"Unable to inject field '" + resolution.getField() + "' annotated with @Dependencies",
e);
}
}
return mojo;
}
@ -730,6 +814,7 @@ public class DefaultMavenPluginManager implements MavenPluginManager {
validateParameters(mojoDescriptor, configuration, expressionEvaluator);
}
}
} catch (ComponentConfigurationException e) {
String message = "Unable to parse configuration of mojo " + mojoDescriptor.getId();
if (e.getFailedConfiguration() != null) {

View File

@ -166,7 +166,6 @@ public class MojoDescriptor extends ComponentDescriptor<Mojo> implements Cloneab
this.setProjectRequired(md.isProjectRequired());
this.setSince(md.getSince());
this.setThreadSafe(true);
this.setV4Api(true);
this.setImplementation(md.getImplementation());
try {
this.setParameters(md.getParameters().stream().map(Parameter::new).collect(Collectors.toList()));
@ -174,6 +173,7 @@ public class MojoDescriptor extends ComponentDescriptor<Mojo> implements Cloneab
throw new IllegalArgumentException(e);
}
this.mojoDescriptorV4 = md;
this.v4Api = true;
}
// ----------------------------------------------------------------------
//
@ -622,10 +622,6 @@ public class MojoDescriptor extends ComponentDescriptor<Mojo> implements Cloneab
return v4Api;
}
public void setV4Api(boolean v4Api) {
this.v4Api = v4Api;
}
/**
* Creates a shallow copy of this mojo descriptor.
*/

View File

@ -385,12 +385,6 @@ public class PluginDescriptorBuilder {
mojo.setThreadSafe(Boolean.parseBoolean(threadSafe));
}
String v4Api = c.getChild("v4Api").getValue();
if (v4Api != null) {
mojo.setV4Api(Boolean.parseBoolean(v4Api));
}
// ----------------------------------------------------------------------
// Configuration
// ----------------------------------------------------------------------