gnodet-bot commented on code in PR #13636:
URL: https://github.com/apache/maven/pull/13636#discussion_r4482489244


##########
impl/maven-core/src/main/java/org/apache/maven/plugin/internal/V4MojoConfigurator.java:
##########
@@ -0,0 +1,421 @@
+/*
+ * 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;
+import 
org.codehaus.plexus.component.configurator.expression.ExpressionEvaluator;
+import 
org.codehaus.plexus.component.configurator.expression.TypeAwareExpressionEvaluator;
+
+/** 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 ExpressionEvaluator evaluator;
+    private final ClassLoader classLoader;
+    private final PluginDescriptor pluginDescriptor;
+
+    public V4MojoConfigurator(
+            ExpressionEvaluator evaluator, ClassLoader classLoader, 
PluginDescriptor pluginDescriptor) {
+        this.evaluator = evaluator;
+        this.classLoader = classLoader;
+        this.pluginDescriptor = 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 ? text.split(",", -1) : 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].trim()), 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(",", -1)) {
+                        values.add(convert(XmlNode.newInstance("item", 
part.trim()), 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 {
+                        // Support key=element-name, value=text: 
<key>value</key>
+                        Object val = convert(child, valueType);
+                        if (val != null) {
+                            properties.put(child.name(), val);
+                        }
+                    }
+                }
+                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 || type == Object.class) {

Review Comment:
   ❌ **`Object.class` treated as scalar silently drops nested XML children.**
   
   `convertScalar` returns `evaluateNode(node, Object.class)` for 
`Object.class` — which returns `null` when the node has children but no text 
content. Back in `convert()`, `null != NOT_SCALAR` is `true`, so `null` is 
returned immediately and the array/collection/map/bean fallbacks are never 
reached.
   
   Any `Map<String, Object>` field or `Object`-typed parameter with nested XML 
children will silently resolve to `null`.
   
   **Fix:** for `Object.class`, check whether the node has children; if so, 
return `NOT_SCALAR` to let `convert()` fall through to the structural branches.
   
   ```suggestion
           if (type == String.class) {
               return evaluateNode(node, type);
           }
           if (type == Object.class) {
               Object evaluated = evaluateNode(node, type);
               return (evaluated != null || node.children().isEmpty()) ? 
evaluated : NOT_SCALAR;
           }
   ```



##########
impl/maven-core/src/main/java/org/apache/maven/plugin/internal/V4MojoConfigurator.java:
##########
@@ -0,0 +1,421 @@
+/*
+ * 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;
+import 
org.codehaus.plexus.component.configurator.expression.ExpressionEvaluator;
+import 
org.codehaus.plexus.component.configurator.expression.TypeAwareExpressionEvaluator;
+
+/** 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 ExpressionEvaluator evaluator;
+    private final ClassLoader classLoader;
+    private final PluginDescriptor pluginDescriptor;
+
+    public V4MojoConfigurator(
+            ExpressionEvaluator evaluator, ClassLoader classLoader, 
PluginDescriptor pluginDescriptor) {
+        this.evaluator = evaluator;
+        this.classLoader = classLoader;
+        this.pluginDescriptor = 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 ? text.split(",", -1) : new 
String[0];

Review Comment:
   ⚠️ **`split(",", -1)` preserves empty tokens — `NumberFormatException` for 
non-String component types.**
   
   `"a,,b".split(",", -1)` → `["a","","b"]`. The `.trim()` call removes 
whitespace but not empty strings. For `int[]` or `List<Integer>`, passing an 
empty string to the downstream `convert()` causes `NumberFormatException`.
   
   Same issue on line 141 for collections.
   
   ```suggestion
                   String[] parts = text != null
                           ? java.util.Arrays.stream(text.split(",", -1))
                                   .map(String::trim)
                                   .filter(s -> !s.isEmpty())
                                   .toArray(String[]::new)
                           : new String[0];
   ```



##########
impl/maven-core/src/main/java/org/apache/maven/plugin/internal/V4MojoConfigurator.java:
##########
@@ -0,0 +1,421 @@
+/*
+ * 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;
+import 
org.codehaus.plexus.component.configurator.expression.ExpressionEvaluator;
+import 
org.codehaus.plexus.component.configurator.expression.TypeAwareExpressionEvaluator;
+
+/** 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 ExpressionEvaluator evaluator;
+    private final ClassLoader classLoader;
+    private final PluginDescriptor pluginDescriptor;
+
+    public V4MojoConfigurator(
+            ExpressionEvaluator evaluator, ClassLoader classLoader, 
PluginDescriptor pluginDescriptor) {
+        this.evaluator = evaluator;
+        this.classLoader = classLoader;
+        this.pluginDescriptor = 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 ? text.split(",", -1) : 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].trim()), 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(",", -1)) {
+                        values.add(convert(XmlNode.newInstance("item", 
part.trim()), 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) {

Review Comment:
   ⚠️ **`<property>` with `<name>` but no `<value>` falls into the wrong 
else-branch.**
   
   When `nameNode != null && valueNode == null`, the else-branch runs:
   ```java
   properties.put(child.name(), val);
   ```
   `child.name()` is `"property"` (the XML tag), not the `<name>` content. The 
`<name>` text is discarded and an entry with the literal key `"property"` is 
added instead — semantically wrong.
   
   ```suggestion
                       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) {
                           // Plexus-style with <name> but missing <value> — 
use null-safe empty default
                           String key = evaluateNodeAsString(nameNode);
                           if (key != null) {
                               properties.put(key, "");
                           }
                       } else {
   ```



##########
impl/maven-core/src/test/java/org/apache/maven/plugin/internal/V4MojoConfiguratorTest.java:
##########
@@ -0,0 +1,433 @@
+/*
+ * 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.nio.file.Path;
+import java.nio.file.Paths;
+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.internal.xml.XmlNodeImpl;
+import org.apache.maven.plugin.PluginConfigurationException;
+import org.apache.maven.plugin.descriptor.PluginDescriptor;
+import 
org.codehaus.plexus.component.configurator.expression.ExpressionEvaluator;
+import 
org.codehaus.plexus.component.configurator.expression.TypeAwareExpressionEvaluator;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+class V4MojoConfiguratorTest {
+
+    private TypeAwareExpressionEvaluator evaluator;
+    private PluginDescriptor pluginDescriptor;
+    private V4MojoConfigurator configurator;
+
+    @BeforeEach
+    void setUp() throws Exception {
+        evaluator = mock(TypeAwareExpressionEvaluator.class);
+        pluginDescriptor = new PluginDescriptor();
+        configurator = new V4MojoConfigurator(evaluator, 
getClass().getClassLoader(), pluginDescriptor);
+
+        when(evaluator.evaluate(anyString())).thenAnswer(invocation -> 
invocation.getArgument(0));
+        when(evaluator.evaluate(anyString(), any())).thenAnswer(invocation -> 
invocation.getArgument(0));
+        
when(evaluator.alignToBaseDirectory(any(File.class))).thenAnswer(invocation -> {
+            File f = invocation.getArgument(0);
+            return f.isAbsolute() ? f : new File(new File("base"), 
f.getPath()).getAbsoluteFile();
+        });
+    }
+
+    public enum Status {
+        ACTIVE,
+        INACTIVE
+    }
+
+    public static class ScalarMojo {
+        String str;
+        int primitiveInt;
+        Integer boxedInt;
+        long primitiveLong;
+        Long boxedLong;
+        double primitiveDouble;
+        Double boxedDouble;
+        float primitiveFloat;
+        Float boxedFloat;
+        short primitiveShort;
+        Short boxedShort;
+        byte primitiveByte;
+        Byte boxedByte;
+        boolean primitiveBool;
+        Boolean boxedBool;
+        char primitiveChar;
+        Character boxedChar;
+        Status status;
+        File file;
+        Path path;
+    }
+
+    @Test
+    void testScalarTypesConfiguration() throws Exception {
+        ScalarMojo mojo = new ScalarMojo();
+        XmlNode config = new XmlNodeImpl(
+                "configuration",
+                null,
+                Map.of(),
+                List.of(
+                        new XmlNodeImpl("str", "hello"),
+                        new XmlNodeImpl("primitiveInt", "42"),
+                        new XmlNodeImpl("boxedInt", "43"),
+                        new XmlNodeImpl("primitiveLong", "100"),
+                        new XmlNodeImpl("boxedLong", "101"),
+                        new XmlNodeImpl("primitiveDouble", "3.14"),
+                        new XmlNodeImpl("boxedDouble", "2.71"),
+                        new XmlNodeImpl("primitiveFloat", "1.5"),
+                        new XmlNodeImpl("boxedFloat", "2.5"),
+                        new XmlNodeImpl("primitiveShort", "10"),
+                        new XmlNodeImpl("boxedShort", "11"),
+                        new XmlNodeImpl("primitiveByte", "1"),
+                        new XmlNodeImpl("boxedByte", "2"),
+                        new XmlNodeImpl("primitiveBool", "true"),
+                        new XmlNodeImpl("boxedBool", "false"),
+                        new XmlNodeImpl("primitiveChar", "X"),
+                        new XmlNodeImpl("boxedChar", "Y"),
+                        new XmlNodeImpl("status", "ACTIVE"),
+                        new XmlNodeImpl("file", "relative/file.txt"),
+                        new XmlNodeImpl("path", "relative/path.txt")),
+                null);
+
+        configurator.configure(mojo, config);
+
+        assertEquals("hello", mojo.str);
+        assertEquals(42, mojo.primitiveInt);
+        assertEquals(43, mojo.boxedInt);
+        assertEquals(100L, mojo.primitiveLong);
+        assertEquals(101L, mojo.boxedLong);
+        assertEquals(3.14, mojo.primitiveDouble, 0.001);
+        assertEquals(2.71, mojo.boxedDouble, 0.001);
+        assertEquals(1.5f, mojo.primitiveFloat, 0.001f);
+        assertEquals(2.5f, mojo.boxedFloat, 0.001f);
+        assertEquals((short) 10, mojo.primitiveShort);
+        assertEquals((short) 11, mojo.boxedShort);
+        assertEquals((byte) 1, mojo.primitiveByte);
+        assertEquals((byte) 2, mojo.boxedByte);
+        assertTrue(mojo.primitiveBool);
+        assertFalse(mojo.boxedBool);
+        assertEquals('X', mojo.primitiveChar);
+        assertEquals('Y', mojo.boxedChar);
+        assertEquals(Status.ACTIVE, mojo.status);
+        assertEquals(new File(new File("base"), 
"relative/file.txt").getAbsoluteFile(), mojo.file);
+        assertEquals(Paths.get(new File(new File("base"), 
"relative/path.txt").getAbsolutePath()), mojo.path);
+    }
+
+    @Test
+    void testUnresolvedExpressionsDoNotThrowAndUseDefaults() throws Exception {
+        ExpressionEvaluator nullEvaluator = mock(ExpressionEvaluator.class);
+        when(nullEvaluator.evaluate(anyString())).thenReturn(null);
+        V4MojoConfigurator nullConfigurator =
+                new V4MojoConfigurator(nullEvaluator, 
getClass().getClassLoader(), pluginDescriptor);
+
+        ScalarMojo mojo = new ScalarMojo();
+        XmlNode config = new XmlNodeImpl(
+                "configuration",
+                null,
+                Map.of(),
+                List.of(
+                        new XmlNodeImpl("str", "${unresolved.str}"),
+                        new XmlNodeImpl("boxedInt", "${unresolved.int}"),
+                        new XmlNodeImpl("primitiveInt", "${unresolved.int}"),
+                        new XmlNodeImpl("boxedChar", "${unresolved.char}"),
+                        new XmlNodeImpl("primitiveChar", "${unresolved.char}"),
+                        new XmlNodeImpl("status", "${unresolved.status}"),
+                        new XmlNodeImpl("file", "${unresolved.file}")),
+                null);
+
+        nullConfigurator.configure(mojo, config);
+
+        assertNull(mojo.str);
+        assertNull(mojo.boxedInt);
+        assertEquals(0, mojo.primitiveInt);
+        assertNull(mojo.boxedChar);
+        assertEquals('\0', mojo.primitiveChar);
+        assertNull(mojo.status);
+        assertNull(mojo.file);
+    }
+
+    @Test
+    void testDefaultValueAttributeFallback() throws Exception {
+        ExpressionEvaluator nullEvaluator = mock(ExpressionEvaluator.class);
+        when(nullEvaluator.evaluate(anyString())).thenAnswer(inv -> {
+            String val = inv.getArgument(0);
+            return val.startsWith("${unresolved") ? null : val;
+        });
+        V4MojoConfigurator fallbackConfigurator =
+                new V4MojoConfigurator(nullEvaluator, 
getClass().getClassLoader(), pluginDescriptor);
+
+        ScalarMojo mojo = new ScalarMojo();
+        XmlNode config = new XmlNodeImpl(
+                "configuration",
+                null,
+                Map.of(),
+                List.of(
+                        new XmlNodeImpl("str", null, Map.of("default-value", 
"fallback-str"), List.of(), null),
+                        new XmlNodeImpl(
+                                "primitiveInt", "${unresolved}", 
Map.of("default-value", "99"), List.of(), null),
+                        new XmlNodeImpl("primitiveChar", null, 
Map.of("default-value", "Z"), List.of(), null),
+                        new XmlNodeImpl("primitiveBool", null, 
Map.of("default-value", "true"), List.of(), null)),
+                null);
+
+        fallbackConfigurator.configure(mojo, config);
+
+        assertEquals("fallback-str", mojo.str);
+        assertEquals(99, mojo.primitiveInt);
+        assertEquals('Z', mojo.primitiveChar);
+        assertTrue(mojo.primitiveBool);
+    }
+
+    public static class CollectionsMojo {
+        String[] array;
+        List<String> list;
+        Set<String> set;
+        Map<String, String> map;
+        Properties properties;
+    }
+
+    @Test
+    void testCollectionsAndMaps() throws Exception {

Review Comment:
   📋 **Missing unit tests for two new code paths in `testCollectionsAndMaps`.**
   
   The existing test only exercises the child-element XML path for arrays and 
collections. The two new code paths added by this commit have no direct 
coverage:
   
   1. **Comma-separated splitting** (`children.isEmpty()` branch): e.g. `new 
XmlNodeImpl("array", "a,b,c")` should produce `["a", "b", "c"]`.
   2. **Plexus-style `<property><name>k</name><value>v</value></property>`** 
map format: the new Plexus-detection branch in the Properties handling code has 
no test at all.
   
   Please add targeted test cases for both paths.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to