View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  package org.apache.maven.internal.impl.model.profile;
20  
21  import java.util.HashMap;
22  import java.util.Map;
23  import java.util.function.Function;
24  
25  import org.apache.maven.api.di.Inject;
26  import org.apache.maven.api.di.Named;
27  import org.apache.maven.api.di.Singleton;
28  import org.apache.maven.api.model.Activation;
29  import org.apache.maven.api.model.Profile;
30  import org.apache.maven.api.services.BuilderProblem.Severity;
31  import org.apache.maven.api.services.ModelProblem.Version;
32  import org.apache.maven.api.services.ModelProblemCollector;
33  import org.apache.maven.api.services.VersionParser;
34  import org.apache.maven.api.services.model.ProfileActivationContext;
35  import org.apache.maven.api.services.model.ProfileActivator;
36  import org.apache.maven.internal.impl.model.DefaultInterpolator;
37  
38  import static org.apache.maven.internal.impl.model.profile.ConditionParser.toBoolean;
39  
40  /**
41   * This class is responsible for activating profiles based on conditions specified in the profile's activation section.
42   * It evaluates the condition expression and determines whether the profile should be active.
43   */
44  @Named("condition")
45  @Singleton
46  public class ConditionProfileActivator implements ProfileActivator {
47  
48      private final VersionParser versionParser;
49  
50      /**
51       * Constructs a new ConditionProfileActivator with the necessary dependencies.
52       *
53       * @param versionParser The parser for handling version comparisons
54       */
55      @Inject
56      public ConditionProfileActivator(VersionParser versionParser) {
57          this.versionParser = versionParser;
58      }
59  
60      /**
61       * Determines whether a profile should be active based on its condition.
62       *
63       * @param profile The profile to evaluate
64       * @param context The context in which the profile is being evaluated
65       * @param problems A collector for any problems encountered during evaluation
66       * @return true if the profile should be active, false otherwise
67       */
68      @Override
69      public boolean isActive(Profile profile, ProfileActivationContext context, ModelProblemCollector problems) {
70          if (profile.getActivation() == null || profile.getActivation().getCondition() == null) {
71              return false;
72          }
73          String condition = profile.getActivation().getCondition();
74          try {
75              Map<String, ConditionParser.ExpressionFunction> functions = registerFunctions(context, versionParser);
76              Function<String, String> propertyResolver = s -> property(context, s);
77              return toBoolean(new ConditionParser(functions, propertyResolver).parse(condition));
78          } catch (Exception e) {
79              problems.add(
80                      Severity.ERROR, Version.V41, "Error parsing profile activation condition: " + e.getMessage(), e);
81              return false;
82          }
83      }
84  
85      /**
86       * Checks if the condition is present in the profile's configuration.
87       *
88       * @param profile The profile to check
89       * @param context The context in which the profile is being evaluated
90       * @param problems A collector for any problems encountered during evaluation
91       * @return true if the condition is present and not blank, false otherwise
92       */
93      @Override
94      public boolean presentInConfig(Profile profile, ProfileActivationContext context, ModelProblemCollector problems) {
95          Activation activation = profile.getActivation();
96          if (activation == null) {
97              return false;
98          }
99          return activation.getCondition() != null && !activation.getCondition().isBlank();
100     }
101 
102     /**
103      * Registers the condition functions that can be used in profile activation expressions.
104      *
105      * @param context The profile activation context
106      * @param versionParser The parser for handling version comparisons
107      * @return A map of function names to their implementations
108      */
109     public static Map<String, ConditionParser.ExpressionFunction> registerFunctions(
110             ProfileActivationContext context, VersionParser versionParser) {
111         Map<String, ConditionParser.ExpressionFunction> functions = new HashMap<>();
112 
113         ConditionFunctions conditionFunctions = new ConditionFunctions(context, versionParser);
114 
115         for (java.lang.reflect.Method method : ConditionFunctions.class.getDeclaredMethods()) {
116             String methodName = method.getName();
117             if (methodName.endsWith("_")) {
118                 methodName = methodName.substring(0, methodName.length() - 1);
119             }
120             final String finalMethodName = methodName;
121 
122             functions.put(finalMethodName, args -> {
123                 try {
124                     return method.invoke(conditionFunctions, args);
125                 } catch (Exception e) {
126                     StringBuilder causeChain = new StringBuilder();
127                     Throwable cause = e;
128                     while (cause != null) {
129                         if (!causeChain.isEmpty()) {
130                             causeChain.append(" Caused by: ");
131                         }
132                         causeChain.append(cause.toString());
133                         cause = cause.getCause();
134                     }
135                     throw new RuntimeException(
136                             "Error invoking function '" + finalMethodName + "': " + e + ". Cause chain: " + causeChain,
137                             e);
138                 }
139             });
140         }
141 
142         return functions;
143     }
144 
145     /**
146      * Retrieves the value of a property from the project context.
147      * Special function used to support the <code>${property}</code> syntax.
148      *
149      * The profile activation is done twice: once on the file model (so the model
150      * which has just been read from the file) and once while computing the effective
151      * model (so the model which will be used to build the project). We do need
152      * those two activations to be consistent, so we need to restrict access to
153      * properties that cannot change between file and effective model.
154      *
155      * @param name The property name
156      * @return The value of the property, or null if not found
157      * @throws IllegalArgumentException if the number of arguments is not exactly one
158      */
159     static String property(ProfileActivationContext context, String name) {
160         String value = doGetProperty(context, name);
161         return new DefaultInterpolator().interpolate(value, s -> doGetProperty(context, s));
162     }
163 
164     static String doGetProperty(ProfileActivationContext context, String name) {
165         // Handle special project-related properties
166         if ("project.basedir".equals(name)) {
167             return context.getModelBaseDirectory();
168         }
169         if ("project.rootDirectory".equals(name)) {
170             return context.getModelRootDirectory();
171         }
172         if ("project.artifactId".equals(name)) {
173             return context.getModelArtifactId();
174         }
175         if ("project.packaging".equals(name)) {
176             return context.getModelPackaging();
177         }
178 
179         // Check user properties
180         String v = context.getUserProperty(name);
181         if (v == null) {
182             // Check project properties
183             // TODO: this may leads to instability between file model activation and effective model activation
184             //       as the effective model properties may be different from the file model
185             v = context.getModelProperty(name);
186         }
187         if (v == null) {
188             // Check system properties
189             v = context.getSystemProperty(name);
190         }
191         return v;
192     }
193 }