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.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();
}
/**

View File

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

View File

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

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.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);
}
}
}

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.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