diff --git a/push-message-provider-hpk/src/main/kotlin/ru/touchin/push/message/provider/hpk/base/builders/Buildable.kt b/push-message-provider-hpk/src/main/kotlin/ru/touchin/push/message/provider/hpk/base/builders/Buildable.kt new file mode 100644 index 0000000..a7c1910 --- /dev/null +++ b/push-message-provider-hpk/src/main/kotlin/ru/touchin/push/message/provider/hpk/base/builders/Buildable.kt @@ -0,0 +1,3 @@ +package ru.touchin.push.message.provider.hpk.base.builders + +internal interface Buildable diff --git a/push-message-provider-hpk/src/main/kotlin/ru/touchin/push/message/provider/hpk/base/clients/ConditionalWebClientParser.kt b/push-message-provider-hpk/src/main/kotlin/ru/touchin/push/message/provider/hpk/base/clients/ConditionalWebClientParser.kt new file mode 100644 index 0000000..d1d3063 --- /dev/null +++ b/push-message-provider-hpk/src/main/kotlin/ru/touchin/push/message/provider/hpk/base/clients/ConditionalWebClientParser.kt @@ -0,0 +1,42 @@ +package ru.touchin.push.message.provider.hpk.base.clients + +import com.fasterxml.jackson.databind.ObjectMapper +import org.springframework.web.reactive.function.client.ClientResponse +import ru.touchin.push.message.provider.hpk.base.clients.dto.ConditionalResponse + +internal open class ConditionalWebClientParser( + private val objectMapper: ObjectMapper, +) { + + open fun isOkResponse(clientResponse: ClientResponse): Boolean { + return clientResponse.statusCode().is2xxSuccessful + } + + @Throws(Exception::class) + inline fun parse( + clientResponse: ClientResponse, + body: String, + ): ConditionalResponse { + return if (isOkResponse(clientResponse)) { + ConditionalResponse( + success = parseValue(body, S::class.java), + failure = null + ) + } else { + ConditionalResponse( + success = null, + failure = parseValue(body, F::class.java) + ) + } + } + + private fun parseValue(source: String?, clazz: Class): T { + return if (clazz.canonicalName != String::class.java.canonicalName) { + objectMapper.readValue(source, clazz) + } else { + @Suppress("UNCHECKED_CAST") + source as T // T is String + } + } + +} diff --git a/push-message-provider-hpk/src/main/kotlin/ru/touchin/push/message/provider/hpk/base/clients/ConfigurableWebClient.kt b/push-message-provider-hpk/src/main/kotlin/ru/touchin/push/message/provider/hpk/base/clients/ConfigurableWebClient.kt new file mode 100644 index 0000000..f2df3d3 --- /dev/null +++ b/push-message-provider-hpk/src/main/kotlin/ru/touchin/push/message/provider/hpk/base/clients/ConfigurableWebClient.kt @@ -0,0 +1,85 @@ +package ru.touchin.push.message.provider.hpk.base.clients + +import io.netty.channel.ChannelOption +import io.netty.handler.ssl.SslContextBuilder +import io.netty.handler.timeout.ReadTimeoutHandler +import io.netty.handler.timeout.WriteTimeoutHandler +import org.springframework.http.client.reactive.ReactorClientHttpConnector +import org.springframework.web.reactive.function.client.ClientResponse +import org.springframework.web.reactive.function.client.WebClient +import reactor.core.publisher.Mono +import reactor.core.scheduler.Schedulers +import reactor.netty.http.client.HttpClient +import ru.touchin.common.spring.web.webclient.BaseLogWebClient +import ru.touchin.common.spring.web.webclient.dto.RequestLogData +import ru.touchin.common.spring.web.webclient.logger.WebClientLogger +import ru.touchin.push.message.provider.hpk.base.clients.dto.ConditionalResponse +import ru.touchin.push.message.provider.hpk.properties.HpkProperties +import java.util.concurrent.TimeUnit + +abstract class ConfigurableWebClient( + webClientLogger: WebClientLogger, + webClientBuilder: WebClient.Builder, + protected val webService: HpkProperties.WebService, +) : BaseLogWebClient(webClientLogger, webClientBuilder) { + + private val conditionalWebClientParser: Lazy = lazy { + ConditionalWebClientParser( + objectMapper = getObjectMapper(), + ) + } + + protected fun WebClient.Builder.setTimeouts(): WebClient.Builder { + val httpClient: HttpClient = HttpClient.create() + .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, webService.http.connectionTimeout.toMillis().toInt()) + .doOnConnected { setup -> + setup.addHandlerLast(ReadTimeoutHandler(webService.http.readTimeout.toMillis(), TimeUnit.MILLISECONDS)) + setup.addHandlerLast(WriteTimeoutHandler(webService.http.writeTimeout.toMillis(), TimeUnit.MILLISECONDS)) + } + .let { httpClient -> + webService.ssl?.let { ssl -> + httpClient.secure { builder -> + builder + .sslContext(SslContextBuilder.forClient().build()) + .handshakeTimeout(ssl.handshakeTimeout) + .closeNotifyFlushTimeout(ssl.notifyFlushTimeout) + .closeNotifyReadTimeout(ssl.notifyReadTimeout) + } + } ?: httpClient + } + + return clientConnector(ReactorClientHttpConnector(httpClient)) + } + + internal inline fun WebClient.RequestHeadersSpec<*>.exchangeWithWrap( + requestLogData: RequestLogData, + ): Mono> { + return exchangeToMono { clientResponse -> + parse(clientResponse) + }.doOnNext { responseWrapper -> + getLogger().log( + requestLogData.copy( + responseBody = responseWrapper.success ?: responseWrapper.failure + ) + ) + } + } + + internal inline fun parse( + clientResponse: ClientResponse, + ): Mono> { + val responseBody = clientResponse + .bodyToMono(String::class.java) + .defaultIfEmpty(String()) + .publishOn(Schedulers.parallel()) + + return responseBody + .map { body -> + conditionalWebClientParser.value.parse( + clientResponse = clientResponse, + body = body, + ) + } + } + +} diff --git a/push-message-provider-hpk/src/main/kotlin/ru/touchin/push/message/provider/hpk/base/clients/dto/ConditionalResponse.kt b/push-message-provider-hpk/src/main/kotlin/ru/touchin/push/message/provider/hpk/base/clients/dto/ConditionalResponse.kt new file mode 100644 index 0000000..ee865b8 --- /dev/null +++ b/push-message-provider-hpk/src/main/kotlin/ru/touchin/push/message/provider/hpk/base/clients/dto/ConditionalResponse.kt @@ -0,0 +1,15 @@ +package ru.touchin.push.message.provider.hpk.base.clients.dto + +internal open class ConditionalResponse( + val success: S?, + val failure: F?, +) { + + init { + // Only one value should be present + check((success == null) != (failure == null)) + } + + val isSuccess: Boolean = success != null + +} diff --git a/push-message-provider-hpk/src/main/kotlin/ru/touchin/push/message/provider/hpk/base/enums/ValueableSerializableEnum.kt b/push-message-provider-hpk/src/main/kotlin/ru/touchin/push/message/provider/hpk/base/enums/ValueableSerializableEnum.kt new file mode 100644 index 0000000..388d0c9 --- /dev/null +++ b/push-message-provider-hpk/src/main/kotlin/ru/touchin/push/message/provider/hpk/base/enums/ValueableSerializableEnum.kt @@ -0,0 +1,12 @@ +package ru.touchin.push.message.provider.hpk.base.enums + +import com.fasterxml.jackson.annotation.JsonValue + +internal interface ValueableSerializableEnum { + + val value: T + + @JsonValue + fun toValue(): T = value + +} diff --git a/push-message-provider-hpk/src/main/kotlin/ru/touchin/push/message/provider/hpk/base/extensions/BuilderExtensions.kt b/push-message-provider-hpk/src/main/kotlin/ru/touchin/push/message/provider/hpk/base/extensions/BuilderExtensions.kt new file mode 100644 index 0000000..fb00d9c --- /dev/null +++ b/push-message-provider-hpk/src/main/kotlin/ru/touchin/push/message/provider/hpk/base/extensions/BuilderExtensions.kt @@ -0,0 +1,7 @@ +package ru.touchin.push.message.provider.hpk.base.extensions + +import ru.touchin.push.message.provider.hpk.base.builders.Buildable + +fun B.ifNotNull(value: V?, setter: B.(V) -> B): B { + return value?.let { setter(it) } ?: this +}