From 9300039f73c2a4352c6389312a75a6512d47aad1 Mon Sep 17 00:00:00 2001 From: vincentescoffier Date: Thu, 4 Jan 2024 10:40:50 +0100 Subject: [PATCH 01/18] update grammar --- src/main/antlr4/Query.g4 | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/main/antlr4/Query.g4 b/src/main/antlr4/Query.g4 index 2a2a373..36a722c 100644 --- a/src/main/antlr4/Query.g4 +++ b/src/main/antlr4/Query.g4 @@ -33,7 +33,9 @@ value op : EQ | GT + | GTE | LT + | LTE | NOT_EQ ; @@ -137,11 +139,17 @@ GT : '>' ; +GTE + : '>=' + ; LT : '<' ; +LTE + : '<=' + ; EQ : ':' From aba2e9731aeca3c3bf73a792a0c60c4e274dfffe Mon Sep 17 00:00:00 2001 From: vincentescoffier Date: Thu, 4 Jan 2024 10:41:00 +0100 Subject: [PATCH 02/18] add tests --- .../SpringSearchApplicationTest.kt | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/src/test/kotlin/com/sipios/springsearch/SpringSearchApplicationTest.kt b/src/test/kotlin/com/sipios/springsearch/SpringSearchApplicationTest.kt index 66eb2e6..453bc73 100644 --- a/src/test/kotlin/com/sipios/springsearch/SpringSearchApplicationTest.kt +++ b/src/test/kotlin/com/sipios/springsearch/SpringSearchApplicationTest.kt @@ -1015,4 +1015,42 @@ class SpringSearchApplicationTest { Assertions.assertEquals(1, robotUsers.size) Assertions.assertEquals(user2UUID, robotUsers[0].uuid) } + + @Test + fun canGetUsersWithNumberOfChildrenLessOrEqualSearch() { + userRepository.save(Users(userFirstName = "john", userChildrenNumber = 2)) + userRepository.save(Users(userFirstName = "jane", userChildrenNumber = 3)) + userRepository.save(Users(userFirstName = "joe", userChildrenNumber = 4)) + val specification = SpecificationsBuilder( + SearchSpec::class.constructors.first().call("", false) + ).withSearch("userChildrenNumber<=2").build() + val users = userRepository.findAll(specification) + Assertions.assertEquals(1, users.size) + Assertions.assertEquals("john", users[0].userFirstName) + } + + @Test + fun canGetUsersWithNumberOfChildrenGreaterOrEqualSearch() { + userRepository.save(Users(userFirstName = "john", userChildrenNumber = 2)) + userRepository.save(Users(userFirstName = "jane", userChildrenNumber = 3)) + userRepository.save(Users(userFirstName = "joe", userChildrenNumber = 4)) + val specification = SpecificationsBuilder( + SearchSpec::class.constructors.first().call("", false) + ).withSearch("userChildrenNumber>=3").build() + val users = userRepository.findAll(specification) + Assertions.assertEquals(2, users.size) + } + + @Test + fun canGetUsersWithNumberOfChildrenLessSearch() { + userRepository.save(Users(userFirstName = "john", userChildrenNumber = 2)) + userRepository.save(Users(userFirstName = "jane", userChildrenNumber = 3)) + userRepository.save(Users(userFirstName = "joe", userChildrenNumber = 4)) + val specification = SpecificationsBuilder( + SearchSpec::class.constructors.first().call("", false) + ).withSearch("userChildrenNumber<3").build() + val users = userRepository.findAll(specification) + Assertions.assertEquals(1, users.size) + Assertions.assertEquals("john", users[0].userFirstName) + } } From 597b1c45a800abb057c52e543c8e7e1ff308a4e0 Mon Sep 17 00:00:00 2001 From: vincentescoffier Date: Thu, 4 Jan 2024 10:41:44 +0100 Subject: [PATCH 03/18] add gte and lte operations and change from chat to string --- .../com/sipios/springsearch/SearchCriteria.kt | 2 +- .../com/sipios/springsearch/SearchOperation.kt | 16 +++++++++------- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/src/main/kotlin/com/sipios/springsearch/SearchCriteria.kt b/src/main/kotlin/com/sipios/springsearch/SearchCriteria.kt index e87ba7e..adcc69c 100644 --- a/src/main/kotlin/com/sipios/springsearch/SearchCriteria.kt +++ b/src/main/kotlin/com/sipios/springsearch/SearchCriteria.kt @@ -5,7 +5,7 @@ class SearchCriteria // Change EQUALS into ENDS_WITH, CONTAINS, STARTS_WITH base var operation: SearchOperation? init { - var op = SearchOperation.getSimpleOperation(operation[0]) + var op = SearchOperation.getSimpleOperation(operation) if (op != null) { // Change EQUALS into ENDS_WITH, CONTAINS, STARTS_WITH based on the presence of * in the value val startsWithAsterisk = prefix != null && prefix.contains(SearchOperation.ZERO_OR_MORE_REGEX) diff --git a/src/main/kotlin/com/sipios/springsearch/SearchOperation.kt b/src/main/kotlin/com/sipios/springsearch/SearchOperation.kt index d6a1a04..333f5c5 100644 --- a/src/main/kotlin/com/sipios/springsearch/SearchOperation.kt +++ b/src/main/kotlin/com/sipios/springsearch/SearchOperation.kt @@ -1,10 +1,10 @@ package com.sipios.springsearch enum class SearchOperation { - EQUALS, NOT_EQUALS, GREATER_THAN, LESS_THAN, STARTS_WITH, ENDS_WITH, CONTAINS, DOESNT_START_WITH, DOESNT_END_WITH, DOESNT_CONTAIN; + EQUALS, NOT_EQUALS, GREATER_THAN, LESS_THAN, STARTS_WITH, ENDS_WITH, CONTAINS, DOESNT_START_WITH, DOESNT_END_WITH, DOESNT_CONTAIN, GREATER_THAN_EQUALS, LESS_THAN_EQUALS; companion object { - val SIMPLE_OPERATION_SET = arrayOf(":", "!", ">", "<", "~") + val SIMPLE_OPERATION_SET = arrayOf(":", "!", ">", "<", "~", ">=", "<=") val ZERO_OR_MORE_REGEX = "*" val OR_OPERATOR = "OR" val AND_OPERATOR = "AND" @@ -17,12 +17,14 @@ enum class SearchOperation { * @param input operation as string * @return The matching operation */ - fun getSimpleOperation(input: Char): SearchOperation? { + fun getSimpleOperation(input: String): SearchOperation? { return when (input) { - ':' -> EQUALS - '!' -> NOT_EQUALS - '>' -> GREATER_THAN - '<' -> LESS_THAN + ":" -> EQUALS + "!" -> NOT_EQUALS + ">" -> GREATER_THAN + "<" -> LESS_THAN + ">=" -> GREATER_THAN_EQUALS + "<=" -> LESS_THAN_EQUALS else -> null } } From e2acaa0c027d2f845f2ac056591b0514ae5b7da6 Mon Sep 17 00:00:00 2001 From: vincentescoffier Date: Thu, 4 Jan 2024 11:03:00 +0100 Subject: [PATCH 04/18] update strategies and add tests --- .../springsearch/strategies/DateStrategy.kt | 2 + .../springsearch/strategies/DoubleStrategy.kt | 2 + .../strategies/DurationStrategy.kt | 2 + .../springsearch/strategies/FloatStrategy.kt | 2 + .../strategies/InstantStrategy.kt | 2 + .../springsearch/strategies/IntStrategy.kt | 2 + .../strategies/LocalDateStrategy.kt | 2 + .../strategies/LocalDateTimeStrategy.kt | 2 + .../strategies/LocalTimeStrategy.kt | 2 + .../SpringSearchApplicationTest.kt | 138 ++++++++++++++++-- 10 files changed, 147 insertions(+), 9 deletions(-) diff --git a/src/main/kotlin/com/sipios/springsearch/strategies/DateStrategy.kt b/src/main/kotlin/com/sipios/springsearch/strategies/DateStrategy.kt index d240e70..61f5c69 100644 --- a/src/main/kotlin/com/sipios/springsearch/strategies/DateStrategy.kt +++ b/src/main/kotlin/com/sipios/springsearch/strategies/DateStrategy.kt @@ -22,6 +22,8 @@ class DateStrategy : ParsingStrategy { return when (ops) { SearchOperation.GREATER_THAN -> builder.greaterThan(path[fieldName], value as Date) SearchOperation.LESS_THAN -> builder.lessThan(path[fieldName], value as Date) + SearchOperation.GREATER_THAN_EQUALS -> builder.greaterThanOrEqualTo(path[fieldName], value as Date) + SearchOperation.LESS_THAN_EQUALS -> builder.lessThanOrEqualTo(path[fieldName], value as Date) else -> super.buildPredicate(builder, path, fieldName, ops, value) } } diff --git a/src/main/kotlin/com/sipios/springsearch/strategies/DoubleStrategy.kt b/src/main/kotlin/com/sipios/springsearch/strategies/DoubleStrategy.kt index 135dfb3..7b1ccc0 100644 --- a/src/main/kotlin/com/sipios/springsearch/strategies/DoubleStrategy.kt +++ b/src/main/kotlin/com/sipios/springsearch/strategies/DoubleStrategy.kt @@ -17,6 +17,8 @@ class DoubleStrategy : ParsingStrategy { return when (ops) { SearchOperation.GREATER_THAN -> builder.greaterThan(path[fieldName], value as Double) SearchOperation.LESS_THAN -> builder.lessThan(path[fieldName], value as Double) + SearchOperation.GREATER_THAN_EQUALS -> builder.greaterThanOrEqualTo(path[fieldName], value as Double) + SearchOperation.LESS_THAN_EQUALS -> builder.lessThanOrEqualTo(path[fieldName], value as Double) else -> super.buildPredicate(builder, path, fieldName, ops, value) } } diff --git a/src/main/kotlin/com/sipios/springsearch/strategies/DurationStrategy.kt b/src/main/kotlin/com/sipios/springsearch/strategies/DurationStrategy.kt index c35a847..a722fe2 100644 --- a/src/main/kotlin/com/sipios/springsearch/strategies/DurationStrategy.kt +++ b/src/main/kotlin/com/sipios/springsearch/strategies/DurationStrategy.kt @@ -18,6 +18,8 @@ class DurationStrategy : ParsingStrategy { return when (ops) { SearchOperation.GREATER_THAN -> builder.greaterThan(path[fieldName], value as Duration) SearchOperation.LESS_THAN -> builder.lessThan(path[fieldName], value as Duration) + SearchOperation.GREATER_THAN_EQUALS -> builder.greaterThanOrEqualTo(path[fieldName], value as Duration) + SearchOperation.LESS_THAN_EQUALS -> builder.lessThanOrEqualTo(path[fieldName], value as Duration) else -> super.buildPredicate(builder, path, fieldName, ops, value) } } diff --git a/src/main/kotlin/com/sipios/springsearch/strategies/FloatStrategy.kt b/src/main/kotlin/com/sipios/springsearch/strategies/FloatStrategy.kt index 65e7525..ea1d1e6 100644 --- a/src/main/kotlin/com/sipios/springsearch/strategies/FloatStrategy.kt +++ b/src/main/kotlin/com/sipios/springsearch/strategies/FloatStrategy.kt @@ -17,6 +17,8 @@ class FloatStrategy : ParsingStrategy { return when (ops) { SearchOperation.GREATER_THAN -> builder.greaterThan(path[fieldName], value as Float) SearchOperation.LESS_THAN -> builder.lessThan(path[fieldName], value as Float) + SearchOperation.GREATER_THAN_EQUALS -> builder.greaterThanOrEqualTo(path[fieldName], value as Float) + SearchOperation.LESS_THAN_EQUALS -> builder.lessThanOrEqualTo(path[fieldName], value as Float) else -> super.buildPredicate(builder, path, fieldName, ops, value) } } diff --git a/src/main/kotlin/com/sipios/springsearch/strategies/InstantStrategy.kt b/src/main/kotlin/com/sipios/springsearch/strategies/InstantStrategy.kt index 95aff1f..66294cd 100644 --- a/src/main/kotlin/com/sipios/springsearch/strategies/InstantStrategy.kt +++ b/src/main/kotlin/com/sipios/springsearch/strategies/InstantStrategy.kt @@ -18,6 +18,8 @@ class InstantStrategy : ParsingStrategy { return when (ops) { SearchOperation.GREATER_THAN -> builder.greaterThan(path[fieldName], value as Instant) SearchOperation.LESS_THAN -> builder.lessThan(path[fieldName], value as Instant) + SearchOperation.GREATER_THAN_EQUALS -> builder.greaterThanOrEqualTo(path[fieldName], value as Instant) + SearchOperation.LESS_THAN_EQUALS -> builder.lessThanOrEqualTo(path[fieldName], value as Instant) else -> super.buildPredicate(builder, path, fieldName, ops, value) } } diff --git a/src/main/kotlin/com/sipios/springsearch/strategies/IntStrategy.kt b/src/main/kotlin/com/sipios/springsearch/strategies/IntStrategy.kt index fc8ceb1..21f3863 100644 --- a/src/main/kotlin/com/sipios/springsearch/strategies/IntStrategy.kt +++ b/src/main/kotlin/com/sipios/springsearch/strategies/IntStrategy.kt @@ -17,6 +17,8 @@ class IntStrategy : ParsingStrategy { return when (ops) { SearchOperation.GREATER_THAN -> builder.greaterThan(path[fieldName], value as Int) SearchOperation.LESS_THAN -> builder.lessThan(path[fieldName], value as Int) + SearchOperation.GREATER_THAN_EQUALS -> builder.greaterThanOrEqualTo(path[fieldName], value as Int) + SearchOperation.LESS_THAN_EQUALS -> builder.lessThanOrEqualTo(path[fieldName], value as Int) else -> super.buildPredicate(builder, path, fieldName, ops, value) } } diff --git a/src/main/kotlin/com/sipios/springsearch/strategies/LocalDateStrategy.kt b/src/main/kotlin/com/sipios/springsearch/strategies/LocalDateStrategy.kt index 57e8ae2..2b435f6 100644 --- a/src/main/kotlin/com/sipios/springsearch/strategies/LocalDateStrategy.kt +++ b/src/main/kotlin/com/sipios/springsearch/strategies/LocalDateStrategy.kt @@ -18,6 +18,8 @@ class LocalDateStrategy : ParsingStrategy { return when (ops) { SearchOperation.GREATER_THAN -> builder.greaterThan(path[fieldName], value as LocalDate) SearchOperation.LESS_THAN -> builder.lessThan(path[fieldName], value as LocalDate) + SearchOperation.GREATER_THAN_EQUALS -> builder.greaterThanOrEqualTo(path[fieldName], value as LocalDate) + SearchOperation.LESS_THAN_EQUALS -> builder.lessThanOrEqualTo(path[fieldName], value as LocalDate) else -> super.buildPredicate(builder, path, fieldName, ops, value) } } diff --git a/src/main/kotlin/com/sipios/springsearch/strategies/LocalDateTimeStrategy.kt b/src/main/kotlin/com/sipios/springsearch/strategies/LocalDateTimeStrategy.kt index 18e691f..763dae5 100644 --- a/src/main/kotlin/com/sipios/springsearch/strategies/LocalDateTimeStrategy.kt +++ b/src/main/kotlin/com/sipios/springsearch/strategies/LocalDateTimeStrategy.kt @@ -18,6 +18,8 @@ class LocalDateTimeStrategy : ParsingStrategy { return when (ops) { SearchOperation.GREATER_THAN -> builder.greaterThan(path[fieldName], value as LocalDateTime) SearchOperation.LESS_THAN -> builder.lessThan(path[fieldName], value as LocalDateTime) + SearchOperation.GREATER_THAN_EQUALS -> builder.greaterThanOrEqualTo(path[fieldName], value as LocalDateTime) + SearchOperation.LESS_THAN_EQUALS -> builder.lessThanOrEqualTo(path[fieldName], value as LocalDateTime) else -> super.buildPredicate(builder, path, fieldName, ops, value) } } diff --git a/src/main/kotlin/com/sipios/springsearch/strategies/LocalTimeStrategy.kt b/src/main/kotlin/com/sipios/springsearch/strategies/LocalTimeStrategy.kt index 998c8de..7682689 100644 --- a/src/main/kotlin/com/sipios/springsearch/strategies/LocalTimeStrategy.kt +++ b/src/main/kotlin/com/sipios/springsearch/strategies/LocalTimeStrategy.kt @@ -18,6 +18,8 @@ class LocalTimeStrategy : ParsingStrategy { return when (ops) { SearchOperation.GREATER_THAN -> builder.greaterThan(path[fieldName], value as LocalTime) SearchOperation.LESS_THAN -> builder.lessThan(path[fieldName], value as LocalTime) + SearchOperation.GREATER_THAN_EQUALS -> builder.greaterThanOrEqualTo(path[fieldName], value as LocalTime) + SearchOperation.LESS_THAN_EQUALS -> builder.lessThanOrEqualTo(path[fieldName], value as LocalTime) else -> super.buildPredicate(builder, path, fieldName, ops, value) } } diff --git a/src/test/kotlin/com/sipios/springsearch/SpringSearchApplicationTest.kt b/src/test/kotlin/com/sipios/springsearch/SpringSearchApplicationTest.kt index 453bc73..4123a0c 100644 --- a/src/test/kotlin/com/sipios/springsearch/SpringSearchApplicationTest.kt +++ b/src/test/kotlin/com/sipios/springsearch/SpringSearchApplicationTest.kt @@ -592,6 +592,44 @@ class SpringSearchApplicationTest { Assertions.assertEquals(0, specificationUsers.size) } + @Test + fun canGetUsersAfterEqualDate() { + val sdf = StdDateFormat() + userRepository.save(Users(createdAt = sdf.parse("2019-01-01"))) + userRepository.save(Users(createdAt = sdf.parse("2019-01-03"))) + + var specification = SpecificationsBuilder( + SearchSpec::class.constructors.first().call("", true) + ).withSearch("createdAt>='2019-01-01'").build() + var specificationUsers = userRepository.findAll(specification) + Assertions.assertEquals(2, specificationUsers.size) + + specification = SpecificationsBuilder( + SearchSpec::class.constructors.first().call("", true) + ).withSearch("createdAt>='2019-01-04'").build() + specificationUsers = userRepository.findAll(specification) + Assertions.assertEquals(0, specificationUsers.size) + } + + @Test + fun canGetUsersEarlierEqualDate() { + val sdf = StdDateFormat() + userRepository.save(Users(createdAt = sdf.parse("2019-01-01"))) + userRepository.save(Users(createdAt = sdf.parse("2019-01-03"))) + + var specification = SpecificationsBuilder( + SearchSpec::class.constructors.first().call("", true) + ).withSearch("createdAt<='2019-01-01'").build() + var specificationUsers = userRepository.findAll(specification) + Assertions.assertEquals(1, specificationUsers.size) + + specification = SpecificationsBuilder( + SearchSpec::class.constructors.first().call("", true) + ).withSearch("createdAt<='2019-01-03'").build() + specificationUsers = userRepository.findAll(specification) + Assertions.assertEquals(2, specificationUsers.size) + } + @Test fun canGetUsersAtPreciseDate() { val sdf = StdDateFormat() @@ -742,7 +780,7 @@ class SpringSearchApplicationTest { } @Test - fun canGetUsersWithUpdatedInstantAtGreaterSearch() { + fun canGetUsersWithUpdateInstantAtGreaterSearch() { userRepository.save( Users( userFirstName = "HamidReza", @@ -760,10 +798,10 @@ class SpringSearchApplicationTest { } @Test - fun canGetUsersWithUpdatedInstantAtLessSearch() { + fun canGetUsersWithUpdateInstantAtGreaterThanEqualSearch() { userRepository.save( Users( - userFirstName = "HamidReza", + userFirstName = "john", updatedInstantAt = Instant.parse("2020-01-10T10:15:30Z") ) ) @@ -771,17 +809,17 @@ class SpringSearchApplicationTest { val specification = SpecificationsBuilder( SearchSpec::class.constructors.first().call("", false) - ).withSearch("updatedInstantAt<'2020-01-11T10:17:30Z'").build() + ).withSearch("updatedInstantAt>='2020-01-11T09:20:30Z'").build() val robotUsers = userRepository.findAll(specification) Assertions.assertEquals(1, robotUsers.size) - Assertions.assertEquals("HamidReza", robotUsers[0].userFirstName) + Assertions.assertEquals("robot", robotUsers[0].userFirstName) } @Test - fun canGetUsersWithUpdatedInstantAtEqualSearch() { + fun canGetUsersWithUpdateInstantAtLessThanEqualSearch() { userRepository.save( Users( - userFirstName = "HamidReza", + userFirstName = "john", updatedInstantAt = Instant.parse("2020-01-10T10:15:30Z") ) ) @@ -789,10 +827,10 @@ class SpringSearchApplicationTest { val specification = SpecificationsBuilder( SearchSpec::class.constructors.first().call("", false) - ).withSearch("updatedInstantAt:'2020-01-10T10:15:30Z'").build() + ).withSearch("updatedInstantAt<='2020-01-11T09:20:30Z'").build() val robotUsers = userRepository.findAll(specification) Assertions.assertEquals(1, robotUsers.size) - Assertions.assertEquals("HamidReza", robotUsers[0].userFirstName) + Assertions.assertEquals("john", robotUsers[0].userFirstName) } @Test @@ -834,6 +872,32 @@ class SpringSearchApplicationTest { Assertions.assertEquals("HamidReza", hamidrezaUsers[0].userFirstName) } + @Test + fun canGetUsersWithUpdatedAtGreaterThanEqualSearch() { + userRepository.save(Users(userFirstName = "john", updatedAt = LocalDateTime.parse("2020-01-10T10:15:30"))) + userRepository.save(Users(userFirstName = "robot", updatedAt = LocalDateTime.parse("2020-01-11T10:20:30"))) + userRepository.save(Users(userFirstName = "robot2", updatedAt = LocalDateTime.parse("2020-01-12T10:20:30"))) + val specification = SpecificationsBuilder( + SearchSpec::class.constructors.first().call("", false) + ).withSearch("updatedAt>='2020-01-11T10:20:30'").build() + val users = userRepository.findAll(specification) + Assertions.assertEquals(2, users.size) + Assertions.assertFalse(users.any { user -> user.userFirstName == "john" }) + } + + @Test + fun canGetUsersWithUpdatedAtLessThanEqualSearch() { + userRepository.save(Users(userFirstName = "john", updatedAt = LocalDateTime.parse("2020-01-10T10:15:30"))) + userRepository.save(Users(userFirstName = "robot", updatedAt = LocalDateTime.parse("2020-01-11T10:20:30"))) + userRepository.save(Users(userFirstName = "robot2", updatedAt = LocalDateTime.parse("2020-01-12T10:20:30"))) + val specification = SpecificationsBuilder( + SearchSpec::class.constructors.first().call("", false) + ).withSearch("updatedAt<='2020-01-11T10:20:30'").build() + val users = userRepository.findAll(specification) + Assertions.assertEquals(2, users.size) + Assertions.assertFalse(users.any { user -> user.userFirstName == "robot2" }) + } + @Test fun canGetUsersWithUpdatedDateAtGreaterSearch() { userRepository.save(Users(userFirstName = "HamidReza", updatedDateAt = LocalDate.parse("2020-01-10"))) @@ -886,6 +950,34 @@ class SpringSearchApplicationTest { Assertions.assertEquals("HamidReza", hamidrezaUsers[0].userFirstName) } + @Test + fun canGetUsersWithUpdatedDateAtLessThanEqualSearch() { + userRepository.save(Users(userFirstName = "john", updatedDateAt = LocalDate.parse("2020-01-10"))) + userRepository.save(Users(userFirstName = "robot", updatedDateAt = LocalDate.parse("2020-01-11"))) + userRepository.save(Users(userFirstName = "robot2", updatedDateAt = LocalDate.parse("2020-01-12"))) + + val specification = SpecificationsBuilder( + SearchSpec::class.constructors.first().call("", false) + ).withSearch("updatedDateAt<='2020-01-11'").build() + val users = userRepository.findAll(specification) + Assertions.assertEquals(2, users.size) + Assertions.assertFalse(users.any { user -> user.userFirstName == "robot2" }) + } + + @Test + fun canGetUsersWithUpdatedDateAtGreaterThanEqualSearch() { + userRepository.save(Users(userFirstName = "john", updatedDateAt = LocalDate.parse("2020-01-10"))) + userRepository.save(Users(userFirstName = "robot", updatedDateAt = LocalDate.parse("2020-01-11"))) + userRepository.save(Users(userFirstName = "robot2", updatedDateAt = LocalDate.parse("2020-01-12"))) + + val specification = SpecificationsBuilder( + SearchSpec::class.constructors.first().call("", false) + ).withSearch("updatedDateAt>='2020-01-11'").build() + val users = userRepository.findAll(specification) + Assertions.assertEquals(2, users.size) + Assertions.assertFalse(users.any { user -> user.userFirstName == "john" }) + } + @Test fun canGetUsersWithUpdatedTimeAtGreaterSearch() { userRepository.save(Users(userFirstName = "HamidReza", updatedTimeAt = LocalTime.parse("10:15:30"))) @@ -925,6 +1017,34 @@ class SpringSearchApplicationTest { Assertions.assertEquals("HamidReza", hamidrezaUsers[0].userFirstName) } + @Test + fun canGetUsersWithUpdatedTimeAtLessThanEqualSearch() { + userRepository.save(Users(userFirstName = "john", updatedTimeAt = LocalTime.parse("10:15:30"))) + userRepository.save(Users(userFirstName = "robot", updatedTimeAt = LocalTime.parse("10:20:30"))) + userRepository.save(Users(userFirstName = "robot2", updatedTimeAt = LocalTime.parse("10:25:30"))) + + val specification = SpecificationsBuilder( + SearchSpec::class.constructors.first().call("", false) + ).withSearch("updatedTimeAt<='10:20:30'").build() + val users = userRepository.findAll(specification) + Assertions.assertEquals(2, users.size) + Assertions.assertFalse(users.any { user -> user.userFirstName == "robot2" }) + } + + @Test + fun canGetUsersWithUpdatedTimeAtGreaterThanEqualSearch() { + userRepository.save(Users(userFirstName = "john", updatedTimeAt = LocalTime.parse("10:15:30"))) + userRepository.save(Users(userFirstName = "robot", updatedTimeAt = LocalTime.parse("10:20:30"))) + userRepository.save(Users(userFirstName = "robot2", updatedTimeAt = LocalTime.parse("10:25:30"))) + + val specification = SpecificationsBuilder( + SearchSpec::class.constructors.first().call("", false) + ).withSearch("updatedTimeAt>='10:20:30'").build() + val users = userRepository.findAll(specification) + Assertions.assertEquals(2, users.size) + Assertions.assertFalse(users.any { user -> user.userFirstName == "john" }) + } + @Test fun canGetUsersWithUpdatedTimeAtNotEqualSearch() { userRepository.save(Users(userFirstName = "HamidReza", updatedTimeAt = LocalTime.parse("10:15:30"))) From 23d10916fae616a4e9426e2d3e7b8a3695876769 Mon Sep 17 00:00:00 2001 From: vincentescoffier Date: Thu, 4 Jan 2024 11:28:18 +0100 Subject: [PATCH 05/18] update documentation --- README.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 3143ca7..745f8ab 100644 --- a/README.md +++ b/README.md @@ -134,10 +134,14 @@ Request : `/cars?search=color!Red` 3. Using the greater than operator `>` Request : `/cars?search=creationyear>2017` +> Note: You can use the `>=` operator as well. + ![greater than operator example](./docs/images/greater-than-example.gif) 4. Using the less than operator `<` -Request : `/cars?search=price<100000` +Request : `/cars?search=price<100000` +> Note: You can use the `<=` operator as well. + ![less than operator example](./docs/images/less-than-example.gif) 5. Using the starts with operator `*` From 77d56db4e47f675534e08d6e4c5971dc97c9a9ee Mon Sep 17 00:00:00 2001 From: vincentescoffier Date: Thu, 4 Jan 2024 16:12:38 +0100 Subject: [PATCH 06/18] change from = to : --- README.md | 4 +-- src/main/antlr4/Query.g4 | 4 +-- .../sipios/springsearch/SearchOperation.kt | 6 ++-- .../SpringSearchApplicationTest.kt | 28 +++++++++---------- 4 files changed, 21 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index 745f8ab..97c9161 100644 --- a/README.md +++ b/README.md @@ -134,13 +134,13 @@ Request : `/cars?search=color!Red` 3. Using the greater than operator `>` Request : `/cars?search=creationyear>2017` -> Note: You can use the `>=` operator as well. +> Note: You can use the `>:` operator as well. ![greater than operator example](./docs/images/greater-than-example.gif) 4. Using the less than operator `<` Request : `/cars?search=price<100000` -> Note: You can use the `<=` operator as well. +> Note: You can use the `<:` operator as well. ![less than operator example](./docs/images/less-than-example.gif) diff --git a/src/main/antlr4/Query.g4 b/src/main/antlr4/Query.g4 index 36a722c..0bf8374 100644 --- a/src/main/antlr4/Query.g4 +++ b/src/main/antlr4/Query.g4 @@ -140,7 +140,7 @@ GT ; GTE - : '>=' + : '>:' ; LT @@ -148,7 +148,7 @@ LT ; LTE - : '<=' + : '<:' ; EQ diff --git a/src/main/kotlin/com/sipios/springsearch/SearchOperation.kt b/src/main/kotlin/com/sipios/springsearch/SearchOperation.kt index 333f5c5..f8f024a 100644 --- a/src/main/kotlin/com/sipios/springsearch/SearchOperation.kt +++ b/src/main/kotlin/com/sipios/springsearch/SearchOperation.kt @@ -4,7 +4,7 @@ enum class SearchOperation { EQUALS, NOT_EQUALS, GREATER_THAN, LESS_THAN, STARTS_WITH, ENDS_WITH, CONTAINS, DOESNT_START_WITH, DOESNT_END_WITH, DOESNT_CONTAIN, GREATER_THAN_EQUALS, LESS_THAN_EQUALS; companion object { - val SIMPLE_OPERATION_SET = arrayOf(":", "!", ">", "<", "~", ">=", "<=") + val SIMPLE_OPERATION_SET = arrayOf(":", "!", ">", "<", "~", ">:", "<:") val ZERO_OR_MORE_REGEX = "*" val OR_OPERATOR = "OR" val AND_OPERATOR = "AND" @@ -23,8 +23,8 @@ enum class SearchOperation { "!" -> NOT_EQUALS ">" -> GREATER_THAN "<" -> LESS_THAN - ">=" -> GREATER_THAN_EQUALS - "<=" -> LESS_THAN_EQUALS + ">:" -> GREATER_THAN_EQUALS + "<:" -> LESS_THAN_EQUALS else -> null } } diff --git a/src/test/kotlin/com/sipios/springsearch/SpringSearchApplicationTest.kt b/src/test/kotlin/com/sipios/springsearch/SpringSearchApplicationTest.kt index 4123a0c..492531b 100644 --- a/src/test/kotlin/com/sipios/springsearch/SpringSearchApplicationTest.kt +++ b/src/test/kotlin/com/sipios/springsearch/SpringSearchApplicationTest.kt @@ -600,13 +600,13 @@ class SpringSearchApplicationTest { var specification = SpecificationsBuilder( SearchSpec::class.constructors.first().call("", true) - ).withSearch("createdAt>='2019-01-01'").build() + ).withSearch("createdAt>:'2019-01-01'").build() var specificationUsers = userRepository.findAll(specification) Assertions.assertEquals(2, specificationUsers.size) specification = SpecificationsBuilder( SearchSpec::class.constructors.first().call("", true) - ).withSearch("createdAt>='2019-01-04'").build() + ).withSearch("createdAt>:'2019-01-04'").build() specificationUsers = userRepository.findAll(specification) Assertions.assertEquals(0, specificationUsers.size) } @@ -619,13 +619,13 @@ class SpringSearchApplicationTest { var specification = SpecificationsBuilder( SearchSpec::class.constructors.first().call("", true) - ).withSearch("createdAt<='2019-01-01'").build() + ).withSearch("createdAt<:'2019-01-01'").build() var specificationUsers = userRepository.findAll(specification) Assertions.assertEquals(1, specificationUsers.size) specification = SpecificationsBuilder( SearchSpec::class.constructors.first().call("", true) - ).withSearch("createdAt<='2019-01-03'").build() + ).withSearch("createdAt<:'2019-01-03'").build() specificationUsers = userRepository.findAll(specification) Assertions.assertEquals(2, specificationUsers.size) } @@ -809,7 +809,7 @@ class SpringSearchApplicationTest { val specification = SpecificationsBuilder( SearchSpec::class.constructors.first().call("", false) - ).withSearch("updatedInstantAt>='2020-01-11T09:20:30Z'").build() + ).withSearch("updatedInstantAt>:'2020-01-11T09:20:30Z'").build() val robotUsers = userRepository.findAll(specification) Assertions.assertEquals(1, robotUsers.size) Assertions.assertEquals("robot", robotUsers[0].userFirstName) @@ -827,7 +827,7 @@ class SpringSearchApplicationTest { val specification = SpecificationsBuilder( SearchSpec::class.constructors.first().call("", false) - ).withSearch("updatedInstantAt<='2020-01-11T09:20:30Z'").build() + ).withSearch("updatedInstantAt<:'2020-01-11T09:20:30Z'").build() val robotUsers = userRepository.findAll(specification) Assertions.assertEquals(1, robotUsers.size) Assertions.assertEquals("john", robotUsers[0].userFirstName) @@ -879,7 +879,7 @@ class SpringSearchApplicationTest { userRepository.save(Users(userFirstName = "robot2", updatedAt = LocalDateTime.parse("2020-01-12T10:20:30"))) val specification = SpecificationsBuilder( SearchSpec::class.constructors.first().call("", false) - ).withSearch("updatedAt>='2020-01-11T10:20:30'").build() + ).withSearch("updatedAt>:'2020-01-11T10:20:30'").build() val users = userRepository.findAll(specification) Assertions.assertEquals(2, users.size) Assertions.assertFalse(users.any { user -> user.userFirstName == "john" }) @@ -892,7 +892,7 @@ class SpringSearchApplicationTest { userRepository.save(Users(userFirstName = "robot2", updatedAt = LocalDateTime.parse("2020-01-12T10:20:30"))) val specification = SpecificationsBuilder( SearchSpec::class.constructors.first().call("", false) - ).withSearch("updatedAt<='2020-01-11T10:20:30'").build() + ).withSearch("updatedAt<:'2020-01-11T10:20:30'").build() val users = userRepository.findAll(specification) Assertions.assertEquals(2, users.size) Assertions.assertFalse(users.any { user -> user.userFirstName == "robot2" }) @@ -958,7 +958,7 @@ class SpringSearchApplicationTest { val specification = SpecificationsBuilder( SearchSpec::class.constructors.first().call("", false) - ).withSearch("updatedDateAt<='2020-01-11'").build() + ).withSearch("updatedDateAt<:'2020-01-11'").build() val users = userRepository.findAll(specification) Assertions.assertEquals(2, users.size) Assertions.assertFalse(users.any { user -> user.userFirstName == "robot2" }) @@ -972,7 +972,7 @@ class SpringSearchApplicationTest { val specification = SpecificationsBuilder( SearchSpec::class.constructors.first().call("", false) - ).withSearch("updatedDateAt>='2020-01-11'").build() + ).withSearch("updatedDateAt>:'2020-01-11'").build() val users = userRepository.findAll(specification) Assertions.assertEquals(2, users.size) Assertions.assertFalse(users.any { user -> user.userFirstName == "john" }) @@ -1025,7 +1025,7 @@ class SpringSearchApplicationTest { val specification = SpecificationsBuilder( SearchSpec::class.constructors.first().call("", false) - ).withSearch("updatedTimeAt<='10:20:30'").build() + ).withSearch("updatedTimeAt<:'10:20:30'").build() val users = userRepository.findAll(specification) Assertions.assertEquals(2, users.size) Assertions.assertFalse(users.any { user -> user.userFirstName == "robot2" }) @@ -1039,7 +1039,7 @@ class SpringSearchApplicationTest { val specification = SpecificationsBuilder( SearchSpec::class.constructors.first().call("", false) - ).withSearch("updatedTimeAt>='10:20:30'").build() + ).withSearch("updatedTimeAt>:'10:20:30'").build() val users = userRepository.findAll(specification) Assertions.assertEquals(2, users.size) Assertions.assertFalse(users.any { user -> user.userFirstName == "john" }) @@ -1143,7 +1143,7 @@ class SpringSearchApplicationTest { userRepository.save(Users(userFirstName = "joe", userChildrenNumber = 4)) val specification = SpecificationsBuilder( SearchSpec::class.constructors.first().call("", false) - ).withSearch("userChildrenNumber<=2").build() + ).withSearch("userChildrenNumber<:2").build() val users = userRepository.findAll(specification) Assertions.assertEquals(1, users.size) Assertions.assertEquals("john", users[0].userFirstName) @@ -1156,7 +1156,7 @@ class SpringSearchApplicationTest { userRepository.save(Users(userFirstName = "joe", userChildrenNumber = 4)) val specification = SpecificationsBuilder( SearchSpec::class.constructors.first().call("", false) - ).withSearch("userChildrenNumber>=3").build() + ).withSearch("userChildrenNumber>:3").build() val users = userRepository.findAll(specification) Assertions.assertEquals(2, users.size) } From dcd6a8891220804fe47643f1503f9cc1b43d25f2 Mon Sep 17 00:00:00 2001 From: vincentescoffier Date: Fri, 5 Jan 2024 09:34:18 +0100 Subject: [PATCH 07/18] draft for array grammar --- src/main/antlr4/Query.g4 | 33 ++++++++++++++++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/src/main/antlr4/Query.g4 b/src/main/antlr4/Query.g4 index 0bf8374..06a7d21 100644 --- a/src/main/antlr4/Query.g4 +++ b/src/main/antlr4/Query.g4 @@ -28,7 +28,7 @@ value | ENCODED_STRING | NUMBER | BOOL - ; + | ARRAY ; op : EQ @@ -37,8 +37,12 @@ op | LT | LTE | NOT_EQ + | IN + | NOT_IN ; + + BOOL : 'true' | 'false' @@ -49,6 +53,8 @@ STRING | '\'' SingleStringCharacter* '\'' ; + + fragment DoubleStringCharacter : ~["\\\r\n] | '\\' EscapeSequence @@ -134,6 +140,24 @@ RPAREN : ')' ; +LBRACKET + : '[' + ; + +RBRACKET + : ']' + ; + +fragment ARRAY_ELEMENT + : STRING + | NUMBER + | BOOL + | IDENTIFIER + ; + +ARRAY + : '[' (ARRAY_ELEMENT(','ARRAY_ELEMENT)* )?']' + ; GT : '>' @@ -155,6 +179,13 @@ EQ : ':' ; +IN + : 'IN' + ; + +NOT_IN + : 'NOT IN' + ; NOT_EQ : '!' From 65092bd41640d213cdee487903ee03f953b76cd0 Mon Sep 17 00:00:00 2001 From: vincentescoffier Date: Fri, 5 Jan 2024 09:52:11 +0100 Subject: [PATCH 08/18] we have to be specific about ws because we use a lexical rule --- src/main/antlr4/Query.g4 | 27 +++++++++++++-------------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/src/main/antlr4/Query.g4 b/src/main/antlr4/Query.g4 index 06a7d21..fff292d 100644 --- a/src/main/antlr4/Query.g4 +++ b/src/main/antlr4/Query.g4 @@ -4,6 +4,7 @@ Copyright (c) 2019, Michael Mollard grammar Query; +// syntactic rules input : query EOF ; @@ -42,7 +43,7 @@ op ; - +// lexical rules BOOL : 'true' | 'false' @@ -118,6 +119,16 @@ fragment HexDigit fragment OctalDigit : [0-7] ; +fragment ARRAY_ELEMENT + : STRING + | NUMBER + | BOOL + | IDENTIFIER + ; + +fragment POINT + : '.' + ; AND : 'AND' @@ -148,15 +159,8 @@ RBRACKET : ']' ; -fragment ARRAY_ELEMENT - : STRING - | NUMBER - | BOOL - | IDENTIFIER - ; - ARRAY - : '[' (ARRAY_ELEMENT(','ARRAY_ELEMENT)* )?']' + : '[' WS* (ARRAY_ELEMENT WS* (',' WS* ARRAY_ELEMENT WS*)* )?']' ; GT @@ -191,11 +195,6 @@ NOT_EQ : '!' ; - -fragment POINT - : '.' - ; - IDENTIFIER : [A-Za-z0-9.]+ ; From d59f56dea244185c386929c8143c0b04a3cfb345 Mon Sep 17 00:00:00 2001 From: vincentescoffier Date: Fri, 5 Jan 2024 10:55:41 +0100 Subject: [PATCH 09/18] array match before encoded_string --- src/main/antlr4/Query.g4 | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/antlr4/Query.g4 b/src/main/antlr4/Query.g4 index fff292d..91e1273 100644 --- a/src/main/antlr4/Query.g4 +++ b/src/main/antlr4/Query.g4 @@ -26,10 +26,11 @@ key value : IDENTIFIER | STRING + | ARRAY | ENCODED_STRING | NUMBER | BOOL - | ARRAY ; + ; op : EQ From 089255cfbe4c790e55dc32a1132a303f0cd77027 Mon Sep 17 00:00:00 2001 From: vincentescoffier Date: Fri, 5 Jan 2024 11:30:10 +0100 Subject: [PATCH 10/18] refactor grammar to ease parsing, array becomes a syntactic rule --- src/main/antlr4/Query.g4 | 35 +++++++++++++---------------------- 1 file changed, 13 insertions(+), 22 deletions(-) diff --git a/src/main/antlr4/Query.g4 b/src/main/antlr4/Query.g4 index 91e1273..3416c58 100644 --- a/src/main/antlr4/Query.g4 +++ b/src/main/antlr4/Query.g4 @@ -1,7 +1,3 @@ -/* -Copyright (c) 2019, Michael Mollard -*/ - grammar Query; // syntactic rules @@ -23,14 +19,18 @@ key : IDENTIFIER ; +array + : LBRACKET (value (COMMA value)* )? RBRACKET + ; + value - : IDENTIFIER + : array + | IDENTIFIER | STRING - | ARRAY | ENCODED_STRING | NUMBER | BOOL - ; + ; op : EQ @@ -43,7 +43,6 @@ op | NOT_IN ; - // lexical rules BOOL : 'true' @@ -55,8 +54,6 @@ STRING | '\'' SingleStringCharacter* '\'' ; - - fragment DoubleStringCharacter : ~["\\\r\n] | '\\' EscapeSequence @@ -120,12 +117,6 @@ fragment HexDigit fragment OctalDigit : [0-7] ; -fragment ARRAY_ELEMENT - : STRING - | NUMBER - | BOOL - | IDENTIFIER - ; fragment POINT : '.' @@ -160,10 +151,6 @@ RBRACKET : ']' ; -ARRAY - : '[' WS* (ARRAY_ELEMENT WS* (',' WS* ARRAY_ELEMENT WS*)* )?']' - ; - GT : '>' ; @@ -196,12 +183,16 @@ NOT_EQ : '!' ; +COMMA + : ',' + ; + IDENTIFIER : [A-Za-z0-9.]+ ; ENCODED_STRING - : ~([ :<>!()])+ + : '"' ( ~[\\"] | '\\' . )* '"' ; LineTerminator @@ -210,4 +201,4 @@ LineTerminator WS : [ \t\r\n]+ -> skip - ; + ; \ No newline at end of file From 238fd7111373959694eae23bf93b184de304b7dd Mon Sep 17 00:00:00 2001 From: vincentescoffier Date: Fri, 5 Jan 2024 11:41:02 +0100 Subject: [PATCH 11/18] parse array value in visitor --- .../sipios/springsearch/QueryVisitorImpl.kt | 23 ++++++++++++++----- 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/src/main/kotlin/com/sipios/springsearch/QueryVisitorImpl.kt b/src/main/kotlin/com/sipios/springsearch/QueryVisitorImpl.kt index 8eda3ce..506aacb 100644 --- a/src/main/kotlin/com/sipios/springsearch/QueryVisitorImpl.kt +++ b/src/main/kotlin/com/sipios/springsearch/QueryVisitorImpl.kt @@ -36,13 +36,18 @@ class QueryVisitorImpl(private val searchSpecAnnotation: SearchSpec) : QueryB var value = ctx.value()!!.text if (ctx.value().STRING() != null) { - value = value - .removeSurrounding("'") - .removeSurrounding("\"") - .replace("\\\"", "\"") - .replace("\\'", "'") + value = clearString(value) + } else if (ctx.value().array() != null) { + val arr = ctx.value().array() + val arrayValues = arr.value() + val valueAsList = arrayValues.map { v -> + if (v.STRING() != null) { + clearString(v.text) + } + v.text + } + value = valueAsList.joinToString(",") } - val matchResult = this.valueRegExp.find(value!!) val criteria = SearchCriteria( key, @@ -54,4 +59,10 @@ class QueryVisitorImpl(private val searchSpecAnnotation: SearchSpec) : QueryB return SpecificationImpl(criteria, searchSpecAnnotation) } + + private fun clearString(value: String) = value + .removeSurrounding("'") + .removeSurrounding("\"") + .replace("\\\"", "\"") + .replace("\\'", "'") } From 42673f1153080504b12aa4357226a18e3f3edb29 Mon Sep 17 00:00:00 2001 From: vincentescoffier Date: Fri, 5 Jan 2024 14:20:38 +0100 Subject: [PATCH 12/18] add simple test, change ENCODED_STRING def to exclude [] and comma add simple array parsing in visitor --- src/main/antlr4/Query.g4 | 20 ++++++++++--------- .../sipios/springsearch/QueryVisitorImpl.kt | 11 +++------- 2 files changed, 14 insertions(+), 17 deletions(-) diff --git a/src/main/antlr4/Query.g4 b/src/main/antlr4/Query.g4 index 3416c58..5c9c403 100644 --- a/src/main/antlr4/Query.g4 +++ b/src/main/antlr4/Query.g4 @@ -1,3 +1,7 @@ +/* +Copyright (c) 2019, Michael Mollard +*/ + grammar Query; // syntactic rules @@ -20,7 +24,7 @@ key ; array - : LBRACKET (value (COMMA value)* )? RBRACKET + : LBRACKET (value (',' value)* )? RBRACKET ; value @@ -118,10 +122,6 @@ fragment OctalDigit : [0-7] ; -fragment POINT - : '.' - ; - AND : 'AND' ; @@ -183,18 +183,20 @@ NOT_EQ : '!' ; -COMMA - : ',' + +fragment POINT + : '.' ; IDENTIFIER : [A-Za-z0-9.]+ ; -ENCODED_STRING - : '"' ( ~[\\"] | '\\' . )* '"' +ENCODED_STRING //anything but these characters :<>!()[], and whitespace + : ~([ ,:<>!()[\]])+ ; + LineTerminator : [\r\n\u2028\u2029] -> channel(HIDDEN) ; diff --git a/src/main/kotlin/com/sipios/springsearch/QueryVisitorImpl.kt b/src/main/kotlin/com/sipios/springsearch/QueryVisitorImpl.kt index 506aacb..14c7066 100644 --- a/src/main/kotlin/com/sipios/springsearch/QueryVisitorImpl.kt +++ b/src/main/kotlin/com/sipios/springsearch/QueryVisitorImpl.kt @@ -34,20 +34,15 @@ class QueryVisitorImpl(private val searchSpecAnnotation: SearchSpec) : QueryB val key = ctx.key()!!.text val op = ctx.op()!!.text var value = ctx.value()!!.text - + var valueAsList: List? = null if (ctx.value().STRING() != null) { value = clearString(value) } else if (ctx.value().array() != null) { val arr = ctx.value().array() val arrayValues = arr.value() - val valueAsList = arrayValues.map { v -> - if (v.STRING() != null) { - clearString(v.text) - } - v.text - } - value = valueAsList.joinToString(",") + valueAsList = arrayValues.map({if (it.STRING() != null) clearString(it.text) else it.text}) } + val matchResult = this.valueRegExp.find(value!!) val criteria = SearchCriteria( key, From 4bd3467f74bc97ab3587ed5a82861a52be62089e Mon Sep 17 00:00:00 2001 From: vincentescoffier Date: Fri, 5 Jan 2024 16:17:21 +0100 Subject: [PATCH 13/18] update strategies --- .../sipios/springsearch/QueryVisitorImpl.kt | 13 +++- .../com/sipios/springsearch/SearchCriteria.kt | 2 +- .../sipios/springsearch/SearchOperation.kt | 4 +- .../strategies/BooleanStrategy.kt | 6 +- .../springsearch/strategies/DateStrategy.kt | 6 +- .../springsearch/strategies/DoubleStrategy.kt | 6 +- .../strategies/DurationStrategy.kt | 6 +- .../springsearch/strategies/EnumStrategy.kt | 9 ++- .../springsearch/strategies/FloatStrategy.kt | 6 +- .../strategies/InstantStrategy.kt | 6 +- .../springsearch/strategies/IntStrategy.kt | 6 +- .../strategies/LocalDateStrategy.kt | 6 +- .../strategies/LocalDateTimeStrategy.kt | 6 +- .../strategies/LocalTimeStrategy.kt | 6 +- .../strategies/ParsingStrategy.kt | 26 ++++++- .../springsearch/strategies/StringStrategy.kt | 3 + .../springsearch/strategies/UUIDStrategy.kt | 6 +- .../SpringSearchApplicationTest.kt | 71 +++++++++++++++++++ 18 files changed, 165 insertions(+), 29 deletions(-) diff --git a/src/main/kotlin/com/sipios/springsearch/QueryVisitorImpl.kt b/src/main/kotlin/com/sipios/springsearch/QueryVisitorImpl.kt index 14c7066..23df9df 100644 --- a/src/main/kotlin/com/sipios/springsearch/QueryVisitorImpl.kt +++ b/src/main/kotlin/com/sipios/springsearch/QueryVisitorImpl.kt @@ -40,9 +40,18 @@ class QueryVisitorImpl(private val searchSpecAnnotation: SearchSpec) : QueryB } else if (ctx.value().array() != null) { val arr = ctx.value().array() val arrayValues = arr.value() - valueAsList = arrayValues.map({if (it.STRING() != null) clearString(it.text) else it.text}) + valueAsList = arrayValues.map({ if (it.STRING() != null) clearString(it.text) else it.text }) + } + if (valueAsList != null) { + val criteria = SearchCriteria( + key, + op, + null, + valueAsList, + null + ) + return SpecificationImpl(criteria, searchSpecAnnotation) } - val matchResult = this.valueRegExp.find(value!!) val criteria = SearchCriteria( key, diff --git a/src/main/kotlin/com/sipios/springsearch/SearchCriteria.kt b/src/main/kotlin/com/sipios/springsearch/SearchCriteria.kt index adcc69c..faf04f1 100644 --- a/src/main/kotlin/com/sipios/springsearch/SearchCriteria.kt +++ b/src/main/kotlin/com/sipios/springsearch/SearchCriteria.kt @@ -1,7 +1,7 @@ package com.sipios.springsearch class SearchCriteria // Change EQUALS into ENDS_WITH, CONTAINS, STARTS_WITH based on the presence of * in the value - (var key: String, operation: String, prefix: String?, var value: String, suffix: String?) { + (var key: String, operation: String, prefix: String?, var value: Any?, suffix: String?) { var operation: SearchOperation? init { diff --git a/src/main/kotlin/com/sipios/springsearch/SearchOperation.kt b/src/main/kotlin/com/sipios/springsearch/SearchOperation.kt index f8f024a..1ae19e9 100644 --- a/src/main/kotlin/com/sipios/springsearch/SearchOperation.kt +++ b/src/main/kotlin/com/sipios/springsearch/SearchOperation.kt @@ -1,7 +1,7 @@ package com.sipios.springsearch enum class SearchOperation { - EQUALS, NOT_EQUALS, GREATER_THAN, LESS_THAN, STARTS_WITH, ENDS_WITH, CONTAINS, DOESNT_START_WITH, DOESNT_END_WITH, DOESNT_CONTAIN, GREATER_THAN_EQUALS, LESS_THAN_EQUALS; + EQUALS, NOT_EQUALS, GREATER_THAN, LESS_THAN, STARTS_WITH, ENDS_WITH, CONTAINS, DOESNT_START_WITH, DOESNT_END_WITH, DOESNT_CONTAIN, GREATER_THAN_EQUALS, LESS_THAN_EQUALS, IN, NOT_IN; companion object { val SIMPLE_OPERATION_SET = arrayOf(":", "!", ">", "<", "~", ">:", "<:") @@ -25,6 +25,8 @@ enum class SearchOperation { "<" -> LESS_THAN ">:" -> GREATER_THAN_EQUALS "<:" -> LESS_THAN_EQUALS + "IN" -> IN + "NOT IN" -> NOT_IN else -> null } } diff --git a/src/main/kotlin/com/sipios/springsearch/strategies/BooleanStrategy.kt b/src/main/kotlin/com/sipios/springsearch/strategies/BooleanStrategy.kt index b87c182..b1967c8 100644 --- a/src/main/kotlin/com/sipios/springsearch/strategies/BooleanStrategy.kt +++ b/src/main/kotlin/com/sipios/springsearch/strategies/BooleanStrategy.kt @@ -3,7 +3,9 @@ package com.sipios.springsearch.strategies import kotlin.reflect.KClass class BooleanStrategy : ParsingStrategy { - override fun parse(value: String?, fieldClass: KClass): Any? { - return value?.toBoolean() + override fun parse(value: Any?, fieldClass: KClass): Any? { + if (value is String) return value.toBoolean() + if (value is List<*>) return value.map { it.toString().toBoolean() } + return value } } diff --git a/src/main/kotlin/com/sipios/springsearch/strategies/DateStrategy.kt b/src/main/kotlin/com/sipios/springsearch/strategies/DateStrategy.kt index 61f5c69..048a1dd 100644 --- a/src/main/kotlin/com/sipios/springsearch/strategies/DateStrategy.kt +++ b/src/main/kotlin/com/sipios/springsearch/strategies/DateStrategy.kt @@ -28,7 +28,9 @@ class DateStrategy : ParsingStrategy { } } - override fun parse(value: String?, fieldClass: KClass): Any? { - return standardDateFormat.parse(value) + override fun parse(value: Any?, fieldClass: KClass): Any? { + if (value is String) return standardDateFormat.parse(value) + if (value is List<*>) return value.map { standardDateFormat.parse(it.toString()) } + return value } } diff --git a/src/main/kotlin/com/sipios/springsearch/strategies/DoubleStrategy.kt b/src/main/kotlin/com/sipios/springsearch/strategies/DoubleStrategy.kt index 7b1ccc0..3bbfcee 100644 --- a/src/main/kotlin/com/sipios/springsearch/strategies/DoubleStrategy.kt +++ b/src/main/kotlin/com/sipios/springsearch/strategies/DoubleStrategy.kt @@ -23,7 +23,9 @@ class DoubleStrategy : ParsingStrategy { } } - override fun parse(value: String?, fieldClass: KClass): Any? { - return value?.toDouble() + override fun parse(value: Any?, fieldClass: KClass): Any? { + if (value is String) return value.toDouble() + if (value is List<*>) return value.map { it.toString().toDouble() } + return value } } diff --git a/src/main/kotlin/com/sipios/springsearch/strategies/DurationStrategy.kt b/src/main/kotlin/com/sipios/springsearch/strategies/DurationStrategy.kt index a722fe2..825e53b 100644 --- a/src/main/kotlin/com/sipios/springsearch/strategies/DurationStrategy.kt +++ b/src/main/kotlin/com/sipios/springsearch/strategies/DurationStrategy.kt @@ -24,7 +24,9 @@ class DurationStrategy : ParsingStrategy { } } - override fun parse(value: String?, fieldClass: KClass): Any? { - return Duration.parse(value) + override fun parse(value: Any?, fieldClass: KClass): Any? { + if (value is String) return Duration.parse(value) + if (value is List<*>) return value.map { Duration.parse(it.toString()) } + return value } } diff --git a/src/main/kotlin/com/sipios/springsearch/strategies/EnumStrategy.kt b/src/main/kotlin/com/sipios/springsearch/strategies/EnumStrategy.kt index 4e74ee5..fdcacd9 100644 --- a/src/main/kotlin/com/sipios/springsearch/strategies/EnumStrategy.kt +++ b/src/main/kotlin/com/sipios/springsearch/strategies/EnumStrategy.kt @@ -3,7 +3,12 @@ package com.sipios.springsearch.strategies import kotlin.reflect.KClass class EnumStrategy : ParsingStrategy { - override fun parse(value: String?, fieldClass: KClass): Any? { - return Class.forName(fieldClass.qualifiedName).getMethod("valueOf", String::class.java).invoke(null, value) + override fun parse(value: Any?, fieldClass: KClass): Any? { + if (value is String) return toValue(fieldClass, value) + if (value is List<*>) return value.map { toValue(fieldClass, it.toString()) } + return value } + + private fun toValue(fieldClass: KClass, value: Any?): Any? = + Class.forName(fieldClass.qualifiedName).getMethod("valueOf", String::class.java).invoke(null, value) } diff --git a/src/main/kotlin/com/sipios/springsearch/strategies/FloatStrategy.kt b/src/main/kotlin/com/sipios/springsearch/strategies/FloatStrategy.kt index ea1d1e6..86d5b1d 100644 --- a/src/main/kotlin/com/sipios/springsearch/strategies/FloatStrategy.kt +++ b/src/main/kotlin/com/sipios/springsearch/strategies/FloatStrategy.kt @@ -23,7 +23,9 @@ class FloatStrategy : ParsingStrategy { } } - override fun parse(value: String?, fieldClass: KClass): Any? { - return value?.toFloat() + override fun parse(value: Any?, fieldClass: KClass): Any? { + if (value is String) return value.toFloat() + if (value is List<*>) return value.map { it.toString().toFloat() } + return value } } diff --git a/src/main/kotlin/com/sipios/springsearch/strategies/InstantStrategy.kt b/src/main/kotlin/com/sipios/springsearch/strategies/InstantStrategy.kt index 66294cd..99cd3ca 100644 --- a/src/main/kotlin/com/sipios/springsearch/strategies/InstantStrategy.kt +++ b/src/main/kotlin/com/sipios/springsearch/strategies/InstantStrategy.kt @@ -24,7 +24,9 @@ class InstantStrategy : ParsingStrategy { } } - override fun parse(value: String?, fieldClass: KClass): Any? { - return Instant.parse(value) + override fun parse(value: Any?, fieldClass: KClass): Any? { + if (value is String) return Instant.parse(value) + if (value is List<*>) return value.map { Instant.parse(it.toString()) } + return value } } diff --git a/src/main/kotlin/com/sipios/springsearch/strategies/IntStrategy.kt b/src/main/kotlin/com/sipios/springsearch/strategies/IntStrategy.kt index 21f3863..526aef6 100644 --- a/src/main/kotlin/com/sipios/springsearch/strategies/IntStrategy.kt +++ b/src/main/kotlin/com/sipios/springsearch/strategies/IntStrategy.kt @@ -23,7 +23,9 @@ class IntStrategy : ParsingStrategy { } } - override fun parse(value: String?, fieldClass: KClass): Any? { - return value?.toInt() + override fun parse(value: Any?, fieldClass: KClass): Any? { + if (value is String) return value.toInt() + if (value is List<*>) return value.map { it.toString().toInt() } + return value } } diff --git a/src/main/kotlin/com/sipios/springsearch/strategies/LocalDateStrategy.kt b/src/main/kotlin/com/sipios/springsearch/strategies/LocalDateStrategy.kt index 2b435f6..e16eda1 100644 --- a/src/main/kotlin/com/sipios/springsearch/strategies/LocalDateStrategy.kt +++ b/src/main/kotlin/com/sipios/springsearch/strategies/LocalDateStrategy.kt @@ -24,7 +24,9 @@ class LocalDateStrategy : ParsingStrategy { } } - override fun parse(value: String?, fieldClass: KClass): Any? { - return LocalDate.parse(value) + override fun parse(value: Any?, fieldClass: KClass): Any? { + if (value is String) return LocalDate.parse(value) + if (value is List<*>) return value.map { LocalDate.parse(it.toString()) } + return value } } diff --git a/src/main/kotlin/com/sipios/springsearch/strategies/LocalDateTimeStrategy.kt b/src/main/kotlin/com/sipios/springsearch/strategies/LocalDateTimeStrategy.kt index 763dae5..2d8a3c6 100644 --- a/src/main/kotlin/com/sipios/springsearch/strategies/LocalDateTimeStrategy.kt +++ b/src/main/kotlin/com/sipios/springsearch/strategies/LocalDateTimeStrategy.kt @@ -24,7 +24,9 @@ class LocalDateTimeStrategy : ParsingStrategy { } } - override fun parse(value: String?, fieldClass: KClass): Any? { - return LocalDateTime.parse(value) + override fun parse(value: Any?, fieldClass: KClass): Any? { + if (value is String) return LocalDateTime.parse(value) + if (value is List<*>) return value.map { LocalDateTime.parse(it.toString()) } + return value } } diff --git a/src/main/kotlin/com/sipios/springsearch/strategies/LocalTimeStrategy.kt b/src/main/kotlin/com/sipios/springsearch/strategies/LocalTimeStrategy.kt index 7682689..7d46851 100644 --- a/src/main/kotlin/com/sipios/springsearch/strategies/LocalTimeStrategy.kt +++ b/src/main/kotlin/com/sipios/springsearch/strategies/LocalTimeStrategy.kt @@ -24,7 +24,9 @@ class LocalTimeStrategy : ParsingStrategy { } } - override fun parse(value: String?, fieldClass: KClass): Any? { - return LocalTime.parse(value) + override fun parse(value: Any?, fieldClass: KClass): Any? { + if (value is String) return LocalTime.parse(value) + if (value is List<*>) return value.map { LocalTime.parse(it.toString()) } + return value } } diff --git a/src/main/kotlin/com/sipios/springsearch/strategies/ParsingStrategy.kt b/src/main/kotlin/com/sipios/springsearch/strategies/ParsingStrategy.kt index 611abff..1d808c0 100644 --- a/src/main/kotlin/com/sipios/springsearch/strategies/ParsingStrategy.kt +++ b/src/main/kotlin/com/sipios/springsearch/strategies/ParsingStrategy.kt @@ -23,7 +23,7 @@ interface ParsingStrategy { * @param fieldClass Kotlin class of the referred field * @return Returns by default the value without any parsing */ - fun parse(value: String?, fieldClass: KClass): Any? { + fun parse(value: Any?, fieldClass: KClass): Any? { return value } @@ -45,6 +45,16 @@ interface ParsingStrategy { value: Any? ): Predicate? { return when (ops) { + SearchOperation.IN -> { + val inClause: CriteriaBuilder.In = getInClause(builder, path, fieldName, value) + inClause + } + + SearchOperation.NOT_IN -> { + val inClause: CriteriaBuilder.In = getInClause(builder, path, fieldName, value) + builder.not(inClause) + } + SearchOperation.EQUALS -> builder.equal(path.get(fieldName), value) SearchOperation.NOT_EQUALS -> builder.notEqual(path.get(fieldName), value) SearchOperation.STARTS_WITH -> builder.like(path[fieldName], "$value%") @@ -56,10 +66,24 @@ interface ParsingStrategy { (path.get(fieldName).`as`(String::class.java)), "%$value%" ) + else -> null } } + fun getInClause( + builder: CriteriaBuilder, + path: Path<*>, + fieldName: String, + value: Any? + ): CriteriaBuilder.In { + val inClause: CriteriaBuilder.In = builder.`in`(path.get(fieldName)) + val values = value as List<*> + values.forEach { inClause.value(it) } + println("inClause: $inClause") + return inClause + } + companion object { fun getStrategy(fieldClass: KClass, searchSpecAnnotation: SearchSpec): ParsingStrategy { return when { diff --git a/src/main/kotlin/com/sipios/springsearch/strategies/StringStrategy.kt b/src/main/kotlin/com/sipios/springsearch/strategies/StringStrategy.kt index 3498959..bd394ca 100644 --- a/src/main/kotlin/com/sipios/springsearch/strategies/StringStrategy.kt +++ b/src/main/kotlin/com/sipios/springsearch/strategies/StringStrategy.kt @@ -15,6 +15,9 @@ data class StringStrategy(var searchSpecAnnotation: SearchSpec) : ParsingStrateg ops: SearchOperation?, value: Any? ): Predicate? { + if (ops == SearchOperation.NOT_IN || ops == SearchOperation.IN) { + return super.buildPredicate(builder, path, fieldName, ops, value) + } if (!searchSpecAnnotation.caseSensitiveFlag) { val lowerCasedValue = (value as String).lowercase(Locale.getDefault()) val loweredFieldValue = builder.lower(path[fieldName]) diff --git a/src/main/kotlin/com/sipios/springsearch/strategies/UUIDStrategy.kt b/src/main/kotlin/com/sipios/springsearch/strategies/UUIDStrategy.kt index 22542f2..e5525e4 100644 --- a/src/main/kotlin/com/sipios/springsearch/strategies/UUIDStrategy.kt +++ b/src/main/kotlin/com/sipios/springsearch/strategies/UUIDStrategy.kt @@ -4,7 +4,9 @@ import java.util.UUID import kotlin.reflect.KClass class UUIDStrategy : ParsingStrategy { - override fun parse(value: String?, fieldClass: KClass): Any? { - return UUID.fromString(value) + override fun parse(value: Any?, fieldClass: KClass): Any? { + if (value is String) return UUID.fromString(value) + if (value is List<*>) return value.map { UUID.fromString(it.toString()) } + return value } } diff --git a/src/test/kotlin/com/sipios/springsearch/SpringSearchApplicationTest.kt b/src/test/kotlin/com/sipios/springsearch/SpringSearchApplicationTest.kt index 492531b..4408ff0 100644 --- a/src/test/kotlin/com/sipios/springsearch/SpringSearchApplicationTest.kt +++ b/src/test/kotlin/com/sipios/springsearch/SpringSearchApplicationTest.kt @@ -1173,4 +1173,75 @@ class SpringSearchApplicationTest { Assertions.assertEquals(1, users.size) Assertions.assertEquals("john", users[0].userFirstName) } + + @Test + fun canGetUserWithNameIn() { + val johnId = userRepository.save(Users(userFirstName = "john", userChildrenNumber = 2)).userId + val janeId = userRepository.save(Users(userFirstName = "jane", userChildrenNumber = 3)).userId + userRepository.save(Users(userFirstName = "joe", userChildrenNumber = 4)) + val specification = SpecificationsBuilder( + SearchSpec::class.constructors.first().call("", false) + ).withSearch("userFirstName IN [\"john\", \"jane\"]").build() + val users = userRepository.findAll(specification) + Assertions.assertTrue(setOf(johnId, janeId) == users.map { user -> user.userId }.toSet()) + } + + @Test + fun canGetUserWithNameNotIn() { + userRepository.save(Users(userFirstName = "john", userChildrenNumber = 2)) + userRepository.save(Users(userFirstName = "jane", userChildrenNumber = 3)) + val joeId = userRepository.save(Users(userFirstName = "joe", userChildrenNumber = 4)).userId + val specification = SpecificationsBuilder( + SearchSpec::class.constructors.first().call("", false) + ).withSearch("userFirstName NOT IN [\"john\", \"jane\"]").build() + val users = userRepository.findAll(specification) + Assertions.assertTrue(setOf(joeId) == users.map { user -> user.userId }.toSet()) + } + + @Test + fun canGetUserWithChildrenNumberNotIn() { + userRepository.save(Users(userFirstName = "john", userChildrenNumber = 2)) + userRepository.save(Users(userFirstName = "jane", userChildrenNumber = 3)) + val joeId = userRepository.save(Users(userFirstName = "joe", userChildrenNumber = 4)).userId + val specification = SpecificationsBuilder( + SearchSpec::class.constructors.first().call("", false) + ).withSearch("userChildrenNumber NOT IN [2, 3]").build() + val users = userRepository.findAll(specification) + Assertions.assertTrue(setOf(joeId) == users.map { user -> user.userId }.toSet()) + } + + @Test + fun canGetUserWithChildrenNumberIn() { + val johnId = userRepository.save(Users(userFirstName = "john", userChildrenNumber = 2)).userId + val janeId = userRepository.save(Users(userFirstName = "jane", userChildrenNumber = 3)).userId + userRepository.save(Users(userFirstName = "joe", userChildrenNumber = 4)) + val specification = SpecificationsBuilder( + SearchSpec::class.constructors.first().call("", false) + ).withSearch("userChildrenNumber IN [2, 3]").build() + val users = userRepository.findAll(specification) + Assertions.assertTrue(setOf(janeId, johnId) == users.map { user -> user.userId }.toSet()) + } + + @Test + fun canGetUserWithTypeIn() { + val johnId = userRepository.save(Users(userFirstName = "john", type = UserType.TEAM_MEMBER)).userId + val janeId = userRepository.save(Users(userFirstName = "jane", type = UserType.ADMINISTRATOR)).userId + userRepository.save(Users(userFirstName = "joe", type = UserType.MANAGER)) + val specification = SpecificationsBuilder( + SearchSpec::class.constructors.first().call("", false) + ).withSearch("type IN [ADMINISTRATOR, TEAM_MEMBER]").build() + val users = userRepository.findAll(specification) + Assertions.assertTrue(setOf(janeId, johnId) == users.map { user -> user.userId }.toSet()) + } + @Test + fun canGetUserWithIn() { + val johnId = userRepository.save(Users(userFirstName = "john", updatedDateAt = LocalDate.parse("2020-01-10"))).userId + val janeId = userRepository.save(Users(userFirstName = "jane", updatedDateAt = LocalDate.parse("2020-01-15"))).userId + userRepository.save(Users(userFirstName = "joe", updatedDateAt = LocalDate.parse("2021-01-10"))) + val specification = SpecificationsBuilder( + SearchSpec::class.constructors.first().call("", false) + ).withSearch("updatedDateAt IN ['2020-01-10', '2020-01-15']").build() + val users = userRepository.findAll(specification) + Assertions.assertTrue(setOf(janeId, johnId) == users.map { user -> user.userId }.toSet()) + } } From e8d7ac265a4e1a3094f04a3d8fafa160adcdcd41 Mon Sep 17 00:00:00 2001 From: vincentescoffier Date: Fri, 5 Jan 2024 16:37:47 +0100 Subject: [PATCH 14/18] remove print --- .../kotlin/com/sipios/springsearch/strategies/ParsingStrategy.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/kotlin/com/sipios/springsearch/strategies/ParsingStrategy.kt b/src/main/kotlin/com/sipios/springsearch/strategies/ParsingStrategy.kt index 1d808c0..abeda9a 100644 --- a/src/main/kotlin/com/sipios/springsearch/strategies/ParsingStrategy.kt +++ b/src/main/kotlin/com/sipios/springsearch/strategies/ParsingStrategy.kt @@ -80,7 +80,6 @@ interface ParsingStrategy { val inClause: CriteriaBuilder.In = builder.`in`(path.get(fieldName)) val values = value as List<*> values.forEach { inClause.value(it) } - println("inClause: $inClause") return inClause } From a36c0e551e57d05d2d3cad6dfcdccac93be53528 Mon Sep 17 00:00:00 2001 From: vincentescoffier Date: Mon, 8 Jan 2024 10:32:47 +0100 Subject: [PATCH 15/18] refactor to move the logic in the parent class --- .../sipios/springsearch/QueryVisitorImpl.kt | 6 ++---- .../sipios/springsearch/SpecificationImpl.kt | 11 +++++++++-- .../strategies/BooleanStrategy.kt | 6 ++---- .../springsearch/strategies/DateStrategy.kt | 6 ++---- .../springsearch/strategies/DoubleStrategy.kt | 6 ++---- .../strategies/DurationStrategy.kt | 6 ++---- .../springsearch/strategies/EnumStrategy.kt | 6 ++---- .../springsearch/strategies/FloatStrategy.kt | 6 ++---- .../strategies/InstantStrategy.kt | 6 ++---- .../springsearch/strategies/IntStrategy.kt | 6 ++---- .../strategies/LocalDateStrategy.kt | 6 ++---- .../strategies/LocalDateTimeStrategy.kt | 6 ++---- .../strategies/LocalTimeStrategy.kt | 6 ++---- .../strategies/ParsingStrategy.kt | 6 +++++- .../springsearch/strategies/UUIDStrategy.kt | 6 ++---- .../SpringSearchApplicationTest.kt | 19 +++++++++++++++++-- 16 files changed, 57 insertions(+), 57 deletions(-) diff --git a/src/main/kotlin/com/sipios/springsearch/QueryVisitorImpl.kt b/src/main/kotlin/com/sipios/springsearch/QueryVisitorImpl.kt index 23df9df..1817f1b 100644 --- a/src/main/kotlin/com/sipios/springsearch/QueryVisitorImpl.kt +++ b/src/main/kotlin/com/sipios/springsearch/QueryVisitorImpl.kt @@ -34,15 +34,13 @@ class QueryVisitorImpl(private val searchSpecAnnotation: SearchSpec) : QueryB val key = ctx.key()!!.text val op = ctx.op()!!.text var value = ctx.value()!!.text - var valueAsList: List? = null if (ctx.value().STRING() != null) { value = clearString(value) } else if (ctx.value().array() != null) { val arr = ctx.value().array() val arrayValues = arr.value() - valueAsList = arrayValues.map({ if (it.STRING() != null) clearString(it.text) else it.text }) - } - if (valueAsList != null) { + val valueAsList: List = + arrayValues.map { if (it.STRING() != null) clearString(it.text) else it.text } val criteria = SearchCriteria( key, op, diff --git a/src/main/kotlin/com/sipios/springsearch/SpecificationImpl.kt b/src/main/kotlin/com/sipios/springsearch/SpecificationImpl.kt index d58ca96..4d73571 100644 --- a/src/main/kotlin/com/sipios/springsearch/SpecificationImpl.kt +++ b/src/main/kotlin/com/sipios/springsearch/SpecificationImpl.kt @@ -19,7 +19,8 @@ import org.springframework.web.server.ResponseStatusException * * @param The class on which the specification will be applied * */ -class SpecificationImpl(private val criteria: SearchCriteria, private val searchSpecAnnotation: SearchSpec) : Specification { +class SpecificationImpl(private val criteria: SearchCriteria, private val searchSpecAnnotation: SearchSpec) : + Specification { @Throws(ResponseStatusException::class) override fun toPredicate(root: Root, query: CriteriaQuery<*>, builder: CriteriaBuilder): Predicate? { val nestedKey = criteria.key.split(".") @@ -29,7 +30,13 @@ class SpecificationImpl(private val criteria: SearchCriteria, private val sea val strategy = ParsingStrategy.getStrategy(fieldClass, searchSpecAnnotation) val value: Any? try { - value = strategy.parse(criteria.value, fieldClass) + value = if (criteria.value is List<*>) { + ParsingStrategy.getStrategy(fieldClass, searchSpecAnnotation) + .parse(criteria.value as List<*>, fieldClass) + } else { + ParsingStrategy.getStrategy(fieldClass, searchSpecAnnotation) + .parse(criteria.value?.toString(), fieldClass) + } } catch (e: Exception) { throw ResponseStatusException( HttpStatus.BAD_REQUEST, diff --git a/src/main/kotlin/com/sipios/springsearch/strategies/BooleanStrategy.kt b/src/main/kotlin/com/sipios/springsearch/strategies/BooleanStrategy.kt index b1967c8..b87c182 100644 --- a/src/main/kotlin/com/sipios/springsearch/strategies/BooleanStrategy.kt +++ b/src/main/kotlin/com/sipios/springsearch/strategies/BooleanStrategy.kt @@ -3,9 +3,7 @@ package com.sipios.springsearch.strategies import kotlin.reflect.KClass class BooleanStrategy : ParsingStrategy { - override fun parse(value: Any?, fieldClass: KClass): Any? { - if (value is String) return value.toBoolean() - if (value is List<*>) return value.map { it.toString().toBoolean() } - return value + override fun parse(value: String?, fieldClass: KClass): Any? { + return value?.toBoolean() } } diff --git a/src/main/kotlin/com/sipios/springsearch/strategies/DateStrategy.kt b/src/main/kotlin/com/sipios/springsearch/strategies/DateStrategy.kt index 048a1dd..61f5c69 100644 --- a/src/main/kotlin/com/sipios/springsearch/strategies/DateStrategy.kt +++ b/src/main/kotlin/com/sipios/springsearch/strategies/DateStrategy.kt @@ -28,9 +28,7 @@ class DateStrategy : ParsingStrategy { } } - override fun parse(value: Any?, fieldClass: KClass): Any? { - if (value is String) return standardDateFormat.parse(value) - if (value is List<*>) return value.map { standardDateFormat.parse(it.toString()) } - return value + override fun parse(value: String?, fieldClass: KClass): Any? { + return standardDateFormat.parse(value) } } diff --git a/src/main/kotlin/com/sipios/springsearch/strategies/DoubleStrategy.kt b/src/main/kotlin/com/sipios/springsearch/strategies/DoubleStrategy.kt index 3bbfcee..7b1ccc0 100644 --- a/src/main/kotlin/com/sipios/springsearch/strategies/DoubleStrategy.kt +++ b/src/main/kotlin/com/sipios/springsearch/strategies/DoubleStrategy.kt @@ -23,9 +23,7 @@ class DoubleStrategy : ParsingStrategy { } } - override fun parse(value: Any?, fieldClass: KClass): Any? { - if (value is String) return value.toDouble() - if (value is List<*>) return value.map { it.toString().toDouble() } - return value + override fun parse(value: String?, fieldClass: KClass): Any? { + return value?.toDouble() } } diff --git a/src/main/kotlin/com/sipios/springsearch/strategies/DurationStrategy.kt b/src/main/kotlin/com/sipios/springsearch/strategies/DurationStrategy.kt index 825e53b..a722fe2 100644 --- a/src/main/kotlin/com/sipios/springsearch/strategies/DurationStrategy.kt +++ b/src/main/kotlin/com/sipios/springsearch/strategies/DurationStrategy.kt @@ -24,9 +24,7 @@ class DurationStrategy : ParsingStrategy { } } - override fun parse(value: Any?, fieldClass: KClass): Any? { - if (value is String) return Duration.parse(value) - if (value is List<*>) return value.map { Duration.parse(it.toString()) } - return value + override fun parse(value: String?, fieldClass: KClass): Any? { + return Duration.parse(value) } } diff --git a/src/main/kotlin/com/sipios/springsearch/strategies/EnumStrategy.kt b/src/main/kotlin/com/sipios/springsearch/strategies/EnumStrategy.kt index fdcacd9..6ac8238 100644 --- a/src/main/kotlin/com/sipios/springsearch/strategies/EnumStrategy.kt +++ b/src/main/kotlin/com/sipios/springsearch/strategies/EnumStrategy.kt @@ -3,10 +3,8 @@ package com.sipios.springsearch.strategies import kotlin.reflect.KClass class EnumStrategy : ParsingStrategy { - override fun parse(value: Any?, fieldClass: KClass): Any? { - if (value is String) return toValue(fieldClass, value) - if (value is List<*>) return value.map { toValue(fieldClass, it.toString()) } - return value + override fun parse(value: String?, fieldClass: KClass): Any? { + return toValue(fieldClass, value) } private fun toValue(fieldClass: KClass, value: Any?): Any? = diff --git a/src/main/kotlin/com/sipios/springsearch/strategies/FloatStrategy.kt b/src/main/kotlin/com/sipios/springsearch/strategies/FloatStrategy.kt index 86d5b1d..ea1d1e6 100644 --- a/src/main/kotlin/com/sipios/springsearch/strategies/FloatStrategy.kt +++ b/src/main/kotlin/com/sipios/springsearch/strategies/FloatStrategy.kt @@ -23,9 +23,7 @@ class FloatStrategy : ParsingStrategy { } } - override fun parse(value: Any?, fieldClass: KClass): Any? { - if (value is String) return value.toFloat() - if (value is List<*>) return value.map { it.toString().toFloat() } - return value + override fun parse(value: String?, fieldClass: KClass): Any? { + return value?.toFloat() } } diff --git a/src/main/kotlin/com/sipios/springsearch/strategies/InstantStrategy.kt b/src/main/kotlin/com/sipios/springsearch/strategies/InstantStrategy.kt index 99cd3ca..66294cd 100644 --- a/src/main/kotlin/com/sipios/springsearch/strategies/InstantStrategy.kt +++ b/src/main/kotlin/com/sipios/springsearch/strategies/InstantStrategy.kt @@ -24,9 +24,7 @@ class InstantStrategy : ParsingStrategy { } } - override fun parse(value: Any?, fieldClass: KClass): Any? { - if (value is String) return Instant.parse(value) - if (value is List<*>) return value.map { Instant.parse(it.toString()) } - return value + override fun parse(value: String?, fieldClass: KClass): Any? { + return Instant.parse(value) } } diff --git a/src/main/kotlin/com/sipios/springsearch/strategies/IntStrategy.kt b/src/main/kotlin/com/sipios/springsearch/strategies/IntStrategy.kt index 526aef6..21f3863 100644 --- a/src/main/kotlin/com/sipios/springsearch/strategies/IntStrategy.kt +++ b/src/main/kotlin/com/sipios/springsearch/strategies/IntStrategy.kt @@ -23,9 +23,7 @@ class IntStrategy : ParsingStrategy { } } - override fun parse(value: Any?, fieldClass: KClass): Any? { - if (value is String) return value.toInt() - if (value is List<*>) return value.map { it.toString().toInt() } - return value + override fun parse(value: String?, fieldClass: KClass): Any? { + return value?.toInt() } } diff --git a/src/main/kotlin/com/sipios/springsearch/strategies/LocalDateStrategy.kt b/src/main/kotlin/com/sipios/springsearch/strategies/LocalDateStrategy.kt index e16eda1..2b435f6 100644 --- a/src/main/kotlin/com/sipios/springsearch/strategies/LocalDateStrategy.kt +++ b/src/main/kotlin/com/sipios/springsearch/strategies/LocalDateStrategy.kt @@ -24,9 +24,7 @@ class LocalDateStrategy : ParsingStrategy { } } - override fun parse(value: Any?, fieldClass: KClass): Any? { - if (value is String) return LocalDate.parse(value) - if (value is List<*>) return value.map { LocalDate.parse(it.toString()) } - return value + override fun parse(value: String?, fieldClass: KClass): Any? { + return LocalDate.parse(value) } } diff --git a/src/main/kotlin/com/sipios/springsearch/strategies/LocalDateTimeStrategy.kt b/src/main/kotlin/com/sipios/springsearch/strategies/LocalDateTimeStrategy.kt index 2d8a3c6..763dae5 100644 --- a/src/main/kotlin/com/sipios/springsearch/strategies/LocalDateTimeStrategy.kt +++ b/src/main/kotlin/com/sipios/springsearch/strategies/LocalDateTimeStrategy.kt @@ -24,9 +24,7 @@ class LocalDateTimeStrategy : ParsingStrategy { } } - override fun parse(value: Any?, fieldClass: KClass): Any? { - if (value is String) return LocalDateTime.parse(value) - if (value is List<*>) return value.map { LocalDateTime.parse(it.toString()) } - return value + override fun parse(value: String?, fieldClass: KClass): Any? { + return LocalDateTime.parse(value) } } diff --git a/src/main/kotlin/com/sipios/springsearch/strategies/LocalTimeStrategy.kt b/src/main/kotlin/com/sipios/springsearch/strategies/LocalTimeStrategy.kt index 7d46851..7682689 100644 --- a/src/main/kotlin/com/sipios/springsearch/strategies/LocalTimeStrategy.kt +++ b/src/main/kotlin/com/sipios/springsearch/strategies/LocalTimeStrategy.kt @@ -24,9 +24,7 @@ class LocalTimeStrategy : ParsingStrategy { } } - override fun parse(value: Any?, fieldClass: KClass): Any? { - if (value is String) return LocalTime.parse(value) - if (value is List<*>) return value.map { LocalTime.parse(it.toString()) } - return value + override fun parse(value: String?, fieldClass: KClass): Any? { + return LocalTime.parse(value) } } diff --git a/src/main/kotlin/com/sipios/springsearch/strategies/ParsingStrategy.kt b/src/main/kotlin/com/sipios/springsearch/strategies/ParsingStrategy.kt index abeda9a..ca3ffc5 100644 --- a/src/main/kotlin/com/sipios/springsearch/strategies/ParsingStrategy.kt +++ b/src/main/kotlin/com/sipios/springsearch/strategies/ParsingStrategy.kt @@ -23,10 +23,14 @@ interface ParsingStrategy { * @param fieldClass Kotlin class of the referred field * @return Returns by default the value without any parsing */ - fun parse(value: Any?, fieldClass: KClass): Any? { + fun parse(value: String?, fieldClass: KClass): Any? { return value } + fun parse(value: List<*>?, fieldClass: KClass): Any? { + return value?.map { parse(it.toString(), fieldClass) } + } + /** * Method to build the predicate * diff --git a/src/main/kotlin/com/sipios/springsearch/strategies/UUIDStrategy.kt b/src/main/kotlin/com/sipios/springsearch/strategies/UUIDStrategy.kt index e5525e4..22542f2 100644 --- a/src/main/kotlin/com/sipios/springsearch/strategies/UUIDStrategy.kt +++ b/src/main/kotlin/com/sipios/springsearch/strategies/UUIDStrategy.kt @@ -4,9 +4,7 @@ import java.util.UUID import kotlin.reflect.KClass class UUIDStrategy : ParsingStrategy { - override fun parse(value: Any?, fieldClass: KClass): Any? { - if (value is String) return UUID.fromString(value) - if (value is List<*>) return value.map { UUID.fromString(it.toString()) } - return value + override fun parse(value: String?, fieldClass: KClass): Any? { + return UUID.fromString(value) } } diff --git a/src/test/kotlin/com/sipios/springsearch/SpringSearchApplicationTest.kt b/src/test/kotlin/com/sipios/springsearch/SpringSearchApplicationTest.kt index 4408ff0..f195469 100644 --- a/src/test/kotlin/com/sipios/springsearch/SpringSearchApplicationTest.kt +++ b/src/test/kotlin/com/sipios/springsearch/SpringSearchApplicationTest.kt @@ -1186,6 +1186,18 @@ class SpringSearchApplicationTest { Assertions.assertTrue(setOf(johnId, janeId) == users.map { user -> user.userId }.toSet()) } + @Test + fun canGetUserWithNameInEmptyList() { + userRepository.save(Users(userFirstName = "john", userChildrenNumber = 2)) + userRepository.save(Users(userFirstName = "jane", userChildrenNumber = 3)) + userRepository.save(Users(userFirstName = "joe", userChildrenNumber = 4)) + val specification = SpecificationsBuilder( + SearchSpec::class.constructors.first().call("", false) + ).withSearch("userFirstName IN []").build() + val users = userRepository.findAll(specification) + Assertions.assertTrue(users.isEmpty()) + } + @Test fun canGetUserWithNameNotIn() { userRepository.save(Users(userFirstName = "john", userChildrenNumber = 2)) @@ -1233,10 +1245,13 @@ class SpringSearchApplicationTest { val users = userRepository.findAll(specification) Assertions.assertTrue(setOf(janeId, johnId) == users.map { user -> user.userId }.toSet()) } + @Test fun canGetUserWithIn() { - val johnId = userRepository.save(Users(userFirstName = "john", updatedDateAt = LocalDate.parse("2020-01-10"))).userId - val janeId = userRepository.save(Users(userFirstName = "jane", updatedDateAt = LocalDate.parse("2020-01-15"))).userId + val johnId = + userRepository.save(Users(userFirstName = "john", updatedDateAt = LocalDate.parse("2020-01-10"))).userId + val janeId = + userRepository.save(Users(userFirstName = "jane", updatedDateAt = LocalDate.parse("2020-01-15"))).userId userRepository.save(Users(userFirstName = "joe", updatedDateAt = LocalDate.parse("2021-01-10"))) val specification = SpecificationsBuilder( SearchSpec::class.constructors.first().call("", false) From 3b5abee6d02d34876aa32d4a5333918f4e45495e Mon Sep 17 00:00:00 2001 From: vincentescoffier Date: Mon, 8 Jan 2024 10:48:47 +0100 Subject: [PATCH 16/18] update documentation --- README.md | 3 +++ src/main/kotlin/com/sipios/springsearch/QueryVisitorImpl.kt | 1 + .../com/sipios/springsearch/strategies/EnumStrategy.kt | 5 +---- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 97c9161..af7a436 100644 --- a/README.md +++ b/README.md @@ -158,6 +158,9 @@ Request : `/cars?search=color:Red OR color:Blue` Request : `/cars?search=brand:Aston* AND price<300000` ![and operator example](./docs/images/and-example.gif) +7. Using the `IN` and `NOT IN` operators +Request : `/cars?search=color IN ['Red', 'Blue']` + 8. Using parenthesis Request : `/cars?search=( brand:Nissan OR brand:Chevrolet ) AND color:Blue` *Note: Spaces inside the parenthesis are not necessary* diff --git a/src/main/kotlin/com/sipios/springsearch/QueryVisitorImpl.kt b/src/main/kotlin/com/sipios/springsearch/QueryVisitorImpl.kt index 1817f1b..ffc5868 100644 --- a/src/main/kotlin/com/sipios/springsearch/QueryVisitorImpl.kt +++ b/src/main/kotlin/com/sipios/springsearch/QueryVisitorImpl.kt @@ -41,6 +41,7 @@ class QueryVisitorImpl(private val searchSpecAnnotation: SearchSpec) : QueryB val arrayValues = arr.value() val valueAsList: List = arrayValues.map { if (it.STRING() != null) clearString(it.text) else it.text } + // there is no need for prefix and suffix (e.g. 'john*') in case of array value val criteria = SearchCriteria( key, op, diff --git a/src/main/kotlin/com/sipios/springsearch/strategies/EnumStrategy.kt b/src/main/kotlin/com/sipios/springsearch/strategies/EnumStrategy.kt index 6ac8238..4e74ee5 100644 --- a/src/main/kotlin/com/sipios/springsearch/strategies/EnumStrategy.kt +++ b/src/main/kotlin/com/sipios/springsearch/strategies/EnumStrategy.kt @@ -4,9 +4,6 @@ import kotlin.reflect.KClass class EnumStrategy : ParsingStrategy { override fun parse(value: String?, fieldClass: KClass): Any? { - return toValue(fieldClass, value) + return Class.forName(fieldClass.qualifiedName).getMethod("valueOf", String::class.java).invoke(null, value) } - - private fun toValue(fieldClass: KClass, value: Any?): Any? = - Class.forName(fieldClass.qualifiedName).getMethod("valueOf", String::class.java).invoke(null, value) } From 515228bfe1a304e4d7d0a083f06f6a0eedc37178 Mon Sep 17 00:00:00 2001 From: vincentescoffier Date: Mon, 8 Jan 2024 10:50:07 +0100 Subject: [PATCH 17/18] update documentation --- README.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index af7a436..7054261 100644 --- a/README.md +++ b/README.md @@ -158,8 +158,10 @@ Request : `/cars?search=color:Red OR color:Blue` Request : `/cars?search=brand:Aston* AND price<300000` ![and operator example](./docs/images/and-example.gif) -7. Using the `IN` and `NOT IN` operators -Request : `/cars?search=color IN ['Red', 'Blue']` +7. Using the `IN` and `NOT IN` operators +Request : `/cars?search=color IN ['Red', 'Blue'] +*Note: Spaces inside the brackets are not necessary* + 8. Using parenthesis Request : `/cars?search=( brand:Nissan OR brand:Chevrolet ) AND color:Blue` From 7526c1427514bfd894f11e8f6e7a82559d3c11ad Mon Sep 17 00:00:00 2001 From: vincentescoffier Date: Mon, 8 Jan 2024 11:04:38 +0100 Subject: [PATCH 18/18] update documentation --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 7054261..5070fd0 100644 --- a/README.md +++ b/README.md @@ -160,7 +160,8 @@ Request : `/cars?search=brand:Aston* AND price<300000` 7. Using the `IN` and `NOT IN` operators Request : `/cars?search=color IN ['Red', 'Blue'] -*Note: Spaces inside the brackets are not necessary* +*Note: Spaces inside the brackets are not necessary* +*Note: You will need to encode the value (e.g. encodeURI) as brackets are not valid url parts* 8. Using parenthesis