From 20bd48d7f8c7a9560c97ed126108567f5db01a78 Mon Sep 17 00:00:00 2001 From: Dana AlTayeh Date: Tue, 19 Nov 2024 19:02:07 +0300 Subject: [PATCH] feat: SDK-1568 expose okhttp client as a configuration in sdk core --- .../sdk/core/client/BaseRapidClient.kt | 61 +++++++- .../expediagroup/sdk/core/client/Client.kt | 8 +- .../client/CompositeOkHttpEventListener.kt | 140 ++++++++++++++++++ .../sdk/core/client/OkHttpEventListener.kt | 61 +++++--- .../configuration/RapidClientConfiguration.kt | 4 +- 5 files changed, 241 insertions(+), 33 deletions(-) create mode 100644 core/src/main/kotlin/com/expediagroup/sdk/core/client/CompositeOkHttpEventListener.kt diff --git a/core/src/main/kotlin/com/expediagroup/sdk/core/client/BaseRapidClient.kt b/core/src/main/kotlin/com/expediagroup/sdk/core/client/BaseRapidClient.kt index c1b903440..5bb9c8fef 100644 --- a/core/src/main/kotlin/com/expediagroup/sdk/core/client/BaseRapidClient.kt +++ b/core/src/main/kotlin/com/expediagroup/sdk/core/client/BaseRapidClient.kt @@ -21,7 +21,9 @@ import com.expediagroup.sdk.core.configuration.provider.ConfigurationProvider import com.expediagroup.sdk.core.configuration.provider.RapidConfigurationProvider import com.expediagroup.sdk.core.plugin.authentication.strategy.AuthenticationStrategy import io.ktor.client.HttpClient -import io.ktor.client.engine.HttpClientEngine +import io.ktor.client.engine.* +import io.ktor.client.engine.okhttp.* +import okhttp3.OkHttpClient /** * The integration point between the SDK Core and the product SDKs. @@ -32,14 +34,25 @@ import io.ktor.client.engine.HttpClientEngine abstract class BaseRapidClient( namespace: String, clientConfiguration: RapidClientConfiguration, - httpClientEngine: HttpClientEngine = DEFAULT_HTTP_CLIENT_ENGINE ) : Client(namespace) { private val _configurationProvider: ConfigurationProvider = ConfigurationCollector.create( clientConfiguration.toProvider(), RapidConfigurationProvider ) - private val _httpClient: HttpClient = buildHttpClient(_configurationProvider, AuthenticationStrategy.AuthenticationType.SIGNATURE, httpClientEngine) + + private val _httpClientEngine: HttpClientEngine = clientConfiguration.okHttpClient?.let { + + // Compose an event listener by joining two event listeners + // Initialize the CompositeOkHttpEventListener with the OkHttpEventListener and the user passed eventListenerFactory + CompositeOkHttpEventListener.initialize(OkHttpEventListener.FACTORY, it.eventListenerFactory) + + OkHttp.create { + preconfigured = it.newBuilder().eventListenerFactory(CompositeOkHttpEventListener).build() + } + } ?: DEFAULT_HTTP_CLIENT_ENGINE + + private val _httpClient: HttpClient = buildHttpClient(_configurationProvider, AuthenticationStrategy.AuthenticationType.SIGNATURE, _httpClientEngine) init { finalize() @@ -53,5 +66,45 @@ abstract class BaseRapidClient( /** A [BaseRapidClient] builder. */ @Suppress("unused", "UnnecessaryAbstractClass") // This is used by the generated SDK clients. - abstract class Builder> : Client.Builder() + abstract class Builder> : Client.Builder() { + /** Sets the custom okHttpClient to use for making requests. */ + protected var okHttpClient: OkHttpClient? = null + + /** Sets the custom okHttpClient to use for making requests. + * + * @param okHttpClient The custom okHttpClient to use for making requests. + * @return The [Builder] instance. + */ + fun okHttpClient(okHttpClient: OkHttpClient): SELF { + + if (this.connectionTimeout != null || this.requestTimeout != null || this.socketTimeout != null) { + throw IllegalArgumentException("Cannot set a custom OkHttp client when using the default httpclient with configured timeouts.") + } + + this.okHttpClient = okHttpClient + return self() + } + + override fun requestTimeout(milliseconds: Long): SELF { + if (this.okHttpClient != null) { + throw IllegalArgumentException("Cannot set timeouts when using a custom OkHttpClient, set timeouts on the OkHttpClient instead.") + } + return super.requestTimeout(milliseconds) + } + + override fun socketTimeout(milliseconds: Long): SELF { + if (this.okHttpClient != null) { + throw IllegalArgumentException("Cannot set timeouts when using a custom OkHttpClient, set timeouts on the OkHttpClient instead.") + } + return super.socketTimeout(milliseconds) + } + + override fun connectionTimeout(milliseconds: Long): SELF { + if (this.okHttpClient != null) { + throw IllegalArgumentException("Cannot set timeouts when using a custom OkHttpClient, set timeouts on the OkHttpClient instead.") + } + return super.connectionTimeout(milliseconds) + } + + } } diff --git a/core/src/main/kotlin/com/expediagroup/sdk/core/client/Client.kt b/core/src/main/kotlin/com/expediagroup/sdk/core/client/Client.kt index 11ed5ea8d..80c4f8e94 100644 --- a/core/src/main/kotlin/com/expediagroup/sdk/core/client/Client.kt +++ b/core/src/main/kotlin/com/expediagroup/sdk/core/client/Client.kt @@ -61,7 +61,7 @@ val dispatcher = Dispatcher().apply { val DEFAULT_HTTP_CLIENT_ENGINE: HttpClientEngine = OkHttp.create { config { - eventListener(OkHttpEventListener) + eventListenerFactory(OkHttpEventListener.FACTORY) dispatcher(dispatcher) } } @@ -231,7 +231,7 @@ abstract class Client( * @param milliseconds The request timeout to be used. * @return The [Builder] instance. */ - fun requestTimeout(milliseconds: Long): SELF { + open fun requestTimeout(milliseconds: Long): SELF { this.requestTimeout = milliseconds log.info(LoggingMessageProvider.getRuntimeConfigurationProviderMessage(ConfigurationName.REQUEST_TIMEOUT_MILLIS, milliseconds.toString())) return self() @@ -245,7 +245,7 @@ abstract class Client( * @param milliseconds The connection timeout to be used. * @return The [Builder] instance. */ - fun connectionTimeout(milliseconds: Long): SELF { + open fun connectionTimeout(milliseconds: Long): SELF { this.connectionTimeout = milliseconds log.info(LoggingMessageProvider.getRuntimeConfigurationProviderMessage(ConfigurationName.CONNECTION_TIMEOUT_MILLIS, milliseconds.toString())) return self() @@ -259,7 +259,7 @@ abstract class Client( * @param milliseconds The socket timeout to be used. * @return The [Builder] instance. */ - fun socketTimeout(milliseconds: Long): SELF { + open fun socketTimeout(milliseconds: Long): SELF { this.socketTimeout = milliseconds log.info(LoggingMessageProvider.getRuntimeConfigurationProviderMessage(ConfigurationName.SOCKET_TIMEOUT_MILLIS, milliseconds.toString())) return self() diff --git a/core/src/main/kotlin/com/expediagroup/sdk/core/client/CompositeOkHttpEventListener.kt b/core/src/main/kotlin/com/expediagroup/sdk/core/client/CompositeOkHttpEventListener.kt new file mode 100644 index 000000000..bbf9877af --- /dev/null +++ b/core/src/main/kotlin/com/expediagroup/sdk/core/client/CompositeOkHttpEventListener.kt @@ -0,0 +1,140 @@ +package com.expediagroup.sdk.core.client + +import okhttp3.Call +import okhttp3.EventListener +import okhttp3.Response +import java.io.IOException + +// A CompositeEventListener that takes multiple event listener factories +class CompositeOkHttpEventListener private constructor(private val listeners: List) : EventListener() { + + companion object Factory : EventListener.Factory { + private lateinit var factories: Array + + fun initialize(vararg factories: EventListener.Factory) { + this.factories = factories + } + + override fun create(call: Call): EventListener { + val listeners = factories.map { it.create(call) } + return CompositeOkHttpEventListener(listeners) + } + } + + override fun callStart(call: Call) { + listeners.forEach { it.callStart(call) } + } + + override fun callEnd(call: Call) { + listeners.forEach { it.callEnd(call) } + } + + override fun callFailed(call: Call, ioe: IOException) { + listeners.forEach { it.callFailed(call, ioe) } + } + + override fun canceled(call: Call) { + listeners.forEach { it.canceled(call) } + } + + override fun connectStart(call: Call, inetSocketAddress: java.net.InetSocketAddress, proxy: java.net.Proxy) { + listeners.forEach { it.connectStart(call, inetSocketAddress, proxy) } + } + + override fun connectEnd(call: Call, inetSocketAddress: java.net.InetSocketAddress, proxy: java.net.Proxy, protocol: okhttp3.Protocol?) { + listeners.forEach { it.connectEnd(call, inetSocketAddress, proxy, protocol) } + } + + override fun connectFailed(call: Call, inetSocketAddress: java.net.InetSocketAddress, proxy: java.net.Proxy, protocol: okhttp3.Protocol?, ioe: IOException) { + listeners.forEach { it.connectFailed(call, inetSocketAddress, proxy, protocol, ioe) } + } + + override fun requestHeadersStart(call: Call) { + listeners.forEach { it.requestHeadersStart(call) } + } + + override fun requestHeadersEnd(call: Call, request: okhttp3.Request) { + listeners.forEach { it.requestHeadersEnd(call, request) } + } + + override fun requestBodyStart(call: Call) { + listeners.forEach { it.requestBodyStart(call) } + } + + override fun requestBodyEnd(call: Call, byteCount: Long) { + listeners.forEach { it.requestBodyEnd(call, byteCount) } + } + + override fun responseHeadersStart(call: Call) { + listeners.forEach { it.responseHeadersStart(call) } + } + + override fun responseHeadersEnd(call: Call, response: okhttp3.Response) { + listeners.forEach { it.responseHeadersEnd(call, response) } + } + + override fun responseBodyStart(call: Call) { + listeners.forEach { it.responseBodyStart(call) } + } + + override fun responseBodyEnd(call: Call, byteCount: Long) { + listeners.forEach { it.responseBodyEnd(call, byteCount) } + } + + override fun responseFailed(call: Call, ioe: IOException) { + listeners.forEach { it.responseFailed(call, ioe) } + } + + override fun proxySelectStart(call: Call, url: okhttp3.HttpUrl) { + listeners.forEach { it.proxySelectStart(call, url) } + } + + override fun proxySelectEnd(call: Call, url: okhttp3.HttpUrl, proxies: List) { + listeners.forEach { it.proxySelectEnd(call, url, proxies) } + } + + override fun dnsStart(call: Call, domainName: String) { + listeners.forEach { it.dnsStart(call, domainName) } + } + + override fun dnsEnd(call: Call, domainName: String, inetAddressList: List) { + listeners.forEach { it.dnsEnd(call, domainName, inetAddressList) } + } + + override fun connectionAcquired(call: Call, connection: okhttp3.Connection) { + listeners.forEach { it.connectionAcquired(call, connection) } + } + + override fun connectionReleased(call: Call, connection: okhttp3.Connection) { + listeners.forEach { it.connectionReleased(call, connection) } + } + + override fun secureConnectStart(call: Call) { + listeners.forEach { it.secureConnectStart(call) } + } + + override fun secureConnectEnd(call: Call, handshake: okhttp3.Handshake?) { + listeners.forEach { it.secureConnectEnd(call, handshake) } + } + + override fun requestFailed(call: Call, ioe: IOException) { + listeners.forEach { it.requestFailed(call, ioe) } + } + + override fun cacheHit(call: Call, response: Response) { + listeners.forEach { it.cacheHit(call, response) } + } + + override fun cacheMiss(call: Call) { + listeners.forEach { it.cacheMiss(call) } + } + + override fun cacheConditionalHit(call: Call, response: Response) { + listeners.forEach { it.cacheConditionalHit(call, response) } + } + + override fun satisfactionFailure(call: Call, response: Response) { + listeners.forEach { it.satisfactionFailure(call, response) } + } + +} diff --git a/core/src/main/kotlin/com/expediagroup/sdk/core/client/OkHttpEventListener.kt b/core/src/main/kotlin/com/expediagroup/sdk/core/client/OkHttpEventListener.kt index 25dbd741a..4845cdf55 100644 --- a/core/src/main/kotlin/com/expediagroup/sdk/core/client/OkHttpEventListener.kt +++ b/core/src/main/kotlin/com/expediagroup/sdk/core/client/OkHttpEventListener.kt @@ -19,28 +19,41 @@ import com.expediagroup.sdk.core.constant.HeaderKey import com.expediagroup.sdk.core.plugin.logging.ExpediaGroupLoggerFactory import okhttp3.Call import okhttp3.Connection -import okhttp3.EventListener import okhttp3.Handshake import okhttp3.Protocol import okhttp3.Request import okhttp3.Response +import okhttp3.EventListener import java.io.IOException import java.net.InetSocketAddress import java.net.Proxy +import java.util.UUID +import java.util.concurrent.atomic.AtomicReference + +data class OkHttpEventListener private constructor(private val transactionId: AtomicReference) : EventListener() { -object OkHttpEventListener : EventListener() { private val log = ExpediaGroupLoggerFactory.getLogger(this::class.java) - fun Call.getTransactionId() = request().headers[HeaderKey.TRANSACTION_ID] + // Factory for creating EventListeners with transaction IDs + companion object { + // Expose the factory as a static property + val FACTORY: EventListener.Factory = object : EventListener.Factory { + override fun create(call: Call): EventListener { + val transactionIdHeader = call.request().header(HeaderKey.TRANSACTION_ID) + val transactionId = AtomicReference(UUID.fromString(transactionIdHeader)) + return OkHttpEventListener(transactionId) + } + } + } override fun callStart(call: Call) { super.callStart(call) - log.debug("Call start for transaction-id: [${call.getTransactionId()}]") + log.debug("Call start for transaction-id: [${transactionId.get()}]") } override fun callEnd(call: Call) { super.callEnd(call) - log.debug("Call end for transaction-id: [${call.getTransactionId()}]") + log.debug("Call end for transaction-id: [${transactionId.get()}]") } override fun callFailed( @@ -48,12 +61,12 @@ object OkHttpEventListener : EventListener() { ioe: IOException ) { super.callFailed(call, ioe) - log.debug("Call failed for transaction-id: [${call.getTransactionId()}] with exception message: ${ioe.message}") + log.debug("Call failed for transaction-id: [${transactionId.get()}] with exception message: ${ioe.message}") } override fun canceled(call: Call) { super.canceled(call) - log.debug("Call canceled for transaction-id: [${call.getTransactionId()}]") + log.debug("Call canceled for transaction-id: [${transactionId.get()}]") } override fun connectStart( @@ -62,7 +75,7 @@ object OkHttpEventListener : EventListener() { proxy: Proxy ) { super.connectStart(call, inetSocketAddress, proxy) - log.debug("Connect start for transaction-id: [${call.getTransactionId()}]") + log.debug("Connect start for transaction-id: [${transactionId.get()}]") } override fun connectEnd( @@ -72,7 +85,7 @@ object OkHttpEventListener : EventListener() { protocol: Protocol? ) { super.connectEnd(call, inetSocketAddress, proxy, protocol) - log.debug("Connect end for transaction-id: [${call.getTransactionId()}]") + log.debug("Connect end for transaction-id: [${transactionId.get()}]") } override fun connectFailed( @@ -83,7 +96,7 @@ object OkHttpEventListener : EventListener() { ioe: IOException ) { super.connectFailed(call, inetSocketAddress, proxy, protocol, ioe) - log.debug("Connect failed for transaction-id: [${call.getTransactionId()}] with exception message: ${ioe.message}") + log.debug("Connect failed for transaction-id: [${transactionId.get()}] with exception message: ${ioe.message}") } override fun connectionAcquired( @@ -91,7 +104,7 @@ object OkHttpEventListener : EventListener() { connection: Connection ) { super.connectionAcquired(call, connection) - log.debug("Connection acquired for transaction-id: [${call.getTransactionId()}]") + log.debug("Connection acquired for transaction-id: [${transactionId.get()}]") } override fun connectionReleased( @@ -99,12 +112,12 @@ object OkHttpEventListener : EventListener() { connection: Connection ) { super.connectionReleased(call, connection) - log.debug("Connection released for transaction-id: [${call.getTransactionId()}]") + log.debug("Connection released for transaction-id: [${transactionId.get()}]") } override fun secureConnectStart(call: Call) { super.secureConnectStart(call) - log.debug("Secure connect start for transaction-id: [${call.getTransactionId()}]") + log.debug("Secure connect start for transaction-id: [${transactionId.get()}]") } override fun secureConnectEnd( @@ -112,12 +125,12 @@ object OkHttpEventListener : EventListener() { handshake: Handshake? ) { super.secureConnectEnd(call, handshake) - log.debug("Secure connect end for transaction-id: [${call.getTransactionId()}]") + log.debug("Secure connect end for transaction-id: [${transactionId.get()}]") } override fun requestHeadersStart(call: Call) { super.requestHeadersStart(call) - log.debug("Sending request headers start for transaction-id: [${call.getTransactionId()}]") + log.debug("Sending request headers start for transaction-id: [${transactionId.get()}]") } override fun requestHeadersEnd( @@ -125,12 +138,12 @@ object OkHttpEventListener : EventListener() { request: Request ) { super.requestHeadersEnd(call, request) - log.debug("Sending request headers end for transaction-id: [${call.getTransactionId()}]") + log.debug("Sending request headers end for transaction-id: [${transactionId.get()}]") } override fun requestBodyStart(call: Call) { super.requestBodyStart(call) - log.debug("Sending request body start for transaction-id: [${call.getTransactionId()}]") + log.debug("Sending request body start for transaction-id: [${transactionId.get()}]") } override fun requestBodyEnd( @@ -138,7 +151,7 @@ object OkHttpEventListener : EventListener() { byteCount: Long ) { super.requestBodyEnd(call, byteCount) - log.debug("Sending request body end for transaction-id: [${call.getTransactionId()}] with byte count: $byteCount") + log.debug("Sending request body end for transaction-id: [${transactionId.get()}] with byte count: $byteCount") } override fun requestFailed( @@ -146,12 +159,12 @@ object OkHttpEventListener : EventListener() { ioe: IOException ) { super.requestFailed(call, ioe) - log.debug("Request failed for transaction-id: [${call.getTransactionId()}] with exception message: ${ioe.message}") + log.debug("Request failed for transaction-id: [${transactionId.get()}] with exception message: ${ioe.message}") } override fun responseHeadersStart(call: Call) { super.responseHeadersStart(call) - log.debug("Receiving response headers start for transaction-id: [${call.getTransactionId()}]") + log.debug("Receiving response headers start for transaction-id: [${transactionId.get()}]") } override fun responseHeadersEnd( @@ -159,12 +172,12 @@ object OkHttpEventListener : EventListener() { response: Response ) { super.responseHeadersEnd(call, response) - log.debug("Receiving response headers end for transaction-id: [${call.getTransactionId()}]") + log.debug("Receiving response headers end for transaction-id: [${transactionId.get()}]") } override fun responseBodyStart(call: Call) { super.responseBodyStart(call) - log.debug("Receiving response body start for transaction-id: [${call.getTransactionId()}]") + log.debug("Receiving response body start for transaction-id: [${transactionId.get()}]") } override fun responseBodyEnd( @@ -172,7 +185,7 @@ object OkHttpEventListener : EventListener() { byteCount: Long ) { super.responseBodyEnd(call, byteCount) - log.debug("Receiving response body end for transaction-id: [${call.getTransactionId()}] with byte count: $byteCount") + log.debug("Receiving response body end for transaction-id: [${transactionId.get()}] with byte count: $byteCount") } override fun responseFailed( @@ -180,6 +193,6 @@ object OkHttpEventListener : EventListener() { ioe: IOException ) { super.responseFailed(call, ioe) - log.debug("Receiving response failed for transaction-id: [${call.getTransactionId()}] with exception message: ${ioe.message}") + log.debug("Receiving response failed for transaction-id: [${transactionId.get()}] with exception message: ${ioe.message}") } } diff --git a/core/src/main/kotlin/com/expediagroup/sdk/core/configuration/RapidClientConfiguration.kt b/core/src/main/kotlin/com/expediagroup/sdk/core/configuration/RapidClientConfiguration.kt index cac3637de..523d116f2 100644 --- a/core/src/main/kotlin/com/expediagroup/sdk/core/configuration/RapidClientConfiguration.kt +++ b/core/src/main/kotlin/com/expediagroup/sdk/core/configuration/RapidClientConfiguration.kt @@ -16,6 +16,7 @@ package com.expediagroup.sdk.core.configuration import com.expediagroup.sdk.core.client.BaseRapidClient +import okhttp3.OkHttpClient /** * Configuration for the [BaseRapidClient]. @@ -37,5 +38,6 @@ data class RapidClientConfiguration( override val connectionTimeout: Long? = null, override val socketTimeout: Long? = null, override val maskedLoggingHeaders: Set? = null, - override val maskedLoggingBodyFields: Set? = null + override val maskedLoggingBodyFields: Set? = null, + val okHttpClient: OkHttpClient? = null ) : ClientConfiguration