You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@maven.apache.org by gn...@apache.org on 2026/09/02 13:46:45 UTC

(maven) branch maven-4.0.x updated: [MNG-8765] Pre-interpolate plugin configuration before type conversion

This is an automated email from the ASF dual-hosted git repository.

gnodet pushed a commit to branch maven-4.0.x
in repository https://gitbox.apache.org/repos/asf/maven.git


The following commit(s) were added to refs/heads/maven-4.0.x by this push:
     new 7465605a0e [MNG-8765] Pre-interpolate plugin configuration before type conversion
7465605a0e is described below

commit 7465605a0ec4545e5763acb2541730f0047a085f
Author: Guillaume Nodet <gn...@gmail.com>
AuthorDate: Wed Sep 2 15:46:08 2026 +0200

    [MNG-8765] Pre-interpolate plugin configuration before type conversion
    
    * [MNG-8765] Pre-interpolate plugin configuration before type conversion
    
    Fix the property interpolation ordering issue for URI-typed plugin
    parameters. The ComponentConfigurator converts raw strings to
    java.net.URI BEFORE resolving ${...} properties, causing
    URISyntaxException because curly braces are illegal URI characters.
    
    The fix pre-interpolates the configuration tree by rebuilding the
    immutable XmlNode hierarchy bottom-up with resolved values before
    passing it to the ComponentConfigurator. This is necessary because
    XmlNode is @Immutable — mutating transient PlexusConfiguration
    wrappers does not propagate changes back to the underlying tree.
    
    Only expressions referencing properties that were NOT available during
    model interpolation are resolved. Properties from the POM's
    <properties>, user properties (-D), and system properties are skipped
    because model interpolation already had a chance to resolve them — any
    surviving ${...} for those properties was intentionally escaped
    (MNG-3558).
    
    Key changes:
    - Add interpolateXmlNode() that rebuilds the XmlNode tree with
      resolved expression values, preserving immutability semantics
    - Only interpolate runtime-set properties (not model-time ones),
      respecting property escaping (MNG-3558)
    - Apply pre-interpolation to both loadV3Mojo and loadV4Mojo paths
    - Add XmlPlexusConfiguration.toXmlNode() accessor
    - Add SetPropertyMojo to IT plugin for runtime property testing
    - Add integration test covering the actual MNG-8765 scenario:
      properties set dynamically at runtime via
      project.getProperties().setProperty() (not available during
      model interpolation)
    
    Co-authored-by: Claude Opus 4.6 <no...@anthropic.com>
    
    * [MNG-8765] Fix IT compilation for maven-4.0.x branch
    
    Adapt MavenITmng8765UriPropertyInterpolationTest to the 4.0.x
    AbstractMavenIntegrationTestCase API: use File instead of Path,
    String constructor instead of no-arg, and leading slash in resource
    paths.
    
    Co-Authored-By: Claude Opus 4.6 <no...@anthropic.com>
    
    ---------
    
    Co-authored-by: Claude Opus 4.6 <no...@anthropic.com>
---
 .../plugin/internal/DefaultMavenPluginManager.java | 178 +++++++++++++++++++++
 .../maven/internal/xml/XmlPlexusConfiguration.java |   9 ++
 ...MavenITmng8765UriPropertyInterpolationTest.java | 131 +++++++++++++++
 .../child/pom.xml                                  |  36 +++++
 .../cli-property/pom.xml                           |  58 +++++++
 .../mng-8765-uri-property-interpolation/pom.xml    |  70 ++++++++
 .../runtime-property/pom.xml                       |  84 ++++++++++
 .../maven-it-plugin-configuration/pom.xml          |   5 +
 .../maven/plugin/coreit/SetPropertyMojo.java       |  62 +++++++
 9 files changed, 633 insertions(+)

diff --git a/impl/maven-core/src/main/java/org/apache/maven/plugin/internal/DefaultMavenPluginManager.java b/impl/maven-core/src/main/java/org/apache/maven/plugin/internal/DefaultMavenPluginManager.java
index 915d3d9968..7e0197651b 100644
--- a/impl/maven-core/src/main/java/org/apache/maven/plugin/internal/DefaultMavenPluginManager.java
+++ b/impl/maven-core/src/main/java/org/apache/maven/plugin/internal/DefaultMavenPluginManager.java
@@ -34,11 +34,16 @@
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.HashMap;
+import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
+import java.util.Properties;
+import java.util.Set;
 import java.util.function.Supplier;
 import java.util.jar.JarFile;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
 import java.util.stream.Collectors;
 import java.util.zip.ZipEntry;
 
