Skip to content

Commit

Permalink
Merge pull request #12 from verissimor/feature/reactive
Browse files Browse the repository at this point in the history
Feature/reactive
  • Loading branch information
verissimor authored Aug 20, 2022
2 parents 55c02bf + 2e23a1d commit 4dcf43f
Show file tree
Hide file tree
Showing 23 changed files with 1,014 additions and 29 deletions.
2 changes: 1 addition & 1 deletion .github/badges/branches.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion .github/badges/jacoco.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
41 changes: 39 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,37 @@ Receive a `MagicFilter` as parameter of a `@GetMapping` and use `filter.getSpec(
//}
```

## Enable support for Spring WebFlux and R2DBC

This lib is compatible with R2DBC. Check out an example in the `examples\java-gradle-reactive`.

```java
@EnableR2dbcMagicFilter
//@SpringBootApplication
//public class DemoApplication
```

You can either use the fluent api:
```java
// https://docs.spring.io/spring-data/r2dbc/docs/current/reference/html/#r2dbc.entityoperations.fluent-api
@GetMapping
Flux<User> getUsers(R2dbcMagicFilter filter, Pageable pageable) {
Criteria criteria = filter.toCriteria(User.class, DbFeatures.NONE);
return r2dbcTemplate.select(User.class).matching(query(criteria)).all();
}
```

Or, extend ReactiveCrudRepository and implement ReactiveSearchRepository

```java
// see how on my discussion on stack overflow (https://stackoverflow.com/questions/73424096/reactivecrudrepository-criteria-query)
@GetMapping("/paged")
Mono<Page<User>> getUsersPaged(R2dbcMagicFilter filter, Pageable pageable) {
Criteria criteria = filter.toCriteria(User.class, DbFeatures.NONE);
return userRepository.findAll(criteria, pageable, User.class);
}
```

## How to use

A `filter predicate` is composed of a `field`, an `operator`, and a `value`.
Expand All @@ -119,7 +150,7 @@ where u.name like '%Matthew%' and u.age > 23 and c.name = 'London'
## Supported Operators

| Operator | Suffix | Example |
| ------------------ | ------------- | --------------------------- |
|--------------------|---------------|-----------------------------|
| EQUAL | | `?name=Matthew` |
| GREATER_THAN | _gt | `?age_gt=32` |
| GREATER_THAN_EQUAL | _ge | `?age_ge=32` |
Expand Down Expand Up @@ -248,9 +279,15 @@ filter.toSpecification(User::class.java, DbFeatures.POSTGRES)

I'd like to suggest you have a look at the tests. You might have some fun exploring it.

Also, you will find an java+maven project example in the folder: `examples/java-maven-h2`.
Also, you will find a java+maven project example in the folder: `examples/java-maven-h2`.

## Contributing to the Project
If you'd like to contribute code to this project you can do so through GitHub by forking the repository and generating a pull request.

By contributing your code, you agree to license your contribution under the terms of the Apache Licence.

## TODO
1. add reactive example
2. rename the examples
3. remove the SEARCH_IN_SEPARATOR_PRM & SEARCH_IN_SEPARATOR_DEF
4. remove POSTGRES unaccent for reactive
27 changes: 16 additions & 11 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,15 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
import java.util.Base64

