Fix comple attribute RSQL filters (#2564)

Signed-off-by: Avgustin Marinov <Avgustin.Marinov@bosch.com>
This commit is contained in:
Avgustin Marinov
2025-07-24 14:01:49 +03:00
committed by GitHub
parent 9b1fdc2f49
commit 7752ec6c24
3 changed files with 36 additions and 15 deletions

View File

@@ -26,6 +26,7 @@ import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Objects; import java.util.Objects;
import java.util.stream.Collectors;
import jakarta.annotation.Nonnull; import jakarta.annotation.Nonnull;
import jakarta.persistence.criteria.CriteriaBuilder; import jakarta.persistence.criteria.CriteriaBuilder;
@@ -44,6 +45,8 @@ import jakarta.persistence.metamodel.MapAttribute;
import jakarta.persistence.metamodel.PluralAttribute; import jakarta.persistence.metamodel.PluralAttribute;
import jakarta.persistence.metamodel.SetAttribute; import jakarta.persistence.metamodel.SetAttribute;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.eclipse.hawkbit.repository.exception.RSQLParameterSyntaxException; import org.eclipse.hawkbit.repository.exception.RSQLParameterSyntaxException;
import org.eclipse.hawkbit.repository.exception.RSQLParameterUnsupportedFieldException; import org.eclipse.hawkbit.repository.exception.RSQLParameterUnsupportedFieldException;
@@ -117,9 +120,10 @@ public class SpecificationBuilder<T> {
.toList() .toList()
.toArray(PREDICATES_ARRAY_0)); .toArray(PREDICATES_ARRAY_0));
} else if (op == Logical.Operator.OR) { } else if (op == Logical.Operator.OR) {
final Map<String, Integer> state = pathResolver.getState();
return cb.or(logical.getChildren().stream() return cb.or(logical.getChildren().stream()
.map(child -> { .map(child -> {
pathResolver.reset(); pathResolver.reset(state);
return build(child); // for or path resolver joins could be reused return build(child); // for or path resolver joins could be reused
}) })
.toList() .toList()
@@ -324,12 +328,13 @@ public class SpecificationBuilder<T> {
private static Path<?> deepGetPath(final Path<?> path, final String subAttributeName) { private static Path<?> deepGetPath(final Path<?> path, final String subAttributeName) {
return deepGetPath(path, subAttributeName.split("\\."), 0); return deepGetPath(path, subAttributeName.split("\\."), 0);
} }
private static Path<?> deepGetPath(final Path<?> path, final String[] subAttributeNameSplit, int startIndex) { private static Path<?> deepGetPath(final Path<?> path, final String[] subAttributeNameSplit, int startIndex) {
final String subAttributeName = subAttributeNameSplit[startIndex++]; final String subAttributeName = subAttributeNameSplit[startIndex++];
if (startIndex == subAttributeNameSplit.length) { if (startIndex == subAttributeNameSplit.length) {
return path.get(subAttributeName); return path.get(subAttributeName);
} else { // else its a deeper path so request left join } else { // else its a deeper path so request left join
if (path instanceof Join<?,?> join) { if (path instanceof Join<?, ?> join) {
return deepGetPath(join.join(subAttributeName, JoinType.LEFT), subAttributeNameSplit, startIndex); return deepGetPath(join.join(subAttributeName, JoinType.LEFT), subAttributeNameSplit, startIndex);
} else { } else {
throw new RSQLParameterSyntaxException("Unexpected sub attribute " + subAttributeName); throw new RSQLParameterSyntaxException("Unexpected sub attribute " + subAttributeName);
@@ -432,8 +437,13 @@ public class SpecificationBuilder<T> {
return getCollectionPathResolver(attribute.getName()).getJoinOnInner(value); return getCollectionPathResolver(attribute.getName()).getJoinOnInner(value);
} }
private void reset() { private Map<String, Integer> getState() {
attributeToPathResolver.values().forEach(CollectionPathResolver::reset); return attributeToPathResolver.entrySet().stream()
.collect(Collectors.toMap(Map.Entry::getKey, resolver -> resolver.getValue().getPos()));
}
private void reset(final Map<String, Integer> toState) {
attributeToPathResolver.forEach((attribute, resolver) -> resolver.setPos(toState.getOrDefault(attribute, 0)));
} }
@Nonnull @Nonnull
@@ -445,6 +455,8 @@ public class SpecificationBuilder<T> {
private final String attributeName; private final String attributeName;
private final List<Path<?>> paths = new ArrayList<>(); private final List<Path<?>> paths = new ArrayList<>();
@Getter
@Setter
private int pos; private int pos;
private final Map<Object, MapJoin<?, ?, ?>> joinOnCache = new HashMap<>(); private final Map<Object, MapJoin<?, ?, ?>> joinOnCache = new HashMap<>();
private final Map<Object, MapJoin<?, ?, ?>> joinOnInnerCache = new HashMap<>(); private final Map<Object, MapJoin<?, ?, ?>> joinOnInnerCache = new HashMap<>();
@@ -479,10 +491,6 @@ public class SpecificationBuilder<T> {
return mapPath; return mapPath;
}); });
} }
private void reset() {
pos = 0;
}
} }
} }
} }

