* [#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:
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user