Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: default OkHttpClient enhancements and testing #125

Merged
merged 8 commits into from
Dec 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 {
[email protected]().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? = [email protected]()?.let {
parse(it.toString())
}

override fun contentLength() = [email protected]()

@Throws(IOException::class)
override fun writeTo(sink: BufferedSink) = [email protected](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 {
[email protected]().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) {
[email protected](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([email protected]()) else null
}
Loading