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:
Avgustin Marinov
2024-07-15 13:04:47 +03:00
committed by GitHub
parent beeb2523e2
commit ff6d7a29f6
7 changed files with 253 additions and 226 deletions

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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.model.helper.TenantConfigurationManagementHolder;
import org.eclipse.hawkbit.repository.rsql.RsqlValidationOracle; import org.eclipse.hawkbit.repository.rsql.RsqlValidationOracle;
import org.eclipse.hawkbit.repository.rsql.RsqlVisitorFactory; 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.VirtualPropertyReplacer;
import org.eclipse.hawkbit.security.HawkbitSecurityProperties; import org.eclipse.hawkbit.security.HawkbitSecurityProperties;
import org.eclipse.hawkbit.security.SecurityTokenGenerator; 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 @Bean
RsqlVisitorFactoryHolder rsqlVisitorFactoryHolder() { RsqlConfigHolder rsqlVisitorFactoryHolder() {
return RsqlVisitorFactoryHolder.getInstance(); return RsqlConfigHolder.getInstance();
} }
/** /**

View File

@@ -52,7 +52,7 @@ import org.springframework.beans.SimpleTypeConverter;
import org.springframework.beans.TypeMismatchException; import org.springframework.beans.TypeMismatchException;
import org.springframework.orm.jpa.vendor.Database; import org.springframework.orm.jpa.vendor.Database;
import org.springframework.util.CollectionUtils; 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 * 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 CriteriaBuilder cb;
private final CriteriaQuery<?> query; private final CriteriaQuery<?> query;
private final Database database; private final Database database;
private final boolean ensureIgnoreCase;
private final Root<T> root; private final Root<T> root;
private final SimpleTypeConverter simpleTypeConverter; private final SimpleTypeConverter simpleTypeConverter;
private final VirtualPropertyReplacer virtualPropertyReplacer; 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, public JpaQueryRsqlVisitor(final Root<T> root, final CriteriaBuilder cb, final Class<A> enumType,
final VirtualPropertyReplacer virtualPropertyReplacer, final Database database, final VirtualPropertyReplacer virtualPropertyReplacer, final Database database,
final CriteriaQuery<?> query) { final CriteriaQuery<?> query, final boolean ensureIgnoreCase) {
super(enumType); super(enumType);
this.root = root; this.root = root;
this.cb = cb; this.cb = cb;
@@ -93,7 +94,7 @@ public class JpaQueryRsqlVisitor<A extends Enum<A> & FieldNameProvider, T> exten
this.virtualPropertyReplacer = virtualPropertyReplacer; this.virtualPropertyReplacer = virtualPropertyReplacer;
this.simpleTypeConverter = new SimpleTypeConverter(); this.simpleTypeConverter = new SimpleTypeConverter();
this.database = database; this.database = database;
this.joinsNeeded = false; this.ensureIgnoreCase = ensureIgnoreCase;
} }
private void beginLevel(final boolean isOr) { private void beginLevel(final boolean isOr) {
@@ -128,10 +129,10 @@ public class JpaQueryRsqlVisitor<A extends Enum<A> & FieldNameProvider, T> exten
@Override @Override
public List<Predicate> visit(final AndNode node, final String param) { public List<Predicate> visit(final AndNode node, final String param) {
beginLevel(false); beginLevel(false);
final List<Predicate> childs = acceptChilds(node); final List<Predicate> childs = acceptChildren(node);
endLevel(); endLevel();
if (!childs.isEmpty()) { 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()); return toSingleList(cb.conjunction());
} }
@@ -139,10 +140,10 @@ public class JpaQueryRsqlVisitor<A extends Enum<A> & FieldNameProvider, T> exten
@Override @Override
public List<Predicate> visit(final OrNode node, final String param) { public List<Predicate> visit(final OrNode node, final String param) {
beginLevel(true); beginLevel(true);
final List<Predicate> childs = acceptChilds(node); final List<Predicate> childs = acceptChildren(node);
endLevel(); endLevel();
if (!childs.isEmpty()) { 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()); 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)); () -> new RSQLParameterUnsupportedFieldException("RSQL field path cannot be empty", null));
} }
@SuppressWarnings("unchecked") @SuppressWarnings({"rawtypes", "unchecked"})
private Path<?> getJoinFieldPath(final Path<?> fieldPath, final String fieldNameSplit) { private Path<?> getJoinFieldPath(final Path<?> fieldPath, final String fieldNameSplit) {
if (fieldPath instanceof PluralJoin) { if (fieldPath instanceof PluralJoin join) {
final Join<Object, ?> join = (Join<Object, ?>) fieldPath; final From joinParent = join.getParent();
final From<?, Object> joinParent = join.getParent();
final Optional<Join<Object, Object>> currentJoinOfType = findCurrentJoinOfType(join.getJavaType()); final Optional<Join<Object, Object>> currentJoinOfType = findCurrentJoinOfType(join.getJavaType());
if (currentJoinOfType.isPresent() && isOrLevel) { if (currentJoinOfType.isPresent() && isOrLevel) {
// remove the additional join and use the existing one // remove the additional join and use the existing one
joinParent.getJoins().remove(join); joinParent.getJoins().remove(join);
return currentJoinOfType.get(); return currentJoinOfType.get();
} else { } else {
final Join<Object, Object> newJoin = joinParent.join(fieldNameSplit, JoinType.LEFT); final Join newJoin = joinParent.join(fieldNameSplit, JoinType.LEFT);
addCurrentJoin(newJoin); addCurrentJoin(newJoin);
return 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, private Object convertValueIfNecessary(final ComparisonNode node, final A fieldName, final String value,
final Path<Object> fieldPath) { 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 // enum we need to handle it separately because JPA needs the
// correct java-type to build an expression. So String and numeric // 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. // 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() throw new RSQLParameterUnsupportedFieldException("field {" + node.getSelector()
+ "} must be one of the following values {" + Arrays.stream(tmpEnumType.getEnumConstants()) + "} 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); + "}", e);
} }
} }
@@ -343,7 +343,7 @@ public class JpaQueryRsqlVisitor<A extends Enum<A> & FieldNameProvider, T> exten
case "=le=": case "=le=":
return cb.lessThanOrEqualTo(pathOfString(fieldPath), value); return cb.lessThanOrEqualTo(pathOfString(fieldPath), value);
case "=in=": case "=in=":
return getInPredicate(transformedValues, fieldPath); return in(pathOfString(fieldPath), transformedValues);
case "=out=": case "=out=":
return getOutPredicate(transformedValues, finalProperty, enumField, fieldPath); return getOutPredicate(transformedValues, finalProperty, enumField, fieldPath);
default: 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) { private Predicate getOutPredicate(
final List<String> inParams = new ArrayList<>(); final List<Object> transformedValues, final String finalProperty,
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,
final A enumField, final Path<Object> fieldPath) { final A enumField, final Path<Object> fieldPath) {
final String[] fieldNames = enumField.getSubAttributes(finalProperty); 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())) { 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(); clearOuterJoinsIfNotNeeded();
return toOutWithSubQueryPredicate(fieldNames, transformedValues, enumField, outParams); return toNotExistsSubQueryPredicate(fieldNames, enumField, expressionToCompare -> in(expressionToCompare, transformedValues));
}
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);
} }
private Path<Object> getMapValueFieldPath(final A enumField, final Path<Object> fieldPath) { 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]; final String keyValue = graph[graph.length - 1];
if (fieldPath instanceof MapJoin) { if (fieldPath instanceof MapJoin) {
// Currently we support only string key .So below cast is safe. // Currently we support only string key. So below cast is safe.
return cb.equal(cb.upper((Expression<String>) (((MapJoin<?, ?, ?>) fieldPath).key())), return equal((Expression<String>) (((MapJoin<?, ?, ?>) fieldPath).key()), keyValue);
keyValue.toUpperCase());
} }
final String keyFieldName = enumField.getSubEntityMapTuple().map(Entry::getKey) final String keyFieldName = enumField.getSubEntityMapTuple().map(Entry::getKey)
.orElseThrow(() -> new UnsupportedOperationException( .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!")); "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()); return equal(fieldPath.get(keyFieldName), keyValue);
} }
private Predicate getEqualToPredicate(final Object transformedValue, final Path<Object> fieldPath) { 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)); return cb.isNull(pathOfString(fieldPath));
} }
if ((transformedValue instanceof String) && !NumberUtils.isCreatable((String) transformedValue)) { if ((transformedValue instanceof String transformedValueStr) && !NumberUtils.isCreatable(transformedValueStr)) {
if (StringUtils.isEmpty(transformedValue)) { if (ObjectUtils.isEmpty(transformedValue)) {
return cb.or(cb.isNull(pathOfString(fieldPath)), cb.equal(pathOfString(fieldPath), "")); return cb.or(cb.isNull(pathOfString(fieldPath)), cb.equal(pathOfString(fieldPath), ""));
} }
final String sqlValue = toSQL((String) transformedValue); if (isPattern(transformedValueStr)) { // a pattern, use like
return cb.like(cb.upper(pathOfString(fieldPath)), sqlValue, ESCAPE_CHAR); return like(pathOfString(fieldPath), toSQL(transformedValueStr));
} else {
return equal(pathOfString(fieldPath), transformedValueStr);
}
} }
return cb.equal(fieldPath, transformedValue); 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) { final String finalProperty, final A enumField) {
if (transformedValue == null) { if (transformedValue == null) {
return toNotNullPredicate(fieldPath); return cb.isNotNull(pathOfString(fieldPath));
} }
if ((transformedValue instanceof String) && !NumberUtils.isCreatable((String) transformedValue)) { if ((transformedValue instanceof String transformedValueStr) && !NumberUtils.isCreatable(transformedValueStr)) {
if (StringUtils.isEmpty(transformedValue)) { if (ObjectUtils.isEmpty(transformedValue)) {
return toNotNullAndNotEmptyPredicate(fieldPath); return cb.and(cb.isNotNull(pathOfString(fieldPath)), cb.notEqual(pathOfString(fieldPath), ""));
} }
final String sqlValue = toSQL((String) transformedValue);
final String[] fieldNames = enumField.getSubAttributes(finalProperty); final String[] fieldNames = enumField.getSubAttributes(finalProperty);
if (isSimpleField(fieldNames, enumField.isMap())) { 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(); 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); 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) { private Predicate toNullOrNotEqualPredicate(final Path<Object> fieldPath, final Object transformedValue) {
return cb.or(cb.isNull(pathOfString(fieldPath)), cb.notEqual(fieldPath, transformedValue)); return cb.or(
} cb.isNull(pathOfString(fieldPath)),
transformedValue instanceof String transformedValueStr
private Predicate toNotNullAndNotEmptyPredicate(final Path<Object> fieldPath) { ? notEqual(pathOfString(fieldPath), transformedValueStr)
return cb.and(cb.isNotNull(pathOfString(fieldPath)), cb.notEqual(pathOfString(fieldPath), "")); : cb.notEqual(fieldPath, transformedValue));
}
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);
} }
@SuppressWarnings({ "unchecked", "rawtypes" }) @SuppressWarnings({ "unchecked", "rawtypes" })
@@ -556,6 +515,14 @@ public class JpaQueryRsqlVisitor<A extends Enum<A> & FieldNameProvider, T> exten
return fieldPath; 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) { private String toSQL(final String transformedValue) {
final String escaped; final String escaped;
@@ -575,7 +542,7 @@ public class JpaQueryRsqlVisitor<A extends Enum<A> & FieldNameProvider, T> exten
} else { } else {
finalizedValue = escapedValue.replace(LIKE_WILDCARD, '%'); finalizedValue = escapedValue.replace(LIKE_WILDCARD, '%');
} }
return finalizedValue.toUpperCase(); return finalizedValue;
} }
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
@@ -583,7 +550,7 @@ public class JpaQueryRsqlVisitor<A extends Enum<A> & FieldNameProvider, T> exten
return (Path<Y>) path; 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<Node> children = node.getChildren();
final List<Predicate> childs = new ArrayList<>(); final List<Predicate> childs = new ArrayList<>();
for (final Node node2 : children) { for (final Node node2 : children) {
@@ -597,4 +564,28 @@ public class JpaQueryRsqlVisitor<A extends Enum<A> & FieldNameProvider, T> exten
return childs; 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;
}
}

View File

@@ -9,6 +9,7 @@
*/ */
package org.eclipse.hawkbit.repository.jpa.rsql; package org.eclipse.hawkbit.repository.jpa.rsql;
import java.io.Serial;
import java.util.List; import java.util.List;
import java.util.Set; 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.FieldNameProvider;
import org.eclipse.hawkbit.repository.exception.RSQLParameterSyntaxException; import org.eclipse.hawkbit.repository.exception.RSQLParameterSyntaxException;
import org.eclipse.hawkbit.repository.exception.RSQLParameterUnsupportedFieldException; 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.VirtualPropertyReplacer;
import org.eclipse.hawkbit.repository.rsql.VirtualPropertyResolver; import org.eclipse.hawkbit.repository.rsql.VirtualPropertyResolver;
import org.springframework.data.jpa.domain.Specification; 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 * query. The specification can be used to filter for JPA entities with the
* given RSQL query. * given RSQL query.
* *
* @param rsql * @param rsql the rsql query to be parsed
* the rsql query to be parsed * @param fieldNameProvider the enum class type which implements the {@link FieldNameProvider}
* @param fieldNameProvider * @param virtualPropertyReplacer holds the logic how the known macros have to be resolved; may be <code>null</code>
* the enum class type which implements the * @param database database in use
* {@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
* *
* @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, public static <A extends Enum<A> & FieldNameProvider, T> Specification<T> buildRsqlSpecification(
final Class<A> fieldNameProvider, final VirtualPropertyReplacer virtualPropertyReplacer, final String rsql, final Class<A> fieldNameProvider,
final Database database) { final VirtualPropertyReplacer virtualPropertyReplacer, final Database database) {
return new RSQLSpecification<>(rsql, fieldNameProvider, virtualPropertyReplacer, database); return new RSQLSpecification<>(rsql, fieldNameProvider, virtualPropertyReplacer, database);
} }
/** /**
* Validates the RSQL string * Validates the RSQL string
* *
* @param rsql * @param rsql RSQL string to validate
* RSQL string to validate
* @param fieldNameProvider * @param fieldNameProvider
* *
* @throws RSQLParserException * @throws RSQLParserException if RSQL syntax is invalid
* if RSQL syntax is invalid * @throws RSQLParameterUnsupportedFieldException if RSQL key is not allowed
* @throws RSQLParameterUnsupportedFieldException
* if RSQL key is not allowed
*/ */
public static <A extends Enum<A> & FieldNameProvider> void validateRsqlFor(final String rsql, public static <A extends Enum<A> & FieldNameProvider> void validateRsqlFor(
final Class<A> fieldNameProvider) { final String rsql, final Class<A> fieldNameProvider) {
final RSQLVisitor<Void, String> visitor = getValidationRsqlVisitor(fieldNameProvider); final RSQLVisitor<Void, String> visitor =
RsqlConfigHolder.getInstance().getRsqlVisitorFactory().validationRsqlVisitor(fieldNameProvider);
final Node rootNode = parseRsql(rsql); final Node rootNode = parseRsql(rsql);
rootNode.accept(visitor); 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) { private static Node parseRsql(final String rsql) {
log.debug("Parsing rsql string {}", rsql);
try { try {
log.debug("Parsing rsql string {}", rsql);
final Set<ComparisonOperator> operators = RSQLOperators.defaultOperators(); 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) { } 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) { } catch (final RSQLParserException e) {
throw new RSQLParameterSyntaxException(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> { private static final class RSQLSpecification<A extends Enum<A> & FieldNameProvider, T> implements Specification<T> {
@Serial
private static final long serialVersionUID = 1L; private static final long serialVersionUID = 1L;
private final String rsql; private final String rsql;
@@ -164,15 +151,14 @@ public final class RSQLUtility {
query.distinct(true); query.distinct(true);
final JpaQueryRsqlVisitor<A, T> jpqQueryRSQLVisitor = new JpaQueryRsqlVisitor<>(root, cb, enumType, final JpaQueryRsqlVisitor<A, T> jpqQueryRSQLVisitor = new JpaQueryRsqlVisitor<>(root, cb, enumType,
virtualPropertyReplacer, database, query); virtualPropertyReplacer, database, query,
final List<Predicate> accept = rootNode.<List<Predicate>, String> accept(jpqQueryRSQLVisitor); !RsqlConfigHolder.getInstance().isCaseInsensitiveDB() && RsqlConfigHolder.getInstance().isIgnoreCase());
final List<Predicate> accept = rootNode.accept(jpqQueryRSQLVisitor);
if (!CollectionUtils.isEmpty(accept)) { if (!CollectionUtils.isEmpty(accept)) {
return cb.and(accept.toArray(new Predicate[accept.size()])); return cb.and(accept.toArray(new Predicate[0]));
} }
return cb.conjunction(); return cb.conjunction();
} }
} }
}
}

View File

@@ -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.ActionType;
import org.eclipse.hawkbit.repository.model.Action.Status; import org.eclipse.hawkbit.repository.model.Action.Status;
import org.eclipse.hawkbit.repository.model.DistributionSet; import org.eclipse.hawkbit.repository.model.DistributionSet;
import org.jetbrains.annotations.NotNull;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.PageRequest;
@@ -43,34 +44,30 @@ public class RSQLActionFieldsTest extends AbstractJpaIntegrationTest {
final DistributionSet dsA = testdataFactory.createDistributionSet("daA"); final DistributionSet dsA = testdataFactory.createDistributionSet("daA");
target = (JpaTarget) targetManagement target = (JpaTarget) targetManagement
.create(entityFactory.target().create().controllerId("targetId123").description("targetId123")); .create(entityFactory.target().create().controllerId("targetId123").description("targetId123"));
action = new JpaAction();
action.setActionType(ActionType.SOFT); action = newJpaAction(dsA, false, null);
action.setDistributionSet(dsA); for (int i = 0; i < 10; i++) {
action.setTarget(target); newJpaAction(dsA, i % 2 == 0, i % 2 == 0 ? "extRef" : "extRef2");
action.setStatus(Status.RUNNING); }
action.setWeight(45); }
action.setInitiatedBy(tenantAware.getCurrentUsername());
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); target.addAction(action);
actionRepository.save(action); return newAction;
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);
}
} }
@Test @Test
@@ -116,11 +113,11 @@ public class RSQLActionFieldsTest extends AbstractJpaIntegrationTest {
} }
private void assertRSQLQuery(final String rsqlParam, final long expectedEntities) { private void assertRSQLQuery(final String rsqlParam, final long expectedEntities) {
final Slice<Action> findEntity = deploymentManagement.findActionsByTarget(rsqlParam, target.getControllerId(),
final Slice<Action> findEnitity = deploymentManagement.findActionsByTarget(rsqlParam, target.getControllerId(),
PageRequest.of(0, 100)); PageRequest.of(0, 100));
final long countAllEntities = deploymentManagement.countActionsByTarget(rsqlParam, target.getControllerId()); final long countAllEntities = deploymentManagement.countActionsByTarget(rsqlParam, target.getControllerId());
assertThat(findEnitity).isNotNull(); assertThat(findEntity).isNotNull();
assertThat(findEntity.getContent().size()).isEqualTo(expectedEntities);
assertThat(countAllEntities).isEqualTo(expectedEntities); assertThat(countAllEntities).isEqualTo(expectedEntities);
} }
} }

