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:
Stefan Behl
2021-07-22 09:41:06 +02:00
committed by GitHub
parent c37c615ea6
commit 1a285849e6
20 changed files with 866 additions and 666 deletions

View File

@@ -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));

View File

@@ -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(

View File

@@ -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);

View File

@@ -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);
}

View File

@@ -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) -> {

View File

@@ -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);

View File

@@ -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

View File

@@ -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);

View File

@@ -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);
}

View File

@@ -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);

View File

@@ -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);
}

View File

@@ -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();
}
}

View File

@@ -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);
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}
}