Move Query Language (RSQL) in separate package and add Entity Matcher (#2531)

* Move Query Language (RSQL) in separate package - hawkbit-repository-ql
* Add Entity Matcher which match an entity object agains filter
* Spec to string utils now in runtime (as a library) - could be used in tests or to dump something in runtimes
* Move eclipselink/hibernate profiles in new QL module, this way provided / set to hawkbit-repository-jpa
* Remove unused javax.el imports
This commit is contained in:
Avgustin Marinov
2025-07-03 14:41:55 +03:00
committed by GitHub
parent 047f94d4cb
commit 426bdbf179
53 changed files with 326 additions and 275 deletions

View File

@@ -154,7 +154,7 @@ import org.eclipse.hawkbit.repository.model.TargetFilterQuery;
import org.eclipse.hawkbit.repository.model.TargetType;
import org.eclipse.hawkbit.repository.model.helper.SystemSecurityContextHolder;
import org.eclipse.hawkbit.repository.model.helper.TenantConfigurationManagementHolder;
import org.eclipse.hawkbit.repository.rsql.RsqlConfigHolder;
import org.eclipse.hawkbit.repository.jpa.rsql.RsqlConfigHolder;
import org.eclipse.hawkbit.repository.rsql.VirtualPropertyReplacer;
import org.eclipse.hawkbit.security.HawkbitSecurityProperties;
import org.eclipse.hawkbit.security.SecurityTokenGenerator;

View File

@@ -66,7 +66,7 @@ import org.eclipse.hawkbit.repository.jpa.model.JpaTarget_;
import org.eclipse.hawkbit.repository.jpa.repository.ActionRepository;
import org.eclipse.hawkbit.repository.jpa.repository.ActionStatusRepository;
import org.eclipse.hawkbit.repository.jpa.repository.TargetRepository;
import org.eclipse.hawkbit.repository.jpa.rsql.RSQLUtility;
import org.eclipse.hawkbit.repository.jpa.rsql.RsqlUtility;
import org.eclipse.hawkbit.repository.jpa.specifications.ActionSpecifications;
import org.eclipse.hawkbit.repository.jpa.specifications.TargetSpecifications;
import org.eclipse.hawkbit.repository.jpa.utils.DeploymentHelper;
@@ -274,7 +274,7 @@ public class JpaDeploymentManagement extends JpaActionManagement implements Depl
assertTargetReadAllowed(controllerId);
final List<Specification<JpaAction>> specList = Arrays.asList(
RSQLUtility.buildRsqlSpecification(rsql, ActionFields.class, virtualPropertyReplacer, database),
RsqlUtility.buildRsqlSpecification(rsql, ActionFields.class, virtualPropertyReplacer, database),
ActionSpecifications.byTargetControllerId(controllerId));
return JpaManagementHelper.countBySpec(actionRepository, specList);
@@ -288,7 +288,7 @@ public class JpaDeploymentManagement extends JpaActionManagement implements Depl
@Override
public long countActions(final String rsql) {
final List<Specification<JpaAction>> specList = List.of(
RSQLUtility.buildRsqlSpecification(rsql, ActionFields.class, virtualPropertyReplacer, database));
RsqlUtility.buildRsqlSpecification(rsql, ActionFields.class, virtualPropertyReplacer, database));
return JpaManagementHelper.countBySpec(actionRepository, specList);
}
@@ -313,7 +313,7 @@ public class JpaDeploymentManagement extends JpaActionManagement implements Depl
@Override
public Slice<Action> findActions(final String rsql, final Pageable pageable) {
final List<Specification<JpaAction>> specList = List.of(
RSQLUtility.buildRsqlSpecification(rsql, ActionFields.class, virtualPropertyReplacer, database));
RsqlUtility.buildRsqlSpecification(rsql, ActionFields.class, virtualPropertyReplacer, database));
return JpaManagementHelper.findAllWithoutCountBySpec(actionRepository, specList, pageable);
}
@@ -322,7 +322,7 @@ public class JpaDeploymentManagement extends JpaActionManagement implements Depl
assertTargetReadAllowed(controllerId);
final List<Specification<JpaAction>> specList = Arrays.asList(
RSQLUtility.buildRsqlSpecification(rsql, ActionFields.class, virtualPropertyReplacer, database),
RsqlUtility.buildRsqlSpecification(rsql, ActionFields.class, virtualPropertyReplacer, database),
ActionSpecifications.byTargetControllerId(controllerId));
return JpaManagementHelper.findAllWithCountBySpec(actionRepository, specList, pageable);

View File

