First level suppor for RsqlQueryFields shortcut support (#2686)

* now it is possible to have a showrtcut for a sub attributes (i.e. calling it directly with enum name, e.g. type -> type.key) with directly specifying the defaultSubEntityAttribute
* no need to have single sub attribute in order to have a default sub attribute
* added TYPE search field for TargetFields (sinonim of targettype)
* targettype is deprecated - to be decided if and when to be removed
* returned back "type" direct search (with meaning type.key) for DistributionSet and SoftwareModule as non-depricated
* add serche with "type" as type.key for Target

Signed-off-by: Avgustin Marinov <Avgustin.Marinov@bosch.com>
This commit is contained in:
Avgustin Marinov
2025-09-23 10:56:26 +03:00
committed by GitHub
parent d98ab779a2
commit 9ab0a8628e
11 changed files with 55 additions and 42 deletions

View File

@@ -46,6 +46,11 @@ public enum DistributionSetFields implements RsqlQueryField {
this.subEntityAttributes = List.of(subEntityAttributes); this.subEntityAttributes = List.of(subEntityAttributes);
} }
@Override
public String getDefaultSubEntityAttribute() {
return this == TYPE ? DistributionSetTypeFields.KEY.getJpaEntityFieldName() : RsqlQueryField.super.getDefaultSubEntityAttribute();
}
@Override @Override
public boolean isMap() { public boolean isMap() {
return this == METADATA; return this == METADATA;

View File

@@ -20,7 +20,7 @@ package org.eclipse.hawkbit.repository;
public interface FieldValueConverter<T extends Enum<T>> { public interface FieldValueConverter<T extends Enum<T>> {
/** /**
* Converts the given {@code value} into the representation to build ageneric query. * Converts the given {@code value} into the representation to build a generic query.
* *
* @param enumValue the enum value to build the value for * @param enumValue the enum value to build the value for
* @param value the value in string representation * @param value the value in string representation

View File

@@ -39,12 +39,22 @@ public interface RsqlQueryField {
return Collections.emptyList(); return Collections.emptyList();
} }
/**
* Return the default sub entity attribute if available. This allows to skip that sub-attribute and call with "shortcut" - the enum name
*
* @return the default sub-attribute or <code>null</code>> if no default is available
*/
default String getDefaultSubEntityAttribute() {
final List<String> subAttributes = getSubEntityAttributes();
return subAttributes.size() == 1 ? subAttributes.get(0) : null;
}
/** /**
* Returns the name of the field, that identifies the entity. * Returns the name of the field, that identifies the entity.
* *
* @return the name of the identifier, by default 'id' * @return the name of the identifier, by default 'id'
*/ */
default String identifierFieldName() { default String getIdentifierFieldName() {
return "id"; return "id";
} }

View File

@@ -41,6 +41,12 @@ public enum SoftwareModuleFields implements RsqlQueryField {
this.subEntityAttributes = List.of(subEntityAttributes); this.subEntityAttributes = List.of(subEntityAttributes);
} }
@Override
public String getDefaultSubEntityAttribute() {
return this == TYPE ? SoftwareModuleTypeFields.KEY.getJpaEntityFieldName() : RsqlQueryField.super.getDefaultSubEntityAttribute();
}
@Override @Override
public boolean isMap() { public boolean isMap() {
return this == METADATA; return this == METADATA;

View File

@@ -39,6 +39,14 @@ public enum TargetFields implements RsqlQueryField {
TAG("tags", TagFields.NAME.getJpaEntityFieldName()), TAG("tags", TagFields.NAME.getJpaEntityFieldName()),
LASTCONTROLLERREQUESTAT("lastTargetQuery"), LASTCONTROLLERREQUESTAT("lastTargetQuery"),
METADATA("metadata"), METADATA("metadata"),
TYPE("targetType",
TargetTypeFields.ID.getJpaEntityFieldName(),
TargetTypeFields.KEY.getJpaEntityFieldName(),
TargetTypeFields.NAME.getJpaEntityFieldName()),
// kept just for backward compatibility for backward compatibility
// could be removed only if in the systems there are no active auto assignments or rollouts (dynamic or starting) with that condition
// to be reconsidered if and when to be removed
@Deprecated(forRemoval = true, since = "0.10.0")
TARGETTYPE("targetType", TARGETTYPE("targetType",
TargetTypeFields.ID.getJpaEntityFieldName(), TargetTypeFields.ID.getJpaEntityFieldName(),
TargetTypeFields.KEY.getJpaEntityFieldName(), TargetTypeFields.KEY.getJpaEntityFieldName(),
@@ -52,6 +60,11 @@ public enum TargetFields implements RsqlQueryField {
this.subEntityAttributes = List.of(subEntityAttributes); this.subEntityAttributes = List.of(subEntityAttributes);
} }
@Override
public String getDefaultSubEntityAttribute() {
return this == TYPE ? TargetTypeFields.KEY.getJpaEntityFieldName() : RsqlQueryField.super.getDefaultSubEntityAttribute();
}
@Override @Override
public boolean isMap() { public boolean isMap() {
return this == ATTRIBUTE || this == METADATA; return this == ATTRIBUTE || this == METADATA;

View File

@@ -24,7 +24,6 @@ import static org.eclipse.hawkbit.repository.jpa.ql.Node.Comparison.Operator.NOT
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Objects;
import java.util.Set; import java.util.Set;
import java.util.function.Function; import java.util.function.Function;
import java.util.function.UnaryOperator; import java.util.function.UnaryOperator;
@@ -42,10 +41,8 @@ import cz.jirutka.rsql.parser.ast.RSQLVisitor;
import lombok.AccessLevel; import lombok.AccessLevel;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.eclipse.hawkbit.repository.DistributionSetFields;
import org.eclipse.hawkbit.repository.FieldValueConverter; import org.eclipse.hawkbit.repository.FieldValueConverter;
import org.eclipse.hawkbit.repository.RsqlQueryField; import org.eclipse.hawkbit.repository.RsqlQueryField;
import org.eclipse.hawkbit.repository.SoftwareModuleFields;
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;
import org.eclipse.hawkbit.repository.jpa.ql.Node; import org.eclipse.hawkbit.repository.jpa.ql.Node;
@@ -58,7 +55,7 @@ import org.eclipse.hawkbit.repository.jpa.ql.Node.Comparison;
* <li>check for * and convert EQ/NEQ to LIKE/NOT_LIKE</li> * <li>check for * and convert EQ/NEQ to LIKE/NOT_LIKE</li>
* <li>replace the rsql fields (enum values) with the JPA entity field names</li> * <li>replace the rsql fields (enum values) with the JPA entity field names</li>
* <li>checks sub-attributes (if allowed)</li> * <li>checks sub-attributes (if allowed)</li>
* <li>append the default sub-attributes if needed)</li> * <li>append the default sub-attributes if needed</li>
* <li>apply value conversion for implementing with FieldValueConverter</li> * <li>apply value conversion for implementing with FieldValueConverter</li>
* </ul> * </ul>
*/ */
@@ -121,20 +118,17 @@ public class RsqlParser {
// just enum name for a complex type (with sub-attributes), should have single (default!) sub-attribute // just enum name for a complex type (with sub-attributes), should have single (default!) sub-attribute
if (enumValue.isMap()) { if (enumValue.isMap()) {
throw new RSQLParameterUnsupportedFieldException("No key specified for a map type " + enumValue); throw new RSQLParameterUnsupportedFieldException("No key specified for a map type " + enumValue);
} else if (enumValue.getSubEntityAttributes().size() == 1) {
// single sub attribute - so, treat it as a default
attribute = enumValue.getJpaEntityFieldName() + SUB_ATTRIBUTE_SEPARATOR + enumValue.getSubEntityAttributes().get(0);
} else if (RsqlUtility.SM_DS_SEARCH_BY_TYPE_BACKWARD_COMPATIBILITY &&
"type".equalsIgnoreCase(enumValue.getJpaEntityFieldName()) &&
(Objects.equals(rsqlQueryFieldType, SoftwareModuleFields.class) ||
Objects.equals(rsqlQueryFieldType, DistributionSetFields.class))) {
// backward compatibility - type for SoftwareModuleTypeFields && DistributionSetFields means type.key
attribute = enumValue.getJpaEntityFieldName() + SUB_ATTRIBUTE_SEPARATOR + "key";
} else { } else {
throw new RSQLParameterUnsupportedFieldException( final String defaultSubEntityAttribute = enumValue.getDefaultSubEntityAttribute();
String.format( if (defaultSubEntityAttribute != null) {
"The given search parameter field {%s} requires one of the following sub-attributes %s", // single sub attribute - so, treat it as a default
key, enumValue.getSubEntityAttributes())); attribute = enumValue.getJpaEntityFieldName() + SUB_ATTRIBUTE_SEPARATOR + defaultSubEntityAttribute;
} else {
throw new RSQLParameterUnsupportedFieldException(
String.format(
"The given search parameter field {%s} requires one of the following sub-attributes %s",
key, enumValue.getSubEntityAttributes()));
}
} }
} }
} else { // field name with sub-attribute } else { // field name with sub-attribute

View File

@@ -71,11 +71,6 @@ import org.springframework.orm.jpa.vendor.Database;
@NoArgsConstructor(access = AccessLevel.PRIVATE) @NoArgsConstructor(access = AccessLevel.PRIVATE)
public class RsqlUtility { public class RsqlUtility {
// to be removed in future releases, use type.key instead of type for software module and distribution set RSQL queries
@Deprecated(forRemoval = true, since = "0.10.0")
public static final boolean SM_DS_SEARCH_BY_TYPE_BACKWARD_COMPATIBILITY =
"true".equalsIgnoreCase(System.getProperty("hawkbit.rsql.sm-ds-search-by-type.backward-compatibility", "true"));
private static final RsqlUtility SINGLETON = new RsqlUtility(); private static final RsqlUtility SINGLETON = new RsqlUtility();
public enum RsqlToSpecBuilder { public enum RsqlToSpecBuilder {

View File

@@ -13,7 +13,6 @@ import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Objects;
import java.util.Set; import java.util.Set;
import java.util.stream.Stream; import java.util.stream.Stream;
@@ -24,12 +23,9 @@ import cz.jirutka.rsql.parser.ast.ComparisonOperator;
import cz.jirutka.rsql.parser.ast.RSQLOperators; import cz.jirutka.rsql.parser.ast.RSQLOperators;
import lombok.Value; import lombok.Value;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.eclipse.hawkbit.repository.DistributionSetFields;
import org.eclipse.hawkbit.repository.RsqlQueryField; import org.eclipse.hawkbit.repository.RsqlQueryField;
import org.eclipse.hawkbit.repository.SoftwareModuleFields;
import org.eclipse.hawkbit.repository.exception.RSQLParameterUnsupportedFieldException; import org.eclipse.hawkbit.repository.exception.RSQLParameterUnsupportedFieldException;
import org.eclipse.hawkbit.repository.jpa.ql.SpecificationBuilder; import org.eclipse.hawkbit.repository.jpa.ql.SpecificationBuilder;
import org.eclipse.hawkbit.repository.jpa.rsql.RsqlUtility;
import org.springframework.util.ObjectUtils; import org.springframework.util.ObjectUtils;
/** /**
@@ -74,14 +70,9 @@ public abstract class AbstractRSQLVisitor<A extends Enum<A> & RsqlQueryField> {
if (!enumValue.isMap()) { if (!enumValue.isMap()) {
// sub entity need minimum 1 dot // sub entity need minimum 1 dot
if (!enumValue.getSubEntityAttributes().isEmpty() && split.length < 2) { if (!enumValue.getSubEntityAttributes().isEmpty() && split.length < 2) {
if (enumValue.getSubEntityAttributes().size() == 1) { // single sub attribute - so add is as a default final String defaultSubEntityAttribute = enumValue.getDefaultSubEntityAttribute();
split = new String[] { split[0], enumValue.getSubEntityAttributes().get(0) }; if (defaultSubEntityAttribute != null) { // single sub attribute - so add is as a default
} else if (RsqlUtility.SM_DS_SEARCH_BY_TYPE_BACKWARD_COMPATIBILITY && split = new String[] { split[0], defaultSubEntityAttribute };
"type".equals(node.getSelector()) &&
(Objects.equals(rsqlQueryFieldType, SoftwareModuleFields.class) ||
Objects.equals(rsqlQueryFieldType, DistributionSetFields.class))) {
// backward compatibility - type for DistributionSetFields means type.key
split = new String[] { split[0], "key" };
} else { } else {
throw createRSQLParameterUnsupportedException(node, null); throw createRSQLParameterUnsupportedException(node, null);
} }

View File

@@ -493,8 +493,8 @@ public class JpaQueryRsqlVisitor<A extends Enum<A> & RsqlQueryField, T> extends
final Class<?> javaType = root.getJavaType(); final Class<?> javaType = root.getJavaType();
final Subquery<?> subquery = query.subquery(javaType); final Subquery<?> subquery = query.subquery(javaType);
final Root subqueryRoot = subquery.from(javaType); final Root subqueryRoot = subquery.from(javaType);
final Predicate equalPredicate = cb.equal(root.get(enumField.identifierFieldName()), final Predicate equalPredicate = cb.equal(root.get(enumField.getIdentifierFieldName()),
subqueryRoot.get(enumField.identifierFieldName())); subqueryRoot.get(enumField.getIdentifierFieldName()));
final Path innerFieldPath = getInnerFieldPath(subqueryRoot, fieldNames, enumField.isMap()); final Path innerFieldPath = getInnerFieldPath(subqueryRoot, fieldNames, enumField.isMap());
final Expression<String> expressionToCompare = getExpressionToCompare(innerFieldPath, enumField); final Expression<String> expressionToCompare = getExpressionToCompare(innerFieldPath, enumField);
final Predicate subQueryPredicate = subQueryPredicateProvider.apply(expressionToCompare); final Predicate subQueryPredicate = subQueryPredicateProvider.apply(expressionToCompare);

View File

@@ -341,8 +341,8 @@ public class JpaQueryRsqlVisitorG2<A extends Enum<A> & RsqlQueryField, T>
subquery.select(subqueryRoot) subquery.select(subqueryRoot)
.where(cb.and( .where(cb.and(
cb.equal( cb.equal(
root.get(queryPath.getEnumValue().identifierFieldName()), root.get(queryPath.getEnumValue().getIdentifierFieldName()),
subqueryRoot.get(queryPath.getEnumValue().identifierFieldName())), subqueryRoot.get(queryPath.getEnumValue().getIdentifierFieldName())),
subQueryPredicateProvider.apply( subQueryPredicateProvider.apply(
getExpressionToCompare(queryPath.getEnumValue(), getFieldPath(subqueryRoot, queryPath))))))); getExpressionToCompare(queryPath.getEnumValue(), getFieldPath(subqueryRoot, queryPath)))))));
} }

View File

@@ -143,8 +143,7 @@ class RsqlSoftwareModuleFieldTest extends AbstractJpaIntegrationTest {
} }
@Test @Test
void testFilterByTypeBackwardCompatibility() { void testFilterByTypeShortcut() {
assertThat(RsqlUtility.SM_DS_SEARCH_BY_TYPE_BACKWARD_COMPATIBILITY).isTrue();
assertRSQLQuery(SoftwareModuleFields.TYPE.name() + "==" + TestdataFactory.SM_TYPE_APP, 2); assertRSQLQuery(SoftwareModuleFields.TYPE.name() + "==" + TestdataFactory.SM_TYPE_APP, 2);
assertRSQLQuery(SoftwareModuleFields.TYPE.name() + "!=" + TestdataFactory.SM_TYPE_APP, 4); assertRSQLQuery(SoftwareModuleFields.TYPE.name() + "!=" + TestdataFactory.SM_TYPE_APP, 4);
assertRSQLQuery(SoftwareModuleFields.TYPE.name() + "==noExist*", 0); assertRSQLQuery(SoftwareModuleFields.TYPE.name() + "==noExist*", 0);