Refactor RSQL Utility - make it singleton and injectable (#2536)

Signed-off-by: Avgustin Marinov <Avgustin.Marinov@bosch.com>
This commit is contained in:
Avgustin Marinov
2025-07-07 17:16:26 +03:00
committed by GitHub
parent 7f97d6f441
commit ea06783fa2
29 changed files with 231 additions and 893 deletions

View File

@@ -19,13 +19,10 @@ import jakarta.persistence.criteria.CriteriaQuery;
import org.eclipse.hawkbit.repository.RsqlQueryField;
import org.eclipse.hawkbit.repository.jpa.rsql.RsqlUtility;
import org.eclipse.hawkbit.repository.jpa.rsql.RsqlConfigHolder;
import org.eclipse.hawkbit.repository.jpa.rsql.RsqlConfigHolder.RsqlToSpecBuilder;
import org.springframework.orm.jpa.vendor.Database;
import org.eclipse.hawkbit.repository.jpa.rsql.RsqlUtility.RsqlToSpecBuilder;
public class HawkbitQlToSql {
private static final Database DATABASE = Database.H2;
private final EntityManager entityManager;
private final boolean isEclipselink;
@@ -61,16 +58,16 @@ public class HawkbitQlToSql {
final Class<T> domainClass, final Class<A> fieldsClass, final String rsql, final RsqlToSpecBuilder rsqlToSpecBuilder) {
final CriteriaQuery<T> query = entityManager.getCriteriaBuilder().createQuery(domainClass);
final CriteriaBuilder cb = entityManager.getCriteriaBuilder();
final RsqlToSpecBuilder defaultRsqlToSpecBuilder = RsqlConfigHolder.getInstance().getRsqlToSpecBuilder();
final RsqlToSpecBuilder defaultRsqlToSpecBuilder = RsqlUtility.getInstance().getRsqlToSpecBuilder();
if (defaultRsqlToSpecBuilder != rsqlToSpecBuilder) {
RsqlConfigHolder.getInstance().setRsqlToSpecBuilder(rsqlToSpecBuilder);
RsqlUtility.getInstance().setRsqlToSpecBuilder(rsqlToSpecBuilder);
}
try {
return query.where(RsqlUtility.<A, T> buildRsqlSpecification(rsql, fieldsClass, null, DATABASE)
return query.where(RsqlUtility.getInstance().<A, T> buildRsqlSpecification(rsql, fieldsClass)
.toPredicate(query.from(domainClass), cb.createQuery(domainClass), cb));
} finally {
if (defaultRsqlToSpecBuilder != rsqlToSpecBuilder) {
RsqlConfigHolder.getInstance().setRsqlToSpecBuilder(defaultRsqlToSpecBuilder);
RsqlUtility.getInstance().setRsqlToSpecBuilder(defaultRsqlToSpecBuilder);
}
}
}

View File

@@ -1,61 +0,0 @@
/**
* 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;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.springframework.beans.factory.annotation.Value;
/**
* Helper class providing static access to the RSQL configuration as managed bean.
*/
@NoArgsConstructor(access = lombok.AccessLevel.PRIVATE)
@Getter
@SuppressWarnings("java:S6548") // singleton holder ensures static access to spring resources in some places
public final class RsqlConfigHolder {
private static final RsqlConfigHolder SINGLETON = new RsqlConfigHolder();
public enum RsqlToSpecBuilder {
LEGACY_G1, // legacy RSQL visitor
LEGACY_G2, // G2 RSQL visitor
G3 // G3 RSQL visitor - still experimental / yet default
}
/**
* If RSQL comparison operators shall ignore the case. If ignore case is <code>true</code> "x == ax" will match "x == aX"
*/
@Value("${hawkbit.rsql.ignore-case:true}")
private boolean ignoreCase;
/**
* Declares if the database is case-insensitive, by default assumes <code>false</code>. In case it is case-sensitive and,
* {@link #ignoreCase} is set to <code>true</code> the SQL queries use upper case comparisons to ignore case.
*
* If the database is declared as case-sensitive and ignoreCase is set to <code>false</code> the RSQL queries shall use strict
* syntax - i.e. 'and' instead of 'AND' / 'aND'. Otherwise, the queries would be case-insensitive regarding operators.
*/
@Value("${hawkbit.rsql.case-insensitive-db:false}")
private boolean caseInsensitiveDB;
/**
* @deprecated in favour fixed final visitor / spec builder of G2 RSQL visitor / G3 spec builder. since 0.6.0
*/
@Setter // for tests only
@Deprecated(forRemoval = true, since = "0.6.0")
@Value("${hawkbit.rsql.rsql-to-spec-builder:G3}") //
private RsqlToSpecBuilder rsqlToSpecBuilder;
/**
* @return The holder singleton instance.
*/
public static RsqlConfigHolder getInstance() {
return SINGLETON;
}
}

View File

@@ -9,15 +9,15 @@
*/
package org.eclipse.hawkbit.repository.jpa.rsql;
import static org.eclipse.hawkbit.repository.jpa.rsql.RsqlConfigHolder.RsqlToSpecBuilder.G3;
import jakarta.persistence.EntityManager;
import jakarta.persistence.criteria.CriteriaBuilder;
import jakarta.persistence.criteria.CriteriaQuery;
import cz.jirutka.rsql.parser.RSQLParserException;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.text.StrLookup;
import org.eclipse.hawkbit.repository.RsqlQueryField;
@@ -27,6 +27,9 @@ import org.eclipse.hawkbit.repository.jpa.ql.SpecificationBuilder;
import org.eclipse.hawkbit.repository.jpa.rsql.legacy.SpecificationBuilderLegacy;
import org.eclipse.hawkbit.repository.rsql.VirtualPropertyReplacer;
import org.eclipse.hawkbit.repository.rsql.VirtualPropertyResolver;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.orm.jpa.JpaProperties;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.orm.jpa.vendor.Database;
@@ -64,8 +67,66 @@ import org.springframework.orm.jpa.vendor.Database;
* $${OVERDUE_TS} would prevent the ${OVERDUE_TS} token from being expanded.
*/
@Slf4j
@Getter
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public final class RsqlUtility {
public class RsqlUtility {
private static final RsqlUtility SINGLETON = new RsqlUtility();
public enum RsqlToSpecBuilder {
LEGACY_G1, // legacy RSQL visitor
LEGACY_G2, // G2 RSQL visitor
G3 // G3 RSQL visitor - still experimental / yet default
}
/**
* If RSQL comparison operators shall ignore the case. If ignore case is <code>true</code> "x == ax" will match "x == aX"
*/
@Value("${hawkbit.rsql.ignore-case:true}")
private boolean ignoreCase;
/**
* Declares if the database is case-insensitive, by default assumes <code>false</code>. In case it is case-sensitive and,
* {@link #ignoreCase} is set to <code>true</code> the SQL queries use upper case comparisons to ignore case.
* <p/>
* If the database is declared as case-sensitive and ignoreCase is set to <code>false</code> the RSQL queries shall use strict
* syntax - i.e. 'and' instead of 'AND' / 'aND'. Otherwise, the queries would be case-insensitive regarding operators.
*/
@Value("${hawkbit.rsql.case-insensitive-db:false}")
private boolean caseInsensitiveDB;
/**
* @deprecated in favour fixed final visitor / spec builder of G2 RSQL visitor / G3 spec builder. since 0.6.0
*/
@Setter // for tests only
@Deprecated(forRemoval = true, since = "0.6.0")
@Value("${hawkbit.rsql.rsql-to-spec-builder:G3}") //
private RsqlToSpecBuilder rsqlToSpecBuilder;
private VirtualPropertyReplacer virtualPropertyReplacer;
private Database database;
private EntityManager entityManager;
/**
* @return The holder singleton instance.
*/
public static RsqlUtility getInstance() {
return SINGLETON;
}
@Autowired(required = false)
void setVirtualPropertyReplacer(final VirtualPropertyReplacer virtualPropertyReplacer) {
this.virtualPropertyReplacer = virtualPropertyReplacer;
}
@Autowired
void setDatabase(final JpaProperties jpaProperties) {
database = jpaProperties.getDatabase();
}
@Autowired
void setEntityManager(final EntityManager entityManager) {
this.entityManager = entityManager;
}
/**
* Builds a JPA {@link Specification} which corresponds with the given RSQL query. The specification can be used to filter for JPA entities
@@ -73,25 +134,16 @@ public final class RsqlUtility {
*
* @param rsql the rsql query to be parsed
* @param rsqlQueryFieldType the enum class type which implements the {@link RsqlQueryField}
* @param virtualPropertyReplacer holds the logic how the known macros have to be resolved; may be <code>null</code>
* @param database database in use
* @return a specification which can be used with JPA
* @throws RSQLParameterUnsupportedFieldException if a field in the RSQL string is used but not provided by the
* given {@code fieldNameProvider}
* @throws RSQLParameterSyntaxException if the RSQL syntax is wrong
*/
public static <A extends Enum<A> & RsqlQueryField, T> Specification<T> buildRsqlSpecification(
final String rsql, final Class<A> rsqlQueryFieldType,
final VirtualPropertyReplacer virtualPropertyReplacer, final Database database) {
if (RsqlConfigHolder.getInstance().getRsqlToSpecBuilder() == G3) {
return new SpecificationBuilder<T>(
virtualPropertyReplacer,
!RsqlConfigHolder.getInstance().isCaseInsensitiveDB() && RsqlConfigHolder.getInstance().isIgnoreCase(),
database)
.specification(RsqlParser.parse(
RsqlConfigHolder.getInstance().isCaseInsensitiveDB() || RsqlConfigHolder.getInstance().isIgnoreCase()
? rsql.toLowerCase() : rsql,
rsqlQueryFieldType));
public <A extends Enum<A> & RsqlQueryField, T> Specification<T> buildRsqlSpecification(
final String rsql, final Class<A> rsqlQueryFieldType) {
if (rsqlToSpecBuilder == RsqlToSpecBuilder.G3) {
return new SpecificationBuilder<T>(virtualPropertyReplacer, !caseInsensitiveDB && ignoreCase, database)
.specification(RsqlParser.parse(caseInsensitiveDB || ignoreCase ? rsql.toLowerCase() : rsql, rsqlQueryFieldType));
} else {
return new SpecificationBuilderLegacy<A, T>(rsqlQueryFieldType, virtualPropertyReplacer, database).specification(rsql);
}
@@ -101,18 +153,16 @@ public final class RsqlUtility {
* Validates the RSQL string
*
* @param rsql RSQL string to validate
* @param rsqlQueryFieldType
* @param rsqlQueryFieldType the enum class type which implements the {@link RsqlQueryField}
* @param jpaType the JPA entity type to validate against
* @throws RSQLParserException if RSQL syntax is invalid
* @throws RSQLParameterUnsupportedFieldException if RSQL key is not allowed
*/
@SuppressWarnings({ "unchecked", "rawtypes" })
public static <A extends Enum<A> & RsqlQueryField> void validateRsqlFor(
final String rsql, final Class<A> rsqlQueryFieldType,
final Class<?> jpaType,
final VirtualPropertyReplacer virtualPropertyReplacer, final EntityManager entityManager) {
public <A extends Enum<A> & RsqlQueryField> void validateRsqlFor(
final String rsql, final Class<A> rsqlQueryFieldType, final Class<?> jpaType) {
final CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
final CriteriaQuery<?> criteriaQuery = criteriaBuilder.createQuery(jpaType);
buildRsqlSpecification(rsql, rsqlQueryFieldType, virtualPropertyReplacer, null)
.toPredicate(criteriaQuery.from((Class) jpaType), criteriaQuery, criteriaBuilder);
buildRsqlSpecification(rsql, rsqlQueryFieldType).toPredicate(criteriaQuery.from((Class) jpaType), criteriaQuery, criteriaBuilder);
}
}

View File

@@ -10,7 +10,7 @@
package org.eclipse.hawkbit.repository.jpa.rsql.legacy;
import static org.eclipse.hawkbit.repository.jpa.rsql.legacy.AbstractRSQLVisitor.OPERATORS;
import static org.eclipse.hawkbit.repository.jpa.rsql.RsqlConfigHolder.RsqlToSpecBuilder.LEGACY_G1;
import static org.eclipse.hawkbit.repository.jpa.rsql.RsqlUtility.RsqlToSpecBuilder.LEGACY_G1;
import java.util.List;
@@ -24,7 +24,7 @@ import lombok.extern.slf4j.Slf4j;
import org.eclipse.hawkbit.repository.RsqlQueryField;
import org.eclipse.hawkbit.repository.exception.RSQLParameterSyntaxException;
import org.eclipse.hawkbit.repository.jpa.ql.SpecificationBuilder;
import org.eclipse.hawkbit.repository.jpa.rsql.RsqlConfigHolder;
import org.eclipse.hawkbit.repository.jpa.rsql.RsqlUtility;
import org.eclipse.hawkbit.repository.rsql.VirtualPropertyReplacer;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.orm.jpa.vendor.Database;
@@ -50,14 +50,14 @@ public class SpecificationBuilderLegacy<A extends Enum<A> & RsqlQueryField, T> {
public Specification<T> specification(final String rsql) {
return (root, query, cb) -> {
final RsqlConfigHolder rsqlConfigHolder = RsqlConfigHolder.getInstance();
final RsqlUtility rsqlUtility = RsqlUtility.getInstance();
final Node rootNode = parseRsql(rsql, rsqlConfigHolder);
final Node rootNode = parseRsql(rsql, rsqlUtility);
query.distinct(true);
final boolean ensureIgnoreCase = !rsqlConfigHolder.isCaseInsensitiveDB() && rsqlConfigHolder.isIgnoreCase();
final boolean ensureIgnoreCase = !rsqlUtility.isCaseInsensitiveDB() && rsqlUtility.isIgnoreCase();
final RSQLVisitor<List<Predicate>, String> jpqQueryRSQLVisitor =
rsqlConfigHolder.getRsqlToSpecBuilder() == LEGACY_G1
rsqlUtility.getRsqlToSpecBuilder() == LEGACY_G1
? new JpaQueryRsqlVisitor<>(root, cb, rsqlQueryFieldType, virtualPropertyReplacer, database, query, ensureIgnoreCase)
: new JpaQueryRsqlVisitorG2<>(rsqlQueryFieldType, root, query, cb, database, virtualPropertyReplacer, ensureIgnoreCase);
final List<Predicate> accept = rootNode.accept(jpqQueryRSQLVisitor);
@@ -70,10 +70,10 @@ public class SpecificationBuilderLegacy<A extends Enum<A> & RsqlQueryField, T> {
};
}
private static Node parseRsql(final String rsql, final RsqlConfigHolder rsqlConfigHolder) {
private static Node parseRsql(final String rsql, final RsqlUtility rsqlUtility) {
log.debug("Parsing rsql string {}", rsql);
try {
return new RSQLParser(OPERATORS).parse(rsqlConfigHolder.isCaseInsensitiveDB() || rsqlConfigHolder.isIgnoreCase() ? rsql.toLowerCase() : rsql);
return new RSQLParser(OPERATORS).parse(rsqlUtility.isCaseInsensitiveDB() || rsqlUtility.isIgnoreCase() ? rsql.toLowerCase() : rsql);
} catch (final IllegalArgumentException e) {
throw new RSQLParameterSyntaxException("RSQL filter must not be null", e);
} catch (final RSQLParserException e) {

View File

@@ -7,7 +7,7 @@
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.eclipse.hawkbit.repository.jpa.rsql.sa;
package org.eclipse.hawkbit.repository.jpa.ql;
import java.util.Map;
import java.util.Set;

View File

@@ -7,7 +7,7 @@
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.eclipse.hawkbit.repository.jpa.rsql.sa;
package org.eclipse.hawkbit.repository.jpa.ql;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
import org.springframework.data.repository.CrudRepository;

View File

@@ -7,10 +7,10 @@
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.eclipse.hawkbit.repository.jpa.rsql.sa;
package org.eclipse.hawkbit.repository.jpa.ql;
import static org.eclipse.hawkbit.repository.jpa.rsql.RsqlConfigHolder.RsqlToSpecBuilder.LEGACY_G1;
import static org.eclipse.hawkbit.repository.jpa.rsql.RsqlConfigHolder.RsqlToSpecBuilder.LEGACY_G2;
import static org.eclipse.hawkbit.repository.jpa.rsql.RsqlUtility.RsqlToSpecBuilder.LEGACY_G1;
import static org.eclipse.hawkbit.repository.jpa.rsql.RsqlUtility.RsqlToSpecBuilder.LEGACY_G2;
import java.util.Arrays;
import java.util.List;
@@ -18,9 +18,9 @@ import java.util.List;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.hawkbit.repository.RsqlQueryField;
import org.eclipse.hawkbit.repository.jpa.rsql.RsqlUtility;
import org.eclipse.hawkbit.repository.jpa.rsql.RsqlUtility.RsqlToSpecBuilder;
import org.eclipse.hawkbit.repository.jpa.rsql.legacy.SpecificationBuilderLegacy;
import org.eclipse.hawkbit.repository.jpa.rsql.RsqlConfigHolder;
import org.eclipse.hawkbit.repository.jpa.rsql.RsqlConfigHolder.RsqlToSpecBuilder;
import org.junit.jupiter.api.Test;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.orm.jpa.vendor.Database;
@@ -32,12 +32,12 @@ class SpecificationBuilderLegacyTest extends SpecificationBuilderTest {
private final SpecificationBuilderLegacy<RootField, Root> builder = new SpecificationBuilderLegacy<>(RootField.class, null, Database.H2);
private static void runWithRsqlToSpecBuilder(final Runnable runnable, final RsqlToSpecBuilder rsqlToSpecBuilder) {
final RsqlToSpecBuilder defaultBuilder = RsqlConfigHolder.getInstance().getRsqlToSpecBuilder();
RsqlConfigHolder.getInstance().setRsqlToSpecBuilder(rsqlToSpecBuilder);
final RsqlToSpecBuilder defaultBuilder = RsqlUtility.getInstance().getRsqlToSpecBuilder();
RsqlUtility.getInstance().setRsqlToSpecBuilder(rsqlToSpecBuilder);
try {
runnable.run();
} finally {
RsqlConfigHolder.getInstance().setRsqlToSpecBuilder(defaultBuilder);
RsqlUtility.getInstance().setRsqlToSpecBuilder(defaultBuilder);
}
}

View File

@@ -7,12 +7,12 @@
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.eclipse.hawkbit.repository.jpa.rsql.sa;
package org.eclipse.hawkbit.repository.jpa.ql;
import static org.assertj.core.api.Assertions.assertThat;
import static org.eclipse.hawkbit.repository.jpa.rsql.RsqlConfigHolder.RsqlToSpecBuilder.G3;
import static org.eclipse.hawkbit.repository.jpa.rsql.RsqlConfigHolder.RsqlToSpecBuilder.LEGACY_G1;
import static org.eclipse.hawkbit.repository.jpa.rsql.RsqlConfigHolder.RsqlToSpecBuilder.LEGACY_G2;
import static org.eclipse.hawkbit.repository.jpa.rsql.RsqlUtility.RsqlToSpecBuilder.G3;
import static org.eclipse.hawkbit.repository.jpa.rsql.RsqlUtility.RsqlToSpecBuilder.LEGACY_G1;
import static org.eclipse.hawkbit.repository.jpa.rsql.RsqlUtility.RsqlToSpecBuilder.LEGACY_G2;
import java.util.List;
import java.util.Map;
@@ -22,11 +22,9 @@ import java.util.stream.StreamSupport;
import jakarta.persistence.EntityManager;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.hawkbit.repository.jpa.ql.EntityMatcher;
import org.eclipse.hawkbit.repository.jpa.ql.utils.HawkbitQlToSql;
import org.eclipse.hawkbit.repository.jpa.rsql.RsqlParser;
import org.eclipse.hawkbit.repository.jpa.ql.SpecificationBuilder;
import org.eclipse.hawkbit.repository.jpa.rsql.RsqlConfigHolder;
import org.eclipse.hawkbit.repository.jpa.rsql.RsqlUtility;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringBootConfiguration;
@@ -491,8 +489,8 @@ class SpecificationBuilderTest {
return builder.specification(RsqlParser.parse(rsql));
}
private static RsqlConfigHolder.RsqlToSpecBuilder getRsqlToSpecBuilder() {
return RsqlConfigHolder.getInstance().getRsqlToSpecBuilder();
private static RsqlUtility.RsqlToSpecBuilder getRsqlToSpecBuilder() {
return RsqlUtility.getInstance().getRsqlToSpecBuilder();
}
@SpringBootConfiguration

View File

@@ -7,16 +7,13 @@
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.eclipse.hawkbit.repository.jpa.rsql.sa;
package org.eclipse.hawkbit.repository.jpa.ql;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.OneToMany;
import jakarta.persistence.OneToOne;
import lombok.Data;
import lombok.experimental.Accessors;

View File

@@ -7,7 +7,7 @@
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.eclipse.hawkbit.repository.jpa.rsql.sa;
package org.eclipse.hawkbit.repository.jpa.ql;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
import org.springframework.data.repository.CrudRepository;

View File

@@ -7,7 +7,7 @@
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.eclipse.hawkbit.repository.jpa.rsql.sa;
package org.eclipse.hawkbit.repository.jpa.ql;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;

View File

@@ -7,7 +7,7 @@
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.eclipse.hawkbit.repository.jpa.rsql.sa;
package org.eclipse.hawkbit.repository.jpa.ql;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
import org.springframework.data.repository.CrudRepository;