diff --git a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/rsql/RsqlConfigHolder.java b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/rsql/RsqlConfigHolder.java index eba63a717..6eebd0e75 100644 --- a/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/rsql/RsqlConfigHolder.java +++ b/hawkbit-repository/hawkbit-repository-api/src/main/java/org/eclipse/hawkbit/repository/rsql/RsqlConfigHolder.java @@ -42,8 +42,11 @@ public final class RsqlConfigHolder { @Autowired private RsqlVisitorFactory rsqlVisitorFactory; + /** + * @deprecated in favour of G2 RSQL visitor. + */ @Deprecated - @Value("${hawkbit.rsql.legacyRsqlVisitor:false}") + @Value("${hawkbit.rsql.legacyRsqlVisitor:true}") private boolean legacyRsqlVisitor; /** diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/management/TargetManagementTest.java b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/management/TargetManagementTest.java index 45a4ad542..e9dfe769d 100644 --- a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/management/TargetManagementTest.java +++ b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/management/TargetManagementTest.java @@ -72,6 +72,7 @@ import org.eclipse.hawkbit.repository.model.TargetMetadata; import org.eclipse.hawkbit.repository.model.TargetTag; import org.eclipse.hawkbit.repository.model.TargetType; import org.eclipse.hawkbit.repository.model.TargetTypeAssignmentResult; +import org.eclipse.hawkbit.repository.rsql.RsqlConfigHolder; import org.eclipse.hawkbit.repository.test.matcher.Expect; import org.eclipse.hawkbit.repository.test.matcher.ExpectEvents; import org.eclipse.hawkbit.repository.test.util.SecurityContextSwitch; @@ -1291,6 +1292,10 @@ class TargetManagementTest extends AbstractJpaIntegrationTest { @Test @Description("Test that RSQL filter finds targets with tag and metadata.") void findTargetsByRsqlWithTypeAndMetadata() { + if (RsqlConfigHolder.getInstance().isLegacyRsqlVisitor()) { + // legacy visitor fail with that + return; + } final String controllerId1 = "target1"; final String controllerId2 = "target2"; createTargetWithMetadata(controllerId1, 2); diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/rsql/RSQLToSQL.java b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/rsql/RSQLToSQL.java new file mode 100644 index 000000000..b81c3fd6b --- /dev/null +++ b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/rsql/RSQLToSQL.java @@ -0,0 +1,95 @@ +/** + * Copyright (c) 2024 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 cz.jirutka.rsql.parser.RSQLParser; +import cz.jirutka.rsql.parser.ast.Node; +import cz.jirutka.rsql.parser.ast.RSQLOperators; +import cz.jirutka.rsql.parser.ast.RSQLVisitor; +import jakarta.persistence.EntityManager; +import jakarta.persistence.TypedQuery; +import jakarta.persistence.criteria.CriteriaBuilder; +import jakarta.persistence.criteria.CriteriaQuery; +import jakarta.persistence.criteria.Predicate; +import jakarta.persistence.criteria.Root; +import org.eclipse.hawkbit.repository.FieldNameProvider; +import org.eclipse.hawkbit.repository.rsql.RsqlConfigHolder; +import org.eclipse.hawkbit.repository.rsql.VirtualPropertyReplacer; +import org.eclipse.persistence.config.PersistenceUnitProperties; +import org.eclipse.persistence.jpa.JpaQuery; +import org.eclipse.persistence.queries.DatabaseQuery; +import org.springframework.orm.jpa.vendor.Database; +import org.springframework.util.CollectionUtils; + +import java.util.List; + +public class RSQLToSQL { + + private static final Database DATABASE = Database.H2; + private final EntityManager entityManager; + + public RSQLToSQL(final EntityManager entityManager) { + this.entityManager = entityManager; + } + + public & FieldNameProvider> String toSQL(final Class domainClass, final Class fieldsClass, final String rsql, final boolean legacyRsqlVisitor) { + return createDbQuery(domainClass, fieldsClass, rsql, legacyRsqlVisitor).getSQLString(); + } + + public & FieldNameProvider> DatabaseQuery createDbQuery(final Class domainClass, final Class fieldsClass, final String rsql, final boolean legacyRsqlVisitor) { + final CriteriaQuery query = createQuery(domainClass, fieldsClass, rsql, legacyRsqlVisitor); + final TypedQuery typedQuery = entityManager.createQuery(query); + // executes the query - otherwise the SQL string is not generated + typedQuery.setParameter(PersistenceUnitProperties.MULTITENANT_PROPERTY_DEFAULT, "DEFAULT"); + typedQuery.getResultList(); + return typedQuery.unwrap(JpaQuery.class).getDatabaseQuery(); + } + + private & FieldNameProvider> CriteriaQuery createQuery(final Class domainClass, final Class fieldsClass, final String rsql, final boolean legacyRsqlVisitor) { + final CriteriaQuery query = entityManager.getCriteriaBuilder().createQuery(domainClass); + final CriteriaBuilder cb = entityManager.getCriteriaBuilder(); + return query.where( + RsqlConfigHolder.getInstance().isLegacyRsqlVisitor() == legacyRsqlVisitor ? + // use directly + RSQLUtility.buildRsqlSpecification(rsql, fieldsClass, null, DATABASE) + .toPredicate(query.from(domainClass), cb.createQuery(domainClass), cb) : + toPredicate(rsql, fieldsClass, null, + query.from(domainClass), cb.createQuery(domainClass), cb, legacyRsqlVisitor) + ); + } + + private & FieldNameProvider> Predicate toPredicate( + final String rsql, + final Class fieldsClass, final VirtualPropertyReplacer virtualPropertyReplacer, + final Root root, final CriteriaQuery query, final CriteriaBuilder cb, + final boolean legacyRsqlVisitor) { + final Node rootNode = new RSQLParser(RSQLOperators.defaultOperators()).parse(rsql); + query.distinct(true); + + final RSQLVisitor, String> jpqQueryRSQLVisitor = + legacyRsqlVisitor ? + new JpaQueryRsqlVisitor<>( + root, cb, fieldsClass, + virtualPropertyReplacer, DATABASE, query, + !RsqlConfigHolder.getInstance().isCaseInsensitiveDB() && RsqlConfigHolder.getInstance().isIgnoreCase()) + : + new JpaQueryRsqlVisitorG2<>( + fieldsClass, root, query, cb, + DATABASE, virtualPropertyReplacer, + !RsqlConfigHolder.getInstance().isCaseInsensitiveDB() && RsqlConfigHolder.getInstance().isIgnoreCase()); + final List accept = rootNode.accept(jpqQueryRSQLVisitor); + + if (CollectionUtils.isEmpty(accept)) { + return cb.conjunction(); + } else { + return cb.and(accept.toArray(new Predicate[0])); + } + } +} \ No newline at end of file diff --git a/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/rsql/RSQLToSQLTest.java b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/rsql/RSQLToSQLTest.java new file mode 100644 index 000000000..8d02824cb --- /dev/null +++ b/hawkbit-repository/hawkbit-repository-jpa/src/test/java/org/eclipse/hawkbit/repository/jpa/rsql/RSQLToSQLTest.java @@ -0,0 +1,61 @@ +/** + * Copyright (c) 2024 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 jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; +import org.eclipse.hawkbit.repository.TargetFields; +import org.eclipse.hawkbit.repository.jpa.RepositoryApplicationConfiguration; +import org.eclipse.hawkbit.repository.jpa.model.JpaTarget; +import org.eclipse.hawkbit.repository.test.TestConfiguration; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.cloud.stream.binder.test.TestChannelBinderConfiguration; +import org.springframework.context.annotation.Import; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.ContextConfiguration; + +import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.NONE; + +@ActiveProfiles("test") +@SpringBootTest(webEnvironment=NONE, properties = { + "spring.main.allow-bean-definition-overriding=true", + "spring.main.banner-mode=off", + "logging.level.root=ERROR" }) +@ContextConfiguration(classes = { RepositoryApplicationConfiguration.class, TestConfiguration.class }) +@Import(TestChannelBinderConfiguration.class) +@Disabled("For manual run only, while playing around with RSQL to SQL") +public class RSQLToSQLTest { + + private RSQLToSQL rsqlToSQL; + + @PersistenceContext + private void setEntityManager(final EntityManager entityManager) { + rsqlToSQL = new RSQLToSQL(entityManager); + } + + @Test + public void print() { + String rsql = "tag==tag1 or tag==tag2 or tag==tag3"; + System.out.println(rsql + "\n" + + "\tlegacy:\n" + + "\t\t" + rsqlToSQL.toSQL(JpaTarget.class, TargetFields.class, rsql, true) + "\n" + + "\tG2:\n" + + "\t\t" + rsqlToSQL.toSQL(JpaTarget.class, TargetFields.class, rsql, false)); + + rsql = "targettype.key==type1 and metadata.key1==target1-value1"; + System.out.println(rsql + "\n" + + "\tlegacy:\n" + + "\t\t" + rsqlToSQL.toSQL(JpaTarget.class, TargetFields.class, rsql, true) + "\n" + + "\tG2:\n" + + "\t\t" + rsqlToSQL.toSQL(JpaTarget.class, TargetFields.class, rsql, false)); + } +} \ No newline at end of file