Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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(),
Expand Down Expand Up @@ -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(),
Expand Down Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -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"));
}
}
Original file line number Diff line number Diff line change
@@ -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>
Loading
Loading