1414
1515package ch .qos .logback .core .boolex ;
1616
17- import java .util .*;
18- import java .util .concurrent .atomic .AtomicInteger ;
17+ import ch .qos .logback .core .util .IntHolder ;
18+
19+ import java .util .ArrayList ;
20+ import java .util .HashMap ;
21+ import java .util .List ;
22+ import java .util .Map ;
23+ import java .util .Stack ;
1924import java .util .function .BiFunction ;
2025import java .util .function .Function ;
2126
22-
27+ /**
28+ * A property condition that evaluates boolean expressions based on property lookups.
29+ * It supports logical operators (NOT, AND, OR) and functions like isNull, isDefined,
30+ * propertyEquals, and propertyContains. Expressions are parsed using the Shunting-Yard
31+ * algorithm into Reverse Polish Notation (RPN) for evaluation.
32+ *
33+ * <p>Example expression: {@code isDefined("key1") && propertyEquals("key2", "value")}</p>
34+ *
35+ * <p>Properties are resolved via {@link PropertyConditionBase#property(String)}.</p>
36+ *
37+ * @since 1.5.24
38+ */
2339public class ExpressionPropertyCondition extends PropertyConditionBase {
2440
2541
26- Map <String , Function <String , Boolean >> functionMap = new HashMap <>();
27- Map <String , BiFunction <String , String , Boolean >> biFunctionMap = new HashMap <>();
42+ /**
43+ * A map that associates a string key with a function for evaluating boolean conditions.
44+ *
45+ * <p>This map defines the known functions. It can be overridden by subclasses to define
46+ * new functions.</p>
47+ *
48+ * <p>In the context of this class, a function is a function that takes a String
49+ * argument and returns a boolean.</p>
50+ */
51+ protected Map <String , Function <String , Boolean >> functionMap = new HashMap <>();
2852
29- private static final String IS_NULL = "isNull" ;
30- private static final String IS_DEFINED = "isDefined" ;
3153
32- private static final String PROPERTY_EQUALS = "propertyEquals" ;
33- private static final String PROPERTY_CONTAINS = "propertyContains" ;
54+ /**
55+ * A map that associates a string key with a bi-function for evaluating boolean conditions.
56+ *
57+ * <p>This map defines the known bi-functions. It can be overridden by subclasses to define
58+ * new bi-functions.</p>
59+ *
60+ * <p>In the context of this class, a bi-function is a function that takes two String
61+ * arguments and returns a boolean.</p>
62+ */
63+ protected Map <String , BiFunction <String , String , Boolean >> biFunctionMap = new HashMap <>();
64+
65+ private static final String IS_NULL_FUNCTION_KEY = "isNull" ;
66+ private static final String IS_DEFINEDP_FUNCTION_KEY = "isDefined" ;
67+
68+ private static final String PROPERTY_EQUALS_FUNCTION_KEY = "propertyEquals" ;
69+ private static final String PROPERTY_CONTAINS_FUNCTION_KEY = "propertyContains" ;
3470
3571 private static final char QUOTE = '"' ;
3672 private static final char COMMA = ',' ;
3773 private static final char LEFT_PAREN = '(' ;
3874 private static final char RIGHT_PAREN = ')' ;
39- private static final String LEFT_PAREN_STR = "(" ;
40- private static final String RIGHT_PAREN_STR = ")" ;
4175
4276
4377 private static final char NOT_CHAR = '!' ;
@@ -99,33 +133,64 @@ public static Token valueOf(char c) {
99133 String expression ;
100134 List <Token > rpn ;
101135
136+ /**
137+ * Constructs an ExpressionPropertyCondition and initializes the function maps
138+ * with supported unary and binary functions.
139+ */
102140 ExpressionPropertyCondition () {
103- functionMap .put (IS_NULL , this ::isNull );
104- functionMap .put (IS_DEFINED , this ::isDefined );
105- biFunctionMap .put (PROPERTY_EQUALS , this ::propertyEquals );
106- biFunctionMap .put (PROPERTY_CONTAINS , this ::propertyContains );
141+ functionMap .put (IS_NULL_FUNCTION_KEY , this ::isNull );
142+ functionMap .put (IS_DEFINEDP_FUNCTION_KEY , this ::isDefined );
143+ biFunctionMap .put (PROPERTY_EQUALS_FUNCTION_KEY , this ::propertyEquals );
144+ biFunctionMap .put (PROPERTY_CONTAINS_FUNCTION_KEY , this ::propertyContains );
107145 }
108146
109-
147+ /**
148+ * Starts the condition by parsing the expression into tokens and converting
149+ * them to Reverse Polish Notation (RPN) for evaluation.
150+ *
151+ * <p>In case of malformed expression, the instance will not enter the "started" state.</p>
152+ */
110153 public void start () {
111- super .start ();
112154 if (expression == null || expression .isEmpty ()) {
113155 addError ("Empty expression" );
114156 return ;
115157 }
116158
117- List <Token > tokens = tokenize (expression .trim ());
118- this .rpn = infixToReversePolishNotation (tokens );
159+ try {
160+ List <Token > tokens = tokenize (expression .trim ());
161+ this .rpn = infixToReversePolishNotation (tokens );
162+ } catch (IllegalArgumentException |IllegalStateException e ) {
163+ addError ("Malformed expression: " + e .getMessage ());
164+ return ;
165+ }
166+ super .start ();
119167 }
120168
169+ /**
170+ * Returns the current expression string.
171+ *
172+ * @return the expression, or null if not set
173+ */
121174 public String getExpression () {
122175 return expression ;
123176 }
124177
178+ /**
179+ * Sets the expression to be evaluated.
180+ *
181+ * @param expression the boolean expression string
182+ */
125183 public void setExpression (String expression ) {
126184 this .expression = expression ;
127185 }
128186
187+ /**
188+ * Evaluates the parsed expression against the current property context.
189+ *
190+ * <p>If the instance is not in started state, returns false.</p>
191+ *
192+ * @return true if the expression evaluates to true, false otherwise
193+ */
129194 @ Override
130195 public boolean evaluate () {
131196 if (!isStarted ()) {
@@ -134,8 +199,15 @@ public boolean evaluate() {
134199 return evaluateRPN (rpn );
135200 }
136201
137- // Tokenize the input string
138- private List <Token > tokenize (String expr ) {
202+ /**
203+ * Tokenizes the input expression string into a list of tokens, handling
204+ * functions, operators, and parentheses.
205+ *
206+ * @param expr the expression string to tokenize
207+ * @return list of tokens
208+ * @throws IllegalArgumentException if the expression is malformed
209+ */
210+ private List <Token > tokenize (String expr ) throws IllegalArgumentException , IllegalStateException {
139211 List <Token > tokens = new ArrayList <>();
140212
141213 int i = 0 ;
@@ -196,19 +268,19 @@ private List<Token> tokenize(String expr) {
196268 checkExpectedCharacter (LEFT_PAREN , i );
197269 i ++; // consume '('
198270
199- AtomicInteger atomicInteger = new AtomicInteger (i );
200- String param0 = extractQuotedString (atomicInteger );
201- i = atomicInteger . get () ;
271+ IntHolder intHolder = new IntHolder (i );
272+ String param0 = extractQuotedString (intHolder );
273+ i = intHolder . value ;
202274 // Skip spaces
203275 i = skipWhitespaces (i );
204276
205277
206278 if (biFunctionMap .containsKey (functionName )) {
207279 checkExpectedCharacter (COMMA , i );
208280 i ++; // consume ','
209- atomicInteger .set (i );
210- String param1 = extractQuotedString (atomicInteger );
211- i = atomicInteger .get ();
281+ intHolder .set (i );
282+ String param1 = extractQuotedString (intHolder );
283+ i = intHolder .get ();
212284 i = skipWhitespaces (i );
213285 tokens .add (new Token (TokenType .BI_FUNCTION , functionName , param0 , param1 ));
214286 } else {
@@ -225,8 +297,8 @@ private List<Token> tokenize(String expr) {
225297 return tokens ;
226298 }
227299
228- private String extractQuotedString (AtomicInteger atomicInteger ) {
229- int i = atomicInteger .get ();
300+ private String extractQuotedString (IntHolder intHolder ) {
301+ int i = intHolder .get ();
230302 i = skipWhitespaces (i );
231303
232304 // Expect starting "
@@ -237,11 +309,11 @@ private String extractQuotedString(AtomicInteger atomicInteger) {
237309 i = findIndexOfClosingQuote (i );
238310 String param = expression .substring (start , i );
239311 i ++; // consume closing "
240- atomicInteger .set (i );
312+ intHolder .set (i );
241313 return param ;
242314 }
243315
244- private int findIndexOfClosingQuote (int i ) {
316+ private int findIndexOfClosingQuote (int i ) throws IllegalStateException {
245317 while (i < expression .length () && expression .charAt (i ) != QUOTE ) {
246318 i ++;
247319 }
@@ -251,14 +323,7 @@ private int findIndexOfClosingQuote(int i) {
251323 return i ;
252324 }
253325
254- private int findIndexOfComma (int i ) {
255- while (i < expression .length () && expression .charAt (i ) != COMMA ) {
256- i ++;
257- }
258- return i ;
259- }
260-
261- void checkExpectedCharacter (char expectedChar , int i ) {
326+ void checkExpectedCharacter (char expectedChar , int i ) throws IllegalArgumentException {
262327 if (i >= expression .length () || expression .charAt (i ) != expectedChar ) {
263328 throw new IllegalArgumentException ("In [" + expression + "] expecting '" + expectedChar + "' at position " + i );
264329 }
@@ -271,10 +336,17 @@ private int skipWhitespaces(int i) {
271336 return i ;
272337 }
273338
274- // Shunting-Yard: convert infix tokens to RPN
339+ /**
340+ * Converts infix notation tokens to Reverse Polish Notation (RPN) using
341+ * the Shunting-Yard algorithm.
342+ *
343+ * @param tokens list of infix tokens
344+ * @return list of tokens in RPN
345+ * @throws IllegalArgumentException if parentheses are mismatched
346+ */
275347 private List <Token > infixToReversePolishNotation (List <Token > tokens ) {
276348 List <Token > output = new ArrayList <>();
277- Deque <Token > operatorStack = new ArrayDeque <>();
349+ Stack <Token > operatorStack = new Stack <>();
278350
279351 for (Token token : tokens ) {
280352 TokenType tokenType = token .tokenType ;
@@ -329,13 +401,19 @@ private int precedence(Token token) {
329401
330402 private Associativity operatorAssociativity (Token token ) {
331403 TokenType tokenType = token .tokenType ;
332- //
404+
333405 return tokenType == TokenType .NOT ? Associativity .RIGHT : Associativity .LEFT ;
334406 }
335407
336- // Evaluate RPN
337- private boolean evaluateRPN (List <Token > rpn ) {
338- Deque <Boolean > resultStack = new ArrayDeque <>();
408+ /**
409+ * Evaluates the Reverse Polish Notation (RPN) expression.
410+ *
411+ * @param rpn list of tokens in RPN
412+ * @return the boolean result of the evaluation
413+ * @throws IllegalStateException if a function is not defined in the function map
414+ */
415+ private boolean evaluateRPN (List <Token > rpn ) throws IllegalStateException {
416+ Stack <Boolean > resultStack = new Stack <>();
339417
340418 for (Token token : rpn ) {
341419 if (isPredicate (token )) {
@@ -366,7 +444,7 @@ private boolean evaluateRPN(List<Token> rpn) {
366444 }
367445
368446 // Evaluate a single predicate like isNull("key1")
369- private boolean evaluateFunctions (Token token ) {
447+ private boolean evaluateFunctions (Token token ) throws IllegalStateException {
370448 String functionName = token .functionName ;
371449 String param0 = token .param0 ;
372450 String param1 = token .param1 ;
@@ -380,8 +458,6 @@ private boolean evaluateFunctions(Token token) {
380458 return biFunction .apply (param0 , param1 );
381459 }
382460
383- throw new IllegalArgumentException ("Unknown function: " + token );
461+ throw new IllegalStateException ("Unknown function: " + token );
384462 }
385-
386-
387463}
0 commit comments