From d80e1e004d00d4a1b1f0f9642b1d7d8240f2a47c Mon Sep 17 00:00:00 2001 From: Avgustin Marinov Date: Tue, 17 Jun 2025 15:44:55 +0300 Subject: [PATCH] Fix G3 - deep attributes (#2462) --- .../repository/jpa/rsql/RsqlParser.java | 11 +- .../jpa/rsql/SpecificationBuilder.java | 25 ++++- .../SpecificationBuilderLegacy.java | 28 ++--- .../jpa/rsql/RSQLRolloutFieldTest.java | 66 +++++++++++ .../jpa/rsql/sa/ReferenceMatcher.java | 19 +++- .../jpa/rsql/sa/RootRepository.java | 3 +- .../sa/SpecificationBuilderLegacyTest.java | 12 +- .../jpa/rsql/sa/SpecificationBuilderTest.java | 106 +++++++++++++++++- .../hawkbit/repository/jpa/rsql/sa/Sub.java | 7 ++ .../repository/jpa/rsql/sa/SubRepository.java | 3 +- .../repository/jpa/rsql/sa/SubSub.java | 33 ++++++ .../jpa/rsql/sa/SubSubRepository.java | 17 +++ 12 files changed, 296 insertions(+), 34 deletions(-) create mode 100644 hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/rsql/RSQLRolloutFieldTest.java create mode 100644 hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/rsql/sa/SubSub.java create mode 100644 hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/rsql/sa/SubSubRepository.java diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/rsql/RsqlParser.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/rsql/RsqlParser.java index 25f6bbbb2..89854a05a 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/rsql/RsqlParser.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/rsql/RsqlParser.java @@ -10,6 +10,7 @@ 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; @@ -94,6 +95,7 @@ public class RsqlParser { } } + @SuppressWarnings("java:S3776") // java:S3776 - group in single method for easier read of whole logic private static & RsqlQueryField> Key resolveKey(final String key, final Class rsqlQueryFieldType) { final int firstSeparatorIndex = key.indexOf(SUB_ATTRIBUTE_SEPARATOR); final String enumName = (firstSeparatorIndex == -1 ? key : key.substring(0, firstSeparatorIndex)).toUpperCase(); @@ -126,18 +128,18 @@ public class RsqlParser { } } } else { // field name with sub-attribute - final String subAttribute = key.substring(firstSeparatorIndex + 1); - if (enumValue.isMap()) { // map, the part after the enum name is the key of the map - attribute = enumValue.getJpaEntityFieldName() + SUB_ATTRIBUTE_SEPARATOR + subAttribute; + 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)) // case normalized + .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", @@ -174,6 +176,7 @@ public class RsqlParser { .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(); diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/rsql/SpecificationBuilder.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/rsql/SpecificationBuilder.java index a0c9e4ad0..5e8d2604c 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/rsql/SpecificationBuilder.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/rsql/SpecificationBuilder.java @@ -31,6 +31,7 @@ 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; @@ -170,13 +171,13 @@ public class SpecificationBuilder { } 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(pathResolver.getPath(attribute).get(split[1]))); + return cb.and(cb.isNotNull(deepGetPath(pathResolver.getPath(attribute), split[1]))); } - return compare(comparison, pathResolver.getPath(attribute).get(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 ? attributePath.get(split[1]) : attributePath); + return compare(comparison, split.length > 1 ? deepGetPath(attributePath, split[1]) : attributePath); } } @@ -212,7 +213,7 @@ public class SpecificationBuilder { final Root subqueryRoot = subquery.from(javaType); final Path joinPath = subAttributeName == null // if null it is a map ? ((MapJoin) subqueryRoot.join(pluralAttribute.getName(), JoinType.LEFT)).value() - : subqueryRoot.join(pluralAttribute.getName(), JoinType.LEFT).get(subAttributeName); + : deepGetPath(subqueryRoot.join(pluralAttribute.getName(), JoinType.LEFT), subAttributeName); final Path fieldPath = joinPath instanceof MapJoin mapJoin ? (Path) mapJoin.value() : stringPath(joinPath); @@ -320,6 +321,22 @@ public class SpecificationBuilder { } } + 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 stringPath(final Path path) { return (Path) path; diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/rsqllegacy/SpecificationBuilderLegacy.java b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/rsqllegacy/SpecificationBuilderLegacy.java index 6cc10c741..47a57f6eb 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/rsqllegacy/SpecificationBuilderLegacy.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/main/java/org/eclipse/hawkbit/repository/jpa/rsqllegacy/SpecificationBuilderLegacy.java @@ -9,6 +9,7 @@ */ 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; @@ -44,20 +45,16 @@ public class SpecificationBuilderLegacy & RsqlQueryField, T> { public Specification specification(final String rsql) { return (root, query, cb) -> { - final Node rootNode = parseRsql(rsql); + final RsqlConfigHolder rsqlConfigHolder = RsqlConfigHolder.getInstance(); + + final Node rootNode = parseRsql(rsql, rsqlConfigHolder); query.distinct(true); + final boolean ensureIgnoreCase = !rsqlConfigHolder.isCaseInsensitiveDB() && rsqlConfigHolder.isIgnoreCase(); final RSQLVisitor, String> jpqQueryRSQLVisitor = - RsqlConfigHolder.getInstance().getRsqlToSpecBuilder() == LEGACY_G1 - ? new JpaQueryRsqlVisitor<>( - root, cb, rsqlQueryFieldType, - virtualPropertyReplacer, database, query, - !RsqlConfigHolder.getInstance().isCaseInsensitiveDB() && RsqlConfigHolder.getInstance().isIgnoreCase()) - : new JpaQueryRsqlVisitorG2<>( - rsqlQueryFieldType, root, query, cb, - database, virtualPropertyReplacer, - !RsqlConfigHolder.getInstance().isCaseInsensitiveDB() && RsqlConfigHolder.getInstance() - .isIgnoreCase()); + rsqlConfigHolder.getRsqlToSpecBuilder() == LEGACY_G1 + ? new JpaQueryRsqlVisitor<>(root, cb, rsqlQueryFieldType, virtualPropertyReplacer, database, query, ensureIgnoreCase) + : new JpaQueryRsqlVisitorG2<>(rsqlQueryFieldType, root, query, cb, database, virtualPropertyReplacer, ensureIgnoreCase); final List accept = rootNode.accept(jpqQueryRSQLVisitor); if (CollectionUtils.isEmpty(accept)) { @@ -68,17 +65,14 @@ public class SpecificationBuilderLegacy & RsqlQueryField, T> { }; } - private static Node parseRsql(final String rsql) { + private static Node parseRsql(final String rsql, final RsqlConfigHolder rsqlConfigHolder) { log.debug("Parsing rsql string {}", rsql); try { - return new RSQLParser(AbstractRSQLVisitor.OPERATORS).parse( - RsqlConfigHolder.getInstance().isCaseInsensitiveDB() || RsqlConfigHolder.getInstance().isIgnoreCase() - ? rsql.toLowerCase() - : rsql); + 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); } } -} +} \ No newline at end of file diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/rsql/RSQLRolloutFieldTest.java b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/rsql/RSQLRolloutFieldTest.java new file mode 100644 index 000000000..79b64e8f9 --- /dev/null +++ b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/rsql/RSQLRolloutFieldTest.java @@ -0,0 +1,66 @@ +/** + * 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.assertj.core.api.Assertions.assertThat; + +import io.qameta.allure.Description; +import io.qameta.allure.Feature; +import io.qameta.allure.Story; +import org.eclipse.hawkbit.repository.RolloutFields; +import org.eclipse.hawkbit.repository.jpa.AbstractJpaIntegrationTest; +import org.eclipse.hawkbit.repository.model.DistributionSet; +import org.eclipse.hawkbit.repository.model.Rollout; +import org.eclipse.hawkbit.repository.model.RolloutGroup.RolloutGroupSuccessCondition; +import org.eclipse.hawkbit.repository.model.RolloutGroupConditionBuilder; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; + +@Feature("Component Tests - Repository") +@Story("RSQL filter rollout group") +class RSQLRolloutFieldTest extends AbstractJpaIntegrationTest { + + private Rollout rollout; + + @BeforeEach + void setupBeforeTest() { + testdataFactory.createTargets(20, "rollout", "rollout"); + final DistributionSet dsA = testdataFactory.createDistributionSet(""); + rollout = createRollout("rollout1", 4, dsA.getId(), "controllerId==rollout*"); + rollout = rolloutManagement.get(rollout.getId()).get(); + } + + @Test + @Description("Test filter rollout by distrbution set type id") + void testFilterByDsType() { + assertRSQLQuery(RolloutFields.DISTRIBUTIONSET.name() + ".type.id" + "==" + rollout.getDistributionSet().getType().getId() + 1, 0); + assertRSQLQuery(RolloutFields.DISTRIBUTIONSET.name() + ".type.id" + "!=" + rollout.getDistributionSet().getType().getId() + 1, 1); + assertRSQLQuery(RolloutFields.DISTRIBUTIONSET.name() + ".type.id" + "==" + rollout.getDistributionSet().getType().getId(), 1); + assertRSQLQuery(RolloutFields.DISTRIBUTIONSET.name() + ".type.id" + "!=" + rollout.getDistributionSet().getType().getId(), 0); + } + + private void assertRSQLQuery(final String rsql, final long expectedTargets) { + final Page findTargetPage = rolloutManagement.findByRsql(rsql, false, PageRequest.of(0, 100)); + final long countTargetsAll = findTargetPage.getTotalElements(); + assertThat(findTargetPage).isNotNull(); + assertThat(countTargetsAll).isEqualTo(expectedTargets); + } + + private Rollout createRollout(final String name, final int amountGroups, final long distributionSetId, final String targetFilterQuery) { + return rolloutManagement.create( + entityFactory.rollout().create().distributionSetId( + distributionSetManagement.get(distributionSetId).get()).name(name).targetFilterQuery(targetFilterQuery), + amountGroups, + false, + new RolloutGroupConditionBuilder().withDefaults().successCondition(RolloutGroupSuccessCondition.THRESHOLD, "100").build()); + } +} \ No newline at end of file diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/rsql/sa/ReferenceMatcher.java b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/rsql/sa/ReferenceMatcher.java index 033336864..bc4c412b3 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/rsql/sa/ReferenceMatcher.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/rsql/sa/ReferenceMatcher.java @@ -107,9 +107,22 @@ class ReferenceMatcher { if (split.length == 1) { return compare(fieldValue, op, map(comparison.getValue(), fieldGetter.getReturnType())); } else { - final Method valueGetter = getGetter(fieldGetter.getReturnType(), split[1]); - return compare(fieldValue == null ? null : valueGetter.invoke(fieldValue), op, - map(comparison.getValue(), valueGetter.getReturnType())); + 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) { diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/rsql/sa/RootRepository.java b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/rsql/sa/RootRepository.java index 0763b1d6b..c6f08e474 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/rsql/sa/RootRepository.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/rsql/sa/RootRepository.java @@ -14,5 +14,4 @@ import org.springframework.data.repository.CrudRepository; import org.springframework.stereotype.Repository; @Repository -public interface RootRepository - extends CrudRepository, JpaSpecificationExecutor {} +public interface RootRepository extends CrudRepository, JpaSpecificationExecutor {} diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/rsql/sa/SpecificationBuilderLegacyTest.java b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/rsql/sa/SpecificationBuilderLegacyTest.java index e8019810c..c746bc18d 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/rsql/sa/SpecificationBuilderLegacyTest.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/rsql/sa/SpecificationBuilderLegacyTest.java @@ -88,6 +88,16 @@ class SpecificationBuilderLegacyTest extends SpecificationBuilderTest { 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 getSpecification(final String rsql) { return builder.specification(rsql); @@ -98,7 +108,7 @@ class SpecificationBuilderLegacyTest extends SpecificationBuilderTest { INTVALUE("intValue"), STRVALUE("strValue"), - SUBENTITY("subEntity", "strValue", "intValue"), + SUBENTITY("subEntity", "strValue", "intValue", "subSub"), SUBSET("subSet", "strValue", "intValue"), SUBMAP("subMap"); diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/rsql/sa/SpecificationBuilderTest.java b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/rsql/sa/SpecificationBuilderTest.java index ad689cbf4..61c00af60 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/rsql/sa/SpecificationBuilderTest.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/rsql/sa/SpecificationBuilderTest.java @@ -12,6 +12,7 @@ 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; @@ -43,6 +44,8 @@ import org.springframework.orm.jpa.vendor.Database; @Slf4j class SpecificationBuilderTest { + @Autowired + private SubSubRepository subSubRepository; @Autowired private SubRepository subRepository; @Autowired @@ -214,7 +217,8 @@ class SpecificationBuilderTest { 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); + 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); @@ -365,6 +369,106 @@ class SpecificationBuilderTest { .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.strValuesubx")).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 filter(final String rsql) { // reference / auto filter (using elements and reflection) final ReferenceMatcher matcher = ReferenceMatcher.ofRsql(rsql); diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/rsql/sa/Sub.java b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/rsql/sa/Sub.java index 9a9f2598e..6f4b8ceeb 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/rsql/sa/Sub.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/rsql/sa/Sub.java @@ -10,9 +10,13 @@ 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; @@ -30,4 +34,7 @@ class Sub { // basic private String strValue; private int intValue; + // entity + @ManyToOne + private SubSub subSub; } diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/rsql/sa/SubRepository.java b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/rsql/sa/SubRepository.java index 6c72a3531..bdf56eacc 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/rsql/sa/SubRepository.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/rsql/sa/SubRepository.java @@ -14,5 +14,4 @@ import org.springframework.data.repository.CrudRepository; import org.springframework.stereotype.Repository; @Repository -public interface SubRepository - extends CrudRepository, JpaSpecificationExecutor {} +public interface SubRepository extends CrudRepository, JpaSpecificationExecutor {} diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/rsql/sa/SubSub.java b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/rsql/sa/SubSub.java new file mode 100644 index 000000000..8dfd88b51 --- /dev/null +++ b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/rsql/sa/SubSub.java @@ -0,0 +1,33 @@ +/** + * 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; +} diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/rsql/sa/SubSubRepository.java b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/rsql/sa/SubSubRepository.java new file mode 100644 index 000000000..8bede04b2 --- /dev/null +++ b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/rsql/sa/SubSubRepository.java @@ -0,0 +1,17 @@ +/** + * 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, JpaSpecificationExecutor {}