@@ -596,6 +601,10 @@ private <T> T loadV4Mojo(
             validator.validate(session, mojoDescriptor, mojo.getClass(), pomConfiguration, expressionEvaluator);
         }
 
+        // MNG-8765: Pre-interpolate configuration values before the ComponentConfigurator
+        // processes them. See interpolateConfiguration() javadoc for details.
+        pomConfiguration = interpolateConfiguration(pomConfiguration, expressionEvaluator, session);
+
         populateMojoExecutionFields(
                 mojo,
                 mojoExecution.getExecutionId(),
@@ -773,6 +782,14 @@ private <T> T loadV3Mojo(
             validator.validate(session, mojoDescriptor, mojo.getClass(), pomConfiguration, expressionEvaluator);
         }
 
+        // MNG-8765: Pre-interpolate configuration values using the expression evaluator before
+        // the ComponentConfigurator processes them. This ensures that ${...} property references
+        // are fully resolved before type converters (like UriConverter) attempt to parse the values.
+        // Without this, properties that are not available during model interpolation (e.g., set
+        // dynamically at runtime by scripts) would reach type converters unresolved, causing
+        // failures like URISyntaxException for URI-typed parameters containing ${...}.
+        pomConfiguration = interpolateConfiguration(pomConfiguration, expressionEvaluator, session);
+
         populateMojoExecutionFields(
                 mojo,
                 mojoExecution.getExecutionId(),
@@ -873,6 +890,167 @@ private void populateMojoExecutionFields(
         }
     }
 
+    /** Pattern to extract property names from {@code ${propName}} expressions. */
+    private static final Pattern EXPRESSION_PATTERN = Pattern.compile("\\$\\{([^}]+)}");
+
+    /**
+     * Pre-interpolates a plugin configuration tree by resolving {@code ${...}} property references
+     * that were set dynamically at runtime (not available during model interpolation). Returns a new
+     * {@link PlexusConfiguration} backed by a fully-interpolated {@link XmlNode} tree.
+     *
+     * <p>This is necessary because {@link XmlNode} is immutable — mutating transient
+     * {@link XmlPlexusConfiguration} wrappers does not propagate changes back to the underlying
+     * tree. Instead, this method rebuilds the {@code XmlNode} tree bottom-up with interpolated
+     * values, ensuring that all access paths ({@code getChild(int)}, {@code getChildren()}, etc.)
+     * see the resolved values.</p>
+     *
+     * <p>Only expressions referencing properties that were NOT available during model interpolation
+     * are resolved. Properties from the POM's {@code <properties>}, user properties ({@code -D}),
+     * and system properties are skipped because model interpolation already had a chance to resolve
+     * them — any surviving {@code ${...}} for those properties was intentionally escaped (MNG-3558).</p>
+     *
+     * @param configuration the plugin configuration to interpolate
+     * @param evaluator the expression evaluator to resolve {@code ${...}} references
+     * @param session the Maven session, used to determine model-time properties
+     * @return a new {@link PlexusConfiguration} wrapping an interpolated {@link XmlNode} tree
+     */
+    private PlexusConfiguration interpolateConfiguration(
+            PlexusConfiguration configuration, ExpressionEvaluator evaluator, MavenSession session) {
+        if (!(configuration instanceof XmlPlexusConfiguration xmlConfig)) {
+            return configuration;
+        }
+
+        Set<String> modelTimePropertyNames = collectModelTimePropertyNames(session);
+
+        XmlNode original = xmlConfig.toXmlNode();
+        XmlNode interpolated = interpolateXmlNode(original, evaluator, modelTimePropertyNames);
+        if (interpolated == original) {
+            return configuration;
+        }
+        return new XmlPlexusConfiguration(interpolated);
+    }
+
+    /**
+     * Collects the set of property names that were available during model interpolation.
+     * Properties in this set should NOT be re-interpolated, because any surviving {@code ${...}}
+     * reference to them was intentionally escaped.
+     */
+    private Set<String> collectModelTimePropertyNames(MavenSession session) {
+        Set<String> names = new HashSet<>();
+        // User properties (from -D on CLI)
+        Properties userProps = session.getUserProperties();
+        if (userProps != null) {
+            names.addAll(userProps.stringPropertyNames());
+        }
+        // System properties
+        Properties sysProps = session.getSystemProperties();
+        if (sysProps != null) {
+            names.addAll(sysProps.stringPropertyNames());
+        }
+        // POM properties from the original model (before runtime additions)
+        MavenProject project = session.getCurrentProject();
+        if (project != null && project.getOriginalModel() != null) {
+            Properties origProps = project.getOriginalModel().getProperties();
+            if (origProps != null) {
+                names.addAll(origProps.stringPropertyNames());
+            }
+        }
+        return names;
+    }
+
+    /**
+     * Checks whether a string value contains any {@code ${propName}} expression where
+     * {@code propName} is NOT in the model-time property set. Only such expressions need
+     * pre-interpolation (they were set at runtime and model interpolation couldn't see them).
+     */
+    private boolean containsRuntimeExpression(String value, Set<String> modelTimePropertyNames) {
+        Matcher matcher = EXPRESSION_PATTERN.matcher(value);
+        while (matcher.find()) {
+            String propName = matcher.group(1);
+            if (!modelTimePropertyNames.contains(propName)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Recursively interpolates an {@link XmlNode} tree, resolving only {@code ${...}} expressions
+     * that reference runtime properties (not available at model interpolation time). Returns the
+     * original node if no interpolation was needed, or a new node with resolved values otherwise.
+     */
+    private XmlNode interpolateXmlNode(
+            XmlNode node, ExpressionEvaluator evaluator, Set<String> modelTimePropertyNames) {
+        boolean changed = false;
+
+        // Interpolate the text value (only if it contains runtime expressions)
+        String value = node.value();
+        String newValue = value;
+        if (value != null && value.contains("${") && containsRuntimeExpression(value, modelTimePropertyNames)) {
+            try {
+                Object evaluated = evaluator.evaluate(value);
+                if (evaluated instanceof String evaluatedStr && !evaluatedStr.equals(value)) {
+                    newValue = evaluatedStr;
+                    changed = true;
+                }
+            } catch (ExpressionEvaluationException e) {
+                logger.debug("Failed to interpolate configuration value '{}': {}", value, e.getMessage());
+            }
+        }
+
+        // Interpolate the default-value attribute if present (only if it contains runtime expressions)
+        Map<String, String> attributes = node.attributes();
+        Map<String, String> newAttributes = attributes;
+        String defaultValue = attributes.get("default-value");
+        if (defaultValue != null
+                && defaultValue.contains("${")
+                && containsRuntimeExpression(defaultValue, modelTimePropertyNames)) {
+            try {
+                Object evaluated = evaluator.evaluate(defaultValue);
+                if (evaluated instanceof String evaluatedStr && !evaluatedStr.equals(defaultValue)) {
+                    newAttributes = new HashMap<>(attributes);
+                    newAttributes.put("default-value", evaluatedStr);
+                    changed = true;
+                }
+            } catch (ExpressionEvaluationException e) {
+                logger.debug(
+                        "Failed to interpolate configuration default-value '{}': {}", defaultValue, e.getMessage());
+            }
+        }
+
+        // Recurse into children
+        List<XmlNode> children = node.children();
+        List<XmlNode> newChildren = children;
+        for (int i = 0; i < children.size(); i++) {
+            XmlNode child = children.get(i);
+            XmlNode newChild = interpolateXmlNode(child, evaluator, modelTimePropertyNames);
+            if (newChild != child && newChildren == children) {
+                newChildren = new ArrayList<>(children.size());
+                for (int j = 0; j < i; j++) {
+                    newChildren.add(children.get(j));
+                }
+                changed = true;
+            }
+            if (newChildren != children) {
+                newChildren.add(newChild);
+            }
+        }
+
+        if (!changed) {
+            return node;
+        }
+
+        return XmlNode.newBuilder()
+                .name(node.name())
+                .value(newValue)
+                .attributes(newAttributes)
+                .children(newChildren)
+                .namespaceUri(node.namespaceUri())
+                .prefix(node.prefix())
+                .inputLocation(node.inputLocation())
+                .build();
+    }
+
     private void validateParameters(
             MojoDescriptor mojoDescriptor, PlexusConfiguration configuration, ExpressionEvaluator expressionEvaluator)
             throws ComponentConfigurationException, PluginParameterException {
diff --git a/impl/maven-xml/src/main/java/org/apache/maven/internal/xml/XmlPlexusConfiguration.java b/impl/maven-xml/src/main/java/org/apache/maven/internal/xml/XmlPlexusConfiguration.java
index afe4026347..50c742cb7f 100644
--- a/impl/maven-xml/src/main/java/org/apache/maven/internal/xml/XmlPlexusConfiguration.java
+++ b/impl/maven-xml/src/main/java/org/apache/maven/internal/xml/XmlPlexusConfiguration.java
@@ -45,6 +45,15 @@ public XmlPlexusConfiguration(XmlNode xmlNode) {
         this.xmlNode = xmlNode;
     }
 
+    /**
+     * Returns the underlying immutable {@link XmlNode} wrapped by this configuration.
+     *
+     * @return the wrapped XmlNode, never {@code null}
+     */
+    public XmlNode toXmlNode() {
+        return xmlNode;
+    }
+
     /**
      * Clears the internal cache when the XML structure is modified.
      */
diff --git a/its/core-it-suite/src/test/java/org/apache/maven/it/MavenITmng8765UriPropertyInterpolationTest.java b/its/core-it-suite/src/test/java/org/apache/maven/it/MavenITmng8765UriPropertyInterpolationTest.java
new file mode 100644
index 0000000000..5a9e356b8d
--- /dev/null
+++ b/its/core-it-suite/src/test/java/org/apache/maven/it/MavenITmng8765UriPropertyInterpolationTest.java
@@ -0,0 +1,131 @@
+/*
+ * 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.it;
+
+import java.io.File;
+import java.util.Properties;
+
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+/**
+ * This is a test set for
+ * <a href="https://issues.apache.org/jira/browse/MNG-8765">MNG-8765</a>.
+ *
+ * <p>Verifies that property interpolation runs before type conversion for
+ * URI-typed plugin parameters. When a URI parameter contains a property
+ * reference like {@code https://example.com/${my.version}/path}, the property
+ * must be resolved before the string is converted to {@link java.net.URI}.
+ * Otherwise, the curly braces cause a {@link java.net.URISyntaxException}.</p>
+ *
+ * <p>The regression was found in CloudStack (gnodet/maven4-testing#34733) where
+ * a URI parameter with {@code ${cs.version}} defined by a Groovy script at
+ * runtime was not interpolated before URI conversion.</p>
+ */
+public class MavenITmng8765UriPropertyInterpolationTest extends AbstractMavenIntegrationTestCase {
+
+    public MavenITmng8765UriPropertyInterpolationTest() {
+        super("[4.0.0-rc-1,)");
+    }
+
+    /**
+     * Verify that property interpolation resolves ${...} in URI-typed plugin
+     * parameters before type conversion, including when properties are
+     * inherited from a parent POM.
+     *
+     * @throws Exception in case of failure
+     */
+    @Test
+    public void testitPomProperty() throws Exception {
+        File testDir = extractResources("/mng-8765-uri-property-interpolation");
+
+        Verifier verifier = newVerifier(testDir.getAbsolutePath());
+        verifier.setAutoclean(false);
+        verifier.deleteDirectory("target");
+        verifier.deleteDirectory("child/target");
+        verifier.addCliArgument("validate");
+        verifier.execute();
+        verifier.verifyErrorFreeLog();
+
+        // Check parent module: property defined in same POM
+        Properties parentProps = verifier.loadProperties("target/plugin-config.properties");
+        assertEquals("https://example.com/1.2.3/path", parentProps.getProperty("uriParam"));
+        assertEquals("https://example.com/1.2.3/path", parentProps.getProperty("urlParam"));
+        assertEquals("1.2.3", parentProps.getProperty("stringParam"));
+
+        // Check child module: property inherited from parent POM
+        Properties childProps = verifier.loadProperties("child/target/plugin-config.properties");
+        assertEquals("https://example.com/1.2.3/path", childProps.getProperty("uriParam"));
+        assertEquals("https://example.com/1.2.3/path", childProps.getProperty("urlParam"));
+        assertEquals("1.2.3", childProps.getProperty("stringParam"));
+    }
+
+    /**
+     * Verify that a property passed via -D on the command line is resolved in
+     * URI-typed plugin parameters.
+     *
+     * @throws Exception in case of failure
+     */
+    @Test
+    public void testitCliProperty() throws Exception {
+        File testDir = extractResources("/mng-8765-uri-property-interpolation/cli-property");
+
+        Verifier verifier = newVerifier(testDir.getAbsolutePath());
+        verifier.setAutoclean(false);
+        verifier.deleteDirectory("target");
+        verifier.addCliArgument("-Dcli.version=2.0.0");
+        verifier.addCliArgument("validate");
+        verifier.execute();
+        verifier.verifyErrorFreeLog();
+
+        Properties props = verifier.loadProperties("target/plugin-config.properties");
+        assertEquals("https://example.com/2.0.0/path", props.getProperty("uriParam"));
+        assertEquals("https://example.com/2.0.0/path", props.getProperty("urlParam"));
+        assertEquals("2.0.0", props.getProperty("stringParam"));
+    }
+
+    /**
+     * Verify that a property set dynamically at runtime via
+     * {@code project.getProperties().setProperty(...)} is resolved in
+     * URI-typed plugin parameters. This is the actual MNG-8765 scenario:
+     * a plugin (like GMaven/gmavenplus) sets a property during the build
+     * lifecycle that was NOT available during model interpolation. Without
+     * the fix, the unresolved {@code ${...}} in the URI causes a
+     * {@link java.net.URISyntaxException}.
+     *
+     * @throws Exception in case of failure
+     */
+    @Test
+    public void testitRuntimeProperty() throws Exception {
+        File testDir = extractResources("/mng-8765-uri-property-interpolation/runtime-property");
+
+        Verifier verifier = newVerifier(testDir.getAbsolutePath());
+        verifier.setAutoclean(false);
+        verifier.deleteDirectory("target");
+        verifier.addCliArgument("process-sources");
+        verifier.execute();
+        verifier.verifyErrorFreeLog();
+
+        Properties props = verifier.loadProperties("target/plugin-config.properties");
+        assertEquals("https://example.com/3.0.0/path", props.getProperty("uriParam"));
+        assertEquals("https://example.com/3.0.0/path", props.getProperty("urlParam"));
+        assertEquals("3.0.0", props.getProperty("stringParam"));
+    }
+}
diff --git a/its/core-it-suite/src/test/resources/mng-8765-uri-property-interpolation/child/pom.xml b/its/core-it-suite/src/test/resources/mng-8765-uri-property-interpolation/child/pom.xml
new file mode 100644
index 0000000000..8fbf7bc6f9
--- /dev/null
+++ b/its/core-it-suite/src/test/resources/mng-8765-uri-property-interpolation/child/pom.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+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.
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0">
+  <modelVersion>4.0.0</modelVersion>
+
+  <parent>
+    <groupId>org.apache.maven.its.mng8765</groupId>
+    <artifactId>parent</artifactId>
+    <version>1.0-SNAPSHOT</version>
+  </parent>
+
+  <artifactId>child</artifactId>
+
+  <name>Maven Integration Test :: MNG-8765 :: Child</name>
+  <description>
+    Child module that inherits the test.version property from the parent
+    and uses it in a URI-typed plugin parameter.
+  </description>
+</project>
diff --git a/its/core-it-suite/src/test/resources/mng-8765-uri-property-interpolation/cli-property/pom.xml b/its/core-it-suite/src/test/resources/mng-8765-uri-property-interpolation/cli-property/pom.xml
new file mode 100644
index 0000000000..ed60c0a8e3
--- /dev/null
+++ b/its/core-it-suite/src/test/resources/mng-8765-uri-property-interpolation/cli-property/pom.xml
@@ -0,0 +1,58 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+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.
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0">
+  <modelVersion>4.0.0</modelVersion>
+
+  <groupId>org.apache.maven.its.mng8765</groupId>
+  <artifactId>cli-property-test</artifactId>
+  <version>1.0-SNAPSHOT</version>
+
+  <name>Maven Integration Test :: MNG-8765 :: CLI Property</name>
+  <description>
+    Verify that a property passed via -D on the CLI is resolved in a URI-typed
+    plugin parameter. The property is NOT defined in POM properties, only via
+    -Dcli.version=2.0.0 on the command line, simulating how some plugins
+    (like Groovy scripts) dynamically set properties at runtime.
+  </description>
+
+  <build>
+    <plugins>
+      <plugin>
+        <groupId>org.apache.maven.its.plugins</groupId>
+        <artifactId>maven-it-plugin-configuration</artifactId>
+        <version>2.1-SNAPSHOT</version>
+        <configuration>
+          <propertiesFile>target/plugin-config.properties</propertiesFile>
+          <uriParam>https://example.com/${cli.version}/path</uriParam>
+          <urlParam>https://example.com/${cli.version}/path</urlParam>
+          <stringParam>${cli.version}</stringParam>
+        </configuration>
+        <executions>
+          <execution>
+            <goals>
+              <goal>config</goal>
+            </goals>
+            <phase>validate</phase>
+          </execution>
+        </executions>
+      </plugin>
+    </plugins>
+  </build>
+</project>
diff --git a/its/core-it-suite/src/test/resources/mng-8765-uri-property-interpolation/pom.xml b/its/core-it-suite/src/test/resources/mng-8765-uri-property-interpolation/pom.xml
new file mode 100644
index 0000000000..a2e720d57f
--- /dev/null
+++ b/its/core-it-suite/src/test/resources/mng-8765-uri-property-interpolation/pom.xml
@@ -0,0 +1,70 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+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.
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0">
+  <modelVersion>4.0.0</modelVersion>
+
+  <groupId>org.apache.maven.its.mng8765</groupId>
+  <artifactId>parent</artifactId>
+  <version>1.0-SNAPSHOT</version>
+  <packaging>pom</packaging>
+
+  <name>Maven Integration Test :: MNG-8765</name>
+  <description>
+    Verify that property interpolation runs before type conversion for URI-typed
+    plugin parameters. When a URI parameter contains a property reference like
+    https://example.com/${my.version}/path, the ${my.version} must be resolved
+    before the string is converted to java.net.URI. Otherwise, the curly braces
+    cause a URISyntaxException.
+  </description>
+
+  <properties>
+    <test.version>1.2.3</test.version>
+  </properties>
+
+  <modules>
+    <module>child</module>
+  </modules>
+
+  <build>
+    <plugins>
+      <plugin>
+        <groupId>org.apache.maven.its.plugins</groupId>
+        <artifactId>maven-it-plugin-configuration</artifactId>
+        <version>2.1-SNAPSHOT</version>
+        <configuration>
+          <propertiesFile>target/plugin-config.properties</propertiesFile>
+          <!-- Property from POM <properties> -->
+          <uriParam>https://example.com/${test.version}/path</uriParam>
+          <urlParam>https://example.com/${test.version}/path</urlParam>
+          <!-- Property from command line -D (not in POM properties) -->
+          <stringParam>${test.version}</stringParam>
+        </configuration>
+        <executions>
+          <execution>
+            <goals>
+              <goal>config</goal>
+            </goals>
+            <phase>validate</phase>
+          </execution>
+        </executions>
+      </plugin>
+    </plugins>
+  </build>
+</project>
diff --git a/its/core-it-suite/src/test/resources/mng-8765-uri-property-interpolation/runtime-property/pom.xml b/its/core-it-suite/src/test/resources/mng-8765-uri-property-interpolation/runtime-property/pom.xml
new file mode 100644
index 0000000000..a0867288db
--- /dev/null
+++ b/its/core-it-suite/src/test/resources/mng-8765-uri-property-interpolation/runtime-property/pom.xml
@@ -0,0 +1,84 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+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.
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0">
+  <modelVersion>4.0.0</modelVersion>
+
+  <groupId>org.apache.maven.its.mng8765</groupId>
+  <artifactId>runtime-property-test</artifactId>
+  <version>1.0-SNAPSHOT</version>
+
+  <name>Maven Integration Test :: MNG-8765 :: Runtime Property</name>
+  <description>
+    Verify that a property set dynamically at runtime via
+    project.getProperties().setProperty() is resolved in a URI-typed
+    plugin parameter. This is the actual MNG-8765 scenario: a plugin
+    (like GMaven/gmavenplus) sets a property during the build lifecycle
+    that was NOT available during model interpolation. Without the fix,
+    the unresolved ${...} in the URI causes a URISyntaxException.
+  </description>
+
+  <build>
+    <plugins>
+      <!--
+        First execution: set a project property at runtime (initialize phase).
+        This simulates Groovy scripts calling project.properties.setProperty().
+        The property is NOT in <properties> and NOT passed via -D, so model
+        interpolation cannot resolve it.
+      -->
+      <plugin>
+        <groupId>org.apache.maven.its.plugins</groupId>
+        <artifactId>maven-it-plugin-configuration</artifactId>
+        <version>2.1-SNAPSHOT</version>
+        <executions>
+          <execution>
+            <id>set-runtime-property</id>
+            <goals>
+              <goal>set-property</goal>
+            </goals>
+            <phase>initialize</phase>
+            <configuration>
+              <propertyName>runtime.version</propertyName>
+              <propertyValue>3.0.0</propertyValue>
+            </configuration>
+          </execution>
+          <!--
+            Second execution: use the runtime property in URI-typed parameters.
+            This runs AFTER the property has been set. The expression evaluator
+            should resolve ${runtime.version} even though model interpolation
+            could not.
+          -->
+          <execution>
+            <id>dump-config</id>
+            <goals>
+              <goal>config</goal>
+            </goals>
+            <phase>process-sources</phase>
+            <configuration>
+              <propertiesFile>target/plugin-config.properties</propertiesFile>
+              <uriParam>https://example.com/${runtime.version}/path</uriParam>
+              <urlParam>https://example.com/${runtime.version}/path</urlParam>
+              <stringParam>${runtime.version}</stringParam>
+            </configuration>
+          </execution>
+        </executions>
+      </plugin>
+    </plugins>
+  </build>
+</project>
diff --git a/its/core-it-support/core-it-plugins/maven-it-plugin-configuration/pom.xml b/its/core-it-support/core-it-plugins/maven-it-plugin-configuration/pom.xml
index 88b2fab8ca..3fac56aed6 100644
--- a/its/core-it-support/core-it-plugins/maven-it-plugin-configuration/pom.xml
+++ b/its/core-it-support/core-it-plugins/maven-it-plugin-configuration/pom.xml
@@ -40,6 +40,11 @@ under the License.
       <artifactId>maven-plugin-api</artifactId>
       <scope>provided</scope>
     </dependency>
+    <dependency>
+      <groupId>org.apache.maven</groupId>
+      <artifactId>maven-compat</artifactId>
+      <scope>provided</scope>
+    </dependency>
     <dependency>
       <groupId>org.codehaus.plexus</groupId>
       <artifactId>plexus-container-default</artifactId>
diff --git a/its/core-it-support/core-it-plugins/maven-it-plugin-configuration/src/main/java/org/apache/maven/plugin/coreit/SetPropertyMojo.java b/its/core-it-support/core-it-plugins/maven-it-plugin-configuration/src/main/java/org/apache/maven/plugin/coreit/SetPropertyMojo.java
new file mode 100644
index 0000000000..464e9bd5cf
--- /dev/null
+++ b/its/core-it-support/core-it-plugins/maven-it-plugin-configuration/src/main/java/org/apache/maven/plugin/coreit/SetPropertyMojo.java
@@ -0,0 +1,62 @@
+/*
+ * 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.coreit;
+
+import org.apache.maven.plugin.AbstractMojo;
+import org.apache.maven.plugin.MojoExecutionException;
+import org.apache.maven.plugins.annotations.LifecyclePhase;
+import org.apache.maven.plugins.annotations.Mojo;
+import org.apache.maven.plugins.annotations.Parameter;
+import org.apache.maven.project.MavenProject;
+
+/**
+ * Sets a property on the current project at runtime. This simulates the behavior
+ * of plugins like GMaven/gmavenplus that dynamically set project properties via
+ * {@code project.getProperties().setProperty(...)} during the build lifecycle.
+ *
+ * <p>Properties set this way are NOT available during model interpolation (which
+ * runs before the build lifecycle), but ARE available to the expression evaluator
+ * at mojo execution time.</p>
+ */
+@Mojo(name = "set-property", defaultPhase = LifecyclePhase.INITIALIZE)
+public class SetPropertyMojo extends AbstractMojo {
+
+    /**
+     * The current Maven project.
+     */
+    @Parameter(defaultValue = "${project}", readonly = true)
+    private MavenProject project;
+
+    /**
+     * The property key to set.
+     */
+    @Parameter(required = true)
+    private String propertyName;
+
+    /**
+     * The property value to set.
+     */
+    @Parameter(required = true)
+    private String propertyValue;
+
+    public void execute() throws MojoExecutionException {
+        getLog().info("[MAVEN-CORE-IT-LOG] Setting project property: " + propertyName + " = " + propertyValue);
+        project.getProperties().setProperty(propertyName, propertyValue);
+    }
+}