V4MojoConfigurator.java
/*
* 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.plugin.internal;
import java.io.File;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.RecordComponent;
import java.lang.reflect.Type;
import java.net.URI;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import org.apache.maven.api.xml.XmlNode;
import org.apache.maven.plugin.PluginConfigurationException;
import org.apache.maven.plugin.descriptor.PluginDescriptor;
/** Configures a Maven 4 mojo directly from the public Maven XML model. */
public final class V4MojoConfigurator {
private static final Object NOT_SCALAR = new Object();
private final MojoExpressionEvaluator evaluator;
private final ClassLoader classLoader;
private final PluginDescriptor pluginDescriptor;
public V4MojoConfigurator(
MojoExpressionEvaluator evaluator, ClassLoader classLoader, PluginDescriptor pluginDescriptor) {
this.evaluator = evaluator;
this.classLoader = classLoader;
this.pluginDescriptor = pluginDescriptor;
}
/**
* Creates a configurator backed by a Plexus {@code TypeAwareExpressionEvaluator}.
*
* @deprecated Use {@link #V4MojoConfigurator(MojoExpressionEvaluator, ClassLoader, PluginDescriptor)} instead.
*/
@Deprecated
public V4MojoConfigurator(
org.codehaus.plexus.component.configurator.expression.TypeAwareExpressionEvaluator evaluator,
ClassLoader classLoader,
PluginDescriptor pluginDescriptor) {
this(new PlexusMojoExpressionEvaluatorAdapter(evaluator), classLoader, pluginDescriptor);
}
public void configure(Object mojo, XmlNode configuration) throws PluginConfigurationException {
if (configuration == null) {
return;
}
for (XmlNode child : configuration.children()) {
try {
set(mojo, child.name(), child);
} catch (Exception e) {
throw new PluginConfigurationException(
pluginDescriptor,
"Unable to configure '" + child.name() + "' on "
+ mojo.getClass().getName(),
e);
}
}
}
private void set(Object bean, String name, XmlNode node) throws Exception {
if (name.isEmpty()) {
throw new NoSuchFieldException("<empty element name>");
}
String title = Character.toUpperCase(name.charAt(0)) + name.substring(1);
for (Method method : bean.getClass().getMethods()) {
if ((method.getName().equals("set" + title) || method.getName().equals("add" + title))
&& method.getParameterCount() == 1) {
if (!method.canAccess(bean)) {
method.setAccessible(true);
}
method.invoke(bean, convert(node, method.getGenericParameterTypes()[0]));
return;
}
}
for (Class<?> type = bean.getClass(); type != null; type = type.getSuperclass()) {
try {
Field field = type.getDeclaredField(name);
field.setAccessible(true);
field.set(bean, convert(node, field.getGenericType()));
return;
} catch (NoSuchFieldException ignored) {
// Continue through the class hierarchy.
}
}
throw new NoSuchFieldException(name);
}
private Object convert(XmlNode node, Type target) throws Exception {
Class<?> type = rawType(target);
if (type == XmlNode.class) {
return node;
}
Object scalar = convertScalar(node, type);
if (scalar != NOT_SCALAR) {
return scalar;
}
if (type.isArray()) {
List<XmlNode> children = node.children();
if (children.isEmpty()) {
// No child elements: treat the text value as a comma-separated list
String text = evaluateNodeAsString(node);
String[] parts = text != null
? java.util.Arrays.stream(text.split(","))
.map(String::trim)
.filter(s -> !s.isEmpty())
.toArray(String[]::new)
: new String[0];
Object array = Array.newInstance(type.getComponentType(), parts.length);
for (int i = 0; i < parts.length; i++) {
Array.set(array, i, convert(XmlNode.newInstance("item", parts[i]), type.getComponentType()));
}
return array;
}
Object array = Array.newInstance(type.getComponentType(), children.size());
for (int i = 0; i < children.size(); i++) {
Array.set(array, i, convert(children.get(i), type.getComponentType()));
}
return array;
}
if (Collection.class.isAssignableFrom(type)) {
Type itemType = target instanceof ParameterizedType p ? p.getActualTypeArguments()[0] : Object.class;
Collection<Object> values =
Set.class.isAssignableFrom(type) ? new java.util.LinkedHashSet<>() : new ArrayList<>();
List<XmlNode> children = node.children();
if (children.isEmpty()) {
// No child elements: treat the text value as a comma-separated list
String text = evaluateNodeAsString(node);
if (text != null) {
for (String part : text.split(",")) {
String trimmed = part.trim();
if (!trimmed.isEmpty()) {
values.add(convert(XmlNode.newInstance("item", trimmed), itemType));
}
}
}
} else {
for (XmlNode child : children) {
values.add(convert(child, itemType));
}
}
return values;
}
if (Map.class.isAssignableFrom(type)) {
Type valueType = target instanceof ParameterizedType p ? p.getActualTypeArguments()[1] : Object.class;
if (Properties.class.isAssignableFrom(type)) {
Properties properties = new Properties();
for (XmlNode child : node.children()) {
// Support Plexus-style: <property><name>k</name><value>v</value></property>
XmlNode nameNode = child.child("name");
XmlNode valueNode = child.child("value");
if (nameNode != null && valueNode != null) {
String key = evaluateNodeAsString(nameNode);
Object val = convert(valueNode, valueType);
if (key != null && val != null) {
properties.put(key, val);
}
} else if (nameNode == null) {
// Simple <key>value</key> style
Object val = convert(child, valueType);
if (val != null) {
properties.put(child.name(), val);
}
}
// nameNode != null && valueNode == null: malformed Plexus entry ��� skip it
}
return properties;
}
Map<String, Object> values = new LinkedHashMap<>();
for (XmlNode child : node.children()) {
values.put(child.name(), convert(child, valueType));
}
return values;
}
type = resolveImplementation(node, type);
String value = evaluateNodeAsString(node);
if (value != null && node.children().isEmpty()) {
Method setter = findDefaultSetter(type);
if (setter != null) {
Object instance = instantiate(type);
if (!setter.canAccess(instance)) {
setter.setAccessible(true);
}
setter.invoke(
instance, convert(XmlNode.newInstance("value", value), setter.getGenericParameterTypes()[0]));
return instance;
}
}
if (type.isRecord()) {
return instantiateRecord(node, type);
}
Object instance = instantiate(type);
for (XmlNode child : node.children()) {
set(instance, child.name(), child);
}
return instance;
}
private Object convertScalar(XmlNode node, Class<?> type) throws Exception {
if (type == String.class) {
return evaluateNode(node, type);
}
if (type == Object.class) {
// For Object-typed parameters: if the node is a leaf (no children), evaluate as String.
// If it has children, convert to Map<String, Object> so that structured XML is
// preserved rather than crashing the bean path (Object has no fields to set).
if (node.children().isEmpty()) {
return evaluateNode(node, String.class);
}
Map<String, Object> map = new LinkedHashMap<>();
for (XmlNode child : node.children()) {
map.put(child.name(), convert(child, Object.class));
}
return map;
}
if (type == Path.class || type == File.class) {
Object evaluated = evaluateNode(node, type);
if (evaluated instanceof Path p) {
return type == File.class ? p.toFile() : p;
}
if (evaluated instanceof File f) {
return type == File.class ? f : f.toPath();
}
String value = evaluated != null ? String.valueOf(evaluated) : null;
if (value == null) {
return null;
}
Path path = Paths.get(value.replace('/', File.separatorChar));
path = evaluator.alignToBaseDirectory(path.toFile()).toPath();
return type == File.class ? path.toFile() : path;
}
if (type == URI.class) {
Object evaluated = evaluateNode(node, URI.class);
if (evaluated instanceof URI uri) {
return uri;
}
String s = evaluated != null ? String.valueOf(evaluated) : null;
return s == null ? null : URI.create(s);
}
if (type == boolean.class || type == Boolean.class) {
String s = evaluateNodeAsString(node);
if (s == null) {
return type == boolean.class ? Boolean.FALSE : null;
}
return Boolean.valueOf(s);
}
if (type == int.class || type == Integer.class) {
String s = evaluateNodeAsString(node);
if (s == null) {
return type == int.class ? Integer.valueOf(0) : null;
}
return Integer.valueOf(s);
}
if (type == long.class || type == Long.class) {
String s = evaluateNodeAsString(node);
if (s == null) {
return type == long.class ? Long.valueOf(0L) : null;
}
return Long.valueOf(s);
}
if (type == double.class || type == Double.class) {
String s = evaluateNodeAsString(node);
if (s == null) {
return type == double.class ? Double.valueOf(0.0d) : null;
}
return Double.valueOf(s);
}
if (type == float.class || type == Float.class) {
String s = evaluateNodeAsString(node);
if (s == null) {
return type == float.class ? Float.valueOf(0.0f) : null;
}
return Float.valueOf(s);
}
if (type == short.class || type == Short.class) {
String s = evaluateNodeAsString(node);
if (s == null) {
return type == short.class ? Short.valueOf((short) 0) : null;
}
return Short.valueOf(s);
}
if (type == byte.class || type == Byte.class) {
String s = evaluateNodeAsString(node);
if (s == null) {
return type == byte.class ? Byte.valueOf((byte) 0) : null;
}
return Byte.valueOf(s);
}
if (type == char.class || type == Character.class) {
String s = evaluateNodeAsString(node);
if (s == null || s.isEmpty()) {
return type == char.class ? Character.valueOf('\0') : null;
}
return s.charAt(0);
}
if (type.isEnum()) {
String s = evaluateNodeAsString(node);
if (s == null) {
return null;
}
@SuppressWarnings({"rawtypes", "unchecked"})
Object val = Enum.valueOf((Class) type, s);
return val;
}
return NOT_SCALAR;
}
private Class<?> resolveImplementation(XmlNode node, Class<?> type) throws Exception {
String implementation = node.attribute("implementation");
if (implementation != null && !implementation.isEmpty()) {
try {
return Class.forName(implementation, true, classLoader);
} catch (ClassNotFoundException e) {
if (!type.isSealed()) {
throw e;
}
String hint = implementation;
List<Class<?>> matches = java.util.Arrays.stream(type.getPermittedSubclasses())
.filter(candidate ->
hint.equals(candidate.getSimpleName()) || hint.equals(candidate.getCanonicalName()))
.toList();
if (matches.size() != 1) {
throw e;
}
return matches.get(0);
}
}
return type;
}
private Object instantiateRecord(XmlNode node, Class<?> type) throws Exception {
RecordComponent[] components = type.getRecordComponents();
Class<?>[] parameterTypes = new Class<?>[components.length];
Object[] arguments = new Object[components.length];
for (int i = 0; i < components.length; i++) {
RecordComponent component = components[i];
parameterTypes[i] = component.getType();
XmlNode child = node.child(component.getName());
arguments[i] =
child == null ? defaultValueFor(component.getType()) : convert(child, component.getGenericType());
}
var constructor = type.getDeclaredConstructor(parameterTypes);
if (!constructor.canAccess(null)) {
constructor.setAccessible(true);
}
return constructor.newInstance(arguments);
}
private static Object instantiate(Class<?> type) throws ReflectiveOperationException {
var ctor = type.getDeclaredConstructor();
if (!ctor.canAccess(null)) {
ctor.setAccessible(true);
}
return ctor.newInstance();
}
private static Object defaultValueFor(Class<?> type) {
if (type == boolean.class) {
return false;
}
if (type == byte.class) {
return (byte) 0;
}
if (type == short.class) {
return (short) 0;
}
if (type == int.class) {
return 0;
}
if (type == long.class) {
return 0L;
}
if (type == float.class) {
return 0.0f;
}
if (type == double.class) {
return 0.0d;
}
if (type == char.class) {
return '\0';
}
return null;
}
private Object evaluateNode(XmlNode node, Class<?> type) throws Exception {
String val = node.value();
Object evaluated = (val != null && !val.isEmpty()) ? evaluate(val, type) : null;
if (evaluated == null) {
String defaultValue = node.attribute("default-value");
if (defaultValue != null && !defaultValue.isEmpty()) {
evaluated = evaluate(defaultValue, type);
}
}
return evaluated;
}
private String evaluateNodeAsString(XmlNode node) throws Exception {
Object evaluated = evaluateNode(node, String.class);
return evaluated != null ? String.valueOf(evaluated) : null;
}
private Object evaluate(String value, Class<?> type) throws Exception {
if (value == null) {
return null;
}
return evaluator.evaluate(value, type);
}
private static Method findDefaultSetter(Class<?> type) {
for (Method method : type.getMethods()) {
if (method.getName().equals("set") && method.getParameterCount() == 1) {
return method;
}
}
return null;
}
private static Class<?> rawType(Type type) {
return type instanceof Class<?> c
? c
: type instanceof ParameterizedType p ? (Class<?>) p.getRawType() : Object.class;
}
}