diff --git a/impl/maven-core/pom.xml b/impl/maven-core/pom.xml index 07ffa6662b12..da961f431eed 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..6fcccadaf597 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; @@ -168,6 +170,7 @@ private boolean matchesExists(Map exists, DefaultProfileA private List inactiveProfileIds = Collections.emptyList(); private Map systemProperties = Collections.emptyMap(); private Map userProperties = Collections.emptyMap(); + private Map cascadedProfileProperties = Collections.emptyMap(); private Model model; final Record record; @@ -333,6 +336,24 @@ public String getModelProperty(String key) { } } + /** + * Gets a model property INCLUDING cascaded profile properties. + * This should ONLY be used for profile activation to enable cascading. + * Regular property lookups should use getModelProperty() which excludes cascaded properties. + */ + public String getModelPropertyForActivation(String key) { + // Do NOT use the usedModelProperties cache here because getModelProperty() + // may have already cached a value without cascaded properties for this key. + // Cascaded properties change between activation rounds, so we must always + // check them fresh. + String value = cascadedProfileProperties.get(key); + if (value != null) { + return value; + } + // Fall back to regular model property (which may use the cache) + return getModelProperty(key); + } + @Override public String getModelBaseDirectory() { if (record != null) { @@ -379,7 +400,7 @@ public String interpolatePath(String path) throws InterpolatorException { if ("project.rootDirectory".equals(s)) { return getModelRootDirectory(); } - String r = getModelProperty(s); + String r = getModelPropertyForActivation(s); if (r == null) { r = getUserProperty(s); } @@ -461,4 +482,34 @@ 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 for cascading activation + // Store them separately instead of mutating the model + if (activatedProfiles != null && !activatedProfiles.isEmpty()) { + Map newCascadedProperties = new HashMap<>(cascadedProfileProperties); + + // Add properties from each activated profile + for (Profile profile : activatedProfiles) { + if (profile.getProperties() != null) { + newCascadedProperties.putAll(profile.getProperties()); + } + } + + // Update the cascaded properties map + this.cascadedProfileProperties = Collections.unmodifiableMap(newCascadedProperties); + } + } + + /** + * Clear all cascaded profile properties. + * This should be called after profile activation is complete to prevent cascaded properties + * from leaking into subsequent uses of the context. + */ + public void clearCascadedProfileProperties() { + this.cascadedProfileProperties = Collections.emptyMap(); + } } 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..a47fcf9653e8 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,54 @@ 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()); + + // Clear cascaded properties to prevent them from leaking into subsequent uses of the context + if (context instanceof org.apache.maven.impl.model.DefaultProfileActivationContext dctx) { + dctx.clearCascadedProfileProperties(); } - 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/ConditionProfileActivator.java b/impl/maven-impl/src/main/java/org/apache/maven/impl/model/profile/ConditionProfileActivator.java index 98aa2c037639..a251f9d6dc6b 100644 --- a/impl/maven-impl/src/main/java/org/apache/maven/impl/model/profile/ConditionProfileActivator.java +++ b/impl/maven-impl/src/main/java/org/apache/maven/impl/model/profile/ConditionProfileActivator.java @@ -77,7 +77,7 @@ public boolean isActive(Profile profile, ProfileActivationContext context, Model String condition = profile.getActivation().getCondition(); try { Map functions = registerFunctions(context, versionParser); - UnaryOperator propertyResolver = s -> property(context, s); + UnaryOperator propertyResolver = s -> property(profile, context, s); return toBoolean(new ConditionParser(functions, propertyResolver).parse(condition)); } catch (Exception e) { problems.add( @@ -160,12 +160,12 @@ public Map registerFunctions( * @return The value of the property, or null if not found * @throws IllegalArgumentException if the number of arguments is not exactly one */ - String property(ProfileActivationContext context, String name) { - String value = doGetProperty(context, name); - return interpolator.interpolate(value, s -> doGetProperty(context, s)); + String property(Profile profile, ProfileActivationContext context, String name) { + String value = doGetProperty(profile, context, name); + return interpolator.interpolate(value, s -> doGetProperty(profile, context, s)); } - static String doGetProperty(ProfileActivationContext context, String name) { + static String doGetProperty(Profile profile, ProfileActivationContext context, String name) { // Handle special project-related properties if ("project.basedir".equals(name)) { return context.getModelBaseDirectory(); @@ -182,16 +182,23 @@ static String doGetProperty(ProfileActivationContext context, String name) { // Check user properties String v = context.getUserProperty(name); - if (v == null) { - // Check project properties - // TODO: this may leads to instability between file model activation and effective model activation - // as the effective model properties may be different from the file model - v = context.getModelProperty(name); - } if (v == null) { // Check system properties v = context.getSystemProperty(name); } + if (v == null) { + // Check project properties (with cascading support if available) + // ONLY for POM profiles - settings profiles should not use model properties + // TODO: this may leads to instability between file model activation and effective model activation + // as the effective model properties may be different from the file model + if (Profile.SOURCE_POM.equals(profile.getSource())) { + if (context instanceof org.apache.maven.impl.model.DefaultProfileActivationContext dctx) { + v = dctx.getModelPropertyForActivation(name); + } else { + v = context.getModelProperty(name); + } + } + } return v; } } 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..6fd144fdfdba 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 @@ -76,6 +76,16 @@ public boolean isActive(Profile profile, ProfileActivationContext context, Model if (sysValue == null) { sysValue = context.getSystemProperty(name); } + // Check model properties last (for cascading profile activation) + // ONLY for POM profiles - settings profiles should not use model properties + if (sysValue == null && Profile.SOURCE_POM.equals(profile.getSource())) { + // Use getModelPropertyForActivation to include cascaded profile properties + if (context instanceof org.apache.maven.impl.model.DefaultProfileActivationContext dctx) { + sysValue = dctx.getModelPropertyForActivation(name); + } else { + sysValue = context.getModelProperty(name); + } + } String propValue = property.getValue(); if (propValue != null && !propValue.isEmpty()) { 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..bac676e69a23 --- /dev/null +++ b/impl/maven-impl/src/test/java/org/apache/maven/impl/model/DefaultProfileSelectorTest.java @@ -0,0 +1,433 @@ +/* + * 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) { + // Use getModelPropertyForActivation to include cascaded profile properties + if (context instanceof DefaultProfileActivationContext dctx) { + actualValue = dctx.getModelPropertyForActivation(name); + } else { + 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; + } + } +}