BaseExpressionProvider.java
/*******************************************************************************
* Copyright (c) 2018 @gt_tech
*
* 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.bitbucket.gt_tech.spring.data.querydsl.value.operators;
import com.querydsl.core.types.Path;
import com.querydsl.core.types.dsl.BooleanExpression;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.Validate;
import org.bitbucket.gt_tech.spring.data.querydsl.value.operators.experimental.QuerydslHttpRequestContext;
import org.bitbucket.gt_tech.spring.data.querydsl.value.operators.experimental.QuerydslHttpRequestContextHolder;
import java.text.MessageFormat;
import java.util.*;
import java.util.stream.Collectors;
/**
* Base implementation of {@link ExpressionProvider} which handles the plumbing
* of extracting operators with their composition and delegates to specific
* implementation for final expression formation.
* <p>
* <p>
* Implementation must support a specific implementation of {@link Path} type
* and may chose to not support every {@link Operator} depending on it's own
* logic. In which case implementation is required to throw an
* {@link UnsupportedOperationException}
* </p>
*
* @param <P> type of {@link Path}
* @author gt_tech
*/
abstract class BaseExpressionProvider<P extends Path> implements ExpressionProvider<P, Object> {
private final List<Operator> SUPPORTED_SINGLE_VALUED_COMPARISON_OPERATORS;
/**
* Constructor
*
* @param supportedSingleValueComparisonOperators Collection of {@link Operator} supported by specific
* implementation of this base class for single-value
* comparisons.
*/
public BaseExpressionProvider(List<Operator> supportedSingleValueComparisonOperators) {
Validate.isTrue(CollectionUtils.isNotEmpty(supportedSingleValueComparisonOperators),
"Supported Single value" + " operators must be > 1");
this.SUPPORTED_SINGLE_VALUED_COMPARISON_OPERATORS = supportedSingleValueComparisonOperators;
}
@Override
public Optional<BooleanExpression> getExpression(P path, Object value) {
return Optional.ofNullable(path) // check path
.map(p -> value) // check for value
.map(v -> {
if (Collection.class.isAssignableFrom(v.getClass())) {
return new MultiValueExpressionBuilder(path, (Collection) v).getExpression();
} else {
/*
* delegate to MultiValueExpressionBuilder instead of SingleValueExpressionBuilder
* as MultiValueExpressionBuilder also checks for QuerydslHttpRequestContext if its
* needed. SingleValueExpressionBuilder doesn't do it.
*/
return new MultiValueExpressionBuilder(path, Arrays.asList(
getStringValue(path, v))).getExpression();
}
});
}
/*
* START: Methods for concrete implementation in sub-classes depending on if
* Path sub-type doesn't support same logic and may require a sub-query
* expression.
*/
/**
* Returns String value for provided object (value supplied by bindings
* during bindings invocation phase)
*
* @param path Specific type of {@link Path}
* @param value Value as received from bindings invoker.
* @return String value for the provided value object.
*/
protected abstract <S extends String> S getStringValue(P path, Object value);
/**
* Creates a expression for equals clause - {@link Operator#EQUAL} operator
*
* @param path Specific type of {@link Path}
* @param value String value to be used for making expression.
* @param ignoreCase if comparison must be done ignoring case if case is applicable to target value type.
* @return {@link BooleanExpression} to be used further by downstream query
* serialization logic for executing actual query
* @throws UnsupportedOperationException if implementation doesn't support this {@link Operator}
*/
protected abstract BooleanExpression eq(P path, String value, boolean ignoreCase);
/**
* Creates a expression for not-equals clause - {@link Operator#NOT_EQUAL}
* operator
*
* @param path Specific type of {@link Path}
* @param value String value to be used for making expression.
* @param ignoreCase if comparison must be done ignoring case if case is applicable to target value type.
* @return {@link BooleanExpression} to be used further by downstream query
* serialization logic for executing actual query
* @throws UnsupportedOperationException if implementation doesn't support this {@link Operator}
*/
protected abstract BooleanExpression ne(P path, String value, boolean ignoreCase);
/**
* Creates a expression for contains/like clause - {@link Operator#CONTAINS}
* operator
*
* @param path Specific type of {@link Path}
* @param value String value to be used for making expression
* @param ignoreCase if comparison must be done ignoring case if case is applicable to target value type. .
* @return {@link BooleanExpression} to be used further by downstream query
* serialization logic for executing actual query
* @throws UnsupportedOperationException if implementation doesn't support this {@link Operator}
*/
protected abstract BooleanExpression contains(P path, String value, boolean ignoreCase);
/**
* Creates a expression for startsWith clause - {@link Operator#STARTS_WITH}
* operator
*
* @param path Specific type of {@link Path}
* @param value String value to be used for making expression.
* @param ignoreCase if comparison must be done ignoring case if case is applicable to target value type.
* @return {@link BooleanExpression} to be used further by downstream query
* serialization logic for executing actual query
* @throws UnsupportedOperationException if implementation doesn't support this {@link Operator}
*/
protected abstract BooleanExpression startsWith(P path, String value, boolean ignoreCase);
/**
* Creates a expression for endsWith clause - {@link Operator#ENDS_WITH}
* operator
*
* @param path Specific type of {@link Path}
* @param value String value to be used for making expression.
* @param ignoreCase if comparison must be done ignoring case if case is applicable to target value type.
* @return {@link BooleanExpression} to be used further by downstream query
* serialization logic for executing actual query
* @throws UnsupportedOperationException if implementation doesn't support this {@link Operator}
*/
protected abstract BooleanExpression endsWith(P path, String value, boolean ignoreCase);
/**
* Creates a expression for matches clause - {@link Operator#MATCHES}
* operator
*
* @param path Specific type of {@link Path}
* @param value String value to be used for making expression.
* @return {@link BooleanExpression} to be used further by downstream query
* serialization logic for executing actual query
* @throws UnsupportedOperationException if implementation doesn't support this {@link Operator}
*/
protected abstract BooleanExpression matches(P path, String value);
/**
* Creates a expression for greater-than clause -
* {@link Operator#GREATER_THAN} operator
*
* @param path Specific type of {@link Path}
* @param value String value to be used for making expression.
* @return {@link BooleanExpression} to be used further by downstream query
* serialization logic for executing actual query
* @throws UnsupportedOperationException if implementation doesn't support this {@link Operator}
*/
protected abstract BooleanExpression gt(P path, String value);
/**
* Creates a expression for greater-than-equal clause -
* {@link Operator#GREATER_THAN_OR_EQUAL} operator
*
* @param path Specific type of {@link Path}
* @param value String value to be used for making expression.
* @return {@link BooleanExpression} to be used further by downstream query
* serialization logic for executing actual query
* @throws UnsupportedOperationException if implementation doesn't support this {@link Operator}
*/
protected abstract BooleanExpression gte(P path, String value);
/**
* Creates a expression for less-than clause - {@link Operator#LESS_THAN}
* operator
*
* @param path Specific type of {@link Path}
* @param value String value to be used for making expression.
* @return {@link BooleanExpression} to be used further by downstream query
* serialization logic for executing actual query
* @throws UnsupportedOperationException if implementation doesn't support this {@link Operator}
*/
protected abstract BooleanExpression lt(P path, String value);
/**
* Creates a expression for less-than or equal clause -
* {@link Operator#LESS_THAN_OR_EQUAL} operator
*
* @param path Specific type of {@link Path}
* @param value String value to be used for making expression.
* @return {@link BooleanExpression} to be used further by downstream query
* serialization logic for executing actual query
* @throws UnsupportedOperationException if implementation doesn't support this {@link Operator}
*/
protected abstract BooleanExpression lte(P path, String value);
/*
* STOP: Abstract methods for concrete implementation
*/
/**
* Logical operators implementation
*/
/**
* Applies a logical NOT (negate) to provided expression.
*
* @param expression
* @return Negated expression that must be used further in
* expression-building process
*/
protected final BooleanExpression not(BooleanExpression expression) {
Validate.notNull(expression);
return expression.not();
}
/**
* Applies logical AND clause to provided expressions.
*
* @param left Left operand for AND operation
* @param right Right operand for AND operation
* @return expression with AND clause applied to provided two values, this
* must be used further in expression-building process
*/
protected final BooleanExpression and(BooleanExpression left, BooleanExpression right) {
Validate.notNull(left);
Validate.notNull(right);
return left.and(right);
}
/**
* Applies logical OR clause to provided expressions.
*
* @param left Left operand for OR operation
* @param right Right operand for OR operation
* @return expression with AND clause applied to provided two values, this
* must be used further in expression-building process
*/
protected final BooleanExpression or(BooleanExpression left, BooleanExpression right) {
Validate.notNull(left);
Validate.notNull(right);
return left.or(right);
}
/**
* Utility class for building stateful expressions from provided values
*/
private class MultiValueExpressionBuilder {
private final P path;
private final Collection<Object> values;
private BooleanExpression expression;
private final Collection<Operator> MULTI_VALUE_LOGICAL_OPERATORS = Collections
.unmodifiableCollection(Arrays.asList(Operator.AND, Operator.OR));
public MultiValueExpressionBuilder(P path, Collection<Object> values) {
this.path = path;
this.values = values;
this.values.forEach(v -> ExpressionProvider.validateComposition(getStringValue(path, v)));
}
public BooleanExpression getExpression() {
Operator default_operator = null; // if first param overrides the
// default for multi-value to be
// AND,
// we set it here, this would help that if first Operator for a
// multi-value comparison has and(..) then all subsequent value will
// use and(..) as default operator instead of default OR
if (CollectionUtils.isNotEmpty(this.values)) {
if (this.values.size() == 1) {
String value = checkIfOriginalRequestValueAvailable(path,
getStringValue(path, this.values.iterator()
.next()));
while (true) {
if (ExpressionProvider
.isOperator(MULTI_VALUE_LOGICAL_OPERATORS
.toArray(new Operator[MULTI_VALUE_LOGICAL_OPERATORS.size()]), value)
.isPresent()) {
// got an ill-placed Logical operator that's meant
// for multi-value searches on fields
value = new OperatorAndValue(value, MULTI_VALUE_LOGICAL_OPERATORS, Operator.OR).getValue();
} else {
// got true value devoid of multi-value logical search operators.
break;
}
}
/*
* Strip any ill-placed logical operator
*/
return new SingleValueExpressionBuilder(path, value).getExpression();
} else {
for (Object o : checkIfOriginalRequestValuesAvailable(path, values).stream()
.filter(Objects::nonNull)
.collect(Collectors.toList())) {
final String v = getStringValue(this.path, o);
OperatorAndValue ov = new OperatorAndValue(v, MULTI_VALUE_LOGICAL_OPERATORS,
default_operator != null ? default_operator
: Operator.OR);
if (default_operator == null && !Operator.NOT.equals(ov.getOperator()))
default_operator = ov.getOperator();
/*
* For NOT. delegate the comparison to
* SingleValueExpressionBuilder. Known issue with NOT
* operator with multiple leafs within MongoDB
* serializer, issue opened on its JIRA site by self.
*/
final SingleValueExpressionBuilder e = new SingleValueExpressionBuilder(path,
Operator.NOT.equals(
ov
.getOperator())
? v : ov.getValue());
BooleanExpression current = e.getExpression();
if (current == null) {
continue;
}
if (expression == null) {
expression = current;
} else {
// compose
switch (ov.getOperator()) {
case AND:
expression = and(expression, current);
break;
case OR:
case NOT: // actual NOT clause is handled by
// SingleValueExpressionBuilder, at this
// level we are treating it as like starting
// without OR, AND and thus defaulting
// to OR
expression = or(expression, current);
break;
default:
String msg = MessageFormat.format(
"Illegal operator: {0}, Search Parameter: " + "{1}, Value: {2}",
new Object[]{
ov.getOperator()
.toString(),
path.toString(),
v
});
throw new IllegalArgumentException(msg);
}
}
}
}
}
return expression;
}
}
/**
* Utility class for building stateful expression from provided Single value.
* This class expects the actual value w/ operators (So if a value has to be exchanged from
* {@link QuerydslHttpRequestContext} it must be done prior to invoking this class)
*/
private class SingleValueExpressionBuilder {
private P path;
private String value;
private Operator operator;
private SingleValueExpressionBuilder parent;
private SingleValueExpressionBuilder next;
private boolean ignoreCase = false;
public SingleValueExpressionBuilder(P path, String value) {
init(path, value);
}
private SingleValueExpressionBuilder(final P path, String value, final SingleValueExpressionBuilder parent) {
this.parent = parent;
init(path, value);
}
/**
* @return if case should be ignored.
*/
public boolean isIgnoreCase() {
return ignoreCase;
}
/**
* @param ignoreCase set <code>true</code> if case sensitivity should be ignored.
*/
public void setIgnoreCase(boolean ignoreCase) {
this.ignoreCase = ignoreCase;
}
/*
* Extract the operator (or operator chain) and form chained expression
* builder from this class.
*/
private void init(P path, String value) {
this.path = path;
OperatorAndValue ov = new OperatorAndValue(value, SUPPORTED_SINGLE_VALUED_COMPARISON_OPERATORS,
Operator.EQUAL);
this.operator = ov.getOperator();
this.value = ov.getValue();
Validate.notNull(this.operator, "Operator must not be null");
if (Operator.NOT.equals(this.operator)) {
Validate.isTrue(StringUtils.isNotBlank(this.value),
"Sub-operation must be available with NOT operator");
this.next = new SingleValueExpressionBuilder(path, ov.getValue(), this);
} else if (Operator.CASE_IGNORE.equals(this.operator)) {
Validate.isTrue(StringUtils.isNotBlank(this.value),
"Sub-operation must be available with CASE_IGNORE operator");
this.next = new SingleValueExpressionBuilder(path, ov.getValue(), this);
this.next.setIgnoreCase(true);
} else if (ExpressionProvider.isOperator(SUPPORTED_SINGLE_VALUED_COMPARISON_OPERATORS.toArray(
new Operator[SUPPORTED_SINGLE_VALUED_COMPARISON_OPERATORS.size()]), this.value)
.isPresent()) { // TODO: Perhaps check for an
// unsupported operator here and throw an error
this.next = new SingleValueExpressionBuilder(path, ov.getValue(), this);
}
// check for misplaced boolean operators, they should always be
// top/first operator but can't be composed within other top level
// operators.
if (this.parent != null) {
Validate.isTrue(
!(Operator.AND.equals(this.operator) || Operator.OR.equals(this.operator)
|| (Operator.NOT.equals(this.operator) && !Operator.NOT.equals(this.parent.operator))),
"Boolean operators cannot be composed within other operators"); // last
// expression
// allows
// for composition of NOT under NOT though it's useless but
// technically doesn't hurt.
}
}
public BooleanExpression getExpression() {
BooleanExpression result;
switch (this.operator) {
case CASE_IGNORE:
result = this.next.getExpression();
break;
case EQUAL:
result = eq(path, this.value, this.isIgnoreCase());
break;
case NOT_EQUAL:
result = ne(path, this.value, this.isIgnoreCase());
break;
case CONTAINS:
result = contains(path, this.value, this.isIgnoreCase());
break;
case STARTS_WITH:
case STARTSWITH:
result = startsWith(path, this.value, this.isIgnoreCase());
break;
case ENDS_WITH:
case ENDSWITH:
result = endsWith(path, this.value, this.isIgnoreCase());
break;
case MATCHES:
result = matches(path, this.value);
break;
case NOT:
result = this.next.getExpression();
if (result != null) {
result = result.not();
}
break;
case LESS_THAN:
result = lt(path, this.value);
break;
case LESS_THAN_OR_EQUAL:
result = lte(path, this.value);
break;
case GREATER_THAN:
result = gt(path, this.value);
break;
case GREATER_THAN_OR_EQUAL:
result = gte(path, this.value);
break;
default:
result = null;
}
return result;
}
}
/*
* Utility method that attempts to get original search input value for
* specific path from QuerydslHttpRequestContext if available for cases when
* experimental features using QuerydslHttpRequestContextAwareServletFilter
* is turned on.
*/
private String checkIfOriginalRequestValueAvailable(Path path, String defaultValue) {
QuerydslHttpRequestContext ctx = QuerydslHttpRequestContextHolder.getContext();
String result = null;
if (ctx != null) {
result = ctx.getSingleValue(path);
}
if (StringUtils.isBlank(result)) {
result = defaultValue;
}
return result;
}
/*
* Utility method that attempts to get original search input value for
* specific path from QuerydslHttpRequestContext if available for cases when
* experimental features using QuerydslHttpRequestContextAwareServletFilter
* is turned on.
*/
private Collection<Object> checkIfOriginalRequestValuesAvailable(Path path, Collection<Object> defaultValues) {
QuerydslHttpRequestContext ctx = QuerydslHttpRequestContextHolder.getContext();
Collection<Object> result = null;
if (ctx != null) {
result = Arrays.stream(Optional.ofNullable(ctx.getAllValues(path))
.orElseGet(() -> new String[]{}))
.map(val -> (Object) val)
.collect(Collectors.toCollection(LinkedList::new));
}
if (CollectionUtils.isEmpty(result)) {
result = defaultValues;
}
return result;
}
}