1
2
3
4
5
6
7
8
9
10
11
12
13
14
15 package ch.qos.logback.core.boolex;
16
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;
24 import java.util.function.BiFunction;
25 import java.util.function.Function;
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40 public class ExpressionPropertyCondition extends PropertyConditionBase {
41
42
43
44
45
46
47
48
49
50
51
52 protected Map<String, Function<String, Boolean>> functionMap = new HashMap<>();
53
54
55
56
57
58
59
60
61
62
63
64 protected Map<String, BiFunction<String, String, Boolean>> biFunctionMap = new HashMap<>();
65
66 private static final String IS_NULL_FUNCTION_KEY = "isNull";
67 private static final String IS_DEFINEDP_FUNCTION_KEY = "isDefined";
68
69 private static final String PROPERTY_EQUALS_FUNCTION_KEY = "propertyEquals";
70 private static final String PROPERTY_CONTAINS_FUNCTION_KEY = "propertyContains";
71
72 private static final char QUOTE = '"';
73 private static final char COMMA = ',';
74 private static final char LEFT_PAREN = '(';
75 private static final char RIGHT_PAREN = ')';
76
77
78 private static final char NOT_CHAR = '!';
79 private static final char AMPERSAND_CHAR = '&';
80 private static final char OR_CHAR = '|';
81
82 enum Associativity {
83 LEFT, RIGHT;
84 }
85
86 enum TokenType {
87 NOT, AND, OR, FUNCTION, BI_FUNCTION, LEFT_PAREN, RIGHT_PAREN;
88
89 boolean isLogicalOperator() {
90 return this == NOT || this == AND || this == OR;
91 }
92 }
93
94
95 static class Token {
96 TokenType tokenType;
97 String functionName;
98 String param0;
99 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
139
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
150
151
152
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
172
173
174
175 public String getExpression() {
176 return expression;
177 }
178
179
180
181
182
183
184 public void setExpression(String expression) {
185 this.expression = expression;
186 }
187
188
189
190
191
192
193
194
195 @Override
196 public boolean evaluate() {
197 if (!isStarted()) {
198 return false;
199 }
200 return evaluateRPN(rpn);
201 }
202
203
204
205
206
207
208
209
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++;
237 c = expr.charAt(i);
238 if (c == AMPERSAND_CHAR) {
239 tokens.add(new Token(TokenType.AND));
240 i++;
241 continue;
242 } else {
243 throw new IllegalArgumentException("Expected '&' after '&'");
244 }
245 }
246
247 if (c == OR_CHAR) {
248 i++;
249 c = expr.charAt(i);
250 if (c == OR_CHAR) {
251 tokens.add(new Token(TokenType.OR));
252 i++;
253 continue;
254 } else {
255 throw new IllegalArgumentException("Expected '|' after '|'");
256 }
257 }
258
259
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
268 i = skipWhitespaces(i);
269 checkExpectedCharacter(LEFT_PAREN, i);
270 i++;
271
272 IntHolder intHolder = new IntHolder(i);
273 String param0 = extractQuotedString(intHolder);
274 i = intHolder.value;
275
276 i = skipWhitespaces(i);
277
278
279 if (biFunctionMap.containsKey(functionName)) {
280 checkExpectedCharacter(COMMA, i);
281 i++;
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
292 checkExpectedCharacter(RIGHT_PAREN, i);
293 i++;
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
306 checkExpectedCharacter(QUOTE, i);
307 i++;
308
309 int start = i;
310 i = findIndexOfClosingQuote(i);
311 String param = expression.substring(start, i);
312 i++;
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
342
343
344
345
346
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()) {
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();
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
411
412
413
414
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
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 }