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.impl.model.profile;
20  
21  import java.util.HashMap;
22  import java.util.Map;
23  import java.util.function.UnaryOperator;
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.Interpolator;
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  
38  import static org.apache.maven.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      private final Interpolator interpolator;
51  
52      /**
53       * Constructs a new ConditionProfileActivator with the necessary dependencies.
54       *
55       * @param versionParser The parser for handling version comparisons
56       * @param interpolator  The interpolator for interpolating the values in the given map using the provided callback function
57       */
58      @Inject
59      public ConditionProfileActivator(VersionParser versionParser, Interpolator interpolator) {
60          this.versionParser = versionParser;
61          this.interpolator = interpolator;
62      }
63  
64      /**
65       * Determines whether a profile should be active based on its condition.
66       *
67       * @param profile The profile to evaluate
68       * @param context The context in which the profile is being evaluated
69       * @param problems A collector for any problems encountered during evaluation
70       * @return true if the profile should be active, false otherwise
71       */
72      @Override
73      public boolean isActive(Profile profile, ProfileActivationContext context, ModelProblemCollector problems) {
74          if (profile.getActivation() == null || profile.getActivation().getCondition() == null) {
75              return false;
76          }
77          String condition = profile.getActivation().getCondition();
78          try {
79              Map<String, ConditionParser.ExpressionFunction> functions = registerFunctions(context, versionParser);
80              UnaryOperator<String> propertyResolver = s -> property(context, s);
81              return toBoolean(new ConditionParser(functions, propertyResolver).parse(condition));
82          } catch (Exception e) {
83              problems.add(
84                      Severity.ERROR, Version.V41, "Error parsing profile activation condition: " + e.getMessage(), e);
85              return false;
86          }
87      }
88  
89      /**
90       * Checks if the condition is present in the profile's configuration.
91       *
92       * @param profile The profile to check
93       * @param context The context in which the profile is being evaluated
94       * @param problems A collector for any problems encountered during evaluation
95       * @return true if the condition is present and not blank, false otherwise
96       */
97      @Override
98      public boolean presentInConfig(Profile profile, ProfileActivationContext context, ModelProblemCollector problems) {
99          Activation activation = profile.getActivation();
100         if (activation == null) {
101             return false;
102         }
103         return activation.getCondition() != null && !activation.getCondition().isBlank();
104     }
105 
106     /**
107      * Registers the condition functions that can be used in profile activation expressions.
108      *
109      * @param context The profile activation context
110      * @param versionParser The parser for handling version comparisons
111      * @return A map of function names to their implementations
112      */
113     public Map<String, ConditionParser.ExpressionFunction> registerFunctions(
114             ProfileActivationContext context, VersionParser versionParser) {
115         Map<String, ConditionParser.ExpressionFunction> functions = new HashMap<>();
116 
117         ConditionFunctions conditionFunctions = new ConditionFunctions(context, versionParser);
118 
119         for (java.lang.reflect.Method method : ConditionFunctions.class.getDeclaredMethods()) {
120             String methodName = method.getName();
121             if (methodName.endsWith("_")) {
122                 methodName = methodName.substring(0, methodName.length() - 1);
123             }
124             final String finalMethodName = methodName;
125 
126             functions.put(finalMethodName, args -> {
127                 try {
128                     return method.invoke(conditionFunctions, args);
129                 } catch (Exception e) {
130                     StringBuilder causeChain = new StringBuilder();
131                     Throwable cause = e;
132                     while (cause != null) {
133                         if (!causeChain.isEmpty()) {
134                             causeChain.append(" Caused by: ");
135                         }
136                         causeChain.append(cause.toString());
137                         cause = cause.getCause();
138                     }
139                     throw new RuntimeException(
140                             "Error invoking function '" + finalMethodName + "': " + e + ". Cause chain: " + causeChain,
141                             e);
142                 }
143             });
144         }
145 
146         return functions;
147     }
148 
149     /**
150      * Retrieves the value of a property from the project context.
151      * Special function used to support the <code>${property}</code> syntax.
152      *
153      * The profile activation is done twice: once on the file model (so the model
154      * which has just been read from the file) and once while computing the effective
155      * model (so the model which will be used to build the project). We do need
156      * those two activations to be consistent, so we need to restrict access to
157      * properties that cannot change between file and effective model.
158      *
159      * @param name The property name
160      * @return The value of the property, or null if not found
161      * @throws IllegalArgumentException if the number of arguments is not exactly one
162      */
163     String property(ProfileActivationContext context, String name) {
164         String value = doGetProperty(context, name);
165         return interpolator.interpolate(value, s -> doGetProperty(context, s));
166     }
167 
168     static String doGetProperty(ProfileActivationContext context, String name) {
169         // Handle special project-related properties
170         if ("project.basedir".equals(name)) {
171             return context.getModelBaseDirectory();
172         }
173         if ("project.rootDirectory".equals(name)) {
174             return context.getModelRootDirectory();
175         }
176         if ("project.artifactId".equals(name)) {
177             return context.getModelArtifactId();
178         }
179         if ("project.packaging".equals(name)) {
180             return context.getModelPackaging();
181         }
182 
183         // Check user properties
184         String v = context.getUserProperty(name);
185         if (v == null) {
186             // Check project properties
187             // TODO: this may leads to instability between file model activation and effective model activation
188             //       as the effective model properties may be different from the file model
189             v = context.getModelProperty(name);
190         }
191         if (v == null) {
192             // Check system properties
193             v = context.getSystemProperty(name);
194         }
195         return v;
196     }
197 }