diff --git a/hawkbit-core/src/main/java/org/eclipse/hawkbit/repository/ActionFields.java b/hawkbit-core/src/main/java/org/eclipse/hawkbit/repository/ActionFields.java index 55e8f509f..3a1e49bed 100644 --- a/hawkbit-core/src/main/java/org/eclipse/hawkbit/repository/ActionFields.java +++ b/hawkbit-core/src/main/java/org/eclipse/hawkbit/repository/ActionFields.java @@ -9,6 +9,7 @@ */ package org.eclipse.hawkbit.repository; +import java.util.Arrays; import java.util.Collections; import java.util.List; @@ -53,13 +54,8 @@ public enum ActionFields implements RsqlQueryField, FieldValueConverter> { /** - * Converts the given {@code value} into the representation to build a - * generic query. + * Converts the given {@code value} into the representation to build ageneric 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 * @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. + * @throws IllegalArgumentException if the value is not supported */ - Object convertValue(final T e, 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); + Object convertValue(final T enumValue, final String value); } \ No newline at end of file diff --git a/hawkbit-core/src/main/java/org/eclipse/hawkbit/repository/TargetFields.java b/hawkbit-core/src/main/java/org/eclipse/hawkbit/repository/TargetFields.java index 2489117ac..471846ac5 100644 --- a/hawkbit-core/src/main/java/org/eclipse/hawkbit/repository/TargetFields.java +++ b/hawkbit-core/src/main/java/org/eclipse/hawkbit/repository/TargetFields.java @@ -33,9 +33,13 @@ public enum TargetFields implements RsqlQueryField { UPDATESTATUS("updateStatus"), IPADDRESS("address"), ATTRIBUTE("controllerAttributes"), - ASSIGNEDDS("assignedDistributionSet", "name", "version"), - INSTALLEDDS("installedDistributionSet", "name", "version"), - TAG("tags", "name"), + ASSIGNEDDS( + "assignedDistributionSet", + DistributionSetFields.NAME.getJpaEntityFieldName(), DistributionSetFields.VERSION.getJpaEntityFieldName()), + INSTALLEDDS( + "installedDistributionSet", + DistributionSetFields.NAME.getJpaEntityFieldName(), DistributionSetFields.VERSION.getJpaEntityFieldName()), + TAG("tags", TagFields.NAME.getJpaEntityFieldName()), LASTCONTROLLERREQUESTAT("lastTargetQuery"), METADATA("metadata", new SimpleImmutableEntry<>("key", "value")), TARGETTYPE("targetType", TargetTypeFields.KEY.getJpaEntityFieldName(), TargetTypeFields.NAME.getJpaEntityFieldName()); diff --git a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/exception/RSQLParameterSyntaxException.java b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/exception/RSQLParameterSyntaxException.java index d86d19e97..d93b0e887 100644 --- a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/exception/RSQLParameterSyntaxException.java +++ b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/exception/RSQLParameterSyntaxException.java @@ -11,12 +11,16 @@ package org.eclipse.hawkbit.repository.exception; import java.io.Serial; +import lombok.EqualsAndHashCode; +import lombok.ToString; import org.eclipse.hawkbit.exception.AbstractServerRtException; import org.eclipse.hawkbit.exception.SpServerError; /** * Exception used by the REST API in case of RSQL search filter query. */ +@ToString(callSuper = true) +@EqualsAndHashCode(callSuper = true) public class RSQLParameterSyntaxException extends AbstractServerRtException { @Serial diff --git a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/exception/RSQLParameterUnsupportedFieldException.java b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/exception/RSQLParameterUnsupportedFieldException.java index 199c65d5d..5c6186030 100644 --- a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/exception/RSQLParameterUnsupportedFieldException.java +++ b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/exception/RSQLParameterUnsupportedFieldException.java @@ -11,12 +11,16 @@ package org.eclipse.hawkbit.repository.exception; import java.io.Serial; +import lombok.EqualsAndHashCode; +import lombok.ToString; import org.eclipse.hawkbit.exception.AbstractServerRtException; import org.eclipse.hawkbit.exception.SpServerError; /** * 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 { @Serial diff --git a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/rsql/RsqlConfigHolder.java b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/rsql/RsqlConfigHolder.java index 83fff6f7b..3835ac33e 100644 --- a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/rsql/RsqlConfigHolder.java +++ b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/rsql/RsqlConfigHolder.java @@ -15,8 +15,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; /** - * Helper class providing static access to the RSQL configuration as managed the ignoreCase and - * the {@link RsqlVisitorFactory} bean. + * Helper class providing static access to the RSQL configuration as managed bean. */ @NoArgsConstructor(access = lombok.AccessLevel.PRIVATE) @Getter @@ -26,8 +25,7 @@ public final class RsqlConfigHolder { private static final RsqlConfigHolder SINGLETON = new RsqlConfigHolder(); /** - * If RSQL comparison operators shall ignore the case. If ignore case is true - * "x == ax" will match "x == aX" + * If RSQL comparison operators shall ignore the case. If ignore case is true "x == ax" will match "x == aX" */ @Value("${hawkbit.rsql.ignore-case:true}") private boolean ignoreCase; @@ -41,13 +39,6 @@ public final class RsqlConfigHolder { @Value("${hawkbit.rsql.case-insensitive-db:false}") 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 */ diff --git a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/rsql/RsqlVisitorFactory.java b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/rsql/RsqlVisitorFactory.java deleted file mode 100644 index 24305f32e..000000000 --- a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/rsql/RsqlVisitorFactory.java +++ /dev/null @@ -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 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. - */ - & RsqlQueryField> RSQLVisitor validationRsqlVisitor(Class fieldNameProvider); - -} diff --git a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/rsql/VirtualPropertyReplacer.java b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/rsql/VirtualPropertyReplacer.java index 96835b8fb..b739dc78c 100644 --- a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/rsql/VirtualPropertyReplacer.java +++ b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/rsql/VirtualPropertyReplacer.java @@ -22,9 +22,8 @@ public interface VirtualPropertyReplacer extends Serializable { /** * Looks up a placeholders and replaces them * - * @param input the input string in which virtual properties should be - * replaced + * @param input the input string in which virtual properties should be replaced * @return the result of the replacement */ String replace(String input); -} +} \ No newline at end of file diff --git a/hawkbit-repository/hawkbit-repository-core/src/main/java/org/eclipse/hawkbit/repository/rsql/VirtualPropertyResolver.java b/hawkbit-repository/hawkbit-repository-core/src/main/java/org/eclipse/hawkbit/repository/rsql/VirtualPropertyResolver.java index ba7b243fe..ad802adc3 100644 --- a/hawkbit-repository/hawkbit-repository-core/src/main/java/org/eclipse/hawkbit/repository/rsql/VirtualPropertyResolver.java +++ b/hawkbit-repository/hawkbit-repository-core/src/main/java/org/eclipse/hawkbit/repository/rsql/VirtualPropertyResolver.java @@ -16,29 +16,21 @@ import org.apache.commons.text.lookup.StringLookupFactory; import org.eclipse.hawkbit.repository.TimestampCalculator; /** - * Adds macro capabilities to RSQL expressions that are used to filter for - * targets. + * Adds macro capabilities to RSQL expressions that are used to filter for targets. *

- * Some (virtual) properties do not have a representation in the database (in - * general these properties are time-related, or more explicitly, they deal with - * time intervals).
- * 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. Therefore a - * placeholder is used in the RSQL expression that is expanded when the RSQL is - * parsed + * Some (virtual) properties do not have a representation in the database (in general these properties are time-related, or more explicitly, + * they deal with time intervals).
+ * 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. + * Therefore, a placeholder is used in the RSQL expression that is expanded when the RSQL is parsed *

- * A virtual property may either be a system value like the current date (aka - * now_ts) or a value derived from (tenant-specific) system + * A virtual property may either be a system value like the current date (aka now_ts) or a value derived from (tenant-specific) system * configuration (e.g. overdue_ts). *

* Known values are:
*

*/ public class VirtualPropertyResolver implements VirtualPropertyReplacer { @@ -46,19 +38,16 @@ public class VirtualPropertyResolver implements VirtualPropertyReplacer { @Serial 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 public String replace(final String input) { - if (substitutor == null) { - substitutor = new StringSubstitutor( - StringLookupFactory.builder().get().functionStringLookup(this::lookup), - StringSubstitutor.DEFAULT_PREFIX, StringSubstitutor.DEFAULT_SUFFIX, StringSubstitutor.DEFAULT_ESCAPE); - } - return substitutor.replace(input); + return STRING_SUBSTITUTOR.replace(input); } - private String lookup(final String rhs) { + private static String lookup(final String rhs) { if ("now_ts".equalsIgnoreCase(rhs)) { return String.valueOf(System.currentTimeMillis()); } else if ("overdue_ts".equalsIgnoreCase(rhs)) { diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/RepositoryApplicationConfiguration.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/RepositoryApplicationConfiguration.java index 3d008ca41..305ed64f2 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/RepositoryApplicationConfiguration.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/RepositoryApplicationConfiguration.java @@ -146,7 +146,6 @@ import org.eclipse.hawkbit.repository.jpa.rollout.condition.RolloutGroupEvaluati import org.eclipse.hawkbit.repository.jpa.rollout.condition.StartNextGroupRolloutGroupSuccessAction; import org.eclipse.hawkbit.repository.jpa.rollout.condition.ThresholdRolloutGroupErrorCondition; import org.eclipse.hawkbit.repository.jpa.rollout.condition.ThresholdRolloutGroupSuccessCondition; -import org.eclipse.hawkbit.repository.jpa.rsql.DefaultRsqlVisitorFactory; import org.eclipse.hawkbit.repository.model.DistributionSet; import org.eclipse.hawkbit.repository.model.DistributionSetType; import org.eclipse.hawkbit.repository.model.Rollout; @@ -159,7 +158,6 @@ import org.eclipse.hawkbit.repository.model.helper.EventPublisherHolder; import org.eclipse.hawkbit.repository.model.helper.SystemSecurityContextHolder; import org.eclipse.hawkbit.repository.model.helper.TenantConfigurationManagementHolder; import org.eclipse.hawkbit.repository.rsql.RsqlConfigHolder; -import org.eclipse.hawkbit.repository.rsql.RsqlVisitorFactory; import org.eclipse.hawkbit.repository.rsql.VirtualPropertyReplacer; import org.eclipse.hawkbit.security.HawkbitSecurityProperties; import org.eclipse.hawkbit.security.SecurityTokenGenerator; @@ -656,10 +654,11 @@ public class RepositoryApplicationConfiguration { final DistributionSetManagement distributionSetManagement, final QuotaManagement quotaManagement, final JpaProperties properties, final TenantConfigurationManagement tenantConfigurationManagement, final RepositoryProperties repositoryProperties, - final SystemSecurityContext systemSecurityContext, final ContextAware contextAware, final AuditorAware auditorAware) { + final SystemSecurityContext systemSecurityContext, final ContextAware contextAware, final AuditorAware auditorAware, + final EntityManager entityManager) { return new JpaTargetFilterQueryManagement(targetFilterQueryRepository, targetManagement, virtualPropertyReplacer, distributionSetManagement, quotaManagement, properties.getDatabase(), - tenantConfigurationManagement, repositoryProperties, systemSecurityContext, contextAware, auditorAware); + tenantConfigurationManagement, repositoryProperties, systemSecurityContext, contextAware, auditorAware, entityManager); } /** @@ -1014,24 +1013,13 @@ public class RepositoryApplicationConfiguration { return new RolloutScheduler(rolloutHandler, systemManagement, systemSecurityContext, threadPoolSize, meterRegistry); } - /** - * Creates the {@link RsqlVisitorFactory} bean. - * - * @return A new {@link RsqlVisitorFactory} bean. - */ - @Bean - @ConditionalOnMissingBean - RsqlVisitorFactory rsqlVisitorFactory() { - return new DefaultRsqlVisitorFactory(); - } - /** * Obtains the {@link RsqlConfigHolder} bean. * * @return The {@link RsqlConfigHolder} singleton. */ @Bean - RsqlConfigHolder rsqlVisitorFactoryHolder() { + RsqlConfigHolder rsqlConfigHolder() { return RsqlConfigHolder.getInstance(); } diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/management/JpaTargetFilterQueryManagement.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/management/JpaTargetFilterQueryManagement.java index c0ac2140e..7ba6a2037 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/management/JpaTargetFilterQueryManagement.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/management/JpaTargetFilterQueryManagement.java @@ -14,6 +14,7 @@ import java.util.Collections; import java.util.List; import java.util.Optional; +import jakarta.persistence.EntityManager; import jakarta.validation.constraints.NotNull; import cz.jirutka.rsql.parser.RSQLParserException; @@ -39,6 +40,7 @@ import org.eclipse.hawkbit.repository.jpa.acm.AccessController; import org.eclipse.hawkbit.repository.jpa.builder.JpaTargetFilterQueryCreate; import org.eclipse.hawkbit.repository.jpa.configuration.Constants; import org.eclipse.hawkbit.repository.jpa.model.JpaDistributionSet; +import org.eclipse.hawkbit.repository.jpa.model.JpaTarget; import org.eclipse.hawkbit.repository.jpa.model.JpaTargetFilterQuery; import org.eclipse.hawkbit.repository.jpa.repository.TargetFilterQueryRepository; import org.eclipse.hawkbit.repository.jpa.rsql.RSQLUtility; @@ -83,6 +85,7 @@ public class JpaTargetFilterQueryManagement implements TargetFilterQueryManageme private final SystemSecurityContext systemSecurityContext; private final ContextAware contextAware; private final AuditorAware auditorAware; + private final EntityManager entityManager; private final Database database; @SuppressWarnings("java:S107") @@ -91,7 +94,8 @@ public class JpaTargetFilterQueryManagement implements TargetFilterQueryManageme final DistributionSetManagement distributionSetManagement, final QuotaManagement quotaManagement, final Database database, final TenantConfigurationManagement tenantConfigurationManagement, final RepositoryProperties repositoryProperties, - final SystemSecurityContext systemSecurityContext, final ContextAware contextAware, final AuditorAware auditorAware) { + final SystemSecurityContext systemSecurityContext, final ContextAware contextAware, final AuditorAware auditorAware, + final EntityManager entityManager) { this.targetFilterQueryRepository = targetFilterQueryRepository; this.targetManagement = targetManagement; this.virtualPropertyReplacer = virtualPropertyReplacer; @@ -103,6 +107,7 @@ public class JpaTargetFilterQueryManagement implements TargetFilterQueryManageme this.systemSecurityContext = systemSecurityContext; this.contextAware = contextAware; this.auditorAware = auditorAware; + this.entityManager = entityManager; } @Override @@ -114,7 +119,7 @@ public class JpaTargetFilterQueryManagement implements TargetFilterQueryManageme create.getQuery().ifPresent(query -> { // validate the RSQL query syntax - RSQLUtility.validateRsqlFor(query, TargetFields.class); + RSQLUtility.validateRsqlFor(query, TargetFields.class, JpaTarget.class, virtualPropertyReplacer, entityManager); // enforce the 'max targets per auto assign' quota right here even // if the result of the filter query can vary over time @@ -143,7 +148,7 @@ public class JpaTargetFilterQueryManagement implements TargetFilterQueryManageme @Override public boolean verifyTargetFilterQuerySyntax(final String query) { try { - RSQLUtility.validateRsqlFor(query, TargetFields.class); + RSQLUtility.validateRsqlFor(query, TargetFields.class, JpaTarget.class, virtualPropertyReplacer, entityManager); return true; } catch (final RSQLParserException | RSQLParameterUnsupportedFieldException e) { log.debug("The RSQL query '{}}' is invalid.", query, e); diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/management/JpaTargetManagement.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/management/JpaTargetManagement.java index bddc0fdaf..43b4cd85a 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/management/JpaTargetManagement.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/management/JpaTargetManagement.java @@ -694,13 +694,12 @@ public class JpaTargetManagement implements TargetManagement { @Override public boolean isTargetMatchingQueryAndDSNotAssignedAndCompatibleAndUpdatable(final String controllerId, final long distributionSetId, final String targetFilterQuery) { - RSQLUtility.validateRsqlFor(targetFilterQuery, TargetFields.class); + RSQLUtility.validateRsqlFor(targetFilterQuery, TargetFields.class, JpaTarget.class, virtualPropertyReplacer, entityManager); final DistributionSet ds = distributionSetManagement.get(distributionSetId) .orElseThrow(() -> new EntityNotFoundException(DistributionSet.class, distributionSetId)); final Long distSetTypeId = ds.getType().getId(); final List> specList = Arrays.asList( - RSQLUtility.buildRsqlSpecification(targetFilterQuery, TargetFields.class, virtualPropertyReplacer, - database), + RSQLUtility.buildRsqlSpecification(targetFilterQuery, TargetFields.class, virtualPropertyReplacer, database), TargetSpecifications.hasNotDistributionSetInActions(distributionSetId), TargetSpecifications.isCompatibleWithDistributionSetType(distSetTypeId), TargetSpecifications.hasControllerId(controllerId)); diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/model/JpaTarget.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/model/JpaTarget.java index 07bf1d647..f862af0a9 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/model/JpaTarget.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/model/JpaTarget.java @@ -188,8 +188,8 @@ public class JpaTarget extends AbstractJpaNamedEntity implements Target, EventAw name = "sp_target_attributes", joinColumns = { @JoinColumn(name = "target", nullable = false) }, foreignKey = @ForeignKey(value = ConstraintMode.CONSTRAINT, name = "fk_target_attributes_target")) - @Column(name = "attribute_value", length = Target.CONTROLLER_ATTRIBUTE_VALUE_SIZE) @MapKeyColumn(name = "attribute_key", length = Target.CONTROLLER_ATTRIBUTE_KEY_SIZE) + @Column(name = "attribute_value", length = Target.CONTROLLER_ATTRIBUTE_VALUE_SIZE) private Map controllerAttributes; @OneToMany(mappedBy = "target", fetch = FetchType.LAZY, cascade = { CascadeType.REMOVE }, targetEntity = JpaTargetMetadata.class) diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/rsql/AbstractRSQLVisitor.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/rsql/AbstractRSQLVisitor.java index 9f8258898..e0c9b461d 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/rsql/AbstractRSQLVisitor.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/rsql/AbstractRSQLVisitor.java @@ -32,7 +32,7 @@ public abstract class AbstractRSQLVisitor
& RsqlQueryField> { } @SuppressWarnings("java:S1066") // java:S1066 - more readable with separate "if" statements - protected QuertPath getQuertPath(final ComparisonNode node) { + protected QueryPath getQueryPath(final ComparisonNode node) { final int firstSeparatorIndex = node.getSelector().indexOf(RsqlQueryField.SUB_ATTRIBUTE_SEPARATOR); final String enumName = (firstSeparatorIndex == -1 ? node.getSelector() @@ -67,7 +67,7 @@ public abstract class AbstractRSQLVisitor & RsqlQueryField> { } } - return new QuertPath(enumValue, split); + return new QueryPath(enumValue, split); } catch (final IllegalArgumentException e) { throw createRSQLParameterUnsupportedException(node, e); } @@ -128,12 +128,12 @@ public abstract class AbstractRSQLVisitor & RsqlQueryField> { } @Value - protected class QuertPath { + protected class QueryPath { A enumValue; String[] jpaPath; - private QuertPath(final A enumValue, final String[] jpaPath) { + private QueryPath(final A enumValue, final String[] jpaPath) { this.enumValue = enumValue; this.jpaPath = jpaPath; } diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/rsql/DefaultRsqlVisitorFactory.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/rsql/DefaultRsqlVisitorFactory.java deleted file mode 100644 index 4d03ac40c..000000000 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/rsql/DefaultRsqlVisitorFactory.java +++ /dev/null @@ -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 & RsqlQueryField> RSQLVisitor validationRsqlVisitor( - final Class fieldNameProvider) { - return new FieldValidationRsqlVisitor<>(fieldNameProvider); - } - -} diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/rsql/FieldValidationRsqlVisitor.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/rsql/FieldValidationRsqlVisitor.java deleted file mode 100644 index e8acbfcf2..000000000 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/rsql/FieldValidationRsqlVisitor.java +++ /dev/null @@ -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 The type the {@link RsqlQueryField} refers to. - */ -public class FieldValidationRsqlVisitor & RsqlQueryField> extends AbstractRSQLVisitor - implements RSQLVisitor { - - /** - * Constructs the visitor and initializes it. - * - * @param fieldNameProvider The {@link RsqlQueryField} to use for validation. - */ - public FieldValidationRsqlVisitor(final Class 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; - } -} \ No newline at end of file diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/rsql/JpaQueryRsqlVisitor.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/rsql/JpaQueryRsqlVisitor.java index 0b7ae4140..df36fbe0a 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/rsql/JpaQueryRsqlVisitor.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/rsql/JpaQueryRsqlVisitor.java @@ -21,7 +21,6 @@ import java.util.Optional; import java.util.Set; import java.util.function.BiFunction; import java.util.function.Function; -import java.util.stream.Collectors; import jakarta.persistence.criteria.CriteriaBuilder; import jakarta.persistence.criteria.CriteriaQuery; @@ -82,14 +81,15 @@ public class JpaQueryRsqlVisitor & RsqlQueryField, T> extends private final Database database; private final boolean ensureIgnoreCase; private final Root root; - private final SimpleTypeConverter simpleTypeConverter; private final VirtualPropertyReplacer virtualPropertyReplacer; + private final SimpleTypeConverter simpleTypeConverter; private int level; private boolean isOrLevel; private boolean joinsNeeded; - public JpaQueryRsqlVisitor(final Root root, final CriteriaBuilder cb, final Class enumType, + public JpaQueryRsqlVisitor( + final Root root, final CriteriaBuilder cb, final Class enumType, final VirtualPropertyReplacer virtualPropertyReplacer, final Database database, final CriteriaQuery query, final boolean ensureIgnoreCase) { super(enumType); @@ -129,7 +129,7 @@ public class JpaQueryRsqlVisitor & RsqlQueryField, T> extends // https://jira.sonarsource.com/browse/SONARJAVA-1478 @SuppressWarnings({ "squid:S2095" }) public List visit(final ComparisonNode node, final String param) { - final QuertPath queryPath = getQuertPath(node); + final QueryPath queryPath = getQueryPath(node); final List values = node.getArguments(); final List transformedValues = new ArrayList<>(); @@ -266,7 +266,7 @@ public class JpaQueryRsqlVisitor & RsqlQueryField, T> extends * @return the Path for a field */ @SuppressWarnings("unchecked") - private Path getFieldPath(final A enumField, final QuertPath queryPath) { + private Path getFieldPath(final A enumField, final QueryPath queryPath) { return (Path) getFieldPath(root, queryPath.getJpaPath(), enumField.isMap(), this::getJoinFieldPath).orElseThrow( () -> new RSQLParameterUnsupportedFieldException("RSQL field path cannot be empty", null)); @@ -303,7 +303,7 @@ public class JpaQueryRsqlVisitor & RsqlQueryField, T> extends return transformEnumValue(node, value, javaType); } if (fieldName instanceof FieldValueConverter) { - return convertFieldConverterValue(node, fieldName, value); + return convertFieldConverterValue(fieldName, value); } if (Boolean.TYPE.equals(javaType)) { @@ -317,28 +317,24 @@ public class JpaQueryRsqlVisitor & RsqlQueryField, T> extends try { return simpleTypeConverter.convertIfNecessary(value, javaType); } catch (final TypeMismatchException e) { - throw new RSQLParameterSyntaxException( + throw new RSQLParameterUnsupportedFieldException( "The value of the given search parameter field {" + node.getSelector() - + "} is not well formed. Only a boolean (true or false) value will be expected {", + + "} is not well formed. Only a boolean (true or false) value will be expected", e); } } @SuppressWarnings({ "rawtypes", "unchecked" }) - private Object convertFieldConverterValue(final ComparisonNode node, final A fieldName, final String value) { - final Object convertedValue = ((FieldValueConverter) fieldName).convertValue(fieldName, value); - if (convertedValue == null) { - throw new RSQLParameterUnsupportedFieldException( - "field {" + node.getSelector() + "} must be one of the following values {" - + Arrays.toString(((FieldValueConverter) fieldName).possibleValues(fieldName)) + "}", - null); - } else { - return convertedValue; + private Object convertFieldConverterValue(final A fieldName, final String value) { + try { + return ((FieldValueConverter) fieldName).convertValue(fieldName, value); + } catch (final Exception e) { + throw new RSQLParameterSyntaxException(e.getMessage(), null); } } private List mapToPredicate(final ComparisonNode node, final Path fieldPath, - final List values, final List transformedValues, final QuertPath queryPath) { + final List values, final List transformedValues, final QueryPath queryPath) { String value = values.get(0); // if lookup is available, replace macros ... @@ -355,7 +351,7 @@ public class JpaQueryRsqlVisitor & RsqlQueryField, T> extends } private Predicate addOperatorPredicate(final ComparisonNode node, final Path fieldPath, - final List transformedValues, final String value, final QuertPath queryPath) { + final List transformedValues, final String value, final QueryPath queryPath) { // only 'equal' and 'notEqual' can handle transformed value like // enums. The JPA API cannot handle object types for greaterThan etc @@ -388,7 +384,7 @@ public class JpaQueryRsqlVisitor & RsqlQueryField, T> extends private Predicate getOutPredicate( final List transformedValues, - final QuertPath queryPath, final Path fieldPath) { + final QueryPath queryPath, final Path fieldPath) { final String[] fieldNames = queryPath.getJpaPath(); if (isSimpleField(fieldNames, queryPath.getEnumValue().isMap())) { @@ -412,7 +408,7 @@ public class JpaQueryRsqlVisitor & RsqlQueryField, T> extends } @SuppressWarnings("unchecked") - private Predicate mapToMapPredicate(final Path fieldPath, final QuertPath queryPath) { + private Predicate mapToMapPredicate(final Path fieldPath, final QueryPath queryPath) { if (!queryPath.getEnumValue().isMap()) { return null; } @@ -452,7 +448,7 @@ public class JpaQueryRsqlVisitor & RsqlQueryField, T> extends } private Predicate getNotEqualToPredicate(final Object transformedValue, final Path fieldPath, - final QuertPath queryPath) { + final QueryPath queryPath) { if (transformedValue == null) { return cb.isNotNull(pathOfString(fieldPath)); diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/rsql/JpaQueryRsqlVisitorG2.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/rsql/JpaQueryRsqlVisitorG2.java index 34c8efe97..7abeb5d08 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/rsql/JpaQueryRsqlVisitorG2.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/rsql/JpaQueryRsqlVisitorG2.java @@ -9,6 +9,7 @@ */ package org.eclipse.hawkbit.repository.jpa.rsql; +import java.util.AbstractMap; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; @@ -23,6 +24,7 @@ import jakarta.persistence.criteria.CriteriaQuery; import jakarta.persistence.criteria.JoinType; import jakarta.persistence.criteria.MapJoin; import jakarta.persistence.criteria.Path; +import jakarta.persistence.criteria.PluralJoin; import jakarta.persistence.criteria.Predicate; import jakarta.persistence.criteria.Root; import jakarta.persistence.criteria.Subquery; @@ -32,9 +34,11 @@ import jakarta.persistence.metamodel.Type; import cz.jirutka.rsql.parser.ast.AndNode; import cz.jirutka.rsql.parser.ast.ComparisonNode; +import cz.jirutka.rsql.parser.ast.ComparisonOperator; import cz.jirutka.rsql.parser.ast.LogicalNode; import cz.jirutka.rsql.parser.ast.Node; import cz.jirutka.rsql.parser.ast.OrNode; +import cz.jirutka.rsql.parser.ast.RSQLOperators; import cz.jirutka.rsql.parser.ast.RSQLVisitor; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.math.NumberUtils; @@ -43,8 +47,6 @@ import org.eclipse.hawkbit.repository.RsqlQueryField; import org.eclipse.hawkbit.repository.exception.RSQLParameterSyntaxException; import org.eclipse.hawkbit.repository.exception.RSQLParameterUnsupportedFieldException; import org.eclipse.hawkbit.repository.rsql.VirtualPropertyReplacer; -import org.springframework.beans.SimpleTypeConverter; -import org.springframework.beans.TypeMismatchException; import org.springframework.orm.jpa.vendor.Database; import org.springframework.util.CollectionUtils; import org.springframework.util.ObjectUtils; @@ -70,11 +72,11 @@ public class JpaQueryRsqlVisitorG2 & RsqlQueryField, T> private final VirtualPropertyReplacer virtualPropertyReplacer; private final boolean ensureIgnoreCase; - private final SimpleTypeConverter simpleTypeConverter = new SimpleTypeConverter(); private final Map> attributeToPath = new HashMap<>(); private boolean inOr; - public JpaQueryRsqlVisitorG2(final Class enumType, + public JpaQueryRsqlVisitorG2( + final Class enumType, final Root root, final CriteriaQuery query, final CriteriaBuilder cb, final Database database, final VirtualPropertyReplacer virtualPropertyReplacer, final boolean ensureIgnoreCase) { super(enumType); @@ -106,69 +108,71 @@ public class JpaQueryRsqlVisitorG2 & RsqlQueryField, T> @Override public List visit(final ComparisonNode node, final String param) { - final QuertPath queryField = getQuertPath(node); - - final List values = node.getArguments(); - final List transformedValues = new ArrayList<>(); - final Path fieldPath = getFieldPath(root, queryField); - - for (final String value : values) { - transformedValues.add(convertValueIfNecessary(node, queryField.getEnumValue(), fieldPath, value)); - } - - return mapToPredicate(node, queryField, fieldPath, node.getArguments(), transformedValues); + final QueryPath queryPath = getQueryPath(node); + final Path fieldPath = getFieldPath(root, queryPath); + return Collections.singletonList(toPredicate(node, queryPath, fieldPath, getValues(node, queryPath, fieldPath))); } - @SuppressWarnings({ "rawtypes", "unchecked" }) - private static Object transformEnumValue(final ComparisonNode node, final Class javaType, final String value) { - final Class tmpEnumType = (Class) javaType; - try { - return Enum.valueOf(tmpEnumType, value.toUpperCase()); - } catch (final IllegalArgumentException e) { - // we could not transform the given string value into the enum - // type, so ignore it and return null and do not filter - log.info("given value {} cannot be transformed into the correct enum type {}", value.toUpperCase(), - javaType); - log.debug("value cannot be transformed to an enum", e); - - throw new RSQLParameterUnsupportedFieldException("field {" + node.getSelector() - + "} must be one of the following values {" + Arrays.stream(tmpEnumType.getEnumConstants()) - .map(v -> v.name().toLowerCase()).toList() - + "}", e); + @SuppressWarnings("java:S3776") // java:S3776 - easier to read at one place + private Predicate toPredicate(final ComparisonNode node, final QueryPath queryPath, final Path fieldPath, final List values) { + final Predicate mapEntryKeyPredicate; + if (queryPath.getEnumValue().isMap()) { + if (node.getOperator() == RSQLUtility.IS) { + // special handling of "not-exists" + if (values.size() != 1) { + throw new RSQLParameterSyntaxException("The operator '" + RSQLUtility.IS + "' can only be used with one value"); + } + if (values.get(0) == null) { + // IS operator for maps and null value is treated as doesn't exist correspondingly + ((PluralJoin) fieldPath).on(toMapEntryKeyPredicate(queryPath, fieldPath)); + return cb.isNull(getValueFieldPath(queryPath, fieldPath)); + } + } else if (node.getOperator() == RSQLUtility.NOT) { + if (values.size() != 1) { + throw new RSQLParameterSyntaxException("The operator '" + RSQLUtility.NOT + "' can only be used with one value"); + } + // NOT operator for maps and null value is treated as does exist correspondingly + ((PluralJoin) fieldPath).on(toMapEntryKeyPredicate(queryPath, fieldPath)); + final Path valueFieldPath = getValueFieldPath(queryPath, fieldPath); + if (values.get(0) == null) { + // special handling of "exists" + return cb.isNotNull(valueFieldPath); + } else { + // special handling or "not equal" or null (same as != but with possible optimized join - no subquery) + return toNotEqualToPredicate(queryPath, valueFieldPath, values.get(0)); + } + } + mapEntryKeyPredicate = toMapEntryKeyPredicate(queryPath, fieldPath); + } else { + mapEntryKeyPredicate = null; } + + final Predicate valuePredicate = toOperatorAndValuePredicate(node, queryPath, getValueFieldPath(queryPath, fieldPath), values); + + return mapEntryKeyPredicate == null ? valuePredicate : cb.and(mapEntryKeyPredicate, valuePredicate); } - private static boolean isSimpleField(final String[] split, final boolean isMapKeyField) { - return split.length == 1 || (split.length == 2 && isMapKeyField); + private Predicate toMapEntryKeyPredicate(final QueryPath queryPath, final Path fieldPath) { + final String[] graph = queryPath.getJpaPath(); + return equal(mapEntryKeyPath(queryPath, fieldPath), graph[graph.length - 1]); } @SuppressWarnings("unchecked") - private static Path pathOfString(final Path path) { - return (Path) path; - } - - private static boolean isPattern(final String transformedValue) { - if (transformedValue.contains(ESCAPE_CHAR_WITH_ASTERISK)) { - return transformedValue.replace(ESCAPE_CHAR_WITH_ASTERISK, "$").indexOf(LIKE_WILDCARD) != -1; - } else { - return transformedValue.indexOf(LIKE_WILDCARD) != -1; + private Path mapEntryKeyPath(final QueryPath queryPath, final Path fieldPath) { + if (fieldPath instanceof MapJoin) { + // Currently we support only string key. So below cast is safe. + return (Path) ((MapJoin) fieldPath).key(); } + + return fieldPath.get(queryPath.getEnumValue().getSubEntityMapTuple() + .map(Entry::getKey) + .orElseThrow(() -> new UnsupportedOperationException(String.format( + "For the fields, defined as Map, only %s java type or tuple in the form of %s are allowed!", + Map.class.getName(), AbstractMap.SimpleImmutableEntry.class.getName())))); } - private List mapToPredicate(final ComparisonNode node, final QuertPath queryField, - final Path fieldPath, - final List values, final List transformedValues) { - // if lookup is available, replace macros ... - final String value = virtualPropertyReplacer == null ? values.get(0) : virtualPropertyReplacer.replace(values.get(0)); - - final Predicate mapPredicate = queryField.getEnumValue().isMap() ? mapToMapPredicate(queryField, fieldPath) : null; - final Predicate valuePredicate = addOperatorPredicate(node, queryField, - getValueFieldPath(queryField.getEnumValue(), fieldPath), transformedValues, value); - - return Collections.singletonList(mapPredicate != null ? cb.and(mapPredicate, valuePredicate) : valuePredicate); - } - - private Path getValueFieldPath(final A enumField, final Path fieldPath) { + private Path getValueFieldPath(final QueryPath queryPath, final Path fieldPath) { + final A enumField = queryPath.getEnumValue(); if (enumField.isMap()) { final Path mapValuePath = enumField.getSubEntityMapTuple().map(Entry::getValue).map(fieldPath::get).orElse(null); return mapValuePath == null ? fieldPath : mapValuePath; @@ -177,117 +181,127 @@ public class JpaQueryRsqlVisitorG2 & RsqlQueryField, T> } } - @SuppressWarnings("unchecked") - private Predicate mapToMapPredicate(final QuertPath queryField, final Path fieldPath) { - final String[] graph = queryField.getJpaPath(); - final String keyValue = graph[graph.length - 1]; - if (fieldPath instanceof MapJoin) { - // Currently we support only string key. So below cast is safe. - return equal((Path) (((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 transformedValues, final String value) { + private Predicate toOperatorAndValuePredicate( + final ComparisonNode node, final QueryPath queryPath, final Path fieldPath, final List values) { // only 'equal' and 'notEqual' can handle transformed value like enums. - // The JPA API cannot handle object types for greaterThan etc. methods. - final Object transformedValue = transformedValues.get(0); + // The JPA API cannot handle object types for greaterThan etc. methods. For them, it shall be a string. + final Object value = values.get(0); final String operator = node.getOperator().getSymbol(); return switch (operator) { - case "==" -> getEqualToPredicate(fieldPath, transformedValue); - case "!=" -> getNotEqualToPredicate(queryField, fieldPath, transformedValue); - case "=gt=" -> cb.greaterThan(pathOfString(fieldPath), value); - case "=ge=" -> cb.greaterThanOrEqualTo(pathOfString(fieldPath), value); - case "=lt=" -> cb.lessThan(pathOfString(fieldPath), value); - case "=le=" -> cb.lessThanOrEqualTo(pathOfString(fieldPath), value); - case "=in=" -> in(pathOfString(fieldPath), transformedValues); - case "=out=" -> getOutPredicate(queryField, fieldPath, transformedValues); - default -> throw new RSQLParameterSyntaxException( - "Operator symbol {" + operator + "} is either not supported or not implemented"); + case "==", "=is=", "=eq=" -> toEqualToPredicate(fieldPath, value); + case "!=", "=not=", "=ne=" -> toNotEqualToPredicate(queryPath, fieldPath, value); + case "=gt=" -> cb.greaterThan(pathOfString(fieldPath), String.valueOf(value)); // JPA handles numbers + case "=ge=" -> cb.greaterThanOrEqualTo(pathOfString(fieldPath), String.valueOf(value)); + case "=lt=" -> cb.lessThan(pathOfString(fieldPath), String.valueOf(value)); + case "=le=" -> cb.lessThanOrEqualTo(pathOfString(fieldPath), String.valueOf(value)); + case "=in=" -> in(pathOfString(fieldPath), values); + case "=out=" -> toOutPredicate(queryPath, fieldPath, values); + default -> throw new RSQLParameterSyntaxException("Operator symbol {" + operator + "} is either not supported or not implemented"); }; } - private Predicate getEqualToPredicate(final Path fieldPath, final Object transformedValue) { - if (transformedValue == null) { + private Predicate toEqualToPredicate(final Path fieldPath, final Object value) { + if (value == null) { return cb.isNull(fieldPath); } - if ((transformedValue instanceof String transformedValueStr) && !NumberUtils.isCreatable(transformedValueStr)) { - if (ObjectUtils.isEmpty(transformedValue)) { + if ((value instanceof String valueStr) && !NumberUtils.isCreatable(valueStr)) { + if (ObjectUtils.isEmpty(value)) { return cb.or(cb.isNull(fieldPath), cb.equal(pathOfString(fieldPath), "")); } final Path stringExpression = pathOfString(fieldPath); - if (isPattern(transformedValueStr)) { // a pattern, use like - return like(stringExpression, toSQL(transformedValueStr)); + if (isPattern(valueStr)) { // a pattern, use like + return like(stringExpression, toSQL(valueStr)); } else { - return equal(stringExpression, transformedValueStr); + return equal(stringExpression, valueStr); } } - return cb.equal(fieldPath, transformedValue); + return cb.equal(fieldPath, value); } - private Predicate getNotEqualToPredicate(final QuertPath queryField, - final Path fieldPath, final Object transformedValue) { - if (transformedValue == null) { + // if value is null -> not null + // if value is not null -> null or not equal value + private Predicate toNotEqualToPredicate(final QueryPath queryPath, final Path fieldPath, final Object value) { + if (value == null) { return cb.isNotNull(fieldPath); } - if ((transformedValue instanceof String transformedValueStr) && !NumberUtils.isCreatable(transformedValueStr)) { - if (ObjectUtils.isEmpty(transformedValue)) { + if (value instanceof String valueStr && !NumberUtils.isCreatable(valueStr)) { + if (ObjectUtils.isEmpty(value)) { return cb.and(cb.isNotNull(fieldPath), cb.notEqual(pathOfString(fieldPath), "")); } - final String[] fieldNames = queryField.getJpaPath(); - - if (isSimpleField(fieldNames, queryField.getEnumValue().isMap())) { - if (isPattern(transformedValueStr)) { // a pattern, use like - return cb.or(cb.isNull(fieldPath), notLike(pathOfString(fieldPath), toSQL(transformedValueStr))); + if (isSimpleField(queryPath)) { + if (isPattern(valueStr)) { // a pattern, use like + return cb.or(cb.isNull(fieldPath), notLike(pathOfString(fieldPath), toSQL(valueStr))); } else { - return toNullOrNotEqualPredicate(fieldPath, transformedValueStr); + return toNullOrNotEqualPredicate(fieldPath, valueStr); } } - return toNotExistsSubQueryPredicate(queryField, fieldPath, expressionToCompare -> - isPattern(transformedValueStr) ? // a pattern, use like - like(expressionToCompare, toSQL(transformedValueStr)) : - equal(expressionToCompare, transformedValueStr)); + return toNotExistsSubQueryPredicate( + queryPath, fieldPath, expressionToCompare -> + isPattern(valueStr) + ? like(expressionToCompare, toSQL(valueStr)) // a pattern, use like + : equal(expressionToCompare, valueStr)); } - return toNullOrNotEqualPredicate(fieldPath, transformedValue); + return toNullOrNotEqualPredicate(fieldPath, value); } - private Predicate getOutPredicate(final QuertPath queryField, final Path fieldPath, - final List transformedValues) { - final String[] subAttributes = queryField.getJpaPath(); - - if (isSimpleField(subAttributes, queryField.getEnumValue().isMap())) { - return cb.or(cb.isNull(fieldPath), cb.not(in(pathOfString(fieldPath), transformedValues))); + private Predicate toOutPredicate(final QueryPath queryPath, final Path fieldPath, final List values) { + if (isSimpleField(queryPath)) { + return cb.or(cb.isNull(fieldPath), cb.not(in(pathOfString(fieldPath), values))); } - return toNotExistsSubQueryPredicate(queryField, fieldPath, expressionToCompare -> in(expressionToCompare, transformedValues)); + return toNotExistsSubQueryPredicate(queryPath, fieldPath, expressionToCompare -> in(expressionToCompare, values)); } - private Path getFieldPath(final Root root, final QuertPath queryField) { - final String[] split = queryField.getJpaPath(); + private Path getFieldPath(final Root root, final QueryPath queryPath) { + final String[] split = queryPath.getJpaPath(); Path fieldPath = null; - for (int i = 0, end = queryField.getEnumValue().isMap() ? split.length - 1 : split.length; i < end; i++) { + for (int i = 0, end = queryPath.getEnumValue().isMap() ? split.length - 1 : split.length; i < end; i++) { final String fieldNameSplit = split[i]; fieldPath = fieldPath == null ? getPath(root, fieldNameSplit) : fieldPath.get(fieldNameSplit); } if (fieldPath == null) { - throw new RSQLParameterUnsupportedFieldException("RSQL field path cannot be empty", null); + throw new RSQLParameterUnsupportedFieldException("RSQL field path must not be null", null); } return fieldPath; } + @SuppressWarnings("java:S3776") // java:S3776 - easier to read at one place + private List getValues(final ComparisonNode node, final AbstractRSQLVisitor.QueryPath queryPath, final Path fieldPath) { + final List values = node.getArguments().stream() + // if lookup is available, replace macros ... + .map(value -> virtualPropertyReplacer == null ? value : virtualPropertyReplacer.replace(value)) + // converts value to the correct type + .map(value -> convertValueIfNecessary(node, queryPath.getEnumValue(), fieldPath, value)) + .toList(); + if (values.isEmpty()) { + throw new RSQLParameterSyntaxException("RSQL values must not be empty", null); + } else if (values.size() == 1) { + if (!(values.get(0) instanceof String)) { // enum or boolean or null - doesn's support >, >=, <, <= + final ComparisonOperator operator = node.getOperator(); + if (operator == RSQLOperators.GREATER_THAN || + operator == RSQLOperators.GREATER_THAN_OR_EQUAL || + operator == RSQLOperators.LESS_THAN || + operator == RSQLOperators.LESS_THAN_OR_EQUAL) { + final String errorMsg = values.get(0) == null ? "to null value" : "to enum or boolean field"; + throw new RSQLParameterSyntaxException(operator.getSymbol() + " operator could not be applied " + errorMsg, null); + } + } + } else { + final ComparisonOperator operator = node.getOperator(); + if (operator != RSQLOperators.IN && operator != RSQLOperators.NOT_IN) { + throw new RSQLParameterSyntaxException(operator.getSymbol() + " operator shall have exactly one value", null); + } + } + return values; + } + // if root.get creates a join we call join directly in order to specify LEFT JOIN type, to include rows for missing in particular // table / criteria (root.get creates INNER JOIN) (see org.eclipse.persistence.internal.jpa.querydef.FromImpl implementation // for more details) otherwise delegate to root.get @@ -306,7 +320,8 @@ public class JpaQueryRsqlVisitorG2 & RsqlQueryField, T> .findFirst() .orElseGet(() -> root.join(fieldNameSplit, JoinType.LEFT)); } - } // if a collection - it is a join + } + // if a collection - it is a join if (inOr && root == this.root) { // try to reuse join of the same "or" level and no subquery return attributeToPath.computeIfAbsent(attribute.getName(), k -> root.join(fieldNameSplit, JoinType.LEFT)); } else { @@ -314,60 +329,15 @@ public class JpaQueryRsqlVisitorG2 & RsqlQueryField, T> } } - private Object convertValueIfNecessary(final ComparisonNode node, final A fieldName, final Path fieldPath, final String value) { - // in case the value of an RSQL query e.g. type==application is an enum we need to handle it separately because JPA needs the - // correct java-type to build an expression. So String and numeric values JPA can do it by its own but not for classes like enums. - // So we need to transform the given value string into the enum class. - final Class javaType = fieldPath.getJavaType(); - if (javaType != null && javaType.isEnum()) { - return transformEnumValue(node, javaType, value); - } - if (fieldName instanceof FieldValueConverter) { - return convertFieldConverterValue(node, fieldName, value); - } - - if (Boolean.TYPE.equals(javaType) || Boolean.class.equals(javaType)) { - return convertBooleanValue(node, javaType, value); - } - - return value; - } - - private Object convertBooleanValue(final ComparisonNode node, final Class javaType, final String value) { - try { - return simpleTypeConverter.convertIfNecessary(value, javaType); - } catch (final TypeMismatchException e) { - throw new RSQLParameterSyntaxException( - "The value of the given search parameter field {" + node.getSelector() + - "} is not well formed. Only a boolean (true or false) value will be expected {", - e); - } - } - - @SuppressWarnings({ "rawtypes", "unchecked" }) - private Object convertFieldConverterValue(final ComparisonNode node, final A fieldName, final String value) { - final Object convertedValue = ((FieldValueConverter) fieldName).convertValue(fieldName, value); - if (convertedValue == null) { - throw new RSQLParameterUnsupportedFieldException( - "field {" + node.getSelector() + "} must be one of the following values {" + - Arrays.toString(((FieldValueConverter) fieldName).possibleValues(fieldName)) + "}", - null); - } else { - return convertedValue; - } - } - - private Predicate toNullOrNotEqualPredicate(final Path fieldPath, final Object transformedValue) { + private Predicate toNullOrNotEqualPredicate(final Path fieldPath, final Object value) { return cb.or( cb.isNull(fieldPath), - transformedValue instanceof String transformedValueStr - ? notEqual(pathOfString(fieldPath), transformedValueStr) - : cb.notEqual(fieldPath, transformedValue)); + value instanceof String valueStr ? notEqual(pathOfString(fieldPath), valueStr) : cb.notEqual(fieldPath, value)); } @SuppressWarnings({ "unchecked", "rawtypes" }) - private Predicate toNotExistsSubQueryPredicate(final QuertPath queryField, final Path fieldPath, - final Function, Predicate> subQueryPredicateProvider) { + private Predicate toNotExistsSubQueryPredicate( + final QueryPath queryPath, final Path fieldPath, final Function, Predicate> subQueryPredicateProvider) { // if a subquery the field's parent joins are not actually used if (!inOr) { // so, if not in or (hence not reused) we remove them. Parent shall be a Join @@ -380,11 +350,11 @@ public class JpaQueryRsqlVisitorG2 & RsqlQueryField, T> return cb.not(cb.exists( subquery.select(subqueryRoot) .where(cb.and( - cb.equal(root.get(queryField.getEnumValue().identifierFieldName()), - subqueryRoot.get(queryField.getEnumValue().identifierFieldName())), + cb.equal( + root.get(queryPath.getEnumValue().identifierFieldName()), + subqueryRoot.get(queryPath.getEnumValue().identifierFieldName())), subQueryPredicateProvider.apply( - getExpressionToCompare(queryField.getEnumValue(), - getFieldPath(subqueryRoot, queryField))))))); + getExpressionToCompare(queryPath.getEnumValue(), getFieldPath(subqueryRoot, queryPath))))))); } @SuppressWarnings({ "rawtypes", "unchecked" }) @@ -405,20 +375,92 @@ public class JpaQueryRsqlVisitorG2 & RsqlQueryField, T> " Neither of those could be found!")); } - private String toSQL(final String transformedValue) { + // result is String, enum value, boolean or null + @SuppressWarnings({ "rawtypes", "unchecked" }) + private Object convertValueIfNecessary(final ComparisonNode node, final A enumValue, final Path fieldPath, final String value) { + // in case the value of an RSQL query is an enum we need to transform the given value to the correspondent java type object + final Class javaType = fieldPath.getJavaType(); + + if (javaType != null && javaType.isEnum()) { + return toEnumValue(node, javaType, value); + } + + if (enumValue instanceof FieldValueConverter fieldValueConverter) { + try { + return fieldValueConverter.convertValue(enumValue, value); + } catch (final Exception e) { + throw new RSQLParameterUnsupportedFieldException(e.getMessage(), null); + } + } + + if (boolean.class.equals(javaType) || Boolean.class.equals(javaType)) { + if ("true".equals(value) || "false".equals(value)) { + return Boolean.valueOf(value); + } else { + throw new RSQLParameterSyntaxException( + "The value of the given search parameter field {" + node.getSelector() + "} is not well formed. " + + "Only a boolean (true or false) value will be expected"); + } + } + + if ("null".equals(value)) { + final ComparisonOperator operator = node.getOperator(); + if (operator == RSQLUtility.IS || operator == RSQLUtility.NOT) { + return null; + } + } + + return value; + } + + private boolean isSimpleField(final QueryPath queryPath) { + return queryPath.getJpaPath().length == 1 || (queryPath.getJpaPath().length == 2 && queryPath.getEnumValue().isMap()); + } + + @SuppressWarnings({ "rawtypes", "unchecked" }) + private static Object toEnumValue(final ComparisonNode node, final Class javaType, final String value) { + final Class tmpEnumType = (Class) 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 pathOfString(final Path path) { + return (Path) path; + } + + private static boolean isPattern(final String value) { + if (value.contains(ESCAPE_CHAR_WITH_ASTERISK)) { + return value.replace(ESCAPE_CHAR_WITH_ASTERISK, "$").indexOf(LIKE_WILDCARD) != -1; + } else { + return value.indexOf(LIKE_WILDCARD) != -1; + } + } + + private String toSQL(final String value) { final String escaped; if (database == Database.SQL_SERVER) { - escaped = transformedValue.replace("%", "[%]").replace("_", "[_]"); + escaped = value.replace("%", "[%]").replace("_", "[_]"); } else { - escaped = transformedValue.replace("%", ESCAPE_CHAR + "%").replace("_", ESCAPE_CHAR + "_"); + escaped = value.replace("%", ESCAPE_CHAR + "%").replace("_", ESCAPE_CHAR + "_"); } return replaceIfRequired(escaped); } - private String replaceIfRequired(final String escapedValue) { + private static String replaceIfRequired(final String escapedValue) { final String finalizedValue; if (escapedValue.contains(ESCAPE_CHAR_WITH_ASTERISK)) { - finalizedValue = escapedValue.replace(ESCAPE_CHAR_WITH_ASTERISK, "$").replace(LIKE_WILDCARD, '%') + finalizedValue = escapedValue.replace(ESCAPE_CHAR_WITH_ASTERISK, "$") + .replace(LIKE_WILDCARD, '%') .replace("$", ESCAPE_CHAR_WITH_ASTERISK); } else { finalizedValue = escapedValue.replace(LIKE_WILDCARD, '%'); @@ -448,11 +490,11 @@ public class JpaQueryRsqlVisitorG2 & RsqlQueryField, T> } } - private Predicate notEqual(final Path expressionToCompare, String transformedValueStr) { + private Predicate notEqual(final Path expressionToCompare, String valueStr) { if (caseWise(expressionToCompare)) { - return cb.notEqual(cb.upper(expressionToCompare), transformedValueStr.toUpperCase()); + return cb.notEqual(cb.upper(expressionToCompare), valueStr.toUpperCase()); } else { - return cb.notEqual(expressionToCompare, transformedValueStr); + return cb.notEqual(expressionToCompare, valueStr); } } @@ -472,14 +514,14 @@ public class JpaQueryRsqlVisitorG2 & RsqlQueryField, T> } } - private Predicate in(final Path expressionToCompare, final List transformedValues) { + private Predicate in(final Path expressionToCompare, final List values) { if (ensureIgnoreCase && expressionToCompare.getJavaType() == String.class) { - final List inParams = transformedValues.stream() + final List inParams = values.stream() .filter(String.class::isInstance) .map(String.class::cast).map(String::toUpperCase).toList(); - return inParams.isEmpty() ? expressionToCompare.in(transformedValues) : cb.upper(expressionToCompare).in(inParams); + return inParams.isEmpty() ? expressionToCompare.in(values) : cb.upper(expressionToCompare).in(inParams); } else { - return expressionToCompare.in(transformedValues); + return expressionToCompare.in(values); } } diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/rsql/RSQLUtility.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/rsql/RSQLUtility.java index 8c84cb2c4..237df0d6e 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/rsql/RSQLUtility.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/rsql/RSQLUtility.java @@ -10,9 +10,12 @@ package org.eclipse.hawkbit.repository.jpa.rsql; import java.io.Serial; +import java.util.Collections; +import java.util.HashSet; import java.util.List; import java.util.Set; +import jakarta.persistence.EntityManager; import jakarta.persistence.criteria.CriteriaBuilder; import jakarta.persistence.criteria.CriteriaQuery; import jakarta.persistence.criteria.Predicate; @@ -103,27 +106,27 @@ public final class RSQLUtility { * @throws RSQLParserException if RSQL syntax is invalid * @throws RSQLParameterUnsupportedFieldException if RSQL key is not allowed */ + @SuppressWarnings({"unchecked", "rawtypes"}) public static & RsqlQueryField> void validateRsqlFor( - final String rsql, final Class fieldNameProvider) { - final RSQLVisitor visitor = - RsqlConfigHolder.getInstance().getRsqlVisitorFactory().validationRsqlVisitor(fieldNameProvider); - final Node rootNode = parseRsql(rsql); - rootNode.accept(visitor); + final String rsql, final Class fieldNameProvider, + final Class jpaType, + final VirtualPropertyReplacer virtualPropertyReplacer, final EntityManager entityManager) { + final CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder(); + final CriteriaQuery criteriaQuery = criteriaBuilder.createQuery(jpaType); + new RSQLSpecification<>(rsql, fieldNameProvider, virtualPropertyReplacer, null) + .toPredicate(criteriaQuery.from((Class)jpaType), criteriaQuery, criteriaBuilder); } - private static Node parseRsql(final String rsql) { - log.debug("Parsing rsql string {}", rsql); - try { - final Set operators = RSQLOperators.defaultOperators(); - return new RSQLParser(operators).parse( - RsqlConfigHolder.getInstance().isCaseInsensitiveDB() || RsqlConfigHolder.getInstance().isIgnoreCase() - ? rsql.toLowerCase() - : rsql); - } catch (final IllegalArgumentException e) { - throw new RSQLParameterSyntaxException("RSQL filter must not be null", e); - } catch (final RSQLParserException e) { - throw new RSQLParameterSyntaxException(e); - } + static final ComparisonOperator IS = new ComparisonOperator("=is=", "=eq="); + static final ComparisonOperator NOT = new ComparisonOperator("=not=", "=ne="); + private static final Set OPERATORS; + + static { + final Set operators = new HashSet<>(RSQLOperators.defaultOperators()); + // == and != alternatives just treating "null" string as null not as a "null" + operators.add(IS); + operators.add(NOT); + OPERATORS = Collections.unmodifiableSet(operators); } private static final class RSQLSpecification & RsqlQueryField, T> implements Specification { @@ -136,7 +139,8 @@ public final class RSQLUtility { private final VirtualPropertyReplacer virtualPropertyReplacer; private final Database database; - private RSQLSpecification(final String rsql, final Class enumType, + private RSQLSpecification( + final String rsql, final Class enumType, final VirtualPropertyReplacer virtualPropertyReplacer, final Database database) { this.rsql = rsql; this.enumType = enumType; @@ -150,17 +154,16 @@ public final class RSQLUtility { query.distinct(true); final RSQLVisitor, String> jpqQueryRSQLVisitor = - RsqlConfigHolder.getInstance().isLegacyRsqlVisitor() ? - new JpaQueryRsqlVisitor<>( + RsqlConfigHolder.getInstance().isLegacyRsqlVisitor() + ? new JpaQueryRsqlVisitor<>( root, cb, enumType, virtualPropertyReplacer, database, query, !RsqlConfigHolder.getInstance().isCaseInsensitiveDB() && RsqlConfigHolder.getInstance().isIgnoreCase()) - : - new JpaQueryRsqlVisitorG2<>( - enumType, root, query, cb, - database, virtualPropertyReplacer, - !RsqlConfigHolder.getInstance().isCaseInsensitiveDB() && RsqlConfigHolder.getInstance() - .isIgnoreCase()); + : new JpaQueryRsqlVisitorG2<>( + enumType, root, query, cb, + database, virtualPropertyReplacer, + !RsqlConfigHolder.getInstance().isCaseInsensitiveDB() && RsqlConfigHolder.getInstance() + .isIgnoreCase()); final List accept = rootNode.accept(jpqQueryRSQLVisitor); if (CollectionUtils.isEmpty(accept)) { @@ -170,4 +173,18 @@ public final class RSQLUtility { } } } + + private static Node parseRsql(final String rsql) { + log.debug("Parsing rsql string {}", rsql); + try { + return new RSQLParser(OPERATORS).parse( + RsqlConfigHolder.getInstance().isCaseInsensitiveDB() || RsqlConfigHolder.getInstance().isIgnoreCase() + ? rsql.toLowerCase() + : rsql); + } catch (final IllegalArgumentException e) { + throw new RSQLParameterSyntaxException("RSQL filter must not be null", e); + } catch (final RSQLParserException e) { + throw new RSQLParameterSyntaxException(e); + } + } } \ No newline at end of file diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/rsql/RSQLDistributionSetFieldTest.java b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/rsql/RSQLDistributionSetFieldTest.java index b0bae4fb2..f4f89ab61 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/rsql/RSQLDistributionSetFieldTest.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/rsql/RSQLDistributionSetFieldTest.java @@ -15,6 +15,8 @@ import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import java.util.Arrays; import java.util.Collections; +import jakarta.persistence.EntityManager; + import io.qameta.allure.Description; import io.qameta.allure.Feature; import io.qameta.allure.Story; @@ -23,20 +25,29 @@ import org.eclipse.hawkbit.repository.SoftwareModuleFields; import org.eclipse.hawkbit.repository.exception.RSQLParameterSyntaxException; import org.eclipse.hawkbit.repository.exception.RSQLParameterUnsupportedFieldException; import org.eclipse.hawkbit.repository.jpa.AbstractJpaIntegrationTest; +import org.eclipse.hawkbit.repository.jpa.model.JpaDistributionSet; import org.eclipse.hawkbit.repository.model.DistributionSet; import org.eclipse.hawkbit.repository.model.DistributionSetTag; import org.eclipse.hawkbit.repository.model.SoftwareModule; +import org.eclipse.hawkbit.repository.rsql.VirtualPropertyReplacer; import org.eclipse.hawkbit.repository.test.util.TestdataFactory; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.orm.jpa.vendor.Database; @Feature("Component Tests - Repository") @Story("RSQL filter distribution set") +@SuppressWarnings("java:S6813") // constructor injects are not possible for test classes class RSQLDistributionSetFieldTest extends AbstractJpaIntegrationTest { + @Autowired + protected VirtualPropertyReplacer virtualPropertyReplacer; + @Autowired + protected EntityManager entityManager; + private DistributionSet ds; private SoftwareModule sm; @@ -190,11 +201,14 @@ class RSQLDistributionSetFieldTest extends AbstractJpaIntegrationTest { assertRSQLQuery(DistributionSetFields.METADATA.name() + ".key*==value.dot", 0); assertRSQLQuery(DistributionSetFields.METADATA.name() + ".*==value.dot", 0); assertRSQLQuery(DistributionSetFields.METADATA.name() + "..==value.dot", 0); - assertRSQLQueryThrowsException(DistributionSetFields.METADATA.name() + ".==value.dot", + assertRSQLQueryThrowsException( + DistributionSetFields.METADATA.name() + ".==value.dot", RSQLParameterUnsupportedFieldException.class); - assertRSQLQueryThrowsException(DistributionSetFields.METADATA.name() + "*==value.dot", + assertRSQLQueryThrowsException( + DistributionSetFields.METADATA.name() + "*==value.dot", RSQLParameterUnsupportedFieldException.class); - assertRSQLQueryThrowsException(DistributionSetFields.METADATA.name() + "==value.dot", + assertRSQLQueryThrowsException( + DistributionSetFields.METADATA.name() + "==value.dot", RSQLParameterUnsupportedFieldException.class); } @@ -206,18 +220,15 @@ class RSQLDistributionSetFieldTest extends AbstractJpaIntegrationTest { assertThat(countAll).as("Found entity size is wrong").isEqualTo(expectedEntity); } - private void assertRSQLQueryThrowsException(final String rsqlParam, - final Class expectedException) { + private void assertRSQLQueryThrowsException(final String rsqlParam, final Class expectedException) { assertThatExceptionOfType(expectedException) - .isThrownBy(() -> RSQLUtility.validateRsqlFor(rsqlParam, DistributionSetFields.class)); + .isThrownBy(() -> RSQLUtility.validateRsqlFor( + rsqlParam, DistributionSetFields.class, JpaDistributionSet.class, virtualPropertyReplacer, entityManager)); } - private DistributionSet createDistributionSetWithMetadata(final String metadataKeyName, - final String metadataValue) { + private DistributionSet createDistributionSetWithMetadata(final String metadataKeyName, final String metadataValue) { final DistributionSet distributionSet = testdataFactory.createDistributionSet(); - createDistributionSetMetadata(distributionSet.getId(), - entityFactory.generateDsMetadata(metadataKeyName, metadataValue)); + createDistributionSetMetadata(distributionSet.getId(), entityFactory.generateDsMetadata(metadataKeyName, metadataValue)); return distributionSet; } - -} +} \ No newline at end of file diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/rsql/RSQLTargetFieldTest.java b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/rsql/RSQLTargetFieldTest.java index 150204e45..5a08820d6 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/rsql/RSQLTargetFieldTest.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/rsql/RSQLTargetFieldTest.java @@ -11,35 +11,44 @@ package org.eclipse.hawkbit.repository.jpa.rsql; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; -import static org.junit.jupiter.api.Assertions.fail; import java.util.Arrays; -import java.util.HashMap; import java.util.Map; +import jakarta.persistence.EntityManager; + import io.qameta.allure.Description; import io.qameta.allure.Feature; import io.qameta.allure.Story; -import org.assertj.core.util.Maps; import org.eclipse.hawkbit.repository.TargetFields; import org.eclipse.hawkbit.repository.TargetTypeFields; import org.eclipse.hawkbit.repository.exception.RSQLParameterUnsupportedFieldException; import org.eclipse.hawkbit.repository.jpa.AbstractJpaIntegrationTest; +import org.eclipse.hawkbit.repository.jpa.model.JpaTarget; import org.eclipse.hawkbit.repository.model.DistributionSet; import org.eclipse.hawkbit.repository.model.Target; import org.eclipse.hawkbit.repository.model.TargetTag; import org.eclipse.hawkbit.repository.model.TargetType; +import org.eclipse.hawkbit.repository.rsql.VirtualPropertyReplacer; import org.eclipse.hawkbit.repository.test.util.TestdataFactory; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.Slice; @Feature("Component Tests - Repository") @Story("RSQL filter target") +@SuppressWarnings("java:S6813") // constructor injects are not possible for test classes class RSQLTargetFieldTest extends AbstractJpaIntegrationTest { private static final String OR = ","; private static final String AND = ";"; + + @Autowired + protected VirtualPropertyReplacer virtualPropertyReplacer; + @Autowired + protected EntityManager entityManager; + private Target target; private Target target2; private TargetType targetType1; @@ -47,45 +56,6 @@ class RSQLTargetFieldTest extends AbstractJpaIntegrationTest { @BeforeEach void setupBeforeTest() { - final DistributionSet ds = testdataFactory.createDistributionSet("AssignedDs"); - - final Map attributes = new HashMap<>(); - - target = targetManagement.create(entityFactory.target().create().controllerId("targetId123") - .name("targetName123").description("targetDesc123")); - attributes.put("revision", "1.1"); - target = controllerManagement.updateControllerAttributes(target.getControllerId(), attributes, null); - target = controllerManagement.findOrRegisterTargetIfItDoesNotExist(target.getControllerId(), LOCALHOST); - createTargetMetadata(target.getControllerId(), entityFactory.generateTargetMetadata("metaKey", "metaValue")); - - target2 = targetManagement - .create(entityFactory.target().create().controllerId("targetId1234").description("targetId1234")); - attributes.put("revision", "1.2"); - - target2 = controllerManagement.updateControllerAttributes(target2.getControllerId(), attributes, null); - target2 = controllerManagement.findOrRegisterTargetIfItDoesNotExist(target2.getControllerId(), LOCALHOST); - createTargetMetadata(target2.getControllerId(), entityFactory.generateTargetMetadata("metaKey", "value")); - - final Target target3 = testdataFactory.createTarget("targetId1235"); - final Target target4 = testdataFactory.createTarget("targetId1236"); - testdataFactory.createTarget("targetId1237"); - - final TargetTag targetTag = targetTagManagement.create(entityFactory.tag().create().name("Tag1")); - final TargetTag targetTag2 = targetTagManagement.create(entityFactory.tag().create().name("Tag2")); - final TargetTag targetTag3 = targetTagManagement.create(entityFactory.tag().create().name("Tag3")); - targetTagManagement.create(entityFactory.tag().create().name("Tag4")); - - targetManagement.assignTag(Arrays.asList(target.getControllerId(), target2.getControllerId()), - targetTag.getId()); - - targetManagement.assignTag(Arrays.asList(target3.getControllerId(), target4.getControllerId()), - targetTag2.getId()); - targetManagement.assignTag( - Arrays.asList(target.getControllerId(), target3.getControllerId(), target4.getControllerId()), - targetTag3.getId()); - - assignDistributionSet(ds.getId(), target.getControllerId()); - targetType1 = targetTypeManagement .create(entityFactory.targetType().create() .name("Type1").description("Desc. Type1") @@ -95,8 +65,43 @@ class RSQLTargetFieldTest extends AbstractJpaIntegrationTest { .name("Type2").description("Desc. Type2") .key("Type2.key")); + final DistributionSet ds = testdataFactory.createDistributionSet("AssignedDs"); + + final TargetTag targetTag = targetTagManagement.create(entityFactory.tag().create().name("Tag1")); + final TargetTag targetTag2 = targetTagManagement.create(entityFactory.tag().create().name("Tag2")); + final TargetTag targetTag3 = targetTagManagement.create(entityFactory.tag().create().name("Tag3")); + targetTagManagement.create(entityFactory.tag().create().name("Tag4")); + + target = targetManagement.create( + entityFactory.target().create() + .controllerId("targetId123") + .name("targetName123") + .description("targetDesc123")); + target = controllerManagement.updateControllerAttributes(target.getControllerId(), Map.of("revision", "1.1"), null); + target = controllerManagement.findOrRegisterTargetIfItDoesNotExist(target.getControllerId(), LOCALHOST); + createTargetMetadata(target.getControllerId(), entityFactory.generateTargetMetadata("metaKey", "metaValue")); + assignDistributionSet(ds.getId(), target.getControllerId()); targetManagement.assignType(target.getControllerId(), targetType1.getId()); + + target2 = targetManagement.create( + entityFactory.target().create() + .controllerId("targetId1234") + .description("targetId1234")); + target2 = controllerManagement.updateControllerAttributes(target2.getControllerId(), Map.of("revision", "1.2"), null); targetManagement.assignType(target2.getControllerId(), targetType2.getId()); + createTargetMetadata(target2.getControllerId(), entityFactory.generateTargetMetadata("metaKey", "value")); + target2 = controllerManagement.findOrRegisterTargetIfItDoesNotExist(target2.getControllerId(), LOCALHOST); + targetManagement.assignTag(Arrays.asList(target.getControllerId(), target2.getControllerId()), targetTag.getId()); + + final Target target3 = testdataFactory.createTarget("targetId1235"); + + final Target target4 = testdataFactory.createTarget("targetId1236"); + testdataFactory.createTarget("targetId1237"); + targetManagement.assignTag(Arrays.asList(target3.getControllerId(), target4.getControllerId()), targetTag2.getId()); + + targetManagement.assignTag( + Arrays.asList(target.getControllerId(), target3.getControllerId(), target4.getControllerId()), + targetTag3.getId()); } @Test @@ -159,8 +164,38 @@ class RSQLTargetFieldTest extends AbstractJpaIntegrationTest { @Test @Description("Test filter target by attribute") void testFilterByAttribute() { - controllerManagement.updateControllerAttributes(testdataFactory.createTarget().getControllerId(), - Maps.newHashMap("test.dot", "value.dot"), null); + controllerManagement.updateControllerAttributes( + testdataFactory.createTarget().getControllerId(), + Map.of( + "test.dot", "value.dot", + "test.null", "null"), + null); + + assertRSQLQuery(TargetFields.ATTRIBUTE.name() + ".test.dot>=value.dos", 1); + assertRSQLQuery(TargetFields.ATTRIBUTE.name() + ".test.dot==value.dot", 1); + assertRSQLQuery(TargetFields.ATTRIBUTE.name() + ".test.null==null", 1); // "null" check + assertRSQLQuery(TargetFields.ATTRIBUTE.name() + ".test.n/a==null", 0); // "null" check + + assertRSQLQuery(TargetFields.ATTRIBUTE.name() + ".test.dot=is=value.dot", 1); + assertRSQLQuery(TargetFields.ATTRIBUTE.name() + ".test.null=is=null", 5); // null check + assertRSQLQuery(TargetFields.ATTRIBUTE.name() + ".test.n/a=is=null", 1 + 5); // null check + assertRSQLQuery(TargetFields.ATTRIBUTE.name() + ".test.dot=eq=value.dot", 1); + assertRSQLQuery(TargetFields.ATTRIBUTE.name() + ".test.null=eq=null", 5); // null check + assertRSQLQuery(TargetFields.ATTRIBUTE.name() + ".test.n/a=eq=null", 1 + 5); // null check + + assertRSQLQuery(TargetFields.ATTRIBUTE.name() + ".test.dot!=value.dot", 0); + assertRSQLQuery(TargetFields.ATTRIBUTE.name() + ".test.null!=null", 0); // "null" check + assertRSQLQuery(TargetFields.ATTRIBUTE.name() + ".test.null!=null2", 1); // value check + assertRSQLQuery(TargetFields.ATTRIBUTE.name() + ".test.n/a!=null", 0); // "null" check + + assertRSQLQuery(TargetFields.ATTRIBUTE.name() + ".test.dot=not=value.dot", 5); + assertRSQLQuery(TargetFields.ATTRIBUTE.name() + ".test.null=not=null", 1); // null check + assertRSQLQuery(TargetFields.ATTRIBUTE.name() + ".test.null=not=null2", 1 + 5); // value check + assertRSQLQuery(TargetFields.ATTRIBUTE.name() + ".test.n/a=not=null", 0); // null check + assertRSQLQuery(TargetFields.ATTRIBUTE.name() + ".test.dot=ne=value.dot", 5); + assertRSQLQuery(TargetFields.ATTRIBUTE.name() + ".test.null=ne=null", 1); // null check + assertRSQLQuery(TargetFields.ATTRIBUTE.name() + ".test.null=ne=null2", 1 + 5); // value check + assertRSQLQuery(TargetFields.ATTRIBUTE.name() + ".test.n/a=ne=null", 0); // null check assertRSQLQuery(TargetFields.ATTRIBUTE.name() + ".revision==1.1", 1); assertRSQLQuery(TargetFields.ATTRIBUTE.name() + ".revision!=1.1", 1); @@ -168,7 +203,6 @@ class RSQLTargetFieldTest extends AbstractJpaIntegrationTest { assertRSQLQuery(TargetFields.ATTRIBUTE.name() + ".revision==noExist*", 0); assertRSQLQuery(TargetFields.ATTRIBUTE.name() + ".revision=in=(1.1,notexist)", 1); assertRSQLQuery(TargetFields.ATTRIBUTE.name() + ".revision=out=(1.1)", 1); - assertRSQLQuery(TargetFields.ATTRIBUTE.name() + ".test.dot==value.dot", 1); assertRSQLQuery(TargetFields.ATTRIBUTE.name() + ".key.dot*==value.dot", 0); assertRSQLQuery(TargetFields.ATTRIBUTE.name() + ".key.*==value.dot", 0); assertRSQLQuery(TargetFields.ATTRIBUTE.name() + ".key.==value.dot", 0); @@ -196,10 +230,8 @@ class RSQLTargetFieldTest extends AbstractJpaIntegrationTest { assertRSQLQuery(TargetFields.ASSIGNEDDS.name() + ".version==" + TestdataFactory.DEFAULT_VERSION, 1); assertRSQLQuery(TargetFields.ASSIGNEDDS.name() + ".version==*1*", 1); assertRSQLQuery(TargetFields.ASSIGNEDDS.name() + ".version==noExist*", 0); - assertRSQLQuery( - TargetFields.ASSIGNEDDS.name() + ".version=in=(" + TestdataFactory.DEFAULT_VERSION + ",notexist)", 1); - assertRSQLQuery( - TargetFields.ASSIGNEDDS.name() + ".version=out=(" + TestdataFactory.DEFAULT_VERSION + ",notexist)", 4); + assertRSQLQuery(TargetFields.ASSIGNEDDS.name() + ".version=in=(" + TestdataFactory.DEFAULT_VERSION + ",notexist)", 1); + assertRSQLQuery(TargetFields.ASSIGNEDDS.name() + ".version=out=(" + TestdataFactory.DEFAULT_VERSION + ",notexist)", 4); } @Test @@ -238,18 +270,41 @@ class RSQLTargetFieldTest extends AbstractJpaIntegrationTest { @Test @Description("Test filter target by metadata") void testFilterByMetadata() { - createTargetMetadata(testdataFactory.createTarget().getControllerId(), - entityFactory.generateTargetMetadata("key.dot", "value.dot")); + createTargetMetadata(testdataFactory.createTarget().getControllerId(), entityFactory.generateTargetMetadata("key.dot", "value.dot")); assertRSQLQuery(TargetFields.METADATA.name() + ".metaKey==metaValue", 1); + assertRSQLQuery(TargetFields.METADATA.name() + ".metaKey==null", 0); // "null" check assertRSQLQuery(TargetFields.METADATA.name() + ".metaKey==*v*", 2); assertRSQLQuery(TargetFields.METADATA.name() + ".metaKey==noExist*", 0); - assertRSQLQuery(TargetFields.METADATA.name() + ".metaKey=in=(metaValue,notexist)", 1); - assertRSQLQuery(TargetFields.METADATA.name() + ".metaKey=out=(metaValue,notexist)", 1); assertRSQLQuery(TargetFields.METADATA.name() + ".notExist==metaValue", 0); + + assertRSQLQuery(TargetFields.METADATA.name() + ".metaKey=is=metaValue", 1); + assertRSQLQuery(TargetFields.METADATA.name() + ".metaKey=is=null", 4); // null check (1 of the initial five has) + assertRSQLQuery(TargetFields.METADATA.name() + ".metaKey=is=*v*", 2); + assertRSQLQuery(TargetFields.METADATA.name() + ".metaKey=is=noExist*", 0); + assertRSQLQuery(TargetFields.METADATA.name() + ".notExist=is=null", 1 + 5); // null check + assertRSQLQuery(TargetFields.METADATA.name() + ".metaKey=eq=metaValue", 1); + assertRSQLQuery(TargetFields.METADATA.name() + ".metaKey=eq=null", 4); // null check (1 of the initial five has) + assertRSQLQuery(TargetFields.METADATA.name() + ".metaKey=eq=*v*", 2); + assertRSQLQuery(TargetFields.METADATA.name() + ".metaKey=eq=noExist*", 0); + assertRSQLQuery(TargetFields.METADATA.name() + ".notExist=eq=null", 1 + 5); // null check + assertRSQLQuery(TargetFields.METADATA.name() + ".metaKey!=metaValue", 1); + assertRSQLQuery(TargetFields.METADATA.name() + ".metaKey!=null", 2); assertRSQLQuery(TargetFields.METADATA.name() + ".notExist!=metaValue", 0); assertRSQLQuery(TargetFields.METADATA.name() + ".metaKey!=notExist", 2); +// + assertRSQLQuery(TargetFields.METADATA.name() + ".metaKey=not=metaValue", 1 + 4); + assertRSQLQuery(TargetFields.METADATA.name() + ".metaKey=not=null", 2); // null check (2 of the initial five) + assertRSQLQuery(TargetFields.METADATA.name() + ".notExist=not=metaValue", 1 + 5); + assertRSQLQuery(TargetFields.METADATA.name() + ".metaKey=not=notExist", 1 + 5); + assertRSQLQuery(TargetFields.METADATA.name() + ".metaKey=ne=metaValue", 1 + 4); + assertRSQLQuery(TargetFields.METADATA.name() + ".metaKey=ne=null", 2); // null check (2 of the initial five) + assertRSQLQuery(TargetFields.METADATA.name() + ".notExist=ne=metaValue", 1 + 5); + assertRSQLQuery(TargetFields.METADATA.name() + ".metaKey=ne=notExist", 1 + 5); + + assertRSQLQuery(TargetFields.METADATA.name() + ".metaKey=in=(metaValue,notexist)", 1); + assertRSQLQuery(TargetFields.METADATA.name() + ".metaKey=out=(metaValue,notexist)", 1); assertRSQLQuery(TargetFields.METADATA.name() + ".key.dot==value.dot", 1); assertRSQLQuery(TargetFields.METADATA.name() + ".key.dot*==value.dot", 0); assertRSQLQuery(TargetFields.METADATA.name() + ".key.*==value.dot", 0); @@ -265,41 +320,37 @@ class RSQLTargetFieldTest extends AbstractJpaIntegrationTest { @Test @Description("Test filter based on more complex RSQL queries") void testFilterByComplexQueries() { + assertRSQLQuery(TargetFields.NAME.name() + "!=targetName123" + AND + TargetFields.METADATA.name() + ".metaKey!=value", 0); assertRSQLQuery( - TargetFields.NAME.name() + "!=targetName123" + AND + TargetFields.METADATA.name() + ".metaKey!=value", - 0); - assertRSQLQuery("(" + TargetFields.TAG.name() + "!=TAG1" + OR + TargetFields.TAG.name() + "!=TAG2)" + AND - + TargetFields.CONTROLLERID.name() + "!=targetId1235", 4); + "(" + TargetFields.TAG.name() + "!=TAG1" + OR + TargetFields.TAG.name() + "!=TAG2)" + + AND + TargetFields.CONTROLLERID.name() + "!=targetId1235", 4); } @Test @Description("Testing allowed RSQL keys based on TargetFields definition") void rsqlValidTargetFields() { - final String rsql1 = "ID == '0123' and NAME == abcd and DESCRIPTION == absd" - + " and CREATEDAT =lt= 0123 and LASTMODIFIEDAT =gt= 0123" - + " and CONTROLLERID == 0123 and UPDATESTATUS == PENDING" - + " and IPADDRESS == 0123 and LASTCONTROLLERREQUESTAT == 0123" + " and tag == beta"; + RSQLUtility.validateRsqlFor( + "ID == '0123' and NAME == abcd and DESCRIPTION == absd and CREATEDAT =lt= 0123 and LASTMODIFIEDAT =gt= 0123" + + " and CONTROLLERID == 0123 and UPDATESTATUS == PENDING and IPADDRESS == 0123 and LASTCONTROLLERREQUESTAT == 0123" + + " and tag == beta", + TargetFields.class, JpaTarget.class, virtualPropertyReplacer, entityManager); + RSQLUtility.validateRsqlFor( + "ASSIGNEDDS.name == abcd and ASSIGNEDDS.version == 0123 and INSTALLEDDS.name == abcd and INSTALLEDDS.version == 0123", + TargetFields.class, JpaTarget.class, virtualPropertyReplacer, entityManager); + RSQLUtility.validateRsqlFor( + "ATTRIBUTE.subkey1 == test and ATTRIBUTE.subkey2 == test and METADATA.metakey1 == abcd and METADATA.metavalue2 == asdfg", + TargetFields.class, JpaTarget.class, virtualPropertyReplacer, entityManager); + RSQLUtility.validateRsqlFor( + "CREATEDAT =lt= ${NOW_TS} and LASTMODIFIEDAT =ge= ${OVERDUE_TS}", + TargetFields.class, JpaTarget.class, virtualPropertyReplacer, entityManager); + RSQLUtility.validateRsqlFor( + "ATTRIBUTE.test.dot == test and ATTRIBUTE.subkey2 == test and METADATA.test.dot == abcd and METADATA.metavalue2 == asdfg", + TargetFields.class, JpaTarget.class, virtualPropertyReplacer, entityManager); - RSQLUtility.validateRsqlFor(rsql1, TargetFields.class); - - final String rsql2 = "ASSIGNEDDS.name == abcd and ASSIGNEDDS.version == 0123" - + " and INSTALLEDDS.name == abcd and INSTALLEDDS.version == 0123"; - RSQLUtility.validateRsqlFor(rsql2, TargetFields.class); - - final String rsql3 = "ATTRIBUTE.subkey1 == test and ATTRIBUTE.subkey2 == test" - + " and METADATA.metakey1 == abcd and METADATA.metavalue2 == asdfg"; - RSQLUtility.validateRsqlFor(rsql3, TargetFields.class); - - final String rsql4 = "CREATEDAT =lt= ${NOW_TS} and LASTMODIFIEDAT =ge= ${OVERDUE_TS}"; - RSQLUtility.validateRsqlFor(rsql4, TargetFields.class); - - final String rsql5 = "wrongfield == abcd"; assertThatExceptionOfType(RSQLParameterUnsupportedFieldException.class) - .isThrownBy(() -> RSQLUtility.validateRsqlFor(rsql5, TargetFields.class)); - - final String rsql6 = "ATTRIBUTE.test.dot == test and ATTRIBUTE.subkey2 == test" - + " and METADATA.test.dot == abcd and METADATA.metavalue2 == asdfg"; - RSQLUtility.validateRsqlFor(rsql6, TargetFields.class); + .isThrownBy(() -> RSQLUtility.validateRsqlFor( + "wrongfield == abcd", + TargetFields.class, JpaTarget.class, virtualPropertyReplacer, entityManager)); } @Test @@ -338,6 +389,7 @@ class RSQLTargetFieldTest extends AbstractJpaIntegrationTest { private void assertRSQLQueryThrowsException(final String rsqlParam) { assertThatExceptionOfType(RSQLParameterUnsupportedFieldException.class) - .isThrownBy(() -> RSQLUtility.validateRsqlFor(rsqlParam, TargetFields.class)); + .isThrownBy(() -> RSQLUtility.validateRsqlFor( + rsqlParam, TargetFields.class, JpaTarget.class, virtualPropertyReplacer, entityManager)); } -} +} \ No newline at end of file diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/rsql/RSQLToSQLTest.java b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/rsql/RSQLToSQLTest.java index 42d878633..31d28701d 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/rsql/RSQLToSQLTest.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/rsql/RSQLToSQLTest.java @@ -57,12 +57,12 @@ class RSQLToSQLTest { print(JpaTarget.class, TargetFields.class, "controllerId==ctrlr1"); // reference - fk to a table print(JpaTarget.class, TargetFields.class, "assignedds.name==x and assignedds.version==y"); + // list (map table that refers main) + print(JpaTarget.class, TargetFields.class, "metadata.key1==value1"); + // map (map table that refers main) + print(JpaTarget.class, TargetFields.class, "attribute.key1==value1"); // list of non-simple (with mapping table) print(JpaTarget.class, TargetFields.class, "tag==tag1"); - // list (map table that refers main) - print(JpaTarget.class, TargetFields.class, "attribute.key1==value1"); - // map (map table that refers main) - print(JpaTarget.class, TargetFields.class, "metadata.key1==value1"); } @Test diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/rsql/RSQLUtilityTest.java b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/rsql/RSQLUtilityTest.java index 5ce08aa2b..16a959b14 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/rsql/RSQLUtilityTest.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/rsql/RSQLUtilityTest.java @@ -44,14 +44,12 @@ import org.eclipse.hawkbit.repository.RsqlQueryField; import org.eclipse.hawkbit.repository.SoftwareModuleFields; import org.eclipse.hawkbit.repository.TargetFields; import org.eclipse.hawkbit.repository.TenantConfigurationManagement; -import org.eclipse.hawkbit.repository.exception.RSQLParameterSyntaxException; import org.eclipse.hawkbit.repository.exception.RSQLParameterUnsupportedFieldException; import org.eclipse.hawkbit.repository.model.SoftwareModule; import org.eclipse.hawkbit.repository.model.TenantConfigurationValue; import org.eclipse.hawkbit.repository.model.helper.SystemSecurityContextHolder; import org.eclipse.hawkbit.repository.model.helper.TenantConfigurationManagementHolder; import org.eclipse.hawkbit.repository.rsql.RsqlConfigHolder; -import org.eclipse.hawkbit.repository.rsql.RsqlVisitorFactory; import org.eclipse.hawkbit.repository.rsql.VirtualPropertyReplacer; import org.eclipse.hawkbit.repository.rsql.VirtualPropertyResolver; import org.eclipse.hawkbit.security.SystemSecurityContext; @@ -88,8 +86,6 @@ class RSQLUtilityTest { private TenantConfigurationManagement confMgmt; @MockitoBean private SystemSecurityContext securityContext; - @MockitoBean - private RsqlVisitorFactory rsqlVisitorFactory; @Mock private Root baseSoftwareModuleRootMock; @Mock @@ -109,63 +105,6 @@ class RSQLUtilityTest { setupRoot(subqueryRootMock); } - @Test - @Description("Testing throwing exception in case of not allowed RSQL key") - void rsqlUnsupportedFieldExceptionTest() { - final String rsql1 = "wrongfield == abcd"; - assertThatExceptionOfType(RSQLParameterUnsupportedFieldException.class) - .isThrownBy(() -> validateRsqlForTestFields(rsql1)); - - final String rsql2 = "wrongfield == abcd or TESTFIELD_WITH_SUB_ENTITIES.subentity11 == 0123"; - assertThatExceptionOfType(RSQLParameterUnsupportedFieldException.class) - .isThrownBy(() -> validateRsqlForTestFields(rsql2)); - } - - @Test - @Description("Testing exception in case of not allowed subkey") - void rsqlUnsupportedSubkeyThrowException() { - final String rsql1 = "TESTFIELD_WITH_SUB_ENTITIES.unsupported == abcd and TESTFIELD_WITH_SUB_ENTITIES.subentity22 == 0123"; - assertThatExceptionOfType(RSQLParameterUnsupportedFieldException.class) - .isThrownBy(() -> validateRsqlForTestFields(rsql1)); - - final String rsql2 = "TESTFIELD_WITH_SUB_ENTITIES.unsupported == abcd or TESTFIELD_WITH_SUB_ENTITIES.subentity22 == 0123"; - assertThatExceptionOfType(RSQLParameterUnsupportedFieldException.class) - .isThrownBy(() -> validateRsqlForTestFields(rsql2)); - - final String rsql3 = "TESTFIELD == abcd or TESTFIELD_WITH_SUB_ENTITIES.unsupported == 0123"; - assertThatExceptionOfType(RSQLParameterUnsupportedFieldException.class) - .isThrownBy(() -> validateRsqlForTestFields(rsql3)); - } - - @Test - @Description("Testing valid RSQL keys based on TestFieldEnum.class") - void rsqlFieldValidation() { - - final String rsql1 = "TESTFIELD_WITH_SUB_ENTITIES.subentity11 == abcd and TESTFIELD_WITH_SUB_ENTITIES.subentity22 == 0123"; - final String rsql2 = "TESTFIELD_WITH_SUB_ENTITIES.subentity11 == abcd or TESTFIELD_WITH_SUB_ENTITIES.subentity22 == 0123"; - final String rsql3 = "TESTFIELD_WITH_SUB_ENTITIES.subentity11 == abcd and TESTFIELD_WITH_SUB_ENTITIES.subentity22 == 0123 and TESTFIELD == any"; - - validateRsqlForTestFields(rsql1); - validateRsqlForTestFields(rsql2); - validateRsqlForTestFields(rsql3); - } - - @Test - @Description("Verify that RSQL expressions are validated case insensitive") - void mixedCaseRsqlFieldValidation() { - when(rsqlVisitorFactory.validationRsqlVisitor(TargetFields.class)).thenReturn(new FieldValidationRsqlVisitor<>(TargetFields.class)); - final String rsqlWithMixedCase = "name==b And name==c aND Name==d OR NAME=iN=y oR nAme=IN=z"; - RSQLUtility.validateRsqlFor(rsqlWithMixedCase, TargetFields.class); - } - - @Test - void wrongRsqlSyntaxThrowSyntaxException() { - final Specification rsqlSpecification = RSQLUtility.buildRsqlSpecification("name==abc;d", SoftwareModuleFields.class, null, testDb); - assertThatExceptionOfType(RSQLParameterSyntaxException.class) - .as("RSQLParameterSyntaxException because of wrong RSQL syntax") - .isThrownBy(() -> rsqlSpecification.toPredicate(baseSoftwareModuleRootMock, criteriaQueryMock, criteriaBuilderMock)); - } - @Test void wrongFieldThrowUnsupportedFieldException() { when(baseSoftwareModuleRootMock.getJavaType()).thenReturn((Class) SoftwareModule.class); @@ -481,12 +420,6 @@ class RSQLUtilityTest { return (Path) path; } - private void validateRsqlForTestFields(final String rsql) { - when(rsqlVisitorFactory.validationRsqlVisitor(TestFieldEnum.class)).thenReturn( - new FieldValidationRsqlVisitor<>(TestFieldEnum.class)); - RSQLUtility.validateRsqlFor(rsql, TestFieldEnum.class); - } - private void reset0(final Object... mocks) { reset(mocks); if (Arrays.asList(mocks).contains(baseSoftwareModuleRootMock)) { @@ -553,8 +486,8 @@ class RSQLUtilityTest { } @Bean - RsqlConfigHolder rsqlVisitorFactoryHolder() { + RsqlConfigHolder rsqlConfigHolder() { return RsqlConfigHolder.getInstance(); } } -} +} \ No newline at end of file diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/test/resources/jpa-test.properties b/hawkbit-repository/hawkbit-repository-jpa/src/test/resources/jpa-test.properties index 176de3681..d5bdb00cf 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/test/resources/jpa-test.properties +++ b/hawkbit-repository/hawkbit-repository-jpa/src/test/resources/jpa-test.properties @@ -12,10 +12,10 @@ logging.level.org.eclipse.persistence=ERROR ## Uncomment to see the debug of persistence, e.g. to see the generated SQLs -#logging.level.org.eclipse.persistence=DEBUG -#spring.jpa.properties.eclipselink.logging.level=FINE -#spring.jpa.properties.eclipselink.logging.level.sql=FINE -#spring.jpa.properties.eclipselink.logging.parameters=true +logging.level.org.eclipse.persistence=DEBUG +spring.jpa.properties.eclipselink.logging.level=FINE +spring.jpa.properties.eclipselink.logging.level.sql=FINE +spring.jpa.properties.eclipselink.logging.parameters=true ## Enable EclipseLink performance monitor (monitoring and profile) #spring.jpa.properties.eclipselink.profiler=PerformanceMonitor