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:
@@ -67,6 +67,10 @@
|
|||||||
<groupId>com.cronutils</groupId>
|
<groupId>com.cronutils</groupId>
|
||||||
<artifactId>cron-utils</artifactId>
|
<artifactId>cron-utils</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>cz.jirutka.rsql</groupId>
|
||||||
|
<artifactId>rsql-parser</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
<!-- TEST -->
|
<!-- TEST -->
|
||||||
<dependency>
|
<dependency>
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2021 Bosch.IO GmbH and others.
|
||||||
|
*
|
||||||
|
* All rights reserved. This program and the accompanying materials
|
||||||
|
* are made available under the terms of the Eclipse Public License v1.0
|
||||||
|
* which accompanies this distribution, and is available at
|
||||||
|
* http://www.eclipse.org/legal/epl-v10.html
|
||||||
|
*/
|
||||||
|
package org.eclipse.hawkbit.repository.rsql;
|
||||||
|
|
||||||
|
import org.eclipse.hawkbit.repository.FieldNameProvider;
|
||||||
|
|
||||||
|
import cz.jirutka.rsql.parser.ast.Node;
|
||||||
|
import cz.jirutka.rsql.parser.ast.RSQLVisitor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Factory to obtain {@link RSQLVisitor} instances that can be used to process
|
||||||
|
* the {@link Node}s representing an RSQL query.
|
||||||
|
*/
|
||||||
|
@FunctionalInterface
|
||||||
|
public interface RsqlVisitorFactory {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides a {@link RSQLVisitor} instance for validating RSQL queries based
|
||||||
|
* on the given {@link FieldNameProvider}.
|
||||||
|
*
|
||||||
|
* @param <A>
|
||||||
|
* The type of the {@link FieldNameProvider}.
|
||||||
|
* @param fieldNameProvider
|
||||||
|
* providing accessing to the relevant field names.
|
||||||
|
*
|
||||||
|
* @return An {@link RSQLVisitor} to validate the {@link Node}s of an RSQL
|
||||||
|
* query.
|
||||||
|
*/
|
||||||
|
<A extends Enum<A> & FieldNameProvider> RSQLVisitor<Void, String> validationRsqlVisitor(Class<A> fieldNameProvider);
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2021 Bosch.IO GmbH and others.
|
||||||
|
*
|
||||||
|
* All rights reserved. This program and the accompanying materials
|
||||||
|
* are made available under the terms of the Eclipse Public License v1.0
|
||||||
|
* which accompanies this distribution, and is available at
|
||||||
|
* http://www.eclipse.org/legal/epl-v10.html
|
||||||
|
*/
|
||||||
|
package org.eclipse.hawkbit.repository.rsql;
|
||||||
|
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper class providing static access to the managed
|
||||||
|
* {@link RsqlVisitorFactory} bean.
|
||||||
|
*/
|
||||||
|
public final class RsqlVisitorFactoryHolder {
|
||||||
|
|
||||||
|
private static final RsqlVisitorFactoryHolder SINGLETON = new RsqlVisitorFactoryHolder();
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private RsqlVisitorFactory rsqlVisitorFactory;
|
||||||
|
|
||||||
|
private RsqlVisitorFactoryHolder() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return The holder singleton instance.
|
||||||
|
*/
|
||||||
|
public static RsqlVisitorFactoryHolder getInstance() {
|
||||||
|
return SINGLETON;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return The managed RsqlVisitorFactory bean
|
||||||
|
*/
|
||||||
|
public RsqlVisitorFactory getRsqlVisitorFactory() {
|
||||||
|
return rsqlVisitorFactory;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -655,7 +655,7 @@ public class JpaDeploymentManagement extends JpaActionManagement implements Depl
|
|||||||
}
|
}
|
||||||
|
|
||||||
private Specification<JpaAction> createSpecificationFor(final String controllerId, final String rsqlParam) {
|
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);
|
database);
|
||||||
return (root, query, cb) -> cb.and(spec.toPredicate(root, query, cb),
|
return (root, query, cb) -> cb.and(spec.toPredicate(root, query, cb),
|
||||||
cb.equal(root.get(JpaAction_.target).get(JpaTarget_.controllerId), controllerId));
|
cb.equal(root.get(JpaAction_.target).get(JpaTarget_.controllerId), controllerId));
|
||||||
|
|||||||
@@ -555,7 +555,7 @@ public class JpaDistributionSetManagement implements DistributionSetManagement {
|
|||||||
|
|
||||||
throwExceptionIfDistributionSetDoesNotExist(distributionSetId);
|
throwExceptionIfDistributionSetDoesNotExist(distributionSetId);
|
||||||
|
|
||||||
final Specification<JpaDistributionSetMetadata> spec = RSQLUtility.parse(rsqlParam,
|
final Specification<JpaDistributionSetMetadata> spec = RSQLUtility.buildRsqlSpecification(rsqlParam,
|
||||||
DistributionSetMetadataFields.class, virtualPropertyReplacer, database);
|
DistributionSetMetadataFields.class, virtualPropertyReplacer, database);
|
||||||
|
|
||||||
return convertMdPage(
|
return convertMdPage(
|
||||||
@@ -784,7 +784,7 @@ public class JpaDistributionSetManagement implements DistributionSetManagement {
|
|||||||
public Page<DistributionSet> findByRsqlAndTag(final Pageable pageable, final String rsqlParam, final long tagId) {
|
public Page<DistributionSet> findByRsqlAndTag(final Pageable pageable, final String rsqlParam, final long tagId) {
|
||||||
throwEntityNotFoundExceptionIfDsTagDoesNotExist(tagId);
|
throwEntityNotFoundExceptionIfDsTagDoesNotExist(tagId);
|
||||||
|
|
||||||
final Specification<JpaDistributionSet> spec = RSQLUtility.parse(rsqlParam, DistributionSetFields.class,
|
final Specification<JpaDistributionSet> spec = RSQLUtility.buildRsqlSpecification(rsqlParam, DistributionSetFields.class,
|
||||||
virtualPropertyReplacer, database);
|
virtualPropertyReplacer, database);
|
||||||
|
|
||||||
return convertDsPage(findByCriteriaAPI(pageable, Arrays.asList(spec, DistributionSetSpecification.hasTag(tagId),
|
return convertDsPage(findByCriteriaAPI(pageable, Arrays.asList(spec, DistributionSetSpecification.hasTag(tagId),
|
||||||
@@ -799,7 +799,7 @@ public class JpaDistributionSetManagement implements DistributionSetManagement {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Page<DistributionSet> findByRsql(final Pageable pageable, final String rsqlParam) {
|
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);
|
virtualPropertyReplacer, database);
|
||||||
|
|
||||||
return convertDsPage(
|
return convertDsPage(
|
||||||
|
|||||||
@@ -134,7 +134,7 @@ public class JpaDistributionSetTagManagement implements DistributionSetTagManage
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Page<DistributionSetTag> findByRsql(final Pageable pageable, final String rsqlParam) {
|
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);
|
virtualPropertyReplacer, database);
|
||||||
|
|
||||||
return convertDsPage(distributionSetTagRepository.findAll(spec, pageable), pageable);
|
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) {
|
public Page<DistributionSetType> findByRsql(final Pageable pageable, final String rsqlParam) {
|
||||||
return convertPage(
|
return convertPage(
|
||||||
findByCriteriaAPI(pageable,
|
findByCriteriaAPI(pageable,
|
||||||
Arrays.asList(RSQLUtility.parse(rsqlParam, DistributionSetTypeFields.class,
|
Arrays.asList(RSQLUtility.buildRsqlSpecification(rsqlParam, DistributionSetTypeFields.class,
|
||||||
virtualPropertyReplacer, database), DistributionSetTypeSpecification.isDeleted(false))),
|
virtualPropertyReplacer, database), DistributionSetTypeSpecification.isDeleted(false))),
|
||||||
pageable);
|
pageable);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -119,7 +119,7 @@ public class JpaRolloutGroupManagement implements RolloutGroupManagement {
|
|||||||
final String rsqlParam) {
|
final String rsqlParam) {
|
||||||
throwEntityNotFoundExceptionIfRolloutDoesNotExist(rolloutId);
|
throwEntityNotFoundExceptionIfRolloutDoesNotExist(rolloutId);
|
||||||
|
|
||||||
final Specification<JpaRolloutGroup> specification = RSQLUtility.parse(rsqlParam, RolloutGroupFields.class,
|
final Specification<JpaRolloutGroup> specification = RSQLUtility.buildRsqlSpecification(rsqlParam, RolloutGroupFields.class,
|
||||||
virtualPropertyReplacer, database);
|
virtualPropertyReplacer, database);
|
||||||
|
|
||||||
return convertPage(rolloutGroupRepository.findAll((root, query, criteriaBuilder) -> criteriaBuilder.and(
|
return convertPage(rolloutGroupRepository.findAll((root, query, criteriaBuilder) -> criteriaBuilder.and(
|
||||||
@@ -211,7 +211,7 @@ public class JpaRolloutGroupManagement implements RolloutGroupManagement {
|
|||||||
|
|
||||||
throwExceptionIfRolloutGroupDoesNotExist(rolloutGroupId);
|
throwExceptionIfRolloutGroupDoesNotExist(rolloutGroupId);
|
||||||
|
|
||||||
final Specification<JpaTarget> rsqlSpecification = RSQLUtility.parse(rsqlParam, TargetFields.class,
|
final Specification<JpaTarget> rsqlSpecification = RSQLUtility.buildRsqlSpecification(rsqlParam, TargetFields.class,
|
||||||
virtualPropertyReplacer, database);
|
virtualPropertyReplacer, database);
|
||||||
|
|
||||||
return convertTPage(targetRepository.findAll((root, query, criteriaBuilder) -> {
|
return convertTPage(targetRepository.findAll((root, query, criteriaBuilder) -> {
|
||||||
|
|||||||
@@ -151,7 +151,7 @@ public class JpaRolloutManagement extends AbstractRolloutManagement {
|
|||||||
@Override
|
@Override
|
||||||
public Page<Rollout> findByRsql(final Pageable pageable, final String rsqlParam, final boolean deleted) {
|
public Page<Rollout> findByRsql(final Pageable pageable, final String rsqlParam, final boolean deleted) {
|
||||||
final List<Specification<JpaRollout>> specList = Lists.newArrayListWithExpectedSize(2);
|
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));
|
specList.add(RolloutSpecification.isDeletedWithDistributionSet(deleted));
|
||||||
|
|
||||||
return JpaRolloutHelper.convertPage(findByCriteriaAPI(pageable, specList), pageable);
|
return JpaRolloutHelper.convertPage(findByCriteriaAPI(pageable, specList), pageable);
|
||||||
|
|||||||
@@ -300,7 +300,7 @@ public class JpaSoftwareModuleManagement implements SoftwareModuleManagement {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Page<SoftwareModule> findByRsql(final Pageable pageable, final String rsqlParam) {
|
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);
|
virtualPropertyReplacer, database);
|
||||||
|
|
||||||
return convertSmPage(softwareModuleRepository.findAll(spec, pageable), pageable);
|
return convertSmPage(softwareModuleRepository.findAll(spec, pageable), pageable);
|
||||||
@@ -608,7 +608,7 @@ public class JpaSoftwareModuleManagement implements SoftwareModuleManagement {
|
|||||||
|
|
||||||
throwExceptionIfSoftwareModuleDoesNotExist(softwareModuleId);
|
throwExceptionIfSoftwareModuleDoesNotExist(softwareModuleId);
|
||||||
|
|
||||||
final Specification<JpaSoftwareModuleMetadata> spec = RSQLUtility.parse(rsqlParam,
|
final Specification<JpaSoftwareModuleMetadata> spec = RSQLUtility.buildRsqlSpecification(rsqlParam,
|
||||||
SoftwareModuleMetadataFields.class, virtualPropertyReplacer, database);
|
SoftwareModuleMetadataFields.class, virtualPropertyReplacer, database);
|
||||||
return convertSmMdPage(
|
return convertSmMdPage(
|
||||||
softwareModuleMetadataRepository
|
softwareModuleMetadataRepository
|
||||||
|
|||||||
@@ -90,7 +90,7 @@ public class JpaSoftwareModuleTypeManagement implements SoftwareModuleTypeManage
|
|||||||
@Override
|
@Override
|
||||||
public Page<SoftwareModuleType> findByRsql(final Pageable pageable, final String rsqlParam) {
|
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);
|
virtualPropertyReplacer, database);
|
||||||
|
|
||||||
return convertPage(softwareModuleTypeRepository.findAll(spec, pageable), pageable);
|
return convertPage(softwareModuleTypeRepository.findAll(spec, pageable), pageable);
|
||||||
|
|||||||
@@ -166,7 +166,7 @@ public class JpaTargetFilterQueryManagement implements TargetFilterQueryManageme
|
|||||||
List<Specification<JpaTargetFilterQuery>> specList = Collections.emptyList();
|
List<Specification<JpaTargetFilterQuery>> specList = Collections.emptyList();
|
||||||
if (!StringUtils.isEmpty(rsqlFilter)) {
|
if (!StringUtils.isEmpty(rsqlFilter)) {
|
||||||
specList = Collections.singletonList(
|
specList = Collections.singletonList(
|
||||||
RSQLUtility.parse(rsqlFilter, TargetFilterQueryFields.class, virtualPropertyReplacer, database));
|
RSQLUtility.buildRsqlSpecification(rsqlFilter, TargetFilterQueryFields.class, virtualPropertyReplacer, database));
|
||||||
}
|
}
|
||||||
return convertPage(findTargetFilterQueryByCriteriaAPI(pageable, specList), pageable);
|
return convertPage(findTargetFilterQueryByCriteriaAPI(pageable, specList), pageable);
|
||||||
}
|
}
|
||||||
@@ -191,7 +191,7 @@ public class JpaTargetFilterQueryManagement implements TargetFilterQueryManageme
|
|||||||
|
|
||||||
if (!StringUtils.isEmpty(rsqlFilter)) {
|
if (!StringUtils.isEmpty(rsqlFilter)) {
|
||||||
specList.add(
|
specList.add(
|
||||||
RSQLUtility.parse(rsqlFilter, TargetFilterQueryFields.class, virtualPropertyReplacer, database));
|
RSQLUtility.buildRsqlSpecification(rsqlFilter, TargetFilterQueryFields.class, virtualPropertyReplacer, database));
|
||||||
}
|
}
|
||||||
return convertPage(findTargetFilterQueryByCriteriaAPI(pageable, specList), pageable);
|
return convertPage(findTargetFilterQueryByCriteriaAPI(pageable, specList), pageable);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -282,7 +282,7 @@ public class JpaTargetManagement implements TargetManagement {
|
|||||||
|
|
||||||
final Long targetId = getByControllerIdAndThrowIfNotFound(controllerId).getId();
|
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);
|
virtualPropertyReplacer, database);
|
||||||
|
|
||||||
return convertMdPage(targetMetadataRepository.findAll((Specification<JpaTargetMetadata>) (root, query, cb) -> cb
|
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));
|
.orElseThrow(() -> new EntityNotFoundException(TargetFilterQuery.class, targetFilterQueryId));
|
||||||
|
|
||||||
return findTargetsBySpec(
|
return findTargetsBySpec(
|
||||||
RSQLUtility.parse(targetFilterQuery.getQuery(), TargetFields.class, virtualPropertyReplacer, database),
|
RSQLUtility.buildRsqlSpecification(targetFilterQuery.getQuery(), TargetFields.class, virtualPropertyReplacer, database),
|
||||||
pageable);
|
pageable);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Page<Target> findByRsql(final Pageable pageable, final String targetFilterQuery) {
|
public Page<Target> findByRsql(final Pageable pageable, final String targetFilterQuery) {
|
||||||
return findTargetsBySpec(
|
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) {
|
private Page<Target> findTargetsBySpec(final Specification<JpaTarget> spec, final Pageable pageable) {
|
||||||
@@ -384,7 +384,7 @@ public class JpaTargetManagement implements TargetManagement {
|
|||||||
final String rsqlParam) {
|
final String rsqlParam) {
|
||||||
throwEntityNotFoundIfDsDoesNotExist(distributionSetID);
|
throwEntityNotFoundIfDsDoesNotExist(distributionSetID);
|
||||||
|
|
||||||
final Specification<JpaTarget> spec = RSQLUtility.parse(rsqlParam, TargetFields.class, virtualPropertyReplacer,
|
final Specification<JpaTarget> spec = RSQLUtility.buildRsqlSpecification(rsqlParam, TargetFields.class, virtualPropertyReplacer,
|
||||||
database);
|
database);
|
||||||
|
|
||||||
return convertPage(
|
return convertPage(
|
||||||
@@ -421,7 +421,7 @@ public class JpaTargetManagement implements TargetManagement {
|
|||||||
final String rsqlParam) {
|
final String rsqlParam) {
|
||||||
throwEntityNotFoundIfDsDoesNotExist(distributionSetId);
|
throwEntityNotFoundIfDsDoesNotExist(distributionSetId);
|
||||||
|
|
||||||
final Specification<JpaTarget> spec = RSQLUtility.parse(rsqlParam, TargetFields.class, virtualPropertyReplacer,
|
final Specification<JpaTarget> spec = RSQLUtility.buildRsqlSpecification(rsqlParam, TargetFields.class, virtualPropertyReplacer,
|
||||||
database);
|
database);
|
||||||
|
|
||||||
return convertPage(
|
return convertPage(
|
||||||
@@ -658,7 +658,7 @@ public class JpaTargetManagement implements TargetManagement {
|
|||||||
final String targetFilterQuery) {
|
final String targetFilterQuery) {
|
||||||
throwEntityNotFoundIfDsDoesNotExist(distributionSetId);
|
throwEntityNotFoundIfDsDoesNotExist(distributionSetId);
|
||||||
|
|
||||||
final Specification<JpaTarget> spec = RSQLUtility.parse(targetFilterQuery, TargetFields.class,
|
final Specification<JpaTarget> spec = RSQLUtility.buildRsqlSpecification(targetFilterQuery, TargetFields.class,
|
||||||
virtualPropertyReplacer, database);
|
virtualPropertyReplacer, database);
|
||||||
|
|
||||||
return findTargetsBySpec(
|
return findTargetsBySpec(
|
||||||
@@ -673,7 +673,7 @@ public class JpaTargetManagement implements TargetManagement {
|
|||||||
public Page<Target> findByTargetFilterQueryAndNotInRolloutGroups(final Pageable pageRequest,
|
public Page<Target> findByTargetFilterQueryAndNotInRolloutGroups(final Pageable pageRequest,
|
||||||
final Collection<Long> groups, final String targetFilterQuery) {
|
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);
|
virtualPropertyReplacer, database);
|
||||||
|
|
||||||
return findTargetsBySpec((root, cq, cb) -> cb.and(spec.toPredicate(root, cq, cb),
|
return findTargetsBySpec((root, cq, cb) -> cb.and(spec.toPredicate(root, cq, cb),
|
||||||
@@ -694,7 +694,7 @@ public class JpaTargetManagement implements TargetManagement {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public long countByRsqlAndNotInRolloutGroups(final Collection<Long> groups, final String targetFilterQuery) {
|
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);
|
virtualPropertyReplacer, database);
|
||||||
final List<Specification<JpaTarget>> specList = Arrays.asList(spec,
|
final List<Specification<JpaTarget>> specList = Arrays.asList(spec,
|
||||||
TargetSpecifications.isNotInRolloutGroups(groups));
|
TargetSpecifications.isNotInRolloutGroups(groups));
|
||||||
@@ -706,7 +706,7 @@ public class JpaTargetManagement implements TargetManagement {
|
|||||||
public long countByRsqlAndNonDS(final long distributionSetId, final String targetFilterQuery) {
|
public long countByRsqlAndNonDS(final long distributionSetId, final String targetFilterQuery) {
|
||||||
throwEntityNotFoundIfDsDoesNotExist(distributionSetId);
|
throwEntityNotFoundIfDsDoesNotExist(distributionSetId);
|
||||||
|
|
||||||
final Specification<JpaTarget> spec = RSQLUtility.parse(targetFilterQuery, TargetFields.class,
|
final Specification<JpaTarget> spec = RSQLUtility.buildRsqlSpecification(targetFilterQuery, TargetFields.class,
|
||||||
virtualPropertyReplacer, database);
|
virtualPropertyReplacer, database);
|
||||||
final List<Specification<JpaTarget>> specList = Lists.newArrayListWithExpectedSize(2);
|
final List<Specification<JpaTarget>> specList = Lists.newArrayListWithExpectedSize(2);
|
||||||
specList.add(spec);
|
specList.add(spec);
|
||||||
@@ -750,7 +750,7 @@ public class JpaTargetManagement implements TargetManagement {
|
|||||||
|
|
||||||
throwEntityNotFoundExceptionIfTagDoesNotExist(tagId);
|
throwEntityNotFoundExceptionIfTagDoesNotExist(tagId);
|
||||||
|
|
||||||
final Specification<JpaTarget> spec = RSQLUtility.parse(rsqlParam, TargetFields.class, virtualPropertyReplacer,
|
final Specification<JpaTarget> spec = RSQLUtility.buildRsqlSpecification(rsqlParam, TargetFields.class, virtualPropertyReplacer,
|
||||||
database);
|
database);
|
||||||
|
|
||||||
return convertPage(targetRepository.findAll((Specification<JpaTarget>) (root, query, cb) -> cb.and(
|
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)
|
final TargetFilterQuery targetFilterQuery = targetFilterQueryRepository.findById(targetFilterQueryId)
|
||||||
.orElseThrow(() -> new EntityNotFoundException(TargetFilterQuery.class, 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);
|
virtualPropertyReplacer, database);
|
||||||
return targetRepository.count(specs);
|
return targetRepository.count(specs);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public long countByRsql(final String targetFilterQuery) {
|
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);
|
virtualPropertyReplacer, database);
|
||||||
return targetRepository.count((root, query, cb) -> {
|
return targetRepository.count((root, query, cb) -> {
|
||||||
query.distinct(true);
|
query.distinct(true);
|
||||||
|
|||||||
@@ -106,7 +106,7 @@ public class JpaTargetTagManagement implements TargetTagManagement {
|
|||||||
@Override
|
@Override
|
||||||
public Page<TargetTag> findByRsql(final Pageable pageable, final String rsqlParam) {
|
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);
|
database);
|
||||||
return convertTPage(targetTagRepository.findAll(spec, pageable), pageable);
|
return convertTPage(targetTagRepository.findAll(spec, pageable), pageable);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,8 +15,6 @@ import java.util.concurrent.ScheduledExecutorService;
|
|||||||
import javax.persistence.EntityManager;
|
import javax.persistence.EntityManager;
|
||||||
import javax.sql.DataSource;
|
import javax.sql.DataSource;
|
||||||
|
|
||||||
import com.google.common.collect.Maps;
|
|
||||||
|
|
||||||
import org.eclipse.hawkbit.artifact.repository.ArtifactRepository;
|
import org.eclipse.hawkbit.artifact.repository.ArtifactRepository;
|
||||||
import org.eclipse.hawkbit.repository.ArtifactManagement;
|
import org.eclipse.hawkbit.repository.ArtifactManagement;
|
||||||
import org.eclipse.hawkbit.repository.ControllerManagement;
|
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.RepositoryDefaultConfiguration;
|
||||||
import org.eclipse.hawkbit.repository.RepositoryProperties;
|
import org.eclipse.hawkbit.repository.RepositoryProperties;
|
||||||
import org.eclipse.hawkbit.repository.RolloutApprovalStrategy;
|
import org.eclipse.hawkbit.repository.RolloutApprovalStrategy;
|
||||||
import org.eclipse.hawkbit.repository.RolloutGroupManagement;
|
|
||||||
import org.eclipse.hawkbit.repository.RolloutExecutor;
|
import org.eclipse.hawkbit.repository.RolloutExecutor;
|
||||||
|
import org.eclipse.hawkbit.repository.RolloutGroupManagement;
|
||||||
import org.eclipse.hawkbit.repository.RolloutManagement;
|
import org.eclipse.hawkbit.repository.RolloutManagement;
|
||||||
import org.eclipse.hawkbit.repository.RolloutStatusCache;
|
import org.eclipse.hawkbit.repository.RolloutStatusCache;
|
||||||
import org.eclipse.hawkbit.repository.SoftwareModuleManagement;
|
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.ThresholdRolloutGroupErrorCondition;
|
||||||
import org.eclipse.hawkbit.repository.jpa.rollout.condition.ThresholdRolloutGroupSuccessCondition;
|
import org.eclipse.hawkbit.repository.jpa.rollout.condition.ThresholdRolloutGroupSuccessCondition;
|
||||||
import org.eclipse.hawkbit.repository.jpa.rsql.RsqlParserValidationOracle;
|
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.DistributionSet;
|
||||||
import org.eclipse.hawkbit.repository.model.DistributionSetType;
|
import org.eclipse.hawkbit.repository.model.DistributionSetType;
|
||||||
import org.eclipse.hawkbit.repository.model.Rollout;
|
import org.eclipse.hawkbit.repository.model.Rollout;
|
||||||
@@ -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.SystemManagementHolder;
|
||||||
import org.eclipse.hawkbit.repository.model.helper.TenantConfigurationManagementHolder;
|
import org.eclipse.hawkbit.repository.model.helper.TenantConfigurationManagementHolder;
|
||||||
import org.eclipse.hawkbit.repository.rsql.RsqlValidationOracle;
|
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.repository.rsql.VirtualPropertyReplacer;
|
||||||
import org.eclipse.hawkbit.security.HawkbitSecurityProperties;
|
import org.eclipse.hawkbit.security.HawkbitSecurityProperties;
|
||||||
import org.eclipse.hawkbit.security.SecurityTokenGenerator;
|
import org.eclipse.hawkbit.security.SecurityTokenGenerator;
|
||||||
@@ -127,6 +128,8 @@ import org.springframework.transaction.annotation.EnableTransactionManagement;
|
|||||||
import org.springframework.transaction.jta.JtaTransactionManager;
|
import org.springframework.transaction.jta.JtaTransactionManager;
|
||||||
import org.springframework.validation.beanvalidation.MethodValidationPostProcessor;
|
import org.springframework.validation.beanvalidation.MethodValidationPostProcessor;
|
||||||
|
|
||||||
|
import com.google.common.collect.Maps;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* General configuration for hawkBit's Repository.
|
* General configuration for hawkBit's Repository.
|
||||||
*
|
*
|
||||||
@@ -415,7 +418,8 @@ public class RepositoryApplicationConfiguration extends JpaBaseConfiguration {
|
|||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
@Bean
|
@Bean
|
||||||
public PlatformTransactionManager transactionManager(ObjectProvider<TransactionManagerCustomizers> transactionManagerCustomizers) {
|
public PlatformTransactionManager transactionManager(
|
||||||
|
ObjectProvider<TransactionManagerCustomizers> transactionManagerCustomizers) {
|
||||||
return new MultiTenantJpaTransactionManager();
|
return new MultiTenantJpaTransactionManager();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -620,15 +624,14 @@ public class RepositoryApplicationConfiguration extends JpaBaseConfiguration {
|
|||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
@ConditionalOnMissingBean
|
@ConditionalOnMissingBean
|
||||||
RolloutExecutor rolloutExecutor(
|
RolloutExecutor rolloutExecutor(final RolloutTargetGroupRepository rolloutTargetGroupRepository,
|
||||||
final RolloutTargetGroupRepository rolloutTargetGroupRepository, final EntityManager entityManager,
|
final EntityManager entityManager, final RolloutRepository rolloutRepository,
|
||||||
final RolloutRepository rolloutRepository, final ActionRepository actionRepository,
|
final ActionRepository actionRepository, final RolloutGroupRepository rolloutGroupRepository,
|
||||||
final RolloutGroupRepository rolloutGroupRepository, final AfterTransactionCommitExecutor afterCommit,
|
final AfterTransactionCommitExecutor afterCommit, final TenantAware tenantAware,
|
||||||
final TenantAware tenantAware, final RolloutGroupManagement rolloutGroupManagement,
|
final RolloutGroupManagement rolloutGroupManagement, final QuotaManagement quotaManagement,
|
||||||
final QuotaManagement quotaManagement, final DeploymentManagement deploymentManagement,
|
final DeploymentManagement deploymentManagement, final TargetManagement targetManagement,
|
||||||
final TargetManagement targetManagement, final EventPublisherHolder eventPublisherHolder,
|
final EventPublisherHolder eventPublisherHolder, final PlatformTransactionManager txManager,
|
||||||
final PlatformTransactionManager txManager, final RolloutApprovalStrategy rolloutApprovalStrategy,
|
final RolloutApprovalStrategy rolloutApprovalStrategy, final ApplicationContext context) {
|
||||||
final ApplicationContext context) {
|
|
||||||
return new JpaRolloutExecutor(rolloutTargetGroupRepository, entityManager, rolloutRepository, actionRepository,
|
return new JpaRolloutExecutor(rolloutTargetGroupRepository, entityManager, rolloutRepository, actionRepository,
|
||||||
rolloutGroupRepository, afterCommit, tenantAware, rolloutGroupManagement, quotaManagement,
|
rolloutGroupRepository, afterCommit, tenantAware, rolloutGroupManagement, quotaManagement,
|
||||||
deploymentManagement, targetManagement, eventPublisherHolder, txManager, rolloutApprovalStrategy,
|
deploymentManagement, targetManagement, eventPublisherHolder, txManager, rolloutApprovalStrategy,
|
||||||
@@ -875,4 +878,26 @@ public class RepositoryApplicationConfiguration extends JpaBaseConfiguration {
|
|||||||
final RolloutManagement rolloutManagement, final SystemSecurityContext systemSecurityContext) {
|
final RolloutManagement rolloutManagement, final SystemSecurityContext systemSecurityContext) {
|
||||||
return new RolloutScheduler(systemManagement, rolloutManagement, 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;
|
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.List;
|
||||||
import java.util.Map;
|
|
||||||
import java.util.Map.Entry;
|
|
||||||
import java.util.Optional;
|
|
||||||
import java.util.Set;
|
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.CriteriaBuilder;
|
||||||
import javax.persistence.criteria.CriteriaQuery;
|
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.Predicate;
|
||||||
import javax.persistence.criteria.Root;
|
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.apache.commons.lang3.text.StrLookup;
|
||||||
import org.eclipse.hawkbit.repository.FieldNameProvider;
|
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.RSQLParameterSyntaxException;
|
||||||
import org.eclipse.hawkbit.repository.exception.RSQLParameterUnsupportedFieldException;
|
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.VirtualPropertyReplacer;
|
||||||
import org.eclipse.hawkbit.repository.rsql.VirtualPropertyResolver;
|
import org.eclipse.hawkbit.repository.rsql.VirtualPropertyResolver;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.beans.SimpleTypeConverter;
|
|
||||||
import org.springframework.beans.TypeMismatchException;
|
|
||||||
import org.springframework.data.jpa.domain.Specification;
|
import org.springframework.data.jpa.domain.Specification;
|
||||||
import org.springframework.orm.jpa.vendor.Database;
|
import org.springframework.orm.jpa.vendor.Database;
|
||||||
import org.springframework.util.CollectionUtils;
|
import org.springframework.util.CollectionUtils;
|
||||||
import org.springframework.util.StringUtils;
|
|
||||||
|
|
||||||
import com.google.common.collect.Lists;
|
|
||||||
|
|
||||||
import cz.jirutka.rsql.parser.RSQLParser;
|
import cz.jirutka.rsql.parser.RSQLParser;
|
||||||
import cz.jirutka.rsql.parser.RSQLParserException;
|
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.ComparisonOperator;
|
||||||
import cz.jirutka.rsql.parser.ast.LogicalNode;
|
|
||||||
import cz.jirutka.rsql.parser.ast.Node;
|
import cz.jirutka.rsql.parser.ast.Node;
|
||||||
import cz.jirutka.rsql.parser.ast.OrNode;
|
|
||||||
import cz.jirutka.rsql.parser.ast.RSQLOperators;
|
import cz.jirutka.rsql.parser.ast.RSQLOperators;
|
||||||
import cz.jirutka.rsql.parser.ast.RSQLVisitor;
|
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
|
* Builds a JPA {@link Specification} which corresponds with the given RSQL
|
||||||
* can be used to filter for JPA entities with the given RSQL query.
|
* query. The specification can be used to filter for JPA entities with the
|
||||||
|
* given RSQL query.
|
||||||
*
|
*
|
||||||
* @param rsql
|
* @param rsql
|
||||||
* the rsql query
|
* the rsql query to be parsed
|
||||||
* @param fieldNameProvider
|
* @param fieldNameProvider
|
||||||
* the enum class type which implements the
|
* the enum class type which implements the
|
||||||
* {@link FieldNameProvider}
|
* {@link FieldNameProvider}
|
||||||
@@ -133,7 +105,7 @@ public final class RSQLUtility {
|
|||||||
* if the RSQL syntax is wrong
|
* 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 Class<A> fieldNameProvider, final VirtualPropertyReplacer virtualPropertyReplacer,
|
||||||
final Database database) {
|
final Database database) {
|
||||||
return new RSQLSpecification<>(rsql, fieldNameProvider, virtualPropertyReplacer, 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,
|
public static <A extends Enum<A> & FieldNameProvider> void validateRsqlFor(final String rsql,
|
||||||
final Class<A> fieldNameProvider) {
|
final Class<A> fieldNameProvider) {
|
||||||
final RSQLVisitor<Void, String> visitor = new ValidationRSQLVisitor<>(fieldNameProvider);
|
final RSQLVisitor<Void, String> visitor = getValidationRsqlVisitor(fieldNameProvider);
|
||||||
final Node rootNode = parseRsql(rsql);
|
final Node rootNode = parseRsql(rsql);
|
||||||
rootNode.accept(visitor);
|
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) {
|
private static Node parseRsql(final String rsql) {
|
||||||
try {
|
try {
|
||||||
LOGGER.debug("Parsing rsql string {}", rsql);
|
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 class RSQLSpecification<A extends Enum<A> & FieldNameProvider, T> implements Specification<T> {
|
||||||
|
|
||||||
private static final long serialVersionUID = 1L;
|
private static final long serialVersionUID = 1L;
|
||||||
@@ -222,7 +169,7 @@ public final class RSQLUtility {
|
|||||||
final Node rootNode = parseRsql(rsql);
|
final Node rootNode = parseRsql(rsql);
|
||||||
query.distinct(true);
|
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);
|
virtualPropertyReplacer, database, query);
|
||||||
final List<Predicate> accept = rootNode.<List<Predicate>, String> accept(jpqQueryRSQLVisitor);
|
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -154,7 +154,7 @@ public class RSQLUtilityTest {
|
|||||||
public void wrongRsqlSyntaxThrowSyntaxException() {
|
public void wrongRsqlSyntaxThrowSyntaxException() {
|
||||||
final String wrongRSQL = "name==abc;d";
|
final String wrongRSQL = "name==abc;d";
|
||||||
try {
|
try {
|
||||||
RSQLUtility.parse(wrongRSQL, SoftwareModuleFields.class, null, testDb)
|
RSQLUtility.buildRsqlSpecification(wrongRSQL, SoftwareModuleFields.class, null, testDb)
|
||||||
.toPredicate(baseSoftwareModuleRootMock, criteriaQueryMock, criteriaBuilderMock);
|
.toPredicate(baseSoftwareModuleRootMock, criteriaQueryMock, criteriaBuilderMock);
|
||||||
fail("Missing expected RSQLParameterSyntaxException because of wrong RSQL syntax");
|
fail("Missing expected RSQLParameterSyntaxException because of wrong RSQL syntax");
|
||||||
} catch (final RSQLParameterSyntaxException e) {
|
} catch (final RSQLParameterSyntaxException e) {
|
||||||
@@ -166,7 +166,7 @@ public class RSQLUtilityTest {
|
|||||||
final String wrongRSQL = "unknownField==abc";
|
final String wrongRSQL = "unknownField==abc";
|
||||||
when(baseSoftwareModuleRootMock.getJavaType()).thenReturn((Class) SoftwareModule.class);
|
when(baseSoftwareModuleRootMock.getJavaType()).thenReturn((Class) SoftwareModule.class);
|
||||||
try {
|
try {
|
||||||
RSQLUtility.parse(wrongRSQL, SoftwareModuleFields.class, null, testDb)
|
RSQLUtility.buildRsqlSpecification(wrongRSQL, SoftwareModuleFields.class, null, testDb)
|
||||||
.toPredicate(baseSoftwareModuleRootMock, criteriaQueryMock, criteriaBuilderMock);
|
.toPredicate(baseSoftwareModuleRootMock, criteriaQueryMock, criteriaBuilderMock);
|
||||||
fail("Missing an expected RSQLParameterUnsupportedFieldException because of unknown RSQL field");
|
fail("Missing an expected RSQLParameterUnsupportedFieldException because of unknown RSQL field");
|
||||||
} catch (final RSQLParameterUnsupportedFieldException e) {
|
} catch (final RSQLParameterUnsupportedFieldException e) {
|
||||||
@@ -178,7 +178,7 @@ public class RSQLUtilityTest {
|
|||||||
public void wrongRsqlMapSyntaxThrowSyntaxException() {
|
public void wrongRsqlMapSyntaxThrowSyntaxException() {
|
||||||
String wrongRSQL = TargetFields.ATTRIBUTE + "==abc";
|
String wrongRSQL = TargetFields.ATTRIBUTE + "==abc";
|
||||||
try {
|
try {
|
||||||
RSQLUtility.parse(wrongRSQL, TargetFields.class, null, testDb).toPredicate(baseSoftwareModuleRootMock,
|
RSQLUtility.buildRsqlSpecification(wrongRSQL, TargetFields.class, null, testDb).toPredicate(baseSoftwareModuleRootMock,
|
||||||
criteriaQueryMock, criteriaBuilderMock);
|
criteriaQueryMock, criteriaBuilderMock);
|
||||||
fail("Missing expected RSQLParameterSyntaxException for target attributes map, caused by wrong RSQL syntax (key was not present)");
|
fail("Missing expected RSQLParameterSyntaxException for target attributes map, caused by wrong RSQL syntax (key was not present)");
|
||||||
} catch (final RSQLParameterUnsupportedFieldException e) {
|
} catch (final RSQLParameterUnsupportedFieldException e) {
|
||||||
@@ -186,7 +186,7 @@ public class RSQLUtilityTest {
|
|||||||
|
|
||||||
wrongRSQL = TargetFields.ATTRIBUTE + ".unknown.wrong==abc";
|
wrongRSQL = TargetFields.ATTRIBUTE + ".unknown.wrong==abc";
|
||||||
try {
|
try {
|
||||||
RSQLUtility.parse(wrongRSQL, TargetFields.class, null, testDb).toPredicate(baseSoftwareModuleRootMock,
|
RSQLUtility.buildRsqlSpecification(wrongRSQL, TargetFields.class, null, testDb).toPredicate(baseSoftwareModuleRootMock,
|
||||||
criteriaQueryMock, criteriaBuilderMock);
|
criteriaQueryMock, criteriaBuilderMock);
|
||||||
fail("Missing expected RSQLParameterSyntaxException for target attributes map, caused by wrong RSQL syntax (key includes dots)");
|
fail("Missing expected RSQLParameterSyntaxException for target attributes map, caused by wrong RSQL syntax (key includes dots)");
|
||||||
} catch (final RSQLParameterUnsupportedFieldException e) {
|
} catch (final RSQLParameterUnsupportedFieldException e) {
|
||||||
@@ -194,7 +194,7 @@ public class RSQLUtilityTest {
|
|||||||
|
|
||||||
wrongRSQL = TargetFields.METADATA + ".unknown.wrong==abc";
|
wrongRSQL = TargetFields.METADATA + ".unknown.wrong==abc";
|
||||||
try {
|
try {
|
||||||
RSQLUtility.parse(wrongRSQL, TargetFields.class, null, testDb).toPredicate(baseSoftwareModuleRootMock,
|
RSQLUtility.buildRsqlSpecification(wrongRSQL, TargetFields.class, null, testDb).toPredicate(baseSoftwareModuleRootMock,
|
||||||
criteriaQueryMock, criteriaBuilderMock);
|
criteriaQueryMock, criteriaBuilderMock);
|
||||||
fail("Missing expected RSQLParameterSyntaxException for target metadata map, caused by wrong RSQL syntax (key includes dots)");
|
fail("Missing expected RSQLParameterSyntaxException for target metadata map, caused by wrong RSQL syntax (key includes dots)");
|
||||||
} catch (final RSQLParameterUnsupportedFieldException e) {
|
} catch (final RSQLParameterUnsupportedFieldException e) {
|
||||||
@@ -202,7 +202,7 @@ public class RSQLUtilityTest {
|
|||||||
|
|
||||||
wrongRSQL = DistributionSetFields.METADATA + "==abc";
|
wrongRSQL = DistributionSetFields.METADATA + "==abc";
|
||||||
try {
|
try {
|
||||||
RSQLUtility.parse(wrongRSQL, DistributionSetFields.class, null, testDb)
|
RSQLUtility.buildRsqlSpecification(wrongRSQL, DistributionSetFields.class, null, testDb)
|
||||||
.toPredicate(baseSoftwareModuleRootMock, criteriaQueryMock, criteriaBuilderMock);
|
.toPredicate(baseSoftwareModuleRootMock, criteriaQueryMock, criteriaBuilderMock);
|
||||||
fail("Missing expected RSQLParameterSyntaxException for distribution set metadata map, caused by wrong RSQL syntax (key was not present)");
|
fail("Missing expected RSQLParameterSyntaxException for distribution set metadata map, caused by wrong RSQL syntax (key was not present)");
|
||||||
} catch (final RSQLParameterUnsupportedFieldException e) {
|
} catch (final RSQLParameterUnsupportedFieldException e) {
|
||||||
@@ -214,7 +214,7 @@ public class RSQLUtilityTest {
|
|||||||
public void wrongRsqlSubEntitySyntaxThrowSyntaxException() {
|
public void wrongRsqlSubEntitySyntaxThrowSyntaxException() {
|
||||||
String wrongRSQL = TargetFields.ASSIGNEDDS + "==abc";
|
String wrongRSQL = TargetFields.ASSIGNEDDS + "==abc";
|
||||||
try {
|
try {
|
||||||
RSQLUtility.parse(wrongRSQL, TargetFields.class, null, testDb).toPredicate(baseSoftwareModuleRootMock,
|
RSQLUtility.buildRsqlSpecification(wrongRSQL, TargetFields.class, null, testDb).toPredicate(baseSoftwareModuleRootMock,
|
||||||
criteriaQueryMock, criteriaBuilderMock);
|
criteriaQueryMock, criteriaBuilderMock);
|
||||||
fail("Missing expected RSQLParameterSyntaxException because of wrong RSQL syntax");
|
fail("Missing expected RSQLParameterSyntaxException because of wrong RSQL syntax");
|
||||||
} catch (final RSQLParameterUnsupportedFieldException e) {
|
} catch (final RSQLParameterUnsupportedFieldException e) {
|
||||||
@@ -222,7 +222,7 @@ public class RSQLUtilityTest {
|
|||||||
|
|
||||||
wrongRSQL = TargetFields.ASSIGNEDDS + ".unknownField==abc";
|
wrongRSQL = TargetFields.ASSIGNEDDS + ".unknownField==abc";
|
||||||
try {
|
try {
|
||||||
RSQLUtility.parse(wrongRSQL, TargetFields.class, null, testDb).toPredicate(baseSoftwareModuleRootMock,
|
RSQLUtility.buildRsqlSpecification(wrongRSQL, TargetFields.class, null, testDb).toPredicate(baseSoftwareModuleRootMock,
|
||||||
criteriaQueryMock, criteriaBuilderMock);
|
criteriaQueryMock, criteriaBuilderMock);
|
||||||
fail("Missing expected RSQLParameterSyntaxException because of wrong RSQL syntax");
|
fail("Missing expected RSQLParameterSyntaxException because of wrong RSQL syntax");
|
||||||
} catch (final RSQLParameterUnsupportedFieldException e) {
|
} catch (final RSQLParameterUnsupportedFieldException e) {
|
||||||
@@ -230,7 +230,7 @@ public class RSQLUtilityTest {
|
|||||||
|
|
||||||
wrongRSQL = TargetFields.ASSIGNEDDS + ".unknownField.ToMuch==abc";
|
wrongRSQL = TargetFields.ASSIGNEDDS + ".unknownField.ToMuch==abc";
|
||||||
try {
|
try {
|
||||||
RSQLUtility.parse(wrongRSQL, TargetFields.class, null, testDb).toPredicate(baseSoftwareModuleRootMock,
|
RSQLUtility.buildRsqlSpecification(wrongRSQL, TargetFields.class, null, testDb).toPredicate(baseSoftwareModuleRootMock,
|
||||||
criteriaQueryMock, criteriaBuilderMock);
|
criteriaQueryMock, criteriaBuilderMock);
|
||||||
fail("Missing expected RSQLParameterSyntaxException because of wrong RSQL syntax");
|
fail("Missing expected RSQLParameterSyntaxException because of wrong RSQL syntax");
|
||||||
} catch (final RSQLParameterUnsupportedFieldException e) {
|
} catch (final RSQLParameterUnsupportedFieldException e) {
|
||||||
@@ -250,7 +250,7 @@ public class RSQLUtilityTest {
|
|||||||
when(criteriaBuilderMock.equal(any(Expression.class), any(String.class))).thenReturn(mock(Predicate.class));
|
when(criteriaBuilderMock.equal(any(Expression.class), any(String.class))).thenReturn(mock(Predicate.class));
|
||||||
|
|
||||||
// test
|
// test
|
||||||
RSQLUtility.parse(correctRsql, SoftwareModuleFields.class, null, testDb).toPredicate(baseSoftwareModuleRootMock,
|
RSQLUtility.buildRsqlSpecification(correctRsql, SoftwareModuleFields.class, null, testDb).toPredicate(baseSoftwareModuleRootMock,
|
||||||
criteriaQueryMock, criteriaBuilderMock);
|
criteriaQueryMock, criteriaBuilderMock);
|
||||||
|
|
||||||
// verification
|
// verification
|
||||||
@@ -271,7 +271,7 @@ public class RSQLUtilityTest {
|
|||||||
.thenReturn(pathOfString(baseSoftwareModuleRootMock));
|
.thenReturn(pathOfString(baseSoftwareModuleRootMock));
|
||||||
|
|
||||||
// test
|
// test
|
||||||
RSQLUtility.parse(correctRsql, SoftwareModuleFields.class, null, testDb).toPredicate(baseSoftwareModuleRootMock,
|
RSQLUtility.buildRsqlSpecification(correctRsql, SoftwareModuleFields.class, null, testDb).toPredicate(baseSoftwareModuleRootMock,
|
||||||
criteriaQueryMock, criteriaBuilderMock);
|
criteriaQueryMock, criteriaBuilderMock);
|
||||||
|
|
||||||
// verification
|
// verification
|
||||||
@@ -301,7 +301,7 @@ public class RSQLUtilityTest {
|
|||||||
when(subqueryMock.select(subqueryRootMock)).thenReturn(subqueryMock);
|
when(subqueryMock.select(subqueryRootMock)).thenReturn(subqueryMock);
|
||||||
|
|
||||||
// test
|
// test
|
||||||
RSQLUtility.parse(correctRsql, SoftwareModuleFields.class, null, testDb).toPredicate(baseSoftwareModuleRootMock,
|
RSQLUtility.buildRsqlSpecification(correctRsql, SoftwareModuleFields.class, null, testDb).toPredicate(baseSoftwareModuleRootMock,
|
||||||
criteriaQueryMock, criteriaBuilderMock);
|
criteriaQueryMock, criteriaBuilderMock);
|
||||||
|
|
||||||
// verification
|
// verification
|
||||||
@@ -320,7 +320,7 @@ public class RSQLUtilityTest {
|
|||||||
when(criteriaBuilderMock.upper(eq(pathOfString(baseSoftwareModuleRootMock))))
|
when(criteriaBuilderMock.upper(eq(pathOfString(baseSoftwareModuleRootMock))))
|
||||||
.thenReturn(pathOfString(baseSoftwareModuleRootMock));
|
.thenReturn(pathOfString(baseSoftwareModuleRootMock));
|
||||||
// test
|
// test
|
||||||
RSQLUtility.parse(correctRsql, SoftwareModuleFields.class, null, Database.H2)
|
RSQLUtility.buildRsqlSpecification(correctRsql, SoftwareModuleFields.class, null, Database.H2)
|
||||||
.toPredicate(baseSoftwareModuleRootMock, criteriaQueryMock, criteriaBuilderMock);
|
.toPredicate(baseSoftwareModuleRootMock, criteriaQueryMock, criteriaBuilderMock);
|
||||||
|
|
||||||
// verification
|
// verification
|
||||||
@@ -342,7 +342,7 @@ public class RSQLUtilityTest {
|
|||||||
.thenReturn(mock(Predicate.class));
|
.thenReturn(mock(Predicate.class));
|
||||||
|
|
||||||
// test
|
// test
|
||||||
RSQLUtility.parse(correctRsql, SoftwareModuleFields.class, null, Database.SQL_SERVER)
|
RSQLUtility.buildRsqlSpecification(correctRsql, SoftwareModuleFields.class, null, Database.SQL_SERVER)
|
||||||
.toPredicate(baseSoftwareModuleRootMock, criteriaQueryMock, criteriaBuilderMock);
|
.toPredicate(baseSoftwareModuleRootMock, criteriaQueryMock, criteriaBuilderMock);
|
||||||
|
|
||||||
// verification
|
// verification
|
||||||
@@ -361,7 +361,7 @@ public class RSQLUtilityTest {
|
|||||||
when(criteriaBuilderMock.<String> greaterThanOrEqualTo(any(Expression.class), any(String.class)))
|
when(criteriaBuilderMock.<String> greaterThanOrEqualTo(any(Expression.class), any(String.class)))
|
||||||
.thenReturn(mock(Predicate.class));
|
.thenReturn(mock(Predicate.class));
|
||||||
// test
|
// test
|
||||||
RSQLUtility.parse(correctRsql, SoftwareModuleFields.class, null, testDb).toPredicate(baseSoftwareModuleRootMock,
|
RSQLUtility.buildRsqlSpecification(correctRsql, SoftwareModuleFields.class, null, testDb).toPredicate(baseSoftwareModuleRootMock,
|
||||||
criteriaQueryMock, criteriaBuilderMock);
|
criteriaQueryMock, criteriaBuilderMock);
|
||||||
|
|
||||||
// verification
|
// verification
|
||||||
@@ -378,7 +378,7 @@ public class RSQLUtilityTest {
|
|||||||
when(criteriaBuilderMock.equal(any(Root.class), any(TestValueEnum.class))).thenReturn(mock(Predicate.class));
|
when(criteriaBuilderMock.equal(any(Root.class), any(TestValueEnum.class))).thenReturn(mock(Predicate.class));
|
||||||
|
|
||||||
// test
|
// test
|
||||||
RSQLUtility.parse(correctRsql, TestFieldEnum.class, null, testDb).toPredicate(baseSoftwareModuleRootMock,
|
RSQLUtility.buildRsqlSpecification(correctRsql, TestFieldEnum.class, null, testDb).toPredicate(baseSoftwareModuleRootMock,
|
||||||
criteriaQueryMock, criteriaBuilderMock);
|
criteriaQueryMock, criteriaBuilderMock);
|
||||||
|
|
||||||
// verification
|
// verification
|
||||||
@@ -396,7 +396,7 @@ public class RSQLUtilityTest {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
// test
|
// test
|
||||||
RSQLUtility.parse(correctRsql, TestFieldEnum.class, null, testDb).toPredicate(baseSoftwareModuleRootMock,
|
RSQLUtility.buildRsqlSpecification(correctRsql, TestFieldEnum.class, null, testDb).toPredicate(baseSoftwareModuleRootMock,
|
||||||
criteriaQueryMock, criteriaBuilderMock);
|
criteriaQueryMock, criteriaBuilderMock);
|
||||||
fail("missing RSQLParameterUnsupportedFieldException for wrong enum value");
|
fail("missing RSQLParameterUnsupportedFieldException for wrong enum value");
|
||||||
} catch (final RSQLParameterUnsupportedFieldException e) {
|
} catch (final RSQLParameterUnsupportedFieldException e) {
|
||||||
@@ -420,7 +420,7 @@ public class RSQLUtilityTest {
|
|||||||
.thenReturn(mock(Predicate.class));
|
.thenReturn(mock(Predicate.class));
|
||||||
|
|
||||||
// test
|
// test
|
||||||
RSQLUtility.parse(correctRsql, TestFieldEnum.class, setupMacroLookup(), testDb)
|
RSQLUtility.buildRsqlSpecification(correctRsql, TestFieldEnum.class, setupMacroLookup(), testDb)
|
||||||
.toPredicate(baseSoftwareModuleRootMock, criteriaQueryMock, criteriaBuilderMock);
|
.toPredicate(baseSoftwareModuleRootMock, criteriaQueryMock, criteriaBuilderMock);
|
||||||
|
|
||||||
// verification
|
// verification
|
||||||
@@ -446,7 +446,7 @@ public class RSQLUtilityTest {
|
|||||||
.thenReturn(mock(Predicate.class));
|
.thenReturn(mock(Predicate.class));
|
||||||
|
|
||||||
// test
|
// test
|
||||||
RSQLUtility.parse(correctRsql, TestFieldEnum.class, setupMacroLookup(), testDb)
|
RSQLUtility.buildRsqlSpecification(correctRsql, TestFieldEnum.class, setupMacroLookup(), testDb)
|
||||||
.toPredicate(baseSoftwareModuleRootMock, criteriaQueryMock, criteriaBuilderMock);
|
.toPredicate(baseSoftwareModuleRootMock, criteriaQueryMock, criteriaBuilderMock);
|
||||||
|
|
||||||
// verification
|
// verification
|
||||||
|
|||||||
Reference in New Issue
Block a user