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