View File

@@ -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.SystemSecurityContextHolder;
import org.eclipse.hawkbit.repository.model.helper.TenantConfigurationManagementHolder; import org.eclipse.hawkbit.repository.model.helper.TenantConfigurationManagementHolder;
import org.eclipse.hawkbit.repository.rsql.RsqlVisitorFactory; 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.VirtualPropertyReplacer;
import org.eclipse.hawkbit.repository.rsql.VirtualPropertyResolver; import org.eclipse.hawkbit.repository.rsql.VirtualPropertyResolver;
import org.eclipse.hawkbit.security.SystemSecurityContext; import org.eclipse.hawkbit.security.SystemSecurityContext;
@@ -116,8 +116,8 @@ public class RSQLUtilityTest {
} }
@Bean @Bean
RsqlVisitorFactoryHolder rsqlVisitorFactoryHolder() { RsqlConfigHolder rsqlVisitorFactoryHolder() {
return RsqlVisitorFactoryHolder.getInstance(); return RsqlConfigHolder.getInstance();
} }
} }
@@ -284,12 +284,36 @@ public class RSQLUtilityTest {
} }
@Test @Test
public void correctRsqlBuildsSimpleNotLikePredicate() { public void correctRsqlBuildsSimpleNotEqualPredicate() {
reset(baseSoftwareModuleRootMock, criteriaQueryMock, criteriaBuilderMock); reset(baseSoftwareModuleRootMock, criteriaQueryMock, criteriaBuilderMock);
final String correctRsql = "name!=abc"; final String correctRsql = "name!=abc";
when(baseSoftwareModuleRootMock.get("name")).thenReturn(baseSoftwareModuleRootMock); when(baseSoftwareModuleRootMock.get("name")).thenReturn(baseSoftwareModuleRootMock);
when(baseSoftwareModuleRootMock.getJavaType()).thenReturn((Class) SoftwareModule.class); 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.isNull(any(Expression.class))).thenReturn(mock(Predicate.class));
when(criteriaBuilderMock.notLike(any(Expression.class), anyString(), eq('\\'))) when(criteriaBuilderMock.notLike(any(Expression.class), anyString(), eq('\\')))
.thenReturn(mock(Predicate.class)); .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)).or(any(Predicate.class), any(Predicate.class));
verify(criteriaBuilderMock, times(1)).isNull(eq(pathOfString(baseSoftwareModuleRootMock))); verify(criteriaBuilderMock, times(1)).isNull(eq(pathOfString(baseSoftwareModuleRootMock)));
verify(criteriaBuilderMock, times(1)).notLike(eq(pathOfString(baseSoftwareModuleRootMock)), verify(criteriaBuilderMock, times(1)).notLike(eq(pathOfString(baseSoftwareModuleRootMock)),
eq("abc".toUpperCase()), eq('\\')); eq("abc%".toUpperCase()), eq('\\'));
} }
@Test @Test
@@ -335,11 +359,32 @@ public class RSQLUtilityTest {
} }
@Test @Test
public void correctRsqlBuildsLikePredicateWithPercentage() { public void correctRsqlBuildsEqualPredicateWithPercentage() {
reset(baseSoftwareModuleRootMock, criteriaQueryMock, criteriaBuilderMock); reset(baseSoftwareModuleRootMock, criteriaQueryMock, criteriaBuilderMock);
final String correctRsql = "name==a%"; final String correctRsql = "name==a%";
when(baseSoftwareModuleRootMock.get("name")).thenReturn(baseSoftwareModuleRootMock); when(baseSoftwareModuleRootMock.get("name")).thenReturn(baseSoftwareModuleRootMock);
when(baseSoftwareModuleRootMock.getJavaType()).thenReturn((Class) SoftwareModule.class); 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.like(any(Expression.class), anyString(), eq('\\'))).thenReturn(mock(Predicate.class));
when(criteriaBuilderMock.<String> greaterThanOrEqualTo(any(Expression.class), any(String.class))) when(criteriaBuilderMock.<String> greaterThanOrEqualTo(any(Expression.class), any(String.class)))
.thenReturn(mock(Predicate.class)); .thenReturn(mock(Predicate.class));
@@ -352,13 +397,13 @@ public class RSQLUtilityTest {
// verification // verification
verify(criteriaBuilderMock, times(1)).and(any(Predicate.class)); verify(criteriaBuilderMock, times(1)).and(any(Predicate.class));
verify(criteriaBuilderMock, times(1)).like(eq(pathOfString(baseSoftwareModuleRootMock)), verify(criteriaBuilderMock, times(1)).like(eq(pathOfString(baseSoftwareModuleRootMock)),
eq("a\\%".toUpperCase()), eq('\\')); eq("a\\%%".toUpperCase()), eq('\\'));
} }
@Test @Test
public void correctRsqlBuildsLikePredicateWithPercentageSQLServer() { public void correctRsqlBuildsLikePredicateWithPercentageSQLServer() {
reset(baseSoftwareModuleRootMock, criteriaQueryMock, criteriaBuilderMock); reset(baseSoftwareModuleRootMock, criteriaQueryMock, criteriaBuilderMock);
final String correctRsql = "name==a%"; final String correctRsql = "name==a%*";
when(baseSoftwareModuleRootMock.get("name")).thenReturn(baseSoftwareModuleRootMock); when(baseSoftwareModuleRootMock.get("name")).thenReturn(baseSoftwareModuleRootMock);
when(baseSoftwareModuleRootMock.getJavaType()).thenReturn((Class) SoftwareModule.class); when(baseSoftwareModuleRootMock.getJavaType()).thenReturn((Class) SoftwareModule.class);
when(criteriaBuilderMock.upper(eq(pathOfString(baseSoftwareModuleRootMock)))) when(criteriaBuilderMock.upper(eq(pathOfString(baseSoftwareModuleRootMock))))
@@ -374,7 +419,7 @@ public class RSQLUtilityTest {
// verification // verification
verify(criteriaBuilderMock, times(1)).and(any(Predicate.class)); verify(criteriaBuilderMock, times(1)).and(any(Predicate.class));
verify(criteriaBuilderMock, times(1)).like(eq(pathOfString(baseSoftwareModuleRootMock)), verify(criteriaBuilderMock, times(1)).like(eq(pathOfString(baseSoftwareModuleRootMock)),
eq("a[%]".toUpperCase()), eq('\\')); eq("a[%]%".toUpperCase()), eq('\\'));
} }
@Test @Test