Skip to content

Commit

Permalink
fix: default OkHttpClient enhancements and testing (#125)
Browse files Browse the repository at this point in the history
  • Loading branch information
Mohammad-Dwairi authored Dec 11, 2024
1 parent d287f74 commit 2fa398b
Show file tree
Hide file tree
Showing 14 changed files with 991 additions and 149 deletions.
3 changes: 2 additions & 1 deletion code/build.gradle
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
plugins {
id 'org.jetbrains.kotlin.jvm' version '2.0.21'
id 'org.jetbrains.kotlin.jvm' version '2.1.0'
id 'org.jetbrains.dokka' version '1.9.20'
id 'com.apollographql.apollo' version '4.1.0'

Expand Down Expand Up @@ -59,6 +59,7 @@ dependencies {
testImplementation platform('org.junit:junit-bom:5.11.3')
testImplementation 'org.junit.jupiter:junit-jupiter-api'
testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine'
testImplementation 'io.mockk:mockk:1.13.13'
}

apply from: "tasks-gradle/apollo.gradle"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@ package com.expediagroup.sdk.core.authentication.bearer
import com.expediagroup.sdk.core.authentication.common.AuthenticationManager
import com.expediagroup.sdk.core.authentication.common.Credentials
import com.expediagroup.sdk.core.client.Transport
import com.expediagroup.sdk.core.http.Method
import com.expediagroup.sdk.core.http.CommonMediaTypes
import com.expediagroup.sdk.core.http.Method
import com.expediagroup.sdk.core.http.Request
import com.expediagroup.sdk.core.http.RequestBody
import com.expediagroup.sdk.core.http.Response
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,20 +28,12 @@ import okhttp3.OkHttpClient
*
* ## Usage
* - Use `getInstance()` to retrieve the singleton instance of `OkHttpClient`.
* - Use `getConfiguredInstance(configuration)` to create a configured `OkHttpClient` instance
* - Use `getInstance(configuration)` to create a configured `OkHttpClient` instance
* with specific settings provided via the `OkHttpClientConfiguration` object.
*
* ## Thread Safety
* This class ensures that the singleton instance is initialized in a thread-safe manner using
* the double-checked locking pattern.
*/
internal object BaseOkHttpClient {
/**
* Volatile storage for the singleton `OkHttpClient` instance.
* Ensures visibility and prevents duplicate initialization in a multithreaded environment.
*/
@Volatile
private var instance: OkHttpClient? = null

private val instance: OkHttpClient = OkHttpClient()

/**
* Retrieves the singleton instance of `OkHttpClient`.
Expand All @@ -51,24 +43,20 @@ internal object BaseOkHttpClient {
*
* @return The singleton instance of `OkHttpClient`.
*/
fun getInstance(): OkHttpClient {
return instance ?: synchronized(this) {
instance ?: OkHttpClient().also { instance = it }
}
}
fun getInstance(): OkHttpClient = instance

/**
* Creates a new `OkHttpClient` instance configured with the provided settings.
*
* This method uses the singleton instance as a base and applies the settings specified
* in the `OkHttpClientConfiguration` object to create a customized `OkHttpClient`.
* Applies the given configuration to a base OkHttpClient and returns a new instance.
* NOTE: The returned instance is not a completely new instance of the OKHttpClient, it shares the same connection pool
* and other shared resources with the base instance. Except for some cases where a custom connection pool is passed
* through the configuration.
*
* @param configuration The `OkHttpClientConfiguration` containing settings for the client.
* @return A new `OkHttpClient` instance configured with the specified settings.
* @param configuration The configuration to apply.
* @return A new OkHttpClient instance that shares resources with the base instance but
* configured with the provided configurations.
*/
fun getConfiguredInstance(configuration: OkHttpClientConfiguration): OkHttpClient = getInstance()
.newBuilder()
.apply {
fun getInstance(configuration: OkHttpClientConfiguration): OkHttpClient {
return instance.newBuilder().apply {
configuration.callTimeout?.let {
callTimeout(Duration.ofMillis(it.toLong()))
}
Expand All @@ -94,4 +82,5 @@ internal object BaseOkHttpClient {
addNetworkInterceptor(it)
}
}.build()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,10 @@ data class OkHttpClientConfiguration(

/**
* Sets the connection pool configuration.
*
* **WARNING: This configuration will create a new connection pool for the new instance and will not be shared
* with the base instance.**
*
* @param connectionPool The connection pool to use.
* @return The builder instance.
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,236 @@
/*
* Copyright (C) 2024 Expedia, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.expediagroup.sdk.core.okhttp

import com.expediagroup.sdk.core.http.Headers
import com.expediagroup.sdk.core.http.MediaType
import com.expediagroup.sdk.core.http.MediaType.Companion.parse
import com.expediagroup.sdk.core.http.Method
import com.expediagroup.sdk.core.http.Protocol
import com.expediagroup.sdk.core.http.Request
import com.expediagroup.sdk.core.http.RequestBody
import com.expediagroup.sdk.core.http.Response
import com.expediagroup.sdk.core.http.ResponseBody
import com.expediagroup.sdk.core.http.ResponseBody.Companion.create
import com.expediagroup.sdk.core.http.Status
import java.io.IOException
import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.ResponseBody.Companion.asResponseBody
import okio.BufferedSink

/**
* Converts an [okhttp3.Request] object to the SDK [Request] object.
*
* The conversion includes the URL, headers, and optionally the request body,
* adapting them to the SDK `Request` structure.
*
* @receiver The [okhttp3.Request] to convert.
* @return A new [Request] object representing the same HTTP request in the SDK format.
*/
fun okhttp3.Request.toSDKRequest(): Request {
val url = url.toUrl()
val method = Method.valueOf(method)
val headers = headers.toSDKHeaders()
val body = body?.toSDKRequestBody()

return Request
.builder()
.url(url)
.method(method)
.headers(headers)
.apply { body?.let { body(body) } }
.build()
}

/**
* Converts [okhttp3.Headers] to the SDK [Headers] object.
*
* This method maps the key-value pairs of OkHttp's headers to the SDK
* `Headers` format, preserving the structure.
*
* @receiver The [okhttp3.Headers] to convert.
* @return A new [Headers] object representing the same HTTP headers in the SDK Headers format.
*/
fun okhttp3.Headers.toSDKHeaders(): Headers {
return Headers
.builder()
.apply {
this@toSDKHeaders.toMultimap().entries.forEach {
add(it.key, it.value)
}
}
.build()
}

/**
* Converts an [okhttp3.RequestBody] to the SDK [RequestBody].
*
* This adapter replicates the behavior of the original [RequestBody],
* including content type, length, and writing logic, for use within the SDK.
*
* @receiver The [okhttp3.RequestBody] to convert.
* @return A new [RequestBody] compatible with the SDK.
*/
fun okhttp3.RequestBody.toSDKRequestBody(): RequestBody {
return object : RequestBody() {
override fun mediaType(): MediaType? = this@toSDKRequestBody.contentType()?.let {
parse(it.toString())
}

override fun contentLength() = this@toSDKRequestBody.contentLength()

@Throws(IOException::class)
override fun writeTo(sink: BufferedSink) = this@toSDKRequestBody.writeTo(sink)
}
}

/**
* Converts the SDK [Request] object to an [okhttp3.Request].
*
* This method adapts the SDK [Request] structure, including the URL, method,
* headers, and body, into a format compatible with OkHttp.
*
* @receiver The SDK [Request] to convert.
* @return A new [okhttp3.Request] representing the same HTTP request in OkHttp's format.
*/
fun Request.toOkHttpRequest(): okhttp3.Request {
val url = this.url
val method = this.method.name
val headers = this.headers.toOkHttpHeaders()
val body = this.body?.toOkHttpRequestBody()

return okhttp3.Request.Builder()
.url(url)
.headers(headers)
.method(method, body)
.build()
}

/**
* Converts the SDK [Headers] to [okhttp3.Headers].
*
* This method maps the SDK [Headers] entries into OkHttp's format, preserving
* the header names and their associated values.
*
* @receiver The SDK [Headers] to convert.
* @return A new [okhttp3.Headers] object representing the same HTTP headers.
*/
fun Headers.toOkHttpHeaders(): okhttp3.Headers {
return okhttp3.Headers.Builder()
.apply {
this@toOkHttpHeaders.entries().forEach { (name, values) ->
values.forEach { value -> this.add(name, value) }
}
}.build()
}

/**
* Converts the SDK [RequestBody] to an [okhttp3.RequestBody].
*
* This adapter replicates the behavior of the original SDK [RequestBody],
* including content type, length, and writing logic, for compatibility with OkHttp.
*
* @receiver The SDK [RequestBody] to convert.
* @return A new [okhttp3.RequestBody] compatible with OkHttp.
*/
fun RequestBody.toOkHttpRequestBody(): okhttp3.RequestBody {
val contentLength = this.contentLength()
val mediaType = this.mediaType().toString().toMediaTypeOrNull()

return object : okhttp3.RequestBody() {
override fun contentType() = mediaType

override fun contentLength() = contentLength

@Throws(IOException::class)
override fun writeTo(sink: BufferedSink) {
this@toOkHttpRequestBody.writeTo(sink)
}
}
}

/**
* Converts SDK [Response] to OkHttp [Response].
*
* @receiver The SDK [Response] to convert.
* @return An OkHttp [Response] object equivalent to the SDK [Response].
*/
fun Response.toOkHttpResponse(): okhttp3.Response {
return okhttp3.Response.Builder()
.request(request.toOkHttpRequest())
.message(message)
.headers(headers.toOkHttpHeaders())
.code(status.code)
.protocol(okhttp3.Protocol.valueOf(protocol.name))
.build()
}

/**
* Converts SDK [ResponseBody] to OkHttp [ResponseBody].
*
* @receiver The SDK [ResponseBody] to convert.
* @return An OkHttp [ResponseBody] object equivalent to the SDK [ResponseBody].
*/
fun ResponseBody.toOkHttpResponseBody(): okhttp3.ResponseBody {
return source().asResponseBody(mediaType().toString().toMediaTypeOrNull(), contentLength())
}

/**
* Converts an [okhttp3.Response] to the SDK `Response`.
*
* This method adapts the response data, including headers, body, status, and protocol,
* to the SDK [Response] structure.
*
* @receiver The [okhttp3.Response] to convert.
* @param request The original SDK [Request] that generated this response.
* @return A new [Response] object in the SDK format.
*/
fun okhttp3.Response.toSDKResponse(request: Request): Response = Response.builder()
.headers(this.headers.toSDKHeaders())
.body(this.body?.toSDKResponseBody())
.request(request)
.protocol(Protocol.valueOf(protocol.name))
.status(Status.fromCode(code))
.message(message)
.build()

/**
* Converts an [okhttp3.ResponseBody] to the SDK [ResponseBody].
*
* This adapter replicates the content of the original response body, including its source,
* content length, and media type, for use within the SDK.
*
* @receiver The [okhttp3.ResponseBody] to convert.
* @return A new [ResponseBody] compatible with the SDK.
*/
fun okhttp3.ResponseBody.toSDKResponseBody(): ResponseBody = run {
create(
source = source(),
contentLength = contentLength(),
mediaType = contentType().toSDKMediaType()
)
}

/**
* Converts SDK [MediaType] to OkHttp [MediaType].
*
* @receiver The SDK [MediaType] to convert.
* @return An OkHttp [MediaType] object equivalent to the SDK [MediaType].
*/
fun okhttp3.MediaType?.toSDKMediaType(): MediaType? {
return if (this != null) parse(this@toSDKMediaType.toString()) else null
}
Loading

0 comments on commit 2fa398b

Please sign in to comment.