ExpressionProvider.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.Predicate;
import com.querydsl.core.types.dsl.BooleanExpression;
import org.apache.commons.lang3.StringUtils;
import java.util.Optional;
/**
* Interface establishes contract for providing {@link Predicate} or {@link BooleanExpression} to be used to pass to
* QueryDSL for querying underlying store.
*
* <p>
* This interface and entire component of QueryDSL extension is to build on top of Spring data QueryDSL extensions
* for QueryDSL by providing further low level of search operators within values which is a powerful extension by
* empowering client application to perform variety searches, an improvement over largely static capability out of
* the box to statically define different binding using QuerydslBinderCustomizer.
* </p>
*
*
* With this extension, client's can request in a variety of forms like below:
* <ul>
* <li>Search all resources which has either any email in 'company.com' domain or else have a
* specific email specified by second parameter
* /api/user/search?emails.value=endsWith(@company.com)&emails.value=johndoe@somemail.com</li>
* <li>Search for a user having <b>any <i>*admin*</i></b> role - /api/user/search?role=contains(admin)</li>
* <li>Search for any user having not having email in some specific domain - /api/user/search?emails
* .value=not(contains(@company.com))</li>
* <li>Search any user not having a specific attribute - job_level as "executive"
* /api/user/search?profile.job_level=ne(executive)</li>
* </ul>
*
*
*
* <p>
* Interface also defines supported operators though specific implementation may only support a subset of them so
* must be checked on implementation on supported Operators to avoid unpredictable errors in search logic/processing.
* Implementation must support all Logical Operators.
* </p>
* @param <P> type of {@link com.querydsl.core.types.Path}, example - {@link com.querydsl.core.types.dsl.StringPath}
* or {@link com.querydsl.core.types.dsl.EnumPath}.
* @param <V> type of value for path. Depending on type of path, this could be a collection of String or else
* Collection of String or String or Enum or Collection of Enum. This must not be wrapped in Optional.
* @author gt_tech
* @see Operator
* @see <a href="https://docs.spring.io/spring-data/mongodb/docs/2.0.6.RELEASE/reference/html/#core.extensions.querydsl">Spring data Querydsl Extensions</a>
* @see <a href="https://docs.spring.io/spring-data/commons/docs/2.0.6.RELEASE/api/org/springframework/data/querydsl/binding/QuerydslBinderCustomizer.html">QuerydslBinderCustomizer</a>
*/
public interface ExpressionProvider<P extends Path, V> {
/**
* Provides a operator value delimiter prefix for when explicit delimiter is provided.
* for e.g. operator(value)
*/
public static final String OPERATOR_VALUE_DELIMITER_PREFIX = "(";
/**
* Provides a operator value delimiter suffix for when explicit delimiter is provided.
* for e.g. operator(value)
*/
public static final String OPERATOR_VALUE_DELIMITER_SUFFIX = ")";
/**
* Method establishes contract to retrieve a predicate based on implementation specific logic's processing of
* supplied value(s).
* <p>
* Default implementation delegates to {@link #getExpression(Path, Object)}
* </p>
*
* @param path Q Path <code>Path</code> for which expression is to be formed using supplied <code>value</code>
* @param value Input value <code>value(s)</code> to be used to form expression, this can be a primitive or a collection of values but must not be wrapped in {@link Optional}
* @return {@link Optional} of {@link Predicate} based on provided value.
*/
default Optional<Predicate> getPredicate(P path, V value) {
return this.getExpression(path, value)
.map(Predicate.class::cast);
}
/**
* Method establishes the contract to retrieve a {@link BooleanExpression} based on implementation specific
* logic's processing of supplied value(s).
* <p>
* Default implementation returns an empty Optional.
* </p>
*
* @param path Q Path <code>Path</code> for which expression is to be formed using supplied <code>value</code>
* @param value Input value <code>value(s)</code> to be used to form expression, this can be a primitive or a collection of values but must not be wrapped in {@link Optional}
* @return {@link Optional} of {@link BooleanExpression} based on provided value.
* @throws UnsupportedOperationException if an operator is used in unsupported order or on unsupported digits
* (for example, startWith operator being used on String values)
*/
default Optional<BooleanExpression> getExpression(P path, V value) {
return Optional.empty();
}
/**
* Utility function to check if provided value starts with an Operator.
* Compares against all available Operators.
*
* @param value input string to check for Operator
* @return <code>Operator</code> if supplied String starts with an operator, <code>empty Optional</code> otherwise
* @throws UnsupportedOperationException if an operator is used in unsupported order or on unsupported digits
* (for example, startWith operator being used on String values)
*/
public static <S extends String> Optional<Operator> isOperator(final S value) {
return isOperator(Operator.values(), value);
}
/**
* Utility function to check if provided value starts with an Operator.
* Compares against provided operators.
*
* @param value input string to check for Operator
* @param operators List of operators to check against
* @return <code>Operator</code> if supplied String starts with an operator, <code>empty Optional</code> otherwise
* @throws UnsupportedOperationException if an operator is used in unsupported order or on unsupported digits
* (for example, startWith operator being used on String values)
*/
public static <S extends String> Optional<Operator> isOperator(Operator[] operators, final S value) {
if (operators != null && StringUtils.isNotBlank(value)) {
for (Operator operator : operators) {
if (isOperator(operator, value)) {
return Optional.of(operator);
}
}
}
return Optional.empty();
}
/**
* Returns <code>true</code> if provided value is wrapped in supplied <code>operator</code>
*
* @param operator <code>Operator</code> to check for on provided value
* @param value <code>value</code> to check against if it's wrapped in provided <code>operator</code>
* @return <code>true</code> if provided value is wrapped in supplied <code>operator</code>, <code>false</code> otherwise
*/
static boolean isOperator(Operator operator, final String value) {
return value.startsWith(new StringBuilder(operator.toString()).append(OPERATOR_VALUE_DELIMITER_PREFIX)
.toString()) && value.endsWith(
OPERATOR_VALUE_DELIMITER_SUFFIX);
}
/**
* Utility method which validates proper ordering and opening/closing delimiters of operators on supplied <code>value</code>
*
* @param value <code>value</code> to check for proper composition
* @throws IllegalArgumentException if an invalid composition is found in provided <code>value</code>
*/
static void validateComposition(final String value) {
if (StringUtils.isNotBlank(value)) {
if (isOperator(value).isPresent()) {
int count = 0;
int opening_bracket = OPERATOR_VALUE_DELIMITER_PREFIX.charAt(0);
int closing_bracket = OPERATOR_VALUE_DELIMITER_SUFFIX.charAt(0);
for (char c : value.toCharArray()) {
if (c == opening_bracket)
count++;
else if (c == closing_bracket) {
if (count <= 0) {
throw new IllegalArgumentException("Malformed (bad-ordering) value: " + value);
}
count--;
}
}
if (count != 0) {
throw new IllegalArgumentException("Malformed (Incompletely closed) value: " + value);
}
}
}
}
}