diff --git a/impl/maven-core/pom.xml b/impl/maven-core/pom.xml
index 534838b51d7e..f01da88b941d 100644
--- a/impl/maven-core/pom.xml
+++ b/impl/maven-core/pom.xml
@@ -31,6 +31,11 @@ under the License.
Maven 4 Core
Maven Core classes.
+
+
+ FileLength
+
+
diff --git a/impl/maven-core/src/test/java/org/apache/maven/project/PomConstructionTest.java b/impl/maven-core/src/test/java/org/apache/maven/project/PomConstructionTest.java
index a6513e409183..d53247b53b9a 100644
--- a/impl/maven-core/src/test/java/org/apache/maven/project/PomConstructionTest.java
+++ b/impl/maven-core/src/test/java/org/apache/maven/project/PomConstructionTest.java
@@ -1534,6 +1534,82 @@ void testProfilePluginMngDependencies() throws Exception {
assertEquals("a", pom.getValue("build/plugins[1]/dependencies[1]/artifactId"));
}
+ /* MNG-3309 */
+ @Test
+ void testCascadingProfileActivation() throws Exception {
+ Properties props = new Properties();
+ props.put("trigger", "start");
+
+ PomTestWrapper pom = buildPom("cascading-profile-activation", props, null);
+
+ // Verify that cascading profile activation works
+ // profile1 should be activated by trigger=start
+ // profile2 should be activated by profile1's cascade.level1=activate property
+ // profile3 should be activated by profile2's cascade.level2=activate property
+
+ List activeProfiles =
+ pom.getMavenProject().getActiveProfiles();
+
+ // Should have 3 active profiles (profile1, profile2, profile3)
+ assertEquals(3, activeProfiles.size());
+
+ // Verify specific profiles are active
+ assertTrue(activeProfiles.stream().anyMatch(p -> "profile1".equals(p.getId())));
+ assertTrue(activeProfiles.stream().anyMatch(p -> "profile2".equals(p.getId())));
+ assertTrue(activeProfiles.stream().anyMatch(p -> "profile3".equals(p.getId())));
+
+ // Verify profile4 is NOT active (no trigger)
+ assertTrue(activeProfiles.stream().noneMatch(p -> "profile4".equals(p.getId())));
+
+ // Verify properties are set correctly (last profile wins)
+ assertEquals("profile3", pom.getValue("properties/test.property"));
+ assertEquals("true", pom.getValue("properties/profile1.activated"));
+ assertEquals("true", pom.getValue("properties/profile2.activated"));
+ assertEquals("true", pom.getValue("properties/profile3.activated"));
+ }
+
+ /* MNG-3309 - Test circular dependency handling */
+ @Test
+ void testCascadingProfileActivationCircular() throws Exception {
+ Properties props = new Properties();
+ props.put("circular", "test");
+
+ PomTestWrapper pom = buildPom("cascading-profile-activation", props, null);
+
+ // Verify that circular dependencies are handled gracefully
+ // profile5 sets trigger=start which would activate profile1
+ // But this should not cause infinite loops
+
+ List activeProfiles =
+ pom.getMavenProject().getActiveProfiles();
+
+ // Should have profile5 and profile1 active (and cascaded profiles)
+ assertTrue(activeProfiles.stream().anyMatch(p -> "profile5".equals(p.getId())));
+ assertTrue(activeProfiles.stream().anyMatch(p -> "profile1".equals(p.getId())));
+
+ // Verify no infinite loop occurred (test should complete)
+ assertTrue(activeProfiles.size() >= 2);
+ }
+
+ /* MNG-3309 - Test no cascading baseline */
+ @Test
+ void testNoCascadingProfileActivation() throws Exception {
+ // Test with no trigger properties - no profiles should be activated
+ PomTestWrapper pom = buildPom("cascading-profile-activation");
+
+ List activeProfiles =
+ pom.getMavenProject().getActiveProfiles();
+
+ // No profiles should be active
+ assertEquals(0, activeProfiles.size());
+
+ // Properties should remain at default values
+ assertEquals("default", pom.getValue("properties/test.property"));
+ assertEquals("false", pom.getValue("properties/profile1.activated"));
+ assertEquals("false", pom.getValue("properties/profile2.activated"));
+ assertEquals("false", pom.getValue("properties/profile3.activated"));
+ }
+
/** MNG-4116 */
@Test
void testPercentEncodedUrlsMustNotBeDecoded() throws Exception {
diff --git a/impl/maven-core/src/test/resources-project-builder/cascading-profile-activation/pom.xml b/impl/maven-core/src/test/resources-project-builder/cascading-profile-activation/pom.xml
new file mode 100644
index 000000000000..a94ef85cb8df
--- /dev/null
+++ b/impl/maven-core/src/test/resources-project-builder/cascading-profile-activation/pom.xml
@@ -0,0 +1,145 @@
+
+
+
+
+ 4.0.0
+
+ org.apache.maven.its.mng3309
+ cascading-profile-activation
+ 1.0-SNAPSHOT
+ jar
+
+ Maven Integration Test :: MNG-3309
+
+ Test cascading profile activation where one profile's properties trigger the activation of other profiles.
+
+
+
+
+ default
+ false
+ false
+ false
+
+
+
+
+
+ profile1
+
+
+ trigger
+ start
+
+
+
+ true
+
+ activate
+ profile1
+
+
+
+
+
+ profile2
+
+
+ cascade.level1
+ activate
+
+
+
+ true
+
+ activate
+ profile2
+
+
+
+
+
+ profile3
+
+
+ cascade.level2
+ activate
+
+
+
+ true
+ profile3
+
+
+
+
+
+ profile4
+
+
+ never.set
+ never
+
+
+
+ true
+ profile4
+
+
+
+
+
+ profile5
+
+
+ circular
+ test
+
+
+
+ true
+
+ start
+ profile5
+
+
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-help-plugin
+ 3.4.0
+
+
+ show-profiles
+ validate
+
+ active-profiles
+
+
+
+
+
+
+
diff --git a/impl/maven-impl/src/main/java/org/apache/maven/api/services/model/ProfileActivationContext.java b/impl/maven-impl/src/main/java/org/apache/maven/api/services/model/ProfileActivationContext.java
index b7951ea3e122..43fec5176afc 100644
--- a/impl/maven-impl/src/main/java/org/apache/maven/api/services/model/ProfileActivationContext.java
+++ b/impl/maven-impl/src/main/java/org/apache/maven/api/services/model/ProfileActivationContext.java
@@ -18,9 +18,12 @@
*/
package org.apache.maven.api.services.model;
+import java.util.Collection;
+
import org.apache.maven.api.annotations.Nonnull;
import org.apache.maven.api.annotations.Nullable;
import org.apache.maven.api.model.Model;
+import org.apache.maven.api.model.Profile;
import org.apache.maven.api.services.InterpolatorException;
import org.apache.maven.api.services.ModelBuilderException;
@@ -130,4 +133,12 @@ public interface ProfileActivationContext {
* @throws InterpolatorException if an error occurs during interpolation
*/
boolean exists(@Nullable String path, boolean glob);
+
+ /**
+ * Inject properties from newly activated profiles in order to trigger the cascading mechanism.
+ * This method allows profiles to contribute properties that can trigger the activation of other profiles.
+ *
+ * @param activatedProfiles The collection of profiles that have been activated that may trigger the cascading effect.
+ */
+ void addProfileProperties(Collection activatedProfiles);
}
diff --git a/impl/maven-impl/src/main/java/org/apache/maven/impl/SettingsUtilsV4.java b/impl/maven-impl/src/main/java/org/apache/maven/impl/SettingsUtilsV4.java
index 0290464ebb77..fabcc214a221 100644
--- a/impl/maven-impl/src/main/java/org/apache/maven/impl/SettingsUtilsV4.java
+++ b/impl/maven-impl/src/main/java/org/apache/maven/impl/SettingsUtilsV4.java
@@ -268,7 +268,7 @@ public static org.apache.maven.api.model.Profile convertFromSettingsProfile(Prof
}
org.apache.maven.api.model.Profile value = profile.build();
- value.setSource("settings.xml");
+ value.setSource(org.apache.maven.api.model.Profile.SOURCE_SETTINGS);
return value;
}
diff --git a/impl/maven-impl/src/main/java/org/apache/maven/impl/model/DefaultProfileActivationContext.java b/impl/maven-impl/src/main/java/org/apache/maven/impl/model/DefaultProfileActivationContext.java
index 9d9ba94546c8..8cb36789364c 100644
--- a/impl/maven-impl/src/main/java/org/apache/maven/impl/model/DefaultProfileActivationContext.java
+++ b/impl/maven-impl/src/main/java/org/apache/maven/impl/model/DefaultProfileActivationContext.java
@@ -27,6 +27,7 @@
import java.nio.file.Paths;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.attribute.BasicFileAttributes;
+import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
@@ -35,6 +36,7 @@
import java.util.concurrent.atomic.AtomicBoolean;
import org.apache.maven.api.model.Model;
+import org.apache.maven.api.model.Profile;
import org.apache.maven.api.services.Interpolator;
import org.apache.maven.api.services.InterpolatorException;
import org.apache.maven.api.services.ModelBuilderException;
@@ -169,6 +171,7 @@ private boolean matchesExists(Map exists, DefaultProfileA
private Map systemProperties = Collections.emptyMap();
private Map userProperties = Collections.emptyMap();
private Model model;
+ private Map cascadingProperties = Collections.emptyMap();
final Record record;
public DefaultProfileActivationContext(
@@ -326,10 +329,21 @@ public String getModelPackaging() {
@Override
public String getModelProperty(String key) {
if (record != null) {
- return record.usedModelProperties.computeIfAbsent(
- key, k -> model.getProperties().get(k));
+ return record.usedModelProperties.computeIfAbsent(key, k -> {
+ // Check cascading properties first, then model properties
+ String value = cascadingProperties.get(k);
+ if (value == null && model.getProperties() != null) {
+ value = model.getProperties().get(k);
+ }
+ return value;
+ });
} else {
- return model.getProperties().get(key);
+ // Check cascading properties first, then model properties
+ String value = cascadingProperties.get(key);
+ if (value == null && model.getProperties() != null) {
+ value = model.getProperties().get(key);
+ }
+ return value;
}
}
@@ -461,4 +475,25 @@ public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {
private static Map unmodifiable(Map map) {
return map != null ? Collections.unmodifiableMap(map) : Collections.emptyMap();
}
+
+ // Cascading profile activation methods
+
+ @Override
+ public void addProfileProperties(Collection activatedProfiles) {
+ // Inject properties from activated profiles into cascading properties
+ // This enables cascading profile activation without modifying the underlying model
+ if (activatedProfiles != null && !activatedProfiles.isEmpty()) {
+ Map newCascadingProperties = new HashMap<>(cascadingProperties);
+
+ // Add properties from each activated profile
+ for (Profile profile : activatedProfiles) {
+ if (profile.getProperties() != null) {
+ newCascadingProperties.putAll(profile.getProperties());
+ }
+ }
+
+ // Update cascading properties for future profile activation checks
+ this.cascadingProperties = Collections.unmodifiableMap(newCascadingProperties);
+ }
+ }
}
diff --git a/impl/maven-impl/src/main/java/org/apache/maven/impl/model/DefaultProfileSelector.java b/impl/maven-impl/src/main/java/org/apache/maven/impl/model/DefaultProfileSelector.java
index 407d35a71c23..19ae7a6492d6 100644
--- a/impl/maven-impl/src/main/java/org/apache/maven/impl/model/DefaultProfileSelector.java
+++ b/impl/maven-impl/src/main/java/org/apache/maven/impl/model/DefaultProfileSelector.java
@@ -63,32 +63,49 @@ public DefaultProfileSelector addProfileActivator(ProfileActivator profileActiva
@Override
public List getActiveProfiles(
Collection profiles, ProfileActivationContext context, ModelProblemCollector problems) {
- List activeProfiles = new ArrayList<>(profiles.size());
+
+ List activeSettingsProfiles = new ArrayList<>();
+ List activePomProfiles = new ArrayList<>();
List activePomProfilesByDefault = new ArrayList<>();
- boolean activatedPomProfileNotByDefault = false;
- for (Profile profile : profiles) {
- if (!context.isProfileInactive(profile.getId())) {
- if (context.isProfileActive(profile.getId()) || isActive(profile, context, problems)) {
- activeProfiles.add(profile);
- if (Profile.SOURCE_POM.equals(profile.getSource())) {
- activatedPomProfileNotByDefault = true;
- }
- } else if (isActiveByDefault(profile)) {
- if (Profile.SOURCE_POM.equals(profile.getSource())) {
- activePomProfilesByDefault.add(profile);
- } else {
- activeProfiles.add(profile);
+ // Cascading mode: iterate until no more profiles are activated
+ List remainingProfiles = new ArrayList<>(profiles);
+ List activatedProfiles;
+ do {
+ activatedProfiles = new ArrayList<>();
+ for (Profile profile : List.copyOf(remainingProfiles)) {
+ if (!context.isProfileInactive(profile.getId())) {
+ boolean activated = context.isProfileActive(profile.getId());
+ boolean active = isActive(profile, context, problems);
+ boolean activeByDefault = isActiveByDefault(profile);
+ if (activated || active || activeByDefault) {
+ if (Profile.SOURCE_POM.equals(profile.getSource())) {
+ if (activated || active) {
+ activePomProfiles.add(profile);
+ } else {
+ activePomProfilesByDefault.add(profile);
+ }
+ } else {
+ activeSettingsProfiles.add(profile);
+ }
+ remainingProfiles.remove(profile);
+ activatedProfiles.add(profile);
}
}
}
- }
+ // Add profile properties for cascading activation
+ context.addProfileProperties(activatedProfiles);
+ } while (!activatedProfiles.isEmpty());
- if (!activatedPomProfileNotByDefault) {
- activeProfiles.addAll(activePomProfilesByDefault);
+ List allActivated = new ArrayList<>();
+ if (activePomProfiles.isEmpty()) {
+ allActivated.addAll(activePomProfilesByDefault);
+ } else {
+ allActivated.addAll(activePomProfiles);
}
+ allActivated.addAll(activeSettingsProfiles);
- return activeProfiles;
+ return allActivated;
}
private boolean isActive(Profile profile, ProfileActivationContext context, ModelProblemCollector problems) {
diff --git a/impl/maven-impl/src/main/java/org/apache/maven/impl/model/profile/PropertyProfileActivator.java b/impl/maven-impl/src/main/java/org/apache/maven/impl/model/profile/PropertyProfileActivator.java
index 8b2114ad3633..95c0e1a9aa14 100644
--- a/impl/maven-impl/src/main/java/org/apache/maven/impl/model/profile/PropertyProfileActivator.java
+++ b/impl/maven-impl/src/main/java/org/apache/maven/impl/model/profile/PropertyProfileActivator.java
@@ -73,6 +73,9 @@ public boolean isActive(Profile profile, ProfileActivationContext context, Model
if (sysValue == null && "packaging".equals(name)) {
sysValue = context.getModelPackaging();
}
+ if (sysValue == null) {
+ sysValue = context.getModelProperty(name);
+ }
if (sysValue == null) {
sysValue = context.getSystemProperty(name);
}
diff --git a/impl/maven-impl/src/test/java/org/apache/maven/impl/model/DefaultProfileSelectorTest.java b/impl/maven-impl/src/test/java/org/apache/maven/impl/model/DefaultProfileSelectorTest.java
new file mode 100644
index 000000000000..96a0a0c43bfd
--- /dev/null
+++ b/impl/maven-impl/src/test/java/org/apache/maven/impl/model/DefaultProfileSelectorTest.java
@@ -0,0 +1,428 @@
+/*
+ * 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.impl.model;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.maven.api.model.Activation;
+import org.apache.maven.api.model.ActivationProperty;
+import org.apache.maven.api.model.Model;
+import org.apache.maven.api.model.Profile;
+import org.apache.maven.api.services.model.ProfileActivationContext;
+import org.apache.maven.api.services.model.ProfileActivator;
+import org.apache.maven.impl.model.profile.PropertyProfileActivator;
+import org.apache.maven.impl.model.profile.SimpleProblemCollector;
+import org.apache.maven.impl.model.rootlocator.DefaultRootLocator;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * Tests {@link DefaultProfileSelector} with focus on cascading activation behavior.
+ */
+public class DefaultProfileSelectorTest {
+
+ private DefaultProfileSelector selector;
+ private SimpleProblemCollector problems;
+
+ @BeforeEach
+ void setUp() {
+ selector = new DefaultProfileSelector();
+ // Add a simple property-based activator for testing
+ selector.addProfileActivator(new PropertyProfileActivator());
+ problems = new SimpleProblemCollector();
+ }
+
+ @Test
+ void testNonCascadingActivation() {
+ // Create profiles with property-based activation
+ Profile profile1 = createProfile("profile1", "prop1", "value1", Profile.SOURCE_POM);
+ Profile profile2 = createProfile("profile2", "prop2", "value2", Profile.SOURCE_POM);
+
+ List profiles = Arrays.asList(profile1, profile2);
+
+ // Create context with prop1 set
+ DefaultProfileActivationContext context = new DefaultProfileActivationContext(
+ new DefaultPathTranslator(), new DefaultRootLocator(), new DefaultInterpolator());
+ context.setModel(Model.newInstance());
+ context.setSystemProperties(Map.of("prop1", "value1"));
+
+ // Test cascading mode (current implementation only supports cascading)
+ List activeProfiles = selector.getActiveProfiles(profiles, context, problems);
+
+ assertEquals(1, activeProfiles.size());
+ assertEquals("profile1", activeProfiles.get(0).getId());
+ assertTrue(problems.getErrors().isEmpty());
+ }
+
+ @Test
+ void testCascadingActivation() {
+ // Create profiles where one activates another through properties
+ Profile profile1 = createProfileWithProperties(
+ "profile1", "prop1", "value1", Map.of("prop2", "value2"), Profile.SOURCE_POM);
+ Profile profile2 = createProfile("profile2", "prop2", "value2", Profile.SOURCE_POM);
+
+ List profiles = Arrays.asList(profile1, profile2);
+
+ // Create context with prop1 set (should activate profile1, which sets prop2, which activates profile2)
+ DefaultProfileActivationContext context = new DefaultProfileActivationContext(
+ new DefaultPathTranslator(), new DefaultRootLocator(), new DefaultInterpolator());
+ context.setSystemProperties(Map.of("prop1", "value1"));
+ context.setModel(Model.newInstance()); // Set a model for property injection
+
+ // Test cascading mode
+ List activeProfiles = selector.getActiveProfiles(profiles, context, problems);
+
+ assertEquals(2, activeProfiles.size());
+ assertTrue(activeProfiles.stream().anyMatch(p -> "profile1".equals(p.getId())));
+ assertTrue(activeProfiles.stream().anyMatch(p -> "profile2".equals(p.getId())));
+ assertTrue(problems.getErrors().isEmpty());
+ }
+
+ @Test
+ void testCascadingVsNonCascadingDifference() {
+ // Create profiles where cascading would activate more profiles
+ Profile profile1 = createProfileWithProperties(
+ "profile1", "prop1", "value1", Map.of("prop2", "value2"), Profile.SOURCE_POM);
+ Profile profile2 = createProfile("profile2", "prop2", "value2", Profile.SOURCE_POM);
+
+ List profiles = Arrays.asList(profile1, profile2);
+
+ DefaultProfileActivationContext context = new DefaultProfileActivationContext(
+ new DefaultPathTranslator(), new DefaultRootLocator(), new DefaultInterpolator());
+ context.setSystemProperties(Map.of("prop1", "value1"));
+ context.setModel(Model.newInstance()); // Set a model for property injection
+
+ // Cascading should activate both profile1 and profile2
+ List cascading = selector.getActiveProfiles(profiles, context, problems);
+ assertEquals(2, cascading.size());
+ }
+
+ @Test
+ void testActiveByDefaultProfiles() {
+ Profile defaultProfile = createActiveByDefaultProfile("default-profile", Profile.SOURCE_POM);
+ Profile conditionalProfile = createProfile("conditional", "prop1", "value1", Profile.SOURCE_POM);
+
+ List profiles = Arrays.asList(defaultProfile, conditionalProfile);
+
+ DefaultProfileActivationContext context = new DefaultProfileActivationContext(
+ new DefaultPathTranslator(), new DefaultRootLocator(), new DefaultInterpolator());
+ context.setModel(Model.newInstance());
+
+ // Should activate default profile when no conditions are met
+ List activeProfiles = selector.getActiveProfiles(profiles, context, problems);
+ assertEquals(1, activeProfiles.size());
+ assertEquals("default-profile", activeProfiles.get(0).getId());
+
+ // Should not activate default profile when conditional profile is active
+ context.setSystemProperties(Map.of("prop1", "value1"));
+ activeProfiles = selector.getActiveProfiles(profiles, context, problems);
+ assertEquals(1, activeProfiles.size());
+ assertEquals("conditional", activeProfiles.get(0).getId());
+ }
+
+ @Test
+ void testMixedSourceProfiles() {
+ Profile pomProfile = createProfile("pom-profile", "prop1", "value1", Profile.SOURCE_POM);
+ Profile settingsProfile = createProfile("settings-profile", "prop2", "value2", Profile.SOURCE_SETTINGS);
+
+ List profiles = Arrays.asList(pomProfile, settingsProfile);
+
+ DefaultProfileActivationContext context = new DefaultProfileActivationContext(
+ new DefaultPathTranslator(), new DefaultRootLocator(), new DefaultInterpolator());
+ context.setModel(Model.newInstance());
+ context.setSystemProperties(Map.of("prop1", "value1", "prop2", "value2"));
+
+ List activeProfiles = selector.getActiveProfiles(profiles, context, problems);
+ assertEquals(2, activeProfiles.size());
+
+ // Settings profiles should come after POM profiles in the result
+ assertEquals("pom-profile", activeProfiles.get(0).getId());
+ assertEquals("settings-profile", activeProfiles.get(1).getId());
+ }
+
+ @Test
+ void testEmptyProfilesList() {
+ List profiles = Collections.emptyList();
+ DefaultProfileActivationContext context = new DefaultProfileActivationContext(
+ new DefaultPathTranslator(), new DefaultRootLocator(), new DefaultInterpolator());
+ context.setModel(Model.newInstance());
+
+ List activeProfiles = selector.getActiveProfiles(profiles, context, problems);
+ assertTrue(activeProfiles.isEmpty());
+ }
+
+ @Test
+ void testExplicitlyActivatedProfiles() {
+ Profile profile1 = createProfile("profile1", "nonexistent", "value", Profile.SOURCE_POM);
+ Profile profile2 = createProfile("profile2", "prop2", "value2", Profile.SOURCE_POM);
+
+ List profiles = Arrays.asList(profile1, profile2);
+
+ DefaultProfileActivationContext context = new DefaultProfileActivationContext(
+ new DefaultPathTranslator(),
+ new DefaultRootLocator(),
+ new DefaultInterpolator(),
+ List.of("profile1"),
+ List.of(),
+ Map.of("prop2", "value2"),
+ Map.of(),
+ Model.newInstance());
+
+ List activeProfiles = selector.getActiveProfiles(profiles, context, problems);
+ assertEquals(2, activeProfiles.size());
+ assertTrue(activeProfiles.stream().anyMatch(p -> "profile1".equals(p.getId())));
+ assertTrue(activeProfiles.stream().anyMatch(p -> "profile2".equals(p.getId())));
+ }
+
+ @Test
+ void testCascadingActivationChain() {
+ // Create a chain of profiles: profile1 -> profile2 -> profile3
+ Profile profile1 = createProfileWithProperties(
+ "profile1", "prop1", "value1", Map.of("prop2", "value2"), Profile.SOURCE_POM);
+ Profile profile2 = createProfileWithProperties(
+ "profile2", "prop2", "value2", Map.of("prop3", "value3"), Profile.SOURCE_POM);
+ Profile profile3 = createProfile("profile3", "prop3", "value3", Profile.SOURCE_POM);
+
+ List profiles = Arrays.asList(profile1, profile2, profile3);
+
+ // Create context with prop1 set
+ DefaultProfileActivationContext context = new DefaultProfileActivationContext(
+ new DefaultPathTranslator(), new DefaultRootLocator(), new DefaultInterpolator());
+ context.setSystemProperties(Map.of("prop1", "value1"));
+ context.setModel(Model.newInstance());
+
+ // Test cascading mode
+ List activeProfiles = selector.getActiveProfiles(profiles, context, problems);
+
+ // All three profiles should be activated through cascading
+ assertEquals(3, activeProfiles.size());
+ assertTrue(activeProfiles.stream().anyMatch(p -> "profile1".equals(p.getId())));
+ assertTrue(activeProfiles.stream().anyMatch(p -> "profile2".equals(p.getId())));
+ assertTrue(activeProfiles.stream().anyMatch(p -> "profile3".equals(p.getId())));
+ assertTrue(problems.getErrors().isEmpty());
+ }
+
+ @Test
+ void testCascadingStopCondition() {
+ // Test that cascading stops when no more profiles can be activated
+ Profile profile1 = createProfileWithProperties(
+ "profile1", "prop1", "value1", Map.of("prop2", "value2"), Profile.SOURCE_POM);
+ Profile profile2 = createProfileWithProperties(
+ "profile2", "prop2", "value2", Map.of("prop3", "value3"), Profile.SOURCE_POM);
+ // profile3 requires prop4 which is never set, so cascading should stop
+ Profile profile3 = createProfile("profile3", "prop4", "value4", Profile.SOURCE_POM);
+
+ List profiles = Arrays.asList(profile1, profile2, profile3);
+
+ // Create context with prop1 set
+ DefaultProfileActivationContext context = new DefaultProfileActivationContext(
+ new DefaultPathTranslator(), new DefaultRootLocator(), new DefaultInterpolator());
+ context.setSystemProperties(Map.of("prop1", "value1"));
+ context.setModel(Model.newInstance());
+
+ // Test cascading mode
+ List activeProfiles = selector.getActiveProfiles(profiles, context, problems);
+
+ // Only profile1 and profile2 should be activated, profile3 should not
+ assertEquals(2, activeProfiles.size());
+ assertTrue(activeProfiles.stream().anyMatch(p -> "profile1".equals(p.getId())));
+ assertTrue(activeProfiles.stream().anyMatch(p -> "profile2".equals(p.getId())));
+ assertTrue(activeProfiles.stream().noneMatch(p -> "profile3".equals(p.getId())));
+ assertTrue(problems.getErrors().isEmpty());
+ }
+
+ @Test
+ void testCascadingWithCircularDependency() {
+ // Test that cascading handles circular dependencies gracefully
+ Profile profile1 = createProfileWithProperties(
+ "profile1", "prop1", "value1", Map.of("prop2", "value2"), Profile.SOURCE_POM);
+ Profile profile2 = createProfileWithProperties(
+ "profile2", "prop2", "value2", Map.of("prop1", "value1"), Profile.SOURCE_POM);
+
+ List profiles = Arrays.asList(profile1, profile2);
+
+ // Create context with prop1 set
+ DefaultProfileActivationContext context = new DefaultProfileActivationContext(
+ new DefaultPathTranslator(), new DefaultRootLocator(), new DefaultInterpolator());
+ context.setSystemProperties(Map.of("prop1", "value1"));
+ context.setModel(Model.newInstance());
+
+ // Test cascading mode
+ List activeProfiles = selector.getActiveProfiles(profiles, context, problems);
+
+ // Both profiles should be activated, but cascading should stop after first iteration
+ assertEquals(2, activeProfiles.size());
+ assertTrue(activeProfiles.stream().anyMatch(p -> "profile1".equals(p.getId())));
+ assertTrue(activeProfiles.stream().anyMatch(p -> "profile2".equals(p.getId())));
+ assertTrue(problems.getErrors().isEmpty());
+ }
+
+ @Test
+ void testCascadingWithInactiveProfile() {
+ // Create profiles where one would activate another, but the second is explicitly deactivated
+ Profile profile1 = createProfileWithProperties(
+ "profile1", "prop1", "value1", Map.of("prop2", "value2"), Profile.SOURCE_POM);
+ Profile profile2 = createProfile("profile2", "prop2", "value2", Profile.SOURCE_POM);
+
+ List profiles = Arrays.asList(profile1, profile2);
+
+ // Create context with prop1 set and profile2 explicitly deactivated
+ DefaultProfileActivationContext context = new DefaultProfileActivationContext(
+ new DefaultPathTranslator(),
+ new DefaultRootLocator(),
+ new DefaultInterpolator(),
+ List.of(),
+ List.of("profile2"),
+ Map.of("prop1", "value1"),
+ Map.of(),
+ Model.newInstance());
+
+ // Test cascading mode
+ List activeProfiles = selector.getActiveProfiles(profiles, context, problems);
+
+ // Only profile1 should be activated, profile2 should be deactivated despite cascading
+ assertEquals(1, activeProfiles.size());
+ assertTrue(activeProfiles.stream().anyMatch(p -> "profile1".equals(p.getId())));
+ assertTrue(activeProfiles.stream().noneMatch(p -> "profile2".equals(p.getId())));
+ assertTrue(problems.getErrors().isEmpty());
+ }
+
+ @Test
+ void testCascadingWithRecordImmutability() {
+ // Test that profile records remain immutable during cascading
+ Profile originalProfile1 = createProfileWithProperties(
+ "profile1", "prop1", "value1", Map.of("prop2", "value2"), Profile.SOURCE_POM);
+ Profile originalProfile2 = createProfile("profile2", "prop2", "value2", Profile.SOURCE_POM);
+
+ List profiles = Arrays.asList(originalProfile1, originalProfile2);
+
+ // Create context with prop1 set
+ DefaultProfileActivationContext context = new DefaultProfileActivationContext(
+ new DefaultPathTranslator(), new DefaultRootLocator(), new DefaultInterpolator());
+ context.setSystemProperties(Map.of("prop1", "value1"));
+ context.setModel(Model.newInstance());
+
+ // Test cascading mode
+ List activeProfiles = selector.getActiveProfiles(profiles, context, problems);
+
+ // Verify that original profiles are unchanged (immutable records)
+ assertEquals("profile1", originalProfile1.getId());
+ assertEquals("profile2", originalProfile2.getId());
+ assertEquals(Map.of("prop2", "value2"), originalProfile1.getProperties());
+ assertEquals(Map.of(), originalProfile2.getProperties());
+
+ // Verify activation worked
+ assertEquals(2, activeProfiles.size());
+ assertTrue(problems.getErrors().isEmpty());
+ }
+
+ // Helper methods for creating test profiles
+
+ private Profile createProfile(String id, String propName, String propValue, String source) {
+ Profile profile = Profile.newBuilder()
+ .id(id)
+ .activation(Activation.newBuilder()
+ .property(ActivationProperty.newBuilder()
+ .name(propName)
+ .value(propValue)
+ .build())
+ .build())
+ .build();
+ profile.setSource(source);
+ return profile;
+ }
+
+ private Profile createProfileWithProperties(
+ String id, String propName, String propValue, Map profileProperties, String source) {
+ Profile profile = Profile.newBuilder()
+ .id(id)
+ .activation(Activation.newBuilder()
+ .property(ActivationProperty.newBuilder()
+ .name(propName)
+ .value(propValue)
+ .build())
+ .build())
+ .properties(profileProperties)
+ .build();
+ profile.setSource(source);
+ return profile;
+ }
+
+ private Profile createActiveByDefaultProfile(String id, String source) {
+ Profile profile = Profile.newBuilder()
+ .id(id)
+ .activation(Activation.newBuilder().activeByDefault(true).build())
+ .build();
+ profile.setSource(source);
+ return profile;
+ }
+
+ /**
+ * Simple property-based profile activator for testing.
+ */
+ private static class PropertyProfileActivator implements ProfileActivator {
+ @Override
+ public boolean isActive(
+ Profile profile,
+ ProfileActivationContext context,
+ org.apache.maven.api.services.ModelProblemCollector problems) {
+ Activation activation = profile.getActivation();
+ if (activation == null || activation.getProperty() == null) {
+ return false;
+ }
+
+ ActivationProperty property = activation.getProperty();
+ String name = property.getName();
+ String expectedValue = property.getValue();
+
+ if (name == null) {
+ return false;
+ }
+
+ // Check user properties first, then model properties (for cascading), then system properties
+ String actualValue = context.getUserProperty(name);
+ if (actualValue == null) {
+ actualValue = context.getModelProperty(name);
+ }
+ if (actualValue == null) {
+ actualValue = context.getSystemProperty(name);
+ }
+
+ if (expectedValue == null || expectedValue.isEmpty()) {
+ return actualValue != null;
+ }
+
+ return expectedValue.equals(actualValue);
+ }
+
+ @Override
+ public boolean presentInConfig(
+ Profile profile,
+ ProfileActivationContext context,
+ org.apache.maven.api.services.ModelProblemCollector problems) {
+ return profile.getActivation() != null && profile.getActivation().getProperty() != null;
+ }
+ }
+}