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

com.github.frtu.coroutine.webclient.SuspendableWebClient.kt Maven / Gradle / Ivy

There is a newer version: 2.0.7
Show newest version
package com.github.frtu.coroutine.webclient

import org.slf4j.LoggerFactory
import com.github.frtu.logs.core.RpcLogger
import com.github.frtu.logs.core.RpcLogger.*
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.reactive.asFlow
import org.reactivestreams.Publisher
import org.springframework.core.io.buffer.DataBuffer
import org.springframework.http.HttpHeaders
import org.springframework.http.MediaType
import org.springframework.http.ReactiveHttpOutputMessage
import org.springframework.web.reactive.function.BodyExtractors
import org.springframework.web.reactive.function.BodyInserter
import org.springframework.web.reactive.function.BodyInserters
import org.springframework.web.reactive.function.client.*
import reactor.core.publisher.Flux
import reactor.util.retry.Retry
import java.io.IOException
import java.io.PipedInputStream
import java.io.PipedOutputStream
import java.util.*
import java.util.function.Consumer

/**
 * Suspendable {@link WebClient} for coroutine. Open class that can be overridden.
 *
 * @see SpringFramework WebClient documentation
 *
 * @author Frédéric TU
 * @since 1.1.1
 */
/**
 * @param webClient built and configured
 */
