001/**
002 * Copyright (C) 2006-2020 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 + " at index "
099                            + 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}