001/**
002 * Copyright (C) 2006-2025 Talend Inc. - www.talend.com
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 * http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016package org.talend.sdk.component.server.service;
017
018import static java.util.Optional.ofNullable;
019
020import java.util.Map;
021import java.util.Objects;
022import java.util.function.BiPredicate;
023import java.util.function.BinaryOperator;
024import java.util.function.Function;
025import java.util.function.Predicate;
026
027import javax.enterprise.context.ApplicationScoped;
028
029import lombok.RequiredArgsConstructor;
030import lombok.ToString;
031
032@ApplicationScoped
033public class SimpleQueryLanguageCompiler {
034
035    private static final BiPredicate<String, String> EQUAL_PREDICATE = new EqualPredicate();
036
037    private static final BiPredicate<String, String> DIFFERENT_PREDICATE = new DifferentPredicate();
038
039    public <T> Predicate<T> compile(final String query, final Map<String, Function<T, Object>> evaluators) {
040        if (query == null || query.trim().isEmpty()) {
041            return t -> true;
042        }
043        return doCompile(query.toCharArray(), 0, evaluators, TokenType.END).predicate;
044    }
045
046    public <T> SubExpression<T> doCompile(final char[] buffer, final int from,
047            final Map<String, Function<T, Object>> evaluators, final TokenType stopToken) {
048        Predicate<T> predicate = null;
049        BinaryOperator<Predicate<T>> combiner = null;
050
051        int index = from;
052        while (true) {
053            final Token token = nextToken(buffer, index);
054            if (stopToken == token.type) {
055                break;
056            }
057
058            index = moveIndex(buffer, token, true);
059
060            switch (token.type) {
061                case VALUE: {
062                    // expecting operator and value
063                    final Token opToken = nextToken(buffer, index);
064                    if (opToken.type != TokenType.OPERATOR) {
065                        throw new IllegalArgumentException(
066                                "Expected an operator after token '" + token.value + "' at index " + token.end);
067                    }
068                    index = moveIndex(buffer, opToken, true);
069                    final Token expectedValueToken = nextToken(buffer, index);
070                    if (expectedValueToken.type == TokenType.VALUE) {
071                        index = moveIndex(buffer, expectedValueToken, false);
072                        final Predicate<T> expr =
073                                toPredicate(token.value, opToken.value, expectedValueToken.value, evaluators);
074
075                        validateCombiner(predicate, combiner, token);
076                        predicate = predicate == null ? expr : combiner.apply(predicate, expr);
077                        combiner = null;
078                        break;
079                    }
080                    throw new IllegalArgumentException("Unsupported token: " + token.type + " at index " + token.end);
081                }
082                case SUB_EXPRESSION_START:
083                    final SubExpression<T> expr = doCompile(buffer, index, evaluators, TokenType.SUB_EXPRESSION_END);
084                    validateCombiner(predicate, combiner, token);
085                    predicate = predicate == null ? expr.predicate : combiner.apply(predicate, expr.predicate);
086                    combiner = null;
087                    index = expr.end + 1;
088                    break;
089                case COMBINER:
090                    switch (token.value) {
091                        case "AND":
092                            combiner = Predicate::and;
093                            break;
094                        case "OR":
095                            combiner = Predicate::or;
096                            break;
097                        default:
098                            throw new IllegalArgumentException("Unsupported combiner operator: " + token.type
099                                    + " at index " + token.end + ", expected 'OR' or 'AND'");
100                    }
101                    break;
102                default:
103                    throw new IllegalArgumentException("Unsupported token: " + token.type + " at index " + token.end);
104            }
105        }
106        return new SubExpression<>(index, predicate == null ? t -> true : predicate);
107    }
108
109    private <T> void validateCombiner(final Predicate<T> predicate, final BinaryOperator<Predicate<T>> combiner,
110            final Token token) {
111        if (combiner == null && predicate != null) {
112            throw new IllegalArgumentException("Missing combiner for predicate at index " + token.end);
113        }
114    }
115
116    private int moveIndex(final char[] buffer, final Token token, final boolean validate) {
117        int index;
118        index = token.end + 1;
119        if (validate && index >= buffer.length) {
120            throw new IllegalArgumentException("Unexpected token '" + token + "' at index " + token.end);
121        }
122        return index;
123    }
124
125    private <T> Predicate<T> toPredicate(final String key, final String operator, final String expectedValue,
126            final Map<String, Function<T, Object>> evaluators) {
127        final BiPredicate<String, String> comparator;
128        switch (operator) {
129            case "=":
130                comparator = EQUAL_PREDICATE;
131                break;
132            case "!=":
133                comparator = DIFFERENT_PREDICATE;
134                break;
135            default:
136                throw new IllegalArgumentException("unknown operator: '" + operator + "'");
137        }
138        final int mapExpr = key.indexOf('[');
139        if (mapExpr > 0) {
140            final int endMapAccess = key.indexOf(']', mapExpr);
141            if (endMapAccess > 0) {
142                final String mapName = key.substring(0, mapExpr);
143                final String mapKey = key.substring(mapExpr + 1, endMapAccess);
144                final Function<T, Object> evaluator = ofNullable(evaluators.get(mapName))
145                        .orElseThrow(() -> new IllegalArgumentException("Missing evaluator for '" + mapName + "'"));
146                return new ComparePredicate<>(comparator, t -> {
147                    final Object map = evaluator.apply(t);
148                    if (!Map.class.isInstance(map)) {
149                        throw new IllegalArgumentException(map + " is not a map");
150                    }
151                    return Map.class.cast(map).get(mapKey);
152                }, expectedValue);
153            }
154        }
155        final Function<T, Object> evaluator = ofNullable(evaluators.get(key))
156                .orElseThrow(() -> new IllegalArgumentException("Missing evaluator for '" + key + "'"));
157        return new ComparePredicate<T>(comparator, evaluator, expectedValue);
158    }
159
160    private Token nextToken(final char[] buffer, final int from) {
161        if (from >= buffer.length) {
162            return new Token(from, TokenType.END, null);
163        }
164
165        int actualFrom = from;
166        int idx = from;
167        while (idx < buffer.length) {
168            switch (buffer[idx]) {
169                case '(':
170                    if (from == idx) {
171                        return new Token(idx, TokenType.SUB_EXPRESSION_START, null);
172                    }
173                    return new Token(idx - 1, TokenType.VALUE, new String(buffer, actualFrom, idx - actualFrom));
174                case ')':
175                    if (from == idx) {
176                        return new Token(idx, TokenType.SUB_EXPRESSION_END, null);
177                    }
178                    return new Token(idx - 1, TokenType.VALUE, new String(buffer, actualFrom, idx - actualFrom));
179                case ' ':
180                    if (idx == from) { // foo = bar, we are at the whitespace before bar
181                        idx++;
182                        actualFrom = from + 1;
183                        continue;
184                    }
185                    final String string = new String(buffer, actualFrom, idx - actualFrom);
186                    switch (string) {
187                        case "AND":
188                        case "OR":
189                            return new Token(idx, TokenType.COMBINER, string);
190                        default:
191                            return new Token(idx, TokenType.VALUE, string);
192                    }
193                case '=':
194                    return new Token(idx, TokenType.OPERATOR, "=");
195                case '!':
196                    idx++;
197                    if (idx < buffer.length && buffer[idx] == '=') {
198                        return new Token(idx, TokenType.OPERATOR, "!=");
199                    }
200                    break;
201                case 'A':
202                    if (idx == from && idx + 3 < buffer.length && buffer[idx + 1] == 'N' && buffer[idx + 2] == 'D'
203                            && buffer[idx + 3] == ' ') {
204                        idx += 3;
205                        return new Token(idx, TokenType.COMBINER, "AND");
206                    }
207                    idx++;
208                    break;
209                case 'O':
210                    if (idx == from && idx + 2 < buffer.length && buffer[idx + 1] == 'R' && buffer[idx + 2] == ' ') {
211                        idx += 2;
212                        return new Token(idx, TokenType.COMBINER, "OR");
213                    }
214                    idx++;
215                    break;
216                default:
217                    idx++;
218            }
219        }
220        return new Token(idx, TokenType.VALUE, new String(buffer, actualFrom, buffer.length - actualFrom));
221    }
222
223    private enum TokenType {
224        SUB_EXPRESSION_START, // (
225        SUB_EXPRESSION_END, // )
226        VALUE, // field name or expected value
227        OPERATOR, // = or !=
228        COMBINER, // OR or AND
229        END // EOL
230    }
231
232    @ToString
233    @RequiredArgsConstructor
234    private static class Token {
235
236        private final int end;
237
238        private final TokenType type;
239
240        private final String value;
241    }
242
243    @ToString
244    @RequiredArgsConstructor
245    private static class SubExpression<T> {
246
247        private final int end;
248
249        private final Predicate<T> predicate;
250    }
251
252    private static class EqualPredicate implements BiPredicate<String, String> {
253
254        @Override
255        public boolean test(final String v1, final String v2) {
256            return (v1 == null && "null".equals(v2)) || Objects.equals(v1, v2);
257        }
258    }
259
260    private static class DifferentPredicate implements BiPredicate<String, String> {
261
262        @Override
263        public boolean test(final String v1, final String v2) {
264            return !EQUAL_PREDICATE.test(v1, v2);
265        }
266    }
267
268    @RequiredArgsConstructor
269    private class ComparePredicate<T> implements Predicate<T> {
270
271        private final BiPredicate<String, String> comparator;
272
273        private final Function<T, Object> evaluator;
274
275        private final String expectedValue;
276
277        @Override
278        public boolean test(final T t) {
279            return comparator.test(String.valueOf(evaluator.apply(t)), expectedValue);
280        }
281    }
282}