open class SuspendableWebClient(
    private val webClient: WebClient,
    // Check https://projectreactor.io/docs/core/3.4.1/api/reactor/util/retry/Retry.html
    private val getRetrySpec: Retry = Retry.max(3),
) {
    companion object {
        fun create(baseUrl: String, builder: WebClient.Builder = WebClient.builder()): SuspendableWebClient =
            SuspendableWebClient(builder.baseUrl(baseUrl).build())

        val binariesMediaTypes = arrayOf(
            MediaType.APPLICATION_OCTET_STREAM,
            MediaType.APPLICATION_PDF,
            MediaType.IMAGE_PNG,
            MediaType.IMAGE_JPEG,
            MediaType.IMAGE_GIF
        )
    }

    /**
     * @param url full URL for the resource
     * @param requestId unique ID for post idempotency
     * @param headerPopulator header populator
     * @param responseConsumer response callback
     */
    fun get(
        url: String, requestId: UUID = UUID.randomUUID(),
        headerPopulator: Consumer = Consumer { _ -> run {} }
    ): Flow {
        val eventSignature = entries(client(), uri(url), requestId(requestId.toString()))!!
        try {
            rpcLogger.debug(eventSignature, phase("PREPARE TO SEND"))
            return webClient.get()
                .uri(url)
                .accept(MediaType.APPLICATION_JSON)
                .headers(headerPopulator)
                .retrieve()
                .bodyToFlux()
                .retryWhen(getRetrySpec)
                .asFlow()
        } catch (e: WebClientResponseException) {
            // Don't log twice
            throw e
        } catch (e: Exception) {
            onException(eventSignature, e)
            throw e
        }
    }

    /**
     * @param url full URL for the resource
     * @param requestId unique ID for post idempotency
     * @param headerPopulator header populator
     * @param responseConsumer response callback
     */
    fun getBinary(
        url: String, requestId: UUID = UUID.randomUUID(),
        headerPopulator: Consumer = Consumer { _ -> run {} },
        responseCallback: Consumer? = null,
    ): Flow {
        val eventSignature = entries(client(), uri(url), requestId(requestId.toString()))!!
        try {
            rpcLogger.debug(eventSignature, phase("PREPARE TO SEND"))

            val spec = webClient.get()
                .uri(url)
                .accept(*binariesMediaTypes)
                .headers(headerPopulator)

            return spec
                .exchange()
                .flatMapMany { clientResponse ->
                    val statusCode = clientResponse.statusCode()
                    rpcLogger.debug(eventSignature, phase("ON_RESPONSE"), statusCode(statusCode.value()))

                    if (statusCode.is2xxSuccessful) {
                        rpcLogger.info(
                            eventSignature,
                            phase("SUCCESS"),
                            statusCode(statusCode.value())
                        )
                    } else {
                        rpcLogger.error(
                            eventSignature,
                            phase("FAILURE"),
                            statusCode(statusCode.value()),
                            errorMessage(statusCode.reasonPhrase)
                        )
                    }
                    // Response consumed, logged and cleaned up

                    // RETURN
//                    if (statusCode.isError) {
//                        throw clientResponse.createExceptionAndAwait()
//                    }

                    val osPipe = PipedOutputStream()
                    val isPipe = PipedInputStream(osPipe)

                    val body: Flux = clientResponse.body(BodyExtractors.toDataBuffers())
                        .doOnError { t ->
                            logger.error("Error reading body.", t)
                            // close pipe to force InputStream to error,
                            // otherwise the returned InputStream will hang forever if an error occurs
                            try {
                                isPipe.use {}
                            } catch (ioe: IOException) {
                                logger.error("Error closing streams", ioe)
                            }
                        }
                        .doFinally { s ->
                            try {
                                osPipe.use {}
                            } catch (ioe: IOException) {
                                logger.error("Error closing streams", ioe)
                            }
                        }
                    body
                }
//                .retrieve()
//                .bodyToFlux(DataBuffer::class.java)
                .asFlow()
        } catch (e: WebClientResponseException) {
            // Don't log twice
            throw e
        } catch (e: Exception) {
            onException(eventSignature, e)
            throw e
        }
    }

    /**
     * @param url full URL for the resource
     * @param requestId unique ID for post idempotency
     * @param requestBody post body object
     * @param headerPopulator header populator
     * @param responseConsumer response callback
     */
    suspend fun  post(
        url: String, requestId: UUID,
        requestBody: T,
        headerPopulator: Consumer = Consumer { _ -> run {} },
        responseCallback: Consumer? = null
    ): WebClientResponse {
        val eventSignature = entries(client(), uri(url), requestId(requestId.toString()))!!
        rpcLogger.debug(eventSignature, phase("PREPARE TO SEND"), requestBody(requestBody, false))

        val requestBodyInserters = BodyInserters.fromValue(requestBody)
        return post(url, requestId, requestBodyInserters, headerPopulator, responseCallback, eventSignature)
    }

    /**
     * @param url full URL for the resource
     * @param requestId unique ID for post idempotency
     * @param publisher reactive publisher
     * @param elementClass class of the produced object from publisher
     * @param headerPopulator header populator
     * @param responseConsumer response callback
     */
    suspend fun > post(
        url: String, requestId: UUID,
        publisher: P, elementClass: Class,
        headerPopulator: Consumer = Consumer { _ -> run {} },
        responseCallback: Consumer? = null
    ): WebClientResponse {
        val eventSignature = entries(client(), uri(url), requestId(requestId.toString()))!!
        rpcLogger.debug(eventSignature, phase("PREPARE TO SEND"))

        return post(
            url,
            requestId,
            BodyInserters.fromPublisher(publisher, elementClass),
            headerPopulator,
            responseCallback,
            eventSignature
        )
    }

    /**
     * @param url full URL for the resource
     * @param requestId unique ID for post idempotency
     * @param requestBodyInserters BodyInserter for request body
     * @param headerPopulator header populator
     * @param responseConsumer response callback
     */
    suspend fun  post(
        url: String, requestId: UUID,
        requestBodyInserters: BodyInserter,
        headerPopulator: Consumer = Consumer { _ -> run {} },
        responseCallback: Consumer? = null,
        previousEventSignature: Array>? = null
    ): WebClientResponse {
        val eventSignature = previousEventSignature ?: let {
            // ONLY log when not previously logged
            val newEventSignature = entries(client(), uri(url), requestId(requestId.toString()))!!
            rpcLogger.debug(newEventSignature, phase("PREPARE TO SEND"))
            newEventSignature
        }

        try {
            val spec = webClient.post()
                .uri(url)
                .contentType(MediaType.APPLICATION_JSON)
                .headers(headerPopulator)
                .body(requestBodyInserters)

            val webClientResult = spec
                .awaitExchange { clientResponse ->
                    val statusCode = clientResponse.statusCode()
                    rpcLogger.debug(eventSignature, phase("ON_RESPONSE"), statusCode(statusCode.value()))

                    val body = clientResponse.awaitBody()
                    if (statusCode.is2xxSuccessful) {
                        rpcLogger.info(
                            eventSignature,
                            phase("SUCCESS"),
                            statusCode(statusCode.value()),
                            responseBody(body, false)
                        )
                    } else {
                        rpcLogger.error(
                            eventSignature,
                            phase("FAILURE"),
                            statusCode(statusCode.value()),
                            errorMessage(statusCode.reasonPhrase),
                            responseBody(body, false)
                        )
                    }
                    // Response consumed, logged and cleaned up

                    // CALLBACK IF NEEDED
                    val webClientResult = WebClientResponse(statusCode, body)
                    responseCallback?.accept(webClientResult)

                    // RETURN
                    if (statusCode.isError) {
                        throw clientResponse.createExceptionAndAwait()
                    }
                    webClientResult
                }
            rpcLogger.debug(eventSignature, statusCode(webClientResult.statusCode.value()), phase("FINISHED"))
            return webClientResult
        } catch (e: WebClientResponseException) {
            // Don't log twice
            throw e
        } catch (e: Exception) {
            onException(eventSignature, e)
            throw e
        }
    }

    fun onException(eventSignature: Array>, e: Exception) {
        rpcLogger.error(eventSignature, errorMessage(e.message))
    }

    /** Logger for all inherited class */
    val logger = LoggerFactory.getLogger(this::class.java)
    val rpcLogger = RpcLogger.create(logger)
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy