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}