Simplify RSQL fields (#2416)

Signed-off-by: Avgustin Marinov <Avgustin.Marinov@bosch.com>
This commit is contained in:
Avgustin Marinov
2025-05-23 11:17:54 +03:00
committed by GitHub
parent 108f03848f
commit 8184aad13c
12 changed files with 64 additions and 175 deletions

View File

@@ -42,11 +42,6 @@ public enum ActionFields implements RsqlQueryField, FieldValueConverter<ActionFi
private final String jpaEntityFieldName;
private final List<String> subEntityAttributes;
ActionFields(final String jpaEntityFieldName) {
this.jpaEntityFieldName = jpaEntityFieldName;
this.subEntityAttributes = Collections.emptyList();
}
ActionFields(final String jpaEntityFieldName, final String... subEntityAttributes) {
this.jpaEntityFieldName = jpaEntityFieldName;
this.subEntityAttributes = List.of(subEntityAttributes);

View File

@@ -34,33 +34,19 @@ public enum DistributionSetFields implements RsqlQueryField {
COMPLETE("complete"),
MODULE("modules", SoftwareModuleFields.ID.getJpaEntityFieldName(), SoftwareModuleFields.NAME.getJpaEntityFieldName()),
TAG("tags", "name"),
METADATA("metadata", new SimpleImmutableEntry<>("key", "value")),
METADATA("metadata"),
VALID("valid");
private final String jpaEntityFieldName;
private final List<String> subEntityAttributes;
private final Entry<String, String> subEntityMapTuple;
DistributionSetFields(final String jpaEntityFieldName) {
this(jpaEntityFieldName, Collections.emptyList(), null);
}
DistributionSetFields(final String jpaEntityFieldName, final String... subEntityAttributes) {
this(jpaEntityFieldName, List.of(subEntityAttributes), null);
}
DistributionSetFields(final String jpaEntityFieldName, final Entry<String, String> subEntityMapTuple) {
this(jpaEntityFieldName, Collections.emptyList(), subEntityMapTuple);
}
DistributionSetFields(final String jpaEntityFieldName, List<String> subEntityAttributes, final Entry<String, String> subEntityMapTuple) {
this.jpaEntityFieldName = jpaEntityFieldName;
this.subEntityMapTuple = subEntityMapTuple;
this.subEntityAttributes = subEntityAttributes;
this.subEntityAttributes = List.of(subEntityAttributes);
}
@Override
public Optional<Entry<String, String>> getSubEntityMapTuple() {
return Optional.ofNullable(subEntityMapTuple);
public boolean isMap() {
return this == METADATA;
}
}

View File

@@ -27,17 +27,13 @@ public enum DistributionSetTagFields implements RsqlQueryField {
NAME(TagFields.NAME.getJpaEntityFieldName()),
DESCRIPTION(TagFields.DESCRIPTION.getJpaEntityFieldName()),
COLOUR(TagFields.COLOUR.getJpaEntityFieldName()),
DISTRIBUTIONSET("assignedToDistributionSet",
DISTRIBUTIONSET(
"assignedToDistributionSet",
DistributionSetFields.ID.getJpaEntityFieldName(), DistributionSetFields.NAME.getJpaEntityFieldName());
private final String jpaEntityFieldName;
private final List<String> subEntityAttributes;
DistributionSetTagFields(final String jpaEntityFieldName) {
this.jpaEntityFieldName = jpaEntityFieldName;
this.subEntityAttributes = Collections.emptyList();
}
DistributionSetTagFields(final String jpaEntityFieldName, final String... subEntityAttributes) {
this.jpaEntityFieldName = jpaEntityFieldName;
this.subEntityAttributes = List.of(subEntityAttributes);

View File

@@ -13,8 +13,7 @@ package org.eclipse.hawkbit.repository;
* A value convert which converts given string based values into an object which
* can be used for building generic queries. Mapping external API values e.g.
* REST API to inside representation on database. E.g. mapping 'pending' or
* 'finished' values in rest queries to Action#isActive boolean
* value.
* 'finished' values in rest queries to Action#isActive boolean value.
*
* @param <T> the enum parameter
*/

View File

@@ -24,18 +24,15 @@ public enum RolloutFields implements RsqlQueryField {
NAME("name"),
DESCRIPTION("description"),
STATUS("status"),
DISTRIBUTIONSET("distributionSet", DistributionSetFields.ID.getJpaEntityFieldName(),
DISTRIBUTIONSET(
"distributionSet",
DistributionSetFields.ID.getJpaEntityFieldName(),
DistributionSetFields.NAME.getJpaEntityFieldName(), DistributionSetFields.VERSION.getJpaEntityFieldName(),
DistributionSetFields.TYPE.getJpaEntityFieldName());
private final String jpaEntityFieldName;
private final List<String> subEntityAttributes;
RolloutFields(final String jpaEntityFieldName) {
this.jpaEntityFieldName = jpaEntityFieldName;
this.subEntityAttributes = Collections.emptyList();
}
RolloutFields(final String jpaEntityFieldName, final String... subEntityAttributes) {
this.jpaEntityFieldName = jpaEntityFieldName;
this.subEntityAttributes = List.of(subEntityAttributes);

View File

@@ -34,30 +34,6 @@ public interface RsqlQueryField {
@NotNull
String getJpaEntityFieldName();
/**
* Contains the sub entity the given field.
*
* @param propertyField the given field
* @return <code>true</code> contains <code>false</code> contains not
*/
default boolean containsSubEntityAttribute(final String propertyField) {
final List<String> subEntityAttributes = getSubEntityAttributes();
if (subEntityAttributes.contains(propertyField)) {
return true;
}
for (final String attribute : subEntityAttributes) {
final String[] graph = attribute.split(SUB_ATTRIBUTE_SPLIT_REGEX);
for (final String subAttribute : graph) {
if (subAttribute.equalsIgnoreCase(propertyField)) {
return true;
}
}
}
return false;
}
/**
* @return all sub entities attributes.
*/
@@ -65,22 +41,6 @@ public interface RsqlQueryField {
return Collections.emptyList();
}
/**
* @return a key/value tuple of a sub entity.
*/
default Optional<Entry<String, String>> getSubEntityMapTuple() {
return Optional.empty();
}
/**
* Is the entity field a {@link Map} consisting of key-value pairs.
*
* @return <code>true</code> is a map <code>false</code> is not a map
*/
default boolean isMap() {
return getSubEntityMapTuple().isPresent();
}
/**
* Returns the name of the field, that identifies the entity.
*
@@ -89,4 +49,13 @@ public interface RsqlQueryField {
default String identifierFieldName() {
return "id";
}
/**
* Is the entity field a {@link Map} consisting of key-value pairs.
*
* @return <code>true</code> is a map <code>false</code> is not a map
*/
default boolean isMap() {
return false;
}
}

View File

@@ -45,25 +45,10 @@ public enum TargetFields implements RsqlQueryField {
private final String jpaEntityFieldName;
private final List<String> subEntityAttributes;
private final Entry<String, String> subEntityMapTuple;
TargetFields(final String jpaEntityFieldName) {
this(jpaEntityFieldName, Collections.emptyList(), null);
}
TargetFields(final String jpaEntityFieldName, final String... subEntityAttributes) {
this(jpaEntityFieldName, List.of(subEntityAttributes), null);
}
TargetFields(final String jpaEntityFieldName, final List<String> subEntityAttributes, final Entry<String, String> subEntityMapTuple) {
this.jpaEntityFieldName = jpaEntityFieldName;
this.subEntityAttributes = subEntityAttributes;
this.subEntityMapTuple = subEntityMapTuple;
}
@Override
public Optional<Entry<String, String>> getSubEntityMapTuple() {
return Optional.ofNullable(subEntityMapTuple);
this.subEntityAttributes = List.of(subEntityAttributes);
}
@Override

View File

@@ -25,7 +25,7 @@ public enum TargetFilterQueryFields implements RsqlQueryField {
AUTOASSIGNDISTRIBUTIONSET("autoAssignDistributionSet", "name", "version");
private final String jpaEntityFieldName;
private List<String> subEntityAttributes;
private final List<String> subEntityAttributes;
TargetFilterQueryFields(final String jpaEntityFieldName) {
this(jpaEntityFieldName, Collections.emptyList());

View File

@@ -36,12 +36,6 @@ class FileNameFieldsTest {
assertThat(matchingClasses).isNotEmpty();
matchingClasses.forEach(providerClass -> {
assertThat(providerClass.getEnumConstants()).isNotEmpty();
for (final RsqlQueryField provider : providerClass.getEnumConstants()) {
if (provider.isMap() && !provider.getSubEntityAttributes().isEmpty()) {
throw new UnsupportedOperationException(
"Currently sub-entity attributes for maps are not supported, alternatively you could use the key/value tuple, defined by SimpleImmutableEntry class");
}
}
});
}
}

View File

@@ -60,7 +60,7 @@ public abstract class AbstractRSQLVisitor<A extends Enum<A> & RsqlQueryField> {
for (int i = 1; i < split.length; i++) {
split[i] = getFormattedSubEntityAttribute(enumValue, split[i]);
if (!enumValue.containsSubEntityAttribute(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
@@ -86,6 +86,32 @@ public abstract class AbstractRSQLVisitor<A extends Enum<A> & RsqlQueryField> {
}
}
/**
* 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}

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2021 Bosch.IO GmbH and others
* 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
@@ -16,7 +16,6 @@ import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Optional;
import java.util.Set;
import java.util.function.BiFunction;
@@ -344,9 +343,8 @@ public class JpaQueryRsqlVisitor<A extends Enum<A> & RsqlQueryField, T> extends
final Predicate mapPredicate = mapToMapPredicate(fieldPath, queryPath);
final Predicate valuePredicate = addOperatorPredicate(node, getMapValueFieldPath(queryPath.getEnumValue(), fieldPath),
transformedValues, value, queryPath);
final Predicate valuePredicate = addOperatorPredicate(node, fieldPath, transformedValues, value, queryPath);
return toSingleList(mapPredicate != null ? cb.and(mapPredicate, valuePredicate) : valuePredicate);
}
@@ -398,15 +396,6 @@ public class JpaQueryRsqlVisitor<A extends Enum<A> & RsqlQueryField, T> extends
expressionToCompare -> in(expressionToCompare, transformedValues));
}
private Path<Object> getMapValueFieldPath(final A enumField, final Path<Object> fieldPath) {
final String valueFieldNameFromSubEntity = enumField.getSubEntityMapTuple().map(Entry::getValue).orElse(null);
if (!enumField.isMap() || valueFieldNameFromSubEntity == null) {
return fieldPath;
}
return fieldPath.get(valueFieldNameFromSubEntity);
}
@SuppressWarnings("unchecked")
private Predicate mapToMapPredicate(final Path<Object> fieldPath, final QueryPath queryPath) {
if (!queryPath.getEnumValue().isMap()) {
@@ -415,16 +404,8 @@ public class JpaQueryRsqlVisitor<A extends Enum<A> & RsqlQueryField, T> extends
final String[] graph = queryPath.getJpaPath();
final String keyValue = graph[graph.length - 1];
if (fieldPath instanceof MapJoin) {
// Currently we support only string key. So below cast is safe.
return equal((Expression<String>) (((MapJoin<?, ?, ?>) fieldPath).key()), keyValue);
}
final String keyFieldName = queryPath.getEnumValue().getSubEntityMapTuple().map(Entry::getKey)
.orElseThrow(() -> new UnsupportedOperationException(
"For the fields, defined as Map, only Map java type or tuple in the form of " +
"SimpleImmutableEntry are allowed. Neither of those could be found!"));
return equal(fieldPath.get(keyFieldName), keyValue);
// 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) {
@@ -514,17 +495,12 @@ public class JpaQueryRsqlVisitor<A extends Enum<A> & RsqlQueryField, T> extends
@SuppressWarnings({ "rawtypes", "unchecked" })
private Expression<String> getExpressionToCompare(final Path innerFieldPath, final A enumField) {
if (!enumField.isMap()) {
return pathOfString(innerFieldPath);
}
if (innerFieldPath instanceof MapJoin) {
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);
}
final String valueFieldName = enumField.getSubEntityMapTuple().map(Entry::getValue)
.orElseThrow(() -> new UnsupportedOperationException(
"For the fields, defined as Map, only Map java type or tuple in the form of SimpleImmutableEntry are allowed. Neither of those could be found!"));
return pathOfString(innerFieldPath).get(valueFieldName);
}
private String toSQL(final String transformedValue) {

View File

@@ -9,14 +9,12 @@
*/
package org.eclipse.hawkbit.repository.jpa.rsql;
import java.util.AbstractMap;
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.Map.Entry;
import java.util.function.Function;
import jakarta.persistence.criteria.CriteriaBuilder;
@@ -125,7 +123,7 @@ public class JpaQueryRsqlVisitorG2<A extends Enum<A> & RsqlQueryField, T>
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(getValueFieldPath(queryPath, fieldPath));
return cb.isNull(fieldPath);
}
} else if (node.getOperator() == RSQLUtility.NOT) {
if (values.size() != 1) {
@@ -133,13 +131,12 @@ public class JpaQueryRsqlVisitorG2<A extends Enum<A> & RsqlQueryField, T>
}
// NOT operator for maps and null value is treated as does exist correspondingly
((PluralJoin<?, ?, ?>) fieldPath).on(toMapEntryKeyPredicate(queryPath, fieldPath));
final Path<?> valueFieldPath = getValueFieldPath(queryPath, fieldPath);
if (values.get(0) == null) {
// special handling of "exists"
return cb.isNotNull(valueFieldPath);
return cb.isNotNull(fieldPath);
} else {
// special handling or "not equal" or null (same as != but with possible optimized join - no subquery)
return toNotEqualToPredicate(queryPath, valueFieldPath, values.get(0));
return toNotEqualToPredicate(queryPath, fieldPath, values.get(0));
}
}
mapEntryKeyPredicate = toMapEntryKeyPredicate(queryPath, fieldPath);
@@ -147,38 +144,15 @@ public class JpaQueryRsqlVisitorG2<A extends Enum<A> & RsqlQueryField, T>
mapEntryKeyPredicate = null;
}
final Predicate valuePredicate = toOperatorAndValuePredicate(node, queryPath, getValueFieldPath(queryPath, fieldPath), values);
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(mapEntryKeyPath(queryPath, fieldPath), graph[graph.length - 1]);
}
@SuppressWarnings("unchecked")
private Path<String> mapEntryKeyPath(final QueryPath queryPath, final Path<?> fieldPath) {
if (fieldPath instanceof MapJoin) {
// Currently we support only string key. So below cast is safe.
return (Path<String>) ((MapJoin<?, ?, ?>) fieldPath).key();
}
return fieldPath.get(queryPath.getEnumValue().getSubEntityMapTuple()
.map(Entry::getKey)
.orElseThrow(() -> new UnsupportedOperationException(String.format(
"For the fields, defined as Map, only %s java type or tuple in the form of %s are allowed!",
Map.class.getName(), AbstractMap.SimpleImmutableEntry.class.getName()))));
}
private Path<?> getValueFieldPath(final QueryPath queryPath, final Path<?> fieldPath) {
final A enumField = queryPath.getEnumValue();
if (enumField.isMap()) {
final Path<?> mapValuePath = enumField.getSubEntityMapTuple().map(Entry::getValue).map(fieldPath::get).orElse(null);
return mapValuePath == null ? fieldPath : mapValuePath;
} else {
return fieldPath;
}
return equal((Path<String>) ((MapJoin<?, ?, ?>) fieldPath).key(), graph[graph.length - 1]);
}
private Predicate toOperatorAndValuePredicate(
@@ -359,20 +333,12 @@ public class JpaQueryRsqlVisitorG2<A extends Enum<A> & RsqlQueryField, T>
@SuppressWarnings({ "rawtypes", "unchecked" })
private Path<String> getExpressionToCompare(final A enumField, final Path fieldPath) {
if (!enumField.isMap()) {
return pathOfString(fieldPath);
}
if (fieldPath instanceof MapJoin) {
if (enumField.isMap()) {
// Currently we support only string key. So below cast is safe.
return (Path<String>) (((MapJoin<?, ?, ?>) fieldPath).value());
} else {
return pathOfString(fieldPath);
}
return enumField.getSubEntityMapTuple()
.map(Entry::getValue)
.map(fieldPath::<String>get)
.orElseThrow(() ->
new UnsupportedOperationException(
"For the fields, defined as Map, only Map java type or tuple in the form of SimpleImmutableEntry are allowed." +
" Neither of those could be found!"));
}
// result is String, enum value, boolean or null