diff --git a/rsql-common/pom.xml b/rsql-common/pom.xml index 3e2e2151..cc2e5c3d 100644 --- a/rsql-common/pom.xml +++ b/rsql-common/pom.xml @@ -37,6 +37,12 @@ hamcrest test + + io.hypersistence + hypersistence-utils-hibernate-62 + 3.5.1 + test + com.h2database h2 diff --git a/rsql-common/src/main/java/io/github/perplexhub/rsql/RSQLVisitorBase.java b/rsql-common/src/main/java/io/github/perplexhub/rsql/RSQLVisitorBase.java index 2f78f765..8391b7d9 100644 --- a/rsql-common/src/main/java/io/github/perplexhub/rsql/RSQLVisitorBase.java +++ b/rsql-common/src/main/java/io/github/perplexhub/rsql/RSQLVisitorBase.java @@ -12,7 +12,9 @@ import javax.persistence.metamodel.ManagedType; import javax.persistence.metamodel.PluralAttribute; +import lombok.Getter; import org.springframework.core.convert.support.ConfigurableConversionService; +import org.springframework.orm.jpa.vendor.Database; import org.springframework.util.StringUtils; import cz.jirutka.rsql.parser.ast.RSQLVisitor; @@ -26,6 +28,7 @@ public abstract class RSQLVisitorBase implements RSQLVisitor { protected static volatile @Setter Map managedTypeMap; protected static volatile @Setter Map entityManagerMap; + protected static volatile @Setter @Getter Map entityManagerDatabase = new HashMap(); protected static final Map primitiveToWrapper; protected static volatile @Setter Map, Map> propertyRemapping; protected static volatile @Setter Map, List> globalPropertyWhitelist; diff --git a/rsql-jpa-spring-boot-starter/src/main/java/io/github/perplexhub/rsql/RSQLJPAAutoConfiguration.java b/rsql-jpa-spring-boot-starter/src/main/java/io/github/perplexhub/rsql/RSQLJPAAutoConfiguration.java index d4ada2e1..644883ea 100644 --- a/rsql-jpa-spring-boot-starter/src/main/java/io/github/perplexhub/rsql/RSQLJPAAutoConfiguration.java +++ b/rsql-jpa-spring-boot-starter/src/main/java/io/github/perplexhub/rsql/RSQLJPAAutoConfiguration.java @@ -1,24 +1,122 @@ package io.github.perplexhub.rsql; -import java.util.Map; +import io.github.perplexhub.rsql.RSQLJPAAutoConfiguration.HibernateEntityManagerDatabaseConfiguration; import javax.persistence.EntityManager; - +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import lombok.extern.slf4j.Slf4j; +import org.hibernate.Session; +import org.hibernate.SessionFactory; +import org.hibernate.dialect.DB2Dialect; +import org.hibernate.dialect.DerbyDialect; +import org.hibernate.dialect.Dialect; +import org.hibernate.dialect.H2Dialect; +import org.hibernate.dialect.HSQLDialect; +import org.hibernate.dialect.MySQLDialect; +import org.hibernate.dialect.OracleDialect; +import org.hibernate.dialect.PostgreSQLDialect; +import org.hibernate.dialect.SQLServerDialect; +import org.hibernate.dialect.SybaseDialect; +import org.hibernate.engine.spi.SessionImplementor; +import org.hibernate.internal.SessionFactoryImpl; +import org.springframework.beans.factory.ObjectProvider; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; - -import lombok.extern.slf4j.Slf4j; +import org.springframework.context.annotation.Import; +import org.springframework.orm.jpa.vendor.Database; +import org.springframework.transaction.annotation.Transactional; @Slf4j @Configuration @ConditionalOnClass(EntityManager.class) +@Import(HibernateEntityManagerDatabaseConfiguration.class) public class RSQLJPAAutoConfiguration { - @Bean - public RSQLCommonSupport rsqlCommonSupport(Map entityManagerMap) { - log.info("RSQLJPAAutoConfiguration.rsqlCommonSupport(entityManagerMap:{})", entityManagerMap.size()); - return new RSQLCommonSupport(entityManagerMap); - } + @Bean + public RSQLCommonSupport rsqlCommonSupport(Map entityManagerMap, + ObjectProvider entityManagerDatabaseProvider) { + log.info("RSQLJPAAutoConfiguration.rsqlCommonSupport(entityManagerMap:{})", entityManagerMap.size()); + EntityManagerDatabase entityManagerDatabase = entityManagerDatabaseProvider.getIfAvailable(() -> new EntityManagerDatabase(new HashMap())); + + return new RSQLJPASupport(entityManagerMap, entityManagerDatabase.value()); + } + + @Configuration + @ConditionalOnClass(SessionImplementor.class) + static + class HibernateEntityManagerDatabaseConfiguration { + + @Transactional + @Bean + public EntityManagerDatabase entityManagerDatabase(ObjectProvider entityManagers) { + Map value = new HashMap<>(); + EntityManager entityManager = entityManagers.getIfAvailable(); + SessionFactory sessionFactory = entityManager.unwrap(Session.class).getSessionFactory(); + Dialect dialect = ((SessionFactoryImpl) sessionFactory).getJdbcServices().getDialect(); + + Database db = toDatabase(dialect); + if (db != null) { + value.put(entityManager, db); + } + + return new EntityManagerDatabase(value); + } + + private Database toDatabase(Dialect dialect) { + if (dialect instanceof PostgreSQLDialect) { + return Database.POSTGRESQL; + } else if (dialect instanceof MySQLDialect) { + return Database.MYSQL; + } else if (dialect instanceof SQLServerDialect) { + return Database.SQL_SERVER; + } else if (dialect instanceof OracleDialect) { + return Database.ORACLE; + } else if (dialect instanceof DerbyDialect) { + return Database.DERBY; + } else if (dialect instanceof DB2Dialect) { + return Database.DB2; + } else if (dialect instanceof H2Dialect) { + return Database.H2; + } else if (dialect instanceof HSQLDialect) { + return Database.HSQL; + } else if (dialect instanceof SybaseDialect) { + return Database.SQL_SERVER; + } + + return null; + } + } + + public static final class EntityManagerDatabase { + private final Map value; + + public EntityManagerDatabase(Map value) { + this.value = value; + } + + public Map value() { + return value; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (obj == null || getClass() != obj.getClass()) return false; + EntityManagerDatabase that = (EntityManagerDatabase) obj; + return Objects.equals(value, that.value); + } + + @Override + public int hashCode() { + return Objects.hash(value); + } + @Override + public String toString() { + return "EntityManagerDatabase[value=" + value + "]"; + } + } } diff --git a/rsql-jpa/pom.xml b/rsql-jpa/pom.xml index e21f5f55..dd804dd0 100644 --- a/rsql-jpa/pom.xml +++ b/rsql-jpa/pom.xml @@ -41,6 +41,34 @@ h2 test + + org.postgresql + postgresql + test + + + org.testcontainers + postgresql + 1.19.0 + test + + + org.hibernate.common + hibernate-commons-annotations + 5.1.2.Final + + + io.hypersistence + hypersistence-utils-hibernate-52 + 3.7.6 + test + + + net.java.dev.jna + jna + 5.14.0 + test + org.projectlombok lombok diff --git a/rsql-jpa/src/main/java/io/github/perplexhub/rsql/RSQLJPAPredicateConverter.java b/rsql-jpa/src/main/java/io/github/perplexhub/rsql/RSQLJPAPredicateConverter.java index 6b587f96..be73a97f 100644 --- a/rsql-jpa/src/main/java/io/github/perplexhub/rsql/RSQLJPAPredicateConverter.java +++ b/rsql-jpa/src/main/java/io/github/perplexhub/rsql/RSQLJPAPredicateConverter.java @@ -2,6 +2,8 @@ import static io.github.perplexhub.rsql.RSQLOperators.*; +import javax.persistence.Column; +import java.lang.reflect.Field; import java.lang.reflect.Modifier; import java.util.*; import java.util.function.Function; @@ -18,14 +20,18 @@ import cz.jirutka.rsql.parser.ast.ComparisonNode; import cz.jirutka.rsql.parser.ast.ComparisonOperator; import cz.jirutka.rsql.parser.ast.OrNode; +import java.util.stream.Stream; import lombok.Getter; import lombok.extern.slf4j.Slf4j; import org.reflections.Reflections; +import org.springframework.orm.jpa.vendor.Database; @Slf4j @SuppressWarnings({ "rawtypes", "unchecked" }) public class RSQLJPAPredicateConverter extends RSQLVisitorBase { + private static final Set JSON_SUPPORT = EnumSet.of(Database.POSTGRESQL); + private final CriteriaBuilder builder; private final Map cachedJoins = new HashMap<>(); private final @Getter Map propertyPathMapper; @@ -134,6 +140,10 @@ RSQLJPAContext findPropertyPath(String propertyPath, Path startRoot) { String keyJoin = getKeyJoin(root, mappedProperty); log.debug("Create a element collection join between [{}] and [{}] using key [{}]", previousClass, classMetadata.getJavaType().getName(), keyJoin); root = join(keyJoin, root, mappedProperty); + } else if (isJsonType(mappedProperty, classMetadata)) { + root = root.get(mappedProperty); + attribute = classMetadata.getAttribute(mappedProperty); + break; } else { log.debug("Create property path for type [{}] property [{}]", classMetadata.getJavaType().getName(), mappedProperty); root = root.get(mappedProperty); @@ -157,6 +167,35 @@ RSQLJPAContext findPropertyPath(String propertyPath, Path startRoot) { return RSQLJPAContext.of(root, attribute, classMetadata); } + private boolean isJsonType(String mappedProperty, ManagedType classMetadata) { + return Optional.ofNullable(classMetadata.getAttribute(mappedProperty)) + .map(this::isJsonType) + .orElse(false); + } + + private boolean isJsonType(Attribute attribute) { + return isJsonColumn(attribute) && getDatabase(attribute).map(JSON_SUPPORT::contains).orElse(false); + } + + private boolean isJsonColumn(Attribute attribute) { + return Optional.ofNullable(attribute) + .filter(attr -> attr.getJavaMember() instanceof Field) + .map(attr -> ((Field) attr.getJavaMember())) + .map(field -> field.getAnnotation(Column.class)) + .map(Column::columnDefinition) + .map("jsonb"::equalsIgnoreCase) + .orElse(false); + } + + private Optional getDatabase(Attribute attribute) { + return getEntityManagerMap() + .values() + .stream() + .filter(em -> em.getMetamodel().getManagedTypes().contains(attribute.getDeclaringType())) + .findFirst() + .map(em -> getEntityManagerDatabase().get(em)); + } + private String getKeyJoin(Path root, String mappedProperty) { return root.getJavaType().getSimpleName().concat(".").concat(mappedProperty); } @@ -195,7 +234,9 @@ public Predicate visit(ComparisonNode node, From root) { return customPredicate.getConverter().apply(RSQLCustomPredicateInput.of(builder, attrPath, attribute, arguments, root)); } - Class type = attribute != null ? attribute.getJavaType() : null; + final boolean json = isJsonType(attribute); + Class type = json ? String.class : attribute != null ? attribute.getJavaType() : null; + if (attribute != null) { if (attribute.getPersistentAttributeType() == PersistentAttributeType.ELEMENT_COLLECTION) { type = getElementCollectionGenericType(type, attribute); @@ -206,6 +247,7 @@ public Predicate visit(ComparisonNode node, From root) { type = RSQLJPASupport.getValueTypeMap().get(type); // if you want to treat Enum as String and apply like search, etc } } + Expression expr = json ? getJsonExpression(attrPath, attribute, node) : attrPath; if (node.getArguments().size() > 1) { List listObject = new ArrayList<>(); @@ -213,51 +255,52 @@ public Predicate visit(ComparisonNode node, From root) { listObject.add(convert(argument, type)); } if (op.equals(IN)) { - return attrPath.in(listObject); + return expr.in(listObject); } if (op.equals(NOT_IN)) { - return attrPath.in(listObject).not(); + return expr.in(listObject).not(); } if (op.equals(BETWEEN) && listObject.size() == 2 && listObject.get(0) instanceof Comparable && listObject.get(1) instanceof Comparable) { - return builder.between(attrPath, (Comparable) listObject.get(0), (Comparable) listObject.get(1)); + return builder.between(expr, (Comparable) listObject.get(0), (Comparable) listObject.get(1)); } if (op.equals(NOT_BETWEEN) && listObject.size() == 2 && listObject.get(0) instanceof Comparable && listObject.get(1) instanceof Comparable) { - return builder.between(attrPath, (Comparable) listObject.get(0), (Comparable) listObject.get(1)).not(); + return builder.between(expr, (Comparable) listObject.get(0), (Comparable) listObject.get(1)).not(); } } else { + if (op.equals(IS_NULL)) { - return builder.isNull(attrPath); + return builder.isNull(expr); } if (op.equals(NOT_NULL)) { - return builder.isNotNull(attrPath); + return builder.isNotNull(expr); } Object argument = convert(node.getArguments().get(0), type); if (op.equals(IN)) { - return builder.equal(attrPath, argument); + return builder.equal(expr, argument); } if (op.equals(NOT_IN)) { - return builder.notEqual(attrPath, argument); + return builder.notEqual(expr, argument); } if (op.equals(LIKE)) { - return builder.like(attrPath, "%" + argument.toString() + "%"); + return builder.like(expr, "%" + argument.toString() + "%"); } if (op.equals(NOT_LIKE)) { - return builder.like(attrPath, "%" + argument.toString() + "%").not(); + return builder.like(expr, "%" + argument.toString() + "%").not(); } if (op.equals(IGNORE_CASE)) { - return builder.equal(builder.upper(attrPath), argument.toString().toUpperCase()); + return builder.equal(builder.upper(expr), argument.toString().toUpperCase()); } if (op.equals(IGNORE_CASE_LIKE)) { - return builder.like(builder.upper(attrPath), "%" + argument.toString().toUpperCase() + "%"); + return builder.like(builder.upper(expr), "%" + argument.toString().toUpperCase() + "%"); } if (op.equals(IGNORE_CASE_NOT_LIKE)) { - return builder.like(builder.upper(attrPath), "%" + argument.toString().toUpperCase() + "%").not(); + return builder.like(builder.upper(expr), "%" + argument.toString().toUpperCase() + "%").not(); } if (op.equals(EQUAL)) { - return equalPredicate(attrPath, type, argument); + return equalPredicate(expr, type, argument); } if (op.equals(NOT_EQUAL)) { - return equalPredicate(attrPath, type, argument).not(); + return equalPredicate(expr, type, argument).not(); } if (!Comparable.class.isAssignableFrom(type)) { log.error("Operator {} can be used only for Comparables", op); @@ -266,43 +309,62 @@ public Predicate visit(ComparisonNode node, From root) { Comparable comparable = (Comparable) argument; if (op.equals(GREATER_THAN)) { - return builder.greaterThan(attrPath, comparable); + return builder.greaterThan(expr, comparable); } if (op.equals(GREATER_THAN_OR_EQUAL)) { - return builder.greaterThanOrEqualTo(attrPath, comparable); + return builder.greaterThanOrEqualTo(expr, comparable); } if (op.equals(LESS_THAN)) { - return builder.lessThan(attrPath, comparable); + return builder.lessThan(expr, comparable); } if (op.equals(LESS_THAN_OR_EQUAL)) { - return builder.lessThanOrEqualTo(attrPath, comparable); + return builder.lessThanOrEqualTo(expr, comparable); } } log.error("Unknown operator: {}", op); throw new RSQLException("Unknown operator: " + op); } - private Predicate equalPredicate(Path attrPath, Class type, Object argument) { + private Expression getJsonExpression(Path path, Attribute attribute, ComparisonNode node) { + Database database = getDatabase(attribute).orElse(null); + + if (database == Database.POSTGRESQL) { + List> args = new ArrayList>(); + args.add(path); + + Stream.of(node.getSelector().split("\\.")) + .skip(1) // skip root + .map(builder::literal) + .map(expr -> expr.as(String.class)) + .forEach(args::add); + + return builder.function("jsonb_extract_path_text", String.class, args.toArray(new Expression[0])); + } + + return path; + } + + private Predicate equalPredicate(Expression expr, Class type, Object argument) { if (type.equals(String.class)) { String argStr = argument.toString(); if (strictEquality) { - return builder.equal(attrPath, argument); + return builder.equal(expr, argument); } else { if (argStr.contains("*") && argStr.contains("^")) { - return builder.like(builder.upper(attrPath), argStr.replace('*', '%').replace("^", "").toUpperCase()); + return builder.like(builder.upper(expr), argStr.replace('*', '%').replace("^", "").toUpperCase()); } else if (argStr.contains("*")) { - return builder.like(attrPath, argStr.replace('*', '%')); + return builder.like(expr, argStr.replace('*', '%')); } else if (argStr.contains("^")) { - return builder.equal(builder.upper(attrPath), argStr.replace("^", "").toUpperCase()); + return builder.equal(builder.upper(expr), argStr.replace("^", "").toUpperCase()); } else { - return builder.equal(attrPath, argument); + return builder.equal(expr, argument); } } } else if (argument == null) { - return builder.isNull(attrPath); + return builder.isNull(expr); } else { - return builder.equal(attrPath, argument); + return builder.equal(expr, argument); } } diff --git a/rsql-jpa/src/main/java/io/github/perplexhub/rsql/RSQLJPASupport.java b/rsql-jpa/src/main/java/io/github/perplexhub/rsql/RSQLJPASupport.java index 9dfd31ed..6f1d8d3b 100644 --- a/rsql-jpa/src/main/java/io/github/perplexhub/rsql/RSQLJPASupport.java +++ b/rsql-jpa/src/main/java/io/github/perplexhub/rsql/RSQLJPASupport.java @@ -15,6 +15,7 @@ import org.springframework.data.jpa.domain.Specification; import org.springframework.data.jpa.repository.JpaSpecificationExecutor; import org.springframework.lang.Nullable; +import org.springframework.orm.jpa.vendor.Database; import org.springframework.util.StringUtils; import cz.jirutka.rsql.parser.RSQLParser; @@ -31,7 +32,12 @@ public RSQLJPASupport() { } public RSQLJPASupport(Map entityManagerMap) { + this(entityManagerMap, new HashMap<>()); + } + + public RSQLJPASupport(Map entityManagerMap, Map entityManagerDatabase) { super(entityManagerMap); + RSQLVisitorBase.setEntityManagerDatabase(entityManagerDatabase); } public static Specification rsql(final String rsqlQuery) { diff --git a/rsql-jpa/src/test/java/io/github/perplexhub/rsql/RSQLJPASupportPostgresJsonTest.java b/rsql-jpa/src/test/java/io/github/perplexhub/rsql/RSQLJPASupportPostgresJsonTest.java new file mode 100644 index 00000000..c0a5c569 --- /dev/null +++ b/rsql-jpa/src/test/java/io/github/perplexhub/rsql/RSQLJPASupportPostgresJsonTest.java @@ -0,0 +1,275 @@ +package io.github.perplexhub.rsql; + +import static io.github.perplexhub.rsql.RSQLJPASupport.toSpecification; +import static org.assertj.core.api.Assertions.assertThat; + +import io.github.perplexhub.rsql.model.PostgresJsonEntity; +import io.github.perplexhub.rsql.repository.jpa.postgres.PostgresJsonEntityRepository; +import javax.persistence.EntityManager; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.junit.After; +import org.junit.Before; +import org.junit.ClassRule; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameters; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.orm.jpa.vendor.Database; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.junit4.rules.SpringClassRule; +import org.springframework.test.context.junit4.rules.SpringMethodRule; + +@SpringBootTest +@ActiveProfiles("postgres") +@RunWith(Parameterized.class) +public class RSQLJPASupportPostgresJsonTest { + @ClassRule + public static final SpringClassRule scr = new SpringClassRule(); + + @Rule + public final SpringMethodRule smr = new SpringMethodRule(); + + @Autowired + private PostgresJsonEntityRepository repository; + @Autowired + private EntityManager em; + + private List users; + private String rsql; + private List expected; + + public RSQLJPASupportPostgresJsonTest(List users, String rsql, List expected) { + this.users = users; + this.rsql = rsql; + this.expected = expected; + } + + @Before + public void setup() { + Map map = new HashMap<>(); + map.put(em, Database.POSTGRESQL); + RSQLVisitorBase.setEntityManagerDatabase(map); + } + + @After + public void tearDown() { + repository.deleteAll(); + repository.flush(); + RSQLVisitorBase.setEntityManagerDatabase(new HashMap<>()); + } + + @Test + public void testJson() { + //given + repository.saveAll(users); + repository.flush(); + + //when + List result = repository.findAll(toSpecification(rsql)); + + //then + assertThat(result) + .hasSameSizeAs(expected) + .containsExactlyInAnyOrderElementsOf(expected); + + users.forEach(e -> e.setId(null)); + } + + @Parameters + public static Collection data() { + List data = new ArrayList<>(); + data.addAll(equalsData()); + data.addAll(inData()); + data.addAll(betweenData()); + data.addAll(likeData()); + data.addAll(gtLtData()); + data.addAll(miscData()); + return data; + } + + private static Collection equalsData() { + Map map1 = new HashMap<>(); + map1.put("a1", "b1"); + PostgresJsonEntity e1 = new PostgresJsonEntity(map1); + + Map innerMap = new HashMap<>(); + Map innerInnerMap = new HashMap<>(); + innerInnerMap.put("a111", "b1"); + innerMap.put("a11", innerInnerMap); + Map map2 = new HashMap<>(); + map2.put("a1", innerMap); + PostgresJsonEntity e2 = new PostgresJsonEntity(map2); + + PostgresJsonEntity e3 = new PostgresJsonEntity(e1); + PostgresJsonEntity e4 = new PostgresJsonEntity(e2); + + Map map5 = new HashMap<>(); + map5.put("a", "b1"); + PostgresJsonEntity e5 = new PostgresJsonEntity(map5); + + Map map6 = new HashMap<>(); + map6.put("a", "b2"); + PostgresJsonEntity e6 = new PostgresJsonEntity(map6); + + Map map7 = new HashMap<>(); + map7.put("a", "c1"); + PostgresJsonEntity e7 = new PostgresJsonEntity(map7); + + List data = new ArrayList<>(); + data.add(new Object[] {Arrays.asList(e1, e2), "properties.a1==b1", Arrays.asList(e1)}); + data.add(new Object[] {Arrays.asList(e1, e2), "properties.a1!=b1", Arrays.asList(e2)}); + data.add(new Object[] {Arrays.asList(e1, e2), "properties.a1=ic=B1", Arrays.asList(e1)}); + data.add(new Object[] {Arrays.asList(e1, e2), "properties.a1==b2", Arrays.asList()}); + data.add(new Object[] {Arrays.asList(e3, e4), "properties.a1.a11.a111==b1", Arrays.asList(e4)}); + data.add(new Object[] {Arrays.asList(e3, e4), "properties.a1.a11.a111==b2", Arrays.asList()}); + + data.add(new Object[] {Arrays.asList(e5, e6, e7), "properties.a==b*", Arrays.asList(e5, e6)}); + data.add(new Object[] {Arrays.asList(e5, e6, e7), "properties.a==c*", Arrays.asList(e7)}); + data.add(new Object[] {Arrays.asList(e5, e6, e7), "properties.a==*1", Arrays.asList(e5, e7)}); + + return data; + } + + private static Collection inData() { + Map map1 = new HashMap<>(); + map1.put("a", "b1"); + PostgresJsonEntity e1 = new PostgresJsonEntity(map1); + + Map map2 = new HashMap<>(); + map2.put("a", "b2"); + PostgresJsonEntity e2 = new PostgresJsonEntity(map2); + + Map map3 = new HashMap<>(); + map3.put("a", "c1"); + PostgresJsonEntity e3 = new PostgresJsonEntity(map3); + + Map map4 = new HashMap<>(); + map4.put("a", "d1"); + PostgresJsonEntity e4 = new PostgresJsonEntity(map4); + + List data = new ArrayList<>(); + data.add(new Object[] {Arrays.asList(e1, e2, e3, e4), "properties.a=in=(b1, c1)", Arrays.asList(e1, e3)}); + data.add(new Object[] {Arrays.asList(e1, e2, e3, e4), "properties.a=out=(b1, c1)", Arrays.asList(e2, e4)}); + data.add(new Object[] {Arrays.asList(e1, e2, e3, e4), "properties.a=in=(b1)", Arrays.asList(e1)}); + data.add(new Object[] {Arrays.asList(e1, e2, e3, e4), "properties.a=out=(b1)", Arrays.asList(e2, e3, e4)}); + + return data; + } + + private static Collection betweenData() { + Map map1 = new HashMap<>(); + map1.put("a", "a"); + PostgresJsonEntity e1 = new PostgresJsonEntity(map1); + + Map map2 = new HashMap<>(); + map2.put("a", "b"); + PostgresJsonEntity e2 = new PostgresJsonEntity(map2); + + Map map3 = new HashMap<>(); + map3.put("a", "c"); + PostgresJsonEntity e3 = new PostgresJsonEntity(map3); + + Map map4 = new HashMap<>(); + map4.put("a", "d"); + PostgresJsonEntity e4 = new PostgresJsonEntity(map4); + + List data = new ArrayList<>(); + data.add(new Object[] {Arrays.asList(e1, e2, e3, e4), "properties.a=bt=(a, c)", Arrays.asList(e1, e2, e3)}); + data.add(new Object[] {Arrays.asList(e1, e2, e3, e4), "properties.a=nb=(b, d)", Arrays.asList(e1)}); + + return data; + } + + private static Collection likeData() { + Map map1 = new HashMap<>(); + map1.put("a", "a b c"); + PostgresJsonEntity e1 = new PostgresJsonEntity(map1); + + Map map2 = new HashMap<>(); + map2.put("a", "b c d"); + PostgresJsonEntity e2 = new PostgresJsonEntity(map2); + + Map map3 = new HashMap<>(); + map3.put("a", "c d e"); + PostgresJsonEntity e3 = new PostgresJsonEntity(map3); + + List data = new ArrayList<>(); + data.add(new Object[] {Arrays.asList(e1, e2, e3), "properties.a=ke='a b'", Arrays.asList(e1)}); + data.add(new Object[] {Arrays.asList(e1, e2, e3), "properties.a=ke='b c'", Arrays.asList(e1, e2)}); + data.add(new Object[] {Arrays.asList(e1, e2, e3), "properties.a=ke='c d'", Arrays.asList(e2, e3)}); + data.add(new Object[] {Arrays.asList(e1, e2, e3), "properties.a=ke='d e'", Arrays.asList(e3)}); + + data.add(new Object[] {Arrays.asList(e1, e2, e3), "properties.a=ik='A B'", Arrays.asList(e1)}); + data.add(new Object[] {Arrays.asList(e1, e2, e3), "properties.a=ik='B C'", Arrays.asList(e1, e2)}); + data.add(new Object[] {Arrays.asList(e1, e2, e3), "properties.a=ik='C D'", Arrays.asList(e2, e3)}); + data.add(new Object[] {Arrays.asList(e1, e2, e3), "properties.a=ik='D E'", Arrays.asList(e3)}); + + data.add(new Object[] {Arrays.asList(e1, e2, e3), "properties.a=nk='a b'", Arrays.asList(e2, e3)}); + data.add(new Object[] {Arrays.asList(e1, e2, e3), "properties.a=ni='A B'", Arrays.asList(e2, e3)}); + + return data; + } + + private static Collection gtLtData() { + Map map1 = new HashMap<>(); + map1.put("a", "a"); + PostgresJsonEntity e1 = new PostgresJsonEntity(map1); + + Map map2 = new HashMap<>(); + map2.put("a", "b"); + PostgresJsonEntity e2 = new PostgresJsonEntity(map2); + + Map map3 = new HashMap<>(); + map3.put("a", "c"); + PostgresJsonEntity e3 = new PostgresJsonEntity(map3); + + Map map4 = new HashMap<>(); + map4.put("a", "d"); + PostgresJsonEntity e4 = new PostgresJsonEntity(map4); + + List data = new ArrayList<>(); + data.add(new Object[] {Arrays.asList(e1, e2, e3, e4), "properties.a>=a", Arrays.asList(e1, e2, e3, e4)}); + data.add(new Object[] {Arrays.asList(e1, e2, e3, e4), "properties.a>a", Arrays.asList(e2, e3, e4)}); + data.add(new Object[] {Arrays.asList(e1, e2, e3, e4), "properties.a miscData() { + Map map1 = new HashMap<>(); + map1.put("a", "b1"); + PostgresJsonEntity e1 = new PostgresJsonEntity(map1); + + Map map2 = new HashMap<>(); + map2.put("a", "b2"); + PostgresJsonEntity e2 = new PostgresJsonEntity(map2); + + Map map3 = new HashMap<>(); + map3.put("b", "c1"); + PostgresJsonEntity e3 = new PostgresJsonEntity(map3); + + Map map4 = new HashMap<>(); + map4.put("b", "d1"); + PostgresJsonEntity e4 = new PostgresJsonEntity(map4); + + List data = new ArrayList<>(); + data.add(new Object[] {Arrays.asList(e1, e2, e3, e4), "properties.a=nn=''", Arrays.asList(e1, e2)}); + data.add(new Object[] {Arrays.asList(e1, e2, e3, e4), "properties.a=na=''", Arrays.asList(e3, e4)}); + + data.add(new Object[] {Arrays.asList(e1, e2, e3, e4), "properties.b=nn=''", Arrays.asList(e3, e4)}); + data.add(new Object[] {Arrays.asList(e1, e2, e3, e4), "properties.b=na=''", Arrays.asList(e1, e2)}); + + return data; + } +} \ No newline at end of file diff --git a/rsql-jpa/src/test/java/io/github/perplexhub/rsql/RSQLJPASupportTest.java b/rsql-jpa/src/test/java/io/github/perplexhub/rsql/RSQLJPASupportTest.java index 59f8d21f..bdb26e72 100644 --- a/rsql-jpa/src/test/java/io/github/perplexhub/rsql/RSQLJPASupportTest.java +++ b/rsql-jpa/src/test/java/io/github/perplexhub/rsql/RSQLJPASupportTest.java @@ -1,6 +1,5 @@ package io.github.perplexhub.rsql; -import static io.github.perplexhub.rsql.RSQLCommonSupport.*; import static io.github.perplexhub.rsql.RSQLJPASupport.*; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.hamcrest.CoreMatchers.*; diff --git a/rsql-jpa/src/test/java/io/github/perplexhub/rsql/model/PostgresJsonEntity.java b/rsql-jpa/src/test/java/io/github/perplexhub/rsql/model/PostgresJsonEntity.java new file mode 100644 index 00000000..2e01e710 --- /dev/null +++ b/rsql-jpa/src/test/java/io/github/perplexhub/rsql/model/PostgresJsonEntity.java @@ -0,0 +1,44 @@ +package io.github.perplexhub.rsql.model; + +import io.hypersistence.utils.hibernate.type.json.JsonType; +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.Id; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.UUID; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.ToString; +import org.hibernate.annotations.Type; +import org.hibernate.annotations.TypeDef; + +@Getter +@Setter +@EqualsAndHashCode(of = "id") +@ToString +@Entity +@NoArgsConstructor +@TypeDef(name="jsonb", typeClass=JsonType.class) +public class PostgresJsonEntity { + + @Id + @GeneratedValue + private UUID id; + + @Type(type="jsonb") + @Column(columnDefinition = "jsonb") + private Map properties = new HashMap<>(); + + public PostgresJsonEntity(Map properties) { + this.properties = Objects.requireNonNull(properties); + } + + public PostgresJsonEntity(PostgresJsonEntity other) { + this(other.getProperties()); + } +} diff --git a/rsql-jpa/src/test/java/io/github/perplexhub/rsql/repository/jpa/postgres/PostgresJsonEntityRepository.java b/rsql-jpa/src/test/java/io/github/perplexhub/rsql/repository/jpa/postgres/PostgresJsonEntityRepository.java new file mode 100644 index 00000000..69606335 --- /dev/null +++ b/rsql-jpa/src/test/java/io/github/perplexhub/rsql/repository/jpa/postgres/PostgresJsonEntityRepository.java @@ -0,0 +1,11 @@ +package io.github.perplexhub.rsql.repository.jpa.postgres; + +import io.github.perplexhub.rsql.model.PostgresJsonEntity; +import java.util.UUID; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; + +public interface PostgresJsonEntityRepository extends JpaRepository, + JpaSpecificationExecutor { + +} diff --git a/rsql-jpa/src/test/resources/application-postgres.yml b/rsql-jpa/src/test/resources/application-postgres.yml new file mode 100644 index 00000000..a457b142 --- /dev/null +++ b/rsql-jpa/src/test/resources/application-postgres.yml @@ -0,0 +1,7 @@ +spring: + datasource: + driver-class-name: org.testcontainers.jdbc.ContainerDatabaseDriver + url: jdbc:tc:postgresql:12://localhost/test + jpa: + hibernate: + ddl-auto: create-drop \ No newline at end of file diff --git a/rsql-querydsl-spring-boot-starter/src/test/java/io/github/perplexhub/rsql/RSQLQueryDslSupportTest.java b/rsql-querydsl-spring-boot-starter/src/test/java/io/github/perplexhub/rsql/RSQLQueryDslSupportTest.java index 1690f9f7..dbb86cc9 100644 --- a/rsql-querydsl-spring-boot-starter/src/test/java/io/github/perplexhub/rsql/RSQLQueryDslSupportTest.java +++ b/rsql-querydsl-spring-boot-starter/src/test/java/io/github/perplexhub/rsql/RSQLQueryDslSupportTest.java @@ -1,6 +1,5 @@ package io.github.perplexhub.rsql; -import static io.github.perplexhub.rsql.RSQLJPASupport.*; import static io.github.perplexhub.rsql.RSQLQueryDslSupport.*; import static org.hamcrest.CoreMatchers.*; import static org.junit.Assert.*; diff --git a/rsql-querydsl/src/test/java/io/github/perplexhub/rsql/RSQLQueryDslSupportTest.java b/rsql-querydsl/src/test/java/io/github/perplexhub/rsql/RSQLQueryDslSupportTest.java index a3d0d24f..d2d6952f 100644 --- a/rsql-querydsl/src/test/java/io/github/perplexhub/rsql/RSQLQueryDslSupportTest.java +++ b/rsql-querydsl/src/test/java/io/github/perplexhub/rsql/RSQLQueryDslSupportTest.java @@ -1,6 +1,5 @@ package io.github.perplexhub.rsql; -import static io.github.perplexhub.rsql.RSQLJPASupport.*; import static io.github.perplexhub.rsql.RSQLQueryDslSupport.*; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.hamcrest.CoreMatchers.*; @@ -15,7 +14,6 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; -import org.springframework.data.jpa.domain.Specification; import org.springframework.test.context.junit4.SpringRunner; import io.github.perplexhub.rsql.model.*; @@ -48,7 +46,7 @@ public final void testQueryMultiLevelAttribute() { assertThat(rsql, users.get(0).getId(), equalTo(1)); } - @Test + @Test public final void testEnumILike() { RSQLJPASupport.addEntityAttributeTypeMap(Status.class, String.class); String rsql = "status==*A*";