Fix != and =out= for maps in G2 RSQL to Specification (#2426)
+ add initial draft of Standalone RSQL test + provide option to override Hibernate / Eclipselink configuration via standard spring environment properties Signed-off-by: Avgustin Marinov <Avgustin.Marinov@bosch.com>
This commit is contained in:
@@ -9,7 +9,7 @@
|
||||
*/
|
||||
package org.eclipse.hawkbit.repository.jpa;
|
||||
|
||||
import jakarta.persistence.TypedQuery;
|
||||
import jakarta.persistence.Query;
|
||||
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
@@ -18,12 +18,12 @@ import org.eclipse.persistence.jpa.JpaQuery;
|
||||
|
||||
@NoArgsConstructor(access = lombok.AccessLevel.PRIVATE)
|
||||
@Slf4j
|
||||
public class Utils {
|
||||
public class EclipselinkUtils {
|
||||
|
||||
public static String toSql(final TypedQuery<?> typedQuery) {
|
||||
typedQuery.setParameter(PersistenceUnitProperties.MULTITENANT_PROPERTY_DEFAULT, "DEFAULT");
|
||||
public static String toSql(final Query query) {
|
||||
query.setParameter(PersistenceUnitProperties.MULTITENANT_PROPERTY_DEFAULT, "DEFAULT");
|
||||
// executes the query - otherwise the SQL string is not generated
|
||||
typedQuery.getResultList();
|
||||
return typedQuery.unwrap(JpaQuery.class).getDatabaseQuery().getSQLString();
|
||||
query.getResultList();
|
||||
return query.unwrap(JpaQuery.class).getDatabaseQuery().getSQLString();
|
||||
}
|
||||
}
|
||||
@@ -14,14 +14,17 @@ import java.util.Map;
|
||||
|
||||
import javax.sql.DataSource;
|
||||
|
||||
import lombok.Data;
|
||||
import org.eclipse.hawkbit.tenancy.TenantAware;
|
||||
import org.eclipse.persistence.config.PersistenceUnitProperties;
|
||||
import org.springframework.beans.factory.ObjectProvider;
|
||||
import org.springframework.boot.autoconfigure.orm.jpa.JpaBaseConfiguration;
|
||||
import org.springframework.boot.autoconfigure.orm.jpa.JpaProperties;
|
||||
import org.springframework.boot.autoconfigure.transaction.TransactionManagerCustomizers;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.orm.jpa.vendor.AbstractJpaVendorAdapter;
|
||||
import org.springframework.orm.jpa.vendor.EclipseLinkJpaDialect;
|
||||
import org.springframework.orm.jpa.vendor.EclipseLinkJpaVendorAdapter;
|
||||
@@ -32,16 +35,29 @@ import org.springframework.transaction.jta.JtaTransactionManager;
|
||||
* General EclipseLink configuration for hawkBit's Repository.
|
||||
*/
|
||||
@Configuration
|
||||
@Import(JpaConfiguration.Properties.class)
|
||||
public class JpaConfiguration extends JpaBaseConfiguration {
|
||||
|
||||
@Data
|
||||
@ConfigurationProperties // predix is "/" intentionally
|
||||
protected static class Properties {
|
||||
|
||||
private final Map<String, String> eclipselink = new HashMap<>();
|
||||
}
|
||||
|
||||
private final TenantAware.TenantResolver tenantResolver;
|
||||
// only for testing purposes ddl generation may be enabled
|
||||
private final Map<String, String> eclipselinkProperties;
|
||||
|
||||
|
||||
protected JpaConfiguration(
|
||||
final DataSource dataSource, final JpaProperties properties,
|
||||
final ObjectProvider<JtaTransactionManager> jtaTransactionManagerProvider,
|
||||
final TenantAware.TenantResolver tenantResolver) {
|
||||
final TenantAware.TenantResolver tenantResolver,
|
||||
final Properties eclipselinkProperties) {
|
||||
super(dataSource, properties, jtaTransactionManagerProvider);
|
||||
this.tenantResolver = tenantResolver;
|
||||
this.eclipselinkProperties = eclipselinkProperties.getEclipselink();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -76,7 +92,7 @@ public class JpaConfiguration extends JpaBaseConfiguration {
|
||||
properties.put(PersistenceUnitProperties.WEAVING, "false");
|
||||
// needed for reports
|
||||
properties.put(PersistenceUnitProperties.ALLOW_NATIVE_SQL_QUERIES, "true");
|
||||
// flyway
|
||||
// by default - none, flyway
|
||||
properties.put(PersistenceUnitProperties.DDL_GENERATION, "none");
|
||||
// Embed into hawkBit logging
|
||||
properties.put(PersistenceUnitProperties.LOGGING_LOGGER, "JavaLogger");
|
||||
@@ -86,6 +102,9 @@ public class JpaConfiguration extends JpaBaseConfiguration {
|
||||
properties.put(PersistenceUnitProperties.BATCH_WRITING, "JDBC");
|
||||
// Batch size
|
||||
properties.put(PersistenceUnitProperties.BATCH_WRITING_SIZE, "500");
|
||||
|
||||
// override with all explicitly configured properties
|
||||
properties.putAll(eclipselinkProperties);
|
||||
return properties;
|
||||
}
|
||||
}
|
||||
@@ -13,6 +13,7 @@ import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import lombok.Data;
|
||||
import org.eclipse.hawkbit.repository.jpa.model.EntityPropertyChangeListener;
|
||||
|
||||
import org.eclipse.hawkbit.repository.jpa.utils.JpaExceptionTranslator;
|
||||
@@ -31,8 +32,10 @@ import org.springframework.beans.factory.ObjectProvider;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.boot.autoconfigure.orm.jpa.JpaBaseConfiguration;
|
||||
import org.springframework.boot.autoconfigure.orm.jpa.JpaProperties;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.orm.jpa.vendor.AbstractJpaVendorAdapter;
|
||||
import org.springframework.orm.jpa.vendor.HibernateJpaDialect;
|
||||
import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter;
|
||||
@@ -44,18 +47,27 @@ import javax.sql.DataSource;
|
||||
* General Hibernate configuration for hawkBit's Repository.
|
||||
*/
|
||||
@Configuration
|
||||
@Import(JpaConfiguration.Properties.class)
|
||||
public class JpaConfiguration extends JpaBaseConfiguration {
|
||||
|
||||
@Data
|
||||
@ConfigurationProperties // predix is "/" intentionally
|
||||
protected static class Properties {
|
||||
|
||||
private final Map<String, String> hibernate = new HashMap<>();
|
||||
}
|
||||
|
||||
private final TenantIdentifier tenantIdentifier;
|
||||
private final boolean enableLazyLoadNoTrans;
|
||||
private final Map<String, String> hibernateProperties;
|
||||
|
||||
protected JpaConfiguration(
|
||||
final DataSource dataSource, final JpaProperties properties,
|
||||
final ObjectProvider<JtaTransactionManager> jtaTransactionManagerProvider,
|
||||
final TenantAware.TenantResolver tenantResolver,
|
||||
@Value("${hibernate.enable-lazy-load-no-trans:true}") final boolean enableLazyLoadNoTrans) {
|
||||
final Properties hibernateProperties) {
|
||||
super(dataSource, properties, jtaTransactionManagerProvider);
|
||||
tenantIdentifier = new TenantIdentifier(tenantResolver);
|
||||
this.enableLazyLoadNoTrans = enableLazyLoadNoTrans;
|
||||
this.hibernateProperties = hibernateProperties.getHibernate();
|
||||
}
|
||||
|
||||
@Bean
|
||||
@@ -77,16 +89,16 @@ public class JpaConfiguration extends JpaBaseConfiguration {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Map<String, Object> getVendorProperties(DataSource dataSource) {
|
||||
Map<String, Object> hibernateProperties = new HashMap<>();
|
||||
hibernateProperties.put("hibernate.physical_naming_strategy", org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl.class.getName());
|
||||
hibernateProperties.put(MultiTenancySettings.MULTI_TENANT_IDENTIFIER_RESOLVER, tenantIdentifier);
|
||||
hibernateProperties.put("hibernate.multiTenancy", "DISCRIMINATOR");
|
||||
protected Map<String, Object> getVendorProperties(final DataSource dataSource) {
|
||||
final Map<String, Object> properties = new HashMap<>();
|
||||
properties.put("hibernate.physical_naming_strategy", org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl.class.getName());
|
||||
properties.put(MultiTenancySettings.MULTI_TENANT_IDENTIFIER_RESOLVER, tenantIdentifier);
|
||||
properties.put("hibernate.multiTenancy", "DISCRIMINATOR");
|
||||
// LAZY_LOAD - Enable lazy loading of lazy fields when session is closed - N + 1 problem occur.
|
||||
// So it would be good if in future hawkBit run without that
|
||||
// Otherwise, if false, call for the lazy field will throw LazyInitializationException
|
||||
hibernateProperties.put("hibernate.enable_lazy_load_no_trans", enableLazyLoadNoTrans);
|
||||
hibernateProperties.put("hibernate.integrator_provider", (IntegratorProvider) () -> Collections.singletonList(new Integrator() {
|
||||
properties.put("hibernate.enable_lazy_load_no_trans", "true");
|
||||
properties.put("hibernate.integrator_provider", (IntegratorProvider) () -> Collections.singletonList(new Integrator() {
|
||||
|
||||
@Override
|
||||
public void integrate(
|
||||
@@ -102,7 +114,10 @@ public class JpaConfiguration extends JpaBaseConfiguration {
|
||||
// do nothing
|
||||
}
|
||||
}));
|
||||
return hibernateProperties;
|
||||
|
||||
// override with all explicitly configured properties
|
||||
properties.putAll(hibernateProperties);
|
||||
return properties;
|
||||
}
|
||||
|
||||
static class CustomHibernateJpaDialect extends HibernateJpaDialect {
|
||||
|
||||
@@ -10,21 +10,12 @@
|
||||
package org.eclipse.hawkbit.repository.jpa.rsql;
|
||||
|
||||
import static org.eclipse.hawkbit.repository.rsql.RsqlConfigHolder.RsqlToSpecBuilder.G3;
|
||||
import static org.eclipse.hawkbit.repository.rsql.RsqlConfigHolder.RsqlToSpecBuilder.LEGACY_G1;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.util.List;
|
||||
|
||||
import jakarta.persistence.EntityManager;
|
||||
import jakarta.persistence.criteria.CriteriaBuilder;
|
||||
import jakarta.persistence.criteria.CriteriaQuery;
|
||||
import jakarta.persistence.criteria.Predicate;
|
||||
import jakarta.persistence.criteria.Root;
|
||||
|
||||
import cz.jirutka.rsql.parser.RSQLParser;
|
||||
import cz.jirutka.rsql.parser.RSQLParserException;
|
||||
import cz.jirutka.rsql.parser.ast.Node;
|
||||
import cz.jirutka.rsql.parser.ast.RSQLVisitor;
|
||||
import lombok.AccessLevel;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
@@ -32,15 +23,12 @@ import org.apache.commons.lang3.text.StrLookup;
|
||||
import org.eclipse.hawkbit.repository.RsqlQueryField;
|
||||
import org.eclipse.hawkbit.repository.exception.RSQLParameterSyntaxException;
|
||||
import org.eclipse.hawkbit.repository.exception.RSQLParameterUnsupportedFieldException;
|
||||
import org.eclipse.hawkbit.repository.jpa.rsqllegacy.JpaQueryRsqlVisitor;
|
||||
import org.eclipse.hawkbit.repository.jpa.rsqllegacy.JpaQueryRsqlVisitorG2;
|
||||
import org.eclipse.hawkbit.repository.jpa.rsqllegacy.SpecificationBuilderLegacy;
|
||||
import org.eclipse.hawkbit.repository.rsql.RsqlConfigHolder;
|
||||
import org.eclipse.hawkbit.repository.rsql.VirtualPropertyReplacer;
|
||||
import org.eclipse.hawkbit.repository.rsql.VirtualPropertyResolver;
|
||||
import org.springframework.data.jpa.domain.Specification;
|
||||
import org.springframework.orm.jpa.vendor.Database;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
|
||||
/**
|
||||
* A utility class which is able to parse RSQL strings into an spring data
|
||||
@@ -99,7 +87,11 @@ public final class RSQLUtility {
|
||||
return new SpecificationBuilder<T>(
|
||||
virtualPropertyReplacer,
|
||||
!RsqlConfigHolder.getInstance().isCaseInsensitiveDB() && RsqlConfigHolder.getInstance().isIgnoreCase(),
|
||||
database).specification(RsqlParser.parse(rsql, rsqlQueryFieldType));
|
||||
database)
|
||||
.specification(RsqlParser.parse(
|
||||
RsqlConfigHolder.getInstance().isCaseInsensitiveDB() || RsqlConfigHolder.getInstance().isIgnoreCase()
|
||||
? rsql.toLowerCase() : rsql,
|
||||
rsqlQueryFieldType));
|
||||
} else {
|
||||
return new SpecificationBuilderLegacy<A, T>(rsqlQueryFieldType, virtualPropertyReplacer, database).specification(rsql);
|
||||
}
|
||||
@@ -113,7 +105,7 @@ public final class RSQLUtility {
|
||||
* @throws RSQLParserException if RSQL syntax is invalid
|
||||
* @throws RSQLParameterUnsupportedFieldException if RSQL key is not allowed
|
||||
*/
|
||||
@SuppressWarnings({"unchecked", "rawtypes"})
|
||||
@SuppressWarnings({ "unchecked", "rawtypes" })
|
||||
public static <A extends Enum<A> & RsqlQueryField> void validateRsqlFor(
|
||||
final String rsql, final Class<A> rsqlQueryFieldType,
|
||||
final Class<?> jpaType,
|
||||
@@ -121,6 +113,6 @@ public final class RSQLUtility {
|
||||
final CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
|
||||
final CriteriaQuery<?> criteriaQuery = criteriaBuilder.createQuery(jpaType);
|
||||
buildRsqlSpecification(rsql, rsqlQueryFieldType, virtualPropertyReplacer, null)
|
||||
.toPredicate(criteriaQuery.from((Class)jpaType), criteriaQuery, criteriaBuilder);
|
||||
.toPredicate(criteriaQuery.from((Class) jpaType), criteriaQuery, criteriaBuilder);
|
||||
}
|
||||
}
|
||||
@@ -45,7 +45,6 @@ import org.eclipse.hawkbit.repository.RsqlQueryField;
|
||||
import org.eclipse.hawkbit.repository.exception.RSQLParameterSyntaxException;
|
||||
import org.eclipse.hawkbit.repository.exception.RSQLParameterUnsupportedFieldException;
|
||||
import org.eclipse.hawkbit.repository.jpa.rsql.Node.Comparison;
|
||||
import org.eclipse.hawkbit.repository.rsql.RsqlConfigHolder;
|
||||
|
||||
/**
|
||||
* {@link RsqlParser} parses RSQL query stings to {@link Node} objects. Doing that it does the following:
|
||||
@@ -82,15 +81,13 @@ public class RsqlParser {
|
||||
}
|
||||
|
||||
public static <T extends Enum<T> & RsqlQueryField> Node parse(final String rsql, final Class<T> rsqlQueryFieldType) {
|
||||
return parse(rsql, key -> resolveKey(key, rsqlQueryFieldType));
|
||||
return parse(rsql, rsqlQueryFieldType == null ? null : key -> resolveKey(key, rsqlQueryFieldType));
|
||||
}
|
||||
|
||||
private static Node parse(final String rsql, final Function<String, Key> keyResolver) {
|
||||
try {
|
||||
return PARSER
|
||||
.parse(RsqlConfigHolder.getInstance().isCaseInsensitiveDB() || RsqlConfigHolder.getInstance().isIgnoreCase()
|
||||
? rsql.toLowerCase()
|
||||
: rsql)
|
||||
.parse(rsql)
|
||||
.accept(new RsqlVisitor(keyResolver));
|
||||
} catch (final RSQLParserException e) {
|
||||
throw new RSQLParameterSyntaxException(e);
|
||||
|
||||
@@ -152,11 +152,9 @@ public class SpecificationBuilder<T> {
|
||||
} else {
|
||||
final MapJoin<?, ?, ?> mapPath = (MapJoin<?, ?, ?>) pathResolver.getPath(attribute);
|
||||
final Path<String> valuePath = (Path<String>) mapPath.value();
|
||||
return cb.and(
|
||||
equal(mapPath.key(), split[1]),
|
||||
isNot(op)
|
||||
? notEqualInLike(comparison, (PluralAttribute<?, ?, ?>) attribute, null)
|
||||
: compare(comparison, valuePath));
|
||||
return isNot(op)
|
||||
? compare(comparison, pathResolver.getJoinOnInner(attribute, split[1]))
|
||||
: cb.and(equal(mapPath.key(), split[1]), compare(comparison, valuePath));
|
||||
}
|
||||
} else if (attribute instanceof SetAttribute<?, ?> setAttribute) {
|
||||
if (split.length < 2 || ObjectUtils.isEmpty(split[1])) {
|
||||
@@ -413,6 +411,10 @@ public class SpecificationBuilder<T> {
|
||||
return getCollectionPathResolver(attribute.getName()).getJoinOn(value);
|
||||
}
|
||||
|
||||
private MapJoin<?, ?, ?> getJoinOnInner(final Attribute<?, ?> attribute, final Object value) {
|
||||
return getCollectionPathResolver(attribute.getName()).getJoinOnInner(value);
|
||||
}
|
||||
|
||||
private void reset() {
|
||||
attributeToPathResolver.values().forEach(CollectionPathResolver::reset);
|
||||
}
|
||||
@@ -428,6 +430,7 @@ public class SpecificationBuilder<T> {
|
||||
private final List<Path<?>> paths = new ArrayList<>();
|
||||
private int pos;
|
||||
private final Map<Object, MapJoin<?, ?, ?>> joinOnCache = new HashMap<>();
|
||||
private final Map<Object, MapJoin<?, ?, ?>> joinOnInnerCache = new HashMap<>();
|
||||
|
||||
private CollectionPathResolver(final String attributeName) {
|
||||
this.attributeName = attributeName;
|
||||
@@ -452,6 +455,14 @@ public class SpecificationBuilder<T> {
|
||||
});
|
||||
}
|
||||
|
||||
private MapJoin<?, ?, ?> getJoinOnInner(final Object value) {
|
||||
return joinOnInnerCache.computeIfAbsent(value, k -> {
|
||||
final MapJoin<?, ?, ?> mapPath = (MapJoin<?, ?, ?>) root.join(attributeName, JoinType.INNER);
|
||||
mapPath.on(equal(mapPath.key(), k));
|
||||
return mapPath;
|
||||
});
|
||||
}
|
||||
|
||||
private void reset() {
|
||||
pos = 0;
|
||||
}
|
||||
|
||||
@@ -7,14 +7,14 @@
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.eclipse.hawkbit.repository.jpa;
|
||||
package org.eclipse.hawkbit.repository.jpa.rsql;
|
||||
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import jakarta.persistence.TypedQuery;
|
||||
import jakarta.persistence.Query;
|
||||
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
@@ -41,7 +41,7 @@ import org.hibernate.sql.exec.spi.JdbcParametersList;
|
||||
|
||||
@NoArgsConstructor(access = lombok.AccessLevel.PRIVATE)
|
||||
@Slf4j
|
||||
public class Utils {
|
||||
public class HibernateUtils {
|
||||
|
||||
private static final Method getSqmTranslatorFactory;
|
||||
|
||||
@@ -55,12 +55,12 @@ public class Utils {
|
||||
getSqmTranslatorFactory = method;
|
||||
}
|
||||
|
||||
public static String toSql(final TypedQuery<?> typedQuery) {
|
||||
public static String toSql(final Query query) {
|
||||
if (getSqmTranslatorFactory == null) {
|
||||
throw new UnsupportedOperationException("SqmTranslatorFactory resolver is not available");
|
||||
}
|
||||
|
||||
final QuerySqmImpl<?> hqlQuery = typedQuery.unwrap(QuerySqmImpl.class);
|
||||
final QuerySqmImpl<?> hqlQuery = query.unwrap(QuerySqmImpl.class);
|
||||
final SessionFactoryImplementor factory = hqlQuery.getSessionFactory();
|
||||
final SharedSessionContractImplementor session = hqlQuery.getSession();
|
||||
final SessionFactoryImplementor sessionFactory = session.getFactory();
|
||||
@@ -9,13 +9,15 @@
|
||||
*/
|
||||
package org.eclipse.hawkbit.repository.jpa.rsql;
|
||||
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
|
||||
import jakarta.persistence.EntityManager;
|
||||
import jakarta.persistence.Query;
|
||||
import jakarta.persistence.TypedQuery;
|
||||
import jakarta.persistence.criteria.CriteriaBuilder;
|
||||
import jakarta.persistence.criteria.CriteriaQuery;
|
||||
|
||||
import org.eclipse.hawkbit.repository.RsqlQueryField;
|
||||
import org.eclipse.hawkbit.repository.jpa.Utils;
|
||||
import org.eclipse.hawkbit.repository.rsql.RsqlConfigHolder;
|
||||
import org.eclipse.hawkbit.repository.rsql.RsqlConfigHolder.RsqlToSpecBuilder;
|
||||
import org.springframework.orm.jpa.vendor.Database;
|
||||
@@ -24,16 +26,34 @@ public class RSQLToSQL {
|
||||
|
||||
private static final Database DATABASE = Database.H2;
|
||||
private final EntityManager entityManager;
|
||||
private final boolean isEclipselink;
|
||||
|
||||
public RSQLToSQL(final EntityManager entityManager) {
|
||||
this.entityManager = entityManager;
|
||||
isEclipselink = entityManager.getProperties().keySet().stream().anyMatch(key -> key.startsWith("eclipselink."));
|
||||
}
|
||||
|
||||
public <T, A extends Enum<A> & RsqlQueryField> String toSQL(
|
||||
final Class<T> domainClass, final Class<A> fieldsClass, final String rsql, final RsqlToSpecBuilder rsqlToSpecBuilder) {
|
||||
final CriteriaQuery<T> query = createQuery(domainClass, fieldsClass, rsql, rsqlToSpecBuilder);
|
||||
final TypedQuery<?> typedQuery = entityManager.createQuery(query);
|
||||
return Utils.toSql(typedQuery);
|
||||
if (isEclipselink) {
|
||||
try {
|
||||
return (String)Class.forName("org.eclipse.hawkbit.repository.jpa.EclipselinkUtils")
|
||||
.getMethod("toSql", Query.class)
|
||||
.invoke(null, typedQuery);
|
||||
} catch (final IllegalAccessException | NoSuchMethodException | ClassNotFoundException e) {
|
||||
throw new IllegalStateException(e);
|
||||
} catch (final InvocationTargetException e) {
|
||||
if (e.getCause() instanceof RuntimeException re) {
|
||||
throw re;
|
||||
} else {
|
||||
throw new IllegalStateException(e.getCause());
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return HibernateUtils.toSql(typedQuery);
|
||||
}
|
||||
}
|
||||
|
||||
private <T, A extends Enum<A> & RsqlQueryField> CriteriaQuery<T> createQuery(
|
||||
|
||||
@@ -46,10 +46,11 @@ class RSQLToSQLTest {
|
||||
private RSQLToSQL rsqlToSQL;
|
||||
|
||||
@Test
|
||||
void p() {
|
||||
print(JpaTarget.class, TargetFields.class, TargetFields.TAG.name() + "==''");
|
||||
print(JpaTarget.class, TargetFields.class, TargetFields.TAG.name() + "!=''");
|
||||
void printPG() {
|
||||
printFrom(JpaTarget.class, TargetFields.class, "tag!=TAG1 and tag==TAG2");
|
||||
printFrom(JpaTarget.class, TargetFields.class, "tag==TAG1 and tag!=TAG2");
|
||||
}
|
||||
|
||||
@Test
|
||||
void print() {
|
||||
print(JpaTarget.class, TargetFields.class, "tag==tag1 and tag==tag2");
|
||||
@@ -97,9 +98,9 @@ class RSQLToSQLTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void printPG() {
|
||||
printFrom(JpaTarget.class, TargetFields.class, "tag!=TAG1 and tag==TAG2");
|
||||
printFrom(JpaTarget.class, TargetFields.class, "tag==TAG1 and tag!=TAG2");
|
||||
void printEmpty() {
|
||||
print(JpaTarget.class, TargetFields.class, TargetFields.TAG.name() + "==''");
|
||||
print(JpaTarget.class, TargetFields.class, TargetFields.TAG.name() + "!=''");
|
||||
}
|
||||
|
||||
private static String from(final String sql) {
|
||||
|
||||
@@ -0,0 +1,222 @@
|
||||
/**
|
||||
* 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.sa;
|
||||
|
||||
import static org.eclipse.hawkbit.repository.jpa.rsql.Node.Comparison.Operator.EQ;
|
||||
import static org.eclipse.hawkbit.repository.jpa.rsql.Node.Comparison.Operator.GT;
|
||||
import static org.eclipse.hawkbit.repository.jpa.rsql.Node.Comparison.Operator.GTE;
|
||||
import static org.eclipse.hawkbit.repository.jpa.rsql.Node.Comparison.Operator.IN;
|
||||
import static org.eclipse.hawkbit.repository.jpa.rsql.Node.Comparison.Operator.LIKE;
|
||||
import static org.eclipse.hawkbit.repository.jpa.rsql.Node.Comparison.Operator.LT;
|
||||
import static org.eclipse.hawkbit.repository.jpa.rsql.Node.Comparison.Operator.LTE;
|
||||
import static org.eclipse.hawkbit.repository.jpa.rsql.Node.Comparison.Operator.NE;
|
||||
import static org.eclipse.hawkbit.repository.jpa.rsql.Node.Comparison.Operator.NOT_IN;
|
||||
import static org.eclipse.hawkbit.repository.jpa.rsql.Node.Comparison.Operator.NOT_LIKE;
|
||||
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Method;
|
||||
import java.lang.reflect.ParameterizedType;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.function.BiFunction;
|
||||
|
||||
import org.eclipse.hawkbit.repository.jpa.rsql.Node;
|
||||
import org.eclipse.hawkbit.repository.jpa.rsql.Node.Comparison.Operator;
|
||||
import org.eclipse.hawkbit.repository.jpa.rsql.RsqlParser;
|
||||
|
||||
/**
|
||||
* Provides matching reference for if an object matches a {@link Node}.
|
||||
*/
|
||||
class ReferenceMatcher {
|
||||
|
||||
private final Node root;
|
||||
|
||||
private ReferenceMatcher(final Node root) {
|
||||
this.root = root;
|
||||
}
|
||||
|
||||
static ReferenceMatcher of(final Node root) {
|
||||
return new ReferenceMatcher(root);
|
||||
}
|
||||
|
||||
static ReferenceMatcher ofRsql(final String rsql) {
|
||||
return of(RsqlParser.parse(rsql));
|
||||
}
|
||||
|
||||
<T> boolean match(final T t) {
|
||||
return match(t, root);
|
||||
}
|
||||
|
||||
private static <T> boolean match(final T t, final Node node) {
|
||||
if (node instanceof Node.Comparison comparison) {
|
||||
final String[] split = comparison.getKey().split("\\.", 2);
|
||||
try {
|
||||
final Method fieldGetter = getGetter(t.getClass(), split[0]);
|
||||
fieldGetter.setAccessible(true);
|
||||
final Object fieldValue = fieldGetter.invoke(t);
|
||||
final Operator op = comparison.getOp();
|
||||
if (Map.class.isAssignableFrom(fieldGetter.getReturnType())) {
|
||||
if ((op == NE || op == NOT_IN || op == NOT_LIKE)
|
||||
&& (fieldValue == null || !((Map<?, ?>) fieldValue).containsKey(split[1]))) {
|
||||
// TODO / recheck - when missing entity shall it be included or not in != or =out=? - now it's not
|
||||
return false;
|
||||
}
|
||||
return compare(
|
||||
fieldValue == null ? null : ((Map<?, ?>) fieldValue).get(split[1]),
|
||||
op,
|
||||
map(
|
||||
comparison.getValue(),
|
||||
(Class<?>) ((ParameterizedType) fieldGetter.getGenericReturnType()).getActualTypeArguments()[1]));
|
||||
} else if (Collection.class.isAssignableFrom(fieldGetter.getReturnType())) { // Set / List
|
||||
final Object value;
|
||||
final BiFunction<Object, Operator, Boolean> compare;
|
||||
if (split.length == 1) {
|
||||
value = map(comparison.getValue(), fieldGetter.getReturnType());
|
||||
compare = (e, operator) -> compare(e, operator, value);
|
||||
} else {
|
||||
final Method valueGetter = getGetter(
|
||||
(Class<?>) ((ParameterizedType) fieldGetter.getGenericReturnType()).getActualTypeArguments()[0], split[1]);
|
||||
value = map(comparison.getValue(), valueGetter.getReturnType());
|
||||
compare = (e, operator) -> {
|
||||
try {
|
||||
return compare(map(e == null ? null : valueGetter.invoke(e), valueGetter.getReturnType()), operator, value);
|
||||
} catch (final IllegalAccessException | InvocationTargetException ex) {
|
||||
throw new IllegalArgumentException(ex);
|
||||
}
|
||||
};
|
||||
}
|
||||
final Collection<?> set = (Collection<?>) fieldValue;
|
||||
return switch (op) {
|
||||
case EQ, GT, GTE, LT, LTE, IN, LIKE -> set == null
|
||||
? false
|
||||
: set.stream().anyMatch(e -> compare.apply(e, op));
|
||||
case NE, NOT_IN, NOT_LIKE -> set == null
|
||||
? true
|
||||
: set.stream().noneMatch(e -> compare.apply(e, op == NE ? EQ : op == NOT_IN ? IN : LIKE));
|
||||
};
|
||||
} else {
|
||||
if (split.length == 1) {
|
||||
return compare(fieldValue, op, map(comparison.getValue(), fieldGetter.getReturnType()));
|
||||
} else {
|
||||
final Method valueGetter = getGetter(fieldGetter.getReturnType(), split[1]);
|
||||
return compare(fieldValue == null ? null : valueGetter.invoke(fieldValue), op,
|
||||
map(comparison.getValue(), valueGetter.getReturnType()));
|
||||
}
|
||||
}
|
||||
} catch (final NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
|
||||
throw new IllegalArgumentException(e);
|
||||
}
|
||||
} else if (node instanceof Node.Logical logical) {
|
||||
return switch (logical.getOp()) {
|
||||
case AND -> logical.getChildren().stream().allMatch(child -> match(t, child));
|
||||
case OR -> logical.getChildren().stream().anyMatch(child -> match(t, child));
|
||||
};
|
||||
} else {
|
||||
throw new IllegalArgumentException("Unsupported node type: " + node.getClass());
|
||||
}
|
||||
}
|
||||
|
||||
private static <T> Method getGetter(final Class<T> t, final String fieldName) throws NoSuchMethodException {
|
||||
final String getterLowercase = "get" + fieldName.toLowerCase();
|
||||
return Arrays.stream(t.getMethods())
|
||||
.filter(method -> getterLowercase.equals(method.getName().toLowerCase()))
|
||||
.findFirst()
|
||||
.map(method -> {
|
||||
method.setAccessible(true);
|
||||
return method;
|
||||
}).orElseThrow(() -> new NoSuchMethodException("No getter found for field: " + fieldName + " in class: " + t.getName()));
|
||||
}
|
||||
|
||||
@SuppressWarnings({ "unchecked", "rawtypes" })
|
||||
private static Object map(final Object value, final Class<?> type) {
|
||||
if (value instanceof Collection<?> collection) { // in / out
|
||||
return collection.stream().map(e -> map(e, type)).toList();
|
||||
}
|
||||
|
||||
if (value == null) {
|
||||
return null;
|
||||
} else if (type.isInstance(value)) {
|
||||
return value;
|
||||
} else if (type.isEnum()) {
|
||||
return Enum.valueOf((Class<Enum>) type, value.toString());
|
||||
} else if (type == Boolean.class || type == boolean.class) {
|
||||
return Boolean.parseBoolean(value.toString());
|
||||
} else if (type == Integer.class || type == int.class) {
|
||||
return Integer.parseInt(value.toString());
|
||||
} else if (type == Long.class || type == long.class) {
|
||||
return Long.parseLong(value.toString());
|
||||
} else if (type == Float.class || type == float.class) {
|
||||
return Float.parseFloat(value.toString());
|
||||
} else if (type == Double.class || type == double.class) {
|
||||
return Double.parseDouble(value.toString());
|
||||
} else if (type == String.class) {
|
||||
return String.valueOf(value);
|
||||
} else {
|
||||
throw new IllegalArgumentException("Unsupported type: " + type);
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean compare(final Object o1, final Operator op, final Object o2) {
|
||||
if ((o1 == null || o2 == null) && // null is not comparable!
|
||||
(op == GT || op == GTE || op == LT || op == LTE)) {
|
||||
return false;
|
||||
}
|
||||
return switch (op) {
|
||||
case EQ -> Objects.equals(o1, o2);
|
||||
case NE -> !Objects.equals(o1, o2);
|
||||
case GT -> compare(o1, o2) > 0;
|
||||
case GTE -> compare(o1, o2) >= 0;
|
||||
case LT -> compare(o1, o2) < 0;
|
||||
case LTE -> compare(o1, o2) <= 0;
|
||||
case IN -> in(o1, o2);
|
||||
case NOT_IN -> !in(o1, o2);
|
||||
case LIKE -> like(o2, o1);
|
||||
case NOT_LIKE -> !like(o2, o1);
|
||||
};
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private static int compare(final Object o1, final Object o2) {
|
||||
return toComparable(o1).compareTo(toComparable(o2));
|
||||
}
|
||||
|
||||
@SuppressWarnings("rawtypes")
|
||||
private static Comparable toComparable(final Object o) {
|
||||
if (o instanceof Comparable<?> comparable) {
|
||||
return comparable;
|
||||
} else {
|
||||
throw new IllegalArgumentException("Can't cast " + o.getClass() + " to Comparable");
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean in(final Object o, final Object elementOrCollection) {
|
||||
if (elementOrCollection instanceof Collection<?> collection) {
|
||||
return collection.contains(o);
|
||||
} else {
|
||||
return Objects.equals(o, elementOrCollection);
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean like(final Object pattern, final Object value) {
|
||||
if (pattern instanceof String patternStr) {
|
||||
if (value instanceof String valueStr) {
|
||||
return valueStr.matches(patternStr.replace("\\*", "$").replace("*", ".*").replace("$", "\\*"));
|
||||
} else if (value == null) {
|
||||
return false; // null value cannot match any pattern
|
||||
} else {
|
||||
throw new IllegalArgumentException("LIKE value must be String. Found: " + value.getClass());
|
||||
}
|
||||
} else {
|
||||
throw new IllegalArgumentException("LIKE pattern must be String. Found: " + (pattern == null ? null : pattern.getClass()));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
/**
|
||||
* 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.sa;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import jakarta.persistence.CollectionTable;
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.ElementCollection;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.GeneratedValue;
|
||||
import jakarta.persistence.GenerationType;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.JoinColumn;
|
||||
import jakarta.persistence.JoinTable;
|
||||
import jakarta.persistence.ManyToMany;
|
||||
import jakarta.persistence.ManyToOne;
|
||||
import jakarta.persistence.MapKeyColumn;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
@Entity
|
||||
@Data
|
||||
@Accessors(chain = true)
|
||||
class Root {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
// singular attributes
|
||||
// basic
|
||||
private String strValue;
|
||||
private int intValue;
|
||||
// entity
|
||||
@ManyToOne
|
||||
private Sub subEntity;
|
||||
// set searchable by key
|
||||
@ManyToMany(targetEntity = Sub.class)
|
||||
@JoinTable(
|
||||
name = "subs",
|
||||
joinColumns = { @JoinColumn(name = "root") },
|
||||
inverseJoinColumns = { @JoinColumn(name = "subs") })
|
||||
private Set<Sub> subSet;
|
||||
// standard map
|
||||
@ElementCollection
|
||||
@CollectionTable(
|
||||
name = "map",
|
||||
joinColumns = { @JoinColumn(name = "root") })
|
||||
@MapKeyColumn(name = "map_key", length = 128)
|
||||
@Column(name = "map_value", length = 128)
|
||||
private Map<String, String> subMap;
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
/**
|
||||
* 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.sa;
|
||||
|
||||
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
|
||||
import org.springframework.data.repository.CrudRepository;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
@Repository
|
||||
public interface RootRepository
|
||||
extends CrudRepository<Root, Long>, JpaSpecificationExecutor<Root> {}
|
||||
@@ -0,0 +1,251 @@
|
||||
/**
|
||||
* 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.sa;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.eclipse.hawkbit.repository.rsql.RsqlConfigHolder.RsqlToSpecBuilder.G3;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.stream.StreamSupport;
|
||||
|
||||
import jakarta.persistence.EntityManager;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.eclipse.hawkbit.repository.jpa.rsql.RSQLToSQL;
|
||||
import org.eclipse.hawkbit.repository.jpa.rsql.RsqlParser;
|
||||
import org.eclipse.hawkbit.repository.jpa.rsql.SpecificationBuilder;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.SpringBootConfiguration;
|
||||
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.flyway.FlywayAutoConfiguration;
|
||||
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
|
||||
import org.springframework.orm.jpa.vendor.Database;
|
||||
|
||||
@SuppressWarnings("java:S5961") // complex check because the matter is very complex
|
||||
@DataJpaTest(properties = {
|
||||
"logging.level.org.eclipse.hawkbit.repository.jpa.rsql=DEBUG"
|
||||
}, excludeAutoConfiguration = { FlywayAutoConfiguration.class } )
|
||||
@EnableAutoConfiguration
|
||||
@Slf4j
|
||||
class SpecificationBuilderTest {
|
||||
|
||||
private static final Database DATABASE = Database.H2;
|
||||
|
||||
@Autowired
|
||||
private SubRepository subRepository;
|
||||
@Autowired
|
||||
private RootRepository rootRepository;
|
||||
@Autowired
|
||||
private EntityManager entityManager;
|
||||
|
||||
private final SpecificationBuilder<Root> builder = new SpecificationBuilder<>(null, false, DATABASE);
|
||||
|
||||
@Test
|
||||
void singularStringAttribute() {
|
||||
final Root root = rootRepository.save(new Root().setStrValue("rootX"));
|
||||
final Root root2 = rootRepository.save(new Root().setStrValue("rootX"));
|
||||
final Root root3 = rootRepository.save(new Root().setStrValue("rootY"));
|
||||
final Root root4 = rootRepository.save(new Root().setStrValue("rootY"));
|
||||
final Root root5 = rootRepository.save(new Root()); // null
|
||||
|
||||
assertThat(filter("strValue==rootX")).hasSize(2).containsExactlyInAnyOrder(root, root2);
|
||||
assertThat(filter("strValue==nostr")).isEmpty();
|
||||
assertThat(filter("strValue=is=null")).hasSize(1).containsExactlyInAnyOrder(root5);
|
||||
assertThat(filter("strValue!=rootX")).hasSize(3).containsExactlyInAnyOrder(root3, root4, root5);
|
||||
assertThat(filter("strValue!=nostr")).hasSize(5);
|
||||
assertThat(filter("strValue=not=null")).hasSize(4).containsExactlyInAnyOrder(root, root2, root3, root4);
|
||||
assertThat(filter("strValue<rootY")).hasSize(2).containsExactlyInAnyOrder(root, root2);
|
||||
assertThat(filter("strValue<=rootY")).hasSize(4).containsExactlyInAnyOrder(root, root2, root3, root4);
|
||||
assertThat(filter("strValue>rootX")).hasSize(2).containsExactlyInAnyOrder(root3, root4);
|
||||
assertThat(filter("strValue>=rootX")).hasSize(4);
|
||||
assertThat(filter("strValue=in=rootX")).hasSize(2).containsExactlyInAnyOrder(root, root2);
|
||||
assertThat(filter("strValue=in=(rootX, rootY)")).hasSize(4).containsExactlyInAnyOrder(root, root2, root3, root4);
|
||||
assertThat(filter("strValue=in=(rootZ, rootT)")).isEmpty();
|
||||
assertThat(filter("strValue=out=rootX")).hasSize(3).containsExactlyInAnyOrder(root3, root4, root5);
|
||||
assertThat(filter("strValue=out=(rootX, rootY)")).hasSize(1).containsExactlyInAnyOrder(root5);
|
||||
// wildcard, like
|
||||
assertThat(filter("strValue==root*")).hasSize(4).containsExactlyInAnyOrder(root, root2, root3, root4);
|
||||
assertThat(filter("strValue==*tX")).hasSize(2).containsExactlyInAnyOrder(root, root2);
|
||||
assertThat(filter("strValue!=root*")).hasSize(1).containsExactlyInAnyOrder(root5);
|
||||
assertThat(filter("strValue!=*tX")).hasSize(3).containsExactlyInAnyOrder(root3, root4, root5);
|
||||
}
|
||||
|
||||
@Test
|
||||
void singularIntAttribute() {
|
||||
final Root root = rootRepository.save(new Root().setIntValue(0));
|
||||
final Root root2 = rootRepository.save(new Root().setIntValue(0));
|
||||
final Root root3 = rootRepository.save(new Root().setIntValue(1));
|
||||
final Root root4 = rootRepository.save(new Root().setIntValue(1));
|
||||
|
||||
assertThat(filter("intValue==0")).hasSize(2).containsExactlyInAnyOrder(root, root2);
|
||||
assertThat(filter("intValue==2")).isEmpty();
|
||||
assertThat(filter("intValue!=0")).hasSize(2).containsExactlyInAnyOrder(root3, root4);
|
||||
assertThat(filter("intValue!=2")).hasSize(4).containsExactlyInAnyOrder(root, root2, root3, root4);
|
||||
assertThat(filter("intValue<1")).hasSize(2).containsExactlyInAnyOrder(root, root2);
|
||||
assertThat(filter("intValue<=1")).hasSize(4).containsExactlyInAnyOrder(root, root2, root3, root4);
|
||||
assertThat(filter("intValue>0")).hasSize(2).containsExactlyInAnyOrder(root3, root4);
|
||||
assertThat(filter("intValue>=0")).hasSize(4).containsExactlyInAnyOrder(root, root2, root3, root4);
|
||||
assertThat(filter("intValue=in=0")).hasSize(2).containsExactlyInAnyOrder(root, root2);
|
||||
assertThat(filter("intValue=in=(0, 1)")).hasSize(4).containsExactlyInAnyOrder(root, root2, root3, root4);
|
||||
assertThat(filter("intValue=in=(2, 3)")).isEmpty();
|
||||
assertThat(filter("intValue=out=0")).hasSize(2).containsExactlyInAnyOrder(root3, root4);
|
||||
assertThat(filter("intValue=out=(0, 1)")).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void singularEntityAttribute() {
|
||||
final Sub sub = subRepository.save(new Sub().setStrValue("subX").setIntValue(0));
|
||||
final Sub sub2 = subRepository.save(new Sub().setStrValue("subY").setIntValue(1));
|
||||
final Root root = rootRepository.save(new Root().setSubEntity(sub));
|
||||
final Root root2 = rootRepository.save(new Root().setSubEntity(sub));
|
||||
final Root root3 = rootRepository.save(new Root().setSubEntity(sub2));
|
||||
final Root root4 = rootRepository.save(new Root().setSubEntity(sub2));
|
||||
final Root root5 = rootRepository.save(new Root()); // no sub set
|
||||
|
||||
// by sub entity string
|
||||
assertThat(filter("subEntity.strValue==subX")).hasSize(2).containsExactlyInAnyOrder(root, root2);
|
||||
assertThat(filter("subEntity.strValue==nostr")).isEmpty();
|
||||
// TODO / recheck - when missing entity shall it be included or not in != or =out=? - now it is
|
||||
assertThat(filter("subEntity.strValue!=subX")).hasSize(3).containsExactlyInAnyOrder(root3, root4, root5);
|
||||
assertThat(filter("subEntity.strValue!=nostr")).hasSize(5);
|
||||
assertThat(filter("subEntity.strValue<subY")).hasSize(2).containsExactlyInAnyOrder(root, root2);
|
||||
assertThat(filter("subEntity.strValue<=subY")).hasSize(4).containsExactlyInAnyOrder(root, root2, root3, root4);
|
||||
assertThat(filter("subEntity.strValue>subX")).hasSize(2).containsExactlyInAnyOrder(root3, root4);
|
||||
assertThat(filter("subEntity.strValue>=subX")).hasSize(4).containsExactlyInAnyOrder(root, root2, root3, root4);
|
||||
assertThat(filter("subEntity.strValue=in=subX")).hasSize(2).containsExactlyInAnyOrder(root, root2);
|
||||
assertThat(filter("subEntity.strValue=in=(subX, subY)")).hasSize(4).containsExactlyInAnyOrder(root, root2, root3, root4);
|
||||
assertThat(filter("subEntity.strValue=in=(subZ, subT)")).isEmpty();
|
||||
assertThat(filter("subEntity.strValue=out=subX")).hasSize(3).containsExactlyInAnyOrder(root3, root4, root5);
|
||||
assertThat(filter("subEntity.strValue=out=(subX, subY)")).hasSize(1).containsExactlyInAnyOrder(root5);
|
||||
// wildcard, like
|
||||
assertThat(filter("subEntity.strValue==sub*")).hasSize(4).containsExactlyInAnyOrder(root, root2, root3, root4);
|
||||
assertThat(filter("subEntity.strValue==*bX")).hasSize(2).containsExactlyInAnyOrder(root, root2);
|
||||
assertThat(filter("subEntity.strValue!=sub*")).hasSize(1).containsExactlyInAnyOrder(root5);
|
||||
assertThat(filter("subEntity.strValue!=*bX")).hasSize(3).containsExactlyInAnyOrder(root3, root4, root5);
|
||||
|
||||
// by sub entity int
|
||||
assertThat(filter("subEntity.intValue==0")).hasSize(2).containsExactlyInAnyOrder(root, root2);
|
||||
assertThat(filter("subEntity.intValue==2")).isEmpty();
|
||||
assertThat(filter("subEntity.intValue!=0")).hasSize(3).containsExactlyInAnyOrder(root3, root4, root5);
|
||||
assertThat(filter("subEntity.intValue!=2")).hasSize(5);
|
||||
assertThat(filter("subEntity.intValue<1")).hasSize(2).containsExactlyInAnyOrder(root, root2);
|
||||
assertThat(filter("subEntity.intValue<=1")).hasSize(4).containsExactlyInAnyOrder(root, root2, root3, root4);
|
||||
assertThat(filter("subEntity.intValue>0")).hasSize(2).containsExactlyInAnyOrder(root3, root4);
|
||||
assertThat(filter("subEntity.intValue>=0")).hasSize(4);
|
||||
assertThat(filter("subEntity.intValue=in=0")).hasSize(2).containsExactlyInAnyOrder(root, root2);
|
||||
assertThat(filter("subEntity.intValue=in=(0, 1)")).hasSize(4).containsExactlyInAnyOrder(root, root2, root3, root4);
|
||||
assertThat(filter("subEntity.intValue=in=(2, 3)")).isEmpty();
|
||||
assertThat(filter("subEntity.intValue=out=0")).hasSize(3).containsExactlyInAnyOrder(root3, root4, root5);
|
||||
assertThat(filter("subEntity.intValue=out=(0, 1)")).hasSize(1).containsExactlyInAnyOrder(root5);
|
||||
}
|
||||
|
||||
@Test
|
||||
void pluralSubSetAttribute() {
|
||||
final Sub sub = subRepository.save(new Sub().setStrValue("subX").setIntValue(0));
|
||||
final Sub sub2 = subRepository.save(new Sub().setStrValue("subY").setIntValue(1));
|
||||
final Sub sub3 = subRepository.save(new Sub().setStrValue("subY").setIntValue(0));
|
||||
final Root root = rootRepository.save(new Root().setSubSet(Set.of(sub)));
|
||||
final Root root2 = rootRepository.save(new Root().setSubSet(Set.of(sub2)));
|
||||
final Root root3 = rootRepository.save(new Root().setSubSet(Set.of(sub3)));
|
||||
final Root root4 = rootRepository.save(new Root().setSubSet(Set.of(sub, sub2)));
|
||||
final Root root5 = rootRepository.save(new Root().setSubSet(Set.of(sub, sub3)));
|
||||
final Root root6 = rootRepository.save(new Root()); // no sub set
|
||||
|
||||
// by sub entity string
|
||||
assertThat(filter("subSet.strValue==subX")).hasSize(3).containsExactlyInAnyOrder(root, root4, root5);
|
||||
assertThat(filter("subSet.strValue==nostr")).isEmpty();
|
||||
assertThat(filter("subSet.strValue!=subX")).hasSize(3).containsExactlyInAnyOrder(root2, root3, root6);
|
||||
assertThat(filter("subSet.strValue!=nostr")).hasSize(6);
|
||||
assertThat(filter("subSet.strValue<subY")).hasSize(3).containsExactlyInAnyOrder(root, root4, root5);
|
||||
assertThat(filter("subSet.strValue<=subY")).hasSize(5).containsExactlyInAnyOrder(root, root2, root3, root4, root5);
|
||||
assertThat(filter("subSet.strValue>subX")).hasSize(4).containsExactlyInAnyOrder(root2, root3, root4, root5);
|
||||
assertThat(filter("subSet.strValue>=subX")).hasSize(5).containsExactlyInAnyOrder(root, root2, root3, root4, root5);
|
||||
assertThat(filter("subSet.strValue=in=subX")).hasSize(3).containsExactlyInAnyOrder(root, root4, root5);
|
||||
assertThat(filter("subSet.strValue=in=(subX, subY)"))
|
||||
.hasSize(5).containsExactlyInAnyOrder(root, root2, root3, root4, root5);
|
||||
assertThat(filter("subSet.strValue=in=(subZ, subT)")).isEmpty();
|
||||
assertThat(filter("subSet.strValue=out=subX")).hasSize(3).containsExactlyInAnyOrder(root2, root3, root6);
|
||||
assertThat(filter("subSet.strValue=out=(subX, subY)")).hasSize(1).containsExactlyInAnyOrder(root6);
|
||||
// wildcard, like
|
||||
assertThat(filter("subSet.strValue==sub*")).hasSize(5).containsExactlyInAnyOrder(root, root2, root3, root4, root5);
|
||||
assertThat(filter("subSet.strValue==*bX")).hasSize(3).containsExactlyInAnyOrder(root, root4, root5);
|
||||
assertThat(filter("subSet.strValue!=sub*")).hasSize(1).containsExactlyInAnyOrder(root6);
|
||||
assertThat(filter("subSet.strValue!=*bX")).hasSize(3).containsExactlyInAnyOrder(root2, root3, root6);
|
||||
|
||||
// by sub entity int
|
||||
assertThat(filter("subSet.intValue==0")).hasSize(4).containsExactlyInAnyOrder(root, root3, root4, root5);
|
||||
assertThat(filter("subSet.intValue==2")).isEmpty();
|
||||
assertThat(filter("subSet.intValue!=0")).hasSize(2).containsExactlyInAnyOrder(root2, root6);
|
||||
assertThat(filter("subSet.intValue!=2")).hasSize(6);
|
||||
assertThat(filter("subSet.intValue<1")).hasSize(4).containsExactlyInAnyOrder(root, root3, root4, root5);
|
||||
assertThat(filter("subSet.intValue<=1")).hasSize(5).containsExactlyInAnyOrder(root, root2, root3, root4, root5);
|
||||
assertThat(filter("subSet.intValue>0")).hasSize(2).containsExactlyInAnyOrder(root2, root4);
|
||||
assertThat(filter("subSet.intValue>=0")).hasSize(5).containsExactlyInAnyOrder(root, root2, root3, root4, root5);
|
||||
assertThat(filter("subSet.intValue=in=0")).hasSize(4).containsExactlyInAnyOrder(root, root3, root4, root5);
|
||||
assertThat(filter("subSet.intValue=in=(0, 1)")).hasSize(5).containsExactlyInAnyOrder(root, root2, root3, root4, root5);
|
||||
assertThat(filter("subSet.intValue=in=(2, 3)")).isEmpty();
|
||||
assertThat(filter("subSet.intValue=out=0")).hasSize(2).containsExactlyInAnyOrder(root2, root6);
|
||||
assertThat(filter("subSet.intValue=out=(0, 1)")).hasSize(1).containsExactlyInAnyOrder(root6);
|
||||
}
|
||||
|
||||
@Test
|
||||
void pluralSubMapAttribute() {
|
||||
final Root root = rootRepository.save(new Root().setSubMap(Map.of("x", "rootX", "y", "rootY")));
|
||||
final Root root2 = rootRepository.save(new Root().setSubMap(Map.of("x", "rootX", "y", "rootX")));
|
||||
final Root root3 = rootRepository.save(new Root().setSubMap(Map.of("x", "rootY", "y", "rootY")));
|
||||
final Root root4 = rootRepository.save(new Root().setSubMap(Map.of("x", "rootY", "y", "rootX")));
|
||||
final Root root5 = rootRepository.save(new Root().setSubMap(Map.of("x", "rootX")));
|
||||
final Root root6 = rootRepository.save(new Root()); // no sub map
|
||||
|
||||
assertThat(filter("subMap.x==rootX")).hasSize(3).containsExactlyInAnyOrder(root, root2, root5);
|
||||
assertThat(filter("subMap.x==nostr")).isEmpty();
|
||||
// TODO / recheck - when missing entity shall it be included or not in != or =out=? - now it's not
|
||||
assertThat(filter("subMap.x!=rootX")).hasSize(2).containsExactlyInAnyOrder(root3, root4);
|
||||
assertThat(filter("subMap.x!=nostr")).hasSize(5).containsExactlyInAnyOrder(root, root2, root3, root4, root5);
|
||||
assertThat(filter("subMap.x<rootY")).hasSize(3).containsExactlyInAnyOrder(root, root2, root5);
|
||||
assertThat(filter("subMap.x<=rootY")).hasSize(5).containsExactlyInAnyOrder(root, root2, root3, root4, root5);
|
||||
assertThat(filter("subMap.x>rootX")).hasSize(2).containsExactlyInAnyOrder(root3, root4);
|
||||
assertThat(filter("subMap.x>=rootX")).hasSize(5).containsExactlyInAnyOrder(root, root2, root3, root4, root5);
|
||||
assertThat(filter("subMap.x=in=rootX")).hasSize(3).containsExactlyInAnyOrder(root, root2, root5);
|
||||
assertThat(filter("subMap.x=in=(rootX, rootY)")).hasSize(5).containsExactlyInAnyOrder(root, root2, root3, root4, root5);
|
||||
assertThat(filter("subMap.x=in=(rootZ, rootT)")).isEmpty();
|
||||
assertThat(filter("subMap.x=out=rootX")).hasSize(2).containsExactlyInAnyOrder(root3, root4);
|
||||
assertThat(filter("subMap.x=out=(rootX, rootY)")).isEmpty();
|
||||
// wildcard, like
|
||||
assertThat(filter("subMap.x==root*")).hasSize(5).containsExactlyInAnyOrder(root, root2, root3, root4, root5);
|
||||
assertThat(filter("subMap.x==*tX")).hasSize(3).containsExactlyInAnyOrder(root, root2, root5);
|
||||
assertThat(filter("subMap.x!=root*")).isEmpty();
|
||||
assertThat(filter("subMap.x!=*tX")).hasSize(2).containsExactlyInAnyOrder(root3, root4);
|
||||
}
|
||||
|
||||
private List<Root> filter(final String rsql) {
|
||||
// reference / auto filter (using elements and reflection)
|
||||
final ReferenceMatcher matcher = ReferenceMatcher.ofRsql(rsql);
|
||||
final List<Root> refResult = StreamSupport.stream(rootRepository.findAll().spliterator(), false).filter(matcher::match).toList();
|
||||
final List<Root> result = rootRepository.findAll(builder.specification(RsqlParser.parse(rsql)));
|
||||
// auto check with reference result
|
||||
try {
|
||||
assertThat(result).containsExactlyInAnyOrder(refResult.toArray(Root[]::new));
|
||||
} catch (final AssertionError e) {
|
||||
log.error(
|
||||
"Fail to get expected result for RSQL: {} with SQL query: {}",
|
||||
rsql, new RSQLToSQL(entityManager).toSQL(Root.class, null, rsql, G3),
|
||||
e);
|
||||
throw e;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@SpringBootConfiguration
|
||||
static class Config {}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
/**
|
||||
* 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.sa;
|
||||
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.GeneratedValue;
|
||||
import jakarta.persistence.GenerationType;
|
||||
import jakarta.persistence.Id;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
@Entity
|
||||
@Data
|
||||
@Accessors(chain = true)
|
||||
class Sub {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
// singular attributes
|
||||
// basic
|
||||
private String strValue;
|
||||
private int intValue;
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
/**
|
||||
* 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.sa;
|
||||
|
||||
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
|
||||
import org.springframework.data.repository.CrudRepository;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
@Repository
|
||||
public interface SubRepository
|
||||
extends CrudRepository<Sub, Long>, JpaSpecificationExecutor<Sub> {}
|
||||
Reference in New Issue
Block a user