All Downloads are FREE. Search and download functionalities are using the official Maven repository.

eu.vaadinonkotlin.restclient.CrudClient.kt Maven / Gradle / Ivy

package eu.vaadinonkotlin.restclient

import com.gitlab.mvysny.jdbiorm.OrderBy
import com.gitlab.mvysny.jdbiorm.Property
import com.gitlab.mvysny.jdbiorm.condition.And
import com.gitlab.mvysny.jdbiorm.condition.Condition
import com.gitlab.mvysny.jdbiorm.condition.Eq
import com.gitlab.mvysny.jdbiorm.condition.Expression
import com.gitlab.mvysny.jdbiorm.condition.FullTextCondition
import com.gitlab.mvysny.jdbiorm.condition.IsNotNull
import com.gitlab.mvysny.jdbiorm.condition.IsNull
import com.gitlab.mvysny.jdbiorm.condition.Like
import com.gitlab.mvysny.jdbiorm.condition.LikeIgnoreCase
import com.gitlab.mvysny.jdbiorm.condition.Op
import com.gitlab.mvysny.uribuilder.net.URIBuilder
import com.vaadin.flow.data.provider.AbstractBackEndDataProvider
import com.vaadin.flow.data.provider.Query
import com.vaadin.flow.data.provider.QuerySortOrder
import com.vaadin.flow.data.provider.SortDirection
import eu.vaadinonkotlin.MediaType
import java.net.URI
import java.net.http.HttpClient
import java.net.http.HttpRequest
import java.util.stream.Stream

/**
 * Uses the CRUD endpoint and serves instances of given item of type [itemClass] over given [client] using [VokRestClient.gson].
 * Expect the CRUD endpoint to be exposed in the following manner:
 * * `GET /rest/users` returns all users
 * * `GET /rest/users?select=count` returns a single number - the count of all users. This is only necessary for [getCount]
 * or if you plan to use this client as a backend for Vaadin Grid.
 * * `GET /rest/users/22` returns one users
 * * `POST /rest/users` will create an user
 * * `PATCH /rest/users/22` will update an user
 * * `DELETE /rest/users/22` will delete an user
 *
 * Paging/sorting/filtering is supported: the following query parameters will simply be added to the "get all" URL request:
 *
 * * `limit` and `offset` for result paging. Both must be 0 or greater. The server may impose max value limit on the `limit` parameter.
 * * `sort_by=-last_modified,+email,first_name` - a list of sorting clauses. The server may restrict sorting by only a selected subset of properties.
 * * The filters are simply converted to query parameters, for example `age=81`. [Op]s are also supported: the value will be prefixed with a special operator prefix:
 * `eq:`, `lt:`, `lte:`, `gt:`, `gte:`, `ilike:`, `like:`, `isnull:`, `isnotnull:`, for example `age=lt:25`. A full example is `name=ilike:martin&age=lte:70&age=gte:20&birthdate=isnull:&grade=5`.
 * OR filters are not supported - passing [Or] will cause [getAll] to throw [IllegalArgumentException].
 *
 * Since this client is also a [DataProvider], you can use this class to feed data into Vaadin Grid or ComboBox etc:
 * ```
 * val crud = CrudClient("http://localhost:8080/rest/person/", Person::class.java)
 * grid.dataProvider = crud
 * ```
 * @property baseUrl the base URL, such as `http://localhost:8080/rest/users/`, must end with a slash.
 * @property converter used to convert filter values to strings passable as query parameters. Defaults to [QueryParameterConverter] with system-default
 * zone; it is pretty much recommended to set a specific time zone.
 * @property client which HTTP client to use, defaults to [VokRestClient.httpClient].
 * @property converter converts filter values to strings when querying for data.
 * Defaults to [QueryParameterConverter].
 */
