[MNG-8081] interpolate available properties during default profile selection (Maven 4.x) (#1446)

Co-authored-by: Guillaume Nodet <gnodet@gmail.com>
This commit is contained in:
Matt Benson 2024-05-02 08:13:07 -05:00 committed by GitHub
parent 6814ba386d
commit 399f8b4ffc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 717 additions and 137 deletions

View File

@ -35,7 +35,9 @@ import java.util.concurrent.Callable;
import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.atomic.AtomicReference;
import java.util.function.BiConsumer; import java.util.function.BiConsumer;
import java.util.function.Supplier; import java.util.function.Supplier;
import java.util.function.UnaryOperator;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.apache.maven.api.VersionRange; import org.apache.maven.api.VersionRange;
import org.apache.maven.api.annotations.Nullable; import org.apache.maven.api.annotations.Nullable;
@ -50,6 +52,7 @@ import org.apache.maven.api.model.Dependency;
import org.apache.maven.api.model.DependencyManagement; import org.apache.maven.api.model.DependencyManagement;
import org.apache.maven.api.model.Exclusion; import org.apache.maven.api.model.Exclusion;
import org.apache.maven.api.model.InputLocation; import org.apache.maven.api.model.InputLocation;
import org.apache.maven.api.model.InputLocationTracker;
import org.apache.maven.api.model.InputSource; import org.apache.maven.api.model.InputSource;
import org.apache.maven.api.model.Model; import org.apache.maven.api.model.Model;
import org.apache.maven.api.model.Parent; import org.apache.maven.api.model.Parent;
@ -80,8 +83,11 @@ import org.apache.maven.api.services.xml.XmlReaderRequest;
import org.apache.maven.internal.impl.InternalSession; import org.apache.maven.internal.impl.InternalSession;
import org.apache.maven.internal.impl.resolver.DefaultModelCache; import org.apache.maven.internal.impl.resolver.DefaultModelCache;
import org.apache.maven.internal.impl.resolver.DefaultModelResolver; import org.apache.maven.internal.impl.resolver.DefaultModelResolver;
import org.apache.maven.model.v4.MavenTransformer;
import org.codehaus.plexus.interpolation.InterpolationException; import org.codehaus.plexus.interpolation.InterpolationException;
import org.codehaus.plexus.interpolation.Interpolator;
import org.codehaus.plexus.interpolation.MapBasedValueSource; import org.codehaus.plexus.interpolation.MapBasedValueSource;
import org.codehaus.plexus.interpolation.RegexBasedInterpolator;
import org.codehaus.plexus.interpolation.StringSearchInterpolator; import org.codehaus.plexus.interpolation.StringSearchInterpolator;
import org.eclipse.aether.impl.RemoteRepositoryManager; import org.eclipse.aether.impl.RemoteRepositoryManager;
@ -377,54 +383,75 @@ public class DefaultModelBuilder implements ModelBuilder {
private List<Profile> interpolateActivations( private List<Profile> interpolateActivations(
List<Profile> profiles, DefaultProfileActivationContext context, DefaultModelProblemCollector problems) { List<Profile> profiles, DefaultProfileActivationContext context, DefaultModelProblemCollector problems) {
List<Profile> newProfiles = null; if (profiles.stream()
for (int index = 0; index < profiles.size(); index++) { .map(org.apache.maven.api.model.Profile::getActivation)
Profile profile = profiles.get(index); .noneMatch(Objects::nonNull)) {
Activation activation = profile.getActivation(); return profiles;
if (activation != null) { }
ActivationFile file = activation.getFile(); final Interpolator xform = new RegexBasedInterpolator();
if (file != null) { xform.setCacheAnswers(true);
String oldExists = file.getExists(); Stream.of(context.getUserProperties(), context.getSystemProperties())
if (isNotEmpty(oldExists)) { .map(MapBasedValueSource::new)
.forEach(xform::addValueSource);
class ProfileInterpolator extends MavenTransformer implements UnaryOperator<Profile> {
ProfileInterpolator() {
super(s -> {
if (isNotEmpty(s)) {
try { try {
String newExists = interpolate(oldExists, context); return xform.interpolate(s);
if (!Objects.equals(oldExists, newExists)) {
if (newProfiles == null) {
newProfiles = new ArrayList<>(profiles);
}
newProfiles.set(
index, profile.withActivation(activation.withFile(file.withExists(newExists))));
}
} catch (InterpolationException e) { } catch (InterpolationException e) {
addInterpolationProblem(problems, file, oldExists, e, "exists"); problems.add(Severity.ERROR, ModelProblem.Version.BASE, e.getMessage(), e);
} }
} else { }
String oldMissing = file.getMissing(); return s;
if (isNotEmpty(oldMissing)) { });
}
@Override
public Profile apply(Profile p) {
return Profile.newBuilder(p)
.activation(transformActivation(p.getActivation()))
.build();
}
@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) {
if (isNotEmpty(path)) {
try { try {
String newMissing = interpolate(oldMissing, context); return profileActivationFilePathInterpolator.interpolate(path, context);
if (!Objects.equals(oldMissing, newMissing)) {
if (newProfiles == null) {
newProfiles = new ArrayList<>(profiles);
}
newProfiles.set(
index,
profile.withActivation(activation.withFile(file.withMissing(newMissing))));
}
} catch (InterpolationException e) { } catch (InterpolationException e) {
addInterpolationProblem(problems, file, oldMissing, e, "missing"); addInterpolationProblem(problems, target, path, e, locationKey);
} }
} }
return path;
} }
} }
} return profiles.stream().map(new ProfileInterpolator()).toList();
}
return newProfiles != null ? newProfiles : profiles;
} }
private static void addInterpolationProblem( private static void addInterpolationProblem(
DefaultModelProblemCollector problems, DefaultModelProblemCollector problems,
ActivationFile file, InputLocationTracker target,
String path, String path,
InterpolationException e, InterpolationException e,
String locationKey) { String locationKey) {
@ -432,14 +459,10 @@ public class DefaultModelBuilder implements ModelBuilder {
Severity.ERROR, Severity.ERROR,
ModelProblem.Version.BASE, ModelProblem.Version.BASE,
"Failed to interpolate file location " + path + ": " + e.getMessage(), "Failed to interpolate file location " + path + ": " + e.getMessage(),
file.getLocation(locationKey), target.getLocation(locationKey),
e); e);
} }
private String interpolate(String path, ProfileActivationContext context) throws InterpolationException {
return isNotEmpty(path) ? profileActivationFilePathInterpolator.interpolate(path, context) : path;
}
private static boolean isNotEmpty(String string) { private static boolean isNotEmpty(String string) {
return string != null && !string.isEmpty(); return string != null && !string.isEmpty();
} }

