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.nio.file.Path;
22  import java.util.HashMap;
23  import java.util.Map;
24  import java.util.function.Function;
25  
26  import org.apache.maven.api.di.Inject;
27  import org.apache.maven.api.di.Named;
28  import org.apache.maven.api.di.Singleton;
29  import org.apache.maven.api.model.Activation;
30  import org.apache.maven.api.model.Profile;
31  import org.apache.maven.api.services.BuilderProblem.Severity;
32  import org.apache.maven.api.services.ModelProblem.Version;
33  import org.apache.maven.api.services.ModelProblemCollector;
34  import org.apache.maven.api.services.VersionParser;
35  import org.apache.maven.api.services.model.ProfileActivationContext;
36  import org.apache.maven.api.services.model.ProfileActivator;
37  import org.apache.maven.api.services.model.RootLocator;
38  import org.apache.maven.internal.impl.model.DefaultInterpolator;
39  import org.apache.maven.internal.impl.model.ProfileActivationFilePathInterpolator;
40  
41  import static org.apache.maven.internal.impl.model.profile.ConditionParser.toBoolean;
42  
43  /**
44   * This class is responsible for activating profiles based on conditions specified in the profile's activation section.
45   * It evaluates the condition expression and determines whether the profile should be active.
46   */
47  @Named("condition")
48  @Singleton
49  public class ConditionProfileActivator implements ProfileActivator {
50  
51      private final VersionParser versionParser;
52      private final ProfileActivationFilePathInterpolator interpolator;
53      private final RootLocator rootLocator;
54  
55      /**
56       * Constructs a new ConditionProfileActivator with the necessary dependencies.
57       *
58       * @param versionParser The parser for handling version comparisons
59       * @param interpolator The interpolator for resolving file paths
60       * @param rootLocator The locator for finding the project root directory
61       */
62      @Inject
63      public ConditionProfileActivator(
64              VersionParser versionParser, ProfileActivationFilePathInterpolator interpolator, RootLocator rootLocator) {
65          this.versionParser = versionParser;
66          this.interpolator = interpolator;
67          this.rootLocator = rootLocator;
68      }
69  
70      /**
71       * Determines whether a profile should be active based on its condition.
72       *
73       * @param profile The profile to evaluate
74       * @param context The context in which the profile is being evaluated
75       * @param problems A collector for any problems encountered during evaluation
76       * @return true if the profile should be active, false otherwise
77       */
78      @Override
79      public boolean isActive(Profile profile, ProfileActivationContext context, ModelProblemCollector problems) {
80          if (profile.getActivation() == null || profile.getActivation().getCondition() == null) {
81              return false;
82          }
83          String condition = profile.getActivation().getCondition();
84          try {
85              Map<String, ConditionParser.ExpressionFunction> functions =
86                      registerFunctions(context, versionParser, interpolator);
87              Function<String, String> propertyResolver = s -> property(context, rootLocator, s);
88              return toBoolean(new ConditionParser(functions, propertyResolver).parse(condition));
89          } catch (Exception e) {
90              problems.add(
91                      Severity.ERROR, Version.V41, "Error parsing profile activation condition: " + e.getMessage(), e);
92              return false;
93          }
94      }
95  
96      /**
97       * Checks if the condition is present in the profile's configuration.
98       *
99       * @param profile The profile to check
100      * @param context The context in which the profile is being evaluated
101      * @param problems A collector for any problems encountered during evaluation
102      * @return true if the condition is present and not blank, false otherwise
103      */
104     @Override
105     public boolean presentInConfig(Profile profile, ProfileActivationContext context, ModelProblemCollector problems) {
106         Activation activation = profile.getActivation();
107         if (activation == null) {
108             return false;
109         }
110         return activation.getCondition() != null && !activation.getCondition().isBlank();
111     }
112 
113     /**
114      * Registers the condition functions that can be used in profile activation expressions.
115      *
116      * @param context The profile activation context
117      * @param versionParser The parser for handling version comparisons
118      * @param interpolator The interpolator for resolving file paths
119      * @return A map of function names to their implementations
120      */
121     public static Map<String, ConditionParser.ExpressionFunction> registerFunctions(
122             ProfileActivationContext context,
123             VersionParser versionParser,
124             ProfileActivationFilePathInterpolator interpolator) {
125         Map<String, ConditionParser.ExpressionFunction> functions = new HashMap<>();
126 
127         ConditionFunctions conditionFunctions = new ConditionFunctions(context, versionParser, interpolator);
128 
129         for (java.lang.reflect.Method method : ConditionFunctions.class.getDeclaredMethods()) {
130             String methodName = method.getName();
131             if (methodName.endsWith("_")) {
132                 methodName = methodName.substring(0, methodName.length() - 1);
133             }
134             final String finalMethodName = methodName;
135 
136             functions.put(finalMethodName, args -> {
137                 try {
138                     return method.invoke(conditionFunctions, args);
139                 } catch (Exception e) {
140                     StringBuilder causeChain = new StringBuilder();
141                     Throwable cause = e;
142                     while (cause != null) {
143                         if (!causeChain.isEmpty()) {
144                             causeChain.append(" Caused by: ");
145                         }
146                         causeChain.append(cause.toString());
147                         cause = cause.getCause();
148                     }
149                     throw new RuntimeException(
150                             "Error invoking function '" + finalMethodName + "': " + e + ". Cause chain: " + causeChain,
151                             e);
152                 }
153             });
154         }
155 
156         return functions;
157     }
158 
159     /**
160      * Retrieves the value of a property from the project context.
161      * Special function used to support the <code>${property}</code> syntax.
162      *
163      * The profile activation is done twice: once on the file model (so the model
164      * which has just been read from the file) and once while computing the effective
165      * model (so the model which will be used to build the project). We do need
166      * those two activations to be consistent, so we need to restrict access to
167      * properties that cannot change between file and effective model.
168      *
169      * @param name The property name
170      * @return The value of the property, or null if not found
171      * @throws IllegalArgumentException if the number of arguments is not exactly one
172      */
173     static String property(ProfileActivationContext context, RootLocator rootLocator, String name) {
174         String value = doGetProperty(context, rootLocator, name);
175         return new DefaultInterpolator().interpolate(value, s -> doGetProperty(context, rootLocator, s));
176     }
177 
178     static String doGetProperty(ProfileActivationContext context, RootLocator rootLocator, String name) {
179         // Handle special project-related properties
180         if ("project.basedir".equals(name)) {
181             Path basedir = context.getModel().getProjectDirectory();
182             return basedir != null ? basedir.toFile().getAbsolutePath() : null;
183         }
184         if ("project.rootDirectory".equals(name)) {
185             Path basedir = context.getModel().getProjectDirectory();
186             if (basedir != null) {
187                 Path root = rootLocator.findMandatoryRoot(basedir);
188                 return root.toFile().getAbsolutePath();
189             }
190             return null;
191         }
192         if ("project.artifactId".equals(name)) {
193             return context.getModel().getArtifactId();
194         }
195         if ("project.packaging".equals(name)) {
196             return context.getModel().getPackaging();
197         }
198 
199         // Check user properties
200         String v = context.getUserProperties().get(name);
201         if (v == null) {
202             // Check project properties
203             // TODO: this may leads to instability between file model activation and effective model activation
204             //       as the effective model properties may be different from the file model
205             v = context.getModel().getProperties().get(name);
206         }
207         if (v == null) {
208             // Check system properties
209             v = context.getSystemProperties().get(name);
210         }
211         return v;
212     }
213 }