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

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

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

* [#2176] Big Refactoring

* RSQL: all maps with joins with on

---------

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

View File

@@ -9,6 +9,7 @@
*/ */
package org.eclipse.hawkbit.repository; package org.eclipse.hawkbit.repository;
import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
@@ -53,13 +54,8 @@ public enum ActionFields implements RsqlQueryField, FieldValueConverter<ActionFi
} }
@Override @Override
public Object convertValue(final ActionFields e, final String value) { public Object convertValue(final ActionFields enumValue, final String value) {
return STATUS == e ? convertStatusValue(value) : value; return STATUS == enumValue ? convertStatusValue(value) : value;
}
@Override
public String[] possibleValues(final ActionFields e) {
return STATUS == e ? new String[] { ACTIVE, INACTIVE } : new String[0];
} }
private static Object convertStatusValue(final String value) { private static Object convertStatusValue(final String value) {
@@ -69,7 +65,7 @@ public enum ActionFields implements RsqlQueryField, FieldValueConverter<ActionFi
} else if (trimmedValue.equalsIgnoreCase(INACTIVE)) { } else if (trimmedValue.equalsIgnoreCase(INACTIVE)) {
return false; return false;
} else { } else {
return null; throw new IllegalArgumentException("field 'status' must be one of the following values {" + ACTIVE + ", " + INACTIVE + "}");
} }
} }
} }

View File

@@ -21,21 +21,13 @@ package org.eclipse.hawkbit.repository;
public interface FieldValueConverter<T extends Enum<T>> { public interface FieldValueConverter<T extends Enum<T>> {
/** /**
* Converts the given {@code value} into the representation to build a * Converts the given {@code value} into the representation to build ageneric query.
* generic query.
* *
* @param e the enum to build the value for * @param enumValue the enum to build the value for
* @param value the value in string representation * @param value the value in string representation
* @return the converted object or {@code null} if conversation fails, if given enum does not need to be converted the * @return the converted object or {@code null} if conversation fails, if given enum does not need to be converted the
* unmodified {@code value} is returned. * unmodified {@code value} is returned.
* @throws IllegalArgumentException if the value is not supported
*/ */
Object convertValue(final T e, final String value); Object convertValue(final T enumValue, final String value);
/**
* returns the possible values associated with the given enum type.
*
* @param e the enum type to retrieve the possible values
* @return the possible values for a specific enum or {@code null}
*/
String[] possibleValues(final T e);
} }

View File

