Improve building of SQL from an RSQL query (#1766)
* Improve building of SQL from an RSQL query * ignore case behavior could be disabled * like is used only when needed Signed-off-by: Marinov Avgustin <Avgustin.Marinov@bosch.com> * Inlining of some methods and unified IN build + fix case Signed-off-by: Marinov Avgustin <Avgustin.Marinov@bosch.com> * Implement more flexible ignore case configuration Signed-off-by: Marinov Avgustin <Avgustin.Marinov@bosch.com> --------- Signed-off-by: Marinov Avgustin <Avgustin.Marinov@bosch.com>
This commit is contained in:
@@ -0,0 +1,51 @@
|
||||
/**
|
||||
* Copyright (c) 2021 Bosch.IO GmbH and others
|
||||
*
|
||||
* This program and the accompanying materials are made
|
||||
* available under the terms of the Eclipse Public License 2.0
|
||||
* which is available at https://www.eclipse.org/legal/epl-2.0/
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.eclipse.hawkbit.repository.rsql;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
|
||||
/**
|
||||
* Helper class providing static access to the RSQL configuration as managed the ignoreCase and
|
||||
* the {@link RsqlVisitorFactory} bean.
|
||||
*/
|
||||
@NoArgsConstructor(access = lombok.AccessLevel.PRIVATE)
|
||||
@Getter
|
||||
public final class RsqlConfigHolder {
|
||||
|
||||
private static final RsqlConfigHolder SINGLETON = new RsqlConfigHolder();
|
||||
|
||||
/**
|
||||
* If RSQL comparison operators shall ignore the case. If ignore case is <code>true</code>
|
||||
* "x == ax" will match "x == aX"
|
||||
*/
|
||||
@Value("${hawkbit.rsql.ignoreCase:true}")
|
||||
private boolean ignoreCase;
|
||||
/**
|
||||
* Declares if the database is case-insensitive, by default assumes <code>false</code>. In case it is case-sensitive and,
|
||||
* {@link #ignoreCase} is set to <code>true</code> the SQL queries use upper case comparisons to ignore case.
|
||||
*
|
||||
* If the database is declared as case-sensitive and ignoreCase is set to <code>false</code> the RSQL queries shall use strict
|
||||
* syntax - i.e. 'and' instead of 'AND' / 'aND'. Otherwise, the queries would be case-insensitive regarding operators.
|
||||
*/
|
||||
@Value("${hawkbit.rsql.caseInsensitiveDB:false}")
|
||||
private boolean caseInsensitiveDB;
|
||||
@Autowired
|
||||
private RsqlVisitorFactory rsqlVisitorFactory;
|
||||
|
||||
/**
|
||||
* @return The holder singleton instance.
|
||||
*/
|
||||
public static RsqlConfigHolder getInstance() {
|
||||
return SINGLETON;
|
||||
}
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2021 Bosch.IO GmbH and others
|
||||
*
|
||||
* This program and the accompanying materials are made
|
||||
* available under the terms of the Eclipse Public License 2.0
|
||||
* which is available at https://www.eclipse.org/legal/epl-2.0/
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.eclipse.hawkbit.repository.rsql;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
|
||||
/**
|
||||
* Helper class providing static access to the managed
|
||||
* {@link RsqlVisitorFactory} bean.
|
||||
*/
|
||||
public final class RsqlVisitorFactoryHolder {
|
||||
|
||||
private static final RsqlVisitorFactoryHolder SINGLETON = new RsqlVisitorFactoryHolder();
|
||||
|
||||
@Autowired
|
||||
private RsqlVisitorFactory rsqlVisitorFactory;
|
||||
|
||||
private RsqlVisitorFactoryHolder() {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The holder singleton instance.
|
||||
*/
|
||||
public static RsqlVisitorFactoryHolder getInstance() {
|
||||
return SINGLETON;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The managed RsqlVisitorFactory bean
|
||||
*/
|
||||
public RsqlVisitorFactory getRsqlVisitorFactory() {
|
||||
return rsqlVisitorFactory;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -157,7 +157,7 @@ import org.eclipse.hawkbit.repository.model.helper.SystemSecurityContextHolder;
|
||||
import org.eclipse.hawkbit.repository.model.helper.TenantConfigurationManagementHolder;
|
||||
import org.eclipse.hawkbit.repository.rsql.RsqlValidationOracle;
|
||||
import org.eclipse.hawkbit.repository.rsql.RsqlVisitorFactory;
|
||||
import org.eclipse.hawkbit.repository.rsql.RsqlVisitorFactoryHolder;
|
||||
import org.eclipse.hawkbit.repository.rsql.RsqlConfigHolder;
|
||||
import org.eclipse.hawkbit.repository.rsql.VirtualPropertyReplacer;
|
||||
import org.eclipse.hawkbit.security.HawkbitSecurityProperties;
|
||||
import org.eclipse.hawkbit.security.SecurityTokenGenerator;
|
||||
@@ -1016,13 +1016,13 @@ public class RepositoryApplicationConfiguration extends JpaBaseConfiguration {
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtains the {@link RsqlVisitorFactoryHolder} bean.
|
||||
* Obtains the {@link RsqlConfigHolder} bean.
|
||||
*
|
||||
* @return The {@link RsqlVisitorFactoryHolder} singleton.
|
||||
* @return The {@link RsqlConfigHolder} singleton.
|
||||
*/
|
||||
@Bean
|
||||
RsqlVisitorFactoryHolder rsqlVisitorFactoryHolder() {
|
||||
return RsqlVisitorFactoryHolder.getInstance();
|
||||
RsqlConfigHolder rsqlVisitorFactoryHolder() {
|
||||
return RsqlConfigHolder.getInstance();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -52,7 +52,7 @@ import org.springframework.beans.SimpleTypeConverter;
|
||||
import org.springframework.beans.TypeMismatchException;
|
||||
import org.springframework.orm.jpa.vendor.Database;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.util.ObjectUtils;
|
||||
|
||||
/**
|
||||
* An implementation of the {@link RSQLVisitor} to visit the parsed tokens and
|
||||
@@ -75,6 +75,7 @@ public class JpaQueryRsqlVisitor<A extends Enum<A> & FieldNameProvider, T> exten
|
||||
private final CriteriaBuilder cb;
|
||||
private final CriteriaQuery<?> query;
|
||||
private final Database database;
|
||||
private final boolean ensureIgnoreCase;
|
||||
private final Root<T> root;
|
||||
private final SimpleTypeConverter simpleTypeConverter;
|
||||
private final VirtualPropertyReplacer virtualPropertyReplacer;
|
||||
@@ -85,7 +86,7 @@ public class JpaQueryRsqlVisitor<A extends Enum<A> & FieldNameProvider, T> exten
|
||||
|
||||
public JpaQueryRsqlVisitor(final Root<T> root, final CriteriaBuilder cb, final Class<A> enumType,
|
||||
final VirtualPropertyReplacer virtualPropertyReplacer, final Database database,
|
||||
final CriteriaQuery<?> query) {
|
||||
final CriteriaQuery<?> query, final boolean ensureIgnoreCase) {
|
||||
super(enumType);
|
||||
this.root = root;
|
||||
this.cb = cb;
|
||||
@@ -93,7 +94,7 @@ public class JpaQueryRsqlVisitor<A extends Enum<A> & FieldNameProvider, T> exten
|
||||
this.virtualPropertyReplacer = virtualPropertyReplacer;
|
||||
this.simpleTypeConverter = new SimpleTypeConverter();
|
||||
this.database = database;
|
||||
this.joinsNeeded = false;
|
||||
this.ensureIgnoreCase = ensureIgnoreCase;
|
||||
}
|
||||
|
||||
private void beginLevel(final boolean isOr) {
|
||||
@@ -128,10 +129,10 @@ public class JpaQueryRsqlVisitor<A extends Enum<A> & FieldNameProvider, T> exten
|
||||
@Override
|
||||
public List<Predicate> visit(final AndNode node, final String param) {
|
||||
beginLevel(false);
|
||||
final List<Predicate> childs = acceptChilds(node);
|
||||
final List<Predicate> childs = acceptChildren(node);
|
||||
endLevel();
|
||||
if (!childs.isEmpty()) {
|
||||
return toSingleList(cb.and(childs.toArray(new Predicate[childs.size()])));
|
||||
return toSingleList(cb.and(childs.toArray(new Predicate[0])));
|
||||
}
|
||||
return toSingleList(cb.conjunction());
|
||||
}
|
||||
@@ -139,10 +140,10 @@ public class JpaQueryRsqlVisitor<A extends Enum<A> & FieldNameProvider, T> exten
|
||||
@Override
|
||||
public List<Predicate> visit(final OrNode node, final String param) {
|
||||
beginLevel(true);
|
||||
final List<Predicate> childs = acceptChilds(node);
|
||||
final List<Predicate> childs = acceptChildren(node);
|
||||
endLevel();
|
||||
if (!childs.isEmpty()) {
|
||||
return toSingleList(cb.or(childs.toArray(new Predicate[childs.size()])));
|
||||
return toSingleList(cb.or(childs.toArray(new Predicate[0])));
|
||||
}
|
||||
return toSingleList(cb.conjunction());
|
||||
}
|
||||
@@ -177,18 +178,17 @@ public class JpaQueryRsqlVisitor<A extends Enum<A> & FieldNameProvider, T> exten
|
||||
() -> new RSQLParameterUnsupportedFieldException("RSQL field path cannot be empty", null));
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@SuppressWarnings({"rawtypes", "unchecked"})
|
||||
private Path<?> getJoinFieldPath(final Path<?> fieldPath, final String fieldNameSplit) {
|
||||
if (fieldPath instanceof PluralJoin) {
|
||||
final Join<Object, ?> join = (Join<Object, ?>) fieldPath;
|
||||
final From<?, Object> joinParent = join.getParent();
|
||||
if (fieldPath instanceof PluralJoin join) {
|
||||
final From joinParent = join.getParent();
|
||||
final Optional<Join<Object, Object>> currentJoinOfType = findCurrentJoinOfType(join.getJavaType());
|
||||
if (currentJoinOfType.isPresent() && isOrLevel) {
|
||||
// remove the additional join and use the existing one
|
||||
joinParent.getJoins().remove(join);
|
||||
return currentJoinOfType.get();
|
||||
} else {
|
||||
final Join<Object, Object> newJoin = joinParent.join(fieldNameSplit, JoinType.LEFT);
|
||||
final Join newJoin = joinParent.join(fieldNameSplit, JoinType.LEFT);
|
||||
addCurrentJoin(newJoin);
|
||||
return newJoin;
|
||||
}
|
||||
@@ -236,7 +236,7 @@ public class JpaQueryRsqlVisitor<A extends Enum<A> & FieldNameProvider, T> exten
|
||||
|
||||
private Object convertValueIfNecessary(final ComparisonNode node, final A fieldName, final String value,
|
||||
final Path<Object> fieldPath) {
|
||||
// in case the value of an rsql query e.g. type==application is an
|
||||
// in case the value of an RSQL query e.g. type==application is an
|
||||
// enum we need to handle it separately because JPA needs the
|
||||
// correct java-type to build an expression. So String and numeric
|
||||
// values JPA can do it by it's own but not for classes like enums.
|
||||
@@ -297,7 +297,7 @@ public class JpaQueryRsqlVisitor<A extends Enum<A> & FieldNameProvider, T> exten
|
||||
|
||||
throw new RSQLParameterUnsupportedFieldException("field {" + node.getSelector()
|
||||
+ "} must be one of the following values {" + Arrays.stream(tmpEnumType.getEnumConstants())
|
||||
.map(v -> v.name().toLowerCase()).collect(Collectors.toList())
|
||||
.map(v -> v.name().toLowerCase()).toList()
|
||||
+ "}", e);
|
||||
}
|
||||
}
|
||||
@@ -343,7 +343,7 @@ public class JpaQueryRsqlVisitor<A extends Enum<A> & FieldNameProvider, T> exten
|
||||
case "=le=":
|
||||
return cb.lessThanOrEqualTo(pathOfString(fieldPath), value);
|
||||
case "=in=":
|
||||
return getInPredicate(transformedValues, fieldPath);
|
||||
return in(pathOfString(fieldPath), transformedValues);
|
||||
case "=out=":
|
||||
return getOutPredicate(transformedValues, finalProperty, enumField, fieldPath);
|
||||
default:
|
||||
@@ -352,53 +352,19 @@ public class JpaQueryRsqlVisitor<A extends Enum<A> & FieldNameProvider, T> exten
|
||||
}
|
||||
}
|
||||
|
||||
private Predicate getInPredicate(final List<Object> transformedValues, final Path<Object> fieldPath) {
|
||||
final List<String> inParams = new ArrayList<>();
|
||||
for (final Object param : transformedValues) {
|
||||
if (param instanceof String) {
|
||||
inParams.add(((String) param).toUpperCase());
|
||||
}
|
||||
}
|
||||
if (!inParams.isEmpty()) {
|
||||
return cb.upper(pathOfString(fieldPath)).in(inParams);
|
||||
} else {
|
||||
return fieldPath.in(transformedValues);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
private Predicate getOutPredicate(final List<Object> transformedValues, final String finalProperty,
|
||||
private Predicate getOutPredicate(
|
||||
final List<Object> transformedValues, final String finalProperty,
|
||||
final A enumField, final Path<Object> fieldPath) {
|
||||
|
||||
final String[] fieldNames = enumField.getSubAttributes(finalProperty);
|
||||
final List<String> outParams = transformedValues.stream().filter(String.class::isInstance)
|
||||
.map(String.class::cast).map(String::toUpperCase).collect(Collectors.toList());
|
||||
|
||||
if (isSimpleField(fieldNames, enumField.isMap())) {
|
||||
return toNullOrNotInPredicate(fieldPath, transformedValues, outParams);
|
||||
final Path<String> pathOfString = pathOfString(fieldPath);
|
||||
return cb.or(cb.isNull(pathOfString), cb.not(in(pathOfString, transformedValues)));
|
||||
}
|
||||
|
||||
clearOuterJoinsIfNotNeeded();
|
||||
|
||||
return toOutWithSubQueryPredicate(fieldNames, transformedValues, enumField, outParams);
|
||||
}
|
||||
|
||||
private Predicate toNullOrNotInPredicate(final Path<Object> fieldPath, final List<Object> transformedValues,
|
||||
final List<String> outParams) {
|
||||
|
||||
final Path<String> pathOfString = pathOfString(fieldPath);
|
||||
final Predicate inPredicate = outParams.isEmpty() ? fieldPath.in(transformedValues)
|
||||
: cb.upper(pathOfString).in(outParams);
|
||||
|
||||
return cb.or(cb.isNull(pathOfString), cb.not(inPredicate));
|
||||
}
|
||||
|
||||
private Predicate toOutWithSubQueryPredicate(final String[] fieldNames, final List<Object> transformedValues,
|
||||
final A enumField, final List<String> outParams) {
|
||||
final Function<Expression<String>, Predicate> inPredicateProvider = expressionToCompare -> outParams.isEmpty()
|
||||
? cb.upper(expressionToCompare).in(transformedValues)
|
||||
: cb.upper(expressionToCompare).in(outParams);
|
||||
return toNotExistsSubQueryPredicate(fieldNames, enumField, inPredicateProvider);
|
||||
return toNotExistsSubQueryPredicate(fieldNames, enumField, expressionToCompare -> in(expressionToCompare, transformedValues));
|
||||
}
|
||||
|
||||
private Path<Object> getMapValueFieldPath(final A enumField, final Path<Object> fieldPath) {
|
||||
@@ -420,16 +386,15 @@ public class JpaQueryRsqlVisitor<A extends Enum<A> & FieldNameProvider, T> exten
|
||||
|
||||
final String keyValue = graph[graph.length - 1];
|
||||
if (fieldPath instanceof MapJoin) {
|
||||
// Currently we support only string key .So below cast is safe.
|
||||
return cb.equal(cb.upper((Expression<String>) (((MapJoin<?, ?, ?>) fieldPath).key())),
|
||||
keyValue.toUpperCase());
|
||||
// Currently we support only string key. So below cast is safe.
|
||||
return equal((Expression<String>) (((MapJoin<?, ?, ?>) fieldPath).key()), keyValue);
|
||||
}
|
||||
|
||||
final String keyFieldName = enumField.getSubEntityMapTuple().map(Entry::getKey)
|
||||
.orElseThrow(() -> new UnsupportedOperationException(
|
||||
"For the fields, defined as Map, only Map java type or tuple in the form of SimpleImmutableEntry are allowed. Neither of those could be found!"));
|
||||
|
||||
return cb.equal(cb.upper(fieldPath.get(keyFieldName)), keyValue.toUpperCase());
|
||||
"For the fields, defined as Map, only Map java type or tuple in the form of " +
|
||||
"SimpleImmutableEntry are allowed. Neither of those could be found!"));
|
||||
return equal(fieldPath.get(keyFieldName), keyValue);
|
||||
}
|
||||
|
||||
private Predicate getEqualToPredicate(final Object transformedValue, final Path<Object> fieldPath) {
|
||||
@@ -437,13 +402,16 @@ public class JpaQueryRsqlVisitor<A extends Enum<A> & FieldNameProvider, T> exten
|
||||
return cb.isNull(pathOfString(fieldPath));
|
||||
}
|
||||
|
||||
if ((transformedValue instanceof String) && !NumberUtils.isCreatable((String) transformedValue)) {
|
||||
if (StringUtils.isEmpty(transformedValue)) {
|
||||
if ((transformedValue instanceof String transformedValueStr) && !NumberUtils.isCreatable(transformedValueStr)) {
|
||||
if (ObjectUtils.isEmpty(transformedValue)) {
|
||||
return cb.or(cb.isNull(pathOfString(fieldPath)), cb.equal(pathOfString(fieldPath), ""));
|
||||
}
|
||||
|
||||
final String sqlValue = toSQL((String) transformedValue);
|
||||
return cb.like(cb.upper(pathOfString(fieldPath)), sqlValue, ESCAPE_CHAR);
|
||||
if (isPattern(transformedValueStr)) { // a pattern, use like
|
||||
return like(pathOfString(fieldPath), toSQL(transformedValueStr));
|
||||
} else {
|
||||
return equal(pathOfString(fieldPath), transformedValueStr);
|
||||
}
|
||||
}
|
||||
|
||||
return cb.equal(fieldPath, transformedValue);
|
||||
@@ -453,24 +421,31 @@ public class JpaQueryRsqlVisitor<A extends Enum<A> & FieldNameProvider, T> exten
|
||||
final String finalProperty, final A enumField) {
|
||||
|
||||
if (transformedValue == null) {
|
||||
return toNotNullPredicate(fieldPath);
|
||||
return cb.isNotNull(pathOfString(fieldPath));
|
||||
}
|
||||
|
||||
if ((transformedValue instanceof String) && !NumberUtils.isCreatable((String) transformedValue)) {
|
||||
if (StringUtils.isEmpty(transformedValue)) {
|
||||
return toNotNullAndNotEmptyPredicate(fieldPath);
|
||||
if ((transformedValue instanceof String transformedValueStr) && !NumberUtils.isCreatable(transformedValueStr)) {
|
||||
if (ObjectUtils.isEmpty(transformedValue)) {
|
||||
return cb.and(cb.isNotNull(pathOfString(fieldPath)), cb.notEqual(pathOfString(fieldPath), ""));
|
||||
}
|
||||
|
||||
final String sqlValue = toSQL((String) transformedValue);
|
||||
final String[] fieldNames = enumField.getSubAttributes(finalProperty);
|
||||
|
||||
if (isSimpleField(fieldNames, enumField.isMap())) {
|
||||
return toNullOrNotLikePredicate(fieldPath, sqlValue);
|
||||
if (isPattern(transformedValueStr)) { // a pattern, use like
|
||||
return cb.or(cb.isNull(pathOfString(fieldPath)), notLike(pathOfString(fieldPath), toSQL(transformedValueStr)));
|
||||
} else {
|
||||
return toNullOrNotEqualPredicate(fieldPath, transformedValueStr);
|
||||
}
|
||||
}
|
||||
|
||||
clearOuterJoinsIfNotNeeded();
|
||||
|
||||
return toNotEqualWithSubQueryPredicate(enumField, sqlValue, fieldNames);
|
||||
if (isPattern(transformedValueStr)) { // a pattern, use like
|
||||
return toNotExistsSubQueryPredicate(fieldNames, enumField, expressionToCompare -> like(expressionToCompare, toSQL(transformedValueStr)));
|
||||
} else {
|
||||
return toNotExistsSubQueryPredicate(fieldNames, enumField, expressionToCompare -> equal(expressionToCompare, transformedValueStr));
|
||||
}
|
||||
}
|
||||
|
||||
return toNullOrNotEqualPredicate(fieldPath, transformedValue);
|
||||
@@ -482,28 +457,12 @@ public class JpaQueryRsqlVisitor<A extends Enum<A> & FieldNameProvider, T> exten
|
||||
}
|
||||
}
|
||||
|
||||
private Predicate toNotNullPredicate(final Path<Object> fieldPath) {
|
||||
return cb.isNotNull(pathOfString(fieldPath));
|
||||
}
|
||||
|
||||
private Predicate toNullOrNotLikePredicate(final Path<Object> fieldPath, final String sqlValue) {
|
||||
return cb.or(cb.isNull(pathOfString(fieldPath)),
|
||||
cb.notLike(cb.upper(pathOfString(fieldPath)), sqlValue, ESCAPE_CHAR));
|
||||
}
|
||||
|
||||
private Predicate toNullOrNotEqualPredicate(final Path<Object> fieldPath, final Object transformedValue) {
|
||||
return cb.or(cb.isNull(pathOfString(fieldPath)), cb.notEqual(fieldPath, transformedValue));
|
||||
}
|
||||
|
||||
private Predicate toNotNullAndNotEmptyPredicate(final Path<Object> fieldPath) {
|
||||
return cb.and(cb.isNotNull(pathOfString(fieldPath)), cb.notEqual(pathOfString(fieldPath), ""));
|
||||
}
|
||||
|
||||
private Predicate toNotEqualWithSubQueryPredicate(final A enumField, final String sqlValue,
|
||||
final String[] fieldNames) {
|
||||
final Function<Expression<String>, Predicate> likePredicateProvider = expressionToCompare -> cb
|
||||
.like(cb.upper(expressionToCompare), sqlValue);
|
||||
return toNotExistsSubQueryPredicate(fieldNames, enumField, likePredicateProvider);
|
||||
return cb.or(
|
||||
cb.isNull(pathOfString(fieldPath)),
|
||||
transformedValue instanceof String transformedValueStr
|
||||
? notEqual(pathOfString(fieldPath), transformedValueStr)
|
||||
: cb.notEqual(fieldPath, transformedValue));
|
||||
}
|
||||
|
||||
@SuppressWarnings({ "unchecked", "rawtypes" })
|
||||
@@ -556,6 +515,14 @@ public class JpaQueryRsqlVisitor<A extends Enum<A> & FieldNameProvider, T> exten
|
||||
return fieldPath;
|
||||
}
|
||||
|
||||
private static boolean isPattern(final String transformedValue) {
|
||||
if (transformedValue.contains(ESCAPE_CHAR_WITH_ASTERISK)) {
|
||||
return transformedValue.replace(ESCAPE_CHAR_WITH_ASTERISK, "$").indexOf(LIKE_WILDCARD) != -1;
|
||||
} else {
|
||||
return transformedValue.indexOf(LIKE_WILDCARD) != -1;
|
||||
}
|
||||
}
|
||||
|
||||
private String toSQL(final String transformedValue) {
|
||||
final String escaped;
|
||||
|
||||
@@ -575,7 +542,7 @@ public class JpaQueryRsqlVisitor<A extends Enum<A> & FieldNameProvider, T> exten
|
||||
} else {
|
||||
finalizedValue = escapedValue.replace(LIKE_WILDCARD, '%');
|
||||
}
|
||||
return finalizedValue.toUpperCase();
|
||||
return finalizedValue;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@@ -583,7 +550,7 @@ public class JpaQueryRsqlVisitor<A extends Enum<A> & FieldNameProvider, T> exten
|
||||
return (Path<Y>) path;
|
||||
}
|
||||
|
||||
private List<Predicate> acceptChilds(final LogicalNode node) {
|
||||
private List<Predicate> acceptChildren(final LogicalNode node) {
|
||||
final List<Node> children = node.getChildren();
|
||||
final List<Predicate> childs = new ArrayList<>();
|
||||
for (final Node node2 : children) {
|
||||
@@ -597,4 +564,28 @@ public class JpaQueryRsqlVisitor<A extends Enum<A> & FieldNameProvider, T> exten
|
||||
return childs;
|
||||
}
|
||||
|
||||
}
|
||||
private Predicate equal(final Expression<String> expressionToCompare, final String sqlValue) {
|
||||
return cb.equal(caseWise(cb, expressionToCompare), caseWise(sqlValue));
|
||||
}
|
||||
private Predicate notEqual(final Expression<String> expressionToCompare, String transformedValueStr) {
|
||||
return cb.notEqual(caseWise(cb, expressionToCompare), caseWise(transformedValueStr));
|
||||
}
|
||||
private Predicate like(final Expression<String> expressionToCompare, final String sqlValue) {
|
||||
return cb.like(caseWise(cb, expressionToCompare), caseWise(sqlValue), ESCAPE_CHAR);
|
||||
}
|
||||
private Predicate notLike(final Expression<String> expressionToCompare, final String sqlValue) {
|
||||
return cb.notLike(caseWise(cb, expressionToCompare), caseWise(sqlValue), ESCAPE_CHAR);
|
||||
}
|
||||
private Predicate in(final Expression<String> expressionToCompare, final List<Object> transformedValues) {
|
||||
final List<String> inParams = transformedValues.stream().filter(String.class::isInstance)
|
||||
.map(String.class::cast).map(this::caseWise).collect(Collectors.toList());
|
||||
return inParams.isEmpty() ? expressionToCompare.in(transformedValues) : caseWise(cb, expressionToCompare).in(inParams);
|
||||
}
|
||||
|
||||
private Expression<String> caseWise(final CriteriaBuilder cb, final Expression<String> expression) {
|
||||
return ensureIgnoreCase ? cb.upper(expression) : expression;
|
||||
}
|
||||
private String caseWise(final String str) {
|
||||
return ensureIgnoreCase ? str.toUpperCase() : str;
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@
|
||||
*/
|
||||
package org.eclipse.hawkbit.repository.jpa.rsql;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
@@ -24,7 +25,7 @@ import org.apache.commons.lang3.text.StrLookup;
|
||||
import org.eclipse.hawkbit.repository.FieldNameProvider;
|
||||
import org.eclipse.hawkbit.repository.exception.RSQLParameterSyntaxException;
|
||||
import org.eclipse.hawkbit.repository.exception.RSQLParameterUnsupportedFieldException;
|
||||
import org.eclipse.hawkbit.repository.rsql.RsqlVisitorFactoryHolder;
|
||||
import org.eclipse.hawkbit.repository.rsql.RsqlConfigHolder;
|
||||
import org.eclipse.hawkbit.repository.rsql.VirtualPropertyReplacer;
|
||||
import org.eclipse.hawkbit.repository.rsql.VirtualPropertyResolver;
|
||||
import org.springframework.data.jpa.domain.Specification;
|
||||
@@ -80,62 +81,47 @@ public final class RSQLUtility {
|
||||
* query. The specification can be used to filter for JPA entities with the
|
||||
* given RSQL query.
|
||||
*
|
||||
* @param rsql
|
||||
* the rsql query to be parsed
|
||||
* @param fieldNameProvider
|
||||
* the enum class type which implements the
|
||||
* {@link FieldNameProvider}
|
||||
* @param virtualPropertyReplacer
|
||||
* holds the logic how the known macros have to be resolved; may
|
||||
* be <code>null</code>
|
||||
* @param database
|
||||
* in use
|
||||
*
|
||||
* @return an specification which can be used with JPA
|
||||
* @throws RSQLParameterUnsupportedFieldException
|
||||
* if a field in the RSQL string is used but not provided by the
|
||||
* given {@code fieldNameProvider}
|
||||
* @throws RSQLParameterSyntaxException
|
||||
* if the RSQL syntax is wrong
|
||||
* @param rsql the rsql query to be parsed
|
||||
* @param fieldNameProvider the enum class type which implements the {@link FieldNameProvider}
|
||||
* @param virtualPropertyReplacer holds the logic how the known macros have to be resolved; may be <code>null</code>
|
||||
* @param database database in use
|
||||
*
|
||||
* @return a specification which can be used with JPA
|
||||
* @throws RSQLParameterUnsupportedFieldException if a field in the RSQL string is used but not provided by the
|
||||
* given {@code fieldNameProvider}
|
||||
* @throws RSQLParameterSyntaxException if the RSQL syntax is wrong
|
||||
*/
|
||||
public static <A extends Enum<A> & FieldNameProvider, T> Specification<T> buildRsqlSpecification(final String rsql,
|
||||
final Class<A> fieldNameProvider, final VirtualPropertyReplacer virtualPropertyReplacer,
|
||||
final Database database) {
|
||||
public static <A extends Enum<A> & FieldNameProvider, T> Specification<T> buildRsqlSpecification(
|
||||
final String rsql, final Class<A> fieldNameProvider,
|
||||
final VirtualPropertyReplacer virtualPropertyReplacer, final Database database) {
|
||||
return new RSQLSpecification<>(rsql, fieldNameProvider, virtualPropertyReplacer, database);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the RSQL string
|
||||
*
|
||||
* @param rsql
|
||||
* RSQL string to validate
|
||||
* @param rsql RSQL string to validate
|
||||
* @param fieldNameProvider
|
||||
*
|
||||
* @throws RSQLParserException
|
||||
* if RSQL syntax is invalid
|
||||
* @throws RSQLParameterUnsupportedFieldException
|
||||
* if RSQL key is not allowed
|
||||
* @throws RSQLParserException if RSQL syntax is invalid
|
||||
* @throws RSQLParameterUnsupportedFieldException if RSQL key is not allowed
|
||||
*/
|
||||
public static <A extends Enum<A> & FieldNameProvider> void validateRsqlFor(final String rsql,
|
||||
final Class<A> fieldNameProvider) {
|
||||
final RSQLVisitor<Void, String> visitor = getValidationRsqlVisitor(fieldNameProvider);
|
||||
public static <A extends Enum<A> & FieldNameProvider> void validateRsqlFor(
|
||||
final String rsql, final Class<A> fieldNameProvider) {
|
||||
final RSQLVisitor<Void, String> visitor =
|
||||
RsqlConfigHolder.getInstance().getRsqlVisitorFactory().validationRsqlVisitor(fieldNameProvider);
|
||||
final Node rootNode = parseRsql(rsql);
|
||||
rootNode.accept(visitor);
|
||||
}
|
||||
|
||||
private static <A extends Enum<A> & FieldNameProvider> RSQLVisitor<Void, String> getValidationRsqlVisitor(
|
||||
final Class<A> fieldNameProvider) {
|
||||
return RsqlVisitorFactoryHolder.getInstance().getRsqlVisitorFactory().validationRsqlVisitor(fieldNameProvider);
|
||||
}
|
||||
|
||||
private static Node parseRsql(final String rsql) {
|
||||
log.debug("Parsing rsql string {}", rsql);
|
||||
try {
|
||||
log.debug("Parsing rsql string {}", rsql);
|
||||
final Set<ComparisonOperator> operators = RSQLOperators.defaultOperators();
|
||||
return new RSQLParser(operators).parse(rsql.toLowerCase());
|
||||
return new RSQLParser(operators).parse(
|
||||
RsqlConfigHolder.getInstance().isCaseInsensitiveDB() || RsqlConfigHolder.getInstance().isIgnoreCase() ? rsql.toLowerCase() : rsql);
|
||||
} catch (final IllegalArgumentException e) {
|
||||
throw new RSQLParameterSyntaxException("rsql filter must not be null", e);
|
||||
throw new RSQLParameterSyntaxException("RSQL filter must not be null", e);
|
||||
} catch (final RSQLParserException e) {
|
||||
throw new RSQLParameterSyntaxException(e);
|
||||
}
|
||||
@@ -143,6 +129,7 @@ public final class RSQLUtility {
|
||||
|
||||
private static final class RSQLSpecification<A extends Enum<A> & FieldNameProvider, T> implements Specification<T> {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private final String rsql;
|
||||
@@ -164,15 +151,14 @@ public final class RSQLUtility {
|
||||
query.distinct(true);
|
||||
|
||||
final JpaQueryRsqlVisitor<A, T> jpqQueryRSQLVisitor = new JpaQueryRsqlVisitor<>(root, cb, enumType,
|
||||
virtualPropertyReplacer, database, query);
|
||||
final List<Predicate> accept = rootNode.<List<Predicate>, String> accept(jpqQueryRSQLVisitor);
|
||||
virtualPropertyReplacer, database, query,
|
||||
!RsqlConfigHolder.getInstance().isCaseInsensitiveDB() && RsqlConfigHolder.getInstance().isIgnoreCase());
|
||||
final List<Predicate> accept = rootNode.accept(jpqQueryRSQLVisitor);
|
||||
|
||||
if (!CollectionUtils.isEmpty(accept)) {
|
||||
return cb.and(accept.toArray(new Predicate[accept.size()]));
|
||||
return cb.and(accept.toArray(new Predicate[0]));
|
||||
}
|
||||
return cb.conjunction();
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -21,6 +21,7 @@ import org.eclipse.hawkbit.repository.model.Action;
|
||||
import org.eclipse.hawkbit.repository.model.Action.ActionType;
|
||||
import org.eclipse.hawkbit.repository.model.Action.Status;
|
||||
import org.eclipse.hawkbit.repository.model.DistributionSet;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.data.domain.PageRequest;
|
||||
@@ -43,34 +44,30 @@ public class RSQLActionFieldsTest extends AbstractJpaIntegrationTest {
|
||||
final DistributionSet dsA = testdataFactory.createDistributionSet("daA");
|
||||
target = (JpaTarget) targetManagement
|
||||
.create(entityFactory.target().create().controllerId("targetId123").description("targetId123"));
|
||||
action = new JpaAction();
|
||||
action.setActionType(ActionType.SOFT);
|
||||
action.setDistributionSet(dsA);
|
||||
action.setTarget(target);
|
||||
action.setStatus(Status.RUNNING);
|
||||
action.setWeight(45);
|
||||
action.setInitiatedBy(tenantAware.getCurrentUsername());
|
||||
|
||||
action = newJpaAction(dsA, false, null);
|
||||
for (int i = 0; i < 10; i++) {
|
||||
newJpaAction(dsA, i % 2 == 0, i % 2 == 0 ? "extRef" : "extRef2");
|
||||
}
|
||||
}
|
||||
|
||||
private @NotNull JpaAction newJpaAction(final DistributionSet dsA, final boolean active, final String extRef) {
|
||||
final JpaAction newAction = new JpaAction();
|
||||
newAction.setActionType(ActionType.SOFT);
|
||||
newAction.setDistributionSet(dsA);
|
||||
newAction.setActive(active);
|
||||
newAction.setStatus(Status.RUNNING);
|
||||
newAction.setTarget(target);
|
||||
newAction.setWeight(45);
|
||||
newAction.setInitiatedBy(tenantAware.getCurrentUsername());
|
||||
if (extRef != null) {
|
||||
newAction.setExternalRef(extRef);
|
||||
}
|
||||
actionRepository.save(newAction);
|
||||
|
||||
target.addAction(action);
|
||||
|
||||
actionRepository.save(action);
|
||||
for (int i = 0; i < 10; i++) {
|
||||
final JpaAction newAction = new JpaAction();
|
||||
newAction.setActionType(ActionType.SOFT);
|
||||
newAction.setDistributionSet(dsA);
|
||||
newAction.setActive((i % 2) == 0);
|
||||
newAction.setStatus(Status.RUNNING);
|
||||
newAction.setTarget(target);
|
||||
newAction.setWeight(45);
|
||||
newAction.setInitiatedBy(tenantAware.getCurrentUsername());
|
||||
if ((i % 2) == 0) {
|
||||
newAction.setExternalRef("extRef");
|
||||
} else {
|
||||
newAction.setExternalRef("extRef2");
|
||||
}
|
||||
actionRepository.save(newAction);
|
||||
target.addAction(newAction);
|
||||
}
|
||||
|
||||
return newAction;
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -116,11 +113,11 @@ public class RSQLActionFieldsTest extends AbstractJpaIntegrationTest {
|
||||
}
|
||||
|
||||
private void assertRSQLQuery(final String rsqlParam, final long expectedEntities) {
|
||||
|
||||
final Slice<Action> findEnitity = deploymentManagement.findActionsByTarget(rsqlParam, target.getControllerId(),
|
||||
final Slice<Action> findEntity = deploymentManagement.findActionsByTarget(rsqlParam, target.getControllerId(),
|
||||
PageRequest.of(0, 100));
|
||||
final long countAllEntities = deploymentManagement.countActionsByTarget(rsqlParam, target.getControllerId());
|
||||
assertThat(findEnitity).isNotNull();
|
||||
assertThat(findEntity).isNotNull();
|
||||
assertThat(findEntity.getContent().size()).isEqualTo(expectedEntities);
|
||||
assertThat(countAllEntities).isEqualTo(expectedEntities);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -46,7 +46,7 @@ import org.eclipse.hawkbit.repository.model.TenantConfigurationValue;
|
||||
import org.eclipse.hawkbit.repository.model.helper.SystemSecurityContextHolder;
|
||||
import org.eclipse.hawkbit.repository.model.helper.TenantConfigurationManagementHolder;
|
||||
import org.eclipse.hawkbit.repository.rsql.RsqlVisitorFactory;
|
||||
import org.eclipse.hawkbit.repository.rsql.RsqlVisitorFactoryHolder;
|
||||
import org.eclipse.hawkbit.repository.rsql.RsqlConfigHolder;
|
||||
import org.eclipse.hawkbit.repository.rsql.VirtualPropertyReplacer;
|
||||
import org.eclipse.hawkbit.repository.rsql.VirtualPropertyResolver;
|
||||
import org.eclipse.hawkbit.security.SystemSecurityContext;
|
||||
@@ -116,8 +116,8 @@ public class RSQLUtilityTest {
|
||||
}
|
||||
|
||||
@Bean
|
||||
RsqlVisitorFactoryHolder rsqlVisitorFactoryHolder() {
|
||||
return RsqlVisitorFactoryHolder.getInstance();
|
||||
RsqlConfigHolder rsqlVisitorFactoryHolder() {
|
||||
return RsqlConfigHolder.getInstance();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -284,12 +284,36 @@ public class RSQLUtilityTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void correctRsqlBuildsSimpleNotLikePredicate() {
|
||||
public void correctRsqlBuildsSimpleNotEqualPredicate() {
|
||||
reset(baseSoftwareModuleRootMock, criteriaQueryMock, criteriaBuilderMock);
|
||||
final String correctRsql = "name!=abc";
|
||||
when(baseSoftwareModuleRootMock.get("name")).thenReturn(baseSoftwareModuleRootMock);
|
||||
when(baseSoftwareModuleRootMock.getJavaType()).thenReturn((Class) SoftwareModule.class);
|
||||
|
||||
when(criteriaBuilderMock.isNull(any(Expression.class))).thenReturn(mock(Predicate.class));
|
||||
when(criteriaBuilderMock.notEqual(any(Expression.class), anyString()))
|
||||
.thenReturn(mock(Predicate.class));
|
||||
when(criteriaBuilderMock.upper(eq(pathOfString(baseSoftwareModuleRootMock))))
|
||||
.thenReturn(pathOfString(baseSoftwareModuleRootMock));
|
||||
|
||||
// test
|
||||
RSQLUtility.buildRsqlSpecification(correctRsql, SoftwareModuleFields.class, null, testDb)
|
||||
.toPredicate(baseSoftwareModuleRootMock, criteriaQueryMock, criteriaBuilderMock);
|
||||
|
||||
// verification
|
||||
verify(criteriaBuilderMock, times(1)).or(any(Predicate.class), any(Predicate.class));
|
||||
verify(criteriaBuilderMock, times(1)).isNull(eq(pathOfString(baseSoftwareModuleRootMock)));
|
||||
verify(criteriaBuilderMock, times(1)).notEqual(eq(pathOfString(baseSoftwareModuleRootMock)),
|
||||
eq("abc".toUpperCase()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void correctRsqlBuildsSimpleNotLikePredicate() {
|
||||
reset(baseSoftwareModuleRootMock, criteriaQueryMock, criteriaBuilderMock);
|
||||
final String correctRsql = "name!=abc*";
|
||||
when(baseSoftwareModuleRootMock.get("name")).thenReturn(baseSoftwareModuleRootMock);
|
||||
when(baseSoftwareModuleRootMock.getJavaType()).thenReturn((Class) SoftwareModule.class);
|
||||
|
||||
when(criteriaBuilderMock.isNull(any(Expression.class))).thenReturn(mock(Predicate.class));
|
||||
when(criteriaBuilderMock.notLike(any(Expression.class), anyString(), eq('\\')))
|
||||
.thenReturn(mock(Predicate.class));
|
||||
@@ -304,7 +328,7 @@ public class RSQLUtilityTest {
|
||||
verify(criteriaBuilderMock, times(1)).or(any(Predicate.class), any(Predicate.class));
|
||||
verify(criteriaBuilderMock, times(1)).isNull(eq(pathOfString(baseSoftwareModuleRootMock)));
|
||||
verify(criteriaBuilderMock, times(1)).notLike(eq(pathOfString(baseSoftwareModuleRootMock)),
|
||||
eq("abc".toUpperCase()), eq('\\'));
|
||||
eq("abc%".toUpperCase()), eq('\\'));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -335,11 +359,32 @@ public class RSQLUtilityTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void correctRsqlBuildsLikePredicateWithPercentage() {
|
||||
public void correctRsqlBuildsEqualPredicateWithPercentage() {
|
||||
reset(baseSoftwareModuleRootMock, criteriaQueryMock, criteriaBuilderMock);
|
||||
final String correctRsql = "name==a%";
|
||||
when(baseSoftwareModuleRootMock.get("name")).thenReturn(baseSoftwareModuleRootMock);
|
||||
when(baseSoftwareModuleRootMock.getJavaType()).thenReturn((Class) SoftwareModule.class);
|
||||
when(criteriaBuilderMock.equal(any(Expression.class), anyString())).thenReturn(mock(Predicate.class));
|
||||
when(criteriaBuilderMock.<String> greaterThanOrEqualTo(any(Expression.class), any(String.class)))
|
||||
.thenReturn(mock(Predicate.class));
|
||||
when(criteriaBuilderMock.upper(eq(pathOfString(baseSoftwareModuleRootMock))))
|
||||
.thenReturn(pathOfString(baseSoftwareModuleRootMock));
|
||||
// test
|
||||
RSQLUtility.buildRsqlSpecification(correctRsql, SoftwareModuleFields.class, null, Database.H2)
|
||||
.toPredicate(baseSoftwareModuleRootMock, criteriaQueryMock, criteriaBuilderMock);
|
||||
|
||||
// verification
|
||||
verify(criteriaBuilderMock, times(1)).and(any(Predicate.class));
|
||||
verify(criteriaBuilderMock, times(1)).equal(eq(pathOfString(baseSoftwareModuleRootMock)),
|
||||
eq("a%".toUpperCase()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void correctRsqlBuildsLikePredicateWithPercentage() {
|
||||
reset(baseSoftwareModuleRootMock, criteriaQueryMock, criteriaBuilderMock);
|
||||
final String correctRsql = "name==a%*";
|
||||
when(baseSoftwareModuleRootMock.get("name")).thenReturn(baseSoftwareModuleRootMock);
|
||||
when(baseSoftwareModuleRootMock.getJavaType()).thenReturn((Class) SoftwareModule.class);
|
||||
when(criteriaBuilderMock.like(any(Expression.class), anyString(), eq('\\'))).thenReturn(mock(Predicate.class));
|
||||
when(criteriaBuilderMock.<String> greaterThanOrEqualTo(any(Expression.class), any(String.class)))
|
||||
.thenReturn(mock(Predicate.class));
|
||||
@@ -352,13 +397,13 @@ public class RSQLUtilityTest {
|
||||
// verification
|
||||
verify(criteriaBuilderMock, times(1)).and(any(Predicate.class));
|
||||
verify(criteriaBuilderMock, times(1)).like(eq(pathOfString(baseSoftwareModuleRootMock)),
|
||||
eq("a\\%".toUpperCase()), eq('\\'));
|
||||
eq("a\\%%".toUpperCase()), eq('\\'));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void correctRsqlBuildsLikePredicateWithPercentageSQLServer() {
|
||||
reset(baseSoftwareModuleRootMock, criteriaQueryMock, criteriaBuilderMock);
|
||||
final String correctRsql = "name==a%";
|
||||
final String correctRsql = "name==a%*";
|
||||
when(baseSoftwareModuleRootMock.get("name")).thenReturn(baseSoftwareModuleRootMock);
|
||||
when(baseSoftwareModuleRootMock.getJavaType()).thenReturn((Class) SoftwareModule.class);
|
||||
when(criteriaBuilderMock.upper(eq(pathOfString(baseSoftwareModuleRootMock))))
|
||||
@@ -374,7 +419,7 @@ public class RSQLUtilityTest {
|
||||
// verification
|
||||
verify(criteriaBuilderMock, times(1)).and(any(Predicate.class));
|
||||
verify(criteriaBuilderMock, times(1)).like(eq(pathOfString(baseSoftwareModuleRootMock)),
|
||||
eq("a[%]".toUpperCase()), eq('\\'));
|
||||
eq("a[%]%".toUpperCase()), eq('\\'));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
Reference in New Issue
Block a user