Cleanup QL parse and tranform API (#2707)

Signed-off-by: Avgustin Marinov <Avgustin.Marinov@bosch.com>
This commit is contained in:
Avgustin Marinov
2025-09-29 16:36:41 +03:00
committed by GitHub
parent e747d55a38
commit 3714d408d0
3 changed files with 136 additions and 68 deletions

View File

@@ -39,16 +39,16 @@ public interface Node {
return op(this, Logical.Operator.OR, other); return op(this, Logical.Operator.OR, other);
} }
// utility method that maps this node with a mapper that could modify comparisons - e.g. change keys, values, operators, or whatever // utility method that maps this node with a transformer that could modify comparisons - e.g. change keys, values, operators, or whatever
// if there are no changes the same instance is returned // if there are no changes the same instance is returned
default Node map(final UnaryOperator<Comparison> mapper) { default Node transform(final UnaryOperator<Comparison> transformer) {
if (this instanceof Comparison comparison) { if (this instanceof Comparison comparison) {
return mapper.apply(comparison); return transformer.apply(comparison);
} else { } else {
final List<Node> mappedChildren = new ArrayList<>(); final List<Node> mappedChildren = new ArrayList<>();
boolean modified = false; boolean modified = false;
for (final Node child : ((Logical) this).getChildren()) { for (final Node child : ((Logical) this).getChildren()) {
final Node mapped = child.map(mapper); final Node mapped = child.transform(transformer);
mappedChildren.add(mapped); mappedChildren.add(mapped);
if (!mapped.equals(child)) { if (!mapped.equals(child)) {
modified = true; modified = true;

View File

@@ -20,21 +20,24 @@ import jakarta.persistence.criteria.CriteriaQuery;
import lombok.AccessLevel; import lombok.AccessLevel;
import lombok.Getter; import lombok.Getter;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
import lombok.NonNull;
import lombok.Setter; import lombok.Setter;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.text.StrLookup; import org.apache.commons.lang3.text.StrLookup;
import org.eclipse.hawkbit.repository.ActionFields;
import org.eclipse.hawkbit.repository.QueryField; import org.eclipse.hawkbit.repository.QueryField;
import org.eclipse.hawkbit.repository.exception.QueryException; import org.eclipse.hawkbit.repository.exception.QueryException;
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.Comparison; import org.eclipse.hawkbit.repository.jpa.ql.Node.Comparison;
import org.eclipse.hawkbit.repository.jpa.rsql.RsqlParser;
import org.eclipse.hawkbit.repository.jpa.rsql.legacy.SpecificationBuilderLegacy; import org.eclipse.hawkbit.repository.jpa.rsql.legacy.SpecificationBuilderLegacy;
import org.eclipse.hawkbit.repository.rsql.VirtualPropertyResolver; import org.eclipse.hawkbit.repository.rsql.VirtualPropertyResolver;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.orm.jpa.JpaProperties; import org.springframework.boot.autoconfigure.orm.jpa.JpaProperties;
import org.springframework.context.ApplicationListener;
import org.springframework.context.event.ContextRefreshedEvent;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.data.jpa.domain.Specification; import org.springframework.data.jpa.domain.Specification;
import org.springframework.orm.jpa.vendor.Database; import org.springframework.orm.jpa.vendor.Database;
@@ -75,7 +78,7 @@ import org.springframework.orm.jpa.vendor.Database;
@Getter @Getter
@NoArgsConstructor(access = AccessLevel.PRIVATE) @NoArgsConstructor(access = AccessLevel.PRIVATE)
@SuppressWarnings("java:S6548") // singleton holder ensures static access to spring resources in some places @SuppressWarnings("java:S6548") // singleton holder ensures static access to spring resources in some places
public class QLSupport { public class QLSupport implements ApplicationListener<ContextRefreshedEvent> {
private static final QLSupport SINGLETON = new QLSupport(); private static final QLSupport SINGLETON = new QLSupport();
@@ -110,6 +113,7 @@ public class QLSupport {
@SuppressWarnings({ "rawtypes", "unchecked" }) @SuppressWarnings({ "rawtypes", "unchecked" })
private QueryParser parser; private QueryParser parser;
private List<NodeTransformer> nodeTransformers;
private Database database; private Database database;
private EntityManager entityManager; private EntityManager entityManager;
private VirtualPropertyResolver virtualPropertyResolver; private VirtualPropertyResolver virtualPropertyResolver;
@@ -126,6 +130,15 @@ public class QLSupport {
this.parser = parser; this.parser = parser;
} }
@Override
public void onApplicationEvent(@NonNull final ContextRefreshedEvent event) {
nodeTransformers = event.getApplicationContext().getBeansOfType(NodeTransformer.class).values().stream().sorted((b1, b2) -> {
final Order o1 = b1.getClass().getAnnotation(Order.class);
final Order o2 = b2.getClass().getAnnotation(Order.class);
return Integer.compare(o1 != null ? o1.value() : Ordered.LOWEST_PRECEDENCE, o2 != null ? o2.value() : Ordered.LOWEST_PRECEDENCE);
}).toList();
}
@Autowired @Autowired
void setDatabase(final JpaProperties jpaProperties) { void setDatabase(final JpaProperties jpaProperties) {
database = jpaProperties.getDatabase(); database = jpaProperties.getDatabase();
@@ -155,7 +168,7 @@ public class QLSupport {
public <A extends Enum<A> & QueryField, T> Specification<T> buildSpec(final String query, final Class<A> queryFieldType) { public <A extends Enum<A> & QueryField, T> Specification<T> buildSpec(final String query, final Class<A> queryFieldType) {
if (specBuilder == SpecBuilder.G3) { if (specBuilder == SpecBuilder.G3) {
return new SpecificationBuilder<T>(!caseInsensitiveDB && ignoreCase, database) return new SpecificationBuilder<T>(!caseInsensitiveDB && ignoreCase, database)
.specification(parser.parse(caseInsensitiveDB || ignoreCase ? query.toLowerCase() : query, queryFieldType)); .specification(parseAndTransform(query, queryFieldType, caseInsensitiveDB || ignoreCase));
} else { } else {
return new SpecificationBuilderLegacy<A, T>(queryFieldType, virtualPropertyResolver, database).specification(query); return new SpecificationBuilderLegacy<A, T>(queryFieldType, virtualPropertyResolver, database).specification(query);
} }
@@ -164,7 +177,7 @@ public class QLSupport {
@SuppressWarnings({ "java:S1117" }) // it is again ignoreCase @SuppressWarnings({ "java:S1117" }) // it is again ignoreCase
public <A extends Enum<A> & QueryField> EntityMatcher entityMatcher(final String query, final Class<A> queryFieldType) { public <A extends Enum<A> & QueryField> EntityMatcher entityMatcher(final String query, final Class<A> queryFieldType) {
final boolean ignoreCase = caseInsensitiveDB || this.ignoreCase; // sync with DB and case sensitivity requirements final boolean ignoreCase = caseInsensitiveDB || this.ignoreCase; // sync with DB and case sensitivity requirements
return EntityMatcher.of(parser.parse(ignoreCase ? query.toLowerCase() : query, queryFieldType), ignoreCase); return EntityMatcher.of(parseAndTransform(query, queryFieldType, ignoreCase), ignoreCase);
} }
/** /**
@@ -182,69 +195,82 @@ public class QLSupport {
buildSpec(query, queryFieldType).toPredicate(criteriaQuery.from((Class) jpaType), criteriaQuery, criteriaBuilder); buildSpec(query, queryFieldType).toPredicate(criteriaQuery.from((Class) jpaType), criteriaQuery, criteriaBuilder);
} }
private <A extends Enum<A> & QueryField> Node parseAndTransform(
final String query, final Class<A> queryFieldType, final boolean ignoreCase) {
Node node = parser.parse(ignoreCase ? query.toLowerCase() : query, queryFieldType);
for (final NodeTransformer transformer : nodeTransformers) {
node = transformer.transform(node, queryFieldType);
}
return node;
}
/**
* By registering a custom {@link QueryParser} (as a {@link org.springframework.context.annotation.Bean}) the entire parsing of the queries
* could be replaced / customized, e.g. the default query language (RSQL) could be replaced with a custom.
*/
public interface QueryParser { public interface QueryParser {
<T extends Enum<T> & QueryField> Node parse(final String query, final Class<T> queryFieldType) throws QueryException; <T extends Enum<T> & QueryField> Node parse(final String query, final Class<T> queryFieldType) throws QueryException;
} }
public static class DefaultQueryParser implements QueryParser { /**
* By registering a custom {@link NodeTransformer} (as a {@link org.springframework.context.annotation.Bean}) the nodes could be
* modified after parsing, e.g. to add implicit nodes or to modify values.
* <p/>
* By default, all transformers are with {@link Ordered#LOWEST_PRECEDENCE} order. So, if you need a specific order use the {@link Order}
* annotation of their class (not on the bean registering methods).
*/
public interface NodeTransformer {
@Override <T extends Enum<T> & QueryField> Node transform(Node node, final Class<T> queryFieldType);
public <T extends Enum<T> & QueryField> Node parse(final String query, final Class<T> queryFieldType) throws QueryException {
return RsqlParser.parse(query, queryFieldType).map(comparison -> map(comparison, queryFieldType));
}
protected <T extends Enum<T> & QueryField> Comparison map(final Comparison comparison, final Class<T> queryFieldType) { /**
final String key = mapKey(comparison.getKey(), comparison, queryFieldType).toString(); * Base implementation that does no real transformation but allows extenders to easily modify keys and / or values by simply extending
final Object value = mapValue(comparison.getValue(), comparison, queryFieldType); * the extension points.
return key.equals(comparison.getKey()) && Objects.equals(value, comparison.getValue()) */
? comparison : Comparison.builder().key(key).op(comparison.getOp()).value(value).build(); abstract class Abstract implements NodeTransformer {
}
// just extension points for subclasses public <T extends Enum<T> & QueryField> Node transform(final Node node, final Class<T> queryFieldType) {
protected <T extends Enum<T> & QueryField> Object mapKey(final String key, final Comparison comparison, final Class<T> queryFieldType) { return node.transform(comparison -> transform(comparison, queryFieldType));
return key;
}
// internal, override only if you really want to replace whole lists
protected <T extends Enum<T> & QueryField> Object mapValue(
final Object value, final Comparison comparison, final Class<T> queryFieldType) {
if (value instanceof List<?> list) {
final List<Object> mappedList = new ArrayList<>();
boolean modified = false;
for (final Object e : list) {
final Object mapped = mapSimpleValue(e, comparison, queryFieldType);
if (!Objects.equals(mapped, value)) {
modified = true;
}
mappedList.add(mapped);
}
return modified ? mappedList : list;
} else {
return mapSimpleValue(value, comparison, queryFieldType);
} }
}
// just extension points for subclasses protected <T extends Enum<T> & QueryField> Comparison transform(final Comparison comparison, final Class<T> queryFieldType) {
protected <T extends Enum<T> & QueryField> Object mapSimpleValue( final String key = transformKey(comparison.getKey(), comparison, queryFieldType).toString();
final Object value, final Comparison comparison, final Class<T> queryFieldType) { final Object value = transformValue(comparison.getValue(), comparison, queryFieldType);
return queryFieldType == (Class<?>) ActionFields.class && "active".equalsIgnoreCase(comparison.getKey()) return key.equals(comparison.getKey()) && Objects.equals(value, comparison.getValue())
? mapActionStatus(value) ? comparison : Comparison.builder().key(key).op(comparison.getOp()).value(value).build();
: value; }
}
private static Object mapActionStatus(final Object value) { // just extension points for subclasses
final String strValue = String.valueOf(value); protected <T extends Enum<T> & QueryField> Object transformKey(
if ("true".equalsIgnoreCase(strValue) || "false".equalsIgnoreCase(strValue)) { final String key, final Comparison comparison, final Class<T> queryFieldType) {
return value; return key;
} else { }
// handle custom action fields status
try { // internal, override only if you really want to replace whole lists
return ActionFields.convertStatusValue(strValue); protected <T extends Enum<T> & QueryField> Object transformValue(
} catch (final IllegalArgumentException e) { final Object value, final Comparison comparison, final Class<T> queryFieldType) {
throw new RSQLParameterUnsupportedFieldException(e.getMessage()); if (value instanceof List<?> list) {
final List<Object> mappedList = new ArrayList<>();
boolean modified = false;
for (final Object e : list) {
final Object mapped = transformValueElement(e, comparison, queryFieldType);
if (!Objects.equals(mapped, value)) {
modified = true;
}
mappedList.add(mapped);
}
return modified ? mappedList : list;
} else {
return transformValueElement(value, comparison, queryFieldType);
} }
} }
// just extension points for subclasses
protected <T extends Enum<T> & QueryField> Object transformValueElement(
final Object value, final Comparison comparison, final Class<T> queryFieldType) {
return value;
}
} }
} }
} }

View File

@@ -23,6 +23,7 @@ import org.eclipse.hawkbit.ContextAware;
import org.eclipse.hawkbit.artifact.encryption.ArtifactEncryption; import org.eclipse.hawkbit.artifact.encryption.ArtifactEncryption;
import org.eclipse.hawkbit.artifact.encryption.ArtifactEncryptionSecretsStorage; import org.eclipse.hawkbit.artifact.encryption.ArtifactEncryptionSecretsStorage;
import org.eclipse.hawkbit.artifact.encryption.ArtifactEncryptionService; import org.eclipse.hawkbit.artifact.encryption.ArtifactEncryptionService;
import org.eclipse.hawkbit.repository.ActionFields;
import org.eclipse.hawkbit.repository.DeploymentManagement; import org.eclipse.hawkbit.repository.DeploymentManagement;
import org.eclipse.hawkbit.repository.PropertiesQuotaManagement; import org.eclipse.hawkbit.repository.PropertiesQuotaManagement;
import org.eclipse.hawkbit.repository.QueryField; import org.eclipse.hawkbit.repository.QueryField;
@@ -45,6 +46,7 @@ import org.eclipse.hawkbit.repository.event.ApplicationEventFilter;
import org.eclipse.hawkbit.repository.event.remote.EventEntityManager; import org.eclipse.hawkbit.repository.event.remote.EventEntityManager;
import org.eclipse.hawkbit.repository.event.remote.EventEntityManagerHolder; import org.eclipse.hawkbit.repository.event.remote.EventEntityManagerHolder;
import org.eclipse.hawkbit.repository.event.remote.TargetPollEvent; import org.eclipse.hawkbit.repository.event.remote.TargetPollEvent;
import org.eclipse.hawkbit.repository.exception.RSQLParameterUnsupportedFieldException;
import org.eclipse.hawkbit.repository.jpa.acm.AccessController; import org.eclipse.hawkbit.repository.jpa.acm.AccessController;
import org.eclipse.hawkbit.repository.jpa.aspects.ExceptionMappingAspectHandler; import org.eclipse.hawkbit.repository.jpa.aspects.ExceptionMappingAspectHandler;
import org.eclipse.hawkbit.repository.jpa.autoassign.AutoAssignChecker; import org.eclipse.hawkbit.repository.jpa.autoassign.AutoAssignChecker;
@@ -69,12 +71,13 @@ import org.eclipse.hawkbit.repository.jpa.model.helper.AfterTransactionCommitExe
import org.eclipse.hawkbit.repository.jpa.model.helper.EntityInterceptorHolder; import org.eclipse.hawkbit.repository.jpa.model.helper.EntityInterceptorHolder;
import org.eclipse.hawkbit.repository.jpa.model.helper.TenantAwareHolder; import org.eclipse.hawkbit.repository.jpa.model.helper.TenantAwareHolder;
import org.eclipse.hawkbit.repository.jpa.ql.Node.Comparison; import org.eclipse.hawkbit.repository.jpa.ql.Node.Comparison;
import org.eclipse.hawkbit.repository.jpa.ql.QLSupport.DefaultQueryParser; import org.eclipse.hawkbit.repository.jpa.ql.QLSupport;
import org.eclipse.hawkbit.repository.jpa.ql.QLSupport.NodeTransformer;
import org.eclipse.hawkbit.repository.jpa.ql.QLSupport.QueryParser; import org.eclipse.hawkbit.repository.jpa.ql.QLSupport.QueryParser;
import org.eclipse.hawkbit.repository.jpa.repository.ActionRepository; import org.eclipse.hawkbit.repository.jpa.repository.ActionRepository;
import org.eclipse.hawkbit.repository.jpa.repository.ArtifactRepository;
import org.eclipse.hawkbit.repository.jpa.repository.DistributionSetRepository; import org.eclipse.hawkbit.repository.jpa.repository.DistributionSetRepository;
import org.eclipse.hawkbit.repository.jpa.repository.DistributionSetTypeRepository; import org.eclipse.hawkbit.repository.jpa.repository.DistributionSetTypeRepository;
import org.eclipse.hawkbit.repository.jpa.repository.ArtifactRepository;
import org.eclipse.hawkbit.repository.jpa.repository.RolloutGroupRepository; import org.eclipse.hawkbit.repository.jpa.repository.RolloutGroupRepository;
import org.eclipse.hawkbit.repository.jpa.repository.RolloutRepository; import org.eclipse.hawkbit.repository.jpa.repository.RolloutRepository;
import org.eclipse.hawkbit.repository.jpa.repository.RolloutTargetGroupRepository; import org.eclipse.hawkbit.repository.jpa.repository.RolloutTargetGroupRepository;
@@ -90,7 +93,7 @@ import org.eclipse.hawkbit.repository.jpa.rollout.condition.RolloutGroupEvaluati
import org.eclipse.hawkbit.repository.jpa.rollout.condition.StartNextGroupRolloutGroupSuccessAction; import org.eclipse.hawkbit.repository.jpa.rollout.condition.StartNextGroupRolloutGroupSuccessAction;
import org.eclipse.hawkbit.repository.jpa.rollout.condition.ThresholdRolloutGroupErrorCondition; import org.eclipse.hawkbit.repository.jpa.rollout.condition.ThresholdRolloutGroupErrorCondition;
import org.eclipse.hawkbit.repository.jpa.rollout.condition.ThresholdRolloutGroupSuccessCondition; import org.eclipse.hawkbit.repository.jpa.rollout.condition.ThresholdRolloutGroupSuccessCondition;
import org.eclipse.hawkbit.repository.jpa.ql.QLSupport; import org.eclipse.hawkbit.repository.jpa.rsql.RsqlParser;
import org.eclipse.hawkbit.repository.jpa.utils.ExceptionMapper; import org.eclipse.hawkbit.repository.jpa.utils.ExceptionMapper;
import org.eclipse.hawkbit.repository.model.RolloutGroup; import org.eclipse.hawkbit.repository.model.RolloutGroup;
import org.eclipse.hawkbit.repository.model.SoftwareModule; import org.eclipse.hawkbit.repository.model.SoftwareModule;
@@ -109,6 +112,7 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.beans.factory.config.BeanPostProcessor; import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.boot.autoconfigure.AutoConfigureAfter; import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.domain.EntityScan; import org.springframework.boot.autoconfigure.domain.EntityScan;
@@ -525,16 +529,54 @@ public class JpaRepositoryConfiguration {
} }
@Bean @Bean
@ConditionalOnMissingBean @ConditionalOnBean(VirtualPropertyResolver.class)
QueryParser queryParser(final Optional<VirtualPropertyResolver> virtualPropertyResolver) { public NodeTransformer virtualPropertyReplacerTransformer(final VirtualPropertyResolver resolver) {
return virtualPropertyResolver.<QueryParser>map(resolver -> new DefaultQueryParser() { return new NodeTransformer.Abstract() {
@Override @Override
protected <T extends Enum<T> & QueryField> Object mapValue( protected <T extends Enum<T> & QueryField> Object transformValueElement(
final Object value, final Comparison comparison, final Class<T> queryFieldType) { final Object value, final Comparison comparison, final Class<T> queryFieldType) {
return super.mapValue(value instanceof String strValue ? resolver.replace(strValue) : value, comparison, queryFieldType); return value instanceof String strValue ? resolver.replace(strValue) : value;
} }
}).orElseGet(DefaultQueryParser::new); };
}
/**
* @deprecated since 0.10.0, will be removed in future releases. Use "active" for querying active status instead of "status".
*/
@Deprecated(since = "0.10.0", forRemoval = true)
@Bean
public NodeTransformer actionStatusTransformer() {
return new NodeTransformer.Abstract() {
// just extension points for subclasses
protected <T extends Enum<T> & QueryField> Object transformValueElement(
final Object value, final Comparison comparison, final Class<T> queryFieldType) {
return queryFieldType == (Class<?>) ActionFields.class && "active".equalsIgnoreCase(comparison.getKey())
? mapActionStatus(value)
: value;
}
private static Object mapActionStatus(final Object value) {
final String strValue = String.valueOf(value);
if ("true".equalsIgnoreCase(strValue) || "false".equalsIgnoreCase(strValue)) {
return value;
} else {
// handle custom action fields status
try {
return ActionFields.convertStatusValue(strValue);
} catch (final IllegalArgumentException e) {
throw new RSQLParameterUnsupportedFieldException(e.getMessage());
}
}
}
};
}
@Bean
@ConditionalOnMissingBean
QueryParser queryParser() {
return RsqlParser::parse;
} }
/** /**