View File

@ -21,20 +21,31 @@ package org.apache.maven.internal.impl.model;
import java.io.File; import java.io.File;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
import java.util.Deque;
import java.util.HashMap; import java.util.HashMap;
import java.util.HashSet; import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Objects; import java.util.Objects;
import java.util.Optional;
import java.util.Set; import java.util.Set;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.function.UnaryOperator;
import java.util.regex.Matcher; import java.util.regex.Matcher;
import java.util.regex.Pattern; import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.StreamSupport;
import org.apache.maven.api.di.Inject; import org.apache.maven.api.di.Inject;
import org.apache.maven.api.di.Named; import org.apache.maven.api.di.Named;
import org.apache.maven.api.di.Singleton; import org.apache.maven.api.di.Singleton;
import org.apache.maven.api.model.Activation; import org.apache.maven.api.model.Activation;
import org.apache.maven.api.model.ActivationFile; import org.apache.maven.api.model.ActivationFile;
import org.apache.maven.api.model.ActivationOS;
import org.apache.maven.api.model.ActivationProperty;
import org.apache.maven.api.model.Build; import org.apache.maven.api.model.Build;
import org.apache.maven.api.model.BuildBase; import org.apache.maven.api.model.BuildBase;
import org.apache.maven.api.model.Dependency; import org.apache.maven.api.model.Dependency;
@ -59,6 +70,7 @@ import org.apache.maven.api.services.ModelProblem.Version;
import org.apache.maven.api.services.ModelProblemCollector; import org.apache.maven.api.services.ModelProblemCollector;
import org.apache.maven.api.services.model.*; import org.apache.maven.api.services.model.*;
import org.apache.maven.model.v4.MavenModelVersion; import org.apache.maven.model.v4.MavenModelVersion;
import org.apache.maven.model.v4.MavenTransformer;
/** /**
*/ */
@ -80,6 +92,196 @@ public class DefaultModelValidator implements ModelValidator {
private static final String EMPTY = ""; private static final String EMPTY = "";
private record ActivationFrame(String location, Optional<? extends InputLocationTracker> parent) {}
private static class ActivationWalker extends MavenTransformer {
private final Deque<ActivationFrame> stk;
ActivationWalker(Deque<ActivationFrame> stk, UnaryOperator<String> transformer) {
super(transformer);
this.stk = stk;
}
private ActivationFrame nextFrame(String property) {
return new ActivationFrame(property, Optional.empty());
}
private <P> ActivationFrame nextFrame(String property, Function<P, InputLocationTracker> child) {
@SuppressWarnings("unchecked")
final Optional<P> parent = (Optional<P>) stk.peek().parent;
return new ActivationFrame(property, parent.map(child));
}
@Override
public Activation transformActivation(Activation target) {
stk.push(new ActivationFrame("activation", Optional.of(target)));
try {
return super.transformActivation(target);
} finally {
stk.pop();
}
}
@Override
protected Activation.Builder transformActivation_ActiveByDefault(
Supplier<? extends Activation.Builder> creator, Activation.Builder builder, Activation target) {
return builder;
}
@Override
protected Activation.Builder transformActivation_File(
Supplier<? extends Activation.Builder> creator, Activation.Builder builder, Activation target) {
stk.push(nextFrame("file", Activation::getFile));
Optional.ofNullable(target.getFile());
try {
return super.transformActivation_File(creator, builder, target);
} finally {
stk.pop();
}
}
@Override
protected ActivationFile.Builder transformActivationFile_Exists(
Supplier<? extends ActivationFile.Builder> creator,
ActivationFile.Builder builder,
ActivationFile target) {
stk.push(nextFrame("exists"));
try {
return super.transformActivationFile_Exists(creator, builder, target);
} finally {
stk.pop();
}
}
@Override
protected ActivationFile.Builder transformActivationFile_Missing(
Supplier<? extends ActivationFile.Builder> creator,
ActivationFile.Builder builder,
ActivationFile target) {
stk.push(nextFrame("missing"));
try {
return super.transformActivationFile_Missing(creator, builder, target);
} finally {
stk.pop();
}
}
@Override
protected Activation.Builder transformActivation_Jdk(
Supplier<? extends Activation.Builder> creator, Activation.Builder builder, Activation target) {
stk.push(nextFrame("jdk"));
try {
return super.transformActivation_Jdk(creator, builder, target);
} finally {
stk.pop();
}
}
@Override
protected Activation.Builder transformActivation_Os(
Supplier<? extends Activation.Builder> creator, Activation.Builder builder, Activation target) {
stk.push(nextFrame("os", Activation::getOs));
try {
return super.transformActivation_Os(creator, builder, target);
} finally {
stk.pop();
}
}
@Override
protected ActivationOS.Builder transformActivationOS_Arch(
Supplier<? extends ActivationOS.Builder> creator, ActivationOS.Builder builder, ActivationOS target) {
stk.push(nextFrame("arch"));
try {
return super.transformActivationOS_Arch(creator, builder, target);
} finally {
stk.pop();
}
}
@Override
protected ActivationOS.Builder transformActivationOS_Family(
Supplier<? extends ActivationOS.Builder> creator, ActivationOS.Builder builder, ActivationOS target) {
stk.push(nextFrame("family"));
try {
return super.transformActivationOS_Family(creator, builder, target);
} finally {
stk.pop();
}
}
@Override
protected ActivationOS.Builder transformActivationOS_Name(
Supplier<? extends ActivationOS.Builder> creator, ActivationOS.Builder builder, ActivationOS target) {
stk.push(nextFrame("name"));
try {
return super.transformActivationOS_Name(creator, builder, target);
} finally {
stk.pop();
}
}
@Override
protected ActivationOS.Builder transformActivationOS_Version(
Supplier<? extends ActivationOS.Builder> creator, ActivationOS.Builder builder, ActivationOS target) {
stk.push(nextFrame("version"));
try {
return super.transformActivationOS_Version(creator, builder, target);
} finally {
stk.pop();
}
}
@Override
protected Activation.Builder transformActivation_Packaging(
Supplier<? extends Activation.Builder> creator, Activation.Builder builder, Activation target) {
stk.push(nextFrame("packaging"));
try {
return super.transformActivation_Packaging(creator, builder, target);
} finally {
stk.pop();
}
}
@Override
protected Activation.Builder transformActivation_Property(
Supplier<? extends Activation.Builder> creator, Activation.Builder builder, Activation target) {
stk.push(nextFrame("property", Activation::getProperty));
try {
return super.transformActivation_Property(creator, builder, target);
} finally {
stk.pop();
}
}
@Override
protected ActivationProperty.Builder transformActivationProperty_Name(
Supplier<? extends ActivationProperty.Builder> creator,
ActivationProperty.Builder builder,
ActivationProperty target) {
stk.push(nextFrame("name"));
try {
return super.transformActivationProperty_Name(creator, builder, target);
} finally {
stk.pop();
}
}
@Override
protected ActivationProperty.Builder transformActivationProperty_Value(
Supplier<? extends ActivationProperty.Builder> creator,
ActivationProperty.Builder builder,
ActivationProperty target) {
stk.push(nextFrame("value"));
try {
return super.transformActivationProperty_Value(creator, builder, target);
} finally {
stk.pop();
}
}
}
private final Set<String> validCoordinateIds = new HashSet<>(); private final Set<String> validCoordinateIds = new HashSet<>();
private final Set<String> validProfileIds = new HashSet<>(); private final Set<String> validProfileIds = new HashSet<>();
@ -282,42 +484,53 @@ public class DefaultModelValidator implements ModelValidator {
} }
private void validate30RawProfileActivation(ModelProblemCollector problems, Activation activation, String prefix) { private void validate30RawProfileActivation(ModelProblemCollector problems, Activation activation, String prefix) {
if (activation == null || activation.getFile() == null) { if (activation == null) {
return; return;
} }
ActivationFile file = activation.getFile(); final Deque<ActivationFrame> stk = new LinkedList<>();
String path; final Supplier<String> pathSupplier = () -> {
String location; final boolean parallel = false;
return StreamSupport.stream(((Iterable<ActivationFrame>) stk::descendingIterator).spliterator(), parallel)
if (file.getExists() != null && !file.getExists().isEmpty()) { .map(ActivationFrame::location)
path = file.getExists(); .collect(Collectors.joining("."));
location = "exists"; };
} else if (file.getMissing() != null && !file.getMissing().isEmpty()) { final Supplier<InputLocation> locationSupplier = () -> {
path = file.getMissing(); if (stk.size() < 2) {
location = "missing"; return null;
} else {
return;
} }
Iterator<ActivationFrame> f = stk.iterator();
if (hasProjectExpression(path)) { String location = f.next().location;
Matcher matcher = EXPRESSION_PROJECT_NAME_PATTERN.matcher(path); ActivationFrame parent = f.next();
return parent.parent.map(p -> p.getLocation(location)).orElse(null);
};
final UnaryOperator<String> transformer = s -> {
if (hasProjectExpression(s)) {
String path = pathSupplier.get();
Matcher matcher = EXPRESSION_PROJECT_NAME_PATTERN.matcher(s);
while (matcher.find()) { while (matcher.find()) {
String propertyName = matcher.group(0); String propertyName = matcher.group(0);
if (!"${project.basedir}".equals(propertyName)) {
if (path.startsWith("activation.file.") && "${project.basedir}".equals(propertyName)) {
continue;
}
addViolation( addViolation(
problems, problems,
Severity.WARNING, Severity.WARNING,
Version.V30, Version.V30,
prefix + "activation.file." + location, prefix + path,
null, null,
"Failed to interpolate file location " + path + ": " + propertyName "Failed to interpolate profile activation property " + s + ": " + propertyName
+ " expressions are not supported during profile activation.", + " expressions are not supported during profile activation.",
file.getLocation(location)); locationSupplier.get());
}
} }
} }
return s;
};
new ActivationWalker(stk, transformer).transformActivation(activation);
} }
private void validate20RawPlugins( private void validate20RawPlugins(

View File

@ -38,10 +38,13 @@ import java.util.Optional;
import java.util.Properties; import java.util.Properties;
import java.util.concurrent.Callable; import java.util.concurrent.Callable;
import java.util.function.Supplier; import java.util.function.Supplier;
import java.util.function.UnaryOperator;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.apache.maven.api.VersionRange; import org.apache.maven.api.VersionRange;
import org.apache.maven.api.feature.Features; import org.apache.maven.api.feature.Features;
import org.apache.maven.api.model.ActivationFile;
import org.apache.maven.api.model.Exclusion; import org.apache.maven.api.model.Exclusion;
import org.apache.maven.api.model.InputSource; import org.apache.maven.api.model.InputSource;
import org.apache.maven.api.services.VersionParserException; import org.apache.maven.api.services.VersionParserException;
@ -57,6 +60,7 @@ import org.apache.maven.model.Plugin;
import org.apache.maven.model.PluginManagement; import org.apache.maven.model.PluginManagement;
import org.apache.maven.model.Profile; import org.apache.maven.model.Profile;
import org.apache.maven.model.building.ModelProblem.Severity; import org.apache.maven.model.building.ModelProblem.Severity;
import org.apache.maven.model.building.ModelProblem.Version;
import org.apache.maven.model.composition.DependencyManagementImporter; import org.apache.maven.model.composition.DependencyManagementImporter;
import org.apache.maven.model.inheritance.InheritanceAssembler; import org.apache.maven.model.inheritance.InheritanceAssembler;
import org.apache.maven.model.interpolation.ModelInterpolator; import org.apache.maven.model.interpolation.ModelInterpolator;
@ -82,11 +86,14 @@ import org.apache.maven.model.resolution.ModelResolver;
import org.apache.maven.model.resolution.UnresolvableModelException; import org.apache.maven.model.resolution.UnresolvableModelException;
import org.apache.maven.model.resolution.WorkspaceModelResolver; import org.apache.maven.model.resolution.WorkspaceModelResolver;
import org.apache.maven.model.superpom.SuperPomProvider; import org.apache.maven.model.superpom.SuperPomProvider;
import org.apache.maven.model.v4.MavenTransformer;
import org.apache.maven.model.validation.DefaultModelValidator; import org.apache.maven.model.validation.DefaultModelValidator;
import org.apache.maven.model.validation.ModelValidator; import org.apache.maven.model.validation.ModelValidator;
import org.apache.maven.model.version.ModelVersionParser; import org.apache.maven.model.version.ModelVersionParser;
import org.codehaus.plexus.interpolation.InterpolationException; import org.codehaus.plexus.interpolation.InterpolationException;
import org.codehaus.plexus.interpolation.Interpolator;
import org.codehaus.plexus.interpolation.MapBasedValueSource; import org.codehaus.plexus.interpolation.MapBasedValueSource;
import org.codehaus.plexus.interpolation.RegexBasedInterpolator;
import org.codehaus.plexus.interpolation.StringSearchInterpolator; import org.codehaus.plexus.interpolation.StringSearchInterpolator;
import org.eclipse.sisu.Nullable; import org.eclipse.sisu.Nullable;
@ -897,69 +904,89 @@ public class DefaultModelBuilder implements ModelBuilder {
List<org.apache.maven.api.model.Profile> profiles, List<org.apache.maven.api.model.Profile> profiles,
DefaultProfileActivationContext context, DefaultProfileActivationContext context,
DefaultModelProblemCollector problems) { DefaultModelProblemCollector problems) {
List<org.apache.maven.api.model.Profile> newProfiles = null; if (profiles.stream()
for (int index = 0; index < profiles.size(); index++) { .map(org.apache.maven.api.model.Profile::getActivation)
org.apache.maven.api.model.Profile profile = profiles.get(index); .noneMatch(Objects::nonNull)) {
org.apache.maven.api.model.Activation activation = profile.getActivation(); return profiles;
if (activation != null) { }
org.apache.maven.api.model.ActivationFile file = activation.getFile(); final Interpolator xform = new RegexBasedInterpolator();
if (file != null) { xform.setCacheAnswers(true);
String oldExists = file.getExists(); Stream.of(context.getUserProperties(), context.getSystemProperties())
if (isNotEmpty(oldExists)) { .map(MapBasedValueSource::new)
.forEach(xform::addValueSource);
class ProfileInterpolator extends MavenTransformer
implements UnaryOperator<org.apache.maven.api.model.Profile> {
ProfileInterpolator() {
super(s -> {
if (isNotEmpty(s)) {
try { try {
String newExists = interpolate(oldExists, context); return xform.interpolate(s);
if (!Objects.equals(oldExists, newExists)) {
if (newProfiles == null) {
newProfiles = new ArrayList<>(profiles);
}
newProfiles.set(
index, profile.withActivation(activation.withFile(file.withExists(newExists))));
}
} catch (InterpolationException e) { } catch (InterpolationException e) {
addInterpolationProblem(problems, file, oldExists, e, "exists"); problems.add(new ModelProblemCollectorRequest(Severity.ERROR, Version.BASE)
.setMessage(e.getMessage())
.setException(e));
} }
} else { }
String oldMissing = file.getMissing(); return s;
if (isNotEmpty(oldMissing)) { });
}
@Override
public org.apache.maven.api.model.Profile apply(org.apache.maven.api.model.Profile p) {
return org.apache.maven.api.model.Profile.newBuilder(p)
.activation(transformActivation(p.getActivation()))
.build();
}
@Override
protected ActivationFile.Builder transformActivationFile_Missing(
Supplier<? extends ActivationFile.Builder> creator,
ActivationFile.Builder builder,
ActivationFile target) {
final String path = target.getMissing();
final 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) {
if (isNotEmpty(path)) {
try { try {
String newMissing = interpolate(oldMissing, context); return profileActivationFilePathInterpolator.interpolate(path, context);
if (!Objects.equals(oldMissing, newMissing)) {
if (newProfiles == null) {
newProfiles = new ArrayList<>(profiles);
}
newProfiles.set(
index,
profile.withActivation(activation.withFile(file.withMissing(newMissing))));
}
} catch (InterpolationException e) { } catch (InterpolationException e) {
addInterpolationProblem(problems, file, oldMissing, e, "missing"); addInterpolationProblem(problems, target, path, e, locationKey);
} }
} }
return path;
} }
} }
} return profiles.stream().map(new ProfileInterpolator()).toList();
}
return newProfiles != null ? newProfiles : profiles;
} }
private static void addInterpolationProblem( private static void addInterpolationProblem(
DefaultModelProblemCollector problems, DefaultModelProblemCollector problems,
org.apache.maven.api.model.ActivationFile file, org.apache.maven.api.model.InputLocationTracker target,
String path, String path,
InterpolationException e, InterpolationException e,
String locationKey) { String locationKey) {
problems.add(new ModelProblemCollectorRequest(Severity.ERROR, ModelProblem.Version.BASE) problems.add(new ModelProblemCollectorRequest(Severity.ERROR, ModelProblem.Version.BASE)
.setMessage("Failed to interpolate file location " + path + ": " + e.getMessage()) .setMessage("Failed to interpolate file location " + path + ": " + e.getMessage())
.setLocation(Optional.ofNullable(file.getLocation(locationKey)) .setLocation(Optional.ofNullable(target.getLocation(locationKey))
.map(InputLocation::new) .map(InputLocation::new)
.orElse(null)) .orElse(null))
.setException(e)); .setException(e));
} }
private String interpolate(String path, ProfileActivationContext context) throws InterpolationException {
return isNotEmpty(path) ? profileActivationFilePathInterpolator.interpolate(path, context) : path;
}
private static boolean isNotEmpty(String string) { private static boolean isNotEmpty(String string) {
return string != null && !string.isEmpty(); return string != null && !string.isEmpty();
} }

View File

@ -25,17 +25,28 @@ import javax.inject.Singleton;
import java.io.File; import java.io.File;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
import java.util.Deque;
import java.util.HashMap; import java.util.HashMap;
import java.util.HashSet; import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Objects; import java.util.Objects;
import java.util.Optional;
import java.util.Set; import java.util.Set;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.function.UnaryOperator;
import java.util.regex.Matcher; import java.util.regex.Matcher;
import java.util.regex.Pattern; import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.StreamSupport;
import org.apache.maven.api.model.Activation; import org.apache.maven.api.model.Activation;
import org.apache.maven.api.model.ActivationFile; import org.apache.maven.api.model.ActivationFile;
import org.apache.maven.api.model.ActivationOS;
import org.apache.maven.api.model.ActivationProperty;
import org.apache.maven.api.model.Build; import org.apache.maven.api.model.Build;
import org.apache.maven.api.model.BuildBase; import org.apache.maven.api.model.BuildBase;
import org.apache.maven.api.model.Dependency; import org.apache.maven.api.model.Dependency;
@ -61,6 +72,7 @@ import org.apache.maven.model.building.ModelProblemCollector;
import org.apache.maven.model.building.ModelProblemCollectorRequest; import org.apache.maven.model.building.ModelProblemCollectorRequest;
import org.apache.maven.model.interpolation.ModelVersionProcessor; import org.apache.maven.model.interpolation.ModelVersionProcessor;
import org.apache.maven.model.v4.MavenModelVersion; import org.apache.maven.model.v4.MavenModelVersion;
import org.apache.maven.model.v4.MavenTransformer;
/** /**
*/ */
@ -82,6 +94,196 @@ public class DefaultModelValidator implements ModelValidator {
private static final String EMPTY = ""; private static final String EMPTY = "";
private record ActivationFrame(String location, Optional<? extends InputLocationTracker> parent) {}
private static class ActivationWalker extends MavenTransformer {
private final Deque<ActivationFrame> stk;
ActivationWalker(Deque<ActivationFrame> stk, UnaryOperator<String> transformer) {
super(transformer);
this.stk = stk;
}
private ActivationFrame nextFrame(String property) {
return new ActivationFrame(property, Optional.empty());
}
private <P> ActivationFrame nextFrame(String property, Function<P, InputLocationTracker> child) {
@SuppressWarnings("unchecked")
final Optional<P> parent = (Optional<P>) stk.peek().parent;
return new ActivationFrame(property, parent.map(child));
}
@Override
public Activation transformActivation(Activation target) {
stk.push(new ActivationFrame("activation", Optional.of(target)));
try {
return super.transformActivation(target);
} finally {
stk.pop();
}
}
@Override
protected Activation.Builder transformActivation_ActiveByDefault(
Supplier<? extends Activation.Builder> creator, Activation.Builder builder, Activation target) {
return builder;
}
@Override
protected Activation.Builder transformActivation_File(
Supplier<? extends Activation.Builder> creator, Activation.Builder builder, Activation target) {
stk.push(nextFrame("file", Activation::getFile));
Optional.ofNullable(target.getFile());
try {
return super.transformActivation_File(creator, builder, target);
} finally {
stk.pop();
}
}
@Override
protected ActivationFile.Builder transformActivationFile_Exists(
Supplier<? extends ActivationFile.Builder> creator,
ActivationFile.Builder builder,
ActivationFile target) {
stk.push(nextFrame("exists"));
try {
return super.transformActivationFile_Exists(creator, builder, target);
} finally {
stk.pop();
}
}
@Override
protected ActivationFile.Builder transformActivationFile_Missing(
Supplier<? extends ActivationFile.Builder> creator,
ActivationFile.Builder builder,
ActivationFile target) {
stk.push(nextFrame("missing"));
try {
return super.transformActivationFile_Missing(creator, builder, target);
} finally {
stk.pop();
}
}
@Override
protected Activation.Builder transformActivation_Jdk(
Supplier<? extends Activation.Builder> creator, Activation.Builder builder, Activation target) {
stk.push(nextFrame("jdk"));
try {
return super.transformActivation_Jdk(creator, builder, target);
} finally {
stk.pop();
}
}
@Override
protected Activation.Builder transformActivation_Os(
Supplier<? extends Activation.Builder> creator, Activation.Builder builder, Activation target) {
stk.push(nextFrame("os", Activation::getOs));
try {
return super.transformActivation_Os(creator, builder, target);
} finally {
stk.pop();
}
}
@Override
protected ActivationOS.Builder transformActivationOS_Arch(
Supplier<? extends ActivationOS.Builder> creator, ActivationOS.Builder builder, ActivationOS target) {
stk.push(nextFrame("arch"));
try {
return super.transformActivationOS_Arch(creator, builder, target);
} finally {
stk.pop();
}
}
@Override
protected ActivationOS.Builder transformActivationOS_Family(
Supplier<? extends ActivationOS.Builder> creator, ActivationOS.Builder builder, ActivationOS target) {
stk.push(nextFrame("family"));
try {
return super.transformActivationOS_Family(creator, builder, target);
} finally {
stk.pop();
}
}
@Override
protected ActivationOS.Builder transformActivationOS_Name(
Supplier<? extends ActivationOS.Builder> creator, ActivationOS.Builder builder, ActivationOS target) {
stk.push(nextFrame("name"));
try {
return super.transformActivationOS_Name(creator, builder, target);
} finally {
stk.pop();
}
}
@Override
protected ActivationOS.Builder transformActivationOS_Version(
Supplier<? extends ActivationOS.Builder> creator, ActivationOS.Builder builder, ActivationOS target) {
stk.push(nextFrame("version"));
try {
return super.transformActivationOS_Version(creator, builder, target);
} finally {
stk.pop();
}
}
@Override
protected Activation.Builder transformActivation_Packaging(
Supplier<? extends Activation.Builder> creator, Activation.Builder builder, Activation target) {
stk.push(nextFrame("packaging"));
try {
return super.transformActivation_Packaging(creator, builder, target);
} finally {
stk.pop();
}
}
@Override
protected Activation.Builder transformActivation_Property(
Supplier<? extends Activation.Builder> creator, Activation.Builder builder, Activation target) {
stk.push(nextFrame("property", Activation::getProperty));
try {
return super.transformActivation_Property(creator, builder, target);
} finally {
stk.pop();
}
}
@Override
protected ActivationProperty.Builder transformActivationProperty_Name(
Supplier<? extends ActivationProperty.Builder> creator,
ActivationProperty.Builder builder,
ActivationProperty target) {
stk.push(nextFrame("name"));
try {
return super.transformActivationProperty_Name(creator, builder, target);
} finally {
stk.pop();
}
}
@Override
protected ActivationProperty.Builder transformActivationProperty_Value(
Supplier<? extends ActivationProperty.Builder> creator,
ActivationProperty.Builder builder,
ActivationProperty target) {
stk.push(nextFrame("value"));
try {
return super.transformActivationProperty_Value(creator, builder, target);
} finally {
stk.pop();
}
}
}
private final Set<String> validCoordinateIds = new HashSet<>(); private final Set<String> validCoordinateIds = new HashSet<>();
private final Set<String> validProfileIds = new HashSet<>(); private final Set<String> validProfileIds = new HashSet<>();
@ -288,42 +490,52 @@ public class DefaultModelValidator implements ModelValidator {
} }
private void validate30RawProfileActivation(ModelProblemCollector problems, Activation activation, String prefix) { private void validate30RawProfileActivation(ModelProblemCollector problems, Activation activation, String prefix) {
if (activation == null || activation.getFile() == null) { if (activation == null) {
return; return;
} }
final Deque<ActivationFrame> stk = new LinkedList<>();
ActivationFile file = activation.getFile(); final Supplier<String> pathSupplier = () -> {
final boolean parallel = false;
String path; return StreamSupport.stream(((Iterable<ActivationFrame>) stk::descendingIterator).spliterator(), parallel)
String location; .map(ActivationFrame::location)
.collect(Collectors.joining("."));
if (file.getExists() != null && !file.getExists().isEmpty()) { };
path = file.getExists(); final Supplier<InputLocation> locationSupplier = () -> {
location = "exists"; if (stk.size() < 2) {
} else if (file.getMissing() != null && !file.getMissing().isEmpty()) { return null;
path = file.getMissing();
location = "missing";
} else {
return;
} }
Iterator<ActivationFrame> f = stk.iterator();
if (hasProjectExpression(path)) { String location = f.next().location;
Matcher matcher = EXPRESSION_PROJECT_NAME_PATTERN.matcher(path); ActivationFrame parent = f.next();
return parent.parent.map(p -> p.getLocation(location)).orElse(null);
};
final UnaryOperator<String> transformer = s -> {
if (hasProjectExpression(s)) {
String path = pathSupplier.get();
Matcher matcher = EXPRESSION_PROJECT_NAME_PATTERN.matcher(s);
while (matcher.find()) { while (matcher.find()) {
String propertyName = matcher.group(0); String propertyName = matcher.group(0);
if (!"${project.basedir}".equals(propertyName)) {
if (path.startsWith("activation.file.") && "${project.basedir}".equals(propertyName)) {
continue;
}
addViolation( addViolation(
problems, problems,
Severity.WARNING, Severity.WARNING,
Version.V30, Version.V30,
prefix + "activation.file." + location, prefix + path,
null, null,
"Failed to interpolate file location " + path + ": " + propertyName "Failed to interpolate profile activation property " + s + ": " + propertyName
+ " expressions are not supported during profile activation.", + " expressions are not supported during profile activation.",
file.getLocation(location)); locationSupplier.get());
}
} }
} }
return s;
};
new ActivationWalker(stk, transformer).transformActivation(activation);
} }
private void validate20RawPlugins( private void validate20RawPlugins(

View File

@ -20,6 +20,8 @@ package org.apache.maven.model.validation;
import java.io.InputStream; import java.io.InputStream;
import java.util.List; import java.util.List;
import java.util.Properties;
import java.util.function.UnaryOperator;
import org.apache.maven.model.Model; import org.apache.maven.model.Model;
import org.apache.maven.model.building.DefaultModelBuildingRequest; import org.apache.maven.model.building.DefaultModelBuildingRequest;
@ -50,32 +52,40 @@ class DefaultModelValidatorTest {
} }
private SimpleProblemCollector validate(String pom) throws Exception { private SimpleProblemCollector validate(String pom) throws Exception {
return validateEffective(pom, ModelBuildingRequest.VALIDATION_LEVEL_STRICT); return validateEffective(pom, UnaryOperator.identity());
} }
private SimpleProblemCollector validateRaw(String pom) throws Exception { private SimpleProblemCollector validateRaw(String pom) throws Exception {
return validateRaw(pom, ModelBuildingRequest.VALIDATION_LEVEL_STRICT); return validateRaw(pom, UnaryOperator.identity());
} }
private SimpleProblemCollector validateEffective(String pom, int level) throws Exception { private SimpleProblemCollector validateEffective(String pom, int level) throws Exception {
ModelBuildingRequest request = new DefaultModelBuildingRequest().setValidationLevel(level); return validateEffective(pom, mbr -> mbr.setValidationLevel(level));
}
private SimpleProblemCollector validateEffective(String pom, UnaryOperator<ModelBuildingRequest> requestConfigurer)
throws Exception {
Model model = read(pom); Model model = read(pom);
SimpleProblemCollector problems = new SimpleProblemCollector(model); SimpleProblemCollector problems = new SimpleProblemCollector(model);
validator.validateEffectiveModel(model, request, problems); validator.validateEffectiveModel(model, requestConfigurer.apply(new DefaultModelBuildingRequest()), problems);
return problems; return problems;
} }
private SimpleProblemCollector validateRaw(String pom, int level) throws Exception { private SimpleProblemCollector validateRaw(String pom, int level) throws Exception {
ModelBuildingRequest request = new DefaultModelBuildingRequest().setValidationLevel(level); return validateRaw(pom, mbr -> mbr.setValidationLevel(level));
}
private SimpleProblemCollector validateRaw(String pom, UnaryOperator<ModelBuildingRequest> requestConfigurer)
throws Exception {
Model model = read(pom); Model model = read(pom);
SimpleProblemCollector problems = new SimpleProblemCollector(model); SimpleProblemCollector problems = new SimpleProblemCollector(model);
ModelBuildingRequest request = requestConfigurer.apply(new DefaultModelBuildingRequest());
validator.validateFileModel(model, request, problems); validator.validateFileModel(model, request, problems);
validator.validateRawModel(model, request, problems); validator.validateRawModel(model, request, problems);
@ -818,24 +828,52 @@ class DefaultModelValidatorTest {
@Test @Test
void profileActivationWithAllowedExpression() throws Exception { void profileActivationWithAllowedExpression() throws Exception {
SimpleProblemCollector result = validateRaw("raw-model/profile-activation-file-with-allowed-expressions.xml"); SimpleProblemCollector result = validateRaw(
"raw-model/profile-activation-file-with-allowed-expressions.xml",
mbr -> mbr.setUserProperties(new Properties() {
private static final long serialVersionUID = 1L;
{
setProperty("foo", "foo");
setProperty("bar", "foo");
}
}));
assertViolations(result, 0, 0, 0); assertViolations(result, 0, 0, 0);
} }
@Test @Test
void profileActivationWithProjectExpression() throws Exception { void profileActivationFileWithProjectExpression() throws Exception {
SimpleProblemCollector result = validateRaw("raw-model/profile-activation-file-with-project-expressions.xml"); SimpleProblemCollector result = validateRaw("raw-model/profile-activation-file-with-project-expressions.xml");
assertViolations(result, 0, 0, 2); assertViolations(result, 0, 0, 2);
assertEquals( assertEquals(
"'profiles.profile[exists-project-version].activation.file.exists' " "'profiles.profile[exists-project-version].activation.file.exists' "
+ "Failed to interpolate file location ${project.version}/test.txt: " + "Failed to interpolate profile activation property ${project.version}/test.txt: "
+ "${project.version} expressions are not supported during profile activation.", + "${project.version} expressions are not supported during profile activation.",
result.getWarnings().get(0)); result.getWarnings().get(0));
assertEquals( assertEquals(
"'profiles.profile[missing-project-version].activation.file.missing' " "'profiles.profile[missing-project-version].activation.file.missing' "
+ "Failed to interpolate file location ${project.version}/test.txt: " + "Failed to interpolate profile activation property ${project.version}/test.txt: "
+ "${project.version} expressions are not supported during profile activation.",
result.getWarnings().get(1));
}
@Test
void profileActivationPropertyWithProjectExpression() throws Exception {
SimpleProblemCollector result =
validateRaw("raw-model/profile-activation-property-with-project-expressions.xml");
assertViolations(result, 0, 0, 2);
assertEquals(
"'profiles.profile[property-name-project-version].activation.property.name' "
+ "Failed to interpolate profile activation property ${project.version}: "
+ "${project.version} expressions are not supported during profile activation.",
result.getWarnings().get(0));
assertEquals(
"'profiles.profile[property-value-project-version].activation.property.value' "
+ "Failed to interpolate profile activation property ${project.version}: "
+ "${project.version} expressions are not supported during profile activation.", + "${project.version} expressions are not supported during profile activation.",
result.getWarnings().get(1)); result.getWarnings().get(1));
} }

View File

@ -60,5 +60,23 @@ under the License.
</activation> </activation>
</profile> </profile>
<profile>
<id>dynamic-property-available</id>
<activation>
<property>
<name>${activationProperty}</name>
</property>
</activation>
</profile>
<profile>
<id>matches-another-property</id>
<activation>
<property>
<name>foo</name>
<value>${bar}</value>
</property>
</activation>
</profile>
</profiles> </profiles>
</project> </project>

View File

@ -0,0 +1,49 @@
<!--
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" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<artifactId>aid</artifactId>
<groupId>gid</groupId>
<version>0.1</version>
<packaging>pom</packaging>
<profiles>
<profile>
<id>property-name-project-version</id>
<activation>
<property>
<name>${project.version}</name>
</property>
</activation>
</profile>
<profile>
<id>property-value-project-version</id>
<activation>
<property>
<name>project.version</name>
<value>${project.version}</value>
</property>
</activation>
</profile>
</profiles>
</project>