Skip to content

Commit

Permalink
[MNG-3309] Cascading profile activation
Browse files Browse the repository at this point in the history
  • Loading branch information
gnodet committed Oct 4, 2024
1 parent d51be79 commit 1ed072b
Show file tree
Hide file tree
Showing 11 changed files with 274 additions and 229 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,12 @@
package org.apache.maven.api.services.model;

import java.nio.file.Path;
import java.util.Collection;
import java.util.List;
import java.util.Map;

import org.apache.maven.api.model.Profile;

/**
* Describes the environmental context used to determine the activation status of profiles.
*
Expand Down Expand Up @@ -78,4 +81,11 @@ public interface ProfileActivationContext {
* @return The project properties, never {@code null}.
*/
Map<String, String> getProjectProperties();

/**
* Inject properties from newly activated profiles in order to trigger the cascading mechanism.
*
* @param activatedProfiles The collection of profiles that have been activated that may trigger the cascading effect.
*/
void addProfileProperties(Collection<Profile> activatedProfiles);
}
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,13 @@ public interface ProfileSelector {
* @param context The environmental context used to determine the activation status of a profile, must not be
* {@code null}.
* @param problems The container used to collect problems that were encountered, must not be {@code null}.
* @param cascade Indicates whether profile activation should cascade, i.e. properties from an activated profile may
* trigger the activation of other profiles.
* @return The profiles that have been activated, never {@code null}.
*/
List<Profile> getActiveProfiles(
Collection<Profile> profiles, ProfileActivationContext context, ModelProblemCollector problems);
Collection<Profile> profiles,
ProfileActivationContext context,
ModelProblemCollector problems,
boolean cascade);
}
Original file line number Diff line number Diff line change
Expand Up @@ -237,7 +237,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;
}

Expand Down

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,15 @@
package org.apache.maven.internal.impl.model;

import java.nio.file.Path;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.stream.Collectors;

import org.apache.maven.api.model.Profile;
import org.apache.maven.api.services.model.ProfileActivationContext;