@@ -62,7 +62,7 @@ import org.eclipse.hawkbit.repository.jpa.repository.DistributionSetTagRepositor
import org.eclipse.hawkbit.repository.jpa.repository.SoftwareModuleRepository;
import org.eclipse.hawkbit.repository.jpa.repository.TargetFilterQueryRepository;
import org.eclipse.hawkbit.repository.jpa.repository.TargetRepository;
import org.eclipse.hawkbit.repository.jpa.rsql.RSQLUtility;
import org.eclipse.hawkbit.repository.jpa.rsql.RsqlUtility;
import org.eclipse.hawkbit.repository.jpa.specifications.DistributionSetSpecification;
import org.eclipse.hawkbit.repository.jpa.specifications.TargetSpecifications;
import org.eclipse.hawkbit.repository.jpa.utils.QuotaHelper;
@@ -282,7 +282,7 @@ public class JpaDistributionSetManagement implements DistributionSetManagement {
@Override
public Page<DistributionSet> findByRsql(final String rsql, final Pageable pageable) {
return JpaManagementHelper.findAllWithCountBySpec(distributionSetRepository, List.of(
RSQLUtility.buildRsqlSpecification(rsql, DistributionSetFields.class, virtualPropertyReplacer, database),
RsqlUtility.buildRsqlSpecification(rsql, DistributionSetFields.class, virtualPropertyReplacer, database),
DistributionSetSpecification.isNotDeleted()), pageable);
}
@@ -551,7 +551,7 @@ public class JpaDistributionSetManagement implements DistributionSetManagement {
assertDsTagExists(tagId);
return JpaManagementHelper.findAllWithCountBySpec(distributionSetRepository, List.of(
RSQLUtility.buildRsqlSpecification(rsql, DistributionSetFields.class, virtualPropertyReplacer,
RsqlUtility.buildRsqlSpecification(rsql, DistributionSetFields.class, virtualPropertyReplacer,
database),
DistributionSetSpecification.hasTag(tagId), DistributionSetSpecification.isNotDeleted()), pageable);
}

View File

@@ -28,7 +28,7 @@ import org.eclipse.hawkbit.repository.jpa.configuration.Constants;
import org.eclipse.hawkbit.repository.jpa.model.JpaDistributionSetTag;
import org.eclipse.hawkbit.repository.jpa.repository.DistributionSetRepository;
import org.eclipse.hawkbit.repository.jpa.repository.DistributionSetTagRepository;
import org.eclipse.hawkbit.repository.jpa.rsql.RSQLUtility;
import org.eclipse.hawkbit.repository.jpa.rsql.RsqlUtility;
import org.eclipse.hawkbit.repository.jpa.specifications.DistributionSetTagSpecifications;
import org.eclipse.hawkbit.repository.jpa.specifications.TagSpecification;
import org.eclipse.hawkbit.repository.model.DistributionSet;
@@ -155,7 +155,7 @@ public class JpaDistributionSetTagManagement implements DistributionSetTagManage
@Override
public Page<DistributionSetTag> findByRsql(final String rsql, final Pageable pageable) {
final Specification<JpaDistributionSetTag> spec = RSQLUtility.buildRsqlSpecification(
final Specification<JpaDistributionSetTag> spec = RsqlUtility.buildRsqlSpecification(
rsql, DistributionSetTagFields.class, virtualPropertyReplacer, database);
return JpaManagementHelper.findAllWithCountBySpec(distributionSetTagRepository, Collections.singletonList(spec), pageable);
}

View File

@@ -39,7 +39,7 @@ import org.eclipse.hawkbit.repository.jpa.repository.DistributionSetRepository;
import org.eclipse.hawkbit.repository.jpa.repository.DistributionSetTypeRepository;
import org.eclipse.hawkbit.repository.jpa.repository.SoftwareModuleTypeRepository;
import org.eclipse.hawkbit.repository.jpa.repository.TargetTypeRepository;
import org.eclipse.hawkbit.repository.jpa.rsql.RSQLUtility;
import org.eclipse.hawkbit.repository.jpa.rsql.RsqlUtility;
import org.eclipse.hawkbit.repository.jpa.specifications.DistributionSetTypeSpecification;
import org.eclipse.hawkbit.repository.jpa.utils.QuotaHelper;
import org.eclipse.hawkbit.repository.model.DistributionSetType;
@@ -198,7 +198,7 @@ public class JpaDistributionSetTypeManagement implements DistributionSetTypeMana
@Override
public Page<DistributionSetType> findByRsql(final String rsql, final Pageable pageable) {
return JpaManagementHelper.findAllWithCountBySpec(distributionSetTypeRepository, List.of(
RSQLUtility.buildRsqlSpecification(rsql, DistributionSetTypeFields.class, virtualPropertyReplacer, database),
RsqlUtility.buildRsqlSpecification(rsql, DistributionSetTypeFields.class, virtualPropertyReplacer, database),
DistributionSetTypeSpecification.isNotDeleted()), pageable);
}

View File

@@ -40,7 +40,7 @@ import org.eclipse.hawkbit.repository.jpa.repository.ActionRepository;
import org.eclipse.hawkbit.repository.jpa.repository.RolloutGroupRepository;
import org.eclipse.hawkbit.repository.jpa.repository.RolloutRepository;
import org.eclipse.hawkbit.repository.jpa.repository.TargetRepository;
import org.eclipse.hawkbit.repository.jpa.rsql.RSQLUtility;
import org.eclipse.hawkbit.repository.jpa.rsql.RsqlUtility;
import org.eclipse.hawkbit.repository.jpa.specifications.TargetSpecifications;
import org.eclipse.hawkbit.repository.model.Rollout;
import org.eclipse.hawkbit.repository.model.Rollout.RolloutStatus;
@@ -122,7 +122,7 @@ public class JpaRolloutGroupManagement implements RolloutGroupManagement {
throwEntityNotFoundExceptionIfRolloutDoesNotExist(rolloutId);
final List<Specification<JpaRolloutGroup>> specList = Arrays.asList(
RSQLUtility.buildRsqlSpecification(rsql, RolloutGroupFields.class, virtualPropertyReplacer,
RsqlUtility.buildRsqlSpecification(rsql, RolloutGroupFields.class, virtualPropertyReplacer,
database),
(root, query, cb) -> cb.equal(root.get(JpaRolloutGroup_.rollout).get(AbstractJpaBaseEntity_.id), rolloutId));
@@ -189,7 +189,7 @@ public class JpaRolloutGroupManagement implements RolloutGroupManagement {
throwExceptionIfRolloutGroupDoesNotExist(rolloutGroupId);
final List<Specification<JpaTarget>> specList = Arrays.asList(
RSQLUtility.buildRsqlSpecification(rsql, TargetFields.class, virtualPropertyReplacer, database),
RsqlUtility.buildRsqlSpecification(rsql, TargetFields.class, virtualPropertyReplacer, database),
(root, query, cb) -> {
final ListJoin<JpaTarget, RolloutTargetGroup> rolloutTargetJoin = root.join(JpaTarget_.rolloutTargetGroup);
return cb.equal(rolloutTargetJoin.get(RolloutTargetGroup_.rolloutGroup).get(AbstractJpaBaseEntity_.id), rolloutGroupId);

View File

@@ -63,7 +63,7 @@ import org.eclipse.hawkbit.repository.jpa.repository.ActionRepository;
import org.eclipse.hawkbit.repository.jpa.repository.RolloutGroupRepository;
import org.eclipse.hawkbit.repository.jpa.repository.RolloutRepository;
import org.eclipse.hawkbit.repository.jpa.rollout.condition.StartNextGroupRolloutGroupSuccessAction;
import org.eclipse.hawkbit.repository.jpa.rsql.RSQLUtility;
import org.eclipse.hawkbit.repository.jpa.rsql.RsqlUtility;
import org.eclipse.hawkbit.repository.jpa.specifications.RolloutSpecification;
import org.eclipse.hawkbit.repository.jpa.utils.QuotaHelper;
import org.eclipse.hawkbit.repository.jpa.utils.WeightValidationHelper;
@@ -273,7 +273,7 @@ public class JpaRolloutManagement implements RolloutManagement {
@Override
public Page<Rollout> findByRsql(final String rsql, final boolean deleted, final Pageable pageable) {
final List<Specification<JpaRollout>> specList = List.of(
RSQLUtility.buildRsqlSpecification(rsql, RolloutFields.class, virtualPropertyReplacer, database),
RsqlUtility.buildRsqlSpecification(rsql, RolloutFields.class, virtualPropertyReplacer, database),
RolloutSpecification.isDeleted(deleted, pageable.getSort()));
return JpaManagementHelper.convertPage(rolloutRepository.findAll(JpaManagementHelper.combineWithAnd(specList), pageable), pageable);
}
@@ -281,7 +281,7 @@ public class JpaRolloutManagement implements RolloutManagement {
@Override
public Page<Rollout> findByRsqlWithDetailedStatus(final String rsql, final boolean deleted, final Pageable pageable) {
final List<Specification<JpaRollout>> specList = List.of(
RSQLUtility.buildRsqlSpecification(rsql, RolloutFields.class, virtualPropertyReplacer, database),
RsqlUtility.buildRsqlSpecification(rsql, RolloutFields.class, virtualPropertyReplacer, database),
RolloutSpecification.isDeleted(deleted, pageable.getSort()));
return appendStatusDetails(JpaManagementHelper.convertPage(
rolloutRepository.findAll(JpaManagementHelper.combineWithAnd(specList), JpaRollout_.GRAPH_ROLLOUT_DS, pageable), pageable));

View File

@@ -50,7 +50,7 @@ import org.eclipse.hawkbit.repository.jpa.repository.DistributionSetRepository;
import org.eclipse.hawkbit.repository.jpa.repository.SoftwareModuleMetadataRepository;
import org.eclipse.hawkbit.repository.jpa.repository.SoftwareModuleRepository;
import org.eclipse.hawkbit.repository.jpa.repository.SoftwareModuleTypeRepository;
import org.eclipse.hawkbit.repository.jpa.rsql.RSQLUtility;
import org.eclipse.hawkbit.repository.jpa.rsql.RsqlUtility;
import org.eclipse.hawkbit.repository.jpa.specifications.SoftwareModuleSpecification;
import org.eclipse.hawkbit.repository.jpa.utils.QuotaHelper;
import org.eclipse.hawkbit.repository.model.Artifact;
@@ -261,7 +261,7 @@ public class JpaSoftwareModuleManagement implements SoftwareModuleManagement {
@Override
public Page<SoftwareModule> findByRsql(final String rsql, final Pageable pageable) {
return JpaManagementHelper.findAllWithCountBySpec(softwareModuleRepository, List.of(
RSQLUtility.buildRsqlSpecification(rsql, SoftwareModuleFields.class, virtualPropertyReplacer,
RsqlUtility.buildRsqlSpecification(rsql, SoftwareModuleFields.class, virtualPropertyReplacer,
database),
SoftwareModuleSpecification.isNotDeleted()), pageable);
}

View File

@@ -28,7 +28,7 @@ import org.eclipse.hawkbit.repository.jpa.model.JpaSoftwareModuleType;
import org.eclipse.hawkbit.repository.jpa.repository.DistributionSetTypeRepository;
import org.eclipse.hawkbit.repository.jpa.repository.SoftwareModuleRepository;
import org.eclipse.hawkbit.repository.jpa.repository.SoftwareModuleTypeRepository;
import org.eclipse.hawkbit.repository.jpa.rsql.RSQLUtility;
import org.eclipse.hawkbit.repository.jpa.rsql.RsqlUtility;
import org.eclipse.hawkbit.repository.jpa.specifications.SoftwareModuleTypeSpecification;
import org.eclipse.hawkbit.repository.model.SoftwareModuleType;
import org.eclipse.hawkbit.repository.rsql.VirtualPropertyReplacer;
@@ -144,7 +144,7 @@ public class JpaSoftwareModuleTypeManagement implements SoftwareModuleTypeManage
@Override
public Page<SoftwareModuleType> findByRsql(final String rsql, final Pageable pageable) {
return JpaManagementHelper.findAllWithCountBySpec(softwareModuleTypeRepository, List.of(
RSQLUtility.buildRsqlSpecification(rsql, SoftwareModuleTypeFields.class,
RsqlUtility.buildRsqlSpecification(rsql, SoftwareModuleTypeFields.class,
virtualPropertyReplacer, database),
SoftwareModuleTypeSpecification.isNotDeleted()), pageable
);

View File

@@ -43,7 +43,7 @@ import org.eclipse.hawkbit.repository.jpa.model.JpaDistributionSet;
import org.eclipse.hawkbit.repository.jpa.model.JpaTarget;
import org.eclipse.hawkbit.repository.jpa.model.JpaTargetFilterQuery;
import org.eclipse.hawkbit.repository.jpa.repository.TargetFilterQueryRepository;
import org.eclipse.hawkbit.repository.jpa.rsql.RSQLUtility;
import org.eclipse.hawkbit.repository.jpa.rsql.RsqlUtility;
import org.eclipse.hawkbit.repository.jpa.specifications.TargetFilterQuerySpecification;
import org.eclipse.hawkbit.repository.jpa.utils.QuotaHelper;
import org.eclipse.hawkbit.repository.jpa.utils.WeightValidationHelper;
@@ -119,7 +119,7 @@ public class JpaTargetFilterQueryManagement implements TargetFilterQueryManageme
create.getQuery().ifPresent(query -> {
// validate the RSQL query syntax
RSQLUtility.validateRsqlFor(query, TargetFields.class, JpaTarget.class, virtualPropertyReplacer, entityManager);
RsqlUtility.validateRsqlFor(query, TargetFields.class, JpaTarget.class, virtualPropertyReplacer, entityManager);
// enforce the 'max targets per auto assign' quota right here even
// if the result of the filter query can vary over time
@@ -148,7 +148,7 @@ public class JpaTargetFilterQueryManagement implements TargetFilterQueryManageme
@Override
public boolean verifyTargetFilterQuerySyntax(final String query) {
try {
RSQLUtility.validateRsqlFor(query, TargetFields.class, JpaTarget.class, virtualPropertyReplacer, entityManager);
RsqlUtility.validateRsqlFor(query, TargetFields.class, JpaTarget.class, virtualPropertyReplacer, entityManager);
return true;
} catch (final RSQLParserException | RSQLParameterUnsupportedFieldException e) {
log.debug("The RSQL query '{}}' is invalid.", query, e);
@@ -195,7 +195,7 @@ public class JpaTargetFilterQueryManagement implements TargetFilterQueryManageme
@Override
public Page<TargetFilterQuery> findByRsql(final String rsqlFilter, final Pageable pageable) {
final List<Specification<JpaTargetFilterQuery>> specList = !ObjectUtils.isEmpty(rsqlFilter)
? Collections.singletonList(RSQLUtility.buildRsqlSpecification(rsqlFilter,
? Collections.singletonList(RsqlUtility.buildRsqlSpecification(rsqlFilter,
TargetFilterQueryFields.class, virtualPropertyReplacer, database))
: Collections.emptyList();
@@ -227,7 +227,7 @@ public class JpaTargetFilterQueryManagement implements TargetFilterQueryManageme
final List<Specification<JpaTargetFilterQuery>> specList = new ArrayList<>(2);
specList.add(TargetFilterQuerySpecification.byAutoAssignDS(distributionSet));
if (!ObjectUtils.isEmpty(rsql)) {
specList.add(RSQLUtility.buildRsqlSpecification(rsql, TargetFilterQueryFields.class,
specList.add(RsqlUtility.buildRsqlSpecification(rsql, TargetFilterQueryFields.class,
virtualPropertyReplacer, database));
}

View File

@@ -61,7 +61,7 @@ import org.eclipse.hawkbit.repository.jpa.repository.TargetFilterQueryRepository
import org.eclipse.hawkbit.repository.jpa.repository.TargetRepository;
import org.eclipse.hawkbit.repository.jpa.repository.TargetTagRepository;
import org.eclipse.hawkbit.repository.jpa.repository.TargetTypeRepository;
import org.eclipse.hawkbit.repository.jpa.rsql.RSQLUtility;
import org.eclipse.hawkbit.repository.jpa.rsql.RsqlUtility;
import org.eclipse.hawkbit.repository.jpa.specifications.SpecificationsBuilder;
import org.eclipse.hawkbit.repository.jpa.specifications.TargetSpecifications;
import org.eclipse.hawkbit.repository.jpa.utils.QuotaHelper;
@@ -157,13 +157,13 @@ public class JpaTargetManagement implements TargetManagement {
public long countByRsql(final String rsql) {
return JpaManagementHelper.countBySpec(
targetRepository,
List.of(RSQLUtility.buildRsqlSpecification(rsql, TargetFields.class, virtualPropertyReplacer, database)));
List.of(RsqlUtility.buildRsqlSpecification(rsql, TargetFields.class, virtualPropertyReplacer, database)));
}
@Override
public long countByRsqlAndUpdatable(String rsql) {
final List<Specification<JpaTarget>> specList = List.of(
RSQLUtility.buildRsqlSpecification(rsql, TargetFields.class, virtualPropertyReplacer, database));
RsqlUtility.buildRsqlSpecification(rsql, TargetFields.class, virtualPropertyReplacer, database));
return targetRepository.count(
AccessController.Operation.UPDATE,
combineWithAnd(specList));
@@ -172,7 +172,7 @@ public class JpaTargetManagement implements TargetManagement {
@Override
public long countByRsqlAndCompatible(final String rsql, final Long distributionSetIdTypeId) {
final List<Specification<JpaTarget>> specList = List.of(
RSQLUtility.buildRsqlSpecification(rsql, TargetFields.class, virtualPropertyReplacer, database),
RsqlUtility.buildRsqlSpecification(rsql, TargetFields.class, virtualPropertyReplacer, database),
TargetSpecifications.isCompatibleWithDistributionSetType(distributionSetIdTypeId));
return JpaManagementHelper.countBySpec(targetRepository, specList);
}
@@ -180,7 +180,7 @@ public class JpaTargetManagement implements TargetManagement {
@Override
public long countByRsqlAndCompatibleAndUpdatable(String rsql, Long distributionSetIdTypeId) {
final List<Specification<JpaTarget>> specList = List.of(
RSQLUtility.buildRsqlSpecification(rsql, TargetFields.class, virtualPropertyReplacer, database),
RsqlUtility.buildRsqlSpecification(rsql, TargetFields.class, virtualPropertyReplacer, database),
TargetSpecifications.isCompatibleWithDistributionSetType(distributionSetIdTypeId));
return targetRepository.count(AccessController.Operation.UPDATE, combineWithAnd(specList));
}
@@ -253,7 +253,7 @@ public class JpaTargetManagement implements TargetManagement {
.findAllWithoutCount(
AccessController.Operation.UPDATE,
combineWithAnd(List.of(
RSQLUtility.buildRsqlSpecification(rsql, TargetFields.class, virtualPropertyReplacer, database),
RsqlUtility.buildRsqlSpecification(rsql, TargetFields.class, virtualPropertyReplacer, database),
TargetSpecifications.hasNotDistributionSetInActions(distributionSetId),
TargetSpecifications.isCompatibleWithDistributionSetType(distSetTypeId))),
pageable)
@@ -268,7 +268,7 @@ public class JpaTargetManagement implements TargetManagement {
return targetRepository.count(
AccessController.Operation.UPDATE,
combineWithAnd(List.of(
RSQLUtility.buildRsqlSpecification(
RsqlUtility.buildRsqlSpecification(
rsql, TargetFields.class, virtualPropertyReplacer, database),
TargetSpecifications.hasNotDistributionSetInActions(distributionSetId),
TargetSpecifications.isCompatibleWithDistributionSetType(distSetTypeId))));
@@ -280,7 +280,7 @@ public class JpaTargetManagement implements TargetManagement {
return targetRepository
.findAllWithoutCount(AccessController.Operation.UPDATE,
combineWithAnd(List.of(
RSQLUtility.buildRsqlSpecification(rsql, TargetFields.class, virtualPropertyReplacer, database),
RsqlUtility.buildRsqlSpecification(rsql, TargetFields.class, virtualPropertyReplacer, database),
TargetSpecifications.isNotInRolloutGroups(groups),
TargetSpecifications.isCompatibleWithDistributionSetType(dsType.getId()))),
pageable)
@@ -292,7 +292,7 @@ public class JpaTargetManagement implements TargetManagement {
final String rsql, final Collection<Long> groups, final DistributionSetType dsType) {
return targetRepository.count(AccessController.Operation.UPDATE,
combineWithAnd(List.of(
RSQLUtility.buildRsqlSpecification(rsql, TargetFields.class, virtualPropertyReplacer, database),
RsqlUtility.buildRsqlSpecification(rsql, TargetFields.class, virtualPropertyReplacer, database),
TargetSpecifications.isNotInRolloutGroups(groups),
TargetSpecifications.isCompatibleWithDistributionSetType(dsType.getId()))));
}
@@ -319,7 +319,7 @@ public class JpaTargetManagement implements TargetManagement {
return targetRepository
.findAllWithoutCount(AccessController.Operation.UPDATE,
combineWithAnd(List.of(
RSQLUtility.buildRsqlSpecification(rsql, TargetFields.class, virtualPropertyReplacer, database),
RsqlUtility.buildRsqlSpecification(rsql, TargetFields.class, virtualPropertyReplacer, database),
TargetSpecifications.hasNoOverridingActionsAndNotInRollout(rolloutId),
TargetSpecifications.isCompatibleWithDistributionSetType(distributionSetType.getId()))),
pageable)
@@ -355,7 +355,7 @@ public class JpaTargetManagement implements TargetManagement {
final DistributionSet validDistSet = distributionSetManagement.getOrElseThrowException(distributionSetId);
final List<Specification<JpaTarget>> specList = List.of(
RSQLUtility.buildRsqlSpecification(rsql, TargetFields.class, virtualPropertyReplacer, database),
RsqlUtility.buildRsqlSpecification(rsql, TargetFields.class, virtualPropertyReplacer, database),
TargetSpecifications.hasAssignedDistributionSet(validDistSet.getId()));
return JpaManagementHelper.findAllWithCountBySpec(targetRepository, specList, pageable);
@@ -395,7 +395,7 @@ public class JpaTargetManagement implements TargetManagement {
final DistributionSet validDistSet = distributionSetManagement.getOrElseThrowException(distributionSetId);
final List<Specification<JpaTarget>> specList = List.of(
RSQLUtility.buildRsqlSpecification(rsql, TargetFields.class, virtualPropertyReplacer, database),
RsqlUtility.buildRsqlSpecification(rsql, TargetFields.class, virtualPropertyReplacer, database),
TargetSpecifications.hasInstalledDistributionSet(validDistSet.getId()));
return JpaManagementHelper.findAllWithCountBySpec(targetRepository, specList, pageable);
@@ -416,7 +416,7 @@ public class JpaTargetManagement implements TargetManagement {
public Slice<Target> findByRsql(final String rsql, final Pageable pageable) {
return JpaManagementHelper.findAllWithoutCountBySpec(
targetRepository,
List.of(RSQLUtility.buildRsqlSpecification(rsql, TargetFields.class, virtualPropertyReplacer, database)), pageable
List.of(RsqlUtility.buildRsqlSpecification(rsql, TargetFields.class, virtualPropertyReplacer, database)), pageable
);
}
@@ -426,7 +426,7 @@ public class JpaTargetManagement implements TargetManagement {
.orElseThrow(() -> new EntityNotFoundException(TargetFilterQuery.class, targetFilterQueryId));
return JpaManagementHelper.findAllWithoutCountBySpec(
targetRepository, List.of(RSQLUtility.buildRsqlSpecification(
targetRepository, List.of(RsqlUtility.buildRsqlSpecification(
targetFilterQuery.getQuery(), TargetFields.class, virtualPropertyReplacer, database)), pageable
);
}
@@ -442,7 +442,7 @@ public class JpaTargetManagement implements TargetManagement {
throwEntityNotFoundExceptionIfTagDoesNotExist(tagId);
final List<Specification<JpaTarget>> specList = List.of(
RSQLUtility.buildRsqlSpecification(rsql, TargetFields.class, virtualPropertyReplacer, database),
RsqlUtility.buildRsqlSpecification(rsql, TargetFields.class, virtualPropertyReplacer, database),
TargetSpecifications.hasTag(tagId));
return JpaManagementHelper.findAllWithCountBySpec(targetRepository, specList, pageable);
@@ -611,12 +611,12 @@ public class JpaTargetManagement implements TargetManagement {
@Override
public boolean isTargetMatchingQueryAndDSNotAssignedAndCompatibleAndUpdatable(
final String controllerId, final long distributionSetId, final String targetFilterQuery) {
RSQLUtility.validateRsqlFor(targetFilterQuery, TargetFields.class, JpaTarget.class, virtualPropertyReplacer, entityManager);
RsqlUtility.validateRsqlFor(targetFilterQuery, TargetFields.class, JpaTarget.class, virtualPropertyReplacer, entityManager);
final DistributionSet ds = distributionSetManagement.get(distributionSetId)
.orElseThrow(() -> new EntityNotFoundException(DistributionSet.class, distributionSetId));
final Long distSetTypeId = ds.getType().getId();
final List<Specification<JpaTarget>> specList = List.of(
RSQLUtility.buildRsqlSpecification(targetFilterQuery, TargetFields.class, virtualPropertyReplacer, database),
RsqlUtility.buildRsqlSpecification(targetFilterQuery, TargetFields.class, virtualPropertyReplacer, database),
TargetSpecifications.hasNotDistributionSetInActions(distributionSetId),
TargetSpecifications.isCompatibleWithDistributionSetType(distSetTypeId),
TargetSpecifications.hasControllerId(controllerId));

View File

@@ -27,7 +27,7 @@ import org.eclipse.hawkbit.repository.jpa.configuration.Constants;
import org.eclipse.hawkbit.repository.jpa.model.AbstractJpaNamedEntity_;
import org.eclipse.hawkbit.repository.jpa.model.JpaTargetTag;
import org.eclipse.hawkbit.repository.jpa.repository.TargetTagRepository;
import org.eclipse.hawkbit.repository.jpa.rsql.RSQLUtility;
import org.eclipse.hawkbit.repository.jpa.rsql.RsqlUtility;
import org.eclipse.hawkbit.repository.model.TargetTag;
import org.eclipse.hawkbit.repository.rsql.VirtualPropertyReplacer;
import org.springframework.dao.ConcurrencyFailureException;
@@ -105,7 +105,7 @@ public class JpaTargetTagManagement implements TargetTagManagement {
@Override
public Page<TargetTag> findByRsql(final String rsql, final Pageable pageable) {
return JpaManagementHelper.findAllWithCountBySpec(targetTagRepository, Collections.singletonList(
RSQLUtility.buildRsqlSpecification(rsql, TargetTagFields.class, virtualPropertyReplacer, database)), pageable);
RsqlUtility.buildRsqlSpecification(rsql, TargetTagFields.class, virtualPropertyReplacer, database)), pageable);
}
@Override

View File

@@ -33,7 +33,7 @@ import org.eclipse.hawkbit.repository.jpa.model.JpaTargetType;
import org.eclipse.hawkbit.repository.jpa.repository.DistributionSetTypeRepository;
import org.eclipse.hawkbit.repository.jpa.repository.TargetRepository;
import org.eclipse.hawkbit.repository.jpa.repository.TargetTypeRepository;
import org.eclipse.hawkbit.repository.jpa.rsql.RSQLUtility;
import org.eclipse.hawkbit.repository.jpa.rsql.RsqlUtility;
import org.eclipse.hawkbit.repository.jpa.specifications.TargetTypeSpecification;
import org.eclipse.hawkbit.repository.jpa.utils.QuotaHelper;
import org.eclipse.hawkbit.repository.model.DistributionSetType;
@@ -146,7 +146,7 @@ public class JpaTargetTypeManagement implements TargetTypeManagement {
@Override
public Page<TargetType> findByRsql(final String rsql, final Pageable pageable) {
return JpaManagementHelper.findAllWithCountBySpec(targetTypeRepository, List.of(
RSQLUtility.buildRsqlSpecification(
RsqlUtility.buildRsqlSpecification(
rsql, TargetTypeFields.class, virtualPropertyReplacer, database)), pageable
);
}

View File

@@ -1,211 +0,0 @@
/**
* Copyright (c) 2025 Contributors to the Eclipse Foundation
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.eclipse.hawkbit.repository.jpa.rsql;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import jakarta.annotation.Nonnull;
import jakarta.annotation.Nullable;
import lombok.Builder;
import lombok.Value;
/**
* Node is always in Disjunctive Normal Form (DNF). This means:
* <ul>
* <li>single comparison</li>
* <li>and composition of comparisons</li>
* <li>or of comparisons or and composition of comparisons</li>
* </ul>
* Node: if there are custom implementation they shall also follow this contract!
*/
public interface Node {
default Logical and(final Node other) {
return op(this, Logical.Operator.AND, other);
}
default Logical or(final Node other) {
return op(this, Logical.Operator.OR, other);
}
static Logical op(final Node node1, final Logical.Operator op, final Node node2) {
if (node1 instanceof Logical logical1 && logical1.getOp() == op) {
if (node2 instanceof Logical logical2 && logical2.getOp() == op) { // same op
final List<Node> children = new ArrayList<>(logical1.getChildren().size() + logical2.getChildren().size());
children.addAll(logical1.getChildren());
children.addAll(logical2.getChildren());
return new Logical(op, children);
} else { // node2 is not a logical
final List<Node> children = new ArrayList<>(logical1.getChildren().size() + 1);
children.addAll(logical1.getChildren());
children.add(node2);
return new Logical(op, children);
}
} else if (node2 instanceof Logical logical2 && logical2.getOp() == op) {
final List<Node> children = new ArrayList<>(logical2.getChildren().size() + 1);
children.add(node1);
children.addAll(logical2.getChildren());
return new Logical(op, children);
} else {
return new Logical(op, List.of(node1, node2));
}
}
@Value
class Logical implements Node {
public enum Operator {
AND("&&"),
OR("||");
private final String symbol;
Operator(String symbol) {
this.symbol = symbol;
}
@Override
public String toString() {
return symbol;
}
}
Logical.Operator op;
List<Node> children;
public Logical(final Operator op, final List<Node> children) {
Objects.requireNonNull(op, "Operator must not be null");
if (children == null || children.size() < 2) {
throw new IllegalArgumentException("Children of a logical must not be null or empty or single element list");
}
this.op = op;
this.children = children;
}
@Override
public String toString() {
return children.stream()
.map(child -> op == Operator.OR || child instanceof Comparison
? child.toString()
: "(" + child.toString() + ")")
.reduce((a, b) -> a + " " + op + " " + b)
.orElse("");
}
}
@Value
@Builder(builderClassName = "Builder")
class Comparison implements Node {
public enum Operator {
EQ("=="),
NE("!="),
GT(">"),
LT("<"),
GTE(">="),
LTE("<="),
IN("in"),
NOT_IN("not in"),
LIKE("like"),
NOT_LIKE("not like");
private final String symbol;
Operator(String symbol) {
this.symbol = symbol;
}
@Override
public String toString() {
return symbol;
}
}
@Nonnull
String key;
@Nonnull
Operator op;
@Nullable
Object value; // String, Number, Boolean, String[] or Number[] or null
@Nullable
Object context;
public Comparison(@Nonnull final String key, @Nonnull final Operator op, @Nullable final Object value) {
this(key, op, value, null);
}
public Comparison(@Nonnull final String key, @Nonnull final Operator op, @Nullable final Object value, @Nullable final Object context) {
Objects.requireNonNull(key, "Key must not be null");
if (key.trim().isEmpty()) {
throw new IllegalArgumentException("Key must not be empty");
}
Objects.requireNonNull(op, "Operator must not be null");
if (value == null) {
switch (op) {
case GT, LT, LIKE, NOT_LIKE:
throw new IllegalArgumentException("Value must not be null for operator " + op);
default: // ok
}
} else if (value instanceof List<?> list) {
switch (op) {
case IN, NOT_IN:
break;
default:
throw new IllegalArgumentException("Value must not be a list for operator " + op);
}
if (list.isEmpty()) {
throw new IllegalArgumentException("List value must not be empty");
}
list.forEach(listElement -> {
if (notValid(listElement)) {
throw new IllegalArgumentException("List type value must be String or Number");
}
});
} else if (notValid(value)) {
throw new IllegalArgumentException(
"Value must be String, Number, Boolean, non-empty String list, non-empty Number list or null");
}
this.key = key.trim();
this.op = op;
this.value = value;
this.context = context;
}
@Override
public String toString() {
return key + " " + op + " " + valueToString(value);
}
private static String valueToString(final Object value) {
if (value == null) {
return "null";
} else if (value instanceof String) {
return String.format("\"%s\"", value);
} else if (value instanceof List<?> list) {
return "(" + list.stream()
.map(Comparison::valueToString)
.reduce((a, b) -> a + ", " + b)
.orElse("") + ")";
} else {
return value.toString();
}
}
private static boolean notValid(final Object value) {
return !(value instanceof String) && !(value instanceof Number) && !(value instanceof Boolean);
}
}
}

View File

@@ -1,118 +0,0 @@
/**
* Copyright (c) 2015 Bosch Software Innovations GmbH and others
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.eclipse.hawkbit.repository.jpa.rsql;
import static org.eclipse.hawkbit.repository.rsql.RsqlConfigHolder.RsqlToSpecBuilder.G3;
import jakarta.persistence.EntityManager;
import jakarta.persistence.criteria.CriteriaBuilder;
import jakarta.persistence.criteria.CriteriaQuery;
import cz.jirutka.rsql.parser.RSQLParserException;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.text.StrLookup;
import org.eclipse.hawkbit.repository.RsqlQueryField;
import org.eclipse.hawkbit.repository.exception.RSQLParameterSyntaxException;
import org.eclipse.hawkbit.repository.exception.RSQLParameterUnsupportedFieldException;
import org.eclipse.hawkbit.repository.jpa.rsqllegacy.SpecificationBuilderLegacy;
import org.eclipse.hawkbit.repository.rsql.RsqlConfigHolder;
import org.eclipse.hawkbit.repository.rsql.VirtualPropertyReplacer;
import org.eclipse.hawkbit.repository.rsql.VirtualPropertyResolver;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.orm.jpa.vendor.Database;
/**
* A utility class which is able to parse RSQL strings into an spring data
* {@link Specification} which then can be enhanced sql queries to filter
* entities. RSQL parser library: https://github.com/jirutka/rsql-parser
*
* <ul>
* <li>{@code Equal to : ==}</li>
* <li>{@code Not equal to : !=}</li>
* <li>{@code Less than : =lt= or <}</li>
* <li>{@code Less than or equal to : =le= or <=}</li>
* <li>{@code Greater than operator : =gt= or >}</li>
* <li>{@code Greater than or equal to : =ge= or >=}</li>
* </ul>
* <p>
* Examples of RSQL expressions in both FIQL-like and alternative notation:
* <ul>
* <li>{@code version==2.0.0}</li>
* <li>{@code name==targetId1;description==plugAndPlay}</li>
* <li>{@code name==targetId1 and description==plugAndPlay}</li>
* <li>{@code name==targetId1,description==plugAndPlay,updateStatus==UNKNOWN}</li>
* <li>{@code name==targetId1 or description==plugAndPlay or updateStatus==UNKNOWN}</li>
* </ul>
* <p>
* There is also a mechanism that allows to refer to known macros that can
* resolved by an optional {@link StrLookup} (cp.
* {@link VirtualPropertyResolver}).<br>
* An example that queries for all overdue targets using the ${OVERDUE_TS}
* placeholder introduced by {@link VirtualPropertyResolver} looks like
* this:<br>
* <em>lastControllerRequestAt=le=${OVERDUE_TS}</em><br>
* It is possible to escape a macro expression by using a second '$':
* $${OVERDUE_TS} would prevent the ${OVERDUE_TS} token from being expanded.
*/
@Slf4j
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public final class RSQLUtility {
/**
* Builds a JPA {@link Specification} which corresponds with the given RSQL query. The specification can be used to filter for JPA entities
* with the given RSQL query.
*
* @param rsql the rsql query to be parsed
* @param rsqlQueryFieldType the enum class type which implements the {@link RsqlQueryField}
* @param virtualPropertyReplacer holds the logic how the known macros have to be resolved; may be <code>null</code>
* @param database database in use
* @return a specification which can be used with JPA
* @throws RSQLParameterUnsupportedFieldException if a field in the RSQL string is used but not provided by the
* given {@code fieldNameProvider}
* @throws RSQLParameterSyntaxException if the RSQL syntax is wrong
*/
public static <A extends Enum<A> & RsqlQueryField, T> Specification<T> buildRsqlSpecification(
final String rsql, final Class<A> rsqlQueryFieldType,
final VirtualPropertyReplacer virtualPropertyReplacer, final Database database) {
if (RsqlConfigHolder.getInstance().getRsqlToSpecBuilder() == G3) {
return new SpecificationBuilder<T>(
virtualPropertyReplacer,
!RsqlConfigHolder.getInstance().isCaseInsensitiveDB() && RsqlConfigHolder.getInstance().isIgnoreCase(),
database)
.specification(RsqlParser.parse(
RsqlConfigHolder.getInstance().isCaseInsensitiveDB() || RsqlConfigHolder.getInstance().isIgnoreCase()
? rsql.toLowerCase() : rsql,
rsqlQueryFieldType));
} else {
return new SpecificationBuilderLegacy<A, T>(rsqlQueryFieldType, virtualPropertyReplacer, database).specification(rsql);
}
}
/**
* Validates the RSQL string
*
* @param rsql RSQL string to validate
* @param rsqlQueryFieldType
* @throws RSQLParserException if RSQL syntax is invalid
* @throws RSQLParameterUnsupportedFieldException if RSQL key is not allowed
*/
@SuppressWarnings({ "unchecked", "rawtypes" })
public static <A extends Enum<A> & RsqlQueryField> void validateRsqlFor(
final String rsql, final Class<A> rsqlQueryFieldType,
final Class<?> jpaType,
final VirtualPropertyReplacer virtualPropertyReplacer, final EntityManager entityManager) {
final CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
final CriteriaQuery<?> criteriaQuery = criteriaBuilder.createQuery(jpaType);
buildRsqlSpecification(rsql, rsqlQueryFieldType, virtualPropertyReplacer, null)
.toPredicate(criteriaQuery.from((Class) jpaType), criteriaQuery, criteriaBuilder);
}
}

View File

@@ -1,270 +0,0 @@
/**
* Copyright (c) 2025 Contributors to the Eclipse Foundation
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.eclipse.hawkbit.repository.jpa.rsql;
import static org.eclipse.hawkbit.repository.RsqlQueryField.SUB_ATTRIBUTE_SEPARATOR;
import static org.eclipse.hawkbit.repository.RsqlQueryField.SUB_ATTRIBUTE_SPLIT_REGEX;
import static org.eclipse.hawkbit.repository.jpa.rsql.Node.Comparison.Operator.EQ;
import static org.eclipse.hawkbit.repository.jpa.rsql.Node.Comparison.Operator.GT;
import static org.eclipse.hawkbit.repository.jpa.rsql.Node.Comparison.Operator.GTE;
import static org.eclipse.hawkbit.repository.jpa.rsql.Node.Comparison.Operator.IN;
import static org.eclipse.hawkbit.repository.jpa.rsql.Node.Comparison.Operator.LIKE;
import static org.eclipse.hawkbit.repository.jpa.rsql.Node.Comparison.Operator.LT;
import static org.eclipse.hawkbit.repository.jpa.rsql.Node.Comparison.Operator.LTE;
import static org.eclipse.hawkbit.repository.jpa.rsql.Node.Comparison.Operator.NE;
import static org.eclipse.hawkbit.repository.jpa.rsql.Node.Comparison.Operator.NOT_IN;
import static org.eclipse.hawkbit.repository.jpa.rsql.Node.Comparison.Operator.NOT_LIKE;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.function.Function;
import java.util.function.UnaryOperator;
import jakarta.annotation.Nullable;
import cz.jirutka.rsql.parser.RSQLParser;
import cz.jirutka.rsql.parser.RSQLParserException;
import cz.jirutka.rsql.parser.ast.AndNode;
import cz.jirutka.rsql.parser.ast.ComparisonNode;
import cz.jirutka.rsql.parser.ast.ComparisonOperator;
import cz.jirutka.rsql.parser.ast.OrNode;
import cz.jirutka.rsql.parser.ast.RSQLOperators;
import cz.jirutka.rsql.parser.ast.RSQLVisitor;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.hawkbit.repository.FieldValueConverter;
import org.eclipse.hawkbit.repository.RsqlQueryField;
import org.eclipse.hawkbit.repository.exception.RSQLParameterSyntaxException;
import org.eclipse.hawkbit.repository.exception.RSQLParameterUnsupportedFieldException;
import org.eclipse.hawkbit.repository.jpa.rsql.Node.Comparison;
/**
* {@link RsqlParser} parses RSQL query stings to {@link Node} objects. Doing that it does the following:
* <ul>
* <li>check for null value and pass it properly (IS, NOT)</li>
* <li>check for * and convert EQ/NEQ to LIKE/NOT_LIKE</li>
* <li>replace the rsql fields (enum values) with the JPA entity field names</li>
* <li>checks sub-attributes (if allowed)</li>
* <li>append the default sub-attributes if needed)</li>
* <li>apply value conversion for implementing with FieldValueConverter</li>
* </ul>
*/
@NoArgsConstructor(access = AccessLevel.PRIVATE)
@Slf4j
public class RsqlParser {
public static final Character LIKE_WILDCARD = '*';
public static final ComparisonOperator IS = new ComparisonOperator("=is=", "=eq=");
public static final ComparisonOperator NOT = new ComparisonOperator("=not=", "=ne=");
private static final RSQLParser PARSER;
static {
final Set<ComparisonOperator> operators = new HashSet<>(RSQLOperators.defaultOperators());
// == and != alternatives just treating "null" string as null not as a "null"
operators.add(IS);
operators.add(NOT);
PARSER = new RSQLParser(operators);
}
public static Node parse(final String rsql) {
return parse(rsql, (Function<String, Key>) null);
}
public static <T extends Enum<T> & RsqlQueryField> Node parse(final String rsql, final Class<T> rsqlQueryFieldType) {
return parse(rsql, rsqlQueryFieldType == null ? null : key -> resolveKey(key, rsqlQueryFieldType));
}
private static Node parse(final String rsql, final Function<String, Key> keyResolver) {
try {
return PARSER
.parse(rsql)
.accept(new RsqlVisitor(keyResolver));
} catch (final RSQLParserException e) {
throw new RSQLParameterSyntaxException(e);
}
}
@SuppressWarnings("java:S3776") // java:S3776 - group in single method for easier read of whole logic
private static <T extends Enum<T> & RsqlQueryField> Key resolveKey(final String key, final Class<T> rsqlQueryFieldType) {
final int firstSeparatorIndex = key.indexOf(SUB_ATTRIBUTE_SEPARATOR);
final String enumName = (firstSeparatorIndex == -1 ? key : key.substring(0, firstSeparatorIndex)).toUpperCase();
log.debug("Get field identifier by name {} of enum type {}", enumName, rsqlQueryFieldType);
final T enumValue;
try {
enumValue = Enum.valueOf(rsqlQueryFieldType, enumName);
} catch (final IllegalArgumentException e) {
throw new RSQLParameterUnsupportedFieldException(e);
}
final String attribute;
if (firstSeparatorIndex == -1) { // just field name without sub-attribute
if (enumValue.getSubEntityAttributes().isEmpty()) {
// no sub-attributes -> simple field
attribute = enumValue.getJpaEntityFieldName();
} else {
// just enum name for a complex type (with sub-attributes), should have single (default!) sub-attribute
if (enumValue.isMap()) {
throw new RSQLParameterUnsupportedFieldException("No key specified for a map type " + enumValue);
} else if (enumValue.getSubEntityAttributes().size() == 1) {
// single sub attribute - so, treat it as a default
attribute = enumValue.getJpaEntityFieldName() + SUB_ATTRIBUTE_SEPARATOR + enumValue.getSubEntityAttributes().get(0);
} else {
throw new RSQLParameterUnsupportedFieldException(
String.format(
"The given search parameter field {%s} requires one of the following sub-attributes %s",
key, enumValue.getSubEntityAttributes()));
}
}
} else { // field name with sub-attribute
if (enumValue.isMap()) {
// map, the part after the enum name is the key of the map
attribute = enumValue.getJpaEntityFieldName() + SUB_ATTRIBUTE_SEPARATOR + key.substring(firstSeparatorIndex + 1);
} else if (enumValue.getSubEntityAttributes().isEmpty()) {
// simple type without sub-attributes, so the sub-attribute is not allowed
throw new RSQLParameterUnsupportedFieldException("Sub-attributes not supported for simple field " + enumValue);
} else {
final String[] subAttribute = key.substring(firstSeparatorIndex + 1).split(SUB_ATTRIBUTE_SPLIT_REGEX, 2);
attribute = enumValue.getJpaEntityFieldName() + SUB_ATTRIBUTE_SEPARATOR + enumValue.getSubEntityAttributes().stream()
.filter(attr -> attr.equalsIgnoreCase(subAttribute[0])) // case normalized
.findFirst()
.map(attr -> subAttribute.length == 1 ? attr : attr + key.substring(firstSeparatorIndex + 1 + attr.length()))
.orElseThrow(() -> new RSQLParameterUnsupportedFieldException(
String.format(
"The given search field {%s} has unsupported sub-attributes. Supported sub-attributes are %s",
key, enumValue.getSubEntityAttributes())));
}
}
return new Key(attribute, RsqlVisitor.valueConverter(enumValue));
}
private record Key(String path, UnaryOperator<Object> converter) {}
private static class RsqlVisitor implements RSQLVisitor<Node, String> {
private final Function<String, Key> keyResolver;
private RsqlVisitor(final Function<String, Key> keyResolver) {
this.keyResolver = keyResolver == null ? str -> new Key(str, UnaryOperator.identity()) : keyResolver;
}
@Override
public Node visit(final AndNode node, final String param) {
return node.getChildren().stream()
.map(child -> child.accept(this))
.reduce(Node::and)
.orElseThrow();
}
@Override
public Node visit(final OrNode node, final String param) {
return node.getChildren().stream()
.map(child -> child.accept(this))
.reduce(Node::or)
.orElseThrow();
}
@SuppressWarnings("java:S3776") // java:S3776 - group in single method for easier read of whole logic
@Override
public Node visit(final ComparisonNode node, final String param) {
final String nodeSelector = node.getSelector();
final Key key = keyResolver.apply(nodeSelector);
final String path = key.path();
final Object value = toValue(node, key);
final ComparisonOperator op = node.getOperator();
if (op == IS || op == RSQLOperators.EQUAL) {
if ("".equals(value)) {
// keep special backward compatible behaviour tags == '' means "null / has not or ''
return new Comparison(path, EQ, null, nodeSelector).or(new Comparison(path, EQ, "", nodeSelector));
}
return new Comparison(path, isLike(value) ? LIKE : EQ, value, nodeSelector);
} else if (op == NOT || op == RSQLOperators.NOT_EQUAL) {
if ("".equals(value)) {
// keep special backward compatible behaviour. != '' means "not null / has and not ''
return new Comparison(path, LIKE, "*", nodeSelector).and(new Comparison(path, NE, "", nodeSelector));
}
return new Comparison(path, isLike(value) ? NOT_LIKE : NE, value, nodeSelector);
} else if (op == RSQLOperators.GREATER_THAN) {
return new Comparison(path, GT, value, nodeSelector);
} else if (op == RSQLOperators.GREATER_THAN_OR_EQUAL) {
return new Comparison(path, GTE, value, nodeSelector);
} else if (op == RSQLOperators.LESS_THAN) {
return new Comparison(path, LT, value, nodeSelector);
} else if (op == RSQLOperators.LESS_THAN_OR_EQUAL) {
return new Comparison(path, LTE, value, nodeSelector);
} else if (op == RSQLOperators.IN) {
return new Comparison(path, IN, value, nodeSelector);
} else if (op == RSQLOperators.NOT_IN) {
return new Comparison(path, NOT_IN, value, nodeSelector);
} else {
throw new IllegalArgumentException("Unsupported operator: " + node.getOperator());
}
}
private Object toValue(final ComparisonNode node, final Key key) {
final List<String> arguments = node.getArguments();
final ComparisonOperator operator = node.getOperator();
if (arguments.isEmpty()) {
throw new IllegalArgumentException("Operator " + operator + " requires at least one argument");
}
if (arguments.size() == 1 && "null".equals(arguments.get(0)) && (operator == IS || operator == NOT)) {
return null;
} else {
if (operator == RSQLOperators.IN || operator == RSQLOperators.NOT_IN) {
return arguments.stream().map(key.converter()).toList();
} else if (arguments.size() > 1) {
throw new IllegalArgumentException(
"Operator " + operator + " requires exactly one argument, but got: " + arguments.size());
} else {
return key.converter().apply(arguments.get(0));
}
}
}
@SuppressWarnings({ "rawtypes", "unchecked" })
private static <T extends Enum<T>> UnaryOperator<Object> valueConverter(@Nullable final T enumValue) {
if (enumValue instanceof FieldValueConverter fieldValueConverter) {
return value -> {
if (value instanceof String strValue) {
try {
return fieldValueConverter.convertValue(enumValue, strValue);
} catch (final Exception e) {
throw new RSQLParameterUnsupportedFieldException(e.getMessage(), null);
}
} else {
return value;
}
};
} else {
return UnaryOperator.identity();
}
}
private static final String ESCAPE_CHAR_WITH_ASTERISK = "\\" + LIKE_WILDCARD;
private static boolean isLike(final Object value) {
if (value instanceof String valueStr) {
if (valueStr.contains(ESCAPE_CHAR_WITH_ASTERISK)) {
return valueStr.replace(ESCAPE_CHAR_WITH_ASTERISK, "$").indexOf(LIKE_WILDCARD) != -1;
} else {
return valueStr.indexOf(LIKE_WILDCARD) != -1;
}
} else {
return false;
}
}
}
}

View File

@@ -1,489 +0,0 @@
/**
* Copyright (c) 2025 Contributors to the Eclipse Foundation
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.eclipse.hawkbit.repository.jpa.rsql;
import static org.eclipse.hawkbit.repository.jpa.rsql.Node.Comparison.Operator.GT;
import static org.eclipse.hawkbit.repository.jpa.rsql.Node.Comparison.Operator.GTE;
import static org.eclipse.hawkbit.repository.jpa.rsql.Node.Comparison.Operator.IN;
import static org.eclipse.hawkbit.repository.jpa.rsql.Node.Comparison.Operator.LIKE;
import static org.eclipse.hawkbit.repository.jpa.rsql.Node.Comparison.Operator.LT;
import static org.eclipse.hawkbit.repository.jpa.rsql.Node.Comparison.Operator.LTE;
import static org.eclipse.hawkbit.repository.jpa.rsql.Node.Comparison.Operator.NE;
import static org.eclipse.hawkbit.repository.jpa.rsql.Node.Comparison.Operator.NOT_IN;
import static org.eclipse.hawkbit.repository.jpa.rsql.Node.Comparison.Operator.NOT_LIKE;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import jakarta.annotation.Nonnull;
import jakarta.persistence.criteria.CriteriaBuilder;
import jakarta.persistence.criteria.CriteriaQuery;
import jakarta.persistence.criteria.Expression;
import jakarta.persistence.criteria.Join;
import jakarta.persistence.criteria.JoinType;
import jakarta.persistence.criteria.MapJoin;
import jakarta.persistence.criteria.Path;
import jakarta.persistence.criteria.Predicate;
import jakarta.persistence.criteria.Root;
import jakarta.persistence.criteria.Subquery;
import jakarta.persistence.metamodel.Attribute;
import jakarta.persistence.metamodel.EntityType;
import jakarta.persistence.metamodel.MapAttribute;
import jakarta.persistence.metamodel.PluralAttribute;
import jakarta.persistence.metamodel.SetAttribute;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.hawkbit.repository.exception.RSQLParameterSyntaxException;
import org.eclipse.hawkbit.repository.exception.RSQLParameterUnsupportedFieldException;
import org.eclipse.hawkbit.repository.jpa.rsql.Node.Comparison;
import org.eclipse.hawkbit.repository.jpa.rsql.Node.Comparison.Operator;
import org.eclipse.hawkbit.repository.jpa.rsql.Node.Logical;
import org.eclipse.hawkbit.repository.rsql.VirtualPropertyReplacer;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.orm.jpa.vendor.Database;
import org.springframework.util.ObjectUtils;
@Slf4j
public class SpecificationBuilder<T> {
private static final char ESCAPE_CHAR = '\\';
private final VirtualPropertyReplacer virtualPropertyReplacer;
private final boolean ensureIgnoreCase;
private final Database database;
public SpecificationBuilder(
final VirtualPropertyReplacer virtualPropertyReplacer,
final boolean ensureIgnoreCase, final Database database) {
this.virtualPropertyReplacer = virtualPropertyReplacer;
this.ensureIgnoreCase = ensureIgnoreCase;
this.database = database;
}
public Specification<T> specification(final Node node) {
return (root, query, cb) -> {
Objects.requireNonNull(query).distinct(true);
return new PredicateBuilder(root, query, cb).build(node);
};
}
private class PredicateBuilder {
private static final char LIKE_WILDCARD = '*';
private static final String LIKE_WILDCARD_STR = String.valueOf(LIKE_WILDCARD);
private static final String ESCAPE_CHAR_WITH_ASTERISK = ESCAPE_CHAR + LIKE_WILDCARD_STR;
private static final Predicate[] PREDICATES_ARRAY_0 = new Predicate[0];
private static final List<Object> NULL_VALUE;
static {
final List<Object> nullList = new ArrayList<>(1);
nullList.add(null);
NULL_VALUE = Collections.unmodifiableList(nullList);
}
private final Root<T> root;
private final CriteriaQuery<?> query;
private final CriteriaBuilder cb;
private final PathResolver pathResolver = new PathResolver();
private PredicateBuilder(final Root<T> root, final CriteriaQuery<?> query, final CriteriaBuilder cb) {
this.root = root;
this.query = query;
this.cb = cb;
}
public Predicate build(final Node node) {
if (node instanceof Comparison comparison) {
return predicate(comparison);
} else if (node instanceof Logical logical) {
final Logical.Operator op = Objects.requireNonNull(logical.getOp());
if (op == Logical.Operator.AND) {
return cb.and(logical.getChildren().stream()
.map(this::build)
.toList()
.toArray(PREDICATES_ARRAY_0));
} else if (op == Logical.Operator.OR) {
return cb.or(logical.getChildren().stream()
.map(child -> {
pathResolver.reset();
return build(child); // for or path resolver joins could be reused
})
.toList()
.toArray(PREDICATES_ARRAY_0));
} else {
throw new IllegalArgumentException("Unsupported logical operator: " + op);
}
} else {
throw new IllegalArgumentException("Unsupported node type: " + node.getClass());
}
}
@SuppressWarnings({ "unchecked", "java:S3776" }) // java:S3776 - easier to read at one place
private Predicate predicate(final Comparison comparison) {
final String[] split = comparison.getKey().split("\\.", 2); // { attributeName [, sub attribute / map key]
final Attribute<? super T, ?> attribute = root.getModel().getAttribute(split[0]);
final Operator op = comparison.getOp();
if (attribute instanceof MapAttribute<?, ?, ?>) {
if (split.length < 2 || ObjectUtils.isEmpty(split[1])) {
throw new RSQLParameterUnsupportedFieldException(
String.format("No key for the map field found. Syntax is: %s.<key name>", getPathContext(comparison)));
}
if (comparison.getValue() == null) {
// map entry with key is null (doesn't exist) / is not null (exists) - use left join with on
return switch (op) {
case EQ -> cb.isNull(pathResolver.getJoinOn(attribute, split[1]));
case NE -> cb.isNotNull(pathResolver.getJoinOn(attribute, split[1]));
default -> throw new RSQLParameterSyntaxException(
String.format("Operator %s is not supported for map fields with value null", op));
};
} else {
final MapJoin<?, ?, ?> mapPath = (MapJoin<?, ?, ?>) pathResolver.getPath(attribute);
final Path<String> valuePath = (Path<String>) mapPath.value();
return isNot(op)
? compare(comparison, pathResolver.getJoinOnInner(attribute, split[1]))
: cb.and(equal(mapPath.key(), split[1]), compare(comparison, valuePath));
}
} else if (attribute instanceof SetAttribute<?, ?> setAttribute) {
if (split.length < 2 || ObjectUtils.isEmpty(split[1])) {
throw new RSQLParameterUnsupportedFieldException(
String.format("No mapping key for a set field found. Syntax is: %s.<ref name>", getPathContext(comparison)));
}
if (isNot(op)) {
if (op == NOT_LIKE && LIKE_WILDCARD_STR.equals(comparison.getValue())) {
// optimized (?) has non-null/empty attribute - do or with on instead of subquery
return cb.and(cb.isNull(pathResolver.getPath(attribute).get(split[1])));
}
return notEqualInLike(comparison, setAttribute, split[1]);
} else {
if (op == LIKE && LIKE_WILDCARD_STR.equals(comparison.getValue())) {
// optimized (?) has non-null/empty attribute - do or with on instead of subquery
return cb.and(cb.isNotNull(deepGetPath(pathResolver.getPath(attribute), split[1])));
}
return compare(comparison, deepGetPath(pathResolver.getPath(attribute), split[1]));
}
} else { // singular attribute (BASIC and EMBEDDABLE) or plural (ListAttribute of entities)
final Path<?> attributePath = pathResolver.getPath(attribute);
return compare(comparison, split.length > 1 ? deepGetPath(attributePath, split[1]) : attributePath);
}
}
private Predicate compare(final Comparison comparison, final Path<?> fieldPath) {
final List<Object> values = getValues(comparison, fieldPath.getJavaType());
final Object firstValue = values.get(0);
return switch (comparison.getOp()) {
case EQ -> firstValue == null ? cb.isNull(fieldPath) : equal(fieldPath, firstValue);
case NE -> firstValue == null ? cb.isNotNull(fieldPath) : cb.or(cb.isNull(fieldPath), notEqual(fieldPath, firstValue));
case GT -> cb.greaterThan(stringPath(fieldPath), String.valueOf(firstValue)); // JPA handles numbers
case GTE -> cb.greaterThanOrEqualTo(stringPath(fieldPath), String.valueOf(firstValue));
case LT -> cb.lessThan(stringPath(fieldPath), String.valueOf(firstValue));
case LTE -> cb.lessThanOrEqualTo(stringPath(fieldPath), String.valueOf(firstValue));
case IN -> in(stringPath(fieldPath), values);
case NOT_IN -> cb.or(cb.isNull(fieldPath), cb.not(in(stringPath(fieldPath), values)));
case LIKE -> like(stringPath(fieldPath), toSqlLikeValue(String.valueOf(firstValue)));
case NOT_LIKE -> cb.or(cb.isNull(fieldPath), notLike(stringPath(fieldPath), toSqlLikeValue(String.valueOf(firstValue))));
};
}
@SuppressWarnings({ "unchecked", "rawtypes" })
private Predicate notEqualInLike( // NE, NOT_IN, NOT_LIKE
final Comparison comparison, final PluralAttribute<?, ?, ?> pluralAttribute, final String subAttributeName) {
final List<Object> values = getValues(comparison, pluralAttribute.getElementType().getJavaType());
final Object value = values.get(0);
final EntityType<T> model = root.getModel();
if (!model.hasSingleIdAttribute()) {
throw new IllegalStateException("Root entity has no single id attribute");
}
final String idAttributeName = model.getId(model.getIdType().getJavaType()).getName();
final Class<? extends T> javaType = root.getJavaType();
final Subquery<? extends T> subquery = query.subquery(javaType);
final Root<? extends T> subqueryRoot = subquery.from(javaType);
final Path<?> joinPath = subAttributeName == null // if null it is a map
? ((MapJoin<?, ?, ?>) subqueryRoot.join(pluralAttribute.getName(), JoinType.LEFT)).value()
: deepGetPath(subqueryRoot.join(pluralAttribute.getName(), JoinType.LEFT), subAttributeName);
final Path<String> fieldPath = joinPath instanceof MapJoin<?, ?, ?> mapJoin
? (Path<String>) mapJoin.value()
: stringPath(joinPath);
return cb.not(cb.exists(
subquery.select((Expression) subqueryRoot)
.where(cb.and(
cb.equal(root.get(idAttributeName), subqueryRoot.get(idAttributeName)),
switch (comparison.getOp()) {
case NE -> equal(fieldPath, value);
case NOT_IN -> in(fieldPath, values);
case NOT_LIKE -> like(fieldPath, toSqlLikeValue((String) value));
default -> throw new IllegalStateException("Uncovered flow. Operator: " + comparison.getOp());
}))));
}
// java:S1066 - easier to understand separately
// java:S3776, java:S3358 - easier to read at one place
@SuppressWarnings({ "java:S1066", "java:S3776", "java:S3358" })
private List<Object> getValues(final Comparison comparison, final Class<?> javaType) {
final Object value = comparison.getValue();
final List<Object> values = (value == null ? NULL_VALUE : (value instanceof List<?> list ? list : List.of(value))).stream()
// if lookup is available, replace macros ...
.map(element -> virtualPropertyReplacer != null && element instanceof String strElement
? virtualPropertyReplacer.replace(strElement)
: element)
// converts value to the correct type
.map(element -> element instanceof String strElement
? convertValueIfNecessary(strElement, javaType, comparison)
: element)
.toList();
if (values.isEmpty()) {
throw new RSQLParameterSyntaxException("RSQL values must not be empty", null);
} else if (values.size() == 1) {
final Operator op = comparison.getOp();
// enum, boolean or null - doesn't support >, >=, <, <=
if (!(values.get(0) instanceof String)) {
if (values.get(0) instanceof Number) {
if (op == LIKE || op == NOT_LIKE) {
throw new RSQLParameterSyntaxException(op + " operator could not be applied number", null);
}
} else if (op == GT || op == GTE || op == LT || op == LTE || op == LIKE || op == NOT_LIKE) {
final String errorMsg = values.get(0) == null ? "null value" : "enum or boolean field";
throw new RSQLParameterSyntaxException(op + " operator could not be applied to " + errorMsg, null);
}
}
} else {
final Operator op = comparison.getOp();
if (op != IN && op != NOT_IN) {
throw new RSQLParameterSyntaxException(op + " operator shall have exactly one value", null);
}
}
return values;
}
// result is String, enum value, boolean or null
private Object convertValueIfNecessary(final String value, final Class<?> javaType, final Comparison comparison) {
if (javaType != null && javaType.isEnum()) {
return toEnumValue(value, javaType, comparison);
}
if (boolean.class.equals(javaType) || Boolean.class.equals(javaType)) {
if ("true".equals(value) || "false".equals(value)) {
return Boolean.valueOf(value);
} else {
throw new RSQLParameterSyntaxException(
String.format(
"The value of %S is not well formed. Only a boolean (true or false) value will be expected",
getPathContext(comparison)));
}
}
return value;
}
@Nonnull
private static Object getPathContext(final Comparison comparison) {
return comparison.getContext() == null ? comparison.getKey() : comparison.getContext();
}
private static boolean isNot(final Operator op) {
return op == NE || op == NOT_IN || op == NOT_LIKE;
}
@SuppressWarnings({ "rawtypes", "unchecked" })
private static Object toEnumValue(final String value, final Class<?> javaType, final Comparison comparison) {
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
if (log.isInfoEnabled()) {
log.info("Provided value '{}' cannot be transformed into the correct enum type {}", value.toUpperCase(), javaType);
} else {
log.debug("Provided value '{}' cannot be transformed into the correct enum type {}", value.toUpperCase(), javaType, e);
}
throw new RSQLParameterUnsupportedFieldException(
String.format(
"Value of %s must be one of the following: %s",
getPathContext(comparison),
Arrays.stream(tmpEnumType.getEnumConstants())
.map(Enum::name)
.map(String::toLowerCase)
.toList()),
e);
}
}
private static Path<?> deepGetPath(final Path<?> path, final String subAttributeName) {
return deepGetPath(path, subAttributeName.split("\\."), 0);
}
private static Path<?> deepGetPath(final Path<?> path, final String[] subAttributeNameSplit, int startIndex) {
final String subAttributeName = subAttributeNameSplit[startIndex++];
if (startIndex == subAttributeNameSplit.length) {
return path.get(subAttributeName);
} else { // else its a deeper path so request left join
if (path instanceof Join<?,?> join) {
return deepGetPath(join.join(subAttributeName, JoinType.LEFT), subAttributeNameSplit, startIndex);
} else {
throw new RSQLParameterSyntaxException("Unexpected sub attribute " + subAttributeName);
}
}
}
@SuppressWarnings("unchecked")
private static Path<String> stringPath(final Path<?> path) {
return (Path<String>) path;
}
private String toSqlLikeValue(final String value) {
final String escaped;
if (database == Database.SQL_SERVER) {
escaped = value.replace("%", "[%]").replace("_", "[_]");
} else {
escaped = value.replace("%", ESCAPE_CHAR + "%").replace("_", ESCAPE_CHAR + "_");
}
final String finalizedValue;
if (escaped.contains(ESCAPE_CHAR_WITH_ASTERISK)) {
finalizedValue = escaped.replace(ESCAPE_CHAR_WITH_ASTERISK, "$")
.replace(LIKE_WILDCARD, '%')
.replace("$", ESCAPE_CHAR_WITH_ASTERISK);
} else {
finalizedValue = escaped.replace(LIKE_WILDCARD, '%');
}
return finalizedValue;
}
@SuppressWarnings("java:S1221") // java:S1221 - intentionally to match the SQL wording
private Predicate equal(final Path<?> fieldPath, final Object value) {
if (value instanceof String valueStr && caseWise(fieldPath)) {
return cb.equal(cb.upper(stringPath(fieldPath)), valueStr.toUpperCase());
} else {
return cb.equal(fieldPath, value);
}
}
private Predicate notEqual(final Path<?> fieldPath, Object value) {
if (value instanceof String valueStr && caseWise(fieldPath)) {
return cb.notEqual(cb.upper(stringPath(fieldPath)), valueStr.toUpperCase());
} else {
return cb.notEqual(fieldPath, value);
}
}
private Predicate like(final Path<String> fieldPath, final String sqlValue) {
if (caseWise(fieldPath)) {
return cb.like(cb.upper(fieldPath), sqlValue.toUpperCase(), ESCAPE_CHAR);
} else {
return cb.like(fieldPath, sqlValue, ESCAPE_CHAR);
}
}
private Predicate notLike(final Path<String> fieldPath, final String sqlValue) {
if (caseWise(fieldPath)) {
return cb.notLike(cb.upper(fieldPath), sqlValue.toUpperCase(), ESCAPE_CHAR);
} else {
return cb.notLike(fieldPath, sqlValue, ESCAPE_CHAR);
}
}
private Predicate in(final Path<String> fieldPath, final List<Object> values) {
if (caseWise(fieldPath)) {
final List<String> inParams = values.stream()
.filter(String.class::isInstance)
.map(String.class::cast).map(String::toUpperCase).toList();
return inParams.isEmpty() ? fieldPath.in(values) : cb.upper(fieldPath).in(inParams);
} else {
return fieldPath.in(values);
}
}
private boolean caseWise(final Path<?> fieldPath) {
return ensureIgnoreCase && fieldPath.getJavaType() == String.class;
}
private class PathResolver {
private final Map<String, CollectionPathResolver> attributeToPathResolver = new HashMap<>();
private Path<?> getPath(final Attribute<? super T, ?> attribute) {
return switch (attribute.getPersistentAttributeType()) {
case BASIC -> root.get(attribute.getName());
case MANY_TO_ONE, ONE_TO_ONE -> root.getJoins().stream()
.filter(join -> join.getAttribute().equals(attribute))
.findFirst()
.orElseGet(() -> root.join(attribute.getName(), JoinType.LEFT));
case MANY_TO_MANY, ONE_TO_MANY, ELEMENT_COLLECTION -> getCollectionPathResolver(attribute.getName()).getPath();
default -> throw new IllegalArgumentException("Unsupported attribute type: " + attribute.getPersistentAttributeType());
};
}
private MapJoin<?, ?, ?> getJoinOn(final Attribute<?, ?> attribute, final Object value) {
return getCollectionPathResolver(attribute.getName()).getJoinOn(value);
}
private MapJoin<?, ?, ?> getJoinOnInner(final Attribute<?, ?> attribute, final Object value) {
return getCollectionPathResolver(attribute.getName()).getJoinOnInner(value);
}
private void reset() {
attributeToPathResolver.values().forEach(CollectionPathResolver::reset);
}
@Nonnull
private CollectionPathResolver getCollectionPathResolver(final String attributeName) {
return attributeToPathResolver.computeIfAbsent(attributeName, CollectionPathResolver::new);
}
private class CollectionPathResolver {
private final String attributeName;
private final List<Path<?>> paths = new ArrayList<>();
private int pos;
private final Map<Object, MapJoin<?, ?, ?>> joinOnCache = new HashMap<>();
private final Map<Object, MapJoin<?, ?, ?>> joinOnInnerCache = new HashMap<>();
private CollectionPathResolver(final String attributeName) {
this.attributeName = attributeName;
}
private Path<?> getPath() {
if (pos < paths.size()) {
return paths.get(pos++);
} else {
final Path<?> path = root.join(attributeName, JoinType.LEFT);
paths.add(path);
pos++;
return path;
}
}
private MapJoin<?, ?, ?> getJoinOn(final Object value) {
return joinOnCache.computeIfAbsent(value, k -> {
final MapJoin<?, ?, ?> mapPath = (MapJoin<?, ?, ?>) root.join(attributeName, JoinType.LEFT);
mapPath.on(equal(mapPath.key(), k));
return mapPath;
});
}
private MapJoin<?, ?, ?> getJoinOnInner(final Object value) {
return joinOnInnerCache.computeIfAbsent(value, k -> {
final MapJoin<?, ?, ?> mapPath = (MapJoin<?, ?, ?>) root.join(attributeName, JoinType.INNER);
mapPath.on(equal(mapPath.key(), k));
return mapPath;
});
}
private void reset() {
pos = 0;
}
}
}
}
}

View File

@@ -1,183 +0,0 @@
/**
* Copyright (c) 2025 Contributors to the Eclipse Foundation
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.eclipse.hawkbit.repository.jpa.rsqllegacy;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Stream;
import jakarta.validation.constraints.NotNull;
import cz.jirutka.rsql.parser.ast.ComparisonNode;
import cz.jirutka.rsql.parser.ast.ComparisonOperator;
import cz.jirutka.rsql.parser.ast.RSQLOperators;
import lombok.Value;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.hawkbit.repository.RsqlQueryField;
import org.eclipse.hawkbit.repository.exception.RSQLParameterUnsupportedFieldException;
import org.springframework.util.ObjectUtils;
@Slf4j
public abstract class AbstractRSQLVisitor<A extends Enum<A> & RsqlQueryField> {
static final ComparisonOperator IS = new ComparisonOperator("=is=", "=eq=");
static final ComparisonOperator NOT = new ComparisonOperator("=not=", "=ne=");
static final Set<ComparisonOperator> OPERATORS;
static {
final Set<ComparisonOperator> operators = new HashSet<>(RSQLOperators.defaultOperators());
// == and != alternatives just treating "null" string as null not as a "null"
operators.add(IS);
operators.add(NOT);
OPERATORS = Collections.unmodifiableSet(operators);
}
private final Class<A> rsqlQueryFieldType;
protected AbstractRSQLVisitor(final Class<A> rsqlQueryFieldType) {
this.rsqlQueryFieldType = rsqlQueryFieldType;
}
@SuppressWarnings("java:S1066") // java:S1066 - more readable with separate "if" statements
protected QueryPath getQueryPath(final ComparisonNode node) {
final int firstSeparatorIndex = node.getSelector().indexOf(RsqlQueryField.SUB_ATTRIBUTE_SEPARATOR);
final String enumName = (firstSeparatorIndex == -1
? node.getSelector()
: node.getSelector().substring(0, firstSeparatorIndex)).toUpperCase();
log.debug("Get field identifier by name {} of enum type {}", enumName, rsqlQueryFieldType);
try {
final A enumValue = Enum.valueOf(rsqlQueryFieldType, enumName);
String[] split = getSplit(enumValue, node.getSelector());
// validate
if (!enumValue.isMap()) {
// sub entity need minimum 1 dot
if (!enumValue.getSubEntityAttributes().isEmpty() && split.length < 2) {
if (enumValue.getSubEntityAttributes().size() == 1) { // single sub attribute - so add is as a default
split = new String[] { split[0], enumValue.getSubEntityAttributes().get(0) };
} else {
throw createRSQLParameterUnsupportedException(node, null);
}
}
}
// validate and normalize (replace enum name with JPA entity field name and format the rest) prepare jpa path
split[0] = enumValue.getJpaEntityFieldName();
for (int i = 1; i < split.length; i++) {
split[i] = getFormattedSubEntityAttribute(enumValue, split[i]);
if (!containsSubEntityAttribute(enumValue, split[i])) {
if (i != split.length - 1 || !enumValue.isMap()) {
throw createRSQLParameterUnsupportedException(node, null);
} // otherwise - the key of map is not in the sub entity attributes
}
}
return new QueryPath(enumValue, split);
} catch (final IllegalArgumentException e) {
throw createRSQLParameterUnsupportedException(node, e);
}
}
private String[] getSplit(final A enumValue, final String rsqlFieldName) {
if (enumValue.isMap()) {
final String[] split = rsqlFieldName.split(RsqlQueryField.SUB_ATTRIBUTE_SPLIT_REGEX, 2);
if (split.length != 2 || ObjectUtils.isEmpty(split[1])) {
throw new RSQLParameterUnsupportedFieldException(
"The syntax of the given map search parameter field {" + rsqlFieldName + "} is wrong. Syntax is: <enum name>.<key name>");
}
return split;
} else {
return rsqlFieldName.split(RsqlQueryField.SUB_ATTRIBUTE_SPLIT_REGEX);
}
}
/**
* Contains the sub entity the given field.
*
* @param propertyField the given field
* @return <code>true</code> contains <code>false</code> contains not
*/
private boolean containsSubEntityAttribute(final A enumField, final String propertyField) {
final List<String> subEntityAttributes = enumField.getSubEntityAttributes();
if (subEntityAttributes.contains(propertyField)) {
return true;
}
for (final String attribute : subEntityAttributes) {
final String[] graph = attribute.split(RsqlQueryField.SUB_ATTRIBUTE_SPLIT_REGEX);
for (final String subAttribute : graph) {
if (subAttribute.equalsIgnoreCase(propertyField)) {
return true;
}
}
}
return false;
}
/**
* @param node current processing node
* @param rootException in case there is a cause otherwise {@code null}
* @return Exception with prepared message extracted from the comparison node.
*/
private RSQLParameterUnsupportedFieldException createRSQLParameterUnsupportedException(
@NotNull final ComparisonNode node, final Exception rootException) {
return new RSQLParameterUnsupportedFieldException(String.format(
"The given search parameter field {%s} does not exist, must be one of the following fields %s",
node.getSelector(), getExpectedFieldList()), rootException);
}
private String getFormattedSubEntityAttribute(final A propertyEnum, final String propertyField) {
return propertyEnum.getSubEntityAttributes().stream()
.filter(attr -> attr.equalsIgnoreCase(propertyField))
.findFirst().orElse(propertyField);
}
private List<String> getExpectedFieldList() {
return Stream.concat(
Arrays.stream(rsqlQueryFieldType.getEnumConstants())
.filter(enumField -> enumField.getSubEntityAttributes().isEmpty()).map(enumField -> {
final String enumFieldName = enumField.name().toLowerCase();
if (enumField.isMap()) {
return enumFieldName + RsqlQueryField.SUB_ATTRIBUTE_SEPARATOR + "keyName";
} else {
return enumFieldName;
}
}),
Arrays.stream(rsqlQueryFieldType.getEnumConstants())
.filter(enumField -> !enumField.getSubEntityAttributes().isEmpty()).flatMap(enumField -> {
final List<String> subEntity = enumField
.getSubEntityAttributes().stream().map(fieldName -> enumField.name().toLowerCase()
+ RsqlQueryField.SUB_ATTRIBUTE_SEPARATOR + fieldName)
.toList();
return subEntity.stream();
}))
.toList();
}
@Value
protected class QueryPath {
A enumValue;
String[] jpaPath;
private QueryPath(final A enumValue, final String[] jpaPath) {
this.enumValue = enumValue;
this.jpaPath = jpaPath;
}
}
}

View File

@@ -1,576 +0,0 @@
/**
* Copyright (c) 2025 Contributors to the Eclipse Foundation
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.eclipse.hawkbit.repository.jpa.rsqllegacy;
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.function.BiFunction;
import java.util.function.Function;
import jakarta.persistence.criteria.CriteriaBuilder;
import jakarta.persistence.criteria.CriteriaQuery;
import jakarta.persistence.criteria.Expression;
import jakarta.persistence.criteria.From;
import jakarta.persistence.criteria.Join;
import jakarta.persistence.criteria.JoinType;
import jakarta.persistence.criteria.MapJoin;
import jakarta.persistence.criteria.Path;
import jakarta.persistence.criteria.PluralJoin;
import jakarta.persistence.criteria.Predicate;
import jakarta.persistence.criteria.Root;
import jakarta.persistence.criteria.Subquery;
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;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.math.NumberUtils;
import org.eclipse.hawkbit.repository.FieldValueConverter;
import org.eclipse.hawkbit.repository.RsqlQueryField;
import org.eclipse.hawkbit.repository.exception.RSQLParameterSyntaxException;
import org.eclipse.hawkbit.repository.exception.RSQLParameterUnsupportedFieldException;
import org.eclipse.hawkbit.repository.rsql.VirtualPropertyReplacer;
import org.springframework.beans.SimpleTypeConverter;
import org.springframework.beans.TypeMismatchException;
import org.springframework.orm.jpa.vendor.Database;
import org.springframework.util.CollectionUtils;
import org.springframework.util.ObjectUtils;
/**
* 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
* @deprecated Old implementation of RSQL Visitor. Deprecated in favour of next gen implementation - {@link JpaQueryRsqlVisitorG2}.
* It will be kept for some time in order to keep backward compatibility and to allow for a smooth transition. Also, in case of
* problems with the new implementation, this one can be used as a fallback.
*/
@Deprecated(forRemoval = true, since = "0.6.0")
@Slf4j
public class JpaQueryRsqlVisitor<A extends Enum<A> & RsqlQueryField, T> extends AbstractRSQLVisitor<A>
implements RSQLVisitor<List<Predicate>, String> {
public static final Character LIKE_WILDCARD = '*';
private static final char ESCAPE_CHAR = '\\';
private static final List<String> NO_JOINS_OPERATOR = List.of("!=", "=out=");
private static final String ESCAPE_CHAR_WITH_ASTERISK = ESCAPE_CHAR + "*";
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 boolean ensureIgnoreCase;
private final Root<T> root;
private final VirtualPropertyReplacer virtualPropertyReplacer;
private final SimpleTypeConverter simpleTypeConverter;
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, final boolean ensureIgnoreCase) {
super(enumType);
this.root = root;
this.cb = cb;
this.query = query;
this.virtualPropertyReplacer = virtualPropertyReplacer;
this.simpleTypeConverter = new SimpleTypeConverter();
this.database = database;
this.ensureIgnoreCase = ensureIgnoreCase;
}
@Override
public List<Predicate> visit(final AndNode node, final String param) {
beginLevel(false);
final List<Predicate> childs = acceptChildren(node);
endLevel();
if (!childs.isEmpty()) {
return toSingleList(cb.and(childs.toArray(new Predicate[0])));
}
return toSingleList(cb.conjunction());
}
@Override
public List<Predicate> visit(final OrNode node, final String param) {
beginLevel(true);
final List<Predicate> childs = acceptChildren(node);
endLevel();
if (!childs.isEmpty()) {
return toSingleList(cb.or(childs.toArray(new Predicate[0])));
}
return toSingleList(cb.conjunction());
}
@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 QueryPath queryPath = getQueryPath(node);
final List<String> values = node.getArguments();
final List<Object> transformedValues = new ArrayList<>();
final Path<Object> fieldPath = getFieldPath(queryPath.getEnumValue(), queryPath);
for (final String value : values) {
transformedValues.add(convertValueIfNecessary(node, queryPath.getEnumValue(), value, fieldPath));
}
this.joinsNeeded = this.joinsNeeded || areJoinsNeeded(node);
return mapToPredicate(node, fieldPath, node.getArguments(), transformedValues, queryPath);
}
private static List<Predicate> toSingleList(final Predicate predicate) {
return Collections.singletonList(predicate);
}
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);
}
private static boolean areJoinsNeeded(final ComparisonNode node) {
return !NO_JOINS_OPERATOR.contains(node.getOperator().getSymbol());
}
// 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
log.info("given value {} cannot be transformed into the correct enum type {}", value.toUpperCase(),
javaType);
log.debug("value cannot be transformed to an enum", e);
throw new RSQLParameterUnsupportedFieldException("field {" + node.getSelector()
+ "} must be one of the following values {" + Arrays.stream(tmpEnumType.getEnumConstants())
.map(v -> v.name().toLowerCase()).toList()
+ "}", e);
}
}
private static boolean isSimpleField(final String[] split, final boolean isMapKeyField) {
return split.length == 1 || (split.length == 2 && isMapKeyField);
}
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 static boolean isPattern(final String transformedValue) {
if (transformedValue.contains(ESCAPE_CHAR_WITH_ASTERISK)) {
return transformedValue.replace(ESCAPE_CHAR_WITH_ASTERISK, "$").indexOf(LIKE_WILDCARD) != -1;
} else {
return transformedValue.indexOf(LIKE_WILDCARD) != -1;
}
}
@SuppressWarnings("unchecked")
private static <Y> Path<Y> pathOfString(final Path<?> path) {
return (Path<Y>) path;
}
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);
}
}
/**
* 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 queryPath RSQL field
* @return the Path for a field
*/
@SuppressWarnings("unchecked")
private Path<Object> getFieldPath(final A enumField, final QueryPath queryPath) {
return (Path<Object>) getFieldPath(root, queryPath.getJpaPath(), enumField.isMap(),
this::getJoinFieldPath).orElseThrow(
() -> new RSQLParameterUnsupportedFieldException("RSQL field path cannot be empty", null));
}
@SuppressWarnings({ "rawtypes", "unchecked" })
private Path<?> getJoinFieldPath(final Path<?> fieldPath, final String fieldNameSplit) {
if (fieldPath instanceof PluralJoin join) {
final From 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 newJoin = joinParent.join(fieldNameSplit, JoinType.LEFT);
addCurrentJoin(newJoin);
return newJoin;
}
}
return fieldPath;
}
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(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 RSQLParameterUnsupportedFieldException(
"The value of the given search parameter field {" + node.getSelector()
+ "} is not well formed. Only a boolean (true or false) value will be expected",
e);
}
}
@SuppressWarnings({ "rawtypes", "unchecked" })
private Object convertFieldConverterValue(final A fieldName, final String value) {
try {
return ((FieldValueConverter) fieldName).convertValue(fieldName, value);
} catch (final Exception e) {
throw new RSQLParameterSyntaxException(e.getMessage(), null);
}
}
private List<Predicate> mapToPredicate(final ComparisonNode node, final Path<Object> fieldPath,
final List<String> values, final List<Object> transformedValues, final QueryPath queryPath) {
String value = values.get(0);
// if lookup is available, replace macros ...
if (virtualPropertyReplacer != null) {
value = virtualPropertyReplacer.replace(value);
}
final Predicate mapPredicate = mapToMapPredicate(fieldPath, queryPath);
final Predicate valuePredicate = addOperatorPredicate(node, fieldPath, transformedValues, value, queryPath);
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 QueryPath queryPath) {
// 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, queryPath);
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 in(pathOfString(fieldPath), transformedValues);
case "=out=":
return getOutPredicate(transformedValues, queryPath, fieldPath);
default:
throw new RSQLParameterSyntaxException(
"operator symbol {" + operator + "} is either not supported or not implemented");
}
}
private Predicate getOutPredicate(
final List<Object> transformedValues,
final QueryPath queryPath, final Path<Object> fieldPath) {
final String[] fieldNames = queryPath.getJpaPath();
if (isSimpleField(fieldNames, queryPath.getEnumValue().isMap())) {
final Path<String> pathOfString = pathOfString(fieldPath);
return cb.or(cb.isNull(pathOfString), cb.not(in(pathOfString, transformedValues)));
}
clearOuterJoinsIfNotNeeded();
return toNotExistsSubQueryPredicate(fieldNames, queryPath.getEnumValue(),
expressionToCompare -> in(expressionToCompare, transformedValues));
}
@SuppressWarnings("unchecked")
private Predicate mapToMapPredicate(final Path<Object> fieldPath, final QueryPath queryPath) {
if (!queryPath.getEnumValue().isMap()) {
return null;
}
final String[] graph = queryPath.getJpaPath();
final String keyValue = graph[graph.length - 1];
// Currently we support only string key. So below cast is safe.
return equal((Expression<String>) (((MapJoin<?, ?, ?>) fieldPath).key()), keyValue);
}
private Predicate getEqualToPredicate(final Object transformedValue, final Path<Object> fieldPath) {
if (transformedValue == null) {
return cb.isNull(pathOfString(fieldPath));
}
if ((transformedValue instanceof String transformedValueStr) && !NumberUtils.isCreatable(transformedValueStr)) {
if (ObjectUtils.isEmpty(transformedValue)) {
return cb.or(cb.isNull(pathOfString(fieldPath)), cb.equal(pathOfString(fieldPath), ""));
}
if (isPattern(transformedValueStr)) { // a pattern, use like
return like(pathOfString(fieldPath), toSQL(transformedValueStr));
} else {
return equal(pathOfString(fieldPath), transformedValueStr);
}
}
return cb.equal(fieldPath, transformedValue);
}
private Predicate getNotEqualToPredicate(final Object transformedValue, final Path<Object> fieldPath,
final QueryPath queryPath) {
if (transformedValue == null) {
return cb.isNotNull(pathOfString(fieldPath));
}
if ((transformedValue instanceof String transformedValueStr) && !NumberUtils.isCreatable(transformedValueStr)) {
if (ObjectUtils.isEmpty(transformedValue)) {
return cb.and(cb.isNotNull(pathOfString(fieldPath)), cb.notEqual(pathOfString(fieldPath), ""));
}
final String[] fieldNames = queryPath.getJpaPath();
if (isSimpleField(fieldNames, queryPath.getEnumValue().isMap())) {
if (isPattern(transformedValueStr)) { // a pattern, use like
return cb.or(cb.isNull(pathOfString(fieldPath)), notLike(pathOfString(fieldPath), toSQL(transformedValueStr)));
} else {
return toNullOrNotEqualPredicate(fieldPath, transformedValueStr);
}
}
clearOuterJoinsIfNotNeeded();
if (isPattern(transformedValueStr)) { // a pattern, use like
return toNotExistsSubQueryPredicate(fieldNames, queryPath.getEnumValue(),
expressionToCompare -> like(expressionToCompare, toSQL(transformedValueStr)));
} else {
return toNotExistsSubQueryPredicate(fieldNames, queryPath.getEnumValue(),
expressionToCompare -> equal(expressionToCompare, transformedValueStr));
}
}
return toNullOrNotEqualPredicate(fieldPath, transformedValue);
}
private void clearOuterJoinsIfNotNeeded() {
if (!joinsNeeded) {
root.getJoins().clear();
}
}
private Predicate toNullOrNotEqualPredicate(final Path<Object> fieldPath, final Object transformedValue) {
return cb.or(
cb.isNull(pathOfString(fieldPath)),
transformedValue instanceof String transformedValueStr
? notEqual(pathOfString(fieldPath), transformedValueStr)
: cb.notEqual(fieldPath, transformedValue));
}
@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));
}
@SuppressWarnings({ "rawtypes", "unchecked" })
private Expression<String> getExpressionToCompare(final Path innerFieldPath, final A enumField) {
if (enumField.isMap()){
// Currently we support only string key. So below cast is safe.
return (Expression<String>) (((MapJoin<?, ?, ?>) pathOfString(innerFieldPath)).value());
} else {
return pathOfString(innerFieldPath);
}
}
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 replaceIfRequired(escaped);
}
private String replaceIfRequired(final String escapedValue) {
final String finalizedValue;
if (escapedValue.contains(ESCAPE_CHAR_WITH_ASTERISK)) {
finalizedValue = escapedValue.replace(ESCAPE_CHAR_WITH_ASTERISK, "$").replace(LIKE_WILDCARD, '%')
.replace("$", ESCAPE_CHAR_WITH_ASTERISK);
} else {
finalizedValue = escapedValue.replace(LIKE_WILDCARD, '%');
}
return finalizedValue;
}
private List<Predicate> acceptChildren(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 {
log.debug("visit logical node children but could not parse it, ignoring {}", node2);
}
}
return childs;
}
@SuppressWarnings("java:S1221") // java:S1221 - intentionally to match the SQL wording
private Predicate equal(final Expression<String> expressionToCompare, final String sqlValue) {
return cb.equal(caseWise(cb, expressionToCompare), caseWise(sqlValue));
}
private Predicate notEqual(final Expression<String> expressionToCompare, String transformedValueStr) {
return cb.notEqual(caseWise(cb, expressionToCompare), caseWise(transformedValueStr));
}
private Predicate like(final Expression<String> expressionToCompare, final String sqlValue) {
return cb.like(caseWise(cb, expressionToCompare), caseWise(sqlValue), ESCAPE_CHAR);
}
private Predicate notLike(final Expression<String> expressionToCompare, final String sqlValue) {
return cb.notLike(caseWise(cb, expressionToCompare), caseWise(sqlValue), ESCAPE_CHAR);
}
private Predicate in(final Expression<String> expressionToCompare, final List<Object> transformedValues) {
final List<String> inParams = transformedValues.stream().filter(String.class::isInstance)
.map(String.class::cast).map(this::caseWise).toList();
return inParams.isEmpty() ? expressionToCompare.in(transformedValues) : caseWise(cb, expressionToCompare).in(inParams);
}
private Expression<String> caseWise(final CriteriaBuilder cb, final Expression<String> expression) {
if (expression instanceof Path<String> path && path.getJavaType() != String.class) {
return expression; // non string values doesn't support uppercase
} else {
return ensureIgnoreCase ? cb.upper(expression) : expression;
}
}
private String caseWise(final String str) {
return ensureIgnoreCase ? str.toUpperCase() : str;
}
}

View File

@@ -1,502 +0,0 @@
/**
* Copyright (c) 2025 Contributors to the Eclipse Foundation
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.eclipse.hawkbit.repository.jpa.rsqllegacy;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import jakarta.persistence.criteria.CriteriaBuilder;
import jakarta.persistence.criteria.CriteriaQuery;
import jakarta.persistence.criteria.JoinType;
import jakarta.persistence.criteria.MapJoin;
import jakarta.persistence.criteria.Path;
import jakarta.persistence.criteria.PluralJoin;
import jakarta.persistence.criteria.Predicate;
import jakarta.persistence.criteria.Root;
import jakarta.persistence.criteria.Subquery;
import jakarta.persistence.metamodel.Attribute;
import jakarta.persistence.metamodel.SingularAttribute;
import jakarta.persistence.metamodel.Type;
import cz.jirutka.rsql.parser.ast.AndNode;
import cz.jirutka.rsql.parser.ast.ComparisonNode;
import cz.jirutka.rsql.parser.ast.ComparisonOperator;
import cz.jirutka.rsql.parser.ast.LogicalNode;
import cz.jirutka.rsql.parser.ast.Node;
import cz.jirutka.rsql.parser.ast.OrNode;
import cz.jirutka.rsql.parser.ast.RSQLOperators;
import cz.jirutka.rsql.parser.ast.RSQLVisitor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.math.NumberUtils;
import org.eclipse.hawkbit.repository.FieldValueConverter;
import org.eclipse.hawkbit.repository.RsqlQueryField;
import org.eclipse.hawkbit.repository.exception.RSQLParameterSyntaxException;
import org.eclipse.hawkbit.repository.exception.RSQLParameterUnsupportedFieldException;
import org.eclipse.hawkbit.repository.rsql.VirtualPropertyReplacer;
import org.springframework.orm.jpa.vendor.Database;
import org.springframework.util.CollectionUtils;
import org.springframework.util.ObjectUtils;
/**
* 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
* @deprecated Old implementation of RSQL Visitor (G2). Deprecated in favour of next gen implementation -
* {@link org.eclipse.hawkbit.repository.jpa.rsql.SpecificationBuilder}.
* It will be kept for some time in order to keep backward compatibility and to allow for a smooth transition. Also, in case of
* problems with the new implementation, this one can be used as a fallback.
*/
@Deprecated(forRemoval = true, since = "0.9.0")
@Slf4j
public class JpaQueryRsqlVisitorG2<A extends Enum<A> & RsqlQueryField, T>
extends AbstractRSQLVisitor<A> implements RSQLVisitor<List<Predicate>, String> {
public static final Character LIKE_WILDCARD = '*';
private static final char ESCAPE_CHAR = '\\';
private static final String ESCAPE_CHAR_WITH_ASTERISK = ESCAPE_CHAR + "*";
private final Root<T> root;
private final CriteriaQuery<?> query;
private final CriteriaBuilder cb;
private final Database database;
private final VirtualPropertyReplacer virtualPropertyReplacer;
private final boolean ensureIgnoreCase;
private final Map<String, Path<?>> attributeToPath = new HashMap<>();
private boolean inOr;
public JpaQueryRsqlVisitorG2(
final Class<A> enumType,
final Root<T> root, final CriteriaQuery<?> query, final CriteriaBuilder cb,
final Database database, final VirtualPropertyReplacer virtualPropertyReplacer, final boolean ensureIgnoreCase) {
super(enumType);
this.root = root;
this.cb = cb;
this.query = query;
this.virtualPropertyReplacer = virtualPropertyReplacer;
this.database = database;
this.ensureIgnoreCase = ensureIgnoreCase;
}
@Override
public List<Predicate> visit(final AndNode node, final String param) {
final List<Predicate> children = acceptChildren(node);
return Collections.singletonList(children.isEmpty() ? cb.conjunction() : cb.and(children.toArray(new Predicate[0])));
}
@Override
public List<Predicate> visit(final OrNode node, final String param) {
inOr = true;
try {
final List<Predicate> children = acceptChildren(node);
return Collections.singletonList(children.isEmpty() ? cb.conjunction() : cb.or(children.toArray(new Predicate[0])));
} finally {
inOr = false;
attributeToPath.clear();
}
}
@Override
public List<Predicate> visit(final ComparisonNode node, final String param) {
final QueryPath queryPath = getQueryPath(node);
final Path<?> fieldPath = getFieldPath(root, queryPath);
return Collections.singletonList(toPredicate(node, queryPath, fieldPath, getValues(node, queryPath, fieldPath)));
}
@SuppressWarnings("java:S3776") // java:S3776 - easier to read at one place
private Predicate toPredicate(final ComparisonNode node, final QueryPath queryPath, final Path<?> fieldPath, final List<Object> values) {
final Predicate mapEntryKeyPredicate;
if (queryPath.getEnumValue().isMap()) {
if (node.getOperator() == IS) {
// special handling of "not-exists"
if (values.size() != 1) {
throw new RSQLParameterSyntaxException("The operator '" + IS + "' can only be used with one value");
}
if (values.get(0) == null) {
// IS operator for maps and null value is treated as doesn't exist correspondingly
((PluralJoin<?, ?, ?>) fieldPath).on(toMapEntryKeyPredicate(queryPath, fieldPath));
return cb.isNull(fieldPath);
}
} else if (node.getOperator() == NOT) {
if (values.size() != 1) {
throw new RSQLParameterSyntaxException("The operator '" + NOT + "' can only be used with one value");
}
// NOT operator for maps and null value is treated as does exist correspondingly
((PluralJoin<?, ?, ?>) fieldPath).on(toMapEntryKeyPredicate(queryPath, fieldPath));
if (values.get(0) == null) {
// special handling of "exists"
return cb.isNotNull(fieldPath);
} else {
// special handling or "not equal" or null (same as != but with possible optimized join - no subquery)
return toNotEqualToPredicate(queryPath, fieldPath, values.get(0));
}
}
mapEntryKeyPredicate = toMapEntryKeyPredicate(queryPath, fieldPath);
} else {
mapEntryKeyPredicate = null;
}
final Predicate valuePredicate = toOperatorAndValuePredicate(node, queryPath, fieldPath, values);
return mapEntryKeyPredicate == null ? valuePredicate : cb.and(mapEntryKeyPredicate, valuePredicate);
}
@SuppressWarnings("unchecked")
private Predicate toMapEntryKeyPredicate(final QueryPath queryPath, final Path<?> fieldPath) {
final String[] graph = queryPath.getJpaPath();
return equal((Path<String>) ((MapJoin<?, ?, ?>) fieldPath).key(), graph[graph.length - 1]);
}
private Predicate toOperatorAndValuePredicate(
final ComparisonNode node, final QueryPath queryPath, final Path<?> fieldPath, final List<Object> values) {
// only 'equal' and 'notEqual' can handle transformed value like enums.
// The JPA API cannot handle object types for greaterThan etc. methods. For them, it shall be a string.
final Object value = values.get(0);
final String operator = node.getOperator().getSymbol();
return switch (operator) {
case "==", "=is=", "=eq=" -> toEqualToPredicate(fieldPath, value);
case "!=", "=not=", "=ne=" -> toNotEqualToPredicate(queryPath, fieldPath, value);
case "=gt=" -> cb.greaterThan(pathOfString(fieldPath), String.valueOf(value)); // JPA handles numbers
case "=ge=" -> cb.greaterThanOrEqualTo(pathOfString(fieldPath), String.valueOf(value));
case "=lt=" -> cb.lessThan(pathOfString(fieldPath), String.valueOf(value));
case "=le=" -> cb.lessThanOrEqualTo(pathOfString(fieldPath), String.valueOf(value));
case "=in=" -> in(pathOfString(fieldPath), values);
case "=out=" -> toOutPredicate(queryPath, fieldPath, values);
default -> throw new RSQLParameterSyntaxException("Operator symbol {" + operator + "} is either not supported or not implemented");
};
}
private Predicate toEqualToPredicate(final Path<?> fieldPath, final Object value) {
if (value == null) {
return cb.isNull(fieldPath);
}
if ((value instanceof String valueStr) && !NumberUtils.isCreatable(valueStr)) {
if (ObjectUtils.isEmpty(value)) {
return cb.or(cb.isNull(fieldPath), cb.equal(pathOfString(fieldPath), ""));
}
final Path<String> stringExpression = pathOfString(fieldPath);
if (isPattern(valueStr)) { // a pattern, use like
return like(stringExpression, toSQL(valueStr));
} else {
return equal(stringExpression, valueStr);
}
}
return cb.equal(fieldPath, value);
}
// if value is null -> not null
// if value is not null -> null or not equal value
private Predicate toNotEqualToPredicate(final QueryPath queryPath, final Path<?> fieldPath, final Object value) {
if (value == null) {
return cb.isNotNull(fieldPath);
}
if (value instanceof String valueStr && !NumberUtils.isCreatable(valueStr)) {
if (ObjectUtils.isEmpty(value)) {
return cb.and(cb.isNotNull(fieldPath), cb.notEqual(pathOfString(fieldPath), ""));
}
if (isSimpleField(queryPath)) {
if (isPattern(valueStr)) { // a pattern, use like
return cb.or(cb.isNull(fieldPath), notLike(pathOfString(fieldPath), toSQL(valueStr)));
} else {
return toNullOrNotEqualPredicate(fieldPath, valueStr);
}
}
return toNotExistsSubQueryPredicate(
queryPath, fieldPath, expressionToCompare ->
isPattern(valueStr)
? like(expressionToCompare, toSQL(valueStr)) // a pattern, use like
: equal(expressionToCompare, valueStr));
}
return toNullOrNotEqualPredicate(fieldPath, value);
}
private Predicate toOutPredicate(final QueryPath queryPath, final Path<?> fieldPath, final List<Object> values) {
if (isSimpleField(queryPath)) {
return cb.or(cb.isNull(fieldPath), cb.not(in(pathOfString(fieldPath), values)));
}
return toNotExistsSubQueryPredicate(queryPath, fieldPath, expressionToCompare -> in(expressionToCompare, values));
}
private Path<?> getFieldPath(final Root<?> root, final QueryPath queryPath) {
final String[] split = queryPath.getJpaPath();
Path<?> fieldPath = null;
for (int i = 0, end = queryPath.getEnumValue().isMap() ? split.length - 1 : split.length; i < end; i++) {
final String fieldNameSplit = split[i];
fieldPath = fieldPath == null ? getPath(root, fieldNameSplit) : fieldPath.get(fieldNameSplit);
}
if (fieldPath == null) {
throw new RSQLParameterUnsupportedFieldException("RSQL field path must not be null", null);
}
return fieldPath;
}
@SuppressWarnings("java:S3776") // java:S3776 - easier to read at one place
private List<Object> getValues(final ComparisonNode node, final AbstractRSQLVisitor<A>.QueryPath queryPath, final Path<?> fieldPath) {
final List<Object> values = node.getArguments().stream()
// if lookup is available, replace macros ...
.map(value -> virtualPropertyReplacer == null ? value : virtualPropertyReplacer.replace(value))
// converts value to the correct type
.map(value -> convertValueIfNecessary(node, queryPath.getEnumValue(), fieldPath, value))
.toList();
if (values.isEmpty()) {
throw new RSQLParameterSyntaxException("RSQL values must not be empty", null);
} else if (values.size() == 1) {
if (!(values.get(0) instanceof String)) { // enum or boolean or null - doesn's support >, >=, <, <=
final ComparisonOperator operator = node.getOperator();
if (operator == RSQLOperators.GREATER_THAN ||
operator == RSQLOperators.GREATER_THAN_OR_EQUAL ||
operator == RSQLOperators.LESS_THAN ||
operator == RSQLOperators.LESS_THAN_OR_EQUAL) {
final String errorMsg = values.get(0) == null ? "to null value" : "to enum or boolean field";
throw new RSQLParameterSyntaxException(operator.getSymbol() + " operator could not be applied " + errorMsg, null);
}
}
} else {
final ComparisonOperator operator = node.getOperator();
if (operator != RSQLOperators.IN && operator != RSQLOperators.NOT_IN) {
throw new RSQLParameterSyntaxException(operator.getSymbol() + " operator shall have exactly one value", null);
}
}
return values;
}
// if root.get creates a join we call join directly in order to specify LEFT JOIN type, to include rows for missing in particular
// table / criteria (root.get creates INNER JOIN) (see org.eclipse.persistence.internal.jpa.querydef.FromImpl implementation
// for more details) otherwise delegate to root.get
@SuppressWarnings("java:S1066") // java:S1066 - better reading this way
private Path<?> getPath(final Root<?> root, final String fieldNameSplit) {
// see org.eclipse.persistence.internal.jpa.querydef.FromImpl implementation for more details when root.get creates a join
final Attribute<?, ?> attribute = root.getModel().getAttribute(fieldNameSplit);
if (!attribute.isCollection()) {
// it is a SingularAttribute and not join if it is of basic or entity persistence type
final Type.PersistenceType persistenceType = ((SingularAttribute<?, ?>) attribute).getType().getPersistenceType();
if (persistenceType == Type.PersistenceType.BASIC) {
return root.get(fieldNameSplit);
} else if (persistenceType == Type.PersistenceType.ENTITY) {
return root.getJoins().stream()
.filter(join -> join.getAttribute().equals(attribute))
.findFirst()
.orElseGet(() -> root.join(fieldNameSplit, JoinType.LEFT));
}
}
// if a collection - it is a join
if (inOr && root == this.root) { // try to reuse join of the same "or" level and no subquery
return attributeToPath.computeIfAbsent(attribute.getName(), k -> root.join(fieldNameSplit, JoinType.LEFT));
} else {
return root.join(fieldNameSplit, JoinType.LEFT);
}
}
private Predicate toNullOrNotEqualPredicate(final Path<?> fieldPath, final Object value) {
return cb.or(
cb.isNull(fieldPath),
value instanceof String valueStr ? notEqual(pathOfString(fieldPath), valueStr) : cb.notEqual(fieldPath, value));
}
@SuppressWarnings({ "unchecked", "rawtypes" })
private Predicate toNotExistsSubQueryPredicate(
final QueryPath queryPath, final Path<?> fieldPath, final Function<Path<String>, Predicate> subQueryPredicateProvider) {
// if a subquery the field's parent joins are not actually used
if (!inOr) {
// so, if not in or (hence not reused) we remove them. Parent shall be a Join
root.getJoins().remove(fieldPath.getParentPath());
}
final Class<?> javaType = root.getJavaType();
final Subquery<?> subquery = query.subquery(javaType);
final Root subqueryRoot = subquery.from(javaType);
return cb.not(cb.exists(
subquery.select(subqueryRoot)
.where(cb.and(
cb.equal(
root.get(queryPath.getEnumValue().identifierFieldName()),
subqueryRoot.get(queryPath.getEnumValue().identifierFieldName())),
subQueryPredicateProvider.apply(
getExpressionToCompare(queryPath.getEnumValue(), getFieldPath(subqueryRoot, queryPath)))))));
}
@SuppressWarnings({ "rawtypes", "unchecked" })
private Path<String> getExpressionToCompare(final A enumField, final Path fieldPath) {
if (enumField.isMap()) {
// Currently we support only string key. So below cast is safe.
return (Path<String>) (((MapJoin<?, ?, ?>) fieldPath).value());
} else {
return pathOfString(fieldPath);
}
}
// result is String, enum value, boolean or null
@SuppressWarnings({ "rawtypes", "unchecked" })
private Object convertValueIfNecessary(final ComparisonNode node, final A enumValue, final Path<?> fieldPath, final String value) {
// in case the value of an RSQL query is an enum we need to transform the given value to the correspondent java type object
final Class<?> javaType = fieldPath.getJavaType();
if (javaType != null && javaType.isEnum()) {
return toEnumValue(node, javaType, value);
}
if (enumValue instanceof FieldValueConverter fieldValueConverter) {
try {
return fieldValueConverter.convertValue(enumValue, value);
} catch (final Exception e) {
throw new RSQLParameterUnsupportedFieldException(e.getMessage(), null);
}
}
if (boolean.class.equals(javaType) || Boolean.class.equals(javaType)) {
if ("true".equals(value) || "false".equals(value)) {
return Boolean.valueOf(value);
} else {
throw new RSQLParameterSyntaxException(
"The value of the given search parameter field {" + node.getSelector() + "} is not well formed. " +
"Only a boolean (true or false) value will be expected");
}
}
if ("null".equals(value)) {
final ComparisonOperator operator = node.getOperator();
if (operator == IS || operator == NOT) {
return null;
}
}
return value;
}
private boolean isSimpleField(final QueryPath queryPath) {
return queryPath.getJpaPath().length == 1 || (queryPath.getJpaPath().length == 2 && queryPath.getEnumValue().isMap());
}
@SuppressWarnings({ "rawtypes", "unchecked" })
private static Object toEnumValue(final ComparisonNode node, final Class<?> javaType, final String value) {
final Class<? 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
log.info("given value {} cannot be transformed into the correct enum type {}", value.toUpperCase(), javaType);
log.debug("value cannot be transformed to an enum", e);
throw new RSQLParameterUnsupportedFieldException(
"field '" + node.getSelector() + "' must be one of the following values " +
"{" + Arrays.stream(tmpEnumType.getEnumConstants()).map(Enum::name).map(String::toLowerCase).toList() + "}",
e);
}
}
@SuppressWarnings("unchecked")
private static Path<String> pathOfString(final Path<?> path) {
return (Path<String>) path;
}
private static boolean isPattern(final String value) {
if (value.contains(ESCAPE_CHAR_WITH_ASTERISK)) {
return value.replace(ESCAPE_CHAR_WITH_ASTERISK, "$").indexOf(LIKE_WILDCARD) != -1;
} else {
return value.indexOf(LIKE_WILDCARD) != -1;
}
}
private String toSQL(final String value) {
final String escaped;
if (database == Database.SQL_SERVER) {
escaped = value.replace("%", "[%]").replace("_", "[_]");
} else {
escaped = value.replace("%", ESCAPE_CHAR + "%").replace("_", ESCAPE_CHAR + "_");
}
return replaceIfRequired(escaped);
}
private static String replaceIfRequired(final String escapedValue) {
final String finalizedValue;
if (escapedValue.contains(ESCAPE_CHAR_WITH_ASTERISK)) {
finalizedValue = escapedValue.replace(ESCAPE_CHAR_WITH_ASTERISK, "$")
.replace(LIKE_WILDCARD, '%')
.replace("$", ESCAPE_CHAR_WITH_ASTERISK);
} else {
finalizedValue = escapedValue.replace(LIKE_WILDCARD, '%');
}
return finalizedValue;
}
private List<Predicate> acceptChildren(final LogicalNode node) {
final List<Predicate> children = new ArrayList<>();
for (final Node child : node.getChildren()) {
final List<Predicate> accept = child.accept(this);
if (!CollectionUtils.isEmpty(accept)) {
children.addAll(accept);
} else {
log.debug("visit logical node children but could not parse it, ignoring {}", child);
}
}
return children;
}
@SuppressWarnings("java:S1221") // java:S1221 - intentionally to match the SQL wording
private Predicate equal(final Path<String> expressionToCompare, final String sqlValue) {
if (caseWise(expressionToCompare)) {
return cb.equal(cb.upper(expressionToCompare), sqlValue.toUpperCase());
} else {
return cb.equal(expressionToCompare, sqlValue);
}
}
private Predicate notEqual(final Path<String> expressionToCompare, String valueStr) {
if (caseWise(expressionToCompare)) {
return cb.notEqual(cb.upper(expressionToCompare), valueStr.toUpperCase());
} else {
return cb.notEqual(expressionToCompare, valueStr);
}
}
private Predicate like(final Path<String> expressionToCompare, final String sqlValue) {
if (caseWise(expressionToCompare)) {
return cb.like(cb.upper(expressionToCompare), sqlValue.toUpperCase(), ESCAPE_CHAR);
} else {
return cb.like(expressionToCompare, sqlValue, ESCAPE_CHAR);
}
}
private Predicate notLike(final Path<String> expressionToCompare, final String sqlValue) {
if (caseWise(expressionToCompare)) {
return cb.notLike(cb.upper(expressionToCompare), sqlValue.toUpperCase(), ESCAPE_CHAR);
} else {
return cb.notLike(expressionToCompare, sqlValue, ESCAPE_CHAR);
}
}
private Predicate in(final Path<String> expressionToCompare, final List<Object> values) {
if (ensureIgnoreCase && expressionToCompare.getJavaType() == String.class) {
final List<String> inParams = values.stream()
.filter(String.class::isInstance)
.map(String.class::cast).map(String::toUpperCase).toList();
return inParams.isEmpty() ? expressionToCompare.in(values) : cb.upper(expressionToCompare).in(inParams);
} else {
return expressionToCompare.in(values);
}
}
private boolean caseWise(final Path<?> fieldPath) {
return ensureIgnoreCase && fieldPath.getJavaType() == String.class;
}
}

View File

@@ -1,78 +0,0 @@
/**
* Copyright (c) 2025 Contributors to the Eclipse Foundation
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.eclipse.hawkbit.repository.jpa.rsqllegacy;
import static org.eclipse.hawkbit.repository.jpa.rsqllegacy.AbstractRSQLVisitor.OPERATORS;
import static org.eclipse.hawkbit.repository.rsql.RsqlConfigHolder.RsqlToSpecBuilder.LEGACY_G1;
import java.util.List;
import jakarta.persistence.criteria.Predicate;
import cz.jirutka.rsql.parser.RSQLParser;
import cz.jirutka.rsql.parser.RSQLParserException;
import cz.jirutka.rsql.parser.ast.Node;
import cz.jirutka.rsql.parser.ast.RSQLVisitor;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.hawkbit.repository.RsqlQueryField;
import org.eclipse.hawkbit.repository.exception.RSQLParameterSyntaxException;
import org.eclipse.hawkbit.repository.rsql.RsqlConfigHolder;
import org.eclipse.hawkbit.repository.rsql.VirtualPropertyReplacer;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.orm.jpa.vendor.Database;
import org.springframework.util.CollectionUtils;
@Slf4j
public class SpecificationBuilderLegacy<A extends Enum<A> & RsqlQueryField, T> {
private final Class<A> rsqlQueryFieldType;
private final VirtualPropertyReplacer virtualPropertyReplacer;
private final Database database;
public SpecificationBuilderLegacy(
final Class<A> rsqlQueryFieldType, final VirtualPropertyReplacer virtualPropertyReplacer, final Database database) {
this.rsqlQueryFieldType = rsqlQueryFieldType;
this.virtualPropertyReplacer = virtualPropertyReplacer;
this.database = database;
}
public Specification<T> specification(final String rsql) {
return (root, query, cb) -> {
final RsqlConfigHolder rsqlConfigHolder = RsqlConfigHolder.getInstance();
final Node rootNode = parseRsql(rsql, rsqlConfigHolder);
query.distinct(true);
final boolean ensureIgnoreCase = !rsqlConfigHolder.isCaseInsensitiveDB() && rsqlConfigHolder.isIgnoreCase();
final RSQLVisitor<List<Predicate>, String> jpqQueryRSQLVisitor =
rsqlConfigHolder.getRsqlToSpecBuilder() == LEGACY_G1
? new JpaQueryRsqlVisitor<>(root, cb, rsqlQueryFieldType, virtualPropertyReplacer, database, query, ensureIgnoreCase)
: new JpaQueryRsqlVisitorG2<>(rsqlQueryFieldType, root, query, cb, database, virtualPropertyReplacer, ensureIgnoreCase);
final List<Predicate> accept = rootNode.accept(jpqQueryRSQLVisitor);
if (CollectionUtils.isEmpty(accept)) {
return cb.conjunction();
} else {
return cb.and(accept.toArray(new Predicate[0]));
}
};
}
private static Node parseRsql(final String rsql, final RsqlConfigHolder rsqlConfigHolder) {
log.debug("Parsing rsql string {}", rsql);
try {
return new RSQLParser(OPERATORS).parse(rsqlConfigHolder.isCaseInsensitiveDB() || rsqlConfigHolder.isIgnoreCase() ? rsql.toLowerCase() : rsql);
} catch (final IllegalArgumentException e) {
throw new RSQLParameterSyntaxException("RSQL filter must not be null", e);
} catch (final RSQLParserException e) {
throw new RSQLParameterSyntaxException(e);
}
}
}

View File

@@ -12,7 +12,7 @@ package org.eclipse.hawkbit.repository.jpa.management;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.assertj.core.api.Assertions.fail;
import static org.eclipse.hawkbit.repository.rsql.RsqlConfigHolder.RsqlToSpecBuilder.LEGACY_G1;
import static org.eclipse.hawkbit.repository.jpa.rsql.RsqlConfigHolder.RsqlToSpecBuilder.LEGACY_G1;
import java.net.URI;
import java.util.ArrayList;
@@ -69,7 +69,7 @@ import org.eclipse.hawkbit.repository.model.TargetTag;
import org.eclipse.hawkbit.repository.model.TargetType;
import org.eclipse.hawkbit.repository.model.TargetTypeAssignmentResult;
import org.eclipse.hawkbit.repository.model.TargetUpdateStatus;
import org.eclipse.hawkbit.repository.rsql.RsqlConfigHolder;
import org.eclipse.hawkbit.repository.jpa.rsql.RsqlConfigHolder;
import org.eclipse.hawkbit.repository.test.matcher.Expect;
import org.eclipse.hawkbit.repository.test.matcher.ExpectEvents;
import org.eclipse.hawkbit.repository.test.util.SecurityContextSwitch;

View File

@@ -1,106 +0,0 @@
/**
* Copyright (c) 2025 Contributors to the Eclipse Foundation
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.eclipse.hawkbit.repository.jpa.rsql;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.List;
import java.util.Map;
import jakarta.persistence.Query;
import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.hibernate.engine.spi.SessionFactoryImplementor;
import org.hibernate.engine.spi.SharedSessionContractImplementor;
import org.hibernate.metamodel.mapping.MappingModelExpressible;
import org.hibernate.query.spi.QueryEngine;
import org.hibernate.query.spi.QueryParameterImplementor;
import org.hibernate.query.sqm.internal.QuerySqmImpl;
import org.hibernate.query.sqm.internal.SqmUtil;
import org.hibernate.query.sqm.spi.SqmParameterMappingModelResolutionAccess;
import org.hibernate.query.sqm.sql.SqmTranslation;
import org.hibernate.query.sqm.sql.SqmTranslator;
import org.hibernate.query.sqm.sql.SqmTranslatorFactory;
import org.hibernate.query.sqm.tree.SqmDmlStatement;
import org.hibernate.query.sqm.tree.expression.SqmParameter;
import org.hibernate.query.sqm.tree.select.SqmSelectStatement;
import org.hibernate.sql.ast.SqlAstTranslatorFactory;
import org.hibernate.sql.ast.tree.MutationStatement;
import org.hibernate.sql.ast.tree.Statement;
import org.hibernate.sql.ast.tree.select.SelectStatement;
import org.hibernate.sql.exec.spi.JdbcParameterBindings;
import org.hibernate.sql.exec.spi.JdbcParametersList;
@NoArgsConstructor(access = lombok.AccessLevel.PRIVATE)
@Slf4j
public class HibernateUtils {
private static final Method getSqmTranslatorFactory;
static {
Method method = null;
try {
method = QueryEngine.class.getMethod("getSqmTranslatorFactory");
} catch (final NoSuchMethodException e) {
log.warn("Can't resolve getSqmTranslatorFactory method (Utils.toString won't work)", e);
}
getSqmTranslatorFactory = method;
}
public static String toSql(final Query query) {
if (getSqmTranslatorFactory == null) {
throw new UnsupportedOperationException("SqmTranslatorFactory resolver is not available");
}
final QuerySqmImpl<?> hqlQuery = query.unwrap(QuerySqmImpl.class);
final SessionFactoryImplementor factory = hqlQuery.getSessionFactory();
final SharedSessionContractImplementor session = hqlQuery.getSession();
final SessionFactoryImplementor sessionFactory = session.getFactory();
final SqmTranslatorFactory sqmTranslatorFactory;
try {
sqmTranslatorFactory = (SqmTranslatorFactory) getSqmTranslatorFactory.invoke(factory.getQueryEngine());
} catch (final IllegalAccessException | InvocationTargetException e) {
throw new UnsupportedOperationException("Can't create SqmTranslatorFactory", e);
}
final SqmTranslator<? extends Statement> sqmSelectTranslator =
hqlQuery.getSqmStatement() instanceof SqmSelectStatement<?> selectStatement
? sqmTranslatorFactory.createSelectTranslator(selectStatement,
hqlQuery.getQueryOptions(), hqlQuery.getDomainParameterXref(), hqlQuery.getQueryParameterBindings(),
hqlQuery.getLoadQueryInfluencers(), sessionFactory, false)
: sqmTranslatorFactory.createMutationTranslator((SqmDmlStatement<?>) hqlQuery.getSqmStatement(),
hqlQuery.getQueryOptions(), hqlQuery.getDomainParameterXref(), hqlQuery.getQueryParameterBindings(),
hqlQuery.getLoadQueryInfluencers(), sessionFactory);
final SqmTranslation<? extends Statement> sqmTranslation = sqmSelectTranslator.translate();
final SqlAstTranslatorFactory sqlAstTranslatorFactory = factory.getJdbcServices().getJdbcEnvironment().getSqlAstTranslatorFactory();
final Map<QueryParameterImplementor<?>, Map<SqmParameter<?>, List<JdbcParametersList>>> jdbcParamsXref = SqmUtil.generateJdbcParamsXref(
hqlQuery.getDomainParameterXref(), sqmTranslation::getJdbcParamsBySqmParam);
final JdbcParameterBindings jdbcParameterBindings = SqmUtil.createJdbcParameterBindings(hqlQuery.getQueryParameterBindings(),
hqlQuery.getDomainParameterXref(), jdbcParamsXref, factory.getRuntimeMetamodels().getMappingMetamodel(),
sqmSelectTranslator.getFromClauseAccess()::findTableGroup, new SqmParameterMappingModelResolutionAccess() {
@Override
@SuppressWarnings("unchecked")
public <T> MappingModelExpressible<T> getResolvedMappingModelType(final SqmParameter<T> parameter) {
return (MappingModelExpressible<T>) sqmTranslation.getSqmParameterMappingModelTypeResolutions().get(parameter);
}
}, hqlQuery.getSession());
return (sqmTranslation.getSqlAst() instanceof SelectStatement selectStatement
? sqlAstTranslatorFactory.buildSelectTranslator(factory, selectStatement)
.translate(jdbcParameterBindings, hqlQuery.getQueryOptions())
: sqlAstTranslatorFactory.buildMutationTranslator(factory, (MutationStatement) sqmTranslation.getSqlAst())
.translate(jdbcParameterBindings, hqlQuery.getQueryOptions()))
.getSqlString();
}
}

View File

@@ -11,6 +11,7 @@ package org.eclipse.hawkbit.repository.jpa.rsql;
import static org.assertj.core.api.Assertions.assertThat;
import org.eclipse.hawkbit.repository.jpa.ql.Node;
import org.junit.jupiter.api.Test;
class NodeTest {

View File

@@ -1,76 +0,0 @@
/**
* Copyright (c) 2024 Contributors to the Eclipse Foundation
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.eclipse.hawkbit.repository.jpa.rsql;
import java.lang.reflect.InvocationTargetException;
import jakarta.persistence.EntityManager;
import jakarta.persistence.Query;
import jakarta.persistence.TypedQuery;
import jakarta.persistence.criteria.CriteriaBuilder;
import jakarta.persistence.criteria.CriteriaQuery;
import org.eclipse.hawkbit.repository.RsqlQueryField;
import org.eclipse.hawkbit.repository.rsql.RsqlConfigHolder;
import org.eclipse.hawkbit.repository.rsql.RsqlConfigHolder.RsqlToSpecBuilder;
import org.springframework.orm.jpa.vendor.Database;
public class RSQLToSQL {
private static final Database DATABASE = Database.H2;
private final EntityManager entityManager;
private final boolean isEclipselink;
public RSQLToSQL(final EntityManager entityManager) {
this.entityManager = entityManager;
isEclipselink = entityManager.getProperties().keySet().stream().anyMatch(key -> key.startsWith("eclipselink."));
}
public <T, A extends Enum<A> & RsqlQueryField> String toSQL(
final Class<T> domainClass, final Class<A> fieldsClass, final String rsql, final RsqlToSpecBuilder rsqlToSpecBuilder) {
final CriteriaQuery<T> query = createQuery(domainClass, fieldsClass, rsql, rsqlToSpecBuilder);
final TypedQuery<?> typedQuery = entityManager.createQuery(query);
if (isEclipselink) {
try {
return (String)Class.forName("org.eclipse.hawkbit.repository.jpa.EclipselinkUtils")
.getMethod("toSql", Query.class)
.invoke(null, typedQuery);
} catch (final IllegalAccessException | NoSuchMethodException | ClassNotFoundException e) {
throw new IllegalStateException(e);
} catch (final InvocationTargetException e) {
if (e.getCause() instanceof RuntimeException re) {
throw re;
} else {
throw new IllegalStateException(e.getCause());
}
}
} else {
return HibernateUtils.toSql(typedQuery);
}
}
private <T, A extends Enum<A> & RsqlQueryField> CriteriaQuery<T> createQuery(
final Class<T> domainClass, final Class<A> fieldsClass, final String rsql, final RsqlToSpecBuilder rsqlToSpecBuilder) {
final CriteriaQuery<T> query = entityManager.getCriteriaBuilder().createQuery(domainClass);
final CriteriaBuilder cb = entityManager.getCriteriaBuilder();
final RsqlToSpecBuilder defaultRsqlToSpecBuilder = RsqlConfigHolder.getInstance().getRsqlToSpecBuilder();
if (defaultRsqlToSpecBuilder != rsqlToSpecBuilder) {
RsqlConfigHolder.getInstance().setRsqlToSpecBuilder(rsqlToSpecBuilder);
}
try {
return query.where(RSQLUtility.<A, T> buildRsqlSpecification(rsql, fieldsClass, null, DATABASE)
.toPredicate(query.from(domainClass), cb.createQuery(domainClass), cb));
} finally {
if (defaultRsqlToSpecBuilder != rsqlToSpecBuilder) {
RsqlConfigHolder.getInstance().setRsqlToSpecBuilder(defaultRsqlToSpecBuilder);
}
}
}
}

View File

@@ -31,7 +31,7 @@ import org.springframework.orm.jpa.vendor.Database;
* Feature: Component Tests - Repository<br/>
* Story: RSQL filter actions
*/
class RSQLActionFieldsTest extends AbstractJpaIntegrationTest {
class RsqlActionFieldsTest extends AbstractJpaIntegrationTest {
private JpaTarget target;
private JpaAction action;

View File

@@ -26,7 +26,7 @@ import org.springframework.data.domain.PageRequest;
* Feature: Component Tests - Repository<br/>
* Story: RSQL filter rollout group
*/
class RSQLRolloutFieldTest extends AbstractJpaIntegrationTest {
class RsqlRolloutFieldTest extends AbstractJpaIntegrationTest {
private Rollout rollout;

View File

@@ -28,7 +28,7 @@ import org.springframework.orm.jpa.vendor.Database;
* Feature: Component Tests - Repository<br/>
* Story: RSQL filter rollout group
*/
class RSQLRolloutGroupFieldTest extends AbstractJpaIntegrationTest {
class RsqlRolloutGroupFieldTest extends AbstractJpaIntegrationTest {
private Long rolloutGroupId;
private Rollout rollout;

View File

@@ -27,7 +27,7 @@ import org.springframework.orm.jpa.vendor.Database;
* Feature: Component Tests - Repository<br/>
* Story: RSQL filter software module
*/
class RSQLSoftwareModuleFieldTest extends AbstractJpaIntegrationTest {
class RsqlSoftwareModuleFieldTest extends AbstractJpaIntegrationTest {
private SoftwareModule ah;

View File

@@ -24,7 +24,7 @@ import org.springframework.orm.jpa.vendor.Database;
* Feature: Component Tests - Repository<br/>
* Story: RSQL filter software module test type
*/
class RSQLSoftwareModuleTypeFieldsTest extends AbstractJpaIntegrationTest {
class RsqlSoftwareModuleTypeFieldsTest extends AbstractJpaIntegrationTest {
/**
* Test filter software module test type by id

View File

@@ -25,7 +25,7 @@ import org.springframework.data.domain.PageRequest;
* Feature: Component Tests - Repository<br/>
* Story: RSQL filter target and distribution set tags
*/
class RSQLTagFieldsTest extends AbstractJpaIntegrationTest {
class RsqlTagFieldsTest extends AbstractJpaIntegrationTest {
@BeforeEach
void seuptBeforeTest() {

View File

@@ -11,8 +11,8 @@ package org.eclipse.hawkbit.repository.jpa.rsql;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.eclipse.hawkbit.repository.rsql.RsqlConfigHolder.RsqlToSpecBuilder.LEGACY_G2;
import static org.eclipse.hawkbit.repository.rsql.RsqlConfigHolder.RsqlToSpecBuilder.LEGACY_G1;
import static org.eclipse.hawkbit.repository.jpa.rsql.RsqlConfigHolder.RsqlToSpecBuilder.LEGACY_G2;
import static org.eclipse.hawkbit.repository.jpa.rsql.RsqlConfigHolder.RsqlToSpecBuilder.LEGACY_G1;
import java.util.Arrays;
import java.util.Map;
@@ -28,7 +28,6 @@ import org.eclipse.hawkbit.repository.model.DistributionSet;
import org.eclipse.hawkbit.repository.model.Target;
import org.eclipse.hawkbit.repository.model.TargetTag;
import org.eclipse.hawkbit.repository.model.TargetType;
import org.eclipse.hawkbit.repository.rsql.RsqlConfigHolder;
import org.eclipse.hawkbit.repository.rsql.VirtualPropertyReplacer;
import org.eclipse.hawkbit.repository.test.util.TestdataFactory;
import org.junit.jupiter.api.BeforeEach;
@@ -41,7 +40,7 @@ import org.springframework.data.domain.Slice;
* Story: RSQL filter target
*/
@SuppressWarnings("java:S6813") // constructor injects are not possible for test classes
class RSQLTargetFieldTest extends AbstractJpaIntegrationTest {
class RsqlTargetFieldTest extends AbstractJpaIntegrationTest {
private static final String OR = ",";
private static final String AND = ";";
@@ -387,26 +386,26 @@ class RSQLTargetFieldTest extends AbstractJpaIntegrationTest {
*/
@Test
void rsqlValidTargetFields() {
RSQLUtility.validateRsqlFor(
RsqlUtility.validateRsqlFor(
"ID == '0123' and NAME == abcd and DESCRIPTION == absd and CREATEDAT =lt= 0123 and LASTMODIFIEDAT =gt= 0123" +
" and CONTROLLERID == 0123 and UPDATESTATUS == PENDING and IPADDRESS == 0123 and LASTCONTROLLERREQUESTAT == 0123" +
" and tag == beta",
TargetFields.class, JpaTarget.class, virtualPropertyReplacer, entityManager);
RSQLUtility.validateRsqlFor(
RsqlUtility.validateRsqlFor(
"ASSIGNEDDS.name == abcd and ASSIGNEDDS.version == 0123 and INSTALLEDDS.name == abcd and INSTALLEDDS.version == 0123",
TargetFields.class, JpaTarget.class, virtualPropertyReplacer, entityManager);
RSQLUtility.validateRsqlFor(
RsqlUtility.validateRsqlFor(
"ATTRIBUTE.subkey1 == test and ATTRIBUTE.subkey2 == test and METADATA.metakey1 == abcd and METADATA.metavalue2 == asdfg",
TargetFields.class, JpaTarget.class, virtualPropertyReplacer, entityManager);
RSQLUtility.validateRsqlFor(
RsqlUtility.validateRsqlFor(
"CREATEDAT =lt= ${NOW_TS} and LASTMODIFIEDAT =ge= ${OVERDUE_TS}",
TargetFields.class, JpaTarget.class, virtualPropertyReplacer, entityManager);
RSQLUtility.validateRsqlFor(
RsqlUtility.validateRsqlFor(
"ATTRIBUTE.test.dot == test and ATTRIBUTE.subkey2 == test and METADATA.test.dot == abcd and METADATA.metavalue2 == asdfg",
TargetFields.class, JpaTarget.class, virtualPropertyReplacer, entityManager);
assertThatExceptionOfType(RSQLParameterUnsupportedFieldException.class)
.isThrownBy(() -> RSQLUtility.validateRsqlFor(
.isThrownBy(() -> RsqlUtility.validateRsqlFor(
"wrongfield == abcd",
TargetFields.class, JpaTarget.class, virtualPropertyReplacer, entityManager));
}
@@ -453,7 +452,7 @@ class RSQLTargetFieldTest extends AbstractJpaIntegrationTest {
private void assertRSQLQueryThrowsException(final String rsql) {
assertThatExceptionOfType(RSQLParameterUnsupportedFieldException.class)
.isThrownBy(() -> RSQLUtility.validateRsqlFor(
.isThrownBy(() -> RsqlUtility.validateRsqlFor(
rsql, TargetFields.class, JpaTarget.class, virtualPropertyReplacer, entityManager));
}
}

View File

@@ -27,7 +27,7 @@ import org.springframework.orm.jpa.vendor.Database;
* Feature: Component Tests - Repository<br/>
* Story: RSQL filter target filter query
*/
class RSQLTargetFilterQueryFieldsTest extends AbstractJpaIntegrationTest {
class RsqlTargetFilterQueryFieldsTest extends AbstractJpaIntegrationTest {
private TargetFilterQuery filter1;
private TargetFilterQuery filter2;

View File

@@ -9,9 +9,9 @@
*/
package org.eclipse.hawkbit.repository.jpa.rsql;
import static org.eclipse.hawkbit.repository.rsql.RsqlConfigHolder.RsqlToSpecBuilder.LEGACY_G2;
import static org.eclipse.hawkbit.repository.rsql.RsqlConfigHolder.RsqlToSpecBuilder.G3;
import static org.eclipse.hawkbit.repository.rsql.RsqlConfigHolder.RsqlToSpecBuilder.LEGACY_G1;
import static org.eclipse.hawkbit.repository.jpa.rsql.RsqlConfigHolder.RsqlToSpecBuilder.LEGACY_G2;
import static org.eclipse.hawkbit.repository.jpa.rsql.RsqlConfigHolder.RsqlToSpecBuilder.G3;
import static org.eclipse.hawkbit.repository.jpa.rsql.RsqlConfigHolder.RsqlToSpecBuilder.LEGACY_G1;
import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.NONE;
import jakarta.persistence.EntityManager;
@@ -21,6 +21,7 @@ import org.eclipse.hawkbit.repository.RsqlQueryField;
import org.eclipse.hawkbit.repository.TargetFields;
import org.eclipse.hawkbit.repository.jpa.RepositoryApplicationConfiguration;
import org.eclipse.hawkbit.repository.jpa.model.JpaTarget;
import org.eclipse.hawkbit.repository.jpa.ql.utils.HawkbitQlToSql;
import org.eclipse.hawkbit.repository.test.TestConfiguration;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
@@ -37,10 +38,10 @@ import org.springframework.test.context.ContextConfiguration;
@ContextConfiguration(classes = { RepositoryApplicationConfiguration.class, TestConfiguration.class })
@Disabled("For manual run only, while playing around with RSQL to SQL")
@SuppressWarnings("java:S2699") // java:S2699 - manual test, don't actually does assertions
class RSQLToSQLTest {
class RsqlToSqlTest {
private static final boolean FULL = Boolean.getBoolean("full");
private RSQLToSQL rsqlToSQL;
private HawkbitQlToSql rsqlToSQL;
@Test
void printPG() {
@@ -107,7 +108,7 @@ class RSQLToSQLTest {
@PersistenceContext
private void setEntityManager(final EntityManager entityManager) {
rsqlToSQL = new RSQLToSQL(entityManager);
rsqlToSQL = new HawkbitQlToSql(entityManager);
}
private <T, A extends Enum<A> & RsqlQueryField> void print(final Class<T> domainClass, final Class<A> fieldsClass, final String rsql) {

View File

@@ -46,7 +46,6 @@ import org.eclipse.hawkbit.repository.model.SoftwareModule;
import org.eclipse.hawkbit.repository.model.TenantConfigurationValue;
import org.eclipse.hawkbit.repository.model.helper.SystemSecurityContextHolder;
import org.eclipse.hawkbit.repository.model.helper.TenantConfigurationManagementHolder;
import org.eclipse.hawkbit.repository.rsql.RsqlConfigHolder;
import org.eclipse.hawkbit.repository.rsql.VirtualPropertyReplacer;
import org.eclipse.hawkbit.repository.rsql.VirtualPropertyResolver;
import org.eclipse.hawkbit.security.SystemSecurityContext;
@@ -73,7 +72,7 @@ import org.springframework.test.context.junit.jupiter.SpringExtension;
@Disabled
// TODO: fully document tests -> description for long text and reasonable
// method name as short text
class RSQLUtilityTest {
class RsqlUtilityTest {
private static final TenantConfigurationValue<String> TEST_POLLING_TIME_INTERVAL =
TenantConfigurationValue.<String> builder().value("00:05:00").build();
@@ -109,7 +108,7 @@ class RSQLUtilityTest {
@Test
void wrongFieldThrowUnsupportedFieldException() {
when(baseSoftwareModuleRootMock.getJavaType()).thenReturn((Class) SoftwareModule.class);
final Specification<Object> rsqlSpecification = RSQLUtility.buildRsqlSpecification("unknownField==abc", SoftwareModuleFields.class, null, testDb);
final Specification<Object> rsqlSpecification = RsqlUtility.buildRsqlSpecification("unknownField==abc", SoftwareModuleFields.class, null, testDb);
assertThatExceptionOfType(RSQLParameterUnsupportedFieldException.class)
.as("RSQLParameterUnsupportedFieldException because of unknown RSQL field")
.isThrownBy(() -> rsqlSpecification.toPredicate(baseSoftwareModuleRootMock, criteriaQueryMock, criteriaBuilderMock));
@@ -118,25 +117,25 @@ class RSQLUtilityTest {
@Test
void wrongRsqlMapSyntaxThrowSyntaxException() {
final Specification<Object> rsqlSpecification =
RSQLUtility.buildRsqlSpecification(TargetFields.ATTRIBUTE + "==abc", TargetFields.class, null, testDb);
RsqlUtility.buildRsqlSpecification(TargetFields.ATTRIBUTE + "==abc", TargetFields.class, null, testDb);
assertThatExceptionOfType(RSQLParameterUnsupportedFieldException.class)
.as("RSQLParameterSyntaxException for target attributes map, caused by wrong RSQL syntax (key was not present)")
.isThrownBy(() -> rsqlSpecification.toPredicate(baseSoftwareModuleRootMock, criteriaQueryMock, criteriaBuilderMock));
final Specification<Object> rsqlSpecification2 =
RSQLUtility.buildRsqlSpecification(TargetFields.ATTRIBUTE + ".unknown.wrong==abc", TargetFields.class, null, testDb);
RsqlUtility.buildRsqlSpecification(TargetFields.ATTRIBUTE + ".unknown.wrong==abc", TargetFields.class, null, testDb);
assertThatExceptionOfType(RSQLParameterUnsupportedFieldException.class)
.as("RSQLParameterSyntaxException for target attributes map, caused by wrong RSQL syntax (key includes dots)")
.isThrownBy(() -> rsqlSpecification2.toPredicate(baseSoftwareModuleRootMock, criteriaQueryMock, criteriaBuilderMock));
final Specification<Object> rsqlSpecification3 =
RSQLUtility.buildRsqlSpecification(TargetFields.METADATA + ".unknown.wrong==abc", TargetFields.class, null, testDb);
RsqlUtility.buildRsqlSpecification(TargetFields.METADATA + ".unknown.wrong==abc", TargetFields.class, null, testDb);
assertThatExceptionOfType(RSQLParameterUnsupportedFieldException.class)
.as("RSQLParameterSyntaxException for target metadata map, caused by wrong RSQL syntax (key includes dots)")
.isThrownBy(() -> rsqlSpecification3.toPredicate(baseSoftwareModuleRootMock, criteriaQueryMock, criteriaBuilderMock));
final Specification<Object> rsqlSpecification4 =
RSQLUtility.buildRsqlSpecification(DistributionSetFields.METADATA + "==abc", TargetFields.class, null, testDb);
RsqlUtility.buildRsqlSpecification(DistributionSetFields.METADATA + "==abc", TargetFields.class, null, testDb);
assertThatExceptionOfType(RSQLParameterUnsupportedFieldException.class)
.as("RSQLParameterSyntaxException for distribution set metadata map, caused by wrong RSQL syntax (key was not present)\"")
.isThrownBy(() -> rsqlSpecification4.toPredicate(baseSoftwareModuleRootMock, criteriaQueryMock, criteriaBuilderMock));
@@ -145,19 +144,19 @@ class RSQLUtilityTest {
@Test
void wrongRsqlSubEntitySyntaxThrowSyntaxException() {
final Specification<Object> rsqlSpecification =
RSQLUtility.buildRsqlSpecification(TargetFields.ASSIGNEDDS + "==abc", TargetFields.class, null, testDb);
RsqlUtility.buildRsqlSpecification(TargetFields.ASSIGNEDDS + "==abc", TargetFields.class, null, testDb);
assertThatExceptionOfType(RSQLParameterUnsupportedFieldException.class)
.as("RSQLParameterSyntaxException because of wrong RSQL syntax")
.isThrownBy(() -> rsqlSpecification.toPredicate(baseSoftwareModuleRootMock, criteriaQueryMock, criteriaBuilderMock));
final Specification<Object> rsqlSpecification2 =
RSQLUtility.buildRsqlSpecification(TargetFields.ASSIGNEDDS + ".unknownField==abc", TargetFields.class, null, testDb);
RsqlUtility.buildRsqlSpecification(TargetFields.ASSIGNEDDS + ".unknownField==abc", TargetFields.class, null, testDb);
assertThatExceptionOfType(RSQLParameterUnsupportedFieldException.class)
.as("RSQLParameterSyntaxException because of wrong RSQL syntax")
.isThrownBy(() -> rsqlSpecification2.toPredicate(baseSoftwareModuleRootMock, criteriaQueryMock, criteriaBuilderMock));
final Specification<Object> rsqlSpecification3 =
RSQLUtility.buildRsqlSpecification(TargetFields.ASSIGNEDDS + ".unknownField.ToMuch==abc", TargetFields.class, null, testDb);
RsqlUtility.buildRsqlSpecification(TargetFields.ASSIGNEDDS + ".unknownField.ToMuch==abc", TargetFields.class, null, testDb);
assertThatExceptionOfType(RSQLParameterUnsupportedFieldException.class)
.as("RSQLParameterSyntaxException because of wrong RSQL syntax")
.isThrownBy(() -> rsqlSpecification3.toPredicate(baseSoftwareModuleRootMock, criteriaQueryMock, criteriaBuilderMock));
@@ -176,7 +175,7 @@ class RSQLUtilityTest {
when(criteriaBuilderMock.and(any(Predicate[].class))).thenReturn(mock(Predicate.class));
// test
RSQLUtility.buildRsqlSpecification(correctRsql, SoftwareModuleFields.class, null, testDb)
RsqlUtility.buildRsqlSpecification(correctRsql, SoftwareModuleFields.class, null, testDb)
.toPredicate(baseSoftwareModuleRootMock, criteriaQueryMock, criteriaBuilderMock);
// verification
@@ -195,7 +194,7 @@ class RSQLUtilityTest {
when(criteriaBuilderMock.upper(pathOfString(baseSoftwareModuleRootMock))).thenReturn(pathOfString(baseSoftwareModuleRootMock));
// test
RSQLUtility.buildRsqlSpecification(correctRsql, SoftwareModuleFields.class, null, testDb)
RsqlUtility.buildRsqlSpecification(correctRsql, SoftwareModuleFields.class, null, testDb)
.toPredicate(baseSoftwareModuleRootMock, criteriaQueryMock, criteriaBuilderMock);
// verification
@@ -217,7 +216,7 @@ class RSQLUtilityTest {
when(criteriaBuilderMock.upper(pathOfString(baseSoftwareModuleRootMock))).thenReturn(pathOfString(baseSoftwareModuleRootMock));
// test
RSQLUtility.buildRsqlSpecification(correctRsql, SoftwareModuleFields.class, null, testDb)
RsqlUtility.buildRsqlSpecification(correctRsql, SoftwareModuleFields.class, null, testDb)
.toPredicate(baseSoftwareModuleRootMock, criteriaQueryMock, criteriaBuilderMock);
// verification
@@ -247,7 +246,7 @@ class RSQLUtilityTest {
when(subqueryMock.where(any(Expression.class))).thenReturn(subqueryMock);
// test
RSQLUtility.buildRsqlSpecification(correctRsql, SoftwareModuleFields.class, null, testDb)
RsqlUtility.buildRsqlSpecification(correctRsql, SoftwareModuleFields.class, null, testDb)
.toPredicate(baseSoftwareModuleRootMock, criteriaQueryMock, criteriaBuilderMock);
// verification
@@ -264,7 +263,7 @@ class RSQLUtilityTest {
when(criteriaBuilderMock.greaterThanOrEqualTo(any(Expression.class), any(String.class))).thenReturn(mock(Predicate.class));
when(criteriaBuilderMock.upper(pathOfString(baseSoftwareModuleRootMock))).thenReturn(pathOfString(baseSoftwareModuleRootMock));
// test
RSQLUtility.buildRsqlSpecification(correctRsql, SoftwareModuleFields.class, null, Database.H2)
RsqlUtility.buildRsqlSpecification(correctRsql, SoftwareModuleFields.class, null, Database.H2)
.toPredicate(baseSoftwareModuleRootMock, criteriaQueryMock, criteriaBuilderMock);
// verification
@@ -282,7 +281,7 @@ class RSQLUtilityTest {
when(criteriaBuilderMock.greaterThanOrEqualTo(any(Expression.class), any(String.class))).thenReturn(mock(Predicate.class));
when(criteriaBuilderMock.upper(pathOfString(baseSoftwareModuleRootMock))).thenReturn(pathOfString(baseSoftwareModuleRootMock));
// test
RSQLUtility.buildRsqlSpecification(correctRsql, SoftwareModuleFields.class, null, Database.H2)
RsqlUtility.buildRsqlSpecification(correctRsql, SoftwareModuleFields.class, null, Database.H2)
.toPredicate(baseSoftwareModuleRootMock, criteriaQueryMock, criteriaBuilderMock);
// verification
@@ -304,7 +303,7 @@ class RSQLUtilityTest {
when(criteriaBuilderMock.<String> greaterThanOrEqualTo(any(Expression.class), any(String.class))).thenReturn(mock(Predicate.class));
// test
RSQLUtility.buildRsqlSpecification(correctRsql, SoftwareModuleFields.class, null, Database.SQL_SERVER)
RsqlUtility.buildRsqlSpecification(correctRsql, SoftwareModuleFields.class, null, Database.SQL_SERVER)
.toPredicate(baseSoftwareModuleRootMock, criteriaQueryMock, criteriaBuilderMock);
// verification
@@ -322,7 +321,7 @@ class RSQLUtilityTest {
when(criteriaBuilderMock.<String> greaterThanOrEqualTo(any(Expression.class), any(String.class)))
.thenReturn(mock(Predicate.class));
// test
RSQLUtility.buildRsqlSpecification(correctRsql, SoftwareModuleFields.class, null, testDb)
RsqlUtility.buildRsqlSpecification(correctRsql, SoftwareModuleFields.class, null, testDb)
.toPredicate(baseSoftwareModuleRootMock, criteriaQueryMock, criteriaBuilderMock);
// verification
@@ -339,7 +338,7 @@ class RSQLUtilityTest {
when(criteriaBuilderMock.equal(any(Root.class), any(TestValueEnum.class))).thenReturn(mock(Predicate.class));
// test
RSQLUtility.buildRsqlSpecification(correctRsql, TestFieldEnum.class, null, testDb)
RsqlUtility.buildRsqlSpecification(correctRsql, TestFieldEnum.class, null, testDb)
.toPredicate(baseSoftwareModuleRootMock, criteriaQueryMock, criteriaBuilderMock);
// verification
@@ -355,7 +354,7 @@ class RSQLUtilityTest {
when(baseSoftwareModuleRootMock.getJavaType()).thenReturn((Class) TestValueEnum.class);
when(criteriaBuilderMock.equal(any(Root.class), anyString())).thenReturn(mock(Predicate.class));
final Specification<Object> rsqlSpecification = RSQLUtility.buildRsqlSpecification(correctRsql, TestFieldEnum.class, null, testDb);
final Specification<Object> rsqlSpecification = RsqlUtility.buildRsqlSpecification(correctRsql, TestFieldEnum.class, null, testDb);
assertThatExceptionOfType(RSQLParameterUnsupportedFieldException.class)
.as("RSQLParameterUnsupportedFieldException for wrong enum value")
.isThrownBy(() -> rsqlSpecification.toPredicate(baseSoftwareModuleRootMock, criteriaQueryMock, criteriaBuilderMock));
@@ -377,7 +376,7 @@ class RSQLUtilityTest {
when(criteriaBuilderMock.lessThanOrEqualTo(any(Expression.class), eq(overduePropPlaceholder))).thenReturn(mock(Predicate.class));
// test
RSQLUtility.buildRsqlSpecification(correctRsql, TestFieldEnum.class, setupMacroLookup(), testDb)
RsqlUtility.buildRsqlSpecification(correctRsql, TestFieldEnum.class, setupMacroLookup(), testDb)
.toPredicate(baseSoftwareModuleRootMock, criteriaQueryMock, criteriaBuilderMock);
// verification
@@ -401,7 +400,7 @@ class RSQLUtilityTest {
.thenReturn(mock(Predicate.class));
// test
RSQLUtility.buildRsqlSpecification(correctRsql, TestFieldEnum.class, setupMacroLookup(), testDb)
RsqlUtility.buildRsqlSpecification(correctRsql, TestFieldEnum.class, setupMacroLookup(), testDb)
.toPredicate(baseSoftwareModuleRootMock, criteriaQueryMock, criteriaBuilderMock);
// verification

View File

@@ -1,235 +0,0 @@
/**
* Copyright (c) 2025 Contributors to the Eclipse Foundation
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.eclipse.hawkbit.repository.jpa.rsql.sa;
import static org.eclipse.hawkbit.repository.jpa.rsql.Node.Comparison.Operator.EQ;
import static org.eclipse.hawkbit.repository.jpa.rsql.Node.Comparison.Operator.GT;
import static org.eclipse.hawkbit.repository.jpa.rsql.Node.Comparison.Operator.GTE;
import static org.eclipse.hawkbit.repository.jpa.rsql.Node.Comparison.Operator.IN;
import static org.eclipse.hawkbit.repository.jpa.rsql.Node.Comparison.Operator.LIKE;
import static org.eclipse.hawkbit.repository.jpa.rsql.Node.Comparison.Operator.LT;
import static org.eclipse.hawkbit.repository.jpa.rsql.Node.Comparison.Operator.LTE;
import static org.eclipse.hawkbit.repository.jpa.rsql.Node.Comparison.Operator.NE;
import static org.eclipse.hawkbit.repository.jpa.rsql.Node.Comparison.Operator.NOT_IN;
import static org.eclipse.hawkbit.repository.jpa.rsql.Node.Comparison.Operator.NOT_LIKE;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.util.Arrays;
import java.util.Collection;
import java.util.Map;
import java.util.Objects;
import java.util.function.BiFunction;
import org.eclipse.hawkbit.repository.jpa.rsql.Node;
import org.eclipse.hawkbit.repository.jpa.rsql.Node.Comparison.Operator;
import org.eclipse.hawkbit.repository.jpa.rsql.RsqlParser;
/**
* Provides matching reference for if an object matches a {@link Node}.
*/
class ReferenceMatcher {
private final Node root;
private ReferenceMatcher(final Node root) {
this.root = root;
}
static ReferenceMatcher of(final Node root) {
return new ReferenceMatcher(root);
}
static ReferenceMatcher ofRsql(final String rsql) {
return of(RsqlParser.parse(rsql));
}
<T> boolean match(final T t) {
return match(t, root);
}
private static <T> boolean match(final T t, final Node node) {
if (node instanceof Node.Comparison comparison) {
final String[] split = comparison.getKey().split("\\.", 2);
try {
final Method fieldGetter = getGetter(t.getClass(), split[0]);
fieldGetter.setAccessible(true);
final Object fieldValue = fieldGetter.invoke(t);
final Operator op = comparison.getOp();
if (Map.class.isAssignableFrom(fieldGetter.getReturnType())) {
if ((op == NE || op == NOT_IN || op == NOT_LIKE)
&& (fieldValue == null || !((Map<?, ?>) fieldValue).containsKey(split[1]))) {
// TODO / recheck - when missing entity shall it be included or not in != or =out=? - now it's not
return false;
}
return compare(
fieldValue == null ? null : ((Map<?, ?>) fieldValue).get(split[1]),
op,
map(
comparison.getValue(),
(Class<?>) ((ParameterizedType) fieldGetter.getGenericReturnType()).getActualTypeArguments()[1]));
} else if (Collection.class.isAssignableFrom(fieldGetter.getReturnType())) { // Set / List
final Object value;
final BiFunction<Object, Operator, Boolean> compare;
if (split.length == 1) {
value = map(comparison.getValue(), fieldGetter.getReturnType());
compare = (e, operator) -> compare(e, operator, value);
} else {
final Method valueGetter = getGetter(
(Class<?>) ((ParameterizedType) fieldGetter.getGenericReturnType()).getActualTypeArguments()[0], split[1]);
value = map(comparison.getValue(), valueGetter.getReturnType());
compare = (e, operator) -> {
try {
return compare(map(e == null ? null : valueGetter.invoke(e), valueGetter.getReturnType()), operator, value);
} catch (final IllegalAccessException | InvocationTargetException ex) {
throw new IllegalArgumentException(ex);
}
};
}
final Collection<?> set = (Collection<?>) fieldValue;
return switch (op) {
case EQ, GT, GTE, LT, LTE, IN, LIKE -> set == null
? false
: set.stream().anyMatch(e -> compare.apply(e, op));
case NE, NOT_IN, NOT_LIKE -> set == null
? true
: set.stream().noneMatch(e -> compare.apply(e, op == NE ? EQ : op == NOT_IN ? IN : LIKE));
};
} else {
if (split.length == 1) {
return compare(fieldValue, op, map(comparison.getValue(), fieldGetter.getReturnType()));
} else {
if (split[1].contains(".")) {
// nested field access
final String[] nestedSplit = split[1].split("\\.", 2);
final Method nestedFieldGetter = getGetter(fieldGetter.getReturnType(), nestedSplit[0]);
nestedFieldGetter.setAccessible(true);
final Method valueGetter = getGetter(nestedFieldGetter.getReturnType(), nestedSplit[1]);
final Object nestedFieldValue = fieldValue == null ? null : nestedFieldGetter.invoke(fieldValue);
return compare(
nestedFieldValue == null ? null : valueGetter.invoke(nestedFieldValue),
op,
map(comparison.getValue(), valueGetter.getReturnType()));
} else {
final Method valueGetter = getGetter(fieldGetter.getReturnType(), split[1]);
return compare(fieldValue == null ? null : valueGetter.invoke(fieldValue), op,
map(comparison.getValue(), valueGetter.getReturnType()));
}
}
}
} catch (final NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
throw new IllegalArgumentException(e);
}
} else if (node instanceof Node.Logical logical) {
return switch (logical.getOp()) {
case AND -> logical.getChildren().stream().allMatch(child -> match(t, child));
case OR -> logical.getChildren().stream().anyMatch(child -> match(t, child));
};
} else {
throw new IllegalArgumentException("Unsupported node type: " + node.getClass());
}
}
private static <T> Method getGetter(final Class<T> t, final String fieldName) throws NoSuchMethodException {
final String getterLowercase = "get" + fieldName.toLowerCase();
return Arrays.stream(t.getMethods())
.filter(method -> getterLowercase.equals(method.getName().toLowerCase()))
.findFirst()
.map(method -> {
method.setAccessible(true);
return method;
}).orElseThrow(() -> new NoSuchMethodException("No getter found for field: " + fieldName + " in class: " + t.getName()));
}
@SuppressWarnings({ "unchecked", "rawtypes" })
private static Object map(final Object value, final Class<?> type) {
if (value instanceof Collection<?> collection) { // in / out
return collection.stream().map(e -> map(e, type)).toList();
}
if (value == null) {
return null;
} else if (type.isInstance(value)) {
return value;
} else if (type.isEnum()) {
return Enum.valueOf((Class<Enum>) type, value.toString());
} else if (type == Boolean.class || type == boolean.class) {
return Boolean.parseBoolean(value.toString());
} else if (type == Integer.class || type == int.class) {
return Integer.parseInt(value.toString());
} else if (type == Long.class || type == long.class) {
return Long.parseLong(value.toString());
} else if (type == Float.class || type == float.class) {
return Float.parseFloat(value.toString());
} else if (type == Double.class || type == double.class) {
return Double.parseDouble(value.toString());
} else if (type == String.class) {
return String.valueOf(value);
} else {
throw new IllegalArgumentException("Unsupported type: " + type);
}
}
private static boolean compare(final Object o1, final Operator op, final Object o2) {
if ((o1 == null || o2 == null) && // null is not comparable!
(op == GT || op == GTE || op == LT || op == LTE)) {
return false;
}
return switch (op) {
case EQ -> Objects.equals(o1, o2);
case NE -> !Objects.equals(o1, o2);
case GT -> compare(o1, o2) > 0;
case GTE -> compare(o1, o2) >= 0;
case LT -> compare(o1, o2) < 0;
case LTE -> compare(o1, o2) <= 0;
case IN -> in(o1, o2);
case NOT_IN -> !in(o1, o2);
case LIKE -> like(o2, o1);
case NOT_LIKE -> !like(o2, o1);
};
}
@SuppressWarnings("unchecked")
private static int compare(final Object o1, final Object o2) {
return toComparable(o1).compareTo(toComparable(o2));
}
@SuppressWarnings("rawtypes")
private static Comparable toComparable(final Object o) {
if (o instanceof Comparable<?> comparable) {
return comparable;
} else {
throw new IllegalArgumentException("Can't cast " + o.getClass() + " to Comparable");
}
}
private static boolean in(final Object o, final Object elementOrCollection) {
if (elementOrCollection instanceof Collection<?> collection) {
return collection.contains(o);
} else {
return Objects.equals(o, elementOrCollection);
}
}
private static boolean like(final Object pattern, final Object value) {
if (pattern instanceof String patternStr) {
if (value instanceof String valueStr) {
return valueStr.matches(patternStr.replace("\\*", "$").replace("*", ".*").replace("$", "\\*"));
} else if (value == null) {
return false; // null value cannot match any pattern
} else {
throw new IllegalArgumentException("LIKE value must be String. Found: " + value.getClass());
}
} else {
throw new IllegalArgumentException("LIKE pattern must be String. Found: " + (pattern == null ? null : pattern.getClass()));
}
}
}

View File

@@ -1,62 +0,0 @@
/**
* Copyright (c) 2025 Contributors to the Eclipse Foundation
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.eclipse.hawkbit.repository.jpa.rsql.sa;
import java.util.Map;
import java.util.Set;
import jakarta.persistence.CollectionTable;
import jakarta.persistence.Column;
import jakarta.persistence.ElementCollection;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.JoinTable;
import jakarta.persistence.ManyToMany;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.MapKeyColumn;
import lombok.Data;
import lombok.experimental.Accessors;
@Entity
@Data
@Accessors(chain = true)
class Root {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
// singular attributes
// basic
private String strValue;
private int intValue;
// entity
@ManyToOne
private Sub subEntity;
// set searchable by key
@ManyToMany(targetEntity = Sub.class)
@JoinTable(
name = "subs",
joinColumns = { @JoinColumn(name = "root") },
inverseJoinColumns = { @JoinColumn(name = "subs") })
private Set<Sub> subSet;
// standard map
@ElementCollection
@CollectionTable(
name = "map",
joinColumns = { @JoinColumn(name = "root") })
@MapKeyColumn(name = "map_key", length = 128)
@Column(name = "map_value", length = 128)
private Map<String, String> subMap;
}

View File

@@ -1,17 +0,0 @@
/**
* Copyright (c) 2025 Contributors to the Eclipse Foundation
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.eclipse.hawkbit.repository.jpa.rsql.sa;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
import org.springframework.data.repository.CrudRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface RootRepository extends CrudRepository<Root, Long>, JpaSpecificationExecutor<Root> {}

View File

@@ -1,128 +0,0 @@
/**
* Copyright (c) 2025 Contributors to the Eclipse Foundation
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.eclipse.hawkbit.repository.jpa.rsql.sa;
import static org.eclipse.hawkbit.repository.rsql.RsqlConfigHolder.RsqlToSpecBuilder.LEGACY_G1;
import static org.eclipse.hawkbit.repository.rsql.RsqlConfigHolder.RsqlToSpecBuilder.LEGACY_G2;
import java.util.Arrays;
import java.util.List;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.hawkbit.repository.RsqlQueryField;
import org.eclipse.hawkbit.repository.jpa.rsqllegacy.SpecificationBuilderLegacy;
import org.eclipse.hawkbit.repository.rsql.RsqlConfigHolder;
import org.eclipse.hawkbit.repository.rsql.RsqlConfigHolder.RsqlToSpecBuilder;
import org.junit.jupiter.api.Test;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.orm.jpa.vendor.Database;
@SuppressWarnings("java:S2699") // java:S2699 - assertions are un the super methods that are called
@Slf4j
class SpecificationBuilderLegacyTest extends SpecificationBuilderTest {
private final SpecificationBuilderLegacy<RootField, Root> builder = new SpecificationBuilderLegacy<>(RootField.class, null, Database.H2);
private static void runWithRsqlToSpecBuilder(final Runnable runnable, final RsqlToSpecBuilder rsqlToSpecBuilder) {
final RsqlToSpecBuilder defaultBuilder = RsqlConfigHolder.getInstance().getRsqlToSpecBuilder();
RsqlConfigHolder.getInstance().setRsqlToSpecBuilder(rsqlToSpecBuilder);
try {
runnable.run();
} finally {
RsqlConfigHolder.getInstance().setRsqlToSpecBuilder(defaultBuilder);
}
}
@Test
void singularStringAttributeG1() {
runWithRsqlToSpecBuilder(super::singularStringAttribute, LEGACY_G1);
}
@Override
@Test
void singularStringAttribute() {
runWithRsqlToSpecBuilder(super::singularStringAttribute, LEGACY_G2);
}
@Test
void singularIntAttributeG1() {
runWithRsqlToSpecBuilder(super::singularIntAttribute, LEGACY_G1);
}
@Override
@Test
void singularIntAttribute() {
runWithRsqlToSpecBuilder(super::singularIntAttribute, LEGACY_G2);
}
@Test
void singularEntityAttributeG1() {
runWithRsqlToSpecBuilder(super::singularEntityAttribute, LEGACY_G1);
}
@Override
@Test
void singularEntityAttribute() {
runWithRsqlToSpecBuilder(super::singularEntityAttribute, LEGACY_G2);
}
@Test
void pluralSubSetAttributeG1() {
runWithRsqlToSpecBuilder(super::pluralSubSetAttribute, LEGACY_G1);
}
@Override
@Test
void pluralSubSetAttribute() {
runWithRsqlToSpecBuilder(super::pluralSubSetAttribute, LEGACY_G2);
}
// Legacy G1 doesn't support hibernate maps
@Override
@Test
void pluralSubMapAttribute() {
runWithRsqlToSpecBuilder(super::pluralSubMapAttribute, LEGACY_G2);
}
@Test
void singularEntitySubSubAttributeG1() {
runWithRsqlToSpecBuilder(super::singularEntitySubSubAttribute, LEGACY_G1);
}
@Override
@Test
void singularEntitySubSubAttribute() {
runWithRsqlToSpecBuilder(super::singularEntitySubSubAttribute, LEGACY_G2);
}
@Override
protected Specification<Root> getSpecification(final String rsql) {
return builder.specification(rsql);
}
@Getter
private enum RootField implements RsqlQueryField {
INTVALUE("intValue"),
STRVALUE("strValue"),
SUBENTITY("subEntity", "strValue", "intValue", "subSub"),
SUBSET("subSet", "strValue", "intValue"),
SUBMAP("subMap");
private final String jpaEntityFieldName;
private final List<String> subEntityAttributes;
RootField(final String jpaEntityFieldName, final String... subFields) {
this.jpaEntityFieldName = jpaEntityFieldName;
this.subEntityAttributes = Arrays.asList(subFields);
}
@Override
public boolean isMap() {
return this == SUBMAP;
}
}
}

View File

@@ -1,500 +0,0 @@
/**
* Copyright (c) 2025 Contributors to the Eclipse Foundation
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.eclipse.hawkbit.repository.jpa.rsql.sa;
import static org.assertj.core.api.Assertions.assertThat;
import static org.eclipse.hawkbit.repository.rsql.RsqlConfigHolder.RsqlToSpecBuilder.G3;
import static org.eclipse.hawkbit.repository.rsql.RsqlConfigHolder.RsqlToSpecBuilder.LEGACY_G1;
import static org.eclipse.hawkbit.repository.rsql.RsqlConfigHolder.RsqlToSpecBuilder.LEGACY_G2;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.StreamSupport;
import jakarta.persistence.EntityManager;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.hawkbit.repository.jpa.rsql.RSQLToSQL;
import org.eclipse.hawkbit.repository.jpa.rsql.RsqlParser;
import org.eclipse.hawkbit.repository.jpa.rsql.SpecificationBuilder;
import org.eclipse.hawkbit.repository.rsql.RsqlConfigHolder;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringBootConfiguration;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.flyway.FlywayAutoConfiguration;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.orm.jpa.vendor.Database;
@SuppressWarnings("java:S5961") // complex check because the matter is very complex
@DataJpaTest(properties = {
"spring.jpa.database=H2",
"logging.level.org.eclipse.hawkbit.repository.jpa.rsql=DEBUG"
}, excludeAutoConfiguration = { FlywayAutoConfiguration.class })
@EnableAutoConfiguration
@Slf4j
class SpecificationBuilderTest {
@Autowired
private SubSubRepository subSubRepository;
@Autowired
private SubRepository subRepository;
@Autowired
private RootRepository rootRepository;
@Autowired
private EntityManager entityManager;
private final SpecificationBuilder<Root> builder = new SpecificationBuilder<>(null, false, Database.H2);
@Test
void singularStringAttribute() {
final Root root1 = rootRepository.save(new Root().setStrValue("rootx"));
final Root root2 = rootRepository.save(new Root().setStrValue("rootx"));
final Root root3 = rootRepository.save(new Root().setStrValue("rooty"));
final Root root4 = rootRepository.save(new Root().setStrValue("rooty"));
final Root root5 = rootRepository.save(new Root()); // null
assertThat(filter("strValue==rootx")).hasSize(2).containsExactlyInAnyOrder(root1, root2);
assertThat(filter("strValue==nostr")).isEmpty();
if (getRsqlToSpecBuilder() != LEGACY_G1) {
assertThat(filter("strValue=is=null")).hasSize(1).containsExactlyInAnyOrder(root5);
}
assertThat(filter("strValue!=rootx")).hasSize(3).containsExactlyInAnyOrder(root3, root4, root5);
assertThat(filter("strValue!=nostr")).hasSize(5);
if (getRsqlToSpecBuilder() != LEGACY_G1) {
assertThat(filter("strValue=not=null")).hasSize(4).containsExactlyInAnyOrder(root1, root2, root3, root4);
}
assertThat(filter("strValue<rooty")).hasSize(2).containsExactlyInAnyOrder(root1, root2);
assertThat(filter("strValue<=rooty")).hasSize(4).containsExactlyInAnyOrder(root1, root2, root3, root4);
assertThat(filter("strValue>rootx")).hasSize(2).containsExactlyInAnyOrder(root3, root4);
assertThat(filter("strValue>=rootx")).hasSize(4).containsExactlyInAnyOrder(root1, root2, root3, root4);
assertThat(filter("strValue=in=rootx")).hasSize(2).containsExactlyInAnyOrder(root1, root2);
assertThat(filter("strValue=in=(rootx, rooty)")).hasSize(4).containsExactlyInAnyOrder(root1, root2, root3, root4);
assertThat(filter("strValue=in=(rootz, roott)")).isEmpty();
assertThat(filter("strValue=out=rootx")).hasSize(3).containsExactlyInAnyOrder(root3, root4, root5);
assertThat(filter("strValue=out=(rootx, rooty)")).hasSize(1).containsExactlyInAnyOrder(root5);
// wildcard, like
assertThat(filter("strValue==root*")).hasSize(4).containsExactlyInAnyOrder(root1, root2, root3, root4);
assertThat(filter("strValue==*tx")).hasSize(2).containsExactlyInAnyOrder(root1, root2);
assertThat(filter("strValue!=root*")).hasSize(1).containsExactlyInAnyOrder(root5);
assertThat(filter("strValue!=*tx")).hasSize(3).containsExactlyInAnyOrder(root3, root4, root5);
assertThat(filter("strValue==*")).hasSize(4).containsExactlyInAnyOrder(root1, root2, root3, root4);
assertThat(filter("strValue!=*")).hasSize(1).containsExactlyInAnyOrder(root5);
if (getRsqlToSpecBuilder() != LEGACY_G1) {
// null checks
assertThat(filter("strValue=is=null")).hasSize(1).containsExactlyInAnyOrder(root5);
assertThat(filter("strValue=not=null")).hasSize(4).containsExactlyInAnyOrder(root1, root2, root3, root4);
}
assertThat(filter("strValue==*tx and strValue==rooty")).isEmpty();
assertThat(filter("strValue==*tx and strValue!=rootx")).isEmpty();
assertThat(filter("strValue==*tx and strValue==rootx")).hasSize(2).containsExactlyInAnyOrder(root1, root2);
assertThat(filter("strValue==*tx or strValue==rooty")).hasSize(4).containsExactlyInAnyOrder(root1, root2, root3, root4);
assertThat(filter("strValue==*tx or strValue!=rootx")).hasSize(5).containsExactlyInAnyOrder(root1, root2, root3, root4, root5);
if (getRsqlToSpecBuilder() != LEGACY_G1) {
assertThat(filter("strValue==*tx or strValue=is=null")).hasSize(3).containsExactlyInAnyOrder(root1, root2, root5);
}
}
@Test
void singularIntAttribute() {
final Root root1 = rootRepository.save(new Root().setIntValue(0));
final Root root2 = rootRepository.save(new Root().setIntValue(0));
final Root root3 = rootRepository.save(new Root().setIntValue(1));
final Root root4 = rootRepository.save(new Root().setIntValue(1));
assertThat(filter("intValue==0")).hasSize(2).containsExactlyInAnyOrder(root1, root2);
assertThat(filter("intValue==2")).isEmpty();
assertThat(filter("intValue!=0")).hasSize(2).containsExactlyInAnyOrder(root3, root4);
assertThat(filter("intValue!=2")).hasSize(4).containsExactlyInAnyOrder(root1, root2, root3, root4);
assertThat(filter("intValue<1")).hasSize(2).containsExactlyInAnyOrder(root1, root2);
assertThat(filter("intValue<=1")).hasSize(4).containsExactlyInAnyOrder(root1, root2, root3, root4);
assertThat(filter("intValue>0")).hasSize(2).containsExactlyInAnyOrder(root3, root4);
assertThat(filter("intValue>=0")).hasSize(4).containsExactlyInAnyOrder(root1, root2, root3, root4);
assertThat(filter("intValue=in=0")).hasSize(2).containsExactlyInAnyOrder(root1, root2);
assertThat(filter("intValue=in=(0, 1)")).hasSize(4).containsExactlyInAnyOrder(root1, root2, root3, root4);
assertThat(filter("intValue=in=(2, 3)")).isEmpty();
assertThat(filter("intValue=out=0")).hasSize(2).containsExactlyInAnyOrder(root3, root4);
assertThat(filter("intValue=out=(0, 1)")).isEmpty();
assertThat(filter("intValue==0 and intValue==1")).isEmpty();
assertThat(filter("intValue==0 and intValue!=1")).hasSize(2).containsExactlyInAnyOrder(root1, root2);
assertThat(filter("intValue==0 and intValue==0")).hasSize(2).containsExactlyInAnyOrder(root1, root2);
assertThat(filter("intValue==0 or intValue==1")).hasSize(4).containsExactlyInAnyOrder(root1, root2, root3, root4);
assertThat(filter("intValue==0 or intValue!=1")).hasSize(2).containsExactlyInAnyOrder(root1, root2);
}
@Test
void singularEntityAttribute() {
final Sub sub1 = subRepository.save(new Sub().setStrValue("subx").setIntValue(0));
final Sub sub2 = subRepository.save(new Sub().setStrValue("suby").setIntValue(1));
final Root root1 = rootRepository.save(new Root().setSubEntity(sub1));
final Root root2 = rootRepository.save(new Root().setSubEntity(sub1));
final Root root3 = rootRepository.save(new Root().setSubEntity(sub2));
final Root root4 = rootRepository.save(new Root().setSubEntity(sub2));
final Root root5 = rootRepository.save(new Root()); // no sub set
// by sub entity string
assertThat(filter("subEntity.strValue==subx")).hasSize(2).containsExactlyInAnyOrder(root1, root2);
assertThat(filter("subEntity.strValue==nostr")).isEmpty();
// TODO / recheck - when missing entity shall it be included or not in != or =out=? - now it is
assertThat(filter("subEntity.strValue!=subx")).hasSize(3).containsExactlyInAnyOrder(root3, root4, root5);
assertThat(filter("subEntity.strValue!=nostr")).hasSize(5);
assertThat(filter("subEntity.strValue<suby")).hasSize(2).containsExactlyInAnyOrder(root1, root2);
assertThat(filter("subEntity.strValue<=suby")).hasSize(4).containsExactlyInAnyOrder(root1, root2, root3, root4);
assertThat(filter("subEntity.strValue>subx")).hasSize(2).containsExactlyInAnyOrder(root3, root4);
assertThat(filter("subEntity.strValue>=subx")).hasSize(4).containsExactlyInAnyOrder(root1, root2, root3, root4);
assertThat(filter("subEntity.strValue=in=subx")).hasSize(2).containsExactlyInAnyOrder(root1, root2);
assertThat(filter("subEntity.strValue=in=(subx, suby)")).hasSize(4).containsExactlyInAnyOrder(root1, root2, root3, root4);
assertThat(filter("subEntity.strValue=in=(subZ, subT)")).isEmpty();
assertThat(filter("subEntity.strValue=out=subx")).hasSize(3).containsExactlyInAnyOrder(root3, root4, root5);
assertThat(filter("subEntity.strValue=out=(subx, suby)")).hasSize(1).containsExactlyInAnyOrder(root5);
// wildcard, like
assertThat(filter("subEntity.strValue==sub*")).hasSize(4).containsExactlyInAnyOrder(root1, root2, root3, root4);
assertThat(filter("subEntity.strValue==*bx")).hasSize(2).containsExactlyInAnyOrder(root1, root2);
assertThat(filter("subEntity.strValue!=sub*")).hasSize(1).containsExactlyInAnyOrder(root5);
assertThat(filter("subEntity.strValue!=*bx")).hasSize(3).containsExactlyInAnyOrder(root3, root4, root5);
assertThat(filter("subEntity.strValue==*")).hasSize(4).containsExactlyInAnyOrder(root1, root2, root3, root4);
assertThat(filter("subEntity.strValue!=*")).hasSize(1).containsExactlyInAnyOrder(root5);
if (getRsqlToSpecBuilder() != LEGACY_G1) {
// null checks
assertThat(filter("subEntity.strValue=is=null")).hasSize(1).containsExactlyInAnyOrder(root5);
assertThat(filter("subEntity.strValue=not=null")).hasSize(4).containsExactlyInAnyOrder(root1, root2, root3, root4);
}
assertThat(filter("subEntity.strValue==*bx and subEntity.strValue==suby")).isEmpty();
assertThat(filter("subEntity.strValue==*bx and subEntity.strValue!=subx")).isEmpty();
assertThat(filter("subEntity.strValue==*bx and subEntity.strValue==subx")).hasSize(2).containsExactlyInAnyOrder(root1, root2);
assertThat(filter("subEntity.strValue==*bx or subEntity.strValue==suby"))
.hasSize(4).containsExactlyInAnyOrder(root1, root2, root3, root4);
if (getRsqlToSpecBuilder() != LEGACY_G1) {
// G1 doesn't get null entity in not, and doesn't support =is= and =not=
assertThat(filter("subEntity.strValue==*bx or subEntity.strValue!=subx"))
.hasSize(5).containsExactlyInAnyOrder(root1, root2, root3, root4, root5);
assertThat(filter("subEntity.strValue==*bx or subEntity.strValue=is=null")).hasSize(3)
.containsExactlyInAnyOrder(root1, root2, root5);
}
// by sub entity int
assertThat(filter("subEntity.intValue==0")).hasSize(2).containsExactlyInAnyOrder(root1, root2);
assertThat(filter("subEntity.intValue==2")).isEmpty();
if (getRsqlToSpecBuilder() != LEGACY_G1) {
// G1 doesn't get null entity in not
assertThat(filter("subEntity.intValue!=0")).hasSize(3).containsExactlyInAnyOrder(root3, root4, root5);
assertThat(filter("subEntity.intValue!=2")).hasSize(5);
}
assertThat(filter("subEntity.intValue<1")).hasSize(2).containsExactlyInAnyOrder(root1, root2);
assertThat(filter("subEntity.intValue<=1")).hasSize(4).containsExactlyInAnyOrder(root1, root2, root3, root4);
assertThat(filter("subEntity.intValue>0")).hasSize(2).containsExactlyInAnyOrder(root3, root4);
assertThat(filter("subEntity.intValue>=0")).hasSize(4);
assertThat(filter("subEntity.intValue=in=0")).hasSize(2).containsExactlyInAnyOrder(root1, root2);
assertThat(filter("subEntity.intValue=in=(0, 1)")).hasSize(4).containsExactlyInAnyOrder(root1, root2, root3, root4);
assertThat(filter("subEntity.intValue=in=(2, 3)")).isEmpty();
assertThat(filter("subEntity.intValue=out=0")).hasSize(3).containsExactlyInAnyOrder(root3, root4, root5);
assertThat(filter("subEntity.intValue=out=(0, 1)")).hasSize(1).containsExactlyInAnyOrder(root5);
assertThat(filter("subEntity.intValue==0 and subEntity.intValue==1")).isEmpty();
assertThat(filter("subEntity.intValue==0 and subEntity.intValue!=1")).hasSize(2).containsExactlyInAnyOrder(root1, root2);
assertThat(filter("subEntity.intValue==0 and subEntity.intValue==0")).hasSize(2).containsExactlyInAnyOrder(root1, root2);
assertThat(filter("subEntity.intValue==0 or subEntity.intValue==1")).hasSize(4).containsExactlyInAnyOrder(root1, root2, root3, root4);
if (getRsqlToSpecBuilder() != LEGACY_G1) {
// G1 doesn't get null entity in not
assertThat(filter("subEntity.intValue==0 or subEntity.intValue!=1")).hasSize(3).containsExactlyInAnyOrder(root1, root2, root5);
}
assertThat(filter("subEntity.strValue==subx and subEntity.intValue==1")).isEmpty();
assertThat(filter("subEntity.strValue==subx and subEntity.intValue!=1")).hasSize(2).containsExactlyInAnyOrder(root1, root2);
assertThat(filter("subEntity.strValue==subx and subEntity.intValue==0")).hasSize(2).containsExactlyInAnyOrder(root1, root2);
assertThat(filter("subEntity.strValue==subx or subEntity.intValue==1")).hasSize(4)
.containsExactlyInAnyOrder(root1, root2, root3, root4);
if (getRsqlToSpecBuilder() != LEGACY_G1) {
// G1 doesn't get null entity in not
assertThat(filter("subEntity.strValue==subx or subEntity.intValue!=1")).hasSize(3).containsExactlyInAnyOrder(root1, root2, root5);
}
}
@Test
void pluralSubSetAttribute() {
final Sub sub1 = subRepository.save(new Sub().setStrValue("subx").setIntValue(0));
final Sub sub2 = subRepository.save(new Sub().setStrValue("suby").setIntValue(1));
final Sub sub3 = subRepository.save(new Sub().setStrValue("suby").setIntValue(0));
final Root root1 = rootRepository.save(new Root().setSubSet(Set.of(sub1)));
final Root root2 = rootRepository.save(new Root().setSubSet(Set.of(sub2)));
final Root root3 = rootRepository.save(new Root().setSubSet(Set.of(sub3)));
final Root root4 = rootRepository.save(new Root().setSubSet(Set.of(sub1, sub2)));
final Root root5 = rootRepository.save(new Root().setSubSet(Set.of(sub1, sub3)));
final Root root6 = rootRepository.save(new Root()); // no sub set
// by sub entity string
assertThat(filter("subSet.strValue==subx")).hasSize(3).containsExactlyInAnyOrder(root1, root4, root5);
assertThat(filter("subSet.strValue==nostr")).isEmpty();
assertThat(filter("subSet.strValue!=subx")).hasSize(3).containsExactlyInAnyOrder(root2, root3, root6);
assertThat(filter("subSet.strValue!=nostr")).hasSize(6);
assertThat(filter("subSet.strValue<suby")).hasSize(3).containsExactlyInAnyOrder(root1, root4, root5);
assertThat(filter("subSet.strValue<=suby")).hasSize(5).containsExactlyInAnyOrder(root1, root2, root3, root4, root5);
assertThat(filter("subSet.strValue>subx")).hasSize(4).containsExactlyInAnyOrder(root2, root3, root4, root5);
assertThat(filter("subSet.strValue>=subx")).hasSize(5).containsExactlyInAnyOrder(root1, root2, root3, root4, root5);
assertThat(filter("subSet.strValue=in=subx")).hasSize(3).containsExactlyInAnyOrder(root1, root4, root5);
assertThat(filter("subSet.strValue=in=(subx, suby)")).hasSize(5).containsExactlyInAnyOrder(root1, root2, root3, root4, root5);
assertThat(filter("subSet.strValue=in=(subZ, subT)")).isEmpty();
assertThat(filter("subSet.strValue=out=subx")).hasSize(3).containsExactlyInAnyOrder(root2, root3, root6);
assertThat(filter("subSet.strValue=out=(subx, suby)")).hasSize(1).containsExactlyInAnyOrder(root6);
// wildcard, like
assertThat(filter("subSet.strValue==sub*")).hasSize(5).containsExactlyInAnyOrder(root1, root2, root3, root4, root5);
assertThat(filter("subSet.strValue==*bx")).hasSize(3).containsExactlyInAnyOrder(root1, root4, root5);
assertThat(filter("subSet.strValue!=sub*")).hasSize(1).containsExactlyInAnyOrder(root6);
assertThat(filter("subSet.strValue!=*bx")).hasSize(3).containsExactlyInAnyOrder(root2, root3, root6);
assertThat(filter("subSet.strValue==*")).hasSize(5).containsExactlyInAnyOrder(root1, root2, root3, root4, root5);
assertThat(filter("subSet.strValue!=*")).hasSize(1).containsExactlyInAnyOrder(root6);
if (getRsqlToSpecBuilder() != LEGACY_G1) {
// G1 treats it like same element shall match - which doesn't match to "has" semantic for sets
assertThat(filter("subSet.strValue==*bx and subSet.strValue==suby")).hasSize(2).containsExactlyInAnyOrder(root4, root5);
}
assertThat(filter("subSet.strValue==*bx and subSet.strValue!=subx")).isEmpty();
assertThat(filter("subSet.strValue==*bx and subSet.strValue!=suby")).hasSize(1).containsExactlyInAnyOrder(root1);
assertThat(filter("subSet.strValue==*bx or subSet.strValue==suby"))
.hasSize(5).containsExactlyInAnyOrder(root1, root2, root3, root4, root5);
if (getRsqlToSpecBuilder() != LEGACY_G1) {
// G1 doesn't get null - has which shall be included as doesn't have
assertThat(filter("subSet.strValue==*bx or subSet.strValue!=subx"))
.hasSize(6).containsExactlyInAnyOrder(root1, root2, root3, root4, root5, root6);
}
// by sub entity int
assertThat(filter("subSet.intValue==0")).hasSize(4).containsExactlyInAnyOrder(root1, root3, root4, root5);
assertThat(filter("subSet.intValue==2")).isEmpty();
if (getRsqlToSpecBuilder() == G3) {
// the legacy builders has different (and wrong semantic)
// G1 - has element != x
// G2 - has element != x or has no elements
// accompanying G3 it is (as it should be) semantic of set != x - has no element with value x (including nhas no elements)
assertThat(filter("subSet.intValue!=0")).hasSize(2).containsExactlyInAnyOrder(root2, root6);
}
if (getRsqlToSpecBuilder() != LEGACY_G1) {
// G1 doesn't get null - has which shall be included as doesn't have
assertThat(filter("subSet.intValue!=2")).hasSize(6);
}
assertThat(filter("subSet.intValue<1")).hasSize(4).containsExactlyInAnyOrder(root1, root3, root4, root5);
assertThat(filter("subSet.intValue<=1")).hasSize(5).containsExactlyInAnyOrder(root1, root2, root3, root4, root5);
assertThat(filter("subSet.intValue>0")).hasSize(2).containsExactlyInAnyOrder(root2, root4);
assertThat(filter("subSet.intValue>=0")).hasSize(5).containsExactlyInAnyOrder(root1, root2, root3, root4, root5);
assertThat(filter("subSet.intValue=in=0")).hasSize(4).containsExactlyInAnyOrder(root1, root3, root4, root5);
assertThat(filter("subSet.intValue=in=(0, 1)")).hasSize(5).containsExactlyInAnyOrder(root1, root2, root3, root4, root5);
assertThat(filter("subSet.intValue=in=(2, 3)")).isEmpty();
assertThat(filter("subSet.intValue=out=0")).hasSize(2).containsExactlyInAnyOrder(root2, root6);
assertThat(filter("subSet.intValue=out=(0, 1)")).hasSize(1).containsExactlyInAnyOrder(root6);
assertThat(filter("subSet.intValue==0 and subSet.intValue==0")).hasSize(4).containsExactlyInAnyOrder(root1, root3, root4, root5);
if (getRsqlToSpecBuilder() == G3) {
assertThat(filter("subSet.intValue==0 and subSet.intValue!=1")).hasSize(3).containsExactlyInAnyOrder(root1, root3, root5);
}
assertThat(filter("subSet.intValue==0 or subSet.intValue==1"))
.hasSize(5).containsExactlyInAnyOrder(root1, root2, root3, root4, root5);
if (getRsqlToSpecBuilder() != LEGACY_G1) {
// G1 doesn't get null - has which shall be included as doesn't have
assertThat(filter("subSet.intValue==0 or subSet.intValue!=1"))
.hasSize(5).containsExactlyInAnyOrder(root1, root3, root4, root5, root6);
}
if (getRsqlToSpecBuilder() != LEGACY_G1) {
// G1 treats it like same element shall match - which doesn't match to "has" semantic for sets
assertThat(filter("subSet.intValue==0 and subSet.strValue==suby")).hasSize(3).containsExactlyInAnyOrder(root3, root4, root5);
}
assertThat(filter("subSet.intValue==0 and subSet.strValue!=subx")).hasSize(1).containsExactlyInAnyOrder(root3);
assertThat(filter("subSet.intValue==0 and subSet.strValue!=suby")).hasSize(1).containsExactlyInAnyOrder(root1);
assertThat(filter("subSet.intValue==0 or subSet.strValue==suby"))
.hasSize(5).containsExactlyInAnyOrder(root1, root2, root3, root4, root5);
if (getRsqlToSpecBuilder() != LEGACY_G1) {
// G1 doesn't get null - has which shall be included as doesn't have
assertThat(filter("subSet.intValue==0 or subSet.strValue!=subx"))
.hasSize(6).containsExactlyInAnyOrder(root1, root2, root3, root4, root5, root6);
}
}
@Test
void pluralSubMapAttribute() {
final Root root1 = rootRepository.save(new Root().setSubMap(Map.of("x", "rootx", "y", "rooty")));
final Root root2 = rootRepository.save(new Root().setSubMap(Map.of("x", "rootx", "y", "rootx")));
final Root root3 = rootRepository.save(new Root().setSubMap(Map.of("x", "rooty", "y", "rooty")));
final Root root4 = rootRepository.save(new Root().setSubMap(Map.of("x", "rooty", "y", "rootx")));
final Root root5 = rootRepository.save(new Root().setSubMap(Map.of("x", "rootx")));
final Root root6 = rootRepository.save(new Root()); // no sub map
assertThat(filter("subMap.x==rootx")).hasSize(3).containsExactlyInAnyOrder(root1, root2, root5);
assertThat(filter("subMap.x==nostr")).isEmpty();
// TODO / recheck - when missing entity shall it be included or not in != or =out=? - now it's not
assertThat(filter("subMap.x!=rootx")).hasSize(2).containsExactlyInAnyOrder(root3, root4);
assertThat(filter("subMap.x!=nostr")).hasSize(5).containsExactlyInAnyOrder(root1, root2, root3, root4, root5);
assertThat(filter("subMap.x<rooty")).hasSize(3).containsExactlyInAnyOrder(root1, root2, root5);
assertThat(filter("subMap.x<=rooty")).hasSize(5).containsExactlyInAnyOrder(root1, root2, root3, root4, root5);
assertThat(filter("subMap.x>rootx")).hasSize(2).containsExactlyInAnyOrder(root3, root4);
assertThat(filter("subMap.x>=rootx")).hasSize(5).containsExactlyInAnyOrder(root1, root2, root3, root4, root5);
assertThat(filter("subMap.x=in=rootx")).hasSize(3).containsExactlyInAnyOrder(root1, root2, root5);
assertThat(filter("subMap.x=in=(rootx, rooty)")).hasSize(5).containsExactlyInAnyOrder(root1, root2, root3, root4, root5);
assertThat(filter("subMap.x=in=(rootz, roott)")).isEmpty();
assertThat(filter("subMap.x=out=rootx")).hasSize(2).containsExactlyInAnyOrder(root3, root4);
assertThat(filter("subMap.x=out=(rootx, rooty)")).isEmpty();
// wildcard, like
assertThat(filter("subMap.x==root*")).hasSize(5).containsExactlyInAnyOrder(root1, root2, root3, root4, root5);
assertThat(filter("subMap.x==*tx")).hasSize(3).containsExactlyInAnyOrder(root1, root2, root5);
assertThat(filter("subMap.x!=root*")).isEmpty();
assertThat(filter("subMap.x!=*tx")).hasSize(2).containsExactlyInAnyOrder(root3, root4);
assertThat(filter("subMap.x==*")).hasSize(5).containsExactlyInAnyOrder(root1, root2, root3, root4, root5);
assertThat(filter("subMap.x!=*")).isEmpty();
if (getRsqlToSpecBuilder() != LEGACY_G1) {
// null checks
assertThat(filter("subMap.x=is=null")).hasSize(1).containsExactlyInAnyOrder(root6);
assertThat(filter("subMap.x=not=null")).hasSize(5).containsExactlyInAnyOrder(root1, root2, root3, root4, root5);
}
assertThat(filter("subMap.x==*tx and subMap.y==rooty")).hasSize(1).containsExactlyInAnyOrder(root1);
assertThat(filter("subMap.x==*tx and subMap.y!=rootx")).hasSize(1).containsExactlyInAnyOrder(root1);
assertThat(filter("subMap.x==*tx or subMap.x==rooty"))
.hasSize(5).containsExactlyInAnyOrder(root1, root2, root3, root4, root5);
assertThat(filter("subMap.x==*tx or subMap.x!=rootx"))
.hasSize(5).containsExactlyInAnyOrder(root1, root2, root3, root4, root5);
}
@Test
void singularEntitySubSubAttribute() {
final SubSub subSub1 = subSubRepository.save(new SubSub().setStrValue("subx").setIntValue(0));
final SubSub subSub2 = subSubRepository.save(new SubSub().setStrValue("suby").setIntValue(1));
final Sub sub1 = subRepository.save(new Sub().setSubSub(subSub1));
final Sub sub2 = subRepository.save(new Sub().setSubSub(subSub2));
final Root root1 = rootRepository.save(new Root().setSubEntity(sub1));
final Root root2 = rootRepository.save(new Root().setSubEntity(sub1));
final Root root3 = rootRepository.save(new Root().setSubEntity(sub2));
final Root root4 = rootRepository.save(new Root().setSubEntity(sub2));
final Root root5 = rootRepository.save(new Root()); // no sub set
// by sub entity string
assertThat(filter("subEntity.subSub.strValue==subx")).hasSize(2).containsExactlyInAnyOrder(root1, root2);
assertThat(filter("subEntity.subSub.strValue==nostr")).isEmpty();
// TODO / recheck - when missing entity shall it be included or not in != or =out=? - now it is
assertThat(filter("subEntity.subSub.strValue!=subx")).hasSize(3).containsExactlyInAnyOrder(root3, root4, root5);
assertThat(filter("subEntity.subSub.strValue!=nostr")).hasSize(5);
assertThat(filter("subEntity.subSub.strValue<suby")).hasSize(2).containsExactlyInAnyOrder(root1, root2);
assertThat(filter("subEntity.subSub.strValue<=suby")).hasSize(4).containsExactlyInAnyOrder(root1, root2, root3, root4);
assertThat(filter("subEntity.subSub.strValue>subx")).hasSize(2).containsExactlyInAnyOrder(root3, root4);
assertThat(filter("subEntity.subSub.strValue>=subx")).hasSize(4).containsExactlyInAnyOrder(root1, root2, root3, root4);
assertThat(filter("subEntity.subSub.strValue=in=subx")).hasSize(2).containsExactlyInAnyOrder(root1, root2);
assertThat(filter("subEntity.subSub.strValue=in=(subx, suby)")).hasSize(4).containsExactlyInAnyOrder(root1, root2, root3, root4);
assertThat(filter("subEntity.subSub.strValue=in=(subZ, subT)")).isEmpty();
assertThat(filter("subEntity.subSub.strValue=out=subx")).hasSize(3).containsExactlyInAnyOrder(root3, root4, root5);
assertThat(filter("subEntity.subSub.strValue=out=(subx, suby)")).hasSize(1).containsExactlyInAnyOrder(root5);
// wildcard, like
assertThat(filter("subEntity.subSub.strValue==sub*")).hasSize(4).containsExactlyInAnyOrder(root1, root2, root3, root4);
assertThat(filter("subEntity.subSub.strValue==*bx")).hasSize(2).containsExactlyInAnyOrder(root1, root2);
assertThat(filter("subEntity.subSub.strValue!=sub*")).hasSize(1).containsExactlyInAnyOrder(root5);
assertThat(filter("subEntity.subSub.strValue!=*bx")).hasSize(3).containsExactlyInAnyOrder(root3, root4, root5);
assertThat(filter("subEntity.subSub.strValue==*")).hasSize(4).containsExactlyInAnyOrder(root1, root2, root3, root4);
assertThat(filter("subEntity.subSub.strValue!=*")).hasSize(1).containsExactlyInAnyOrder(root5);
if (getRsqlToSpecBuilder() != LEGACY_G1) {
// null checks
if (getRsqlToSpecBuilder() != LEGACY_G2) {
assertThat(filter("subEntity.subSub.strValue=is=null")).hasSize(1).containsExactlyInAnyOrder(root5);
}
assertThat(filter("subEntity.subSub.strValue=not=null")).hasSize(4).containsExactlyInAnyOrder(root1, root2, root3, root4);
}
assertThat(filter("subEntity.subSub.strValue==*bx and subEntity.subSub.strValue==suby")).isEmpty();
assertThat(filter("subEntity.subSub.strValue==*bx and subEntity.subSub.strValue!=subx")).isEmpty();
assertThat(filter("subEntity.subSub.strValue==*bx and subEntity.subSub.strValue==subx"))
.hasSize(2).containsExactlyInAnyOrder(root1, root2);
assertThat(filter("subEntity.subSub.strValue==*bx or subEntity.subSub.strValue==suby"))
.hasSize(4).containsExactlyInAnyOrder(root1, root2, root3, root4);
if (getRsqlToSpecBuilder() != LEGACY_G1 && getRsqlToSpecBuilder() != LEGACY_G2) {
// G1 doesn't get null entity in not, and doesn't support =is= and =not=
assertThat(filter("subEntity.subSub.strValue==*bx or subEntity.subSub.strValue!=subx"))
.hasSize(5).containsExactlyInAnyOrder(root1, root2, root3, root4, root5);
assertThat(filter("subEntity.subSub.strValue==*bx or subEntity.subSub.strValue=is=null"))
.hasSize(3).containsExactlyInAnyOrder(root1, root2, root5);
}
// by sub entity int
assertThat(filter("subEntity.subSub.intValue==0")).hasSize(2).containsExactlyInAnyOrder(root1, root2);
assertThat(filter("subEntity.subSub.intValue==2")).isEmpty();
if (getRsqlToSpecBuilder() != LEGACY_G1 && getRsqlToSpecBuilder() != LEGACY_G2) {
// G1 doesn't get null entity in not
assertThat(filter("subEntity.subSub.intValue!=0")).hasSize(3).containsExactlyInAnyOrder(root3, root4, root5);
assertThat(filter("subEntity.subSub.intValue!=2")).hasSize(5);
}
assertThat(filter("subEntity.subSub.intValue<1")).hasSize(2).containsExactlyInAnyOrder(root1, root2);
assertThat(filter("subEntity.subSub.intValue<=1")).hasSize(4).containsExactlyInAnyOrder(root1, root2, root3, root4);
assertThat(filter("subEntity.subSub.intValue>0")).hasSize(2).containsExactlyInAnyOrder(root3, root4);
assertThat(filter("subEntity.subSub.intValue>=0")).hasSize(4);
assertThat(filter("subEntity.subSub.intValue=in=0")).hasSize(2).containsExactlyInAnyOrder(root1, root2);
assertThat(filter("subEntity.subSub.intValue=in=(0, 1)")).hasSize(4).containsExactlyInAnyOrder(root1, root2, root3, root4);
assertThat(filter("subEntity.subSub.intValue=in=(2, 3)")).isEmpty();
assertThat(filter("subEntity.subSub.intValue=out=0")).hasSize(3).containsExactlyInAnyOrder(root3, root4, root5);
assertThat(filter("subEntity.subSub.intValue=out=(0, 1)")).hasSize(1).containsExactlyInAnyOrder(root5);
assertThat(filter("subEntity.subSub.intValue==0 and subEntity.subSub.intValue==1")).isEmpty();
assertThat(filter("subEntity.subSub.intValue==0 and subEntity.subSub.intValue!=1")).hasSize(2).containsExactlyInAnyOrder(root1, root2);
assertThat(filter("subEntity.subSub.intValue==0 and subEntity.subSub.intValue==0")).hasSize(2).containsExactlyInAnyOrder(root1, root2);
assertThat(filter("subEntity.subSub.intValue==0 or subEntity.subSub.intValue==1")).hasSize(4)
.containsExactlyInAnyOrder(root1, root2, root3, root4);
if (getRsqlToSpecBuilder() != LEGACY_G1 && getRsqlToSpecBuilder() != LEGACY_G2) {
// G1 doesn't get null entity in not
assertThat(filter("subEntity.subSub.intValue==0 or subEntity.subSub.intValue!=1"))
.hasSize(3).containsExactlyInAnyOrder(root1, root2, root5);
}
assertThat(filter("subEntity.subSub.strValue==subx and subEntity.subSub.intValue==1")).isEmpty();
assertThat(filter("subEntity.subSub.strValue==subx and subEntity.subSub.intValue!=1")).hasSize(2)
.containsExactlyInAnyOrder(root1, root2);
assertThat(filter("subEntity.subSub.strValue==subx and subEntity.subSub.intValue==0"))
.hasSize(2).containsExactlyInAnyOrder(root1, root2);
assertThat(filter("subEntity.subSub.strValue==subx or subEntity.subSub.intValue==1"))
.hasSize(4).containsExactlyInAnyOrder(root1, root2, root3, root4);
if (getRsqlToSpecBuilder() != LEGACY_G1 && getRsqlToSpecBuilder() != LEGACY_G2) {
// G1 doesn't get null entity in not
assertThat(filter("subEntity.subSub.strValue==subx or subEntity.subSub.intValue!=1"))
.hasSize(3).containsExactlyInAnyOrder(root1, root2, root5);
}
}
private List<Root> filter(final String rsql) {
// reference / auto filter (using elements and reflection)
final ReferenceMatcher matcher = ReferenceMatcher.ofRsql(rsql);
final List<Root> refResult = StreamSupport.stream(rootRepository.findAll().spliterator(), false).filter(matcher::match).toList();
final List<Root> result = rootRepository.findAll(getSpecification(rsql));
// auto check with reference result
try {
assertThat(result).containsExactlyInAnyOrder(refResult.toArray(Root[]::new));
} catch (final AssertionError e) {
log.error(
"Fail to get expected result for RSQL: {} with SQL query: {}",
rsql, new RSQLToSQL(entityManager).toSQL(Root.class, null, rsql, G3),
e);
throw e;
}
return result;
}
protected Specification<Root> getSpecification(final String rsql) {
return builder.specification(RsqlParser.parse(rsql));
}
private static RsqlConfigHolder.RsqlToSpecBuilder getRsqlToSpecBuilder() {
return RsqlConfigHolder.getInstance().getRsqlToSpecBuilder();
}
@SpringBootConfiguration
static class Config {}
}

View File

@@ -1,40 +0,0 @@
/**
* Copyright (c) 2025 Contributors to the Eclipse Foundation
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.eclipse.hawkbit.repository.jpa.rsql.sa;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.OneToMany;
import jakarta.persistence.OneToOne;
import lombok.Data;
import lombok.experimental.Accessors;
@Entity
@Data
@Accessors(chain = true)
class Sub {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
// singular attributes
// basic
private String strValue;
private int intValue;
// entity
@ManyToOne
private SubSub subSub;
}

View File

@@ -1,17 +0,0 @@
/**
* Copyright (c) 2025 Contributors to the Eclipse Foundation
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.eclipse.hawkbit.repository.jpa.rsql.sa;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
import org.springframework.data.repository.CrudRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface SubRepository extends CrudRepository<Sub, Long>, JpaSpecificationExecutor<Sub> {}

View File

@@ -1,33 +0,0 @@
/**
* Copyright (c) 2025 Contributors to the Eclipse Foundation
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.eclipse.hawkbit.repository.jpa.rsql.sa;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import lombok.Data;
import lombok.experimental.Accessors;
@Entity
@Data
@Accessors(chain = true)
class SubSub {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
// singular attributes
// basic
private String strValue;
private int intValue;
}

View File

@@ -1,17 +0,0 @@
/**
* Copyright (c) 2025 Contributors to the Eclipse Foundation
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.eclipse.hawkbit.repository.jpa.rsql.sa;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
import org.springframework.data.repository.CrudRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface SubSubRepository extends CrudRepository<SubSub, Long>, JpaSpecificationExecutor<SubSub> {}