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.ArrayList;
22  import java.util.List;
23  import java.util.Map;
24  import java.util.function.Function;
25  
26  /**
27   * The {@code ConditionParser} class is responsible for parsing and evaluating expressions.
28   * It supports tokenizing the input expression and resolving custom functions passed in a map.
29   * This class implements a recursive descent parser to handle various operations including
30   * arithmetic, logical, and comparison operations, as well as function calls.
31   */
32  public class ConditionParser {
33  
34      /**
35       * A functional interface that represents an expression function to be applied
36       * to a list of arguments. Implementers can define custom functions.
37       */
38      public interface ExpressionFunction {
39          /**
40           * Applies the function to the given list of arguments.
41           *
42           * @param args the list of arguments passed to the function
43           * @return the result of applying the function
44           */
45          Object apply(List<Object> args);
46      }
47  
48      private final Map<String, ExpressionFunction> functions; // Map to store functions by their names
49      private final Function<String, String> propertyResolver; // Property resolver
50      private List<String> tokens; // List of tokens derived from the expression
51      private int current; // Keeps track of the current token index
52  
53      /**
54       * Constructs a new {@code ConditionParser} with the given function mappings.
55       *
56       * @param functions a map of function names to their corresponding {@code ExpressionFunction} implementations
57       * @param propertyResolver the property resolver
58       */
59      public ConditionParser(Map<String, ExpressionFunction> functions, Function<String, String> propertyResolver) {
60          this.functions = functions;
61          this.propertyResolver = propertyResolver;
62      }
63  
64      /**
65       * Parses the given expression and returns the result of the evaluation.
66       *
67       * @param expression the expression to be parsed
68       * @return the result of parsing and evaluating the expression
69       */
70      public Object parse(String expression) {
71          this.tokens = tokenize(expression);
72          this.current = 0;
73          return parseExpression();
74      }
75  
76      /**
77       * Tokenizes the input expression into a list of string tokens for further parsing.
78       * This method handles quoted strings, property aliases, and various operators.
79       *
80       * @param expression the expression to tokenize
81       * @return a list of tokens
82       */
83      private List<String> tokenize(String expression) {
84          List<String> tokens = new ArrayList<>();
85          StringBuilder sb = new StringBuilder();
86          char quoteType = 0;
87          boolean inPropertyReference = false;
88  
89          for (int i = 0; i < expression.length(); i++) {
90              char c = expression.charAt(i);
91  
92              if (quoteType != 0) {
93                  if (c == quoteType) {
94                      quoteType = 0;
95                      sb.append(c);
96                      tokens.add(sb.toString());
97                      sb.setLength(0);
98                  } else {
99                      sb.append(c);
100                 }
101                 continue;
102             }
103 
104             if (inPropertyReference) {
105                 if (c == '}') {
106                     inPropertyReference = false;
107                     tokens.add("${" + sb + "}");
108                     sb.setLength(0);
109                 } else {
110                     sb.append(c);
111                 }
112                 continue;
113             }
114 
115             if (c == '$' && i + 1 < expression.length() && expression.charAt(i + 1) == '{') {
116                 if (!sb.isEmpty()) {
117                     tokens.add(sb.toString());
118                     sb.setLength(0);
119                 }
120                 inPropertyReference = true;
121                 i++; // Skip the '{'
122                 continue;
123             }
124 
125             if (c == '"' || c == '\'') {
126                 if (!sb.isEmpty()) {
127                     tokens.add(sb.toString());
128                     sb.setLength(0);
129                 }
130                 quoteType = c;
131                 sb.append(c);
132             } else if (c == ' ' || c == '(' || c == ')' || c == ',' || c == '+' || c == '>' || c == '<' || c == '='
133                     || c == '!') {
134                 if (!sb.isEmpty()) {
135                     tokens.add(sb.toString());
136                     sb.setLength(0);
137                 }
138                 if (c != ' ') {
139                     if ((c == '>' || c == '<' || c == '=' || c == '!')
140                             && i + 1 < expression.length()
141                             && expression.charAt(i + 1) == '=') {
142                         tokens.add(c + "=");
143                         i++; // Skip the next character
144                     } else {
145                         tokens.add(String.valueOf(c));
146                     }
147                 }
148             } else {
149                 sb.append(c);
150             }
151         }
152 
153         if (inPropertyReference) {
154             throw new RuntimeException("Unclosed property reference: ${");
155         }
156 
157         if (!sb.isEmpty()) {
158             tokens.add(sb.toString());
159         }
160 
161         return tokens;
162     }
163 
164     /**
165      * Parses the next expression from the list of tokens.
166      *
167      * @return the parsed expression as an object
168      * @throws RuntimeException if there are unexpected tokens after the end of the expression
169      */
170     private Object parseExpression() {
171         Object result = parseLogicalOr();
172         if (current < tokens.size()) {
173             throw new RuntimeException("Unexpected tokens after end of expression");
174         }
175         return result;
176     }
177 
178     /**
179      * Parses logical OR operations.
180      *
181      * @return the result of parsing logical OR operations
182      */
183     private Object parseLogicalOr() {
184         Object left = parseLogicalAnd();
185         while (current < tokens.size() && tokens.get(current).equals("||")) {
186             current++;
187             Object right = parseLogicalAnd();
188             left = (boolean) left || (boolean) right;
189         }
190         return left;
191     }
192 
193     /**
194      * Parses logical AND operations.
195      *
196      * @return the result of parsing logical AND operations
197      */
198     private Object parseLogicalAnd() {
199         Object left = parseComparison();
200         while (current < tokens.size() && tokens.get(current).equals("&&")) {
201             current++;
202             Object right = parseComparison();
203             left = (boolean) left && (boolean) right;
204         }
205         return left;
206     }
207 
208     /**
209      * Parses comparison operations.
210      *
211      * @return the result of parsing comparison operations
212      */
213     private Object parseComparison() {
214         Object left = parseAddSubtract();
215         while (current < tokens.size()
216                 && (tokens.get(current).equals(">")
217                         || tokens.get(current).equals("<")
218                         || tokens.get(current).equals(">=")
219                         || tokens.get(current).equals("<=")
220                         || tokens.get(current).equals("==")
221                         || tokens.get(current).equals("!="))) {
222             String operator = tokens.get(current);
223             current++;
224             Object right = parseAddSubtract();
225             left = compare(left, operator, right);
226         }
227         return left;
228     }
229 
230     /**
231      * Parses addition and subtraction operations.
232      *
233      * @return the result of parsing addition and subtraction operations
234      */
235     private Object parseAddSubtract() {
236         Object left = parseMultiplyDivide();
237         while (current < tokens.size()
238                 && (tokens.get(current).equals("+") || tokens.get(current).equals("-"))) {
239             String operator = tokens.get(current);
240             current++;
241             Object right = parseMultiplyDivide();
242             if (operator.equals("+")) {
243                 left = add(left, right);
244             } else {
245                 left = subtract(left, right);
246             }
247         }
248         return left;
249     }
250 
251     /**
252      * Parses multiplication and division operations.
253      *
254      * @return the result of parsing multiplication and division operations
255      */
256     private Object parseMultiplyDivide() {
257         Object left = parseUnary();
258         while (current < tokens.size()
259                 && (tokens.get(current).equals("*") || tokens.get(current).equals("/"))) {
260             String operator = tokens.get(current);
261             current++;
262             Object right = parseUnary();
263             if (operator.equals("*")) {
264                 left = multiply(left, right);
265             } else {
266                 left = divide(left, right);
267             }
268         }
269         return left;
270     }
271 
272     /**
273      * Parses unary operations (negation).
274      *
275      * @return the result of parsing unary operations
276      */
277     private Object parseUnary() {
278         if (current < tokens.size() && tokens.get(current).equals("-")) {
279             current++;
280             Object value = parseUnary();
281             return negate(value);
282         }
283         return parseTerm();
284     }
285 
286     /**
287      * Parses individual terms (numbers, strings, booleans, parentheses, functions).
288      *
289      * @return the parsed term
290      * @throws RuntimeException if the expression ends unexpectedly or contains unknown tokens
291      */
292     private Object parseTerm() {
293         if (current >= tokens.size()) {
294             throw new RuntimeException("Unexpected end of expression");
295         }
296 
297         String token = tokens.get(current);
298         if (token.equals("(")) {
299             return parseParentheses();
300         } else if (functions.containsKey(token)) {
301             return parseFunction();
302         } else if ((token.startsWith("\"") && token.endsWith("\"")) || (token.startsWith("'") && token.endsWith("'"))) {
303             current++;
304             return token.length() > 1 ? token.substring(1, token.length() - 1) : "";
305         } else if (token.equalsIgnoreCase("true") || token.equalsIgnoreCase("false")) {
306             current++;
307             return Boolean.parseBoolean(token);
308         } else if (token.startsWith("${") && token.endsWith("}")) {
309             current++;
310             String propertyName = token.substring(2, token.length() - 1);
311             return propertyResolver.apply(propertyName);
312         } else {
313             try {
314                 current++;
315                 return Double.parseDouble(token);
316             } catch (NumberFormatException e) {
317                 // If it's not a number, treat it as a variable or unknown function
318                 return parseVariableOrUnknownFunction();
319             }
320         }
321     }
322 
323     /**
324      * Parses a token that could be either a variable or an unknown function.
325      *
326      * @return the result of parsing a variable or unknown function
327      * @throws RuntimeException if an unknown function is encountered
328      */
329     private Object parseVariableOrUnknownFunction() {
330         current--; // Move back to the token we couldn't parse as a number
331         String name = tokens.get(current);
332         current++;
333 
334         // Check if it's followed by an opening parenthesis, indicating a function call
335         if (current < tokens.size() && tokens.get(current).equals("(")) {
336             // It's a function call, parse it as such
337             List<Object> args = parseArgumentList();
338             if (functions.containsKey(name)) {
339                 return functions.get(name).apply(args);
340             } else {
341                 throw new RuntimeException("Unknown function: " + name);
342             }
343         } else {
344             // It's a variable
345             // Here you might want to handle variables differently
346             // For now, we'll throw an exception
347             throw new RuntimeException("Unknown variable: " + name);
348         }
349     }
350 
351     /**
352      * Parses a list of arguments for a function call.
353      *
354      * @return a list of parsed arguments
355      * @throws RuntimeException if there's a mismatch in parentheses
356      */
357     private List<Object> parseArgumentList() {
358         List<Object> args = new ArrayList<>();
359         current++; // Skip the opening parenthesis
360         while (current < tokens.size() && !tokens.get(current).equals(")")) {
361             args.add(parseLogicalOr());
362             if (current < tokens.size() && tokens.get(current).equals(",")) {
363                 current++;
364             }
365         }
366         if (current >= tokens.size() || !tokens.get(current).equals(")")) {
367             throw new RuntimeException("Mismatched parentheses: missing closing parenthesis in function call");
368         }
369         current++; // Skip the closing parenthesis
370         return args;
371     }
372 
373     /**
374      * Parses a function call.
375      *
376      * @return the result of the function call
377      */
378     private Object parseFunction() {
379         String functionName = tokens.get(current);
380         current++;
381         List<Object> args = parseArgumentList();
382         return functions.get(functionName).apply(args);
383     }
384 
385     /**
386      * Parses an expression within parentheses.
387      *
388      * @return the result of parsing the expression within parentheses
389      * @throws RuntimeException if there's a mismatch in parentheses
390      */
391     private Object parseParentheses() {
392         current++; // Skip the opening parenthesis
393         Object result = parseLogicalOr();
394         if (current >= tokens.size() || !tokens.get(current).equals(")")) {
395             throw new RuntimeException("Mismatched parentheses: missing closing parenthesis");
396         }
397         current++; // Skip the closing parenthesis
398         return result;
399     }
400 
401     /**
402      * Adds two objects, handling string concatenation and numeric addition.
403      *
404      * @param left the left operand
405      * @param right the right operand
406      * @return the result of the addition
407      * @throws RuntimeException if the operands cannot be added
408      */
409     private static Object add(Object left, Object right) {
410         if (left instanceof String || right instanceof String) {
411             return toString(left) + toString(right);
412         } else if (left instanceof Number && right instanceof Number) {
413             return ((Number) left).doubleValue() + ((Number) right).doubleValue();
414         } else {
415             throw new RuntimeException("Cannot add " + left + " and " + right);
416         }
417     }
418 
419     /**
420      * Negates a numeric value.
421      *
422      * @param value the value to negate
423      * @return the negated value
424      * @throws RuntimeException if the value cannot be negated
425      */
426     private Object negate(Object value) {
427         if (value instanceof Number) {
428             return -((Number) value).doubleValue();
429         }
430         throw new RuntimeException("Cannot negate non-numeric value: " + value);
431     }
432 
433     /**
434      * Subtracts the right operand from the left operand.
435      *
436      * @param left the left operand
437      * @param right the right operand
438      * @return the result of the subtraction
439      * @throws RuntimeException if the operands cannot be subtracted
440      */
441     private static Object subtract(Object left, Object right) {
442         if (left instanceof Number && right instanceof Number) {
443             return ((Number) left).doubleValue() - ((Number) right).doubleValue();
444         } else {
445             throw new RuntimeException("Cannot subtract " + right + " from " + left);
446         }
447     }
448 
449     /**
450      * Multiplies two numeric operands.
451      *
452      * @param left the left operand
453      * @param right the right operand
454      * @return the result of the multiplication
455      * @throws RuntimeException if the operands cannot be multiplied
456      */
457     private static Object multiply(Object left, Object right) {
458         if (left instanceof Number && right instanceof Number) {
459             return ((Number) left).doubleValue() * ((Number) right).doubleValue();
460         } else {
461             throw new RuntimeException("Cannot multiply " + left + " and " + right);
462         }
463     }
464 
465     /**
466      * Divides the left operand by the right operand.
467      *
468      * @param left the left operand (dividend)
469      * @param right the right operand (divisor)
470      * @return the result of the division
471      * @throws RuntimeException if the operands cannot be divided
472      * @throws ArithmeticException if attempting to divide by zero
473      */
474     private static Object divide(Object left, Object right) {
475         if (left instanceof Number && right instanceof Number) {
476             double divisor = ((Number) right).doubleValue();
477             if (divisor == 0) {
478                 throw new ArithmeticException("Division by zero");
479             }
480             return ((Number) left).doubleValue() / divisor;
481         } else {
482             throw new RuntimeException("Cannot divide " + left + " by " + right);
483         }
484     }
485 
486     /**
487      * Compares two objects based on the given operator.
488      * Supports comparison of numbers and strings, and equality checks for null values.
489      *
490      * @param left the left operand
491      * @param operator the comparison operator (">", "<", ">=", "<=", "==", or "!=")
492      * @param right the right operand
493      * @return the result of the comparison (a boolean value)
494      * @throws IllegalStateException if an unknown operator is provided
495      * @throws RuntimeException if the operands cannot be compared
496      */
497     private static Object compare(Object left, String operator, Object right) {
498         if (left == null && right == null) {
499             return true;
500         }
501         if (left == null || right == null) {
502             if ("==".equals(operator)) {
503                 return false;
504             } else if ("!=".equals(operator)) {
505                 return true;
506             }
507         }
508         if (left instanceof Number && right instanceof Number) {
509             double leftVal = ((Number) left).doubleValue();
510             double rightVal = ((Number) right).doubleValue();
511             return switch (operator) {
512                 case ">" -> leftVal > rightVal;
513                 case "<" -> leftVal < rightVal;
514                 case ">=" -> leftVal >= rightVal;
515                 case "<=" -> leftVal <= rightVal;
516                 case "==" -> Math.abs(leftVal - rightVal) < 1e-9;
517                 case "!=" -> Math.abs(leftVal - rightVal) >= 1e-9;
518                 default -> throw new IllegalStateException("Unknown operator: " + operator);
519             };
520         } else if (left instanceof String && right instanceof String) {
521             int comparison = ((String) left).compareTo((String) right);
522             return switch (operator) {
523                 case ">" -> comparison > 0;
524                 case "<" -> comparison < 0;
525                 case ">=" -> comparison >= 0;
526                 case "<=" -> comparison <= 0;
527                 case "==" -> comparison == 0;
528                 case "!=" -> comparison != 0;
529                 default -> throw new IllegalStateException("Unknown operator: " + operator);
530             };
531         }
532         throw new RuntimeException("Cannot compare " + left + " and " + right + " with operator " + operator);
533     }
534 
535     /**
536      * Converts an object to a string representation.
537      * If the object is a {@code Double}, it formats it without any decimal places.
538      * Otherwise, it uses the {@code String.valueOf} method.
539      *
540      * @param value the object to convert to a string
541      * @return the string representation of the object
542      */
543     public static String toString(Object value) {
544         if (value instanceof Double || value instanceof Float) {
545             double doubleValue = ((Number) value).doubleValue();
546             if (doubleValue == Math.floor(doubleValue) && !Double.isInfinite(doubleValue)) {
547                 return String.format("%.0f", doubleValue);
548             }
549         }
550         return String.valueOf(value);
551     }
552 
553     /**
554      * Converts an object to a boolean value.
555      * If the object is:
556      * - a {@code Boolean}, returns its value directly.
557      * - a {@code String}, returns {@code true} if the string is non-blank.
558      * - a {@code Number}, returns {@code true} if its integer value is not zero.
559      * For other object types, returns {@code true} if the object is non-null.
560      *
561      * @param value the object to convert to a boolean
562      * @return the boolean representation of the object
563      */
564     public static Boolean toBoolean(Object value) {
565         if (value instanceof Boolean b) {
566             return b; // Returns the boolean value
567         } else if (value instanceof String s) {
568             return !s.isBlank(); // True if the string is not blank
569         } else if (value instanceof Number b) {
570             return b.intValue() != 0; // True if the number is not zero
571         } else {
572             return value != null; // True if the object is not null
573         }
574     }
575 
576     /**
577      * Converts an object to a double value.
578      * If the object is:
579      * - a {@code Number}, returns its double value.
580      * - a {@code String}, tries to parse it as a double.
581      * - a {@code Boolean}, returns {@code 1.0} for {@code true}, {@code 0.0} for {@code false}.
582      * If the object cannot be converted, a {@code RuntimeException} is thrown.
583      *
584      * @param value the object to convert to a double
585      * @return the double representation of the object
586      * @throws RuntimeException if the object cannot be converted to a double
587      */
588     public static double toDouble(Object value) {
589         if (value instanceof Number) {
590             return ((Number) value).doubleValue(); // Converts number to double
591         } else if (value instanceof String) {
592             try {
593                 return Double.parseDouble((String) value); // Tries to parse string as double
594             } catch (NumberFormatException e) {
595                 throw new RuntimeException("Cannot convert string to number: " + value);
596             }
597         } else if (value instanceof Boolean) {
598             return ((Boolean) value) ? 1.0 : 0.0; // True = 1.0, False = 0.0
599         } else {
600             throw new RuntimeException("Cannot convert to number: " + value);
601         }
602     }
603 
604     /**
605      * Converts an object to an integer value.
606      * If the object is:
607      * - a {@code Number}, returns its integer value.
608      * - a {@code String}, tries to parse it as an integer, or as a double then converted to an integer.
609      * - a {@code Boolean}, returns {@code 1} for {@code true}, {@code 0} for {@code false}.
610      * If the object cannot be converted, a {@code RuntimeException} is thrown.
611      *
612      * @param value the object to convert to an integer
613      * @return the integer representation of the object
614      * @throws RuntimeException if the object cannot be converted to an integer
615      */
616     public static int toInt(Object value) {
617         if (value instanceof Number) {
618             return ((Number) value).intValue(); // Converts number to int
619         } else if (value instanceof String) {
620             try {
621                 return Integer.parseInt((String) value); // Tries to parse string as int
622             } catch (NumberFormatException e) {
623                 // If string is not an int, tries parsing as double and converting to int
624                 try {
625                     return (int) Double.parseDouble((String) value);
626                 } catch (NumberFormatException e2) {
627                     throw new RuntimeException("Cannot convert string to integer: " + value);
628                 }
629             }
630         } else if (value instanceof Boolean) {
631             return ((Boolean) value) ? 1 : 0; // True = 1, False = 0
632         } else {
633             throw new RuntimeException("Cannot convert to integer: " + value);
634         }
635     }
636 }