Skip to content

Commit 95e588c

Browse files
committed
minor changes in ExpressionPropertyCondition
Signed-off-by: ceki <ceki@qos.ch>
1 parent 859f5a1 commit 95e588c

3 files changed

Lines changed: 207 additions & 49 deletions

File tree

logback-core/src/main/java/ch/qos/logback/core/boolex/ExpressionPropertyCondition.java

Lines changed: 125 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -14,30 +14,64 @@
1414

1515
package 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;
1924
import java.util.function.BiFunction;
2025
import 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+
*/
2339
public 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
}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
/*
2+
* Logback: the reliable, generic, fast and flexible logging framework.
3+
* Copyright (C) 1999-2026, QOS.ch. All rights reserved.
4+
*
5+
* This program and the accompanying materials are dual-licensed under
6+
* either the terms of the Eclipse Public License v1.0 as published by
7+
* the Eclipse Foundation
8+
*
9+
* or (per the licensee's choosing)
10+
*
11+
* under the terms of the GNU Lesser General Public License version 2.1
12+
* as published by the Free Software Foundation.
13+
*/
14+
15+
package ch.qos.logback.core.util;
16+
17+
/**
18+
* A simple mutable holder for an integer value, providing basic operations
19+
* like incrementing, setting, and retrieving the value. This class is not
20+
* thread-safe and should be used in single-threaded contexts or with external
21+
* synchronization.
22+
*
23+
* @since 1.5.24
24+
*/
25+
public class IntHolder {
26+
public int value;
27+
28+
/**
29+
* Constructs an IntHolder with the specified initial value.
30+
*
31+
* @param value the initial integer value to hold
32+
*/
33+
public IntHolder(int value) {
34+
this.value = value;
35+
}
36+
37+
/**
38+
* Increments the held value by 1.
39+
*/
40+
public void inc() {
41+
value++;
42+
}
43+
44+
/**
45+
* Sets the held value to the specified new value.
46+
*
47+
* @param newValue the new integer value to set
48+
*/
49+
public void set(int newValue) {
50+
value = newValue;
51+
}
52+
53+
/**
54+
* Returns the current held value.
55+
*
56+
* @return the current integer value
57+
*/
58+
public int get(){
59+
return value;
60+
}
61+
}

logback-core/src/test/java/ch/qos/logback/core/boolex/ExpressionPropertyConditionTest.java

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,27 @@ public void smokeBiFunction() {
8585
check(expression, true);
8686
}
8787

88+
89+
@Test
90+
public void propertyContains() {
91+
String expression = String.format("propertyContains(\"%s\", \"%s\")" , SMOKE_KEY, SMOKE_VALUE.substring(0, 2));
92+
check(expression, true);
93+
}
94+
95+
96+
@Test
97+
public void notPropertyContainsX() {
98+
String expression = String.format("!propertyContains(\"%s\", \"%s\")" , SMOKE_KEY, SMOKE_VALUE+"x");
99+
check(expression, true);
100+
}
101+
102+
@Test
103+
public void propertyEqualsOrIsNull() {
104+
String expression = String.format("!propertyEquals(\"%s\", \"%s\") || !isNull(\"%s\")" , SMOKE_KEY, SMOKE_VALUE, UNDEFINED_KEY);
105+
check(expression, false);
106+
}
107+
108+
88109
void check(String expression, boolean expected) {
89110
epc.setExpression(expression);
90111
epc.start();

0 commit comments

Comments
 (0)