[#2176] RSQL filtering with exist/not-exist support (#2396)

* [#2176] RSQL filtering with exist/not-exist support

Signed-off-by: Avgustin Marinov <Avgustin.Marinov@bosch.com>

* [#2176] Big Refactoring

* RSQL: all maps with joins with on

---------

Signed-off-by: Avgustin Marinov <Avgustin.Marinov@bosch.com>
This commit is contained in:
Avgustin Marinov
2025-05-16 16:13:04 +03:00
committed by GitHub
parent c0e89fbbee
commit 12140e468d
24 changed files with 518 additions and 616 deletions

View File

@@ -146,7 +146,6 @@ import org.eclipse.hawkbit.repository.jpa.rollout.condition.RolloutGroupEvaluati
import org.eclipse.hawkbit.repository.jpa.rollout.condition.StartNextGroupRolloutGroupSuccessAction;
import org.eclipse.hawkbit.repository.jpa.rollout.condition.ThresholdRolloutGroupErrorCondition;
import org.eclipse.hawkbit.repository.jpa.rollout.condition.ThresholdRolloutGroupSuccessCondition;
import org.eclipse.hawkbit.repository.jpa.rsql.DefaultRsqlVisitorFactory;
import org.eclipse.hawkbit.repository.model.DistributionSet;
import org.eclipse.hawkbit.repository.model.DistributionSetType;
import org.eclipse.hawkbit.repository.model.Rollout;
@@ -159,7 +158,6 @@ import org.eclipse.hawkbit.repository.model.helper.EventPublisherHolder;
import org.eclipse.hawkbit.repository.model.helper.SystemSecurityContextHolder;
import org.eclipse.hawkbit.repository.model.helper.TenantConfigurationManagementHolder;
import org.eclipse.hawkbit.repository.rsql.RsqlConfigHolder;
import org.eclipse.hawkbit.repository.rsql.RsqlVisitorFactory;
import org.eclipse.hawkbit.repository.rsql.VirtualPropertyReplacer;
import org.eclipse.hawkbit.security.HawkbitSecurityProperties;
import org.eclipse.hawkbit.security.SecurityTokenGenerator;
@@ -656,10 +654,11 @@ public class RepositoryApplicationConfiguration {
final DistributionSetManagement distributionSetManagement, final QuotaManagement quotaManagement,
final JpaProperties properties, final TenantConfigurationManagement tenantConfigurationManagement,
final RepositoryProperties repositoryProperties,
final SystemSecurityContext systemSecurityContext, final ContextAware contextAware, final AuditorAware<String> auditorAware) {
final SystemSecurityContext systemSecurityContext, final ContextAware contextAware, final AuditorAware<String> auditorAware,
final EntityManager entityManager) {
return new JpaTargetFilterQueryManagement(targetFilterQueryRepository, targetManagement,
virtualPropertyReplacer, distributionSetManagement, quotaManagement, properties.getDatabase(),
tenantConfigurationManagement, repositoryProperties, systemSecurityContext, contextAware, auditorAware);
tenantConfigurationManagement, repositoryProperties, systemSecurityContext, contextAware, auditorAware, entityManager);
}
/**
@@ -1014,24 +1013,13 @@ public class RepositoryApplicationConfiguration {
return new RolloutScheduler(rolloutHandler, systemManagement, systemSecurityContext, threadPoolSize, meterRegistry);
}
/**
* Creates the {@link RsqlVisitorFactory} bean.
*
* @return A new {@link RsqlVisitorFactory} bean.
*/
@Bean
@ConditionalOnMissingBean
RsqlVisitorFactory rsqlVisitorFactory() {
return new DefaultRsqlVisitorFactory();
}
/**
* Obtains the {@link RsqlConfigHolder} bean.
*
* @return The {@link RsqlConfigHolder} singleton.
*/
@Bean
RsqlConfigHolder rsqlVisitorFactoryHolder() {
RsqlConfigHolder rsqlConfigHolder() {
return RsqlConfigHolder.getInstance();
}

View File

@@ -14,6 +14,7 @@ import java.util.Collections;
import java.util.List;
import java.util.Optional;
import jakarta.persistence.EntityManager;
import jakarta.validation.constraints.NotNull;
import cz.jirutka.rsql.parser.RSQLParserException;
@@ -39,6 +40,7 @@ import org.eclipse.hawkbit.repository.jpa.acm.AccessController;
import org.eclipse.hawkbit.repository.jpa.builder.JpaTargetFilterQueryCreate;
import org.eclipse.hawkbit.repository.jpa.configuration.Constants;
import org.eclipse.hawkbit.repository.jpa.model.JpaDistributionSet;
import org.eclipse.hawkbit.repository.jpa.model.JpaTarget;
import org.eclipse.hawkbit.repository.jpa.model.JpaTargetFilterQuery;
import org.eclipse.hawkbit.repository.jpa.repository.TargetFilterQueryRepository;
import org.eclipse.hawkbit.repository.jpa.rsql.RSQLUtility;
@@ -83,6 +85,7 @@ public class JpaTargetFilterQueryManagement implements TargetFilterQueryManageme
private final SystemSecurityContext systemSecurityContext;
private final ContextAware contextAware;
private final AuditorAware<String> auditorAware;
private final EntityManager entityManager;
private final Database database;
@SuppressWarnings("java:S107")
@@ -91,7 +94,8 @@ public class JpaTargetFilterQueryManagement implements TargetFilterQueryManageme
final DistributionSetManagement distributionSetManagement, final QuotaManagement quotaManagement,
final Database database, final TenantConfigurationManagement tenantConfigurationManagement,
final RepositoryProperties repositoryProperties,
final SystemSecurityContext systemSecurityContext, final ContextAware contextAware, final AuditorAware<String> auditorAware) {
final SystemSecurityContext systemSecurityContext, final ContextAware contextAware, final AuditorAware<String> auditorAware,
final EntityManager entityManager) {
this.targetFilterQueryRepository = targetFilterQueryRepository;
this.targetManagement = targetManagement;
this.virtualPropertyReplacer = virtualPropertyReplacer;
@@ -103,6 +107,7 @@ public class JpaTargetFilterQueryManagement implements TargetFilterQueryManageme
this.systemSecurityContext = systemSecurityContext;
this.contextAware = contextAware;
this.auditorAware = auditorAware;
this.entityManager = entityManager;
}
@Override
@@ -114,7 +119,7 @@ public class JpaTargetFilterQueryManagement implements TargetFilterQueryManageme
create.getQuery().ifPresent(query -> {
// validate the RSQL query syntax
RSQLUtility.validateRsqlFor(query, TargetFields.class);
RSQLUtility.validateRsqlFor(query, TargetFields.class, JpaTarget.class, virtualPropertyReplacer, entityManager);
// enforce the 'max targets per auto assign' quota right here even
// if the result of the filter query can vary over time
@@ -143,7 +148,7 @@ public class JpaTargetFilterQueryManagement implements TargetFilterQueryManageme
@Override
public boolean verifyTargetFilterQuerySyntax(final String query) {
try {
RSQLUtility.validateRsqlFor(query, TargetFields.class);
RSQLUtility.validateRsqlFor(query, TargetFields.class, JpaTarget.class, virtualPropertyReplacer, entityManager);
return true;
} catch (final RSQLParserException | RSQLParameterUnsupportedFieldException e) {
log.debug("The RSQL query '{}}' is invalid.", query, e);

View File

@@ -694,13 +694,12 @@ public class JpaTargetManagement implements TargetManagement {
@Override
public boolean isTargetMatchingQueryAndDSNotAssignedAndCompatibleAndUpdatable(final String controllerId,
final long distributionSetId, final String targetFilterQuery) {
RSQLUtility.validateRsqlFor(targetFilterQuery, TargetFields.class);
RSQLUtility.validateRsqlFor(targetFilterQuery, TargetFields.class, JpaTarget.class, virtualPropertyReplacer, entityManager);
final DistributionSet ds = distributionSetManagement.get(distributionSetId)
.orElseThrow(() -> new EntityNotFoundException(DistributionSet.class, distributionSetId));
final Long distSetTypeId = ds.getType().getId();
final List<Specification<JpaTarget>> specList = Arrays.asList(
RSQLUtility.buildRsqlSpecification(targetFilterQuery, TargetFields.class, virtualPropertyReplacer,
database),
RSQLUtility.buildRsqlSpecification(targetFilterQuery, TargetFields.class, virtualPropertyReplacer, database),
TargetSpecifications.hasNotDistributionSetInActions(distributionSetId),
TargetSpecifications.isCompatibleWithDistributionSetType(distSetTypeId),
TargetSpecifications.hasControllerId(controllerId));

View File

@@ -188,8 +188,8 @@ public class JpaTarget extends AbstractJpaNamedEntity implements Target, EventAw
name = "sp_target_attributes",
joinColumns = { @JoinColumn(name = "target", nullable = false) },
foreignKey = @ForeignKey(value = ConstraintMode.CONSTRAINT, name = "fk_target_attributes_target"))
@Column(name = "attribute_value", length = Target.CONTROLLER_ATTRIBUTE_VALUE_SIZE)
@MapKeyColumn(name = "attribute_key", length = Target.CONTROLLER_ATTRIBUTE_KEY_SIZE)
@Column(name = "attribute_value", length = Target.CONTROLLER_ATTRIBUTE_VALUE_SIZE)
private Map<String, String> controllerAttributes;
@OneToMany(mappedBy = "target", fetch = FetchType.LAZY, cascade = { CascadeType.REMOVE }, targetEntity = JpaTargetMetadata.class)

View File

@@ -32,7 +32,7 @@ public abstract class AbstractRSQLVisitor<A extends Enum<A> & RsqlQueryField> {
}
@SuppressWarnings("java:S1066") // java:S1066 - more readable with separate "if" statements
protected QuertPath getQuertPath(final ComparisonNode node) {
protected QueryPath getQueryPath(final ComparisonNode node) {
final int firstSeparatorIndex = node.getSelector().indexOf(RsqlQueryField.SUB_ATTRIBUTE_SEPARATOR);
final String enumName = (firstSeparatorIndex == -1
? node.getSelector()
@@ -67,7 +67,7 @@ public abstract class AbstractRSQLVisitor<A extends Enum<A> & RsqlQueryField> {
}
}
return new QuertPath(enumValue, split);
return new QueryPath(enumValue, split);
} catch (final IllegalArgumentException e) {
throw createRSQLParameterUnsupportedException(node, e);
}
@@ -128,12 +128,12 @@ public abstract class AbstractRSQLVisitor<A extends Enum<A> & RsqlQueryField> {
}
@Value
protected class QuertPath {
protected class QueryPath {
A enumValue;
String[] jpaPath;
private QuertPath(final A enumValue, final String[] jpaPath) {
private QueryPath(final A enumValue, final String[] jpaPath) {
this.enumValue = enumValue;
this.jpaPath = jpaPath;
}

View File

@@ -1,28 +0,0 @@
/**
* Copyright (c) 2021 Bosch.IO GmbH and others
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.eclipse.hawkbit.repository.jpa.rsql;
import cz.jirutka.rsql.parser.ast.RSQLVisitor;
import org.eclipse.hawkbit.repository.RsqlQueryField;
import org.eclipse.hawkbit.repository.rsql.RsqlVisitorFactory;
/**
* Factory providing {@link RSQLVisitor} instances which validate the nodes
* based on a given {@link RsqlQueryField}.
*/
public class DefaultRsqlVisitorFactory implements RsqlVisitorFactory {
@Override
public <A extends Enum<A> & RsqlQueryField> RSQLVisitor<Void, String> validationRsqlVisitor(
final Class<A> fieldNameProvider) {
return new FieldValidationRsqlVisitor<>(fieldNameProvider);
}
}

View File

@@ -1,58 +0,0 @@
/**
* Copyright (c) 2021 Bosch.IO GmbH and others
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.eclipse.hawkbit.repository.jpa.rsql;
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.OrNode;
import cz.jirutka.rsql.parser.ast.RSQLVisitor;
import org.eclipse.hawkbit.repository.RsqlQueryField;
/**
* {@link RSQLVisitor} implementation which validates the nodes (fields) based
* on a given {@link RsqlQueryField} for a given entity type.
*
* @param <A> The type the {@link RsqlQueryField} refers to.
*/
public class FieldValidationRsqlVisitor<A extends Enum<A> & RsqlQueryField> extends AbstractRSQLVisitor<A>
implements RSQLVisitor<Void, String> {
/**
* Constructs the visitor and initializes it.
*
* @param fieldNameProvider The {@link RsqlQueryField} to use for validation.
*/
public FieldValidationRsqlVisitor(final Class<A> fieldNameProvider) {
super(fieldNameProvider);
}
@Override
public Void visit(final AndNode node, final String param) {
return visitNode(node, param);
}
@Override
public Void visit(final OrNode node, final String param) {
return visitNode(node, param);
}
@Override
public Void visit(final ComparisonNode node, final String param) {
// get AND validates
getQuertPath(node);
return null;
}
private Void visitNode(final LogicalNode node, final String param) {
node.getChildren().forEach(child -> child.accept(this, param));
return null;
}
}

View File

@@ -21,7 +21,6 @@ import java.util.Optional;
import java.util.Set;
import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.stream.Collectors;
import jakarta.persistence.criteria.CriteriaBuilder;
import jakarta.persistence.criteria.CriteriaQuery;
@@ -82,14 +81,15 @@ public class JpaQueryRsqlVisitor<A extends Enum<A> & RsqlQueryField, T> extends
private final Database database;
private final boolean ensureIgnoreCase;
private final Root<T> root;
private final SimpleTypeConverter simpleTypeConverter;
private final VirtualPropertyReplacer virtualPropertyReplacer;
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,
public JpaQueryRsqlVisitor(
final Root<T> root, final CriteriaBuilder cb, final Class<A> enumType,
final VirtualPropertyReplacer virtualPropertyReplacer, final Database database,
final CriteriaQuery<?> query, final boolean ensureIgnoreCase) {
super(enumType);
@@ -129,7 +129,7 @@ public class JpaQueryRsqlVisitor<A extends Enum<A> & RsqlQueryField, T> extends
// https://jira.sonarsource.com/browse/SONARJAVA-1478
@SuppressWarnings({ "squid:S2095" })
public List<Predicate> visit(final ComparisonNode node, final String param) {
final QuertPath queryPath = getQuertPath(node);
final QueryPath queryPath = getQueryPath(node);
final List<String> values = node.getArguments();
final List<Object> transformedValues = new ArrayList<>();
@@ -266,7 +266,7 @@ public class JpaQueryRsqlVisitor<A extends Enum<A> & RsqlQueryField, T> extends
* @return the Path for a field
*/
@SuppressWarnings("unchecked")
private Path<Object> getFieldPath(final A enumField, final QuertPath queryPath) {
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));
@@ -303,7 +303,7 @@ public class JpaQueryRsqlVisitor<A extends Enum<A> & RsqlQueryField, T> extends
return transformEnumValue(node, value, javaType);
}
if (fieldName instanceof FieldValueConverter) {
return convertFieldConverterValue(node, fieldName, value);
return convertFieldConverterValue(fieldName, value);
}
if (Boolean.TYPE.equals(javaType)) {
@@ -317,28 +317,24 @@ public class JpaQueryRsqlVisitor<A extends Enum<A> & RsqlQueryField, T> extends
try {
return simpleTypeConverter.convertIfNecessary(value, javaType);
} catch (final TypeMismatchException e) {
throw new RSQLParameterSyntaxException(
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 {",
+ "} is not well formed. Only a boolean (true or false) value will be expected",
e);
}
}
@SuppressWarnings({ "rawtypes", "unchecked" })
private Object convertFieldConverterValue(final ComparisonNode node, final A fieldName, final String value) {
final Object convertedValue = ((FieldValueConverter) fieldName).convertValue(fieldName, value);
if (convertedValue == null) {
throw new RSQLParameterUnsupportedFieldException(
"field {" + node.getSelector() + "} must be one of the following values {"
+ Arrays.toString(((FieldValueConverter) fieldName).possibleValues(fieldName)) + "}",
null);
} else {
return convertedValue;
private Object convertFieldConverterValue(final A fieldName, final String value) {
try {
return ((FieldValueConverter) fieldName).convertValue(fieldName, value);
} catch (final Exception e) {
throw new RSQLParameterSyntaxException(e.getMessage(), null);
}
}
private List<Predicate> mapToPredicate(final ComparisonNode node, final Path<Object> fieldPath,
final List<String> values, final List<Object> transformedValues, final QuertPath queryPath) {
final List<String> values, final List<Object> transformedValues, final QueryPath queryPath) {
String value = values.get(0);
// if lookup is available, replace macros ...
@@ -355,7 +351,7 @@ public class JpaQueryRsqlVisitor<A extends Enum<A> & RsqlQueryField, T> extends
}
private Predicate addOperatorPredicate(final ComparisonNode node, final Path<Object> fieldPath,
final List<Object> transformedValues, final String value, final QuertPath queryPath) {
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
@@ -388,7 +384,7 @@ public class JpaQueryRsqlVisitor<A extends Enum<A> & RsqlQueryField, T> extends
private Predicate getOutPredicate(
final List<Object> transformedValues,
final QuertPath queryPath, final Path<Object> fieldPath) {
final QueryPath queryPath, final Path<Object> fieldPath) {
final String[] fieldNames = queryPath.getJpaPath();
if (isSimpleField(fieldNames, queryPath.getEnumValue().isMap())) {
@@ -412,7 +408,7 @@ public class JpaQueryRsqlVisitor<A extends Enum<A> & RsqlQueryField, T> extends
}
@SuppressWarnings("unchecked")
private Predicate mapToMapPredicate(final Path<Object> fieldPath, final QuertPath queryPath) {
private Predicate mapToMapPredicate(final Path<Object> fieldPath, final QueryPath queryPath) {
if (!queryPath.getEnumValue().isMap()) {
return null;
}
@@ -452,7 +448,7 @@ public class JpaQueryRsqlVisitor<A extends Enum<A> & RsqlQueryField, T> extends
}
private Predicate getNotEqualToPredicate(final Object transformedValue, final Path<Object> fieldPath,
final QuertPath queryPath) {
final QueryPath queryPath) {
if (transformedValue == null) {
return cb.isNotNull(pathOfString(fieldPath));

View File

@@ -9,6 +9,7 @@
*/
package org.eclipse.hawkbit.repository.jpa.rsql;
import java.util.AbstractMap;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
@@ -23,6 +24,7 @@ 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;
@@ -32,9 +34,11 @@ 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;
@@ -43,8 +47,6 @@ import org.eclipse.hawkbit.repository.RsqlQueryField;
import org.eclipse.hawkbit.repository.exception.RSQLParameterSyntaxException;
import org.eclipse.hawkbit.repository.exception.RSQLParameterUnsupportedFieldException;
import org.eclipse.hawkbit.repository.rsql.VirtualPropertyReplacer;
import org.springframework.beans.SimpleTypeConverter;
import org.springframework.beans.TypeMismatchException;
import org.springframework.orm.jpa.vendor.Database;
import org.springframework.util.CollectionUtils;
import org.springframework.util.ObjectUtils;
@@ -70,11 +72,11 @@ public class JpaQueryRsqlVisitorG2<A extends Enum<A> & RsqlQueryField, T>
private final VirtualPropertyReplacer virtualPropertyReplacer;
private final boolean ensureIgnoreCase;
private final SimpleTypeConverter simpleTypeConverter = new SimpleTypeConverter();
private final Map<String, Path<?>> attributeToPath = new HashMap<>();
private boolean inOr;
public JpaQueryRsqlVisitorG2(final Class<A> enumType,
public JpaQueryRsqlVisitorG2(
final Class<A> enumType,
final Root<T> root, final CriteriaQuery<?> query, final CriteriaBuilder cb,
final Database database, final VirtualPropertyReplacer virtualPropertyReplacer, final boolean ensureIgnoreCase) {
super(enumType);
@@ -106,69 +108,71 @@ public class JpaQueryRsqlVisitorG2<A extends Enum<A> & RsqlQueryField, T>
@Override
public List<Predicate> visit(final ComparisonNode node, final String param) {
final QuertPath queryField = getQuertPath(node);
final List<String> values = node.getArguments();
final List<Object> transformedValues = new ArrayList<>();
final Path<?> fieldPath = getFieldPath(root, queryField);
for (final String value : values) {
transformedValues.add(convertValueIfNecessary(node, queryField.getEnumValue(), fieldPath, value));
}
return mapToPredicate(node, queryField, fieldPath, node.getArguments(), transformedValues);
final QueryPath queryPath = getQueryPath(node);
final Path<?> fieldPath = getFieldPath(root, queryPath);
return Collections.singletonList(toPredicate(node, queryPath, fieldPath, getValues(node, queryPath, fieldPath)));
}
@SuppressWarnings({ "rawtypes", "unchecked" })
private static Object transformEnumValue(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(v -> v.name().toLowerCase()).toList()
+ "}", e);
@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() == RSQLUtility.IS) {
// special handling of "not-exists"
if (values.size() != 1) {
throw new RSQLParameterSyntaxException("The operator '" + RSQLUtility.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(getValueFieldPath(queryPath, fieldPath));
}
} else if (node.getOperator() == RSQLUtility.NOT) {
if (values.size() != 1) {
throw new RSQLParameterSyntaxException("The operator '" + RSQLUtility.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));
final Path<?> valueFieldPath = getValueFieldPath(queryPath, fieldPath);
if (values.get(0) == null) {
// special handling of "exists"
return cb.isNotNull(valueFieldPath);
} else {
// special handling or "not equal" or null (same as != but with possible optimized join - no subquery)
return toNotEqualToPredicate(queryPath, valueFieldPath, values.get(0));
}
}
mapEntryKeyPredicate = toMapEntryKeyPredicate(queryPath, fieldPath);
} else {
mapEntryKeyPredicate = null;
}
final Predicate valuePredicate = toOperatorAndValuePredicate(node, queryPath, getValueFieldPath(queryPath, fieldPath), values);
return mapEntryKeyPredicate == null ? valuePredicate : cb.and(mapEntryKeyPredicate, valuePredicate);
}
private static boolean isSimpleField(final String[] split, final boolean isMapKeyField) {
return split.length == 1 || (split.length == 2 && isMapKeyField);
private Predicate toMapEntryKeyPredicate(final QueryPath queryPath, final Path<?> fieldPath) {
final String[] graph = queryPath.getJpaPath();
return equal(mapEntryKeyPath(queryPath, fieldPath), graph[graph.length - 1]);
}
@SuppressWarnings("unchecked")
private static Path<String> pathOfString(final Path<?> path) {
return (Path<String>) path;
}
private static boolean isPattern(final String transformedValue) {
if (transformedValue.contains(ESCAPE_CHAR_WITH_ASTERISK)) {
return transformedValue.replace(ESCAPE_CHAR_WITH_ASTERISK, "$").indexOf(LIKE_WILDCARD) != -1;
} else {
return transformedValue.indexOf(LIKE_WILDCARD) != -1;
private Path<String> mapEntryKeyPath(final QueryPath queryPath, final Path<?> fieldPath) {
if (fieldPath instanceof MapJoin) {
// Currently we support only string key. So below cast is safe.
return (Path<String>) ((MapJoin<?, ?, ?>) fieldPath).key();
}
return fieldPath.get(queryPath.getEnumValue().getSubEntityMapTuple()
.map(Entry::getKey)
.orElseThrow(() -> new UnsupportedOperationException(String.format(
"For the fields, defined as Map, only %s java type or tuple in the form of %s are allowed!",
Map.class.getName(), AbstractMap.SimpleImmutableEntry.class.getName()))));
}
private List<Predicate> mapToPredicate(final ComparisonNode node, final QuertPath queryField,
final Path<?> fieldPath,
final List<String> values, final List<Object> transformedValues) {
// if lookup is available, replace macros ...
final String value = virtualPropertyReplacer == null ? values.get(0) : virtualPropertyReplacer.replace(values.get(0));
final Predicate mapPredicate = queryField.getEnumValue().isMap() ? mapToMapPredicate(queryField, fieldPath) : null;
final Predicate valuePredicate = addOperatorPredicate(node, queryField,
getValueFieldPath(queryField.getEnumValue(), fieldPath), transformedValues, value);
return Collections.singletonList(mapPredicate != null ? cb.and(mapPredicate, valuePredicate) : valuePredicate);
}
private Path<?> getValueFieldPath(final A enumField, final Path<?> fieldPath) {
private Path<?> getValueFieldPath(final QueryPath queryPath, final Path<?> fieldPath) {
final A enumField = queryPath.getEnumValue();
if (enumField.isMap()) {
final Path<?> mapValuePath = enumField.getSubEntityMapTuple().map(Entry::getValue).map(fieldPath::get).orElse(null);
return mapValuePath == null ? fieldPath : mapValuePath;
@@ -177,117 +181,127 @@ public class JpaQueryRsqlVisitorG2<A extends Enum<A> & RsqlQueryField, T>
}
}
@SuppressWarnings("unchecked")
private Predicate mapToMapPredicate(final QuertPath queryField, final Path<?> fieldPath) {
final String[] graph = queryField.getJpaPath();
final String keyValue = graph[graph.length - 1];
if (fieldPath instanceof MapJoin) {
// Currently we support only string key. So below cast is safe.
return equal((Path<String>) (((MapJoin<?, ?, ?>) fieldPath).key()), keyValue);
}
final String keyFieldName = queryField.getEnumValue().getSubEntityMapTuple().map(Entry::getKey)
.orElseThrow(() -> new UnsupportedOperationException(
"For the fields, defined as Map, only Map java type or tuple in the form of " +
"SimpleImmutableEntry are allowed. Neither of those could be found!"));
return equal(fieldPath.get(keyFieldName), keyValue);
}
private Predicate addOperatorPredicate(final ComparisonNode node, final QuertPath queryField,
final Path<?> fieldPath, final List<Object> transformedValues, final String value) {
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.
final Object transformedValue = transformedValues.get(0);
// 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 "==" -> getEqualToPredicate(fieldPath, transformedValue);
case "!=" -> getNotEqualToPredicate(queryField, fieldPath, transformedValue);
case "=gt=" -> cb.greaterThan(pathOfString(fieldPath), value);
case "=ge=" -> cb.greaterThanOrEqualTo(pathOfString(fieldPath), value);
case "=lt=" -> cb.lessThan(pathOfString(fieldPath), value);
case "=le=" -> cb.lessThanOrEqualTo(pathOfString(fieldPath), value);
case "=in=" -> in(pathOfString(fieldPath), transformedValues);
case "=out=" -> getOutPredicate(queryField, fieldPath, transformedValues);
default -> throw new RSQLParameterSyntaxException(
"Operator symbol {" + operator + "} is either not supported or not implemented");
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 getEqualToPredicate(final Path<?> fieldPath, final Object transformedValue) {
if (transformedValue == null) {
private Predicate toEqualToPredicate(final Path<?> fieldPath, final Object value) {
if (value == null) {
return cb.isNull(fieldPath);
}
if ((transformedValue instanceof String transformedValueStr) && !NumberUtils.isCreatable(transformedValueStr)) {
if (ObjectUtils.isEmpty(transformedValue)) {
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(transformedValueStr)) { // a pattern, use like
return like(stringExpression, toSQL(transformedValueStr));
if (isPattern(valueStr)) { // a pattern, use like
return like(stringExpression, toSQL(valueStr));
} else {
return equal(stringExpression, transformedValueStr);
return equal(stringExpression, valueStr);
}
}
return cb.equal(fieldPath, transformedValue);
return cb.equal(fieldPath, value);
}
private Predicate getNotEqualToPredicate(final QuertPath queryField,
final Path<?> fieldPath, final Object transformedValue) {
if (transformedValue == null) {
// 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 ((transformedValue instanceof String transformedValueStr) && !NumberUtils.isCreatable(transformedValueStr)) {
if (ObjectUtils.isEmpty(transformedValue)) {
if (value instanceof String valueStr && !NumberUtils.isCreatable(valueStr)) {
if (ObjectUtils.isEmpty(value)) {
return cb.and(cb.isNotNull(fieldPath), cb.notEqual(pathOfString(fieldPath), ""));
}
final String[] fieldNames = queryField.getJpaPath();
if (isSimpleField(fieldNames, queryField.getEnumValue().isMap())) {
if (isPattern(transformedValueStr)) { // a pattern, use like
return cb.or(cb.isNull(fieldPath), notLike(pathOfString(fieldPath), toSQL(transformedValueStr)));
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, transformedValueStr);
return toNullOrNotEqualPredicate(fieldPath, valueStr);
}
}
return toNotExistsSubQueryPredicate(queryField, fieldPath, expressionToCompare ->
isPattern(transformedValueStr) ? // a pattern, use like
like(expressionToCompare, toSQL(transformedValueStr)) :
equal(expressionToCompare, transformedValueStr));
return toNotExistsSubQueryPredicate(
queryPath, fieldPath, expressionToCompare ->
isPattern(valueStr)
? like(expressionToCompare, toSQL(valueStr)) // a pattern, use like
: equal(expressionToCompare, valueStr));
}
return toNullOrNotEqualPredicate(fieldPath, transformedValue);
return toNullOrNotEqualPredicate(fieldPath, value);
}
private Predicate getOutPredicate(final QuertPath queryField, final Path<?> fieldPath,
final List<Object> transformedValues) {
final String[] subAttributes = queryField.getJpaPath();
if (isSimpleField(subAttributes, queryField.getEnumValue().isMap())) {
return cb.or(cb.isNull(fieldPath), cb.not(in(pathOfString(fieldPath), transformedValues)));
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(queryField, fieldPath, expressionToCompare -> in(expressionToCompare, transformedValues));
return toNotExistsSubQueryPredicate(queryPath, fieldPath, expressionToCompare -> in(expressionToCompare, values));
}
private Path<?> getFieldPath(final Root<?> root, final QuertPath queryField) {
final String[] split = queryField.getJpaPath();
private Path<?> getFieldPath(final Root<?> root, final QueryPath queryPath) {
final String[] split = queryPath.getJpaPath();
Path<?> fieldPath = null;
for (int i = 0, end = queryField.getEnumValue().isMap() ? split.length - 1 : split.length; i < end; i++) {
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 cannot be empty", 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 -> virtualPropertyReplacer == null ? value : virtualPropertyReplacer.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
@@ -306,7 +320,8 @@ public class JpaQueryRsqlVisitorG2<A extends Enum<A> & RsqlQueryField, T>
.findFirst()
.orElseGet(() -> root.join(fieldNameSplit, JoinType.LEFT));
}
} // if a collection - it is a join
}
// 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 {
@@ -314,60 +329,15 @@ public class JpaQueryRsqlVisitorG2<A extends Enum<A> & RsqlQueryField, T>
}
}
private Object convertValueIfNecessary(final ComparisonNode node, final A fieldName, final Path<?> fieldPath, final String value) {
// 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 its 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, javaType, value);
}
if (fieldName instanceof FieldValueConverter) {
return convertFieldConverterValue(node, fieldName, value);
}
if (Boolean.TYPE.equals(javaType) || Boolean.class.equals(javaType)) {
return convertBooleanValue(node, javaType, value);
}
return value;
}
private Object convertBooleanValue(final ComparisonNode node, final Class<?> javaType, final String value) {
try {
return simpleTypeConverter.convertIfNecessary(value, javaType);
} catch (final TypeMismatchException e) {
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 {",
e);
}
}
@SuppressWarnings({ "rawtypes", "unchecked" })
private Object convertFieldConverterValue(final ComparisonNode node, final A fieldName, final String value) {
final Object convertedValue = ((FieldValueConverter) fieldName).convertValue(fieldName, value);
if (convertedValue == null) {
throw new RSQLParameterUnsupportedFieldException(
"field {" + node.getSelector() + "} must be one of the following values {" +
Arrays.toString(((FieldValueConverter) fieldName).possibleValues(fieldName)) + "}",
null);
} else {
return convertedValue;
}
}
private Predicate toNullOrNotEqualPredicate(final Path<?> fieldPath, final Object transformedValue) {
private Predicate toNullOrNotEqualPredicate(final Path<?> fieldPath, final Object value) {
return cb.or(
cb.isNull(fieldPath),
transformedValue instanceof String transformedValueStr
? notEqual(pathOfString(fieldPath), transformedValueStr)
: cb.notEqual(fieldPath, transformedValue));
value instanceof String valueStr ? notEqual(pathOfString(fieldPath), valueStr) : cb.notEqual(fieldPath, value));
}
@SuppressWarnings({ "unchecked", "rawtypes" })
private Predicate toNotExistsSubQueryPredicate(final QuertPath queryField, final Path<?> fieldPath,
final Function<Path<String>, Predicate> subQueryPredicateProvider) {
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
@@ -380,11 +350,11 @@ public class JpaQueryRsqlVisitorG2<A extends Enum<A> & RsqlQueryField, T>
return cb.not(cb.exists(
subquery.select(subqueryRoot)
.where(cb.and(
cb.equal(root.get(queryField.getEnumValue().identifierFieldName()),
subqueryRoot.get(queryField.getEnumValue().identifierFieldName())),
cb.equal(
root.get(queryPath.getEnumValue().identifierFieldName()),
subqueryRoot.get(queryPath.getEnumValue().identifierFieldName())),
subQueryPredicateProvider.apply(
getExpressionToCompare(queryField.getEnumValue(),
getFieldPath(subqueryRoot, queryField)))))));
getExpressionToCompare(queryPath.getEnumValue(), getFieldPath(subqueryRoot, queryPath)))))));
}
@SuppressWarnings({ "rawtypes", "unchecked" })
@@ -405,20 +375,92 @@ public class JpaQueryRsqlVisitorG2<A extends Enum<A> & RsqlQueryField, T>
" Neither of those could be found!"));
}
private String toSQL(final String transformedValue) {
// 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 (enumValue instanceof FieldValueConverter fieldValueConverter) {
try {
return fieldValueConverter.convertValue(enumValue, value);
} catch (final Exception e) {
throw new RSQLParameterUnsupportedFieldException(e.getMessage(), null);
}
}
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 == RSQLUtility.IS || operator == RSQLUtility.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;
if (database == Database.SQL_SERVER) {
escaped = transformedValue.replace("%", "[%]").replace("_", "[_]");
escaped = value.replace("%", "[%]").replace("_", "[_]");
} else {
escaped = transformedValue.replace("%", ESCAPE_CHAR + "%").replace("_", ESCAPE_CHAR + "_");
escaped = value.replace("%", ESCAPE_CHAR + "%").replace("_", ESCAPE_CHAR + "_");
}
return replaceIfRequired(escaped);
}
private String replaceIfRequired(final String escapedValue) {
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, '%')
finalizedValue = escapedValue.replace(ESCAPE_CHAR_WITH_ASTERISK, "$")
.replace(LIKE_WILDCARD, '%')
.replace("$", ESCAPE_CHAR_WITH_ASTERISK);
} else {
finalizedValue = escapedValue.replace(LIKE_WILDCARD, '%');
@@ -448,11 +490,11 @@ public class JpaQueryRsqlVisitorG2<A extends Enum<A> & RsqlQueryField, T>
}
}
private Predicate notEqual(final Path<String> expressionToCompare, String transformedValueStr) {
private Predicate notEqual(final Path<String> expressionToCompare, String valueStr) {
if (caseWise(expressionToCompare)) {
return cb.notEqual(cb.upper(expressionToCompare), transformedValueStr.toUpperCase());
return cb.notEqual(cb.upper(expressionToCompare), valueStr.toUpperCase());
} else {
return cb.notEqual(expressionToCompare, transformedValueStr);
return cb.notEqual(expressionToCompare, valueStr);
}
}
@@ -472,14 +514,14 @@ public class JpaQueryRsqlVisitorG2<A extends Enum<A> & RsqlQueryField, T>
}
}
private Predicate in(final Path<String> expressionToCompare, final List<Object> transformedValues) {
private Predicate in(final Path<String> expressionToCompare, final List<Object> values) {
if (ensureIgnoreCase && expressionToCompare.getJavaType() == String.class) {
final List<String> inParams = transformedValues.stream()
final List<String> inParams = values.stream()
.filter(String.class::isInstance)
.map(String.class::cast).map(String::toUpperCase).toList();
return inParams.isEmpty() ? expressionToCompare.in(transformedValues) : cb.upper(expressionToCompare).in(inParams);
return inParams.isEmpty() ? expressionToCompare.in(values) : cb.upper(expressionToCompare).in(inParams);
} else {
return expressionToCompare.in(transformedValues);
return expressionToCompare.in(values);
}
}

View File

@@ -10,9 +10,12 @@
package org.eclipse.hawkbit.repository.jpa.rsql;
import java.io.Serial;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import jakarta.persistence.EntityManager;
import jakarta.persistence.criteria.CriteriaBuilder;
import jakarta.persistence.criteria.CriteriaQuery;
import jakarta.persistence.criteria.Predicate;
@@ -103,27 +106,27 @@ public final class RSQLUtility {
* @throws RSQLParserException if RSQL syntax is invalid
* @throws RSQLParameterUnsupportedFieldException if RSQL key is not allowed
*/
@SuppressWarnings({"unchecked", "rawtypes"})
public static <A extends Enum<A> & RsqlQueryField> void validateRsqlFor(
final String rsql, final Class<A> fieldNameProvider) {
final RSQLVisitor<Void, String> visitor =
RsqlConfigHolder.getInstance().getRsqlVisitorFactory().validationRsqlVisitor(fieldNameProvider);
final Node rootNode = parseRsql(rsql);
rootNode.accept(visitor);
final String rsql, final Class<A> fieldNameProvider,
final Class<?> jpaType,
final VirtualPropertyReplacer virtualPropertyReplacer, final EntityManager entityManager) {
final CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
final CriteriaQuery<?> criteriaQuery = criteriaBuilder.createQuery(jpaType);
new RSQLSpecification<>(rsql, fieldNameProvider, virtualPropertyReplacer, null)
.toPredicate(criteriaQuery.from((Class)jpaType), criteriaQuery, criteriaBuilder);
}
private static Node parseRsql(final String rsql) {
log.debug("Parsing rsql string {}", rsql);
try {
final Set<ComparisonOperator> operators = RSQLOperators.defaultOperators();
return new RSQLParser(operators).parse(
RsqlConfigHolder.getInstance().isCaseInsensitiveDB() || RsqlConfigHolder.getInstance().isIgnoreCase()
? rsql.toLowerCase()
: rsql);
} catch (final IllegalArgumentException e) {
throw new RSQLParameterSyntaxException("RSQL filter must not be null", e);
} catch (final RSQLParserException e) {
throw new RSQLParameterSyntaxException(e);
}
static final ComparisonOperator IS = new ComparisonOperator("=is=", "=eq=");
static final ComparisonOperator NOT = new ComparisonOperator("=not=", "=ne=");
private 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 static final class RSQLSpecification<A extends Enum<A> & RsqlQueryField, T> implements Specification<T> {
@@ -136,7 +139,8 @@ public final class RSQLUtility {
private final VirtualPropertyReplacer virtualPropertyReplacer;
private final Database database;
private RSQLSpecification(final String rsql, final Class<A> enumType,
private RSQLSpecification(
final String rsql, final Class<A> enumType,
final VirtualPropertyReplacer virtualPropertyReplacer, final Database database) {
this.rsql = rsql;
this.enumType = enumType;
@@ -150,17 +154,16 @@ public final class RSQLUtility {
query.distinct(true);
final RSQLVisitor<List<Predicate>, String> jpqQueryRSQLVisitor =
RsqlConfigHolder.getInstance().isLegacyRsqlVisitor() ?
new JpaQueryRsqlVisitor<>(
RsqlConfigHolder.getInstance().isLegacyRsqlVisitor()
? new JpaQueryRsqlVisitor<>(
root, cb, enumType,
virtualPropertyReplacer, database, query,
!RsqlConfigHolder.getInstance().isCaseInsensitiveDB() && RsqlConfigHolder.getInstance().isIgnoreCase())
:
new JpaQueryRsqlVisitorG2<>(
enumType, root, query, cb,
database, virtualPropertyReplacer,
!RsqlConfigHolder.getInstance().isCaseInsensitiveDB() && RsqlConfigHolder.getInstance()
.isIgnoreCase());
: new JpaQueryRsqlVisitorG2<>(
enumType, root, query, cb,
database, virtualPropertyReplacer,
!RsqlConfigHolder.getInstance().isCaseInsensitiveDB() && RsqlConfigHolder.getInstance()
.isIgnoreCase());
final List<Predicate> accept = rootNode.accept(jpqQueryRSQLVisitor);
if (CollectionUtils.isEmpty(accept)) {
@@ -170,4 +173,18 @@ public final class RSQLUtility {
}
}
}
private static Node parseRsql(final String rsql) {
log.debug("Parsing rsql string {}", rsql);
try {
return new RSQLParser(OPERATORS).parse(
RsqlConfigHolder.getInstance().isCaseInsensitiveDB() || RsqlConfigHolder.getInstance().isIgnoreCase()
? rsql.toLowerCase()
: rsql);
} catch (final IllegalArgumentException e) {
throw new RSQLParameterSyntaxException("RSQL filter must not be null", e);
} catch (final RSQLParserException e) {
throw new RSQLParameterSyntaxException(e);
}
}
}