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);
+ }
+}