From f5f978e0e6f1545e754177d58dc5a46469dd91c1 Mon Sep 17 00:00:00 2001 From: Sylwester Lachiewicz Date: Fri, 9 Oct 2026 23:24:50 +0200 Subject: [PATCH] Treat a model built from a POM file as the project, whatever its validation level The repository-model sandbox (restricted interpolation, profile activation without user properties or file activation) keyed only on validationLevel < MAVEN_2_0. Embedders build their own project at MINIMAL, so -D profile activation broke for them (#13347). getPomFile() is the documented project-mode signal; repository models never set it. --- .../model/building/DefaultModelBuilder.java | 11 ++- .../AbstractStringBasedModelInterpolator.java | 16 ++-- ...solvedDependencyProfileActivationTest.java | 79 +++++++++++++++++++ 3 files changed, 95 insertions(+), 11 deletions(-) diff --git a/maven-model-builder/src/main/java/org/apache/maven/model/building/DefaultModelBuilder.java b/maven-model-builder/src/main/java/org/apache/maven/model/building/DefaultModelBuilder.java index e9041cd60126..4f0bd97dcf78 100644 --- a/maven-model-builder/src/main/java/org/apache/maven/model/building/DefaultModelBuilder.java +++ b/maven-model-builder/src/main/java/org/apache/maven/model/building/DefaultModelBuilder.java @@ -506,13 +506,16 @@ void performFor(String value, String locationKey, Consumer mutator) { /** * Determines whether the given request builds a model to resolve a dependency, i.e. a POM * read from a remote repository (a dependency POM, one of its parents, or an imported BOM) - * rather than a POM belonging to the project being built. Such requests use + * rather than a POM belonging to the project being built. Such requests have no POM file + * (see {@link ModelBuildingRequest#setPomFile(File)}) and use * {@link ModelBuildingRequest#VALIDATION_LEVEL_MINIMAL}, see for instance - * {@code DefaultArtifactDescriptorReader#loadPom}; a project build uses - * {@link ModelBuildingRequest#VALIDATION_LEVEL_MAVEN_2_0} or higher. + * {@code DefaultArtifactDescriptorReader#loadPom}. A request with a POM file is in project + * mode whatever its validation level, so an embedder that builds its own project at + * {@code VALIDATION_LEVEL_MINIMAL} keeps full profile activation. */ private static boolean isExternalModelBuildingRequest(ModelBuildingRequest request) { - return request.getValidationLevel() < ModelBuildingRequest.VALIDATION_LEVEL_MAVEN_2_0; + return request.getPomFile() == null + && request.getValidationLevel() < ModelBuildingRequest.VALIDATION_LEVEL_MAVEN_2_0; } /** diff --git a/maven-model-builder/src/main/java/org/apache/maven/model/interpolation/AbstractStringBasedModelInterpolator.java b/maven-model-builder/src/main/java/org/apache/maven/model/interpolation/AbstractStringBasedModelInterpolator.java index 41399878eb8f..5d32efe7a6a4 100644 --- a/maven-model-builder/src/main/java/org/apache/maven/model/interpolation/AbstractStringBasedModelInterpolator.java +++ b/maven-model-builder/src/main/java/org/apache/maven/model/interpolation/AbstractStringBasedModelInterpolator.java @@ -196,12 +196,13 @@ public Object getValue(String expression) { valueSources.add(modelValueSource1); - // Models built at VALIDATION_LEVEL_MINIMAL are the models Maven builds while resolving - // dependency, parent and BOM-import POMs from a repository, not the operator's own - // project. Such models interpolate only against their own properties and a small set - // of environment-independent expressions; everything else in the user/system property - // space stays uninterpolated. Operator project builds use a higher validation level and - // keep the full set of value sources, unchanged from previous behavior. + // Models built without a POM file at VALIDATION_LEVEL_MINIMAL are the models Maven builds + // while resolving dependency, parent and BOM-import POMs from a repository, not the + // operator's own project. Such models interpolate only against their own properties and a + // small set of environment-independent expressions; everything else in the user/system + // property space stays uninterpolated. Operator project builds have a POM file or use a + // higher validation level and keep the full set of value sources, unchanged from previous + // behavior. boolean restricted = restrictExternalModelInterpolation(config); ValueSource userPropertiesValueSource = new MapBasedValueSource(config.getUserProperties()); @@ -234,7 +235,8 @@ public Object getValue(String expression) { } private static boolean restrictExternalModelInterpolation(ModelBuildingRequest config) { - return config.getValidationLevel() < ModelBuildingRequest.VALIDATION_LEVEL_MAVEN_2_0 + return config.getPomFile() == null + && config.getValidationLevel() < ModelBuildingRequest.VALIDATION_LEVEL_MAVEN_2_0 && !Boolean.parseBoolean(config.getSystemProperties().getProperty(FULL_EXTERNAL_INTERPOLATION_PROPERTY)) && !Boolean.parseBoolean(config.getUserProperties().getProperty(FULL_EXTERNAL_INTERPOLATION_PROPERTY)); } diff --git a/maven-model-builder/src/test/java/org/apache/maven/model/building/ResolvedDependencyProfileActivationTest.java b/maven-model-builder/src/test/java/org/apache/maven/model/building/ResolvedDependencyProfileActivationTest.java index 3ecc74e4433b..04b0ae2c1e8a 100644 --- a/maven-model-builder/src/test/java/org/apache/maven/model/building/ResolvedDependencyProfileActivationTest.java +++ b/maven-model-builder/src/test/java/org/apache/maven/model/building/ResolvedDependencyProfileActivationTest.java @@ -18,10 +18,15 @@ */ package org.apache.maven.model.building; +import java.io.File; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; import java.util.Properties; import org.apache.maven.model.Model; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNull; @@ -166,6 +171,44 @@ private Model build(int validationLevel, Properties systemProperties, Properties return builder.build(request).getEffectiveModel(); } + private Model buildProjectAtMinimal(File pomFile, Properties systemProperties, Properties userProperties) + throws Exception { + ModelBuilder builder = new DefaultModelBuilderFactory().newInstance(); + + DefaultModelBuildingRequest request = new DefaultModelBuildingRequest(); + request.setPomFile(pomFile); + request.setValidationLevel(ModelBuildingRequest.VALIDATION_LEVEL_MINIMAL); + request.setSystemProperties(systemProperties); + request.setUserProperties(userProperties); + request.setModelResolver(new DefaultModelBuilderTest.BaseModelResolver()); + + return builder.build(request).getEffectiveModel(); + } + + private Model buildFromRepositoryAtMinimal(File pomFile, Properties systemProperties, Properties userProperties) + throws Exception { + ModelBuilder builder = new DefaultModelBuilderFactory().newInstance(); + + DefaultModelBuildingRequest request = new DefaultModelBuildingRequest(); + request.setModelSource(new FileModelSource(pomFile)); + request.setValidationLevel(ModelBuildingRequest.VALIDATION_LEVEL_MINIMAL); + request.setSystemProperties(systemProperties); + request.setUserProperties(userProperties); + request.setModelResolver(new DefaultModelBuilderTest.BaseModelResolver()); + + return builder.build(request).getEffectiveModel(); + } + + private File writePom(Path dir) throws Exception { + String pom = POM.replace( + "hello", + "hello\n" + + " ${user.gating.prop}"); + Path file = dir.resolve("pom.xml"); + Files.write(file, pom.getBytes(StandardCharsets.UTF_8)); + return file.toFile(); + } + private Properties systemPropertiesWithTestValues() { Properties sp = new Properties(); sp.putAll(System.getProperties()); @@ -213,6 +256,42 @@ void testProjectBuildEvaluatesAllActivators() throws Exception { "profile repository must be present in project build"); } + @Test + void testProjectModeAtMinimalValidationEvaluatesUserPropertyAndFileActivators(@TempDir Path dir) throws Exception { + // An embedder (an IDE) builds the operator's own project from its POM file at + // VALIDATION_LEVEL_MINIMAL for leniency. A POM file means project mode, so the + // repository-model sandbox must not apply. + Model model = + buildProjectAtMinimal(writePom(dir), systemPropertiesWithTestValues(), userPropertiesWithTestValues()); + + assertEquals( + "activated", + model.getProperties().get("profile.user.property"), + "user-property profile must fire for a project built from its POM file"); + assertEquals( + "activated", + model.getProperties().get("profile.file"), + "file profile must fire for a project built from its POM file"); + assertEquals( + "true", + model.getProperties().get("interpolated.user.property"), + "user properties must be interpolated for a project built from its POM file"); + } + + @Test + void testSamePomFromRepositoryAtMinimalValidationStaysSandboxed(@TempDir Path dir) throws Exception { + // The same POM loaded as a repository model (model source, no POM file) keeps the sandbox. + Model model = buildFromRepositoryAtMinimal( + writePom(dir), systemPropertiesWithTestValues(), userPropertiesWithTestValues()); + + assertNull(model.getProperties().get("profile.user.property"), "user-property profile must NOT fire"); + assertNull(model.getProperties().get("profile.file"), "file profile must NOT fire"); + assertEquals( + "${user.gating.prop}", + model.getProperties().get("interpolated.user.property"), + "user properties must stay literal in a repository model"); + } + @Test void testExternalModelFileActivationIsSuppressed() throws Exception { // File activation must be suppressed in external model builds regardless of whether the