/**
Expand Down Expand Up @@ -170,6 +173,14 @@ public DefaultProfileActivationContext setProjectProperties(Map<String, String>
return this;
}

public void addProfileProperties(Collection<Profile> profiles) {
Map<String, String> props = new HashMap<>(this.projectProperties);
for (var profile : profiles) {
props.putAll(profile.getProperties());
}
this.projectProperties = unmodifiable(props);
}

private static List<String> unmodifiable(List<String> list) {
return list != null ? Collections.unmodifiableList(list) : Collections.emptyList();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,77 +22,166 @@
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.function.Supplier;
import java.util.function.UnaryOperator;

import org.apache.maven.api.di.Inject;
import org.apache.maven.api.di.Named;
import org.apache.maven.api.di.Singleton;
import org.apache.maven.api.model.Activation;
import org.apache.maven.api.model.ActivationFile;
import org.apache.maven.api.model.Profile;
import org.apache.maven.api.services.BuilderProblem.Severity;
import org.apache.maven.api.services.Interpolator;
import org.apache.maven.api.services.InterpolatorException;
import org.apache.maven.api.services.ModelProblem.Version;
import org.apache.maven.api.services.ModelProblemCollector;
import org.apache.maven.api.services.model.ProfileActivationContext;
import org.apache.maven.api.services.model.ProfileActivator;
import org.apache.maven.api.services.model.ProfileSelector;
import org.apache.maven.model.v4.MavenTransformer;

/**
* Calculates the active profiles among a given collection of profiles.
*
*/
@Named
@Singleton
public class DefaultProfileSelector implements ProfileSelector {

private final Interpolator interpolator;
private final ProfileActivationFilePathInterpolator profileActivationFilePathInterpolator;
private final List<ProfileActivator> activators;

public DefaultProfileSelector() {
this.activators = new ArrayList<>();
public DefaultProfileSelector(
Interpolator interpolator, ProfileActivationFilePathInterpolator profileActivationFilePathInterpolator) {
this(interpolator, profileActivationFilePathInterpolator, new ArrayList<>());
}

@Inject
public DefaultProfileSelector(List<ProfileActivator> activators) {
public DefaultProfileSelector(
Interpolator interpolator,
ProfileActivationFilePathInterpolator profileActivationFilePathInterpolator,
List<ProfileActivator> activators) {
this.interpolator = interpolator;
this.profileActivationFilePathInterpolator = profileActivationFilePathInterpolator;
this.activators = new ArrayList<>(activators);
}

public DefaultProfileSelector addProfileActivator(ProfileActivator profileActivator) {
if (profileActivator != null) {
activators.add(profileActivator);
@Override
public List<Profile> getActiveProfiles(
Collection<Profile> orgProfiles,
ProfileActivationContext context,
ModelProblemCollector problems,
boolean cascade) {

if (cascade) {
return getActiveProfilesCascading(orgProfiles, context, problems);
} else {
return getActiveProfilesNonCascading(orgProfiles, context, problems);
}
return this;
}

@Override
public List<Profile> getActiveProfiles(
public List<Profile> getActiveProfilesNonCascading(
Collection<Profile> profiles, ProfileActivationContext context, ModelProblemCollector problems) {

Collection<String> activatedIds = new HashSet<>(context.getActiveProfileIds());
Collection<String> deactivatedIds = new HashSet<>(context.getInactiveProfileIds());

List<Profile> activeProfiles = new ArrayList<>(profiles.size());
List<Profile> activeSettingsProfiles = new ArrayList<>();
List<Profile> activePomProfiles = new ArrayList<>();
List<Profile> activePomProfilesByDefault = new ArrayList<>();
boolean activatedPomProfileNotByDefault = false;

for (Profile profile : profiles) {
if (!deactivatedIds.contains(profile.getId())) {
if (activatedIds.contains(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);

ProfileActivationInterpolator activationInterpolator = new ProfileActivationInterpolator(context, problems);
for (String source : List.of(Profile.SOURCE_SETTINGS, Profile.SOURCE_POM)) {
// Iterate over the profiles and check if a given profile is activated
List<Profile> activatedProfiles = new ArrayList<>();
for (Profile profile : profiles) {
if (Objects.equals(source, profile.getSource())) {
Profile iprofile = activationInterpolator.apply(profile);
if (!deactivatedIds.contains(iprofile.getId())) {
boolean activated = activatedIds.contains(iprofile.getId());
boolean active = isActive(iprofile, context, problems);
boolean activeByDefault = isActiveByDefault(iprofile);
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);
}
activatedProfiles.add(profile);
}
}
}
}
context.addProfileProperties(activatedProfiles);
}

List<Profile> allActivated = new ArrayList<>();
if (activePomProfiles.isEmpty()) {
allActivated.addAll(activePomProfilesByDefault);
} else {
allActivated.addAll(activePomProfiles);
}
allActivated.addAll(activeSettingsProfiles);

return allActivated;
}

public List<Profile> getActiveProfilesCascading(
Collection<Profile> orgProfiles, ProfileActivationContext context, ModelProblemCollector problems) {

Collection<String> activatedIds = new HashSet<>(context.getActiveProfileIds());
Collection<String> deactivatedIds = new HashSet<>(context.getInactiveProfileIds());

List<Profile> activeSettingsProfiles = new ArrayList<>();
List<Profile> activePomProfiles = new ArrayList<>();
List<Profile> activePomProfilesByDefault = new ArrayList<>();

List<Profile> profiles = new ArrayList<>(orgProfiles);
ProfileActivationInterpolator activationInterpolator = new ProfileActivationInterpolator(context, problems);
List<Profile> activatedProfiles;
do {
// Iterate over the profiles and check if a given profile is activated
activatedProfiles = new ArrayList<>();
for (Profile profile : List.copyOf(profiles)) {
Profile iprofile = activationInterpolator.apply(profile);
if (!deactivatedIds.contains(iprofile.getId())) {
boolean activated = activatedIds.contains(iprofile.getId());
boolean active = isActive(iprofile, context, problems);
boolean activeByDefault = isActiveByDefault(iprofile);
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);
}
profiles.remove(profile);
activatedProfiles.add(profile);
}
}
}
context.addProfileProperties(activatedProfiles);
} while (!activatedProfiles.isEmpty());

if (!activatedPomProfileNotByDefault) {
activeProfiles.addAll(activePomProfilesByDefault);
List<Profile> 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) {
Expand Down Expand Up @@ -122,4 +211,73 @@ private boolean isActiveByDefault(Profile profile) {
Activation activation = profile.getActivation();
return activation != null && activation.isActiveByDefault();
}

private class ProfileActivationInterpolator extends MavenTransformer implements UnaryOperator<Profile> {
private final ProfileActivationContext context;
private final ModelProblemCollector problems;

ProfileActivationInterpolator(ProfileActivationContext context, ModelProblemCollector problems) {
super(s -> {
try {
Map<String, String> map1 = context.getUserProperties();
Map<String, String> map2 = context.getProjectProperties();
Map<String, String> map3 = context.getSystemProperties();
return interpolator.interpolate(s, Interpolator.chain(List.of(map1::get, map2::get, map3::get)));
} catch (InterpolatorException e) {
problems.add(Severity.ERROR, Version.BASE, e.getMessage(), e);
}
return s;
});
this.context = context;
this.problems = problems;
}

@Override
public Profile apply(Profile p) {
return Profile.newBuilder(p)
.activation(transformActivation(p.getActivation()))
.build();
}

@Override
protected Activation.Builder transformActivation_Condition(
Supplier<? extends Activation.Builder> creator, Activation.Builder builder, Activation target) {
// do not interpolate the condition activation
return builder;
}

@Override
protected ActivationFile.Builder transformActivationFile_Missing(
Supplier<? extends ActivationFile.Builder> creator,
ActivationFile.Builder builder,
ActivationFile target) {
String path = target.getMissing();
String xformed = transformPath(path, target, "missing");
return xformed != path ? (builder != null ? builder : creator.get()).missing(xformed) : builder;
}

@Override
protected ActivationFile.Builder transformActivationFile_Exists(
Supplier<? extends ActivationFile.Builder> creator,
ActivationFile.Builder builder,
ActivationFile target) {
final String path = target.getExists();
final String xformed = transformPath(path, target, "exists");
return xformed != path ? (builder != null ? builder : creator.get()).exists(xformed) : builder;
}

private String transformPath(String path, ActivationFile target, String locationKey) {
try {
return profileActivationFilePathInterpolator.interpolate(path, context);
} catch (InterpolatorException e) {
problems.add(
Severity.ERROR,
Version.BASE,
"Failed to interpolate file location " + path + ": " + e.getMessage(),
target.getLocation(locationKey),
e);
}
return path;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ private List<String> tokenize(String expression) {
inPropertyAlias = false;
tokens.add("property");
tokens.add("(");
tokens.add("'" + sb.toString() + "'");
tokens.add("'" + sb + "'");
tokens.add(")");
sb.setLength(0);
} else {
Expand All @@ -89,7 +89,7 @@ private List<String> tokenize(String expression) {
}

if (c == '$' && i + 1 < expression.length() && expression.charAt(i + 1) == '{') {
if (sb.length() > 0) {
if (!sb.isEmpty()) {
tokens.add(sb.toString());
sb.setLength(0);
}
Expand All @@ -100,7 +100,7 @@ private List<String> tokenize(String expression) {
}

if (c == '"' || c == '\'') {
if (sb.length() > 0) {
if (!sb.isEmpty()) {
tokens.add(sb.toString());
sb.setLength(0);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,9 @@ public boolean isActive(Profile profile, ProfileActivationContext context, Model
}

String sysValue = context.getUserProperties().get(name);
if (sysValue == null) {
sysValue = context.getProjectProperties().get(name);
}
if (sysValue == null) {
sysValue = context.getSystemProperties().get(name);
}
Expand Down
Loading

0 comments on commit 1ed072b

Please sign in to comment.