Make RSQL visitor pluggable via a managed bean (#1145)
* Add a managed bean for a RsqlVisitorFactory Signed-off-by: Stefan Behl <stefan.behl@bosch.io> * Fix failing unit tests * Fix Sonar findings * Fix PR review findings
This commit is contained in:
@@ -655,7 +655,7 @@ public class JpaDeploymentManagement extends JpaActionManagement implements Depl
|
||||
}
|
||||
|
||||
private Specification<JpaAction> createSpecificationFor(final String controllerId, final String rsqlParam) {
|
||||
final Specification<JpaAction> spec = RSQLUtility.parse(rsqlParam, ActionFields.class, virtualPropertyReplacer,
|
||||
final Specification<JpaAction> 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));
|
||||
|
||||
@@ -555,7 +555,7 @@ public class JpaDistributionSetManagement implements DistributionSetManagement {
|
||||
|
||||
throwExceptionIfDistributionSetDoesNotExist(distributionSetId);
|
||||
|
||||
final Specification<JpaDistributionSetMetadata> spec = RSQLUtility.parse(rsqlParam,
|
||||
final Specification<JpaDistributionSetMetadata> spec = RSQLUtility.buildRsqlSpecification(rsqlParam,
|
||||
DistributionSetMetadataFields.class, virtualPropertyReplacer, database);
|
||||
|
||||
return convertMdPage(
|
||||
@@ -784,7 +784,7 @@ public class JpaDistributionSetManagement implements DistributionSetManagement {
|
||||
public Page<DistributionSet> findByRsqlAndTag(final Pageable pageable, final String rsqlParam, final long tagId) {
|
||||
throwEntityNotFoundExceptionIfDsTagDoesNotExist(tagId);
|
||||
|
||||
final Specification<JpaDistributionSet> spec = RSQLUtility.parse(rsqlParam, DistributionSetFields.class,
|
||||
final Specification<JpaDistributionSet> 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<DistributionSet> findByRsql(final Pageable pageable, final String rsqlParam) {
|
||||
final Specification<JpaDistributionSet> spec = RSQLUtility.parse(rsqlParam, DistributionSetFields.class,
|
||||
final Specification<JpaDistributionSet> spec = RSQLUtility.buildRsqlSpecification(rsqlParam, DistributionSetFields.class,
|
||||
virtualPropertyReplacer, database);
|
||||
|
||||
return convertDsPage(
|
||||
|
||||
@@ -134,7 +134,7 @@ public class JpaDistributionSetTagManagement implements DistributionSetTagManage
|
||||
|
||||
@Override
|
||||
public Page<DistributionSetTag> findByRsql(final Pageable pageable, final String rsqlParam) {
|
||||
final Specification<JpaDistributionSetTag> spec = RSQLUtility.parse(rsqlParam, TagFields.class,
|
||||
final Specification<JpaDistributionSetTag> spec = RSQLUtility.buildRsqlSpecification(rsqlParam, TagFields.class,
|
||||
virtualPropertyReplacer, database);
|
||||
|
||||
return convertDsPage(distributionSetTagRepository.findAll(spec, pageable), pageable);
|
||||
|
||||
@@ -227,7 +227,7 @@ public class JpaDistributionSetTypeManagement implements DistributionSetTypeMana
|
||||
public Page<DistributionSetType> 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);
|
||||
}
|
||||
|
||||
@@ -119,7 +119,7 @@ public class JpaRolloutGroupManagement implements RolloutGroupManagement {
|
||||
final String rsqlParam) {
|
||||
throwEntityNotFoundExceptionIfRolloutDoesNotExist(rolloutId);
|
||||
|
||||
final Specification<JpaRolloutGroup> specification = RSQLUtility.parse(rsqlParam, RolloutGroupFields.class,
|
||||
final Specification<JpaRolloutGroup> 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<JpaTarget> rsqlSpecification = RSQLUtility.parse(rsqlParam, TargetFields.class,
|
||||
final Specification<JpaTarget> rsqlSpecification = RSQLUtility.buildRsqlSpecification(rsqlParam, TargetFields.class,
|
||||
virtualPropertyReplacer, database);
|
||||
|
||||
return convertTPage(targetRepository.findAll((root, query, criteriaBuilder) -> {
|
||||
|
||||
@@ -151,7 +151,7 @@ public class JpaRolloutManagement extends AbstractRolloutManagement {
|
||||
@Override
|
||||
public Page<Rollout> findByRsql(final Pageable pageable, final String rsqlParam, final boolean deleted) {
|
||||
final List<Specification<JpaRollout>> 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);
|
||||
|
||||
@@ -300,7 +300,7 @@ public class JpaSoftwareModuleManagement implements SoftwareModuleManagement {
|
||||
|
||||
@Override
|
||||
public Page<SoftwareModule> findByRsql(final Pageable pageable, final String rsqlParam) {
|
||||
final Specification<JpaSoftwareModule> spec = RSQLUtility.parse(rsqlParam, SoftwareModuleFields.class,
|
||||
final Specification<JpaSoftwareModule> 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<JpaSoftwareModuleMetadata> spec = RSQLUtility.parse(rsqlParam,
|
||||
final Specification<JpaSoftwareModuleMetadata> spec = RSQLUtility.buildRsqlSpecification(rsqlParam,
|
||||
SoftwareModuleMetadataFields.class, virtualPropertyReplacer, database);
|
||||
return convertSmMdPage(
|
||||
softwareModuleMetadataRepository
|
||||
|
||||
@@ -90,7 +90,7 @@ public class JpaSoftwareModuleTypeManagement implements SoftwareModuleTypeManage
|
||||
@Override
|
||||
public Page<SoftwareModuleType> findByRsql(final Pageable pageable, final String rsqlParam) {
|
||||
|
||||
final Specification<JpaSoftwareModuleType> spec = RSQLUtility.parse(rsqlParam, SoftwareModuleTypeFields.class,
|
||||
final Specification<JpaSoftwareModuleType> spec = RSQLUtility.buildRsqlSpecification(rsqlParam, SoftwareModuleTypeFields.class,
|
||||
virtualPropertyReplacer, database);
|
||||
|
||||
return convertPage(softwareModuleTypeRepository.findAll(spec, pageable), pageable);
|
||||
|
||||
@@ -166,7 +166,7 @@ public class JpaTargetFilterQueryManagement implements TargetFilterQueryManageme
|
||||
List<Specification<JpaTargetFilterQuery>> 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);
|
||||
}
|
||||
|
||||
@@ -282,7 +282,7 @@ public class JpaTargetManagement implements TargetManagement {
|
||||
|
||||
final Long targetId = getByControllerIdAndThrowIfNotFound(controllerId).getId();
|
||||
|
||||
final Specification<JpaTargetMetadata> spec = RSQLUtility.parse(rsqlParam, TargetMetadataFields.class,
|
||||
final Specification<JpaTargetMetadata> spec = RSQLUtility.buildRsqlSpecification(rsqlParam, TargetMetadataFields.class,
|
||||
virtualPropertyReplacer, database);
|
||||
|
||||
return convertMdPage(targetMetadataRepository.findAll((Specification<JpaTargetMetadata>) (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<Target> 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<Target> findTargetsBySpec(final Specification<JpaTarget> spec, final Pageable pageable) {
|
||||
@@ -384,7 +384,7 @@ public class JpaTargetManagement implements TargetManagement {
|
||||
final String rsqlParam) {
|
||||
throwEntityNotFoundIfDsDoesNotExist(distributionSetID);
|
||||
|
||||
final Specification<JpaTarget> spec = RSQLUtility.parse(rsqlParam, TargetFields.class, virtualPropertyReplacer,
|
||||
final Specification<JpaTarget> 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<JpaTarget> spec = RSQLUtility.parse(rsqlParam, TargetFields.class, virtualPropertyReplacer,
|
||||
final Specification<JpaTarget> 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<JpaTarget> spec = RSQLUtility.parse(targetFilterQuery, TargetFields.class,
|
||||
final Specification<JpaTarget> spec = RSQLUtility.buildRsqlSpecification(targetFilterQuery, TargetFields.class,
|
||||
virtualPropertyReplacer, database);
|
||||
|
||||
return findTargetsBySpec(
|
||||
@@ -673,7 +673,7 @@ public class JpaTargetManagement implements TargetManagement {
|
||||
public Page<Target> findByTargetFilterQueryAndNotInRolloutGroups(final Pageable pageRequest,
|
||||
final Collection<Long> groups, final String targetFilterQuery) {
|
||||
|
||||
final Specification<JpaTarget> spec = RSQLUtility.parse(targetFilterQuery, TargetFields.class,
|
||||
final Specification<JpaTarget> 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<Long> groups, final String targetFilterQuery) {
|
||||
final Specification<JpaTarget> spec = RSQLUtility.parse(targetFilterQuery, TargetFields.class,
|
||||
final Specification<JpaTarget> spec = RSQLUtility.buildRsqlSpecification(targetFilterQuery, TargetFields.class,
|
||||
virtualPropertyReplacer, database);
|
||||
final List<Specification<JpaTarget>> 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<JpaTarget> spec = RSQLUtility.parse(targetFilterQuery, TargetFields.class,
|
||||
final Specification<JpaTarget> spec = RSQLUtility.buildRsqlSpecification(targetFilterQuery, TargetFields.class,
|
||||
virtualPropertyReplacer, database);
|
||||
final List<Specification<JpaTarget>> specList = Lists.newArrayListWithExpectedSize(2);
|
||||
specList.add(spec);
|
||||
@@ -750,7 +750,7 @@ public class JpaTargetManagement implements TargetManagement {
|
||||
|
||||
throwEntityNotFoundExceptionIfTagDoesNotExist(tagId);
|
||||
|
||||
final Specification<JpaTarget> spec = RSQLUtility.parse(rsqlParam, TargetFields.class, virtualPropertyReplacer,
|
||||
final Specification<JpaTarget> spec = RSQLUtility.buildRsqlSpecification(rsqlParam, TargetFields.class, virtualPropertyReplacer,
|
||||
database);
|
||||
|
||||
return convertPage(targetRepository.findAll((Specification<JpaTarget>) (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<JpaTarget> specs = RSQLUtility.parse(targetFilterQuery.getQuery(), TargetFields.class,
|
||||
final Specification<JpaTarget> specs = RSQLUtility.buildRsqlSpecification(targetFilterQuery.getQuery(), TargetFields.class,
|
||||
virtualPropertyReplacer, database);
|
||||
return targetRepository.count(specs);
|
||||
}
|
||||
|
||||
@Override
|
||||
public long countByRsql(final String targetFilterQuery) {
|
||||
final Specification<JpaTarget> specs = RSQLUtility.parse(targetFilterQuery, TargetFields.class,
|
||||
final Specification<JpaTarget> specs = RSQLUtility.buildRsqlSpecification(targetFilterQuery, TargetFields.class,
|
||||
virtualPropertyReplacer, database);
|
||||
return targetRepository.count((root, query, cb) -> {
|
||||
query.distinct(true);
|
||||
|
||||
@@ -106,7 +106,7 @@ public class JpaTargetTagManagement implements TargetTagManagement {
|
||||
@Override
|
||||
public Page<TargetTag> findByRsql(final Pageable pageable, final String rsqlParam) {
|
||||
|
||||
final Specification<JpaTargetTag> spec = RSQLUtility.parse(rsqlParam, TagFields.class, virtualPropertyReplacer,
|
||||
final Specification<JpaTargetTag> spec = RSQLUtility.buildRsqlSpecification(rsqlParam, TagFields.class, virtualPropertyReplacer,
|
||||
database);
|
||||
return convertTPage(targetTagRepository.findAll(spec, pageable), pageable);
|
||||
}
|
||||
|
||||
@@ -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> transactionManagerCustomizers) {
|
||||
public PlatformTransactionManager transactionManager(
|
||||
ObjectProvider<TransactionManagerCustomizers> 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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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 <A extends Enum<A> & FieldNameProvider> RSQLVisitor<Void, String> validationRsqlVisitor(
|
||||
final Class<A> fieldNameProvider) {
|
||||
return new FieldValidationRsqlVisitor<>(fieldNameProvider);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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 <A>
|
||||
* The type the {@link FieldNameProvider} refers to.
|
||||
*/
|
||||
public class FieldValidationRsqlVisitor<A extends Enum<A> & FieldNameProvider> extends AbstractFieldNameRSQLVisitor<A>
|
||||
implements RSQLVisitor<Void, String> {
|
||||
|
||||
/**
|
||||
* Constructs the visitor and initializes it.
|
||||
*
|
||||
* @param fieldNameProvider
|
||||
* The {@link FieldNameProvider} to use for validation.
|
||||
*/
|
||||
public FieldValidationRsqlVisitor(final Class<A> fieldNameProvider) {
|
||||
super(fieldNameProvider);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Void visit(final AndNode node, final String param) {
|
||||
return visitNode(node, param);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Void visit(final OrNode node, final String param) {
|
||||
return visitNode(node, param);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Void visit(final ComparisonNode node, final String param) {
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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 <A>
|
||||
* the enum for providing the field name of the entity field to
|
||||
* filter on.
|
||||
* @param <T>
|
||||
* the entity type referenced by the root
|
||||
*/
|
||||
public class JpaQueryRsqlVisitor<A extends Enum<A> & FieldNameProvider, T> extends AbstractFieldNameRSQLVisitor<A>
|
||||
implements RSQLVisitor<List<Predicate>, 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<String> NO_JOINS_OPERATOR = Lists.newArrayList("!=", "=out=");
|
||||
|
||||
private final Map<Integer, Set<Join<Object, Object>>> joinsInLevel = new HashMap<>(3);
|
||||
|
||||
private final CriteriaBuilder cb;
|
||||
private final CriteriaQuery<?> query;
|
||||
private final Database database;
|
||||
private final Root<T> root;
|
||||
private final SimpleTypeConverter simpleTypeConverter;
|
||||
private final VirtualPropertyReplacer virtualPropertyReplacer;
|
||||
|
||||
private int level;
|
||||
private boolean isOrLevel;
|
||||
private boolean joinsNeeded;
|
||||
|
||||
public JpaQueryRsqlVisitor(final Root<T> root, final CriteriaBuilder cb, final Class<A> 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<Join<Object, Object>> getCurrentJoins() {
|
||||
if (level > 0) {
|
||||
return joinsInLevel.get(level);
|
||||
}
|
||||
return Collections.emptySet();
|
||||
}
|
||||
|
||||
private Optional<Join<Object, Object>> findCurrentJoinOfType(final Class<?> type) {
|
||||
return getCurrentJoins().stream().filter(j -> type.equals(j.getJavaType())).findAny();
|
||||
}
|
||||
|
||||
private void addCurrentJoin(final Join<Object, Object> join) {
|
||||
if (level > 0) {
|
||||
getCurrentJoins().add(join);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Predicate> visit(final AndNode node, final String param) {
|
||||
beginLevel(false);
|
||||
final List<Predicate> childs = acceptChilds(node);
|
||||
endLevel();
|
||||
if (!childs.isEmpty()) {
|
||||
return toSingleList(cb.and(childs.toArray(new Predicate[childs.size()])));
|
||||
}
|
||||
return toSingleList(cb.conjunction());
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Predicate> visit(final OrNode node, final String param) {
|
||||
beginLevel(true);
|
||||
final List<Predicate> childs = acceptChilds(node);
|
||||
endLevel();
|
||||
if (!childs.isEmpty()) {
|
||||
return toSingleList(cb.or(childs.toArray(new Predicate[childs.size()])));
|
||||
}
|
||||
return toSingleList(cb.conjunction());
|
||||
}
|
||||
|
||||
private static List<Predicate> 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<Object> getFieldPath(final A enumField, final String finalProperty) {
|
||||
return (Path<Object>) 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<Object, ?> join = (Join<Object, ?>) fieldPath;
|
||||
final From<?, Object> joinParent = join.getParent();
|
||||
final Optional<Join<Object, Object>> 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<Object, Object> newJoin = joinParent.join(fieldNameSplit, JoinType.LEFT);
|
||||
addCurrentJoin(newJoin);
|
||||
return newJoin;
|
||||
}
|
||||
}
|
||||
return fieldPath;
|
||||
}
|
||||
|
||||
private static Optional<Path<?>> getFieldPath(final Root<?> root, final String[] split, final boolean isMapKeyField,
|
||||
final BiFunction<Path<?>, 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<Predicate> visit(final ComparisonNode node, final String param) {
|
||||
final A fieldName = getFieldEnumByName(node);
|
||||
final String finalProperty = getAndValidatePropertyFieldName(fieldName, node);
|
||||
|
||||
final List<String> values = node.getArguments();
|
||||
final List<Object> transformedValues = new ArrayList<>();
|
||||
final Path<Object> 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<Object> 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<? extends Enum> tmpEnumType = (Class<? extends Enum>) javaType;
|
||||
try {
|
||||
return Enum.valueOf(tmpEnumType, value.toUpperCase());
|
||||
} catch (final IllegalArgumentException e) {
|
||||
// we could not transform the given string value into the enum
|
||||
// type, so ignore it and return null and do not filter
|
||||
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<Predicate> mapToPredicate(final ComparisonNode node, final Path<Object> fieldPath,
|
||||
final List<String> values, final List<Object> 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<Object> fieldPath,
|
||||
final List<Object> 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<Object> transformedValues, final Path<Object> fieldPath) {
|
||||
final List<String> 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<Object> transformedValues, final String finalProperty,
|
||||
final A enumField, final Path<Object> fieldPath) {
|
||||
|
||||
final String[] fieldNames = getSubAttributesFrom(finalProperty);
|
||||
final List<String> 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<Object> fieldPath, final List<Object> transformedValues,
|
||||
final List<String> outParams) {
|
||||
|
||||
final Path<String> 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<Object> transformedValues,
|
||||
final A enumField, final List<String> outParams) {
|
||||
final Function<Expression<String>, Predicate> inPredicateProvider = expressionToCompare -> outParams.isEmpty()
|
||||
? cb.upper(expressionToCompare).in(transformedValues)
|
||||
: cb.upper(expressionToCompare).in(outParams);
|
||||
return toNotExistsSubQueryPredicate(fieldNames, enumField, inPredicateProvider);
|
||||
}
|
||||
|
||||
private Path<Object> getMapValueFieldPath(final A enumField, final Path<Object> 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<Object> 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<String>) (((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<Object> 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<Object> 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<Object> fieldPath) {
|
||||
return cb.isNotNull(pathOfString(fieldPath));
|
||||
}
|
||||
|
||||
private Predicate toNullOrNotLikePredicate(final Path<Object> 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<Object> fieldPath, final Object transformedValue) {
|
||||
return cb.or(cb.isNull(pathOfString(fieldPath)), cb.notEqual(fieldPath, transformedValue));
|
||||
}
|
||||
|
||||
private Predicate toNotNullAndNotEmptyPredicate(final Path<Object> 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<Expression<String>, 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<Expression<String>, 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<String> 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<String> 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<String>) (((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 <Y> Path<Y> pathOfString(final Path<?> path) {
|
||||
return (Path<Y>) path;
|
||||
}
|
||||
|
||||
private List<Predicate> acceptChilds(final LogicalNode node) {
|
||||
final List<Node> children = node.getChildren();
|
||||
final List<Predicate> childs = new ArrayList<>();
|
||||
for (final Node node2 : children) {
|
||||
final List<Predicate> 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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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 <A extends Enum<A> & FieldNameProvider, T> Specification<T> parse(final String rsql,
|
||||
public static <A extends Enum<A> & FieldNameProvider, T> Specification<T> buildRsqlSpecification(final String rsql,
|
||||
final Class<A> fieldNameProvider, final VirtualPropertyReplacer virtualPropertyReplacer,
|
||||
final Database database) {
|
||||
return new RSQLSpecification<>(rsql, fieldNameProvider, virtualPropertyReplacer, database);
|
||||
@@ -153,11 +125,16 @@ public final class RSQLUtility {
|
||||
*/
|
||||
public static <A extends Enum<A> & FieldNameProvider> void validateRsqlFor(final String rsql,
|
||||
final Class<A> fieldNameProvider) {
|
||||
final RSQLVisitor<Void, String> visitor = new ValidationRSQLVisitor<>(fieldNameProvider);
|
||||
final RSQLVisitor<Void, String> visitor = getValidationRsqlVisitor(fieldNameProvider);
|
||||
final Node rootNode = parseRsql(rsql);
|
||||
rootNode.accept(visitor);
|
||||
}
|
||||
|
||||
private static <A extends Enum<A> & FieldNameProvider> RSQLVisitor<Void, String> getValidationRsqlVisitor(
|
||||
final Class<A> 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<A extends Enum<A> & FieldNameProvider>
|
||||
extends AbstractFieldNameRSQLVisitor<A> implements RSQLVisitor<Void, String> {
|
||||
|
||||
public ValidationRSQLVisitor(final Class<A> fieldNameProvider) {
|
||||
super(fieldNameProvider);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Void visit(final AndNode node, final String param) {
|
||||
return visitNode(node, param);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Void visit(final OrNode node, final String param) {
|
||||
return visitNode(node, param);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Void visit(final ComparisonNode node, final String param) {
|
||||
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<A extends Enum<A> & FieldNameProvider, T> implements Specification<T> {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
@@ -222,7 +169,7 @@ public final class RSQLUtility {
|
||||
final Node rootNode = parseRsql(rsql);
|
||||
query.distinct(true);
|
||||
|
||||
final JpaQueryRSQLVisitor<A, T> jpqQueryRSQLVisitor = new JpaQueryRSQLVisitor<>(root, cb, enumType,
|
||||
final JpaQueryRsqlVisitor<A, T> jpqQueryRSQLVisitor = new JpaQueryRsqlVisitor<>(root, cb, enumType,
|
||||
virtualPropertyReplacer, database, query);
|
||||
final List<Predicate> accept = rootNode.<List<Predicate>, 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 <A>
|
||||
* the enum for providing the field name of the entity field to
|
||||
* filter on.
|
||||
* @param <T>
|
||||
* the entity type referenced by the root
|
||||
*/
|
||||
private static final class JpaQueryRSQLVisitor<A extends Enum<A> & FieldNameProvider, T>
|
||||
extends AbstractFieldNameRSQLVisitor<A> implements RSQLVisitor<List<Predicate>, 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<String> NO_JOINS_OPERATOR = Lists.newArrayList("!=", "=out=");
|
||||
|
||||
private final Map<Integer, Set<Join<Object, Object>>> joinsInLevel = new HashMap<>(3);
|
||||
|
||||
private final CriteriaBuilder cb;
|
||||
private final CriteriaQuery<?> query;
|
||||
private final Database database;
|
||||
private final Root<T> root;
|
||||
private final SimpleTypeConverter simpleTypeConverter;
|
||||
private final VirtualPropertyReplacer virtualPropertyReplacer;
|
||||
|
||||
private int level;
|
||||
private boolean isOrLevel;
|
||||
private boolean joinsNeeded;
|
||||
|
||||
private JpaQueryRSQLVisitor(final Root<T> root, final CriteriaBuilder cb, final Class<A> 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<Join<Object, Object>> getCurrentJoins() {
|
||||
if (level > 0) {
|
||||
return joinsInLevel.get(level);
|
||||
}
|
||||
return Collections.emptySet();
|
||||
}
|
||||
|
||||
private Optional<Join<Object, Object>> findCurrentJoinOfType(final Class<?> type) {
|
||||
return getCurrentJoins().stream().filter(j -> type.equals(j.getJavaType())).findAny();
|
||||
}
|
||||
|
||||
private void addCurrentJoin(final Join<Object, Object> join) {
|
||||
if (level > 0) {
|
||||
getCurrentJoins().add(join);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Predicate> visit(final AndNode node, final String param) {
|
||||
beginLevel(false);
|
||||
final List<Predicate> childs = acceptChilds(node);
|
||||
endLevel();
|
||||
if (!childs.isEmpty()) {
|
||||
return toSingleList(cb.and(childs.toArray(new Predicate[childs.size()])));
|
||||
}
|
||||
return toSingleList(cb.conjunction());
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Predicate> visit(final OrNode node, final String param) {
|
||||
beginLevel(true);
|
||||
final List<Predicate> childs = acceptChilds(node);
|
||||
endLevel();
|
||||
if (!childs.isEmpty()) {
|
||||
return toSingleList(cb.or(childs.toArray(new Predicate[childs.size()])));
|
||||
}
|
||||
return toSingleList(cb.conjunction());
|
||||
}
|
||||
|
||||
private static List<Predicate> 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<Object> getFieldPath(final A enumField, final String finalProperty) {
|
||||
return (Path<Object>) 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<Object, ?> join = (Join<Object, ?>) fieldPath;
|
||||
final From<?, Object> joinParent = join.getParent();
|
||||
final Optional<Join<Object, Object>> 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<Object, Object> newJoin = joinParent.join(fieldNameSplit, JoinType.LEFT);
|
||||
addCurrentJoin(newJoin);
|
||||
return newJoin;
|
||||
}
|
||||
}
|
||||
return fieldPath;
|
||||
}
|
||||
|
||||
private static Optional<Path<?>> getFieldPath(final Root<?> root, final String[] split,
|
||||
final boolean isMapKeyField, final BiFunction<Path<?>, 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<Predicate> visit(final ComparisonNode node, final String param) {
|
||||
final A fieldName = getFieldEnumByName(node);
|
||||
final String finalProperty = getAndValidatePropertyFieldName(fieldName, node);
|
||||
|
||||
final List<String> values = node.getArguments();
|
||||
final List<Object> transformedValues = new ArrayList<>();
|
||||
final Path<Object> 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<Object> 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<? extends Enum> tmpEnumType = (Class<? extends Enum>) javaType;
|
||||
try {
|
||||
return Enum.valueOf(tmpEnumType, value.toUpperCase());
|
||||
} catch (final IllegalArgumentException e) {
|
||||
// we could not transform the given string value into the enum
|
||||
// type, so ignore it and return null and do not filter
|
||||
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<Predicate> mapToPredicate(final ComparisonNode node, final Path<Object> fieldPath,
|
||||
final List<String> values, final List<Object> 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<Object> fieldPath,
|
||||
final List<Object> 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<Object> transformedValues, final Path<Object> fieldPath) {
|
||||
final List<String> 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<Object> transformedValues, final String finalProperty,
|
||||
final A enumField, final Path<Object> fieldPath) {
|
||||
|
||||
final String[] fieldNames = getSubAttributesFrom(finalProperty);
|
||||
final List<String> 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<Object> fieldPath, final List<Object> transformedValues,
|
||||
final List<String> outParams) {
|
||||
|
||||
final Path<String> 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<Object> transformedValues,
|
||||
final A enumField, final List<String> outParams) {
|
||||
final Function<Expression<String>, Predicate> inPredicateProvider = expressionToCompare -> outParams
|
||||
.isEmpty() ? cb.upper(expressionToCompare).in(transformedValues)
|
||||
: cb.upper(expressionToCompare).in(outParams);
|
||||
return toNotExistsSubQueryPredicate(fieldNames, enumField, inPredicateProvider);
|
||||
}
|
||||
|
||||
private Path<Object> getMapValueFieldPath(final A enumField, final Path<Object> 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<Object> 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<String>) (((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<Object> 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<Object> 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<Object> fieldPath) {
|
||||
return cb.isNotNull(pathOfString(fieldPath));
|
||||
}
|
||||
|
||||
private Predicate toNullOrNotLikePredicate(final Path<Object> 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<Object> fieldPath, final Object transformedValue) {
|
||||
return cb.or(cb.isNull(pathOfString(fieldPath)), cb.notEqual(fieldPath, transformedValue));
|
||||
}
|
||||
|
||||
private Predicate toNotNullAndNotEmptyPredicate(final Path<Object> 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<Expression<String>, 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<Expression<String>, 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<String> 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<String> 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<String>) (((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 <Y> Path<Y> pathOfString(final Path<?> path) {
|
||||
return (Path<Y>) path;
|
||||
}
|
||||
|
||||
private List<Predicate> acceptChilds(final LogicalNode node) {
|
||||
final List<Node> children = node.getChildren();
|
||||
final List<Predicate> childs = new ArrayList<>();
|
||||
for (final Node node2 : children) {
|
||||
final List<Predicate> 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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user