| /******************************************************************************** |
| * Copyright (c) 2015-2019 Contributors to the Eclipse Foundation |
| * |
| * See the NOTICE file(s) distributed with this work for additional |
| * information regarding copyright ownership. |
| * |
| * This program and the accompanying materials are made available under the |
| * terms of the Eclipse Public License v. 2.0 which is available at |
| * http://www.eclipse.org/legal/epl-2.0. |
| * |
| * SPDX-License-Identifier: EPL-2.0 |
| * |
| ********************************************************************************/ |
| |
| package org.eclipse.mdm.businessobjects.control; |
| |
| import java.time.LocalDateTime; |
| import java.time.format.DateTimeParseException; |
| import java.util.ArrayList; |
| import java.util.List; |
| |
| import org.antlr.v4.runtime.ANTLRInputStream; |
| import org.antlr.v4.runtime.BaseErrorListener; |
| import org.antlr.v4.runtime.CommonTokenStream; |
| import org.antlr.v4.runtime.ParserRuleContext; |
| import org.antlr.v4.runtime.RecognitionException; |
| import org.antlr.v4.runtime.Recognizer; |
| import org.antlr.v4.runtime.misc.ParseCancellationException; |
| import org.antlr.v4.runtime.tree.TerminalNode; |
| import org.eclipse.mdm.api.base.adapter.Attribute; |
| import org.eclipse.mdm.api.base.adapter.EntityType; |
| import org.eclipse.mdm.api.base.model.ValueType; |
| import org.eclipse.mdm.api.base.query.ComparisonOperator; |
| import org.eclipse.mdm.api.base.query.Filter; |
| import org.eclipse.mdm.businessobjects.filter.FilterGrammarBaseVisitor; |
| import org.eclipse.mdm.businessobjects.filter.FilterGrammarLexer; |
| import org.eclipse.mdm.businessobjects.filter.FilterGrammarParser; |
| import org.eclipse.mdm.businessobjects.filter.FilterGrammarParser.AndExpressionContext; |
| import org.eclipse.mdm.businessobjects.filter.FilterGrammarParser.AttributeContext; |
| import org.eclipse.mdm.businessobjects.filter.FilterGrammarParser.ComparatorExpressionContext; |
| import org.eclipse.mdm.businessobjects.filter.FilterGrammarParser.ListComparatorExpressionContext; |
| import org.eclipse.mdm.businessobjects.filter.FilterGrammarParser.NotExpressionContext; |
| import org.eclipse.mdm.businessobjects.filter.FilterGrammarParser.OrExpressionContext; |
| import org.eclipse.mdm.businessobjects.filter.FilterGrammarParser.UnaryComparatorExpressionContext; |
| import org.eclipse.mdm.businessobjects.filter.FilterGrammarParser.ValueContext; |
| import org.eclipse.mdm.businessobjects.filter.FilterGrammarParser.ValuesContext; |
| import org.eclipse.mdm.businessobjects.utils.ServiceUtils; |
| |
| import com.google.common.base.Strings; |
| import com.google.common.primitives.Booleans; |
| import com.google.common.primitives.Bytes; |
| import com.google.common.primitives.Doubles; |
| import com.google.common.primitives.Floats; |
| import com.google.common.primitives.Ints; |
| import com.google.common.primitives.Longs; |
| import com.google.common.primitives.Shorts; |
| |
| /** |
| * Class for parsing filter strings. |
| * |
| * @author Matthias Koller |
| * |
| */ |
| public class FilterParser { |
| |
| private FilterParser() { |
| } |
| |
| /** |
| * Visitor class to convert the parsed tree into a {@link Filter}. |
| */ |
| private static final class FilterVisitor extends FilterGrammarBaseVisitor<Filter> { |
| private List<EntityType> availableEntityTypes; |
| |
| /** |
| * Constructs a new Visitor operating on the given search attributes. |
| * |
| * @param availableEntityTypes List of {@link EntityType}s to match the parsed |
| * attributes against. |
| */ |
| private FilterVisitor(List<EntityType> availableEntityTypes) { |
| this.availableEntityTypes = availableEntityTypes; |
| } |
| |
| /* |
| * (non-Javadoc) |
| * |
| * @see org.eclipse.mdm.businessobjects.filter.FilterGrammarBaseVisitor# |
| * visitAndExpression(org.eclipse.mdm.businessobjects.filter.FilterGrammarParser |
| * .AndExpressionContext) |
| */ |
| @Override |
| public Filter visitAndExpression(AndExpressionContext ctx) { |
| return Filter.and().merge(visit(ctx.left), visit(ctx.right)); |
| } |
| |
| /* |
| * (non-Javadoc) |
| * |
| * @see org.eclipse.mdm.businessobjects.filter.FilterGrammarBaseVisitor# |
| * visitOrExpression(org.eclipse.mdm.businessobjects.filter.FilterGrammarParser. |
| * OrExpressionContext) |
| */ |
| @Override |
| public Filter visitOrExpression(OrExpressionContext ctx) { |
| return Filter.or().merge(visit(ctx.left), visit(ctx.right)); |
| } |
| |
| /* |
| * (non-Javadoc) |
| * |
| * @see org.eclipse.mdm.businessobjects.filter.FilterGrammarBaseVisitor# |
| * visitNotExpression(org.eclipse.mdm.businessobjects.filter.FilterGrammarParser |
| * .NotExpressionContext) |
| */ |
| @Override |
| public Filter visitNotExpression(NotExpressionContext ctx) { |
| return super.visitNotExpression(ctx).invert(); |
| } |
| |
| /* |
| * (non-Javadoc) |
| * |
| * @see org.eclipse.mdm.businessobjects.filter.FilterGrammarBaseVisitor# |
| * visitComparatorExpression(org.eclipse.mdm.businessobjects.filter. |
| * FilterGrammarParser.ComparatorExpressionContext) |
| */ |
| @Override |
| public Filter visitComparatorExpression(ComparatorExpressionContext ctx) { |
| ComparisonOperator operator = getOperator(ctx.op); |
| Attribute attribute = getAttribute(ctx.left); |
| Object value = createConditionValue(attribute.getValueType(), getValue(ctx.right)); |
| |
| return Filter.and().add(operator.create(attribute, value)); |
| } |
| |
| /* |
| * (non-Javadoc) |
| * |
| * @see org.eclipse.mdm.businessobjects.filter.FilterGrammarBaseVisitor# |
| * visitListComparatorExpression(org.eclipse.mdm.businessobjects.filter. |
| * FilterGrammarParser.ListComparatorExpressionContext) |
| */ |
| @Override |
| public Filter visitListComparatorExpression(ListComparatorExpressionContext ctx) { |
| ComparisonOperator operator = getOperator(ctx.op); |
| Attribute attribute = getAttribute(ctx.left); |
| Object value = createConditionValues(attribute.getValueType(), getValues(ctx.right)); |
| |
| return Filter.and().add(operator.create(attribute, value)); |
| } |
| |
| /* |
| * (non-Javadoc) |
| * |
| * @see org.eclipse.mdm.businessobjects.filter.FilterGrammarBaseVisitor# |
| * visitUnaryComparatorExpression(org.eclipse.mdm.businessobjects.filter. |
| * FilterGrammarParser.UnaryComparatorExpressionContext) |
| */ |
| @Override |
| public Filter visitUnaryComparatorExpression(UnaryComparatorExpressionContext ctx) { |
| ComparisonOperator operator = getOperator(ctx.op); |
| Attribute attribute = getAttribute(ctx.left); |
| Object value = createConditionValue(attribute.getValueType(), null); |
| |
| return Filter.and().add(operator.create(attribute, value)); |
| } |
| |
| /* |
| * (non-Javadoc) |
| * |
| * @see |
| * org.antlr.v4.runtime.tree.AbstractParseTreeVisitor#aggregateResult(java.lang. |
| * Object, java.lang.Object) |
| */ |
| @Override |
| protected Filter aggregateResult(Filter aggregate, Filter nextResult) { |
| if (nextResult == null) { |
| return aggregate; |
| } |
| return super.aggregateResult(aggregate, nextResult); |
| } |
| |
| /** |
| * Extract a string from the given {@link ValueContext}. Basically this methods |
| * returns a string representation of the value without enclosing quotes (if |
| * there were any). |
| * |
| * @param ctx {@link ValueContext} containing the parsed value. |
| * @return string representation of the value given by {@link ValueContext} |
| */ |
| private String getValue(ValueContext ctx) { |
| |
| TerminalNode typeNode = (TerminalNode) ctx.getChild(0); |
| switch (typeNode.getSymbol().getType()) { |
| case FilterGrammarLexer.STRINGLITERAL: |
| String str = ctx.STRINGLITERAL().getText(); |
| if (!str.isEmpty() && str.charAt(0) == '\'') { |
| // replace leading and trailing ' and unescape ' |
| return ctx.STRINGLITERAL().getText().replaceAll("(\\A')|('\\z)", "").replaceAll("\\\\'", "'"); |
| } else if (!str.isEmpty() && str.charAt(0) == '\"') { |
| // replace leading and trailing " and unescape " |
| return ctx.STRINGLITERAL().getText().replaceAll("(\\A\")|(\"\\z)", "").replaceAll("\\\\\\\"", "\""); |
| } else { |
| return str; |
| } |
| case FilterGrammarLexer.DECIMAL: |
| case FilterGrammarLexer.LONG: |
| case FilterGrammarLexer.BOOL: |
| return ctx.getText(); |
| default: |
| throw new RuntimeException("Unsupported Symbol: " + typeNode.getSymbol().getType()); |
| } |
| } |
| |
| /** |
| * Extract a list string from the given {@link ValuesContext}. Basically this |
| * methods returns a list of string representations of the values without |
| * enclosing quotes (if there were any). |
| * |
| * @param ctx {@link ValuesContext} containing the parsed values. |
| * @return string representations of the values given by {@link ValuesContext} |
| */ |
| private List<String> getValues(ValuesContext ctx) { |
| List<String> values = new ArrayList<>(); |
| for (org.antlr.v4.runtime.tree.ParseTree child : ctx.children) { |
| if (child instanceof ValueContext) { |
| values.add(getValue((ValueContext) child)); |
| } |
| } |
| return values; |
| } |
| |
| /** |
| * Converts an {@link AttributeContext} into an {@link Attribute} using the list |
| * of available EntityTypes. |
| * |
| * @param ctx parsed entitytype / attribute |
| * @return the matched {@link Attribute} given by {@link AttributeContext} |
| * @throws IllegalArgumentException if {@link EntityType} or {@link Attribute} |
| * given by <code>ctx</code> cannot be found. |
| */ |
| private Attribute getAttribute(AttributeContext ctx) { |
| String[] name = ctx.getText().split("\\."); |
| return availableEntityTypes.stream().filter(e -> ServiceUtils.workaroundForTypeMapping(e).equals(name[0])) |
| .findAny() |
| .orElseThrow(() -> new IllegalArgumentException("Entity " + name[0] + " not found in data source!")) |
| .getAttribute(name[1]); |
| } |
| |
| /** |
| * Converts a {@link ParserRuleContext} containing a {@link TerminalNode} into a |
| * {@link ComparisonOperator}. |
| * |
| * @param ctx {@link UnaryComparatorExpressionContext}, |
| * {@link ComparatorExpressionContext} or |
| * {@link ListComparatorExpressionContext} or |
| * @return converted {@link ComparisonOperator} |
| * @throws IllegalArgumentException if the operator given by <code>ctx</code> is |
| * unknown or cannot be converted. |
| */ |
| private ComparisonOperator getOperator(ParserRuleContext ctx) { |
| TerminalNode typeNode = (TerminalNode) ctx.getChild(0); |
| switch (typeNode.getSymbol().getType()) { |
| case FilterGrammarLexer.EQUAL: |
| return ComparisonOperator.EQUAL; |
| case FilterGrammarLexer.NOT_EQUAL: |
| return ComparisonOperator.NOT_EQUAL; |
| case FilterGrammarLexer.LESS_THAN: |
| return ComparisonOperator.LESS_THAN; |
| case FilterGrammarLexer.LESS_THAN_OR_EQUAL: |
| return ComparisonOperator.LESS_THAN_OR_EQUAL; |
| case FilterGrammarLexer.GREATER_THAN: |
| return ComparisonOperator.GREATER_THAN; |
| case FilterGrammarLexer.GREATER_THAN_OR_EQUAL: |
| return ComparisonOperator.GREATER_THAN_OR_EQUAL; |
| case FilterGrammarLexer.IN_SET: |
| return ComparisonOperator.IN_SET; |
| case FilterGrammarLexer.NOT_IN_SET: |
| return ComparisonOperator.NOT_IN_SET; |
| case FilterGrammarLexer.LIKE: |
| return ComparisonOperator.LIKE; |
| case FilterGrammarLexer.NOT_LIKE: |
| return ComparisonOperator.NOT_LIKE; |
| case FilterGrammarLexer.CASE_INSENSITIVE_EQUAL: |
| return ComparisonOperator.CASE_INSENSITIVE_EQUAL; |
| case FilterGrammarLexer.CASE_INSENSITIVE_NOT_EQUAL: |
| return ComparisonOperator.CASE_INSENSITIVE_NOT_EQUAL; |
| case FilterGrammarLexer.CASE_INSENSITIVE_LESS_THAN: |
| return ComparisonOperator.CASE_INSENSITIVE_LESS_THAN; |
| case FilterGrammarLexer.CASE_INSENSITIVE_LESS_THAN_OR_EQUAL: |
| return ComparisonOperator.CASE_INSENSITIVE_LESS_THAN_OR_EQUAL; |
| case FilterGrammarLexer.CASE_INSENSITIVE_GREATER_THAN: |
| return ComparisonOperator.CASE_INSENSITIVE_GREATER_THAN; |
| case FilterGrammarLexer.CASE_INSENSITIVE_GREATER_THAN_OR_EQUAL: |
| return ComparisonOperator.CASE_INSENSITIVE_GREATER_THAN_OR_EQUAL; |
| case FilterGrammarLexer.CASE_INSENSITIVE_IN_SET: |
| return ComparisonOperator.CASE_INSENSITIVE_IN_SET; |
| case FilterGrammarLexer.CASE_INSENSITIVE_NOT_IN_SET: |
| return ComparisonOperator.CASE_INSENSITIVE_NOT_IN_SET; |
| case FilterGrammarLexer.CASE_INSENSITIVE_LIKE: |
| return ComparisonOperator.CASE_INSENSITIVE_LIKE; |
| case FilterGrammarLexer.CASE_INSENSITIVE_NOT_LIKE: |
| return ComparisonOperator.CASE_INSENSITIVE_NOT_LIKE; |
| case FilterGrammarLexer.IS_NULL: |
| return ComparisonOperator.IS_NULL; |
| case FilterGrammarLexer.IS_NOT_NULL: |
| return ComparisonOperator.IS_NOT_NULL; |
| case FilterGrammarLexer.BETWEEN: |
| return ComparisonOperator.BETWEEN; |
| default: |
| throw new IllegalArgumentException( |
| "Operator " + typeNode.getSymbol().getType() + " not supported yet!"); |
| } |
| } |
| } |
| |
| /** |
| * Class to convert a antlr syntax error into a unchecked |
| * ParserCancellationException. |
| */ |
| private static class ThrowingErrorListener extends BaseErrorListener { |
| |
| public static final ThrowingErrorListener INSTANCE = new ThrowingErrorListener(); |
| |
| /* |
| * (non-Javadoc) |
| * |
| * @see org.antlr.v4.runtime.BaseErrorListener#syntaxError(org.antlr.v4.runtime. |
| * Recognizer, java.lang.Object, int, int, java.lang.String, |
| * org.antlr.v4.runtime.RecognitionException) |
| */ |
| @Override |
| public void syntaxError(Recognizer<?, ?> recognizer, Object offendingSymbol, int line, int charPositionInLine, |
| String msg, RecognitionException e) throws ParseCancellationException { |
| throw new ParseCancellationException("line " + line + ":" + charPositionInLine + " " + msg); |
| } |
| } |
| |
| /** |
| * Parses the given filter string. The filter string must conform to the ANTLR |
| * grammer defined in FilterGrammar.g4. |
| * |
| * @param possibleEntityTypes The possible {@link EntityType}s / |
| * {@link Attribute}s |
| * @param filterString The filter string to parse. |
| * @return the parsed {@link Filter} |
| * @throws IllegalArgumentExceptionThrown if parsing fails. |
| */ |
| public static Filter parseFilterString(List<EntityType> possibleEntityTypes, String filterString) |
| throws IllegalArgumentException { |
| |
| if (Strings.isNullOrEmpty(filterString)) { |
| return Filter.and(); |
| } |
| |
| try { |
| FilterGrammarLexer lexer = new FilterGrammarLexer(new ANTLRInputStream(filterString)); |
| lexer.removeErrorListeners(); |
| lexer.addErrorListener(ThrowingErrorListener.INSTANCE); |
| |
| FilterGrammarParser parser = new FilterGrammarParser(new CommonTokenStream(lexer)); |
| parser.removeErrorListeners(); |
| parser.addErrorListener(ThrowingErrorListener.INSTANCE); |
| |
| return new FilterVisitor(possibleEntityTypes).visit(parser.parse()); |
| } catch (ParseCancellationException e) { |
| throw new IllegalArgumentException( |
| "Could not parse filter string '" + filterString + "'. Error: " + e.getMessage(), e); |
| } |
| } |
| |
| /** |
| * Creates the value for the condition from the value given as string. |
| * |
| * @param valueType The type that the value should have. |
| * @param valueAsString The value as string. |
| * @return The created value for the condition. |
| * @throws IllegalArgumentException Thrown if the value type is not supported |
| */ |
| private static Object createConditionValue(ValueType<?> valueType, String valueAsString) { |
| Object ret = null; |
| if (ValueType.BOOLEAN.equals(valueType)) { |
| ret = Boolean.valueOf(valueAsString); |
| } else if (ValueType.LONG.equals(valueType)) { |
| ret = Long.valueOf(valueAsString); |
| } else if (ValueType.STRING.equals(valueType)) { |
| ret = valueAsString; |
| } else if (ValueType.BYTE.equals(valueType)) { |
| ret = Byte.valueOf(valueAsString); |
| } else if (ValueType.DOUBLE.equals(valueType)) { |
| ret = Double.valueOf(valueAsString); |
| } else if (ValueType.FLOAT.equals(valueType)) { |
| ret = Float.valueOf(valueAsString); |
| } else if (ValueType.INTEGER.equals(valueType)) { |
| ret = Integer.valueOf(valueAsString); |
| } else if (ValueType.SHORT.equals(valueType)) { |
| ret = Short.valueOf(valueAsString); |
| } else if (ValueType.DATE.equals(valueType)) { |
| try { |
| ret = LocalDateTime.parse(valueAsString); |
| } catch (DateTimeParseException e) { |
| throw new IllegalArgumentException( |
| "Unsupported value for date: '" + valueAsString + "'. Expected format: '2007-12-03T10:15:30'"); |
| } |
| } else { |
| throw new IllegalArgumentException("Unsupported value type: " + valueType.toString()); |
| } |
| return ret; |
| } |
| |
| /** |
| * Creates the values for the condition from the values given in the list of |
| * strings. |
| * |
| * @param valueType The type that the value should have. |
| * @param valuesAsString The values as a list of string. |
| * @return The created value for the condition. |
| * @throws IllegalArgumentException Thrown if the value type is not supported |
| */ |
| private static Object createConditionValues(ValueType<?> valueType, List<String> valuesAsStrings) { |
| |
| if (ValueType.BOOLEAN.equals(valueType)) { |
| List<Boolean> list = new ArrayList<>(); |
| for (String valueAsString : valuesAsStrings) { |
| list.add(Boolean.valueOf(valueAsString)); |
| } |
| return Booleans.toArray(list); |
| } else if (ValueType.LONG.equals(valueType)) { |
| List<Long> list = new ArrayList<>(); |
| for (String valueAsString : valuesAsStrings) { |
| list.add(Long.valueOf(valueAsString)); |
| } |
| return Longs.toArray(list); |
| } else if (ValueType.STRING.equals(valueType)) { |
| List<String> list = new ArrayList<>(); |
| for (String valueAsString : valuesAsStrings) { |
| list.add(valueAsString); |
| } |
| return list.toArray(new String[0]); |
| } else if (ValueType.BYTE.equals(valueType)) { |
| List<Byte> list = new ArrayList<>(); |
| for (String valueAsString : valuesAsStrings) { |
| list.add(Byte.valueOf(valueAsString)); |
| } |
| return Bytes.toArray(list); |
| } else if (ValueType.DOUBLE.equals(valueType)) { |
| List<Double> list = new ArrayList<>(); |
| for (String valueAsString : valuesAsStrings) { |
| list.add(Double.valueOf(valueAsString)); |
| } |
| return Doubles.toArray(list); |
| } else if (ValueType.FLOAT.equals(valueType)) { |
| List<Float> list = new ArrayList<>(); |
| for (String valueAsString : valuesAsStrings) { |
| list.add(Float.valueOf(valueAsString)); |
| } |
| return Floats.toArray(list); |
| } else if (ValueType.INTEGER.equals(valueType)) { |
| List<Integer> list = new ArrayList<>(); |
| for (String valueAsString : valuesAsStrings) { |
| list.add(Integer.valueOf(valueAsString)); |
| } |
| return Ints.toArray(list); |
| } else if (ValueType.SHORT.equals(valueType)) { |
| List<Short> list = new ArrayList<>(); |
| for (String valueAsString : valuesAsStrings) { |
| list.add(Short.valueOf(valueAsString)); |
| } |
| return Shorts.toArray(list); |
| } else if (ValueType.DATE.equals(valueType)) { |
| List<LocalDateTime> list = new ArrayList<>(); |
| for (String valueAsString : valuesAsStrings) { |
| try { |
| list.add(LocalDateTime.parse(valueAsString)); |
| } catch (DateTimeParseException e) { |
| throw new IllegalArgumentException("Unsupported value for date: '" + valueAsString |
| + "'. Expected format: '2007-12-03T10:15:30'"); |
| } |
| } |
| return list.toArray(new LocalDateTime[0]); |
| } else { |
| throw new IllegalArgumentException("Unsupported value type: " + valueType.toString()); |
| } |
| } |
| } |