001/* 002 * Logback: the reliable, generic, fast and flexible logging framework. 003 * Copyright (C) 1999-2026, QOS.ch. All rights reserved. 004 * 005 * This program and the accompanying materials are dual-licensed under 006 * either the terms of the Eclipse Public License v2.0 as published by 007 * the Eclipse Foundation 008 * 009 * or (per the licensee's choosing) 010 * 011 * under the terms of the GNU Lesser General Public License version 2.1 012 * as published by the Free Software Foundation. 013 */ 014 015package ch.qos.logback.core.boolex; 016 017import ch.qos.logback.core.util.IntHolder; 018 019import java.util.ArrayList; 020import java.util.HashMap; 021import java.util.List; 022import java.util.Map; 023import java.util.Stack; 024import java.util.function.BiFunction; 025import java.util.function.Function; 026 027/** 028 * This class evaluates boolean expressions based on property lookups. 029 * <p>It supports logical operators (NOT, AND, OR), boolean literals ({@code true} and 030 * {@code false}), and functions like isNull, isDefined, propertyEquals, and propertyContains. 031 * Expressions are parsed using the Shunting-Yard algorithm into Reverse Polish Notation (RPN) 032 * for evaluation. 033 * </p> 034 * 035 * <p>Example expression: {@code true && isDefined("key1") && propertyEquals("key2", "value")}</p> 036 * 037 * <p>Properties are resolved via {@link PropertyConditionBase#property(String)}.</p> 038 * 039 * @since 1.5.24 040 */ 041public class ExpressionPropertyCondition extends PropertyConditionBase { 042 043 044 public static final String MALFORMED_EXPRESSION = "Malformed expression: "; 045 046 /** 047 * A map that associates a string key with a function for evaluating boolean conditions. 048 * 049 * <p>This map defines the known functions. It can be overridden by subclasses to define 050 * new functions.</p> 051 * 052 * <p>In the context of this class, a function is a function that takes a String 053 * argument and returns a boolean.</p> 054 */ 055 protected Map<String, Function<String, Boolean>> functionMap = new HashMap<>(); 056 057 058 /** 059 * A map that associates a string key with a bi-function for evaluating boolean conditions. 060 * 061 * <p>This map defines the known bi-functions. It can be overridden by subclasses to define 062 * new bi-functions.</p> 063 * 064 * <p>In the context of this class, a bi-function is a function that takes two String 065 * arguments and returns a boolean.</p> 066 */ 067 protected Map<String, BiFunction<String, String, Boolean>> biFunctionMap = new HashMap<>(); 068 069 private static final String IS_NULL_FUNCTION_KEY = "isNull"; 070 private static final String IS_DEFINED_FUNCTION_KEY = "isDefined"; 071 072 private static final String PROPERTY_EQUALS_FUNCTION_KEY = "propertyEquals"; 073 private static final String PROPERTY_CONTAINS_FUNCTION_KEY = "propertyContains"; 074 075 private static final String TRUE_LITERAL = "true"; 076 private static final String FALSE_LITERAL = "false"; 077 078 private static final char QUOTE = '"'; 079 private static final char COMMA = ','; 080 private static final char LEFT_PAREN = '('; 081 private static final char RIGHT_PAREN = ')'; 082 083 084 private static final char NOT_CHAR = '!'; 085 private static final char AMPERSAND_CHAR = '&'; 086 private static final char OR_CHAR = '|'; 087 088 enum Associativity { 089 LEFT, RIGHT; 090 } 091 092 enum TokenType { 093 NOT, AND, OR, TRUE, FALSE, FUNCTION, BI_FUNCTION, LEFT_PAREN, RIGHT_PAREN; 094 095 boolean isLogicalOperator() { 096 return this == NOT || this == AND || this == OR; 097 } 098 } 099 100 101 static class Token { 102 TokenType tokenType; 103 String functionName; 104 String param0; 105 String param1; 106 107 Token(TokenType tokenType) { 108 this.tokenType = tokenType; 109 110 switch (tokenType) { 111 case LEFT_PAREN: 112 case RIGHT_PAREN: 113 case NOT: 114 case AND: 115 case OR: 116 case TRUE: 117 case FALSE: 118 break; 119 default: 120 throw new IllegalStateException("Unexpected value: " + tokenType); 121 } 122 } 123 124 Token(TokenType tokenType, String functionName, String propertyKey, String value) { 125 this.tokenType = tokenType; 126 this.functionName = functionName; 127 this.param0 = propertyKey; 128 this.param1 = value; 129 } 130 131 public static Token valueOf(char c) { 132 133 if (c == LEFT_PAREN) 134 return new Token(TokenType.LEFT_PAREN); 135 if (c == RIGHT_PAREN) 136 return new Token(TokenType.RIGHT_PAREN); 137 throw new IllegalArgumentException("Unexpected char: " + c); 138 } 139 } 140 141 142 String expression; 143 List<Token> rpn; 144 145 /** 146 * Constructs an ExpressionPropertyCondition and initializes the function maps 147 * with supported unary and binary functions. 148 */ 149 public ExpressionPropertyCondition() { 150 functionMap.put(IS_NULL_FUNCTION_KEY, this::isNull); 151 functionMap.put(IS_DEFINED_FUNCTION_KEY, this::isDefined); 152 biFunctionMap.put(PROPERTY_EQUALS_FUNCTION_KEY, this::propertyEquals); 153 biFunctionMap.put(PROPERTY_CONTAINS_FUNCTION_KEY, this::propertyContains); 154 } 155 156 /** 157 * Starts the condition by parsing the expression into tokens and converting 158 * them to Reverse Polish Notation (RPN) for evaluation. 159 * 160 * <p>In case of malformed expression, the instance will not enter the "started" state.</p> 161 */ 162 public void start() { 163 if (expression == null || expression.isEmpty()) { 164 addError("Empty expression"); 165 return; 166 } 167 168 try { 169 List<Token> tokens = tokenize(expression.trim()); 170 this.rpn = infixToReversePolishNotation(tokens); 171 } catch (IllegalArgumentException|IllegalStateException e) { 172 addError(MALFORMED_EXPRESSION + e.getMessage()); 173 return; 174 } 175 super.start(); 176 } 177 178 /** 179 * Returns the current expression string. 180 * 181 * @return the expression, or null if not set 182 */ 183 public String getExpression() { 184 return expression; 185 } 186 187 /** 188 * Sets the expression to be evaluated. 189 * 190 * @param expression the boolean expression string 191 */ 192 public void setExpression(String expression) { 193 this.expression = expression; 194 } 195 196 /** 197 * Evaluates the parsed expression against the current property context. 198 * 199 * <p>If the instance is not in started state, returns false.</p> 200 * 201 * @return true if the expression evaluates to true, false otherwise 202 */ 203 @Override 204 public boolean evaluate() { 205 if (!isStarted()) { 206 return false; 207 } 208 return evaluateRPN(rpn); 209 } 210 211 /** 212 * Tokenizes the input expression string into a list of tokens, handling 213 * functions, operators, and parentheses. 214 * 215 * @param expr the expression string to tokenize 216 * @return list of tokens 217 * @throws IllegalArgumentException if the expression is malformed 218 */ 219 private List<Token> tokenize(String expr) throws IllegalArgumentException, IllegalStateException { 220 List<Token> tokens = new ArrayList<>(); 221 222 int i = 0; 223 while (i < expr.length()) { 224 char c = expr.charAt(i); 225 226 if (Character.isWhitespace(c)) { 227 i++; 228 continue; 229 } 230 231 if (c == LEFT_PAREN || c == RIGHT_PAREN) { 232 tokens.add(Token.valueOf(c)); 233 i++; 234 continue; 235 } 236 237 if (c == NOT_CHAR) { 238 tokens.add(new Token(TokenType.NOT)); 239 i++; 240 continue; 241 } 242 243 if (c == AMPERSAND_CHAR) { 244 i++; // consume '&' 245 c = expr.charAt(i); 246 if (c == AMPERSAND_CHAR) { 247 tokens.add(new Token(TokenType.AND)); 248 i++; // consume '&' 249 continue; 250 } else { 251 throw new IllegalArgumentException("Expected '&' after '&'"); 252 } 253 } 254 255 if (c == OR_CHAR) { 256 i++; // consume '|' 257 c = expr.charAt(i); 258 if (c == OR_CHAR) { 259 tokens.add(new Token(TokenType.OR)); 260 i++; // consume '|' 261 continue; 262 } else { 263 throw new IllegalArgumentException("Expected '|' after '|'"); 264 } 265 } 266 267 // Parse identifiers like isNull, isNotNull, etc. 268 if (Character.isLetter(c)) { 269 StringBuilder sb = new StringBuilder(); 270 while (i < expr.length() && Character.isLetter(expr.charAt(i))) { 271 sb.append(expr.charAt(i++)); 272 } 273 String identifier = sb.toString(); 274 275 if (TRUE_LITERAL.equals(identifier)) { 276 tokens.add(new Token(TokenType.TRUE)); 277 continue; 278 } 279 if (FALSE_LITERAL.equals(identifier)) { 280 tokens.add(new Token(TokenType.FALSE)); 281 continue; 282 } 283 284 String functionName = identifier; 285 286 // Skip spaces 287 i = skipWhitespaces(i); 288 checkExpectedCharacter(LEFT_PAREN, i); 289 i++; // consume '(' 290 291 IntHolder intHolder = new IntHolder(i); 292 String param0 = extractQuotedString(intHolder); 293 i = intHolder.value; 294 // Skip spaces 295 i = skipWhitespaces(i); 296 297 298 if (biFunctionMap.containsKey(functionName)) { 299 checkExpectedCharacter(COMMA, i); 300 i++; // consume ',' 301 intHolder.set(i); 302 String param1 = extractQuotedString(intHolder); 303 i = intHolder.get(); 304 i = skipWhitespaces(i); 305 tokens.add(new Token(TokenType.BI_FUNCTION, functionName, param0, param1)); 306 } else { 307 tokens.add(new Token(TokenType.FUNCTION, functionName, param0, null)); 308 } 309 310 // Skip spaces and expect ')' 311 checkExpectedCharacter(RIGHT_PAREN, i); 312 i++; // consume ')' 313 314 continue; 315 } 316 317 throw new IllegalArgumentException("Unexpected character '" + c + "' at position " + i + " in [" + expr + "]"); 318 } 319 return tokens; 320 } 321 322 private String extractQuotedString(IntHolder intHolder) { 323 int i = intHolder.get(); 324 i = skipWhitespaces(i); 325 326 // Expect starting " 327 checkExpectedCharacter(QUOTE, i); 328 i++; // consume starting " 329 330 int start = i; 331 i = findIndexOfClosingQuote(i); 332 String param = expression.substring(start, i); 333 i++; // consume closing " 334 intHolder.set(i); 335 return param; 336 } 337 338 private int findIndexOfClosingQuote(int i) throws IllegalStateException{ 339 while (i < expression.length() && expression.charAt(i) != QUOTE) { 340 i++; 341 } 342 if (i >= expression.length()) { 343 throw new IllegalStateException("Missing closing quote"); 344 } 345 return i; 346 } 347 348 void checkExpectedCharacter(char expectedChar, int i) throws IllegalArgumentException{ 349 if (i >= expression.length() || expression.charAt(i) != expectedChar) { 350 throw new IllegalArgumentException("In [" + expression + "] expecting '" + expectedChar + "' at position " + i); 351 } 352 } 353 354 private int skipWhitespaces(int i) { 355 while (i < expression.length() && Character.isWhitespace(expression.charAt(i))) { 356 i++; 357 } 358 return i; 359 } 360 361 /** 362 * Converts infix notation tokens to Reverse Polish Notation (RPN) using 363 * the Shunting-Yard algorithm. 364 * 365 * @param tokens list of infix tokens 366 * @return list of tokens in RPN 367 * @throws IllegalArgumentException if parentheses are mismatched 368 */ 369 private List<Token> infixToReversePolishNotation(List<Token> tokens) { 370 List<Token> output = new ArrayList<>(); 371 Stack<Token> operatorStack = new Stack<>(); 372 373 for (Token token : tokens) { 374 TokenType tokenType = token.tokenType; 375 if (isPredicate(token)) { 376 output.add(token); 377 } else if (tokenType.isLogicalOperator()) { // one of NOT, AND, OR types 378 while (!operatorStack.isEmpty() && precedence(operatorStack.peek()) >= precedence(token) && 379 operatorAssociativity(token) == Associativity.LEFT) { 380 output.add(operatorStack.pop()); 381 } 382 operatorStack.push(token); 383 } else if (tokenType == TokenType.LEFT_PAREN) { 384 operatorStack.push(token); 385 } else if (tokenType == TokenType.RIGHT_PAREN) { 386 while (!operatorStack.isEmpty() && operatorStack.peek().tokenType != TokenType.LEFT_PAREN) { 387 output.add(operatorStack.pop()); 388 } 389 if (operatorStack.isEmpty()) 390 throw new IllegalArgumentException("Mismatched parentheses, expecting '('"); 391 operatorStack.pop(); // remove '(' 392 } 393 } 394 395 while (!operatorStack.isEmpty()) { 396 Token token = operatorStack.pop(); 397 TokenType tokenType = token.tokenType; 398 if (tokenType == TokenType.LEFT_PAREN) 399 throw new IllegalArgumentException("Mismatched parentheses"); 400 output.add(token); 401 } 402 403 return output; 404 } 405 406 private boolean isPredicate(Token token) { 407 TokenType tokenType = token.tokenType; 408 return tokenType == TokenType.FUNCTION 409 || tokenType == TokenType.BI_FUNCTION 410 || tokenType == TokenType.TRUE 411 || tokenType == TokenType.FALSE; 412 } 413 414 private int precedence(Token token) { 415 TokenType tokenType = token.tokenType; 416 switch (tokenType) { 417 case NOT: 418 return 3; 419 case AND: 420 return 2; 421 case OR: 422 return 1; 423 default: 424 return 0; 425 } 426 } 427 428 private Associativity operatorAssociativity(Token token) { 429 TokenType tokenType = token.tokenType; 430 431 return tokenType == TokenType.NOT ? Associativity.RIGHT : Associativity.LEFT; 432 } 433 434 /** 435 * Evaluates the Reverse Polish Notation (RPN) expression. 436 * 437 * @param rpn list of tokens in RPN 438 * @return the boolean result of the evaluation 439 * @throws IllegalStateException if a function is not defined in the function map 440 */ 441 private boolean evaluateRPN(List<Token> rpn) throws IllegalStateException { 442 Stack<Boolean> resultStack = new Stack<>(); 443 444 for (Token token : rpn) { 445 if (isPredicate(token)) { 446 boolean value = evaluateFunctions(token); 447 resultStack.push(value); 448 } else { 449 switch (token.tokenType) { 450 case NOT: 451 boolean a3 = resultStack.pop(); 452 resultStack.push(!a3); 453 break; 454 case AND: 455 boolean b2 = resultStack.pop(); 456 boolean a2 = resultStack.pop(); 457 resultStack.push(a2 && b2); 458 break; 459 460 case OR: 461 boolean b1 = resultStack.pop(); 462 boolean a1 = resultStack.pop(); 463 resultStack.push(a1 || b1); 464 break; 465 } 466 } 467 } 468 469 return resultStack.pop(); 470 } 471 472 // Evaluate a single predicate like isNull("key1") or a boolean literal 473 private boolean evaluateFunctions(Token token) throws IllegalStateException { 474 if (token.tokenType == TokenType.TRUE) { 475 return true; 476 } 477 if (token.tokenType == TokenType.FALSE) { 478 return false; 479 } 480 481 String functionName = token.functionName; 482 String param0 = token.param0; 483 String param1 = token.param1; 484 Function<String, Boolean> function = functionMap.get(functionName); 485 if (function != null) { 486 return function.apply(param0); 487 } 488 489 BiFunction<String, String, Boolean> biFunction = biFunctionMap.get(functionName); 490 if (biFunction != null) { 491 return biFunction.apply(param0, param1); 492 } 493 494 throw new IllegalStateException("Unknown function: " + token); 495 } 496}