[MNG-8286] Add a condition profile based on a simple expressions (#1771)

This commit is contained in:
Guillaume Nodet 2024-11-20 21:36:48 +01:00 committed by GitHub
parent fcd9c0f018
commit 986f59683e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
24 changed files with 2957 additions and 1173 deletions

View File

@ -2742,10 +2742,79 @@
<class> <class>
<name>Activation</name> <name>Activation</name>
<version>4.0.0+</version> <version>4.0.0+</version>
<description>The conditions within the build runtime environment which will trigger the <description><![CDATA[
The conditions within the build runtime environment which will trigger the
automatic inclusion of the build profile. Multiple conditions can be defined, which must automatic inclusion of the build profile. Multiple conditions can be defined, which must
be all satisfied to activate the profile. be all satisfied to activate the profile.
</description>
<p>In addition to the traditional activation mechanisms (JDK version, OS properties,
file existence, etc.), Maven now supports a powerful condition-based activation
through the {@code condition} field. This new mechanism allows for more flexible
and expressive profile activation rules.</p>
<h2>Condition Syntax</h2>
<p>The condition is specified as a string expression that can include various
functions, comparisons, and logical operators. Some key features include:</p>
<ul>
<li>Property access: {@code ${property.name}}</li>
<li>Comparison operators: {@code ==}, {@code !=}, {@code <}, {@code >}, {@code <=}, {@code >=}</li>
<li>Logical operators: {@code &&} (AND), {@code ||} (OR), {@code not(...)}</li>
<li>Functions: {@code exists(...)}, {@code missing(...)}, {@code matches(...)}, {@code inrange(...)}, and more</li>
</ul>
<h2>Supported Functions</h2>
<p>The following functions are supported in condition expressions:</p>
<ul>
<li>{@code length(string)}: Returns the length of the given string.</li>
<li>{@code upper(string)}: Converts the string to uppercase.</li>
<li>{@code lower(string)}: Converts the string to lowercase.</li>
<li>{@code substring(string, start, [end])}: Returns a substring of the given string.</li>
<li>{@code indexOf(string, substring)}: Returns the index of the first occurrence of substring in string, or -1 if not found.</li>
<li>{@code contains(string, substring)}: Checks if the string contains the substring.</li>
<li>{@code matches(string, regex)}: Checks if the string matches the given regular expression.</li>
<li>{@code not(condition)}: Negates the given condition.</li>
<li>{@code if(condition, trueValue, falseValue)}: Returns trueValue if the condition is true, falseValue otherwise.</li>
<li>{@code exists(path)}: Checks if a file matching the given glob pattern exists.</li>
<li>{@code missing(path)}: Checks if a file matching the given glob pattern does not exist.</li>
<li>{@code inrange(version, range)}: Checks if the given version is within the specified version range.</li>
</ul>
<h2>Supported properties</h2>
<p>The following properties are supported in expressions:</p>
<ul>
<li>`project.basedir`: The project directory</li>
<li>`project.rootDirectory`: The root directory of the project</li>
<li>`project.artifactId`: The artifactId of the project</li>
<li>`project.packaging`: The packaging of the project</li>
<li>user properties</li>
<li>system properties (including environment variables prefixed with `env.`)</li>
</ul>
<h2>Examples</h2>
<ul>
<li>JDK version range: {@code inrange(${java.version}, '[11,)')} (JDK 11 or higher)</li>
<li>OS check: {@code ${os.name} == 'windows'}</li>
<li>File existence: {@code exists('${project.basedir}/src/**}{@code /*.xsd')}</li>
<li>Property check: {@code ${my.property} != 'some-value'}</li>
<li>Regex matching: {@code matches(${os.version}, '.*aws')}</li>
<li>Complex condition: {@code ${os.name} == 'windows' && ${os.arch} != 'amd64' && inrange(${os.version}, '[10,)')}</li>
<li>String length check: {@code length(${user.name}) > 5}</li>
<li>Substring with version: {@code substring(${java.version}, 0, 3) == '1.8'}</li>
<li>Using indexOf: {@code indexOf(${java.version}, '-') > 0}</li>
<li>Conditional logic: {@code if(contains(${java.version}, '-'), substring(${java.version}, 0, indexOf(${java.version}, '-')), ${java.version})}</li>
</ul>
<p>This flexible condition mechanism allows for more precise control over profile
activation, enabling developers to create profiles that respond to a wide range of
environmental factors and project states.</p>
]]></description>
<fields> <fields>
<field> <field>
<name>activeByDefault</name> <name>activeByDefault</name>
@ -2798,6 +2867,12 @@
<type>String</type> <type>String</type>
<description>Specifies that this profile will be activated based on the project's packaging.</description> <description>Specifies that this profile will be activated based on the project's packaging.</description>
</field> </field>
<field>
<name>condition</name>
<version>4.1.0+</version>
<type>String</type>
<description>The condition which must be satisfied to activate the profile.</description>
</field>
<!-- <!--
This could be included once we teach Maven to deal with multiple versions of the model This could be included once we teach Maven to deal with multiple versions of the model
<field> <field>

View File

@ -744,6 +744,14 @@
Specifies that this profile will be activated based on the project's packaging. Specifies that this profile will be activated based on the project's packaging.
</description> </description>
</field> </field>
<field>
<name>condition</name>
<version>2.0.0+</version>
<type>String</type>
<description>
The condition which must be satisfied to activate the profile.
</description>
</field>
</fields> </fields>
</class> </class>

View File

@ -18,27 +18,27 @@
*/ */
package org.apache.maven.api.services.model; package org.apache.maven.api.services.model;
import java.nio.file.Path;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import org.apache.maven.api.annotations.Nonnull;
import org.apache.maven.api.model.Model;
/** /**
* Describes the environmental context used to determine the activation status of profiles. * Describes the environmental context used to determine the activation status of profiles.
* *
* The {@link Model} is available from the activation context, but only static parts of it
* are allowed to be used, i.e. those that do not change between file model and effective model.
*
*/ */
public interface ProfileActivationContext { public interface ProfileActivationContext {
/**
* Key of the property containing the project's packaging.
* Available in {@link #getUserProperties()}.
* @since 4.0.0
*/
String PROPERTY_NAME_PACKAGING = "packaging";
/** /**
* Gets the identifiers of those profiles that should be activated by explicit demand. * Gets the identifiers of those profiles that should be activated by explicit demand.
* *
* @return The identifiers of those profiles to activate, never {@code null}. * @return The identifiers of those profiles to activate, never {@code null}.
*/ */
@Nonnull
List<String> getActiveProfileIds(); List<String> getActiveProfileIds();
/** /**
@ -46,6 +46,7 @@ public interface ProfileActivationContext {
* *
* @return The identifiers of those profiles to deactivate, never {@code null}. * @return The identifiers of those profiles to deactivate, never {@code null}.
*/ */
@Nonnull
List<String> getInactiveProfileIds(); List<String> getInactiveProfileIds();
/** /**
@ -54,6 +55,7 @@ public interface ProfileActivationContext {
* *
* @return The execution properties, never {@code null}. * @return The execution properties, never {@code null}.
*/ */
@Nonnull
Map<String, String> getSystemProperties(); Map<String, String> getSystemProperties();
/** /**
@ -63,19 +65,14 @@ public interface ProfileActivationContext {
* *
* @return The user properties, never {@code null}. * @return The user properties, never {@code null}.
*/ */
@Nonnull
Map<String, String> getUserProperties(); Map<String, String> getUserProperties();
/** /**
* Gets the base directory of the current project (if any). * Gets the model which is being activated.
* *
* @return The base directory of the current project or {@code null} if none. * @return The project model, never {@code null}.
*/ */
Path getProjectDirectory(); @Nonnull
Model getModel();
/**
* Gets current calculated project properties
*
* @return The project properties, never {@code null}.
*/
Map<String, String> getProjectProperties();
} }

View File

@ -30,6 +30,7 @@ import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.function.Function; import java.util.function.Function;
import java.util.function.Supplier;
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;
@ -43,6 +44,7 @@ import org.apache.maven.api.services.Source;
import org.apache.maven.api.services.xml.SettingsXmlFactory; import org.apache.maven.api.services.xml.SettingsXmlFactory;
import org.apache.maven.api.services.xml.XmlReaderException; import org.apache.maven.api.services.xml.XmlReaderException;
import org.apache.maven.api.services.xml.XmlReaderRequest; import org.apache.maven.api.services.xml.XmlReaderRequest;
import org.apache.maven.api.settings.Activation;
import org.apache.maven.api.settings.Profile; import org.apache.maven.api.settings.Profile;
import org.apache.maven.api.settings.Repository; import org.apache.maven.api.settings.Repository;
import org.apache.maven.api.settings.RepositoryPolicy; import org.apache.maven.api.settings.RepositoryPolicy;
@ -245,10 +247,23 @@ public class DefaultSettingsBuilder implements SettingsBuilder {
Map<String, String> systemProperties = request.getSession().getSystemProperties(); Map<String, String> systemProperties = request.getSession().getSystemProperties();
src = Interpolator.chain(userProperties::get, systemProperties::get); src = Interpolator.chain(userProperties::get, systemProperties::get);
} }
return new SettingsTransformer(value -> value != null ? interpolator.interpolate(value, src) : null) return new DefSettingsTransformer(value -> value != null ? interpolator.interpolate(value, src) : null)
.visit(settings); .visit(settings);
} }
static class DefSettingsTransformer extends SettingsTransformer {
DefSettingsTransformer(Function<String, String> transformer) {
super(transformer);
}
@Override
protected Activation.Builder transformActivation_Condition(
Supplier<? extends Activation.Builder> creator, Activation.Builder builder, Activation target) {
// do not interpolate the condition activation
return builder;
}
}
private Settings decrypt( private Settings decrypt(
Source settingsSource, Settings settings, SettingsBuilderRequest request, List<BuilderProblem> problems) { Source settingsSource, Settings settings, SettingsBuilderRequest request, List<BuilderProblem> problems) {
if (secDispatcher == null) { if (secDispatcher == null) {

View File

@ -127,6 +127,8 @@ public final class SettingsUtilsV4 {
activation.packaging(modelActivation.getPackaging()); activation.packaging(modelActivation.getPackaging());
activation.condition(modelActivation.getCondition());
profile.activation(activation.build()); profile.activation(activation.build());
} }
@ -212,6 +214,8 @@ public final class SettingsUtilsV4 {
activation.packaging(settingsActivation.getPackaging()); activation.packaging(settingsActivation.getPackaging());
activation.condition(settingsActivation.getCondition());
profile.activation(activation.build()); profile.activation(activation.build());
} }

View File

@ -102,7 +102,6 @@ import org.apache.maven.api.services.model.ModelValidator;
import org.apache.maven.api.services.model.ModelVersionParser; import org.apache.maven.api.services.model.ModelVersionParser;
import org.apache.maven.api.services.model.PluginConfigurationExpander; import org.apache.maven.api.services.model.PluginConfigurationExpander;
import org.apache.maven.api.services.model.PluginManagementInjector; import org.apache.maven.api.services.model.PluginManagementInjector;
import org.apache.maven.api.services.model.ProfileActivationContext;
import org.apache.maven.api.services.model.ProfileInjector; import org.apache.maven.api.services.model.ProfileInjector;
import org.apache.maven.api.services.model.ProfileSelector; import org.apache.maven.api.services.model.ProfileSelector;
import org.apache.maven.api.services.model.RootLocator; import org.apache.maven.api.services.model.RootLocator;
@ -1125,7 +1124,7 @@ public class DefaultModelBuilder implements ModelBuilder {
profileActivationContext.setUserProperties(profileProps); profileActivationContext.setUserProperties(profileProps);
} }
profileActivationContext.setProjectProperties(inputModel.getProperties()); profileActivationContext.setModel(inputModel);
setSource(inputModel); setSource(inputModel);
List<Profile> activePomProfiles = getActiveProfiles(inputModel.getProfiles(), profileActivationContext); List<Profile> activePomProfiles = getActiveProfiles(inputModel.getProfiles(), profileActivationContext);
@ -1203,7 +1202,7 @@ public class DefaultModelBuilder implements ModelBuilder {
model = modelNormalizer.mergeDuplicates(model, request, this); model = modelNormalizer.mergeDuplicates(model, request, this);
// profile activation // profile activation
profileActivationContext.setProjectProperties(model.getProperties()); profileActivationContext.setModel(model);
List<Profile> interpolatedProfiles = List<Profile> interpolatedProfiles =
interpolateActivations(model.getProfiles(), profileActivationContext, this); interpolateActivations(model.getProfiles(), profileActivationContext, this);
@ -1782,6 +1781,13 @@ public class DefaultModelBuilder implements ModelBuilder {
.build(); .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 @Override
protected ActivationFile.Builder transformActivationFile_Missing( protected ActivationFile.Builder transformActivationFile_Missing(
Supplier<? extends ActivationFile.Builder> creator, Supplier<? extends ActivationFile.Builder> creator,
@ -1860,13 +1866,8 @@ public class DefaultModelBuilder implements ModelBuilder {
context.setActiveProfileIds(request.getActiveProfileIds()); context.setActiveProfileIds(request.getActiveProfileIds());
context.setInactiveProfileIds(request.getInactiveProfileIds()); context.setInactiveProfileIds(request.getInactiveProfileIds());
context.setSystemProperties(request.getSystemProperties()); context.setSystemProperties(request.getSystemProperties());
// enrich user properties with project packaging context.setUserProperties(request.getUserProperties());
Map<String, String> userProperties = new HashMap<>(request.getUserProperties()); context.setModel(model);
if (!userProperties.containsKey(ProfileActivationContext.PROPERTY_NAME_PACKAGING)) {
userProperties.put(ProfileActivationContext.PROPERTY_NAME_PACKAGING, model.getPackaging());
}
context.setUserProperties(userProperties);
context.setProjectDirectory(model.getProjectDirectory());
return context; return context;
} }

View File

@ -282,6 +282,17 @@ public class DefaultModelValidator implements ModelValidator {
stk.pop(); stk.pop();
} }
} }
@Override
protected Activation.Builder transformActivation_Condition(
Supplier<? extends Activation.Builder> creator, Activation.Builder builder, Activation target) {
stk.push(nextFrame("condition"));
try {
return super.transformActivation_Condition(creator, builder, target);
} finally {
stk.pop();
}
}
} }
private final Set<String> validCoordinatesIds = ConcurrentHashMap.newKeySet(); private final Set<String> validCoordinatesIds = ConcurrentHashMap.newKeySet();
@ -470,6 +481,8 @@ public class DefaultModelValidator implements ModelValidator {
} }
} }
validateStringNoExpression("packaging", problems, Severity.WARNING, Version.V20, m.getPackaging(), m);
validate20RawDependencies( validate20RawDependencies(
problems, problems,
m.getDependencies(), m.getDependencies(),

View File

@ -18,13 +18,13 @@
*/ */
package org.apache.maven.internal.impl.model; package org.apache.maven.internal.impl.model;
import java.nio.file.Path;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Properties; import java.util.Properties;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import org.apache.maven.api.model.Model;
import org.apache.maven.api.services.model.ProfileActivationContext; import org.apache.maven.api.services.model.ProfileActivationContext;
/** /**
@ -41,9 +41,7 @@ public class DefaultProfileActivationContext implements ProfileActivationContext
private Map<String, String> userProperties = Collections.emptyMap(); private Map<String, String> userProperties = Collections.emptyMap();
private Map<String, String> projectProperties = Collections.emptyMap(); private Model model;
private Path projectDirectory;
@Override @Override
public List<String> getActiveProfileIds() { public List<String> getActiveProfileIds() {
@ -138,34 +136,12 @@ public class DefaultProfileActivationContext implements ProfileActivationContext
} }
@Override @Override
public Path getProjectDirectory() { public Model getModel() {
return projectDirectory; return model;
} }
/** public DefaultProfileActivationContext setModel(Model model) {
* Sets the base directory of the current project. this.model = model;
*
* @param projectDirectory The base directory of the current project, may be {@code null} if profile activation
* happens in the context of metadata retrieval rather than project building.
* @return This context, never {@code null}.
*/
public DefaultProfileActivationContext setProjectDirectory(Path projectDirectory) {
this.projectDirectory = projectDirectory;
return this;
}
@Override
public Map<String, String> getProjectProperties() {
return projectProperties;
}
public DefaultProfileActivationContext setProjectProperties(Properties projectProperties) {
return setProjectProperties(toMap(projectProperties));
}
public DefaultProfileActivationContext setProjectProperties(Map<String, String> projectProperties) {
this.projectProperties = unmodifiable(projectProperties);
return this; return this;
} }

View File

@ -61,7 +61,7 @@ public class ProfileActivationFilePathInterpolator {
return null; return null;
} }
Path basedir = context.getProjectDirectory(); Path basedir = context.getModel().getProjectDirectory();
String absolutePath = interpolator.interpolate(path, s -> { String absolutePath = interpolator.interpolate(path, s -> {
if ("basedir".equals(s) || "project.basedir".equals(s)) { if ("basedir".equals(s) || "project.basedir".equals(s)) {
@ -71,7 +71,7 @@ public class ProfileActivationFilePathInterpolator {
Path root = rootLocator.findMandatoryRoot(basedir); Path root = rootLocator.findMandatoryRoot(basedir);
return root.toFile().getAbsolutePath(); return root.toFile().getAbsolutePath();
} }
String r = context.getProjectProperties().get(s); String r = context.getModel().getProperties().get(s);
if (r == null) { if (r == null) {
r = context.getUserProperties().get(s); r = context.getUserProperties().get(s);
} }

View File

@ -0,0 +1,296 @@
/*
* 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.internal.impl.model.profile;
import java.io.File;
import java.io.IOException;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.PathMatcher;
import java.nio.file.Paths;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
import org.apache.maven.api.services.VersionParser;
import org.apache.maven.api.services.model.ProfileActivationContext;
import org.apache.maven.internal.impl.model.ProfileActivationFilePathInterpolator;
import static org.apache.maven.internal.impl.model.profile.ConditionParser.toInt;
/**
* Provides a set of functions for evaluating profile activation conditions.
* These functions can be used in profile activation expressions to determine
* whether a profile should be activated based on various criteria.
*/
@SuppressWarnings("unused")
public class ConditionFunctions {
private final ProfileActivationContext context;
private final VersionParser versionParser;
private final ProfileActivationFilePathInterpolator interpolator;
/**
* Constructs a new ConditionFunctions instance.
*
* @param context The profile activation context
* @param versionParser The version parser for comparing versions
* @param interpolator The interpolator for resolving file paths
*/
public ConditionFunctions(
ProfileActivationContext context,
VersionParser versionParser,
ProfileActivationFilePathInterpolator interpolator) {
this.context = context;
this.versionParser = versionParser;
this.interpolator = interpolator;
}
/**
* Returns the length of the given string.
*
* @param args A list containing a single string argument
* @return The length of the string
* @throws IllegalArgumentException if the number of arguments is not exactly one
*/
public Object length(List<Object> args) {
if (args.size() != 1) {
throw new IllegalArgumentException("length function requires exactly one argument");
}
String s = ConditionParser.toString(args.get(0));
return s.length();
}
/**
* Converts the given string to uppercase.
*
* @param args A list containing a single string argument
* @return The uppercase version of the input string
* @throws IllegalArgumentException if the number of arguments is not exactly one
*/
public Object upper(List<Object> args) {
if (args.size() != 1) {
throw new IllegalArgumentException("upper function requires exactly one argument");
}
String s = ConditionParser.toString(args.get(0));
return s.toUpperCase();
}
/**
* Converts the given string to lowercase.
*
* @param args A list containing a single string argument
* @return The lowercase version of the input string
* @throws IllegalArgumentException if the number of arguments is not exactly one
*/
public Object lower(List<Object> args) {
if (args.size() != 1) {
throw new IllegalArgumentException("lower function requires exactly one argument");
}
String s = ConditionParser.toString(args.get(0));
return s.toLowerCase();
}
/**
* Returns a substring of the given string.
*
* @param args A list containing 2 or 3 arguments: the string, start index, and optionally end index
* @return The substring
* @throws IllegalArgumentException if the number of arguments is not 2 or 3
*/
public Object substring(List<Object> args) {
if (args.size() < 2 || args.size() > 3) {
throw new IllegalArgumentException("substring function requires 2 or 3 arguments");
}
String s = ConditionParser.toString(args.get(0));
int start = toInt(args.get(1));
int end = args.size() == 3 ? toInt(args.get(2)) : s.length();
return s.substring(start, end);
}
/**
* Finds the index of a substring within a string.
*
* @param args A list containing two strings: the main string and the substring to find
* @return The index of the substring, or -1 if not found
* @throws IllegalArgumentException if the number of arguments is not exactly two
*/
public Object indexOf(List<Object> args) {
if (args.size() != 2) {
throw new IllegalArgumentException("indexOf function requires exactly two arguments");
}
String s = ConditionParser.toString(args.get(0));
String substring = ConditionParser.toString(args.get(1));
return s.indexOf(substring);
}
/**
* Checks if a string contains a given substring.
*
* @param args A list containing two strings: the main string and the substring to check
* @return true if the main string contains the substring, false otherwise
* @throws IllegalArgumentException if the number of arguments is not exactly two
*/
public Object contains(List<Object> args) {
if (args.size() != 2) {
throw new IllegalArgumentException("contains function requires exactly two arguments");
}
String s = ConditionParser.toString(args.get(0));
String substring = ConditionParser.toString(args.get(1));
return s.contains(substring);
}
/**
* Checks if a string matches a given regular expression.
*
* @param args A list containing two strings: the string to check and the regex pattern
* @return true if the string matches the regex, false otherwise
* @throws IllegalArgumentException if the number of arguments is not exactly two
*/
public Object matches(List<Object> args) {
if (args.size() != 2) {
throw new IllegalArgumentException("matches function requires exactly two arguments");
}
String s = ConditionParser.toString(args.get(0));
String regex = ConditionParser.toString(args.get(1));
return s.matches(regex);
}
/**
* Negates a boolean value.
*
* @param args A list containing a single boolean argument
* @return The negation of the input boolean
* @throws IllegalArgumentException if the number of arguments is not exactly one
*/
public Object not(List<Object> args) {
if (args.size() != 1) {
throw new IllegalArgumentException("not function requires exactly one argument");
}
return !ConditionParser.toBoolean(args.get(0));
}
/**
* Implements an if-then-else operation.
*
* @param args A list containing three arguments: condition, value if true, value if false
* @return The second argument if the condition is true, the third argument otherwise
* @throws IllegalArgumentException if the number of arguments is not exactly three
*/
@SuppressWarnings("checkstyle:MethodName")
public Object if_(List<Object> args) {
if (args.size() != 3) {
throw new IllegalArgumentException("if function requires exactly three arguments");
}
boolean condition = ConditionParser.toBoolean(args.get(0));
return condition ? args.get(1) : args.get(2);
}
/**
* Checks if a file or directory exists at the given path.
*
* @param args A list containing a single string argument representing the path
* @return true if the file or directory exists, false otherwise
* @throws IllegalArgumentException if the number of arguments is not exactly one
* @throws IOException if a problem occurs while walking the file system
*/
public Object exists(List<Object> args) throws IOException {
if (args.size() != 1) {
throw new IllegalArgumentException("exists function requires exactly one argument");
}
String path = ConditionParser.toString(args.get(0));
return fileExists(path);
}
/**
* Checks if a file or directory is missing at the given path.
*
* @param args A list containing a single string argument representing the path
* @return true if the file or directory does not exist, false otherwise
* @throws IllegalArgumentException if the number of arguments is not exactly one
* @throws IOException if a problem occurs while walking the file system
*/
public Object missing(List<Object> args) throws IOException {
if (args.size() != 1) {
throw new IllegalArgumentException("missing function requires exactly one argument");
}
String path = ConditionParser.toString(args.get(0));
return !fileExists(path);
}
private boolean fileExists(String path) throws IOException {
String pattern = interpolator.interpolate(path, context);
int asteriskIndex = pattern.indexOf('*');
int questionMarkIndex = pattern.indexOf('?');
int firstWildcardIndex = questionMarkIndex < 0
? asteriskIndex
: asteriskIndex < 0 ? questionMarkIndex : Math.min(asteriskIndex, questionMarkIndex);
String fixed, glob;
if (firstWildcardIndex < 0) {
fixed = pattern;
glob = "";
} else {
int lastSep = pattern.substring(0, firstWildcardIndex).lastIndexOf(File.separatorChar);
if (lastSep < 0) {
fixed = "";
glob = pattern;
} else {
fixed = pattern.substring(0, lastSep);
glob = pattern.substring(lastSep + 1);
}
}
Path fixedPath = Paths.get(fixed);
if (!Files.exists(fixedPath)) {
return false;
}
if (!glob.isEmpty()) {
PathMatcher matcher = fixedPath.getFileSystem().getPathMatcher("glob:" + glob);
AtomicBoolean found = new AtomicBoolean(false);
Files.walkFileTree(fixedPath, new SimpleFileVisitor<>() {
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {
if (found.get() || matcher.matches(fixedPath.relativize(file))) {
found.set(true);
return FileVisitResult.TERMINATE;
}
return FileVisitResult.CONTINUE;
}
});
return found.get();
}
return true;
}
/**
* Checks if a version is within a specified version range.
*
* @param args A list containing two strings: the version to check and the version range
* @return true if the version is within the range, false otherwise
* @throws IllegalArgumentException if the number of arguments is not exactly two
*/
public Object inrange(List<Object> args) {
if (args.size() != 2) {
throw new IllegalArgumentException("inrange function requires exactly two arguments");
}
String version = ConditionParser.toString(args.get(0));
String range = ConditionParser.toString(args.get(1));
return versionParser.parseVersionRange(range).contains(versionParser.parseVersion(version));
}
}

View File

@ -0,0 +1,636 @@
/*
* 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.internal.impl.model.profile;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
/**
* The {@code ConditionParser} class is responsible for parsing and evaluating expressions.
* It supports tokenizing the input expression and resolving custom functions passed in a map.
* This class implements a recursive descent parser to handle various operations including
* arithmetic, logical, and comparison operations, as well as function calls.
*/
public class ConditionParser {
/**
* A functional interface that represents an expression function to be applied
* to a list of arguments. Implementers can define custom functions.
*/
public interface ExpressionFunction {
/**
* Applies the function to the given list of arguments.
*
* @param args the list of arguments passed to the function
* @return the result of applying the function
*/
Object apply(List<Object> args);
}
private final Map<String, ExpressionFunction> functions; // Map to store functions by their names
private final Function<String, String> propertyResolver; // Property resolver
private List<String> tokens; // List of tokens derived from the expression
private int current; // Keeps track of the current token index
/**
* Constructs a new {@code ConditionParser} with the given function mappings.
*
* @param functions a map of function names to their corresponding {@code ExpressionFunction} implementations
* @param propertyResolver the property resolver
*/
public ConditionParser(Map<String, ExpressionFunction> functions, Function<String, String> propertyResolver) {
this.functions = functions;
this.propertyResolver = propertyResolver;
}
/**
* Parses the given expression and returns the result of the evaluation.
*
* @param expression the expression to be parsed
* @return the result of parsing and evaluating the expression
*/
public Object parse(String expression) {
this.tokens = tokenize(expression);
this.current = 0;
return parseExpression();
}
/**
* Tokenizes the input expression into a list of string tokens for further parsing.
* This method handles quoted strings, property aliases, and various operators.
*
* @param expression the expression to tokenize
* @return a list of tokens
*/
private List<String> tokenize(String expression) {
List<String> tokens = new ArrayList<>();
StringBuilder sb = new StringBuilder();
char quoteType = 0;
boolean inPropertyReference = false;
for (int i = 0; i < expression.length(); i++) {
char c = expression.charAt(i);
if (quoteType != 0) {
if (c == quoteType) {
quoteType = 0;
sb.append(c);
tokens.add(sb.toString());
sb.setLength(0);
} else {
sb.append(c);
}
continue;
}
if (inPropertyReference) {
if (c == '}') {
inPropertyReference = false;
tokens.add("${" + sb + "}");
sb.setLength(0);
} else {
sb.append(c);
}
continue;
}
if (c == '$' && i + 1 < expression.length() && expression.charAt(i + 1) == '{') {
if (!sb.isEmpty()) {
tokens.add(sb.toString());
sb.setLength(0);
}
inPropertyReference = true;
i++; // Skip the '{'
continue;
}
if (c == '"' || c == '\'') {
if (!sb.isEmpty()) {
tokens.add(sb.toString());
sb.setLength(0);
}
quoteType = c;
sb.append(c);
} else if (c == ' ' || c == '(' || c == ')' || c == ',' || c == '+' || c == '>' || c == '<' || c == '='
|| c == '!') {
if (!sb.isEmpty()) {
tokens.add(sb.toString());
sb.setLength(0);
}
if (c != ' ') {
if ((c == '>' || c == '<' || c == '=' || c == '!')
&& i + 1 < expression.length()
&& expression.charAt(i + 1) == '=') {
tokens.add(c + "=");
i++; // Skip the next character
} else {
tokens.add(String.valueOf(c));
}
}
} else {
sb.append(c);
}
}
if (inPropertyReference) {
throw new RuntimeException("Unclosed property reference: ${");
}
if (!sb.isEmpty()) {
tokens.add(sb.toString());
}
return tokens;
}
/**
* Parses the next expression from the list of tokens.
*
* @return the parsed expression as an object
* @throws RuntimeException if there are unexpected tokens after the end of the expression
*/
private Object parseExpression() {
Object result = parseLogicalOr();
if (current < tokens.size()) {
throw new RuntimeException("Unexpected tokens after end of expression");
}
return result;
}
/**
* Parses logical OR operations.
*
* @return the result of parsing logical OR operations
*/
private Object parseLogicalOr() {
Object left = parseLogicalAnd();
while (current < tokens.size() && tokens.get(current).equals("||")) {
current++;
Object right = parseLogicalAnd();
left = (boolean) left || (boolean) right;
}
return left;
}
/**
* Parses logical AND operations.
*
* @return the result of parsing logical AND operations
*/
private Object parseLogicalAnd() {
Object left = parseComparison();
while (current < tokens.size() && tokens.get(current).equals("&&")) {
current++;
Object right = parseComparison();
left = (boolean) left && (boolean) right;
}
return left;
}
/**
* Parses comparison operations.
*
* @return the result of parsing comparison operations
*/
private Object parseComparison() {
Object left = parseAddSubtract();
while (current < tokens.size()
&& (tokens.get(current).equals(">")
|| tokens.get(current).equals("<")
|| tokens.get(current).equals(">=")
|| tokens.get(current).equals("<=")
|| tokens.get(current).equals("==")
|| tokens.get(current).equals("!="))) {
String operator = tokens.get(current);
current++;
Object right = parseAddSubtract();
left = compare(left, operator, right);
}
return left;
}
/**
* Parses addition and subtraction operations.
*
* @return the result of parsing addition and subtraction operations
*/
private Object parseAddSubtract() {
Object left = parseMultiplyDivide();
while (current < tokens.size()
&& (tokens.get(current).equals("+") || tokens.get(current).equals("-"))) {
String operator = tokens.get(current);
current++;
Object right = parseMultiplyDivide();
if (operator.equals("+")) {
left = add(left, right);
} else {
left = subtract(left, right);
}
}
return left;
}
/**
* Parses multiplication and division operations.
*
* @return the result of parsing multiplication and division operations
*/
private Object parseMultiplyDivide() {
Object left = parseUnary();
while (current < tokens.size()
&& (tokens.get(current).equals("*") || tokens.get(current).equals("/"))) {
String operator = tokens.get(current);
current++;
Object right = parseUnary();
if (operator.equals("*")) {
left = multiply(left, right);
} else {
left = divide(left, right);
}
}
return left;
}
/**
* Parses unary operations (negation).
*
* @return the result of parsing unary operations
*/
private Object parseUnary() {
if (current < tokens.size() && tokens.get(current).equals("-")) {
current++;
Object value = parseUnary();
return negate(value);
}
return parseTerm();
}
/**
* Parses individual terms (numbers, strings, booleans, parentheses, functions).
*
* @return the parsed term
* @throws RuntimeException if the expression ends unexpectedly or contains unknown tokens
*/
private Object parseTerm() {
if (current >= tokens.size()) {
throw new RuntimeException("Unexpected end of expression");
}
String token = tokens.get(current);
if (token.equals("(")) {
return parseParentheses();
} else if (functions.containsKey(token)) {
return parseFunction();
} else if ((token.startsWith("\"") && token.endsWith("\"")) || (token.startsWith("'") && token.endsWith("'"))) {
current++;
return token.length() > 1 ? token.substring(1, token.length() - 1) : "";
} else if (token.equalsIgnoreCase("true") || token.equalsIgnoreCase("false")) {
current++;
return Boolean.parseBoolean(token);
} else if (token.startsWith("${") && token.endsWith("}")) {
current++;
String propertyName = token.substring(2, token.length() - 1);
return propertyResolver.apply(propertyName);
} else {
try {
current++;
return Double.parseDouble(token);
} catch (NumberFormatException e) {
// If it's not a number, treat it as a variable or unknown function
return parseVariableOrUnknownFunction();
}
}
}
/**
* Parses a token that could be either a variable or an unknown function.
*
* @return the result of parsing a variable or unknown function
* @throws RuntimeException if an unknown function is encountered
*/
private Object parseVariableOrUnknownFunction() {
current--; // Move back to the token we couldn't parse as a number
String name = tokens.get(current);
current++;
// Check if it's followed by an opening parenthesis, indicating a function call
if (current < tokens.size() && tokens.get(current).equals("(")) {
// It's a function call, parse it as such
List<Object> args = parseArgumentList();
if (functions.containsKey(name)) {
return functions.get(name).apply(args);
} else {
throw new RuntimeException("Unknown function: " + name);
}
} else {
// It's a variable
// Here you might want to handle variables differently
// For now, we'll throw an exception
throw new RuntimeException("Unknown variable: " + name);
}
}
/**
* Parses a list of arguments for a function call.
*
* @return a list of parsed arguments
* @throws RuntimeException if there's a mismatch in parentheses
*/
private List<Object> parseArgumentList() {
List<Object> args = new ArrayList<>();
current++; // Skip the opening parenthesis
while (current < tokens.size() && !tokens.get(current).equals(")")) {
args.add(parseLogicalOr());
if (current < tokens.size() && tokens.get(current).equals(",")) {
current++;
}
}
if (current >= tokens.size() || !tokens.get(current).equals(")")) {
throw new RuntimeException("Mismatched parentheses: missing closing parenthesis in function call");
}
current++; // Skip the closing parenthesis
return args;
}
/**
* Parses a function call.
*
* @return the result of the function call
*/
private Object parseFunction() {
String functionName = tokens.get(current);
current++;
List<Object> args = parseArgumentList();
return functions.get(functionName).apply(args);
}
/**
* Parses an expression within parentheses.
*
* @return the result of parsing the expression within parentheses
* @throws RuntimeException if there's a mismatch in parentheses
*/
private Object parseParentheses() {
current++; // Skip the opening parenthesis
Object result = parseLogicalOr();
if (current >= tokens.size() || !tokens.get(current).equals(")")) {
throw new RuntimeException("Mismatched parentheses: missing closing parenthesis");
}
current++; // Skip the closing parenthesis
return result;
}
/**
* Adds two objects, handling string concatenation and numeric addition.
*
* @param left the left operand
* @param right the right operand
* @return the result of the addition
* @throws RuntimeException if the operands cannot be added
*/
private static Object add(Object left, Object right) {
if (left instanceof String || right instanceof String) {
return toString(left) + toString(right);
} else if (left instanceof Number && right instanceof Number) {
return ((Number) left).doubleValue() + ((Number) right).doubleValue();
} else {
throw new RuntimeException("Cannot add " + left + " and " + right);
}
}
/**
* Negates a numeric value.
*
* @param value the value to negate
* @return the negated value
* @throws RuntimeException if the value cannot be negated
*/
private Object negate(Object value) {
if (value instanceof Number) {
return -((Number) value).doubleValue();
}
throw new RuntimeException("Cannot negate non-numeric value: " + value);
}
/**
* Subtracts the right operand from the left operand.
*
* @param left the left operand
* @param right the right operand
* @return the result of the subtraction
* @throws RuntimeException if the operands cannot be subtracted
*/
private static Object subtract(Object left, Object right) {
if (left instanceof Number && right instanceof Number) {
return ((Number) left).doubleValue() - ((Number) right).doubleValue();
} else {
throw new RuntimeException("Cannot subtract " + right + " from " + left);
}
}
/**
* Multiplies two numeric operands.
*
* @param left the left operand
* @param right the right operand
* @return the result of the multiplication
* @throws RuntimeException if the operands cannot be multiplied
*/
private static Object multiply(Object left, Object right) {
if (left instanceof Number && right instanceof Number) {
return ((Number) left).doubleValue() * ((Number) right).doubleValue();
} else {
throw new RuntimeException("Cannot multiply " + left + " and " + right);
}
}
/**
* Divides the left operand by the right operand.
*
* @param left the left operand (dividend)
* @param right the right operand (divisor)
* @return the result of the division
* @throws RuntimeException if the operands cannot be divided
* @throws ArithmeticException if attempting to divide by zero
*/
private static Object divide(Object left, Object right) {
if (left instanceof Number && right instanceof Number) {
double divisor = ((Number) right).doubleValue();
if (divisor == 0) {
throw new ArithmeticException("Division by zero");
}
return ((Number) left).doubleValue() / divisor;
} else {
throw new RuntimeException("Cannot divide " + left + " by " + right);
}
}
/**
* Compares two objects based on the given operator.
* Supports comparison of numbers and strings, and equality checks for null values.
*
* @param left the left operand
* @param operator the comparison operator (">", "<", ">=", "<=", "==", or "!=")
* @param right the right operand
* @return the result of the comparison (a boolean value)
* @throws IllegalStateException if an unknown operator is provided
* @throws RuntimeException if the operands cannot be compared
*/
private static Object compare(Object left, String operator, Object right) {
if (left == null && right == null) {
return true;
}
if (left == null || right == null) {
if ("==".equals(operator)) {
return false;
} else if ("!=".equals(operator)) {
return true;
}
}
if (left instanceof Number && right instanceof Number) {
double leftVal = ((Number) left).doubleValue();
double rightVal = ((Number) right).doubleValue();
return switch (operator) {
case ">" -> leftVal > rightVal;
case "<" -> leftVal < rightVal;
case ">=" -> leftVal >= rightVal;
case "<=" -> leftVal <= rightVal;
case "==" -> Math.abs(leftVal - rightVal) < 1e-9;
case "!=" -> Math.abs(leftVal - rightVal) >= 1e-9;
default -> throw new IllegalStateException("Unknown operator: " + operator);
};
} else if (left instanceof String && right instanceof String) {
int comparison = ((String) left).compareTo((String) right);
return switch (operator) {
case ">" -> comparison > 0;
case "<" -> comparison < 0;
case ">=" -> comparison >= 0;
case "<=" -> comparison <= 0;
case "==" -> comparison == 0;
case "!=" -> comparison != 0;
default -> throw new IllegalStateException("Unknown operator: " + operator);
};
}
throw new RuntimeException("Cannot compare " + left + " and " + right + " with operator " + operator);
}
/**
* Converts an object to a string representation.
* If the object is a {@code Double}, it formats it without any decimal places.
* Otherwise, it uses the {@code String.valueOf} method.
*
* @param value the object to convert to a string
* @return the string representation of the object
*/
public static String toString(Object value) {
if (value instanceof Double || value instanceof Float) {
double doubleValue = ((Number) value).doubleValue();
if (doubleValue == Math.floor(doubleValue) && !Double.isInfinite(doubleValue)) {
return String.format("%.0f", doubleValue);
}
}
return String.valueOf(value);
}
/**
* Converts an object to a boolean value.
* If the object is:
* - a {@code Boolean}, returns its value directly.
* - a {@code String}, returns {@code true} if the string is non-blank.
* - a {@code Number}, returns {@code true} if its integer value is not zero.
* For other object types, returns {@code true} if the object is non-null.
*
* @param value the object to convert to a boolean
* @return the boolean representation of the object
*/
public static Boolean toBoolean(Object value) {
if (value instanceof Boolean b) {
return b; // Returns the boolean value
} else if (value instanceof String s) {
return !s.isBlank(); // True if the string is not blank
} else if (value instanceof Number b) {
return b.intValue() != 0; // True if the number is not zero
} else {
return value != null; // True if the object is not null
}
}
/**
* Converts an object to a double value.
* If the object is:
* - a {@code Number}, returns its double value.
* - a {@code String}, tries to parse it as a double.
* - a {@code Boolean}, returns {@code 1.0} for {@code true}, {@code 0.0} for {@code false}.
* If the object cannot be converted, a {@code RuntimeException} is thrown.
*
* @param value the object to convert to a double
* @return the double representation of the object
* @throws RuntimeException if the object cannot be converted to a double
*/
public static double toDouble(Object value) {
if (value instanceof Number) {
return ((Number) value).doubleValue(); // Converts number to double
} else if (value instanceof String) {
try {
return Double.parseDouble((String) value); // Tries to parse string as double
} catch (NumberFormatException e) {
throw new RuntimeException("Cannot convert string to number: " + value);
}
} else if (value instanceof Boolean) {
return ((Boolean) value) ? 1.0 : 0.0; // True = 1.0, False = 0.0
} else {
throw new RuntimeException("Cannot convert to number: " + value);
}
}
/**
* Converts an object to an integer value.
* If the object is:
* - a {@code Number}, returns its integer value.
* - a {@code String}, tries to parse it as an integer, or as a double then converted to an integer.
* - a {@code Boolean}, returns {@code 1} for {@code true}, {@code 0} for {@code false}.
* If the object cannot be converted, a {@code RuntimeException} is thrown.
*
* @param value the object to convert to an integer
* @return the integer representation of the object
* @throws RuntimeException if the object cannot be converted to an integer
*/
public static int toInt(Object value) {
if (value instanceof Number) {
return ((Number) value).intValue(); // Converts number to int
} else if (value instanceof String) {
try {
return Integer.parseInt((String) value); // Tries to parse string as int
} catch (NumberFormatException e) {
// If string is not an int, tries parsing as double and converting to int
try {
return (int) Double.parseDouble((String) value);
} catch (NumberFormatException e2) {
throw new RuntimeException("Cannot convert string to integer: " + value);
}
}
} else if (value instanceof Boolean) {
return ((Boolean) value) ? 1 : 0; // True = 1, False = 0
} else {
throw new RuntimeException("Cannot convert to integer: " + value);
}
}
}

View File

@ -0,0 +1,213 @@
/*
* 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.internal.impl.model.profile;
import java.nio.file.Path;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Function;
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.Profile;
import org.apache.maven.api.services.BuilderProblem.Severity;
import org.apache.maven.api.services.ModelProblem.Version;
import org.apache.maven.api.services.ModelProblemCollector;
import org.apache.maven.api.services.VersionParser;
import org.apache.maven.api.services.model.ProfileActivationContext;
import org.apache.maven.api.services.model.ProfileActivator;
import org.apache.maven.api.services.model.RootLocator;
import org.apache.maven.internal.impl.model.DefaultInterpolator;
import org.apache.maven.internal.impl.model.ProfileActivationFilePathInterpolator;
import static org.apache.maven.internal.impl.model.profile.ConditionParser.toBoolean;
/**
* This class is responsible for activating profiles based on conditions specified in the profile's activation section.
* It evaluates the condition expression and determines whether the profile should be active.
*/
@Named("condition")
@Singleton
public class ConditionProfileActivator implements ProfileActivator {
private final VersionParser versionParser;
private final ProfileActivationFilePathInterpolator interpolator;
private final RootLocator rootLocator;
/**
* Constructs a new ConditionProfileActivator with the necessary dependencies.
*
* @param versionParser The parser for handling version comparisons
* @param interpolator The interpolator for resolving file paths
* @param rootLocator The locator for finding the project root directory
*/
@Inject
public ConditionProfileActivator(
VersionParser versionParser, ProfileActivationFilePathInterpolator interpolator, RootLocator rootLocator) {
this.versionParser = versionParser;
this.interpolator = interpolator;
this.rootLocator = rootLocator;
}
/**
* Determines whether a profile should be active based on its condition.
*
* @param profile The profile to evaluate
* @param context The context in which the profile is being evaluated
* @param problems A collector for any problems encountered during evaluation
* @return true if the profile should be active, false otherwise
*/
@Override
public boolean isActive(Profile profile, ProfileActivationContext context, ModelProblemCollector problems) {
if (profile.getActivation() == null || profile.getActivation().getCondition() == null) {
return false;
}
String condition = profile.getActivation().getCondition();
try {
Map<String, ConditionParser.ExpressionFunction> functions =
registerFunctions(context, versionParser, interpolator);
Function<String, String> propertyResolver = s -> property(context, rootLocator, s);
return toBoolean(new ConditionParser(functions, propertyResolver).parse(condition));
} catch (Exception e) {
problems.add(
Severity.ERROR, Version.V41, "Error parsing profile activation condition: " + e.getMessage(), e);
return false;
}
}
/**
* Checks if the condition is present in the profile's configuration.
*
* @param profile The profile to check
* @param context The context in which the profile is being evaluated
* @param problems A collector for any problems encountered during evaluation
* @return true if the condition is present and not blank, false otherwise
*/
@Override
public boolean presentInConfig(Profile profile, ProfileActivationContext context, ModelProblemCollector problems) {
Activation activation = profile.getActivation();
if (activation == null) {
return false;
}
return activation.getCondition() != null && !activation.getCondition().isBlank();
}
/**
* Registers the condition functions that can be used in profile activation expressions.
*
* @param context The profile activation context
* @param versionParser The parser for handling version comparisons
* @param interpolator The interpolator for resolving file paths
* @return A map of function names to their implementations
*/
public static Map<String, ConditionParser.ExpressionFunction> registerFunctions(
ProfileActivationContext context,
VersionParser versionParser,
ProfileActivationFilePathInterpolator interpolator) {
Map<String, ConditionParser.ExpressionFunction> functions = new HashMap<>();
ConditionFunctions conditionFunctions = new ConditionFunctions(context, versionParser, interpolator);
for (java.lang.reflect.Method method : ConditionFunctions.class.getDeclaredMethods()) {
String methodName = method.getName();
if (methodName.endsWith("_")) {
methodName = methodName.substring(0, methodName.length() - 1);
}
final String finalMethodName = methodName;
functions.put(finalMethodName, args -> {
try {
return method.invoke(conditionFunctions, args);
} catch (Exception e) {
StringBuilder causeChain = new StringBuilder();
Throwable cause = e;
while (cause != null) {
if (!causeChain.isEmpty()) {
causeChain.append(" Caused by: ");
}
causeChain.append(cause.toString());
cause = cause.getCause();
}
throw new RuntimeException(
"Error invoking function '" + finalMethodName + "': " + e + ". Cause chain: " + causeChain,
e);
}
});
}
return functions;
}
/**
* Retrieves the value of a property from the project context.
* Special function used to support the <code>${property}</code> syntax.
*
* The profile activation is done twice: once on the file model (so the model
* which has just been read from the file) and once while computing the effective
* model (so the model which will be used to build the project). We do need
* those two activations to be consistent, so we need to restrict access to
* properties that cannot change between file and effective model.
*
* @param name The property name
* @return The value of the property, or null if not found
* @throws IllegalArgumentException if the number of arguments is not exactly one
*/
static String property(ProfileActivationContext context, RootLocator rootLocator, String name) {
String value = doGetProperty(context, rootLocator, name);
return new DefaultInterpolator().interpolate(value, s -> doGetProperty(context, rootLocator, s));
}
static String doGetProperty(ProfileActivationContext context, RootLocator rootLocator, String name) {
// Handle special project-related properties
if ("project.basedir".equals(name)) {
Path basedir = context.getModel().getProjectDirectory();
return basedir != null ? basedir.toFile().getAbsolutePath() : null;
}
if ("project.rootDirectory".equals(name)) {
Path basedir = context.getModel().getProjectDirectory();
if (basedir != null) {
Path root = rootLocator.findMandatoryRoot(basedir);
return root.toFile().getAbsolutePath();
}
return null;
}
if ("project.artifactId".equals(name)) {
return context.getModel().getArtifactId();
}
if ("project.packaging".equals(name)) {
return context.getModel().getPackaging();
}
// Check user properties
String v = context.getUserProperties().get(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.getModel().getProperties().get(name);
}
if (v == null) {
// Check system properties
v = context.getSystemProperties().get(name);
}
return v;
}
}

View File

@ -100,7 +100,7 @@ public class OperatingSystemProfileActivator implements ProfileActivator {
} }
private boolean determineVersionMatch(String expectedVersion, String actualVersion) { private boolean determineVersionMatch(String expectedVersion, String actualVersion) {
String test = expectedVersion; String test = expectedVersion.toLowerCase(Locale.ENGLISH);
boolean reverse = false; boolean reverse = false;
final boolean result; final boolean result;
if (test.startsWith(REGEX_PREFIX)) { if (test.startsWith(REGEX_PREFIX)) {
@ -117,7 +117,7 @@ public class OperatingSystemProfileActivator implements ProfileActivator {
} }
private boolean determineArchMatch(String expectedArch, String actualArch) { private boolean determineArchMatch(String expectedArch, String actualArch) {
String test = expectedArch; String test = expectedArch.toLowerCase(Locale.ENGLISH);
boolean reverse = false; boolean reverse = false;
if (test.startsWith("!")) { if (test.startsWith("!")) {
@ -130,8 +130,8 @@ public class OperatingSystemProfileActivator implements ProfileActivator {
return reverse != result; return reverse != result;
} }
private boolean determineNameMatch(String expectedName, String actualName) { private boolean determineNameMatch(String family, String actualName) {
String test = expectedName; String test = family.toLowerCase(Locale.ENGLISH);
boolean reverse = false; boolean reverse = false;
if (test.startsWith("!")) { if (test.startsWith("!")) {

View File

@ -47,7 +47,7 @@ public class PackagingProfileActivator implements ProfileActivator {
} }
private static boolean isPackaging(ProfileActivationContext context, String p) { private static boolean isPackaging(ProfileActivationContext context, String p) {
String packaging = context.getUserProperties().get(ProfileActivationContext.PROPERTY_NAME_PACKAGING); String packaging = context.getModel().getPackaging();
return Objects.equals(p, packaging); return Objects.equals(p, packaging);
} }

View File

@ -70,6 +70,9 @@ public class PropertyProfileActivator implements ProfileActivator {
} }
String sysValue = context.getUserProperties().get(name); String sysValue = context.getUserProperties().get(name);
if (sysValue == null && "packaging".equals(name)) {
sysValue = context.getModel().getPackaging();
}
if (sysValue == null) { if (sysValue == null) {
sysValue = context.getSystemProperties().get(name); sysValue = context.getSystemProperties().get(name);
} }

View File

@ -0,0 +1,64 @@
/*
* 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.internal.impl.model.profile;
import java.util.Properties;
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.internal.impl.model.DefaultProfileActivationContext;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import static org.junit.jupiter.api.Assertions.assertEquals;
/**
* Provides common services to test {@link ProfileActivator} implementations.
*
*/
public abstract class AbstractProfileActivatorTest<T extends ProfileActivator> {
protected T activator;
@BeforeEach
abstract void setUp() throws Exception;
@AfterEach
void tearDown() throws Exception {
activator = null;
}
protected ProfileActivationContext newContext(final Properties userProperties, final Properties systemProperties) {
DefaultProfileActivationContext context = new DefaultProfileActivationContext();
return context.setUserProperties(userProperties)
.setSystemProperties(systemProperties)
.setModel(Model.newInstance());
}
protected void assertActivation(boolean active, Profile profile, ProfileActivationContext context) {
SimpleProblemCollector problems = new SimpleProblemCollector();
boolean res = activator.isActive(profile, context, problems);
assertEquals(0, problems.getErrors().size(), problems.getErrors().toString());
assertEquals(0, problems.getWarnings().size(), problems.getWarnings().toString());
assertEquals(active, res);
}
}

View File

@ -0,0 +1,300 @@
/*
* 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.internal.impl.model.profile;
import java.util.Map;
import java.util.Properties;
import java.util.function.Function;
import org.apache.maven.api.model.Model;
import org.apache.maven.api.services.model.ProfileActivationContext;
import org.apache.maven.internal.impl.DefaultModelVersionParser;
import org.apache.maven.internal.impl.DefaultVersionParser;
import org.apache.maven.internal.impl.model.DefaultInterpolator;
import org.apache.maven.internal.impl.model.DefaultPathTranslator;
import org.apache.maven.internal.impl.model.DefaultProfileActivationContext;
import org.apache.maven.internal.impl.model.DefaultRootLocator;
import org.apache.maven.internal.impl.model.ProfileActivationFilePathInterpolator;
import org.apache.maven.internal.impl.model.profile.ConditionParser.ExpressionFunction;
import org.eclipse.aether.util.version.GenericVersionScheme;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
class ConditionParserTest {
ConditionParser parser;
Map<String, ExpressionFunction> functions;
Function<String, String> propertyResolver;
@BeforeEach
void setUp() {
ProfileActivationContext context = createMockContext();
DefaultVersionParser versionParser =
new DefaultVersionParser(new DefaultModelVersionParser(new GenericVersionScheme()));
ProfileActivationFilePathInterpolator interpolator = new ProfileActivationFilePathInterpolator(
new DefaultPathTranslator(), bd -> true, new DefaultInterpolator());
DefaultRootLocator rootLocator = new DefaultRootLocator();
functions = ConditionProfileActivator.registerFunctions(context, versionParser, interpolator);
propertyResolver = s -> ConditionProfileActivator.property(context, rootLocator, s);
parser = new ConditionParser(functions, propertyResolver);
}
private ProfileActivationContext createMockContext() {
DefaultProfileActivationContext context = new DefaultProfileActivationContext();
Properties systemProperties = new Properties();
systemProperties.setProperty("os.name", "windows");
systemProperties.setProperty("os.arch", "amd64");
systemProperties.setProperty("os.version", "10.0");
systemProperties.setProperty("java.version", "1.8.0_292");
context.setSystemProperties(systemProperties);
context.setModel(Model.newInstance());
return context;
}
@Test
void testStringLiterals() {
assertEquals("Hello, World!", parser.parse("'Hello, World!'"));
assertEquals("Hello, World!", parser.parse("\"Hello, World!\""));
}
@Test
void testStringConcatenation() {
assertEquals("HelloWorld", parser.parse("'Hello' + 'World'"));
assertEquals("Hello123", parser.parse("'Hello' + 123"));
}
@Test
void testLengthFunction() {
assertEquals(13, parser.parse("length('Hello, World!')"));
assertEquals(5, parser.parse("length(\"Hello\")"));
}
@Test
void testCaseConversionFunctions() {
assertEquals("HELLO", parser.parse("upper('hello')"));
assertEquals("world", parser.parse("lower('WORLD')"));
}
@Test
void testConcatFunction() {
assertEquals("HelloWorld", parser.parse("'Hello' + 'World'"));
assertEquals("The answer is 42", parser.parse("'The answer is ' + 42"));
assertEquals("The answer is 42", parser.parse("'The answer is ' + 42.0"));
assertEquals("The answer is 42", parser.parse("'The answer is ' + 42.0f"));
assertEquals("Pi is approximately 3.14", parser.parse("'Pi is approximately ' + 3.14"));
assertEquals("Pi is approximately 3.14", parser.parse("'Pi is approximately ' + 3.14f"));
}
@Test
void testToString() {
assertEquals("42", ConditionParser.toString(42));
assertEquals("42", ConditionParser.toString(42L));
assertEquals("42", ConditionParser.toString(42.0));
assertEquals("42", ConditionParser.toString(42.0f));
assertEquals("3.14", ConditionParser.toString(3.14));
assertEquals("3.14", ConditionParser.toString(3.14f));
assertEquals("true", ConditionParser.toString(true));
assertEquals("false", ConditionParser.toString(false));
assertEquals("hello", ConditionParser.toString("hello"));
}
@Test
void testSubstringFunction() {
assertEquals("World", parser.parse("substring('Hello, World!', 7, 12)"));
assertEquals("World!", parser.parse("substring('Hello, World!', 7)"));
}
@Test
void testIndexOf() {
assertEquals(7, parser.parse("indexOf('Hello, World!', 'World')"));
assertEquals(-1, parser.parse("indexOf('Hello, World!', 'OpenAI')"));
}
@Test
void testInRange() {
assertTrue((Boolean) parser.parse("inrange('1.8.0_292', '[1.8,2.0)')"));
assertFalse((Boolean) parser.parse("inrange('1.7.0', '[1.8,2.0)')"));
}
@Test
void testIfFunction() {
assertEquals("long", parser.parse("if(length('test') > 3, 'long', 'short')"));
assertEquals("short", parser.parse("if(length('hi') > 3, 'long', 'short')"));
}
@Test
void testContainsFunction() {
assertTrue((Boolean) parser.parse("contains('Hello, World!', 'World')"));
assertFalse((Boolean) parser.parse("contains('Hello, World!', 'OpenAI')"));
}
@Test
void testMatchesFunction() {
assertTrue((Boolean) parser.parse("matches('test123', '\\w+')"));
assertFalse((Boolean) parser.parse("matches('test123', '\\d+')"));
}
@Test
void testComplexExpression() {
String expression = "if(contains(lower('HELLO WORLD'), 'hello'), upper('success') + '!', 'failure')";
assertEquals("SUCCESS!", parser.parse(expression));
}
@Test
void testStringComparison() {
assertTrue((Boolean) parser.parse("'abc' != 'cdf'"));
assertFalse((Boolean) parser.parse("'abc' != 'abc'"));
assertTrue((Boolean) parser.parse("'abc' == 'abc'"));
assertFalse((Boolean) parser.parse("'abc' == 'cdf'"));
}
@Test
void testParenthesesMismatch() {
functions.put("property", args -> "foo");
functions.put("inrange", args -> false);
assertThrows(
RuntimeException.class,
() -> parser.parse(
"property('os.name') == 'windows' && property('os.arch') != 'amd64') && inrange(property('os.version'), '[99,)')"),
"Should throw RuntimeException due to parentheses mismatch");
assertThrows(
RuntimeException.class,
() -> parser.parse(
"(property('os.name') == 'windows' && property('os.arch') != 'amd64') && inrange(property('os.version'), '[99,)'"),
"Should throw RuntimeException due to parentheses mismatch");
// This should not throw an exception if parentheses are balanced and properties are handled correctly
assertDoesNotThrow(
() -> parser.parse(
"(property('os.name') == 'windows' && property('os.arch') != 'amd64') && inrange(property('os.version'), '[99,)')"));
}
@Test
void testBasicArithmetic() {
assertEquals(5.0, parser.parse("2 + 3"));
assertEquals(10.0, parser.parse("15 - 5"));
assertEquals(24.0, parser.parse("6 * 4"));
assertEquals(3.0, parser.parse("9 / 3"));
}
@Test
void testArithmeticPrecedence() {
assertEquals(14.0, parser.parse("2 + 3 * 4"));
assertEquals(20.0, parser.parse("(2 + 3) * 4"));
assertEquals(11.0, parser.parse("15 - 6 + 2"));
assertEquals(10.0, parser.parse("10 / 2 + 2 * 2.5"));
}
@Test
void testFloatingPointArithmetic() {
assertEquals(5.5, parser.parse("2.2 + 3.3"));
assertEquals(0.1, (Double) parser.parse("3.3 - 3.2"), 1e-10);
assertEquals(6.25, parser.parse("2.5 * 2.5"));
assertEquals(2.5, parser.parse("5 / 2"));
}
@Test
void testArithmeticComparisons() {
assertTrue((Boolean) parser.parse("5 > 3"));
assertTrue((Boolean) parser.parse("3 < 5"));
assertTrue((Boolean) parser.parse("5 >= 5"));
assertTrue((Boolean) parser.parse("3 <= 3"));
assertTrue((Boolean) parser.parse("5 == 5"));
assertTrue((Boolean) parser.parse("5 != 3"));
assertFalse((Boolean) parser.parse("5 < 3"));
assertFalse((Boolean) parser.parse("3 > 5"));
assertFalse((Boolean) parser.parse("5 != 5"));
}
@Test
void testComplexArithmeticExpressions() {
assertFalse((Boolean) parser.parse("(2 + 3 * 4) > (10 + 5)"));
assertTrue((Boolean) parser.parse("(2 + 3 * 4) < (10 + 5)"));
assertTrue((Boolean) parser.parse("(10 / 2 + 3) == 8"));
assertTrue((Boolean) parser.parse("(10 / 2 + 3) != 9"));
}
@Test
void testArithmeticFunctions() {
assertEquals(5.0, parser.parse("2 + 3"));
assertEquals(2.0, parser.parse("5 - 3"));
assertEquals(15.0, parser.parse("3 * 5"));
assertEquals(2.5, parser.parse("5 / 2"));
}
@Test
void testCombinedArithmeticAndLogic() {
assertTrue((Boolean) parser.parse("(5 > 3) && (10 / 2 == 5)"));
assertFalse((Boolean) parser.parse("(5 < 3) || (10 / 2 != 5)"));
assertTrue((Boolean) parser.parse("2 + 3 == 1 * 5"));
}
@Test
void testDivisionByZero() {
assertThrows(ArithmeticException.class, () -> parser.parse("5 / 0"));
}
@Test
void testPropertyAlias() {
assertTrue((Boolean) parser.parse("${os.name} == 'windows'"));
assertFalse((Boolean) parser.parse("${os.name} == 'linux'"));
assertTrue((Boolean) parser.parse("${os.arch} == 'amd64' && ${os.name} == 'windows'"));
assertThrows(RuntimeException.class, () -> parser.parse("${unclosed"));
}
@Test
void testNestedPropertyAlias() {
functions.put("property", args -> {
if (args.get(0).equals("project.rootDirectory")) return "/home/user/project";
return null;
});
functions.put("exists", args -> true); // Mock implementation
Object result = parser.parse("exists('${project.rootDirectory}/someFile.txt')");
assertTrue((Boolean) result);
result = parser.parse("exists('${project.rootDirectory}/${nested.property}/someFile.txt')");
assertTrue((Boolean) result);
assertDoesNotThrow(() -> parser.parse("property('')"));
}
@Test
void testToInt() {
assertEquals(123, ConditionParser.toInt(123));
assertEquals(123, ConditionParser.toInt(123L));
assertEquals(123, ConditionParser.toInt(123.0));
assertEquals(123, ConditionParser.toInt(123.5)); // This will truncate the decimal part
assertEquals(123, ConditionParser.toInt("123"));
assertEquals(123, ConditionParser.toInt("123.0"));
assertEquals(123, ConditionParser.toInt("123.5")); // This will truncate the decimal part
assertEquals(1, ConditionParser.toInt(true));
assertEquals(0, ConditionParser.toInt(false));
assertThrows(RuntimeException.class, () -> ConditionParser.toInt("not a number"));
assertThrows(RuntimeException.class, () -> ConditionParser.toInt(new Object()));
}
}

View File

@ -0,0 +1,503 @@
/*
* 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.internal.impl.model.profile;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Properties;
import org.apache.maven.api.model.Activation;
import org.apache.maven.api.model.ActivationFile;
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.RootLocator;
import org.apache.maven.internal.impl.DefaultModelVersionParser;
import org.apache.maven.internal.impl.DefaultVersionParser;
import org.apache.maven.internal.impl.model.DefaultInterpolator;
import org.apache.maven.internal.impl.model.DefaultPathTranslator;
import org.apache.maven.internal.impl.model.DefaultProfileActivationContext;
import org.apache.maven.internal.impl.model.DefaultRootLocator;
import org.apache.maven.internal.impl.model.ProfileActivationFilePathInterpolator;
import org.eclipse.aether.util.version.GenericVersionScheme;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
public class ConditionProfileActivatorTest extends AbstractProfileActivatorTest<ConditionProfileActivator> {
@TempDir
Path tempDir;
@BeforeEach
@Override
void setUp() throws Exception {
activator = new ConditionProfileActivator(
new DefaultVersionParser(new DefaultModelVersionParser(new GenericVersionScheme())),
new ProfileActivationFilePathInterpolator(
new DefaultPathTranslator(), bd -> true, new DefaultInterpolator()),
new DefaultRootLocator());
Path file = tempDir.resolve("file.txt");
Files.createFile(file);
Path dir = tempDir.resolve("dir");
Files.createDirectory(dir);
Files.createFile(dir.resolve("test.xsd"));
}
private Profile newProfile(String condition) {
Activation a = Activation.newBuilder().condition(condition).build();
Profile p = Profile.newBuilder().activation(a).build();
return p;
}
private Properties newJdkProperties(String javaVersion) {
Properties props = new Properties();
props.setProperty("java.version", javaVersion);
return props;
}
@Test
void testNullSafe() throws Exception {
Profile p = Profile.newInstance();
assertActivation(false, p, newContext(null, null));
p = p.withActivation(Activation.newInstance());
assertActivation(false, p, newContext(null, null));
}
@Test
void testJdkPrefix() throws Exception {
Profile profile = newProfile("inrange(${java.version}, '[1.4,1.5)')");
assertActivation(true, profile, newContext(null, newJdkProperties("1.4")));
assertActivation(true, profile, newContext(null, newJdkProperties("1.4.2")));
assertActivation(true, profile, newContext(null, newJdkProperties("1.4.2_09")));
assertActivation(true, profile, newContext(null, newJdkProperties("1.4.2_09-b03")));
assertActivation(false, profile, newContext(null, newJdkProperties("1.3")));
assertActivation(false, profile, newContext(null, newJdkProperties("1.5")));
}
@Test
void testJdkPrefixNegated() throws Exception {
Profile profile = newProfile("not(inrange(${java.version}, '[1.4,1.5)'))");
assertActivation(false, profile, newContext(null, newJdkProperties("1.4")));
assertActivation(false, profile, newContext(null, newJdkProperties("1.4.2")));
assertActivation(false, profile, newContext(null, newJdkProperties("1.4.2_09")));
assertActivation(false, profile, newContext(null, newJdkProperties("1.4.2_09-b03")));
assertActivation(true, profile, newContext(null, newJdkProperties("1.3")));
assertActivation(true, profile, newContext(null, newJdkProperties("1.5")));
}
@Test
void testJdkVersionRangeInclusiveBounds() throws Exception {
Profile profile = newProfile("inrange(${java.version}, '[1.5,1.6.1]')");
assertActivation(false, profile, newContext(null, newJdkProperties("1.4")));
assertActivation(false, profile, newContext(null, newJdkProperties("1.4.2")));
assertActivation(false, profile, newContext(null, newJdkProperties("1.4.2_09")));
assertActivation(false, profile, newContext(null, newJdkProperties("1.4.2_09-b03")));
assertActivation(true, profile, newContext(null, newJdkProperties("1.5")));
assertActivation(true, profile, newContext(null, newJdkProperties("1.5.0")));
assertActivation(true, profile, newContext(null, newJdkProperties("1.5.0_09")));
assertActivation(true, profile, newContext(null, newJdkProperties("1.5.0_09-b03")));
assertActivation(true, profile, newContext(null, newJdkProperties("1.5.1")));
assertActivation(true, profile, newContext(null, newJdkProperties("1.6")));
assertActivation(true, profile, newContext(null, newJdkProperties("1.6.0")));
assertActivation(true, profile, newContext(null, newJdkProperties("1.6.0_09")));
assertActivation(true, profile, newContext(null, newJdkProperties("1.6.0_09-b03")));
}
@Test
void testJdkVersionRangeExclusiveBounds() throws Exception {
Profile profile = newProfile("inrange(${java.version}, '[1.3.1,1.6)')");
assertActivation(false, profile, newContext(null, newJdkProperties("1.3")));
assertActivation(false, profile, newContext(null, newJdkProperties("1.3.0")));
assertActivation(false, profile, newContext(null, newJdkProperties("1.3.0_09")));
assertActivation(false, profile, newContext(null, newJdkProperties("1.3.0_09-b03")));
assertActivation(true, profile, newContext(null, newJdkProperties("1.3.1")));
assertActivation(true, profile, newContext(null, newJdkProperties("1.3.1_09")));
assertActivation(true, profile, newContext(null, newJdkProperties("1.3.1_09-b03")));
assertActivation(true, profile, newContext(null, newJdkProperties("1.5")));
assertActivation(true, profile, newContext(null, newJdkProperties("1.5.0")));
assertActivation(true, profile, newContext(null, newJdkProperties("1.5.0_09")));
assertActivation(true, profile, newContext(null, newJdkProperties("1.5.0_09-b03")));
assertActivation(true, profile, newContext(null, newJdkProperties("1.5.1")));
assertActivation(false, profile, newContext(null, newJdkProperties("1.6")));
}
@Test
void testJdkVersionRangeInclusiveLowerBound() throws Exception {
Profile profile = newProfile("inrange(${java.version}, '[1.5,)')");
assertActivation(false, profile, newContext(null, newJdkProperties("1.4")));
assertActivation(false, profile, newContext(null, newJdkProperties("1.4.2")));
assertActivation(false, profile, newContext(null, newJdkProperties("1.4.2_09")));
assertActivation(false, profile, newContext(null, newJdkProperties("1.4.2_09-b03")));
assertActivation(true, profile, newContext(null, newJdkProperties("1.5")));
assertActivation(true, profile, newContext(null, newJdkProperties("1.5.0")));
assertActivation(true, profile, newContext(null, newJdkProperties("1.5.0_09")));
assertActivation(true, profile, newContext(null, newJdkProperties("1.5.0_09-b03")));
assertActivation(true, profile, newContext(null, newJdkProperties("1.5.1")));
assertActivation(true, profile, newContext(null, newJdkProperties("1.6")));
assertActivation(true, profile, newContext(null, newJdkProperties("1.6.0")));
assertActivation(true, profile, newContext(null, newJdkProperties("1.6.0_09")));
assertActivation(true, profile, newContext(null, newJdkProperties("1.6.0_09-b03")));
}
@Test
void testJdkVersionRangeExclusiveUpperBound() throws Exception {
Profile profile = newProfile("inrange(${java.version}, '(,1.6)')");
assertActivation(true, profile, newContext(null, newJdkProperties("1.5")));
assertActivation(true, profile, newContext(null, newJdkProperties("1.5.0")));
assertActivation(true, profile, newContext(null, newJdkProperties("1.5.0_09")));
assertActivation(true, profile, newContext(null, newJdkProperties("1.5.0_09-b03")));
assertActivation(true, profile, newContext(null, newJdkProperties("1.5.1")));
assertActivation(false, profile, newContext(null, newJdkProperties("1.6")));
assertActivation(false, profile, newContext(null, newJdkProperties("1.6.0")));
assertActivation(false, profile, newContext(null, newJdkProperties("1.6.0_09")));
assertActivation(false, profile, newContext(null, newJdkProperties("1.6.0_09-b03")));
}
@Disabled
@Test
void testJdkRubbishJavaVersion() {
Profile profile = newProfile("inrange(${java.version}, '[1.8,)')");
assertActivationWithProblems(profile, newContext(null, newJdkProperties("Pūteketeke")), "invalid JDK version");
assertActivationWithProblems(profile, newContext(null, newJdkProperties("rubbish")), "invalid JDK version");
assertActivationWithProblems(profile, newContext(null, newJdkProperties("1.a.0_09")), "invalid JDK version");
assertActivationWithProblems(profile, newContext(null, newJdkProperties("1.a.2.b")), "invalid JDK version");
}
private void assertActivationWithProblems(
Profile profile, ProfileActivationContext context, String warningContains) {
SimpleProblemCollector problems = new SimpleProblemCollector();
assertFalse(activator.isActive(profile, context, problems));
assertEquals(0, problems.getErrors().size(), problems.getErrors().toString());
assertEquals(1, problems.getWarnings().size(), problems.getWarnings().toString());
assertTrue(
problems.getWarnings().get(0).contains(warningContains),
problems.getWarnings().toString());
}
private Properties newOsProperties(String osName, String osVersion, String osArch) {
Properties props = new Properties();
props.setProperty("os.name", osName);
props.setProperty("os.version", osVersion);
props.setProperty("os.arch", osArch);
return props;
}
@Test
void testOsVersionStringComparison() throws Exception {
Profile profile = newProfile("inrange(${os.version}, '[6.5.0-1014-aws,6.6)')");
assertActivation(true, profile, newContext(null, newOsProperties("linux", "6.5.0-1014-aws", "amd64")));
assertActivation(true, profile, newContext(null, newOsProperties("windows", "6.5.0-1014-aws", "aarch64")));
assertActivation(false, profile, newContext(null, newOsProperties("linux", "3.1.0", "amd64")));
}
@Test
void testOsVersionRegexMatching() throws Exception {
Profile profile = newProfile("matches(${os.version}, '.*aws')");
assertActivation(true, profile, newContext(null, newOsProperties("linux", "6.5.0-1014-aws", "amd64")));
assertActivation(true, profile, newContext(null, newOsProperties("windows", "6.5.0-1014-aws", "aarch64")));
assertActivation(false, profile, newContext(null, newOsProperties("linux", "3.1.0", "amd64")));
}
@Test
void testOsName() {
Profile profile = newProfile("${os.name} == 'windows'");
assertActivation(false, profile, newContext(null, newOsProperties("linux", "6.5.0-1014-aws", "amd64")));
assertActivation(true, profile, newContext(null, newOsProperties("windows", "6.5.0-1014-aws", "aarch64")));
}
@Test
void testOsNegatedName() {
Profile profile = newProfile("${os.name} != 'windows'");
assertActivation(true, profile, newContext(null, newOsProperties("linux", "6.5.0-1014-aws", "amd64")));
assertActivation(false, profile, newContext(null, newOsProperties("windows", "6.5.0-1014-aws", "aarch64")));
}
@Test
void testOsArch() {
Profile profile = newProfile("${os.arch} == 'amd64'");
assertActivation(true, profile, newContext(null, newOsProperties("linux", "6.5.0-1014-aws", "amd64")));
assertActivation(false, profile, newContext(null, newOsProperties("windows", "6.5.0-1014-aws", "aarch64")));
}
@Test
void testOsNegatedArch() {
Profile profile = newProfile("${os.arch} != 'amd64'");
assertActivation(false, profile, newContext(null, newOsProperties("linux", "6.5.0-1014-aws", "amd64")));
assertActivation(true, profile, newContext(null, newOsProperties("windows", "6.5.0-1014-aws", "aarch64")));
}
/*
@Test
void testOsFamily() {
Profile profile = newProfile(ActivationOS.newBuilder().family("windows"));
assertActivation(false, profile, newContext(null, newOsProperties("linux", "6.5.0-1014-aws", "amd64")));
assertActivation(true, profile, newContext(null, newOsProperties("windows", "6.5.0-1014-aws", "aarch64")));
}
@Test
void testOsNegatedFamily() {
Profile profile = newProfile(ActivationOS.newBuilder().family("!windows"));
assertActivation(true, profile, newContext(null, newOsProperties("linux", "6.5.0-1014-aws", "amd64")));
assertActivation(false, profile, newContext(null, newOsProperties("windows", "6.5.0-1014-aws", "aarch64")));
}
*/
@Test
void testOsAllConditions() {
Profile profile =
newProfile("${os.name} == 'windows' && ${os.arch} != 'amd64' && inrange(${os.version}, '[99,)')");
assertActivation(false, profile, newContext(null, newOsProperties("linux", "6.5.0-1014-aws", "amd64")));
assertActivation(false, profile, newContext(null, newOsProperties("windows", "1", "aarch64")));
assertActivation(false, profile, newContext(null, newOsProperties("windows", "99", "amd64")));
assertActivation(true, profile, newContext(null, newOsProperties("windows", "99", "aarch64")));
}
@Test
public void testOsCapitalName() {
Profile profile = newProfile("lower(${os.name}) == 'mac os x'");
assertActivation(false, profile, newContext(null, newOsProperties("linux", "6.5.0-1014-aws", "amd64")));
assertActivation(false, profile, newContext(null, newOsProperties("windows", "1", "aarch64")));
assertActivation(false, profile, newContext(null, newOsProperties("windows", "99", "amd64")));
assertActivation(true, profile, newContext(null, newOsProperties("Mac OS X", "14.5", "aarch64")));
}
private Properties newPropProperties(String key, String value) {
Properties props = new Properties();
props.setProperty(key, value);
return props;
}
@Test
void testPropWithNameOnly_UserProperty() throws Exception {
Profile profile = newProfile("${prop}");
assertActivation(true, profile, newContext(newPropProperties("prop", "value"), null));
assertActivation(false, profile, newContext(newPropProperties("prop", ""), null));
assertActivation(false, profile, newContext(newPropProperties("other", "value"), null));
}
@Test
void testPropWithNameOnly_SystemProperty() throws Exception {
Profile profile = newProfile("${prop}");
assertActivation(true, profile, newContext(null, newPropProperties("prop", "value")));
assertActivation(false, profile, newContext(null, newPropProperties("prop", "")));
assertActivation(false, profile, newContext(null, newPropProperties("other", "value")));
}
@Test
void testPropWithNegatedNameOnly_UserProperty() throws Exception {
Profile profile = newProfile("not(${prop})");
assertActivation(false, profile, newContext(newPropProperties("prop", "value"), null));
assertActivation(true, profile, newContext(newPropProperties("prop", ""), null));
assertActivation(true, profile, newContext(newPropProperties("other", "value"), null));
}
@Test
void testPropWithNegatedNameOnly_SystemProperty() throws Exception {
Profile profile = newProfile("not(${prop})");
assertActivation(false, profile, newContext(null, newPropProperties("prop", "value")));
assertActivation(true, profile, newContext(null, newPropProperties("prop", "")));
assertActivation(true, profile, newContext(null, newPropProperties("other", "value")));
}
@Test
void testPropWithValue_UserProperty() throws Exception {
Profile profile = newProfile("${prop} == 'value'");
assertActivation(true, profile, newContext(newPropProperties("prop", "value"), null));
assertActivation(false, profile, newContext(newPropProperties("prop", "other"), null));
assertActivation(false, profile, newContext(newPropProperties("prop", ""), null));
}
@Test
void testPropWithValue_SystemProperty() throws Exception {
Profile profile = newProfile("${prop} == 'value'");
assertActivation(true, profile, newContext(null, newPropProperties("prop", "value")));
assertActivation(false, profile, newContext(null, newPropProperties("prop", "other")));
assertActivation(false, profile, newContext(null, newPropProperties("other", "")));
}
@Test
void testPropWithNegatedValue_UserProperty() throws Exception {
Profile profile = newProfile("${prop} != 'value'");
assertActivation(false, profile, newContext(newPropProperties("prop", "value"), null));
assertActivation(true, profile, newContext(newPropProperties("prop", "other"), null));
assertActivation(true, profile, newContext(newPropProperties("prop", ""), null));
}
@Test
void testPropWithNegatedValue_SystemProperty() throws Exception {
Profile profile = newProfile("${prop} != 'value'");
assertActivation(false, profile, newContext(null, newPropProperties("prop", "value")));
assertActivation(true, profile, newContext(null, newPropProperties("prop", "other")));
assertActivation(true, profile, newContext(null, newPropProperties("other", "")));
}
@Test
void testPropWithValue_UserPropertyDominantOverSystemProperty() throws Exception {
Profile profile = newProfile("${prop} == 'value'");
Properties props1 = newPropProperties("prop", "value");
Properties props2 = newPropProperties("prop", "other");
assertActivation(true, profile, newContext(props1, props2));
assertActivation(false, profile, newContext(props2, props1));
}
@Test
@Disabled
void testFileRootDirectoryWithNull() {
IllegalStateException e = assertThrows(
IllegalStateException.class,
() -> assertActivation(false, newProfile("exists('${project.rootDirectory}')"), newFileContext(null)));
assertEquals(RootLocator.UNABLE_TO_FIND_ROOT_PROJECT_MESSAGE, e.getMessage());
}
@Test
void testFileRootDirectory() {
assertActivation(false, newProfile("exists('${project.rootDirectory}/someFile.txt')"), newFileContext());
assertActivation(true, newProfile("missing('${project.rootDirectory}/someFile.txt')"), newFileContext());
assertActivation(true, newProfile("exists('${project.rootDirectory}')"), newFileContext());
assertActivation(true, newProfile("exists('${project.rootDirectory}/file.txt')"), newFileContext());
assertActivation(false, newProfile("missing('${project.rootDirectory}')"), newFileContext());
assertActivation(false, newProfile("missing('${project.rootDirectory}/file.txt')"), newFileContext());
}
@Test
@Disabled
void testFileWilcards() {
assertActivation(true, newProfile("exists('${project.rootDirectory}/**/*.xsd')"), newFileContext());
assertActivation(true, newProfile("exists('${project.basedir}/**/*.xsd')"), newFileContext());
assertActivation(true, newProfile("exists('${project.basedir}/**/*.xsd')"), newFileContext());
assertActivation(true, newProfile("exists('**/*.xsd')"), newFileContext());
assertActivation(true, newProfile("missing('**/*.xml')"), newFileContext());
}
@Test
void testFileIsActiveNoFileWithShortBasedir() {
assertActivation(false, newExistsProfile(null), newFileContext());
assertActivation(false, newProfile("exists('someFile.txt')"), newFileContext());
assertActivation(false, newProfile("exists('${basedir}/someFile.txt')"), newFileContext());
assertActivation(false, newMissingProfile(null), newFileContext());
assertActivation(true, newProfile("missing('someFile.txt')"), newFileContext());
assertActivation(true, newProfile("missing('${basedir}/someFile.txt')"), newFileContext());
}
@Test
void testFileIsActiveNoFile() {
assertActivation(false, newExistsProfile(null), newFileContext());
assertActivation(false, newProfile("exists('someFile.txt')"), newFileContext());
assertActivation(false, newProfile("exists('${project.basedir}/someFile.txt')"), newFileContext());
assertActivation(false, newMissingProfile(null), newFileContext());
assertActivation(true, newProfile("missing('someFile.txt')"), newFileContext());
assertActivation(true, newProfile("missing('${project.basedir}/someFile.txt')"), newFileContext());
}
@Test
void testFileIsActiveExistsFileExists() {
assertActivation(true, newProfile("exists('file.txt')"), newFileContext());
assertActivation(true, newProfile("exists('${project.basedir}')"), newFileContext());
assertActivation(true, newProfile("exists('${project.basedir}/file.txt')"), newFileContext());
assertActivation(false, newProfile("missing('file.txt')"), newFileContext());
assertActivation(false, newProfile("missing('${project.basedir}')"), newFileContext());
assertActivation(false, newProfile("missing('${project.basedir}/file.txt')"), newFileContext());
}
private Profile newExistsProfile(String filePath) {
ActivationFile activationFile =
ActivationFile.newBuilder().exists(filePath).build();
return newProfile(activationFile);
}
private Profile newMissingProfile(String filePath) {
ActivationFile activationFile =
ActivationFile.newBuilder().missing(filePath).build();
return newProfile(activationFile);
}
private Profile newProfile(ActivationFile activationFile) {
Activation activation = Activation.newBuilder().file(activationFile).build();
return Profile.newBuilder().activation(activation).build();
}
protected ProfileActivationContext newFileContext(Path path) {
DefaultProfileActivationContext context = new DefaultProfileActivationContext();
context.setModel(Model.newBuilder().pomFile(path.resolve("pom.xml")).build());
return context;
}
protected ProfileActivationContext newFileContext() {
return newFileContext(tempDir);
}
}

View File

@ -0,0 +1,145 @@
/*
* 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.internal.impl.model.profile;
import java.io.File;
import java.io.IOException;
import java.nio.file.Path;
import org.apache.maven.api.model.Activation;
import org.apache.maven.api.model.ActivationFile;
import org.apache.maven.api.model.Model;
import org.apache.maven.api.model.Profile;
import org.apache.maven.api.services.model.RootLocator;
import org.apache.maven.internal.impl.model.DefaultInterpolator;
import org.apache.maven.internal.impl.model.DefaultPathTranslator;
import org.apache.maven.internal.impl.model.DefaultProfileActivationContext;
import org.apache.maven.internal.impl.model.ProfileActivationFilePathInterpolator;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
/**
* Tests {@link FileProfileActivator}.
*
*/
class FileProfileActivatorTest extends AbstractProfileActivatorTest<FileProfileActivator> {
@TempDir
Path tempDir;
private final DefaultProfileActivationContext context = new DefaultProfileActivationContext();
@BeforeEach
@Override
void setUp() throws Exception {
activator = new FileProfileActivator(new ProfileActivationFilePathInterpolator(
new DefaultPathTranslator(), bd -> true, new DefaultInterpolator()));
context.setModel(Model.newBuilder().pomFile(tempDir.resolve("pom.xml")).build());
File file = new File(tempDir.resolve("file.txt").toString());
if (!file.createNewFile()) {
throw new IOException("Can't create " + file);
}
}
@Test
void testRootDirectoryWithNull() {
context.setModel(Model.newInstance());
IllegalStateException e = assertThrows(
IllegalStateException.class,
() -> assertActivation(false, newExistsProfile("${project.rootDirectory}"), context));
assertEquals(RootLocator.UNABLE_TO_FIND_ROOT_PROJECT_MESSAGE, e.getMessage());
}
@Test
void testRootDirectory() {
assertActivation(false, newExistsProfile("${project.rootDirectory}/someFile.txt"), context);
assertActivation(true, newMissingProfile("${project.rootDirectory}/someFile.txt"), context);
assertActivation(true, newExistsProfile("${project.rootDirectory}"), context);
assertActivation(true, newExistsProfile("${project.rootDirectory}/" + "file.txt"), context);
assertActivation(false, newMissingProfile("${project.rootDirectory}"), context);
assertActivation(false, newMissingProfile("${project.rootDirectory}/" + "file.txt"), context);
}
@Test
void testIsActiveNoFileWithShortBasedir() {
assertActivation(false, newExistsProfile(null), context);
assertActivation(false, newExistsProfile("someFile.txt"), context);
assertActivation(false, newExistsProfile("${basedir}/someFile.txt"), context);
assertActivation(false, newMissingProfile(null), context);
assertActivation(true, newMissingProfile("someFile.txt"), context);
assertActivation(true, newMissingProfile("${basedir}/someFile.txt"), context);
}
@Test
void testIsActiveNoFile() {
assertActivation(false, newExistsProfile(null), context);
assertActivation(false, newExistsProfile("someFile.txt"), context);
assertActivation(false, newExistsProfile("${project.basedir}/someFile.txt"), context);
assertActivation(false, newMissingProfile(null), context);
assertActivation(true, newMissingProfile("someFile.txt"), context);
assertActivation(true, newMissingProfile("${project.basedir}/someFile.txt"), context);
}
@Test
void testIsActiveExistsFileExists() {
assertActivation(true, newExistsProfile("file.txt"), context);
assertActivation(true, newExistsProfile("${project.basedir}"), context);
assertActivation(true, newExistsProfile("${project.basedir}/" + "file.txt"), context);
assertActivation(false, newMissingProfile("file.txt"), context);
assertActivation(false, newMissingProfile("${project.basedir}"), context);
assertActivation(false, newMissingProfile("${project.basedir}/" + "file.txt"), context);
}
@Test
void testIsActiveExistsLeavesFileUnchanged() {
Profile profile = newExistsProfile("file.txt");
assertEquals("file.txt", profile.getActivation().getFile().getExists());
assertActivation(true, profile, context);
assertEquals("file.txt", profile.getActivation().getFile().getExists());
}
private Profile newExistsProfile(String filePath) {
ActivationFile activationFile =
ActivationFile.newBuilder().exists(filePath).build();
return newProfile(activationFile);
}
private Profile newMissingProfile(String filePath) {
ActivationFile activationFile =
ActivationFile.newBuilder().missing(filePath).build();
return newProfile(activationFile);
}
private Profile newProfile(ActivationFile activationFile) {
Activation activation = Activation.newBuilder().file(activationFile).build();
return Profile.newBuilder().activation(activation).build();
}
}

View File

@ -0,0 +1,198 @@
/*
* 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.internal.impl.model.profile;
import java.util.Properties;
import org.apache.maven.api.model.Activation;
import org.apache.maven.api.model.Profile;
import org.apache.maven.api.services.model.ProfileActivationContext;
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.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
/**
* Tests {@link JdkVersionProfileActivator}.
*
*/
class JdkVersionProfileActivatorTest extends AbstractProfileActivatorTest<JdkVersionProfileActivator> {
@Override
@BeforeEach
void setUp() throws Exception {
activator = new JdkVersionProfileActivator();
}
private Profile newProfile(String jdkVersion) {
Activation a = Activation.newBuilder().jdk(jdkVersion).build();
Profile p = Profile.newBuilder().activation(a).build();
return p;
}
private Properties newProperties(String javaVersion) {
Properties props = new Properties();
props.setProperty("java.version", javaVersion);
return props;
}
@Test
void testNullSafe() throws Exception {
Profile p = Profile.newInstance();
assertActivation(false, p, newContext(null, null));
p = p.withActivation(Activation.newInstance());
assertActivation(false, p, newContext(null, null));
}
@Test
void testPrefix() throws Exception {
Profile profile = newProfile("1.4");
assertActivation(true, profile, newContext(null, newProperties("1.4")));
assertActivation(true, profile, newContext(null, newProperties("1.4.2")));
assertActivation(true, profile, newContext(null, newProperties("1.4.2_09")));
assertActivation(true, profile, newContext(null, newProperties("1.4.2_09-b03")));
assertActivation(false, profile, newContext(null, newProperties("1.3")));
assertActivation(false, profile, newContext(null, newProperties("1.5")));
}
@Test
void testPrefixNegated() throws Exception {
Profile profile = newProfile("!1.4");
assertActivation(false, profile, newContext(null, newProperties("1.4")));
assertActivation(false, profile, newContext(null, newProperties("1.4.2")));
assertActivation(false, profile, newContext(null, newProperties("1.4.2_09")));
assertActivation(false, profile, newContext(null, newProperties("1.4.2_09-b03")));
assertActivation(true, profile, newContext(null, newProperties("1.3")));
assertActivation(true, profile, newContext(null, newProperties("1.5")));
}
@Test
void testVersionRangeInclusiveBounds() throws Exception {
Profile profile = newProfile("[1.5,1.6]");
assertActivation(false, profile, newContext(null, newProperties("1.4")));
assertActivation(false, profile, newContext(null, newProperties("1.4.2")));
assertActivation(false, profile, newContext(null, newProperties("1.4.2_09")));
assertActivation(false, profile, newContext(null, newProperties("1.4.2_09-b03")));
assertActivation(true, profile, newContext(null, newProperties("1.5")));
assertActivation(true, profile, newContext(null, newProperties("1.5.0")));
assertActivation(true, profile, newContext(null, newProperties("1.5.0_09")));
assertActivation(true, profile, newContext(null, newProperties("1.5.0_09-b03")));
assertActivation(true, profile, newContext(null, newProperties("1.5.1")));
assertActivation(true, profile, newContext(null, newProperties("1.6")));
assertActivation(true, profile, newContext(null, newProperties("1.6.0")));
assertActivation(true, profile, newContext(null, newProperties("1.6.0_09")));
assertActivation(true, profile, newContext(null, newProperties("1.6.0_09-b03")));
}
@Test
void testVersionRangeExclusiveBounds() throws Exception {
Profile profile = newProfile("(1.3,1.6)");
assertActivation(false, profile, newContext(null, newProperties("1.3")));
assertActivation(false, profile, newContext(null, newProperties("1.3.0")));
assertActivation(false, profile, newContext(null, newProperties("1.3.0_09")));
assertActivation(false, profile, newContext(null, newProperties("1.3.0_09-b03")));
assertActivation(true, profile, newContext(null, newProperties("1.3.1")));
assertActivation(true, profile, newContext(null, newProperties("1.3.1_09")));
assertActivation(true, profile, newContext(null, newProperties("1.3.1_09-b03")));
assertActivation(true, profile, newContext(null, newProperties("1.5")));
assertActivation(true, profile, newContext(null, newProperties("1.5.0")));
assertActivation(true, profile, newContext(null, newProperties("1.5.0_09")));
assertActivation(true, profile, newContext(null, newProperties("1.5.0_09-b03")));
assertActivation(true, profile, newContext(null, newProperties("1.5.1")));
assertActivation(false, profile, newContext(null, newProperties("1.6")));
}
@Test
void testVersionRangeInclusiveLowerBound() throws Exception {
Profile profile = newProfile("[1.5,)");
assertActivation(false, profile, newContext(null, newProperties("1.4")));
assertActivation(false, profile, newContext(null, newProperties("1.4.2")));
assertActivation(false, profile, newContext(null, newProperties("1.4.2_09")));
assertActivation(false, profile, newContext(null, newProperties("1.4.2_09-b03")));
assertActivation(true, profile, newContext(null, newProperties("1.5")));
assertActivation(true, profile, newContext(null, newProperties("1.5.0")));
assertActivation(true, profile, newContext(null, newProperties("1.5.0_09")));
assertActivation(true, profile, newContext(null, newProperties("1.5.0_09-b03")));
assertActivation(true, profile, newContext(null, newProperties("1.5.1")));
assertActivation(true, profile, newContext(null, newProperties("1.6")));
assertActivation(true, profile, newContext(null, newProperties("1.6.0")));
assertActivation(true, profile, newContext(null, newProperties("1.6.0_09")));
assertActivation(true, profile, newContext(null, newProperties("1.6.0_09-b03")));
}
@Test
void testVersionRangeExclusiveUpperBound() throws Exception {
Profile profile = newProfile("(,1.6)");
assertActivation(true, profile, newContext(null, newProperties("1.5")));
assertActivation(true, profile, newContext(null, newProperties("1.5.0")));
assertActivation(true, profile, newContext(null, newProperties("1.5.0_09")));
assertActivation(true, profile, newContext(null, newProperties("1.5.0_09-b03")));
assertActivation(true, profile, newContext(null, newProperties("1.5.1")));
assertActivation(false, profile, newContext(null, newProperties("1.6")));
assertActivation(false, profile, newContext(null, newProperties("1.6.0")));
assertActivation(false, profile, newContext(null, newProperties("1.6.0_09")));
assertActivation(false, profile, newContext(null, newProperties("1.6.0_09-b03")));
}
@Test
void testRubbishJavaVersion() {
Profile profile = newProfile("[1.8,)");
assertActivationWithProblems(profile, newContext(null, newProperties("Pūteketeke")), "invalid JDK version");
assertActivationWithProblems(profile, newContext(null, newProperties("rubbish")), "invalid JDK version");
assertActivationWithProblems(profile, newContext(null, newProperties("1.a.0_09")), "invalid JDK version");
assertActivationWithProblems(profile, newContext(null, newProperties("1.a.2.b")), "invalid JDK version");
}
private void assertActivationWithProblems(
Profile profile, ProfileActivationContext context, String warningContains) {
SimpleProblemCollector problems = new SimpleProblemCollector();
assertFalse(activator.isActive(profile, context, problems));
assertEquals(0, problems.getErrors().size());
assertEquals(1, problems.getWarnings().size());
assertTrue(problems.getWarnings().get(0).contains(warningContains));
}
}

View File

@ -0,0 +1,152 @@
/*
* 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.internal.impl.model.profile;
import java.util.Properties;
import org.apache.maven.api.model.Activation;
import org.apache.maven.api.model.ActivationOS;
import org.apache.maven.api.model.Profile;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
/**
* Tests {@link OperatingSystemProfileActivator}.
*
*/
class OperatingSystemProfileActivatorTest extends AbstractProfileActivatorTest<OperatingSystemProfileActivator> {
@Override
@BeforeEach
void setUp() throws Exception {
activator = new OperatingSystemProfileActivator();
}
private Profile newProfile(ActivationOS.Builder activationBuilder) {
Activation a = Activation.newBuilder().os(activationBuilder.build()).build();
Profile p = Profile.newBuilder().activation(a).build();
return p;
}
private Properties newProperties(String osName, String osVersion, String osArch) {
Properties props = new Properties();
props.setProperty("os.name", osName);
props.setProperty("os.version", osVersion);
props.setProperty("os.arch", osArch);
return props;
}
@Test
void testVersionStringComparison() throws Exception {
Profile profile = newProfile(ActivationOS.newBuilder().version("6.5.0-1014-aws"));
assertActivation(true, profile, newContext(null, newProperties("linux", "6.5.0-1014-aws", "amd64")));
assertActivation(true, profile, newContext(null, newProperties("windows", "6.5.0-1014-aws", "aarch64")));
assertActivation(false, profile, newContext(null, newProperties("linux", "3.1.0", "amd64")));
}
@Test
void testVersionRegexMatching() throws Exception {
Profile profile = newProfile(ActivationOS.newBuilder().version("regex:.*aws"));
assertActivation(true, profile, newContext(null, newProperties("linux", "6.5.0-1014-aws", "amd64")));
assertActivation(true, profile, newContext(null, newProperties("windows", "6.5.0-1014-aws", "aarch64")));
assertActivation(false, profile, newContext(null, newProperties("linux", "3.1.0", "amd64")));
}
@Test
void testName() {
Profile profile = newProfile(ActivationOS.newBuilder().name("windows"));
assertActivation(false, profile, newContext(null, newProperties("linux", "6.5.0-1014-aws", "amd64")));
assertActivation(true, profile, newContext(null, newProperties("windows", "6.5.0-1014-aws", "aarch64")));
}
@Test
void testNegatedName() {
Profile profile = newProfile(ActivationOS.newBuilder().name("!windows"));
assertActivation(true, profile, newContext(null, newProperties("linux", "6.5.0-1014-aws", "amd64")));
assertActivation(false, profile, newContext(null, newProperties("windows", "6.5.0-1014-aws", "aarch64")));
}
@Test
void testArch() {
Profile profile = newProfile(ActivationOS.newBuilder().arch("amd64"));
assertActivation(true, profile, newContext(null, newProperties("linux", "6.5.0-1014-aws", "amd64")));
assertActivation(false, profile, newContext(null, newProperties("windows", "6.5.0-1014-aws", "aarch64")));
}
@Test
void testNegatedArch() {
Profile profile = newProfile(ActivationOS.newBuilder().arch("!amd64"));
assertActivation(false, profile, newContext(null, newProperties("linux", "6.5.0-1014-aws", "amd64")));
assertActivation(true, profile, newContext(null, newProperties("windows", "6.5.0-1014-aws", "aarch64")));
}
@Test
void testFamily() {
Profile profile = newProfile(ActivationOS.newBuilder().family("windows"));
assertActivation(false, profile, newContext(null, newProperties("linux", "6.5.0-1014-aws", "amd64")));
assertActivation(true, profile, newContext(null, newProperties("windows", "6.5.0-1014-aws", "aarch64")));
}
@Test
void testNegatedFamily() {
Profile profile = newProfile(ActivationOS.newBuilder().family("!windows"));
assertActivation(true, profile, newContext(null, newProperties("linux", "6.5.0-1014-aws", "amd64")));
assertActivation(false, profile, newContext(null, newProperties("windows", "6.5.0-1014-aws", "aarch64")));
}
@Test
void testAllOsConditions() {
Profile profile = newProfile(ActivationOS.newBuilder()
.family("windows")
.name("windows")
.arch("aarch64")
.version("99"));
assertActivation(false, profile, newContext(null, newProperties("linux", "6.5.0-1014-aws", "amd64")));
assertActivation(false, profile, newContext(null, newProperties("windows", "1", "aarch64")));
assertActivation(false, profile, newContext(null, newProperties("windows", "99", "amd64")));
assertActivation(true, profile, newContext(null, newProperties("windows", "99", "aarch64")));
}
@Test
public void testCapitalOsName() {
Profile profile = newProfile(ActivationOS.newBuilder()
.family("Mac")
.name("Mac OS X")
.arch("aarch64")
.version("14.5"));
assertActivation(false, profile, newContext(null, newProperties("linux", "6.5.0-1014-aws", "amd64")));
assertActivation(false, profile, newContext(null, newProperties("windows", "1", "aarch64")));
assertActivation(false, profile, newContext(null, newProperties("windows", "99", "amd64")));
assertActivation(true, profile, newContext(null, newProperties("Mac OS X", "14.5", "aarch64")));
}
}

View File

@ -0,0 +1,168 @@
/*
* 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.internal.impl.model.profile;
import java.util.Properties;
import org.apache.maven.api.model.Activation;
import org.apache.maven.api.model.ActivationProperty;
import org.apache.maven.api.model.Profile;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
/**
* Tests {@link PropertyProfileActivator}.
*
*/
class PropertyProfileActivatorTest extends AbstractProfileActivatorTest<PropertyProfileActivator> {
@BeforeEach
@Override
void setUp() throws Exception {
activator = new PropertyProfileActivator();
}
private Profile newProfile(String key, String value) {
ActivationProperty ap =
ActivationProperty.newBuilder().name(key).value(value).build();
Activation a = Activation.newBuilder().property(ap).build();
Profile p = Profile.newBuilder().activation(a).build();
return p;
}
private Properties newProperties(String key, String value) {
Properties props = new Properties();
props.setProperty(key, value);
return props;
}
@Test
void testNullSafe() throws Exception {
Profile p = Profile.newInstance();
assertActivation(false, p, newContext(null, null));
p = p.withActivation(Activation.newInstance());
assertActivation(false, p, newContext(null, null));
}
@Test
void testWithNameOnly_UserProperty() throws Exception {
Profile profile = newProfile("prop", null);
assertActivation(true, profile, newContext(newProperties("prop", "value"), null));
assertActivation(false, profile, newContext(newProperties("prop", ""), null));
assertActivation(false, profile, newContext(newProperties("other", "value"), null));
}
@Test
void testWithNameOnly_SystemProperty() throws Exception {
Profile profile = newProfile("prop", null);
assertActivation(true, profile, newContext(null, newProperties("prop", "value")));
assertActivation(false, profile, newContext(null, newProperties("prop", "")));
assertActivation(false, profile, newContext(null, newProperties("other", "value")));
}
@Test
void testWithNegatedNameOnly_UserProperty() throws Exception {
Profile profile = newProfile("!prop", null);
assertActivation(false, profile, newContext(newProperties("prop", "value"), null));
assertActivation(true, profile, newContext(newProperties("prop", ""), null));
assertActivation(true, profile, newContext(newProperties("other", "value"), null));
}
@Test
void testWithNegatedNameOnly_SystemProperty() throws Exception {
Profile profile = newProfile("!prop", null);
assertActivation(false, profile, newContext(null, newProperties("prop", "value")));
assertActivation(true, profile, newContext(null, newProperties("prop", "")));
assertActivation(true, profile, newContext(null, newProperties("other", "value")));
}
@Test
void testWithValue_UserProperty() throws Exception {
Profile profile = newProfile("prop", "value");
assertActivation(true, profile, newContext(newProperties("prop", "value"), null));
assertActivation(false, profile, newContext(newProperties("prop", "other"), null));
assertActivation(false, profile, newContext(newProperties("prop", ""), null));
}
@Test
void testWithValue_SystemProperty() throws Exception {
Profile profile = newProfile("prop", "value");
assertActivation(true, profile, newContext(null, newProperties("prop", "value")));
assertActivation(false, profile, newContext(null, newProperties("prop", "other")));
assertActivation(false, profile, newContext(null, newProperties("other", "")));
}
@Test
void testWithNegatedValue_UserProperty() throws Exception {
Profile profile = newProfile("prop", "!value");
assertActivation(false, profile, newContext(newProperties("prop", "value"), null));
assertActivation(true, profile, newContext(newProperties("prop", "other"), null));
assertActivation(true, profile, newContext(newProperties("prop", ""), null));
}
@Test
void testWithNegatedValue_SystemProperty() throws Exception {
Profile profile = newProfile("prop", "!value");
assertActivation(false, profile, newContext(null, newProperties("prop", "value")));
assertActivation(true, profile, newContext(null, newProperties("prop", "other")));
assertActivation(true, profile, newContext(null, newProperties("other", "")));
}
@Test
void testWithValue_UserPropertyDominantOverSystemProperty() throws Exception {
Profile profile = newProfile("prop", "value");
Properties props1 = newProperties("prop", "value");
Properties props2 = newProperties("prop", "other");
assertActivation(true, profile, newContext(props1, props2));
assertActivation(false, profile, newContext(props2, props1));
}
}

View File

@ -0,0 +1,121 @@
/*
* 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.internal.impl.model.profile;
import java.util.ArrayList;
import java.util.List;
import org.apache.maven.api.model.InputLocation;
import org.apache.maven.api.model.Model;
import org.apache.maven.api.services.BuilderProblem;
import org.apache.maven.api.services.ModelBuilderException;
import org.apache.maven.api.services.ModelProblem;
import org.apache.maven.api.services.ModelProblemCollector;
import org.apache.maven.internal.impl.model.DefaultModelProblem;
/**
* A simple model problem collector for testing the model building components.
*/
public class SimpleProblemCollector implements ModelProblemCollector {
final List<ModelProblem> problems = new ArrayList<>();
@Override
public List<ModelProblem> getProblems() {
return problems;
}
@Override
public boolean hasErrors() {
return problems.stream()
.anyMatch(p -> p.getSeverity() == ModelProblem.Severity.FATAL
|| p.getSeverity() == ModelProblem.Severity.ERROR);
}
@Override
public boolean hasFatalErrors() {
return problems.stream().anyMatch(p -> p.getSeverity() == ModelProblem.Severity.FATAL);
}
@Override
public void add(
BuilderProblem.Severity severity,
ModelProblem.Version version,
String message,
InputLocation location,
Exception exception) {
add(new DefaultModelProblem(
message,
severity,
version,
null,
location != null ? location.getLineNumber() : -1,
location != null ? location.getColumnNumber() : -1,
exception));
}
@Override
public void add(ModelProblem problem) {
this.problems.add(problem);
}
@Override
public ModelBuilderException newModelBuilderException() {
throw new UnsupportedOperationException();
}
@Override
public void setSource(String location) {
throw new UnsupportedOperationException();
}
@Override
public void setSource(Model model) {
throw new UnsupportedOperationException();
}
@Override
public String getSource() {
throw new UnsupportedOperationException();
}
@Override
public void setRootModel(Model model) {
throw new UnsupportedOperationException();
}
@Override
public Model getRootModel() {
throw new UnsupportedOperationException();
}
public List<String> getErrors() {
return problems.stream()
.filter(p -> p.getSeverity() == ModelProblem.Severity.ERROR)
.map(p -> p.getMessage())
.toList();
}
public List<String> getWarnings() {
return problems.stream()
.filter(p -> p.getSeverity() == ModelProblem.Severity.WARNING)
.map(p -> p.getMessage())
.toList();
}
}