View File

@@ -11,17 +11,16 @@ package org.eclipse.hawkbit.repository.jpa.specifications;
import java.util.List; import java.util.List;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import org.springframework.data.jpa.domain.Specification; import org.springframework.data.jpa.domain.Specification;
/** /**
* Helper class to easily combine {@link Specification} instances. * Helper class to easily combine {@link Specification} instances.
*/ */
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public final class SpecificationsBuilder { public final class SpecificationsBuilder {
private SpecificationsBuilder() {
}
/** /**
* Combine all given specification with and. The first specification is the * Combine all given specification with and. The first specification is the
* where clause. * where clause.
@@ -33,11 +32,10 @@ public final class SpecificationsBuilder {
if (specList.isEmpty()) { if (specList.isEmpty()) {
return null; return null;
} }
Specification<T> specs = Specification.where(specList.get(0)); Specification<T> specs = specList.get(0);
for (final Specification<T> specification : specList.subList(1, specList.size())) { for (final Specification<T> specification : specList.subList(1, specList.size())) {
specs = specs.and(specification); specs = specs.and(specification);
} }
return specs; return specs;
} }
}
}

View File

@@ -102,6 +102,21 @@ class RsqlToSqlTest {
print(JpaTarget.class, TargetFields.class, TargetFields.TAG.name() + "!=''"); print(JpaTarget.class, TargetFields.class, TargetFields.TAG.name() + "!=''");
} }
@Test
void printComplex() {
print(JpaTarget.class, TargetFields.class, "attribute.key1==00 and (attribute.key2==02 or attribute.key2==01)");
print(JpaTarget.class, TargetFields.class, "(attribute.key1==00 or attribute.key1==01) and (attribute.key2==02 or attribute.key2==01) and attribute.key3==01");
print(JpaTarget.class, TargetFields.class, "(attribute.key1==00 or attribute.key1==01) and (attribute.key2==02 or attribute.key2==01) and attribute.key3==01 and updateStatus!=pending");
print(JpaTarget.class, TargetFields.class, "((attribute.key1==00 or attribute.key1==01) and (attribute.key2==02 or attribute.key2==01) and attribute.key3==03 and updateStatus!=pending)");
print(JpaTarget.class, TargetFields.class, "((attribute.key1==00 or attribute.key1==01) and (attribute.key2==02 or attribute.key2==01) and attribute.key3==01 and updateStatus!=pending)");
}
@Test
void printVeryComplex() {
print(JpaTarget.class, TargetFields.class, "(attribute.key1==00 or attribute.key1==01) and ((attribute.key2==02 or attribute.key2==01) and (attribute.key4==02 or attribute.key5==01)) and attribute.key3==01 and updateStatus!=pending");
print(JpaTarget.class, TargetFields.class, "(attribute.key1==00 or attribute.key1==01) and ((attribute.key2==02 or attribute.key2==01) or (attribute.key4==02 or attribute.key5==01)) and attribute.key3==01 and updateStatus!=pending");
}
private static String from(final String sql) { private static String from(final String sql) {
return sql.substring(sql.indexOf("FROM")); return sql.substring(sql.indexOf("FROM"));
} }