Fix != and =out= for maps in G2 RSQL to Specification (#2426)

+ add initial draft of Standalone RSQL test
+ provide option to override Hibernate / Eclipselink configuration via standard spring environment properties

Signed-off-by: Avgustin Marinov <Avgustin.Marinov@bosch.com>
This commit is contained in:
Avgustin Marinov
2025-06-04 16:30:33 +03:00
committed by GitHub
parent d3341f7c73
commit 23fa4cdd56
15 changed files with 716 additions and 57 deletions

View File

@@ -10,21 +10,12 @@
package org.eclipse.hawkbit.repository.jpa.rsql;
import static org.eclipse.hawkbit.repository.rsql.RsqlConfigHolder.RsqlToSpecBuilder.G3;
import static org.eclipse.hawkbit.repository.rsql.RsqlConfigHolder.RsqlToSpecBuilder.LEGACY_G1;
import java.io.Serial;
import java.util.List;
import jakarta.persistence.EntityManager;
import jakarta.persistence.criteria.CriteriaBuilder;
import jakarta.persistence.criteria.CriteriaQuery;
import jakarta.persistence.criteria.Predicate;
import jakarta.persistence.criteria.Root;
import cz.jirutka.rsql.parser.RSQLParser;
import cz.jirutka.rsql.parser.RSQLParserException;
import cz.jirutka.rsql.parser.ast.Node;
import cz.jirutka.rsql.parser.ast.RSQLVisitor;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@@ -32,15 +23,12 @@ import org.apache.commons.lang3.text.StrLookup;
import org.eclipse.hawkbit.repository.RsqlQueryField;
import org.eclipse.hawkbit.repository.exception.RSQLParameterSyntaxException;
import org.eclipse.hawkbit.repository.exception.RSQLParameterUnsupportedFieldException;
import org.eclipse.hawkbit.repository.jpa.rsqllegacy.JpaQueryRsqlVisitor;
import org.eclipse.hawkbit.repository.jpa.rsqllegacy.JpaQueryRsqlVisitorG2;
import org.eclipse.hawkbit.repository.jpa.rsqllegacy.SpecificationBuilderLegacy;
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;
import org.springframework.orm.jpa.vendor.Database;
import org.springframework.util.CollectionUtils;
/**
* A utility class which is able to parse RSQL strings into an spring data
@@ -99,7 +87,11 @@ public final class RSQLUtility {
return new SpecificationBuilder<T>(
virtualPropertyReplacer,
!RsqlConfigHolder.getInstance().isCaseInsensitiveDB() && RsqlConfigHolder.getInstance().isIgnoreCase(),
database).specification(RsqlParser.parse(rsql, rsqlQueryFieldType));
database)
.specification(RsqlParser.parse(
RsqlConfigHolder.getInstance().isCaseInsensitiveDB() || RsqlConfigHolder.getInstance().isIgnoreCase()
? rsql.toLowerCase() : rsql,
rsqlQueryFieldType));
} else {
return new SpecificationBuilderLegacy<A, T>(rsqlQueryFieldType, virtualPropertyReplacer, database).specification(rsql);
}
@@ -113,7 +105,7 @@ public final class RSQLUtility {
* @throws RSQLParserException if RSQL syntax is invalid
* @throws RSQLParameterUnsupportedFieldException if RSQL key is not allowed
*/
@SuppressWarnings({"unchecked", "rawtypes"})
@SuppressWarnings({ "unchecked", "rawtypes" })
public static <A extends Enum<A> & RsqlQueryField> void validateRsqlFor(
final String rsql, final Class<A> rsqlQueryFieldType,
final Class<?> jpaType,
@@ -121,6 +113,6 @@ public final class RSQLUtility {
final CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
final CriteriaQuery<?> criteriaQuery = criteriaBuilder.createQuery(jpaType);
buildRsqlSpecification(rsql, rsqlQueryFieldType, virtualPropertyReplacer, null)
.toPredicate(criteriaQuery.from((Class)jpaType), criteriaQuery, criteriaBuilder);
.toPredicate(criteriaQuery.from((Class) jpaType), criteriaQuery, criteriaBuilder);
}
}

View File

@@ -45,7 +45,6 @@ import org.eclipse.hawkbit.repository.RsqlQueryField;
import org.eclipse.hawkbit.repository.exception.RSQLParameterSyntaxException;
import org.eclipse.hawkbit.repository.exception.RSQLParameterUnsupportedFieldException;
import org.eclipse.hawkbit.repository.jpa.rsql.Node.Comparison;
import org.eclipse.hawkbit.repository.rsql.RsqlConfigHolder;
/**
* {@link RsqlParser} parses RSQL query stings to {@link Node} objects. Doing that it does the following:
@@ -82,15 +81,13 @@ public class RsqlParser {
}
public static <T extends Enum<T> & RsqlQueryField> Node parse(final String rsql, final Class<T> rsqlQueryFieldType) {
return parse(rsql, key -> resolveKey(key, rsqlQueryFieldType));
return parse(rsql, rsqlQueryFieldType == null ? null : key -> resolveKey(key, rsqlQueryFieldType));
}
private static Node parse(final String rsql, final Function<String, Key> keyResolver) {
try {
return PARSER
.parse(RsqlConfigHolder.getInstance().isCaseInsensitiveDB() || RsqlConfigHolder.getInstance().isIgnoreCase()
? rsql.toLowerCase()
: rsql)
.parse(rsql)
.accept(new RsqlVisitor(keyResolver));
} catch (final RSQLParserException e) {
throw new RSQLParameterSyntaxException(e);

View File

@@ -152,11 +152,9 @@ public class SpecificationBuilder<T> {
} else {
final MapJoin<?, ?, ?> mapPath = (MapJoin<?, ?, ?>) pathResolver.getPath(attribute);
final Path<String> valuePath = (Path<String>) mapPath.value();
return cb.and(
equal(mapPath.key(), split[1]),
isNot(op)
? notEqualInLike(comparison, (PluralAttribute<?, ?, ?>) attribute, null)
: compare(comparison, valuePath));
return isNot(op)
? compare(comparison, pathResolver.getJoinOnInner(attribute, split[1]))
: cb.and(equal(mapPath.key(), split[1]), compare(comparison, valuePath));
}
} else if (attribute instanceof SetAttribute<?, ?> setAttribute) {
if (split.length < 2 || ObjectUtils.isEmpty(split[1])) {
@@ -413,6 +411,10 @@ public class SpecificationBuilder<T> {
return getCollectionPathResolver(attribute.getName()).getJoinOn(value);
}
private MapJoin<?, ?, ?> getJoinOnInner(final Attribute<?, ?> attribute, final Object value) {
return getCollectionPathResolver(attribute.getName()).getJoinOnInner(value);
}
private void reset() {
attributeToPathResolver.values().forEach(CollectionPathResolver::reset);
}
@@ -428,6 +430,7 @@ public class SpecificationBuilder<T> {
private final List<Path<?>> paths = new ArrayList<>();
private int pos;
private final Map<Object, MapJoin<?, ?, ?>> joinOnCache = new HashMap<>();
private final Map<Object, MapJoin<?, ?, ?>> joinOnInnerCache = new HashMap<>();
private CollectionPathResolver(final String attributeName) {
this.attributeName = attributeName;
@@ -452,6 +455,14 @@ public class SpecificationBuilder<T> {
});
}
private MapJoin<?, ?, ?> getJoinOnInner(final Object value) {
return joinOnInnerCache.computeIfAbsent(value, k -> {
final MapJoin<?, ?, ?> mapPath = (MapJoin<?, ?, ?>) root.join(attributeName, JoinType.INNER);
mapPath.on(equal(mapPath.key(), k));
return mapPath;
});
}
private void reset() {
pos = 0;
}