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 }