@@ -33,9 +33,13 @@ public enum TargetFields implements RsqlQueryField {
UPDATESTATUS("updateStatus"), UPDATESTATUS("updateStatus"),
IPADDRESS("address"), IPADDRESS("address"),
ATTRIBUTE("controllerAttributes"), ATTRIBUTE("controllerAttributes"),
ASSIGNEDDS("assignedDistributionSet", "name", "version"), ASSIGNEDDS(
INSTALLEDDS("installedDistributionSet", "name", "version"), "assignedDistributionSet",
TAG("tags", "name"), DistributionSetFields.NAME.getJpaEntityFieldName(), DistributionSetFields.VERSION.getJpaEntityFieldName()),
INSTALLEDDS(
"installedDistributionSet",
DistributionSetFields.NAME.getJpaEntityFieldName(), DistributionSetFields.VERSION.getJpaEntityFieldName()),
TAG("tags", TagFields.NAME.getJpaEntityFieldName()),
LASTCONTROLLERREQUESTAT("lastTargetQuery"), LASTCONTROLLERREQUESTAT("lastTargetQuery"),
METADATA("metadata", new SimpleImmutableEntry<>("key", "value")), METADATA("metadata", new SimpleImmutableEntry<>("key", "value")),
TARGETTYPE("targetType", TargetTypeFields.KEY.getJpaEntityFieldName(), TargetTypeFields.NAME.getJpaEntityFieldName()); TARGETTYPE("targetType", TargetTypeFields.KEY.getJpaEntityFieldName(), TargetTypeFields.NAME.getJpaEntityFieldName());

View File

@@ -11,12 +11,16 @@ package org.eclipse.hawkbit.repository.exception;
import java.io.Serial; import java.io.Serial;
import lombok.EqualsAndHashCode;
import lombok.ToString;
import org.eclipse.hawkbit.exception.AbstractServerRtException; import org.eclipse.hawkbit.exception.AbstractServerRtException;
import org.eclipse.hawkbit.exception.SpServerError; import org.eclipse.hawkbit.exception.SpServerError;
/** /**
* Exception used by the REST API in case of RSQL search filter query. * Exception used by the REST API in case of RSQL search filter query.
*/ */
@ToString(callSuper = true)
@EqualsAndHashCode(callSuper = true)
public class RSQLParameterSyntaxException extends AbstractServerRtException { public class RSQLParameterSyntaxException extends AbstractServerRtException {
@Serial @Serial

View File

@@ -11,12 +11,16 @@ package org.eclipse.hawkbit.repository.exception;
import java.io.Serial; import java.io.Serial;
import lombok.EqualsAndHashCode;
import lombok.ToString;
import org.eclipse.hawkbit.exception.AbstractServerRtException; import org.eclipse.hawkbit.exception.AbstractServerRtException;
import org.eclipse.hawkbit.exception.SpServerError; import org.eclipse.hawkbit.exception.SpServerError;
/** /**
* Exception used by the REST API in case of invalid field name in the rsql search parameter. * Exception used by the REST API in case of invalid field name in the rsql search parameter.
*/ */
@ToString(callSuper = true)
@EqualsAndHashCode(callSuper = true)
public class RSQLParameterUnsupportedFieldException extends AbstractServerRtException { public class RSQLParameterUnsupportedFieldException extends AbstractServerRtException {
@Serial @Serial

View File

@@ -15,8 +15,7 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
/** /**
* Helper class providing static access to the RSQL configuration as managed the ignoreCase and * Helper class providing static access to the RSQL configuration as managed bean.
* the {@link RsqlVisitorFactory} bean.
*/ */
@NoArgsConstructor(access = lombok.AccessLevel.PRIVATE) @NoArgsConstructor(access = lombok.AccessLevel.PRIVATE)
@Getter @Getter
@@ -26,8 +25,7 @@ public final class RsqlConfigHolder {
private static final RsqlConfigHolder SINGLETON = new RsqlConfigHolder(); private static final RsqlConfigHolder SINGLETON = new RsqlConfigHolder();
/** /**
* If RSQL comparison operators shall ignore the case. If ignore case is <code>true</code> * If RSQL comparison operators shall ignore the case. If ignore case is <code>true</code> "x == ax" will match "x == aX"
* "x == ax" will match "x == aX"
*/ */
@Value("${hawkbit.rsql.ignore-case:true}") @Value("${hawkbit.rsql.ignore-case:true}")
private boolean ignoreCase; private boolean ignoreCase;
@@ -41,13 +39,6 @@ public final class RsqlConfigHolder {
@Value("${hawkbit.rsql.case-insensitive-db:false}") @Value("${hawkbit.rsql.case-insensitive-db:false}")
private boolean caseInsensitiveDB; private boolean caseInsensitiveDB;
private RsqlVisitorFactory rsqlVisitorFactory;
@Autowired
public void setRsqlVisitorFactory(final RsqlVisitorFactory rsqlVisitorFactory) {
this.rsqlVisitorFactory = rsqlVisitorFactory;
}
/** /**
* @deprecated in favour of G2 RSQL visitor. since 0.6.0 * @deprecated in favour of G2 RSQL visitor. since 0.6.0
*/ */

View File

@@ -1,34 +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.rsql;
import cz.jirutka.rsql.parser.ast.Node;
import cz.jirutka.rsql.parser.ast.RSQLVisitor;
import org.eclipse.hawkbit.repository.RsqlQueryField;
/**
* Factory to obtain {@link RSQLVisitor} instances that can be used to process
* the {@link Node}s representing an RSQL query.
*/
@FunctionalInterface
public interface RsqlVisitorFactory {
/**
* Provides a {@link RSQLVisitor} instance for validating RSQL queries based
* on the given {@link RsqlQueryField}.
*
* @param <A> The type of the {@link RsqlQueryField}.
* @param fieldNameProvider providing accessing to the relevant field names.
* @return An {@link RSQLVisitor} to validate the {@link Node}s of an RSQL
* query.
*/
<A extends Enum<A> & RsqlQueryField> RSQLVisitor<Void, String> validationRsqlVisitor(Class<A> fieldNameProvider);
}

View File

@@ -22,8 +22,7 @@ public interface VirtualPropertyReplacer extends Serializable {
/** /**
* Looks up a placeholders and replaces them * Looks up a placeholders and replaces them
* *
* @param input the input string in which virtual properties should be * @param input the input string in which virtual properties should be replaced
* replaced
* @return the result of the replacement * @return the result of the replacement
*/ */
String replace(String input); String replace(String input);

View File

@@ -16,29 +16,21 @@ import org.apache.commons.text.lookup.StringLookupFactory;
import org.eclipse.hawkbit.repository.TimestampCalculator; import org.eclipse.hawkbit.repository.TimestampCalculator;
/** /**
* Adds macro capabilities to RSQL expressions that are used to filter for * Adds macro capabilities to RSQL expressions that are used to filter for targets.
* targets.
* <p> * <p>
* Some (virtual) properties do not have a representation in the database (in * Some (virtual) properties do not have a representation in the database (in general these properties are time-related, or more explicitly,
* general these properties are time-related, or more explicitly, they deal with * they deal with time intervals).<br>
* time intervals).<br> * Such a virtual property needs to be calculated on Java-side before it may be used in a target filter query that is passed to the database.
* Such a virtual property needs to be calculated on Java-side before it may be * Therefore, a placeholder is used in the RSQL expression that is expanded when the RSQL is parsed
* used in a target filter query that is passed to the database. Therefore a
* placeholder is used in the RSQL expression that is expanded when the RSQL is
* parsed
* <p> * <p>
* A virtual property may either be a system value like the current date (aka * A virtual property may either be a system value like the current date (aka <em>now_ts</em>) or a value derived from (tenant-specific) system
* <em>now_ts</em>) or a value derived from (tenant-specific) system
* configuration (e.g. <em>overdue_ts</em>). * configuration (e.g. <em>overdue_ts</em>).
* <p> * <p>
* Known values are:<br> * Known values are:<br>
* <ul> * <ul>
* <li><em>now_ts</em>: maps to system UTC time in milliseconds since Unix epoch * <li><em>now_ts</em>: maps to system UTC time in milliseconds since Unix epoch as long value</li>
* as long value</li> * <li><em>overdue_ts</em>: is a calculated value: <em>overdue_ts = now_ts - pollingInterval - pollingOverdueInterval</em>; pollingInterval
* <li><em>overdue_ts</em>: is a calculated value: <em>overdue_ts = now_ts - * and pollingOverdueInterval are retrieved from tenant-specific system configuration.</li>
* pollingInterval - pollingOverdueInterval</em>; pollingInterval and
* pollingOverdueInterval are retrieved from tenant-specific system
* configuration.</li>
* </ul> * </ul>
*/ */
public class VirtualPropertyResolver implements VirtualPropertyReplacer { public class VirtualPropertyResolver implements VirtualPropertyReplacer {
@@ -46,19 +38,16 @@ public class VirtualPropertyResolver implements VirtualPropertyReplacer {
@Serial @Serial
private static final long serialVersionUID = 1L; private static final long serialVersionUID = 1L;
private transient StringSubstitutor substitutor; private static final StringSubstitutor STRING_SUBSTITUTOR = new StringSubstitutor(
StringLookupFactory.builder().get().functionStringLookup(VirtualPropertyResolver::lookup),
StringSubstitutor.DEFAULT_PREFIX, StringSubstitutor.DEFAULT_SUFFIX, StringSubstitutor.DEFAULT_ESCAPE);
@Override @Override
public String replace(final String input) { public String replace(final String input) {
if (substitutor == null) { return STRING_SUBSTITUTOR.replace(input);
substitutor = new StringSubstitutor(
StringLookupFactory.builder().get().functionStringLookup(this::lookup),
StringSubstitutor.DEFAULT_PREFIX, StringSubstitutor.DEFAULT_SUFFIX, StringSubstitutor.DEFAULT_ESCAPE);
}
return substitutor.replace(input);
} }
private String lookup(final String rhs) { private static String lookup(final String rhs) {
if ("now_ts".equalsIgnoreCase(rhs)) { if ("now_ts".equalsIgnoreCase(rhs)) {
return String.valueOf(System.currentTimeMillis()); return String.valueOf(System.currentTimeMillis());
} else if ("overdue_ts".equalsIgnoreCase(rhs)) { } else if ("overdue_ts".equalsIgnoreCase(rhs)) {

View File

@@ -146,7 +146,6 @@ import org.eclipse.hawkbit.repository.jpa.rollout.condition.RolloutGroupEvaluati
import org.eclipse.hawkbit.repository.jpa.rollout.condition.StartNextGroupRolloutGroupSuccessAction; import org.eclipse.hawkbit.repository.jpa.rollout.condition.StartNextGroupRolloutGroupSuccessAction;
import org.eclipse.hawkbit.repository.jpa.rollout.condition.ThresholdRolloutGroupErrorCondition; import org.eclipse.hawkbit.repository.jpa.rollout.condition.ThresholdRolloutGroupErrorCondition;
import org.eclipse.hawkbit.repository.jpa.rollout.condition.ThresholdRolloutGroupSuccessCondition; 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.DistributionSet;
import org.eclipse.hawkbit.repository.model.DistributionSetType; import org.eclipse.hawkbit.repository.model.DistributionSetType;
import org.eclipse.hawkbit.repository.model.Rollout; 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.SystemSecurityContextHolder;
import org.eclipse.hawkbit.repository.model.helper.TenantConfigurationManagementHolder; import org.eclipse.hawkbit.repository.model.helper.TenantConfigurationManagementHolder;
import org.eclipse.hawkbit.repository.rsql.RsqlConfigHolder; 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.VirtualPropertyReplacer;
import org.eclipse.hawkbit.security.HawkbitSecurityProperties; import org.eclipse.hawkbit.security.HawkbitSecurityProperties;
import org.eclipse.hawkbit.security.SecurityTokenGenerator; import org.eclipse.hawkbit.security.SecurityTokenGenerator;
@@ -656,10 +654,11 @@ public class RepositoryApplicationConfiguration {
final DistributionSetManagement distributionSetManagement, final QuotaManagement quotaManagement, final DistributionSetManagement distributionSetManagement, final QuotaManagement quotaManagement,
final JpaProperties properties, final TenantConfigurationManagement tenantConfigurationManagement, final JpaProperties properties, final TenantConfigurationManagement tenantConfigurationManagement,
final RepositoryProperties repositoryProperties, 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, return new JpaTargetFilterQueryManagement(targetFilterQueryRepository, targetManagement,
virtualPropertyReplacer, distributionSetManagement, quotaManagement, properties.getDatabase(), 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); 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. * Obtains the {@link RsqlConfigHolder} bean.
* *
* @return The {@link RsqlConfigHolder} singleton. * @return The {@link RsqlConfigHolder} singleton.
*/ */
@Bean @Bean
RsqlConfigHolder rsqlVisitorFactoryHolder() { RsqlConfigHolder rsqlConfigHolder() {
return RsqlConfigHolder.getInstance(); return RsqlConfigHolder.getInstance();
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -21,7 +21,6 @@ import java.util.Optional;
import java.util.Set; import java.util.Set;
import java.util.function.BiFunction; import java.util.function.BiFunction;
import java.util.function.Function; import java.util.function.Function;
import java.util.stream.Collectors;
import jakarta.persistence.criteria.CriteriaBuilder; import jakarta.persistence.criteria.CriteriaBuilder;
import jakarta.persistence.criteria.CriteriaQuery; 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 Database database;
private final boolean ensureIgnoreCase; private final boolean ensureIgnoreCase;
private final Root<T> root; private final Root<T> root;
private final SimpleTypeConverter simpleTypeConverter;
private final VirtualPropertyReplacer virtualPropertyReplacer; private final VirtualPropertyReplacer virtualPropertyReplacer;
private final SimpleTypeConverter simpleTypeConverter;
private int level; private int level;
private boolean isOrLevel; private boolean isOrLevel;
private boolean joinsNeeded; 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 VirtualPropertyReplacer virtualPropertyReplacer, final Database database,
final CriteriaQuery<?> query, final boolean ensureIgnoreCase) { final CriteriaQuery<?> query, final boolean ensureIgnoreCase) {
super(enumType); super(enumType);
@@ -129,7 +129,7 @@ public class JpaQueryRsqlVisitor<A extends Enum<A> & RsqlQueryField, T> extends
// https://jira.sonarsource.com/browse/SONARJAVA-1478 // https://jira.sonarsource.com/browse/SONARJAVA-1478
@SuppressWarnings({ "squid:S2095" }) @SuppressWarnings({ "squid:S2095" })
public List<Predicate> visit(final ComparisonNode node, final String param) { 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<String> values = node.getArguments();
final List<Object> transformedValues = new ArrayList<>(); 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 * @return the Path for a field
*/ */
@SuppressWarnings("unchecked") @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(), return (Path<Object>) getFieldPath(root, queryPath.getJpaPath(), enumField.isMap(),
this::getJoinFieldPath).orElseThrow( this::getJoinFieldPath).orElseThrow(
() -> new RSQLParameterUnsupportedFieldException("RSQL field path cannot be empty", null)); () -> 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); return transformEnumValue(node, value, javaType);
} }
if (fieldName instanceof FieldValueConverter) { if (fieldName instanceof FieldValueConverter) {
return convertFieldConverterValue(node, fieldName, value); return convertFieldConverterValue(fieldName, value);
} }
if (Boolean.TYPE.equals(javaType)) { if (Boolean.TYPE.equals(javaType)) {
@@ -317,28 +317,24 @@ public class JpaQueryRsqlVisitor<A extends Enum<A> & RsqlQueryField, T> extends
try { try {
return simpleTypeConverter.convertIfNecessary(value, javaType); return simpleTypeConverter.convertIfNecessary(value, javaType);
} catch (final TypeMismatchException e) { } catch (final TypeMismatchException e) {
throw new RSQLParameterSyntaxException( throw new RSQLParameterUnsupportedFieldException(
"The value of the given search parameter field {" + node.getSelector() "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); e);
} }
} }
@SuppressWarnings({ "rawtypes", "unchecked" }) @SuppressWarnings({ "rawtypes", "unchecked" })
private Object convertFieldConverterValue(final ComparisonNode node, final A fieldName, final String value) { private Object convertFieldConverterValue(final A fieldName, final String value) {
final Object convertedValue = ((FieldValueConverter) fieldName).convertValue(fieldName, value); try {
if (convertedValue == null) { return ((FieldValueConverter) fieldName).convertValue(fieldName, value);
throw new RSQLParameterUnsupportedFieldException( } catch (final Exception e) {
"field {" + node.getSelector() + "} must be one of the following values {" throw new RSQLParameterSyntaxException(e.getMessage(), null);
+ Arrays.toString(((FieldValueConverter) fieldName).possibleValues(fieldName)) + "}",
null);
} else {
return convertedValue;
} }
} }
private List<Predicate> mapToPredicate(final ComparisonNode node, final Path<Object> fieldPath, 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); String value = values.get(0);
// if lookup is available, replace macros ... // 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, 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 // only 'equal' and 'notEqual' can handle transformed value like
// enums. The JPA API cannot handle object types for greaterThan etc // 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( private Predicate getOutPredicate(
final List<Object> transformedValues, final List<Object> transformedValues,
final QuertPath queryPath, final Path<Object> fieldPath) { final QueryPath queryPath, final Path<Object> fieldPath) {
final String[] fieldNames = queryPath.getJpaPath(); final String[] fieldNames = queryPath.getJpaPath();
if (isSimpleField(fieldNames, queryPath.getEnumValue().isMap())) { if (isSimpleField(fieldNames, queryPath.getEnumValue().isMap())) {
@@ -412,7 +408,7 @@ public class JpaQueryRsqlVisitor<A extends Enum<A> & RsqlQueryField, T> extends
} }
@SuppressWarnings("unchecked") @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()) { if (!queryPath.getEnumValue().isMap()) {
return null; 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, private Predicate getNotEqualToPredicate(final Object transformedValue, final Path<Object> fieldPath,
final QuertPath queryPath) { final QueryPath queryPath) {
if (transformedValue == null) { if (transformedValue == null) {
return cb.isNotNull(pathOfString(fieldPath)); return cb.isNotNull(pathOfString(fieldPath));

View File

@@ -9,6 +9,7 @@
*/ */
package org.eclipse.hawkbit.repository.jpa.rsql; package org.eclipse.hawkbit.repository.jpa.rsql;
import java.util.AbstractMap;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
@@ -23,6 +24,7 @@ import jakarta.persistence.criteria.CriteriaQuery;
import jakarta.persistence.criteria.JoinType; import jakarta.persistence.criteria.JoinType;
import jakarta.persistence.criteria.MapJoin; import jakarta.persistence.criteria.MapJoin;
import jakarta.persistence.criteria.Path; import jakarta.persistence.criteria.Path;
import jakarta.persistence.criteria.PluralJoin;
import jakarta.persistence.criteria.Predicate; import jakarta.persistence.criteria.Predicate;
import jakarta.persistence.criteria.Root; import jakarta.persistence.criteria.Root;
import jakarta.persistence.criteria.Subquery; 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.AndNode;
import cz.jirutka.rsql.parser.ast.ComparisonNode; 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.LogicalNode;
import cz.jirutka.rsql.parser.ast.Node; import cz.jirutka.rsql.parser.ast.Node;
import cz.jirutka.rsql.parser.ast.OrNode; import cz.jirutka.rsql.parser.ast.OrNode;
import cz.jirutka.rsql.parser.ast.RSQLOperators;
import cz.jirutka.rsql.parser.ast.RSQLVisitor; import cz.jirutka.rsql.parser.ast.RSQLVisitor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.math.NumberUtils; 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.RSQLParameterSyntaxException;
import org.eclipse.hawkbit.repository.exception.RSQLParameterUnsupportedFieldException; import org.eclipse.hawkbit.repository.exception.RSQLParameterUnsupportedFieldException;
import org.eclipse.hawkbit.repository.rsql.VirtualPropertyReplacer; 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.orm.jpa.vendor.Database;
import org.springframework.util.CollectionUtils; import org.springframework.util.CollectionUtils;
import org.springframework.util.ObjectUtils; import org.springframework.util.ObjectUtils;
@@ -70,11 +72,11 @@ public class JpaQueryRsqlVisitorG2<A extends Enum<A> & RsqlQueryField, T>
private final VirtualPropertyReplacer virtualPropertyReplacer; private final VirtualPropertyReplacer virtualPropertyReplacer;
private final boolean ensureIgnoreCase; private final boolean ensureIgnoreCase;
private final SimpleTypeConverter simpleTypeConverter = new SimpleTypeConverter();
private final Map<String, Path<?>> attributeToPath = new HashMap<>(); private final Map<String, Path<?>> attributeToPath = new HashMap<>();
private boolean inOr; 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 Root<T> root, final CriteriaQuery<?> query, final CriteriaBuilder cb,
final Database database, final VirtualPropertyReplacer virtualPropertyReplacer, final boolean ensureIgnoreCase) { final Database database, final VirtualPropertyReplacer virtualPropertyReplacer, final boolean ensureIgnoreCase) {
super(enumType); super(enumType);
@@ -106,69 +108,71 @@ public class JpaQueryRsqlVisitorG2<A extends Enum<A> & RsqlQueryField, T>
@Override @Override
public List<Predicate> visit(final ComparisonNode node, final String param) { public List<Predicate> visit(final ComparisonNode node, final String param) {
final QuertPath queryField = getQuertPath(node); final QueryPath queryPath = getQueryPath(node);
final Path<?> fieldPath = getFieldPath(root, queryPath);
final List<String> values = node.getArguments(); return Collections.singletonList(toPredicate(node, queryPath, fieldPath, getValues(node, queryPath, fieldPath)));
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);
} }
@SuppressWarnings({ "rawtypes", "unchecked" }) @SuppressWarnings("java:S3776") // java:S3776 - easier to read at one place
private static Object transformEnumValue(final ComparisonNode node, final Class<?> javaType, final String value) { private Predicate toPredicate(final ComparisonNode node, final QueryPath queryPath, final Path<?> fieldPath, final List<Object> values) {
final Class<? extends Enum> tmpEnumType = (Class<? extends Enum>) javaType; final Predicate mapEntryKeyPredicate;
try { if (queryPath.getEnumValue().isMap()) {
return Enum.valueOf(tmpEnumType, value.toUpperCase()); if (node.getOperator() == RSQLUtility.IS) {
} catch (final IllegalArgumentException e) { // special handling of "not-exists"
// we could not transform the given string value into the enum if (values.size() != 1) {
// type, so ignore it and return null and do not filter throw new RSQLParameterSyntaxException("The operator '" + RSQLUtility.IS + "' can only be used with one value");
log.info("given value {} cannot be transformed into the correct enum type {}", value.toUpperCase(), }
javaType); if (values.get(0) == null) {
log.debug("value cannot be transformed to an enum", e); // IS operator for maps and null value is treated as doesn't exist correspondingly
((PluralJoin<?, ?, ?>) fieldPath).on(toMapEntryKeyPredicate(queryPath, fieldPath));
throw new RSQLParameterUnsupportedFieldException("field {" + node.getSelector() return cb.isNull(getValueFieldPath(queryPath, fieldPath));
+ "} must be one of the following values {" + Arrays.stream(tmpEnumType.getEnumConstants()) }
.map(v -> v.name().toLowerCase()).toList() } else if (node.getOperator() == RSQLUtility.NOT) {
+ "}", e); 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) { private Predicate toMapEntryKeyPredicate(final QueryPath queryPath, final Path<?> fieldPath) {
return split.length == 1 || (split.length == 2 && isMapKeyField); final String[] graph = queryPath.getJpaPath();
return equal(mapEntryKeyPath(queryPath, fieldPath), graph[graph.length - 1]);
} }
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
private static Path<String> pathOfString(final Path<?> path) { private Path<String> mapEntryKeyPath(final QueryPath queryPath, final Path<?> fieldPath) {
return (Path<String>) path; if (fieldPath instanceof MapJoin) {
} // Currently we support only string key. So below cast is safe.
return (Path<String>) ((MapJoin<?, ?, ?>) fieldPath).key();
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;
} }
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, private Path<?> getValueFieldPath(final QueryPath queryPath, final Path<?> fieldPath) {
final Path<?> fieldPath, final A enumField = queryPath.getEnumValue();
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) {
if (enumField.isMap()) { if (enumField.isMap()) {
final Path<?> mapValuePath = enumField.getSubEntityMapTuple().map(Entry::getValue).map(fieldPath::get).orElse(null); final Path<?> mapValuePath = enumField.getSubEntityMapTuple().map(Entry::getValue).map(fieldPath::get).orElse(null);
return mapValuePath == null ? fieldPath : mapValuePath; return mapValuePath == null ? fieldPath : mapValuePath;
@@ -177,117 +181,127 @@ public class JpaQueryRsqlVisitorG2<A extends Enum<A> & RsqlQueryField, T>
} }
} }
@SuppressWarnings("unchecked") private Predicate toOperatorAndValuePredicate(
private Predicate mapToMapPredicate(final QuertPath queryField, final Path<?> fieldPath) { final ComparisonNode node, final QueryPath queryPath, final Path<?> fieldPath, final List<Object> values) {
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) {
// only 'equal' and 'notEqual' can handle transformed value like enums. // only 'equal' and 'notEqual' can handle transformed value like enums.
// The JPA API cannot handle object types for greaterThan etc. methods. // The JPA API cannot handle object types for greaterThan etc. methods. For them, it shall be a string.
final Object transformedValue = transformedValues.get(0); final Object value = values.get(0);
final String operator = node.getOperator().getSymbol(); final String operator = node.getOperator().getSymbol();
return switch (operator) { return switch (operator) {
case "==" -> getEqualToPredicate(fieldPath, transformedValue); case "==", "=is=", "=eq=" -> toEqualToPredicate(fieldPath, value);
case "!=" -> getNotEqualToPredicate(queryField, fieldPath, transformedValue); case "!=", "=not=", "=ne=" -> toNotEqualToPredicate(queryPath, fieldPath, value);
case "=gt=" -> cb.greaterThan(pathOfString(fieldPath), value); case "=gt=" -> cb.greaterThan(pathOfString(fieldPath), String.valueOf(value)); // JPA handles numbers
case "=ge=" -> cb.greaterThanOrEqualTo(pathOfString(fieldPath), value); case "=ge=" -> cb.greaterThanOrEqualTo(pathOfString(fieldPath), String.valueOf(value));
case "=lt=" -> cb.lessThan(pathOfString(fieldPath), value); case "=lt=" -> cb.lessThan(pathOfString(fieldPath), String.valueOf(value));
case "=le=" -> cb.lessThanOrEqualTo(pathOfString(fieldPath), value); case "=le=" -> cb.lessThanOrEqualTo(pathOfString(fieldPath), String.valueOf(value));
case "=in=" -> in(pathOfString(fieldPath), transformedValues); case "=in=" -> in(pathOfString(fieldPath), values);
case "=out=" -> getOutPredicate(queryField, fieldPath, transformedValues); case "=out=" -> toOutPredicate(queryPath, fieldPath, values);
default -> throw new RSQLParameterSyntaxException( default -> throw new RSQLParameterSyntaxException("Operator symbol {" + operator + "} is either not supported or not implemented");
"Operator symbol {" + operator + "} is either not supported or not implemented");
}; };
} }
private Predicate getEqualToPredicate(final Path<?> fieldPath, final Object transformedValue) { private Predicate toEqualToPredicate(final Path<?> fieldPath, final Object value) {
if (transformedValue == null) { if (value == null) {
return cb.isNull(fieldPath); return cb.isNull(fieldPath);
} }
if ((transformedValue instanceof String transformedValueStr) && !NumberUtils.isCreatable(transformedValueStr)) { if ((value instanceof String valueStr) && !NumberUtils.isCreatable(valueStr)) {
if (ObjectUtils.isEmpty(transformedValue)) { if (ObjectUtils.isEmpty(value)) {
return cb.or(cb.isNull(fieldPath), cb.equal(pathOfString(fieldPath), "")); return cb.or(cb.isNull(fieldPath), cb.equal(pathOfString(fieldPath), ""));
} }
final Path<String> stringExpression = pathOfString(fieldPath); final Path<String> stringExpression = pathOfString(fieldPath);
if (isPattern(transformedValueStr)) { // a pattern, use like if (isPattern(valueStr)) { // a pattern, use like
return like(stringExpression, toSQL(transformedValueStr)); return like(stringExpression, toSQL(valueStr));
} else { } 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, // if value is null -> not null
final Path<?> fieldPath, final Object transformedValue) { // if value is not null -> null or not equal value
if (transformedValue == null) { private Predicate toNotEqualToPredicate(final QueryPath queryPath, final Path<?> fieldPath, final Object value) {
if (value == null) {
return cb.isNotNull(fieldPath); return cb.isNotNull(fieldPath);
} }
if ((transformedValue instanceof String transformedValueStr) && !NumberUtils.isCreatable(transformedValueStr)) { if (value instanceof String valueStr && !NumberUtils.isCreatable(valueStr)) {
if (ObjectUtils.isEmpty(transformedValue)) { if (ObjectUtils.isEmpty(value)) {
return cb.and(cb.isNotNull(fieldPath), cb.notEqual(pathOfString(fieldPath), "")); return cb.and(cb.isNotNull(fieldPath), cb.notEqual(pathOfString(fieldPath), ""));
} }
final String[] fieldNames = queryField.getJpaPath(); if (isSimpleField(queryPath)) {
if (isPattern(valueStr)) { // a pattern, use like
if (isSimpleField(fieldNames, queryField.getEnumValue().isMap())) { return cb.or(cb.isNull(fieldPath), notLike(pathOfString(fieldPath), toSQL(valueStr)));
if (isPattern(transformedValueStr)) { // a pattern, use like
return cb.or(cb.isNull(fieldPath), notLike(pathOfString(fieldPath), toSQL(transformedValueStr)));
} else { } else {
return toNullOrNotEqualPredicate(fieldPath, transformedValueStr); return toNullOrNotEqualPredicate(fieldPath, valueStr);
} }
} }
return toNotExistsSubQueryPredicate(queryField, fieldPath, expressionToCompare -> return toNotExistsSubQueryPredicate(
isPattern(transformedValueStr) ? // a pattern, use like queryPath, fieldPath, expressionToCompare ->
like(expressionToCompare, toSQL(transformedValueStr)) : isPattern(valueStr)
equal(expressionToCompare, transformedValueStr)); ? 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, private Predicate toOutPredicate(final QueryPath queryPath, final Path<?> fieldPath, final List<Object> values) {
final List<Object> transformedValues) { if (isSimpleField(queryPath)) {
final String[] subAttributes = queryField.getJpaPath(); return cb.or(cb.isNull(fieldPath), cb.not(in(pathOfString(fieldPath), values)));
if (isSimpleField(subAttributes, queryField.getEnumValue().isMap())) {
return cb.or(cb.isNull(fieldPath), cb.not(in(pathOfString(fieldPath), transformedValues)));
} }
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) { private Path<?> getFieldPath(final Root<?> root, final QueryPath queryPath) {
final String[] split = queryField.getJpaPath(); final String[] split = queryPath.getJpaPath();
Path<?> fieldPath = null; 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]; final String fieldNameSplit = split[i];
fieldPath = fieldPath == null ? getPath(root, fieldNameSplit) : fieldPath.get(fieldNameSplit); fieldPath = fieldPath == null ? getPath(root, fieldNameSplit) : fieldPath.get(fieldNameSplit);
} }
if (fieldPath == null) { 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; 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 // 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 // table / criteria (root.get creates INNER JOIN) (see org.eclipse.persistence.internal.jpa.querydef.FromImpl implementation
// for more details) otherwise delegate to root.get // for more details) otherwise delegate to root.get
@@ -306,7 +320,8 @@ public class JpaQueryRsqlVisitorG2<A extends Enum<A> & RsqlQueryField, T>
.findFirst() .findFirst()
.orElseGet(() -> root.join(fieldNameSplit, JoinType.LEFT)); .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 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)); return attributeToPath.computeIfAbsent(attribute.getName(), k -> root.join(fieldNameSplit, JoinType.LEFT));
} else { } 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) { private Predicate toNullOrNotEqualPredicate(final Path<?> fieldPath, final Object 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) {
return cb.or( return cb.or(
cb.isNull(fieldPath), cb.isNull(fieldPath),
transformedValue instanceof String transformedValueStr value instanceof String valueStr ? notEqual(pathOfString(fieldPath), valueStr) : cb.notEqual(fieldPath, value));
? notEqual(pathOfString(fieldPath), transformedValueStr)
: cb.notEqual(fieldPath, transformedValue));
} }
@SuppressWarnings({ "unchecked", "rawtypes" }) @SuppressWarnings({ "unchecked", "rawtypes" })
private Predicate toNotExistsSubQueryPredicate(final QuertPath queryField, final Path<?> fieldPath, private Predicate toNotExistsSubQueryPredicate(
final Function<Path<String>, Predicate> subQueryPredicateProvider) { 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 a subquery the field's parent joins are not actually used
if (!inOr) { if (!inOr) {
// so, if not in or (hence not reused) we remove them. Parent shall be a Join // 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( return cb.not(cb.exists(
subquery.select(subqueryRoot) subquery.select(subqueryRoot)
.where(cb.and( .where(cb.and(
cb.equal(root.get(queryField.getEnumValue().identifierFieldName()), cb.equal(
subqueryRoot.get(queryField.getEnumValue().identifierFieldName())), root.get(queryPath.getEnumValue().identifierFieldName()),
subqueryRoot.get(queryPath.getEnumValue().identifierFieldName())),
subQueryPredicateProvider.apply( subQueryPredicateProvider.apply(
getExpressionToCompare(queryField.getEnumValue(), getExpressionToCompare(queryPath.getEnumValue(), getFieldPath(subqueryRoot, queryPath)))))));
getFieldPath(subqueryRoot, queryField)))))));
} }
@SuppressWarnings({ "rawtypes", "unchecked" }) @SuppressWarnings({ "rawtypes", "unchecked" })
@@ -405,20 +375,92 @@ public class JpaQueryRsqlVisitorG2<A extends Enum<A> & RsqlQueryField, T>
" Neither of those could be found!")); " 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; final String escaped;
if (database == Database.SQL_SERVER) { if (database == Database.SQL_SERVER) {
escaped = transformedValue.replace("%", "[%]").replace("_", "[_]"); escaped = value.replace("%", "[%]").replace("_", "[_]");
} else { } else {
escaped = transformedValue.replace("%", ESCAPE_CHAR + "%").replace("_", ESCAPE_CHAR + "_"); escaped = value.replace("%", ESCAPE_CHAR + "%").replace("_", ESCAPE_CHAR + "_");
} }
return replaceIfRequired(escaped); return replaceIfRequired(escaped);
} }
private String replaceIfRequired(final String escapedValue) { private static String replaceIfRequired(final String escapedValue) {
final String finalizedValue; final String finalizedValue;
if (escapedValue.contains(ESCAPE_CHAR_WITH_ASTERISK)) { 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); .replace("$", ESCAPE_CHAR_WITH_ASTERISK);
} else { } else {
finalizedValue = escapedValue.replace(LIKE_WILDCARD, '%'); 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)) { if (caseWise(expressionToCompare)) {
return cb.notEqual(cb.upper(expressionToCompare), transformedValueStr.toUpperCase()); return cb.notEqual(cb.upper(expressionToCompare), valueStr.toUpperCase());
} else { } 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) { if (ensureIgnoreCase && expressionToCompare.getJavaType() == String.class) {
final List<String> inParams = transformedValues.stream() final List<String> inParams = values.stream()
.filter(String.class::isInstance) .filter(String.class::isInstance)
.map(String.class::cast).map(String::toUpperCase).toList(); .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 { } else {
return expressionToCompare.in(transformedValues); return expressionToCompare.in(values);
} }
} }

View File

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

View File

@@ -15,6 +15,8 @@ import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
import jakarta.persistence.EntityManager;
import io.qameta.allure.Description; import io.qameta.allure.Description;
import io.qameta.allure.Feature; import io.qameta.allure.Feature;
import io.qameta.allure.Story; 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.RSQLParameterSyntaxException;
import org.eclipse.hawkbit.repository.exception.RSQLParameterUnsupportedFieldException; import org.eclipse.hawkbit.repository.exception.RSQLParameterUnsupportedFieldException;
import org.eclipse.hawkbit.repository.jpa.AbstractJpaIntegrationTest; 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.DistributionSet;
import org.eclipse.hawkbit.repository.model.DistributionSetTag; import org.eclipse.hawkbit.repository.model.DistributionSetTag;
import org.eclipse.hawkbit.repository.model.SoftwareModule; import org.eclipse.hawkbit.repository.model.SoftwareModule;
import org.eclipse.hawkbit.repository.rsql.VirtualPropertyReplacer;
import org.eclipse.hawkbit.repository.test.util.TestdataFactory; import org.eclipse.hawkbit.repository.test.util.TestdataFactory;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page; import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.PageRequest;
import org.springframework.orm.jpa.vendor.Database; import org.springframework.orm.jpa.vendor.Database;
@Feature("Component Tests - Repository") @Feature("Component Tests - Repository")
@Story("RSQL filter distribution set") @Story("RSQL filter distribution set")
@SuppressWarnings("java:S6813") // constructor injects are not possible for test classes
class RSQLDistributionSetFieldTest extends AbstractJpaIntegrationTest { class RSQLDistributionSetFieldTest extends AbstractJpaIntegrationTest {
@Autowired
protected VirtualPropertyReplacer virtualPropertyReplacer;
@Autowired
protected EntityManager entityManager;
private DistributionSet ds; private DistributionSet ds;
private SoftwareModule sm; private SoftwareModule sm;
@@ -190,11 +201,14 @@ class RSQLDistributionSetFieldTest extends AbstractJpaIntegrationTest {
assertRSQLQuery(DistributionSetFields.METADATA.name() + ".key*==value.dot", 0); assertRSQLQuery(DistributionSetFields.METADATA.name() + ".key*==value.dot", 0);
assertRSQLQuery(DistributionSetFields.METADATA.name() + ".*==value.dot", 0); assertRSQLQuery(DistributionSetFields.METADATA.name() + ".*==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); RSQLParameterUnsupportedFieldException.class);
assertRSQLQueryThrowsException(DistributionSetFields.METADATA.name() + "*==value.dot", assertRSQLQueryThrowsException(
DistributionSetFields.METADATA.name() + "*==value.dot",
RSQLParameterUnsupportedFieldException.class); RSQLParameterUnsupportedFieldException.class);
assertRSQLQueryThrowsException(DistributionSetFields.METADATA.name() + "==value.dot", assertRSQLQueryThrowsException(
DistributionSetFields.METADATA.name() + "==value.dot",
RSQLParameterUnsupportedFieldException.class); RSQLParameterUnsupportedFieldException.class);
} }
@@ -206,18 +220,15 @@ class RSQLDistributionSetFieldTest extends AbstractJpaIntegrationTest {
assertThat(countAll).as("Found entity size is wrong").isEqualTo(expectedEntity); assertThat(countAll).as("Found entity size is wrong").isEqualTo(expectedEntity);
} }
private <T extends Throwable> void assertRSQLQueryThrowsException(final String rsqlParam, private <T extends Throwable> void assertRSQLQueryThrowsException(final String rsqlParam, final Class<T> expectedException) {
final Class<T> expectedException) {
assertThatExceptionOfType(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, private DistributionSet createDistributionSetWithMetadata(final String metadataKeyName, final String metadataValue) {
final String metadataValue) {
final DistributionSet distributionSet = testdataFactory.createDistributionSet(); final DistributionSet distributionSet = testdataFactory.createDistributionSet();
createDistributionSetMetadata(distributionSet.getId(), createDistributionSetMetadata(distributionSet.getId(), entityFactory.generateDsMetadata(metadataKeyName, metadataValue));
entityFactory.generateDsMetadata(metadataKeyName, metadataValue));
return distributionSet; return distributionSet;
} }
} }

View File

@@ -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.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.junit.jupiter.api.Assertions.fail;
import java.util.Arrays; import java.util.Arrays;
import java.util.HashMap;
import java.util.Map; import java.util.Map;
import jakarta.persistence.EntityManager;
import io.qameta.allure.Description; import io.qameta.allure.Description;
import io.qameta.allure.Feature; import io.qameta.allure.Feature;
import io.qameta.allure.Story; import io.qameta.allure.Story;
import org.assertj.core.util.Maps;
import org.eclipse.hawkbit.repository.TargetFields; import org.eclipse.hawkbit.repository.TargetFields;
import org.eclipse.hawkbit.repository.TargetTypeFields; import org.eclipse.hawkbit.repository.TargetTypeFields;
import org.eclipse.hawkbit.repository.exception.RSQLParameterUnsupportedFieldException; import org.eclipse.hawkbit.repository.exception.RSQLParameterUnsupportedFieldException;
import org.eclipse.hawkbit.repository.jpa.AbstractJpaIntegrationTest; 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.DistributionSet;
import org.eclipse.hawkbit.repository.model.Target; import org.eclipse.hawkbit.repository.model.Target;
import org.eclipse.hawkbit.repository.model.TargetTag; import org.eclipse.hawkbit.repository.model.TargetTag;
import org.eclipse.hawkbit.repository.model.TargetType; import org.eclipse.hawkbit.repository.model.TargetType;
import org.eclipse.hawkbit.repository.rsql.VirtualPropertyReplacer;
import org.eclipse.hawkbit.repository.test.util.TestdataFactory; import org.eclipse.hawkbit.repository.test.util.TestdataFactory;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Slice; import org.springframework.data.domain.Slice;
@Feature("Component Tests - Repository") @Feature("Component Tests - Repository")
@Story("RSQL filter target") @Story("RSQL filter target")
@SuppressWarnings("java:S6813") // constructor injects are not possible for test classes
class RSQLTargetFieldTest extends AbstractJpaIntegrationTest { class RSQLTargetFieldTest extends AbstractJpaIntegrationTest {
private static final String OR = ","; private static final String OR = ",";
private static final String AND = ";"; private static final String AND = ";";
@Autowired
protected VirtualPropertyReplacer virtualPropertyReplacer;
@Autowired
protected EntityManager entityManager;
private Target target; private Target target;
private Target target2; private Target target2;
private TargetType targetType1; private TargetType targetType1;
@@ -47,45 +56,6 @@ class RSQLTargetFieldTest extends AbstractJpaIntegrationTest {
@BeforeEach @BeforeEach
void setupBeforeTest() { 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 targetType1 = targetTypeManagement
.create(entityFactory.targetType().create() .create(entityFactory.targetType().create()
.name("Type1").description("Desc. Type1") .name("Type1").description("Desc. Type1")
@@ -95,8 +65,43 @@ class RSQLTargetFieldTest extends AbstractJpaIntegrationTest {
.name("Type2").description("Desc. Type2") .name("Type2").description("Desc. Type2")
.key("Type2.key")); .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()); 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()); 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 @Test
@@ -159,8 +164,38 @@ class RSQLTargetFieldTest extends AbstractJpaIntegrationTest {
@Test @Test
@Description("Test filter target by attribute") @Description("Test filter target by attribute")
void testFilterByAttribute() { void testFilterByAttribute() {
controllerManagement.updateControllerAttributes(testdataFactory.createTarget().getControllerId(), controllerManagement.updateControllerAttributes(
Maps.newHashMap("test.dot", "value.dot"), null); 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);
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==noExist*", 0);
assertRSQLQuery(TargetFields.ATTRIBUTE.name() + ".revision=in=(1.1,notexist)", 1); assertRSQLQuery(TargetFields.ATTRIBUTE.name() + ".revision=in=(1.1,notexist)", 1);
assertRSQLQuery(TargetFields.ATTRIBUTE.name() + ".revision=out=(1.1)", 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.dot*==value.dot", 0);
assertRSQLQuery(TargetFields.ATTRIBUTE.name() + ".key.*==value.dot", 0); assertRSQLQuery(TargetFields.ATTRIBUTE.name() + ".key.*==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==" + TestdataFactory.DEFAULT_VERSION, 1);
assertRSQLQuery(TargetFields.ASSIGNEDDS.name() + ".version==*1*", 1); assertRSQLQuery(TargetFields.ASSIGNEDDS.name() + ".version==*1*", 1);
assertRSQLQuery(TargetFields.ASSIGNEDDS.name() + ".version==noExist*", 0); assertRSQLQuery(TargetFields.ASSIGNEDDS.name() + ".version==noExist*", 0);
assertRSQLQuery( assertRSQLQuery(TargetFields.ASSIGNEDDS.name() + ".version=in=(" + TestdataFactory.DEFAULT_VERSION + ",notexist)", 1);
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=out=(" + TestdataFactory.DEFAULT_VERSION + ",notexist)", 4);
} }
@Test @Test
@@ -238,18 +270,41 @@ class RSQLTargetFieldTest extends AbstractJpaIntegrationTest {
@Test @Test
@Description("Test filter target by metadata") @Description("Test filter target by metadata")
void testFilterByMetadata() { void testFilterByMetadata() {
createTargetMetadata(testdataFactory.createTarget().getControllerId(), createTargetMetadata(testdataFactory.createTarget().getControllerId(), entityFactory.generateTargetMetadata("key.dot", "value.dot"));
entityFactory.generateTargetMetadata("key.dot", "value.dot"));
assertRSQLQuery(TargetFields.METADATA.name() + ".metaKey==metaValue", 1); 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==*v*", 2);
assertRSQLQuery(TargetFields.METADATA.name() + ".metaKey==noExist*", 0); 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() + ".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!=metaValue", 1);
assertRSQLQuery(TargetFields.METADATA.name() + ".metaKey!=null", 2);
assertRSQLQuery(TargetFields.METADATA.name() + ".notExist!=metaValue", 0); assertRSQLQuery(TargetFields.METADATA.name() + ".notExist!=metaValue", 0);
assertRSQLQuery(TargetFields.METADATA.name() + ".metaKey!=notExist", 2); assertRSQLQuery(TargetFields.METADATA.name() + ".metaKey!=notExist", 2);
//
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", 1);
assertRSQLQuery(TargetFields.METADATA.name() + ".key.dot*==value.dot", 0); assertRSQLQuery(TargetFields.METADATA.name() + ".key.dot*==value.dot", 0);
assertRSQLQuery(TargetFields.METADATA.name() + ".key.*==value.dot", 0); assertRSQLQuery(TargetFields.METADATA.name() + ".key.*==value.dot", 0);
@@ -265,41 +320,37 @@ class RSQLTargetFieldTest extends AbstractJpaIntegrationTest {
@Test @Test
@Description("Test filter based on more complex RSQL queries") @Description("Test filter based on more complex RSQL queries")
void testFilterByComplexQueries() { void testFilterByComplexQueries() {
assertRSQLQuery(TargetFields.NAME.name() + "!=targetName123" + AND + TargetFields.METADATA.name() + ".metaKey!=value", 0);
assertRSQLQuery( assertRSQLQuery(
TargetFields.NAME.name() + "!=targetName123" + AND + TargetFields.METADATA.name() + ".metaKey!=value", "(" + TargetFields.TAG.name() + "!=TAG1" + OR + TargetFields.TAG.name() + "!=TAG2)" +
0); AND + TargetFields.CONTROLLERID.name() + "!=targetId1235", 4);
assertRSQLQuery("(" + TargetFields.TAG.name() + "!=TAG1" + OR + TargetFields.TAG.name() + "!=TAG2)" + AND
+ TargetFields.CONTROLLERID.name() + "!=targetId1235", 4);
} }
@Test @Test
@Description("Testing allowed RSQL keys based on TargetFields definition") @Description("Testing allowed RSQL keys based on TargetFields definition")
void rsqlValidTargetFields() { void rsqlValidTargetFields() {
final String rsql1 = "ID == '0123' and NAME == abcd and DESCRIPTION == absd" RSQLUtility.validateRsqlFor(
+ " and CREATEDAT =lt= 0123 and LASTMODIFIEDAT =gt= 0123" "ID == '0123' and NAME == abcd and DESCRIPTION == absd and CREATEDAT =lt= 0123 and LASTMODIFIEDAT =gt= 0123" +
+ " and CONTROLLERID == 0123 and UPDATESTATUS == PENDING" " and CONTROLLERID == 0123 and UPDATESTATUS == PENDING and IPADDRESS == 0123 and LASTCONTROLLERREQUESTAT == 0123" +
+ " and IPADDRESS == 0123 and LASTCONTROLLERREQUESTAT == 0123" + " and tag == beta"; " 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) assertThatExceptionOfType(RSQLParameterUnsupportedFieldException.class)
.isThrownBy(() -> RSQLUtility.validateRsqlFor(rsql5, TargetFields.class)); .isThrownBy(() -> RSQLUtility.validateRsqlFor(
"wrongfield == abcd",
final String rsql6 = "ATTRIBUTE.test.dot == test and ATTRIBUTE.subkey2 == test" TargetFields.class, JpaTarget.class, virtualPropertyReplacer, entityManager));
+ " and METADATA.test.dot == abcd and METADATA.metavalue2 == asdfg";
RSQLUtility.validateRsqlFor(rsql6, TargetFields.class);
} }
@Test @Test
@@ -338,6 +389,7 @@ class RSQLTargetFieldTest extends AbstractJpaIntegrationTest {
private void assertRSQLQueryThrowsException(final String rsqlParam) { private void assertRSQLQueryThrowsException(final String rsqlParam) {
assertThatExceptionOfType(RSQLParameterUnsupportedFieldException.class) assertThatExceptionOfType(RSQLParameterUnsupportedFieldException.class)
.isThrownBy(() -> RSQLUtility.validateRsqlFor(rsqlParam, TargetFields.class)); .isThrownBy(() -> RSQLUtility.validateRsqlFor(
rsqlParam, TargetFields.class, JpaTarget.class, virtualPropertyReplacer, entityManager));
} }
} }

View File

@@ -57,12 +57,12 @@ class RSQLToSQLTest {
print(JpaTarget.class, TargetFields.class, "controllerId==ctrlr1"); print(JpaTarget.class, TargetFields.class, "controllerId==ctrlr1");
// reference - fk to a table // reference - fk to a table
print(JpaTarget.class, TargetFields.class, "assignedds.name==x and assignedds.version==y"); 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) // list of non-simple (with mapping table)
print(JpaTarget.class, TargetFields.class, "tag==tag1"); 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 @Test

View File

@@ -44,14 +44,12 @@ import org.eclipse.hawkbit.repository.RsqlQueryField;
import org.eclipse.hawkbit.repository.SoftwareModuleFields; import org.eclipse.hawkbit.repository.SoftwareModuleFields;
import org.eclipse.hawkbit.repository.TargetFields; import org.eclipse.hawkbit.repository.TargetFields;
import org.eclipse.hawkbit.repository.TenantConfigurationManagement; 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.exception.RSQLParameterUnsupportedFieldException;
import org.eclipse.hawkbit.repository.model.SoftwareModule; import org.eclipse.hawkbit.repository.model.SoftwareModule;
import org.eclipse.hawkbit.repository.model.TenantConfigurationValue; import org.eclipse.hawkbit.repository.model.TenantConfigurationValue;
import org.eclipse.hawkbit.repository.model.helper.SystemSecurityContextHolder; import org.eclipse.hawkbit.repository.model.helper.SystemSecurityContextHolder;
import org.eclipse.hawkbit.repository.model.helper.TenantConfigurationManagementHolder; import org.eclipse.hawkbit.repository.model.helper.TenantConfigurationManagementHolder;
import org.eclipse.hawkbit.repository.rsql.RsqlConfigHolder; 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.VirtualPropertyReplacer;
import org.eclipse.hawkbit.repository.rsql.VirtualPropertyResolver; import org.eclipse.hawkbit.repository.rsql.VirtualPropertyResolver;
import org.eclipse.hawkbit.security.SystemSecurityContext; import org.eclipse.hawkbit.security.SystemSecurityContext;
@@ -88,8 +86,6 @@ class RSQLUtilityTest {
private TenantConfigurationManagement confMgmt; private TenantConfigurationManagement confMgmt;
@MockitoBean @MockitoBean
private SystemSecurityContext securityContext; private SystemSecurityContext securityContext;
@MockitoBean
private RsqlVisitorFactory rsqlVisitorFactory;
@Mock @Mock
private Root<Object> baseSoftwareModuleRootMock; private Root<Object> baseSoftwareModuleRootMock;
@Mock @Mock
@@ -109,63 +105,6 @@ class RSQLUtilityTest {
setupRoot(subqueryRootMock); 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 @Test
void wrongFieldThrowUnsupportedFieldException() { void wrongFieldThrowUnsupportedFieldException() {
when(baseSoftwareModuleRootMock.getJavaType()).thenReturn((Class) SoftwareModule.class); when(baseSoftwareModuleRootMock.getJavaType()).thenReturn((Class) SoftwareModule.class);
@@ -481,12 +420,6 @@ class RSQLUtilityTest {
return (Path<Y>) path; 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) { private void reset0(final Object... mocks) {
reset(mocks); reset(mocks);
if (Arrays.asList(mocks).contains(baseSoftwareModuleRootMock)) { if (Arrays.asList(mocks).contains(baseSoftwareModuleRootMock)) {
@@ -553,7 +486,7 @@ class RSQLUtilityTest {
} }
@Bean @Bean
RsqlConfigHolder rsqlVisitorFactoryHolder() { RsqlConfigHolder rsqlConfigHolder() {
return RsqlConfigHolder.getInstance(); return RsqlConfigHolder.getInstance();
} }
} }

View File

@@ -12,10 +12,10 @@
logging.level.org.eclipse.persistence=ERROR logging.level.org.eclipse.persistence=ERROR
## Uncomment to see the debug of persistence, e.g. to see the generated SQLs ## Uncomment to see the debug of persistence, e.g. to see the generated SQLs
#logging.level.org.eclipse.persistence=DEBUG logging.level.org.eclipse.persistence=DEBUG
#spring.jpa.properties.eclipselink.logging.level=FINE spring.jpa.properties.eclipselink.logging.level=FINE
#spring.jpa.properties.eclipselink.logging.level.sql=FINE spring.jpa.properties.eclipselink.logging.level.sql=FINE
#spring.jpa.properties.eclipselink.logging.parameters=true spring.jpa.properties.eclipselink.logging.parameters=true
## Enable EclipseLink performance monitor (monitoring and profile) ## Enable EclipseLink performance monitor (monitoring and profile)
#spring.jpa.properties.eclipselink.profiler=PerformanceMonitor #spring.jpa.properties.eclipselink.profiler=PerformanceMonitor