Fix G3 - deep attributes (#2462)
This commit is contained in:
@@ -10,6 +10,7 @@
|
|||||||
package org.eclipse.hawkbit.repository.jpa.rsql;
|
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_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.EQ;
|
||||||
import static org.eclipse.hawkbit.repository.jpa.rsql.Node.Comparison.Operator.GT;
|
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.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 <T extends Enum<T> & RsqlQueryField> Key resolveKey(final String key, final Class<T> rsqlQueryFieldType) {
|
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 int firstSeparatorIndex = key.indexOf(SUB_ATTRIBUTE_SEPARATOR);
|
||||||
final String enumName = (firstSeparatorIndex == -1 ? key : key.substring(0, firstSeparatorIndex)).toUpperCase();
|
final String enumName = (firstSeparatorIndex == -1 ? key : key.substring(0, firstSeparatorIndex)).toUpperCase();
|
||||||
@@ -126,18 +128,18 @@ public class RsqlParser {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else { // field name with sub-attribute
|
} else { // field name with sub-attribute
|
||||||
final String subAttribute = key.substring(firstSeparatorIndex + 1);
|
|
||||||
|
|
||||||
if (enumValue.isMap()) {
|
if (enumValue.isMap()) {
|
||||||
// map, the part after the enum name is the key of the map
|
// 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()) {
|
} else if (enumValue.getSubEntityAttributes().isEmpty()) {
|
||||||
// simple type without sub-attributes, so the sub-attribute is not allowed
|
// simple type without sub-attributes, so the sub-attribute is not allowed
|
||||||
throw new RSQLParameterUnsupportedFieldException("Sub-attributes not supported for simple field " + enumValue);
|
throw new RSQLParameterUnsupportedFieldException("Sub-attributes not supported for simple field " + enumValue);
|
||||||
} else {
|
} else {
|
||||||
|
final String[] subAttribute = key.substring(firstSeparatorIndex + 1).split(SUB_ATTRIBUTE_SPLIT_REGEX, 2);
|
||||||
attribute = enumValue.getJpaEntityFieldName() + SUB_ATTRIBUTE_SEPARATOR + enumValue.getSubEntityAttributes().stream()
|
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()
|
.findFirst()
|
||||||
|
.map(attr -> subAttribute.length == 1 ? attr : attr + key.substring(firstSeparatorIndex + 1 + attr.length()))
|
||||||
.orElseThrow(() -> new RSQLParameterUnsupportedFieldException(
|
.orElseThrow(() -> new RSQLParameterUnsupportedFieldException(
|
||||||
String.format(
|
String.format(
|
||||||
"The given search field {%s} has unsupported sub-attributes. Supported sub-attributes are %s",
|
"The given search field {%s} has unsupported sub-attributes. Supported sub-attributes are %s",
|
||||||
@@ -174,6 +176,7 @@ public class RsqlParser {
|
|||||||
.orElseThrow();
|
.orElseThrow();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("java:S3776") // java:S3776 - group in single method for easier read of whole logic
|
||||||
@Override
|
@Override
|
||||||
public Node visit(final ComparisonNode node, final String param) {
|
public Node visit(final ComparisonNode node, final String param) {
|
||||||
final String nodeSelector = node.getSelector();
|
final String nodeSelector = node.getSelector();
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ import jakarta.annotation.Nonnull;
|
|||||||
import jakarta.persistence.criteria.CriteriaBuilder;
|
import jakarta.persistence.criteria.CriteriaBuilder;
|
||||||
import jakarta.persistence.criteria.CriteriaQuery;
|
import jakarta.persistence.criteria.CriteriaQuery;
|
||||||
import jakarta.persistence.criteria.Expression;
|
import jakarta.persistence.criteria.Expression;
|
||||||
|
import jakarta.persistence.criteria.Join;
|
||||||
import jakarta.persistence.criteria.JoinType;
|
import jakarta.persistence.criteria.JoinType;
|
||||||
import jakarta.persistence.criteria.MapJoin;
|
import jakarta.persistence.criteria.MapJoin;
|
||||||
import jakarta.persistence.criteria.Path;
|
import jakarta.persistence.criteria.Path;
|
||||||
@@ -170,13 +171,13 @@ public class SpecificationBuilder<T> {
|
|||||||
} else {
|
} else {
|
||||||
if (op == LIKE && LIKE_WILDCARD_STR.equals(comparison.getValue())) {
|
if (op == LIKE && LIKE_WILDCARD_STR.equals(comparison.getValue())) {
|
||||||
// optimized (?) has non-null/empty attribute - do or with on instead of subquery
|
// 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)
|
} else { // singular attribute (BASIC and EMBEDDABLE) or plural (ListAttribute of entities)
|
||||||
final Path<?> attributePath = pathResolver.getPath(attribute);
|
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<T> {
|
|||||||
final Root<? extends T> subqueryRoot = subquery.from(javaType);
|
final Root<? extends T> subqueryRoot = subquery.from(javaType);
|
||||||
final Path<?> joinPath = subAttributeName == null // if null it is a map
|
final Path<?> joinPath = subAttributeName == null // if null it is a map
|
||||||
? ((MapJoin<?, ?, ?>) subqueryRoot.join(pluralAttribute.getName(), JoinType.LEFT)).value()
|
? ((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<String> fieldPath = joinPath instanceof MapJoin<?, ?, ?> mapJoin
|
final Path<String> fieldPath = joinPath instanceof MapJoin<?, ?, ?> mapJoin
|
||||||
? (Path<String>) mapJoin.value()
|
? (Path<String>) mapJoin.value()
|
||||||
: stringPath(joinPath);
|
: stringPath(joinPath);
|
||||||
@@ -320,6 +321,22 @@ public class SpecificationBuilder<T> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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")
|
@SuppressWarnings("unchecked")
|
||||||
private static Path<String> stringPath(final Path<?> path) {
|
private static Path<String> stringPath(final Path<?> path) {
|
||||||
return (Path<String>) path;
|
return (Path<String>) path;
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
*/
|
*/
|
||||||
package org.eclipse.hawkbit.repository.jpa.rsqllegacy;
|
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 static org.eclipse.hawkbit.repository.rsql.RsqlConfigHolder.RsqlToSpecBuilder.LEGACY_G1;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@@ -44,20 +45,16 @@ public class SpecificationBuilderLegacy<A extends Enum<A> & RsqlQueryField, T> {
|
|||||||
|
|
||||||
public Specification<T> specification(final String rsql) {
|
public Specification<T> specification(final String rsql) {
|
||||||
return (root, query, cb) -> {
|
return (root, query, cb) -> {
|
||||||
final Node rootNode = parseRsql(rsql);
|
final RsqlConfigHolder rsqlConfigHolder = RsqlConfigHolder.getInstance();
|
||||||
|
|
||||||
|
final Node rootNode = parseRsql(rsql, rsqlConfigHolder);
|
||||||
query.distinct(true);
|
query.distinct(true);
|
||||||
|
|
||||||
|
final boolean ensureIgnoreCase = !rsqlConfigHolder.isCaseInsensitiveDB() && rsqlConfigHolder.isIgnoreCase();
|
||||||
final RSQLVisitor<List<Predicate>, String> jpqQueryRSQLVisitor =
|
final RSQLVisitor<List<Predicate>, String> jpqQueryRSQLVisitor =
|
||||||
RsqlConfigHolder.getInstance().getRsqlToSpecBuilder() == LEGACY_G1
|
rsqlConfigHolder.getRsqlToSpecBuilder() == LEGACY_G1
|
||||||
? new JpaQueryRsqlVisitor<>(
|
? new JpaQueryRsqlVisitor<>(root, cb, rsqlQueryFieldType, virtualPropertyReplacer, database, query, ensureIgnoreCase)
|
||||||
root, cb, rsqlQueryFieldType,
|
: new JpaQueryRsqlVisitorG2<>(rsqlQueryFieldType, root, query, cb, database, virtualPropertyReplacer, ensureIgnoreCase);
|
||||||
virtualPropertyReplacer, database, query,
|
|
||||||
!RsqlConfigHolder.getInstance().isCaseInsensitiveDB() && RsqlConfigHolder.getInstance().isIgnoreCase())
|
|
||||||
: new JpaQueryRsqlVisitorG2<>(
|
|
||||||
rsqlQueryFieldType, root, query, cb,
|
|
||||||
database, virtualPropertyReplacer,
|
|
||||||
!RsqlConfigHolder.getInstance().isCaseInsensitiveDB() && RsqlConfigHolder.getInstance()
|
|
||||||
.isIgnoreCase());
|
|
||||||
final List<Predicate> accept = rootNode.accept(jpqQueryRSQLVisitor);
|
final List<Predicate> accept = rootNode.accept(jpqQueryRSQLVisitor);
|
||||||
|
|
||||||
if (CollectionUtils.isEmpty(accept)) {
|
if (CollectionUtils.isEmpty(accept)) {
|
||||||
@@ -68,13 +65,10 @@ public class SpecificationBuilderLegacy<A extends Enum<A> & 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);
|
log.debug("Parsing rsql string {}", rsql);
|
||||||
try {
|
try {
|
||||||
return new RSQLParser(AbstractRSQLVisitor.OPERATORS).parse(
|
return new RSQLParser(OPERATORS).parse(rsqlConfigHolder.isCaseInsensitiveDB() || rsqlConfigHolder.isIgnoreCase() ? rsql.toLowerCase() : rsql);
|
||||||
RsqlConfigHolder.getInstance().isCaseInsensitiveDB() || RsqlConfigHolder.getInstance().isIgnoreCase()
|
|
||||||
? rsql.toLowerCase()
|
|
||||||
: rsql);
|
|
||||||
} catch (final IllegalArgumentException e) {
|
} catch (final IllegalArgumentException e) {
|
||||||
throw new RSQLParameterSyntaxException("RSQL filter must not be null", e);
|
throw new RSQLParameterSyntaxException("RSQL filter must not be null", e);
|
||||||
} catch (final RSQLParserException e) {
|
} catch (final RSQLParserException e) {
|
||||||
|
|||||||
@@ -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<Rollout> 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());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -107,9 +107,22 @@ class ReferenceMatcher {
|
|||||||
if (split.length == 1) {
|
if (split.length == 1) {
|
||||||
return compare(fieldValue, op, map(comparison.getValue(), fieldGetter.getReturnType()));
|
return compare(fieldValue, op, map(comparison.getValue(), fieldGetter.getReturnType()));
|
||||||
} else {
|
} else {
|
||||||
final Method valueGetter = getGetter(fieldGetter.getReturnType(), split[1]);
|
if (split[1].contains(".")) {
|
||||||
return compare(fieldValue == null ? null : valueGetter.invoke(fieldValue), op,
|
// nested field access
|
||||||
map(comparison.getValue(), valueGetter.getReturnType()));
|
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) {
|
} catch (final NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
|
||||||
|
|||||||
@@ -14,5 +14,4 @@ import org.springframework.data.repository.CrudRepository;
|
|||||||
import org.springframework.stereotype.Repository;
|
import org.springframework.stereotype.Repository;
|
||||||
|
|
||||||
@Repository
|
@Repository
|
||||||
public interface RootRepository
|
public interface RootRepository extends CrudRepository<Root, Long>, JpaSpecificationExecutor<Root> {}
|
||||||
extends CrudRepository<Root, Long>, JpaSpecificationExecutor<Root> {}
|
|
||||||
|
|||||||
@@ -88,6 +88,16 @@ class SpecificationBuilderLegacyTest extends SpecificationBuilderTest {
|
|||||||
runWithRsqlToSpecBuilder(super::pluralSubMapAttribute, LEGACY_G2);
|
runWithRsqlToSpecBuilder(super::pluralSubMapAttribute, LEGACY_G2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void singularEntitySubSubAttributeG1() {
|
||||||
|
runWithRsqlToSpecBuilder(super::singularEntitySubSubAttribute, LEGACY_G1);
|
||||||
|
}
|
||||||
|
@Override
|
||||||
|
@Test
|
||||||
|
void singularEntitySubSubAttribute() {
|
||||||
|
runWithRsqlToSpecBuilder(super::singularEntitySubSubAttribute, LEGACY_G2);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected Specification<Root> getSpecification(final String rsql) {
|
protected Specification<Root> getSpecification(final String rsql) {
|
||||||
return builder.specification(rsql);
|
return builder.specification(rsql);
|
||||||
@@ -98,7 +108,7 @@ class SpecificationBuilderLegacyTest extends SpecificationBuilderTest {
|
|||||||
|
|
||||||
INTVALUE("intValue"),
|
INTVALUE("intValue"),
|
||||||
STRVALUE("strValue"),
|
STRVALUE("strValue"),
|
||||||
SUBENTITY("subEntity", "strValue", "intValue"),
|
SUBENTITY("subEntity", "strValue", "intValue", "subSub"),
|
||||||
SUBSET("subSet", "strValue", "intValue"),
|
SUBSET("subSet", "strValue", "intValue"),
|
||||||
SUBMAP("subMap");
|
SUBMAP("subMap");
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ package org.eclipse.hawkbit.repository.jpa.rsql.sa;
|
|||||||
import static org.assertj.core.api.Assertions.assertThat;
|
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.G3;
|
||||||
import static org.eclipse.hawkbit.repository.rsql.RsqlConfigHolder.RsqlToSpecBuilder.LEGACY_G1;
|
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.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
@@ -43,6 +44,8 @@ import org.springframework.orm.jpa.vendor.Database;
|
|||||||
@Slf4j
|
@Slf4j
|
||||||
class SpecificationBuilderTest {
|
class SpecificationBuilderTest {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private SubSubRepository subSubRepository;
|
||||||
@Autowired
|
@Autowired
|
||||||
private SubRepository subRepository;
|
private SubRepository subRepository;
|
||||||
@Autowired
|
@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")).isEmpty();
|
||||||
assertThat(filter("subEntity.strValue==subx and subEntity.intValue!=1")).hasSize(2).containsExactlyInAnyOrder(root1, root2);
|
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 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) {
|
if (getRsqlToSpecBuilder() != LEGACY_G1) {
|
||||||
// G1 doesn't get null entity in not
|
// G1 doesn't get null entity in not
|
||||||
assertThat(filter("subEntity.strValue==subx or subEntity.intValue!=1")).hasSize(3).containsExactlyInAnyOrder(root1, root2, root5);
|
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);
|
.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) {
|
private List<Root> filter(final String rsql) {
|
||||||
// reference / auto filter (using elements and reflection)
|
// reference / auto filter (using elements and reflection)
|
||||||
final ReferenceMatcher matcher = ReferenceMatcher.ofRsql(rsql);
|
final ReferenceMatcher matcher = ReferenceMatcher.ofRsql(rsql);
|
||||||
|
|||||||
@@ -10,9 +10,13 @@
|
|||||||
package org.eclipse.hawkbit.repository.jpa.rsql.sa;
|
package org.eclipse.hawkbit.repository.jpa.rsql.sa;
|
||||||
|
|
||||||
import jakarta.persistence.Entity;
|
import jakarta.persistence.Entity;
|
||||||
|
import jakarta.persistence.FetchType;
|
||||||
import jakarta.persistence.GeneratedValue;
|
import jakarta.persistence.GeneratedValue;
|
||||||
import jakarta.persistence.GenerationType;
|
import jakarta.persistence.GenerationType;
|
||||||
import jakarta.persistence.Id;
|
import jakarta.persistence.Id;
|
||||||
|
import jakarta.persistence.ManyToOne;
|
||||||
|
import jakarta.persistence.OneToMany;
|
||||||
|
import jakarta.persistence.OneToOne;
|
||||||
|
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import lombok.experimental.Accessors;
|
import lombok.experimental.Accessors;
|
||||||
@@ -30,4 +34,7 @@ class Sub {
|
|||||||
// basic
|
// basic
|
||||||
private String strValue;
|
private String strValue;
|
||||||
private int intValue;
|
private int intValue;
|
||||||
|
// entity
|
||||||
|
@ManyToOne
|
||||||
|
private SubSub subSub;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,5 +14,4 @@ import org.springframework.data.repository.CrudRepository;
|
|||||||
import org.springframework.stereotype.Repository;
|
import org.springframework.stereotype.Repository;
|
||||||
|
|
||||||
@Repository
|
@Repository
|
||||||
public interface SubRepository
|
public interface SubRepository extends CrudRepository<Sub, Long>, JpaSpecificationExecutor<Sub> {}
|
||||||
extends CrudRepository<Sub, Long>, JpaSpecificationExecutor<Sub> {}
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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<SubSub, Long>, JpaSpecificationExecutor<SubSub> {}
|
||||||
Reference in New Issue
Block a user