diff --git a/hawkbit-repository/hawkbit-repository-api/pom.xml b/hawkbit-repository/hawkbit-repository-api/pom.xml index 541770d1a..d1a0df30a 100644 --- a/hawkbit-repository/hawkbit-repository-api/pom.xml +++ b/hawkbit-repository/hawkbit-repository-api/pom.xml @@ -67,6 +67,10 @@ com.cronutils cron-utils + + cz.jirutka.rsql + rsql-parser + 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 new file mode 100644 index 000000000..ab93f87c5 --- /dev/null +++ b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/rsql/RsqlVisitorFactory.java @@ -0,0 +1,37 @@ +/** + * Copyright (c) 2021 Bosch.IO GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.repository.rsql; + +import org.eclipse.hawkbit.repository.FieldNameProvider; + +import cz.jirutka.rsql.parser.ast.Node; +import cz.jirutka.rsql.parser.ast.RSQLVisitor; + +/** + * 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 FieldNameProvider}. + * + * @param + * The type of the {@link FieldNameProvider}. + * @param fieldNameProvider + * providing accessing to the relevant field names. + * + * @return An {@link RSQLVisitor} to validate the {@link Node}s of an RSQL + * query. + */ + & FieldNameProvider> RSQLVisitor validationRsqlVisitor(Class fieldNameProvider); + +} diff --git a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/rsql/RsqlVisitorFactoryHolder.java b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/rsql/RsqlVisitorFactoryHolder.java new file mode 100644 index 000000000..98f0d8fe5 --- /dev/null +++ b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/rsql/RsqlVisitorFactoryHolder.java @@ -0,0 +1,42 @@ +/** + * Copyright (c) 2021 Bosch.IO GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.repository.rsql; + +import org.springframework.beans.factory.annotation.Autowired; + +/** + * Helper class providing static access to the managed + * {@link RsqlVisitorFactory} bean. + */ +public final class RsqlVisitorFactoryHolder { + + private static final RsqlVisitorFactoryHolder SINGLETON = new RsqlVisitorFactoryHolder(); + + @Autowired + private RsqlVisitorFactory rsqlVisitorFactory; + + private RsqlVisitorFactoryHolder() { + + } + + /** + * @return The holder singleton instance. + */ + public static RsqlVisitorFactoryHolder getInstance() { + return SINGLETON; + } + + /** + * @return The managed RsqlVisitorFactory bean + */ + public RsqlVisitorFactory getRsqlVisitorFactory() { + return rsqlVisitorFactory; + } + +} diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaDeploymentManagement.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaDeploymentManagement.java index 82d5b4fc1..e564b40e0 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaDeploymentManagement.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaDeploymentManagement.java @@ -655,7 +655,7 @@ public class JpaDeploymentManagement extends JpaActionManagement implements Depl } private Specification createSpecificationFor(final String controllerId, final String rsqlParam) { - final Specification spec = RSQLUtility.parse(rsqlParam, ActionFields.class, virtualPropertyReplacer, + final Specification spec = RSQLUtility.buildRsqlSpecification(rsqlParam, ActionFields.class, virtualPropertyReplacer, database); return (root, query, cb) -> cb.and(spec.toPredicate(root, query, cb), cb.equal(root.get(JpaAction_.target).get(JpaTarget_.controllerId), controllerId)); diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaDistributionSetManagement.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaDistributionSetManagement.java index abc9465fd..9f652a455 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaDistributionSetManagement.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaDistributionSetManagement.java @@ -555,7 +555,7 @@ public class JpaDistributionSetManagement implements DistributionSetManagement { throwExceptionIfDistributionSetDoesNotExist(distributionSetId); - final Specification spec = RSQLUtility.parse(rsqlParam, + final Specification spec = RSQLUtility.buildRsqlSpecification(rsqlParam, DistributionSetMetadataFields.class, virtualPropertyReplacer, database); return convertMdPage( @@ -784,7 +784,7 @@ public class JpaDistributionSetManagement implements DistributionSetManagement { public Page findByRsqlAndTag(final Pageable pageable, final String rsqlParam, final long tagId) { throwEntityNotFoundExceptionIfDsTagDoesNotExist(tagId); - final Specification spec = RSQLUtility.parse(rsqlParam, DistributionSetFields.class, + final Specification spec = RSQLUtility.buildRsqlSpecification(rsqlParam, DistributionSetFields.class, virtualPropertyReplacer, database); return convertDsPage(findByCriteriaAPI(pageable, Arrays.asList(spec, DistributionSetSpecification.hasTag(tagId), @@ -799,7 +799,7 @@ public class JpaDistributionSetManagement implements DistributionSetManagement { @Override public Page findByRsql(final Pageable pageable, final String rsqlParam) { - final Specification spec = RSQLUtility.parse(rsqlParam, DistributionSetFields.class, + final Specification spec = RSQLUtility.buildRsqlSpecification(rsqlParam, DistributionSetFields.class, virtualPropertyReplacer, database); return convertDsPage( diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaDistributionSetTagManagement.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaDistributionSetTagManagement.java index 37344deee..fca185f48 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaDistributionSetTagManagement.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaDistributionSetTagManagement.java @@ -134,7 +134,7 @@ public class JpaDistributionSetTagManagement implements DistributionSetTagManage @Override public Page findByRsql(final Pageable pageable, final String rsqlParam) { - final Specification spec = RSQLUtility.parse(rsqlParam, TagFields.class, + final Specification spec = RSQLUtility.buildRsqlSpecification(rsqlParam, TagFields.class, virtualPropertyReplacer, database); return convertDsPage(distributionSetTagRepository.findAll(spec, pageable), pageable); diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaDistributionSetTypeManagement.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaDistributionSetTypeManagement.java index 390ac3c12..b8cb28dfb 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaDistributionSetTypeManagement.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaDistributionSetTypeManagement.java @@ -227,7 +227,7 @@ public class JpaDistributionSetTypeManagement implements DistributionSetTypeMana public Page findByRsql(final Pageable pageable, final String rsqlParam) { return convertPage( findByCriteriaAPI(pageable, - Arrays.asList(RSQLUtility.parse(rsqlParam, DistributionSetTypeFields.class, + Arrays.asList(RSQLUtility.buildRsqlSpecification(rsqlParam, DistributionSetTypeFields.class, virtualPropertyReplacer, database), DistributionSetTypeSpecification.isDeleted(false))), pageable); } diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaRolloutGroupManagement.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaRolloutGroupManagement.java index 80a990f32..721ea1fb9 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaRolloutGroupManagement.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaRolloutGroupManagement.java @@ -119,7 +119,7 @@ public class JpaRolloutGroupManagement implements RolloutGroupManagement { final String rsqlParam) { throwEntityNotFoundExceptionIfRolloutDoesNotExist(rolloutId); - final Specification specification = RSQLUtility.parse(rsqlParam, RolloutGroupFields.class, + final Specification specification = RSQLUtility.buildRsqlSpecification(rsqlParam, RolloutGroupFields.class, virtualPropertyReplacer, database); return convertPage(rolloutGroupRepository.findAll((root, query, criteriaBuilder) -> criteriaBuilder.and( @@ -211,7 +211,7 @@ public class JpaRolloutGroupManagement implements RolloutGroupManagement { throwExceptionIfRolloutGroupDoesNotExist(rolloutGroupId); - final Specification rsqlSpecification = RSQLUtility.parse(rsqlParam, TargetFields.class, + final Specification rsqlSpecification = RSQLUtility.buildRsqlSpecification(rsqlParam, TargetFields.class, virtualPropertyReplacer, database); return convertTPage(targetRepository.findAll((root, query, criteriaBuilder) -> { diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaRolloutManagement.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaRolloutManagement.java index 73e21945c..1d45755d0 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaRolloutManagement.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaRolloutManagement.java @@ -151,7 +151,7 @@ public class JpaRolloutManagement extends AbstractRolloutManagement { @Override public Page findByRsql(final Pageable pageable, final String rsqlParam, final boolean deleted) { final List> specList = Lists.newArrayListWithExpectedSize(2); - specList.add(RSQLUtility.parse(rsqlParam, RolloutFields.class, virtualPropertyReplacer, database)); + specList.add(RSQLUtility.buildRsqlSpecification(rsqlParam, RolloutFields.class, virtualPropertyReplacer, database)); specList.add(RolloutSpecification.isDeletedWithDistributionSet(deleted)); return JpaRolloutHelper.convertPage(findByCriteriaAPI(pageable, specList), pageable); diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaSoftwareModuleManagement.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaSoftwareModuleManagement.java index ea85e11f6..d35da4ea4 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaSoftwareModuleManagement.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaSoftwareModuleManagement.java @@ -300,7 +300,7 @@ public class JpaSoftwareModuleManagement implements SoftwareModuleManagement { @Override public Page findByRsql(final Pageable pageable, final String rsqlParam) { - final Specification spec = RSQLUtility.parse(rsqlParam, SoftwareModuleFields.class, + final Specification spec = RSQLUtility.buildRsqlSpecification(rsqlParam, SoftwareModuleFields.class, virtualPropertyReplacer, database); return convertSmPage(softwareModuleRepository.findAll(spec, pageable), pageable); @@ -608,7 +608,7 @@ public class JpaSoftwareModuleManagement implements SoftwareModuleManagement { throwExceptionIfSoftwareModuleDoesNotExist(softwareModuleId); - final Specification spec = RSQLUtility.parse(rsqlParam, + final Specification spec = RSQLUtility.buildRsqlSpecification(rsqlParam, SoftwareModuleMetadataFields.class, virtualPropertyReplacer, database); return convertSmMdPage( softwareModuleMetadataRepository diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaSoftwareModuleTypeManagement.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaSoftwareModuleTypeManagement.java index a9eb11c7a..3e9fbc514 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaSoftwareModuleTypeManagement.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaSoftwareModuleTypeManagement.java @@ -90,7 +90,7 @@ public class JpaSoftwareModuleTypeManagement implements SoftwareModuleTypeManage @Override public Page findByRsql(final Pageable pageable, final String rsqlParam) { - final Specification spec = RSQLUtility.parse(rsqlParam, SoftwareModuleTypeFields.class, + final Specification spec = RSQLUtility.buildRsqlSpecification(rsqlParam, SoftwareModuleTypeFields.class, virtualPropertyReplacer, database); return convertPage(softwareModuleTypeRepository.findAll(spec, pageable), pageable); diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaTargetFilterQueryManagement.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaTargetFilterQueryManagement.java index 91a9a247c..1c169109d 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaTargetFilterQueryManagement.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaTargetFilterQueryManagement.java @@ -166,7 +166,7 @@ public class JpaTargetFilterQueryManagement implements TargetFilterQueryManageme List> specList = Collections.emptyList(); if (!StringUtils.isEmpty(rsqlFilter)) { specList = Collections.singletonList( - RSQLUtility.parse(rsqlFilter, TargetFilterQueryFields.class, virtualPropertyReplacer, database)); + RSQLUtility.buildRsqlSpecification(rsqlFilter, TargetFilterQueryFields.class, virtualPropertyReplacer, database)); } return convertPage(findTargetFilterQueryByCriteriaAPI(pageable, specList), pageable); } @@ -191,7 +191,7 @@ public class JpaTargetFilterQueryManagement implements TargetFilterQueryManageme if (!StringUtils.isEmpty(rsqlFilter)) { specList.add( - RSQLUtility.parse(rsqlFilter, TargetFilterQueryFields.class, virtualPropertyReplacer, database)); + RSQLUtility.buildRsqlSpecification(rsqlFilter, TargetFilterQueryFields.class, virtualPropertyReplacer, database)); } return convertPage(findTargetFilterQueryByCriteriaAPI(pageable, specList), pageable); } diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaTargetManagement.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaTargetManagement.java index e2549c32f..105911edc 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaTargetManagement.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaTargetManagement.java @@ -282,7 +282,7 @@ public class JpaTargetManagement implements TargetManagement { final Long targetId = getByControllerIdAndThrowIfNotFound(controllerId).getId(); - final Specification spec = RSQLUtility.parse(rsqlParam, TargetMetadataFields.class, + final Specification spec = RSQLUtility.buildRsqlSpecification(rsqlParam, TargetMetadataFields.class, virtualPropertyReplacer, database); return convertMdPage(targetMetadataRepository.findAll((Specification) (root, query, cb) -> cb @@ -309,14 +309,14 @@ public class JpaTargetManagement implements TargetManagement { .orElseThrow(() -> new EntityNotFoundException(TargetFilterQuery.class, targetFilterQueryId)); return findTargetsBySpec( - RSQLUtility.parse(targetFilterQuery.getQuery(), TargetFields.class, virtualPropertyReplacer, database), + RSQLUtility.buildRsqlSpecification(targetFilterQuery.getQuery(), TargetFields.class, virtualPropertyReplacer, database), pageable); } @Override public Page findByRsql(final Pageable pageable, final String targetFilterQuery) { return findTargetsBySpec( - RSQLUtility.parse(targetFilterQuery, TargetFields.class, virtualPropertyReplacer, database), pageable); + RSQLUtility.buildRsqlSpecification(targetFilterQuery, TargetFields.class, virtualPropertyReplacer, database), pageable); } private Page findTargetsBySpec(final Specification spec, final Pageable pageable) { @@ -384,7 +384,7 @@ public class JpaTargetManagement implements TargetManagement { final String rsqlParam) { throwEntityNotFoundIfDsDoesNotExist(distributionSetID); - final Specification spec = RSQLUtility.parse(rsqlParam, TargetFields.class, virtualPropertyReplacer, + final Specification spec = RSQLUtility.buildRsqlSpecification(rsqlParam, TargetFields.class, virtualPropertyReplacer, database); return convertPage( @@ -421,7 +421,7 @@ public class JpaTargetManagement implements TargetManagement { final String rsqlParam) { throwEntityNotFoundIfDsDoesNotExist(distributionSetId); - final Specification spec = RSQLUtility.parse(rsqlParam, TargetFields.class, virtualPropertyReplacer, + final Specification spec = RSQLUtility.buildRsqlSpecification(rsqlParam, TargetFields.class, virtualPropertyReplacer, database); return convertPage( @@ -658,7 +658,7 @@ public class JpaTargetManagement implements TargetManagement { final String targetFilterQuery) { throwEntityNotFoundIfDsDoesNotExist(distributionSetId); - final Specification spec = RSQLUtility.parse(targetFilterQuery, TargetFields.class, + final Specification spec = RSQLUtility.buildRsqlSpecification(targetFilterQuery, TargetFields.class, virtualPropertyReplacer, database); return findTargetsBySpec( @@ -673,7 +673,7 @@ public class JpaTargetManagement implements TargetManagement { public Page findByTargetFilterQueryAndNotInRolloutGroups(final Pageable pageRequest, final Collection groups, final String targetFilterQuery) { - final Specification spec = RSQLUtility.parse(targetFilterQuery, TargetFields.class, + final Specification spec = RSQLUtility.buildRsqlSpecification(targetFilterQuery, TargetFields.class, virtualPropertyReplacer, database); return findTargetsBySpec((root, cq, cb) -> cb.and(spec.toPredicate(root, cq, cb), @@ -694,7 +694,7 @@ public class JpaTargetManagement implements TargetManagement { @Override public long countByRsqlAndNotInRolloutGroups(final Collection groups, final String targetFilterQuery) { - final Specification spec = RSQLUtility.parse(targetFilterQuery, TargetFields.class, + final Specification spec = RSQLUtility.buildRsqlSpecification(targetFilterQuery, TargetFields.class, virtualPropertyReplacer, database); final List> specList = Arrays.asList(spec, TargetSpecifications.isNotInRolloutGroups(groups)); @@ -706,7 +706,7 @@ public class JpaTargetManagement implements TargetManagement { public long countByRsqlAndNonDS(final long distributionSetId, final String targetFilterQuery) { throwEntityNotFoundIfDsDoesNotExist(distributionSetId); - final Specification spec = RSQLUtility.parse(targetFilterQuery, TargetFields.class, + final Specification spec = RSQLUtility.buildRsqlSpecification(targetFilterQuery, TargetFields.class, virtualPropertyReplacer, database); final List> specList = Lists.newArrayListWithExpectedSize(2); specList.add(spec); @@ -750,7 +750,7 @@ public class JpaTargetManagement implements TargetManagement { throwEntityNotFoundExceptionIfTagDoesNotExist(tagId); - final Specification spec = RSQLUtility.parse(rsqlParam, TargetFields.class, virtualPropertyReplacer, + final Specification spec = RSQLUtility.buildRsqlSpecification(rsqlParam, TargetFields.class, virtualPropertyReplacer, database); return convertPage(targetRepository.findAll((Specification) (root, query, cb) -> cb.and( @@ -763,14 +763,14 @@ public class JpaTargetManagement implements TargetManagement { final TargetFilterQuery targetFilterQuery = targetFilterQueryRepository.findById(targetFilterQueryId) .orElseThrow(() -> new EntityNotFoundException(TargetFilterQuery.class, targetFilterQueryId)); - final Specification specs = RSQLUtility.parse(targetFilterQuery.getQuery(), TargetFields.class, + final Specification specs = RSQLUtility.buildRsqlSpecification(targetFilterQuery.getQuery(), TargetFields.class, virtualPropertyReplacer, database); return targetRepository.count(specs); } @Override public long countByRsql(final String targetFilterQuery) { - final Specification specs = RSQLUtility.parse(targetFilterQuery, TargetFields.class, + final Specification specs = RSQLUtility.buildRsqlSpecification(targetFilterQuery, TargetFields.class, virtualPropertyReplacer, database); return targetRepository.count((root, query, cb) -> { query.distinct(true); diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaTargetTagManagement.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaTargetTagManagement.java index 3bad1eab3..368e7833d 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaTargetTagManagement.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/JpaTargetTagManagement.java @@ -106,7 +106,7 @@ public class JpaTargetTagManagement implements TargetTagManagement { @Override public Page findByRsql(final Pageable pageable, final String rsqlParam) { - final Specification spec = RSQLUtility.parse(rsqlParam, TagFields.class, virtualPropertyReplacer, + final Specification spec = RSQLUtility.buildRsqlSpecification(rsqlParam, TagFields.class, virtualPropertyReplacer, database); return convertTPage(targetTagRepository.findAll(spec, pageable), pageable); } 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 750f28ed6..5cf1a5101 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 @@ -15,8 +15,6 @@ import java.util.concurrent.ScheduledExecutorService; import javax.persistence.EntityManager; import javax.sql.DataSource; -import com.google.common.collect.Maps; - import org.eclipse.hawkbit.artifact.repository.ArtifactRepository; import org.eclipse.hawkbit.repository.ArtifactManagement; import org.eclipse.hawkbit.repository.ControllerManagement; @@ -30,8 +28,8 @@ import org.eclipse.hawkbit.repository.QuotaManagement; import org.eclipse.hawkbit.repository.RepositoryDefaultConfiguration; import org.eclipse.hawkbit.repository.RepositoryProperties; import org.eclipse.hawkbit.repository.RolloutApprovalStrategy; -import org.eclipse.hawkbit.repository.RolloutGroupManagement; import org.eclipse.hawkbit.repository.RolloutExecutor; +import org.eclipse.hawkbit.repository.RolloutGroupManagement; import org.eclipse.hawkbit.repository.RolloutManagement; import org.eclipse.hawkbit.repository.RolloutStatusCache; import org.eclipse.hawkbit.repository.SoftwareModuleManagement; @@ -80,6 +78,7 @@ import org.eclipse.hawkbit.repository.jpa.rollout.condition.StartNextGroupRollou 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.RsqlParserValidationOracle; +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; @@ -90,6 +89,8 @@ import org.eclipse.hawkbit.repository.model.helper.EventPublisherHolder; import org.eclipse.hawkbit.repository.model.helper.SystemManagementHolder; import org.eclipse.hawkbit.repository.model.helper.TenantConfigurationManagementHolder; import org.eclipse.hawkbit.repository.rsql.RsqlValidationOracle; +import org.eclipse.hawkbit.repository.rsql.RsqlVisitorFactory; +import org.eclipse.hawkbit.repository.rsql.RsqlVisitorFactoryHolder; import org.eclipse.hawkbit.repository.rsql.VirtualPropertyReplacer; import org.eclipse.hawkbit.security.HawkbitSecurityProperties; import org.eclipse.hawkbit.security.SecurityTokenGenerator; @@ -127,6 +128,8 @@ import org.springframework.transaction.annotation.EnableTransactionManagement; import org.springframework.transaction.jta.JtaTransactionManager; import org.springframework.validation.beanvalidation.MethodValidationPostProcessor; +import com.google.common.collect.Maps; + /** * General configuration for hawkBit's Repository. * @@ -415,7 +418,8 @@ public class RepositoryApplicationConfiguration extends JpaBaseConfiguration { */ @Override @Bean - public PlatformTransactionManager transactionManager(ObjectProvider transactionManagerCustomizers) { + public PlatformTransactionManager transactionManager( + ObjectProvider transactionManagerCustomizers) { return new MultiTenantJpaTransactionManager(); } @@ -620,15 +624,14 @@ public class RepositoryApplicationConfiguration extends JpaBaseConfiguration { @Bean @ConditionalOnMissingBean - RolloutExecutor rolloutExecutor( - final RolloutTargetGroupRepository rolloutTargetGroupRepository, final EntityManager entityManager, - final RolloutRepository rolloutRepository, final ActionRepository actionRepository, - final RolloutGroupRepository rolloutGroupRepository, final AfterTransactionCommitExecutor afterCommit, - final TenantAware tenantAware, final RolloutGroupManagement rolloutGroupManagement, - final QuotaManagement quotaManagement, final DeploymentManagement deploymentManagement, - final TargetManagement targetManagement, final EventPublisherHolder eventPublisherHolder, - final PlatformTransactionManager txManager, final RolloutApprovalStrategy rolloutApprovalStrategy, - final ApplicationContext context) { + RolloutExecutor rolloutExecutor(final RolloutTargetGroupRepository rolloutTargetGroupRepository, + final EntityManager entityManager, final RolloutRepository rolloutRepository, + final ActionRepository actionRepository, final RolloutGroupRepository rolloutGroupRepository, + final AfterTransactionCommitExecutor afterCommit, final TenantAware tenantAware, + final RolloutGroupManagement rolloutGroupManagement, final QuotaManagement quotaManagement, + final DeploymentManagement deploymentManagement, final TargetManagement targetManagement, + final EventPublisherHolder eventPublisherHolder, final PlatformTransactionManager txManager, + final RolloutApprovalStrategy rolloutApprovalStrategy, final ApplicationContext context) { return new JpaRolloutExecutor(rolloutTargetGroupRepository, entityManager, rolloutRepository, actionRepository, rolloutGroupRepository, afterCommit, tenantAware, rolloutGroupManagement, quotaManagement, deploymentManagement, targetManagement, eventPublisherHolder, txManager, rolloutApprovalStrategy, @@ -875,4 +878,26 @@ public class RepositoryApplicationConfiguration extends JpaBaseConfiguration { final RolloutManagement rolloutManagement, final SystemSecurityContext systemSecurityContext) { return new RolloutScheduler(systemManagement, rolloutManagement, systemSecurityContext); } + + /** + * Creates the {@link RsqlVisitorFactory} bean. + * + * @return A new {@link RsqlVisitorFactory} bean. + */ + @Bean + @ConditionalOnMissingBean + RsqlVisitorFactory rsqlVisitorFactory() { + return new DefaultRsqlVisitorFactory(); + } + + /** + * Obtains the {@link RsqlVisitorFactoryHolder} bean. + * + * @return The {@link RsqlVisitorFactoryHolder} singleton. + */ + @Bean + RsqlVisitorFactoryHolder rsqlVisitorFactoryHolder() { + return RsqlVisitorFactoryHolder.getInstance(); + } + } 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 new file mode 100644 index 000000000..7f49abd1a --- /dev/null +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/rsql/DefaultRsqlVisitorFactory.java @@ -0,0 +1,28 @@ +/** + * Copyright (c) 2021 Bosch.IO GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.repository.jpa.rsql; + +import org.eclipse.hawkbit.repository.FieldNameProvider; +import org.eclipse.hawkbit.repository.rsql.RsqlVisitorFactory; + +import cz.jirutka.rsql.parser.ast.RSQLVisitor; + +/** + * Factory providing {@link RSQLVisitor} instances which validate the nodes + * based on a given {@link FieldNameProvider}. + */ +public class DefaultRsqlVisitorFactory implements RsqlVisitorFactory { + + @Override + public & FieldNameProvider> 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 new file mode 100644 index 000000000..021443749 --- /dev/null +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/rsql/FieldValidationRsqlVisitor.java @@ -0,0 +1,61 @@ +/** + * Copyright (c) 2021 Bosch.IO GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.repository.jpa.rsql; + +import org.eclipse.hawkbit.repository.FieldNameProvider; + +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; + +/** + * {@link RSQLVisitor} implementation which validates the nodes (fields) based + * on a given {@link FieldNameProvider} for a given entity type. + * + * @param + * The type the {@link FieldNameProvider} refers to. + */ +public class FieldValidationRsqlVisitor & FieldNameProvider> extends AbstractFieldNameRSQLVisitor + implements RSQLVisitor { + + /** + * Constructs the visitor and initializes it. + * + * @param fieldNameProvider + * The {@link FieldNameProvider} 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) { + final A fieldName = getFieldEnumByName(node); + getAndValidatePropertyFieldName(fieldName, node); + return null; + } + + private Void visitNode(final LogicalNode node, final String param) { + node.getChildren().forEach(child -> child.accept(this, param)); + return null; + } + +} 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 new file mode 100644 index 000000000..eb9871134 --- /dev/null +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/rsql/JpaQueryRsqlVisitor.java @@ -0,0 +1,597 @@ +/** + * Copyright (c) 2021 Bosch.IO GmbH and others. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.eclipse.hawkbit.repository.jpa.rsql; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.Map.Entry; +import java.util.function.BiFunction; +import java.util.function.Function; +import java.util.stream.Collectors; + +import javax.persistence.criteria.CriteriaBuilder; +import javax.persistence.criteria.CriteriaQuery; +import javax.persistence.criteria.Expression; +import javax.persistence.criteria.From; +import javax.persistence.criteria.Join; +import javax.persistence.criteria.JoinType; +import javax.persistence.criteria.MapJoin; +import javax.persistence.criteria.Path; +import javax.persistence.criteria.PluralJoin; +import javax.persistence.criteria.Predicate; +import javax.persistence.criteria.Root; +import javax.persistence.criteria.Subquery; + +import org.apache.commons.lang3.math.NumberUtils; +import org.eclipse.hawkbit.repository.FieldNameProvider; +import org.eclipse.hawkbit.repository.FieldValueConverter; +import org.eclipse.hawkbit.repository.exception.RSQLParameterSyntaxException; +import org.eclipse.hawkbit.repository.exception.RSQLParameterUnsupportedFieldException; +import org.eclipse.hawkbit.repository.rsql.VirtualPropertyReplacer; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +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.StringUtils; + +import com.google.common.collect.Lists; + +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.Node; +import cz.jirutka.rsql.parser.ast.OrNode; +import cz.jirutka.rsql.parser.ast.RSQLVisitor; + +/** + * An implementation of the {@link RSQLVisitor} to visit the parsed tokens and + * build JPA where clauses. + * + * @param + * the enum for providing the field name of the entity field to + * filter on. + * @param + * the entity type referenced by the root + */ +public class JpaQueryRsqlVisitor & FieldNameProvider, T> extends AbstractFieldNameRSQLVisitor + implements RSQLVisitor, String> { + + private static final Logger LOGGER = LoggerFactory.getLogger(JpaQueryRsqlVisitor.class); + + public static final Character LIKE_WILDCARD = '*'; + private static final char ESCAPE_CHAR = '\\'; + private static final List NO_JOINS_OPERATOR = Lists.newArrayList("!=", "=out="); + + private final Map>> joinsInLevel = new HashMap<>(3); + + private final CriteriaBuilder cb; + private final CriteriaQuery query; + private final Database database; + private final Root root; + private final SimpleTypeConverter simpleTypeConverter; + private final VirtualPropertyReplacer virtualPropertyReplacer; + + private int level; + private boolean isOrLevel; + private boolean joinsNeeded; + + public JpaQueryRsqlVisitor(final Root root, final CriteriaBuilder cb, final Class enumType, + final VirtualPropertyReplacer virtualPropertyReplacer, final Database database, + final CriteriaQuery query) { + super(enumType); + this.root = root; + this.cb = cb; + this.query = query; + this.virtualPropertyReplacer = virtualPropertyReplacer; + this.simpleTypeConverter = new SimpleTypeConverter(); + this.database = database; + this.joinsNeeded = false; + } + + private void beginLevel(final boolean isOr) { + level++; + isOrLevel = isOr; + joinsInLevel.put(level, new HashSet<>(2)); + } + + private void endLevel() { + joinsInLevel.remove(level); + level--; + isOrLevel = false; + } + + private Set> getCurrentJoins() { + if (level > 0) { + return joinsInLevel.get(level); + } + return Collections.emptySet(); + } + + private Optional> findCurrentJoinOfType(final Class type) { + return getCurrentJoins().stream().filter(j -> type.equals(j.getJavaType())).findAny(); + } + + private void addCurrentJoin(final Join join) { + if (level > 0) { + getCurrentJoins().add(join); + } + } + + @Override + public List visit(final AndNode node, final String param) { + beginLevel(false); + final List childs = acceptChilds(node); + endLevel(); + if (!childs.isEmpty()) { + return toSingleList(cb.and(childs.toArray(new Predicate[childs.size()]))); + } + return toSingleList(cb.conjunction()); + } + + @Override + public List visit(final OrNode node, final String param) { + beginLevel(true); + final List childs = acceptChilds(node); + endLevel(); + if (!childs.isEmpty()) { + return toSingleList(cb.or(childs.toArray(new Predicate[childs.size()]))); + } + return toSingleList(cb.conjunction()); + } + + private static List toSingleList(final Predicate predicate) { + return Collections.singletonList(predicate); + } + + /** + * Resolves the Path for a field in the persistence layer and joins the + * required models. This operation is part of a tree traversal through an + * RSQL expression. It creates for every field that is not part of the root + * model a join to the foreign model. This behavior is optimized when + * several joins happen directly under an OR node in the traversed tree. The + * same foreign model is only joined once. + * + * Example: tags.name==M;(tags.name==A,tags.name==B,tags.name==C) This + * example joins the tags model only twice, because for the OR node in + * brackets only one join is used. + * + * @param enumField + * field from a FieldNameProvider to resolve on the persistence + * layer + * @param finalProperty + * dot notated field path + * @return the Path for a field + */ + @SuppressWarnings("unchecked") + private Path getFieldPath(final A enumField, final String finalProperty) { + return (Path) getFieldPath(root, getSubAttributesFrom(finalProperty), enumField.isMap(), + this::getJoinFieldPath).orElseThrow( + () -> createRSQLParameterUnsupportedException("RSQL field path cannot be empty", null)); + } + + @SuppressWarnings("unchecked") + private Path getJoinFieldPath(final Path fieldPath, final String fieldNameSplit) { + if (fieldPath instanceof PluralJoin) { + final Join join = (Join) fieldPath; + final From joinParent = join.getParent(); + final Optional> currentJoinOfType = findCurrentJoinOfType(join.getJavaType()); + if (currentJoinOfType.isPresent() && isOrLevel) { + // remove the additional join and use the existing one + joinParent.getJoins().remove(join); + return currentJoinOfType.get(); + } else { + final Join newJoin = joinParent.join(fieldNameSplit, JoinType.LEFT); + addCurrentJoin(newJoin); + return newJoin; + } + } + return fieldPath; + } + + private static Optional> getFieldPath(final Root root, final String[] split, final boolean isMapKeyField, + final BiFunction, String, Path> joinFieldPathProvider) { + Path fieldPath = null; + for (int i = 0; i < split.length; i++) { + if (!(isMapKeyField && i == (split.length - 1))) { + final String fieldNameSplit = split[i]; + fieldPath = (fieldPath != null) ? fieldPath.get(fieldNameSplit) : root.get(fieldNameSplit); + fieldPath = joinFieldPathProvider.apply(fieldPath, fieldNameSplit); + } + } + return Optional.ofNullable(fieldPath); + } + + @Override + // Exception squid:S2095 - see + // https://jira.sonarsource.com/browse/SONARJAVA-1478 + @SuppressWarnings({ "squid:S2095" }) + public List visit(final ComparisonNode node, final String param) { + final A fieldName = getFieldEnumByName(node); + final String finalProperty = getAndValidatePropertyFieldName(fieldName, node); + + final List values = node.getArguments(); + final List transformedValues = new ArrayList<>(); + final Path fieldPath = getFieldPath(fieldName, finalProperty); + + for (final String value : values) { + transformedValues.add(convertValueIfNecessary(node, fieldName, value, fieldPath)); + } + + this.joinsNeeded = this.joinsNeeded || areJoinsNeeded(node); + + return mapToPredicate(node, fieldPath, node.getArguments(), transformedValues, fieldName, finalProperty); + } + + private static boolean areJoinsNeeded(final ComparisonNode node) { + return !NO_JOINS_OPERATOR.contains(node.getOperator().getSymbol()); + } + + private Object convertValueIfNecessary(final ComparisonNode node, final A fieldName, final String value, + final Path fieldPath) { + // 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 it's 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, value, javaType); + } + if (fieldName instanceof FieldValueConverter) { + return convertFieldConverterValue(node, fieldName, value); + } + + if (Boolean.TYPE.equals(javaType)) { + return convertBooleanValue(node, value, javaType); + } + + return value; + } + + private Object convertBooleanValue(final ComparisonNode node, final String value, final Class javaType) { + 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 createRSQLParameterUnsupportedException( + "field {" + node.getSelector() + "} must be one of the following values {" + + Arrays.toString(((FieldValueConverter) fieldName).possibleValues(fieldName)) + "}", + null); + } else { + return convertedValue; + } + } + + // Exception squid:S2095 - see + // https://jira.sonarsource.com/browse/SONARJAVA-1478 + @SuppressWarnings({ "rawtypes", "unchecked", "squid:S2095" }) + private static Object transformEnumValue(final ComparisonNode node, final String value, final Class javaType) { + 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 + LOGGER.info("given value {} cannot be transformed into the correct enum type {}", value.toUpperCase(), + javaType); + LOGGER.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()).collect(Collectors.toList()) + + "}", e); + } + } + + private List mapToPredicate(final ComparisonNode node, final Path fieldPath, + final List values, final List transformedValues, final A enumField, + final String finalProperty) { + + String value = values.get(0); + // if lookup is available, replace macros ... + if (virtualPropertyReplacer != null) { + value = virtualPropertyReplacer.replace(value); + } + + final Predicate mapPredicate = mapToMapPredicate(node, fieldPath, enumField); + + final Predicate valuePredicate = addOperatorPredicate(node, getMapValueFieldPath(enumField, fieldPath), + transformedValues, value, finalProperty, enumField); + + return toSingleList(mapPredicate != null ? cb.and(mapPredicate, valuePredicate) : valuePredicate); + } + + private Predicate addOperatorPredicate(final ComparisonNode node, final Path fieldPath, + final List transformedValues, final String value, final String finalProperty, final A enumField) { + + // 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); + final String operator = node.getOperator().getSymbol(); + + switch (operator) { + case "==": + return getEqualToPredicate(transformedValue, fieldPath); + case "!=": + return getNotEqualToPredicate(transformedValue, fieldPath, finalProperty, enumField); + case "=gt=": + return cb.greaterThan(pathOfString(fieldPath), value); + case "=ge=": + return cb.greaterThanOrEqualTo(pathOfString(fieldPath), value); + case "=lt=": + return cb.lessThan(pathOfString(fieldPath), value); + case "=le=": + return cb.lessThanOrEqualTo(pathOfString(fieldPath), value); + case "=in=": + return getInPredicate(transformedValues, fieldPath); + case "=out=": + return getOutPredicate(transformedValues, finalProperty, enumField, fieldPath); + default: + throw new RSQLParameterSyntaxException( + "operator symbol {" + operator + "} is either not supported or not implemented"); + } + } + + private Predicate getInPredicate(final List transformedValues, final Path fieldPath) { + final List inParams = new ArrayList<>(); + for (final Object param : transformedValues) { + if (param instanceof String) { + inParams.add(((String) param).toUpperCase()); + } + } + if (!inParams.isEmpty()) { + return cb.upper(pathOfString(fieldPath)).in(inParams); + } else { + return fieldPath.in(transformedValues); + + } + } + + private Predicate getOutPredicate(final List transformedValues, final String finalProperty, + final A enumField, final Path fieldPath) { + + final String[] fieldNames = getSubAttributesFrom(finalProperty); + final List outParams = transformedValues.stream().filter(String.class::isInstance) + .map(String.class::cast).map(String::toUpperCase).collect(Collectors.toList()); + + if (isSimpleField(fieldNames, enumField.isMap())) { + return toNullOrNotInPredicate(fieldPath, transformedValues, outParams); + } + + clearOuterJoinsIfNotNeeded(); + + return toOutWithSubQueryPredicate(fieldNames, transformedValues, enumField, outParams); + } + + private Predicate toNullOrNotInPredicate(final Path fieldPath, final List transformedValues, + final List outParams) { + + final Path pathOfString = pathOfString(fieldPath); + final Predicate inPredicate = outParams.isEmpty() ? fieldPath.in(transformedValues) + : cb.upper(pathOfString).in(outParams); + + return cb.or(cb.isNull(pathOfString), cb.not(inPredicate)); + } + + private Predicate toOutWithSubQueryPredicate(final String[] fieldNames, final List transformedValues, + final A enumField, final List outParams) { + final Function, Predicate> inPredicateProvider = expressionToCompare -> outParams.isEmpty() + ? cb.upper(expressionToCompare).in(transformedValues) + : cb.upper(expressionToCompare).in(outParams); + return toNotExistsSubQueryPredicate(fieldNames, enumField, inPredicateProvider); + } + + private Path getMapValueFieldPath(final A enumField, final Path fieldPath) { + final String valueFieldNameFromSubEntity = enumField.getSubEntityMapTuple().map(Entry::getValue).orElse(null); + + if (!enumField.isMap() || valueFieldNameFromSubEntity == null) { + return fieldPath; + } + return fieldPath.get(valueFieldNameFromSubEntity); + } + + @SuppressWarnings("unchecked") + private Predicate mapToMapPredicate(final ComparisonNode node, final Path fieldPath, final A enumField) { + if (!enumField.isMap()) { + return null; + } + + final String[] graph = getSubAttributesFrom(node.getSelector()); + + final String keyValue = graph[graph.length - 1]; + if (fieldPath instanceof MapJoin) { + // Currently we support only string key .So below cast is safe. + return cb.equal(cb.upper((Expression) (((MapJoin) fieldPath).key())), + keyValue.toUpperCase()); + } + + final String keyFieldName = enumField.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 cb.equal(cb.upper(fieldPath.get(keyFieldName)), keyValue.toUpperCase()); + } + + private Predicate getEqualToPredicate(final Object transformedValue, final Path fieldPath) { + if (transformedValue == null) { + return cb.isNull(pathOfString(fieldPath)); + } + + if ((transformedValue instanceof String) && !NumberUtils.isCreatable((String) transformedValue)) { + if (StringUtils.isEmpty(transformedValue)) { + return cb.or(cb.isNull(pathOfString(fieldPath)), cb.equal(pathOfString(fieldPath), "")); + } + + final String sqlValue = toSQL((String) transformedValue); + return cb.like(cb.upper(pathOfString(fieldPath)), sqlValue, ESCAPE_CHAR); + } + + return cb.equal(fieldPath, transformedValue); + } + + private Predicate getNotEqualToPredicate(final Object transformedValue, final Path fieldPath, + final String finalProperty, final A enumField) { + + if (transformedValue == null) { + return toNotNullPredicate(fieldPath); + } + + if ((transformedValue instanceof String) && !NumberUtils.isCreatable((String) transformedValue)) { + if (StringUtils.isEmpty(transformedValue)) { + return toNotNullAndNotEmptyPredicate(fieldPath); + } + + final String sqlValue = toSQL((String) transformedValue); + final String[] fieldNames = getSubAttributesFrom(finalProperty); + + if (isSimpleField(fieldNames, enumField.isMap())) { + return toNullOrNotLikePredicate(fieldPath, sqlValue); + } + + clearOuterJoinsIfNotNeeded(); + + return toNotEqualWithSubQueryPredicate(enumField, sqlValue, fieldNames); + } + + return toNullOrNotEqualPredicate(fieldPath, transformedValue); + } + + private void clearOuterJoinsIfNotNeeded() { + if (!joinsNeeded) { + root.getJoins().clear(); + } + } + + private Predicate toNotNullPredicate(final Path fieldPath) { + return cb.isNotNull(pathOfString(fieldPath)); + } + + private Predicate toNullOrNotLikePredicate(final Path fieldPath, final String sqlValue) { + return cb.or(cb.isNull(pathOfString(fieldPath)), + cb.notLike(cb.upper(pathOfString(fieldPath)), sqlValue, ESCAPE_CHAR)); + } + + private Predicate toNullOrNotEqualPredicate(final Path fieldPath, final Object transformedValue) { + return cb.or(cb.isNull(pathOfString(fieldPath)), cb.notEqual(fieldPath, transformedValue)); + } + + private Predicate toNotNullAndNotEmptyPredicate(final Path fieldPath) { + return cb.and(cb.isNotNull(pathOfString(fieldPath)), cb.notEqual(pathOfString(fieldPath), "")); + } + + private Predicate toNotEqualWithSubQueryPredicate(final A enumField, final String sqlValue, + final String[] fieldNames) { + final Function, Predicate> likePredicateProvider = expressionToCompare -> cb + .like(cb.upper(expressionToCompare), sqlValue); + return toNotExistsSubQueryPredicate(fieldNames, enumField, likePredicateProvider); + } + + @SuppressWarnings({ "unchecked", "rawtypes" }) + private Predicate toNotExistsSubQueryPredicate(final String[] fieldNames, final A enumField, + final Function, Predicate> subQueryPredicateProvider) { + final Class javaType = root.getJavaType(); + final Subquery subquery = query.subquery(javaType); + final Root subqueryRoot = subquery.from(javaType); + final Predicate equalPredicate = cb.equal(root.get(enumField.identifierFieldName()), + subqueryRoot.get(enumField.identifierFieldName())); + final Path innerFieldPath = getInnerFieldPath(subqueryRoot, fieldNames, enumField.isMap()); + final Expression expressionToCompare = getExpressionToCompare(innerFieldPath, enumField); + final Predicate subQueryPredicate = subQueryPredicateProvider.apply(expressionToCompare); + subquery.select(subqueryRoot).where(cb.and(equalPredicate, subQueryPredicate)); + return cb.not(cb.exists(subquery)); + } + + private static boolean isSimpleField(final String[] split, final boolean isMapKeyField) { + return split.length == 1 || (split.length == 2 && isMapKeyField); + } + + @SuppressWarnings({ "rawtypes", "unchecked" }) + private Expression getExpressionToCompare(final Path innerFieldPath, final A enumField) { + if (!enumField.isMap()) { + return pathOfString(innerFieldPath); + } + if (innerFieldPath instanceof MapJoin) { + // Currently we support only string key. So below cast is safe. + return (Expression) (((MapJoin) pathOfString(innerFieldPath)).value()); + } + final String valueFieldName = enumField.getSubEntityMapTuple().map(Entry::getValue) + .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 pathOfString(innerFieldPath).get(valueFieldName); + } + + private static Path getInnerFieldPath(final Root subqueryRoot, final String[] split, + final boolean isMapKeyField) { + return getFieldPath(subqueryRoot, split, isMapKeyField, + (fieldPath, fieldNameSplit) -> getInnerJoinFieldPath(subqueryRoot, fieldPath, fieldNameSplit)) + .orElseThrow(() -> new RSQLParameterUnsupportedFieldException("RSQL field path cannot be empty", + null)); + } + + private static Path getInnerJoinFieldPath(final Root subqueryRoot, final Path fieldPath, + final String fieldNameSplit) { + if (fieldPath instanceof Join) { + return subqueryRoot.join(fieldNameSplit, JoinType.INNER); + } + return fieldPath; + } + + private String toSQL(final String transformedValue) { + final String escaped; + + if (database == Database.SQL_SERVER) { + escaped = transformedValue.replace("%", "[%]").replace("_", "[_]"); + } else { + escaped = transformedValue.replace("%", ESCAPE_CHAR + "%").replace("_", ESCAPE_CHAR + "_"); + } + + return escaped.replace(LIKE_WILDCARD, '%').toUpperCase(); + } + + @SuppressWarnings("unchecked") + private static Path pathOfString(final Path path) { + return (Path) path; + } + + private List acceptChilds(final LogicalNode node) { + final List children = node.getChildren(); + final List childs = new ArrayList<>(); + for (final Node node2 : children) { + final List accept = node2.accept(this); + if (!CollectionUtils.isEmpty(accept)) { + childs.addAll(accept); + } else { + LOGGER.debug("visit logical node children but could not parse it, ignoring {}", node2); + } + } + return childs; + } + +} 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 69e5ece21..89e639351 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 @@ -8,60 +8,31 @@ */ package org.eclipse.hawkbit.repository.jpa.rsql; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.HashMap; -import java.util.HashSet; import java.util.List; -import java.util.Map; -import java.util.Map.Entry; -import java.util.Optional; import java.util.Set; -import java.util.function.BiFunction; -import java.util.function.Function; -import java.util.stream.Collectors; import javax.persistence.criteria.CriteriaBuilder; import javax.persistence.criteria.CriteriaQuery; -import javax.persistence.criteria.Expression; -import javax.persistence.criteria.From; -import javax.persistence.criteria.Join; -import javax.persistence.criteria.JoinType; -import javax.persistence.criteria.MapJoin; -import javax.persistence.criteria.Path; -import javax.persistence.criteria.PluralJoin; import javax.persistence.criteria.Predicate; import javax.persistence.criteria.Root; -import javax.persistence.criteria.Subquery; -import org.apache.commons.lang3.math.NumberUtils; import org.apache.commons.lang3.text.StrLookup; import org.eclipse.hawkbit.repository.FieldNameProvider; -import org.eclipse.hawkbit.repository.FieldValueConverter; import org.eclipse.hawkbit.repository.exception.RSQLParameterSyntaxException; import org.eclipse.hawkbit.repository.exception.RSQLParameterUnsupportedFieldException; +import org.eclipse.hawkbit.repository.rsql.RsqlVisitorFactoryHolder; import org.eclipse.hawkbit.repository.rsql.VirtualPropertyReplacer; import org.eclipse.hawkbit.repository.rsql.VirtualPropertyResolver; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.springframework.beans.SimpleTypeConverter; -import org.springframework.beans.TypeMismatchException; import org.springframework.data.jpa.domain.Specification; import org.springframework.orm.jpa.vendor.Database; import org.springframework.util.CollectionUtils; -import org.springframework.util.StringUtils; - -import com.google.common.collect.Lists; import cz.jirutka.rsql.parser.RSQLParser; import cz.jirutka.rsql.parser.RSQLParserException; -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; @@ -111,11 +82,12 @@ public final class RSQLUtility { } /** - * parses an RSQL valid string into an JPA {@link Specification} which then - * can be used to filter for JPA entities with the given RSQL query. + * Builds a JPA {@link Specification} which corresponds with the given RSQL + * query. The specification can be used to filter for JPA entities with the + * given RSQL query. * * @param rsql - * the rsql query + * the rsql query to be parsed * @param fieldNameProvider * the enum class type which implements the * {@link FieldNameProvider} @@ -133,7 +105,7 @@ public final class RSQLUtility { * if the RSQL syntax is wrong * */ - public static & FieldNameProvider, T> Specification parse(final String rsql, + public static & FieldNameProvider, T> Specification buildRsqlSpecification(final String rsql, final Class fieldNameProvider, final VirtualPropertyReplacer virtualPropertyReplacer, final Database database) { return new RSQLSpecification<>(rsql, fieldNameProvider, virtualPropertyReplacer, database); @@ -153,11 +125,16 @@ public final class RSQLUtility { */ public static & FieldNameProvider> void validateRsqlFor(final String rsql, final Class fieldNameProvider) { - final RSQLVisitor visitor = new ValidationRSQLVisitor<>(fieldNameProvider); + final RSQLVisitor visitor = getValidationRsqlVisitor(fieldNameProvider); final Node rootNode = parseRsql(rsql); rootNode.accept(visitor); } + private static & FieldNameProvider> RSQLVisitor getValidationRsqlVisitor( + final Class fieldNameProvider) { + return RsqlVisitorFactoryHolder.getInstance().getRsqlVisitorFactory().validationRsqlVisitor(fieldNameProvider); + } + private static Node parseRsql(final String rsql) { try { LOGGER.debug("Parsing rsql string {}", rsql); @@ -170,36 +147,6 @@ public final class RSQLUtility { } } - private static final class ValidationRSQLVisitor & FieldNameProvider> - extends AbstractFieldNameRSQLVisitor implements RSQLVisitor { - - public ValidationRSQLVisitor(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) { - final A fieldName = getFieldEnumByName(node); - getAndValidatePropertyFieldName(fieldName, node); - return null; - } - - private Void visitNode(final LogicalNode node, final String param) { - node.getChildren().forEach(child -> child.accept(this, param)); - return null; - } - } - private static final class RSQLSpecification & FieldNameProvider, T> implements Specification { private static final long serialVersionUID = 1L; @@ -222,7 +169,7 @@ public final class RSQLUtility { final Node rootNode = parseRsql(rsql); query.distinct(true); - final JpaQueryRSQLVisitor jpqQueryRSQLVisitor = new JpaQueryRSQLVisitor<>(root, cb, enumType, + final JpaQueryRsqlVisitor jpqQueryRSQLVisitor = new JpaQueryRsqlVisitor<>(root, cb, enumType, virtualPropertyReplacer, database, query); final List accept = rootNode., String> accept(jpqQueryRSQLVisitor); @@ -234,545 +181,4 @@ public final class RSQLUtility { } } - /** - * An implementation of the {@link RSQLVisitor} to visit the parsed tokens - * and build JPA where clauses. - * - * @param - * the enum for providing the field name of the entity field to - * filter on. - * @param - * the entity type referenced by the root - */ - private static final class JpaQueryRSQLVisitor & FieldNameProvider, T> - extends AbstractFieldNameRSQLVisitor implements RSQLVisitor, String> { - - private static final Logger LOGGER = LoggerFactory.getLogger(JpaQueryRSQLVisitor.class); - - public static final Character LIKE_WILDCARD = '*'; - private static final char ESCAPE_CHAR = '\\'; - private static final List NO_JOINS_OPERATOR = Lists.newArrayList("!=", "=out="); - - private final Map>> joinsInLevel = new HashMap<>(3); - - private final CriteriaBuilder cb; - private final CriteriaQuery query; - private final Database database; - private final Root root; - private final SimpleTypeConverter simpleTypeConverter; - private final VirtualPropertyReplacer virtualPropertyReplacer; - - private int level; - private boolean isOrLevel; - private boolean joinsNeeded; - - private JpaQueryRSQLVisitor(final Root root, final CriteriaBuilder cb, final Class enumType, - final VirtualPropertyReplacer virtualPropertyReplacer, final Database database, - final CriteriaQuery query) { - super(enumType); - this.root = root; - this.cb = cb; - this.query = query; - this.virtualPropertyReplacer = virtualPropertyReplacer; - this.simpleTypeConverter = new SimpleTypeConverter(); - this.database = database; - this.joinsNeeded = false; - } - - private void beginLevel(final boolean isOr) { - level++; - isOrLevel = isOr; - joinsInLevel.put(level, new HashSet<>(2)); - } - - private void endLevel() { - joinsInLevel.remove(level); - level--; - isOrLevel = false; - } - - private Set> getCurrentJoins() { - if (level > 0) { - return joinsInLevel.get(level); - } - return Collections.emptySet(); - } - - private Optional> findCurrentJoinOfType(final Class type) { - return getCurrentJoins().stream().filter(j -> type.equals(j.getJavaType())).findAny(); - } - - private void addCurrentJoin(final Join join) { - if (level > 0) { - getCurrentJoins().add(join); - } - } - - @Override - public List visit(final AndNode node, final String param) { - beginLevel(false); - final List childs = acceptChilds(node); - endLevel(); - if (!childs.isEmpty()) { - return toSingleList(cb.and(childs.toArray(new Predicate[childs.size()]))); - } - return toSingleList(cb.conjunction()); - } - - @Override - public List visit(final OrNode node, final String param) { - beginLevel(true); - final List childs = acceptChilds(node); - endLevel(); - if (!childs.isEmpty()) { - return toSingleList(cb.or(childs.toArray(new Predicate[childs.size()]))); - } - return toSingleList(cb.conjunction()); - } - - private static List toSingleList(final Predicate predicate) { - return Collections.singletonList(predicate); - } - - /** - * Resolves the Path for a field in the persistence layer and joins the - * required models. This operation is part of a tree traversal through - * an RSQL expression. It creates for every field that is not part of - * the root model a join to the foreign model. This behavior is - * optimized when several joins happen directly under an OR node in the - * traversed tree. The same foreign model is only joined once. - * - * Example: tags.name==M;(tags.name==A,tags.name==B,tags.name==C) This - * example joins the tags model only twice, because for the OR node in - * brackets only one join is used. - * - * @param enumField - * field from a FieldNameProvider to resolve on the - * persistence layer - * @param finalProperty - * dot notated field path - * @return the Path for a field - */ - @SuppressWarnings("unchecked") - private Path getFieldPath(final A enumField, final String finalProperty) { - return (Path) getFieldPath(root, getSubAttributesFrom(finalProperty), enumField.isMap(), - this::getJoinFieldPath).orElseThrow( - () -> createRSQLParameterUnsupportedException("RSQL field path cannot be empty", null)); - } - - @SuppressWarnings("unchecked") - private Path getJoinFieldPath(final Path fieldPath, final String fieldNameSplit) { - if (fieldPath instanceof PluralJoin) { - final Join join = (Join) fieldPath; - final From joinParent = join.getParent(); - final Optional> currentJoinOfType = findCurrentJoinOfType(join.getJavaType()); - if (currentJoinOfType.isPresent() && isOrLevel) { - // remove the additional join and use the existing one - joinParent.getJoins().remove(join); - return currentJoinOfType.get(); - } else { - final Join newJoin = joinParent.join(fieldNameSplit, JoinType.LEFT); - addCurrentJoin(newJoin); - return newJoin; - } - } - return fieldPath; - } - - private static Optional> getFieldPath(final Root root, final String[] split, - final boolean isMapKeyField, final BiFunction, String, Path> joinFieldPathProvider) { - Path fieldPath = null; - for (int i = 0; i < split.length; i++) { - if (!(isMapKeyField && i == (split.length - 1))) { - final String fieldNameSplit = split[i]; - fieldPath = (fieldPath != null) ? fieldPath.get(fieldNameSplit) : root.get(fieldNameSplit); - fieldPath = joinFieldPathProvider.apply(fieldPath, fieldNameSplit); - } - } - return Optional.ofNullable(fieldPath); - } - - @Override - // Exception squid:S2095 - see - // https://jira.sonarsource.com/browse/SONARJAVA-1478 - @SuppressWarnings({ "squid:S2095" }) - public List visit(final ComparisonNode node, final String param) { - final A fieldName = getFieldEnumByName(node); - final String finalProperty = getAndValidatePropertyFieldName(fieldName, node); - - final List values = node.getArguments(); - final List transformedValues = new ArrayList<>(); - final Path fieldPath = getFieldPath(fieldName, finalProperty); - - for (final String value : values) { - transformedValues.add(convertValueIfNecessary(node, fieldName, value, fieldPath)); - } - - this.joinsNeeded = this.joinsNeeded || areJoinsNeeded(node); - - return mapToPredicate(node, fieldPath, node.getArguments(), transformedValues, fieldName, finalProperty); - } - - private static boolean areJoinsNeeded(final ComparisonNode node) { - return !NO_JOINS_OPERATOR.contains(node.getOperator().getSymbol()); - } - - private Object convertValueIfNecessary(final ComparisonNode node, final A fieldName, final String value, - final Path fieldPath) { - // 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 it's 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, value, javaType); - } - if (fieldName instanceof FieldValueConverter) { - return convertFieldConverterValue(node, fieldName, value); - } - - if (Boolean.TYPE.equals(javaType)) { - return convertBooleanValue(node, value, javaType); - } - - return value; - } - - private Object convertBooleanValue(final ComparisonNode node, final String value, final Class javaType) { - 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 createRSQLParameterUnsupportedException( - "field {" + node.getSelector() + "} must be one of the following values {" - + Arrays.toString(((FieldValueConverter) fieldName).possibleValues(fieldName)) + "}", - null); - } else { - return convertedValue; - } - } - - // Exception squid:S2095 - see - // https://jira.sonarsource.com/browse/SONARJAVA-1478 - @SuppressWarnings({ "rawtypes", "unchecked", "squid:S2095" }) - private static Object transformEnumValue(final ComparisonNode node, final String value, - final Class javaType) { - 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 - LOGGER.info("given value {} cannot be transformed into the correct enum type {}", value.toUpperCase(), - javaType); - LOGGER.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()).collect(Collectors.toList()) - + "}", e); - } - } - - private List mapToPredicate(final ComparisonNode node, final Path fieldPath, - final List values, final List transformedValues, final A enumField, - final String finalProperty) { - - String value = values.get(0); - // if lookup is available, replace macros ... - if (virtualPropertyReplacer != null) { - value = virtualPropertyReplacer.replace(value); - } - - final Predicate mapPredicate = mapToMapPredicate(node, fieldPath, enumField); - - final Predicate valuePredicate = addOperatorPredicate(node, getMapValueFieldPath(enumField, fieldPath), - transformedValues, value, finalProperty, enumField); - - return toSingleList(mapPredicate != null ? cb.and(mapPredicate, valuePredicate) : valuePredicate); - } - - private Predicate addOperatorPredicate(final ComparisonNode node, final Path fieldPath, - final List transformedValues, final String value, final String finalProperty, - final A enumField) { - - // 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); - final String operator = node.getOperator().getSymbol(); - - switch (operator) { - case "==": - return getEqualToPredicate(transformedValue, fieldPath); - case "!=": - return getNotEqualToPredicate(transformedValue, fieldPath, finalProperty, enumField); - case "=gt=": - return cb.greaterThan(pathOfString(fieldPath), value); - case "=ge=": - return cb.greaterThanOrEqualTo(pathOfString(fieldPath), value); - case "=lt=": - return cb.lessThan(pathOfString(fieldPath), value); - case "=le=": - return cb.lessThanOrEqualTo(pathOfString(fieldPath), value); - case "=in=": - return getInPredicate(transformedValues, fieldPath); - case "=out=": - return getOutPredicate(transformedValues, finalProperty, enumField, fieldPath); - default: - throw new RSQLParameterSyntaxException( - "operator symbol {" + operator + "} is either not supported or not implemented"); - } - } - - private Predicate getInPredicate(final List transformedValues, final Path fieldPath) { - final List inParams = new ArrayList<>(); - for (final Object param : transformedValues) { - if (param instanceof String) { - inParams.add(((String) param).toUpperCase()); - } - } - if (!inParams.isEmpty()) { - return cb.upper(pathOfString(fieldPath)).in(inParams); - } else { - return fieldPath.in(transformedValues); - - } - } - - private Predicate getOutPredicate(final List transformedValues, final String finalProperty, - final A enumField, final Path fieldPath) { - - final String[] fieldNames = getSubAttributesFrom(finalProperty); - final List outParams = transformedValues.stream().filter(String.class::isInstance) - .map(String.class::cast).map(String::toUpperCase).collect(Collectors.toList()); - - if (isSimpleField(fieldNames, enumField.isMap())) { - return toNullOrNotInPredicate(fieldPath, transformedValues, outParams); - } - - clearOuterJoinsIfNotNeeded(); - - return toOutWithSubQueryPredicate(fieldNames, transformedValues, enumField, outParams); - } - - private Predicate toNullOrNotInPredicate(final Path fieldPath, final List transformedValues, - final List outParams) { - - final Path pathOfString = pathOfString(fieldPath); - final Predicate inPredicate = outParams.isEmpty() ? fieldPath.in(transformedValues) - : cb.upper(pathOfString).in(outParams); - - return cb.or(cb.isNull(pathOfString), cb.not(inPredicate)); - } - - private Predicate toOutWithSubQueryPredicate(final String[] fieldNames, final List transformedValues, - final A enumField, final List outParams) { - final Function, Predicate> inPredicateProvider = expressionToCompare -> outParams - .isEmpty() ? cb.upper(expressionToCompare).in(transformedValues) - : cb.upper(expressionToCompare).in(outParams); - return toNotExistsSubQueryPredicate(fieldNames, enumField, inPredicateProvider); - } - - private Path getMapValueFieldPath(final A enumField, final Path fieldPath) { - final String valueFieldNameFromSubEntity = enumField.getSubEntityMapTuple().map(Entry::getValue) - .orElse(null); - - if (!enumField.isMap() || valueFieldNameFromSubEntity == null) { - return fieldPath; - } - return fieldPath.get(valueFieldNameFromSubEntity); - } - - @SuppressWarnings("unchecked") - private Predicate mapToMapPredicate(final ComparisonNode node, final Path fieldPath, - final A enumField) { - if (!enumField.isMap()) { - return null; - } - - final String[] graph = getSubAttributesFrom(node.getSelector()); - - final String keyValue = graph[graph.length - 1]; - if (fieldPath instanceof MapJoin) { - // Currently we support only string key .So below cast is safe. - return cb.equal(cb.upper((Expression) (((MapJoin) fieldPath).key())), - keyValue.toUpperCase()); - } - - final String keyFieldName = enumField.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 cb.equal(cb.upper(fieldPath.get(keyFieldName)), keyValue.toUpperCase()); - } - - private Predicate getEqualToPredicate(final Object transformedValue, final Path fieldPath) { - if (transformedValue == null) { - return cb.isNull(pathOfString(fieldPath)); - } - - if ((transformedValue instanceof String) && !NumberUtils.isCreatable((String) transformedValue)) { - if (StringUtils.isEmpty(transformedValue)) { - return cb.or(cb.isNull(pathOfString(fieldPath)), cb.equal(pathOfString(fieldPath), "")); - } - - final String sqlValue = toSQL((String) transformedValue); - return cb.like(cb.upper(pathOfString(fieldPath)), sqlValue, ESCAPE_CHAR); - } - - return cb.equal(fieldPath, transformedValue); - } - - private Predicate getNotEqualToPredicate(final Object transformedValue, final Path fieldPath, - final String finalProperty, final A enumField) { - - if (transformedValue == null) { - return toNotNullPredicate(fieldPath); - } - - if ((transformedValue instanceof String) && !NumberUtils.isCreatable((String) transformedValue)) { - if (StringUtils.isEmpty(transformedValue)) { - return toNotNullAndNotEmptyPredicate(fieldPath); - } - - final String sqlValue = toSQL((String) transformedValue); - final String[] fieldNames = getSubAttributesFrom(finalProperty); - - if (isSimpleField(fieldNames, enumField.isMap())) { - return toNullOrNotLikePredicate(fieldPath, sqlValue); - } - - clearOuterJoinsIfNotNeeded(); - - return toNotEqualWithSubQueryPredicate(enumField, sqlValue, fieldNames); - } - - return toNullOrNotEqualPredicate(fieldPath, transformedValue); - } - - private void clearOuterJoinsIfNotNeeded() { - if (!joinsNeeded) { - root.getJoins().clear(); - } - } - - private Predicate toNotNullPredicate(final Path fieldPath) { - return cb.isNotNull(pathOfString(fieldPath)); - } - - private Predicate toNullOrNotLikePredicate(final Path fieldPath, final String sqlValue) { - return cb.or(cb.isNull(pathOfString(fieldPath)), - cb.notLike(cb.upper(pathOfString(fieldPath)), sqlValue, ESCAPE_CHAR)); - } - - private Predicate toNullOrNotEqualPredicate(final Path fieldPath, final Object transformedValue) { - return cb.or(cb.isNull(pathOfString(fieldPath)), cb.notEqual(fieldPath, transformedValue)); - } - - private Predicate toNotNullAndNotEmptyPredicate(final Path fieldPath) { - return cb.and(cb.isNotNull(pathOfString(fieldPath)), cb.notEqual(pathOfString(fieldPath), "")); - } - - private Predicate toNotEqualWithSubQueryPredicate(final A enumField, final String sqlValue, - final String[] fieldNames) { - final Function, Predicate> likePredicateProvider = expressionToCompare -> cb - .like(cb.upper(expressionToCompare), sqlValue); - return toNotExistsSubQueryPredicate(fieldNames, enumField, likePredicateProvider); - } - - @SuppressWarnings({ "unchecked", "rawtypes" }) - private Predicate toNotExistsSubQueryPredicate(final String[] fieldNames, final A enumField, - final Function, Predicate> subQueryPredicateProvider) { - final Class javaType = root.getJavaType(); - final Subquery subquery = query.subquery(javaType); - final Root subqueryRoot = subquery.from(javaType); - final Predicate equalPredicate = cb.equal(root.get(enumField.identifierFieldName()), - subqueryRoot.get(enumField.identifierFieldName())); - final Path innerFieldPath = getInnerFieldPath(subqueryRoot, fieldNames, enumField.isMap()); - final Expression expressionToCompare = getExpressionToCompare(innerFieldPath, enumField); - final Predicate subQueryPredicate = subQueryPredicateProvider.apply(expressionToCompare); - subquery.select(subqueryRoot).where(cb.and(equalPredicate, subQueryPredicate)); - return cb.not(cb.exists(subquery)); - } - - private static boolean isSimpleField(final String[] split, final boolean isMapKeyField) { - return split.length == 1 || (split.length == 2 && isMapKeyField); - } - - private Expression getExpressionToCompare(final Path innerFieldPath, final A enumField) { - if (!enumField.isMap()) { - return pathOfString(innerFieldPath); - } - if (innerFieldPath instanceof MapJoin) { - // Currently we support only string key. So below cast is safe. - return (Expression) (((MapJoin) pathOfString(innerFieldPath)).value()); - } - final String valueFieldName = enumField.getSubEntityMapTuple().map(Entry::getValue) - .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 pathOfString(innerFieldPath).get(valueFieldName); - } - - private static Path getInnerFieldPath(final Root subqueryRoot, final String[] split, - final boolean isMapKeyField) { - return getFieldPath(subqueryRoot, split, isMapKeyField, - (fieldPath, fieldNameSplit) -> getInnerJoinFieldPath(subqueryRoot, fieldPath, fieldNameSplit)) - .orElseThrow(() -> new RSQLParameterUnsupportedFieldException( - "RSQL field path cannot be empty", null)); - } - - private static Path getInnerJoinFieldPath(final Root subqueryRoot, final Path fieldPath, - final String fieldNameSplit) { - if (fieldPath instanceof Join) { - return subqueryRoot.join(fieldNameSplit, JoinType.INNER); - } - return fieldPath; - } - - private String toSQL(final String transformedValue) { - final String escaped; - - if (database == Database.SQL_SERVER) { - escaped = transformedValue.replace("%", "[%]").replace("_", "[_]"); - } else { - escaped = transformedValue.replace("%", ESCAPE_CHAR + "%").replace("_", ESCAPE_CHAR + "_"); - } - - return escaped.replace(LIKE_WILDCARD, '%').toUpperCase(); - } - - @SuppressWarnings("unchecked") - private static Path pathOfString(final Path path) { - return (Path) path; - } - - private List acceptChilds(final LogicalNode node) { - final List children = node.getChildren(); - final List childs = new ArrayList<>(); - for (final Node node2 : children) { - final List accept = node2.accept(this); - if (!CollectionUtils.isEmpty(accept)) { - childs.addAll(accept); - } else { - LOGGER.debug("visit logical node children but could not parse it, ignoring {}", node2); - } - } - return childs; - } - - } - } 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 5e5a83d89..a98a27403 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 @@ -154,7 +154,7 @@ public class RSQLUtilityTest { public void wrongRsqlSyntaxThrowSyntaxException() { final String wrongRSQL = "name==abc;d"; try { - RSQLUtility.parse(wrongRSQL, SoftwareModuleFields.class, null, testDb) + RSQLUtility.buildRsqlSpecification(wrongRSQL, SoftwareModuleFields.class, null, testDb) .toPredicate(baseSoftwareModuleRootMock, criteriaQueryMock, criteriaBuilderMock); fail("Missing expected RSQLParameterSyntaxException because of wrong RSQL syntax"); } catch (final RSQLParameterSyntaxException e) { @@ -166,7 +166,7 @@ public class RSQLUtilityTest { final String wrongRSQL = "unknownField==abc"; when(baseSoftwareModuleRootMock.getJavaType()).thenReturn((Class) SoftwareModule.class); try { - RSQLUtility.parse(wrongRSQL, SoftwareModuleFields.class, null, testDb) + RSQLUtility.buildRsqlSpecification(wrongRSQL, SoftwareModuleFields.class, null, testDb) .toPredicate(baseSoftwareModuleRootMock, criteriaQueryMock, criteriaBuilderMock); fail("Missing an expected RSQLParameterUnsupportedFieldException because of unknown RSQL field"); } catch (final RSQLParameterUnsupportedFieldException e) { @@ -178,7 +178,7 @@ public class RSQLUtilityTest { public void wrongRsqlMapSyntaxThrowSyntaxException() { String wrongRSQL = TargetFields.ATTRIBUTE + "==abc"; try { - RSQLUtility.parse(wrongRSQL, TargetFields.class, null, testDb).toPredicate(baseSoftwareModuleRootMock, + RSQLUtility.buildRsqlSpecification(wrongRSQL, TargetFields.class, null, testDb).toPredicate(baseSoftwareModuleRootMock, criteriaQueryMock, criteriaBuilderMock); fail("Missing expected RSQLParameterSyntaxException for target attributes map, caused by wrong RSQL syntax (key was not present)"); } catch (final RSQLParameterUnsupportedFieldException e) { @@ -186,7 +186,7 @@ public class RSQLUtilityTest { wrongRSQL = TargetFields.ATTRIBUTE + ".unknown.wrong==abc"; try { - RSQLUtility.parse(wrongRSQL, TargetFields.class, null, testDb).toPredicate(baseSoftwareModuleRootMock, + RSQLUtility.buildRsqlSpecification(wrongRSQL, TargetFields.class, null, testDb).toPredicate(baseSoftwareModuleRootMock, criteriaQueryMock, criteriaBuilderMock); fail("Missing expected RSQLParameterSyntaxException for target attributes map, caused by wrong RSQL syntax (key includes dots)"); } catch (final RSQLParameterUnsupportedFieldException e) { @@ -194,7 +194,7 @@ public class RSQLUtilityTest { wrongRSQL = TargetFields.METADATA + ".unknown.wrong==abc"; try { - RSQLUtility.parse(wrongRSQL, TargetFields.class, null, testDb).toPredicate(baseSoftwareModuleRootMock, + RSQLUtility.buildRsqlSpecification(wrongRSQL, TargetFields.class, null, testDb).toPredicate(baseSoftwareModuleRootMock, criteriaQueryMock, criteriaBuilderMock); fail("Missing expected RSQLParameterSyntaxException for target metadata map, caused by wrong RSQL syntax (key includes dots)"); } catch (final RSQLParameterUnsupportedFieldException e) { @@ -202,7 +202,7 @@ public class RSQLUtilityTest { wrongRSQL = DistributionSetFields.METADATA + "==abc"; try { - RSQLUtility.parse(wrongRSQL, DistributionSetFields.class, null, testDb) + RSQLUtility.buildRsqlSpecification(wrongRSQL, DistributionSetFields.class, null, testDb) .toPredicate(baseSoftwareModuleRootMock, criteriaQueryMock, criteriaBuilderMock); fail("Missing expected RSQLParameterSyntaxException for distribution set metadata map, caused by wrong RSQL syntax (key was not present)"); } catch (final RSQLParameterUnsupportedFieldException e) { @@ -214,7 +214,7 @@ public class RSQLUtilityTest { public void wrongRsqlSubEntitySyntaxThrowSyntaxException() { String wrongRSQL = TargetFields.ASSIGNEDDS + "==abc"; try { - RSQLUtility.parse(wrongRSQL, TargetFields.class, null, testDb).toPredicate(baseSoftwareModuleRootMock, + RSQLUtility.buildRsqlSpecification(wrongRSQL, TargetFields.class, null, testDb).toPredicate(baseSoftwareModuleRootMock, criteriaQueryMock, criteriaBuilderMock); fail("Missing expected RSQLParameterSyntaxException because of wrong RSQL syntax"); } catch (final RSQLParameterUnsupportedFieldException e) { @@ -222,7 +222,7 @@ public class RSQLUtilityTest { wrongRSQL = TargetFields.ASSIGNEDDS + ".unknownField==abc"; try { - RSQLUtility.parse(wrongRSQL, TargetFields.class, null, testDb).toPredicate(baseSoftwareModuleRootMock, + RSQLUtility.buildRsqlSpecification(wrongRSQL, TargetFields.class, null, testDb).toPredicate(baseSoftwareModuleRootMock, criteriaQueryMock, criteriaBuilderMock); fail("Missing expected RSQLParameterSyntaxException because of wrong RSQL syntax"); } catch (final RSQLParameterUnsupportedFieldException e) { @@ -230,7 +230,7 @@ public class RSQLUtilityTest { wrongRSQL = TargetFields.ASSIGNEDDS + ".unknownField.ToMuch==abc"; try { - RSQLUtility.parse(wrongRSQL, TargetFields.class, null, testDb).toPredicate(baseSoftwareModuleRootMock, + RSQLUtility.buildRsqlSpecification(wrongRSQL, TargetFields.class, null, testDb).toPredicate(baseSoftwareModuleRootMock, criteriaQueryMock, criteriaBuilderMock); fail("Missing expected RSQLParameterSyntaxException because of wrong RSQL syntax"); } catch (final RSQLParameterUnsupportedFieldException e) { @@ -250,7 +250,7 @@ public class RSQLUtilityTest { when(criteriaBuilderMock.equal(any(Expression.class), any(String.class))).thenReturn(mock(Predicate.class)); // test - RSQLUtility.parse(correctRsql, SoftwareModuleFields.class, null, testDb).toPredicate(baseSoftwareModuleRootMock, + RSQLUtility.buildRsqlSpecification(correctRsql, SoftwareModuleFields.class, null, testDb).toPredicate(baseSoftwareModuleRootMock, criteriaQueryMock, criteriaBuilderMock); // verification @@ -271,7 +271,7 @@ public class RSQLUtilityTest { .thenReturn(pathOfString(baseSoftwareModuleRootMock)); // test - RSQLUtility.parse(correctRsql, SoftwareModuleFields.class, null, testDb).toPredicate(baseSoftwareModuleRootMock, + RSQLUtility.buildRsqlSpecification(correctRsql, SoftwareModuleFields.class, null, testDb).toPredicate(baseSoftwareModuleRootMock, criteriaQueryMock, criteriaBuilderMock); // verification @@ -301,7 +301,7 @@ public class RSQLUtilityTest { when(subqueryMock.select(subqueryRootMock)).thenReturn(subqueryMock); // test - RSQLUtility.parse(correctRsql, SoftwareModuleFields.class, null, testDb).toPredicate(baseSoftwareModuleRootMock, + RSQLUtility.buildRsqlSpecification(correctRsql, SoftwareModuleFields.class, null, testDb).toPredicate(baseSoftwareModuleRootMock, criteriaQueryMock, criteriaBuilderMock); // verification @@ -320,7 +320,7 @@ public class RSQLUtilityTest { when(criteriaBuilderMock.upper(eq(pathOfString(baseSoftwareModuleRootMock)))) .thenReturn(pathOfString(baseSoftwareModuleRootMock)); // test - RSQLUtility.parse(correctRsql, SoftwareModuleFields.class, null, Database.H2) + RSQLUtility.buildRsqlSpecification(correctRsql, SoftwareModuleFields.class, null, Database.H2) .toPredicate(baseSoftwareModuleRootMock, criteriaQueryMock, criteriaBuilderMock); // verification @@ -342,7 +342,7 @@ public class RSQLUtilityTest { .thenReturn(mock(Predicate.class)); // test - RSQLUtility.parse(correctRsql, SoftwareModuleFields.class, null, Database.SQL_SERVER) + RSQLUtility.buildRsqlSpecification(correctRsql, SoftwareModuleFields.class, null, Database.SQL_SERVER) .toPredicate(baseSoftwareModuleRootMock, criteriaQueryMock, criteriaBuilderMock); // verification @@ -361,7 +361,7 @@ public class RSQLUtilityTest { when(criteriaBuilderMock. greaterThanOrEqualTo(any(Expression.class), any(String.class))) .thenReturn(mock(Predicate.class)); // test - RSQLUtility.parse(correctRsql, SoftwareModuleFields.class, null, testDb).toPredicate(baseSoftwareModuleRootMock, + RSQLUtility.buildRsqlSpecification(correctRsql, SoftwareModuleFields.class, null, testDb).toPredicate(baseSoftwareModuleRootMock, criteriaQueryMock, criteriaBuilderMock); // verification @@ -378,7 +378,7 @@ public class RSQLUtilityTest { when(criteriaBuilderMock.equal(any(Root.class), any(TestValueEnum.class))).thenReturn(mock(Predicate.class)); // test - RSQLUtility.parse(correctRsql, TestFieldEnum.class, null, testDb).toPredicate(baseSoftwareModuleRootMock, + RSQLUtility.buildRsqlSpecification(correctRsql, TestFieldEnum.class, null, testDb).toPredicate(baseSoftwareModuleRootMock, criteriaQueryMock, criteriaBuilderMock); // verification @@ -396,7 +396,7 @@ public class RSQLUtilityTest { try { // test - RSQLUtility.parse(correctRsql, TestFieldEnum.class, null, testDb).toPredicate(baseSoftwareModuleRootMock, + RSQLUtility.buildRsqlSpecification(correctRsql, TestFieldEnum.class, null, testDb).toPredicate(baseSoftwareModuleRootMock, criteriaQueryMock, criteriaBuilderMock); fail("missing RSQLParameterUnsupportedFieldException for wrong enum value"); } catch (final RSQLParameterUnsupportedFieldException e) { @@ -420,7 +420,7 @@ public class RSQLUtilityTest { .thenReturn(mock(Predicate.class)); // test - RSQLUtility.parse(correctRsql, TestFieldEnum.class, setupMacroLookup(), testDb) + RSQLUtility.buildRsqlSpecification(correctRsql, TestFieldEnum.class, setupMacroLookup(), testDb) .toPredicate(baseSoftwareModuleRootMock, criteriaQueryMock, criteriaBuilderMock); // verification @@ -446,7 +446,7 @@ public class RSQLUtilityTest { .thenReturn(mock(Predicate.class)); // test - RSQLUtility.parse(correctRsql, TestFieldEnum.class, setupMacroLookup(), testDb) + RSQLUtility.buildRsqlSpecification(correctRsql, TestFieldEnum.class, setupMacroLookup(), testDb) .toPredicate(baseSoftwareModuleRootMock, criteriaQueryMock, criteriaBuilderMock); // verification