ConditionProfileActivator.java

/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */
package org.apache.maven.impl.model.profile;

import java.util.HashMap;
import java.util.Map;
import java.util.function.UnaryOperator;

import org.apache.maven.api.di.Inject;
import org.apache.maven.api.di.Named;
import org.apache.maven.api.di.Singleton;
import org.apache.maven.api.model.Activation;
import org.apache.maven.api.model.Profile;
import org.apache.maven.api.services.BuilderProblem.Severity;
import org.apache.maven.api.services.Interpolator;
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 static org.apache.maven.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 Interpolator interpolator;

    /**
     * Constructs a new ConditionProfileActivator with the necessary dependencies.
     *
     * @param versionParser The parser for handling version comparisons
     * @param interpolator  The interpolator for interpolating the values in the given map using the provided callback function
     */
    @Inject
    public ConditionProfileActivator(VersionParser versionParser, Interpolator interpolator) {
        this.versionParser = versionParser;
        this.interpolator = interpolator;
    }

    /**
     * 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);
            UnaryOperator<String> propertyResolver = s -> property(context, 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
     * @return A map of function names to their implementations
     */
    public Map<String, ConditionParser.ExpressionFunction> registerFunctions(
            ProfileActivationContext context, VersionParser versionParser) {
        Map<String, ConditionParser.ExpressionFunction> functions = new HashMap<>();

        ConditionFunctions conditionFunctions = new ConditionFunctions(context, versionParser);

        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);
                        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
     */
    String property(ProfileActivationContext context, String name) {
        String value = doGetProperty(context, name);
        return interpolator.interpolate(value, s -> doGetProperty(context, s));
    }

    static String doGetProperty(ProfileActivationContext context, String name) {
        // Handle special project-related properties
        if ("project.basedir".equals(name)) {
            return context.getModelBaseDirectory();
        }
        if ("project.rootDirectory".equals(name)) {
            return context.getModelRootDirectory();
        }
        if ("project.artifactId".equals(name)) {
            return context.getModelArtifactId();
        }
        if ("project.packaging".equals(name)) {
            return context.getModelPackaging();
        }

        // Check user properties
        String v = context.getUserProperty(name);
        if (v == null) {
            // Check project properties
            // TODO: this may leads to instability between file model activation and effective model activation
            //       as the effective model properties may be different from the file model
            v = context.getModelProperty(name);
        }
        if (v == null) {
            // Check system properties
            v = context.getSystemProperty(name);
        }
        return v;
    }
}