plugins {
id("org.springframework.boot") version "2.6.1"
id("io.spring.dependency-management") version "1.0.11.RELEASE"
kotlin("jvm") version "1.6.0"
kotlin("plugin.spring") version "1.6.0"
kotlin("plugin.jpa") version "1.6.10"
id("org.springframework.boot") version "2.7.3"
id("io.spring.dependency-management") version "1.0.13.RELEASE"
kotlin("jvm") version "1.6.21"
kotlin("plugin.spring") version "1.6.21"
kotlin("plugin.jpa") version "1.6.21"

kotlin("kapt") version "1.6.0"
kotlin("kapt") version "1.6.21"

id("org.jlleitschuh.gradle.ktlint") version "10.2.0"
id("org.jlleitschuh.gradle.ktlint") version "10.3.0"
`java-library`
`maven-publish`
id("io.github.gradle-nexus.publish-plugin") version "1.1.0"
Expand All @@ -19,7 +19,7 @@ plugins {
}

group = "io.github.verissimor.lib"
version = System.getenv("RELEASE_VERSION") ?: "0.0.11-SNAPSHOT"
version = System.getenv("RELEASE_VERSION") ?: "1.0.0-SNAPSHOT"

java {
sourceCompatibility = JavaVersion.VERSION_1_8
Expand All @@ -41,10 +41,12 @@ repositories {
dependencies {

// spring
implementation("org.springframework.boot:spring-boot-starter-data-jpa")
implementation("org.springframework.boot:spring-boot-starter-web")
implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
compileOnly("org.springframework.boot:spring-boot-starter-data-jpa")
compileOnly("org.springframework.boot:spring-boot-starter-web")
compileOnly("org.springframework.boot:spring-boot-starter-data-r2dbc")
compileOnly("org.springframework.boot:spring-boot-starter-webflux")

implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
implementation("org.jetbrains.kotlin:kotlin-reflect")
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")

Expand All @@ -55,6 +57,9 @@ dependencies {

// test service
testImplementation("com.h2database:h2")
testImplementation("org.springframework.boot:spring-boot-starter-data-jpa")
testImplementation("org.springframework.boot:spring-boot-starter-web")
testImplementation("org.springframework.data:spring-data-relational:2.4.2")
}

tasks.withType<KotlinCompile> {
Expand Down
25 changes: 25 additions & 0 deletions examples/java-gradle-reactive/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# R2dbc Magic Filter Example

This is an example of usage of the library `R2dbc Magic Filter`

## Running the project

To run this project, use:

```shell
./gradlew bootRun
```

## get users

Use the paraters as described in the readme.md of the main lib. Eg.:

```
http://localhost:8080/api/users?name_like=erick&gender_in=MALE,FEMALE&cityId_gt=1
http://localhost:8080/api/users/paged?size=1&gender=FEMALE
http://localhost:8080/api/users/fluent?size=1&gender=FEMALE
```

Have fun ;)
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package io.github.verissimor.examples.reactive.javagradlereactive.configuration;

import org.springframework.context.annotation.Configuration;
import org.springframework.data.web.ReactivePageableHandlerMethodArgumentResolver;
import org.springframework.web.reactive.config.WebFluxConfigurer;
import org.springframework.web.reactive.result.method.annotation.ArgumentResolverConfigurer;

@Configuration
public class PageableWebFluxConfiguration implements WebFluxConfigurer {

@Override
public void configureArgumentResolvers(ArgumentResolverConfigurer configurer) {
configurer.addCustomResolver(new ReactivePageableHandlerMethodArgumentResolver());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import java.lang.reflect.Field
import java.math.BigDecimal
import java.time.Instant
import java.time.LocalDate
import javax.persistence.Enumerated
import javax.persistence.criteria.Path
import javax.persistence.criteria.Root

Expand All @@ -30,18 +29,29 @@ data class ParsedField<T>(
return fullPath ?: error("unexpected condition to parse path of $resolvedFieldName")
}

fun getFieldType(): FieldType? = when {
fieldClass == null -> null
fieldClass.getDeclaredAnnotationsByType(Enumerated::class.java).isNotEmpty() -> ENUMERATED
fieldClass.type == LocalDate::class.java -> LOCAL_DATE
fieldClass.type == Instant::class.java -> INSTANT
fieldClass.type == Boolean::class.java -> BOOLEAN
fun getFieldType(): FieldType? = getFieldType(fieldClass)

fieldClass.type == Int::class.java ||
fieldClass.type == Long::class.java ||
fieldClass.type == BigDecimal::class.java ||
fieldClass.type.isAssignableFrom(Number::class.java) -> NUMBER
companion object {
fun getFieldType(fieldClass: Field?): FieldType? = when {
fieldClass == null -> null
fieldClass.type?.superclass?.name == "java.lang.Enum" -> ENUMERATED
fieldClass.type == LocalDate::class.java -> LOCAL_DATE
fieldClass.type == Instant::class.java -> INSTANT
fieldClass.type == Boolean::class.java ||
// this solves conflicts between kotlin/java
fieldClass.type.name == "java.lang.Boolean" -> BOOLEAN

else -> FieldType.GENERIC
fieldClass.type == Int::class.java ||
fieldClass.type == Long::class.java ||
fieldClass.type == BigDecimal::class.java ||
fieldClass.type.isAssignableFrom(Number::class.java) ||
// this solves conflicts between kotlin/java
fieldClass.type.name == "java.lang.Integer" ||
fieldClass.type.name == "java.math.BigDecimal" ||
fieldClass.type.name == "java.lang.Long" ||
fieldClass.type.isAssignableFrom(java.lang.Number::class.java) -> NUMBER

else -> FieldType.GENERIC
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package io.github.verissimor.lib.r2dbcmagicfilter

import org.springframework.context.annotation.Import

@Import(R2dbcMagicFilterConfigurer::class)
annotation class EnableR2dbcMagicFilter
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package io.github.verissimor.lib.r2dbcmagicfilter

import io.github.verissimor.lib.jpamagicfilter.domain.FilterOperator
import io.github.verissimor.lib.r2dbcmagicfilter.domain.R2dbcParsedField
import java.lang.reflect.Field

object R2dbcFieldParser {

fun parseField(field: String, value: Array<String>?, clazz: Class<*>): R2dbcParsedField {
val normalized = normalize(field)
val filterType = fieldToType(normalized, value)
val resolvedFieldName = resolveFieldName(normalized, filterType)
val fieldClass: Field? = fieldToClass(resolvedFieldName, clazz)

return R2dbcParsedField(filterType, resolvedFieldName, fieldClass)
}

private fun normalize(field: String) = field.trim().replace("[]", "")

private fun fieldToType(field: String, value: Array<String>?): FilterOperator {
val type = FilterOperator.values()
.sortedByDescending { it.suffix.length }
.firstOrNull { field.contains(it.suffix) } ?: FilterOperator.EQUAL

if (value != null && type == FilterOperator.EQUAL && value.size > 1) {
return FilterOperator.IN
}

return type
}

private fun resolveFieldName(field: String, type: FilterOperator?) =
type?.let { field.replace(it.suffix, "") } ?: field

private fun fieldToClass(field: String, root: Class<*>): Field? {
var resultField: Field? = null
field.split(".")
.forEach { fieldP ->
resultField = if (resultField == null) {
root.declaredFields.firstOrNull { it.name == fieldP }
} else {
resultField?.type?.declaredFields?.firstOrNull { it.name == fieldP }
}
}

return resultField
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package io.github.verissimor.lib.r2dbcmagicfilter

import io.github.verissimor.lib.jpamagicfilter.MagicFilter.Companion.SEARCH_TYPE_AND
import io.github.verissimor.lib.jpamagicfilter.MagicFilter.Companion.SEARCH_TYPE_OR
import io.github.verissimor.lib.jpamagicfilter.MagicFilter.Companion.SEARCH_TYPE_PRM
import io.github.verissimor.lib.jpamagicfilter.domain.DbFeatures
import io.github.verissimor.lib.jpamagicfilter.domain.DbFeatures.NONE
import io.github.verissimor.lib.jpamagicfilter.toSingleParameter
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import org.springframework.data.relational.core.query.Criteria
import org.springframework.util.MultiValueMap

class R2dbcMagicFilter(
private val parameterMap: MultiValueMap<String, String>
) {

val log: Logger = LoggerFactory.getLogger(R2dbcMagicFilter::class.java)

fun toCriteria(clazz: Class<*>, dbFeatures: DbFeatures = NONE): Criteria {

val map: Map<String, Array<String>?> = parameterMap.keys.associateWith { parameterMap[it]?.toTypedArray() }
val parsed = R2dbcPredicateParser.parsePredicates(map, clazz, dbFeatures)

if (parsed.isEmpty()) {
return Criteria.empty()
}

val criteria = when (map.toSingleParameter(SEARCH_TYPE_PRM)) {
SEARCH_TYPE_AND, null -> parsed.reduce { acc, criteria -> acc.and(criteria) }
SEARCH_TYPE_OR -> parsed.reduce { acc, criteria -> acc.or(criteria) }
else -> error("Invalid searchType. Only allowed: and, or")
}

log.info(criteria.toString())

return criteria
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package io.github.verissimor.lib.r2dbcmagicfilter

import org.springframework.context.annotation.Configuration
import org.springframework.core.MethodParameter
import org.springframework.web.reactive.BindingContext
import org.springframework.web.reactive.config.WebFluxConfigurer
import org.springframework.web.reactive.result.method.HandlerMethodArgumentResolver
import org.springframework.web.reactive.result.method.annotation.ArgumentResolverConfigurer
import org.springframework.web.server.ServerWebExchange
import org.springframework.web.servlet.config.annotation.EnableWebMvc
import reactor.core.publisher.Mono

@Configuration
@EnableWebMvc
class R2dbcMagicFilterConfigurer : WebFluxConfigurer {
override fun configureArgumentResolvers(configurer: ArgumentResolverConfigurer) {
super.configureArgumentResolvers(configurer)
configurer.addCustomResolver(R2dbcMagicFilterAttributeResolver())
}
}

class R2dbcMagicFilterAttributeResolver : HandlerMethodArgumentResolver {
override fun supportsParameter(parameter: MethodParameter): Boolean {
return parameter.parameterType == R2dbcMagicFilter::class.java
}

override fun resolveArgument(parameter: MethodParameter, bindingContext: BindingContext, exchange: ServerWebExchange): Mono<Any> {
return Mono.just(R2dbcMagicFilter(exchange.request.queryParams))
}
}
Loading

0 comments on commit 4dcf43f

Please sign in to comment.