* [#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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,8 @@ import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
|
||||
import jakarta.persistence.EntityManager;
|
||||
|
||||
import io.qameta.allure.Description;
|
||||
import io.qameta.allure.Feature;
|
||||
import io.qameta.allure.Story;
|
||||
@@ -23,20 +25,29 @@ import org.eclipse.hawkbit.repository.SoftwareModuleFields;
|
||||
import org.eclipse.hawkbit.repository.exception.RSQLParameterSyntaxException;
|
||||
import org.eclipse.hawkbit.repository.exception.RSQLParameterUnsupportedFieldException;
|
||||
import org.eclipse.hawkbit.repository.jpa.AbstractJpaIntegrationTest;
|
||||
import org.eclipse.hawkbit.repository.jpa.model.JpaDistributionSet;
|
||||
import org.eclipse.hawkbit.repository.model.DistributionSet;
|
||||
import org.eclipse.hawkbit.repository.model.DistributionSetTag;
|
||||
import org.eclipse.hawkbit.repository.model.SoftwareModule;
|
||||
import org.eclipse.hawkbit.repository.rsql.VirtualPropertyReplacer;
|
||||
import org.eclipse.hawkbit.repository.test.util.TestdataFactory;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.PageRequest;
|
||||
import org.springframework.orm.jpa.vendor.Database;
|
||||
|
||||
@Feature("Component Tests - Repository")
|
||||
@Story("RSQL filter distribution set")
|
||||
@SuppressWarnings("java:S6813") // constructor injects are not possible for test classes
|
||||
class RSQLDistributionSetFieldTest extends AbstractJpaIntegrationTest {
|
||||
|
||||
@Autowired
|
||||
protected VirtualPropertyReplacer virtualPropertyReplacer;
|
||||
@Autowired
|
||||
protected EntityManager entityManager;
|
||||
|
||||
private DistributionSet ds;
|
||||
private SoftwareModule sm;
|
||||
|
||||
@@ -190,11 +201,14 @@ class RSQLDistributionSetFieldTest extends AbstractJpaIntegrationTest {
|
||||
assertRSQLQuery(DistributionSetFields.METADATA.name() + ".key*==value.dot", 0);
|
||||
assertRSQLQuery(DistributionSetFields.METADATA.name() + ".*==value.dot", 0);
|
||||
assertRSQLQuery(DistributionSetFields.METADATA.name() + "..==value.dot", 0);
|
||||
assertRSQLQueryThrowsException(DistributionSetFields.METADATA.name() + ".==value.dot",
|
||||
assertRSQLQueryThrowsException(
|
||||
DistributionSetFields.METADATA.name() + ".==value.dot",
|
||||
RSQLParameterUnsupportedFieldException.class);
|
||||
assertRSQLQueryThrowsException(DistributionSetFields.METADATA.name() + "*==value.dot",
|
||||
assertRSQLQueryThrowsException(
|
||||
DistributionSetFields.METADATA.name() + "*==value.dot",
|
||||
RSQLParameterUnsupportedFieldException.class);
|
||||
assertRSQLQueryThrowsException(DistributionSetFields.METADATA.name() + "==value.dot",
|
||||
assertRSQLQueryThrowsException(
|
||||
DistributionSetFields.METADATA.name() + "==value.dot",
|
||||
RSQLParameterUnsupportedFieldException.class);
|
||||
|
||||
}
|
||||
@@ -206,18 +220,15 @@ class RSQLDistributionSetFieldTest extends AbstractJpaIntegrationTest {
|
||||
assertThat(countAll).as("Found entity size is wrong").isEqualTo(expectedEntity);
|
||||
}
|
||||
|
||||
private <T extends Throwable> void assertRSQLQueryThrowsException(final String rsqlParam,
|
||||
final Class<T> expectedException) {
|
||||
private <T extends Throwable> void assertRSQLQueryThrowsException(final String rsqlParam, final Class<T> expectedException) {
|
||||
assertThatExceptionOfType(expectedException)
|
||||
.isThrownBy(() -> RSQLUtility.validateRsqlFor(rsqlParam, DistributionSetFields.class));
|
||||
.isThrownBy(() -> RSQLUtility.validateRsqlFor(
|
||||
rsqlParam, DistributionSetFields.class, JpaDistributionSet.class, virtualPropertyReplacer, entityManager));
|
||||
}
|
||||
|
||||
private DistributionSet createDistributionSetWithMetadata(final String metadataKeyName,
|
||||
final String metadataValue) {
|
||||
private DistributionSet createDistributionSetWithMetadata(final String metadataKeyName, final String metadataValue) {
|
||||
final DistributionSet distributionSet = testdataFactory.createDistributionSet();
|
||||
createDistributionSetMetadata(distributionSet.getId(),
|
||||
entityFactory.generateDsMetadata(metadataKeyName, metadataValue));
|
||||
createDistributionSetMetadata(distributionSet.getId(), entityFactory.generateDsMetadata(metadataKeyName, metadataValue));
|
||||
return distributionSet;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -11,35 +11,44 @@ package org.eclipse.hawkbit.repository.jpa.rsql;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
import static org.junit.jupiter.api.Assertions.fail;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import jakarta.persistence.EntityManager;
|
||||
|
||||
import io.qameta.allure.Description;
|
||||
import io.qameta.allure.Feature;
|
||||
import io.qameta.allure.Story;
|
||||
import org.assertj.core.util.Maps;
|
||||
import org.eclipse.hawkbit.repository.TargetFields;
|
||||
import org.eclipse.hawkbit.repository.TargetTypeFields;
|
||||
import org.eclipse.hawkbit.repository.exception.RSQLParameterUnsupportedFieldException;
|
||||
import org.eclipse.hawkbit.repository.jpa.AbstractJpaIntegrationTest;
|
||||
import org.eclipse.hawkbit.repository.jpa.model.JpaTarget;
|
||||
import org.eclipse.hawkbit.repository.model.DistributionSet;
|
||||
import org.eclipse.hawkbit.repository.model.Target;
|
||||
import org.eclipse.hawkbit.repository.model.TargetTag;
|
||||
import org.eclipse.hawkbit.repository.model.TargetType;
|
||||
import org.eclipse.hawkbit.repository.rsql.VirtualPropertyReplacer;
|
||||
import org.eclipse.hawkbit.repository.test.util.TestdataFactory;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.data.domain.Slice;
|
||||
|
||||
@Feature("Component Tests - Repository")
|
||||
@Story("RSQL filter target")
|
||||
@SuppressWarnings("java:S6813") // constructor injects are not possible for test classes
|
||||
class RSQLTargetFieldTest extends AbstractJpaIntegrationTest {
|
||||
|
||||
private static final String OR = ",";
|
||||
private static final String AND = ";";
|
||||
|
||||
@Autowired
|
||||
protected VirtualPropertyReplacer virtualPropertyReplacer;
|
||||
@Autowired
|
||||
protected EntityManager entityManager;
|
||||
|
||||
private Target target;
|
||||
private Target target2;
|
||||
private TargetType targetType1;
|
||||
@@ -47,45 +56,6 @@ class RSQLTargetFieldTest extends AbstractJpaIntegrationTest {
|
||||
|
||||
@BeforeEach
|
||||
void setupBeforeTest() {
|
||||
final DistributionSet ds = testdataFactory.createDistributionSet("AssignedDs");
|
||||
|
||||
final Map<String, String> attributes = new HashMap<>();
|
||||
|
||||
target = targetManagement.create(entityFactory.target().create().controllerId("targetId123")
|
||||
.name("targetName123").description("targetDesc123"));
|
||||
attributes.put("revision", "1.1");
|
||||
target = controllerManagement.updateControllerAttributes(target.getControllerId(), attributes, null);
|
||||
target = controllerManagement.findOrRegisterTargetIfItDoesNotExist(target.getControllerId(), LOCALHOST);
|
||||
createTargetMetadata(target.getControllerId(), entityFactory.generateTargetMetadata("metaKey", "metaValue"));
|
||||
|
||||
target2 = targetManagement
|
||||
.create(entityFactory.target().create().controllerId("targetId1234").description("targetId1234"));
|
||||
attributes.put("revision", "1.2");
|
||||
|
||||
target2 = controllerManagement.updateControllerAttributes(target2.getControllerId(), attributes, null);
|
||||
target2 = controllerManagement.findOrRegisterTargetIfItDoesNotExist(target2.getControllerId(), LOCALHOST);
|
||||
createTargetMetadata(target2.getControllerId(), entityFactory.generateTargetMetadata("metaKey", "value"));
|
||||
|
||||
final Target target3 = testdataFactory.createTarget("targetId1235");
|
||||
final Target target4 = testdataFactory.createTarget("targetId1236");
|
||||
testdataFactory.createTarget("targetId1237");
|
||||
|
||||
final TargetTag targetTag = targetTagManagement.create(entityFactory.tag().create().name("Tag1"));
|
||||
final TargetTag targetTag2 = targetTagManagement.create(entityFactory.tag().create().name("Tag2"));
|
||||
final TargetTag targetTag3 = targetTagManagement.create(entityFactory.tag().create().name("Tag3"));
|
||||
targetTagManagement.create(entityFactory.tag().create().name("Tag4"));
|
||||
|
||||
targetManagement.assignTag(Arrays.asList(target.getControllerId(), target2.getControllerId()),
|
||||
targetTag.getId());
|
||||
|
||||
targetManagement.assignTag(Arrays.asList(target3.getControllerId(), target4.getControllerId()),
|
||||
targetTag2.getId());
|
||||
targetManagement.assignTag(
|
||||
Arrays.asList(target.getControllerId(), target3.getControllerId(), target4.getControllerId()),
|
||||
targetTag3.getId());
|
||||
|
||||
assignDistributionSet(ds.getId(), target.getControllerId());
|
||||
|
||||
targetType1 = targetTypeManagement
|
||||
.create(entityFactory.targetType().create()
|
||||
.name("Type1").description("Desc. Type1")
|
||||
@@ -95,8 +65,43 @@ class RSQLTargetFieldTest extends AbstractJpaIntegrationTest {
|
||||
.name("Type2").description("Desc. Type2")
|
||||
.key("Type2.key"));
|
||||
|
||||
final DistributionSet ds = testdataFactory.createDistributionSet("AssignedDs");
|
||||
|
||||
final TargetTag targetTag = targetTagManagement.create(entityFactory.tag().create().name("Tag1"));
|
||||
final TargetTag targetTag2 = targetTagManagement.create(entityFactory.tag().create().name("Tag2"));
|
||||
final TargetTag targetTag3 = targetTagManagement.create(entityFactory.tag().create().name("Tag3"));
|
||||
targetTagManagement.create(entityFactory.tag().create().name("Tag4"));
|
||||
|
||||
target = targetManagement.create(
|
||||
entityFactory.target().create()
|
||||
.controllerId("targetId123")
|
||||
.name("targetName123")
|
||||
.description("targetDesc123"));
|
||||
target = controllerManagement.updateControllerAttributes(target.getControllerId(), Map.of("revision", "1.1"), null);
|
||||
target = controllerManagement.findOrRegisterTargetIfItDoesNotExist(target.getControllerId(), LOCALHOST);
|
||||
createTargetMetadata(target.getControllerId(), entityFactory.generateTargetMetadata("metaKey", "metaValue"));
|
||||
assignDistributionSet(ds.getId(), target.getControllerId());
|
||||
targetManagement.assignType(target.getControllerId(), targetType1.getId());
|
||||
|
||||
target2 = targetManagement.create(
|
||||
entityFactory.target().create()
|
||||
.controllerId("targetId1234")
|
||||
.description("targetId1234"));
|
||||
target2 = controllerManagement.updateControllerAttributes(target2.getControllerId(), Map.of("revision", "1.2"), null);
|
||||
targetManagement.assignType(target2.getControllerId(), targetType2.getId());
|
||||
createTargetMetadata(target2.getControllerId(), entityFactory.generateTargetMetadata("metaKey", "value"));
|
||||
target2 = controllerManagement.findOrRegisterTargetIfItDoesNotExist(target2.getControllerId(), LOCALHOST);
|
||||
targetManagement.assignTag(Arrays.asList(target.getControllerId(), target2.getControllerId()), targetTag.getId());
|
||||
|
||||
final Target target3 = testdataFactory.createTarget("targetId1235");
|
||||
|
||||
final Target target4 = testdataFactory.createTarget("targetId1236");
|
||||
testdataFactory.createTarget("targetId1237");
|
||||
targetManagement.assignTag(Arrays.asList(target3.getControllerId(), target4.getControllerId()), targetTag2.getId());
|
||||
|
||||
targetManagement.assignTag(
|
||||
Arrays.asList(target.getControllerId(), target3.getControllerId(), target4.getControllerId()),
|
||||
targetTag3.getId());
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -159,8 +164,38 @@ class RSQLTargetFieldTest extends AbstractJpaIntegrationTest {
|
||||
@Test
|
||||
@Description("Test filter target by attribute")
|
||||
void testFilterByAttribute() {
|
||||
controllerManagement.updateControllerAttributes(testdataFactory.createTarget().getControllerId(),
|
||||
Maps.newHashMap("test.dot", "value.dot"), null);
|
||||
controllerManagement.updateControllerAttributes(
|
||||
testdataFactory.createTarget().getControllerId(),
|
||||
Map.of(
|
||||
"test.dot", "value.dot",
|
||||
"test.null", "null"),
|
||||
null);
|
||||
|
||||
assertRSQLQuery(TargetFields.ATTRIBUTE.name() + ".test.dot>=value.dos", 1);
|
||||
assertRSQLQuery(TargetFields.ATTRIBUTE.name() + ".test.dot==value.dot", 1);
|
||||
assertRSQLQuery(TargetFields.ATTRIBUTE.name() + ".test.null==null", 1); // "null" check
|
||||
assertRSQLQuery(TargetFields.ATTRIBUTE.name() + ".test.n/a==null", 0); // "null" check
|
||||
|
||||
assertRSQLQuery(TargetFields.ATTRIBUTE.name() + ".test.dot=is=value.dot", 1);
|
||||
assertRSQLQuery(TargetFields.ATTRIBUTE.name() + ".test.null=is=null", 5); // null check
|
||||
assertRSQLQuery(TargetFields.ATTRIBUTE.name() + ".test.n/a=is=null", 1 + 5); // null check
|
||||
assertRSQLQuery(TargetFields.ATTRIBUTE.name() + ".test.dot=eq=value.dot", 1);
|
||||
assertRSQLQuery(TargetFields.ATTRIBUTE.name() + ".test.null=eq=null", 5); // null check
|
||||
assertRSQLQuery(TargetFields.ATTRIBUTE.name() + ".test.n/a=eq=null", 1 + 5); // null check
|
||||
|
||||
assertRSQLQuery(TargetFields.ATTRIBUTE.name() + ".test.dot!=value.dot", 0);
|
||||
assertRSQLQuery(TargetFields.ATTRIBUTE.name() + ".test.null!=null", 0); // "null" check
|
||||
assertRSQLQuery(TargetFields.ATTRIBUTE.name() + ".test.null!=null2", 1); // value check
|
||||
assertRSQLQuery(TargetFields.ATTRIBUTE.name() + ".test.n/a!=null", 0); // "null" check
|
||||
|
||||
assertRSQLQuery(TargetFields.ATTRIBUTE.name() + ".test.dot=not=value.dot", 5);
|
||||
assertRSQLQuery(TargetFields.ATTRIBUTE.name() + ".test.null=not=null", 1); // null check
|
||||
assertRSQLQuery(TargetFields.ATTRIBUTE.name() + ".test.null=not=null2", 1 + 5); // value check
|
||||
assertRSQLQuery(TargetFields.ATTRIBUTE.name() + ".test.n/a=not=null", 0); // null check
|
||||
assertRSQLQuery(TargetFields.ATTRIBUTE.name() + ".test.dot=ne=value.dot", 5);
|
||||
assertRSQLQuery(TargetFields.ATTRIBUTE.name() + ".test.null=ne=null", 1); // null check
|
||||
assertRSQLQuery(TargetFields.ATTRIBUTE.name() + ".test.null=ne=null2", 1 + 5); // value check
|
||||
assertRSQLQuery(TargetFields.ATTRIBUTE.name() + ".test.n/a=ne=null", 0); // null check
|
||||
|
||||
assertRSQLQuery(TargetFields.ATTRIBUTE.name() + ".revision==1.1", 1);
|
||||
assertRSQLQuery(TargetFields.ATTRIBUTE.name() + ".revision!=1.1", 1);
|
||||
@@ -168,7 +203,6 @@ class RSQLTargetFieldTest extends AbstractJpaIntegrationTest {
|
||||
assertRSQLQuery(TargetFields.ATTRIBUTE.name() + ".revision==noExist*", 0);
|
||||
assertRSQLQuery(TargetFields.ATTRIBUTE.name() + ".revision=in=(1.1,notexist)", 1);
|
||||
assertRSQLQuery(TargetFields.ATTRIBUTE.name() + ".revision=out=(1.1)", 1);
|
||||
assertRSQLQuery(TargetFields.ATTRIBUTE.name() + ".test.dot==value.dot", 1);
|
||||
assertRSQLQuery(TargetFields.ATTRIBUTE.name() + ".key.dot*==value.dot", 0);
|
||||
assertRSQLQuery(TargetFields.ATTRIBUTE.name() + ".key.*==value.dot", 0);
|
||||
assertRSQLQuery(TargetFields.ATTRIBUTE.name() + ".key.==value.dot", 0);
|
||||
@@ -196,10 +230,8 @@ class RSQLTargetFieldTest extends AbstractJpaIntegrationTest {
|
||||
assertRSQLQuery(TargetFields.ASSIGNEDDS.name() + ".version==" + TestdataFactory.DEFAULT_VERSION, 1);
|
||||
assertRSQLQuery(TargetFields.ASSIGNEDDS.name() + ".version==*1*", 1);
|
||||
assertRSQLQuery(TargetFields.ASSIGNEDDS.name() + ".version==noExist*", 0);
|
||||
assertRSQLQuery(
|
||||
TargetFields.ASSIGNEDDS.name() + ".version=in=(" + TestdataFactory.DEFAULT_VERSION + ",notexist)", 1);
|
||||
assertRSQLQuery(
|
||||
TargetFields.ASSIGNEDDS.name() + ".version=out=(" + TestdataFactory.DEFAULT_VERSION + ",notexist)", 4);
|
||||
assertRSQLQuery(TargetFields.ASSIGNEDDS.name() + ".version=in=(" + TestdataFactory.DEFAULT_VERSION + ",notexist)", 1);
|
||||
assertRSQLQuery(TargetFields.ASSIGNEDDS.name() + ".version=out=(" + TestdataFactory.DEFAULT_VERSION + ",notexist)", 4);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -238,18 +270,41 @@ class RSQLTargetFieldTest extends AbstractJpaIntegrationTest {
|
||||
@Test
|
||||
@Description("Test filter target by metadata")
|
||||
void testFilterByMetadata() {
|
||||
createTargetMetadata(testdataFactory.createTarget().getControllerId(),
|
||||
entityFactory.generateTargetMetadata("key.dot", "value.dot"));
|
||||
createTargetMetadata(testdataFactory.createTarget().getControllerId(), entityFactory.generateTargetMetadata("key.dot", "value.dot"));
|
||||
|
||||
assertRSQLQuery(TargetFields.METADATA.name() + ".metaKey==metaValue", 1);
|
||||
assertRSQLQuery(TargetFields.METADATA.name() + ".metaKey==null", 0); // "null" check
|
||||
assertRSQLQuery(TargetFields.METADATA.name() + ".metaKey==*v*", 2);
|
||||
assertRSQLQuery(TargetFields.METADATA.name() + ".metaKey==noExist*", 0);
|
||||
assertRSQLQuery(TargetFields.METADATA.name() + ".metaKey=in=(metaValue,notexist)", 1);
|
||||
assertRSQLQuery(TargetFields.METADATA.name() + ".metaKey=out=(metaValue,notexist)", 1);
|
||||
assertRSQLQuery(TargetFields.METADATA.name() + ".notExist==metaValue", 0);
|
||||
|
||||
assertRSQLQuery(TargetFields.METADATA.name() + ".metaKey=is=metaValue", 1);
|
||||
assertRSQLQuery(TargetFields.METADATA.name() + ".metaKey=is=null", 4); // null check (1 of the initial five has)
|
||||
assertRSQLQuery(TargetFields.METADATA.name() + ".metaKey=is=*v*", 2);
|
||||
assertRSQLQuery(TargetFields.METADATA.name() + ".metaKey=is=noExist*", 0);
|
||||
assertRSQLQuery(TargetFields.METADATA.name() + ".notExist=is=null", 1 + 5); // null check
|
||||
assertRSQLQuery(TargetFields.METADATA.name() + ".metaKey=eq=metaValue", 1);
|
||||
assertRSQLQuery(TargetFields.METADATA.name() + ".metaKey=eq=null", 4); // null check (1 of the initial five has)
|
||||
assertRSQLQuery(TargetFields.METADATA.name() + ".metaKey=eq=*v*", 2);
|
||||
assertRSQLQuery(TargetFields.METADATA.name() + ".metaKey=eq=noExist*", 0);
|
||||
assertRSQLQuery(TargetFields.METADATA.name() + ".notExist=eq=null", 1 + 5); // null check
|
||||
|
||||
assertRSQLQuery(TargetFields.METADATA.name() + ".metaKey!=metaValue", 1);
|
||||
assertRSQLQuery(TargetFields.METADATA.name() + ".metaKey!=null", 2);
|
||||
assertRSQLQuery(TargetFields.METADATA.name() + ".notExist!=metaValue", 0);
|
||||
assertRSQLQuery(TargetFields.METADATA.name() + ".metaKey!=notExist", 2);
|
||||
//
|
||||
assertRSQLQuery(TargetFields.METADATA.name() + ".metaKey=not=metaValue", 1 + 4);
|
||||
assertRSQLQuery(TargetFields.METADATA.name() + ".metaKey=not=null", 2); // null check (2 of the initial five)
|
||||
assertRSQLQuery(TargetFields.METADATA.name() + ".notExist=not=metaValue", 1 + 5);
|
||||
assertRSQLQuery(TargetFields.METADATA.name() + ".metaKey=not=notExist", 1 + 5);
|
||||
assertRSQLQuery(TargetFields.METADATA.name() + ".metaKey=ne=metaValue", 1 + 4);
|
||||
assertRSQLQuery(TargetFields.METADATA.name() + ".metaKey=ne=null", 2); // null check (2 of the initial five)
|
||||
assertRSQLQuery(TargetFields.METADATA.name() + ".notExist=ne=metaValue", 1 + 5);
|
||||
assertRSQLQuery(TargetFields.METADATA.name() + ".metaKey=ne=notExist", 1 + 5);
|
||||
|
||||
assertRSQLQuery(TargetFields.METADATA.name() + ".metaKey=in=(metaValue,notexist)", 1);
|
||||
assertRSQLQuery(TargetFields.METADATA.name() + ".metaKey=out=(metaValue,notexist)", 1);
|
||||
assertRSQLQuery(TargetFields.METADATA.name() + ".key.dot==value.dot", 1);
|
||||
assertRSQLQuery(TargetFields.METADATA.name() + ".key.dot*==value.dot", 0);
|
||||
assertRSQLQuery(TargetFields.METADATA.name() + ".key.*==value.dot", 0);
|
||||
@@ -265,41 +320,37 @@ class RSQLTargetFieldTest extends AbstractJpaIntegrationTest {
|
||||
@Test
|
||||
@Description("Test filter based on more complex RSQL queries")
|
||||
void testFilterByComplexQueries() {
|
||||
assertRSQLQuery(TargetFields.NAME.name() + "!=targetName123" + AND + TargetFields.METADATA.name() + ".metaKey!=value", 0);
|
||||
assertRSQLQuery(
|
||||
TargetFields.NAME.name() + "!=targetName123" + AND + TargetFields.METADATA.name() + ".metaKey!=value",
|
||||
0);
|
||||
assertRSQLQuery("(" + TargetFields.TAG.name() + "!=TAG1" + OR + TargetFields.TAG.name() + "!=TAG2)" + AND
|
||||
+ TargetFields.CONTROLLERID.name() + "!=targetId1235", 4);
|
||||
"(" + TargetFields.TAG.name() + "!=TAG1" + OR + TargetFields.TAG.name() + "!=TAG2)" +
|
||||
AND + TargetFields.CONTROLLERID.name() + "!=targetId1235", 4);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Description("Testing allowed RSQL keys based on TargetFields definition")
|
||||
void rsqlValidTargetFields() {
|
||||
final String rsql1 = "ID == '0123' and NAME == abcd and DESCRIPTION == absd"
|
||||
+ " and CREATEDAT =lt= 0123 and LASTMODIFIEDAT =gt= 0123"
|
||||
+ " and CONTROLLERID == 0123 and UPDATESTATUS == PENDING"
|
||||
+ " and IPADDRESS == 0123 and LASTCONTROLLERREQUESTAT == 0123" + " and tag == beta";
|
||||
RSQLUtility.validateRsqlFor(
|
||||
"ID == '0123' and NAME == abcd and DESCRIPTION == absd and CREATEDAT =lt= 0123 and LASTMODIFIEDAT =gt= 0123" +
|
||||
" and CONTROLLERID == 0123 and UPDATESTATUS == PENDING and IPADDRESS == 0123 and LASTCONTROLLERREQUESTAT == 0123" +
|
||||
" and tag == beta",
|
||||
TargetFields.class, JpaTarget.class, virtualPropertyReplacer, entityManager);
|
||||
RSQLUtility.validateRsqlFor(
|
||||
"ASSIGNEDDS.name == abcd and ASSIGNEDDS.version == 0123 and INSTALLEDDS.name == abcd and INSTALLEDDS.version == 0123",
|
||||
TargetFields.class, JpaTarget.class, virtualPropertyReplacer, entityManager);
|
||||
RSQLUtility.validateRsqlFor(
|
||||
"ATTRIBUTE.subkey1 == test and ATTRIBUTE.subkey2 == test and METADATA.metakey1 == abcd and METADATA.metavalue2 == asdfg",
|
||||
TargetFields.class, JpaTarget.class, virtualPropertyReplacer, entityManager);
|
||||
RSQLUtility.validateRsqlFor(
|
||||
"CREATEDAT =lt= ${NOW_TS} and LASTMODIFIEDAT =ge= ${OVERDUE_TS}",
|
||||
TargetFields.class, JpaTarget.class, virtualPropertyReplacer, entityManager);
|
||||
RSQLUtility.validateRsqlFor(
|
||||
"ATTRIBUTE.test.dot == test and ATTRIBUTE.subkey2 == test and METADATA.test.dot == abcd and METADATA.metavalue2 == asdfg",
|
||||
TargetFields.class, JpaTarget.class, virtualPropertyReplacer, entityManager);
|
||||
|
||||
RSQLUtility.validateRsqlFor(rsql1, TargetFields.class);
|
||||
|
||||
final String rsql2 = "ASSIGNEDDS.name == abcd and ASSIGNEDDS.version == 0123"
|
||||
+ " and INSTALLEDDS.name == abcd and INSTALLEDDS.version == 0123";
|
||||
RSQLUtility.validateRsqlFor(rsql2, TargetFields.class);
|
||||
|
||||
final String rsql3 = "ATTRIBUTE.subkey1 == test and ATTRIBUTE.subkey2 == test"
|
||||
+ " and METADATA.metakey1 == abcd and METADATA.metavalue2 == asdfg";
|
||||
RSQLUtility.validateRsqlFor(rsql3, TargetFields.class);
|
||||
|
||||
final String rsql4 = "CREATEDAT =lt= ${NOW_TS} and LASTMODIFIEDAT =ge= ${OVERDUE_TS}";
|
||||
RSQLUtility.validateRsqlFor(rsql4, TargetFields.class);
|
||||
|
||||
final String rsql5 = "wrongfield == abcd";
|
||||
assertThatExceptionOfType(RSQLParameterUnsupportedFieldException.class)
|
||||
.isThrownBy(() -> RSQLUtility.validateRsqlFor(rsql5, TargetFields.class));
|
||||
|
||||
final String rsql6 = "ATTRIBUTE.test.dot == test and ATTRIBUTE.subkey2 == test"
|
||||
+ " and METADATA.test.dot == abcd and METADATA.metavalue2 == asdfg";
|
||||
RSQLUtility.validateRsqlFor(rsql6, TargetFields.class);
|
||||
.isThrownBy(() -> RSQLUtility.validateRsqlFor(
|
||||
"wrongfield == abcd",
|
||||
TargetFields.class, JpaTarget.class, virtualPropertyReplacer, entityManager));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -338,6 +389,7 @@ class RSQLTargetFieldTest extends AbstractJpaIntegrationTest {
|
||||
|
||||
private void assertRSQLQueryThrowsException(final String rsqlParam) {
|
||||
assertThatExceptionOfType(RSQLParameterUnsupportedFieldException.class)
|
||||
.isThrownBy(() -> RSQLUtility.validateRsqlFor(rsqlParam, TargetFields.class));
|
||||
.isThrownBy(() -> RSQLUtility.validateRsqlFor(
|
||||
rsqlParam, TargetFields.class, JpaTarget.class, virtualPropertyReplacer, entityManager));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -57,12 +57,12 @@ class RSQLToSQLTest {
|
||||
print(JpaTarget.class, TargetFields.class, "controllerId==ctrlr1");
|
||||
// reference - fk to a table
|
||||
print(JpaTarget.class, TargetFields.class, "assignedds.name==x and assignedds.version==y");
|
||||
// list (map table that refers main)
|
||||
print(JpaTarget.class, TargetFields.class, "metadata.key1==value1");
|
||||
// map (map table that refers main)
|
||||
print(JpaTarget.class, TargetFields.class, "attribute.key1==value1");
|
||||
// list of non-simple (with mapping table)
|
||||
print(JpaTarget.class, TargetFields.class, "tag==tag1");
|
||||
// list (map table that refers main)
|
||||
print(JpaTarget.class, TargetFields.class, "attribute.key1==value1");
|
||||
// map (map table that refers main)
|
||||
print(JpaTarget.class, TargetFields.class, "metadata.key1==value1");
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -44,14 +44,12 @@ import org.eclipse.hawkbit.repository.RsqlQueryField;
|
||||
import org.eclipse.hawkbit.repository.SoftwareModuleFields;
|
||||
import org.eclipse.hawkbit.repository.TargetFields;
|
||||
import org.eclipse.hawkbit.repository.TenantConfigurationManagement;
|
||||
import org.eclipse.hawkbit.repository.exception.RSQLParameterSyntaxException;
|
||||
import org.eclipse.hawkbit.repository.exception.RSQLParameterUnsupportedFieldException;
|
||||
import org.eclipse.hawkbit.repository.model.SoftwareModule;
|
||||
import org.eclipse.hawkbit.repository.model.TenantConfigurationValue;
|
||||
import org.eclipse.hawkbit.repository.model.helper.SystemSecurityContextHolder;
|
||||
import org.eclipse.hawkbit.repository.model.helper.TenantConfigurationManagementHolder;
|
||||
import org.eclipse.hawkbit.repository.rsql.RsqlConfigHolder;
|
||||
import org.eclipse.hawkbit.repository.rsql.RsqlVisitorFactory;
|
||||
import org.eclipse.hawkbit.repository.rsql.VirtualPropertyReplacer;
|
||||
import org.eclipse.hawkbit.repository.rsql.VirtualPropertyResolver;
|
||||
import org.eclipse.hawkbit.security.SystemSecurityContext;
|
||||
@@ -88,8 +86,6 @@ class RSQLUtilityTest {
|
||||
private TenantConfigurationManagement confMgmt;
|
||||
@MockitoBean
|
||||
private SystemSecurityContext securityContext;
|
||||
@MockitoBean
|
||||
private RsqlVisitorFactory rsqlVisitorFactory;
|
||||
@Mock
|
||||
private Root<Object> baseSoftwareModuleRootMock;
|
||||
@Mock
|
||||
@@ -109,63 +105,6 @@ class RSQLUtilityTest {
|
||||
setupRoot(subqueryRootMock);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Description("Testing throwing exception in case of not allowed RSQL key")
|
||||
void rsqlUnsupportedFieldExceptionTest() {
|
||||
final String rsql1 = "wrongfield == abcd";
|
||||
assertThatExceptionOfType(RSQLParameterUnsupportedFieldException.class)
|
||||
.isThrownBy(() -> validateRsqlForTestFields(rsql1));
|
||||
|
||||
final String rsql2 = "wrongfield == abcd or TESTFIELD_WITH_SUB_ENTITIES.subentity11 == 0123";
|
||||
assertThatExceptionOfType(RSQLParameterUnsupportedFieldException.class)
|
||||
.isThrownBy(() -> validateRsqlForTestFields(rsql2));
|
||||
}
|
||||
|
||||
@Test
|
||||
@Description("Testing exception in case of not allowed subkey")
|
||||
void rsqlUnsupportedSubkeyThrowException() {
|
||||
final String rsql1 = "TESTFIELD_WITH_SUB_ENTITIES.unsupported == abcd and TESTFIELD_WITH_SUB_ENTITIES.subentity22 == 0123";
|
||||
assertThatExceptionOfType(RSQLParameterUnsupportedFieldException.class)
|
||||
.isThrownBy(() -> validateRsqlForTestFields(rsql1));
|
||||
|
||||
final String rsql2 = "TESTFIELD_WITH_SUB_ENTITIES.unsupported == abcd or TESTFIELD_WITH_SUB_ENTITIES.subentity22 == 0123";
|
||||
assertThatExceptionOfType(RSQLParameterUnsupportedFieldException.class)
|
||||
.isThrownBy(() -> validateRsqlForTestFields(rsql2));
|
||||
|
||||
final String rsql3 = "TESTFIELD == abcd or TESTFIELD_WITH_SUB_ENTITIES.unsupported == 0123";
|
||||
assertThatExceptionOfType(RSQLParameterUnsupportedFieldException.class)
|
||||
.isThrownBy(() -> validateRsqlForTestFields(rsql3));
|
||||
}
|
||||
|
||||
@Test
|
||||
@Description("Testing valid RSQL keys based on TestFieldEnum.class")
|
||||
void rsqlFieldValidation() {
|
||||
|
||||
final String rsql1 = "TESTFIELD_WITH_SUB_ENTITIES.subentity11 == abcd and TESTFIELD_WITH_SUB_ENTITIES.subentity22 == 0123";
|
||||
final String rsql2 = "TESTFIELD_WITH_SUB_ENTITIES.subentity11 == abcd or TESTFIELD_WITH_SUB_ENTITIES.subentity22 == 0123";
|
||||
final String rsql3 = "TESTFIELD_WITH_SUB_ENTITIES.subentity11 == abcd and TESTFIELD_WITH_SUB_ENTITIES.subentity22 == 0123 and TESTFIELD == any";
|
||||
|
||||
validateRsqlForTestFields(rsql1);
|
||||
validateRsqlForTestFields(rsql2);
|
||||
validateRsqlForTestFields(rsql3);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Description("Verify that RSQL expressions are validated case insensitive")
|
||||
void mixedCaseRsqlFieldValidation() {
|
||||
when(rsqlVisitorFactory.validationRsqlVisitor(TargetFields.class)).thenReturn(new FieldValidationRsqlVisitor<>(TargetFields.class));
|
||||
final String rsqlWithMixedCase = "name==b And name==c aND Name==d OR NAME=iN=y oR nAme=IN=z";
|
||||
RSQLUtility.validateRsqlFor(rsqlWithMixedCase, TargetFields.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
void wrongRsqlSyntaxThrowSyntaxException() {
|
||||
final Specification<Object> rsqlSpecification = RSQLUtility.buildRsqlSpecification("name==abc;d", SoftwareModuleFields.class, null, testDb);
|
||||
assertThatExceptionOfType(RSQLParameterSyntaxException.class)
|
||||
.as("RSQLParameterSyntaxException because of wrong RSQL syntax")
|
||||
.isThrownBy(() -> rsqlSpecification.toPredicate(baseSoftwareModuleRootMock, criteriaQueryMock, criteriaBuilderMock));
|
||||
}
|
||||
|
||||
@Test
|
||||
void wrongFieldThrowUnsupportedFieldException() {
|
||||
when(baseSoftwareModuleRootMock.getJavaType()).thenReturn((Class) SoftwareModule.class);
|
||||
@@ -481,12 +420,6 @@ class RSQLUtilityTest {
|
||||
return (Path<Y>) path;
|
||||
}
|
||||
|
||||
private void validateRsqlForTestFields(final String rsql) {
|
||||
when(rsqlVisitorFactory.validationRsqlVisitor(TestFieldEnum.class)).thenReturn(
|
||||
new FieldValidationRsqlVisitor<>(TestFieldEnum.class));
|
||||
RSQLUtility.validateRsqlFor(rsql, TestFieldEnum.class);
|
||||
}
|
||||
|
||||
private void reset0(final Object... mocks) {
|
||||
reset(mocks);
|
||||
if (Arrays.asList(mocks).contains(baseSoftwareModuleRootMock)) {
|
||||
@@ -553,8 +486,8 @@ class RSQLUtilityTest {
|
||||
}
|
||||
|
||||
@Bean
|
||||
RsqlConfigHolder rsqlVisitorFactoryHolder() {
|
||||
RsqlConfigHolder rsqlConfigHolder() {
|
||||
return RsqlConfigHolder.getInstance();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -12,10 +12,10 @@
|
||||
|
||||
logging.level.org.eclipse.persistence=ERROR
|
||||
## Uncomment to see the debug of persistence, e.g. to see the generated SQLs
|
||||
#logging.level.org.eclipse.persistence=DEBUG
|
||||
#spring.jpa.properties.eclipselink.logging.level=FINE
|
||||
#spring.jpa.properties.eclipselink.logging.level.sql=FINE
|
||||
#spring.jpa.properties.eclipselink.logging.parameters=true
|
||||
logging.level.org.eclipse.persistence=DEBUG
|
||||
spring.jpa.properties.eclipselink.logging.level=FINE
|
||||
spring.jpa.properties.eclipselink.logging.level.sql=FINE
|
||||
spring.jpa.properties.eclipselink.logging.parameters=true
|
||||
|
||||
## Enable EclipseLink performance monitor (monitoring and profile)
|
||||
#spring.jpa.properties.eclipselink.profiler=PerformanceMonitor
|
||||
|
||||
Reference in New Issue
Block a user