Remove legacy RSQL to Specification builders (#2801)
Signed-off-by: Avgustin Marinov <Avgustin.Marinov@bosch.com>
This commit is contained in:
@@ -21,7 +21,6 @@ import lombok.AccessLevel;
|
|||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import lombok.NoArgsConstructor;
|
import lombok.NoArgsConstructor;
|
||||||
import lombok.NonNull;
|
import lombok.NonNull;
|
||||||
import lombok.Setter;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.apache.commons.lang3.text.StrLookup;
|
import org.apache.commons.lang3.text.StrLookup;
|
||||||
import org.eclipse.hawkbit.repository.QueryField;
|
import org.eclipse.hawkbit.repository.QueryField;
|
||||||
@@ -29,7 +28,6 @@ import org.eclipse.hawkbit.repository.exception.QueryException;
|
|||||||
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.jpa.ql.Node.Comparison;
|
import org.eclipse.hawkbit.repository.jpa.ql.Node.Comparison;
|
||||||
import org.eclipse.hawkbit.repository.jpa.rsql.legacy.SpecificationBuilderLegacy;
|
|
||||||
import org.eclipse.hawkbit.repository.rsql.VirtualPropertyResolver;
|
import org.eclipse.hawkbit.repository.rsql.VirtualPropertyResolver;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
@@ -80,12 +78,6 @@ public class QLSupport implements ApplicationListener<ContextRefreshedEvent> {
|
|||||||
|
|
||||||
private static final QLSupport SINGLETON = new QLSupport();
|
private static final QLSupport SINGLETON = new QLSupport();
|
||||||
|
|
||||||
public enum SpecBuilder {
|
|
||||||
LEGACY_G1, // legacy RSQL visitor
|
|
||||||
LEGACY_G2, // G2 RSQL visitor
|
|
||||||
G3 // G3 specification builder - still experimental / yet default
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* If QL comparison operators shall ignore the case. If ignore case is <code>true</code> "x == ax" will match "x == aX"
|
* If QL comparison operators shall ignore the case. If ignore case is <code>true</code> "x == ax" will match "x == aX"
|
||||||
*/
|
*/
|
||||||
@@ -101,15 +93,6 @@ public class QLSupport implements ApplicationListener<ContextRefreshedEvent> {
|
|||||||
@Value("${hawkbit.ql.case-insensitive-db:false}")
|
@Value("${hawkbit.ql.case-insensitive-db:false}")
|
||||||
private boolean caseInsensitiveDB;
|
private boolean caseInsensitiveDB;
|
||||||
|
|
||||||
/**
|
|
||||||
* @deprecated in favour fixed final visitor / spec builder of G2 RSQL visitor / G3 spec builder. since 0.6.0
|
|
||||||
*/
|
|
||||||
@Setter // for tests only
|
|
||||||
@Deprecated(forRemoval = true, since = "0.6.0")
|
|
||||||
@Value("${hawkbit.ql.spec-builder:G3}") //
|
|
||||||
private SpecBuilder specBuilder;
|
|
||||||
|
|
||||||
@SuppressWarnings({ "rawtypes", "unchecked" })
|
|
||||||
private QueryParser parser;
|
private QueryParser parser;
|
||||||
private List<NodeTransformer> nodeTransformers;
|
private List<NodeTransformer> nodeTransformers;
|
||||||
private EntityManager entityManager;
|
private EntityManager entityManager;
|
||||||
@@ -166,12 +149,8 @@ public class QLSupport implements ApplicationListener<ContextRefreshedEvent> {
|
|||||||
* @throws RSQLParameterSyntaxException if the RSQL syntax is wrong
|
* @throws RSQLParameterSyntaxException if the RSQL syntax is wrong
|
||||||
*/
|
*/
|
||||||
public <A extends Enum<A> & QueryField, T> Specification<T> buildSpec(final String query, final Class<A> queryFieldType) {
|
public <A extends Enum<A> & QueryField, T> Specification<T> buildSpec(final String query, final Class<A> queryFieldType) {
|
||||||
if (specBuilder == SpecBuilder.G3) {
|
return new SpecificationBuilder<T>(ignoreCase && !caseInsensitiveDB)
|
||||||
return new SpecificationBuilder<T>(ignoreCase && !caseInsensitiveDB)
|
|
||||||
.specification(transform(parse(query, queryFieldType), queryFieldType));
|
.specification(transform(parse(query, queryFieldType), queryFieldType));
|
||||||
} else {
|
|
||||||
return new SpecificationBuilderLegacy<A, T>(queryFieldType, virtualPropertyResolver).specification(query);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public <A extends Enum<A> & QueryField, T> Specification<T> buildSpec(final Node query, final Class<A> queryFieldType) {
|
public <A extends Enum<A> & QueryField, T> Specification<T> buildSpec(final Node query, final Class<A> queryFieldType) {
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ import jakarta.persistence.criteria.CriteriaQuery;
|
|||||||
|
|
||||||
import org.eclipse.hawkbit.repository.QueryField;
|
import org.eclipse.hawkbit.repository.QueryField;
|
||||||
import org.eclipse.hawkbit.repository.jpa.ql.QLSupport;
|
import org.eclipse.hawkbit.repository.jpa.ql.QLSupport;
|
||||||
import org.eclipse.hawkbit.repository.jpa.ql.QLSupport.SpecBuilder;
|
|
||||||
|
|
||||||
public class HawkbitQlToSql {
|
public class HawkbitQlToSql {
|
||||||
|
|
||||||
@@ -31,9 +30,8 @@ public class HawkbitQlToSql {
|
|||||||
isEclipselink = entityManager.getProperties().keySet().stream().anyMatch(key -> key.startsWith("eclipselink."));
|
isEclipselink = entityManager.getProperties().keySet().stream().anyMatch(key -> key.startsWith("eclipselink."));
|
||||||
}
|
}
|
||||||
|
|
||||||
public <T, A extends Enum<A> & QueryField> String toSQL(
|
public <T, A extends Enum<A> & QueryField> String toSQL(final Class<T> domainClass, final Class<A> fieldsClass, final String rsql) {
|
||||||
final Class<T> domainClass, final Class<A> fieldsClass, final String rsql, final SpecBuilder rsqlToSpecBuilder) {
|
final CriteriaQuery<T> query = createQuery(domainClass, fieldsClass, rsql);
|
||||||
final CriteriaQuery<T> query = createQuery(domainClass, fieldsClass, rsql, rsqlToSpecBuilder);
|
|
||||||
final TypedQuery<?> typedQuery = entityManager.createQuery(query);
|
final TypedQuery<?> typedQuery = entityManager.createQuery(query);
|
||||||
if (isEclipselink) {
|
if (isEclipselink) {
|
||||||
try {
|
try {
|
||||||
@@ -55,20 +53,10 @@ public class HawkbitQlToSql {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private <T, A extends Enum<A> & QueryField> CriteriaQuery<T> createQuery(
|
private <T, A extends Enum<A> & QueryField> CriteriaQuery<T> createQuery(
|
||||||
final Class<T> domainClass, final Class<A> fieldsClass, final String rsql, final SpecBuilder rsqlToSpecBuilder) {
|
final Class<T> domainClass, final Class<A> fieldsClass, final String rsql) {
|
||||||
final CriteriaQuery<T> query = entityManager.getCriteriaBuilder().createQuery(domainClass);
|
final CriteriaQuery<T> query = entityManager.getCriteriaBuilder().createQuery(domainClass);
|
||||||
final CriteriaBuilder cb = entityManager.getCriteriaBuilder();
|
final CriteriaBuilder cb = entityManager.getCriteriaBuilder();
|
||||||
final SpecBuilder defaultRsqlToSpecBuilder = QLSupport.getInstance().getSpecBuilder();
|
return query.where(QLSupport.getInstance().<A, T> buildSpec(rsql, fieldsClass)
|
||||||
if (defaultRsqlToSpecBuilder != rsqlToSpecBuilder) {
|
.toPredicate(query.from(domainClass), cb.createQuery(domainClass), cb));
|
||||||
QLSupport.getInstance().setSpecBuilder(rsqlToSpecBuilder);
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
return query.where(QLSupport.getInstance().<A, T> buildSpec(rsql, fieldsClass)
|
|
||||||
.toPredicate(query.from(domainClass), cb.createQuery(domainClass), cb));
|
|
||||||
} finally {
|
|
||||||
if (defaultRsqlToSpecBuilder != rsqlToSpecBuilder) {
|
|
||||||
QLSupport.getInstance().setSpecBuilder(defaultRsqlToSpecBuilder);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,189 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright (c) 2025 Contributors to the Eclipse Foundation
|
|
||||||
*
|
|
||||||
* 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.jpa.rsql.legacy;
|
|
||||||
|
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.HashSet;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Set;
|
|
||||||
import java.util.stream.Stream;
|
|
||||||
|
|
||||||
import jakarta.validation.constraints.NotNull;
|
|
||||||
|
|
||||||
import cz.jirutka.rsql.parser.ast.ComparisonNode;
|
|
||||||
import cz.jirutka.rsql.parser.ast.ComparisonOperator;
|
|
||||||
import cz.jirutka.rsql.parser.ast.RSQLOperators;
|
|
||||||
import lombok.Value;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
import org.eclipse.hawkbit.repository.QueryField;
|
|
||||||
import org.eclipse.hawkbit.repository.exception.RSQLParameterUnsupportedFieldException;
|
|
||||||
import org.eclipse.hawkbit.repository.jpa.ql.SpecificationBuilder;
|
|
||||||
import org.springframework.util.ObjectUtils;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @deprecated Old implementation of RSQL Visitor (G2). Deprecated in favour of next gen implementation - {@link SpecificationBuilder}.
|
|
||||||
*/
|
|
||||||
@Deprecated(forRemoval = true, since = "0.9.0")
|
|
||||||
@Slf4j
|
|
||||||
public abstract class AbstractRSQLVisitor<A extends Enum<A> & QueryField> {
|
|
||||||
|
|
||||||
static final ComparisonOperator IS = new ComparisonOperator("=is=", "=eq=");
|
|
||||||
static final ComparisonOperator NOT = new ComparisonOperator("=not=", "=ne=");
|
|
||||||
|
|
||||||
static final Set<ComparisonOperator> OPERATORS;
|
|
||||||
|
|
||||||
static {
|
|
||||||
final Set<ComparisonOperator> operators = new HashSet<>(RSQLOperators.defaultOperators());
|
|
||||||
// == and != alternatives just treating "null" string as null not as a "null"
|
|
||||||
operators.add(IS);
|
|
||||||
operators.add(NOT);
|
|
||||||
OPERATORS = Collections.unmodifiableSet(operators);
|
|
||||||
}
|
|
||||||
|
|
||||||
private final Class<A> rsqlQueryFieldType;
|
|
||||||
|
|
||||||
protected AbstractRSQLVisitor(final Class<A> rsqlQueryFieldType) {
|
|
||||||
this.rsqlQueryFieldType = rsqlQueryFieldType;
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressWarnings("java:S1066") // java:S1066 - more readable with separate "if" statements
|
|
||||||
protected QueryPath getQueryPath(final ComparisonNode node) {
|
|
||||||
final int firstSeparatorIndex = node.getSelector().indexOf(QueryField.SUB_ATTRIBUTE_SEPARATOR);
|
|
||||||
final String enumName = (firstSeparatorIndex == -1
|
|
||||||
? node.getSelector()
|
|
||||||
: node.getSelector().substring(0, firstSeparatorIndex)).toUpperCase();
|
|
||||||
log.debug("Get field identifier by name {} of enum type {}", enumName, rsqlQueryFieldType);
|
|
||||||
|
|
||||||
try {
|
|
||||||
final A enumValue = Enum.valueOf(rsqlQueryFieldType, enumName);
|
|
||||||
String[] split = getSplit(enumValue, node.getSelector());
|
|
||||||
|
|
||||||
// validate
|
|
||||||
if (!enumValue.isMap()) {
|
|
||||||
// sub entity need minimum 1 dot
|
|
||||||
if (!enumValue.getSubEntityAttributes().isEmpty() && split.length < 2) {
|
|
||||||
final String defaultSubEntityAttribute = enumValue.getDefaultSubEntityAttribute();
|
|
||||||
if (defaultSubEntityAttribute != null) { // single sub attribute - so add is as a default
|
|
||||||
split = new String[] { split[0], defaultSubEntityAttribute };
|
|
||||||
} else {
|
|
||||||
throw createRSQLParameterUnsupportedException(node, null);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// validate and normalize (replace enum name with JPA entity field name and format the rest) prepare jpa path
|
|
||||||
split[0] = enumValue.getJpaEntityFieldName();
|
|
||||||
for (int i = 1; i < split.length; i++) {
|
|
||||||
split[i] = getFormattedSubEntityAttribute(enumValue, split[i]);
|
|
||||||
|
|
||||||
if (!containsSubEntityAttribute(enumValue, split[i])) {
|
|
||||||
if (i != split.length - 1 || !enumValue.isMap()) {
|
|
||||||
throw createRSQLParameterUnsupportedException(node, null);
|
|
||||||
} // otherwise - the key of map is not in the sub entity attributes
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return new QueryPath(enumValue, split);
|
|
||||||
} catch (final IllegalArgumentException e) {
|
|
||||||
throw createRSQLParameterUnsupportedException(node, e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private String[] getSplit(final A enumValue, final String rsqlFieldName) {
|
|
||||||
if (enumValue.isMap()) {
|
|
||||||
final String[] split = rsqlFieldName.split(QueryField.SUB_ATTRIBUTE_SPLIT_REGEX, 2);
|
|
||||||
if (split.length != 2 || ObjectUtils.isEmpty(split[1])) {
|
|
||||||
throw new RSQLParameterUnsupportedFieldException(
|
|
||||||
"The syntax of the given map search parameter field {" + rsqlFieldName + "} is wrong. Syntax is: <enum name>.<key name>");
|
|
||||||
}
|
|
||||||
return split;
|
|
||||||
} else {
|
|
||||||
return rsqlFieldName.split(QueryField.SUB_ATTRIBUTE_SPLIT_REGEX);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Contains the sub entity the given field.
|
|
||||||
*
|
|
||||||
* @param propertyField the given field
|
|
||||||
* @return <code>true</code> contains <code>false</code> contains not
|
|
||||||
*/
|
|
||||||
private boolean containsSubEntityAttribute(final A enumField, final String propertyField) {
|
|
||||||
final List<String> subEntityAttributes = enumField.getSubEntityAttributes();
|
|
||||||
if (subEntityAttributes.contains(propertyField)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (final String attribute : subEntityAttributes) {
|
|
||||||
final String[] graph = attribute.split(QueryField.SUB_ATTRIBUTE_SPLIT_REGEX);
|
|
||||||
for (final String subAttribute : graph) {
|
|
||||||
if (subAttribute.equalsIgnoreCase(propertyField)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param node current processing node
|
|
||||||
* @param rootException in case there is a cause otherwise {@code null}
|
|
||||||
* @return Exception with prepared message extracted from the comparison node.
|
|
||||||
*/
|
|
||||||
private RSQLParameterUnsupportedFieldException createRSQLParameterUnsupportedException(
|
|
||||||
@NotNull final ComparisonNode node, final Exception rootException) {
|
|
||||||
return new RSQLParameterUnsupportedFieldException(String.format(
|
|
||||||
"The given search parameter field {%s} does not exist, must be one of the following fields %s",
|
|
||||||
node.getSelector(), getExpectedFieldList()), rootException);
|
|
||||||
}
|
|
||||||
|
|
||||||
private String getFormattedSubEntityAttribute(final A propertyEnum, final String propertyField) {
|
|
||||||
return propertyEnum.getSubEntityAttributes().stream()
|
|
||||||
.filter(attr -> attr.equalsIgnoreCase(propertyField))
|
|
||||||
.findFirst().orElse(propertyField);
|
|
||||||
}
|
|
||||||
|
|
||||||
private List<String> getExpectedFieldList() {
|
|
||||||
return Stream.concat(
|
|
||||||
Arrays.stream(rsqlQueryFieldType.getEnumConstants())
|
|
||||||
.filter(enumField -> enumField.getSubEntityAttributes().isEmpty()).map(enumField -> {
|
|
||||||
final String enumFieldName = enumField.name().toLowerCase();
|
|
||||||
if (enumField.isMap()) {
|
|
||||||
return enumFieldName + QueryField.SUB_ATTRIBUTE_SEPARATOR + "keyName";
|
|
||||||
} else {
|
|
||||||
return enumFieldName;
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
Arrays.stream(rsqlQueryFieldType.getEnumConstants())
|
|
||||||
.filter(enumField -> !enumField.getSubEntityAttributes().isEmpty()).flatMap(enumField -> {
|
|
||||||
final List<String> subEntity = enumField
|
|
||||||
.getSubEntityAttributes().stream().map(fieldName -> enumField.name().toLowerCase()
|
|
||||||
+ QueryField.SUB_ATTRIBUTE_SEPARATOR + fieldName)
|
|
||||||
.toList();
|
|
||||||
|
|
||||||
return subEntity.stream();
|
|
||||||
}))
|
|
||||||
.toList();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Value
|
|
||||||
protected class QueryPath {
|
|
||||||
|
|
||||||
A enumValue;
|
|
||||||
String[] jpaPath;
|
|
||||||
|
|
||||||
private QueryPath(final A enumValue, final String[] jpaPath) {
|
|
||||||
this.enumValue = enumValue;
|
|
||||||
this.jpaPath = jpaPath;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,563 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright (c) 2025 Contributors to the Eclipse Foundation
|
|
||||||
*
|
|
||||||
* 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.jpa.rsql.legacy;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.HashSet;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.Optional;
|
|
||||||
import java.util.Set;
|
|
||||||
import java.util.function.BiFunction;
|
|
||||||
import java.util.function.Function;
|
|
||||||
|
|
||||||
import jakarta.persistence.criteria.CriteriaBuilder;
|
|
||||||
import jakarta.persistence.criteria.CriteriaQuery;
|
|
||||||
import jakarta.persistence.criteria.Expression;
|
|
||||||
import jakarta.persistence.criteria.From;
|
|
||||||
import jakarta.persistence.criteria.Join;
|
|
||||||
import jakarta.persistence.criteria.JoinType;
|
|
||||||
import jakarta.persistence.criteria.MapJoin;
|
|
||||||
import jakarta.persistence.criteria.Path;
|
|
||||||
import jakarta.persistence.criteria.PluralJoin;
|
|
||||||
import jakarta.persistence.criteria.Predicate;
|
|
||||||
import jakarta.persistence.criteria.Root;
|
|
||||||
import jakarta.persistence.criteria.Subquery;
|
|
||||||
|
|
||||||
import cz.jirutka.rsql.parser.ast.AndNode;
|
|
||||||
import cz.jirutka.rsql.parser.ast.ComparisonNode;
|
|
||||||
import cz.jirutka.rsql.parser.ast.LogicalNode;
|
|
||||||
import cz.jirutka.rsql.parser.ast.Node;
|
|
||||||
import cz.jirutka.rsql.parser.ast.OrNode;
|
|
||||||
import cz.jirutka.rsql.parser.ast.RSQLVisitor;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
import org.apache.commons.lang3.math.NumberUtils;
|
|
||||||
import org.eclipse.hawkbit.repository.QueryField;
|
|
||||||
import org.eclipse.hawkbit.repository.exception.RSQLParameterSyntaxException;
|
|
||||||
import org.eclipse.hawkbit.repository.exception.RSQLParameterUnsupportedFieldException;
|
|
||||||
import org.eclipse.hawkbit.repository.rsql.VirtualPropertyResolver;
|
|
||||||
import org.springframework.beans.SimpleTypeConverter;
|
|
||||||
import org.springframework.beans.TypeMismatchException;
|
|
||||||
import org.springframework.util.CollectionUtils;
|
|
||||||
import org.springframework.util.ObjectUtils;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* An implementation of the {@link RSQLVisitor} to visit the parsed tokens and
|
|
||||||
* build JPA where clauses.
|
|
||||||
*
|
|
||||||
* @param <A> the enum for providing the field name of the entity field to filter on.
|
|
||||||
* @param <T> the entity type referenced by the root
|
|
||||||
* @deprecated Old implementation of RSQL Visitor. Deprecated in favour of next gen implementation - {@link JpaQueryRsqlVisitorG2}.
|
|
||||||
* It will be kept for some time in order to keep backward compatibility and to allow for a smooth transition. Also, in case of
|
|
||||||
* problems with the new implementation, this one can be used as a fallback.
|
|
||||||
*/
|
|
||||||
@Deprecated(forRemoval = true, since = "0.6.0")
|
|
||||||
@Slf4j
|
|
||||||
public class JpaQueryRsqlVisitor<A extends Enum<A> & QueryField, T> extends AbstractRSQLVisitor<A>
|
|
||||||
implements RSQLVisitor<List<Predicate>, String> {
|
|
||||||
|
|
||||||
public static final Character LIKE_WILDCARD = '*';
|
|
||||||
private static final char ESCAPE_CHAR = '\\';
|
|
||||||
private static final List<String> NO_JOINS_OPERATOR = List.of("!=", "=out=");
|
|
||||||
private static final String ESCAPE_CHAR_WITH_ASTERISK = ESCAPE_CHAR + "*";
|
|
||||||
|
|
||||||
private final Map<Integer, Set<Join<Object, Object>>> joinsInLevel = new HashMap<>(3);
|
|
||||||
|
|
||||||
private final CriteriaBuilder cb;
|
|
||||||
private final CriteriaQuery<?> query;
|
|
||||||
private final boolean ensureIgnoreCase;
|
|
||||||
private final Root<T> root;
|
|
||||||
private final VirtualPropertyResolver virtualPropertyResolver;
|
|
||||||
private final SimpleTypeConverter simpleTypeConverter;
|
|
||||||
|
|
||||||
private int level;
|
|
||||||
private boolean isOrLevel;
|
|
||||||
private boolean joinsNeeded;
|
|
||||||
|
|
||||||
public JpaQueryRsqlVisitor(
|
|
||||||
final Root<T> root, final CriteriaBuilder cb, final Class<A> enumType,
|
|
||||||
final VirtualPropertyResolver virtualPropertyResolver,
|
|
||||||
final CriteriaQuery<?> query, final boolean ensureIgnoreCase) {
|
|
||||||
super(enumType);
|
|
||||||
this.root = root;
|
|
||||||
this.cb = cb;
|
|
||||||
this.query = query;
|
|
||||||
this.virtualPropertyResolver = virtualPropertyResolver;
|
|
||||||
this.simpleTypeConverter = new SimpleTypeConverter();
|
|
||||||
this.ensureIgnoreCase = ensureIgnoreCase;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public List<Predicate> visit(final AndNode node, final String param) {
|
|
||||||
beginLevel(false);
|
|
||||||
final List<Predicate> childs = acceptChildren(node);
|
|
||||||
endLevel();
|
|
||||||
if (!childs.isEmpty()) {
|
|
||||||
return toSingleList(cb.and(childs.toArray(new Predicate[0])));
|
|
||||||
}
|
|
||||||
return toSingleList(cb.conjunction());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public List<Predicate> visit(final OrNode node, final String param) {
|
|
||||||
beginLevel(true);
|
|
||||||
final List<Predicate> childs = acceptChildren(node);
|
|
||||||
endLevel();
|
|
||||||
if (!childs.isEmpty()) {
|
|
||||||
return toSingleList(cb.or(childs.toArray(new Predicate[0])));
|
|
||||||
}
|
|
||||||
return toSingleList(cb.conjunction());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
// Exception squid:S2095 - see
|
|
||||||
// https://jira.sonarsource.com/browse/SONARJAVA-1478
|
|
||||||
@SuppressWarnings({ "squid:S2095" })
|
|
||||||
public List<Predicate> visit(final ComparisonNode node, final String param) {
|
|
||||||
final QueryPath queryPath = getQueryPath(node);
|
|
||||||
|
|
||||||
final List<String> values = node.getArguments();
|
|
||||||
final List<Object> transformedValues = new ArrayList<>();
|
|
||||||
final Path<Object> fieldPath = getFieldPath(queryPath.getEnumValue(), queryPath);
|
|
||||||
|
|
||||||
for (final String value : values) {
|
|
||||||
transformedValues.add(convertValueIfNecessary(node, queryPath.getEnumValue(), value, fieldPath));
|
|
||||||
}
|
|
||||||
|
|
||||||
this.joinsNeeded = this.joinsNeeded || areJoinsNeeded(node);
|
|
||||||
|
|
||||||
return mapToPredicate(node, fieldPath, node.getArguments(), transformedValues, queryPath);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static List<Predicate> toSingleList(final Predicate predicate) {
|
|
||||||
return Collections.singletonList(predicate);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Optional<Path<?>> getFieldPath(final Root<?> root, final String[] split, final boolean isMapKeyField,
|
|
||||||
final BiFunction<Path<?>, String, Path<?>> joinFieldPathProvider) {
|
|
||||||
Path<?> fieldPath = null;
|
|
||||||
for (int i = 0; i < split.length; i++) {
|
|
||||||
if (!(isMapKeyField && i == (split.length - 1))) {
|
|
||||||
final String fieldNameSplit = split[i];
|
|
||||||
fieldPath = (fieldPath != null) ? fieldPath.get(fieldNameSplit) : root.get(fieldNameSplit);
|
|
||||||
fieldPath = joinFieldPathProvider.apply(fieldPath, fieldNameSplit);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return Optional.ofNullable(fieldPath);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static boolean areJoinsNeeded(final ComparisonNode node) {
|
|
||||||
return !NO_JOINS_OPERATOR.contains(node.getOperator().getSymbol());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Exception squid:S2095 - see
|
|
||||||
// https://jira.sonarsource.com/browse/SONARJAVA-1478
|
|
||||||
@SuppressWarnings({ "rawtypes", "unchecked", "squid:S2095" })
|
|
||||||
private static Object transformEnumValue(final ComparisonNode node, final String value, final Class<?> javaType) {
|
|
||||||
final Class<? extends Enum> tmpEnumType = (Class<? extends Enum>) javaType;
|
|
||||||
try {
|
|
||||||
return Enum.valueOf(tmpEnumType, value.toUpperCase());
|
|
||||||
} catch (final IllegalArgumentException e) {
|
|
||||||
// we could not transform the given string value into the enum
|
|
||||||
// type, so ignore it and return null and do not filter
|
|
||||||
log.info("given value {} cannot be transformed into the correct enum type {}", value.toUpperCase(),
|
|
||||||
javaType);
|
|
||||||
log.debug("value cannot be transformed to an enum", e);
|
|
||||||
|
|
||||||
throw new RSQLParameterUnsupportedFieldException("field {" + node.getSelector()
|
|
||||||
+ "} must be one of the following values {" + Arrays.stream(tmpEnumType.getEnumConstants())
|
|
||||||
.map(v -> v.name().toLowerCase()).toList()
|
|
||||||
+ "}", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static boolean isSimpleField(final String[] split, final boolean isMapKeyField) {
|
|
||||||
return split.length == 1 || (split.length == 2 && isMapKeyField);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Path<?> getInnerFieldPath(final Root<?> subqueryRoot, final String[] split,
|
|
||||||
final boolean isMapKeyField) {
|
|
||||||
return getFieldPath(subqueryRoot, split, isMapKeyField,
|
|
||||||
(fieldPath, fieldNameSplit) -> getInnerJoinFieldPath(subqueryRoot, fieldPath, fieldNameSplit))
|
|
||||||
.orElseThrow(() -> new RSQLParameterUnsupportedFieldException("RSQL field path cannot be empty",
|
|
||||||
null));
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Path<?> getInnerJoinFieldPath(final Root<?> subqueryRoot, final Path<?> fieldPath,
|
|
||||||
final String fieldNameSplit) {
|
|
||||||
if (fieldPath instanceof Join) {
|
|
||||||
return subqueryRoot.join(fieldNameSplit, JoinType.INNER);
|
|
||||||
}
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
|
||||||
private static <Y> Path<Y> pathOfString(final Path<?> path) {
|
|
||||||
return (Path<Y>) path;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void beginLevel(final boolean isOr) {
|
|
||||||
level++;
|
|
||||||
isOrLevel = isOr;
|
|
||||||
joinsInLevel.put(level, new HashSet<>(2));
|
|
||||||
}
|
|
||||||
|
|
||||||
private void endLevel() {
|
|
||||||
joinsInLevel.remove(level);
|
|
||||||
level--;
|
|
||||||
isOrLevel = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private Set<Join<Object, Object>> getCurrentJoins() {
|
|
||||||
if (level > 0) {
|
|
||||||
return joinsInLevel.get(level);
|
|
||||||
}
|
|
||||||
return Collections.emptySet();
|
|
||||||
}
|
|
||||||
|
|
||||||
private Optional<Join<Object, Object>> findCurrentJoinOfType(final Class<?> type) {
|
|
||||||
return getCurrentJoins().stream().filter(j -> type.equals(j.getJavaType())).findAny();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void addCurrentJoin(final Join<Object, Object> join) {
|
|
||||||
if (level > 0) {
|
|
||||||
getCurrentJoins().add(join);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Resolves the Path for a field in the persistence layer and joins the
|
|
||||||
* required models. This operation is part of a tree traversal through an
|
|
||||||
* RSQL expression. It creates for every field that is not part of the root
|
|
||||||
* model a join to the foreign model. This behavior is optimized when
|
|
||||||
* several joins happen directly under an OR node in the traversed tree. The
|
|
||||||
* same foreign model is only joined once.
|
|
||||||
*
|
|
||||||
* Example: tags.name==M;(tags.name==A,tags.name==B,tags.name==C) This
|
|
||||||
* example joins the tags model only twice, because for the OR node in
|
|
||||||
* brackets only one join is used.
|
|
||||||
*
|
|
||||||
* @param enumField field from a FieldNameProvider to resolve on the persistence
|
|
||||||
* layer
|
|
||||||
* @param queryPath RSQL field
|
|
||||||
* @return the Path for a field
|
|
||||||
*/
|
|
||||||
@SuppressWarnings("unchecked")
|
|
||||||
private Path<Object> getFieldPath(final A enumField, final QueryPath queryPath) {
|
|
||||||
return (Path<Object>) getFieldPath(root, queryPath.getJpaPath(), enumField.isMap(),
|
|
||||||
this::getJoinFieldPath).orElseThrow(
|
|
||||||
() -> new RSQLParameterUnsupportedFieldException("RSQL field path cannot be empty", null));
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressWarnings({ "rawtypes", "unchecked" })
|
|
||||||
private Path<?> getJoinFieldPath(final Path<?> fieldPath, final String fieldNameSplit) {
|
|
||||||
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 newJoin = joinParent.join(fieldNameSplit, JoinType.LEFT);
|
|
||||||
addCurrentJoin(newJoin);
|
|
||||||
return newJoin;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return fieldPath;
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
// 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.
|
|
||||||
// So we need to transform the given value string into the enum
|
|
||||||
// class.
|
|
||||||
final Class<?> javaType = fieldPath.getJavaType();
|
|
||||||
if (javaType != null && javaType.isEnum()) {
|
|
||||||
return transformEnumValue(node, value, javaType);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Boolean.TYPE.equals(javaType)) {
|
|
||||||
return convertBooleanValue(node, value, javaType);
|
|
||||||
}
|
|
||||||
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
|
|
||||||
private Object convertBooleanValue(final ComparisonNode node, final String value, final Class<?> javaType) {
|
|
||||||
try {
|
|
||||||
return simpleTypeConverter.convertIfNecessary(value, javaType);
|
|
||||||
} catch (final TypeMismatchException e) {
|
|
||||||
throw new RSQLParameterUnsupportedFieldException(
|
|
||||||
"The value of the given search parameter field {" + node.getSelector()
|
|
||||||
+ "} is not well formed. Only a boolean (true or false) value will be expected",
|
|
||||||
e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private List<Predicate> mapToPredicate(final ComparisonNode node, final Path<Object> fieldPath,
|
|
||||||
final List<String> values, final List<Object> transformedValues, final QueryPath queryPath) {
|
|
||||||
|
|
||||||
String value = values.get(0);
|
|
||||||
// if lookup is available, replace macros ...
|
|
||||||
if (virtualPropertyResolver != null) {
|
|
||||||
value = virtualPropertyResolver.replace(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
final Predicate mapPredicate = mapToMapPredicate(fieldPath, queryPath);
|
|
||||||
|
|
||||||
final Predicate valuePredicate = addOperatorPredicate(node,
|
|
||||||
queryPath.getEnumValue().isMap() ? (Path) toMapValuePath(fieldPath) : fieldPath, transformedValues, value, queryPath);
|
|
||||||
return toSingleList(mapPredicate != null ? cb.and(mapPredicate, valuePredicate) : valuePredicate);
|
|
||||||
}
|
|
||||||
|
|
||||||
private Predicate addOperatorPredicate(final ComparisonNode node, final Path<Object> fieldPath,
|
|
||||||
final List<Object> transformedValues, final String value, final QueryPath queryPath) {
|
|
||||||
|
|
||||||
// only 'equal' and 'notEqual' can handle transformed value like
|
|
||||||
// enums. The JPA API cannot handle object types for greaterThan etc
|
|
||||||
// methods.
|
|
||||||
final Object transformedValue = transformedValues.get(0);
|
|
||||||
final String operator = node.getOperator().getSymbol();
|
|
||||||
|
|
||||||
switch (operator) {
|
|
||||||
case "==":
|
|
||||||
return getEqualToPredicate(transformedValue, fieldPath);
|
|
||||||
case "!=":
|
|
||||||
return getNotEqualToPredicate(transformedValue, fieldPath, queryPath);
|
|
||||||
case "=gt=":
|
|
||||||
return cb.greaterThan(pathOfString(fieldPath), value);
|
|
||||||
case "=ge=":
|
|
||||||
return cb.greaterThanOrEqualTo(pathOfString(fieldPath), value);
|
|
||||||
case "=lt=":
|
|
||||||
return cb.lessThan(pathOfString(fieldPath), value);
|
|
||||||
case "=le=":
|
|
||||||
return cb.lessThanOrEqualTo(pathOfString(fieldPath), value);
|
|
||||||
case "=in=":
|
|
||||||
return in(pathOfString(fieldPath), transformedValues);
|
|
||||||
case "=out=":
|
|
||||||
return getOutPredicate(transformedValues, queryPath, fieldPath);
|
|
||||||
default:
|
|
||||||
throw new RSQLParameterSyntaxException(
|
|
||||||
"operator symbol {" + operator + "} is either not supported or not implemented");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
|
||||||
private static Path<String> toMapValuePath(final Path<?> fieldPath) {
|
|
||||||
final Path<?> mapValuePath = ((MapJoin<?, ?, ?>) pathOfString(fieldPath)).value();
|
|
||||||
if (mapValuePath.getJavaType() == String.class) {
|
|
||||||
return (Path<String>) mapValuePath;
|
|
||||||
} else {
|
|
||||||
return mapValuePath.get("value");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private Predicate getOutPredicate(
|
|
||||||
final List<Object> transformedValues,
|
|
||||||
final QueryPath queryPath, final Path<Object> fieldPath) {
|
|
||||||
final String[] fieldNames = queryPath.getJpaPath();
|
|
||||||
|
|
||||||
if (isSimpleField(fieldNames, queryPath.getEnumValue().isMap())) {
|
|
||||||
final Path<String> pathOfString = pathOfString(fieldPath);
|
|
||||||
return cb.or(cb.isNull(pathOfString), cb.not(in(pathOfString, transformedValues)));
|
|
||||||
}
|
|
||||||
|
|
||||||
clearOuterJoinsIfNotNeeded();
|
|
||||||
|
|
||||||
return toNotExistsSubQueryPredicate(fieldNames, queryPath.getEnumValue(),
|
|
||||||
expressionToCompare -> in(expressionToCompare, transformedValues));
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
|
||||||
private Predicate mapToMapPredicate(final Path<Object> fieldPath, final QueryPath queryPath) {
|
|
||||||
if (!queryPath.getEnumValue().isMap()) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
final String[] graph = queryPath.getJpaPath();
|
|
||||||
final String keyValue = graph[graph.length - 1];
|
|
||||||
// Currently we support only string key. So below cast is safe.
|
|
||||||
return equal((Expression<String>) (((MapJoin<?, ?, ?>) fieldPath).key()), keyValue);
|
|
||||||
}
|
|
||||||
|
|
||||||
private Predicate getEqualToPredicate(final Object transformedValue, final Path<Object> fieldPath) {
|
|
||||||
if (transformedValue == null) {
|
|
||||||
return cb.isNull(pathOfString(fieldPath));
|
|
||||||
}
|
|
||||||
|
|
||||||
if ((transformedValue instanceof String transformedValueStr) && !NumberUtils.isCreatable(transformedValueStr)) {
|
|
||||||
if (ObjectUtils.isEmpty(transformedValue)) {
|
|
||||||
return cb.or(cb.isNull(pathOfString(fieldPath)), cb.equal(pathOfString(fieldPath), ""));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isPattern(transformedValueStr)) { // a pattern, use like
|
|
||||||
return like(pathOfString(fieldPath), toSQL(transformedValueStr));
|
|
||||||
} else {
|
|
||||||
return equal(pathOfString(fieldPath), transformedValueStr);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return cb.equal(fieldPath, transformedValue);
|
|
||||||
}
|
|
||||||
|
|
||||||
private Predicate getNotEqualToPredicate(final Object transformedValue, final Path<Object> fieldPath,
|
|
||||||
final QueryPath queryPath) {
|
|
||||||
|
|
||||||
if (transformedValue == null) {
|
|
||||||
return cb.isNotNull(pathOfString(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[] fieldNames = queryPath.getJpaPath();
|
|
||||||
|
|
||||||
if (isSimpleField(fieldNames, queryPath.getEnumValue().isMap())) {
|
|
||||||
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();
|
|
||||||
|
|
||||||
if (isPattern(transformedValueStr)) { // a pattern, use like
|
|
||||||
return toNotExistsSubQueryPredicate(fieldNames, queryPath.getEnumValue(),
|
|
||||||
expressionToCompare -> like(expressionToCompare, toSQL(transformedValueStr)));
|
|
||||||
} else {
|
|
||||||
return toNotExistsSubQueryPredicate(fieldNames, queryPath.getEnumValue(),
|
|
||||||
expressionToCompare -> equal(expressionToCompare, transformedValueStr));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return toNullOrNotEqualPredicate(fieldPath, transformedValue);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void clearOuterJoinsIfNotNeeded() {
|
|
||||||
if (!joinsNeeded) {
|
|
||||||
root.getJoins().clear();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private Predicate toNullOrNotEqualPredicate(final Path<Object> fieldPath, final Object transformedValue) {
|
|
||||||
return cb.or(
|
|
||||||
cb.isNull(pathOfString(fieldPath)),
|
|
||||||
transformedValue instanceof String transformedValueStr
|
|
||||||
? notEqual(pathOfString(fieldPath), transformedValueStr)
|
|
||||||
: cb.notEqual(fieldPath, transformedValue));
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressWarnings({ "unchecked", "rawtypes" })
|
|
||||||
private Predicate toNotExistsSubQueryPredicate(final String[] fieldNames, final A enumField,
|
|
||||||
final Function<Expression<String>, Predicate> subQueryPredicateProvider) {
|
|
||||||
final Class<?> javaType = root.getJavaType();
|
|
||||||
final Subquery<?> subquery = query.subquery(javaType);
|
|
||||||
final Root subqueryRoot = subquery.from(javaType);
|
|
||||||
final Predicate equalPredicate = cb.equal(root.get(enumField.getIdentifierFieldName()),
|
|
||||||
subqueryRoot.get(enumField.getIdentifierFieldName()));
|
|
||||||
final Path innerFieldPath = getInnerFieldPath(subqueryRoot, fieldNames, enumField.isMap());
|
|
||||||
final Expression<String> expressionToCompare = getExpressionToCompare(innerFieldPath, enumField);
|
|
||||||
final Predicate subQueryPredicate = subQueryPredicateProvider.apply(expressionToCompare);
|
|
||||||
subquery.select(subqueryRoot).where(cb.and(equalPredicate, subQueryPredicate));
|
|
||||||
return cb.not(cb.exists(subquery));
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressWarnings({ "rawtypes", "unchecked" })
|
|
||||||
private Expression<String> getExpressionToCompare(final Path innerFieldPath, final A enumField) {
|
|
||||||
if (enumField.isMap()) {
|
|
||||||
return toMapValuePath(innerFieldPath);
|
|
||||||
} else {
|
|
||||||
return pathOfString(innerFieldPath);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private String toSQL(final String transformedValue) {
|
|
||||||
final String escaped = transformedValue.replace("%", ESCAPE_CHAR + "%").replace("_", ESCAPE_CHAR + "_");
|
|
||||||
return replaceIfRequired(escaped);
|
|
||||||
}
|
|
||||||
|
|
||||||
private String replaceIfRequired(final String escapedValue) {
|
|
||||||
final String finalizedValue;
|
|
||||||
if (escapedValue.contains(ESCAPE_CHAR_WITH_ASTERISK)) {
|
|
||||||
finalizedValue = escapedValue.replace(ESCAPE_CHAR_WITH_ASTERISK, "$").replace(LIKE_WILDCARD, '%')
|
|
||||||
.replace("$", ESCAPE_CHAR_WITH_ASTERISK);
|
|
||||||
} else {
|
|
||||||
finalizedValue = escapedValue.replace(LIKE_WILDCARD, '%');
|
|
||||||
}
|
|
||||||
return finalizedValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
private List<Predicate> acceptChildren(final LogicalNode node) {
|
|
||||||
final List<Node> children = node.getChildren();
|
|
||||||
final List<Predicate> childs = new ArrayList<>();
|
|
||||||
for (final Node node2 : children) {
|
|
||||||
final List<Predicate> accept = node2.accept(this);
|
|
||||||
if (!CollectionUtils.isEmpty(accept)) {
|
|
||||||
childs.addAll(accept);
|
|
||||||
} else {
|
|
||||||
log.debug("visit logical node children but could not parse it, ignoring {}", node2);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return childs;
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressWarnings("java:S1221") // java:S1221 - intentionally to match the SQL wording
|
|
||||||
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).toList();
|
|
||||||
return inParams.isEmpty() ? expressionToCompare.in(transformedValues) : caseWise(cb, expressionToCompare).in(inParams);
|
|
||||||
}
|
|
||||||
|
|
||||||
private Expression<String> caseWise(final CriteriaBuilder cb, final Expression<String> expression) {
|
|
||||||
if (expression instanceof Path<String> path && path.getJavaType() != String.class) {
|
|
||||||
return expression; // non string values doesn't support uppercase
|
|
||||||
} else {
|
|
||||||
return ensureIgnoreCase ? cb.upper(expression) : expression;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private String caseWise(final String str) {
|
|
||||||
return ensureIgnoreCase ? str.toUpperCase() : str;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,497 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright (c) 2025 Contributors to the Eclipse Foundation
|
|
||||||
*
|
|
||||||
* 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.jpa.rsql.legacy;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.function.Function;
|
|
||||||
|
|
||||||
import jakarta.persistence.criteria.CriteriaBuilder;
|
|
||||||
import jakarta.persistence.criteria.CriteriaQuery;
|
|
||||||
import jakarta.persistence.criteria.JoinType;
|
|
||||||
import jakarta.persistence.criteria.MapJoin;
|
|
||||||
import jakarta.persistence.criteria.Path;
|
|
||||||
import jakarta.persistence.criteria.PluralJoin;
|
|
||||||
import jakarta.persistence.criteria.Predicate;
|
|
||||||
import jakarta.persistence.criteria.Root;
|
|
||||||
import jakarta.persistence.criteria.Subquery;
|
|
||||||
import jakarta.persistence.metamodel.Attribute;
|
|
||||||
import jakarta.persistence.metamodel.SingularAttribute;
|
|
||||||
import jakarta.persistence.metamodel.Type;
|
|
||||||
|
|
||||||
import cz.jirutka.rsql.parser.ast.AndNode;
|
|
||||||
import cz.jirutka.rsql.parser.ast.ComparisonNode;
|
|
||||||
import cz.jirutka.rsql.parser.ast.ComparisonOperator;
|
|
||||||
import cz.jirutka.rsql.parser.ast.LogicalNode;
|
|
||||||
import cz.jirutka.rsql.parser.ast.Node;
|
|
||||||
import cz.jirutka.rsql.parser.ast.OrNode;
|
|
||||||
import cz.jirutka.rsql.parser.ast.RSQLOperators;
|
|
||||||
import cz.jirutka.rsql.parser.ast.RSQLVisitor;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
import org.apache.commons.lang3.math.NumberUtils;
|
|
||||||
import org.eclipse.hawkbit.repository.QueryField;
|
|
||||||
import org.eclipse.hawkbit.repository.exception.RSQLParameterSyntaxException;
|
|
||||||
import org.eclipse.hawkbit.repository.exception.RSQLParameterUnsupportedFieldException;
|
|
||||||
import org.eclipse.hawkbit.repository.jpa.ql.SpecificationBuilder;
|
|
||||||
import org.eclipse.hawkbit.repository.rsql.VirtualPropertyResolver;
|
|
||||||
import org.springframework.util.CollectionUtils;
|
|
||||||
import org.springframework.util.ObjectUtils;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* An implementation of the {@link RSQLVisitor} to visit the parsed tokens and build JPA where clauses.
|
|
||||||
*
|
|
||||||
* @param <A> the enum for providing the field name of the entity field to filter on.
|
|
||||||
* @param <T> the entity type referenced by the root
|
|
||||||
* @deprecated Old implementation of RSQL Visitor (G2). Deprecated in favour of next gen implementation -
|
|
||||||
* {@link SpecificationBuilder}.
|
|
||||||
* It will be kept for some time in order to keep backward compatibility and to allow for a smooth transition. Also, in case of
|
|
||||||
* problems with the new implementation, this one can be used as a fallback.
|
|
||||||
*/
|
|
||||||
@Deprecated(forRemoval = true, since = "0.9.0")
|
|
||||||
@Slf4j
|
|
||||||
public class JpaQueryRsqlVisitorG2<A extends Enum<A> & QueryField, T>
|
|
||||||
extends AbstractRSQLVisitor<A> implements RSQLVisitor<List<Predicate>, String> {
|
|
||||||
|
|
||||||
public static final Character LIKE_WILDCARD = '*';
|
|
||||||
private static final char ESCAPE_CHAR = '\\';
|
|
||||||
private static final String ESCAPE_CHAR_WITH_ASTERISK = ESCAPE_CHAR + "*";
|
|
||||||
|
|
||||||
private final Root<T> root;
|
|
||||||
private final CriteriaQuery<?> query;
|
|
||||||
private final CriteriaBuilder cb;
|
|
||||||
private final VirtualPropertyResolver virtualPropertyResolver;
|
|
||||||
private final boolean ensureIgnoreCase;
|
|
||||||
|
|
||||||
private final Map<String, Path<?>> attributeToPath = new HashMap<>();
|
|
||||||
private boolean inOr;
|
|
||||||
|
|
||||||
public JpaQueryRsqlVisitorG2(
|
|
||||||
final Class<A> enumType,
|
|
||||||
final Root<T> root, final CriteriaQuery<?> query, final CriteriaBuilder cb,
|
|
||||||
final VirtualPropertyResolver virtualPropertyResolver, final boolean ensureIgnoreCase) {
|
|
||||||
super(enumType);
|
|
||||||
this.root = root;
|
|
||||||
this.cb = cb;
|
|
||||||
this.query = query;
|
|
||||||
this.virtualPropertyResolver = virtualPropertyResolver;
|
|
||||||
this.ensureIgnoreCase = ensureIgnoreCase;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public List<Predicate> visit(final AndNode node, final String param) {
|
|
||||||
final List<Predicate> children = acceptChildren(node);
|
|
||||||
return Collections.singletonList(children.isEmpty() ? cb.conjunction() : cb.and(children.toArray(new Predicate[0])));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public List<Predicate> visit(final OrNode node, final String param) {
|
|
||||||
inOr = true;
|
|
||||||
try {
|
|
||||||
final List<Predicate> children = acceptChildren(node);
|
|
||||||
return Collections.singletonList(children.isEmpty() ? cb.conjunction() : cb.or(children.toArray(new Predicate[0])));
|
|
||||||
} finally {
|
|
||||||
inOr = false;
|
|
||||||
attributeToPath.clear();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public List<Predicate> visit(final ComparisonNode node, final String param) {
|
|
||||||
final QueryPath queryPath = getQueryPath(node);
|
|
||||||
final Path<?> fieldPath = getFieldPath(root, queryPath);
|
|
||||||
return Collections.singletonList(toPredicate(node, queryPath, fieldPath, getValues(node, queryPath, fieldPath)));
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressWarnings("java:S3776") // java:S3776 - easier to read at one place
|
|
||||||
private Predicate toPredicate(final ComparisonNode node, final QueryPath queryPath, final Path<?> fieldPath, final List<Object> values) {
|
|
||||||
final Predicate mapEntryKeyPredicate;
|
|
||||||
if (queryPath.getEnumValue().isMap()) {
|
|
||||||
if (node.getOperator() == IS) {
|
|
||||||
// special handling of "not-exists"
|
|
||||||
if (values.size() != 1) {
|
|
||||||
throw new RSQLParameterSyntaxException("The operator '" + IS + "' can only be used with one value");
|
|
||||||
}
|
|
||||||
if (values.get(0) == null) {
|
|
||||||
// IS operator for maps and null value is treated as doesn't exist correspondingly
|
|
||||||
((PluralJoin<?, ?, ?>) fieldPath).on(toMapEntryKeyPredicate(queryPath, fieldPath));
|
|
||||||
return cb.isNull(toMapValuePath(fieldPath));
|
|
||||||
}
|
|
||||||
} else if (node.getOperator() == NOT) {
|
|
||||||
if (values.size() != 1) {
|
|
||||||
throw new RSQLParameterSyntaxException("The operator '" + NOT + "' can only be used with one value");
|
|
||||||
}
|
|
||||||
// NOT operator for maps and null value is treated as does exist correspondingly
|
|
||||||
((PluralJoin<?, ?, ?>) fieldPath).on(toMapEntryKeyPredicate(queryPath, fieldPath));
|
|
||||||
if (values.get(0) == null) {
|
|
||||||
// special handling of "exists"
|
|
||||||
return cb.isNotNull(toMapValuePath(fieldPath));
|
|
||||||
} else {
|
|
||||||
// special handling or "not equal" or null (same as != but with possible optimized join - no subquery)
|
|
||||||
return toNotEqualToPredicate(queryPath, toMapValuePath(fieldPath), values.get(0));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
mapEntryKeyPredicate = toMapEntryKeyPredicate(queryPath, fieldPath);
|
|
||||||
} else {
|
|
||||||
mapEntryKeyPredicate = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
final Predicate valuePredicate = toOperatorAndValuePredicate(node, queryPath,
|
|
||||||
queryPath.getEnumValue().isMap() ? toMapValuePath(fieldPath) : fieldPath, values);
|
|
||||||
|
|
||||||
return mapEntryKeyPredicate == null ? valuePredicate : cb.and(mapEntryKeyPredicate, valuePredicate);
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
|
||||||
private static Path<String> toMapValuePath(final Path<?> fieldPath) {
|
|
||||||
final Path<?> mapValuePath = ((MapJoin<?, ?, ?>) pathOfString(fieldPath)).value();
|
|
||||||
if (mapValuePath.getJavaType() == String.class) {
|
|
||||||
return (Path<String>) mapValuePath;
|
|
||||||
} else {
|
|
||||||
return mapValuePath.get("value");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
|
||||||
private Predicate toMapEntryKeyPredicate(final QueryPath queryPath, final Path<?> fieldPath) {
|
|
||||||
final String[] graph = queryPath.getJpaPath();
|
|
||||||
return equal((Path<String>) ((MapJoin<?, ?, ?>) fieldPath).key(), graph[graph.length - 1]);
|
|
||||||
}
|
|
||||||
|
|
||||||
private Predicate toOperatorAndValuePredicate(
|
|
||||||
final ComparisonNode node, final QueryPath queryPath, final Path<?> fieldPath, final List<Object> values) {
|
|
||||||
// only 'equal' and 'notEqual' can handle transformed value like enums.
|
|
||||||
// The JPA API cannot handle object types for greaterThan etc. methods. For them, it shall be a string.
|
|
||||||
final Object value = values.get(0);
|
|
||||||
final String operator = node.getOperator().getSymbol();
|
|
||||||
return switch (operator) {
|
|
||||||
case "==", "=is=", "=eq=" -> toEqualToPredicate(fieldPath, value);
|
|
||||||
case "!=", "=not=", "=ne=" -> toNotEqualToPredicate(queryPath, fieldPath, value);
|
|
||||||
case "=gt=" -> cb.greaterThan(pathOfString(fieldPath), String.valueOf(value)); // JPA handles numbers
|
|
||||||
case "=ge=" -> cb.greaterThanOrEqualTo(pathOfString(fieldPath), String.valueOf(value));
|
|
||||||
case "=lt=" -> cb.lessThan(pathOfString(fieldPath), String.valueOf(value));
|
|
||||||
case "=le=" -> cb.lessThanOrEqualTo(pathOfString(fieldPath), String.valueOf(value));
|
|
||||||
case "=in=" -> in(pathOfString(fieldPath), values);
|
|
||||||
case "=out=" -> toOutPredicate(queryPath, fieldPath, values);
|
|
||||||
default -> throw new RSQLParameterSyntaxException("Operator symbol {" + operator + "} is either not supported or not implemented");
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private Predicate toEqualToPredicate(final Path<?> fieldPath, final Object value) {
|
|
||||||
if (value == null) {
|
|
||||||
return cb.isNull(fieldPath);
|
|
||||||
}
|
|
||||||
|
|
||||||
if ((value instanceof String valueStr) && !NumberUtils.isCreatable(valueStr)) {
|
|
||||||
if (ObjectUtils.isEmpty(value)) {
|
|
||||||
return cb.or(cb.isNull(fieldPath), cb.equal(pathOfString(fieldPath), ""));
|
|
||||||
}
|
|
||||||
|
|
||||||
final Path<String> stringExpression = pathOfString(fieldPath);
|
|
||||||
if (isPattern(valueStr)) { // a pattern, use like
|
|
||||||
return like(stringExpression, toSQL(valueStr));
|
|
||||||
} else {
|
|
||||||
return equal(stringExpression, valueStr);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return cb.equal(fieldPath, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
// if value is null -> not null
|
|
||||||
// if value is not null -> null or not equal value
|
|
||||||
private Predicate toNotEqualToPredicate(final QueryPath queryPath, final Path<?> fieldPath, final Object value) {
|
|
||||||
if (value == null) {
|
|
||||||
return cb.isNotNull(fieldPath);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (value instanceof String valueStr && !NumberUtils.isCreatable(valueStr)) {
|
|
||||||
if (ObjectUtils.isEmpty(value)) {
|
|
||||||
return cb.and(cb.isNotNull(fieldPath), cb.notEqual(pathOfString(fieldPath), ""));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isSimpleField(queryPath)) {
|
|
||||||
if (isPattern(valueStr)) { // a pattern, use like
|
|
||||||
return cb.or(cb.isNull(fieldPath), notLike(pathOfString(fieldPath), toSQL(valueStr)));
|
|
||||||
} else {
|
|
||||||
return toNullOrNotEqualPredicate(fieldPath, valueStr);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return toNotExistsSubQueryPredicate(
|
|
||||||
queryPath, fieldPath, expressionToCompare ->
|
|
||||||
isPattern(valueStr)
|
|
||||||
? like(expressionToCompare, toSQL(valueStr)) // a pattern, use like
|
|
||||||
: equal(expressionToCompare, valueStr));
|
|
||||||
}
|
|
||||||
|
|
||||||
return toNullOrNotEqualPredicate(fieldPath, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
private Predicate toOutPredicate(final QueryPath queryPath, final Path<?> fieldPath, final List<Object> values) {
|
|
||||||
if (isSimpleField(queryPath)) {
|
|
||||||
return cb.or(cb.isNull(fieldPath), cb.not(in(pathOfString(fieldPath), values)));
|
|
||||||
}
|
|
||||||
|
|
||||||
return toNotExistsSubQueryPredicate(queryPath, fieldPath, expressionToCompare -> in(expressionToCompare, values));
|
|
||||||
}
|
|
||||||
|
|
||||||
private Path<?> getFieldPath(final Root<?> root, final QueryPath queryPath) {
|
|
||||||
final String[] split = queryPath.getJpaPath();
|
|
||||||
Path<?> fieldPath = null;
|
|
||||||
for (int i = 0, end = queryPath.getEnumValue().isMap() ? split.length - 1 : split.length; i < end; i++) {
|
|
||||||
final String fieldNameSplit = split[i];
|
|
||||||
fieldPath = fieldPath == null ? getPath(root, fieldNameSplit) : fieldPath.get(fieldNameSplit);
|
|
||||||
}
|
|
||||||
if (fieldPath == null) {
|
|
||||||
throw new RSQLParameterUnsupportedFieldException("RSQL field path must not be null", null);
|
|
||||||
}
|
|
||||||
return fieldPath;
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressWarnings("java:S3776") // java:S3776 - easier to read at one place
|
|
||||||
private List<Object> getValues(final ComparisonNode node, final AbstractRSQLVisitor<A>.QueryPath queryPath, final Path<?> fieldPath) {
|
|
||||||
final List<Object> values = node.getArguments().stream()
|
|
||||||
// if lookup is available, replace macros ...
|
|
||||||
.map(value -> virtualPropertyResolver == null ? value : virtualPropertyResolver.replace(value))
|
|
||||||
// converts value to the correct type
|
|
||||||
.map(value -> convertValueIfNecessary(node, queryPath.getEnumValue(), fieldPath, value))
|
|
||||||
.toList();
|
|
||||||
if (values.isEmpty()) {
|
|
||||||
throw new RSQLParameterSyntaxException("RSQL values must not be empty", null);
|
|
||||||
} else if (values.size() == 1) {
|
|
||||||
if (!(values.get(0) instanceof String)) { // enum or boolean or null - doesn's support >, >=, <, <=
|
|
||||||
final ComparisonOperator operator = node.getOperator();
|
|
||||||
if (operator == RSQLOperators.GREATER_THAN ||
|
|
||||||
operator == RSQLOperators.GREATER_THAN_OR_EQUAL ||
|
|
||||||
operator == RSQLOperators.LESS_THAN ||
|
|
||||||
operator == RSQLOperators.LESS_THAN_OR_EQUAL) {
|
|
||||||
final String errorMsg = values.get(0) == null ? "to null value" : "to enum or boolean field";
|
|
||||||
throw new RSQLParameterSyntaxException(operator.getSymbol() + " operator could not be applied " + errorMsg, null);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
final ComparisonOperator operator = node.getOperator();
|
|
||||||
if (operator != RSQLOperators.IN && operator != RSQLOperators.NOT_IN) {
|
|
||||||
throw new RSQLParameterSyntaxException(operator.getSymbol() + " operator shall have exactly one value", null);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return values;
|
|
||||||
}
|
|
||||||
|
|
||||||
// if root.get creates a join we call join directly in order to specify LEFT JOIN type, to include rows for missing in particular
|
|
||||||
// table / criteria (root.get creates INNER JOIN) (see org.eclipse.persistence.internal.jpa.querydef.FromImpl implementation
|
|
||||||
// for more details) otherwise delegate to root.get
|
|
||||||
@SuppressWarnings("java:S1066") // java:S1066 - better reading this way
|
|
||||||
private Path<?> getPath(final Root<?> root, final String fieldNameSplit) {
|
|
||||||
// see org.eclipse.persistence.internal.jpa.querydef.FromImpl implementation for more details when root.get creates a join
|
|
||||||
final Attribute<?, ?> attribute = root.getModel().getAttribute(fieldNameSplit);
|
|
||||||
if (!attribute.isCollection()) {
|
|
||||||
// it is a SingularAttribute and not join if it is of basic or entity persistence type
|
|
||||||
final Type.PersistenceType persistenceType = ((SingularAttribute<?, ?>) attribute).getType().getPersistenceType();
|
|
||||||
if (persistenceType == Type.PersistenceType.BASIC) {
|
|
||||||
return root.get(fieldNameSplit);
|
|
||||||
} else if (persistenceType == Type.PersistenceType.ENTITY) {
|
|
||||||
return root.getJoins().stream()
|
|
||||||
.filter(join -> join.getAttribute().equals(attribute))
|
|
||||||
.findFirst()
|
|
||||||
.orElseGet(() -> root.join(fieldNameSplit, JoinType.LEFT));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// if a collection - it is a join
|
|
||||||
if (inOr && root == this.root) { // try to reuse join of the same "or" level and no subquery
|
|
||||||
return attributeToPath.computeIfAbsent(attribute.getName(), k -> root.join(fieldNameSplit, JoinType.LEFT));
|
|
||||||
} else {
|
|
||||||
return root.join(fieldNameSplit, JoinType.LEFT);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private Predicate toNullOrNotEqualPredicate(final Path<?> fieldPath, final Object value) {
|
|
||||||
return cb.or(
|
|
||||||
cb.isNull(fieldPath),
|
|
||||||
value instanceof String valueStr ? notEqual(pathOfString(fieldPath), valueStr) : cb.notEqual(fieldPath, value));
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressWarnings({ "unchecked", "rawtypes" })
|
|
||||||
private Predicate toNotExistsSubQueryPredicate(
|
|
||||||
final QueryPath queryPath, final Path<?> fieldPath, final Function<Path<String>, Predicate> subQueryPredicateProvider) {
|
|
||||||
// if a subquery the field's parent joins are not actually used
|
|
||||||
if (!inOr) {
|
|
||||||
// so, if not in or (hence not reused) we remove them. Parent shall be a Join
|
|
||||||
root.getJoins().remove(fieldPath.getParentPath());
|
|
||||||
}
|
|
||||||
|
|
||||||
final Class<?> javaType = root.getJavaType();
|
|
||||||
final Subquery<?> subquery = query.subquery(javaType);
|
|
||||||
final Root subqueryRoot = subquery.from(javaType);
|
|
||||||
return cb.not(cb.exists(
|
|
||||||
subquery.select(subqueryRoot)
|
|
||||||
.where(cb.and(
|
|
||||||
cb.equal(
|
|
||||||
root.get(queryPath.getEnumValue().getIdentifierFieldName()),
|
|
||||||
subqueryRoot.get(queryPath.getEnumValue().getIdentifierFieldName())),
|
|
||||||
subQueryPredicateProvider.apply(
|
|
||||||
getExpressionToCompare(queryPath.getEnumValue(), getFieldPath(subqueryRoot, queryPath)))))));
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressWarnings({ "rawtypes", "unchecked" })
|
|
||||||
private Path<String> getExpressionToCompare(final A enumField, final Path fieldPath) {
|
|
||||||
if (enumField.isMap()) {
|
|
||||||
// Currently we support only string key. So below cast is safe.
|
|
||||||
return toMapValuePath(fieldPath);
|
|
||||||
} else {
|
|
||||||
return pathOfString(fieldPath);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// result is String, enum value, boolean or null
|
|
||||||
@SuppressWarnings({ "rawtypes", "unchecked" })
|
|
||||||
private Object convertValueIfNecessary(final ComparisonNode node, final A enumValue, final Path<?> fieldPath, final String value) {
|
|
||||||
// in case the value of an RSQL query is an enum we need to transform the given value to the correspondent java type object
|
|
||||||
final Class<?> javaType = fieldPath.getJavaType();
|
|
||||||
|
|
||||||
if (javaType != null && javaType.isEnum()) {
|
|
||||||
return toEnumValue(node, javaType, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (boolean.class.equals(javaType) || Boolean.class.equals(javaType)) {
|
|
||||||
if ("true".equals(value) || "false".equals(value)) {
|
|
||||||
return Boolean.valueOf(value);
|
|
||||||
} else {
|
|
||||||
throw new RSQLParameterSyntaxException(
|
|
||||||
"The value of the given search parameter field {" + node.getSelector() + "} is not well formed. " +
|
|
||||||
"Only a boolean (true or false) value will be expected");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ("null".equals(value)) {
|
|
||||||
final ComparisonOperator operator = node.getOperator();
|
|
||||||
if (operator == IS || operator == NOT) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean isSimpleField(final QueryPath queryPath) {
|
|
||||||
return queryPath.getJpaPath().length == 1 || (queryPath.getJpaPath().length == 2 && queryPath.getEnumValue().isMap());
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressWarnings({ "rawtypes", "unchecked" })
|
|
||||||
private static Object toEnumValue(final ComparisonNode node, final Class<?> javaType, final String value) {
|
|
||||||
final Class<? extends Enum> tmpEnumType = (Class<? extends Enum>) javaType;
|
|
||||||
try {
|
|
||||||
return Enum.valueOf(tmpEnumType, value.toUpperCase());
|
|
||||||
} catch (final IllegalArgumentException e) {
|
|
||||||
// we could not transform the given string value into the enum type, so ignore it and return null and do not filter
|
|
||||||
log.info("given value {} cannot be transformed into the correct enum type {}", value.toUpperCase(), javaType);
|
|
||||||
log.debug("value cannot be transformed to an enum", e);
|
|
||||||
throw new RSQLParameterUnsupportedFieldException(
|
|
||||||
"field '" + node.getSelector() + "' must be one of the following values " +
|
|
||||||
"{" + Arrays.stream(tmpEnumType.getEnumConstants()).map(Enum::name).map(String::toLowerCase).toList() + "}",
|
|
||||||
e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
|
||||||
private static Path<String> pathOfString(final Path<?> path) {
|
|
||||||
return (Path<String>) path;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static boolean isPattern(final String value) {
|
|
||||||
if (value.contains(ESCAPE_CHAR_WITH_ASTERISK)) {
|
|
||||||
return value.replace(ESCAPE_CHAR_WITH_ASTERISK, "$").indexOf(LIKE_WILDCARD) != -1;
|
|
||||||
} else {
|
|
||||||
return value.indexOf(LIKE_WILDCARD) != -1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private String toSQL(final String value) {
|
|
||||||
final String escaped = value.replace("%", ESCAPE_CHAR + "%").replace("_", ESCAPE_CHAR + "_");
|
|
||||||
return replaceIfRequired(escaped);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String replaceIfRequired(final String escapedValue) {
|
|
||||||
final String finalizedValue;
|
|
||||||
if (escapedValue.contains(ESCAPE_CHAR_WITH_ASTERISK)) {
|
|
||||||
finalizedValue = escapedValue.replace(ESCAPE_CHAR_WITH_ASTERISK, "$")
|
|
||||||
.replace(LIKE_WILDCARD, '%')
|
|
||||||
.replace("$", ESCAPE_CHAR_WITH_ASTERISK);
|
|
||||||
} else {
|
|
||||||
finalizedValue = escapedValue.replace(LIKE_WILDCARD, '%');
|
|
||||||
}
|
|
||||||
return finalizedValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
private List<Predicate> acceptChildren(final LogicalNode node) {
|
|
||||||
final List<Predicate> children = new ArrayList<>();
|
|
||||||
for (final Node child : node.getChildren()) {
|
|
||||||
final List<Predicate> accept = child.accept(this);
|
|
||||||
if (!CollectionUtils.isEmpty(accept)) {
|
|
||||||
children.addAll(accept);
|
|
||||||
} else {
|
|
||||||
log.debug("visit logical node children but could not parse it, ignoring {}", child);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return children;
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressWarnings("java:S1221") // java:S1221 - intentionally to match the SQL wording
|
|
||||||
private Predicate equal(final Path<String> expressionToCompare, final String sqlValue) {
|
|
||||||
if (caseWise(expressionToCompare)) {
|
|
||||||
return cb.equal(cb.upper(expressionToCompare), sqlValue.toUpperCase());
|
|
||||||
} else {
|
|
||||||
return cb.equal(expressionToCompare, sqlValue);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private Predicate notEqual(final Path<String> expressionToCompare, String valueStr) {
|
|
||||||
if (caseWise(expressionToCompare)) {
|
|
||||||
return cb.notEqual(cb.upper(expressionToCompare), valueStr.toUpperCase());
|
|
||||||
} else {
|
|
||||||
return cb.notEqual(expressionToCompare, valueStr);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private Predicate like(final Path<String> expressionToCompare, final String sqlValue) {
|
|
||||||
if (caseWise(expressionToCompare)) {
|
|
||||||
return cb.like(cb.upper(expressionToCompare), sqlValue.toUpperCase(), ESCAPE_CHAR);
|
|
||||||
} else {
|
|
||||||
return cb.like(expressionToCompare, sqlValue, ESCAPE_CHAR);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private Predicate notLike(final Path<String> expressionToCompare, final String sqlValue) {
|
|
||||||
if (caseWise(expressionToCompare)) {
|
|
||||||
return cb.notLike(cb.upper(expressionToCompare), sqlValue.toUpperCase(), ESCAPE_CHAR);
|
|
||||||
} else {
|
|
||||||
return cb.notLike(expressionToCompare, sqlValue, ESCAPE_CHAR);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private Predicate in(final Path<String> expressionToCompare, final List<Object> values) {
|
|
||||||
if (ensureIgnoreCase && expressionToCompare.getJavaType() == String.class) {
|
|
||||||
final List<String> inParams = values.stream()
|
|
||||||
.filter(String.class::isInstance)
|
|
||||||
.map(String.class::cast).map(String::toUpperCase).toList();
|
|
||||||
return inParams.isEmpty() ? expressionToCompare.in(values) : cb.upper(expressionToCompare).in(inParams);
|
|
||||||
} else {
|
|
||||||
return expressionToCompare.in(values);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean caseWise(final Path<?> fieldPath) {
|
|
||||||
return ensureIgnoreCase && fieldPath.getJavaType() == String.class;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,79 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright (c) 2025 Contributors to the Eclipse Foundation
|
|
||||||
*
|
|
||||||
* 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.jpa.rsql.legacy;
|
|
||||||
|
|
||||||
import static org.eclipse.hawkbit.repository.jpa.ql.QLSupport.SpecBuilder.LEGACY_G1;
|
|
||||||
import static org.eclipse.hawkbit.repository.jpa.rsql.legacy.AbstractRSQLVisitor.OPERATORS;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import jakarta.persistence.criteria.Predicate;
|
|
||||||
|
|
||||||
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.extern.slf4j.Slf4j;
|
|
||||||
import org.eclipse.hawkbit.repository.QueryField;
|
|
||||||
import org.eclipse.hawkbit.repository.exception.RSQLParameterSyntaxException;
|
|
||||||
import org.eclipse.hawkbit.repository.jpa.ql.QLSupport;
|
|
||||||
import org.eclipse.hawkbit.repository.jpa.ql.SpecificationBuilder;
|
|
||||||
import org.eclipse.hawkbit.repository.rsql.VirtualPropertyResolver;
|
|
||||||
import org.springframework.data.jpa.domain.Specification;
|
|
||||||
import org.springframework.util.CollectionUtils;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @deprecated Old implementation of RSQL Visitor (G2). Deprecated in favour of next gen implementation - {@link SpecificationBuilder}.
|
|
||||||
*/
|
|
||||||
@Deprecated(forRemoval = true, since = "0.9.0")
|
|
||||||
@Slf4j
|
|
||||||
public class SpecificationBuilderLegacy<A extends Enum<A> & QueryField, T> {
|
|
||||||
|
|
||||||
private final Class<A> rsqlQueryFieldType;
|
|
||||||
private final VirtualPropertyResolver virtualPropertyResolver;
|
|
||||||
|
|
||||||
public SpecificationBuilderLegacy(final Class<A> rsqlQueryFieldType, final VirtualPropertyResolver virtualPropertyResolver) {
|
|
||||||
this.rsqlQueryFieldType = rsqlQueryFieldType;
|
|
||||||
this.virtualPropertyResolver = virtualPropertyResolver;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Specification<T> specification(final String rsql) {
|
|
||||||
return (root, query, cb) -> {
|
|
||||||
final QLSupport qlSupport = QLSupport.getInstance();
|
|
||||||
|
|
||||||
final Node rootNode = parseRsql(rsql, qlSupport);
|
|
||||||
query.distinct(true);
|
|
||||||
|
|
||||||
final boolean ensureIgnoreCase = !qlSupport.isCaseInsensitiveDB() && qlSupport.isIgnoreCase();
|
|
||||||
final RSQLVisitor<List<Predicate>, String> jpqQueryRSQLVisitor =
|
|
||||||
qlSupport.getSpecBuilder() == LEGACY_G1
|
|
||||||
? new JpaQueryRsqlVisitor<>(root, cb, rsqlQueryFieldType, virtualPropertyResolver, query, ensureIgnoreCase)
|
|
||||||
: new JpaQueryRsqlVisitorG2<>(rsqlQueryFieldType, root, query, cb, virtualPropertyResolver, ensureIgnoreCase);
|
|
||||||
final List<Predicate> accept = rootNode.accept(jpqQueryRSQLVisitor);
|
|
||||||
|
|
||||||
if (CollectionUtils.isEmpty(accept)) {
|
|
||||||
return cb.conjunction();
|
|
||||||
} else {
|
|
||||||
return cb.and(accept.toArray(new Predicate[0]));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Node parseRsql(final String rsql, final QLSupport rsqlUtility) {
|
|
||||||
log.debug("Parsing rsql string {}", rsql);
|
|
||||||
try {
|
|
||||||
return new RSQLParser(OPERATORS).parse(rsqlUtility.isCaseInsensitiveDB() || rsqlUtility.isIgnoreCase() ? rsql.toLowerCase() : rsql);
|
|
||||||
} catch (final IllegalArgumentException e) {
|
|
||||||
throw new RSQLParameterSyntaxException("RSQL filter must not be null", e);
|
|
||||||
} catch (final RSQLParserException e) {
|
|
||||||
throw new RSQLParameterSyntaxException(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,137 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright (c) 2025 Contributors to the Eclipse Foundation
|
|
||||||
*
|
|
||||||
* 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.jpa.ql;
|
|
||||||
|
|
||||||
import static org.eclipse.hawkbit.repository.jpa.ql.QLSupport.SpecBuilder.LEGACY_G1;
|
|
||||||
import static org.eclipse.hawkbit.repository.jpa.ql.QLSupport.SpecBuilder.LEGACY_G2;
|
|
||||||
|
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import lombok.Getter;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
import org.eclipse.hawkbit.repository.QueryField;
|
|
||||||
import org.eclipse.hawkbit.repository.jpa.ql.QLSupport.SpecBuilder;
|
|
||||||
import org.eclipse.hawkbit.repository.jpa.rsql.legacy.SpecificationBuilderLegacy;
|
|
||||||
import org.junit.jupiter.api.Test;
|
|
||||||
import org.springframework.data.jpa.domain.Specification;
|
|
||||||
|
|
||||||
@SuppressWarnings("java:S2699") // java:S2699 - assertions are un the super methods that are called
|
|
||||||
@Slf4j
|
|
||||||
class SpecificationBuilderLegacyTest extends SpecificationBuilderTest {
|
|
||||||
|
|
||||||
private final SpecificationBuilderLegacy<RootField, Root> builder = new SpecificationBuilderLegacy<>(RootField.class, null);
|
|
||||||
|
|
||||||
private static void runWithRsqlToSpecBuilder(final Runnable runnable, final SpecBuilder rsqlToSpecBuilder) {
|
|
||||||
final SpecBuilder defaultBuilder = QLSupport.getInstance().getSpecBuilder();
|
|
||||||
QLSupport.getInstance().setSpecBuilder(rsqlToSpecBuilder);
|
|
||||||
try {
|
|
||||||
runnable.run();
|
|
||||||
} finally {
|
|
||||||
QLSupport.getInstance().setSpecBuilder(defaultBuilder);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void singularStringAttributeG1() {
|
|
||||||
runWithRsqlToSpecBuilder(super::singularStringAttribute, LEGACY_G1);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
@Test
|
|
||||||
void singularStringAttribute() {
|
|
||||||
runWithRsqlToSpecBuilder(super::singularStringAttribute, LEGACY_G2);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void singularIntAttributeG1() {
|
|
||||||
runWithRsqlToSpecBuilder(super::singularIntAttribute, LEGACY_G1);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
@Test
|
|
||||||
void singularIntAttribute() {
|
|
||||||
runWithRsqlToSpecBuilder(super::singularIntAttribute, LEGACY_G2);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void singularEntityAttributeG1() {
|
|
||||||
runWithRsqlToSpecBuilder(super::singularEntityAttribute, LEGACY_G1);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
@Test
|
|
||||||
void singularEntityAttribute() {
|
|
||||||
runWithRsqlToSpecBuilder(super::singularEntityAttribute, LEGACY_G2);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void pluralSubSetAttributeG1() {
|
|
||||||
runWithRsqlToSpecBuilder(super::pluralSubSetAttribute, LEGACY_G1);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
@Test
|
|
||||||
void pluralSubSetAttribute() {
|
|
||||||
runWithRsqlToSpecBuilder(super::pluralSubSetAttribute, LEGACY_G2);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Legacy G1 doesn't support hibernate maps
|
|
||||||
@Override
|
|
||||||
@Test
|
|
||||||
void pluralSubMapAttribute() {
|
|
||||||
runWithRsqlToSpecBuilder(super::pluralSubMapAttribute, LEGACY_G2);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void singularEntitySubSubAttributeG1() {
|
|
||||||
runWithRsqlToSpecBuilder(super::singularEntitySubSubAttribute, LEGACY_G1);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
@Test
|
|
||||||
void singularEntitySubSubAttribute() {
|
|
||||||
runWithRsqlToSpecBuilder(super::singularEntitySubSubAttribute, LEGACY_G2);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
@Test
|
|
||||||
void deapSearchSubSubSubSubAttribute() {
|
|
||||||
// legacy builders doesn't support deep search
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected Specification<Root> getSpecification(final String rsql) {
|
|
||||||
return builder.specification(rsql);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Getter
|
|
||||||
private enum RootField implements QueryField {
|
|
||||||
|
|
||||||
INTVALUE("intValue"),
|
|
||||||
STRVALUE("strValue"),
|
|
||||||
SUBENTITY("subEntity", "strValue", "intValue", "subSub"),
|
|
||||||
SUBSET("subSet", "strValue", "intValue"),
|
|
||||||
SUBMAP("subMap");
|
|
||||||
|
|
||||||
private final String jpaEntityFieldName;
|
|
||||||
private final List<String> subEntityAttributes;
|
|
||||||
|
|
||||||
RootField(final String jpaEntityFieldName, final String... subFields) {
|
|
||||||
this.jpaEntityFieldName = jpaEntityFieldName;
|
|
||||||
this.subEntityAttributes = Arrays.asList(subFields);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean isMap() {
|
|
||||||
return this == SUBMAP;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -10,9 +10,6 @@
|
|||||||
package org.eclipse.hawkbit.repository.jpa.ql;
|
package org.eclipse.hawkbit.repository.jpa.ql;
|
||||||
|
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
import static org.eclipse.hawkbit.repository.jpa.ql.QLSupport.SpecBuilder.G3;
|
|
||||||
import static org.eclipse.hawkbit.repository.jpa.ql.QLSupport.SpecBuilder.LEGACY_G1;
|
|
||||||
import static org.eclipse.hawkbit.repository.jpa.ql.QLSupport.SpecBuilder.LEGACY_G2;
|
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
@@ -22,7 +19,6 @@ import java.util.stream.StreamSupport;
|
|||||||
import jakarta.persistence.EntityManager;
|
import jakarta.persistence.EntityManager;
|
||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.eclipse.hawkbit.repository.jpa.ql.QLSupport.SpecBuilder;
|
|
||||||
import org.eclipse.hawkbit.repository.jpa.ql.utils.HawkbitQlToSql;
|
import org.eclipse.hawkbit.repository.jpa.ql.utils.HawkbitQlToSql;
|
||||||
import org.eclipse.hawkbit.repository.jpa.rsql.RsqlParser;
|
import org.eclipse.hawkbit.repository.jpa.rsql.RsqlParser;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
@@ -32,7 +28,6 @@ import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
|||||||
import org.springframework.boot.autoconfigure.flyway.FlywayAutoConfiguration;
|
import org.springframework.boot.autoconfigure.flyway.FlywayAutoConfiguration;
|
||||||
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
|
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
|
||||||
import org.springframework.data.jpa.domain.Specification;
|
import org.springframework.data.jpa.domain.Specification;
|
||||||
import org.springframework.orm.jpa.vendor.Database;
|
|
||||||
|
|
||||||
@SuppressWarnings("java:S5961") // complex check because the matter is very complex
|
@SuppressWarnings("java:S5961") // complex check because the matter is very complex
|
||||||
@DataJpaTest(properties = {
|
@DataJpaTest(properties = {
|
||||||
@@ -63,14 +58,10 @@ class SpecificationBuilderTest {
|
|||||||
|
|
||||||
assertThat(filter("strValue==rootx")).hasSize(2).containsExactlyInAnyOrder(root1, root2);
|
assertThat(filter("strValue==rootx")).hasSize(2).containsExactlyInAnyOrder(root1, root2);
|
||||||
assertThat(filter("strValue==nostr")).isEmpty();
|
assertThat(filter("strValue==nostr")).isEmpty();
|
||||||
if (getRsqlToSpecBuilder() != LEGACY_G1) {
|
assertThat(filter("strValue=is=null")).hasSize(1).containsExactlyInAnyOrder(root5);
|
||||||
assertThat(filter("strValue=is=null")).hasSize(1).containsExactlyInAnyOrder(root5);
|
|
||||||
}
|
|
||||||
assertThat(filter("strValue!=rootx")).hasSize(3).containsExactlyInAnyOrder(root3, root4, root5);
|
assertThat(filter("strValue!=rootx")).hasSize(3).containsExactlyInAnyOrder(root3, root4, root5);
|
||||||
assertThat(filter("strValue!=nostr")).hasSize(5);
|
assertThat(filter("strValue!=nostr")).hasSize(5);
|
||||||
if (getRsqlToSpecBuilder() != LEGACY_G1) {
|
assertThat(filter("strValue=not=null")).hasSize(4).containsExactlyInAnyOrder(root1, root2, root3, root4);
|
||||||
assertThat(filter("strValue=not=null")).hasSize(4).containsExactlyInAnyOrder(root1, root2, root3, root4);
|
|
||||||
}
|
|
||||||
assertThat(filter("strValue<rooty")).hasSize(2).containsExactlyInAnyOrder(root1, root2);
|
assertThat(filter("strValue<rooty")).hasSize(2).containsExactlyInAnyOrder(root1, root2);
|
||||||
assertThat(filter("strValue<=rooty")).hasSize(4).containsExactlyInAnyOrder(root1, root2, root3, root4);
|
assertThat(filter("strValue<=rooty")).hasSize(4).containsExactlyInAnyOrder(root1, root2, root3, root4);
|
||||||
assertThat(filter("strValue>rootx")).hasSize(2).containsExactlyInAnyOrder(root3, root4);
|
assertThat(filter("strValue>rootx")).hasSize(2).containsExactlyInAnyOrder(root3, root4);
|
||||||
@@ -89,20 +80,16 @@ class SpecificationBuilderTest {
|
|||||||
assertThat(filter("strValue==*")).hasSize(4).containsExactlyInAnyOrder(root1, root2, root3, root4);
|
assertThat(filter("strValue==*")).hasSize(4).containsExactlyInAnyOrder(root1, root2, root3, root4);
|
||||||
assertThat(filter("strValue!=*")).hasSize(1).containsExactlyInAnyOrder(root5);
|
assertThat(filter("strValue!=*")).hasSize(1).containsExactlyInAnyOrder(root5);
|
||||||
|
|
||||||
if (getRsqlToSpecBuilder() != LEGACY_G1) {
|
// null checks
|
||||||
// null checks
|
assertThat(filter("strValue=is=null")).hasSize(1).containsExactlyInAnyOrder(root5);
|
||||||
assertThat(filter("strValue=is=null")).hasSize(1).containsExactlyInAnyOrder(root5);
|
assertThat(filter("strValue=not=null")).hasSize(4).containsExactlyInAnyOrder(root1, root2, root3, root4);
|
||||||
assertThat(filter("strValue=not=null")).hasSize(4).containsExactlyInAnyOrder(root1, root2, root3, root4);
|
|
||||||
}
|
|
||||||
|
|
||||||
assertThat(filter("strValue==*tx and strValue==rooty")).isEmpty();
|
assertThat(filter("strValue==*tx and strValue==rooty")).isEmpty();
|
||||||
assertThat(filter("strValue==*tx and strValue!=rootx")).isEmpty();
|
assertThat(filter("strValue==*tx and strValue!=rootx")).isEmpty();
|
||||||
assertThat(filter("strValue==*tx and strValue==rootx")).hasSize(2).containsExactlyInAnyOrder(root1, root2);
|
assertThat(filter("strValue==*tx and strValue==rootx")).hasSize(2).containsExactlyInAnyOrder(root1, root2);
|
||||||
assertThat(filter("strValue==*tx or strValue==rooty")).hasSize(4).containsExactlyInAnyOrder(root1, root2, root3, root4);
|
assertThat(filter("strValue==*tx or strValue==rooty")).hasSize(4).containsExactlyInAnyOrder(root1, root2, root3, root4);
|
||||||
assertThat(filter("strValue==*tx or strValue!=rootx")).hasSize(5).containsExactlyInAnyOrder(root1, root2, root3, root4, root5);
|
assertThat(filter("strValue==*tx or strValue!=rootx")).hasSize(5).containsExactlyInAnyOrder(root1, root2, root3, root4, root5);
|
||||||
if (getRsqlToSpecBuilder() != LEGACY_G1) {
|
assertThat(filter("strValue==*tx or strValue=is=null")).hasSize(3).containsExactlyInAnyOrder(root1, root2, root5);
|
||||||
assertThat(filter("strValue==*tx or strValue=is=null")).hasSize(3).containsExactlyInAnyOrder(root1, root2, root5);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -166,33 +153,25 @@ class SpecificationBuilderTest {
|
|||||||
assertThat(filter("subEntity.strValue==*")).hasSize(4).containsExactlyInAnyOrder(root1, root2, root3, root4);
|
assertThat(filter("subEntity.strValue==*")).hasSize(4).containsExactlyInAnyOrder(root1, root2, root3, root4);
|
||||||
assertThat(filter("subEntity.strValue!=*")).hasSize(1).containsExactlyInAnyOrder(root5);
|
assertThat(filter("subEntity.strValue!=*")).hasSize(1).containsExactlyInAnyOrder(root5);
|
||||||
|
|
||||||
if (getRsqlToSpecBuilder() != LEGACY_G1) {
|
// null checks
|
||||||
// null checks
|
assertThat(filter("subEntity.strValue=is=null")).hasSize(1).containsExactlyInAnyOrder(root5);
|
||||||
assertThat(filter("subEntity.strValue=is=null")).hasSize(1).containsExactlyInAnyOrder(root5);
|
assertThat(filter("subEntity.strValue=not=null")).hasSize(4).containsExactlyInAnyOrder(root1, root2, root3, root4);
|
||||||
assertThat(filter("subEntity.strValue=not=null")).hasSize(4).containsExactlyInAnyOrder(root1, root2, root3, root4);
|
|
||||||
}
|
|
||||||
|
|
||||||
assertThat(filter("subEntity.strValue==*bx and subEntity.strValue==suby")).isEmpty();
|
assertThat(filter("subEntity.strValue==*bx and subEntity.strValue==suby")).isEmpty();
|
||||||
assertThat(filter("subEntity.strValue==*bx and subEntity.strValue!=subx")).isEmpty();
|
assertThat(filter("subEntity.strValue==*bx and subEntity.strValue!=subx")).isEmpty();
|
||||||
assertThat(filter("subEntity.strValue==*bx and subEntity.strValue==subx")).hasSize(2).containsExactlyInAnyOrder(root1, root2);
|
assertThat(filter("subEntity.strValue==*bx and subEntity.strValue==subx")).hasSize(2).containsExactlyInAnyOrder(root1, root2);
|
||||||
assertThat(filter("subEntity.strValue==*bx or subEntity.strValue==suby"))
|
assertThat(filter("subEntity.strValue==*bx or subEntity.strValue==suby"))
|
||||||
.hasSize(4).containsExactlyInAnyOrder(root1, root2, root3, root4);
|
.hasSize(4).containsExactlyInAnyOrder(root1, root2, root3, root4);
|
||||||
if (getRsqlToSpecBuilder() != LEGACY_G1) {
|
assertThat(filter("subEntity.strValue==*bx or subEntity.strValue!=subx"))
|
||||||
// G1 doesn't get null entity in not, and doesn't support =is= and =not=
|
.hasSize(5).containsExactlyInAnyOrder(root1, root2, root3, root4, root5);
|
||||||
assertThat(filter("subEntity.strValue==*bx or subEntity.strValue!=subx"))
|
assertThat(filter("subEntity.strValue==*bx or subEntity.strValue=is=null")).hasSize(3)
|
||||||
.hasSize(5).containsExactlyInAnyOrder(root1, root2, root3, root4, root5);
|
.containsExactlyInAnyOrder(root1, root2, root5);
|
||||||
assertThat(filter("subEntity.strValue==*bx or subEntity.strValue=is=null")).hasSize(3)
|
|
||||||
.containsExactlyInAnyOrder(root1, root2, root5);
|
|
||||||
}
|
|
||||||
|
|
||||||
// by sub entity int
|
// by sub entity int
|
||||||
assertThat(filter("subEntity.intValue==0")).hasSize(2).containsExactlyInAnyOrder(root1, root2);
|
assertThat(filter("subEntity.intValue==0")).hasSize(2).containsExactlyInAnyOrder(root1, root2);
|
||||||
assertThat(filter("subEntity.intValue==2")).isEmpty();
|
assertThat(filter("subEntity.intValue==2")).isEmpty();
|
||||||
if (getRsqlToSpecBuilder() != LEGACY_G1) {
|
assertThat(filter("subEntity.intValue!=0")).hasSize(3).containsExactlyInAnyOrder(root3, root4, root5);
|
||||||
// G1 doesn't get null entity in not
|
assertThat(filter("subEntity.intValue!=2")).hasSize(5);
|
||||||
assertThat(filter("subEntity.intValue!=0")).hasSize(3).containsExactlyInAnyOrder(root3, root4, root5);
|
|
||||||
assertThat(filter("subEntity.intValue!=2")).hasSize(5);
|
|
||||||
}
|
|
||||||
assertThat(filter("subEntity.intValue<1")).hasSize(2).containsExactlyInAnyOrder(root1, root2);
|
assertThat(filter("subEntity.intValue<1")).hasSize(2).containsExactlyInAnyOrder(root1, root2);
|
||||||
assertThat(filter("subEntity.intValue<=1")).hasSize(4).containsExactlyInAnyOrder(root1, root2, root3, root4);
|
assertThat(filter("subEntity.intValue<=1")).hasSize(4).containsExactlyInAnyOrder(root1, root2, root3, root4);
|
||||||
assertThat(filter("subEntity.intValue>0")).hasSize(2).containsExactlyInAnyOrder(root3, root4);
|
assertThat(filter("subEntity.intValue>0")).hasSize(2).containsExactlyInAnyOrder(root3, root4);
|
||||||
@@ -207,20 +186,14 @@ class SpecificationBuilderTest {
|
|||||||
assertThat(filter("subEntity.intValue==0 and subEntity.intValue!=1")).hasSize(2).containsExactlyInAnyOrder(root1, root2);
|
assertThat(filter("subEntity.intValue==0 and subEntity.intValue!=1")).hasSize(2).containsExactlyInAnyOrder(root1, root2);
|
||||||
assertThat(filter("subEntity.intValue==0 and subEntity.intValue==0")).hasSize(2).containsExactlyInAnyOrder(root1, root2);
|
assertThat(filter("subEntity.intValue==0 and subEntity.intValue==0")).hasSize(2).containsExactlyInAnyOrder(root1, root2);
|
||||||
assertThat(filter("subEntity.intValue==0 or subEntity.intValue==1")).hasSize(4).containsExactlyInAnyOrder(root1, root2, root3, root4);
|
assertThat(filter("subEntity.intValue==0 or subEntity.intValue==1")).hasSize(4).containsExactlyInAnyOrder(root1, root2, root3, root4);
|
||||||
if (getRsqlToSpecBuilder() != LEGACY_G1) {
|
assertThat(filter("subEntity.intValue==0 or subEntity.intValue!=1")).hasSize(3).containsExactlyInAnyOrder(root1, root2, root5);
|
||||||
// G1 doesn't get null entity in not
|
|
||||||
assertThat(filter("subEntity.intValue==0 or subEntity.intValue!=1")).hasSize(3).containsExactlyInAnyOrder(root1, root2, root5);
|
|
||||||
}
|
|
||||||
|
|
||||||
assertThat(filter("subEntity.strValue==subx and subEntity.intValue==1")).isEmpty();
|
assertThat(filter("subEntity.strValue==subx and subEntity.intValue==1")).isEmpty();
|
||||||
assertThat(filter("subEntity.strValue==subx and subEntity.intValue!=1")).hasSize(2).containsExactlyInAnyOrder(root1, root2);
|
assertThat(filter("subEntity.strValue==subx and subEntity.intValue!=1")).hasSize(2).containsExactlyInAnyOrder(root1, root2);
|
||||||
assertThat(filter("subEntity.strValue==subx and subEntity.intValue==0")).hasSize(2).containsExactlyInAnyOrder(root1, root2);
|
assertThat(filter("subEntity.strValue==subx and subEntity.intValue==0")).hasSize(2).containsExactlyInAnyOrder(root1, root2);
|
||||||
assertThat(filter("subEntity.strValue==subx or subEntity.intValue==1")).hasSize(4)
|
assertThat(filter("subEntity.strValue==subx or subEntity.intValue==1")).hasSize(4)
|
||||||
.containsExactlyInAnyOrder(root1, root2, root3, root4);
|
.containsExactlyInAnyOrder(root1, root2, root3, root4);
|
||||||
if (getRsqlToSpecBuilder() != LEGACY_G1) {
|
assertThat(filter("subEntity.strValue==subx or subEntity.intValue!=1")).hasSize(3).containsExactlyInAnyOrder(root1, root2, root5);
|
||||||
// G1 doesn't get null entity in not
|
|
||||||
assertThat(filter("subEntity.strValue==subx or subEntity.intValue!=1")).hasSize(3).containsExactlyInAnyOrder(root1, root2, root5);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -257,34 +230,23 @@ class SpecificationBuilderTest {
|
|||||||
assertThat(filter("subSet.strValue==*")).hasSize(5).containsExactlyInAnyOrder(root1, root2, root3, root4, root5);
|
assertThat(filter("subSet.strValue==*")).hasSize(5).containsExactlyInAnyOrder(root1, root2, root3, root4, root5);
|
||||||
assertThat(filter("subSet.strValue!=*")).hasSize(1).containsExactlyInAnyOrder(root6);
|
assertThat(filter("subSet.strValue!=*")).hasSize(1).containsExactlyInAnyOrder(root6);
|
||||||
|
|
||||||
if (getRsqlToSpecBuilder() != LEGACY_G1) {
|
assertThat(filter("subSet.strValue==*bx and subSet.strValue==suby")).hasSize(2).containsExactlyInAnyOrder(root4, root5);
|
||||||
// G1 treats it like same element shall match - which doesn't match to "has" semantic for sets
|
|
||||||
assertThat(filter("subSet.strValue==*bx and subSet.strValue==suby")).hasSize(2).containsExactlyInAnyOrder(root4, root5);
|
|
||||||
}
|
|
||||||
assertThat(filter("subSet.strValue==*bx and subSet.strValue!=subx")).isEmpty();
|
assertThat(filter("subSet.strValue==*bx and subSet.strValue!=subx")).isEmpty();
|
||||||
assertThat(filter("subSet.strValue==*bx and subSet.strValue!=suby")).hasSize(1).containsExactlyInAnyOrder(root1);
|
assertThat(filter("subSet.strValue==*bx and subSet.strValue!=suby")).hasSize(1).containsExactlyInAnyOrder(root1);
|
||||||
assertThat(filter("subSet.strValue==*bx or subSet.strValue==suby"))
|
assertThat(filter("subSet.strValue==*bx or subSet.strValue==suby"))
|
||||||
.hasSize(5).containsExactlyInAnyOrder(root1, root2, root3, root4, root5);
|
.hasSize(5).containsExactlyInAnyOrder(root1, root2, root3, root4, root5);
|
||||||
if (getRsqlToSpecBuilder() != LEGACY_G1) {
|
assertThat(filter("subSet.strValue==*bx or subSet.strValue!=subx"))
|
||||||
// G1 doesn't get null - has which shall be included as doesn't have
|
.hasSize(6).containsExactlyInAnyOrder(root1, root2, root3, root4, root5, root6);
|
||||||
assertThat(filter("subSet.strValue==*bx or subSet.strValue!=subx"))
|
|
||||||
.hasSize(6).containsExactlyInAnyOrder(root1, root2, root3, root4, root5, root6);
|
|
||||||
}
|
|
||||||
|
|
||||||
// by sub entity int
|
// by sub entity int
|
||||||
assertThat(filter("subSet.intValue==0")).hasSize(4).containsExactlyInAnyOrder(root1, root3, root4, root5);
|
assertThat(filter("subSet.intValue==0")).hasSize(4).containsExactlyInAnyOrder(root1, root3, root4, root5);
|
||||||
assertThat(filter("subSet.intValue==2")).isEmpty();
|
assertThat(filter("subSet.intValue==2")).isEmpty();
|
||||||
if (getRsqlToSpecBuilder() == G3) {
|
// the legacy builders has different (and wrong semantic)
|
||||||
// the legacy builders has different (and wrong semantic)
|
// G1 - has element != x
|
||||||
// G1 - has element != x
|
// G2 - has element != x or has no elements
|
||||||
// G2 - has element != x or has no elements
|
// accompanying G3 it is (as it should be) semantic of set != x - has no element with value x (including nhas no elements)
|
||||||
// accompanying G3 it is (as it should be) semantic of set != x - has no element with value x (including nhas no elements)
|
assertThat(filter("subSet.intValue!=0")).hasSize(2).containsExactlyInAnyOrder(root2, root6);
|
||||||
assertThat(filter("subSet.intValue!=0")).hasSize(2).containsExactlyInAnyOrder(root2, root6);
|
assertThat(filter("subSet.intValue!=2")).hasSize(6);
|
||||||
}
|
|
||||||
if (getRsqlToSpecBuilder() != LEGACY_G1) {
|
|
||||||
// G1 doesn't get null - has which shall be included as doesn't have
|
|
||||||
assertThat(filter("subSet.intValue!=2")).hasSize(6);
|
|
||||||
}
|
|
||||||
assertThat(filter("subSet.intValue<1")).hasSize(4).containsExactlyInAnyOrder(root1, root3, root4, root5);
|
assertThat(filter("subSet.intValue<1")).hasSize(4).containsExactlyInAnyOrder(root1, root3, root4, root5);
|
||||||
assertThat(filter("subSet.intValue<=1")).hasSize(5).containsExactlyInAnyOrder(root1, root2, root3, root4, root5);
|
assertThat(filter("subSet.intValue<=1")).hasSize(5).containsExactlyInAnyOrder(root1, root2, root3, root4, root5);
|
||||||
assertThat(filter("subSet.intValue>0")).hasSize(2).containsExactlyInAnyOrder(root2, root4);
|
assertThat(filter("subSet.intValue>0")).hasSize(2).containsExactlyInAnyOrder(root2, root4);
|
||||||
@@ -296,30 +258,19 @@ class SpecificationBuilderTest {
|
|||||||
assertThat(filter("subSet.intValue=out=(0, 1)")).hasSize(1).containsExactlyInAnyOrder(root6);
|
assertThat(filter("subSet.intValue=out=(0, 1)")).hasSize(1).containsExactlyInAnyOrder(root6);
|
||||||
|
|
||||||
assertThat(filter("subSet.intValue==0 and subSet.intValue==0")).hasSize(4).containsExactlyInAnyOrder(root1, root3, root4, root5);
|
assertThat(filter("subSet.intValue==0 and subSet.intValue==0")).hasSize(4).containsExactlyInAnyOrder(root1, root3, root4, root5);
|
||||||
if (getRsqlToSpecBuilder() == G3) {
|
assertThat(filter("subSet.intValue==0 and subSet.intValue!=1")).hasSize(3).containsExactlyInAnyOrder(root1, root3, root5);
|
||||||
assertThat(filter("subSet.intValue==0 and subSet.intValue!=1")).hasSize(3).containsExactlyInAnyOrder(root1, root3, root5);
|
|
||||||
}
|
|
||||||
assertThat(filter("subSet.intValue==0 or subSet.intValue==1"))
|
assertThat(filter("subSet.intValue==0 or subSet.intValue==1"))
|
||||||
.hasSize(5).containsExactlyInAnyOrder(root1, root2, root3, root4, root5);
|
.hasSize(5).containsExactlyInAnyOrder(root1, root2, root3, root4, root5);
|
||||||
if (getRsqlToSpecBuilder() != LEGACY_G1) {
|
assertThat(filter("subSet.intValue==0 or subSet.intValue!=1"))
|
||||||
// G1 doesn't get null - has which shall be included as doesn't have
|
.hasSize(5).containsExactlyInAnyOrder(root1, root3, root4, root5, root6);
|
||||||
assertThat(filter("subSet.intValue==0 or subSet.intValue!=1"))
|
|
||||||
.hasSize(5).containsExactlyInAnyOrder(root1, root3, root4, root5, root6);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (getRsqlToSpecBuilder() != LEGACY_G1) {
|
assertThat(filter("subSet.intValue==0 and subSet.strValue==suby")).hasSize(3).containsExactlyInAnyOrder(root3, root4, root5);
|
||||||
// G1 treats it like same element shall match - which doesn't match to "has" semantic for sets
|
|
||||||
assertThat(filter("subSet.intValue==0 and subSet.strValue==suby")).hasSize(3).containsExactlyInAnyOrder(root3, root4, root5);
|
|
||||||
}
|
|
||||||
assertThat(filter("subSet.intValue==0 and subSet.strValue!=subx")).hasSize(1).containsExactlyInAnyOrder(root3);
|
assertThat(filter("subSet.intValue==0 and subSet.strValue!=subx")).hasSize(1).containsExactlyInAnyOrder(root3);
|
||||||
assertThat(filter("subSet.intValue==0 and subSet.strValue!=suby")).hasSize(1).containsExactlyInAnyOrder(root1);
|
assertThat(filter("subSet.intValue==0 and subSet.strValue!=suby")).hasSize(1).containsExactlyInAnyOrder(root1);
|
||||||
assertThat(filter("subSet.intValue==0 or subSet.strValue==suby"))
|
assertThat(filter("subSet.intValue==0 or subSet.strValue==suby"))
|
||||||
.hasSize(5).containsExactlyInAnyOrder(root1, root2, root3, root4, root5);
|
.hasSize(5).containsExactlyInAnyOrder(root1, root2, root3, root4, root5);
|
||||||
if (getRsqlToSpecBuilder() != LEGACY_G1) {
|
assertThat(filter("subSet.intValue==0 or subSet.strValue!=subx"))
|
||||||
// G1 doesn't get null - has which shall be included as doesn't have
|
.hasSize(6).containsExactlyInAnyOrder(root1, root2, root3, root4, root5, root6);
|
||||||
assertThat(filter("subSet.intValue==0 or subSet.strValue!=subx"))
|
|
||||||
.hasSize(6).containsExactlyInAnyOrder(root1, root2, root3, root4, root5, root6);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -353,11 +304,9 @@ class SpecificationBuilderTest {
|
|||||||
assertThat(filter("subMap.x==*")).hasSize(5).containsExactlyInAnyOrder(root1, root2, root3, root4, root5);
|
assertThat(filter("subMap.x==*")).hasSize(5).containsExactlyInAnyOrder(root1, root2, root3, root4, root5);
|
||||||
assertThat(filter("subMap.x!=*")).isEmpty();
|
assertThat(filter("subMap.x!=*")).isEmpty();
|
||||||
|
|
||||||
if (getRsqlToSpecBuilder() != LEGACY_G1) {
|
// null checks
|
||||||
// null checks
|
assertThat(filter("subMap.x=is=null")).hasSize(1).containsExactlyInAnyOrder(root6);
|
||||||
assertThat(filter("subMap.x=is=null")).hasSize(1).containsExactlyInAnyOrder(root6);
|
assertThat(filter("subMap.x=not=null")).hasSize(5).containsExactlyInAnyOrder(root1, root2, root3, root4, root5);
|
||||||
assertThat(filter("subMap.x=not=null")).hasSize(5).containsExactlyInAnyOrder(root1, root2, root3, root4, root5);
|
|
||||||
}
|
|
||||||
|
|
||||||
assertThat(filter("subMap.x==*tx and subMap.y==rooty")).hasSize(1).containsExactlyInAnyOrder(root1);
|
assertThat(filter("subMap.x==*tx and subMap.y==rooty")).hasSize(1).containsExactlyInAnyOrder(root1);
|
||||||
assertThat(filter("subMap.x==*tx and subMap.y!=rootx")).hasSize(1).containsExactlyInAnyOrder(root1);
|
assertThat(filter("subMap.x==*tx and subMap.y!=rootx")).hasSize(1).containsExactlyInAnyOrder(root1);
|
||||||
@@ -402,13 +351,8 @@ class SpecificationBuilderTest {
|
|||||||
assertThat(filter("subEntity.subSub.strValue==*")).hasSize(4).containsExactlyInAnyOrder(root1, root2, root3, root4);
|
assertThat(filter("subEntity.subSub.strValue==*")).hasSize(4).containsExactlyInAnyOrder(root1, root2, root3, root4);
|
||||||
assertThat(filter("subEntity.subSub.strValue!=*")).hasSize(1).containsExactlyInAnyOrder(root5);
|
assertThat(filter("subEntity.subSub.strValue!=*")).hasSize(1).containsExactlyInAnyOrder(root5);
|
||||||
|
|
||||||
if (getRsqlToSpecBuilder() != LEGACY_G1) {
|
assertThat(filter("subEntity.subSub.strValue=is=null")).hasSize(1).containsExactlyInAnyOrder(root5);
|
||||||
// null checks
|
assertThat(filter("subEntity.subSub.strValue=not=null")).hasSize(4).containsExactlyInAnyOrder(root1, root2, root3, root4);
|
||||||
if (getRsqlToSpecBuilder() != LEGACY_G2) {
|
|
||||||
assertThat(filter("subEntity.subSub.strValue=is=null")).hasSize(1).containsExactlyInAnyOrder(root5);
|
|
||||||
}
|
|
||||||
assertThat(filter("subEntity.subSub.strValue=not=null")).hasSize(4).containsExactlyInAnyOrder(root1, root2, root3, root4);
|
|
||||||
}
|
|
||||||
|
|
||||||
assertThat(filter("subEntity.subSub.strValue==*bx and subEntity.subSub.strValue==suby")).isEmpty();
|
assertThat(filter("subEntity.subSub.strValue==*bx and subEntity.subSub.strValue==suby")).isEmpty();
|
||||||
assertThat(filter("subEntity.subSub.strValue==*bx and subEntity.subSub.strValue!=subx")).isEmpty();
|
assertThat(filter("subEntity.subSub.strValue==*bx and subEntity.subSub.strValue!=subx")).isEmpty();
|
||||||
@@ -416,22 +360,16 @@ class SpecificationBuilderTest {
|
|||||||
.hasSize(2).containsExactlyInAnyOrder(root1, root2);
|
.hasSize(2).containsExactlyInAnyOrder(root1, root2);
|
||||||
assertThat(filter("subEntity.subSub.strValue==*bx or subEntity.subSub.strValue==suby"))
|
assertThat(filter("subEntity.subSub.strValue==*bx or subEntity.subSub.strValue==suby"))
|
||||||
.hasSize(4).containsExactlyInAnyOrder(root1, root2, root3, root4);
|
.hasSize(4).containsExactlyInAnyOrder(root1, root2, root3, root4);
|
||||||
if (getRsqlToSpecBuilder() != LEGACY_G1 && getRsqlToSpecBuilder() != LEGACY_G2) {
|
assertThat(filter("subEntity.subSub.strValue==*bx or subEntity.subSub.strValue!=subx"))
|
||||||
// G1 doesn't get null entity in not, and doesn't support =is= and =not=
|
.hasSize(5).containsExactlyInAnyOrder(root1, root2, root3, root4, root5);
|
||||||
assertThat(filter("subEntity.subSub.strValue==*bx or subEntity.subSub.strValue!=subx"))
|
assertThat(filter("subEntity.subSub.strValue==*bx or subEntity.subSub.strValue=is=null"))
|
||||||
.hasSize(5).containsExactlyInAnyOrder(root1, root2, root3, root4, root5);
|
.hasSize(3).containsExactlyInAnyOrder(root1, root2, root5);
|
||||||
assertThat(filter("subEntity.subSub.strValue==*bx or subEntity.subSub.strValue=is=null"))
|
|
||||||
.hasSize(3).containsExactlyInAnyOrder(root1, root2, root5);
|
|
||||||
}
|
|
||||||
|
|
||||||
// by sub entity int
|
// by sub entity int
|
||||||
assertThat(filter("subEntity.subSub.intValue==0")).hasSize(2).containsExactlyInAnyOrder(root1, root2);
|
assertThat(filter("subEntity.subSub.intValue==0")).hasSize(2).containsExactlyInAnyOrder(root1, root2);
|
||||||
assertThat(filter("subEntity.subSub.intValue==2")).isEmpty();
|
assertThat(filter("subEntity.subSub.intValue==2")).isEmpty();
|
||||||
if (getRsqlToSpecBuilder() != LEGACY_G1 && getRsqlToSpecBuilder() != LEGACY_G2) {
|
assertThat(filter("subEntity.subSub.intValue!=0")).hasSize(3).containsExactlyInAnyOrder(root3, root4, root5);
|
||||||
// G1 doesn't get null entity in not
|
assertThat(filter("subEntity.subSub.intValue!=2")).hasSize(5);
|
||||||
assertThat(filter("subEntity.subSub.intValue!=0")).hasSize(3).containsExactlyInAnyOrder(root3, root4, root5);
|
|
||||||
assertThat(filter("subEntity.subSub.intValue!=2")).hasSize(5);
|
|
||||||
}
|
|
||||||
assertThat(filter("subEntity.subSub.intValue<1")).hasSize(2).containsExactlyInAnyOrder(root1, root2);
|
assertThat(filter("subEntity.subSub.intValue<1")).hasSize(2).containsExactlyInAnyOrder(root1, root2);
|
||||||
assertThat(filter("subEntity.subSub.intValue<=1")).hasSize(4).containsExactlyInAnyOrder(root1, root2, root3, root4);
|
assertThat(filter("subEntity.subSub.intValue<=1")).hasSize(4).containsExactlyInAnyOrder(root1, root2, root3, root4);
|
||||||
assertThat(filter("subEntity.subSub.intValue>0")).hasSize(2).containsExactlyInAnyOrder(root3, root4);
|
assertThat(filter("subEntity.subSub.intValue>0")).hasSize(2).containsExactlyInAnyOrder(root3, root4);
|
||||||
@@ -447,11 +385,8 @@ class SpecificationBuilderTest {
|
|||||||
assertThat(filter("subEntity.subSub.intValue==0 and subEntity.subSub.intValue==0")).hasSize(2).containsExactlyInAnyOrder(root1, root2);
|
assertThat(filter("subEntity.subSub.intValue==0 and subEntity.subSub.intValue==0")).hasSize(2).containsExactlyInAnyOrder(root1, root2);
|
||||||
assertThat(filter("subEntity.subSub.intValue==0 or subEntity.subSub.intValue==1")).hasSize(4)
|
assertThat(filter("subEntity.subSub.intValue==0 or subEntity.subSub.intValue==1")).hasSize(4)
|
||||||
.containsExactlyInAnyOrder(root1, root2, root3, root4);
|
.containsExactlyInAnyOrder(root1, root2, root3, root4);
|
||||||
if (getRsqlToSpecBuilder() != LEGACY_G1 && getRsqlToSpecBuilder() != LEGACY_G2) {
|
assertThat(filter("subEntity.subSub.intValue==0 or subEntity.subSub.intValue!=1"))
|
||||||
// G1 doesn't get null entity in not
|
.hasSize(3).containsExactlyInAnyOrder(root1, root2, root5);
|
||||||
assertThat(filter("subEntity.subSub.intValue==0 or subEntity.subSub.intValue!=1"))
|
|
||||||
.hasSize(3).containsExactlyInAnyOrder(root1, root2, root5);
|
|
||||||
}
|
|
||||||
|
|
||||||
assertThat(filter("subEntity.subSub.strValue==subx and subEntity.subSub.intValue==1")).isEmpty();
|
assertThat(filter("subEntity.subSub.strValue==subx and subEntity.subSub.intValue==1")).isEmpty();
|
||||||
assertThat(filter("subEntity.subSub.strValue==subx and subEntity.subSub.intValue!=1")).hasSize(2)
|
assertThat(filter("subEntity.subSub.strValue==subx and subEntity.subSub.intValue!=1")).hasSize(2)
|
||||||
@@ -460,11 +395,8 @@ class SpecificationBuilderTest {
|
|||||||
.hasSize(2).containsExactlyInAnyOrder(root1, root2);
|
.hasSize(2).containsExactlyInAnyOrder(root1, root2);
|
||||||
assertThat(filter("subEntity.subSub.strValue==subx or subEntity.subSub.intValue==1"))
|
assertThat(filter("subEntity.subSub.strValue==subx or subEntity.subSub.intValue==1"))
|
||||||
.hasSize(4).containsExactlyInAnyOrder(root1, root2, root3, root4);
|
.hasSize(4).containsExactlyInAnyOrder(root1, root2, root3, root4);
|
||||||
if (getRsqlToSpecBuilder() != LEGACY_G1 && getRsqlToSpecBuilder() != LEGACY_G2) {
|
assertThat(filter("subEntity.subSub.strValue==subx or subEntity.subSub.intValue!=1"))
|
||||||
// G1 doesn't get null entity in not
|
.hasSize(3).containsExactlyInAnyOrder(root1, root2, root5);
|
||||||
assertThat(filter("subEntity.subSub.strValue==subx or subEntity.subSub.intValue!=1"))
|
|
||||||
.hasSize(3).containsExactlyInAnyOrder(root1, root2, root5);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -533,8 +465,10 @@ class SpecificationBuilderTest {
|
|||||||
assertThat(filter("subEntity.subSub.subSub.intValue=out=(0, 1)")).hasSize(1).containsExactlyInAnyOrder(root5);
|
assertThat(filter("subEntity.subSub.subSub.intValue=out=(0, 1)")).hasSize(1).containsExactlyInAnyOrder(root5);
|
||||||
|
|
||||||
assertThat(filter("subEntity.subSub.subSub.intValue==0 and subEntity.subSub.intValue==1")).isEmpty();
|
assertThat(filter("subEntity.subSub.subSub.intValue==0 and subEntity.subSub.intValue==1")).isEmpty();
|
||||||
assertThat(filter("subEntity.subSub.subSub.intValue==0 and subEntity.subSub.intValue!=1")).hasSize(2).containsExactlyInAnyOrder(root1, root2);
|
assertThat(filter("subEntity.subSub.subSub.intValue==0 and subEntity.subSub.intValue!=1")).hasSize(2)
|
||||||
assertThat(filter("subEntity.subSub.subSub.intValue==0 and subEntity.subSub.intValue==0")).hasSize(2).containsExactlyInAnyOrder(root1, root2);
|
.containsExactlyInAnyOrder(root1, root2);
|
||||||
|
assertThat(filter("subEntity.subSub.subSub.intValue==0 and subEntity.subSub.intValue==0")).hasSize(2)
|
||||||
|
.containsExactlyInAnyOrder(root1, root2);
|
||||||
assertThat(filter("subEntity.subSub.subSub.intValue==0 or subEntity.subSub.intValue==1")).hasSize(4)
|
assertThat(filter("subEntity.subSub.subSub.intValue==0 or subEntity.subSub.intValue==1")).hasSize(4)
|
||||||
.containsExactlyInAnyOrder(root1, root2, root3, root4);
|
.containsExactlyInAnyOrder(root1, root2, root3, root4);
|
||||||
assertThat(filter("subEntity.subSub.subSub.intValue==0 or subEntity.subSub.subSub.intValue!=1"))
|
assertThat(filter("subEntity.subSub.subSub.intValue==0 or subEntity.subSub.subSub.intValue!=1"))
|
||||||
@@ -562,7 +496,7 @@ class SpecificationBuilderTest {
|
|||||||
} catch (final AssertionError e) {
|
} catch (final AssertionError e) {
|
||||||
log.error(
|
log.error(
|
||||||
"Fail to get expected result for RSQL: {} with SQL query: {}",
|
"Fail to get expected result for RSQL: {} with SQL query: {}",
|
||||||
rsql, new HawkbitQlToSql(entityManager).toSQL(Root.class, null, rsql, G3),
|
rsql, new HawkbitQlToSql(entityManager).toSQL(Root.class, null, rsql),
|
||||||
e);
|
e);
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
@@ -573,10 +507,6 @@ class SpecificationBuilderTest {
|
|||||||
return builder.specification(RsqlParser.parse(rsql));
|
return builder.specification(RsqlParser.parse(rsql));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static SpecBuilder getRsqlToSpecBuilder() {
|
|
||||||
return QLSupport.getInstance().getSpecBuilder();
|
|
||||||
}
|
|
||||||
|
|
||||||
@SpringBootConfiguration
|
@SpringBootConfiguration
|
||||||
static class Config {}
|
static class Config {}
|
||||||
}
|
}
|
||||||
@@ -12,7 +12,6 @@ package org.eclipse.hawkbit.repository.jpa.management;
|
|||||||
import static org.assertj.core.api.Assertions.assertThat;
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||||
import static org.assertj.core.api.Assertions.fail;
|
import static org.assertj.core.api.Assertions.fail;
|
||||||
import static org.eclipse.hawkbit.repository.jpa.ql.QLSupport.SpecBuilder.LEGACY_G1;
|
|
||||||
|
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
@@ -62,7 +61,6 @@ import org.eclipse.hawkbit.repository.jpa.model.JpaAction;
|
|||||||
import org.eclipse.hawkbit.repository.jpa.model.JpaTarget;
|
import org.eclipse.hawkbit.repository.jpa.model.JpaTarget;
|
||||||
import org.eclipse.hawkbit.repository.jpa.model.JpaTargetTag;
|
import org.eclipse.hawkbit.repository.jpa.model.JpaTargetTag;
|
||||||
import org.eclipse.hawkbit.repository.jpa.model.JpaTarget_;
|
import org.eclipse.hawkbit.repository.jpa.model.JpaTarget_;
|
||||||
import org.eclipse.hawkbit.repository.jpa.ql.QLSupport;
|
|
||||||
import org.eclipse.hawkbit.repository.model.Action;
|
import org.eclipse.hawkbit.repository.model.Action;
|
||||||
import org.eclipse.hawkbit.repository.model.Action.ActionStatusCreate;
|
import org.eclipse.hawkbit.repository.model.Action.ActionStatusCreate;
|
||||||
import org.eclipse.hawkbit.repository.model.Action.Status;
|
import org.eclipse.hawkbit.repository.model.Action.Status;
|
||||||
@@ -636,10 +634,6 @@ class TargetManagementTest extends AbstractRepositoryManagementWithMetadataTest<
|
|||||||
*/
|
*/
|
||||||
@Test
|
@Test
|
||||||
void findTargetsByRsqlWithTypeAndMetadata() {
|
void findTargetsByRsqlWithTypeAndMetadata() {
|
||||||
if (QLSupport.getInstance().getSpecBuilder() == LEGACY_G1) {
|
|
||||||
// legacy visitor fail with that
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
final String controllerId1 = "target1";
|
final String controllerId1 = "target1";
|
||||||
final String controllerId2 = "target2";
|
final String controllerId2 = "target2";
|
||||||
createTargetWithMetadata(controllerId1, 2);
|
createTargetWithMetadata(controllerId1, 2);
|
||||||
|
|||||||
@@ -11,8 +11,6 @@ package org.eclipse.hawkbit.repository.jpa.rsql;
|
|||||||
|
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||||
import static org.eclipse.hawkbit.repository.jpa.ql.QLSupport.SpecBuilder.LEGACY_G2;
|
|
||||||
import static org.eclipse.hawkbit.repository.jpa.ql.QLSupport.SpecBuilder.LEGACY_G1;
|
|
||||||
|
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
@@ -178,11 +176,9 @@ class RsqlTargetFieldTest extends AbstractJpaIntegrationTest {
|
|||||||
assertRSQLQuery(TargetFields.ATTRIBUTE.name() + ".test.null==null", 1); // "null" check
|
assertRSQLQuery(TargetFields.ATTRIBUTE.name() + ".test.null==null", 1); // "null" check
|
||||||
assertRSQLQuery(TargetFields.ATTRIBUTE.name() + ".test.n/a==null", 0); // "null" check
|
assertRSQLQuery(TargetFields.ATTRIBUTE.name() + ".test.n/a==null", 0); // "null" check
|
||||||
|
|
||||||
if (QLSupport.getInstance().getSpecBuilder() != LEGACY_G1) {
|
assertRSQLQuery(TargetFields.ATTRIBUTE.name() + ".test.dot=is=value.dot", 1);
|
||||||
assertRSQLQuery(TargetFields.ATTRIBUTE.name() + ".test.dot=is=value.dot", 1);
|
assertRSQLQuery(TargetFields.ATTRIBUTE.name() + ".test.null=is=null", 5); // null check
|
||||||
assertRSQLQuery(TargetFields.ATTRIBUTE.name() + ".test.null=is=null", 5); // null check
|
assertRSQLQuery(TargetFields.ATTRIBUTE.name() + ".test.n/a=is=null", 1 + 5); // null check
|
||||||
assertRSQLQuery(TargetFields.ATTRIBUTE.name() + ".test.n/a=is=null", 1 + 5); // null check
|
|
||||||
}
|
|
||||||
assertRSQLQuery(TargetFields.ATTRIBUTE.name() + ".test.dot=eq=value.dot", 1);
|
assertRSQLQuery(TargetFields.ATTRIBUTE.name() + ".test.dot=eq=value.dot", 1);
|
||||||
assertRSQLQuery(TargetFields.ATTRIBUTE.name() + ".test.null=eq=null", 5); // null check
|
assertRSQLQuery(TargetFields.ATTRIBUTE.name() + ".test.null=eq=null", 5); // null check
|
||||||
assertRSQLQuery(TargetFields.ATTRIBUTE.name() + ".test.n/a=eq=null", 1 + 5); // null check
|
assertRSQLQuery(TargetFields.ATTRIBUTE.name() + ".test.n/a=eq=null", 1 + 5); // null check
|
||||||
@@ -192,24 +188,14 @@ class RsqlTargetFieldTest extends AbstractJpaIntegrationTest {
|
|||||||
assertRSQLQuery(TargetFields.ATTRIBUTE.name() + ".test.null!=null2", 1); // value check
|
assertRSQLQuery(TargetFields.ATTRIBUTE.name() + ".test.null!=null2", 1); // value check
|
||||||
assertRSQLQuery(TargetFields.ATTRIBUTE.name() + ".test.n/a!=null", 0); // "null" check
|
assertRSQLQuery(TargetFields.ATTRIBUTE.name() + ".test.n/a!=null", 0); // "null" check
|
||||||
|
|
||||||
if (QLSupport.getInstance().getSpecBuilder() != LEGACY_G1) {
|
assertRSQLQuery(TargetFields.ATTRIBUTE.name() + ".test.dot=not=value.dot", 0);
|
||||||
assertRSQLQuery(
|
assertRSQLQuery(TargetFields.ATTRIBUTE.name() + ".test.null=not=null", 1); // null check
|
||||||
TargetFields.ATTRIBUTE.name() + ".test.dot=not=value.dot",
|
assertRSQLQuery(TargetFields.ATTRIBUTE.name() + ".test.null=not=null2", 1); // value check
|
||||||
QLSupport.getInstance().getSpecBuilder() == LEGACY_G2 ? 5 : 0);
|
assertRSQLQuery(TargetFields.ATTRIBUTE.name() + ".test.n/a=not=null", 0); // null check
|
||||||
assertRSQLQuery(TargetFields.ATTRIBUTE.name() + ".test.null=not=null", 1); // null check
|
assertRSQLQuery(TargetFields.ATTRIBUTE.name() + ".test.dot=ne=value.dot", 0);
|
||||||
assertRSQLQuery(
|
assertRSQLQuery(TargetFields.ATTRIBUTE.name() + ".test.null=ne=null", 1); // null check
|
||||||
TargetFields.ATTRIBUTE.name() + ".test.null=not=null2",
|
assertRSQLQuery(TargetFields.ATTRIBUTE.name() + ".test.null=ne=null2", 1); // value check
|
||||||
1 + (QLSupport.getInstance().getSpecBuilder() == LEGACY_G2 ? 5 : 0)); // value check
|
assertRSQLQuery(TargetFields.ATTRIBUTE.name() + ".test.n/a=ne=null", 0); // null check
|
||||||
assertRSQLQuery(TargetFields.ATTRIBUTE.name() + ".test.n/a=not=null", 0); // null check
|
|
||||||
assertRSQLQuery(
|
|
||||||
TargetFields.ATTRIBUTE.name() + ".test.dot=ne=value.dot",
|
|
||||||
QLSupport.getInstance().getSpecBuilder() == LEGACY_G2 ? 5 : 0);
|
|
||||||
assertRSQLQuery(TargetFields.ATTRIBUTE.name() + ".test.null=ne=null", 1); // null check
|
|
||||||
assertRSQLQuery(
|
|
||||||
TargetFields.ATTRIBUTE.name() + ".test.null=ne=null2",
|
|
||||||
1 + (QLSupport.getInstance().getSpecBuilder() == LEGACY_G2 ? 5 : 0)); // value check
|
|
||||||
assertRSQLQuery(TargetFields.ATTRIBUTE.name() + ".test.n/a=ne=null", 0); // null check
|
|
||||||
}
|
|
||||||
|
|
||||||
assertRSQLQuery(TargetFields.ATTRIBUTE.name() + ".revision==1.1", 1);
|
assertRSQLQuery(TargetFields.ATTRIBUTE.name() + ".revision==1.1", 1);
|
||||||
assertRSQLQuery(TargetFields.ATTRIBUTE.name() + ".revision!=1.1", 1);
|
assertRSQLQuery(TargetFields.ATTRIBUTE.name() + ".revision!=1.1", 1);
|
||||||
@@ -304,13 +290,11 @@ class RsqlTargetFieldTest extends AbstractJpaIntegrationTest {
|
|||||||
assertRSQLQuery(TargetFields.METADATA.name() + ".metaKey==noExist*", 0);
|
assertRSQLQuery(TargetFields.METADATA.name() + ".metaKey==noExist*", 0);
|
||||||
assertRSQLQuery(TargetFields.METADATA.name() + ".notExist==metaValue", 0);
|
assertRSQLQuery(TargetFields.METADATA.name() + ".notExist==metaValue", 0);
|
||||||
|
|
||||||
if (QLSupport.getInstance().getSpecBuilder() != LEGACY_G1) {
|
assertRSQLQuery(TargetFields.METADATA.name() + ".metaKey=is=metaValue", 1);
|
||||||
assertRSQLQuery(TargetFields.METADATA.name() + ".metaKey=is=metaValue", 1);
|
assertRSQLQuery(TargetFields.METADATA.name() + ".metaKey=is=null", 4); // null check (1 of the initial five has)
|
||||||
assertRSQLQuery(TargetFields.METADATA.name() + ".metaKey=is=null", 4); // null check (1 of the initial five has)
|
assertRSQLQuery(TargetFields.METADATA.name() + ".metaKey=is=*v*", 2);
|
||||||
assertRSQLQuery(TargetFields.METADATA.name() + ".metaKey=is=*v*", 2);
|
assertRSQLQuery(TargetFields.METADATA.name() + ".metaKey=is=noExist*", 0);
|
||||||
assertRSQLQuery(TargetFields.METADATA.name() + ".metaKey=is=noExist*", 0);
|
assertRSQLQuery(TargetFields.METADATA.name() + ".notExist=is=null", 1 + 5); // null check
|
||||||
assertRSQLQuery(TargetFields.METADATA.name() + ".notExist=is=null", 1 + 5); // null check
|
|
||||||
}
|
|
||||||
assertRSQLQuery(TargetFields.METADATA.name() + ".metaKey=eq=metaValue", 1);
|
assertRSQLQuery(TargetFields.METADATA.name() + ".metaKey=eq=metaValue", 1);
|
||||||
assertRSQLQuery(TargetFields.METADATA.name() + ".metaKey=eq=null", 4); // null check (1 of the initial five has)
|
assertRSQLQuery(TargetFields.METADATA.name() + ".metaKey=eq=null", 4); // null check (1 of the initial five has)
|
||||||
assertRSQLQuery(TargetFields.METADATA.name() + ".metaKey=eq=*v*", 2);
|
assertRSQLQuery(TargetFields.METADATA.name() + ".metaKey=eq=*v*", 2);
|
||||||
@@ -322,28 +306,14 @@ class RsqlTargetFieldTest extends AbstractJpaIntegrationTest {
|
|||||||
assertRSQLQuery(TargetFields.METADATA.name() + ".notExist!=metaValue", 0);
|
assertRSQLQuery(TargetFields.METADATA.name() + ".notExist!=metaValue", 0);
|
||||||
assertRSQLQuery(TargetFields.METADATA.name() + ".metaKey!=notExist", 2);
|
assertRSQLQuery(TargetFields.METADATA.name() + ".metaKey!=notExist", 2);
|
||||||
|
|
||||||
if (QLSupport.getInstance().getSpecBuilder() != LEGACY_G1) {
|
assertRSQLQuery(TargetFields.METADATA.name() + ".metaKey=not=metaValue", 1);
|
||||||
assertRSQLQuery(
|
assertRSQLQuery(TargetFields.METADATA.name() + ".metaKey=not=null", 2); // null check (2 of the initial five)
|
||||||
TargetFields.METADATA.name() + ".metaKey=not=metaValue",
|
assertRSQLQuery(TargetFields.METADATA.name() + ".notExist=not=metaValue", 0);
|
||||||
QLSupport.getInstance().getSpecBuilder() == LEGACY_G2 ? 1 + 4 : 1);
|
assertRSQLQuery(TargetFields.METADATA.name() + ".metaKey=not=notExist", 2);
|
||||||
assertRSQLQuery(TargetFields.METADATA.name() + ".metaKey=not=null", 2); // null check (2 of the initial five)
|
assertRSQLQuery(TargetFields.METADATA.name() + ".metaKey=ne=metaValue", 1);
|
||||||
assertRSQLQuery(
|
assertRSQLQuery(TargetFields.METADATA.name() + ".metaKey=ne=null", 2); // null check (2 of the initial five)
|
||||||
TargetFields.METADATA.name() + ".notExist=not=metaValue",
|
assertRSQLQuery(TargetFields.METADATA.name() + ".notExist=ne=metaValue", 0);
|
||||||
QLSupport.getInstance().getSpecBuilder() == LEGACY_G2 ? 1 + 5 : 0);
|
assertRSQLQuery(TargetFields.METADATA.name() + ".metaKey=ne=notExist", 2);
|
||||||
assertRSQLQuery(
|
|
||||||
TargetFields.METADATA.name() + ".metaKey=not=notExist",
|
|
||||||
QLSupport.getInstance().getSpecBuilder() == LEGACY_G2 ? 2 + 4 : 2);
|
|
||||||
assertRSQLQuery(
|
|
||||||
TargetFields.METADATA.name() + ".metaKey=ne=metaValue",
|
|
||||||
QLSupport.getInstance().getSpecBuilder() == LEGACY_G2 ? 1 + 4 : 1);
|
|
||||||
assertRSQLQuery(TargetFields.METADATA.name() + ".metaKey=ne=null", 2); // null check (2 of the initial five)
|
|
||||||
assertRSQLQuery(
|
|
||||||
TargetFields.METADATA.name() + ".notExist=ne=metaValue",
|
|
||||||
QLSupport.getInstance().getSpecBuilder() == LEGACY_G2 ? 1 + 5 : 0);
|
|
||||||
assertRSQLQuery(
|
|
||||||
TargetFields.METADATA.name() + ".metaKey=ne=notExist",
|
|
||||||
QLSupport.getInstance().getSpecBuilder() == LEGACY_G2 ? 2 + 4 : 2);
|
|
||||||
}
|
|
||||||
|
|
||||||
assertRSQLQuery(TargetFields.METADATA.name() + ".metaKey=in=(metaValue,notexist)", 1);
|
assertRSQLQuery(TargetFields.METADATA.name() + ".metaKey=in=(metaValue,notexist)", 1);
|
||||||
assertRSQLQuery(TargetFields.METADATA.name() + ".metaKey=out=(metaValue,notexist)", 1);
|
assertRSQLQuery(TargetFields.METADATA.name() + ".metaKey=out=(metaValue,notexist)", 1);
|
||||||
@@ -365,9 +335,8 @@ class RsqlTargetFieldTest extends AbstractJpaIntegrationTest {
|
|||||||
@Test
|
@Test
|
||||||
void testFilterByComplexQueries() {
|
void testFilterByComplexQueries() {
|
||||||
assertRSQLQuery(TargetFields.NAME.name() + "!=targetName123" + AND + TargetFields.METADATA.name() + ".metaKey!=value", 0);
|
assertRSQLQuery(TargetFields.NAME.name() + "!=targetName123" + AND + TargetFields.METADATA.name() + ".metaKey!=value", 0);
|
||||||
assertRSQLQuery(
|
assertRSQLQuery("(" + TargetFields.TAG.name() + "!=TAG1" + OR + TargetFields.TAG.name() + "!=TAG2)" +
|
||||||
"(" + TargetFields.TAG.name() + "!=TAG1" + OR + TargetFields.TAG.name() + "!=TAG2)" +
|
AND + TargetFields.CONTROLLERID.name() + "!=targetId1235", 4);
|
||||||
AND + TargetFields.CONTROLLERID.name() + "!=targetId1235", 4);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -9,9 +9,6 @@
|
|||||||
*/
|
*/
|
||||||
package org.eclipse.hawkbit.repository.jpa.rsql;
|
package org.eclipse.hawkbit.repository.jpa.rsql;
|
||||||
|
|
||||||
import static org.eclipse.hawkbit.repository.jpa.ql.QLSupport.SpecBuilder.LEGACY_G2;
|
|
||||||
import static org.eclipse.hawkbit.repository.jpa.ql.QLSupport.SpecBuilder.G3;
|
|
||||||
import static org.eclipse.hawkbit.repository.jpa.ql.QLSupport.SpecBuilder.LEGACY_G1;
|
|
||||||
import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.NONE;
|
import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.NONE;
|
||||||
|
|
||||||
import jakarta.persistence.EntityManager;
|
import jakarta.persistence.EntityManager;
|
||||||
@@ -132,28 +129,12 @@ class RsqlToSqlTest {
|
|||||||
|
|
||||||
private <T, A extends Enum<A> & QueryField> void print(final Class<T> domainClass, final Class<A> fieldsClass, final String rsql) {
|
private <T, A extends Enum<A> & QueryField> void print(final Class<T> domainClass, final Class<A> fieldsClass, final String rsql) {
|
||||||
System.out.println(rsql);
|
System.out.println(rsql);
|
||||||
final String legacy = rsqlToSQL.toSQL(domainClass, fieldsClass, rsql, LEGACY_G1);
|
System.out.println("\tSQL:\n\t\t" + useStar(rsqlToSQL.toSQL(domainClass, fieldsClass, rsql)));
|
||||||
final String g2 = rsqlToSQL.toSQL(domainClass, fieldsClass, rsql, LEGACY_G2);
|
|
||||||
final String g3 = rsqlToSQL.toSQL(domainClass, fieldsClass, rsql, G3);
|
|
||||||
System.out.println("\tlegacy:\n\t\t" + useStar(legacy));
|
|
||||||
System.out.println("\tG2:\n\t\t" + useStar(g2));
|
|
||||||
System.out.println("\tG3:\n\t\t" + useStar(g3));
|
|
||||||
if (!g2.equals(g3)) {
|
|
||||||
System.out.println("\tG2 != G3 for rsql: " + rsql);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private <T, A extends Enum<A> & QueryField> void printFrom(final Class<T> domainClass, final Class<A> fieldsClass, final String rsql) {
|
private <T, A extends Enum<A> & QueryField> void printFrom(final Class<T> domainClass, final Class<A> fieldsClass, final String rsql) {
|
||||||
System.out.println(rsql);
|
System.out.println(rsql);
|
||||||
final String legacy = rsqlToSQL.toSQL(domainClass, fieldsClass, rsql, LEGACY_G1);
|
System.out.println("\tSQL:\n\t\t" + from(rsqlToSQL.toSQL(domainClass, fieldsClass, rsql)));
|
||||||
final String g2 = rsqlToSQL.toSQL(domainClass, fieldsClass, rsql, LEGACY_G2);
|
|
||||||
final String g3 = rsqlToSQL.toSQL(domainClass, fieldsClass, rsql, G3);
|
|
||||||
System.out.println("\tlegacy:\n\t\t" + from(legacy));
|
|
||||||
System.out.println("\tG2:\n\t\t" + from(g2));
|
|
||||||
System.out.println("\tG2:\n\t\t" + from(g3));
|
|
||||||
if (!g2.equals(g3)) {
|
|
||||||
System.out.println("\tG2 != G3 for rsql: " + rsql);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static String useStar(final String sql) {
|
private static String useStar(final String sql) {
|
||||||
|
|||||||
Reference in New Issue
Block a user