All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.talend.sdk.component.server.service.SimpleQueryLanguageCompiler Maven / Gradle / Ivy

/**
 * Copyright (C) 2006-2020 Talend Inc. - www.talend.com
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.talend.sdk.component.server.service;

import static java.util.Optional.ofNullable;

import java.util.Map;
import java.util.Objects;
import java.util.function.BiPredicate;
import java.util.function.BinaryOperator;
import java.util.function.Function;
import java.util.function.Predicate;

import javax.enterprise.context.ApplicationScoped;

import lombok.RequiredArgsConstructor;
import lombok.ToString;

@ApplicationScoped
public class SimpleQueryLanguageCompiler {

    private static final BiPredicate EQUAL_PREDICATE = new EqualPredicate();

    private static final BiPredicate DIFFERENT_PREDICATE = new DifferentPredicate();

    public  Predicate compile(final String query, final Map> evaluators) {
        if (query == null || query.trim().isEmpty()) {
            return t -> true;
        }
        return doCompile(query.toCharArray(), 0, evaluators, TokenType.END).predicate;
    }

    public  SubExpression doCompile(final char[] buffer, final int from,
            final Map> evaluators, final TokenType stopToken) {
        Predicate predicate = null;
        BinaryOperator> combiner = null;

        int index = from;
        while (true) {
            final Token token = nextToken(buffer, index);
            if (stopToken == token.type) {
                break;
            }

            index = moveIndex(buffer, token, true);

            switch (token.type) {
            case VALUE: {
                // expecting operator and value
                final Token opToken = nextToken(buffer, index);
                if (opToken.type != TokenType.OPERATOR) {
                    throw new IllegalArgumentException(
                            "Expected an operator after token '" + token.value + "' at index " + token.end);
                }
                index = moveIndex(buffer, opToken, true);
                final Token expectedValueToken = nextToken(buffer, index);
                if (expectedValueToken.type == TokenType.VALUE) {
                    index = moveIndex(buffer, expectedValueToken, false);
                    final Predicate expr =
                            toPredicate(token.value, opToken.value, expectedValueToken.value, evaluators);

                    validateCombiner(predicate, combiner, token);
                    predicate = predicate == null ? expr : combiner.apply(predicate, expr);
                    combiner = null;
                    break;
                }
                throw new IllegalArgumentException("Unsupported token: " + token.type + " at index " + token.end);
            }
            case SUB_EXPRESSION_START:
                final SubExpression expr = doCompile(buffer, index, evaluators, TokenType.SUB_EXPRESSION_END);
                validateCombiner(predicate, combiner, token);
                predicate = predicate == null ? expr.predicate : combiner.apply(predicate, expr.predicate);
                combiner = null;
                index = expr.end + 1;
                break;
            case COMBINER:
                switch (token.value) {
                case "AND":
                    combiner = Predicate::and;
                    break;
                case "OR":
                    combiner = Predicate::or;
                    break;
                default:
                    throw new IllegalArgumentException("Unsupported combiner operator: " + token.type + " at index "
                            + token.end + ", expected 'OR' or 'AND'");
                }
                break;
            default:
                throw new IllegalArgumentException("Unsupported token: " + token.type + " at index " + token.end);
            }
        }
        return new SubExpression<>(index, predicate == null ? t -> true : predicate);
    }

    private  void validateCombiner(final Predicate predicate, final BinaryOperator> combiner,
            final Token token) {
        if (combiner == null && predicate != null) {
            throw new IllegalArgumentException("Missing combiner for predicate at index " + token.end);
        }
    }

    private int moveIndex(final char[] buffer, final Token token, final boolean validate) {
        int index;
        index = token.end + 1;
        if (validate && index >= buffer.length) {
            throw new IllegalArgumentException("Unexpected token '" + token + "' at index " + token.end);
        }
        return index;
    }

    private  Predicate toPredicate(final String key, final String operator, final String expectedValue,
            final Map> evaluators) {
        final BiPredicate comparator;
        switch (operator) {
        case "=":
            comparator = EQUAL_PREDICATE;
            break;
        case "!=":
            comparator = DIFFERENT_PREDICATE;
            break;
        default:
            throw new IllegalArgumentException("unknown operator: '" + operator + "'");
        }
        final int mapExpr = key.indexOf('[');
        if (mapExpr > 0) {
            final int endMapAccess = key.indexOf(']', mapExpr);
            if (endMapAccess > 0) {
                final String mapName = key.substring(0, mapExpr);
                final String mapKey = key.substring(mapExpr + 1, endMapAccess);
                final Function evaluator = ofNullable(evaluators.get(mapName))
                        .orElseThrow(() -> new IllegalArgumentException("Missing evaluator for '" + mapName + "'"));
                return new ComparePredicate<>(comparator, t -> {
                    final Object map = evaluator.apply(t);
                    if (!Map.class.isInstance(map)) {
                        throw new IllegalArgumentException(map + " is not a map");
                    }
                    return Map.class.cast(map).get(mapKey);
                }, expectedValue);
            }
        }
        final Function evaluator = ofNullable(evaluators.get(key))
                .orElseThrow(() -> new IllegalArgumentException("Missing evaluator for '" + key + "'"));
        return new ComparePredicate(comparator, evaluator, expectedValue);
    }

    private Token nextToken(final char[] buffer, final int from) {
        if (from >= buffer.length) {
            return new Token(from, TokenType.END, null);
        }

        int actualFrom = from;
        int idx = from;
        while (idx < buffer.length) {
            switch (buffer[idx]) {
            case '(':
                if (from == idx) {
                    return new Token(idx, TokenType.SUB_EXPRESSION_START, null);
                }
                return new Token(idx - 1, TokenType.VALUE, new String(buffer, actualFrom, idx - actualFrom));
            case ')':
                if (from == idx) {
                    return new Token(idx, TokenType.SUB_EXPRESSION_END, null);
                }
                return new Token(idx - 1, TokenType.VALUE, new String(buffer, actualFrom, idx - actualFrom));
            case ' ':
                if (idx == from) { // foo = bar, we are at the whitespace before bar
                    idx++;
                    actualFrom = from + 1;
                    continue;
                }
                final String string = new String(buffer, actualFrom, idx - actualFrom);
                switch (string) {
                case "AND":
                case "OR":
                    return new Token(idx, TokenType.COMBINER, string);
                default:
                    return new Token(idx, TokenType.VALUE, string);
                }
            case '=':
                return new Token(idx, TokenType.OPERATOR, "=");
            case '!':
                idx++;
                if (idx < buffer.length && buffer[idx] == '=') {
                    return new Token(idx, TokenType.OPERATOR, "!=");
                }
                break;
            case 'A':
                if (idx == from && idx + 3 < buffer.length && buffer[idx + 1] == 'N' && buffer[idx + 2] == 'D'
                        && buffer[idx + 3] == ' ') {
                    idx += 3;
                    return new Token(idx, TokenType.COMBINER, "AND");
                }
                idx++;
                break;
            case 'O':
                if (idx == from && idx + 2 < buffer.length && buffer[idx + 1] == 'R' && buffer[idx + 2] == ' ') {
                    idx += 2;
                    return new Token(idx, TokenType.COMBINER, "OR");
                }
                idx++;
                break;
            default:
                idx++;
            }
        }
        return new Token(idx, TokenType.VALUE, new String(buffer, actualFrom, buffer.length - actualFrom));
    }

    private enum TokenType {
        SUB_EXPRESSION_START, // (
        SUB_EXPRESSION_END, // )
        VALUE, // field name or expected value
        OPERATOR, // = or !=
        COMBINER, // OR or AND
        END // EOL
    }

    @ToString
    @RequiredArgsConstructor
    private static class Token {

        private final int end;

        private final TokenType type;

        private final String value;
    }

    @ToString
    @RequiredArgsConstructor
    private static class SubExpression {

        private final int end;

        private final Predicate predicate;
    }

    private static class EqualPredicate implements BiPredicate {

        @Override
        public boolean test(final String v1, final String v2) {
            return (v1 == null && "null".equals(v2)) || Objects.equals(v1, v2);
        }
    }

    private static class DifferentPredicate implements BiPredicate {

        @Override
        public boolean test(final String v1, final String v2) {
            return !EQUAL_PREDICATE.test(v1, v2);
        }
    }

    @RequiredArgsConstructor
    private class ComparePredicate implements Predicate {

        private final BiPredicate comparator;

        private final Function evaluator;

        private final String expectedValue;

        @Override
        public boolean test(final T t) {
            return comparator.test(String.valueOf(evaluator.apply(t)), expectedValue);
        }
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy