diff --git a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/rsql/RsqlConfigHolder.java b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/rsql/RsqlConfigHolder.java new file mode 100644 index 000000000..5795f9743 --- /dev/null +++ b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/rsql/RsqlConfigHolder.java @@ -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 true + * "x == ax" will match "x == aX" + */ + @Value("${hawkbit.rsql.ignoreCase:true}") + private boolean ignoreCase; + /** + * Declares if the database is case-insensitive, by default assumes false. In case it is case-sensitive and, + * {@link #ignoreCase} is set to true the SQL queries use upper case comparisons to ignore case. + * + * If the database is declared as case-sensitive and ignoreCase is set to false 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; + } +} \ No newline at end of file diff --git a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/rsql/RsqlVisitorFactoryHolder.java b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/rsql/RsqlVisitorFactoryHolder.java deleted file mode 100644 index 2b108b00e..000000000 --- a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/rsql/RsqlVisitorFactoryHolder.java +++ /dev/null @@ -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; - } - -} diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/RepositoryApplicationConfiguration.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/RepositoryApplicationConfiguration.java index 2d2689668..cbfffe743 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/RepositoryApplicationConfiguration.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/RepositoryApplicationConfiguration.java @@ -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(); } /** diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/rsql/JpaQueryRsqlVisitor.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/rsql/JpaQueryRsqlVisitor.java index 5222150d6..9efa097d1 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/rsql/JpaQueryRsqlVisitor.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/rsql/JpaQueryRsqlVisitor.java @@ -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 & FieldNameProvider, T> exten private final CriteriaBuilder cb; private final CriteriaQuery query; private final Database database; + private final boolean ensureIgnoreCase; private final Root root; private final SimpleTypeConverter simpleTypeConverter; private final VirtualPropertyReplacer virtualPropertyReplacer; @@ -85,7 +86,7 @@ public class JpaQueryRsqlVisitor & FieldNameProvider, T> exten public JpaQueryRsqlVisitor(final Root root, final CriteriaBuilder cb, final Class 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 & 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 & FieldNameProvider, T> exten @Override public List visit(final AndNode node, final String param) { beginLevel(false); - final List childs = acceptChilds(node); + final List 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 & FieldNameProvider, T> exten @Override public List visit(final OrNode node, final String param) { beginLevel(true); - final List childs = acceptChilds(node); + final List 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 & 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 join = (Join) fieldPath; - final From joinParent = join.getParent(); + if (fieldPath instanceof PluralJoin join) { + final From joinParent = join.getParent(); final Optional> 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 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 & FieldNameProvider, T> exten private Object convertValueIfNecessary(final ComparisonNode node, final A fieldName, final String value, final Path 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 & 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 & 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 & FieldNameProvider, T> exten } } - private Predicate getInPredicate(final List transformedValues, final Path fieldPath) { - final List 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 transformedValues, final String finalProperty, + private Predicate getOutPredicate( + final List transformedValues, final String finalProperty, final A enumField, final Path fieldPath) { - final String[] fieldNames = enumField.getSubAttributes(finalProperty); - final List 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 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 fieldPath, final List transformedValues, - final List outParams) { - - final Path 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 transformedValues, - final A enumField, final List outParams) { - final Function, 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 getMapValueFieldPath(final A enumField, final Path fieldPath) { @@ -420,16 +386,15 @@ public class JpaQueryRsqlVisitor & 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) (((MapJoin) fieldPath).key())), - keyValue.toUpperCase()); + // Currently we support only string key. So below cast is safe. + return equal((Expression) (((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 fieldPath) { @@ -437,13 +402,16 @@ public class JpaQueryRsqlVisitor & 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 & 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 & FieldNameProvider, T> exten } } - private Predicate toNotNullPredicate(final Path fieldPath) { - return cb.isNotNull(pathOfString(fieldPath)); - } - - private Predicate toNullOrNotLikePredicate(final Path 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 fieldPath, final Object transformedValue) { - return cb.or(cb.isNull(pathOfString(fieldPath)), cb.notEqual(fieldPath, transformedValue)); - } - - private Predicate toNotNullAndNotEmptyPredicate(final Path 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, 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 & 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 & FieldNameProvider, T> exten } else { finalizedValue = escapedValue.replace(LIKE_WILDCARD, '%'); } - return finalizedValue.toUpperCase(); + return finalizedValue; } @SuppressWarnings("unchecked") @@ -583,7 +550,7 @@ public class JpaQueryRsqlVisitor & FieldNameProvider, T> exten return (Path) path; } - private List acceptChilds(final LogicalNode node) { + private List acceptChildren(final LogicalNode node) { final List children = node.getChildren(); final List childs = new ArrayList<>(); for (final Node node2 : children) { @@ -597,4 +564,28 @@ public class JpaQueryRsqlVisitor & FieldNameProvider, T> exten return childs; } -} + private Predicate equal(final Expression expressionToCompare, final String sqlValue) { + return cb.equal(caseWise(cb, expressionToCompare), caseWise(sqlValue)); + } + private Predicate notEqual(final Expression expressionToCompare, String transformedValueStr) { + return cb.notEqual(caseWise(cb, expressionToCompare), caseWise(transformedValueStr)); + } + private Predicate like(final Expression expressionToCompare, final String sqlValue) { + return cb.like(caseWise(cb, expressionToCompare), caseWise(sqlValue), ESCAPE_CHAR); + } + private Predicate notLike(final Expression expressionToCompare, final String sqlValue) { + return cb.notLike(caseWise(cb, expressionToCompare), caseWise(sqlValue), ESCAPE_CHAR); + } + private Predicate in(final Expression expressionToCompare, final List transformedValues) { + final List 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 caseWise(final CriteriaBuilder cb, final Expression expression) { + return ensureIgnoreCase ? cb.upper(expression) : expression; + } + private String caseWise(final String str) { + return ensureIgnoreCase ? str.toUpperCase() : str; + } +} \ No newline at end of file diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/rsql/RSQLUtility.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/rsql/RSQLUtility.java index 35f6a5d28..f0effa00c 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/rsql/RSQLUtility.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/rsql/RSQLUtility.java @@ -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 null - * @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 null + * @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 & FieldNameProvider, T> Specification buildRsqlSpecification(final String rsql, - final Class fieldNameProvider, final VirtualPropertyReplacer virtualPropertyReplacer, - final Database database) { + public static & FieldNameProvider, T> Specification buildRsqlSpecification( + final String rsql, final Class 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 & FieldNameProvider> void validateRsqlFor(final String rsql, - final Class fieldNameProvider) { - final RSQLVisitor visitor = getValidationRsqlVisitor(fieldNameProvider); + public static & FieldNameProvider> void validateRsqlFor( + final String rsql, final Class fieldNameProvider) { + final RSQLVisitor visitor = + RsqlConfigHolder.getInstance().getRsqlVisitorFactory().validationRsqlVisitor(fieldNameProvider); final Node rootNode = parseRsql(rsql); rootNode.accept(visitor); } - private static & FieldNameProvider> RSQLVisitor getValidationRsqlVisitor( - final Class 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 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 & FieldNameProvider, T> implements Specification { + @Serial private static final long serialVersionUID = 1L; private final String rsql; @@ -164,15 +151,14 @@ public final class RSQLUtility { query.distinct(true); final JpaQueryRsqlVisitor jpqQueryRSQLVisitor = new JpaQueryRsqlVisitor<>(root, cb, enumType, - virtualPropertyReplacer, database, query); - final List accept = rootNode., String> accept(jpqQueryRSQLVisitor); + virtualPropertyReplacer, database, query, + !RsqlConfigHolder.getInstance().isCaseInsensitiveDB() && RsqlConfigHolder.getInstance().isIgnoreCase()); + final List 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(); - } } - -} +} \ No newline at end of file diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/rsql/RSQLActionFieldsTest.java b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/rsql/RSQLActionFieldsTest.java index de485602e..28ba4e501 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/rsql/RSQLActionFieldsTest.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/rsql/RSQLActionFieldsTest.java @@ -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 findEnitity = deploymentManagement.findActionsByTarget(rsqlParam, target.getControllerId(), + final Slice 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); } -} +} \ No newline at end of file diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/rsql/RSQLUtilityTest.java b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/rsql/RSQLUtilityTest.java index 4caf1200e..28c0784bb 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/rsql/RSQLUtilityTest.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/rsql/RSQLUtilityTest.java @@ -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. 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. 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