public open class CrudClient(
    public val baseUrl: String,
    public val itemClass: Class,
    public val client: HttpClient = VokRestClient.httpClient,
    public val converter: Converter = QueryParameterConverter()) : AbstractBackEndDataProvider() {
    init {
        require(baseUrl.endsWith("/")) { "$baseUrl must end with /" }
    }

    /**
     * Fetches data from the back end. The items must match given [filter]. This function does exactly the same as [fetch].
     * @param filter optional filter which defines filtering to be used for counting the
     * number of items. If null all items are considered.
     * @param sortBy optionally sort the beans according to given fields. By default sorts ASC; if you prepend the field with the "-"
     * character the sorting will be DESC.
     * @param range offset and limit to fetch
     * @return a list of items matching the query, may be empty.
     */
    private fun getAll(filter: Condition = Condition.NO_CONDITION, sortBy: List = listOf(), offset: Long, limit: Long): List {
        val url: URI = baseUrl.buildUrl {
            addParameter("offset", offset.toString())
            addParameter("limit", limit.toString())
            if (sortBy.isNotEmpty()) {
                addParameter("sort_by", sortBy.joinToString(",") { "${if(it.order == OrderBy.Order.ASC)"+" else "-"}${it.property.name}" })
            }
            addFilterQueryParameters(filter)
        }
        val request: HttpRequest = url.buildRequest()
        return client.exec(request) { response -> response.jsonArray(itemClass) }
    }

    private fun URIBuilder.addFilterQueryParameters(condition: Condition) {
        fun opToRest(op: Op.Operator): String = when (op) {
            Op.Operator.EQ -> "eq"
            Op.Operator.GE -> "gte"
            Op.Operator.GT -> "gt"
            Op.Operator.LE -> "lte"
            Op.Operator.LT -> "lt"
            Op.Operator.NE -> "ne"
        }

        fun Expression<*>.getPropertyName(): String {
            val propName = (this as Property<*>).name.name
            require(propName != "limit" && propName != "offset" && propName != "sort_by" && propName != "select") {
                "cannot filter on reserved query parameter name $propName"
            }
            return propName
        }
        fun Expression<*>.getValue(): String? {
            val value = (this as Expression.Value<*>).value ?: return null
            return converter.convert(value)
        }

        when {
            condition == Condition.NO_CONDITION -> return
            condition is Eq -> addParameter(condition.arg1.getPropertyName(), condition.arg2.getValue())
            condition is IsNotNull -> addParameter(condition.arg.getPropertyName(), "isnotnull:")
            condition is IsNull -> addParameter(condition.arg.getPropertyName(), "isnull:")
            condition is Like -> addParameter(condition.arg1.getPropertyName(), "like:" + condition.arg2.getValue())
            condition is LikeIgnoreCase -> addParameter(condition.arg1.getPropertyName(), "ilike:" + condition.arg2.getValue())
            condition is Op -> addParameter(condition.arg1.getPropertyName(), opToRest(condition.operator) + ":" + condition.arg2.getValue())
            condition is FullTextCondition -> addParameter(condition.arg.getPropertyName(), "fulltext:" + condition.query)
            condition is And -> { addFilterQueryParameters(condition.condition1); addFilterQueryParameters(condition.condition2) }
            else -> throw IllegalArgumentException("Unsupported condition $condition")
        }
    }

    public fun getOne(id: String): T {
        val request: HttpRequest = "$baseUrl$id".buildUrl().buildRequest()
        return client.exec(request) { response -> response.json(itemClass) }
    }

    public fun create(entity: T) {
        val json: String = VokRestClient.gson.toJson(entity)
        val request: HttpRequest = baseUrl.buildUrl().buildRequest { post(json, MediaType.jsonUtf8) }
        client.exec(request) {}
    }

    public fun update(id: String, entity: T) {
        val json: String = VokRestClient.gson.toJson(entity)
        val request: HttpRequest = "$baseUrl$id".buildUrl().buildRequest { patch(json, MediaType.jsonUtf8) }
        client.exec(request) {}
    }

    public fun delete(id: String) {
        val request: HttpRequest = "$baseUrl$id".buildUrl().buildRequest { DELETE() }
        client.exec(request) {}
    }

    private val QuerySortOrder.orderBy: OrderBy get() = OrderBy.of(itemClass, sorted, if (direction == SortDirection.ASCENDING) OrderBy.Order.ASC else OrderBy.Order.DESC)
    private val Query<*, *>.range: LongRange get() = offset.toLong() .. (offset + limit).toLong()

    override fun fetchFromBackEnd(query: Query): Stream {
        val sortBy: List = (query.sortOrders ?: listOf()).map { it.orderBy }
        val condition: Condition = query.filter.orElse(Condition.NO_CONDITION)
        val result = getAll(condition, sortBy, query.offset.toLong(), query.limit.toLong())
        return result.stream()
    }

    override fun sizeInBackEnd(query: Query): Int {
        val condition: Condition = query.filter.orElse(Condition.NO_CONDITION)
        val request: HttpRequest = "$baseUrl?select=count".buildUrl {
            addFilterQueryParameters(condition)
        }.buildRequest()
        return client.exec(request) { response -> response.body().reader().readText().toInt() }
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy