Skip to content

Commit

Permalink
Initial implementation of the operator registry
Browse files Browse the repository at this point in the history
  • Loading branch information
Jean Sossmeier committed Apr 4, 2024
1 parent 1279484 commit 1eba776
Show file tree
Hide file tree
Showing 10 changed files with 302 additions and 301 deletions.
39 changes: 22 additions & 17 deletions src/main/java/com/intuit/graphql/filter/ast/Operator.java
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,13 @@ public class Operator {
private String type;
private Kind kind;

public Operator(String name, String type, Kind kind) {
public Operator(String name, Kind kind, String... type) {
this.name = name;
this.type = type;
this.kind = kind;
this.type = String.join("|", type);
}


public String getName() { return name; }
public String getType() { return type; }
public Kind getKind() { return kind; }
Expand All @@ -41,19 +42,23 @@ public enum Kind {
COMPOUND, BINARY, UNARY
}

public static final Operator AND = new Operator("and", "Logical", Operator.Kind.COMPOUND);

public static final Operator OR = new Operator("or", "Logical", Operator.Kind.COMPOUND);
public static final Operator NOT = new Operator("not", "Logical", Operator.Kind.UNARY);
public static final Operator EQUALS = new Operator("equals", "String", Operator.Kind.BINARY);
public static final Operator CONTAINS = new Operator("contains", "String", Operator.Kind.BINARY);
public static final Operator STARTS = new Operator("starts", "String", Operator.Kind.BINARY);
public static final Operator ENDS = new Operator("ends", "String", Operator.Kind.BINARY);
public static final Operator EQ = new Operator("eq", "Numeric", Operator.Kind.BINARY);
public static final Operator GT = new Operator("gt", "Numeric", Operator.Kind.BINARY);
public static final Operator GTE = new Operator("gte", "Numeric", Operator.Kind.BINARY);
public static final Operator LT = new Operator("lt", "Numeric", Operator.Kind.BINARY);
public static final Operator LTE = new Operator("lte", "Numeric", Operator.Kind.BINARY);
public static final Operator IN = new Operator("in", "String|Numeric", Operator.Kind.BINARY);
public static final Operator BETWEEN = new Operator("between", "DateTime|Numeric", Operator.Kind.BINARY);
public static final String TYPE_LOGICAL = "Logical";
public static final String TYPE_STRING = "String";
public static final String TYPE_NUMERIC = "Numeric";
public static final String TYPE_DATETIME = "DateTime";

public static final Operator AND = new Operator("and", Operator.Kind.COMPOUND, TYPE_LOGICAL);
public static final Operator OR = new Operator("or", Operator.Kind.COMPOUND, TYPE_LOGICAL);
public static final Operator NOT = new Operator("not", Operator.Kind.UNARY, TYPE_LOGICAL);
public static final Operator EQUALS = new Operator("equals", Operator.Kind.BINARY, TYPE_STRING);
public static final Operator CONTAINS = new Operator("contains", Operator.Kind.BINARY, TYPE_STRING);
public static final Operator STARTS = new Operator("starts", Operator.Kind.BINARY, TYPE_STRING);
public static final Operator ENDS = new Operator("ends", Operator.Kind.BINARY, TYPE_STRING);
public static final Operator EQ = new Operator("eq", Operator.Kind.BINARY, TYPE_NUMERIC);
public static final Operator GT = new Operator("gt", Operator.Kind.BINARY, TYPE_NUMERIC);
public static final Operator GTE = new Operator("gte", Operator.Kind.BINARY, TYPE_NUMERIC);
public static final Operator LT = new Operator("lt", Operator.Kind.BINARY, TYPE_NUMERIC);
public static final Operator LTE = new Operator("lte", Operator.Kind.BINARY, TYPE_NUMERIC);
public static final Operator IN = new Operator("in", Operator.Kind.BINARY, TYPE_STRING, TYPE_NUMERIC);
public static final Operator BETWEEN = new Operator("between", Operator.Kind.BINARY, TYPE_DATETIME, TYPE_NUMERIC);
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,14 +35,14 @@
import static com.intuit.graphql.filter.ast.Operator.STARTS;

/**
* Class that represents a registry
* Class that represents an operator registry
*
* @author jeansossmeier
*/
public class OperatorRegistry {
private static final OperatorRegistry INSTANCE = new OperatorRegistry();

public static OperatorRegistry getInstance() {
public static OperatorRegistry defaultInstance() {
return INSTANCE;
}

Expand All @@ -61,7 +61,7 @@ public Operator getOperator(String name) {
}

public static OperatorRegistry withDefaultOperators() {
final OperatorRegistry registry = OperatorRegistry.getInstance();
final OperatorRegistry registry = OperatorRegistry.defaultInstance();

// Logical Operators
registry.registerOperator(AND);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,9 @@
* expression visitors.
*
* @author sjaiswal
* @author jeansossmeier
*/
class ExpressionVisitorFactory {
public class ExpressionVisitorFactory {

/**
* Factory method for creating and returning
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
* of specified format.
*
* @author sjaiswal
* @author jeansossmeier
*/
public class FilterExpression {

Expand All @@ -48,12 +49,12 @@ private FilterExpression(FilterExpressionBuilder expressionBuilder) {
* instance of FilterExpression class.
*/
public static class FilterExpressionBuilder {
private static final String FILTER_ARG = "filter";

private Field field;
private Map<String,String> fieldMap;
private Expression expressionAst;
private Map args;
private final String FILTER_ARG = "filter";
private FieldValueTransformer fieldValueTransformer;

private FilterExpressionBuilder () {
Expand Down Expand Up @@ -93,8 +94,8 @@ public FilterExpression build() {
expressionAst = expressionParser.parseFilterExpression((Map) filter);
}
}
FilterExpression expression = new FilterExpression(this);
return expression;

return new FilterExpression(this);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,17 +15,43 @@
*/
package com.intuit.graphql.filter.client;

import com.intuit.graphql.filter.ast.*;

import java.time.*;
import java.util.*;
import com.intuit.graphql.filter.ast.BinaryExpression;
import com.intuit.graphql.filter.ast.CompoundExpression;
import com.intuit.graphql.filter.ast.Expression;
import com.intuit.graphql.filter.ast.ExpressionField;
import com.intuit.graphql.filter.ast.ExpressionValue;
import com.intuit.graphql.filter.ast.Operator;
import com.intuit.graphql.filter.ast.OperatorRegistry;
import com.intuit.graphql.filter.ast.UnaryExpression;

import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.OffsetDateTime;
import java.time.ZoneId;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Deque;
import java.util.List;
import java.util.Map;
import java.util.Set;

/**
* GraphQL Filter Expression Parser.
*
* @author sjaiswal
* @author jeansossmeier
*/
class FilterExpressionParser {
public class FilterExpressionParser {
private final OperatorRegistry operatorRegistry;

public FilterExpressionParser() {
this.operatorRegistry = OperatorRegistry.withDefaultOperators();
}

public FilterExpressionParser(OperatorRegistry operatorRegistry) {
this.operatorRegistry = operatorRegistry;
}

/**
* Parses the given graphql filter expression AST.
Expand All @@ -46,7 +72,7 @@ private Expression createExpressionTree(Map filterMap) {
for (Map.Entry entry : entries) {
String key = entry.getKey().toString();
if (isOperator(key)) {
String kind = Operator.getOperatorKind(key);
String kind = getOperatorKind(key);
switch (kind) {

/* Case to handle the compound expression.*/
Expand All @@ -57,7 +83,7 @@ private Expression createExpressionTree(Map filterMap) {
Expression left = expressionStack.peek();
if (validateExpression(right) && validateExpression(left)) {
left = expressionStack.pop();
Expression newExp = new CompoundExpression(left, Operator.getOperator(key), right);
Expression newExp = new CompoundExpression(left, getOperator(key), right);
expressionStack.push(newExp);
} else {
expressionStack.push(right);
Expand All @@ -69,7 +95,7 @@ private Expression createExpressionTree(Map filterMap) {
/* Case to handle the binary expression.*/
case "BINARY":
BinaryExpression binaryExpression = new BinaryExpression();
binaryExpression.setOperator(Operator.getOperator(key));
binaryExpression.setOperator(getOperator(key));
if (entry.getValue() instanceof Collection) {
List<Comparable> expressionValues = new ArrayList<>();
List<Comparable> operandValues = (List<Comparable>) entry.getValue();
Expand All @@ -87,7 +113,7 @@ private Expression createExpressionTree(Map filterMap) {

case "UNARY":
Expression operand = createExpressionTree((Map)entry.getValue());
expression = new UnaryExpression(operand,Operator.getOperator(key), null);
expression = new UnaryExpression(operand, getOperator(key), null);
break;
}
} else {
Expand All @@ -102,10 +128,18 @@ private Expression createExpressionTree(Map filterMap) {
return expression;
}

private Operator getOperator(String key) {
return operatorRegistry.getOperator(key);
}

private String getOperatorKind(String key) {
return getOperator(key).getKind().name();
}

private boolean isOperator(String key) {
Operator operator = null;
try {
operator = Operator.getOperator(key);
operator = operatorRegistry.getOperator(key);
} catch (Exception ex) {

}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,15 @@
import com.intuit.graphql.filter.ast.Expression;
import com.intuit.graphql.filter.ast.ExpressionField;
import com.intuit.graphql.filter.ast.ExpressionValue;
import com.intuit.graphql.filter.ast.Operator;
import com.intuit.graphql.filter.ast.UnaryExpression;
import com.intuit.graphql.filter.client.FieldValuePair;
import com.intuit.graphql.filter.client.FieldValueTransformer;
import org.springframework.data.elasticsearch.core.query.Criteria;

import java.util.ArrayDeque;
import java.util.Deque;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

Expand All @@ -44,6 +46,38 @@ public class ElasticsearchCriteriaExpressionVisitor implements ExpressionVisitor
private final Deque<String> fieldStack;
private final FieldValueTransformer fieldValueTransformer;

@FunctionalInterface
public interface CriteriaStrategy {
Criteria apply(Criteria criteria, String fieldName, Comparable<?> value);
}

private static final Map<Operator, CriteriaStrategy> MAPPINGS = new HashMap<>();

static {
MAPPINGS.put(Operator.STARTS, (criteria, fieldName, value) -> criteria.where(fieldName).startsWith(value.toString()));
MAPPINGS.put(Operator.ENDS, (criteria, fieldName, value) -> criteria.where(fieldName).endsWith(value.toString()));
MAPPINGS.put(Operator.CONTAINS, (criteria, fieldName, value) -> criteria.where(fieldName).contains(value.toString()));
MAPPINGS.put(Operator.EQUALS, (criteria, fieldName, value) -> criteria.where(fieldName).expression("'" + value.toString() + "'"));
MAPPINGS.put(Operator.LT, (criteria, fieldName, value) -> criteria.where(fieldName).lessThan(value));
MAPPINGS.put(Operator.LTE, (criteria, fieldName, value) -> criteria.where(fieldName).lessThanEqual(value));
MAPPINGS.put(Operator.GT, (criteria, fieldName, value) -> criteria.where(fieldName).greaterThan(value));
MAPPINGS.put(Operator.GTE, (criteria, fieldName, value) -> criteria.where(fieldName).greaterThanEqual(value));
MAPPINGS.put(Operator.EQ, (criteria, fieldName, value) -> criteria.where(fieldName).is(value));
MAPPINGS.put(Operator.IN, (criteria, fieldName, value) -> criteria.where(fieldName).in((List<?>) value));
MAPPINGS.put(Operator.BETWEEN, (criteria, fieldName, value) -> {
List<?> values = (List<?>) value;
return criteria.where(fieldName).greaterThanEqual(values.get(0)).lessThanEqual(values.get(1));
});
}

public static void addStrategy(Operator operator, CriteriaStrategy strategy) {
MAPPINGS.put(operator, strategy);
}

public static void removeStrategy(Operator operator) {
MAPPINGS.remove(operator);
}

public ElasticsearchCriteriaExpressionVisitor(final Map<String, String> fieldMap, final FieldValueTransformer fieldValueTransformer) {
this.fieldMap = fieldMap;
this.fieldStack = new ArrayDeque<>();
Expand Down Expand Up @@ -75,19 +109,17 @@ public Criteria expression(final Expression expression) {
@Override
public Criteria visitCompoundExpression(final CompoundExpression compoundExpression, final Criteria data) {
Criteria result = null;
switch (compoundExpression.getOperator()) {
/* Logical operations.*/
case AND:
Criteria left = compoundExpression.getLeftOperand().accept(this, null);
Criteria right = compoundExpression.getRightOperand().accept(this, null);
result = left.and(right);
break;

case OR:
left = compoundExpression.getLeftOperand().accept(this, null);
right = compoundExpression.getRightOperand().accept(this, null);
result = left.or(right);
break;
Operator operator = compoundExpression.getOperator();
if (Operator.AND.equals(operator)) {
Criteria left = compoundExpression.getLeftOperand().accept(this, null);
Criteria right = compoundExpression.getRightOperand().accept(this, null);
result = left.and(right);
} else if (Operator.OR.equals(operator)) {
Criteria right;
Criteria left;
left = compoundExpression.getLeftOperand().accept(this, null);
right = compoundExpression.getRightOperand().accept(this, null);
result = left.or(right);
}
return result;
}
Expand All @@ -101,61 +133,18 @@ public Criteria visitCompoundExpression(final CompoundExpression compoundExpress
*/
@Override
public Criteria visitBinaryExpression(final BinaryExpression binaryExpression, final Criteria data) {
Criteria criteria = null;
final String fieldName = mappedFieldName(binaryExpression.getLeftOperand().infix());
ExpressionValue<? extends Comparable> operandValue = (ExpressionValue<? extends Comparable>) binaryExpression.getRightOperand();
operandValue = getTransformedValue(operandValue);
return applyOperator(binaryExpression, data, fieldName, operandValue.value());
}

switch (binaryExpression.getOperator()) {
/* String operations.*/
case STARTS:
criteria = Criteria.where(fieldName).startsWith(operandValue.value().toString());
break;

case ENDS:
criteria = Criteria.where(fieldName).endsWith(operandValue.value().toString());
break;

case CONTAINS:
criteria = Criteria.where(fieldName).contains(operandValue.value().toString());
break;

case EQUALS:
criteria = Criteria.where(fieldName).expression(QUOTE_CHARACTER + operandValue.value().toString() + QUOTE_CHARACTER);
break;

/* Numeric operations.*/
case LT:
criteria = Criteria.where(fieldName).lessThan(operandValue.value());
break;

case LTE:
criteria = Criteria.where(fieldName).lessThanEqual(operandValue.value());
break;

case EQ:
criteria = Criteria.where(fieldName).is(operandValue.value());
break;

case GT:
criteria = Criteria.where(fieldName).greaterThan(operandValue.value());
break;

case GTE:
criteria = Criteria.where(fieldName).greaterThanEqual(operandValue.value());
break;

case IN:
List<Comparable> expressionInValues = (List<Comparable>) operandValue.value();
criteria = Criteria.where(fieldName).in(expressionInValues);
break;

case BETWEEN:
List<Comparable> expressionBetweenValues = (List<Comparable>) operandValue.value();
criteria = Criteria.where(fieldName).greaterThanEqual(expressionBetweenValues.get(0)).lessThanEqual(expressionBetweenValues.get(1));
break;
public Criteria applyOperator(BinaryExpression binaryExpression, Criteria data, String fieldName, Comparable<?> value) {
CriteriaStrategy strategy = MAPPINGS.get(binaryExpression.getOperator());
if (strategy == null) {
throw new UnsupportedOperationException("Unsupported operator: " + binaryExpression.getOperator());
}
return criteria;
return strategy.apply(data, fieldName, value);
}

/**
Expand Down
Loading

0 comments on commit 1eba776

Please sign in to comment.